Post

Building Searchable Keybinding Cheatsheets with dmenu

Building Searchable Keybinding Cheatsheets with dmenu

Terminal-centric workflows rely heavily on keyboard shortcuts. As configurations grow across editors, multiplexers, and window managers, the cognitive load of memorizing hundreds of keybindings becomes a significant obstacle. This post presents a shell-based cheatsheet system using dmenu that provides instant, searchable access to keybindings organized by category.

Problem Statement

The Keybinding Proliferation Challenge

Power users accumulate keybindings across multiple tools:

  • Text Editors: Neovim alone can have 200+ bindings across modes, plugins, and LSP features
  • Terminal Multiplexers: tmux session, window, and pane management shortcuts
  • Window Managers: i3/Sway workspace switching, container manipulation, launcher bindings
  • Additional Tools: Git clients, debuggers, file managers, each with their own shortcuts

Several factors compound this challenge:

  1. Context Switching: Moving between tools requires mental context switches for different binding conventions
  2. Infrequent Bindings: Rarely-used but powerful commands are forgotten between uses
  3. Configuration Drift: New bindings are added but memory of existing ones fades
  4. Documentation Scatter: Binding references exist in config files, man pages, and external documentation

Existing Solutions and Limitations

Man pages and help commands provide comprehensive references but require leaving the current context and navigating verbose documentation.

Printed cheatsheets become outdated as configurations evolve and offer no search capability.

which-key style popup hints show continuations after pressing a leader key but cannot display all bindings for a concept (e.g., “all git-related bindings”) across different prefixes.

Browser-based references break keyboard-centric workflows and require window switching.

The desired solution provides instant access from any context, supports fuzzy search, integrates with the existing visual theme, and requires minimal maintenance overhead.

Technical Background

dmenu as a Fuzzy Selection Interface

dmenu (dynamic menu) is a minimalist X11 application that reads lines from standard input and presents them as a selectable menu. The selected line is written to standard output.

Core characteristics:

  • Stdin/stdout interface: Composable with standard Unix pipelines
  • Fuzzy matching: Built-in incremental search narrows options
  • Keyboard-driven: Navigation via arrow keys or Ctrl-n/Ctrl-p
  • Themeable: Colors, fonts, and dimensions configurable via command-line arguments

Basic usage pattern:

1
echo -e "option1\noption2\noption3" | dmenu -p "Select:"

The -p flag sets the prompt text. dmenu blocks until the user makes a selection or dismisses the menu, then outputs the selected line.

Hierarchical vs. Flat Navigation

Two navigation patterns apply to keybinding lookups:

Flat Navigation: All bindings appear in a single searchable list. This approach works well for small binding sets (under 50 entries) where the full list remains scannable.

graph TD
    A["i3 Shortcuts"] --> B["$mod+Return → Open terminal"]
    A --> C["$mod+d → Launch dmenu"]
    A --> D["$mod+Shift+q → Kill focused"]
    A --> E["$mod+1..0 → Switch workspace"]
    A --> F["..."]

Hierarchical Navigation: Categories are presented first; selecting a category reveals its bindings. This pattern scales to hundreds of bindings while maintaining clarity.

graph LR
    A["Neovim Shortcuts<br/>─<br/>Leader Key<br/>Standard Vim Bindings<br/>Core Neovim Normal<br/>LSP Normal Mode<br/>Harpoon Normal Mode<br/>..."] -->|User selects| B["LSP Normal Mode<br/>─<br/>n: gr → Telescope refs<br/>n: gd → Go to definition<br/>n: K → Hover<br/>n: &lt;leader&gt;vca → Code action<br/>n: &lt;leader&gt;vrn → Rename"]
    A -->|Category Selection| C[" "]
    B -->|Binding List| D[" "]
    C -.-> D
    style C display:none
    style D display:none

Architecture

Heredoc-Based Binding Database

Each cheatsheet script embeds its bindings in a heredoc structure. This approach offers several advantages:

  • Single-file deployment: No external data files to manage
  • Version control friendly: Changes appear as simple diffs
  • Shell-native: No parsing libraries or external dependencies
  • Human-readable: The heredoc serves as documentation itself

The binding format uses a consistent structure:

1
2
3
4
5
6
# Category Name
keybinding          → description
keybinding          → description

# Another Category
keybinding          → description

Category headers begin with # followed by a space and the category name. Binding entries use an arrow separator () between the key sequence and its description. Whitespace alignment improves readability in both the source file and the dmenu display.

Script Structure

A two-level cheatsheet script contains four logical sections:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

# 1. Theme configuration
DMENU="dmenu -fn 'Hack-12' -nb #232136 -nf #e0def4 -sb #9ccfd8 -sf #232136"

# 2. Binding database (heredoc)
choices=$(cat <<'EOF'
# Category One
binding → description
...
EOF
)

# 3. Category extraction
categories=$(echo "$choices" | grep '^# [A-Z]' | sed 's/# //')

# 4. Two-level menu logic
selected_category=$(echo "$categories" | $DMENU -i -l 32 -p "Tool Shortcuts")

if [ -n "$selected_category" ]; then
    # Extract and display bindings for selected category
fi

Data Flow Diagram

The two-level navigation follows this data flow:

graph TD
    A["HEREDOC<br/>---<br/># Leader Key<br/>&lt;Space&gt; → Leader key<br/># Standard Vim Bindings<br/>h/j/k/l → Move cursor<br/>..."]
    B["grep '^# [A-Z]' | sed 's/# //'<br/>Category Extraction"]
    C["First dmenu<br/>Category Selection"]
    D["User selects category"]
    E["sed -n '/pattern1/,/pattern2/p'<br/>OR awk section extraction<br/>Section Filtering"]
    F["Second dmenu<br/>Binding Display"]

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F

Implementation Details

Category Extraction with grep and sed

Categories are extracted from the heredoc using pattern matching:

1
categories=$(echo "$choices" | grep '^# [A-Z]' | sed 's/# //')

This pipeline:

  1. grep ‘^# [A-Z]’: Matches lines starting with #, a space, and an uppercase letter. This pattern captures top-level category headers while excluding subcategory markers (e.g., ## Normal Mode).

  2. sed ‘s/# //’: Removes the # prefix, leaving only the category name for display in dmenu.

Example transformation:

1
2
3
4
5
6
7
Input:                          Output:
# Leader Key                    Leader Key
<Space> → Leader key
# Standard Vim Bindings    →    Standard Vim Bindings
## Normal Mode
h/j/k/l → Move cursor
# Core Neovim (Normal)          Core Neovim (Normal)

Section Extraction with sed Range Addresses

The sed approach uses range addresses to extract lines between two patterns:

1
echo "$choices" | sed -n "/# $selected_category/,/^# [^$selected_category]/p" | grep -v '^#'

Breaking down this pattern:

  • -n: Suppresses automatic printing; only explicit p commands produce output
  • /# $selected_category/: Start address matches the selected category header
  • ,: Range operator
  • /^# [^...]/: End address matches the next top-level category header
  • p: Print lines in range
  • grep -v '^#': Remove category headers from output, showing only bindings

This approach works for most categories but requires special handling for category names that share prefixes (e.g., “Standard Vim Bindings” would match before “Standard” alone).

Section Extraction with awk

The awk approach provides more precise control over section boundaries:

1
2
3
4
5
6
section=$(echo "$choices" | awk -v section="$selected_category" '
    BEGIN { print_section=0 }
    $0 ~ "^# "section"$" { print_section=1; next }
    /^# / && print_section { exit }
    print_section && !/^#/ { print }
')

Line-by-line analysis:

LineFunction
BEGIN { print_section=0 }Initialize state variable to “not printing”
$0 ~ "^# "section"$"Match exact category header; require end-of-line anchor
{ print_section=1; next }Enable printing, skip to next line (exclude header)
/^# / && print_sectionIf another category header is found while printing
{ exit }Stop processing; section is complete
print_section && !/^#/If printing is enabled and line is not a header
{ print }Output the binding line

The awk approach handles edge cases more robustly:

  • Exact category name matching prevents prefix collisions
  • Explicit state machine logic makes behavior predictable
  • The exit command terminates early, improving performance for large files

Flat Navigation Implementation

For smaller binding sets, flat navigation suffices:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

choices=$(cat <<'EOF'
$mod+Return      → Open terminal
$mod+d           → Launch dmenu
$mod+Shift+q     → Kill focused window
...
EOF
)

echo "$choices" | dmenu -i -l 20 -p "i3 Shortcuts"

This implementation:

  • Omits category headers entirely
  • Pipes the complete binding list directly to dmenu
  • Uses -i for case-insensitive matching
  • Uses -l 20 to display 20 lines in vertical list mode

Theming Integration

dmenu Color Arguments

dmenu accepts color configuration via command-line arguments:

ArgumentPurposeFormat
-nbNormal backgroundHex color
-nfNormal foregroundHex color
-sbSelected backgroundHex color
-sfSelected foregroundHex color
-fnFont specificationXft font string

Rose Pine Moon Theme Application

The Rose Pine Moon color palette provides a cohesive visual theme:

1
2
3
4
5
DMENU="dmenu -fn 'Hack-12' \
             -nb #232136 \
             -nf #e0def4 \
             -sb #9ccfd8 \
             -sf #232136"

Color mapping:

ElementColorRose Pine Token
Normal background#232136Base
Normal foreground#e0def4Text
Selected background#9ccfd8Foam (cyan accent)
Selected foreground#232136Base (inverted)

The selected item uses an inverted color scheme (light background with dark text) for clear visual distinction.

Font Configuration

The font specification follows Xft naming conventions:

1
'Hack-12'

This specifies:

  • Font family: Hack (a monospace programming font)
  • Size: 12 points

More complex specifications support additional properties:

1
'Hack Nerd Font:size=12:style=Regular'

Monospace fonts ensure consistent column alignment for the arrow separators in binding entries.

Extension Patterns

Adding a New Application Cheatsheet

To create a cheatsheet for a new application:

Step 1: Determine navigation complexity

  • Fewer than 50 bindings: Use flat navigation
  • 50+ bindings: Use hierarchical navigation with categories

Step 2: Create the script structure

For hierarchical navigation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash

DMENU="dmenu -fn 'Hack-12' -nb #232136 -nf #e0def4 -sb #9ccfd8 -sf #232136"

choices=$(cat <<'EOF'
# Category One
binding1 → description
binding2 → description

# Category Two
binding3 → description
binding4 → description
EOF
)

categories=$(echo "$choices" | grep '^# [A-Z]' | sed 's/# //')

selected_category=$(echo "$categories" | $DMENU -i -l 20 -p "AppName Shortcuts")

if [ -n "$selected_category" ]; then
    section=$(echo "$choices" | awk -v section="$selected_category" '
        BEGIN { print_section=0 }
        $0 ~ "^# "section"$" { print_section=1; next }
        /^# / && print_section { exit }
        print_section && !/^#/ { print }
    ')
    echo "$section" | $DMENU -i -l 20 -p "$selected_category"
fi

Step 3: Populate the heredoc with bindings organized by logical category

Step 4: Bind to a global hotkey via the window manager

1
2
# i3 config
bindsym $mod+F1 exec ~/.scripts/cheatsheet-appname.sh

Centralizing Theme Configuration

For multiple cheatsheet scripts, theme duplication can be eliminated by sourcing a common configuration:

1
2
# ~/.scripts/dmenu-theme.sh
export DMENU_THEME="-fn 'Hack-12' -nb #232136 -nf #e0def4 -sb #9ccfd8 -sf #232136"
1
2
3
# Individual cheatsheet scripts
source ~/.scripts/dmenu-theme.sh
DMENU="dmenu $DMENU_THEME"

Adding Search-and-Execute Capability

The basic implementation displays bindings for reference. An enhanced version could execute actions:

1
2
3
4
5
6
7
8
9
# After second dmenu selection
selected_binding=$(echo "$section" | $DMENU -i -l 20 -p "$selected_category")

if [ -n "$selected_binding" ]; then
    # Extract key sequence before arrow
    key=$(echo "$selected_binding" | sed 's/ *→.*//')
    # Send to active window via xdotool (for demonstration)
    notify-send "Selected binding" "$key"
fi

This pattern enables building launcher-style interfaces where selecting an action executes it rather than simply displaying it.

ASCII Mockup: Two-Level Navigation Flow

graph TD
    A["User presses hotkey e.g. Mod+F1"]
    B["First dmenu shows categories<br/>─<br/>Leader Key<br/>Standard Vim Bindings<br/>Core Neovim Normal Mode<br/>Core Neovim Visual Mode<br/>Terminal Integration Normal Mode<br/>Buffer Management Normal Mode<br/>Window Management Normal Mode<br/>LSP Normal Mode<br/>Telescope Normal Mode<br/>Harpoon Normal Mode<br/>Fugitive Git Normal Mode<br/>Gitsigns Git Normal Mode<br/>DAP Debugging Normal Mode"]
    C["User types 'lsp' and presses Enter"]
    D["Second dmenu shows bindings<br/>─<br/>n: gr → Telescope refs<br/>n: gd → Go to definition<br/>n: K → Hover<br/>n: &lt;leader&gt;vws → Workspace symbol<br/>n: &lt;leader&gt;vca → Code action<br/>n: &lt;leader&gt;vrr → References<br/>n: &lt;leader&gt;vrn → Rename"]
    E["User views binding or presses Escape"]
    F["dmenu closes, user returns to previous context"]

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F

Integration with Window Manager

i3/Sway Keybinding Example

1
2
3
4
5
6
# ~/.config/i3/config

# Cheatsheet bindings
bindsym $mod+F1 exec ~/.scripts/cheatsheet-nvim.sh
bindsym $mod+F2 exec ~/.scripts/cheatsheet-tmux.sh
bindsym $mod+F3 exec ~/.scripts/cheatsheet-i3.sh

The function keys provide consistent access regardless of the currently focused application.

Alternative: Unified Launcher

A meta-script can present all cheatsheets in a single interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

DMENU="dmenu -fn 'Hack-12' -nb #232136 -nf #e0def4 -sb #9ccfd8 -sf #232136"

apps="Neovim
tmux
i3"

selected=$(echo "$apps" | $DMENU -i -l 10 -p "Cheatsheets")

case "$selected" in
    "Neovim") exec ~/.scripts/cheatsheet-nvim.sh ;;
    "tmux")   exec ~/.scripts/cheatsheet-tmux.sh ;;
    "i3")     exec ~/.scripts/cheatsheet-i3.sh ;;
esac

Summary

The dmenu-based cheatsheet system provides a lightweight, keyboard-driven solution for keybinding reference. Key architectural decisions include:

  • Heredoc embedding: Self-contained scripts with no external dependencies
  • Hierarchical navigation: Two-level dmenu interaction scales to hundreds of bindings
  • Pattern-based extraction: grep, sed, and awk provide robust text processing
  • Theme consistency: Centralized color configuration maintains visual coherence

The implementation requires only bash and dmenu, tools already present in most Linux desktop environments. Scripts remain human-readable and version-control friendly, evolving alongside the configurations they document.

This approach complements rather than replaces in-editor solutions like which-key or Telescope-based pickers. dmenu cheatsheets provide cross-application access from any context, while editor-native solutions offer deeper integration within their respective tools.

This post is licensed under CC BY 4.0 by the author.