A fast app launcher for Linux and Windows. Written in Zig with Qt6.
Scans your .desktop files from the usual places, shows you everything in a searchable list, and lets you filter through it as you type with fuzzy matching. Built because apparently 50 other launchers weren't enough.
On Windows, Zenkai scans per-user and shared Start Menu folders for .lnk and .url shortcuts and opens them with the Windows shell. Configuration is stored under %APPDATA%\zenkai.
Thanks to rcalixte for libqt6zig, the Zig bindings this project is built on.
- Zig 0.17.0 - the compiler and build system. Download from ziglang.org/download or use your distro's package manager if it has a recent enough version.
- Qt 6.8.2 development libraries (Core, Gui, Widgets, Svg) - the GUI toolkit. Install
qt6-base-devandlibqt6svg6-devor equivalent for your distro. - GCC or Clang - used by Zig to link C++ code (Qt is written in C++).
- libstdc++ or libc++ - the C++ standard library, comes with your compiler.
- pkg-config - helps the build system find Qt headers and libraries.
Windows builds also require Zig 0.17.0 and a Qt 6.8.2 development installation for the Windows target. Use the deployment build step to bundle the Qt and LLVM MinGW runtimes plus the platform plugin beside the executable:
zig build deploy-windows -Dtarget=x86_64-windows-gnuThe output is zig-out/bin/zenkai.exe; it can be launched without setting PATH or QT_PLUGIN_PATH. If Qt is installed outside C:/Qt/6.8.3/llvm-mingw_64, pass its installation root with -Dqt-win-root=<path>.
Zig fetches the following automatically when you run zig build:
zig buildBinary at zig-out/bin/zenkai.
Download the tarball for your architecture from ziglang.org/download, extract it, and put the zig binary somewhere in your PATH. Most distro repositories lag behind and 0.17.0 is required.
sudo pkg install qt6-basesudo apt install gcc libstdc++-14-dev-$(dpkg --print-architecture)-cross qt6-base-devsudo dnf install gcc libstdc++-devel qt6-qtbase-develsudo pacman -S gcc qt6-basesudo zypper install qt6-base-develKick it off:
./zig-out/bin/zenkaiType to filter through your apps. Enter to launch. That's it.
Up and Down cycle through results. Ctrl+L selects the search text so you can start a new query. Home and End move within the query; Ctrl+Home and Ctrl+End jump to the first and last results. Escape closes the launcher.
Use native to follow system light/dark appearance and accent color. It selects a macOS-style surface on macOS and a Windows-style surface on Windows. Windows defaults to native; an explicit config or --theme selection overrides it.
./zig-out/bin/zenkai --theme=dracula
./zig-out/bin/zenkai --theme=./my-custom-theme.qssName one of the 65 built-in themes or point at a .qss file. See screenshots to save yourself the trouble of running each individually.
Uses an icon grid with a pill-shaped search bar:
Opens the launcher in fullscreen mode on the selected monitor (or the cursor's monitor). Uses ShowFullScreen() internally. On Wayland, this auto-switches to XWayland when combined with --monitor=N since Wayland compositors ignore output assignment requests.
./zig-out/bin/zenkai --theme=launchpad --fullscreen
./zig-out/bin/zenkai --theme=launchpad --fullscreen --monitor=1Select monitor by index (0-based). Defaults to the monitor the cursor is on. Use --list-monitors to see available indices.
./zig-out/bin/zenkai --monitor=0
./zig-out/bin/zenkai --fullscreen --monitor=1Prints all available monitors with their index, name, geometry, and whether each is the primary display.
./zig-out/bin/zenkai --list-monitorsDumps every theme name and description to the terminal and exits.
Icon size. Default 32.
Window width. Default 600.
Window height. Default 500.
./zig-out/bin/zenkai --menu="Terminal|alacritty|terminal"Adds a custom entry. Pipe separates name, command, icon. Pass it more than once for more entries. Skips .desktop file scanning entirely.
Skips .desktop file scanning. Use this when you only want plugins or --menu entries.
Skips loading plugins.
Starts as a dedicated window switcher. Open windows are listed immediately, and typing filters them without the win: prefix. Desktop application scanning is skipped.
./zig-out/bin/zenkai --window-switcherOpens Zenkai in Windows Run mode. Type a command or URL and press Enter to open it with the Windows shell, without a run: prefix.
zenkai.exe --run./zig-out/bin/zenkai --plugin=calculator
./zig-out/bin/zenkai --plugin=calculator --plugin=notesOnly loads plugins with a matching directory name. Repeatable.
Hides icons in the list.
Hides the bottom bar.
Parses [Desktop Action ...] entries from .desktop files (e.g. "New Window", "New Private Window").
Shows desktop actions in the bottom bar instead of the list.
Closes the launcher when it loses focus.
Keeps the launcher open when it loses focus.
Shows a transparent fullscreen layer behind the launcher. Clicking it closes the launcher.
Pipes content into a command instead of opening it. Hook this up with api.open_url() in plugins.
./zig-out/bin/zenkai --clipboard="xclip -selection c"
./zig-out/bin/zenkai --clipboard="wl-copy"No more xdg-open launching a browser when you just want a URL in your clipboard.
Tells plugins what to run when they want to open a URL. Defaults to xdg-open.
./zig-out/bin/zenkai --url-handler="firefox --new-tab"Makes it talk more.
Makes it talk more and prints how long everything took to start up.
Prints timing for every stage of startup.
Watches .qss files and applies changes on the fly. Needs --debug.
Prints everything you can pass and bails out.
# Everyday launch
./zig-out/bin/zenkai
# Dracula theme, bigger icons, wider window
./zig-out/bin/zenkai --theme=dracula --size=48 --width=800
# Launch with a custom menu, skip .desktop scanning
./zig-out/bin/zenkai --menu="Firefox|firefox|firefox" --menu="Terminal|alacritty|terminal"
# Minimal look, no icons, no bottom bar
./zig-out/bin/zenkai --no-icons --no-bottom-bar
# Copy URLs to clipboard instead of opening them
./zig-out/bin/zenkai --clipboard="wl-copy"
# Quit when clicking outside the launcher
./zig-out/bin/zenkai --close-on-focus-out
# See how fast it starts
./zig-out/bin/zenkai --debug
# Hot-reload a theme you are working on
./zig-out/bin/zenkai --debug --theme-reloader --theme=./my-theme.qss
# Low memory mode (Nvidia or otherwise)
./zenkai.sh
# Plugin-only mode, no desktop apps
./zig-out/bin/zenkai --no-dapps --plugin=calculator
# Calculator with a theme, no icons
./zig-out/bin/zenkai --no-dapps --plugin=calculator --theme=dracula --no-iconsOn Nvidia or if you just want a lighter footprint, the included zenkai.sh wrapper sets some environment variables that bring RAM down to 20-50MB.
Drop a directory into external/plugins/ with a manifest.json and a main.lua and the launcher picks it up automatically. No flags needed.
Plugin discovery and ordinary plugin initialization are queued after the list's first paint. If a query arrives first, initialization happens before that query is dispatched so its results are not lost. Plugins run on the GUI thread; this defers their cost rather than moving it to a worker thread.
Plugins used only for specific queries can opt into lazy loading with an optional manifest field:
"activation_prefixes": ["run:"]The manifest is discovered after the first paint, but the Lua script and its configuration are not loaded until a query starts with one of these prefixes (ASCII case-insensitive). Prefix matching is literal: run: matches RUN: echo hello, but not run. Once activated, the plugin keeps its Lua state until the launcher exits. Its on_startup hook runs once, before its first query. Other hooks only begin participating after activation; a plugin that needs lifecycle events from startup should omit this field. Empty lists or lists containing an empty prefix fall back to ordinary initialization. Plugins that never activate do not receive close/shutdown hooks. The bundled run, Hyprland, and macOS palettes opt in; the calculator initializes after the first paint because its queries have no dedicated prefix.
--plugin=NAME still restricts discovery to the named directories, and --no-plugins skips plugin work entirely.
Application icons are resolved and cached when a row is actually painted. Layout uses a transparent placeholder of the configured icon size, so measuring offscreen rows does not load their icons. Scrolling resolves newly visible icons on demand; there is no eager pass over the remaining list. Application refresh invalidates the cache, and new plugin results discard their previous icons.
{
"name": "my-plugin",
"version": "1.0.0",
"main": "main.lua",
"description": "What it does",
"author": "you"
}Hooks are detected automatically from your Lua functions. The available hooks:
on_query(query)- called when the user types, add results withapi.add_result()on_open(id)- called when a result is selected,idmatches whatadd_resultreturned
-- Add a result to the list
-- Returns an id that gets passed to on_open()
api.add_result(title, subtitle, icon_name, result_type)
-- Open a URL when the result is selected
api.open_url("https://example.com")
-- Print to the debug log
api.log("something happened")result_type is optional. Use "NoReturn" to keep the launcher open after selecting the result (like a calculator showing an answer). The default "ExecCmd" closes the launcher and fires on_open.
function on_query(query)
if query == "ping" then
api.add_result("Pong!", "it works", "face-smile", "NoReturn")
end
endSee external/plugins/calculator/ for a full working example.
These use the same Lua hooks and plugin configuration as the other bundled
plugins. --no-plugins disables them; --plugin=NAME can select them individually.
| Query | Action |
|---|---|
2 + 3 * 4 |
Show the calculator answer; Enter copies it and keeps the launcher open. |
link: zig allocator |
Search Google or GitHub for the query. Enter opens the selected result. |
link: docs |
Find a saved link by its title. |
snip: greeting |
Find a saved snippet by title or description. Enter copies its full text. |
www.google.com |
Suggest opening https://www.google.com in the system default browser. |
win: editor |
Find an open window by title or application name; Enter switches to it. |
Quicklinks and snippets load only when their prefixes are used. Prefixes are case-insensitive. Snippets copy text; they do not type it into another app.
Copy the plugin's config.example.json to
<config directory>/<plugin name>.json, then edit it. On Windows,
the config directory is %APPDATA%/zenkai; on Linux it is
${XDG_CONFIG_HOME:-~/.config}/zenkai. Restart the launcher after changing it.
A config.json beside the plugin's main.lua is also supported as a fallback.
Quicklinks use commands entries with a title and an HTTP(S) url. A
{query} placeholder is replaced with URL-encoded search text. Set "replace": true to omit the bundled Google and GitHub searches. Snippets use title,
optional subtitle, and text; JSON \n escapes preserve line breaks.
-- Copy text through the launcher's native clipboard.
-- Returns true when a clipboard handler is available.
api.copy_text("Text to copy")on_open(id) runs for both result types. "NoReturn" keeps the launcher open
after the hook, so plugins can copy a value without closing the search.
The bottom bar is visible by default, including when the search has no results.
Only --no-bottom-bar omits it. Ctrl+K opens the searchable Actions menu
for the selected result, including when the footer is hidden. Type to filter,
use Up/Down to select, Enter to run, and Escape to dismiss. Application actions
loaded with --actions-bottombar and plugin actions keep Ctrl+1…Ctrl+0 shortcuts
for their first ten entries; the menu displays all actions.
The bundled files Lua plugin browses explicit paths and searches filenames:
| Query | Behavior |
|---|---|
/home/dayvster/workspace |
Browse the directory on Linux. |
~/Documents |
Browse a home directory path on Linux or macOS. |
C:\Users\dayvi\Workspace or C:/Users/dayvi/Workspace |
Browse a Windows directory. |
~\Workspace |
Browse a Windows home directory path. |
~/Documents/repo |
Show an exact path, or filter its parent by the partial filename. |
file: report |
Search filenames recursively under the configured roots (home by default). |
file: |
Browse home. |
Enter browses folders inside Zenkai and opens files with their default app.
The .. row navigates up. Directories show folders first and page in groups of
128. Explicit browsing includes hidden entries. File and folder icons come from
Qt's native file icon provider on Windows, macOS, and Linux; --no-icons still
hides them. Linux opening requires xdg-open.
Search is incremental, not indexed: select Continue searching to scan the next folders. Each step scans up to four directory pages and returns up to 100 matches. Matches use case-insensitive ASCII substring matching. Search skips symlink directories and common dependency/cache directories; unavailable folders are reported. Individual directories larger than 100,000 entries are rejected. Network and large directories can still take time to enumerate.
Copy external/plugins/files/config.example.json to the config directory as
files.json and customize it. Windows uses %APPDATA%\zenkai\files.json;
Linux and macOS use ${XDG_CONFIG_HOME:-~/.config}/zenkai/files.json.
Use forward slashes or escaped backslashes in JSON Windows paths:
{
"roots": ["~/Workspace", "~/Documents"],
"exclude": [".git", "node_modules", ".cache", ".zig-cache", "zig-pkg", "Library", "AppData"],
"include_hidden": false
}Restart after changing configuration. Explicit paths work outside the search
roots. Use --plugin=files to load only this plugin or --no-plugins to disable it.
The Actions menu includes common actions such as Copy Name and Copy Launch Command, plus actions supplied by the selected result:
- Files: Open with Default App, Copy Path, Browse Containing Folder, Refresh.
- Folders: Open in File Manager, Copy Path, Browse Folder, Refresh.
- Windows: Switch to Window, Copy Window Title, Refresh Windows.
- URLs and quicklinks: Copy URL.
- Snippets: Copy Snippet.
- Calculator: Copy Result and Copy Expression.
- Browser applications: New Window and New Private Window (InPrivate for Edge).
Browser actions are detected when opening the menu, without adding startup work. Supported executables include Chrome, Chromium, Brave, Vivaldi, Edge, Firefox, and LibreWolf, including recognized Flatpak/Snap application IDs. Windows resolves ordinary named browser shortcuts to their executable; shortcut-specific arguments are not inherited. Direct launch commands retain profile/package arguments. macOS invokes the browser's bundle executable so a running browser receives the window request. Safari, opaque shell wrappers, and recognized web-app/kiosk entries do not receive these actions. These actions apply to application results.
Launch options follow the browsers' implementations: Chromium switches, Brave flags, and Firefox parameters.
Lua plugins register actions during on_query and handle them separately:
local id = api.add_result("Example", "Description", "edit-copy", "NoReturn")
api.add_action(id, "Copy Value", 1, "edit-copy")
function on_action(result_id, action_id)
if action_id == 1 then api.copy_text("Value") end
endUp to 64 actions may be attached to each result. Actions keep the launcher open
unless the hook calls api.close(). api.set_query(text) from on_open or
on_action changes the query after the hook returns, so plugins can navigate
without rebuilding results during a Lua callback. Calls from on_query are
ignored.
Filesystem primitives are available to plugins; traversal policy stays in Lua:
local entry, err = api.path_info("~/Documents")
-- entry: path, name, parent, is_dir, is_link, icon
local page, err = api.read_dir("~/Documents", 0, "report")
-- page: path, parent, entries, more; offset and filename filter are optional
local ok, err = api.open_path("~/Documents/report.pdf")Paths are passed as native paths/arguments, not shell commands. Missing paths
and filesystem errors return nil, message.
The bundled calculator Lua plugin accepts expressions directly in the search bar:
| Example | Result |
|---|---|
sqrt(144) + max(2, 3) |
15 |
sin(pi / 2) |
1 (trigonometry uses radians) |
1e3 / 4 |
250 |
15% of 80 |
12 |
100 cm to m |
1 m |
32 f to c |
0 c |
2 GiB to MiB |
2048 MiB |
100 USD to EUR |
Latest available reference rate, with its date |
Functions include sqrt, abs, floor, ceil, round, min, max,
sin, cos, tan, asin, acos, atan, ln, log (base 10), exp,
rad, and deg; constants are pi, tau, and e. Conversions support
length, mass, time, volume, speed, temperature, angles, and data sizes.
Data units distinguish bits (Mb) from bytes (MB) and decimal from binary (MiB).
Enter copies the result; Ctrl+K also offers Copy Expression.
Currency requests and parsing are implemented in Lua using the asynchronous native
api.http_get bridge. Frankfurter supplies daily
reference rates without an API key; these are not intraday trading quotes.
Only currency codes are sent, never the amount. The dated rate is cached for the
launcher session. Offline failures show an unavailable result; restart to retry.
Normal calculations and unit conversions work offline.
For plugin authors, api.http_get(https_url) returns nil, "pending" while
fetching, then body, "ok", or nil, error_message. Completion dispatches
on_idle, where the plugin can request a query refresh with api.set_query.
Responses are cached until exit, capped at 64 KiB each and 32 distinct URLs per
session, with a five-second transfer inactivity timeout. TLS certificates are
validated by Qt. No external interpreter or command-line HTTP tool is required.
Window switching is built into the Zig core. Type win: to list open windows,
filter by title or application name, and focus the selected window. It omits
Zenkai itself. It is enabled by default, works with --no-plugins, and can be
disabled independently with --no-window-switcher. Pass --window-switcher to
start directly in this mode with the window list already populated and the
win: prefix optional. Run mode does not use it.
Clear back to win: (or clear the search in window-switcher mode) to refresh
the window list; typing a filter searches the snapshot without repeatedly
starting desktop tools. Failed focus keeps the launcher open and displays the
error; changing the query refreshes the list.
Select a window result and press Ctrl+K for Left/Right/Top/Bottom Half, Maximize, Center Window, and Move to Next Display. Layouts use monitor work areas, including negative monitor coordinates and desktop scaling. Center and Next Display preserve the window size when it fits. Applications may enforce a minimum size. On macOS, Maximize fills the desktop rather than creating a new fullscreen Space. Hyprland and Sway actions make tiled windows floating; leave Hyprland fullscreen mode before arranging a window.
Window results display the owning application's icon: executable icons on
Windows, application bundle icons on macOS, and desktop entry Icon values on
Linux (matched by app ID or StartupWMClass). Missing metadata falls back to
the usual launcher icon. Linux searches the XDG data directories, including
user-installed desktop entries.
| Platform | Backend and requirements |
|---|---|
| Windows | Native Win32 enumeration and focus; minimized windows are restored. No helper installation. |
| macOS | Native AppKit and Accessibility APIs called from Zig. Allow Zenkai Accessibility access in System Settings → Privacy & Security. |
| Linux / X11 | Install wmctrl and xrandr for window layouts; the window manager must support EWMH. |
| Linux / Hyprland | hyprctl in PATH and the compositor's session environment. |
| Linux / Sway | swaymsg in PATH and the compositor's session environment. |
Other Wayland compositors, including GNOME and KDE Wayland, currently show an unsupported-session message. They require their own compositor integration; XWayland alone cannot enumerate their native Wayland windows. macOS retains the exact native window reference until the list is refreshed. The macOS and Linux backends have not been tested on a live desktop from this Windows development environment.
URL suggestions display the system default browser's icon, separately for HTTP
and HTTPS. Windows uses protocol associations, macOS uses NSWorkspace, and Linux
uses xdg-mime plus the handler's desktop entry. The URL plugin caches those
descriptors for its lifetime; restart Zenkai after changing your default browser.
Native icons are loaded through Qt only when their result row is painted, and
--no-icons still hides them.
The url Lua plugin uses anchored Lua patterns and host/port validation to
recognize HTTP(S) URLs, bare domains, IPv4 addresses, and localhost:PORT.
Bare domains default to HTTPS; localhost with a port defaults to HTTP. Paths,
queries, and fragments are preserved. Ordinary multiword searches, email
addresses, unsupported schemes, and malformed hosts are ignored. Unicode
domains should be entered in their ASCII/punycode form; IPv6 literals are not
currently recognized.
Opening requires selecting the suggestion and pressing Enter. This plugin uses
the system default browser, independent of --clipboard and --url-handler.
Disable it with --no-plugins, or use --plugin=NAME to select other plugins.
The native services are also available to other Lua plugins:
api.open_browser("https://example.com") -- system browser; HTTP(S) only
local icon = api.browser_icon("https") -- default handler's icon; also accepts "http"
api.close() -- request closing after on_open, including for NoReturn results

