pop-os / launcher

Modular IPC-based desktop launcher service
Mozilla Public License 2.0
220 stars 42 forks source link

[WIP] Flexible search plugin (via Unix pipes) #172

Open canadaduane opened 1 year ago

canadaduane commented 1 year ago

NOTE: This is a WORK IN PROGRESS, and is not yet ready to be merged. Looking for early feedback, as well as rust tips and improvements to memory management and/or style. Also looking for discussion around which existing launcher plugins this could/should replace, if any.

Intro

This is a generalized "search" plugin that can be configured to pass a query from the Pop Launcher frontend to any shell command that returns results (the shell command must return one result per line).

For example, the current "find" plugin (which has 245 lines of rust code), can be implemented in about 10 lines of config, and offers more flexibility to the end user since it becomes trivial to modify the fdfind shell args if desired:

# ~/.local/share/pop-launcher/plugins/search/config.ron
(
  rules: [
    (
      pattern: StartsWithKeyword(["f", "find"]),
      action: (
        query_command: "fdfind --ignore-case --full-path $KEYWORD1",
        output_captures: "^(.+)/([^/]+)$",
        result_name: "$CAPTURE2",
        result_desc: "$CAPTURE1",
        run_command: "xdg-open '$OUTPUT'",
      )
    ),
  ]
)

The pattern line indicates when to match the rule. It corresponds with the "prefix" that you type into the launcher frontend. In the above example, the search plugin would begin an fdfind query if the user starts typing "f" or "find" into the launcher frontend.

The result_name and result_desc settings correspond to the 1st and 2nd lines of each search result (the launcher frontend can show two lines per result). The result_name and result_desc settings are optional. By default result_name is "$OUTPUT", and result_desc is blank. Keywords from the query string, and captures from the output_captures regex can be used as needed to format a search result for the user.

The query_command is the shell command to use as the query function, e.g. fdfind in our example above. As long as the shell command returns only one line (on STDOUT) per result, any command may be used. For example, ls -1, apt list, a shell command that lists your address book contacts, etc.

Finally, the run_command is the command to run on the active result chosen by the user, i.e. what to do when the user presses "Enter" in the launcher frontend.

Note that variables are expanded using shellexpand and therefore can use curly braces and default values, e.g. ${KEYWORD1:-someDefault}.

Example Configurations

Example 1: List hard drives

A launcher command, "drives" that shows a list of block devices:

    (
      pattern: StartsWithKeyword(["drives"]),
      action: (
        query_command: "lsblk -lno NAME,SIZE,MOUNTPOINTS",
        run_command: "notify-send '$OUTPUT'",
      )
    ),

Example 2: Show running processes

A launcher command, "ps" that lists processes, their PIDs, and how much CPU they are using. This example demonstrates using bash to write a "mini shell script" with a pipe between ps and head:

    (
      pattern: StartsWithKeyword(["ps"]),
      action: (
        query_command: "/bin/bash -c 'ps --sort=-pcpu -axo pid,ucmd,pcpu | head -10'",
        output_captures: "^\\s+([0-9]+)\\s+(.*)$",
        result_name: "${CAPTURE2}",
        result_desc: "${CAPTURE1}",
        run_command: "notify-send '$OUTPUT'",
      )
    ),

Example 3: Search apt packages

A launcher command, "apt" that searches for packages using glob syntax of apt list, e.g. "apt zu*":

    (
      pattern: StartsWithKeyword(["apt"]),
      action: (
        query_command: "apt list $KEYWORD1",
        output_captures: "^([^/]+)/(.+)$",
        result_name: "$CAPTURE1",
        result_desc: "$CAPTURE2",
        run_command: "notify-send '$OUTPUT'",
      )
    ),

To understand the above, it's helpful to know how the shell command apt list [search_term] sends results to STDOUT--one per line, in the following format:

$ apt list zu*
Listing... Done
zulucrypt-cli/jammy 5.7.1-2 amd64
zulucrypt-gui/jammy 5.7.1-2 amd64
zulumount-cli/jammy 5.7.1-2 amd64
zulumount-gui/jammy 5.7.1-2 amd64
zulupolkit/jammy 5.7.1-2 amd64
zulusafe-cli/jammy 5.7.1-2 amd64
zurl/jammy 1.11.1-1 amd64
zutils/jammy 1.11-1 amd64
zutty/jammy 0.11.2.20220109.192032+dfsg1-1 amd64

Note that "Listing... Done" is skipped, because it does not match the output_capture's regular expression, which requires that a "/" must be present in the search result line.

Example 4: Calculator

    (
      pattern: StartsWith(["="]),
      split: Regex("^="),
      action: (
        query_command: "qalc -u8 -set 'maxdeci 9' -t $KEYWORD1",
        result_name: "$KEYWORD1",
        result_desc: "$OUTPUT",
        run_command: "/bin/bash -c 'wl-copy \"$OUTPUT\" && notify-send \"Copied to clipboard\"'",
      )
    ),

The split setting is new in the example above: it allows us to split the query using a regular expression. Normally, the query is split into keywords using the default "ShellWords" algorithm, which splits on spaces but also understands things like single and double quotes (treating words enclosed in quotes as a single entity). Here, however, to enter calculator mode with an initial "=" regardless of whether a space follows the "=", we need to split the query differently. In this example, we choose to split on the initial "=", making $KEYWORD1 absorb the remaining equation to be sent to qalc as input (as well as the frontend as the name).

Fixes #164

mmstick commented 1 year ago

Some things that would improve this:

I'm also wondering if we could merge this and the scripts plugin together with a solution based on rhai. It's possible to pass values and functions from the Rust side to the Rhai scripts, so there could be a function for setting up Rhai-based plugins in this way.

canadaduane commented 1 year ago

Thanks for the feedback! I have a few questions:

Each command should have its own configuration file somewhere, similar to how scripts are in /usr/lib/pop-launcher/scripts

I'm certainly willing to do this, but I'm not sure if I understand the benefit. Is this to enable programs/packages to install their own launcher scripts without having to mutate the "big config file"?

Some way of enabling and disabling a command, perhaps with command line arguments to pop-launcher itself

I'm guessing this is in anticipation of a settings/configuration page of some kind, so we can let the user decide what system-installed scripts are enabled/disabled? Should we require a "search plugin" config script to have an ID? This would allow a user to override the system script with their own, while still allowing a settings page to enable/disable.

Ability to define a condition(s) that must be true for the command to be queried; such as Exists("apt").

Cool, good idea.

Custom icons for each command, and custom icons for each result

Yes, I was thinking the same.

I'm also wondering if we could merge this and the scripts plugin together with a solution based on rhai. It's possible to pass values and functions from the Rust side to the Rhai scripts, so there could be a function for setting up Rhai-based plugins in this way.

I'm a bit intimidated by the complexity of this, in conjunction with the focus that this search plugin has on "just piping". Would it be OK to create a 2nd plugin with Rhai as the focus? Or is there a distinct advantage to combining that capability with this search plugin's capability?

canadaduane commented 1 year ago

@mmstick If you have a moment, I'm looking for feedback before proceeding.

mmstick commented 1 year ago

Sorry for the delay.

I want to simplify configuration in the future. If each plugin has its own configuration file, it will be easier for people to install third party configurations without having to append them.

I may also make the transition from RON to KDL for first party plugins since that's easier for a human for a human to edit. Much reduced chance of syntax errors.

There may eventually be a settings page for the launcher and its plugins, and it's a common request to disable a plugin or an item from a plugin.

I can convert plugins to Rhai in the future. Same with KDL.