dnsc

Scripting with fuzzel (and niri)

I’ve recently moved to linux on my desktop PC. Growing accustomed to some workflows and tools on my other machines, I wanted some of the niceties on linux as well - to be precise:

  • Quickly launch a SSH session to some of my configured hosts in a new terminal window
  • Have a clipboard history accessible from my launcher
  • Open a browser and a terminal at a specific project location in a separate workspace
  • Trigger device actions (shutdown, screenshot, logout) through my launcher

While some of the more popular Wayland launchers on linux provide these features out of the box, I’ve chosen fuzzel for its simplicity and therefore wanted to extend its functionality for my specific use case. fuzzel provides a dmenu mode which allows for fuzzy finding generic text and providing a selected option for further processing. The most accessible way of scripting fuzzel therefore are some good-old bash scripts, which I’ve gathered here for reference.

Launch SSH sessions for hosts in SSH config

I often have the need to quickly open a SSH session to one of the devices defined in my SSH config, so I’ve written a script that:

  1. Reads the possible hosts from a SSH config file with cat, grep, sort and tail
  2. Provides the hosts as newline-separated text to fuzzel in dmenu mode
  3. Opens a new ghostty terminal that initializes a shell (fish in my case) and immediately connects to the selected host

This is the script I’ve ended up with (change out ghostty and fish for your use case):

#!/bin/sh
fish_executable="/run/current-system/sw/bin/fish" # Only applicable for NixOS
selected=$(cat ~/.ssh/config | grep "Host " | cut -d " " -f 2 | sort | tail -n +2 | fuzzel --dmenu --prompt "Connect to: ")

if [ "$selected" != "" ]; then
  ghostty -e "$fish_executable" -c "ssh $selected"
fi

Searching through clipboard history

My clipboard manager of choice is cliphist, which already provides instructions on how to use it with fuzzel in their documentation.

Open a workspace with project tools

Up until my move to ghostty I’ve used wezterm to quickly open a terminal at a project’s location to start working on it. I do not have the need for persistent sessions as of now, but the ability to quickly start working on a project was something I wanted to keep. To allow for this, I moved the project selector out of wezterm and into fuzzel as ghostty does not have the same capabilities as of now. I already keep all my projects in a single /dev directory in home/$USER so reading these with fd was not a huge task. Similarly to the other scripts, the selected option provided by fuzzel is then used to start a new ghostty instance with its working directory set to the project root. To further separate the project from what I am doing now, I am using niri’s IPC to create a new workspace with both the project terminal as well as a new browser window.

To prettify the output I provided both the project name as well as their full paths to fuzzel. It already supports selecting parts of a line of text for display and output by utilizing the --with-nth and --accept-nth options. Text is split into n parts on \t (configurable through --nth-delimiter) when passing it to fuzzel. --with-nth=1 is then used to show only the first part of the string - in this case the path basename of the project - and --accept-nth=2 makes sure that the bash variable I declare in my script is set to the full path.

#!/bin/sh
projects=$(fd -d=1 -t=d . ~/dev)
projects_with_names=()

for project in ${projects[*]}; do
  projects_with_names+=("$(basename $project)\t$project")
done

selected=$(printf "$projects_with_names" | fuzzel --dmenu --with-nth=1 --accept-nth=2 --prompt "Work on: ")

if [ "$selected" != "" ]; then
  niri msg action focus-workspace "code"
  firefox &
  ghostty --working-directory="$selected"
fi

Trigger device actions

Lastly I wanted to trigger some device actions not only through their CLI commands but also visually from my launcher. The easiest way I’ve found to achive that, was to create XDG desktop entries. These are already searchable from Fuzzel by default and thus no further setup is needed. The only potential downside is that they are mixed in with other applications - which does not matter to me in the slightest, as the fuzzy search takes care of selecting the correct entry.

This is an example desktop entry used to start niri’s built-in screenshot tool from fuzzel (refer to the specification for more options):

[Desktop Entry]
Type=Application
Version=1.0
Name=Screenshot
Comment=Niris screenshot tool
Exec=niri msg action screenshot
# Provided by the Reversal icon theme that I use
Icon=screenie
Terminal=false

I’ve added multiple desktop entries for all kinds of actions as well as entries for triggering the other fuzzel scripts. This allows me to e.g. open the Connect with SSH entry, which starts a subsequent fuzzel selection as described above.

Putting it all together with Nix

Overall these little niceties allow me for seamless context switching, action triggering and generally efficient computer usage. As I am using nix for all of my devices the last step was to write a Home Manager configuration, so that this setup is replicated wherever needed:

{ config, pkgs, ... }:

{
  # The scripts are copied from my nix-config repo to ~/.config/fuzzel/bin
  xdg.configFile."fuzzel/bin" = {
    source = config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/dev/nix-config/modules/wm/fuzzel/scripts";
  };

  xdg.desktopEntries = {
    fuzzel-ssh = {
      type = "Application";
      name = "SSH";
      exec = "${config.home.homeDirectory}/.config/fuzzel/bin/fuzzel-ssh.sh";
      icon = "ksmserver";
    };
    fuzzel-cliphist = {
      type = "Application";
      name = "Clipboard History";
      exec = "${config.home.homeDirectory}/.config/fuzzel/bin/fuzzel-cliphist.sh";
      icon = "xclipboard";
    };
    open-project = {
      type = "Application";
      name = "Open Project";
      exec = "${config.home.homeDirectory}/.config/fuzzel/bin/fuzzel-projects.sh";
      icon = "multitasking-view";
    };
    screenshot = {
      type = "Application";
      name = "Screenshot";
      exec = "niri msg action screenshot";
      icon = "screenie";
    };
    screenshot-screen = {
      type = "Application";
      name = "Screenshot Screen";
      exec = "niri msg action screenshot-screen";
      icon = "screenie";
    };
    color-pickers = {
      type = "Application";
      name = "Color Picker";
      exec = "hyprpicker -a -f=hex -n -l -q";
      icon = "colorpicker";
    };
    # This opens the Snacks picker in neovim directly within my notes
    # repo in a new ghostty window
    notes = {
      type = "Application";
      name = "Notes";
      exec = "ghostty --working-directory=${config.home.homeDirectory}/notes -e nvim -c \":lua Snacks.picker('files')\"";
      icon = "gnotes";
    };
    lock = {
      type = "Application";
      name = "Lock";
      exec = "hyprlock";
      icon = "lock-screen";
    };
    logout = {
      type = "Application";
      name = "Logout";
      exec = "niri msg action quit";
      icon = "administration";
    };
    shutdown = {
      type = "Application";
      name = "Shutdown";
      exec = "shutdown now";
      icon = "com.github.bcedu.shutdownscheduler";
    };
  };

  programs.fuzzel = {
    enable = true;
    settings = { ... };
  };
}

Alternatives

The vicinae launcher looks very promising - especially because I am using Raycast on MacOS. It brings most of the features I use (as well as those that I scripted above) and allows for extensions as well as a dmenu mode. I might try out that in the future. Currently I am happy with the simplicity of fuzzel, so the need for change is not too great.