1 unstable release

new 0.1.0 Sep 12, 2024

#28 in Configuration

Download history 98/week @ 2024-09-09

98 downloads per month

MIT license

73KB
1.5K SLoC

FZS

Fuzzy selector Everything at your fingertips

Description

This is a program to help you manage your binaries. The idea is this: Put all of them in a root directory, categorized in folders For example:

  • vcs
    • _libs
      • insert-under-heading
    • _bins
      • add-commit-all
    • verson-control_vcs_select
  • python
    • _libs
    • __ignored
      • some-dependency
    • python_py_select
      • colors
  • fzf
    • fzf_ff_main_select
  • kube
    • docker_dk
      • imagesearch
      • containers
      • compose
    • kube_kb
  • text
    • bat_select
      • follow
      • journal
      • log

fzs will scan FZS_ROOT_DIR (~/.fzs) for folders whose names match PLUGIN_REGEX. By default, this follows the format of name_alias?_description?_select (Literally it is, ^([a-zA-Z0-9]+)(?:_([a-zA-Z0-9-]+))?(?:_([a-zA-Z0-9-]+))?_select$). Any such folders will get parsed as a Plugin. (alias defaults to name, description is empty by default, other fields are described in Types).

Firstly, (the name of) every executable under the plugin (non-recursive) is parsed into a Fn (by default via the regex ^([a-zA-Z0-9-]*)(?:_([a-zA-Z0-9-]+))?(?:_([a-zA-Z0-9-]+))?(?:\.[a-zA-Z0-9]+)?$ which essentially translates to name_alias?_description?), belonging to the Plugin associated to the containing folder. Altogether, a Vec<Plugin> is built. Finally, every such executable is symlinked to FZS_PATH (Defaults to $HOME/.local/fzs), with the name plugin-alias.fn-name, from where it will be accessible from the command-line.

We also scan for folders whose names match the LINKEDBIN_REGEX (Default: _name_alias?_description?) . These are not parsed into objects, this functionality is only to help you namespace your executables. Mainly, the executables within these folders are also symlinked to FZS_PATH in the same way.

i.e. vcs/_vcs__bins/add-commit-all -> ${FZS_PATH}/vcs.add-commit-all.

Also, any errors at this stage such as duplicate final names will cause an error and early exit, affecting nothing.

Note on name vs alias alias is just a mechanism for namespacing your executables, we don't check that it is unique across plugins. On the one hand, this allows you to put different plugins under the same namespace. But on the other hand, since we don't check for uniqueness, you have to check yourself, otherwise you get strange behavior. The purpose of the plugin name is just to serve as an identifier for the plugin folder, which itself serves as a grouping mechanism for a set of binaries + associated script(s). FZS generates a script whose (main) purpose is to create a selector for this plugin for you, and also an env file which provides variables you can use to define your own associated scripts.

Next, config.toml is parsed for user defined plugins and plugin settings, which are merged into the scanned Vec<Plugin>.

  • Declared functions whose names match a scanned function in the same plugin will override the scanned one.
  • You can also further customize your functions here, i.e. by adding keybinds.
  • You can also append additional functions and widgets (including other selectors) for your plugin here too.
[[plugins]]
name = "bat"
functions = [
    { name = "read", description = "read epubs and pdf", cmd = "rustic-reader" }
    { name = "follow", description = "Override the description of the scanned follow command"}
    { name = "example", description = "The command bat.example will be aliased to ex, and bound to ^x^x", bind = "^x^x", alias = "ex"}
]
widgets = [
	{ name = "monitor", description = "monitor selector", cmd = "mn._select_widget" }
]

This combined Vec<Plugin> structure is then used to build a widget plugin_alias._select_widget within a demarcated section in an initialization script plugin_folder/plugin_name.zshrc for each plugin. This widget is compiled and included in your .zshrc, allowing you to easily access the functions and widgets described for that plugin through just a couple keystrokes.

Todo

  • In main.rs: Parse config.toml file into global_config: GlobalConfig and config_plugins: Vec<RawPlugins>
  • Scan for all directories and filter for those matching global_config.plugin_regex. The capturing groups of the regex will be used to form a InitialPlugin. Also filter for those matching linkedbin_regex, and do the same to form a linkedbins: Vec<InitialPlugin>.
    • An InitialPlugin has the fields {"path", "name", "alias", "description"}. The fields are filled from the regex capturing groups. The linkedbin_regex only has two capturing groups, and produces an InitialPlugin with empty name field.
  • Check for no duplicate names in the resulting plugin list scanned_plugins , also, no duplicate function names in a plugin
  • Merge config_plugins into scanned_plugins:
    • For each cplugin in config_plugins:
      • If its name doesn't appear in config_plugins: set the disabled field to true and add it to scanned_plugins
      • Otherwise, let splugin be the corresponding plugin in config_plugins.
      • For each field that is not fns or name with value Some, override the pre-existing field in splugin with the value from cplugin.
        • When overriding alias, check that it is not present in to_symlink
      • For each fn in cplugins.fns:
        • If its name doesn't appear in the fns of splugin, append it.
        • Otherwise, for each field of the fns with value Some, override the pre-existing field in splugin.
  • For each lb of linkedbins:
    • As above, scan the folder for executables matching fn_regex
    • Use the regex capturing groups to form the { path, pl_alias, name }: InitialFn directly and append it to to_symlink: Vec<InitialFn>.
      • If there is already another entry in to_symlink with the same plugin_alias and alias, throw an error and exit.
  • For each pl in scanned_plugins:
    • Let select_widget_script be the result of substituting pl.name, pl.alias, pl.description, pl.function_template, pl.widget_template into TEMPLATE_FILE.
    • For each fn of pl.fns for which fn.cmd is not None: Run fn_cond_block(fn, select_widget_script) to insert some text below the line {{ fns }} inside select_widget_script.
    • For each wg of pl.widgets : Run wg_cond_block(wg, select_widget_script) to insert some text below the line {{ wgs }} inside select_widget_script.
    • Run cleanup_widget_script(select_widget_script) to remove extra template variables like {{ text }} from select_widget_script
    • Look for a {plugin.name}.zshrc in plugin.path
      • If it doesn't exist, create a new file containing just the two lines ### BEGIN SELECT_WIDGET and ### END SELECT_WIDGET
      • Look for the section between ### BEGIN SELECT_WIDGET and ### END SELECT_WIDGET.
        • If it doesn't exist, throw an error.
        • Otherwise, replace that section with the contents of select_widget_script.
  • Improve output format
    • Use env_logging
    • Custom error types
  • Convert existing setup
  • Delete all symlinks in FZS_PATH. For each ts of to_symlink, symlink ts.path to {FZS_PATH}/{fn_template(pl_alias, name)}
  • Compile all FZS_PATH/**/*.zshrc
  • Print Execution Complete
  • Release
    • Github actions (Upload release)
    • CHANGELOG
    • Refactor
    • Tests on all platforms
  • Extra features
    • Alt-names and default exports? For now, we're just using pgname_pgname
    • More options to autoconfigure widgets from file
    • Unsure whether to use eval $cmd or eval "$cmd" in template, reading a var seems to disable word splitting?. Also, maybe can support custom run flags.
    • Precompile soucrs
    • Frequency sort
    • Make one-accept an option over default
    • fuzzy search over all commands
    • AL flag to handle aliases
    • Widget modes (Puts command onto BUFFER, puts output onto BUFFER)
    • template out fzf_help_cmd
    • Add an extra field to fzf to store id, and add cli option to pretty print from id
      • .zshrc and other formats: .c, rust, etc?
    • ! flag to register the following command, and use flags to set the name
    • Convert flags to Hashset

Types

Extras

  • should template_file handle cmd_template
    • No, lets prefer to keep templating in rust
  • BUFFER options?
  • exit code options?

Case study

# manually replicate widget structure
$this._rg.wg() {
  command fzf.rg "$@"
}
zle -N $this.$_rg.wg

vs

# Parse pg_name.zshrc, specify to not include in selector
# : flags=wg,na binds=""
$_rg() {
  command fzf.rg "$@"
}
zle -N $this.$_rg.wg

vs

# .pw specifies a ProvisionWidget flag on the binary rg
rg.pw
# which autocreates the following code:

FAQ

  • 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 the config.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 namespace
  • end and begin your scripts with newline
  • 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 output?
    • I don't know ._. fold -w \${FZF_PREVIEW_COLUMNS} works okay sometimes.
  • The variables injected by fzs do not work inside of functions. This is a downside of using variables compared to retemplating. The upside is more portable, faster, leaner and syntax adherence. Instead, just refer to it by the generated name, it's not too difficult to change your mind, a find and replace works.

Dependencies

~4–14MB
~198K SLoC