FZS is a fuzzy selector for your binaries.
It generalizes the function of launchers like rofi and alfred/raycast. Using the concept of a plugin to group related actions, it allows you to access any of your binaries/scripts/shell functions/aliases within 2-3 keystrokes in the vast majority of cases.
Note
It does this by auto-accepting based on the starting substring.
If there is no starting substring match, the selector falls back to fuzzy selection.
- Enter/Space can be used to manually accept.
FZS was defined to be simple, hackable, modular and portable. It uses a few rules to parse your plugin folders and convert the contents into functions. Using the parsed structure, it creates:
- a folder of executables which get added to your PATH
- ...allowing you to autocomplete your (now namespaced) actions
- a short initialization script providing some base functionality.
- and fully customizable individual selectors for each plugin.
- ...built from by supplying the parsed content to a template, which is itself customizable, as are most other things.
Note
Once the creation is done, fzs gets out of your way.
- It leaves your files untouched, and doesn't necessitate any specific changes or extra work in your setup to accomodate it, beyond sorting your commands and scripts into plugin folders.
- However, it does make use of an optional special naming format which allows you to specify finer details about your commands.
- The finished initialization scripts are pure compiled zsh, and and take less than <1ms on an M1.
Other features include:
- Turning functions into zle widgets, (allowing you to modify your command line on the fly or access them through keybinds)
- Auto-compile your source code (In progress)
- Declutter your PATH while keeping everything organized
- This is can really impove your computer's performance
- Complete and helpful error checking, together with an optimized mode that can be run on shell startup to auto-update (In progress)
- ...More features
FZS is mainly an organizational framework. Related actions are grouped into Plugins, which live as folders within a FZS_ROOT_DIR
(default: ~/.fzs
) on your machine.
A folder is treated as a Plugin if its name matches a plugin_regex
. The root directory scans for plugins non-recursively, and for each, the following happens:
Note
By default, this is ^([a-zA-Z0-9]+)(?:_([a-zA-Z0-9-]+))?(?:_([a-zA-Z0-9-]+))?_select$
(See). That means it any folder that looks like name_alias?_description?_select
will become a Plugin with the fields of name, alias, description
.
- The name of the plugin should be a unique (descriptive) identifier
- The alias of a plugin is the actual namespace the plugin's actions get put under.
- For example, an executable
dlv
living inside a foldervideo_v_video-plugins_select
, is accessible usingv.dlv
, orv.<TAB>
if you have autocomplete. - Defaults to name if not provided.
- Aliases need not be unique, so that you may combine multiple related plugins.
- For example, an executable
- The description is shown sometimes when using certain selectors/autocomplete functions.
The plugin is populated with executables from its folder matching the fn_regex
.
The functions get symlinked to fn_template (Default: {{ pg_alias }}.{{ name }}
).
Note
By default, this is, ^(_*[a-zA-Z0-9-]+)(?:_([a-zA-Z0-9-]*))?(?:_([a-zA-Z0-9-\(\)_ ]+))?(?:\.[a-zA-Z0-9,\. ]+)?
meaning that any executable that looks like name_alias_description.modifiers(.ext)?
gets parsed into the expected function, which is included in a plugin selector, and shown below.
- Modifiers make it easy for you configure extra behavior your functions without resorting to a config file, at the expense of weird file names.
Plugins can also be configured in sources: non-executable files with a specific extension within the associated directory (Default: .zshrc
), allowing you to effortlessly include your aliases and shell functions (more on that below).
Note
There is also neutered version of a plugin, called a linkedbin. It parses a pattern of _name_description?
, and namespaces your functions in that folder, but the executables under it do not get added to the selector. Note that if you use a name
which collides with an existing plugin, the linkedbin is merged with that plugin, and its functions are accessible under the alias
of that plugin.
Essentially, it just auto-adds the NA
flag to every parsed executable.
This is helpful for helper scripts, you may choose to use a folder name like _docker_libs
or _provision_terraform-libs
.
Next, the sources are scanned for lines beginning with # :
. Include this above your function, named like $name_alias?_description?
(The same fn_regex
, but prefixed with a $
), and your function will be included in the plugin, just like an actual binary.
Note
ZSH variable declaration rules means you are a limited to valid characters which don't include -
, but you can get around it by overriding the name on the hash line, using the CMD flag, or using a different namingScheme.
Finally, fzs handles the rest, and creates:
- A plugin selector
- A selector for all parsed functions (even linkedbins)
- Selectors for all your plugins
- Various widgets, keybinds, aliases associated to your actions
The rest is up to the plugins you choose.
The hash line allows further options, in general, you may use the form # : FLAG1,...FLAGN name=nothing-nice alias=tosay cmd=echo binds=^E desc=nothing at all
These field=value tokens are called modifiers, and are space seperated, except for desc
, which must be the last if present.
The are allowed on executable filenames too, after the first period. i.e. rg.wjr binds=^[[1;2B
will allow you to call your rg script using shift-down
. (This format for parsing is non-configurable, but these options can also be set through a config file).
cargo install fzs
Binaries are also available from the release page.
- Optionally, install:
- bat/eza to improve fzf preview experience.
- Add the following to your config:
fzf_dir_cmd = "eza -T -L 2 --icons --color=always ${@}"
fzf_pager_cmd = "bat -p --color=always --terminal-width \\$FZF_PREVIEW_COLUMNS"
- Add the following to your config:
- pueue to allow running commands in background.
- Use the
PBG
flag to enable this on a compatible action.
- Use the
- bat/eza to improve fzf preview experience.
Put your binaries and shell functions in FZS_ROOT_DIR
(default: ~/.fzs
)
The config file lives in ~/.config/fzs
or can be supplied with fzs --config <filepath>
(todo). Check Structs for all available options.
- Note that any useful plugin setting can also be configured using modifiers and decorators, so that only the
[settings]
block is needed. - The config file can however, override any values set in the scanning stage.
- For example:
[[plugins]]
name = "pijul"
alias = "pj"
fns = [
{ name="diff", alias="pjd", cmd="pj diff | bat -l diff" }, # available as pj.diff or pjd
{ name="log-hashes", alias="pjlh", cmd='pijul log --hash-only' },
{ name="changes", FLAGS="WJSUB" }, # Adds the output of the pj.changes to your command line buffer
{ name = "pull", binds = [ "^x^p" ] }, # (turns the binary pj.pull, which must be defined elsewhere, into a widget, and) adds the ^x^p keybind to call it. Use cat -v to find your keybind keycode.
{ name = "push", flags = [ "PBG" ] } # Runs pj.push in the background when selected, requires pueue
]
[[plugins]]
name = "eza"
fns = [
{ name="list-all", cmd="eza -la", bind='^[OQ' },
{ name = "find", flags = [ "PG" ] }, # adds the find plugin to the eza_selector
]
# MAIN
# Having each option start with a different letter makes selection quicker.
[[plugins]]
name = "main"
alias = "m"
fns = [
{ name = "command.wg", alias="ffex", flags = [ "WG" ], binds = ["^[^X"] }, # defined as a widget in, say, $HOME/.zsh/paths/main_select/main.zshrc
{ name = "background", flags = [ "PG" ] }, # references a folder named background_select somewhere inside `root_dir`
{ name = "explain", flags = [ "PG" ] },
{ name = "find", flags = [ "PG" ] },
{ name = "git", flags = [ "PG" ] },
{ name = "integrations", flags = [ "PG" ] },
{ name = "kube", flags = [ "PG" ] },
{ name = "l", flags = [ "PG" ] },
{ name = "ranger", flags = [ "PG" ] },
{ name = "monitor", flags = [ "PG" ] },
{ name = "network", flags = [ "PG" ] },
{ name = "peek", flags = [ "PG" ] },
{ name = "system", flags = [ "PG" ] },
{ name = "vid", flags = [ "PG" ] },
]
binds = [ "^[w" ]
[settings]
root_dir = "$HOME/.zsh/paths"
plugin_selector_binds = [ "^[p" ]
fzf_dir_cmd = "eza -T -L 2"
fzf_pager_cmd = "bat -p --color=always --terminal-width \\$FZF_PREVIEW_COLUMNS"
A window, app, file, directory, quick peek launcher can be found here.
- Clone it into
FZS_ROOT_DIR
- (Migrate/download other plugins)
- Run fzs
- Paste the output into your
.zshrc
- Run
exec zsh
- Activate the default selectors with
^]f
,^]p
! (Make sure they aren't overridden).
Bind a key to switch to terminal and activate your "main" selector. On mac, this requires Karabiner or shkd (and yabai if you need instant desktop switching). Here is my karabiner json:
{
"conditions": [
{
"bundle_identifiers": [
"^com\\.apple\\.Terminal$",
"^com\\.googlecode\\.iterm2$",
"^co\\.zeit\\.hyperterm$",
"^co\\.zeit\\.hyper$",
"^io\\.alacritty$",
"^org\\.alacritty$",
"^net\\.kovidgoyal\\.kitty$",
],
"type": "frontmost_application_unless"
}
],
"from": {
"key_code": "w",
"modifiers": { "mandatory": ["control", "left_option"] }
},
"parameters": { "basic.to_delayed_action_delay_milliseconds": 200 },
"to": [
{ "shell_command": "/opt/homebrew/bin/yabai -m space --focus 7" }
],
"to_delayed_action": {
"to_if_canceled": [
{
"key_code": "w",
"modifiers": ["right_option"]
}
],
"to_if_invoked": [
{
"key_code": "w",
"modifiers": ["right_option"]
}
]
},
"type": "basic"
},
On Linux, it's a lot easier and smoother:
(todo)
Note
You may also need to include yabai -m signal --add event=space_changed action='yabai -m window --focus $(yabai -m query --windows --space | jq -r '\''[.[]|select(."is-visible")][0].id'\'')'
if you are selecting by space.
- Having each option start with a different letter makes selection quicker.
- To set define a "default action" for your plugin, you may want to use a name for the action sharing the same first letter with the plugin, so that you can double-tap to activate.
fzs also treats a plugin whose name is base
as a special case:
- The executables inside are not namespaced (The
fn_template
is not applied to them) - All executables within are detected, not just
So i.e., you could just drop/symlink all your existing extra $PATH
directories in here, and use fzs as just a selector for them, if you wish.
You may want to implement some shared keybinds on fzf:
export FZF_DEFAULT_OPTS="
--info=inline
--preview-window=:hidden:cycle
--cycle
--ansi
--preview 'lessfilter {}'
--prompt='∼ ' --pointer='▶' --marker='✓'
--bind '?:toggle-preview'
--bind 'ctrl-a:select-all'
--bind 'ctrl-y:execute-silent(echo {+} | pbcopy)' # your clipboard command of choice
--bind 'ctrl-o:execute(o {1})' # open -a if you're on mac, xdg-open if on linux
--bind 'alt-o:execute($EDITOR {1})'"
- Rofi
- Alfred
- Raycast
- fzf-tab
TODO
struct Plugin {
name: String,
alias: Option<String>,
desc: Option<String>,
fns: HashMap<String, Action>,
sources: Vec<PathBuf>,
binds: Vec<Keybind>,
// not recommended to set
path: PathBuf
fn_template: Option<String>,
fn_table_template: Option<String>,
}
struct Fun {
name: String,
alias: Option<String>,
desc: Option<String>,
cmd: Option<String>,
flags: FnFlags,
binds: Vec<Keybind>,
}
struct GlobalConfig {
root_dir: PathBuf, // the directory to scan
path_dir: PathBuf, // where your binaries are symlinked to
config_dir: PathBuf, // not available in .config
data_dir: PathBuf, // where the resultant initialization scripts live
plugin_regex: Regex, // The regex used to detect plugins. Supports name alias? desc? as the capturing groups in order
linkedbin_regex: Regex, // The regex used to detect linkedbins. Supports alias? desc? as the capturing groups in order
fn_regex: Regex, // The regex used to detect actions. Supports name alias? desc? as the capturing groups in order
parse_cmd_regex: Regex, // The inverse regex for command -> action. An autogenerated implementation is provided and should suffice in most cases.
name_from_alias_template: String, // When decorated with # AL, the following alias declaration is parsed into an action using this name. See # Templates.
name_from_widget_template: String, // It's useful for autocomplete, to be able to tell which commands should only be run as widgets. A widget with the toggle-scope flag will apply this transformation to the name field of its command.
selector_widget_template: String, // The command template for a selector widget
fn_template: String, // What a binary gets symlinked to
fn_table_template: String, // The format used to pass a function into the fzf selector, see # Templates
all_fn_table_template: String, // The format used to pass a function into the fzf selector for all functions, see # Templates
template_file: PathBuf, // The template file used to build a selector widget. Allows for full customization of the selector behavior. One is created by default in config_dir if not specified.
init_file: PathBuf, // The path to use for the generated file which initializes fzs and sources your source scripts, relative to `data_dir`.
fzs_name: String, // The namespace for fzs related helper shell functions
generated_file: PathBuf, // The path to use for the generated file which initializes plugins, relative to `data_dir`.
plugin_selector_binds: Keybinds, // The keybinds to activate the selector for all plugins (default: ^[p)
all_fn_selector_binds: Keybinds, // The keybinds to activate the selector for all functions (default: ^[f)
fzs_fzf_dir_cmd: String, // Templated into the init_file to configure which command is used to preview a directory (default: ls -la)
fzs_fzf_pager_cmd: String, // Templated into the init_file to configure which command is used as a pager (default: less -RX)
fzs_fzf_cmd_preview: String, // Templated into the init_file to configure which command is used as a pager (default: source $fzs_init_file > /dev/null 2>&1; source $fzs_plugins_file > /dev/null 2>&1; which -a {3})
// This sources your functions so that all definitions are available. The effect should not be noticable
}
enum FnFlag {
WG, // Widget: Just makes the selector invoke this function with zle. Will also register the action with zle if cmd was not set directly.
// Auto-provisioning
WJR, // Creates a widget from the target. Adds the current command-line to the stack and replaces it with the target's command, but does not execute (allowing the user to supply arguments).
WJSUB, // Same as above, but the output is added to the command line buffer.
WR, // Creates a widget from the target.
WSUB, // Creates a widget from the target. The output is added to the command line buffer.
PBG, // Replaces the function such that calling it will run it in the background. (Requires pueue).
// Running modes
SS, // Subshell: When selected, runs the command in a subshell
NR, // NR: When selected, fzs will not run the command, only add it to your command line buffer.
// Plugins
PG, // Plugin. Creates an action targeting the selector interface of another plugin.
PGI, // flatmap's the target plugin's actions into the containing plugin.
// Parsing and generating
NA, // NoAdd: The default flag for an executable in a linkedbin folder. The function is not included in the selector [of its parent].
NN, // The generated name of the function is not namespaced by its parent plugin, you can call it directly by it's name.
AL, // Only inside sources: Use it above a line of the form: alias name='echo hi'. It will add it to your plugin with a name built from name_from_alias_template. Contiguous alias declarations beneath it are also parsed.
TN, // Transform name. Applies a flag-dependent change to the name before geenerating its command.
// FZL
TV, // Invert Visibility. When zle is active, widgets are hidden. This flag enables the widget even when zle is not active. On a non-widget, this hides the action when zle is inactive.
TK, // Keeps the command alive when $FZL_MODE is true.
}
fzs builds the resulting functions from the parsed structure using templates. Here are the defaults:
-
fn_template:
{{ pg_alias }}.{{ name }}
-
selector_widget_template:
{{ alias }}._select.wg
The last two have to do with the how the lines describing each action which are fed into the $fzs_name._base-select.wg/$fzs_name.all-fn-select.wg
are built (See fzs_init.zsh
in the source code):
- name_from_alias_template:
{{ alias }}.al
{{ name }} {{ flags }} {{ cmds }} {{ desc }}
- all_fn_table_template:
{{ pg_alias }} {{ cmds }} {{ name }} {{ alias }} {{ desc }}
fzs also provides the variables this=plugin_alias, this_name=plugin_name
for your sources, which may aid you in defining your functions. For example, the following snippet runs the current command line using pueue:
# : NN,WG
$this.add.wg() {
pueue add -- $BUFFER
zle reset-prompt
zle kill-buffer
}
zle -N $this.add.wg
bindkey '^[z' $this.add.wg
- The use of
NN
(no namespace) is because$add.wg
is not a valid variable name and fzf won't be able to supply the actual name correctly. NN prevents fzf from renaming your command.
Note
FZS does not template your sources! Advantages:
- Your scripts remain valid zsh, allowing editors to work as normal
- FZS only has to do very few write operations, and can be run at every shell startup if desired, ensuring your setup is always up to date.
Disadvantages:
- Do not make sourced files executable (You can use
fd -g "*.zshrc" -t x -x chmod -x
) - When parsing fns from files, the available function characters are limited to
A-Za-z0-9_
due to shell syntax. You can use theconfig.toml
to override if necessary. It is also recommended to use camelCase if your name or alias consists of multiple words. - Use the base plugin to gather binaries without namespacing
- If your binds don't register, make sure to load your fzs_plugins file(s) after other zsh plugins
- How to always properly fold fzf preview?
- I don't know ...
fold -w \${FZF_PREVIEW_COLUMNS}
works okay sometimes.
- I don't know ...
- fzs works well in conjunction with fzs-tab: type your plugin prefix, and you can search through all registered bins under it.
- Some flags are incompatible
- (Todo) Working on covering every case with an explanatory error message.
- Why use
.
and_
as delimiter rather than say,::
which is recommended by google?:
shouldn't be used in filenames, as it's not supported on mac..
is easy to type, and is used in many languages tonamespace
things.- The choice of delimiter is not fixed, you are free to configure it.
- wjr
- Overloading a file with WG
Note
The main functionality of fzs has all been fully implemented, and tested. The remaining minor improvements which are mentioned above and below are planned but haven't a concrete timeline.
- Release
- Github actions (Upload release)
- Refactor
- Tests on all platforms
- Wiki
- Extra features
- Refactor!
- Rename fun to action
- a
--quick
mode that skips checks and uses caching -
--quick
should be able to run on shell startup (<0.05 seconds) - By default fzs_init should be refactored, and copy source file contents
- More options to autoconfigure widgets from file
- Explain eval $cmd in README
- Frequency sort
- Make one-accept an option over default
- More widget modes
- template out fzf_help_cmd
- .zshrc and other formats: .c, rust, etc?
- Would be nice to also bring functions defined elsewhere under a snippet, we could have a macro # : target=have which can do this + add conditionally check existence to decide whether to add to fn_table during runtime
- git hook example
- Properly format columns
- Extend structures
- Flatmap plugins
- Customizable name template
- Composition
- Flatmap plugins
- Cow for functions
- Better previews for plugins
- Should accept a function which takes as input the code of the action. Should output to command.alnum_only
- Conditional plugin
- Seperate flags for visibility
- Refactor!
Footnotes
-
If the default fn_template is kept, FZS provides
this(){echo ${${funcstack[2]}%%.*};}
which can used to refer to the current plugin's alias inside a function one level deep as an ugly workaround. ↩