// This file is the *reasoning*: what each setting is for, what it cost to // get right, and which ones are opinions. The values themselves come out of // the code -- `slosh --dump-config` writes every knob with its default, and // `C-a e` seeds a missing config with exactly that. // // So if the two ever disagree, the dump is right and this file is stale; // tests/test_dump_config.py fails when a setting exists and is not rendered. // // ---- what reloads ---- // // Saving this file applies it to every running session, and nearly everything // here takes effect on the next frame: colours, geometry, the status bar and // line, the marks, shader chains and their states, hints, and the keys -- // including the leader itself. // // The exceptions, both because of *when* they are read rather than any // reluctance to reload: // // shell consulted when a pane is made, so it applies to the next // pane you open rather than to the ones already running // // A session started before this file existed still notices it appearing: the // watch is on the directory, and slosh creates that directory at startup so // there is always something to watch. // // slosh — every value here is also the compiled-in default, so this file is // documentation as much as configuration. Copy it to // ~/.config/slosh/config.kdl (or point $SLOSH_CONFIG at it) and edit. // // A broken file costs a warning on stderr and nothing else: the defaults // stand. Saving this file re-reads it into every running session on its own; // `slosh --no-reload` starts one that does not watch, and // `slosh cmd '{"cmd":"reload"}'` asks for a re-read explicitly. And // refuses a file it cannot parse rather than half-applying it. // ── include ───────────────────────────────────────────────────────────────── // // A config can be made of files: // // include "themes/amber.kdl" // contrib/themes has seven // include "keys/vim.kdl" "shaders/crt.kdl" // // which is how you switch a theme by editing one line instead of pasting one. // Nothing about it is theme-specific: an include is another config file applied // here, so the same line composes keys, shader chains, states or geometry. // // A relative path is relative to *the file doing the including*, never to the // directory you happened to start the session from -- a config is a thing on // disk that refers to its neighbours. A leading `~` is your home directory, and // an absolute path is itself. Included files may include others. // // What you include is the *base*: the file with the `include` line in it wins // wherever they disagree, and a later include wins over an earlier one. That is // true wherever the line sits in the file, because the loader reads a document // by asking it for the keys it knows rather than walking it top to bottom -- so // "here" is not a position it could honour, and "the file you are editing wins" // is the rule worth having anyway. // // `keys` blocks *add* to what came before, like every keys block; a `shaders` // or `states` block replaces the one it inherited, like every one of those. // // A file that will not load is skipped and the rest of the config still // applies -- losing your keybindings over a mistyped theme name would be the // worse answer -- and the session says which file it could not honour, in the // same place it says "config reloaded". That goes for anything a config // complains about: an unknown shader, a binding that does not parse, an include // that is not there. The file applied; one line of it did not; you get told. // // `slosh --check` says all of them at once, with a file and line each, and // exits non-zero if there were any. A session only has room for the first. // ── geometry ──────────────────────────────────────────────────────────────── // Blank cells between panes and around the screen edge, in ROWS. gap 1 // Columns per row. Terminal cells are about twice as tall as they are wide, so // a gap of 1 row and 2 columns looks square. gap_aspect 2 // Space between a pane's frame and its contents, in ROWS like `gap` -- a // horizontal one is multiplied by `gap_aspect`, so `padding 1` is a ring that // looks square rather than a squashed one. One number does every side; two are // vertical then horizontal; four are CSS order, top right bottom left: // // padding 1 // all round // padding 0 2 // none above or below, two either side // padding 1 2 1 2 // the same thing, spelled out // padding 2 0 0 1 // room for a title above, a gutter on the left // // A number means the same thing however many of them you write, which is why // the first three of those are two paddings rather than three. Three values is // refused: CSS reads it as top/horizontal/bottom, and a line whose meaning you // have to look up is a line nobody can read. padding 0 // Shared borders instead of gaps. Panes pack flush against 1-cell divider // lines that meet in real junctions (├ ┬ ┼), one frame rings the whole tab, // and each pane's title rides the line above it -- so a boundary costs one // cell instead of four. `gap` stops applying (the boundary *is* the divider); // `padding` still works, and `gap_aspect` keeps its other jobs -- the // square-looking padding, the float nudge step. The dividers carry the same // drag the gaps did; interior edges give up click-to-split -- a one-cell line // cannot hold two verbs -- and the outer frame keeps it. A pane's status row // (a dead pane's epitaph, OSC 5577) overlays its last content row, there // being no bottom border left to carry it. compact false // ╭╮╰╯ versus ┌┐└┘ rounded true // "left" | "center" | "right" title_align "center" // Cells between a frame's corner and the start of its title, for the edge the // title is anchored to. The title always carries a space on each side on top // of this, so it can never end up welded to the rule beside it. title_inset 2 // ── the bell ──────────────────────────────────────────────────────────────── // // A BEL from a program marks the pane it came from, and the tab that pane is // in -- a bell exists for the pane you are *not* looking at, and one that only // showed on screen would be useless in a background tab. Looking at the pane // is what puts it out. Off makes a bell silent *and* invisible, which is a // real choice rather than an oversight. // // The mark is one cell wide, which is not news you can see from the corner of // your eye -- so by default the pane says it too, in theme's `bell` colour. // The *frame*: three sharp blinks the moment it rings, then a slow breathe // until it is answered. The *body*: a shimmer -- one soft diagonal sheen // gliding across, the way light crosses something glossy, gone within the // same 900ms the blinks take. It is quiet on purpose: peak 95 where the // blinks are 255, its colour softened halfway towards default_fg so it reads // as light rather than paint, and its width breathing on a sine as it // travels. Motion is what pulls the eye, not brightness, so it draws you to // the pane without shouting over what it says. Both are derived, like // the scrolled wash: built from the theme after the theme is read, so a theme // moves the mark, the flash and the shimmer together, and they are commented // out in the states block below for the same reason scrolled is. Both read // `since`, so the frame clock they cost runs only while a rung pane is // actually on screen. // // Turning it off is either of the things it hangs from: `bell_indicator // false` silences the bell everywhere (mark and flash -- that is what // invisible means), and `states { bell { } }` removes just the flash. A // `bell { ... }` chain of your own replaces it outright, like every state -- // the chrome passes further down have a few worth stealing. // ── the frame's own buttons ───────────────────────────────────────────────── // // Minimise, zoom and close, in the top-right of every pane's frame: the mark // and one space each, seven columns for the three of them. They are drawn // right to left and stop when they run out of room, so a narrow pane keeps // close and zoom and loses minimise first. // // The split button was removed from this frame once already, on the grounds // that one glyph cannot mean four directions and the border can. These are two // verbs a single glyph *can* mean, which is why they are here and splitting is // still done by clicking an edge. They do cost six columns of every frame, so // this turns them off -- the keybindings (C-a z, C-a x) are unaffected, because // the setting is about the affordance and not the verb. // A word in the middle of the status line for whatever the pointer is on: // which way a border would split, what a one-character button does, that the // gap between two panes can be dragged. The frame's affordances are mostly one // character wide and a border that splits when clicked looks exactly like a // border, so this is where they say so. // // It sits in the middle of the *row*, not in the space the two ends leave // over: centred in the gap it moved every time a pane title changed length, // and a thing you glance at should be in the same place twice. When the row is // too full for it there it is not drawn at all -- the ends say which session // and pane you are in, and a hint you can get back by hovering is the part // that can afford to disappear. hints true // When there is no hint to show — which is most of the time, since a hint // needs the pointer to be on something — that space says which slosh this // is: `slosh 0.1.0+d9dee7f`. It reads as decoration and is not: a session // keeps the binary it started with, so after a rebuild the version on screen // is the one you are actually talking to, and `slosh --version` is the one // on disk. When those disagree, that is the answer to whatever you were about // to debug. (contrib/slosh-dev restarts into the new one.) // // It gives way to a hint, and both give way to the ends of the line -- they // are centred on the row and simply not drawn when that would overlap. version_banner true pane_buttons true // Four shapes, one verb each: down for away, an outline for "make it fill the // tab", the same outline filled in for "it is filling it", a cross for gone. // // Deliberately not one tidy family. A set of boxed operators (⊞ ⊟ ⊡ ⊠) was // tried and read as four identical smudges: they are *math* symbols, sized to // sit beside x and y in an equation, so at a terminal's cell size the interior // carrying the whole meaning is two or three pixels of it. Geometric shapes and // dingbats are drawn at full cell size, which is what makes them readable at // all, and picking glyphs that fill their cell is also the answer to the // evenness that put plain ASCII here originally -- a glyph that does not fill // its cell hands the slack to the gap beside it, by an amount the font decides. // // Single width, and never emoji: many terminals draw those two columns wide // while chrome here is booked as one, which shifts everything after them. // // A mark may be more than one character; the button is measured rather than // assumed, so "[]" and "][" work if you prefer them. Put the character itself // here; this file has no \u escape. zoom_mark "□" // click to fill the tab with this pane zoom_on_mark "■" // ...and this is what it says while it is filling it close_mark "✕" min_mark "▼" // into the strip along the bottom, still running // The new-tab button at the end of the tab strip. Drawn with a space on each // side, so a one-character mark is still a three-cell target -- a one-cell // target is a thing you miss with a mouse. It reads as a bare "+" because // nothing else in the UI is one: the frame's old "+" for splitting went when // the border became the button. Try "[+]" or "new" if you prefer a word. newtab_mark "+" // Whether that button is drawn at all, and how much nothing sits between it // and the last tab -- rows in a sidebar, columns in the top strip. // // The air is there because the button is the odd one out in a list whose every // other row means "go here": flush against the tabs, the row below the last one // is a new tab you did not ask for, and in a sidebar the whole width is the // target so the pointer only has to be one row low. One cell turns that into a // miss. It is paint, not layout -- the panes' rectangle is the same either way. // // Turn the button off if you make tabs from the keyboard (`C-a c`, or the // palette): the verb does not go anywhere, only the place to click it. newtab_button true newtab_pad 1 // Which dead panes stay, and which just go. // // commands panes that were *given* a command -- from a layout, or the // control API. Default. Their death is news: the pane keeps what // it printed, says in its own backlog why it ended, and offers // [re-run] and [close] in its frame. // all shells too. A pane you split off and typed `exit` in stays // until you dismiss it. // none nothing stays; the pane is closed on the next paint, and with // the last pane, the session. // // The line is the command, not who made the pane: a shell is a shell whether // it came from a layout or from C-a \, and when you type `exit` in one you // are done with it. A pane running `npm run dev` is a different thing -- you // want the error it left, and the button that runs it again. // // (`true` and `false` are still read, as `all` and `none`, because they used // to be the spelling.) keep_dead "commands" // How far the panes you are *not* in are pushed back, 0..255, and 0 for not // at all. On by default and gently: at 60 white text lands on #c3c3c3, which // reads as "not this one" without reading as "not available", because half // the time you are still reading the other pane. // // This is the one taste shipped as a default, and it is shipped because // turning it off is a single obvious number rather than a shader incantation. // Writing the chain yourself still wins -- `states { unfocused { ... } }` // replaces it, and `states { unfocused { } }` means nothing at all. dim_unfocused 60 // How dark the cell of shade a floating pane casts on what it covers, // 0..255, and 0 for no shadow. A float wears the same frame as a tile, so // the shadow is what tells the two apart at a glance -- which is why it // ships on, and why it is a number rather than a `states { floating { } }` // chain: turning it off should not require knowing the states block exists. // (The floating *state* exists too, empty by default, for colour on top.) float_shadow 110 bell_indicator true // What to draw. A short string rather than a fixed glyph, because the obvious // choice is an emoji bell and emoji are drawn two columns wide by many // terminals while chrome here is booked per glyph -- so the default is plain // ASCII that says what it means, and anyone who knows their own terminal can // say otherwise (a "•" if you like it quiet, a "🔔" if yours draws it well; // this file has no \u escape, so put the character itself here). bell_mark "[!]" // A pane can also say that what its status describes is still *happening* // (`busy` over OSC 5577, see the scripting chapter). In a sidebar that status // then turns the theme's `tab_status_busy` colour and carries this spinner -- // in `tab_status_spinner`, its own entry, so a theme can shout with the mark // and keep the words calm; unset it follows the words -- // which is how "running" and "finished" tell themselves apart from another tab // without reading the words. // // The frames are separated by spaces. One frame is a static mark and costs // nothing; several make it turn, and only then does the session keep a frame // clock -- and only while such a row is actually on screen. "" draws no mark // at all and leaves the colour to say it. The mark sits in the column // `tab_bar_padding` holds blank, so nothing moves when a program starts or // stops working, and the rest of that padding is the space after it. busy_mark "⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏" busy_ms 120 // The one-row bar with tabs, the new-tab button and the pane count. status_bar true // Air inside a *sidebar*, between its frame and the list in it: 1, 2 or 4 // values in CSS order, the same shape `padding` takes. Counted in cells and // not in rows-times-`gap_aspect`, because the two sides are doing different // jobs here -- the columns are an indent for labels and statuses, the rows // are air above the first tab and below the last thing in the strip -- and // scaling the columns would make `2` mean four of a sixteen-column sidebar. // // The right column is where the tab number stops. The left is two, because // that indent is also the gutter a busy pane's spinner sits in: the mark takes // the first column of it and whatever is left over is the air between the mark // and the words, so one column would put a spinner against the first letter of // a status. `tab_bar_padding 0 1` is that squashed look for anyone who wants // the column back, and `0` puts the names against the frame -- which the // hard-coded leading space they used to carry never allowed. // // It is air *inside* the plate; a tab's fill still spans the whole row, // because the row is what you click. // // `tab_bar_pad` is the older name for the top of this alone, still honoured, // and it wins where a config writes both. tab_bar_padding 0 1 0 2 // top right bottom left // Where a tab's number sits in a *sidebar*. The default, "right", puts it at // the far end of the row, so every name starts in the same column and the // numbers line up in one of their own -- a list with a shape rather than a // column of digits in front of the words you are reading. "prefix" is the // older form, `1:api`, the number against the name. // // Sidebars only, and not by omission: a tab in the top strip is exactly as // wide as its label, so there is no far end to align against, and the setting // is ignored there rather than half-honoured. A sidebar row too narrow to // spare the columns also keeps the prefix -- the name is the part you came // for. tab_bar_index "right" // The line along the bottom: which session and tab you are in, what the // focused pane calls itself, and whether it is in a state worth knowing about // - scrolled back, on an alternate screen, or not started yet. On the right it // says which of *this tab's* panes is open, as `pane 2/5`; the strip above // counts the whole session, so the two disagree as soon as you have two tabs. status_line true // How far the strip and the line are held off the left and right edges. This // is separate from `gap`, which is the space around the panes, and wider than // it by default so the two bars read as chrome outside the layout rather than // as another row of it. status_pad 4 // Focus follows the pointer. It never steals focus mid-chord (while the prefix // is held), never reaches past the finder, never interrupts a drag, and never // expands a collapsed pane just because the pointer crossed its header. focus_follows_mouse true // Several terminals may attach to one session at once. Turning this off // restores the classic rule: a new client displaces the one before it -- // which is what "move my session over here, and close it over there" // actually wants. multi_attach true // When several clients share the session, each one gets a small tag in its // own top-right corner: how many terminals are watching, and -- on a client // whose view is a crop panned to follow the active cursor -- where that // crop sits in the shared screen ("+col+row"). Off to hide it. attach_indicator true // Whose size the shared screen takes when several clients are attached: // "active" the newest attachment, then the last client to type or // click. Smaller clients see a cursor-following crop, // larger ones get blank filler. // "smallest" the smallest attached terminal (the tmux rule): nobody is // ever cropped, and the roomiest terminal pays in filler. // "largest" the largest one: the big display gets everything, small // ones follow the cursor over a crop. size_follows "active" // Ctrl-D at an idle shell prompt exits that shell. // // On POSIX the tty does this: ^D on an empty line is EOF, the shell reads it // and goes. Windows has no such character -- the console driver does not know // it, so cmd.exe and PowerShell ignore the one key that closes a pane // everywhere else. Typing `exit` works, which is the tell: the intent was // never ambiguous, only unspoken. So slosh sends exactly that word, and the // pane ends the way it would have anyway -- the shell's own cleanup, its own // exit status, and keep_dead above deciding what is left behind. // // Default: true on Windows, false everywhere else, where the real thing is // already there and better than an imitation. Turn it on if you want the key // to mean the same on both. // // It refuses unless all three hold, because a wrong guess costs a pane: // nothing running but the shell (a program in the foreground may want the EOF // itself), the primary screen (in an editor or a pager ^D is half a page // down), and an empty line (POSIX sends EOF there; it does not throw away // what you had typed). ctrl_d_exits false // Whether the program running in a pane may set that pane's shader chains, by // printing an escape sequence: // // printf '\033]5577;1;shader;chrome;tint color="#7aa2f7" amount=255\033\\' // // The payload after `chrome` (or `content`) is a chain in exactly the syntax the // `shaders` block below uses, so a chain you arrive at this way is a line you can // paste. `contrib/shader-repl` is a prompt that does it for you, and prints the // block to paste when you like what you see. // // Off, because a program restyling the session it happens to be running in is a // hazard and not a feature -- `cat` some hostile file and it can dim your panes. // On, it is scoped to the pane that asked: it cannot touch another pane, and it // cannot change anything this file said about everybody else. The pane is told // `off` rather than ignored, so a program can tell "you said no" from "this // build has never heard of it". // // The way back, whatever a pane did to itself and whether or not this is still // true afterwards: the `clear-shaders` action (bind it, or run it from the // palette), or `slosh cmd '{"cmd":"clear-shaders","id":0}'`. in_band_shaders false // The floor below which a pane is not small but useless. A split that cannot // give every child this much collapses into a stack instead, with one pane // expanded and its siblings as one-row headers. Recomputed every frame, so // widening the terminal always restores exactly the layout you had. min_pane cols=24 rows=6 // The smallest pane a split is allowed to *produce*. min_pane above is where // the layout gives up and collapses; this is where splitting stopped being // worth offering, which is a larger number and a different question — two // twenty-five column panes technically fit and are not worth having. // // Below this the border stops being a button: no guide arms, clicking it does // nothing, and the keyboard says so with a toast. The control API and layout // files are deliberately exempt, because a script asking for a pane is saying // what it wants rather than being offered something, and the responsive // collapse is what catches it there. // // Never used below min_pane: refusing to offer what the layout would collapse // anyway is the floor under the floor. Raise it if you like fewer, bigger // panes; a column split needs 2*cols + 2 columns of pane to be offered, a row // split 2*rows + 1. min_split cols=32 rows=8 // Rows per wheel notch, and per scroll-up/scroll-down. scroll_lines 3 // A selection dragged past a pane's top or bottom edge keeps going: the // viewport scrolls a step this often, for as long as the button is down and // the pointer stays past the edge. Each step is as many rows as the pointer // is past the edge -- one row past creeps, further hurries -- so the hand // that is already dragging is also the throttle. Lower is faster. select_scroll_ms 50 // How many lines of history a pane keeps. `0` keeps none: the screen and // nothing behind it. // // lib-vt is handed no limit by default and picks its own -- 10,000 *bytes*, // which measured at 622 lines of an 80-column pane, less than one `make` run. // This is that number replaced with the one every other multiplexer settled on. // // Read when a pane is created, so it applies to the next pane you open rather // than to the ones already holding history. Same as `shell`, and for the same // reason: retroactively shrinking a pane's scrollback would throw away output // somebody is reading. scrollback 10000 // ...and the ceiling that line count runs into, per pane, in bytes. The two // limits work together because either can be reached first and neither can see // what the other depends on: a line count cannot know how wide your terminal is // or how many styles a program used, and a byte count cannot know how many lines // that bought. `0` removes the ceiling. // // Measured, 10,000 lines of a pane filled with styled output: // // 80 columns ~6 MB the ceiling is never reached // 200 columns ~15 MB it costs about 2% of the lines // 400 columns ~32 MB it costs about half of them // // So it is invisible at ordinary widths and a brake at extraordinary ones, which // is its whole job. It is per *pane*: sixty panes full of history is sixty times // this, which is the reason there is no "unlimited" -- with this many panes that // is a memory leak with a friendly name. Raise it if you have one very wide pane // and want every line of it; lower `scrollback` if you have very many. scrollback_bytes 16777216 // How long the pointer must rest on a pane border before the split guide // arms. Contact alone is not enough: a pointer crossing a border on its way // somewhere else should not make the screen flash. 0 arms immediately. hover_delay_ms 250 // How close together two clicks must be to count as a double-click. On a // pane's title that renames it in place -- Enter keeps the new name, Escape // abandons it, and an empty name gives the pane back to whatever its program // calls itself. On a pane's contents it selects the word under the pointer and // copies it, like any other selection. double_click_ms 400 // What that word stops at. Listed as *separators*, not as word characters: // anything unlisted belongs to a word, so text nobody enumerated -- CJK, // accents, emoji -- needs no table and comes out whole. Whitespace always // separates, listed or not, because a double-click that selected the line // would be a different gesture arrived at by surprise. // // The default deliberately leaves `/ . - _ : = @ # % ? & + ~ $` *inside* words: // a path, a dotted name, a snake_case identifier, a key=value pair, a URL and // `src/app.c:1234` are each one thing you double-clicked to copy whole. `:` is // the difference between copying an error location and copying half of one. word_separators "\"'`()[]{}<>;,|" // How far everything behind a modal (the cheatsheet, C-a ?) is pushed back, // 0..255, and 0 for not at all. A modal floating over a screen as bright as // it is reads as another pane rather than as something in front. // // It is the shader pass aimed at the whole screen, so the same caveat applies // as for shaders: cells with no colour of their own are resolved through // theme's default_fg/default_bg first, and if those do not match your // terminal's real colours the screen will shift slightly while a modal is up. modal_scrim 120 // ── projects ──────────────────────────────────────────────────────────────── // // Where you keep your work. Each of these is scanned for subdirectories that // are projects, and a project opens as a *workspace* -- a tab dedicated to it, // reached with `C-a w`. `depth` is how many levels below each root to look; a // directory that *is* a project is never descended into, which is what keeps // the scan off node_modules without a rule about node_modules. // // A subdirectory is a project when it has a `slosh.layout` saying what it // needs -- or a `.git`, which is the project that has not said yet. Both, // because a ~/dev of forty checkouts and three layout files would otherwise be // a picker with three rows, and `C-a W` writes the missing file from whatever // you arranged by hand. // // Nothing is watched. The list is worked out when something asks for it, which // is one readdir per root and a stat or two per entry -- next to the keystroke // that asked, nothing -- and an answer nobody remembered cannot be stale. // // Unset, so nothing is scanned until you say where to look: // project_roots "~/dev" "~/work" depth=2 // The layout a project with no file of its own opens as: yours, rather than a // guess about your stack from whatever config files it happens to contain. A // relative `cwd=` in it binds to whichever project is being opened, not to the // directory this file lives in, so one file is the shape you start every // checkout in -- and a project that grows its own `slosh.layout` wins // over it, the way an included file is the base and the file including it wins. // // Unset, so a project with no layout is one pane running your shell in it: // project_layout "~/.config/slosh/project.layout" // ── shaders ───────────────────────────────────────────────────────────────── // // Colour passes over a pane's contents by default; `where="chrome"` aims one // at the frame instead (see the chrome passes further down), and the tab // strip and status line are never shaded. Strengths run 0 to 255, where 0 is // off and 255 is fully applied, and a pass resolves default-coloured text // through theme's default_fg/default_bg -- set those to your terminal's real // colours or a shaded pane will shift. // What a pane looks like while it is in a particular state. Each state takes // the same chain of passes `shaders` does. // // A pane is usually in several states at once — unfocused *and* scrolled, // suspended *and* something you could drop onto — so exactly one wins: the // first that matches, tested in this order. // // dragging the pane you have hold of // drop_hover the one under the pointer, where it would land // drop_target the others, all of them somewhere it could go // dead its program exited; waiting to be re-run or closed // suspended laid out, never started (its label is its contents) // bell a BEL arrived and nobody has looked since; ends when you do // scrolled looking at scrollback rather than at the present // floating lifted out of the layout, drawn on top (see float_shadow) // unfocused the weakest thing that can be true of a pane // // The order is fixed rather than read from this block, because it is a ranking // by urgency and not a preference: a pane you are holding should not be // recoloured by anything, and a mode the whole screen is in outranks an // ambient hint about one pane. They do not stack, for the same reason two // reasons to be grey would compound into one muddy grey that reads as neither. // // Naming a state replaces its default outright — including with nothing, which // is how you turn one off: states { drop_target { } } // // Which states have a default, and why the line falls where it does: // // a pane that is *not live* gets one -- dead, suspended, scrolled. In all // three the cells are showing something other than a running program's // present: output from something that exited, a pane that never started, or // the past. None of that is discoverable by looking unless something says // so, which is the entire argument for colouring it. // // a pane that is *news* gets one too -- bell. A ring is an event rather // than a condition, so its default moves: a shimmer across the body that is // gone within a second, and blinks then a breathe on the frame -- the one // chrome pass in this table -- in theme's bell colour (see the bell section // above, and `bell_indicator false` to silence it with the mark). // // a pane that is merely *not the one you are in* is a taste rather than a // fact, and gets a default anyway -- but through `dim_unfocused` above, // which is one number to change or zero out. The objection to shipping a // taste was never the taste, it was that turning it off should not require // knowing this block exists. `dragging` still has no default: the pane in // your hand keeping its colour is what makes it lift off the page. // `floating` has none for the same reason -- full strength is what keeps // the thing on top reading as lifted -- and because it ranks above // unfocused, a float is never dimmed as merely-unfocused. Its telling-apart // is the shadow, which is `float_shadow` above, one number. // // All the defaults are gentle: a dead pane, a suspended one and scrollback are // all things you still want to read. The scrolled default is *derived*, not // stored, and has two parts. The wash: the scrollback indicator's own colour // (theme's scroll_bg), worked out after the theme is read, so a theme moves // the wash and the indicator together. The edge fades: the viewport's top and // bottom rows melt towards the edge, the way a scrollable list fades where it // continues -- and each fade exists only while there is content past that // edge, so hitting the top of the buffer turns the top edge solid, and // returning to the present ends the state and everything is opaque again. Up // to four rows a side, never more than a quarter of the viewport each -- // the edge row nearly gone, so the melt is a statement rather than a // suspicion. All // of it is commented below because naming a state replaces its default // outright -- a declared copy would pin the wash to one theme's colour and // quietly stop following the next. states { drop_hover { grayscale amount=200; dim amount=140 } drop_target { grayscale amount=200; dim amount=140 } dead { grayscale amount=200; dim amount=90 } suspended { grayscale amount=170; dim amount=60 } // scrolled { // derived: a wash of theme's scroll_bg, and edge fades where more content is // tint amount=22 color="#7aa2f7" // dim amount="(above > 0) * max(0, (min(4, rows / 4) - y) * 60)" // dim amount="(below > 0) * max(0, (y - rows + min(4, rows / 4) + 1) * 60)" // } // dragging { } // floating { } // bell { // derived: a body shimmer, and a frame blink-then-breathe, in theme's bell colour // tint color="#efd7b3" amount="(since < 900) * max(0, 95 - abs(x + 2 * y - since / 4) * (6 + sin(since / 2) / 96) - since / 64)" // tint where="chrome" channel="fg" color="#e0af68" amount="since < 900 ? (since / 150 % 2 == 0) * 255 : 120 + sin(since / 8 + 158) / 3" // } // unfocused { dim amount=60 } // = dim_unfocused above; yours replaces it } // While a pane is being dragged its border stays solid and every other pane's // turns dashed, marking them as places it could be dropped. That is chrome, // not a shader, so it happens whatever the states above are set to. // Colour passes every pane gets, in the order written — a chain is a sequence, // so `grayscale` then `tint` is not `tint` then `grayscale`. Each node is one // pass; `amount` is 0..255 and defaults to 128. Listing a shader twice is how // you get two of it (two rulers, say). An unknown name is refused with a // warning and skipped, never guessed at. // // dim amount darken towards black // grayscale amount drain colour, weighted for the eye // tint amount color="#rrggbb" pull towards a colour // vignette amount darken towards the pane's edges // gradient amount direction=0..3 fade towards the background // 0 down, 1 up, 2 right, 3 left // zebra amount band=1 darken alternate bands of `band` rows; // band=1 is also a CRT scanline // ruler amount at=80 color= mark a column, background only, so the // text on it stays readable // margin amount at=100 everything from that column recedes // spotlight amount radius=12 brightness falls away from the cursor // // Two props are the *pass's* rather than the shader's, and work on any of them: // // where="content"|"chrome" the contents, or the frame (see further down) // channel="both"|"fg"|"bg" which of the cell's two colours it may keep; // `fg` recolours glyphs and leaves backgrounds // alone, which is what a border wants // // `amount` can also be an *expression* instead of a number, evaluated for // every cell. That is how you get an effect nobody built in, without building // anything: // // shaders { // dim amount="(y % 2) * 40" // scanlines // dim amount="(x > cols - 10) * 120" // the right margin // tint amount="255 - dist(x, y, curx, cury) * 12" color="#7aa2f7" // } // // The expression produces the *strength*, never the colour: the mixing stays // in C, so an expression cannot make an invalid cell, and its result is one // byte per cell -- which is what lets the whole program be evaluated once into // a per-cell map and then reused. A user-written positional shader therefore // costs about what a compiled one does. One that reads the clock cannot be // cached and is evaluated per cell per frame, which is the price of animation. // // variables x y the cell, inside the pane's content rect // cols rows the size of that rect // curx cury the cursor, and `cursor` (0 or 1) for whether // this pane has it // focused 0 or 1 // t milliseconds, for animation // since milliseconds the pane has been in its current // state -- what a one-shot effect needs, because // `t` is the time, not how long ago // above below lines of scrollback hidden past the viewport's // top and bottom edges, both 0 at the present -- // what lets an effect say "there is more this way" // operators + - * / % integer only; division by zero is 0, not a crash // < > <= >= == != and && || !, which give 0 or 1 -- so // `(x < 10) * 200` is how you write a rule // & | ^ ~ << >> on the 32-bit value. They bind *tighter* than // comparison, so `x & 7 == 0` means what it looks // like -- C reads that as `x & (7 == 0)`, which // Ritchie called a mistake and it is. The shift // count is masked to five bits and `>>` keeps the // sign, both matching the browser preview exactly. // `(x ^ y) & 1` is the cheapest interesting texture // there is. // ? : both sides are evaluated, then one is chosen // functions min(a,b) max(a,b) abs(a) clamp(v,lo,hi) // dist(x1,y1,x2,y2) counts a row double, because a cell is // about twice as tall as it is wide // sin(deg) cos(deg) degrees in, -255..255 out. Degrees because // they are what you guess, and 255 because it // is the strength scale everything else here // uses -- so `128 + sin(t / 4) / 2` is a // breathe with nothing to rescale. Any angle // wraps, negative ones included, since `t / 4` // does not stop at 360. // constants PI TAU 180 and 360: pi as an *angle*, which is all it can // be here -- a half turn and a whole one. So a // radian-shaped formula ports across as written, // `sin(TAU * x / cols)` is one cycle over the pane, // and the common fractions stay exact where // `3.14159 / 6` in integers would be 0. There is no // deg2rad/rad2deg: one unit, nothing to convert. // // There are no loops and no recursion, on purpose: a config cannot spin, and // there is nothing to sandbox. An expression that does not compile drops that // one shader with a warning -- never the config, and never a half-strength // version of what you asked for. // // Empty by default: these are yours to ask for, not opinions we hold. // // shaders { // vignette amount=70 // zebra amount=22 // ruler amount=60 at=80 color="#7aa2f7" // spotlight amount=90 radius=14 // } // ── chrome passes ─────────────────────────────────────────────────────────── // // `where="chrome"` on any entry above runs that pass over a pane's *frame* // instead of its contents: the border, the padding inside it, the title and // the buttons. Everything else is identical -- same shaders, same `amount`, // same expressions -- because a frame is just another rect, so it is just // another pass. `where="content"` is the default and never has to be written. // // The two never touch each other's cells: the contents are cut out of the // frame pass, so a full-strength chrome tint leaves your text alone and a // full-strength content tint leaves the border alone. // // `channel="fg"` (or `"bg"`, or `"both"`, the default) says which of a cell's // two colours a pass may keep. On a frame you almost always want `fg`: a // border's *background* is the terminal's own default, and mixing that towards // a colour paints a rectangle behind the glyphs instead of colouring them -- // and leaves it there until the pane changes state. `fg` also keeps the padding // inside the border out of it, since a blank cell has no foreground to colour, // so the effect lands on exactly the rules, the title and the buttons. // // `x`/`y` are the *whole frame's*, counted from its top-left corner, with // `cols`/`rows` the size of the pane's rect -- not of one side. That is what // lets an effect travel round a border rather than restart at every corner: // // shaders { // // the top rule only, where the title is // tint where="chrome" channel="fg" color="#7aa2f7" amount="(y==0)*255" // // a corner glow. One node per line: a newline ends a node, so a // // pass split over two lines is two broken nodes, not one pass. // tint where="chrome" channel="fg" color="#00ffd7" amount="255-dist(x,y,0,0)*20" // } // // For anything that *travels*, though, `x`/`y` are rarely what you want: it // wants distance *along the ring*, which the language can work // out -- clockwise from the top-left corner, in cells: // // (y == 0 ? x : x == cols - 1 ? cols - 1 + y // : y == rows - 1 ? 2 * cols + rows - 3 - x : 2 * cols + 2 * rows - 4 - y) // // with the perimeter itself `2 * cols + 2 * rows - 4`. Two things fall out of // that: `min(d, P - d)` is distance the short way round, so an effect crosses // the corner it started from instead of stopping at a seam; and // `(p - head + P) % P` is "how far ahead of the head", which makes an arc // rotate rather than a blob slide. There is no way to name a subexpression, so // it has to be written out in full -- which is why a small effect is a long // line. contrib/chrome has a dozen-odd ready to paste -- a shine running // round the border, ripples from a corner, a rotating comet, corner glints, a // sheen across the title rule, and more; the directory is the list. // // Hung off a state, this is a border that says what a pane is doing -- which // is what chrome is for, and it costs no room and no text: // // states { // // a dead pane's frame breathes red until you deal with it: a // // triangle wave, up and back down over about four seconds // dead { tint where="chrome" channel="fg" color="#ff0033" amount="abs(t/8%510-255)" } // // a bell nobody has answered yet: `since` is how long the pane has // // been in this state, so this is a real *flash* -- full strength for // // a quarter second and then gone, without the bell being answered. // // `t` could not say that: it is the time, not how long ago. (The // // default bell flash is this idea grown up: blinks, then a breathe.) // bell { tint where="chrome" channel="fg" color="#ffcc00" amount="(since<250)*255" } // // one that has not started yet is outlined, not filled // suspended { tint where="chrome" channel="fg" color="#8a8a95" amount=160 } // // the panes you are not in recede, frame and contents together. // // Both passes are written out because naming a state replaces the // // *whole* state: a block holding only chrome passes is a state with // // no content pass, which for `unfocused` means dim_unfocused above // // stops applying to it. // unfocused { dim amount=60; dim where="chrome" amount=90 } // } // // A pane a small window has collapsed to a single row (see min_pane) is chrome // all the way through, so a chrome pass is the only kind that can still colour // it -- which is the state where a colour is worth the most. // // The global chain is empty by default, like the content passes: yours to ask // for. One chrome pass does ship, hung off a state rather than here: the bell // flash (see the bell section, and the states table above). // How often to repaint while a shader is animated, in milliseconds -- which // means one whose `amount` reads `t` or `since`. Nothing else needs a frame // clock: every // other repaint has an event behind it, and this one is only asked for while // such a shader is actually on screen. 0 turns it off, which leaves an // animated shader advancing only when something else causes a frame. anim_ms 50 // How long an announcement ("copied 13 chars", a pane's OSC 9 notification, // "config reloaded") stays in the bottom-right corner. toast_ms 2500 // The logo, centered over everything for a moment when a client attaches, // wearing a different shader effect each time -- a greeting, and an // advertisement for the pass machinery that painted it (the effects are // ordinary amount-expressions over the logo's rect). Any key or click ends // it early and still does what it was going to do; a screen the logo does // not fit on gets no greeting rather than a cropped one; headless and // scripted sessions have nobody to greet. 0 for never. splash_ms 900 // What a new pane runs. Unset means $SHELL. // shell "/bin/bash" // ── theme ─────────────────────────────────────────────────────────────────── // A theme is a file of colours in a directory, named here rather than pasted // in. Named, because a name is a thing you can change: `theme_name` is one // line to edit, one `{"cmd":"theme","name":"amber"}` to switch while the // session runs, and one entry in a list a picker can show you. Sixty colours // pasted into this file are none of those things -- and they freeze the // palette at the moment you pasted them, so every later improvement to the // theme you copied from is one you never see. // // theme_name "phosphor" // Where those files are looked for. Unset, it is `themes/` beside this file // (so ~/.config/slosh/themes for the usual config), and then the ones slosh // installed in /share/slosh/themes -- a shipped theme is nameable // without being copied first, and a theme of yours with the same name shadows // it. Yours is searched first for exactly that reason. // // theme_dir "~/.config/slosh/themes" // A theme file *is* a config file -- one `theme { }` block in it and nothing // else -- which is why `slosh --check` lints one, why saving one reloads the // session wearing it, and why `include` still works for a theme that lives // somewhere a directory of names cannot reach. // // The shipped `default` theme is the annotated reference: every colour with // what it is for, and it matches what no config at all gets you. To start one // from the session in front of you: // // slosh --dump-theme > ~/.config/slosh/themes/mine.kdl // // Anything written in a `theme { }` block here beats the named theme, colour // by colour, so "that one, but with my green frames" is two lines: // // theme_name "phosphor" // theme { frame_focus "#00ff88" } // ── keys ──────────────────────────────────────────────────────────────────── // // Everything is `prefix` then a key. Pressing the prefix twice sends the // prefix itself to the pane. Modifiers are ctrl+ alt+ shift+ super+, and named // keys are left/right/up/down/enter/tab/escape/space/backspace/home/end/ // pageup/pagedown/delete/insert/backslash/minus/slash/comma/period/f1..f12. // // The cheatsheet's shorthand also parses -- C- M- S-, and ← → ↑ ↓ -- so a chord // copied off C-a ? is a chord you can paste in here. Anything else is the // character you press, shift included: "?" is shift+slash, "H" is shift+h. // // `slosh --check` reads this file and says what it could not honour. // // Actions: split split-cols split-rows close-pane focus-left focus-right focus-up // focus-down focus-next resize-left resize-right resize-up resize-down equalize // rotate-layout clear-shaders pane-to-next-tab pane-to-prev-tab pane-to-new-tab // scroll-up scroll-down scroll-page-up scroll-page-down scroll-top // scroll-bottom search zoom minimize float new-float float-grow float-shrink rerun // set-purpose rename-pane rename-tab new-tab next-tab prev-tab // select-tab-1..9 finder palette help detach quit // literal-prefix none // What C-a e opens this file with. Empty means $EDITOR, and then vi -- the // one editor a POSIX system is required to have, so a wrong guess you can see // beats a pane that opens empty. // // The pane it opens is *ephemeral*: quit the editor and the pane goes, with // no corpse to dismiss and no trace in a dumped layout. You asked for an // editor, not for a record of having had one. // // editor "nvim" keys { // The leader. Every binding below is this key, then the key on the line. // ctrl+a is a default and not a decision -- it is start-of-line to half // the world, and if that is you, change it here and nothing else: the // badge in the status bar and the cheatsheet (C-a ?) both name whatever // is bound, and the old key goes straight through to your shell. // // Pressing it twice always sends the key itself, whichever key it is. prefix "ctrl+a" // Enter splits whichever way there is room for, preferring the longer side; // the next two are for when you mean a particular axis. bind "enter" "split" bind "\\" "split-cols" bind "-" "split-rows" bind "x" "close-pane" // Run a dead pane's command again, in the same pane and on top of the // same scrollback. Refused on a pane that is still running. bind "r" "rerun" bind "z" "zoom" bind "m" "minimize" // Lift the pane out of the layout (it pops to the centre) and drop it // back in: f is what "float" sounds like. The finder lives on `s`, // where it reads as *search*. `F` is the same verb on a fresh thing -- // a new floating shell, the throwaway terminal. bind "f" "float" bind "F" "new-float" // The =/+ key grows a focused float about its centre and `-` shrinks it // (through split-rows, whose split is a refusal on a float anyway). // Bound both ways because whether `+` arrives shifted depends on the // outer terminal -- the same deal `?` has with `/` -- which is also why // equalize moved to `0` rather than sharing the key: two verbs on one // key survive only until a terminal drops the shift. bind "=" "float-grow" bind "+" "float-grow" bind "h" "focus-left" bind "l" "focus-right" bind "k" "focus-up" bind "j" "focus-down" bind "left" "focus-left" bind "right" "focus-right" bind "up" "focus-up" bind "down" "focus-down" bind "o" "focus-next" // The boundary moves in the direction you press, whichever side of it the // focused pane is on. bind "shift+h" "resize-left" bind "shift+l" "resize-right" bind "shift+k" "resize-up" bind "shift+j" "resize-down" // Undo every drag and nudge at once: each split divided between the panes // behind it, so every visible pane gets an even share of the rows and // columns it competes for. On the digit row it already lives on: 1..9 // pick a tab, 0 resets the shares. bind "0" "equalize" // A quarter turn of the whole tab, clockwise: every split changes axis and // a row split's children reverse, because the pane on top of a stack is the // pane on the right once you turn it. Four presses come back exactly, so // there is nothing to undo -- which is what earns it the space bar, the // easiest key there is and the one tmux spends on cycling layouts. Refused // with a word when the turn would leave a pane under the floor: a share of // the height is not always a share the width can afford, and `=` first // usually fixes that. bind "space" "rotate-layout" // Push the focused pane one tab along, or out into a tab of its own. `>` and // `<` are the shifted period and comma; a terminal does not report the shift // for punctuation, so `.` and `,` answer to them too -- the same deal `/` // already has with `?`. bind ">" "pane-to-next-tab" bind "<" "pane-to-prev-tab" bind "b" "pane-to-new-tab" bind "c" "new-tab" // Tab cycles tabs, the way every other tabbed thing does. `n` and `p` // used to, and are free again: `p` is worth more as the palette, which is // pressed rather more often than "the tab before this one". bind "tab" "next-tab" bind "shift+tab" "prev-tab" bind "1" "select-tab-1" bind "2" "select-tab-2" bind "3" "select-tab-3" bind "s" "finder" // Every action by name, including the ones nothing is bound to -- which // is the whole point of it, since an unbound action has no other way in. bind "p" "palette" // The cheatsheet, built from whatever is bound below rather than from a // list of what the defaults were -- rebind something and it says so. // Bound twice because a terminal may or may not tell us `?` was shifted. bind "?" "help" bind "/" "help" // Scrollback. The wheel does this too, unless the program in the pane // wants the mouse itself, or is a full-screen program with no scrollback // of ours to show — then the wheel means arrow keys to it. bind "pageup" "scroll-page-up" bind "pagedown" "scroll-page-down" bind "home" "scroll-top" bind "end" "scroll-bottom" bind "d" "detach" bind "q" "quit" // ── bindings that need no leader ──────────────────────────────────────── // // Anything in here fires the moment you press it, with no prefix first. // That also means the chord is *gone* from every program in every pane: // vim will never see it, your shell will never see it, and the only sign // will be that the key does not work in there any more. // // It is a footgun and it is yours: this is your keyboard, and a // multiplexer that refuses to get out of the way is its own kind of // wrong. Prefer chords no program wants -- ctrl+alt+something is usually // safe, a bare letter certainly is not. // // `none` gives a chord back, the same as anywhere else. // // direct { // bind "ctrl+alt+left" "focus-left" // bind "ctrl+alt+right" "focus-right" // bind "ctrl+alt+up" "focus-up" // bind "ctrl+alt+down" "focus-down" // } }