1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
|
/* The opinionated defaults, and the file that overrides them.
*
* Everything here has a working value compiled in, so a missing or broken
* config file costs you a warning, never a terminal (the fail-open property
* D9 asks of the CLI, applied to the mux itself).
*/
#ifndef SLOSH_CONFIG_H
#define SLOSH_CONFIG_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "expr.h"
#include "shader.h"
#include "slosh.h"
/* Pane states a shader can be hung off, in the order they are tested. Only
* states a pane can actually be seen in — which `dead` now is: a pane whose
* program exits is kept until it is dismissed, so there is a frame to draw.
* It ranks just under the drag states, above every ambient one: nothing about
* a pane is more worth knowing than that it is over. */
typedef enum {
PSTATE_DRAGGING, /* the pane you have hold of */
PSTATE_DROP_HOVER, /* the one under the pointer, where it would land */
PSTATE_DROP_TARGET, /* the others, all of them somewhere it could go */
PSTATE_DEAD, /* its program exited; waiting to be re-run or closed */
PSTATE_SUSPENDED, /* laid out, never started */
/* A BEL arrived and nobody has looked since. Above the ambient states
* because it is *news* — the whole point of a bell is to be noticed on a
* pane you were not watching — and below `dead`, because a pane that rang
* and then exited is a pane that exited. It ends when you look at the pane,
* which is what answering a bell is. */
PSTATE_BELL,
PSTATE_SCROLLED, /* looking at scrollback rather than the present */
/* Lifted out of the layout, drawn on top. Above `unfocused`, so a float is
* never dimmed by dim_unfocused: full strength is what keeps the thing on
* top reading as lifted off the page — the same reason `dragging` keeps no
* default. Ships no chain of its own either; the shadow does the telling
* apart, and a config that wants colour writes `states { floating { } }`. */
PSTATE_FLOATING,
PSTATE_UNFOCUSED,
PSTATE_COUNT,
} pane_state_t;
/* The config name for a state, e.g. "drop_target". */
const char *pane_state_name(pane_state_t s);
typedef enum {
ACT_NONE = 0,
ACT_SPLIT,
ACT_SPLIT_COLS,
ACT_SPLIT_ROWS,
ACT_CLOSE_PANE,
ACT_RERUN,
ACT_ZOOM,
ACT_MINIMIZE,
ACT_FLOAT,
ACT_NEW_FLOAT,
ACT_FLOAT_GROW,
ACT_FLOAT_SHRINK,
ACT_SET_PURPOSE,
ACT_RENAME_PANE,
ACT_RENAME_TAB,
ACT_ROTATE_LAYOUT,
ACT_FOCUS_LEFT,
ACT_FOCUS_RIGHT,
ACT_FOCUS_UP,
ACT_FOCUS_DOWN,
ACT_FOCUS_NEXT,
ACT_NEW_TAB,
ACT_CLOSE_TAB,
ACT_NEXT_TAB,
ACT_PREV_TAB,
ACT_FINDER,
ACT_PALETTE,
ACT_WORKSPACES,
ACT_SAVE_WORKSPACE,
ACT_SCROLL_UP,
ACT_SCROLL_DOWN,
ACT_SCROLL_PAGE_UP,
ACT_SCROLL_PAGE_DOWN,
ACT_SCROLL_TOP,
ACT_SCROLL_BOTTOM,
ACT_RESIZE_LEFT,
ACT_RESIZE_RIGHT,
ACT_RESIZE_UP,
ACT_RESIZE_DOWN,
ACT_EQUALIZE,
ACT_CLEAR_SHADERS,
ACT_PANE_TO_NEXT_TAB,
ACT_PANE_TO_PREV_TAB,
ACT_PANE_TO_NEW_TAB,
ACT_DETACH,
ACT_QUIT,
ACT_HELP,
ACT_EDIT_CONFIG,
ACT_LITERAL_PREFIX,
ACT_SELECT_TAB_1, /* .. +8, so digits stay one entry each */
} action_t;
typedef struct {
int key; /* GhosttyKey */
uint16_t mods; /* MOD_* */
action_t action;
/* Fires without the leader, taking that chord away from every program in
* every pane. Deliberately possible and deliberately opt-in: it is the
* user's terminal and their keyboard, and a multiplexer that refuses to get
* out of the way is its own kind of wrong. */
bool direct;
} binding_t;
typedef enum { ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT } align_t;
/* How many files one config may be built from: itself plus its includes. Small
* and fixed, because a config assembled from more than this many pieces is a
* thing nobody can read. */
#define CONFIG_FILES_MAX 16
/* How many project roots one config may name. `~/dev` and `~/work` is the case;
* a person with more than this many places they keep code has a search problem
* rather than a list problem. */
#define PROJECT_ROOTS_MAX 8
/* How many complaints one load keeps. A config with more than this many
* problems in it has one problem, and the list is long enough to say so. */
#define CONFIG_MSGS_MAX 32
typedef struct {
/* geometry */
uint16_t gap, gap_aspect;
/* Space between a pane's frame and its contents, per side, in the same unit
* `gap` uses: ROWS. A horizontal one is multiplied by `gap_aspect` on the way
* out, so `padding 1` is a square-looking ring rather than a squashed one —
* and so a number means the same thing wherever it appears, however many of
* them were written. Written as 1, 2 or 4 values, CSS order. */
uint16_t pad_top, pad_right, pad_bottom, pad_left;
bool rounded;
align_t title_align;
/* Cells between a frame's corner and the start of its title. The title
* always carries a space on each side on top of this, so a title can never
* end up welded to the rule beside it. */
uint16_t title_inset;
/* Mark a pane's titlebar when a BEL arrives there and nobody has looked
* since. Off makes a bell silent *and* invisible, which is a real choice. */
/* Buttons in the top-right of a pane's frame: zoom, and close. Off leaves
* the keyboard bindings alone; it is about the affordance, not the verb. */
/* A word in the middle of the status line for whatever the pointer is on.
* Discoverability for a frame whose affordances are mostly one character
* wide; off if you already know them. */
bool hints;
/* What slosh this is, in the middle of the status line, whenever there is
* no hint to put there. The slot is otherwise empty most of the time, and
* "which build am I attached to" is the first question when a session is
* behaving oddly -- a session keeps the binary it started with, so the
* answer is not whatever was built last. */
bool version_banner;
bool pane_buttons;
/* Marks of similar visual weight, so the even spacing between the buttons
* reads as even: a glyph that floats in a mostly-empty cell donates its own
* whitespace to the gap beside it and looks further away than its
* neighbours, which is what an en dash did here. */
/* 32 bytes because a mark is a grapheme cluster, not a character: a
family emoji is eighteen bytes on its own. Truncating one mid-
codepoint used to put invalid UTF-8 on the wire. */
char zoom_mark[32], zoom_on_mark[32], close_mark[32], min_mark[32];
/* The new-tab button at the end of the strip. Drawn with a space each side,
* so a one-character mark is still a three-cell target. */
char newtab_mark[32];
bool bell_indicator;
/* What to draw. A short string rather than a fixed glyph: the obvious
* choice is an emoji bell, and emoji are drawn two columns wide by many
* terminals while chrome here is booked as one — so the default is narrow
* and anyone who knows their terminal can say otherwise. */
char bell_mark[32];
/* Which dead panes stay, showing what they printed and offering to run
* again, and which just go.
*
* The line is whether the pane was given a *command*. A pane told to run
* `npm run dev` is part of the session's shape and its death is news: you
* want the error, and you want the button that runs it again. A pane that
* is a shell -- one you split off to do something in -- is finished when
* you type `exit`, and leaving its corpse on screen to be dismissed is
* exactly the fussiness a terminal should not have.
*
* Not *who made it*: a shell is a shell whether it came from a layout file
* or from C-a \. That also means a session restored from dump-layout
* behaves like the one it was dumped from, since the dump writes `command=`
* only for panes that had one. */
enum { KEEP_DEAD_NONE, KEEP_DEAD_COMMANDS, KEEP_DEAD_ALL } keep_dead;
uint16_t min_pane_cols, min_pane_rows;
/* The smallest pane a split is allowed to *produce*. min_pane is the point
* below which the layout gives up and collapses a pane; this is the point
* below which splitting stopped being worth offering, which is a larger
* number and a different question. Never used below min_pane: refusing to
* offer something the layout would collapse anyway is the floor. */
uint16_t min_split_cols, min_split_rows;
uint16_t scroll_lines; /* rows per wheel notch */
/* How much history a pane keeps. `scrollback` is the number people mean --
* lines -- and `scrollback_bytes` is the ceiling that keeps one very wide,
* heavily styled pane from spending the machine to honour it: lib-vt applies
* whichever limit is reached first, and both depend on things a line count
* cannot see. 0 lines is no scrollback; 0 bytes is no ceiling.
*
* Not a `uint16_t`, because 65,535 is a number somebody will want to exceed
* and a silently wrapped limit is worse than a refused one. */
size_t scrollback;
size_t scrollback_bytes;
uint16_t toast_ms; /* how long an announcement stays up */
/* How long the logo splash stays over a freshly attached screen, 0 for
* never. Attach-only: a headless or scripted session has nobody to greet. */
uint16_t splash_ms;
uint16_t hover_delay_ms; /* how long the pointer must rest to arm a guide */
uint16_t double_click_ms; /* how close two clicks must be to be a double */
/* What a double-click's word stops at, listed as *separators* rather than as
* word characters: anything unlisted belongs to a word, so text nobody
* enumerated -- CJK, accents, emoji -- needs no table. Whitespace always
* separates, listed or not. */
char word_separators[64];
/* How far everything behind a modal is pushed back, 0..255, 0 being off.
* A modal that floats over a screen as bright as it is reads as another
* pane rather than as something in front. */
uint8_t modal_scrim;
/* How far the panes you are *not* in are pushed back, 0..255 and 0 for not
* at all. A knob rather than only a `states { unfocused { } }` chain,
* because "dim the other panes" is a thing people want by name and should
* not have to know the shader vocabulary to ask for -- and because the
* argument against shipping it on was that turning it off should be
* obvious, which a single number is and a chain is not.
*
* Writing the state chain by hand still wins: the knob only fills it in
* when the config has not. */
uint8_t dim_unfocused;
/* How dark the cell of shade a floating pane casts on what it covers,
* 0..255 and 0 for no shadow. The shadow is what tells a float from a tile
* at a glance — the frame is the same frame — which is why it ships on and
* why it is a number rather than a state chain: turning it off should not
* require knowing the states block exists (the dim_unfocused argument). */
uint8_t float_shadow;
/* Colour passes every pane gets, in the order they were written. Ordinary
* shaders rather than policy: the session has no opinion about these, you
* asked for them. */
shader_t shaders[SHADE_MAX];
size_t nshaders;
/* What a pane looks like when it is in a particular state. A pane is
* usually in several at once — unfocused *and* scrolled, suspended *and*
* something you could drop onto — so exactly one wins, the first that
* matches in the order below. That order is fixed rather than taken from
* the config, 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.
*
* Stacking them instead would let two reasons to be grey compound into one
* muddy grey that reads as neither, which is the mistake the drag policy
* already had to avoid by hand. */
shader_t state_shaders[PSTATE_COUNT][SHADE_MAX];
size_t state_n[PSTATE_COUNT];
/* Whether any file in this load named `scrolled` (or `bell`) itself. The
* default wash over scrollback and the default bell flash are derived from
* the theme *after* each file's theme block -- scroll_bg and bell
* respectively -- so a theme moves them with their indicators. But a chain
* somebody wrote, in this file or in one it included, must stand. A local
* flag cannot say that across an include chain, so they live here and are
* cleared with the rest of the defaults. */
bool scrolled_declared;
bool bell_declared;
/* The same two things again, over a pane's *frame* rather than its contents:
* any entry in `shaders` or in a state's block carrying `where="chrome"`
* lands here instead. Separate chains rather than a flag on each shader,
* because a pass is per rect: the contents and the frame are two rects and
* therefore two passes, and a chain is exactly the list one pass runs. It
* also keeps the flag out of the plugin ABI, which has no business knowing
* where a session chose to run an effect. */
shader_t chrome_shaders[SHADE_MAX];
size_t nchrome_shaders;
shader_t chrome_state_shaders[PSTATE_COUNT][SHADE_MAX];
size_t chrome_state_n[PSTATE_COUNT];
/* How often to repaint while something on screen is animated -- which means
* a shader whose amount reads the clock. Nothing else needs a frame clock:
* every other repaint has an event behind it. 0 turns animation off, in the
* sense that an animated shader then only advances when something else
* causes a frame. */
uint16_t anim_ms;
bool status_bar; /* the strip along the top: tabs, prefix, pane count */
bool status_line; /* the line along the bottom: what you are looking at */
/* How far the strip and the line are held off the left and right edges.
* Separate from `gap`, which is the space around the *panes*: the two happen
* to match by default and there is no reason they must. */
uint16_t status_pad;
bool focus_follows_mouse;
/* Whether the program running in a pane may set that pane's shader chains
* over OSC 5577. Off, because a program restyling the session it happens to
* be running in is a hazard and not a feature (D13) -- and on for anyone
* prototyping a chain, because the alternative is edit, save, look, and the
* whole point of a colour pass is that you can see it.
*
* Scoped to the pane that asked: it cannot touch another pane, the chrome
* outside itself, or anything the config said about everybody else. */
bool in_band_shaders;
/* What a cell's "terminal default" colour resolves to when a shader has to
* compute on it. We cannot know the client's real default, and most text is
* default-coloured, so a shader would otherwise leave nearly everything
* alone. Only consulted while shading: an unshaded pane still defers to the
* terminal exactly as before. */
color_t default_fg, default_bg;
/* theme
*
* One name per decision. These began as six names doing thirty jobs, which
* meant the split guide could not be recoloured without also recolouring the
* focused frame, because they were the same entry. Several still share a
* default — that is a statement about what looks right together, not about
* them being the same thing. */
/* panes */
color_t frame_focus, frame_idle;
color_t title_focus, title_idle;
/* things you can press: the pane's own OSC 5577 buttons */
color_t button_fg, button_bg, button_bg_idle;
/* affordances that appear under the pointer */
color_t guide; /* the split guide: armed edge and dashed boundary */
color_t resize; /* the handle in the gap between two panes */
color_t drop_target; /* the frame of a pane a drag could land on */
/* the scrollback indicator in a pane's frame */
color_t scroll_fg, scroll_bg;
/* rows of a tab that has collapsed into a list */
color_t header, header_hover, header_hover_title;
/* the strip along the top */
color_t tab_active_fg, tab_active_bg;
/* The active tab keeps its fill while the pointer is on it -- it is still
* the tab you are in -- so its hover shows in the text instead. */
color_t tab_active_hover_fg;
color_t tab_idle, tab_hover;
color_t prefix_fg, prefix_bg; /* the "C-a" badge */
color_t tab_count; /* how many panes in the session */
/* the line along the bottom */
color_t status, status_state;
/* the pane finder */
color_t finder_fg, finder_bg, finder_sel_fg, finder_sel_bg;
/* the in-place editor a double-click opens on a pane's title */
color_t rename_fg, rename_bg;
/* the bell mark in a pane's titlebar */
color_t bell;
/* what a dead pane says: the line in its backlog, and the word in the
* status line and its own frame */
color_t dead;
/* the frame's own buttons */
color_t pane_button, pane_button_hover;
/* the row of minimised panes along the bottom */
color_t minbar, minbar_hover;
/* the hint in the middle of the status line */
color_t hint;
/* Modals: a surface that floats over the layout, with its own frame. Named
* separately from the pane colours it resembles because it is a different
* *surface* — it sits on top of everything, it is opaque, and it has to
* stay legible against a background that has been dimmed underneath it.
* Borrowing the pane button colour here is what made the close button
* invisible: dim on dim. */
color_t modal_fg, modal_bg;
color_t modal_border, modal_title;
color_t modal_button, modal_button_hover;
/* announcements */
color_t toast_fg, toast_bg;
/* keys */
int prefix_key;
uint16_t prefix_mods;
binding_t *binds;
size_t nbinds;
/* Every amount expression this config compiled. A shader_t is copied by
* value all over the place and several copies can point at one program, so
* ownership lives here — with the config that parsed them, freed when it is
* replaced. */
expr_prog_t **exprs;
size_t nexprs;
/* what a new pane runs; NULL means $SHELL */
char *shell;
/* What `edit-config` opens the file with; NULL means $EDITOR, then vi. */
char *editor;
/* Where to look for shader plugins (`*.so`). NULL means the default: a
* `shaders` directory beside the config file. Read before the `shaders` and
* `states` blocks are parsed, because what they are allowed to name depends
* on what has been loaded. */
char *shader_dir;
/* Where projects live: `project_roots "~/dev" "~/work" depth=2`. A project is
* a subdirectory of one of these with a `slosh.layout` in it -- or a
* `.git`, which gets `project_layout` instead. Empty means the feature is
* dormant and costs nothing.
*
* `depth` is levels below each root, and a directory that *is* a project is
* never descended into, so 2 covers `~/dev/work/api` without walking any
* checkout's insides. */
char *project_roots[PROJECT_ROOTS_MAX];
size_t nproject_roots;
int project_depth;
/* The layout a project with no file of its own opens as. Yours rather than a
* guess about your stack: relative paths in it bind to whichever project is
* being opened, so one file is the shape you start every project in. NULL is
* one pane running your shell. */
char *project_layout;
/* Every file this config was built from, in the order they were read: the one
* that was loaded and everything it included, whether or not each existed.
* Kept because the watcher needs it — a theme you can include is a theme you
* expect to reload when you save it — and recorded even for a file that is
* not there, so a session started before you wrote one still notices it
* appearing. */
char *files[CONFIG_FILES_MAX];
size_t nfiles;
/* Everything this load had to complain about, in the order it found them,
* each already carrying the file and line it happened at. A session shows the
* first (there is one status line); `--check` shows them all, which is the
* whole difference between a warning and a linter. */
char msgs[CONFIG_MSGS_MAX][192];
size_t nmsgs;
/* Loader scratch: the file being read right now, so a complaint can say which
* one it came from without every call site being handed the path. Meaningless
* once loading has finished. */
const char *loading;
} config_t;
/* Defaults, then <config dir>/config.kdl on top. Never fails: on a bad file
* it keeps the defaults and writes the reason to `err`. */
void config_defaults(config_t *c);
bool config_load(config_t *c, const char *path, char *err, size_t errcap);
/* $SLOSH_CONFIG, else $XDG_CONFIG_HOME/slosh/config.kdl, else ~/.config/… */
const char *config_default_path(void);
/* Every complaint from the last load, oldest first: "config.kdl:12: ...".
* Returns how many were written. */
size_t config_messages(const config_t *c, const char **out, size_t max);
void config_free(config_t *c);
/* The files this config was read from, oldest first: `files[0]` is the one that
* was loaded, the rest are what it included. Returns how many were written. */
size_t config_files(const config_t *c, const char **out, size_t max);
/* Shader chains from text rather than from a file: one entry, several separated by
* `;`, or a whole `shaders { ... }` block. Every shape a config file has, because
* the point of using the config's syntax at a prompt is that the two directions
* match -- what you prototype is what you paste, and what you paste is something
* you can type back.
*
* Each pass goes where its own `where=` says; `default_chrome` is only what a pass
* that keeps quiet means. Here because the parser belongs with the config: a chain
* typed at a running pane and a chain in a config file have to mean the same
* thing, or prototyping teaches you something that then does not work.
*
* Both arrays hold SHADE_MAX and `exprs` twice that; the compiled programs belong
* to the caller, since a pane's chain outlives no load in particular. Returns how
* many entries were understood, writing the first refusal to `err`. */
size_t config_parse_chain_doc(const char *text, color_t default_color,
bool default_chrome, shader_t *content,
size_t *ncontent, shader_t *chrome,
size_t *nchrome, expr_prog_t **exprs,
size_t *nexprs, char *err, size_t errcap);
/* The same, from a file: a `shaders { }` block as `contrib/chrome` and
* `contrib/shaders` ship them, read with the parser that already knows this
* format. Entries are routed to the two chains by their own `where=`, defaulting
* to content as a config file does, so a preset lands where its author said.
* Anything else at the top level is ignored -- hand it a whole config.kdl and
* only the shaders apply. Both arrays hold SHADE_MAX; `exprs` holds twice that.
* Returns how many entries were understood. */
size_t config_parse_chain_file(const char *path, color_t default_color,
shader_t *content, size_t *ncontent,
shader_t *chrome, size_t *nchrome,
expr_prog_t **exprs, size_t *nexprs, char *err,
size_t errcap);
/* Whether a name is one of the config's own settings. For the layout loader, which
* shares this syntax and needs to tell a config apart from a session file -- one
* list, so the two answers cannot disagree. */
bool config_is_setting(const char *name);
/* What a chord does after the leader. Direct bindings answer here too, so
* pressing the leader first never makes a binding stop working. */
action_t config_lookup(const config_t *c, int key, uint16_t mods);
/* What a chord does on its own, with no leader pressed. ACT_NONE for the
* overwhelming majority of keys, which is what lets them reach the pane. */
action_t config_lookup_direct(const config_t *c, int key, uint16_t mods);
/* Whether any binding fires without the leader, so the cheatsheet knows
* whether it has a section to draw. */
bool config_has_direct(const config_t *c);
/* The reverse of config_parse_chord: what to call a binding on screen. A
* shifted letter is written the way you would type it ("H"), because "S-h" is
* a description of a keystroke rather than a keystroke. */
void config_chord_name(int key, uint16_t mods, char *out, size_t cap);
/* A phrase for the cheatsheet, and which group it belongs under. `group` is
* one of a fixed set, in the order they should be shown. */
const char *config_action_label(action_t a);
/* The name this action is written as in a config file. */
const char *config_action_name(action_t a);
/* The whole config as a file you could have written, with the values it
* currently holds. Generated rather than kept on disk, because a checked-in
* copy of the defaults is a second source of truth and drifts. Caller frees. */
char *config_render(const config_t *c);
/* The same, for a config nobody has edited: what a fresh install would do.
* Caller frees. */
char *config_dump_defaults(void);
const char *config_action_group(action_t a);
/* "ctrl+a", "alt+left", "\\", "f" -> key + mods. False if unparseable. */
bool config_parse_chord(const char *text, int *out_key, uint16_t *out_mods);
#endif /* SLOSH_CONFIG_H */
|