# Dalil — the command palette
**Dalil** (دليل, *"guide"*) is a fuzzy-search palette for your whole desktop. It is
its own program, `dalil`, started by the window manager from the config
(`startup-command "dalil"`); bind a key to `signal "palette"` and it appears.
```kdl
keybindings {
bind "alt+space" { signal "palette" }
bind "alt+shift+space" { signal "palette" "commands" }
}
dalil {
open-on-signal "palette"
show-unmanaged #true
confirm-destructive #true
}
```
Dalil is opened by a **signal**, not by a hard-wired command — which means Shubbak
does not know Dalil exists. That is the same extension point anything else can use;
see [Scripting](scripting.md).
## Modes
Eight of them. Every one has a prefix, and every one has a **Ctrl+digit** that jumps
straight to it in the order the hint bar draws them — because a prefix is faster and
`~` is a dead key on several European layouts, where it produces no character at all
until you press something else:
| Prefix | Jump | Mode | |
|---|---|---|---|
| *(none)* | `Ctrl+1` | Windows | Every window on the desktop, managed or not |
| `>` | `Ctrl+2` | Commands | Every verb, plus your own named actions |
| `#` | `Ctrl+3` | Workspaces | With window count, layout and monitor |
| `!` | `Ctrl+4` | Inspect | Every window Shubbak is **not** managing, and why not — with a row that widens it to the tool windows and popups it never lists, which are the ones a `manage` rule is for |
| `$` | `Ctrl+5` | Scratchpad | Everything you have stashed, by slot |
| `~` | `Ctrl+6` | Layouts | What each one actually does, and the one you are in |
| `%` | `Ctrl+7` | Monitors | Size, DPI, and what each is showing |
| `?` | `Ctrl+8` | Help | The palette's keys — **and your own keybindings** |
Prefixes are yours to move: `dalil { prefixes { layouts "^" } }`. Keep them to
punctuation - a letter or a digit as a prefix would turn the first keystroke of every
search into a change of mode, and the palette says so (`DAL0018`) if you try.
Inside the palette: type to filter, up/down or Ctrl+N/Ctrl+P to move, Tab to change
mode, Enter to act, Escape or Alt+F4 to dismiss. The mouse works too. The search box
is a text field with the manners of one: Left/Right and Home/End move the caret by
whole characters - an emoji or a letter with its mark is one step and one Backspace -
**Ctrl+V** or **Shift+Insert** pastes (a copied line with its newline becomes one
line), and **Ctrl+A** selects what you typed so the next key replaces it. `?` lists
every key.
## Searching
The matcher is a subsequence matcher with opinions: every letter you type must appear
in order, and the score comes from *where* it lands. Letters at the start of a word,
after a separator or at a camel-case boundary are worth far more than letters in the
middle of one - that is what an abbreviation is made of - and an unbroken run is worth
more than the same letters scattered, so a prefix beats a coincidence and `dsc` finds
Discord. Of every way the letters could be placed, the best one is taken: `st` lights
the **St** of *Visual Studio*, not the *s* of Visual and the *t* of Studio.
Letters are compared folded. Case does not matter; nor do accents, so `cafe` finds
*Café*; nor do the Arabic spellings of one sound - the alifs with and without hamza,
`ة` and `ه`, `ى` and `ي`, the Persian kaf and yeh and the Arabic ones - so `احمد` finds
*أحمد*, and vowel marks are stepped over on both sides, so `محمد` finds *مُحَمَّد*. A
title that reads right to left is drawn whole rather than highlighted, because placing
a colour inside shaped, reordered text needs the shaper's own positions and a title
read correctly beats one underlined and unreadable.
A space separates words that must all match, in any order: `code proj` finds *My
Project - Visual Studio Code*. The space you have just typed before your next word
is not yet a word and does not empty the list.
**The command list learns.** What you run from it is remembered - a count per verb or
action that halves every fortnight - and before you type, the list is in that order:
the command you run every day is at the top, the hundred you never touch are
alphabetical below it. Once you type, the match decides and the history only settles
ties. The record is `%LOCALAPPDATA%\Shubbak\dalil-frecency.tsv`, one line per command,
and deleting it forgets everything.
## What it does well
**Type a command and it is parsed for real.** Whatever you type becomes a top-ranked
row, run through the *same* parser your config file uses. So a bad argument gives
you the same message it would at load time, right there, before you press Enter.
**Mark windows and act on all of them.** `Ctrl+Space` marks; `Ctrl+Enter` then acts
on the set — move them all to one workspace, float them, close them. Doing that with
keybindings is six rounds of find-it, focus-it, move-it, with the focus landing
somewhere different after each one. This is the thing a palette is genuinely *for*.
**`shubbak inspect`, without leaving the palette.** Press **Ctrl+Shift+I** on any
window and you get the full report — attributes, verdict, which rules matched, which
app definitions missed and on which matcher. Any line too long to fit opens in full
with Enter, and Escape or Backspace steps back out. **Ctrl+C** copies the selected
line; **Ctrl+Shift+C** copies the whole report, which is the version that belongs in
a bug report.
**And then it writes the rule for you — and adds it, if you say so.** "Write a rule
for it…" opens the rules that could be written for the window, each complete and
named for what it does, best first. A managed window is offered **Ignore it** first;
a window the filter turned down is offered **Manage it** (only where a rule could
actually overrule the filter — a cloaked window is not offered one that would look
right and do nothing); a window a rule already ignores or forces is offered **Stop
ignoring it** / **Stop forcing it**, which removes that rule. Float it, tile it, send
it to a workspace, and "Match it, decide later" — the matchers written and the `do`
block left to you — are always there.
Enter on a choice reads the rule. **Ctrl+Enter** is where it leaves the palette:
**Add it to the config and reload** appends it to `shubbak.kdl` and reloads, and the
answer says what happened — *Added "ignore ms-teams" at line 412 and reloaded. "Teams"
was released.* — with **Remove it again** right under it, because it did not ask
first. **Copy the rule** and **Open the config** remain for the user who would rather
place it by hand. Inside a rule, Ctrl+C copies one line and Ctrl+Shift+C copies all of
it, and the hint bar says so.
The window manager does the writing: it validates the whole file as it would load it
and refuses rather than leave it broken, and a rule that runs `shell-exec` is refused
unless the pipe may run it directly. Every rule row in a report can be removed the same
way, and removal prints the rule so it can be put back. Nothing is guessed: the `do`
block holds what you chose, and the same window one person wants floated is one
another wants ignored. `general { allow-config-edits-over-ipc #false }` turns the
whole thing off; `shubbak rule add` and `shubbak rule remove` do the same from a
terminal. See [Configuration](configuration.md#window-rules).
**Every row has actions** (Ctrl+Enter): go to it, bring it here, send it to another
workspace, float/tile, minimise/restore, make it sticky, edit its tags, write a rule
for it, close it, start or stop managing it, and inspect it. Closing asks first —
whichever route you reached it by, chord included — and nothing else does, because
nothing else is irreversible.
## Your own actions
Keybindings are a scarce resource; palette rows are not.
```kdl
dalil {
action "Dev layout" description="Editor left, terminal right, on 2" {
focus --workspace "2"
layout --set "master-left"
equalise
}
}
```
They are validated against the real parser at load time, so a typo is reported on the
row rather than swallowed.
**A row can ask.** A `param` turns one row into a question, so a single entry stands
in for one per workspace — nineteen of them, in the author's config, each of which
would otherwise need its own name to invent and its own line to keep:
```kdl
dalil {
action "Send it to..." description="Move it there and stay where you are" {
param "ws" from="workspaces"
move --workspace "{ws}"
}
action "Arrange..." description="Go somewhere and lay it out, in one gesture" {
param "ws" from="workspaces"
param "l" from="layouts"
focus --workspace "{ws}"
layout --set "{l}"
equalise
}
}
```
Enter opens the picker; Escape goes back one question rather than dismissing. Choices
come `from=` a list the palette already holds — `workspaces`, `layouts`,
`binding-modes`, `scratchpads`, `directions`, `contexts`, `arrangements` — or from
`values="a b c"` when you want a set the window manager does not know. Workspaces are
shown as `3 — Code`, because a picker reading `\` is not one anybody can choose from.
**A program can answer the question.** `run=` on a `param` names a program, and each
line it prints is a choice — the palette's version of the bar's `kind="command"`
source, and how a list the window manager knows nothing about gets onto the palette:
projects, notes, bookmarks, whatever a script can enumerate.
```kdl
general { allow-shell-exec-over-ipc #true } // see below
dalil {
action "Open a project..." description="Any folder under ~/Github" {
param "p" run="pwsh -NoProfile -File projects.ps1"
shell-exec code "{p}"
}
}
```
The program runs when the question is asked, not when the palette opens, so a palette
opened for a window pays nothing; the frame opens at once saying what it is asking and
the rows replace that when the program has printed. A line is the value; a line with a
tab in it is what to show, then what to substitute — `ShubbakW:\Github\Shubbak`
— so a path is chosen by its name and the row's dim half still says where it goes.
Blank lines are skipped and a value seen twice is offered once. A program that prints
nothing usable, cannot be started, or has not finished in ten seconds is said so in
the frame, with what it wrote to standard error; one that outstays its welcome is
stopped, along with anything it started. The first thousand lines are read. `run=`
with nothing to run is an error (`DAL0019`); a later `param` may be a program too,
and a program's answer may lead to another question.
The example above runs `shell-exec`, which the window manager refuses over the pipe
unless `general { allow-shell-exec-over-ipc #true }` — and every palette action
travels over the pipe, which is why the example sets it. Without it, rather than close
and do nothing, such a row is listed as unable to run with the setting named, and
`check-config` says the same (`DAL0020`). The pipe is scoped to your account, not your
integrity level, which is why the default is off; see [Scripting](scripting.md#security).
The checking is real: a placeholder nothing declares is an error with a line and a
caret, a question no command asks is a warning, and `move --direction "{d}"` is probed
with an actual direction before the parser sees it rather than waved through.
**Put an action on a key without writing it twice.** `signal "palette" "run" ""`
runs a named action outright and shows nothing:
```kdl
bind "alt+ctrl+d" { signal "palette" "run" "Deep work" }
```
Shubbak still has no idea what an action is — it carries the name without reading it,
and the palette is what knows. An action that *asks* cannot be answered by a key, so
those open the palette with the name already typed and the picker one Enter away.
**A row can follow a context.** `when-context=` offers a row only while a context
holds; `unless-context=` only while it does not. That is what makes a pair of rows read
as one switch — and it is how a program the palette knows nothing about gets its state
onto the palette: it holds a context, as the focus timer in
[Extending](extending.md#a-worked-example-a-focus-timer) does and as the watcher does
for the microphone, and the rows follow.
```kdl
contexts {
context "focusing" { }
context "meeting-live" { }
context "meeting-muted" { }
}
dalil {
action "Start focus timer" unless-context="focusing" { signal "focus" "start" "25" }
action "Stop focus timer" when-context="focusing" { signal "focus" "stop" }
action "Mute the mic" when-context="meeting-live" { signal "ayn" "microphone" "mute" }
action "Unmute the mic" when-context="meeting-muted" { signal "ayn" "microphone" "unmute" }
}
```
While nothing is running the list has "Start" and not "Stop"; while the timer holds
`focusing` it has "Stop" and not "Start"; and the microphone rows appear only in a
meeting, one at a time. The list changes as the context does — the palette already
re-reads the world on `context.changed` — and a row that applies is an ordinary row.
A row kept back is not gone: `>actions`, the palette's own list of every action you
have written, shows it greyed with the condition beside it (`not now · only while
context 'focusing' holds`), so a row you cannot find is one lookup away and says why.
A key bound to `signal "palette" "run" "Start focus timer"` is judged the same way,
against what holds at the keystroke: a row that is kept back opens that list rather
than running or doing nothing. Both keys on one action must hold together; a name the
`contexts` section does not declare never holds and is `DAL0021`, one context named
by both keys is a row that can never be offered and is `DAL0022`, and a misspelt key
on an action — `when-contxt=` — is `DAL0023` rather than a condition quietly not there.
## What the rows tell you
Rows carry the application's icon and badges so you can see at a glance what you are
looking at: `unmanaged`, `minimised`, `cloaked`, `floating`, `fullscreen`, `sticky`,
`elevated`, `stashed`, `also on `. Unmanaged windows also carry the reason
in the dim text, so you do not have to open anything to find out why. The icons come
from the window manager over the pipe — the same [`window-icon`](scripting.md#asking)
the bar uses — fetched on the background thread that reads the list, once per window
the palette has not seen, so a window that never set an icon still shows the one its
taskbar button does, and the palette itself never sends a message to a window it
lists.
The search box tells you when tiling is paused, when a binding mode is eating your
keys, when the window manager has suspended itself, and when it cannot be reached at
all — because all four look exactly like a crash from the outside — and, quieter than
any of those, which contexts it holds, because the keys in force are then not the
ones in the file. Typing `context --toggle ` completes the declared names, and
`>config` lists anything wrong with the palette's own section. `>config path` copies
the path of the file in effect and `>open config` opens it, with whatever Windows
opens `.kdl` files with — the palette does not pick an editor.
## Settings
The `dalil` section, all optional: `open-on-signal` (the signal that opens it),
`width`, `row-height`, `visible-rows` (a cap on drawing, not on searching),
`close-on-blur`, `show-unmanaged`, `confirm-destructive`, `show-icons`,
`shrink-to-fit`, `placement` (`focused-monitor`, `cursor-monitor` or `primary`),
`prefixes`, and the `action` blocks above. Sizes and the font are written at 96 DPI
and scaled to whichever monitor the palette opens on - and stay scaled through a
reload of the file. `shubbak check-config` reports a misspelt setting here with a
line, a column and a caret, like everywhere else, and a prefix that is a letter or a
digit with `DAL0018`.
An empty list says why it is empty in the words of the mode it is in: an empty
scratchpad says nothing is stashed and how to stash something, an empty inspect list
says every window is managed, and only a list the window manager fills - layouts,
displays, commands - says it is still waiting for an answer. A window manager that
cannot be reached says that, whatever the mode.
## When the window manager goes away
After `wm-exit` the palette stays and reconnects when the window manager returns; its
search box says it cannot be reached meanwhile. That is for restarting the window
manager. After `exit-all` - which is what the tray's Exit and the starter config's
`alt+shift+e` run - it leaves with everything else. `shubbak dalil-exit` closes it on
its own, and `shubbak stop` closes it along with everything else from outside, window
manager or no window manager. Only one palette runs per account.