Skip to content
yukimemiPublic

Latest commit

 

History

902 Commits

Folders and files

Repository files navigation

shun icon

shun (瞬)

A cross-platform, keyboard-driven minimal launcher — like Alfred/Raycast, but yours.

Release License Built with Tauri Ask DeepWiki View Code Wiki

shun demo

Features

  • Instant popup — global hotkey brings up the launcher anywhere; appears on cursor's monitor by default, or fixed to "primary" / index via monitor config
  • Fuzzy / exact / migemo / combined search — fuzzy via nucleo-matcher; migemo via rustmigemo / jsmigemo (type hajime to match 初めて); fuzzy_migemo / exact_migemo return union of both
  • Status badges — subtle pill in the input corner shows current search mode (≋ / ― / あ / ≋あ / ―あ) and sort order (# / ⌚); click or use keybindings to cycle
  • Args mode — press Tab to pass extra arguments; path / list / command completion with ghost text
  • Launch history — frecency sorting; previous args remembered and suggested as ghost text
  • File preview panel — Ctrl+Shift+p toggles a side panel with Shiki syntax highlighting; scroll with Ctrl+j / Ctrl+k
  • Slash commands — /reload, /config, /update, /theme, /save, /reset and more
  • Auto-update — detects new releases on startup; one command to download, install, and restart
  • Theming — built-in presets (Catppuccin, Nord, Dracula, Tokyo Night) + per-color overrides
  • Movable window — drag the grip at the top of the launcher to reposition; save with /save position
  • Cross-platform — Windows, macOS, Linux; portable zip with no admin rights required

Installation

Windows (PowerShell, no admin required)

irm https://yukimemi.github.io/shun/install.ps1 | iex

macOS / Linux

curl -fsSL https://yukimemi.github.io/shun/install.sh | sh
Package managers & direct download

Windows — winget

winget install yukimemi.shun

Windows — Scoop

scoop bucket add yukimemi https://github.com/yukimemi/scoop-bucket
scoop install yukimemi/shun

macOS — Homebrew

brew tap yukimemi/tap
brew install --cask yukimemi/tap/shun

Direct download — latest installer from Releases:

Platform File
Windows .msi (recommended) or shun-windows-x64.zip (portable, no admin required)
macOS .dmg (universal — Apple Silicon + Intel)
Linux .AppImage or .deb

macOS note: shun's macOS builds are not Apple code-signed or notarized. After /update replaces ~/Applications/shun.app, shun strips the com.apple.quarantine attribute from the new bundle before restarting itself so Gatekeeper doesn't silently block the automatic relaunch; if you ever need to do this by hand, run xattr -dr com.apple.quarantine ~/Applications/shun.app.

Quick start

Works out of the box with zero config. Config file is created automatically on first launch:

Platform Path
Windows %APPDATA%\shun\config.toml
macOS ~/Library/Application Support/shun/config.toml
Linux ~/.config/shun/config.toml

Window corners: the launcher window is transparent and undecorated. Windows 11 rounds it natively; on macOS and Linux shun rounds the corners itself (10px). macOS uses Tauri's macos-private-api for real transparency. On Linux a compositor is required for transparency; without one the window stays square rather than showing black corners.

On Windows and macOS, if ~/.config/shun/config.toml already exists, it takes priority over the path above and is used instead (Linux already resolves to ~/.config/shun, so this has no effect there). This also determines where history.json (frecency data, see /history) is read from and written to, since it always lives alongside config.toml — switching to ~/.config/shun starts history fresh there rather than migrating entries from the old location.

A minimal config to get started (non-existent paths are silently ignored):

[keybindings]
launch = "Ctrl+Space"   # global hotkey to show/hide

[[apps]]
name       = "Neovide"
path       = "neovide"
completion = "path"

# Windows
[[scan_dirs]]
path       = "~/bin"
extensions = ["exe", "bat", "ps1", "cmd"]

# macOS / Linux
[[scan_dirs]]
path       = "~/.local/bin"
extensions = ["sh", "py"]

After editing config, run /reload to apply all changes without restarting.

Slash commands

Command Action
/reload Reload config — re-registers global shortcut, rescans apps, re-applies all settings
/config Open a config file (Tab to pick; delete_item key to delete; creates new config.*.toml if typed manually; via editor_command if set, else default editor)
/theme <name> Switch theme for this session (Tab to pick; set in config.toml to persist)
/update Install latest release (shows version if update available)
/history Open history file (via editor_command if set, else default editor)
/version Show current version
/save Save a setting to config.local.toml (Tab to pick: monitor, position, theme, search_mode, sort_order)
/reset Reset a setting in config.local.toml — falls back to config.toml or default (Tab to pick)
/help Show keybindings & current status (theme, search mode, sort order)
/exit Quit shun

The tray icon's "Config" menu item opens config.toml the same way as /config (via editor_command if set, else default editor).

Full configuration reference

All options

# Search mode: "fuzzy" (default) | "exact" | "migemo" | "fuzzy_migemo" | "exact_migemo"
search_mode = "fuzzy"

# Sort order: "count_first" (default) | "recent_first"
sort_order = "count_first"

# Auto-hide when the launcher loses focus
hide_on_blur = false

# Start automatically at login (default: true); set to false to disable
auto_start = true

# Linux only: write shun's .desktop entry + icon for AppImage (default: true).
desktop_integration = true

# Linux/niri only: opt in to shun-binds.kdl synchronization (default: false).
niri = false

# Update check interval in seconds (0 to disable)
update_check_interval = 3600

# Launcher window width in pixels (default: 620)
window_width = 620

# Max items shown in the results list (default: 8)
max_items = 8

# Max items shown in the completion dropdown (default: 6)
max_completions = 6

# Font size in pixels (default: 14)
# font_size = 14

# Window opacity 0.0–1.0 (default: 1.0)
# opacity = 1.0

# Maximum number of history entries to keep (default: 1000)
# history_max_items = 1000

# Status badge icon style: "unicode" (default) | "svg"
# icon_style = "unicode"

# Monitor to show the launcher on: "cursor" (default) | "primary" | 0 | 1 | ...
# Useful in config.local.toml for per-machine override
# monitor = "cursor"

# Saved window position (set via /save position; delete to reset to default centering)
# position_x = 960.0
# position_y = 300.0

# Preview panel: show when browsing files in args mode (default: true)
# preview_args = true

# Preview panel: show when navigating search results (default: true)
# preview_search = true

# Preview panel width in pixels (default: 400)
# preview_width = 400

# Max lines to load for file preview (default: 500)
# max_preview_lines = 500

# Note: preview panel height is fixed to max_items × item height when visible,
# so increasing max_items also increases the preview panel height.

# Editor command used by /config and /history (default: unset → OS file association)
# Space-separated command + args (no quoting support), e.g. "code -n" or "todoke"
# editor_command = "code -n"

[keybindings]
launch      = "Ctrl+Space"   # Global hotkey to show/hide
next        = "Ctrl+n"
prev        = "Ctrl+p"
confirm     = "Enter"
arg_mode    = "Tab"
accept_word = "Ctrl+f"      # Accept next word/segment of ghost text
accept_line = "Ctrl+e"      # Accept full ghost text
delete_word = "Ctrl+w"      # Delete word before cursor
delete_line = "Ctrl+u"      # Delete to beginning of line
run_query   = "Shift+Enter" # Run typed query directly (skip history results)
close             = "Escape"
delete_item       = "Ctrl+d"        # Delete selected history/config item
cycle_search_mode = "Ctrl+Shift+m"  # Cycle search mode (fuzzy → exact → migemo)
cycle_sort_order  = "Ctrl+Shift+o"  # Cycle sort order (count_first ↔ recent_first)
toggle_preview    = "Ctrl+Shift+p"  # Toggle file preview panel
preview_scroll_down = "Ctrl+j"      # Scroll preview panel down
preview_scroll_up   = "Ctrl+k"      # Scroll preview panel up

# Logging
[log]
level = "info"            # "debug" | "info" (default) | "warn" | "error" | "off"
max_file_size_kb = 2048   # rotate when the active file passes this size (default: 2 MB)
rotation = "16"           # number of rotated files to keep (default: 16) | "keep_one" | "keep_all"
retention_days = 30       # delete rotated logs older than this at startup (default: 30, 0 = never)
# Worst-case disk use with the defaults: 2 MB x (16 rotated + 1 active) = 34 MB.
# Each startup logs one `startup:` line (pid, parent process, TTY state, and a fixed allowlist of
# env vars: NO_COLOR, TERM, COLORTERM, XPC_SERVICE_NAME, SHELL, LANG) so you can tell how an
# instance was started; the update path logs `update:` lines for what it spawns.

# Theme
[theme]
preset = "catppuccin-mocha"   # "catppuccin-mocha" | "catppuccin-latte" | "nord" | "dracula" | "tokyo-night" | "one-half-dark" | "solarized-dark" | "solarized-light"
# bg      = "#1e1e2e"
# surface = "#313244"
# overlay = "#45475a"
# muted   = "#585b70"
# text    = "#cdd6f4"
# blue    = "#89b4fa"
# purple  = "#cba6f7"
# green   = "#a6e3a1"
# red     = "#f38ba8"

# Open editor with file path completion
[[apps]]
name       = "Neovide"
path       = "neovide"
completion = "path"       # "path" | "none" | "list" | "command"

# Dedicated global hotkey: toggle Neovide's window without opening shun
[[apps]]
name        = "Neovide (toggle)"
path        = "neovide"
hotkey      = "Ctrl+Alt+N"
hotkey_mode = "toggle"    # "launch" (default) | "activate" | "toggle"

# Search Google (Tab → type query → Enter)
[[apps]]
name = "Google"
path = "https://www.google.com/search?q={{ args | urlencode }}"

# docker exec with container name completion
[[apps]]
name               = "docker exec"
path               = "docker"
args               = ["exec", "-it"]
completion         = "command"
completion_command = "docker ps --format '{{.Names}}'"

# git checkout with branch completion (exact search for this app)
[[apps]]
name                   = "git checkout"
path                   = "git"
args                   = ["checkout"]
completion             = "command"
completion_command     = "git branch --format='%(refname:short)'"
completion_search_mode = "exact"  # "fuzzy" | "exact" | "migemo" (overrides global)
workdir                = "~/src/myproject"

# Launch a registered custom protocol handler
[[apps]]
name = "Microsoft Teams"
path = "ms-teams:"

# Override completion for scan_dirs items
[[overrides]]
name            = "scoop"
completion      = "list"
completion_list = ["install", "uninstall", "update", "search", "info"]

# Open .xlsx files with LibreOffice Calc (ext-based override)
[[overrides]]
ext  = "xlsx"                   # match by extension (no dot)
path = "C:/Program Files/LibreOffice/program/scalc.exe"
args = ["{{ file_path }}"]      # pass original file path as arg

# Auto-register scripts from a directory
[[scan_dirs]]
path       = "~/.local/bin"
recursive  = false
extensions = ["sh", "py", "ps1", "cmd"]

[[overrides]] fields:

When several overrides match the same item, the last one wins (so config.local.toml entries beat config.toml). Overrides also apply to per-app global hotkeys ([[apps]].hotkey). On Windows, a bare path = "brave" that is not on PATH is resolved via the App Paths registry.

Field Type Description
name string Case-insensitive stem name match (optional)
ext string Extension match without dot (e.g. "xlsx", "pdf"). ANDed with name when both are set — omit either to match on the other alone
path string Executable to launch. Supports {{ file_path }} and other override variables. On macOS, shun resolves your login shell's PATH once at first launch ($SHELL -lc 'echo $PATH') and uses it when spawning path/args — LaunchServices-launched GUI apps otherwise only get a minimal default PATH (/usr/bin:/bin:/usr/sbin:/sbin), so Homebrew/mise/asdf/cargo/npm-installed commands wouldn't resolve by bare name (applies one level down too: a command a launched terminal spawns inside itself also gets this PATH, since it's inherited). If this resolution ever fails (e.g. an unusual $SHELL), shun falls back to its own minimal inherited PATH and a bare command name would need an absolute path instead
args string[] Default arguments. Supports override template variables
workdir string Working directory
completion string "list" | "command" | "path"
completion_list string[] Candidates when completion = "list"
completion_command string Command to run for completion = "command"

Per-app global hotkeys

Each [[apps]] entry can register its own global hotkey, independent of keybindings.launch:

Field Type Description
hotkey string Optional. A shortcut string parseable the same way as keybindings.launch (e.g. "Ctrl+Alt+N"). Omit to skip hotkey registration for this app
hotkey_mode string "launch" (default) | "activate" | "toggle"
window_app string Optional, all OSes. App/process name used to find the window for activate / toggle: executable name on Windows (without .exe), .app name on macOS (without .app), WM_CLASS on X11 / app_id on niri. Case-insensitive on Windows/Linux. Defaults to the file name (stem) of path; name is never used. Needed when path is a launcher stub, e.g. path = "wt" → window_app = "WindowsTerminal"
window_title string Optional, Windows/macOS/niri. Additionally require the window title to contain this string (case-insensitive) for activate / toggle. Distinguishes windows of the same app, e.g. a dedicated yazi window in Windows Terminal or WezTerm. On macOS this requires the Accessibility permission — shun shows the standard system prompt once (the first time a window_title hotkey fires, if not already granted); falls back to whole-app activate/toggle with a logged warning if it's denied or dismissed
window_title_exclude string Optional, Windows/macOS/niri. Skip windows whose title contains this string (case-insensitive) for activate / toggle. E.g. keep the F12 hotkey from grabbing a dedicated yazi window
  • launch — starts a new process every time, same as pressing Enter on the item.
  • activate — brings the app's window to the foreground if it's already running; launches it otherwise.
  • toggle — minimizes the app's window if it's currently focused; otherwise same as activate.

Conflict resolution: keybindings.launch always wins. Among [[apps]] entries, the first one (in config file order) to claim a given key combination wins; later entries with the same hotkey are skipped and logged as a warning (shown as a warning item in the launcher list, same as other config warnings). Shortcut strings are compared after parsing, so "Ctrl+Alt+N" and "ctrl+alt+n" are treated as the same key. Run /reload after editing hotkey / hotkey_mode to re-register.

Reserved keys (Windows): some keys can't be registered via RegisterHotKey — notably a bare F12, which Windows reserves for debuggers — or are already taken by another app. In that case shun falls back to a low-level keyboard hook (the same technique AutoHotkey uses), so the hotkey still works and the key press is not passed on to the focused app.

Platform support for activate / toggle:

Platform Behavior
Windows Full support. Matches the app's own top-level window by executable file name (case-insensitive). If the app has multiple windows, only the first one found is operated on. Foreground-lock restrictions imposed by Windows may occasionally prevent focus-stealing (the taskbar icon flashes instead); this is logged as a warning, not an error
macOS Uses NSWorkspace/NSRunningApplication (AppKit) directly for whole-app activate/toggle — no osascript/AppleScript, no Accessibility permission needed for that path. The target is window_app, or the file stem of path if unset (not name); matched against the app's localized display name or its .app file stem (locale-independent), so it works whether you write the English bundle name or it only matches your system's localized name. If the app is not running, shun launches path instead. toggle's "already frontmost" case (whole-app path) calls hide() (like ⌘H — hides the whole app's windows, but the app itself stays visible in the Dock/⌘Tab) rather than minimizing individual windows. If window_title is set, shun instead uses the Accessibility API (AXUIElement) to find and operate on that one specific window (AXRaise to bring it forward, the AXMinimized attribute to hide just it on toggle) without affecting the app's other windows. The first time such a hotkey fires without the Accessibility permission already granted, shun shows the standard system prompt (once per run — it won't nag on every press); denied or dismissed, it logs a warning and falls back to the whole-app behavior. Unlike Windows/Linux, a found-but-failed operation here does not fall back to launching (would duplicate an already-running app). Likewise, if the app is running but its window list cannot be read via Accessibility, shun retries once after ~150 ms and then does nothing (warning in the log; press the hotkey again) — it neither launches nor activates the whole app, since that would raise the wrong window while reporting success. This also applies to window_title_exclude-only entries, and an app whose Accessibility API never returns windows will do nothing for its window_title hotkey
Linux (niri) Uses niri's IPC (niri msg): matches window_app (or the stem of path) as a case-insensitive substring of the window's app_id, plus window_title / window_title_exclude, and focuses the most recently focused match. toggle really toggles: if the target is already focused, focus returns to the previously focused window (niri has no minimize). Configure the keys in niri's own config — see "Command line & Linux / Wayland"
Linux (X11) Requires wmctrl to be installed; matches window_app (or the stem of path) against the window's WM_CLASS via wmctrl -x -a (substring, case-insensitive, so short names can match the wrong app); falls back to launch if it's missing. toggle behaves the same as activate (no reliable way to detect focus without extra tooling), i.e. it never minimizes
Linux (other Wayland compositors) Not supported: the global hotkeys can't be grabbed and windows can't be found, so activate / toggle fall back to launching. shun toggle / shun hotkey <KEY> (below) can be bound from the compositor's own config

If a window can't be found or the platform is unsupported, shun falls back to launching a new instance rather than doing nothing (macOS: a found app whose hide/activate operation itself fails does not fall back to launching, to avoid spawning a duplicate of an app already running).

Known limitation (macOS, window_title): if an app is run more than once as separate OS processes sharing the same underlying binary (e.g. wezterm-gui invoked directly as a bare path/args command, rather than through its .app launch path, can sometimes register as a second, independent process instead of reusing the already-running one's mux/session) — the extra instance's window is still found and activated correctly, but macOS's own "is this app in the foreground" signals (NSRunningApplication.isActive(), AXFrontmostAttribute, even NSWorkspace.frontmostApplication()) can disagree with what's actually on screen for that instance specifically. toggle then always activates rather than minimizing on repeat presses for that one instance — a caveat, not a regression (finding and focusing the window keeps working every time; only the "press again to hide it" half of toggle doesn't fire in this specific case).

Command line & Linux / Wayland

A second shun ... invocation is handed to the running instance (single-instance), so the hotkey actions can be triggered from outside — from a compositor bind, a script, or a launcher:

Command Action
shun / shun show Show the launcher (show also opens it when shun starts fresh)
shun toggle Show / hide the launcher — what keybindings.launch does
shun hotkey <KEY> Run the [[apps]] entry whose hotkey is <KEY> (as written in config.toml, e.g. shun hotkey F10); <KEY> equal to keybindings.launch toggles the launcher
shun install-desktop Linux: write the .desktop entry and icon, then exit

On Wayland, global keys belong to the compositor. shun does not initialize the X11 global-shortcut backend in a Wayland session, call the single-instance plugin's private D-Bus API directly, or reclaim focus after a blur. Compositor config is untouched by default; optional niri synchronization is described below. Windows, macOS, and native X11 continue to register shortcuts normally.

For example, add these to niri's config.kdl (replace the executable path with your installed shun path, and the app hotkeys with those defined in config.toml):

binds {
    Ctrl+Return repeat=false { spawn "/home/you/.local/bin/shun" "toggle"; }
    F11 repeat=false { spawn "/home/you/.local/bin/shun" "hotkey" "F11"; }
    F12 repeat=false { spawn "/home/you/.local/bin/shun" "hotkey" "F12"; }
}

The normal single-instance plugin forwards these CLI commands to the resident shun. This also starts shun if it is not running. AppImage invocations have startup overhead; this deliberately uses the normal CLI rather than maintaining a separate fast-path transport. For other Wayland compositors, bind the same commands using their native configuration syntax. By default, /reload reloads shun's config, not the compositor's keys.

Optional niri synchronization (off by default)

To derive the niri keys from shun's config, set this at the top level of shun's config.toml:

niri = true

Keep the existing top-level include in niri's config.kdl, or add it once if it is absent:

include "shun-binds.kdl"

No begin/end markers are needed. On startup and /reload, shun replaces only the dedicated shun-binds.kdl file beside that config with bindings derived from keybindings.launch and the valid [[apps]].hotkey entries. The main config.kdl and its include line are never edited. Changing an app's hotkey in shun and running /reload therefore updates niri's key too. The generated actions invoke the normal shun toggle / shun hotkey <KEY> CLI directly, without a shell. niri live-reloads the updated include.

  • The main config is $NIRI_CONFIG, or the XDG config directory's niri/config.kdl (normally ~/.config/niri/config.kdl). Synchronization only runs in a niri session.
  • Exactly one active top-level include "shun-binds.kdl" is required. Missing or duplicate includes, invalid config, or an unsupported keybindings.launch key mapping cause a log warning and no write. An unsupported [[apps]].hotkey key mapping is skipped with a warning; the launcher binding and other valid app bindings are still synchronized. shun never adds include lines automatically.
  • shun-binds.kdl is entirely generated: keep hand-written bindings in the main config or another include. If the dedicated file is missing, shun creates it after validation.
  • Before replacing the binds file, shun stages both the new bindings and a temporary main-config copy pointing at them, then runs niri validate. Validation failure or detected edits to either live file leave the existing files untouched; no invalid intermediate binds are installed.
  • Config symlinks and existing binds-file permissions are preserved. Unchanged generated content does not rewrite the file. Cross-file binding overrides follow niri's own include precedence rather than a separate shun conflict policy.
  • niri = false stops synchronization without changing existing binds. To remove synchronized shortcuts, remove the include line yourself before deleting its file. desktop_integration is independent of this setting.

Existing generated niri binds: if an older shun already added include "shun-binds.kdl", keep it and set niri = true to regenerate that same dedicated file using the normal CLI path. No markers or main-config migration are required. Alternatively, keep synchronization off and manage the included file yourself.

Desktop entry (AppImage) — the AppImage ships no .desktop file, so app launchers don't list it. On start shun writes ~/.local/share/applications/shun.desktop (running <AppImage> show) and an icon, and keeps its Exec path current if you move the AppImage. An entry you edited by hand (remove the # Generated by shun line) is never touched. desktop_integration = false disables this automatic desktop entry/icon installation; it does not control compositor shortcuts.

Override files (config.*.toml)

Any config.*.toml files in the same directory as config.toml are loaded and merged in alphabetical order after the base config. Useful for per-machine or per-context overrides with chezmoi or other dotfile managers.

Examples: config.local.toml, config.work.toml, config.home.toml

Platform Directory
Windows %APPDATA%\shun\
macOS ~/Library/Application Support/shun/
Linux ~/.config/shun/

On Windows and macOS, this directory follows config.toml's own resolution: if ~/.config/shun/config.toml exists, override files (config.local.toml, config.*.toml) are also read from ~/.config/shun/ instead of the path above, not both.

Merge rules:

Field Behavior
apps, scan_dirs, overrides Entries are appended
search_mode, sort_order, hide_on_blur, auto_start, font_size, opacity, monitor, etc. Local value overrides (only when explicitly set)
[keybindings], [theme], [log] Per-field override — only specified keys are overridden
Template placeholders in path and args

You can use Tera template syntax in path, args, and most other string fields. {{ vars.* }}, {{ env.* }}, and {{ os }} are resolved once when the config loads (so they work in hotkey, name, workdir, enum fields, etc. too); {{ args }} / {{ args_list }} / {{ file_* }} are only resolved at launch time for path, args, and workdir (when args are provided via Args mode (Tab) or an override matches) — any string referencing them, including inside {% if %}, is left untouched by the load-time pass. Plain {% if %}...{% endif %} control blocks with no {{ }} inside are expanded the same way as {{ }} placeholders.

Context variables:

Variable Value
{{ args }} Extra args joined by space (raw)
{{ args_list }} Extra args as an array
{{ env.VAR_NAME }} Environment variable
{{ vars.my_var }} User-defined variable from [vars] in config
{{ os }} Current OS: "windows" | "macos" | "linux"

Override-only variables (available in [[overrides]] path, args, and completion_list when a scanned file is matched — expose the original file's path information for use in templates):

Variable Value
{{ file_path }} Full path of the matched file
{{ file_name }} Filename with extension (e.g. report.xlsx)
{{ file_stem }} Filename without extension (e.g. report)
{{ file_ext }} Extension only (e.g. xlsx)
{{ file_dir }} Parent directory path

Useful Tera expressions:

Expression Result
{{ args | urlencode }} URL-encoded (spaces → %20)
{{ now() | date(format="%Y%m%d") }} Today's date e.g. 20260321
{{ now() | date(format="%Y-%m-%d") }} Today's date e.g. 2026-03-21
{{ get_env(name="VAR", default="fallback") }} Env var with default

Example — Web search:

[[apps]]
name = "Google"
path = "https://www.google.com/search?q={{ args | urlencode }}"

Open shun → goo → Tab → rust borrow checker → Enter → opens the search. Query is saved to history.

Example — Date-stamped memos:

# MemoNew: Tab → type title → Enter → opens "20260321-title.md"
[[apps]]
name       = "MemoNew"
path       = "nvim"
args       = ['~/memo/{{ now() | date(format="%Y%m%d") }}-{{ args }}.md']
completion = "none"

# MemoList: Tab → pick existing memo with migemo path completion → Enter
# (type "hajime" to find "初めて.md")
[[apps]]
name                   = "MemoList"
path                   = "nvim"
args                   = ["~/memo/{{ args }}"]
completion             = "path"
completion_search_mode = "migemo"

Example — User-defined variables:

[vars]
src_dir = "~/src/github.com/yourname"

[[apps]]
name       = "Open Project"
path       = "neovide"
args       = ["{{ vars.src_dir }}/{{ args }}"]
completion = "path"

Example — OS-conditional app (one shared config.toml across machines):

# path picked per OS; only the matching OS gets a hotkey.
# ({% if %} with no {{ }} inside still expands. On a non-matching OS
# the hotkey renders to an empty string and is simply not registered —
# silently, with no warning. Only a non-empty but unparseable shortcut
# warns, so a typo'd OS name here just means "no hotkey anywhere".)
[[apps]]
name        = "Terminal"
path        = "wt"
window_app  = "WindowsTerminal"   # wt.exe is a stub; the window belongs to WindowsTerminal.exe
hotkey      = "{% if os == \"windows\" %}F12{% endif %}"
hotkey_mode = "toggle"

[[apps]]
name        = "WezTerm"
path        = "wezterm-gui"
window_app  = "WezTerm"
window_title_exclude = "yazi" # never grab the dedicated yazi window below
hotkey      = "{% if os == \"macos\" %}F12{% endif %}"
hotkey_mode = "toggle"

# A dedicated yazi window in WezTerm: window_title picks it out from the
# main terminal window above by title substring (yazi's own terminal title
# contains "yazi"), so repeated presses reuse it instead of piling up new
# windows. Uses `activate`, not `toggle` — you generally want a launcher
# shortcut like this to just bring the window forward every time, not hide
# it away on alternate presses (use `toggle` for the "everyday shell"
# hotkey above instead, where hiding it out of the way is the point).
[[apps]]
name        = "yazi (WezTerm)"
path        = "wezterm-gui"
args        = ["start", "--", "yazi"]
window_app  = "WezTerm"
window_title = "yazi"
hotkey      = "{% if os == \"macos\" %}Ctrl+F10{% endif %}"
hotkey_mode = "activate"

Example — args_list for multi-argument commands:

# git diff: Tab → "main feature-branch" → Enter → runs "git diff main feature-branch"
[[apps]]
name               = "git diff"
path               = "git"
args               = ["diff", "{{ args_list.0 }}", "{{ args_list.1 | default(value='HEAD') }}"]
completion         = "command"
completion_command = "git branch --format='%(refname:short)'"

# ripgrep with comma-separated globs: Tab → "*.rs *.toml" → passes as separate args
[[apps]]
name = "rg"
path = "rg"
args = ["--glob={{ args_list | join(sep=',') }}"]
Theming

Switch theme for the current session with /theme <name> (Tab to pick). To persist across restarts, set it in config.toml:

[theme]
preset = "nord"
bg     = "#1a1a2e"  # optional per-color override

Built-in presets:

Preset Description
catppuccin-mocha Default — dark, muted pastel
catppuccin-latte Light variant of Catppuccin
nord Arctic, blue-toned dark
dracula Dark with vibrant purple/pink
tokyo-night Dark blue-purple, Tokyo Night style
one-half-dark One Half Dark (as used in VS Code / Windows Terminal)
solarized-dark Solarized Dark
solarized-light Solarized Light

Color keys (all optional — unset keys fall back to the preset):

Key Role
bg Launcher background
surface Selected item background / border
overlay Ghost text / separator / count
muted Placeholder / source labels
text Main text
blue Accent — dirs, URLs, args app name
purple Slash commands
green Path items
red History items
Keybindings reference

All keybindings are configurable via [keybindings] in config.toml. Changes take effect after /reload — no restart needed.

Search mode (default keys):

Key Action
Ctrl+Space Show / hide launcher
Ctrl+n / ↓ Next item
Ctrl+p / ↑ Previous item
Enter Launch selected item
Shift+Enter Launch typed query directly (skip history results)
Tab Enter args mode
Ctrl+f Accept next word of ghost text
Ctrl+e Accept full ghost text
Ctrl+w Delete word before cursor
Ctrl+u Delete to beginning of line
Ctrl+d Delete selected history item
Ctrl+Shift+m Cycle search mode (fuzzy → exact → migemo)
Ctrl+Shift+o Cycle sort order (count_first ↔ recent_first)
Ctrl+Shift+p Toggle file preview panel
Ctrl+j Scroll preview panel down
Ctrl+k Scroll preview panel up
Escape Hide launcher

Args mode (default keys):

Key Action
Enter Launch with args
Shift+Enter Launch typed query directly
Tab Apply selected completion
Ctrl+n / Ctrl+p Navigate completion list
Ctrl+f / Ctrl+e Accept ghost text (word / full)
Ctrl+w Delete word before cursor
Ctrl+u Delete to beginning of line
Ctrl+d Delete selected history completion
Ctrl+Shift+m Cycle search mode
Ctrl+Shift+o Cycle sort order
Ctrl+Shift+p Toggle file preview panel
Ctrl+j / Ctrl+k Scroll preview panel down / up
Escape Back to search

Special input:

Input Action
scheme:... (http/https/mailto/ms-excel, etc.) Open with the OS default protocol handler
~/..., C:/... Browse filesystem, open in file manager

URL schemes must contain at least two characters, and URL input must not contain whitespace (encode spaces as %20). Windows drive paths and shell: folders remain filesystem inputs.

Building from source
# Prerequisites: Node.js, Rust

git clone https://github.com/yukimemi/shun
cd shun
npm install
npm run tauri dev     # development
npm run tauri build   # production build

Credits

shun's migemo feature is powered by the following libraries and data by oguna:

Component Role License
rustmigemo Rust migemo engine MIT
jsmigemo JavaScript migemo engine MIT
yet-another-migemo-dict Bundled compact dictionary (Mozc + UniDic based) BSD-3-Clause

Full license texts are in the NOTICE file.

License

MIT

Releases

Packages

Contributors

Languages