@vibelord/lanes 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +36 -5
  2. package/bin/lanes-install.mjs +166 -13
  3. package/lib/plan.mjs +113 -3
  4. package/package.json +1 -1
  5. package/payload/lanes/README.md +47 -23
  6. package/payload/lanes/VERSION +1 -1
  7. package/payload/lanes/bin/lanes +2753 -202
  8. package/payload/lanes/client/ghostty/lanes-palette-light.conf +19 -19
  9. package/payload/lanes/client/ghostty/lanes-palette.conf +19 -19
  10. package/payload/lanes/client/lanes-launch +289 -40
  11. package/payload/lanes/client/lanes-window +15 -3
  12. package/payload/lanes/container/hooks/claude-settings.json +1 -0
  13. package/payload/lanes/container/lane +147 -92
  14. package/payload/lanes/container/lane-edit +100 -0
  15. package/payload/lanes/container/lane-mcp +1150 -0
  16. package/payload/lanes/container/lane-signal +109 -29
  17. package/payload/lanes/container/lane_core.py +1318 -0
  18. package/payload/lanes/container/vimrc +31 -0
  19. package/payload/lanes/docs/ARCHITECTURE.md +793 -16
  20. package/payload/lanes/docs/DESIGN.md +127 -46
  21. package/payload/lanes/docs/INSTALL.md +175 -18
  22. package/payload/lanes/docs/RUNBOOK.md +968 -76
  23. package/payload/lanes/docs/SHARING.md +3 -2
  24. package/payload/lanes/docs/USERGUIDE.md +967 -150
  25. package/payload/lanes/holders.lock +5 -0
  26. package/payload/lanes/install/enable-docs.sh +78 -0
  27. package/payload/lanes/install/enable-web.sh +2 -0
  28. package/payload/lanes/install/host-install.sh +197 -7
  29. package/payload/lanes/install/services.py +148 -18
  30. package/payload/lanes/install.sh +68 -7
  31. package/payload/lanes/libexec/agent_state.py +74 -1
  32. package/payload/lanes/libexec/cursor_sync.py +6 -0
  33. package/payload/lanes/libexec/device_sync.py +47 -10
  34. package/payload/lanes/libexec/docs_store.py +995 -0
  35. package/payload/lanes/libexec/docsd.py +2010 -0
  36. package/payload/lanes/libexec/economy_poll.py +1 -0
  37. package/payload/lanes/libexec/enrold.py +41 -22
  38. package/payload/lanes/libexec/keys.py +54 -23
  39. package/payload/lanes/libexec/lanes_lib.py +620 -22
  40. package/payload/lanes/libexec/mdlite.py +575 -0
  41. package/payload/lanes/libexec/members.py +21 -2
  42. package/payload/lanes/libexec/notify.py +169 -29
  43. package/payload/lanes/libexec/registrations.py +216 -18
  44. package/payload/lanes/libexec/session_actions.py +182 -14
  45. package/payload/lanes/libexec/sidebar +11 -3
  46. package/payload/lanes/libexec/sidebar.py +2232 -365
  47. package/payload/lanes/libexec/statusd.py +231 -12
  48. package/payload/lanes/libexec/studio_host.py +8 -1
  49. package/payload/lanes/libexec/tokens.py +148 -35
  50. package/payload/lanes/libexec/tokens.sh +86 -2
  51. package/payload/lanes/libexec/update.py +1044 -0
  52. package/payload/lanes/libexec/uploads.py +538 -0
  53. package/payload/lanes/libexec/users.py +614 -0
  54. package/payload/lanes/libexec/view.sh +34 -6
  55. package/payload/lanes/systemd/lanes-docsd.service +29 -0
  56. package/payload/lanes/systemd/lanes-ensure.service +3 -0
  57. package/payload/lanes/systemd/lanes-gc.service +1 -1
  58. package/payload/lanes/systemd/lanes-update.service +17 -0
  59. package/payload/lanes/systemd/lanes-update.timer +17 -0
  60. package/payload/lanes/web/Caddyfile +24 -1
  61. package/payload/lanes/web/README.md +165 -28
  62. package/payload/lanes/web/browser.js +4 -8
  63. package/payload/lanes/web/docs.css +52 -0
  64. package/payload/lanes/web/docs.js +36 -0
  65. package/payload/lanes/web/mobile.css +17 -17
  66. package/payload/lanes/web/mobile.js +45 -32
  67. package/payload/lanes/web/upload.css +17 -0
  68. package/payload/lanes/web/upload.js +84 -0
  69. package/payload/lanes/workspaces.example/README.md +3 -1
  70. package/payload/lanes/zellij/config.kdl +106 -56
  71. package/payload/manifest.json +6 -3
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """The Lanes design tokens: ONE table, rendered seven ways. The sidebar imports `sgr_table`; `--kdl`, `--css`, `--sh`,
3
- `--md`, `--js` and `--ghostty-dark|--ghostty-light` write the Zellij theme, the phone CSS, the viewer's shell variables,
4
- the docs table, the phone's terminal palette and the optional Ghostty palette files between markers; `--check` fails
5
- when any committed block differs. Spec: docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md.
2
+ """The Lanes design tokens: ONE table, rendered eight ways. The sidebar imports `sgr_table` (truecolor, 256, 16 or
3
+ none); `--kdl`, `--css`, `--sh`, `--md`, `--js`, `--ghostty-dark|--ghostty-light` and `--docs-css` write the Zellij
4
+ theme, the phone CSS, the viewer's shell colour sets, the docs tables, the phone's terminal palette, the optional
5
+ Ghostty palette files and the docs page's CSS (plan W10 W6) between markers; `--check` fails when any committed block
6
+ differs. Spec:
7
+ docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md; palette v2 + the 256-colour set: cockpit v3 P1 (LNS-69).
6
8
  No third-party code; importable with no side effects."""
7
9
  import difflib
8
10
  import os
@@ -12,30 +14,54 @@ from pathlib import Path
12
14
  ROLES = ('surface', 'surface-raised', 'line', 'accent-wash', 'accent-line', 'press', 'chip',
13
15
  'ink', 'ink-dim', 'ink-faint', 'accent', 'on-accent', 'on-accent-urgent',
14
16
  'tone-urgent', 'tone-attention', 'tone-working', 'tone-done', 'scrim', 'shadow')
17
+ # Palette v2 (cockpit v3 P1, 2026-10-06): vivid, high-contrast roles in both variants. v1 had darkened the light tones
18
+ # until they passed (amber #8a5a00 read olive-brown). Surfaces are v1's but for the dark accent-wash, the cursor row.
15
19
  TOKENS = {
16
- 'dark': {'surface': '#101318', 'surface-raised': '#191e25', 'line': '#2c333d', 'accent-wash': '#1a2721',
17
- 'accent-line': '#4b6955', 'press': '#26332c', 'chip': '#ffffff0f', 'ink': '#edf1f5', 'ink-dim': '#9ca7b5',
18
- 'ink-faint': '#6b7683', 'accent': '#b4e3bf', 'on-accent': '#101318', 'on-accent-urgent': '#7a1a12',
19
- 'tone-urgent': '#f28b7d', 'tone-attention': '#e5b84a', 'tone-working': '#7fb3f2', 'tone-done': '#6fd39a',
20
+ 'dark': {'surface': '#101318', 'surface-raised': '#191e25', 'line': '#2c333d', 'accent-wash': '#1c3326',
21
+ 'accent-line': '#4b6955', 'press': '#26332c', 'chip': '#ffffff0f', 'ink': '#f2f5f8', 'ink-dim': '#a6b1be',
22
+ 'ink-faint': '#6f7a87', 'accent': '#8fe3a6', 'on-accent': '#101318', 'on-accent-urgent': '#7a1a12',
23
+ 'tone-urgent': '#ff7b6e', 'tone-attention': '#ffc233', 'tone-working': '#6fb6ff', 'tone-done': '#5fdc8a',
20
24
  'scrim': '#000000a6', 'shadow': '0 -16px 80px #0007'},
21
25
  'light': {'surface': '#f3f5f2', 'surface-raised': '#ffffff', 'line': '#d5ded6', 'accent-wash': '#e5f0e6',
22
- 'accent-line': '#acc9b4', 'press': '#d8e9db', 'chip': '#edf3ec', 'ink': '#17241b', 'ink-dim': '#56616d',
23
- 'ink-faint': '#7f8a96', 'accent': '#2f6b45', 'on-accent': '#ffffff', 'on-accent-urgent': '#ffd3cc',
24
- 'tone-urgent': '#b7322a', 'tone-attention': '#8a5a00', 'tone-working': '#2456a8', 'tone-done': '#1b7142',
26
+ 'accent-line': '#acc9b4', 'press': '#d8e9db', 'chip': '#edf3ec', 'ink': '#111a14', 'ink-dim': '#4a5661',
27
+ 'ink-faint': '#6f7b87', 'accent': '#1f6f3f', 'on-accent': '#ffffff', 'on-accent-urgent': '#ffd3cc',
28
+ 'tone-urgent': '#c41c1c', 'tone-attention': '#a04d00', 'tone-working': '#1a56c4', 'tone-done': '#0d7338',
25
29
  'scrim': '#1a222b80', 'shadow': '0 -16px 80px #0003'},
26
30
  }
27
31
  # 16-colour fallback: the user's own palette resolves these. Selection is achromatic (bold) there, dim is bright-black.
28
32
  ANSI16 = {'ink': '39', 'ink-dim': '90', 'ink-faint': '90', 'accent': '1;39',
29
33
  'tone-urgent': '31', 'tone-attention': '33', 'tone-working': '34', 'tone-done': '32'}
34
+ # Painted roles that are a BACKGROUND, by the token they take their colour from. `selected` is the cursor row (cockpit
35
+ # v3 P2 paints it): the accent-wash the Zellij themes' *_selected components and the phone's current row
36
+ # (--lanes-accent-wash) already use, with ink on it. Not a CSS variable of its own: the phone needs none, and a copy
37
+ # per variant would cost 58 B of the 37 B mobile.css still has under spec §8's cap.
38
+ BACKGROUNDS = {'selected': 'accent-wash'}
39
+ PAINTED = tuple(ANSI16) + tuple(BACKGROUNDS) # the roles the TUI ever emits
40
+ # The colour modes whose colours Lanes defines (cockpit v3 P2/P3): the sidebar spends the palette in them, and view.sh
41
+ # (`LZ_VIVID`) the warning tone on its lead. 16 is the user's own palette and none has no colour: both keep v1's bytes.
42
+ VIVID = ('truecolor', '256')
43
+ # The 256-colour set: one xterm index per painted role, always from 16-255 (fixed RGB; 0-15 are the user's palette).
44
+ # Each is the nearest entry (RGB distance) in its token's hue family — greys for the inks — that keeps every text role
45
+ # >= 4.2:1 on surface, surface-raised and the 256 `selected` band; `selected` is the nearest entry to the wash. The
46
+ # floor is 4.2, not 4.5, for the cube's six levels alone (test_tokens.Xterm256 re-derives every index). Light accent
47
+ # and done share 22 (#005f00): the nearest passing entry, 23, is a teal.
48
+ XTERM256 = {
49
+ 'dark': {'ink': 255, 'ink-dim': 249, 'ink-faint': 243, 'accent': 115, 'tone-urgent': 209, 'tone-attention': 215,
50
+ 'tone-working': 75, 'tone-done': 78, 'selected': 235},
51
+ 'light': {'ink': 233, 'ink-dim': 240, 'ink-faint': 243, 'accent': 22, 'tone-urgent': 160, 'tone-attention': 94,
52
+ 'tone-working': 26, 'tone-done': 22, 'selected': 254},
53
+ }
30
54
  # Lane-identity palette for the two places that DEFINE a palette: Zellij's `web_client { theme {} }` (the phone's
31
55
  # xterm.js; dark only, see _web_client) and the optional Ghostty file. A Zellij theme's legacy `black…white` keys are
32
- # NOT one of them (DEF-1413). The TUI itself keeps 38;5;N. Slots 1-4 are the four tones. Every slot >= 4.5:1.
56
+ # NOT one of them (DEF-1413). The TUI itself keeps 38;5;N. Slots 1-4 are the four tones and 7 / 15 the ink pair
57
+ # (PALETTE16_ROLES); v2 re-saturates the rest (more chroma than v1 in every chromatic slot). Every slot >= 4.5:1.
33
58
  PALETTE16 = {
34
- 'dark': ['#7d8896', '#f28b7d', '#6fd39a', '#e5b84a', '#7fb3f2', '#c9a6ff', '#6fd6d0', '#9ca7b5',
35
- '#8a95a3', '#ff9d96', '#8fe0a3', '#f7cf7f', '#a5cbff', '#dcc6ff', '#9ae6e0', '#edf1f5'],
36
- 'light': ['#3b4a42', '#b7322a', '#1b7142', '#8a5a00', '#2456a8', '#7a3fb8', '#0f7b7b', '#56616d',
37
- '#5f6d66', '#c2332a', '#1b7a38', '#8f5a00', '#2860c4', '#8a4dc8', '#0f7272', '#17241b'],
59
+ 'dark': ['#8792a0', '#ff7b6e', '#5fdc8a', '#ffc233', '#6fb6ff', '#c58bff', '#3fd9cf', '#a6b1be',
60
+ '#97a3b1', '#ff9a8e', '#86f0ad', '#ffd866', '#98cbff', '#dcb2ff', '#7deee6', '#f2f5f8'],
61
+ 'light': ['#2f3b34', '#c41c1c', '#0d7338', '#a04d00', '#1a56c4', '#8228c4', '#006f73', '#4a5661',
62
+ '#55625b', '#d0221a', '#0e7a3a', '#a85200', '#1f5fd8', '#9133d0', '#00797c', '#111a14'],
38
63
  }
64
+ PALETTE16_ROLES = {1: 'tone-urgent', 2: 'tone-done', 3: 'tone-attention', 4: 'tone-working', 7: 'ink-dim', 15: 'ink'}
39
65
  # key -> (unicode, ascii twin, tone). Position disambiguates: agent glyphs in column 2, stream glyphs in column 33.
40
66
  AGENT_GLYPHS = {'needs_approval': ('◆', '!', 'tone-urgent'), 'needs_input': ('◇', '?', 'tone-attention'),
41
67
  'working': ('▸', '>', 'tone-working'), 'done': ('✓', 'v', 'tone-done'),
@@ -52,11 +78,30 @@ MARKS = {'cursor': ('▌', '|', 'accent'), 'viewing': ('›', '=', 'accent'), 'v
52
78
  # Tone-less punctuation (spec §3 completeness rule): every ` · ` and every quote the sidebar draws is a lookup, so
53
79
  # LANES_GLYPHS=ascii yields a seven-bit frame. `sep` shares `-` with `rule` on purpose: neither carries state.
54
80
  'sep': ('·', '-', 'ink-dim'), 'quote-open': ('‘', "'", 'ink'), 'quote-close': ('’', "'", 'ink'),
55
- 'ok': ('✓', 'OK', 'tone-done'), 'fail': ('✗', 'X', 'tone-urgent')}
81
+ # The confirmation's dash (` › close ‘sh’ — y⏎ confirm · Esc cancel`, cockpit v3 S3/S6); `-` like `sep`.
82
+ 'dash': ('—', '-', 'ink-dim'),
83
+ # The focus legend's arrow (` ▌ keys go to sidebar · Esc → lane`, cockpit v3 S4).
84
+ 'arrow': ('→', '->', 'accent'),
85
+ # Column 3 of a row whose registration is on its way (`P`, cockpit v3 S8): the session stays listed, dimmed
86
+ # mark, until the registry lists its lane. The truncation pair on purpose — position disambiguates (column 3,
87
+ # never the end of a name) — and seven-bit `~` is free in column 3 (`=` `2` `3` are the other marks there).
88
+ 'pending': ('…', '~', 'ink-dim'),
89
+ # Column 3 of a lane its owner pinned (`p`, cockpit v3 R5): it leads its section and never falls below `⋮ N
90
+ # more`. The lowest of column 3's marks: pending, the viewports and the viewing mark win over it. Seven-bit
91
+ # `^` is free in column 3 (`=` `2` `3` `~` are the others; the upload strip's `^` is no column-3 mark).
92
+ 'pinned': ('▲', '^', 'accent'),
93
+ 'ok': ('✓', 'OK', 'tone-done'), 'fail': ('✗', 'X', 'tone-urgent'),
94
+ # The docs cockpit on the upload strip (` ↑ uploads · ▤ docs 3 (U)`, plan W10 W7): the strip's own tone.
95
+ # Seven-bit `#` is free on that row (`^` uploads, `v` armed, `-` sep are the others).
96
+ 'docs': ('▤', '#', 'ink-dim')}
56
97
  ECONOMY_TAIL = {'starving': ('⚠', '!', 'tone-attention'), 'downgraded': ('↓', 'v', 'tone-attention'), 'paid': ('$', '$', 'ink-dim')}
57
98
  MOBILE_AGENT_LABELS = {'needs_approval': 'Needs approval', 'needs_input': 'Needs you', 'working': 'Working',
58
99
  'done': 'Done', 'errored': 'Error', 'shell': 'Shell', 'idle': 'Idle'}
59
- PAINTED = tuple(ANSI16) # the roles the TUI ever emits
100
+
101
+
102
+ def hex_of(variant, role):
103
+ """The colour a role paints: its own token, or for a background role the token it takes (`selected` -> wash)."""
104
+ return TOKENS[variant][BACKGROUNDS.get(role, role)]
60
105
 
61
106
 
62
107
  def luminance(hexval):
@@ -74,17 +119,24 @@ def rgb(hexval):
74
119
  return tuple(int(hexval[i:i + 2], 16) for i in (1, 3, 5))
75
120
 
76
121
 
77
- def sgr_table(mode, variant):
78
- """Complete escape per painted role. `mode` truecolor|16|none; `variant` dark|light. Resolved ONCE per process."""
122
+ def sgr_table(mode, variant, dim16=None):
123
+ """Complete escape per painted role. `mode` truecolor|256|16|none; `variant` dark|light. Resolved ONCE per process.
124
+ Foreground roles are `38;…`, `selected` (the cursor row) `48;…`; in 16/none it is empty — the palette there is the
125
+ user's, and the cursor stays the bold bar. `dim16` overrides the 16-colour dim (default: `LANES_DIM=2` -> SGR 2,
126
+ else 90); the shell renderer pins 90 so the committed block never depends on the generator's environment."""
79
127
  table = {'bold': '\033[1m', 'reset': '\033[0m'}
80
- dim16 = '\033[2m' if os.environ.get('LANES_DIM') == '2' else '\033[90m'
128
+ if dim16 is None:
129
+ dim16 = '\033[2m' if os.environ.get('LANES_DIM') == '2' else '\033[90m'
81
130
  for role in PAINTED:
131
+ bold, layer = '\033[1m' if role == 'accent' else '', 48 if role in BACKGROUNDS else 38
82
132
  if mode == 'none':
83
- table[role] = '\033[1m' if role == 'accent' else ''
133
+ table[role] = bold
84
134
  elif mode == '16':
85
- table[role] = dim16 if role in ('ink-dim', 'ink-faint') else f'\033[{ANSI16[role]}m'
135
+ table[role] = '' if role in BACKGROUNDS else dim16 if role in ('ink-dim', 'ink-faint') else f'\033[{ANSI16[role]}m'
136
+ elif mode == '256':
137
+ table[role] = bold + '\033[%d;5;%dm' % (layer, XTERM256[variant][role])
86
138
  else:
87
- table[role] = ('\033[1m' if role == 'accent' else '') + '\033[38;2;%d;%d;%dm' % rgb(TOKENS[variant][role])
139
+ table[role] = bold + '\033[%d;2;%d;%d;%dm' % ((layer,) + rgb(hex_of(variant, role)))
88
140
  return table
89
141
 
90
142
 
@@ -194,18 +246,77 @@ def render_css():
194
246
  return '\n'.join(out) + '\n'
195
247
 
196
248
 
249
+ # The docs page (plan W10 W6, LNS-107; web/docs.css) is its own HTML document, served by lanes-docsd, not the phone
250
+ # shell: no html.lanes-mobile, no Zellij data-zj-theme. Its block puts the roles it paints on :root, dark by default and
251
+ # light under the system's prefers-color-scheme, and only those roles (docs.css has a 2.5 KB cap; the full --css block
252
+ # alone is 2 KB). test_docs_assets pins that docs.css uses exactly these.
253
+ DOCS_ROLES = ('surface', 'surface-raised', 'line', 'ink', 'ink-dim', 'accent')
254
+
255
+
256
+ def render_docs_css():
257
+ out = ['/* BEGIN lanes tokens (generated by libexec/tokens.py --docs-css; do not edit by hand) */', ':root {']
258
+ out += [f' --lanes-{role}: {TOKENS["dark"][role]};' for role in DOCS_ROLES]
259
+ out += [' color-scheme: dark;', '}', '@media (prefers-color-scheme: light) {', ' :root {']
260
+ out += [f' --lanes-{role}: {TOKENS["light"][role]};' for role in DOCS_ROLES]
261
+ out += [' color-scheme: light;', ' }', '}', '/* END lanes tokens */']
262
+ return '\n'.join(out) + '\n'
263
+
264
+
265
+ # The painted roles as the viewer's shell variables (`tokens.sh`), in PAINTED order. LZ_DIM kept its v1 name.
266
+ SH_NAMES = {'ink': 'LZ_INK', 'ink-dim': 'LZ_DIM', 'ink-faint': 'LZ_FAINT', 'accent': 'LZ_ACCENT',
267
+ 'tone-urgent': 'LZ_URGENT', 'tone-attention': 'LZ_ATTENTION', 'tone-working': 'LZ_WORKING',
268
+ 'tone-done': 'LZ_DONE', 'selected': 'LZ_SELECTED'}
269
+ SH_SETS = (('256', 'dark'), ('256', 'light'), ('truecolor', 'dark'), ('truecolor', 'light'))
270
+
271
+
272
+ def _sh_set(mode, variant, indent):
273
+ """One set's assignments: every painted role, then `LZ_VIVID` (`1` in a VIVID mode, else empty) — what view.sh asks
274
+ instead of re-matching the mode words itself (cockpit v3 P3)."""
275
+ table = sgr_table(mode, variant, dim16='\033[90m')
276
+ lines = [indent + "%s='%s'" % (SH_NAMES[role], table[role].replace('\033', '\\033')) for role in PAINTED]
277
+ return lines + [indent + "LZ_VIVID='%s'" % ('1' if mode in VIVID else '')]
278
+
279
+
197
280
  def render_sh():
198
- return ('# BEGIN lanes tokens (generated by libexec/tokens.py --sh; do not edit by hand)\n'
199
- "LZ_BOLD='\\033[1m'\nLZ_DIM='\\033[90m'\n"
200
- f"LZ_URGENT='\\033[{ANSI16['tone-urgent']}m'\nLZ_ATTENTION='\\033[{ANSI16['tone-attention']}m'\n"
201
- f"LZ_WORKING='\\033[{ANSI16['tone-working']}m'\nLZ_DONE='\\033[{ANSI16['tone-done']}m'\nLZ_RESET='\\033[0m'\n"
202
- '# END lanes tokens\n')
281
+ """The viewer's colour sets as POSIX-shell literals holding `\\033` as text (they go into printf's FORMAT). A plain
282
+ `. tokens.sh` assigns the 16-colour set — the user's own palette; `lz_tones MODE VARIANT` re-points every LZ_* tone
283
+ variable to the 256 / truecolor set of dark / light, `lz_tones none` (one argument) to none, and anything else —
284
+ `lz_tones 16`, no argument, an unknown mode or variant — back to 16 colours. Every set also says `LZ_VIVID` (`1`
285
+ in truecolor and 256). The `case` word reads `${1:-}:${2:-}`: view.sh runs `set -u`, where a bare `$2` aborts the
286
+ shell on a one-argument call (DEF-1772). Assignments and one `case` only: sourcing or calling it forks nothing
287
+ (cockpit v3 P1). view.sh calls `lz_tones "$color" "$theme"` with its device's `.color`/`.theme` stamps on every
288
+ status screen (cockpit v3 P3)."""
289
+ lines = ['# BEGIN lanes tokens (generated by libexec/tokens.py --sh; do not edit by hand)',
290
+ "LZ_BOLD='\\033[1m'", "LZ_RESET='\\033[0m'"]
291
+ lines += _sh_set('16', 'dark', '')
292
+ lines += ['lz_tones() {', ' case "${1:-}:${2:-}" in']
293
+ for mode, variant in SH_SETS:
294
+ lines += [f' {mode}:{variant})'] + _sh_set(mode, variant, ' ') + [' ;;']
295
+ lines += [' none:*)'] + _sh_set('none', 'dark', ' ') + [' ;;']
296
+ lines += [' *)'] + _sh_set('16', 'dark', ' ') + [' ;;']
297
+ lines += [' esac', '}', '# END lanes tokens']
298
+ return '\n'.join(lines) + '\n'
299
+
300
+
301
+ def _md_role(role):
302
+ """One row of the role table: hex per variant, the 256 index per variant, the 16-colour SGR, the CSS variable."""
303
+ painted = role in PAINTED
304
+ x256 = [f'`{XTERM256[v][role]}`' if painted else '`—`' for v in ('dark', 'light')]
305
+ ansi = '—' if role in BACKGROUNDS else ANSI16.get(role, '—')
306
+ return (f'| {role} | `{hex_of("dark", role)}` | `{hex_of("light", role)}` | {x256[0]} | {x256[1]} | `{ansi}` | '
307
+ f'`--lanes-{BACKGROUNDS.get(role, role)}` |')
203
308
 
204
309
 
205
310
  def render_md():
206
311
  rows = ['<!-- BEGIN lanes tokens (generated by libexec/tokens.py --md; do not edit by hand) -->',
207
- '| Role | Dark | Light | ANSI-16 | CSS variable |', '|---|---|---|---|---|']
208
- rows += [f'| {r} | `{TOKENS["dark"][r]}` | `{TOKENS["light"][r]}` | `{ANSI16.get(r, "—")}` | `--lanes-{r}` |' for r in ROLES]
312
+ '| Role | Dark | Light | 256 dark | 256 light | ANSI-16 | CSS variable |', '|---|---|---|---|---|---|---|']
313
+ for role in ROLES:
314
+ rows.append(_md_role(role))
315
+ rows += [_md_role(bg) for bg, source in BACKGROUNDS.items() if source == role] # `selected` under its wash
316
+ rows += ['', '| Slot | ANSI | Dark | Light | Role |', '|---|---|---|---|---|']
317
+ names = ANSI_NAMES + tuple('bright ' + n for n in ANSI_NAMES)
318
+ rows += [f'| {i} | {names[i]} | `{PALETTE16["dark"][i]}` | `{PALETTE16["light"][i]}` | {PALETTE16_ROLES.get(i, "—")} |'
319
+ for i in range(16)]
209
320
  rows += ['', '| Glyph | Codepoint | ASCII | Tone | Meaning |', '|---|---|---|---|---|']
210
321
  for name, table in (('agent', AGENT_GLYPHS), ('stream', GLYPHS), ('mark', MARKS), ('economy', ECONOMY_TAIL)):
211
322
  for key, (glyph, ascii_twin, tone) in table.items():
@@ -242,14 +353,16 @@ def render_ghostty(variant):
242
353
 
243
354
 
244
355
  RENDERERS = {'kdl': render_kdl, 'css': render_css, 'sh': render_sh, 'md': render_md, 'js': render_js,
245
- 'ghostty-dark': lambda: render_ghostty('dark'), 'ghostty-light': lambda: render_ghostty('light')}
356
+ 'ghostty-dark': lambda: render_ghostty('dark'), 'ghostty-light': lambda: render_ghostty('light'),
357
+ 'docs-css': render_docs_css}
246
358
  TARGETS = {'kdl': 'zellij/config.kdl', 'css': 'web/mobile.css', 'sh': 'libexec/tokens.sh', 'md': 'docs/DESIGN.md',
247
359
  'js': 'web/mobile.js', 'ghostty-dark': 'client/ghostty/lanes-palette.conf',
248
- 'ghostty-light': 'client/ghostty/lanes-palette-light.conf'}
360
+ 'ghostty-light': 'client/ghostty/lanes-palette-light.conf', 'docs-css': 'web/docs.css'}
249
361
  MARKERS = {'kdl': ('// BEGIN lanes tokens', '// END lanes tokens'), 'css': ('/* BEGIN lanes tokens', '/* END lanes tokens */'),
250
362
  'sh': ('# BEGIN lanes tokens', '# END lanes tokens'), 'md': ('<!-- BEGIN lanes tokens', '<!-- END lanes tokens -->'),
251
363
  'js': ('// BEGIN lanes tokens', '// END lanes tokens'),
252
- 'ghostty-dark': ('# BEGIN lanes tokens', '# END lanes tokens'), 'ghostty-light': ('# BEGIN lanes tokens', '# END lanes tokens')}
364
+ 'ghostty-dark': ('# BEGIN lanes tokens', '# END lanes tokens'), 'ghostty-light': ('# BEGIN lanes tokens', '# END lanes tokens'),
365
+ 'docs-css': ('/* BEGIN lanes tokens', '/* END lanes tokens */')}
253
366
 
254
367
 
255
368
  def render(kind):
@@ -301,7 +414,7 @@ def main(argv):
301
414
  return 0
302
415
  kind = argv[0].lstrip('-') if argv else ''
303
416
  if kind not in RENDERERS:
304
- sys.exit('usage: tokens.py --kdl|--css|--sh|--md|--js|--ghostty-dark|--ghostty-light|--check|--write')
417
+ sys.exit('usage: tokens.py --kdl|--css|--sh|--md|--js|--ghostty-dark|--ghostty-light|--docs-css|--check|--write')
305
418
  sys.stdout.write(render(kind))
306
419
  return 0
307
420
 
@@ -1,11 +1,95 @@
1
- # Lanes 16-colour SGR literals for view.sh status screens. Sourced, never executed.
1
+ # Lanes SGR literals for view.sh status screens: the 16-colour set on sourcing, and `lz_tones MODE VARIANT` for the
2
+ # 256-colour, truecolor and none sets (`lz_tones none` takes one argument; safe under view.sh's `set -u`). view.sh calls
3
+ # `lz_tones "$color" "$theme"` with its device's clients/<device>.color and .theme stamps on every status screen; every
4
+ # set also says LZ_VIVID (`1` in truecolor and 256, the colours Lanes defines). Sourced, never executed.
2
5
  # The block below is generated: edit libexec/tokens.py and run `python3 libexec/tokens.py --write`.
3
6
  # BEGIN lanes tokens (generated by libexec/tokens.py --sh; do not edit by hand)
4
7
  LZ_BOLD='\033[1m'
8
+ LZ_RESET='\033[0m'
9
+ LZ_INK='\033[39m'
5
10
  LZ_DIM='\033[90m'
11
+ LZ_FAINT='\033[90m'
12
+ LZ_ACCENT='\033[1;39m'
6
13
  LZ_URGENT='\033[31m'
7
14
  LZ_ATTENTION='\033[33m'
8
15
  LZ_WORKING='\033[34m'
9
16
  LZ_DONE='\033[32m'
10
- LZ_RESET='\033[0m'
17
+ LZ_SELECTED=''
18
+ LZ_VIVID=''
19
+ lz_tones() {
20
+ case "${1:-}:${2:-}" in
21
+ 256:dark)
22
+ LZ_INK='\033[38;5;255m'
23
+ LZ_DIM='\033[38;5;249m'
24
+ LZ_FAINT='\033[38;5;243m'
25
+ LZ_ACCENT='\033[1m\033[38;5;115m'
26
+ LZ_URGENT='\033[38;5;209m'
27
+ LZ_ATTENTION='\033[38;5;215m'
28
+ LZ_WORKING='\033[38;5;75m'
29
+ LZ_DONE='\033[38;5;78m'
30
+ LZ_SELECTED='\033[48;5;235m'
31
+ LZ_VIVID='1'
32
+ ;;
33
+ 256:light)
34
+ LZ_INK='\033[38;5;233m'
35
+ LZ_DIM='\033[38;5;240m'
36
+ LZ_FAINT='\033[38;5;243m'
37
+ LZ_ACCENT='\033[1m\033[38;5;22m'
38
+ LZ_URGENT='\033[38;5;160m'
39
+ LZ_ATTENTION='\033[38;5;94m'
40
+ LZ_WORKING='\033[38;5;26m'
41
+ LZ_DONE='\033[38;5;22m'
42
+ LZ_SELECTED='\033[48;5;254m'
43
+ LZ_VIVID='1'
44
+ ;;
45
+ truecolor:dark)
46
+ LZ_INK='\033[38;2;242;245;248m'
47
+ LZ_DIM='\033[38;2;166;177;190m'
48
+ LZ_FAINT='\033[38;2;111;122;135m'
49
+ LZ_ACCENT='\033[1m\033[38;2;143;227;166m'
50
+ LZ_URGENT='\033[38;2;255;123;110m'
51
+ LZ_ATTENTION='\033[38;2;255;194;51m'
52
+ LZ_WORKING='\033[38;2;111;182;255m'
53
+ LZ_DONE='\033[38;2;95;220;138m'
54
+ LZ_SELECTED='\033[48;2;28;51;38m'
55
+ LZ_VIVID='1'
56
+ ;;
57
+ truecolor:light)
58
+ LZ_INK='\033[38;2;17;26;20m'
59
+ LZ_DIM='\033[38;2;74;86;97m'
60
+ LZ_FAINT='\033[38;2;111;123;135m'
61
+ LZ_ACCENT='\033[1m\033[38;2;31;111;63m'
62
+ LZ_URGENT='\033[38;2;196;28;28m'
63
+ LZ_ATTENTION='\033[38;2;160;77;0m'
64
+ LZ_WORKING='\033[38;2;26;86;196m'
65
+ LZ_DONE='\033[38;2;13;115;56m'
66
+ LZ_SELECTED='\033[48;2;229;240;230m'
67
+ LZ_VIVID='1'
68
+ ;;
69
+ none:*)
70
+ LZ_INK=''
71
+ LZ_DIM=''
72
+ LZ_FAINT=''
73
+ LZ_ACCENT='\033[1m'
74
+ LZ_URGENT=''
75
+ LZ_ATTENTION=''
76
+ LZ_WORKING=''
77
+ LZ_DONE=''
78
+ LZ_SELECTED=''
79
+ LZ_VIVID=''
80
+ ;;
81
+ *)
82
+ LZ_INK='\033[39m'
83
+ LZ_DIM='\033[90m'
84
+ LZ_FAINT='\033[90m'
85
+ LZ_ACCENT='\033[1;39m'
86
+ LZ_URGENT='\033[31m'
87
+ LZ_ATTENTION='\033[33m'
88
+ LZ_WORKING='\033[34m'
89
+ LZ_DONE='\033[32m'
90
+ LZ_SELECTED=''
91
+ LZ_VIVID=''
92
+ ;;
93
+ esac
94
+ }
11
95
  # END lanes tokens