// Shubbak configuration. // // This file is the KDL translation of the author's GlazeWM config, kept as the // annotated reference: every setting appears here with the reasoning beside it. It // is something to read and borrow from rather than to copy whole - the monitors are // named by the author's connector paths, the nineteen workspaces are the author's, // and a few rules match programs you may not have. `shubbak config init` writes a // short starter that is meant to be inherited; `shubbak monitors` prints the // `monitor` definitions for your own displays, ready to paste. // // Two things it does that the original could not: // // * `for-each "workspace"` generates the per-workspace bindings, replacing // 40 lines of near-identical entries with six. // * every mistake is reported with a line, a column and a caret when the config // loads, rather than silently doing nothing when a key is eventually pressed. general { // Switch back to the previous workspace when re-focusing the active one. toggle-workspace-on-refocus #true // Whether `move --workspace N` takes the view with the window. // // Off, because "put this away" and "go there with it" are separate intentions. // Sending a window somewhere you are not looking is the common one - it is how // a workspace gets tidied without leaving it - and a move that dragged the // screen along every time would make that impossible to express. // // The keys below say the other intention per-binding, with `move --workspace N // --focus`. Turn this on if you want every move to follow, including the ones // window rules and the command palette make: the palette's "send it to 3 and // leave it there" then does not leave it there. follow-window-on-move #false // New windows start tiled. initial-window-state "tiling" // Which workspace a new window lands on. // // focus (default) - the workspace you are looking at. // window - the active workspace of whichever monitor the window // itself opened on. // // These differ only on a multi-monitor desktop, and there they differ often: // Windows reopens most applications wherever they were last, which has nothing // to do with where you are now. "window" was the original behaviour and is // right for an application that picks its display deliberately - a slideshow // going to the projector - but as a default it meant launching something while // working on one monitor could open it on the other. new-window-placement "focus" // Where the keyboard goes when the workspace you are looking at is empty. // // hold (default) - on an invisible, zero-sized window of Shubbak's own, // placed on that monitor. // desktop - on the desktop, which is what Shubbak did before. // // The keyboard has to be somewhere, and Windows gives it back to whatever had // it when the next thing to take it lets go. On an empty workspace the next // thing is a launcher, and when the launcher closes the keyboard would go to // the window that had it before - which the desktop is never chosen to be, so // on two monitors it went to whatever was displayed on the other one, and the // application the launcher started opened there. Holding it on a window of // Shubbak's own on the empty monitor is what keeps both where you are looking. // The same window catches the last window on a workspace closing or minimising // after it, which used to move the point of action to the other monitor too. // // Alt+Tab and the taskbar never list it. Alt+Esc, which walks windows in // stacking order without the Alt+Tab picture, does reach it - and it passes the // keyboard straight on to the next window, so no press is lost to it. empty-workspace-focus "hold" // What a command that targets a window does when the focused window is one // Shubbak does not manage - a dialog, a tray popup, an app it passed over. // // refuse (default) - do nothing, and say which window and why. A command // that does nothing is recoverable; one that acts on the // wrong window may not be. // adopt - take the window on first, then run the command. // // Either way `toggle-managed` still works, which is the way in. unmanaged-window-commands "refuse" // Whether `shell-exec` may be sent over the IPC pipe. // // Off, because a window manager is not an execution service. The command exists // so a keybinding or a startup command below can launch a terminal - decisions // made deliberately in this file - and it stays available to those either way. // // The pipe is scoped to the account, not to the integrity level, so leaving this // open means any process running as you can ask an elevated Shubbak to start // something elevated. allow-shell-exec-over-ipc #false // Whether tools may add and remove rules in this file over the pipe - the // palette's "Add it to the config and reload", and `shubbak rule add`. // // On, and the asymmetry with the setting above is deliberate: every process // running as you can already open this file and write to it, so refusing to do // it on their behalf would protect nothing. What is permitted is narrow - rules, // and only rules, appended as a block of their own and validated first - and a // rule that runs `shell-exec` is refused unless the setting above allows it. allow-config-edits-over-ipc #true // Whether saving this file reloads it. // // On. The window manager watches the file and reloads a moment after any editor // saves it, with the same gate as `wm-reload-config`: a file with errors is // reported and the running configuration is kept. Nothing is polled - the folder // tells the window manager when the file changes - so a file nobody is editing // costs nothing. Turn it off to reload by the key alone. reload-on-save #true // The layout a new workspace starts in. An unrecognised name is an error at // load rather than a silent fall back to horizontal. // // splith splitv fibonacci fibonacci-v fibonacci-mirrored // master-left master-right master-top master-bottom grid monocle default-layout "splith" // How windows on inactive workspaces are concealed. // // "cloak" (default) is strongly preferred: a cloaked window still reports as // visible to Win32, so if Shubbak exits or is killed the next run adopts it and // un-cloaks it. "hide" is unrecoverable - the filter rejects invisible windows, // so they stay stranded with their process still running. Only fall back to it // if the compositor is unavailable, which mainly means remote sessions. hide-method "cloak" // The bar, the palette and the watcher are separate programs, started here when // the window manager starts. Each reads its own section of this file. A bare // name is looked for beside shubbak-wm.exe first and then on PATH, so this works // from a fresh install before any terminal has seen the new PATH. startup-command "taj" startup-command "dalil" startup-command "ayn" } gaps { inner 6 outer { // The bar reserves its own strip of the screen (it registers as an appbar), // so this is a gap between the bar and the first window, not room for it. top 26 right 4 bottom 4 left 4 } } animation { enabled #true // Frames a second to aim for while anything is moving. // // "auto" (the default) follows the refresh rate of the fastest display attached, // re-read as monitors come and go. Any other answer is a guess about hardware // Shubbak can simply ask about, and the guess is wrong in both directions: on a // 60 Hz panel, asking for 90 means half as many frames again as the display can // present, thrown away by the compositor after every application has already been // told to repaint. On a 240 Hz panel a fixed 60 delivers a quarter of what the // panel would take. // // A number overrides it, and is worth having on a very fast panel where the // applications rather than the display are the limit. 15 to 240. fps "auto" // Below this many pixels a move is applied instantly. Animating a three-pixel // nudge costs a dozen frames and reads as lag rather than motion. minimum-distance 8 // Whether a window joining the layout for the first time slides into its tile. // // Off, because the rectangle it would travel from is whatever size the // application opened at - it was never part of the arrangement, so the motion // describes nothing that happened. It is also the most expensive animation // there is: a window that relays out its contents on every resize does so once // per frame, which File Explorer makes very obvious. // // When on it uses the window-open profile below rather than window-move, so a // shorter open than move is easy to arrange - which is usually what stops it // feeling sluggish. animate-new-windows #false // Curves matter more than durations for how motion feels: ease-out covers most // of the distance immediately, so it reads as responsive. // Available: linear, ease-in, ease-out, ease-in-out, ease-out-back, ease-out-expo window-open duration=180 curve="ease-out-expo" window-move duration=140 curve="ease-out" layout-change duration=180 curve="ease-out" workspace-switch duration=120 curve="ease-out" // A resize is instant: two windows share the edge being moved, and frames posted // to two applications land at two speeds. Give it a duration to animate anyway. // resize duration=100 curve="ease-out" } window-effects { border #true // A hex colour, or `accent` for the colour Windows is set to - resolved each // time a border is painted, so a change of accent reaches the next focus change. focused-colour "#8dbcff" unfocused-colour "#a1a1a1" // Windows outside the tiling flow can be marked differently. They look // identical to tiled ones otherwise, while behaving completely differently: // they ignore the layout, they can be dragged anywhere, and directional focus // passes over them. Both fall back to the colours above when unset. floating-colour "#f9e2af" floating-unfocused-colour "#6c5f3f" } // --------------------------------------------------------------------------- // Named app definitions. Rules reference these by name so they read // semantically instead of as a wall of inline regexes. // --------------------------------------------------------------------------- app "taj" { process = "taj" } app "browser-picture-in-picture" { // Raw strings (r"...") need no backslash escaping, which matters for regexes. title regex=r"[Pp]icture.in.[Pp]icture" class regex=r"Chrome_WidgetWin_1|MozillaDialogClass" } app "powertoys-accent" { process = "PowerToys.PowerAccent" } app "lively-wallpaper" { process = "Lively" class regex=r"HwndWrapper.*" } app "powerpoint-slideshow" { // Note: no slashes around the pattern. Shubbak warns if you add them, because // they would be matched literally - the bug this config used to have. title regex=r"[Pp]ower[Pp]oint [Ss]lide [Ss]how" } app "command-palette" { title = "Command Palette" } rules { rule "ignore chrome" { match { app "taj" app "browser-picture-in-picture" app "powertoys-accent" app "lively-wallpaper" app "powerpoint-slideshow" app "command-palette" } do { ignore } } // `manage` is the other direction: it takes on a window the built-in filter // passed over. Those exclusions are heuristics - no title yet, declares itself // a tool window, owned by an invisible parent so it never reaches Alt+Tab - and // they are wrong for some applications. Run `shubbak inspect` on the window and // the "manageable" line names the exact reason. // // What cannot be overridden: the desktop, the shell, child controls, and // windows that are cloaked or have no area. Those are not opinions. // // rule "whatsapp" { // match { process = "WhatsApp" } // do { manage } // } // `float` and `tile` state a fact, unlike `toggle-floating`, which flips // whatever is currently true - so a toggle in a rule does the opposite of what // was meant as soon as something else has already floated the window. // // rule "keep the calculator loose" { // match { process = "CalculatorApp" } // do { float } // } // Matchers: title, class, process, path. Operators are `=` exact, `~=` regex, // `^=` prefix, `$=` suffix, `*=` substring - or the named forms `equals`, // `regex`, `prefix`, `suffix`, `contains`. Prefix a matcher with `!` to negate. // // Rules run on `manage` by default; `on="title-change"` and `on="focus"` are // also available, which is how to catch a window that only identifies itself // once it has loaded. } // --------------------------------------------------------------------------- // Monitors, named by what they are. // // `monitor=1` on a workspace is a position in the order Windows reports displays, // and Windows reorders that on replug, on DisplayPort wake and on a driver restart // - which is how a workspace bound to "the right-hand screen" ends up on the left // one after a dock. A definition here names the screen instead, by what the // display configuration reports about it, and a workspace bound to the name goes // back to that screen whenever it is attached. // // `shubbak monitors` prints one of these for every attached display, ready to // paste. Match on `name` (the panel's EDID name, e.g. "DELL U3219Q"), `path` (the // connector's device path, which is what tells two of the same model apart), or // `device` (the \\.\DISPLAYn name, for what that is worth); and on the two flags // `internal` and `primary`. Same operators and `!` negation as an `app`. // --------------------------------------------------------------------------- monitor "laptop" { internal } // Two of the same monitor report the same name, so the path is what tells them // apart. The segment after the last `&` is the connector's own id. monitor "dell-left" { // DELL U3219Q path *= "UID4355" } monitor "dell-right" { // DELL U3219Q path *= "UID4357" } // --------------------------------------------------------------------------- // Workspaces. Declared workspaces persist when empty, so their bindings keep // working; workspaces created on demand are reaped once their last window goes. // // `monitor=` takes a declared name or a position. A workspace whose monitor is // unplugged moves to a survivor and moves back when the monitor returns. // --------------------------------------------------------------------------- workspaces { workspace "1" display-name="Firefox" monitor="dell-left" workspace "2" display-name="Edge" monitor="dell-left" workspace "3" display-name="Code" monitor="dell-left" workspace "4" workspace "5" workspace "6" workspace "7" monitor="dell-left" workspace "8" monitor="dell-left" workspace "9" workspace "0" display-name="Docs" monitor="dell-left" workspace "-" display-name="Chat" workspace "\\" display-name="Presentation" monitor="dell-left" workspace "=" display-name="Notes" workspace "]" display-name="Mail" monitor="dell-left" workspace "/" display-name="Second" monitor="dell-right" workspace "`" display-name="System" monitor="dell-right" workspace "'" display-name="AI" monitor="dell-left" workspace ";" display-name="Slides" monitor="dell-left" workspace "[" display-name="Recording" monitor=2 } // --------------------------------------------------------------------------- // Contexts: named conditions on the desktop that layer overrides on this file // while they hold. // // A context is level-triggered - active exactly while a `when` block holds, no // stuck state, no exit rule to forget. Within a block every condition must hold; // several blocks mean any one of them is enough. Prefix a condition with `!` to // negate it. Several contexts can hold at once, and their overrides cascade in // declaration order, later winning, with this file underneath as layer zero. // // Conditions, all of them things Shubbak already knows or Windows says about the // session in one cheap call - never what an application is doing internally: // // window app="x" / window { process = "..." } some top-level window matches, // managed or not // focused app="x" the foreground window matches // fullscreen [app="x"] a managed window is full-screen // workspace active="3" / workspace focused="3" // monitors count=2 / min=2 / max=1 // monitor present="dell-left" / monitor absent="dell-left" // display-topology "extend" internal, clone, extend, external (Win+P) // remote-session // system-state "presenting" ordinary, presenting, fullscreen-app, // fullscreen-game, quiet-time, away // context "meeting" another context holds // // What a context can change: gaps, window-effects, animation (each a delta on // the section above - only what you write changes), bindings (laid over the // default keys; an empty binding disarms a key), rules (only while it holds), // workspaces (a workspace lives elsewhere while it holds), and on-enter / // on-exit commands, run once each way. // // Pin one by hand with `context --set presenting`, `--clear`, `--toggle`, or hand // it back to its conditions with `--auto`. `shubbak contexts` says why each one is // the way it is, condition by condition. // --------------------------------------------------------------------------- contexts { // On stage. The slide show window exists, or the shell says presentation mode // (the Windows Mobility Center toggle). Gaps and borders go, animation stops, // the close key is disarmed so a slip cannot take the deck with it, and the // slides workspace moves to the projector when one is attached. context "presenting" { when { window app="powerpoint-slideshow" } when { system-state "presenting" } // PowerPoint creates and destroys several windows while a show starts. // Half a second of patience stops the context flapping through them. linger 500 gaps { inner 0; outer { top 0; right 0; bottom 0; left 0 } } window-effects { border #false } animation { enabled #false } bindings { bind "alt+shift+q" { } } on-enter { focus --workspace ";" } } // Docked: the right-hand Dell is attached. Nothing to change here; it exists // so a bar rule, a palette row or a script can key off it, and so that // "docked-meeting" below can compose it. context "docked" { when { monitor present="dell-right" } } // External: nothing on the desktop decides these, so they have no `when`. // Another program sets them over the pipe - `shubbak context --set // camera-in-use --ttl 10s` from a script, or a held connection with --lease so // the pin dies with the program. Ayn, the watcher that ships beside the bar and // the palette, holds exactly these three while they are true; see the `ayn` // section at the end of the file. Named subject-state, so the facts about one // device sit together and the next device slots in. Nothing to change here: // they are facts, and "meeting" below is the policy. context "camera-in-use" { } context "microphone-in-use" { } context "microphone-muted" { } // A meeting is the microphone being open - Teams and its kind keep the device // open for the whole call, muted in-app or not - or whatever else you decide it // is: a calendar, a Stream Deck key, composed from facts that arrive from // outside. The config says what a meeting does; the program never needs to know. context "meeting" { when { context "microphone-in-use" } window-effects { focused-colour "#f38ba8" } } // Muted while in a meeting: the only time being muted is worth an icon on the // bar, which is what this context is for. Its twin, with the condition negated, // is for the other glyph: one of the two holds during a meeting, never both, so // the bar shows exactly one microphone. See the `mic` widgets below. context "meeting-muted" { when { context "meeting"; context "microphone-muted" } } context "meeting-live" { when { context "meeting"; !context "microphone-muted" } } // Composed. Both hold, so both sets of changes are already in force; this one // adds what only the combination wants. context "docked-meeting" { when { context "docked"; context "meeting" } workspaces { workspace "-" monitor="dell-right" } } } // --------------------------------------------------------------------------- // Keybindings // --------------------------------------------------------------------------- // // Holding a key repeats it. That is what makes holding alt+h walk the focus // across a workspace, and it is the default. // // Commands where repeating would be wrong opt out on their own: close, // shell-exec, exit, reload-config, and every toggle. Any binding can override // that either way with repeat=#true or repeat=#false. // // Keys you can name, beyond letters, digits and F1-F24: enter, escape, space, // tab, backspace, delete, insert, home, end, pageup, pagedown, the four arrows, // numpad0-9 (also kp0-9 or num0-9), numpad_add and its siblings, volume_up, // media_play_pause, browser_back, printscreen, capslock, numlock, scrolllock, // pause, apps, oem_102, and bare punctuation such as - or [. // // Two things Windows decides rather than Shubbak. A numpad key only reports as // numpad0-9 while Num Lock is on; with it off those keys report home, end and // the arrows instead. And letters resolve positionally, so alt+a on AZERTY is // the key where Q sits on QWERTY. keybindings { // Focus. bind "alt+h" { focus --direction left } bind "alt+l" { focus --direction right } bind "alt+y" { focus --recent-workspace } // Back to the window you were just in. Press it twice and you are where you // started, because leaving a window makes it the most recent one. bind "alt+tab" { focus-recent-window } // Ask a connected client to do something. Shubbak does not interpret the name; // whichever program is subscribed decides what it means, which is how a window // palette or a launcher can live outside the window manager without Shubbak // needing to know it exists. // // Nothing happens if no client is listening - the log says so, at info. // // bind "alt+space" { signal "palette" } // bind "alt+shift+space" { signal "palette" "commands" } // // And one more shape, which runs a named action outright without showing // anything - the bridge between an `action` below and a key up here, so the // same sequence does not have to be written twice: // // 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, which is the same arrangement // that keeps the palette out of the window manager in the first place. // Move the focused window. bind "alt+shift+h" { move --direction left } bind "alt+shift+l" { move --direction right } bind "alt+shift+k" { move --direction up } bind "alt+shift+j" { move --direction down } // Resize. bind "alt+u" { resize --width -2% } bind "alt+p" { resize --width +2% } bind "alt+o" { resize --height +2% } bind "alt+i" { resize --height -2% } // Modes. bind "alt+r" { wm-enable-binding-mode --name resize } bind "alt+shift+p" { wm-enable-binding-mode --name pause } // Structure. bind "alt+v" { toggle-tiling-direction } // Untiles the window without releasing it: still tracked and focusable, but // free to be moved and resized. `toggle-tiling` is an alias for the same // command, so binding both is binding one thing twice. bind "alt+shift+m" { toggle-floating } // Takes on the window in front, or lets go of it. For the window Shubbak has // passed over, or the one it has taken on that you would rather it had not, // when you have not written a rule for it yet. bind "alt+shift+n" { toggle-managed } // Two fullscreens, differing only in which rectangle the window gets. Plain // alt+x fills the work area, so anything docked - the bar, the taskbar - stays // visible. The shifted one fills the whole monitor and covers them, which is // what GlazeWM's alt+x does. `--whole-monitor` is the same flag spelled out. bind "alt+x" { toggle-fullscreen } bind "alt+shift+x" { toggle-fullscreen --monitor } bind "alt+m" { toggle-minimized } bind "alt+shift+q" { close } // Move the current workspace between monitors: by direction, or by the name a // `monitor` definition gives a display (a position or DISPLAYn works too). bind "alt+a" { move-workspace --direction left } bind "alt+f" { move-workspace --direction right } bind "alt+d" { move-workspace --direction up } bind "alt+s" { move-workspace --direction down } bind "alt+shift+o" { move-workspace --monitor "laptop" } // Hold a context on or off by hand, or hand it back to its conditions. The // pin beats whatever the conditions say until --auto takes it off. bind "alt+shift+u" { context --toggle "presenting" } bind "alt+ctrl+u" { context --auto "presenting" } // A demo whose windows have been dragged about, back where they were. --save // records the focused workspace's tree - containers, layouts, ratios, which // window sits where, by process and class - and --restore puts the windows that // are on the workspace back into it. `shubbak arrangements` lists what is saved. bind "alt+shift+d" { arrangement --save "demo" } bind "alt+ctrl+d" { arrangement --restore "demo" } // Tags: make a window a member of several workspaces at once. Unlike `move`, // which relocates a window, tagging means "I want this here too" - the window // then follows to whichever tagged workspace you switch to. bind "alt+t" { tag --toggle "-" } // also show on the Chat workspace bind "alt+shift+t" { sticky } // follow every workspace bind "alt+ctrl+t" { tag --clear } // Scratchpad: stash a window out of sight, summon it back on top. Named slots // let several windows be stashed independently. bind "alt+n" { scratchpad --name notes } bind "alt+ctrl+n" { scratchpad --name terminal } // Lifecycle. wm-reload-config re-reads this file for the window manager and // tells the bar to re-read it too, so one key updates both. bind "alt+shift+r" { wm-reload-config } bind "alt+shift+w" { wm-redraw } // exit-all stops everything: the window manager, the bar, the palette and the // watcher. wm-exit stops the window manager alone - the palette and the watcher // stay and reconnect when it is back, and the bar waits a while for the same // reason - which is what you want when restarting it, and not what you want // when you are done. bind "alt+shift+e" { exit-all } // Get out of the way entirely, for a game. // // This releases the keyboard hook and the window event hooks and leaves every // window where it is. It is not the same as wm-toggle-pause, which stops // windows being rearranged but keeps the keyboard - and a chord Shubbak // swallows is a chord the game never receives, which matters far more than the // microsecond the hook costs. // // The same key brings it back. While suspended, Shubbak asks the system to // watch for this one chord and tell it - which is not a hook, and so costs // nothing on any other keystroke. `shubbak wm-resume` works too, and is the way // back if something else on the machine has already claimed this combination. bind "alt+shift+g" { wm-toggle-suspend } // Per-workspace bindings, generated from the workspaces declared above. // This one block replaces 40 hand-written lines, and can never drift out of // sync with the workspace list. // // `--focus` is what makes the second binding "send it there and go with it". // It used to be a second command on the same key - `move --workspace "{name}"; // focus --workspace "{name}"` - which reads well and is wrong: press it for the // workspace the window is already on and the move does nothing, leaving a plain // focus command that `toggle-workspace-on-refocus` answers as a re-focus. The // window stayed put and the screen jumped to the previous workspace, from a key // whose whole subject was moving a window. for-each "workspace" { bind "alt+{name}" { focus --workspace "{name}" } bind "alt+shift+{name}" { move --workspace "{name}" --focus } } } binding-modes { // Resize with HJKL or the arrow keys until escape or enter. mode "resize" { bind "h" { resize --width -2% } bind "left" { resize --width -2% } bind "l" { resize --width +2% } bind "right" { resize --width +2% } bind "k" { resize --height +2% } bind "up" { resize --height +2% } bind "j" { resize --height -2% } bind "down" { resize --height -2% } bind "escape" { wm-disable-binding-mode } bind "enter" { wm-disable-binding-mode } } // Swallow everything except the keys that resume. // // A mode with `pass-through` false makes the keyboard inert, so more than one way // out is worth having - the keyboard is the very thing it disables. Shubbak // refuses to load a swallowing mode that binds no way out at all. // // The bar's mode indicator can be given on-click="wm-disable-binding-mode", and // from any shell: shubbak wm-disable-binding-mode mode "pause" { bind "alt+shift+p" { wm-disable-binding-mode } bind "escape" { wm-disable-binding-mode } } } // --------------------------------------------------------------------------- // Taj - the bar. Same file, same parser, same diagnostics as everything above. // // Widgets bind to sources with {{ name }} templates and re-render only when a // source they use actually changes, so an idle desktop does not repaint. // --------------------------------------------------------------------------- bar { // How long to keep waiting for the window manager after losing it, in // seconds, before closing the bar. 0 waits for ever. // // Only counted once the bar has connected at least once: a bar that has never // reached a window manager waits indefinitely, because it is normally launched // by the window manager's own startup command and can win the race. // // If the window manager comes back inside the window, the bar reconnects and // the clock resets - so restarting the daemon does not cost you the bar. // // A clean `wm-exit` or `exit-all` does not wait for this at all: the window // manager announces that it is going and the bar follows within a frame. This is // the safety net for the times it cannot, such as being killed outright. window-manager-timeout 30 // The interval is how often a value is *checked*, not how often the bar // redraws: an unchanged value is suppressed. So a clock showing minutes can be // polled twice a second for a prompt tick without redrawing twice a second. // // `timezone` accepts a Windows id ("Pacific Standard Time") or an IANA one // ("America/Los_Angeles"). source "clock" kind="time" format="ddd d MMM HH:mm" interval=500 source "seattle" kind="time" format="ddd d MMM HH:mm" interval=500 timezone="America/Los_Angeles" // The input language of whichever window is in front, as a two-letter code. // // Per window, not per system: Windows keeps the layout per input thread, which // is the thing that decides what typing actually produces. Polled, because no // notification of a layout change crosses process boundaries - it is two cheap // calls, so a few times a second costs nothing. source "keyboard" kind="keyboard" interval=250 // Built-in template values, which need no source at all: // // {{ window.title }} {{ window.state }} {{ layout }} {{ workspace }} // {{ paused }} {{ suspended }} {{ status }} {{ binding_mode }} // {{ config }} {{ contexts }} // // The last six are empty almost all of the time, and a widget whose template // renders empty hides itself - so they cost no room until something is unusual. // // `contexts` names the contexts the window manager holds - see the `contexts` // section above - joined with commas in the order they are declared, the way // `shubbak status` spells them. // // `config` says the bar's own settings could not be read: a misspelt setting, // or a file that would not parse at all. Each process reports only the part it // reads, so the window manager and the palette say the same about their own // sections in their own logs, and `>config` in the palette lists its own. // // Worth having because the failure is otherwise quiet. A setting that parses // but is not understood simply never does anything; and a file that does not // parse leaves every process running on what it already had, so nothing on // screen changes and the edit appears to have been ignored. // Any program that writes lines to stdout can drive a widget - in any // language, with no Taj source code. This is the extension point that means // Taj never has to grow a widget for everything. The program is restarted if // it exits, and stopped - with anything it started - when the bar exits or // reloads this file. // // source "weather" kind="command" command="pwsh -NoProfile -File C:\\Users\\me\\.config\\shubbak\\weather.ps1" // // `history=N` keeps the source's last N readings - repeats included, so a value // that holds steady is a flat line and not a line that stopped - and publishes // them as `.history`, which is what a `sparkline` graphs. See docs/taj.md, // "Graphs and meters". // // source "cpu" kind="command" command="pwsh -NoProfile -File C:\\Users\\me\\.config\\shubbak\\cpu.ps1" interval=1000 history=60 // The battery's percentage, as the watcher publishes it - `power` in the `ayn` // section at the end of this file names the signal. Drawn as a level by the // `meter` below; empty, and so hidden, on a desktop with no battery. source "battery" kind="signal" profile "default" { height 34 background "#1e1e2e" foreground "#cdd6f4" font "Segoe UI" font-size 14 // The surface can be see-through. A background with an alpha channel is a // genuinely translucent bar, and `backdrop` is what shows through it: // "acrylic" blurs whatever is behind the bar, "mica" tints the wallpaper, // "tabbed" is Mica's stronger variant, "none" is the desktop as it is. // Windows 11 only; elsewhere the request is ignored. Try: // // background "#1e1e2eb3" // backdrop "acrylic" // border "#ffffff14" // a one-pixel hairline along the inner edge // // `margin` floats the bar off the screen edges, with the desktop around it, // and `radius` rounds its corners - meant for a floating bar: // // margin 8 // radius 8 // // All of these inherit through `extends`. // // Anywhere a colour goes, `accent` is the colour Windows is set to, and any // colour may be followed by an opacity - `accent 40%` is a translucent // accent pill. The bar follows a change of accent as the title bars do. // Zones are flex containers. Three is a convention, not a limit - add as // many as you like, each with its own alignment and growth. zone "left" justify="start" gap=4 { // hide-empty leaves out workspaces with no windows, except the active // one - which is kept so a freshly switched-to workspace still shows. // // Set it to #false to keep all 19 visible; positional muscle memory // works better when the list does not move, so it is worth trying both. workspaces hide-empty=#true active-background="#8dbcff" active-colour="#1e1e2e" } zone "centre" justify="center" grow=1 { // The focused window's icon - the one its taskbar button shows - fetched // from the window manager over the pipe and cached, so it costs one read // per window rather than one per focus change. Hidden when nothing is // focused, so the title beside it does not gain a gap. `size` is the // square it is drawn in; add background= and radius= for a pill behind it. icon size=20 // `window.state` is the focused window's state: tiling, floating, // fullscreen, monitorfullscreen, maximised or minimised. The `state-icon` // filter turns the two fullscreens into a hollow and a solid square and // everything else into nothing, and a widget whose template renders empty // hides itself - so this costs no space until there is something to say. // // Nothing marks the whole-monitor mode, because that mode covers the bar. text id="window-state" template="{{ window.state | state-icon }}" text template="{{ window.title | truncate:90 }}" { when of="window.state" value="fullscreen" colour="#f9e2af" } } // Two clocks, as in the Zebar config: Seattle dimmed on the left, local // accented on the right. zone "right" justify="end" gap=12 { // `when` restates only what differs, so marking a value costs a colour. // Useful for anything worth noticing at a glance rather than reading: a // keyboard in the wrong language, a battery about to go, a live mic. // // when value="X" - matches X // when not="X" - matches everything except X // when of="source" - tests a source instead of the drawn text // // First match wins, so the block reads top to bottom as written. // // `keyboard next` is the one click command the bar performs itself rather // than sending to the window manager: it switches the window in front to // its next installed layout, and the indicator follows on its next poll. // `keyboard previous` goes the other way; `keyboard he` picks a language. text id="lang" template="{{ keyboard }}" colour="#7f849c" on-click="keyboard next" { when value="HE" colour="#f38ba8" bold=#true } text id="seattle" template="{{ seattle }}" colour="#ffffff80" // Shubbak has let go of the keyboard - see wm-toggle-suspend above. // // This is the only thing on screen that says so, which is why it is here. // Suspended is indistinguishable from crashed by looking: windows stay // where they are and no key does anything. Click it to resume, which is a // way back that does not need the keyboard - the one thing suspending // took away. text id="suspended" template="{{ suspended }}" colour="#1e1e2e" \ background="#f38ba8" on-click="wm-resume" // Shubbak has stopped arranging windows. Less alarming - the keyboard // still works - so amber rather than red. Click it to start again. text id="paused" template="{{ paused }}" colour="#1e1e2e" \ background="#f9e2af" on-click="wm-toggle-pause" // Something in this file is wrong, and the bar is running on whatever it // had before. Clicking it re-reads the file, which is the other half of // the loop; `shubbak check-config` says exactly what and where. text id="config" template="{{ config }}" colour="#1e1e2e" \ background="#fab387" on-click="wm-reload-config" // Empty most of the time, so it takes no room. When it is showing, the // keyboard is behaving unusually - click it to return to the default // bindings, which is the one way out that does not need the keyboard. text id="mode" template="{{ binding_mode }}" colour="#1e1e2e" \ background="#f38ba8" on-click="wm-disable-binding-mode" // One glyph per context, in the icon font Windows ships. Each // `context.` value is the name while the context holds and empty // when it does not; `then:` turns that into a glyph or nothing, and a // widget whose template renders empty hides - so these take no room // until there is something to say. `font=` on a widget is what lets one // widget use Segoe Fluent Icons beside text in the profile's face. // // Clicking the microphone raises a signal the window manager carries // without reading; ayn flips the system mute; the endpoint's own change // notification then swaps the glyph. The window manager never learns the // word "mute". text id="camera" template="{{ context.camera-in-use | then:\u{E722} }}" \ font="Segoe Fluent Icons" colour="#a6e3a1" text id="mic" template="{{ context.meeting-live | then:\u{E720} }}" \ font="Segoe Fluent Icons" colour="#a6e3a1" on-click="signal ayn microphone mute" text id="mic-off" template="{{ context.meeting-muted | then:\u{EC54} }}" \ font="Segoe Fluent Icons" colour="#1e1e2e" background="#f38ba8" \ on-click="signal ayn microphone unmute" // The contexts in force, when any are: the configuration the keys obey is // then not quite the one in this file, which is worth a quiet word. Click // it to open the palette where `context --toggle` completes the names. text id="contexts" template="{{ contexts }}" colour="#1e1e2e" \ background="#8dbcff" on-click="signal palette commands" // The layout as a picture of itself: the workspace drawn sixteen pixels // across, with a pane for each window the layout would place - the big // pane and the dwindling rest is the spiral, a column of equal strips is // the master layout, four squares the grid, one solid square monocle. The // arithmetic is the window manager's own, so the picture is right by // construction. Four panes, since three cannot tell a spiral from a master // layout; `panes="windows"` follows the workspace instead - never below // four, with the panes it does not yet have drawn faint, so one window in // a spiral is the large pane solid and the rest a ghost of where they would // go. `main-colour` picks out the first window's pane, where the main // window goes. // // Dim while it is the ordinary split, green the moment it is anything // else. Clicking it steps the focused workspace to the next layout - the // same `layout --cycle` a keybinding sends; `--cycle-back` goes the other // way. `{{ layout | icon }}` in a text widget is the older indicator, a // box-drawing glyph per layout, for a bar that wants text. layout id="layout" colour="#7f849c" main-colour="#a6adc8" \ on-click="layout --cycle" on-right-click="layout --cycle-back" { when not="splith" colour="#a6e3a1" } // A level rather than a figure: the battery as a bar a quarter full is a // shape to see where "23%" is a number to read. `colour` is the fill and // `background` the track; `min`/`max` default to 0 and 100. A `when` block // may compare a number - `below=20`, `above=89`, or both for a band - and // here it recolours the fill. The meter hides while the source has no // number in it, so a desktop without a battery never shows an empty one. // The value comes from the watcher; see `power` in the `ayn` section. // // The other two shapes a number can take, for a `cpu` source with // `history=60` on it: // // meter source="cpu" shape="ring" size=18 thickness=3 colour="accent" // sparkline source="cpu.history" width=60 height=14 colour="#8dbcff" fill="#8dbcff40" min=0 max=100 meter id="battery" source="battery" width=40 height=4 colour="#a6e3a1" { when below=20 colour="#f38ba8" } // `media` is the other click command the bar performs itself: it presses // the keyboard's media key of that name, and Windows routes it to whichever // player is current - so one pill serves every player. A widget with both // on-click and on-double-click holds the single click for the double-click // time, so the two cannot both fire. text id="media" template="\u{E768}" font="Segoe Fluent Icons" colour="#7f849c" \ on-click="media play-pause" on-double-click="media next" on-right-click="media previous" \ on-scroll-up="media volume-up" on-scroll-down="media volume-down" on-middle-click="media mute" text id="clock" template="{{ clock }}" colour="#8dbcff" } } // A slimmer, quieter bar for presenting. `extends` inherits everything not // overridden, so a variant costs a few lines rather than a duplicate. profile "presentation" extends="default" { height 24 zone "left" justify="start" gap=4 { workspaces hide-empty=#true active-background="#8dbcff" active-colour="#1e1e2e" } zone "right" justify="end" gap=10 { text id="clock" template="{{ clock }}" colour="#8dbcff" } } // First matching rule wins. Profiles are all built up front, so switching is // a pointer swap rather than a restart. `monitor=` takes the same names the // `monitor` definitions above declare, or a position; `context=` takes the // names the `contexts` section declares, and holds while the context does - // so the bar slims down for a talk however the talk was detected, and a rule // on a context nobody declares is pointed out rather than silently never // matching. Every attribute on a rule has to hold; leave one out to mean any. rule use="presentation" context="presenting" rule use="presentation" workspace="\\" rule use="presentation" workspace=";" rule use="presentation" monitor="laptop" } // --------------------------------------------------------------------------- // Dalil - the palette // --------------------------------------------------------------------------- // A searchable index of every window on the desktop, managed or not, plus the // commands, workspaces and layouts. Its own process: run dalil alongside the // bar, from startup-commands or however you start things. // // Shubbak does not know Dalil exists. The keybinding raises a named signal and // whichever client is listening decides what it means - which is why the palette // survives you preferring a different bar, and why anyone can write their own. // // bind "alt+space" { signal "palette" } // bind "alt+shift+space" { signal "palette" "commands" } // // Inside the palette: type to filter, up/down or ctrl+n/ctrl+p to move, Tab to // change mode, Enter to act, Escape to dismiss. Ctrl+Enter shows what else can be // done to the selected window. The prefixes >, #, ~, % and $ jump straight to // commands, workspaces, layouts, monitors and the scratchpad; "?" lists every key, // including the ones you have bound yourself. The mouse works too. // // Ctrl and a digit jumps straight to a mode, in the order the bar along the bottom // draws them - Ctrl+1 is the window list and Ctrl+8 is the key reference. That is // the route worth knowing if your keyboard cannot easily produce the prefixes: on a // German layout "~" is a dead key, so it does not arrive until you press something // else and the mode never changes. You can also move them; see `prefixes` below. // // Ctrl+Space marks a window. Mark several and Ctrl+Enter acts on all of them at // once - move them to one workspace, float them, close them - which is the thing // keybindings genuinely cannot do: six windows by keyboard is six rounds of // find-it, focus-it, move-it, with the focus somewhere different after each one. // // The search box says so when tiling is paused, when a binding mode is swallowing // keys, when the window manager has let go of the keyboard, and when it cannot be // reached at all. All four look exactly like a crash from the outside, and the // palette is where you would go to find out, so it is the last place that should // stay quiet. // // A window tagged onto another workspace is badged "also on ", because a // window that relocates itself reads as a fault rather than as something that was // asked for. Ctrl+Enter then "Inspect this window" says the same thing the command // line's `shubbak inspect ` does - styles, cloak state, tags, and which // rules matched - without leaving the window you are asking about. "Tags..." picks // which workspaces a window follows you to, "Move it to..." sends it somewhere and // leaves it there, and "Write a rule for it..." offers the rules that could be written // for it - ignore it, manage it, float it, send it somewhere - each complete, and // Ctrl+Enter on one adds it to this file and reloads. Escape goes back one level // rather than dismissing. // --------------------------------------------------------------------------- dalil { // Checked by `shubbak check-config`, which reports a misspelt setting here with // a line, a column and a caret - the same as everywhere else in this file. It // did not until recently: the section name was on the allow-list and its // contents were on nobody's, so a typo was accepted in silence and the setting // simply never did anything. // The signal that opens it. Must match what your keybinding raises. open-on-signal "palette" // Sizes and the font are written at 96 DPI and scaled to whichever monitor // the palette opens on, so these do not need changing per display. width 720 row-height 38 // A cap on drawing, not on searching. Everything is still matched and // ranked; only this many rows are measured and painted, because measuring // text is a round trip into GDI per row. visible-rows 12 // Whether the palette is put away when it loses the keyboard - a click // elsewhere, Alt+Tab. The first half-second after opening is not a blur // whatever this says: a foreground that has not landed yet is retried, not // given up on. A palette that never gets the keyboard at all - something // elevated is in front - is put away regardless, with a warning saying what // stood in the way. close-on-blur #true // Windows Shubbak does not manage are listed by default, with the reason it // passed over each one. Those are the ones most likely to be lost: nothing // is arranging them, so nothing put them anywhere findable. show-unmanaged #true // Closing a window cannot be undone, so it asks first - by whichever route you // reached it, including the chord. Everything else is a toggle and just happens. // // This is what `action-guard` became. That setting turned every direct chord // off at once, which meant the action list printed "Ctrl+Shift+F" beside "Float // it" and pressing it did nothing. `action-guard` is still read and still means // this, so nothing you have written stops working. confirm-destructive #true // An application's icon on each window row. One non-blocking read per window, // worked out on a background thread and cached, never during a repaint. show-icons #true // A search that matched two things is drawn two rows tall rather than two rows // of text above ten rows of empty background. shrink-to-fit #true // focused-monitor | cursor-monitor | primary placement "focused-monitor" // Move a mode's prefix, if your keyboard cannot comfortably produce the // default. Everything you do not mention keeps what it had, and an empty // string gives a prefix up entirely - the mode is still reachable with Tab and // with its Ctrl+digit, so nothing is lost but the shortcut. // // prefixes { // layouts "l" // monitors "m" // } // Named sequences, run from the commands list by typing roughly what you called // them. This is the thing keybindings cannot be: there are only so many chords a // person can hold, so anything done twice a week never gets bound and is then // done by hand for ever. A palette row costs nothing to have and nothing to // remember. // // The commands are written exactly as they are in a keybinding - one per child // node - and are checked against the same parser at load time, so a mistake is // reported on the row rather than swallowed. // // action "Dev layout" description="Editor left, terminal right, on 2" { // focus --workspace "2" // layout --set "master-left" // equalise // } // // action "Reset this workspace" { // layout --set "splith" // equalise // wm-redraw // } // A `param` turns one row into a question. The placeholder is filled in at // the moment the value is chosen, which is what lets a single row stand in // for one per workspace - nineteen of them in this file, each of which // would otherwise need its own name to invent and its own line to keep. // // Enter opens the picker; Escape goes back one question rather than // dismissing. Two params ask one after the other. // // `from=` draws the choices from a list the palette already holds: // workspaces layouts binding-modes scratchpads directions contexts // `values=` writes them out instead, for a set the window manager does not // know. That is the right answer for stashing: the list it does know is the // slots currently holding a window, and the slot you want is the empty one. // // action "Send it to..." description="Move it there and stay here" { // param "ws" from="workspaces" // move --workspace "{ws}" // } // // action "Stash it in..." { // param "s" values="notes terminal chat" // scratchpad --name "{s}" // } // // 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 // } // // Every context the `contexts` section declares, held or not - the action is // how the others are turned on. `--toggle` pins it to the opposite of what it // is now; `context --auto "{c}"` would hand it back to its conditions. // // action "Toggle a context..." description="Hold it on or off by hand" { // param "c" from="contexts" // context --toggle "{c}" // } // // Checked at load time like everything else: a placeholder nothing declares // is an error with a line and a caret, and so is a question no command asks. // A direction is probed with a real direction before the parser sees it, so // `move --direction "{d}"` is checked rather than waved through. // // An action that asks cannot be answered by a key, so a keybinding pointing // at one opens the palette with its name already typed and the picker one // Enter away. // A row can follow a context. `when-context=` offers it only while the // context holds, `unless-context=` only while it does not, so a pair of rows // reads as one switch: during a meeting exactly one of these two is listed, // and outside one neither is. The signal goes to ayn, which flips the system // mute (see the `ayn` section at the end of the file); the contexts are the // ones declared above, and `check-config` says so when a name is not. A row // kept back is still in `>actions`, the palette's own list of every action, // greyed with the condition beside it. action "Mute the mic" description="The system mute; Teams says 'muted by your system'" when-context="meeting-live" { signal "ayn" "microphone" "mute" } action "Unmute the mic" description="Lift the system mute" when-context="meeting-muted" { signal "ayn" "microphone" "unmute" } // Six colours, not ten. The chip behind the mode name, the pill behind a // badge, the accent down the selected row and the hairlines between sections // are all derived from these, so changing background and match moves the // whole palette together. background "#16161C" foreground "#E8E8EE" match "#7DD3FC" secondary "#878796" selection-background "#242C3E" border "#393948" // The one colour whose job is to be noticed rather than to be harmonious: // "close it", and the row that asks whether you meant it. danger "#F38BA8" font "Segoe UI" font-size 15 } // --------------------------------------------------------------------------- // Ayn - the eye // --------------------------------------------------------------------------- // // The window manager's eyes on the rest of the machine. What is not a window - a // device in use, a switch flipped - is not the window manager's to read, so this // process reads it and hands each fact over as a context. Today that is the camera // and the microphone; the next subject goes in this section beside them. // // Its own process, started however you start the bar and the palette - a // startup-command, a shortcut, by hand. It reads this section and nothing else in // this file, and sleeps until something changes: the record Windows keeps of which // programs have the camera or the microphone open (the one the privacy indicator // in the tray reads), and the default microphone's mute switch (the one the Sound // settings toggle). While a fact is true it holds the named context on the window // manager with a lease, so the pin dies with ayn and a crashed watcher leaves // nothing behind. // // It also answers `signal "ayn" "microphone" "mute" | "unmute" | "toggle-mute"` // from a keybinding, the bar or the palette, by flipping the system mute. That is // the system's mute - a call's own mute button is the call's, and invisible from // here - but Teams and its kind notice it and say "muted by your system". // // `ayn --report` prints what Windows says right now; `shubbak ayn-exit` stops it. // Every setting has a default, so the section can be left out entirely. ayn { // The contexts to hold, by subject. `camera #false` says nothing about the // camera at all; `muted #false` turns one fact off. The names must be declared // in `contexts` above, and `check-config` says so when one is not. camera { in-use "camera-in-use" } microphone { in-use "microphone-in-use"; muted "microphone-muted" } // How long a change of use must last before it is believed, in milliseconds. A // call opens and closes the devices several times while it is setting up. The // mute is never settled: a person who pressed the key wants the icon now. settle 500 // Three readings can go to the bar as words rather than as contexts, each as a // signal the window manager carries without reading: the battery's percentage // and the names of the default speaker and microphone. The bar shows one with // `source "battery" kind="signal"` and `{{ battery }}` - or draws it, with the // `meter` above - and asks for them again when it connects, so a bar started // after the watcher is not blank. Each is off until named; the battery is, for // the meter: // // speaker { device-name "speaker" } // microphone { device-name "microphone" } power { battery-percent "battery" } }