---
title: Scripting with fuzzel (and niri)
tags:
  - linux
  - nixos
  - ghostty
  - niri
publishedAt: 2025-11-30:09:00+01:00
lastUpdatedAt: 2025-11-30:09:00+01:00
isPublic: true
---

I've recently [moved to linux](./another-move-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](https://codeberg.org/dnkl/fuzzel) for its simplicity and therefore wanted to extend its functionality for my specific use case. `fuzzel` provides a [dmenu mode](https://codeberg.org/dnkl/fuzzel/src/branch/master/doc/fuzzel.1.scd) 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):

```sh
#!/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](https://github.com/sentriz/cliphist?tab=readme-ov-file#picker-examples).

## 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](https://github.com/YaLTeR/niri/wiki/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.

```sh
#!/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](https://specifications.freedesktop.org/desktop-entry/latest/) for more options):

```sh
[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:

```sh
{ 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](https://github.com/vicinaehq/vicinae) 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.
