@vibelord/lanes 1.1.1 → 1.2.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.
- package/README.md +36 -5
- package/bin/lanes-install.mjs +166 -13
- package/lib/plan.mjs +113 -3
- package/package.json +1 -1
- package/payload/lanes/README.md +45 -22
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +2459 -164
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +19 -19
- package/payload/lanes/client/ghostty/lanes-palette.conf +19 -19
- package/payload/lanes/client/lanes-launch +289 -40
- package/payload/lanes/client/lanes-window +15 -3
- package/payload/lanes/container/hooks/claude-settings.json +1 -0
- package/payload/lanes/container/lane +106 -92
- package/payload/lanes/container/lane-edit +100 -0
- package/payload/lanes/container/lane-mcp +1091 -0
- package/payload/lanes/container/lane-signal +109 -29
- package/payload/lanes/container/lane_core.py +1140 -0
- package/payload/lanes/container/vimrc +31 -0
- package/payload/lanes/docs/ARCHITECTURE.md +618 -13
- package/payload/lanes/docs/DESIGN.md +122 -44
- package/payload/lanes/docs/INSTALL.md +157 -17
- package/payload/lanes/docs/RUNBOOK.md +603 -72
- package/payload/lanes/docs/SHARING.md +3 -2
- package/payload/lanes/docs/USERGUIDE.md +777 -136
- package/payload/lanes/install/enable-web.sh +2 -0
- package/payload/lanes/install/host-install.sh +163 -7
- package/payload/lanes/install/services.py +148 -18
- package/payload/lanes/install.sh +68 -7
- package/payload/lanes/libexec/agent_state.py +74 -1
- package/payload/lanes/libexec/cursor_sync.py +6 -0
- package/payload/lanes/libexec/device_sync.py +47 -10
- package/payload/lanes/libexec/economy_poll.py +1 -0
- package/payload/lanes/libexec/enrold.py +41 -22
- package/payload/lanes/libexec/keys.py +54 -23
- package/payload/lanes/libexec/lanes_lib.py +620 -22
- package/payload/lanes/libexec/members.py +16 -2
- package/payload/lanes/libexec/notify.py +104 -24
- package/payload/lanes/libexec/registrations.py +216 -18
- package/payload/lanes/libexec/session_actions.py +182 -14
- package/payload/lanes/libexec/sidebar +11 -3
- package/payload/lanes/libexec/sidebar.py +2135 -361
- package/payload/lanes/libexec/statusd.py +137 -10
- package/payload/lanes/libexec/studio_host.py +8 -1
- package/payload/lanes/libexec/tokens.py +121 -30
- package/payload/lanes/libexec/tokens.sh +86 -2
- package/payload/lanes/libexec/update.py +972 -0
- package/payload/lanes/libexec/users.py +614 -0
- package/payload/lanes/libexec/view.sh +34 -6
- package/payload/lanes/systemd/lanes-ensure.service +3 -0
- package/payload/lanes/systemd/lanes-gc.service +1 -1
- package/payload/lanes/systemd/lanes-update.service +17 -0
- package/payload/lanes/systemd/lanes-update.timer +17 -0
- package/payload/lanes/web/README.md +83 -24
- package/payload/lanes/web/browser.js +3 -5
- package/payload/lanes/web/mobile.css +17 -17
- package/payload/lanes/web/mobile.js +44 -32
- package/payload/lanes/workspaces.example/README.md +3 -1
- package/payload/lanes/zellij/config.kdl +106 -56
- package/payload/manifest.json +5 -3
|
@@ -6,7 +6,8 @@ dead sockets are never probed), run one read-only `list-panes -a` per live serve
|
|
|
6
6
|
at most PROBE_WORKERS at once, the whole round cut at PROBE_TIMEOUT + PROBE_GRACE seconds, the servers the last
|
|
7
7
|
round cut started first — and publish ~/.local/state/lanes/status.json
|
|
8
8
|
atomically, plus the one-line ~/.local/state/lanes/health. The sidebar, `lanes status`, `lanes doctor` and the
|
|
9
|
-
future Throttle web harness all read those files.
|
|
9
|
+
future Throttle web harness all read those files. Each workspace container that has the lane contract also gets its
|
|
10
|
+
own slice as ~/.lz/status.json (LNS-32, `write_container_status`), for readers inside it.
|
|
10
11
|
|
|
11
12
|
It never INVENTS a state. "No answer" (a tmux, cgroup, docker or zmx read that timed out or failed on an overloaded
|
|
12
13
|
host) is not an observation: the lane keeps its last published state, marked stale, for at most STALE_MAX seconds
|
|
@@ -39,6 +40,7 @@ PROBE_WORKERS = 8
|
|
|
39
40
|
PROBE_GRACE = 2
|
|
40
41
|
CONTAINER_REFRESH = 300 # docker is asked again this often as a safety net; liveness itself comes from /proc/<pid>
|
|
41
42
|
CONTAINER_RETRY = 30 # ... and at most this often while a container is down or docker did not answer
|
|
43
|
+
HOT_SYNC_EVERY_S = 10 # hot_sync starts at most one `lanes sync` per this many seconds (the sidebar's PENDING_S)
|
|
42
44
|
ACTIVE_WINDOW = 20 # seconds since last output for a lane to count as active
|
|
43
45
|
STALE_MAX = 300 # a lane nobody could observe for this long is published as 'unknown', no longer as last seen
|
|
44
46
|
FIELDS = ['session_name', 'pane_pid', 'pane_dead', 'window_activity', 'pane_current_command',
|
|
@@ -236,8 +238,9 @@ def keep(previous, lane_id, outcome):
|
|
|
236
238
|
def lane_entry(pane, now, activity=None, viewing=(), agent=None):
|
|
237
239
|
"""One live session's published row. `activity` is the NEWEST `window_activity` across the session's panes
|
|
238
240
|
(see `newest_activity`) and `viewing` the rows of the sessions that VIEW it (see `split_viewers`); both
|
|
239
|
-
default to "this pane alone", which is what a caller with a single row has. `agent` is
|
|
240
|
-
|
|
241
|
+
default to "this pane alone", which is what a caller with a single row has. `agent` is the lane's settled
|
|
242
|
+
agent{} (`settled`: `agent_state.merge`'s verdict debounced by `agent_state.settle`): published as the additive
|
|
243
|
+
`agent` key, absent when no signal names an agent."""
|
|
241
244
|
if activity is None:
|
|
242
245
|
try:
|
|
243
246
|
activity = int(pane['window_activity'])
|
|
@@ -267,6 +270,15 @@ def lane_entry(pane, now, activity=None, viewing=(), agent=None):
|
|
|
267
270
|
return entry
|
|
268
271
|
|
|
269
272
|
|
|
273
|
+
def settled(previous, lane_id, now, raw):
|
|
274
|
+
"""What a live lane publishes as its agent{} this cycle: `merge`'s verdict `raw` debounced against the agent{} the
|
|
275
|
+
PREVIOUS payload published for the same lane id (`agent_state.settle`, cockpit v3 S1). `previous` is the last
|
|
276
|
+
published payload — `new_context()` seeds it from status.json, so a restarted daemon keeps every lane's
|
|
277
|
+
`stable_since`. Called at both merge sites in `observe`: declared lanes and undeclared sessions alike."""
|
|
278
|
+
old = ((previous.get('lanes') or {}).get(lane_id) or {}) if isinstance(previous, dict) else {}
|
|
279
|
+
return AS.settle(old.get('agent') if isinstance(old, dict) else None, raw, now)
|
|
280
|
+
|
|
281
|
+
|
|
270
282
|
def agent_signals(containers, sessions_by_server):
|
|
271
283
|
"""The per-container inputs `agent_state.merge` needs, read ONCE per container per cycle through the
|
|
272
284
|
container's root as the host sees it (`/proc/<init pid>/root`), so liveness is judged in the container's pid
|
|
@@ -546,6 +558,11 @@ def observe(registry, containers, live, previous=None, report=None):
|
|
|
546
558
|
try:
|
|
547
559
|
pane = own_sessions[name]
|
|
548
560
|
old_ws, old = prior.get((container, socket, name), (None, None))
|
|
561
|
+
if old is None:
|
|
562
|
+
# Listed for the first time: maybe a session `registrations.unregister` just released. It had
|
|
563
|
+
# no listing to carry its id over (it was a lane), and its directory may name another board
|
|
564
|
+
# now — the hand-back keeps the board and id it was registered under (S7 fix, DEF-1826).
|
|
565
|
+
old_ws, old = A.handed_back(container, socket, name, pane)
|
|
549
566
|
board_ids = {w['id'] for w in registry['workspaces']}
|
|
550
567
|
workspace_id = A.workspace_for(registry, container, socket, pane['pane_current_path'],
|
|
551
568
|
owners.get((container, socket), set()),
|
|
@@ -586,8 +603,9 @@ def observe(registry, containers, live, previous=None, report=None):
|
|
|
586
603
|
if not L.TMUX_SESSION.match(name):
|
|
587
604
|
entry['note'] = 'session name outside the lane declaration grammar'
|
|
588
605
|
found = lane_entry(pane, now, activities.get(name), viewers.get(name, []),
|
|
589
|
-
|
|
590
|
-
|
|
606
|
+
settled(previous, entry['id'], now,
|
|
607
|
+
AS.merge(pane, claude.get(container, {}),
|
|
608
|
+
sidecars.get(container, {}).get(entry['id']))))
|
|
591
609
|
except Exception as error:
|
|
592
610
|
log(f'observe {container}/{socket}/{name}', error)
|
|
593
611
|
continue
|
|
@@ -606,8 +624,9 @@ def observe(registry, containers, live, previous=None, report=None):
|
|
|
606
624
|
else:
|
|
607
625
|
try:
|
|
608
626
|
lanes[lane['id']] = lane_entry(pane, now, activities.get(session), viewers.get(session, []),
|
|
609
|
-
|
|
610
|
-
|
|
627
|
+
settled(previous, lane['id'], now,
|
|
628
|
+
AS.merge(pane, claude.get(container, {}),
|
|
629
|
+
sidecars.get(container, {}).get(lane['id']))))
|
|
611
630
|
except Exception as error: # a torn row: the session is there, its details are not readable
|
|
612
631
|
log(f"observe {lane['id']}", error)
|
|
613
632
|
lanes[lane['id']] = keep(previous, lane['id'], 'error')
|
|
@@ -909,7 +928,110 @@ def new_context():
|
|
|
909
928
|
for entry in (seed.get('lanes') or {}).values():
|
|
910
929
|
entry.setdefault('stale_since', int(written))
|
|
911
930
|
return {'registry': None, 'stamp': 0, 'containers': {}, 'containers_at': 0.0,
|
|
912
|
-
'payload': seed, 'published': None, 'status_at': written, 'cut': 0, 'unwalkable': []
|
|
931
|
+
'payload': seed, 'published': None, 'status_at': written, 'cut': 0, 'unwalkable': [],
|
|
932
|
+
'container_status': {}, 'container_status_errors': {}}
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
# ------------------------------------------- the in-container projection (LNS-32)
|
|
936
|
+
# `~/.lz/status.json` in each workspace container: that container's slice of the payload, for the in-container Agent
|
|
937
|
+
# API (`lane-mcp`, plan Track M), which cannot read the host's status.json. v1 = {v, at, lanes{<id>: {state, agent?,
|
|
938
|
+
# attached?, viewers?, activity?, size?, stale?}}, unregistered[{id, name, workspace, boards, tmux, identity}]}.
|
|
939
|
+
CONTAINER_STATUS_FIELDS = ('state', 'agent', 'attached', 'viewers', 'activity', 'size', 'stale')
|
|
940
|
+
CONTAINER_STATUS_DIR = tuple(L.CONTAINER_HOME.strip('/').split('/')) + ('.lz',)
|
|
941
|
+
CONTAINER_STATUS_FILE = 'status.json'
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def container_status(registry, payload):
|
|
945
|
+
"""{container: {'lanes': …, 'unregistered': […]}} — what each workspace container may read about ITS OWN lanes
|
|
946
|
+
(pure). `lanes` holds every lane the registry places in the container (declared and registered: the ids its
|
|
947
|
+
`~/.lz/registry.json` holds) and every unregistered session statusd found there, each reduced to the fields a
|
|
948
|
+
reader in the container needs; a row a cycle has not observed yet is `unknown`, never invented. The host-side
|
|
949
|
+
details (cwd, pid, tty, command, where an unanswered probe failed) stay on the host, and `stale` is carried so a
|
|
950
|
+
kept row is never read as a fresh one. A session listed under several boards is ONE `unregistered` row naming
|
|
951
|
+
them all in `boards`. zmx lanes are not projected: the container's own `lane ls`/`lane status` read them from
|
|
952
|
+
zmx and lane-run directly, and their `agent` is lane-run's phase record, not an `agent_state` verdict."""
|
|
953
|
+
rows = (payload or {}).get('lanes') or {}
|
|
954
|
+
ids = {}
|
|
955
|
+
for workspace in (registry or {}).get('workspaces') or []:
|
|
956
|
+
for lane in workspace.get('lanes') or []:
|
|
957
|
+
if lane.get('container') and lane.get('id'):
|
|
958
|
+
ids.setdefault(lane['container'], []).append(lane['id'])
|
|
959
|
+
ghosts = {}
|
|
960
|
+
for board, entries in sorted(((payload or {}).get('unregistered') or {}).items()):
|
|
961
|
+
for entry in entries or []:
|
|
962
|
+
if not isinstance(entry, dict) or not entry.get('container') or not entry.get('id'):
|
|
963
|
+
continue
|
|
964
|
+
ghost = ghosts.setdefault(entry['container'], {}).setdefault(entry['id'], {
|
|
965
|
+
'id': entry['id'], 'name': entry.get('name'), 'workspace': entry.get('workspace') or board,
|
|
966
|
+
'boards': [], 'tmux': entry.get('tmux'), 'identity': entry.get('identity')})
|
|
967
|
+
if board not in ghost['boards']:
|
|
968
|
+
ghost['boards'].append(board)
|
|
969
|
+
out = {}
|
|
970
|
+
for container in sorted(set(ids) | set(ghosts)):
|
|
971
|
+
found = ghosts.get(container, {})
|
|
972
|
+
lanes = {}
|
|
973
|
+
for lane_id in ids.get(container, []) + sorted(found):
|
|
974
|
+
row = rows.get(lane_id) if isinstance(rows.get(lane_id), dict) else {}
|
|
975
|
+
lanes[lane_id] = {key: row[key] for key in CONTAINER_STATUS_FIELDS if key in row}
|
|
976
|
+
lanes[lane_id].setdefault('state', 'unknown')
|
|
977
|
+
out[container] = {'lanes': lanes, 'unregistered': [found[lane_id] for lane_id in sorted(found)]}
|
|
978
|
+
return out
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def write_container_status(ctx, payload, now=None):
|
|
982
|
+
"""Write `container_status` into each running workspace container that has the lane contract (`~/.lz/bin/lane`,
|
|
983
|
+
the rule `lanes_lib.export_container_registries` follows) -> the containers whose file was written.
|
|
984
|
+
|
|
985
|
+
At most one small write per container per cycle, and none while that container's slice is unchanged: `at` is
|
|
986
|
+
the time of the last write, renewed on a change or once HEARTBEAT has passed, so an unchanged slice renders the
|
|
987
|
+
same bytes and `atomic_write_if_changed_at` skips it, while a reader can still tell a live statusd from a dead
|
|
988
|
+
one by the age of `at`. Written through /proc/<init pid>/root like every other read of the container here, into
|
|
989
|
+
a directory the container owns: `lanes_lib.open_real_dir` refuses a symlinked component and the write replaces
|
|
990
|
+
whatever is at the name without following or reading it (a planted link or FIFO). A container without the
|
|
991
|
+
contract is skipped silently; any other failure is logged once per distinct error and costs only that container."""
|
|
992
|
+
now = time.time() if now is None else now
|
|
993
|
+
last, errors = ctx.setdefault('container_status', {}), ctx.setdefault('container_status_errors', {})
|
|
994
|
+
written = []
|
|
995
|
+
slices = container_status(ctx.get('registry'), payload)
|
|
996
|
+
for gone in set(last) - set(slices):
|
|
997
|
+
del last[gone]
|
|
998
|
+
for container, content in slices.items():
|
|
999
|
+
info = (ctx.get('containers') or {}).get(container) or {}
|
|
1000
|
+
if not info.get('pid') or not info.get('running'):
|
|
1001
|
+
continue
|
|
1002
|
+
before = last.get(container)
|
|
1003
|
+
fresh = before and before['content'] == content and now - before['at'] <= HEARTBEAT
|
|
1004
|
+
at = before['at'] if fresh else now
|
|
1005
|
+
text = json.dumps({'v': 1, 'at': at, **content}, ensure_ascii=False, indent=1) + '\n'
|
|
1006
|
+
try:
|
|
1007
|
+
fd = L.open_real_dir(Path(L.proc_root()) / str(info['pid']) / 'root', CONTAINER_STATUS_DIR)
|
|
1008
|
+
except FileNotFoundError:
|
|
1009
|
+
continue # no ~/.lz: the lane contract is not installed there
|
|
1010
|
+
except (OSError, L.LanesError) as error:
|
|
1011
|
+
_container_status_error(errors, container, error)
|
|
1012
|
+
continue
|
|
1013
|
+
try:
|
|
1014
|
+
try:
|
|
1015
|
+
os.stat('bin/lane', dir_fd=fd)
|
|
1016
|
+
except FileNotFoundError:
|
|
1017
|
+
continue # a ~/.lz without `lane`: not installed either
|
|
1018
|
+
if L.atomic_write_if_changed_at(fd, CONTAINER_STATUS_FILE, text, mode=0o644):
|
|
1019
|
+
written.append(container)
|
|
1020
|
+
last[container] = {'content': content, 'at': at}
|
|
1021
|
+
errors.pop(container, None)
|
|
1022
|
+
except (OSError, L.LanesError) as error:
|
|
1023
|
+
_container_status_error(errors, container, error)
|
|
1024
|
+
finally:
|
|
1025
|
+
os.close(fd)
|
|
1026
|
+
return written
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def _container_status_error(errors, container, error):
|
|
1030
|
+
"""Log a container's projection failure once per distinct error, not every INTERVAL."""
|
|
1031
|
+
message = f'{type(error).__name__}: {error}'
|
|
1032
|
+
if errors.get(container) != message:
|
|
1033
|
+
errors[container] = message
|
|
1034
|
+
log(f'container status {container}', error)
|
|
913
1035
|
|
|
914
1036
|
|
|
915
1037
|
def cycle(ctx, reconcile=True):
|
|
@@ -938,6 +1060,10 @@ def cycle(ctx, reconcile=True):
|
|
|
938
1060
|
ctx['published'], ctx['status_at'] = payload, written
|
|
939
1061
|
except Exception as error:
|
|
940
1062
|
log('status.json', error)
|
|
1063
|
+
try: # LNS-32: each container's own slice, into its ~/.lz (one small write per container at most)
|
|
1064
|
+
write_container_status(ctx, payload)
|
|
1065
|
+
except Exception as error:
|
|
1066
|
+
log('container status', error)
|
|
941
1067
|
try: # "servers alive, sockets deleted" as a flag file: view.sh, the sidebar and doctor read it without parsing
|
|
942
1068
|
lost = sorted(key for key, server in ((payload or {}).get('servers') or {}).items() if server.get('outcome') == 'nosocket')
|
|
943
1069
|
flag = L.STATE / 'flags' / 'sockets-lost'
|
|
@@ -1014,6 +1140,7 @@ def hot_sync(ctx, source_mtimes):
|
|
|
1014
1140
|
|
|
1015
1141
|
|
|
1016
1142
|
def main():
|
|
1143
|
+
L.record_tree('statusd') # W9 U0: the tree this daemon runs from (`lanes version`, doctor); --once never records
|
|
1017
1144
|
ctx = new_context()
|
|
1018
1145
|
last_triggered_sync = 0.0
|
|
1019
1146
|
source_mtimes = {} # host path of a terminals.json -> its mtime_ns at last check
|
|
@@ -1021,8 +1148,8 @@ def main():
|
|
|
1021
1148
|
started = time.time()
|
|
1022
1149
|
level, _payload = cycle(ctx)
|
|
1023
1150
|
# Hot-reload: check if any terminals.json changed since we last synced.
|
|
1024
|
-
# Rate-limited to at most one triggered sync per
|
|
1025
|
-
if time.time() - last_triggered_sync >
|
|
1151
|
+
# Rate-limited to at most one triggered sync per HOT_SYNC_EVERY_S to avoid thrashing.
|
|
1152
|
+
if time.time() - last_triggered_sync > HOT_SYNC_EVERY_S and ctx['registry']:
|
|
1026
1153
|
if hot_sync(ctx, source_mtimes):
|
|
1027
1154
|
last_triggered_sync = time.time()
|
|
1028
1155
|
ctx['children'] = [p for p in ctx.get('children', []) if p.poll() is None] # reap finished syncs
|
|
@@ -39,6 +39,9 @@ import tempfile
|
|
|
39
39
|
import time
|
|
40
40
|
from pathlib import Path
|
|
41
41
|
|
|
42
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
43
|
+
import lanes_lib as L # noqa: E402
|
|
44
|
+
|
|
42
45
|
UNIT = 'lanes-studio-host.service'
|
|
43
46
|
ENROL_SERVICE = 'lanes-studio-host-enrol.service'
|
|
44
47
|
ENROL_TIMER = 'lanes-studio-host-enrol.timer'
|
|
@@ -1040,7 +1043,10 @@ def _enrol(layout, options, plan, record, receipt, key_file):
|
|
|
1040
1043
|
|
|
1041
1044
|
|
|
1042
1045
|
def _units(layout, plan):
|
|
1043
|
-
"""Step 6: render every lanes unit
|
|
1046
|
+
"""Step 6: render every lanes unit (services.py), then enable the Host and its retry timer. A re-render, never an
|
|
1047
|
+
install: services.register(install=False) renders the release `current` names and moves no link, so this step run
|
|
1048
|
+
from a tree an update has moved past cannot put the old release back (DEF-1835); it links `current` to this tree
|
|
1049
|
+
only where it names none yet."""
|
|
1044
1050
|
repo = Path(__file__).resolve().parent.parent
|
|
1045
1051
|
|
|
1046
1052
|
def render():
|
|
@@ -1169,6 +1175,7 @@ def enable(argv, layout=None):
|
|
|
1169
1175
|
backup = _ensure_config(layout, options, plan)
|
|
1170
1176
|
if not options['dry_run']:
|
|
1171
1177
|
_remember_paths(layout, options, record)
|
|
1178
|
+
L.record_tree('studio_host', layout.state) # W9 U0: the enrol unit's tree, past every static refusal
|
|
1172
1179
|
replaced = [] # set once the hand-built unit is saved, disabled and stopped
|
|
1173
1180
|
|
|
1174
1181
|
def give_back():
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""The Lanes design tokens: ONE table, rendered seven ways. The sidebar imports `sgr_table
|
|
3
|
-
`--md`, `--js` and `--ghostty-dark|--ghostty-light` write the Zellij theme, the phone
|
|
4
|
-
the docs
|
|
5
|
-
when any committed block differs. Spec:
|
|
2
|
+
"""The Lanes design tokens: ONE table, rendered seven ways. The sidebar imports `sgr_table` (truecolor, 256, 16 or
|
|
3
|
+
none); `--kdl`, `--css`, `--sh`, `--md`, `--js` and `--ghostty-dark|--ghostty-light` write the Zellij theme, the phone
|
|
4
|
+
CSS, the viewer's shell colour sets, the docs tables, the phone's terminal palette and the optional Ghostty palette
|
|
5
|
+
files between markers; `--check` fails when any committed block differs. Spec:
|
|
6
|
+
docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md; palette v2 + the 256-colour set: cockpit v3 P1 (LNS-69).
|
|
6
7
|
No third-party code; importable with no side effects."""
|
|
7
8
|
import difflib
|
|
8
9
|
import os
|
|
@@ -12,30 +13,54 @@ from pathlib import Path
|
|
|
12
13
|
ROLES = ('surface', 'surface-raised', 'line', 'accent-wash', 'accent-line', 'press', 'chip',
|
|
13
14
|
'ink', 'ink-dim', 'ink-faint', 'accent', 'on-accent', 'on-accent-urgent',
|
|
14
15
|
'tone-urgent', 'tone-attention', 'tone-working', 'tone-done', 'scrim', 'shadow')
|
|
16
|
+
# Palette v2 (cockpit v3 P1, 2026-10-06): vivid, high-contrast roles in both variants. v1 had darkened the light tones
|
|
17
|
+
# until they passed (amber #8a5a00 read olive-brown). Surfaces are v1's but for the dark accent-wash, the cursor row.
|
|
15
18
|
TOKENS = {
|
|
16
|
-
'dark': {'surface': '#101318', 'surface-raised': '#191e25', 'line': '#2c333d', 'accent-wash': '#
|
|
17
|
-
'accent-line': '#4b6955', 'press': '#26332c', 'chip': '#ffffff0f', 'ink': '#
|
|
18
|
-
'ink-faint': '#
|
|
19
|
-
'tone-urgent': '#
|
|
19
|
+
'dark': {'surface': '#101318', 'surface-raised': '#191e25', 'line': '#2c333d', 'accent-wash': '#1c3326',
|
|
20
|
+
'accent-line': '#4b6955', 'press': '#26332c', 'chip': '#ffffff0f', 'ink': '#f2f5f8', 'ink-dim': '#a6b1be',
|
|
21
|
+
'ink-faint': '#6f7a87', 'accent': '#8fe3a6', 'on-accent': '#101318', 'on-accent-urgent': '#7a1a12',
|
|
22
|
+
'tone-urgent': '#ff7b6e', 'tone-attention': '#ffc233', 'tone-working': '#6fb6ff', 'tone-done': '#5fdc8a',
|
|
20
23
|
'scrim': '#000000a6', 'shadow': '0 -16px 80px #0007'},
|
|
21
24
|
'light': {'surface': '#f3f5f2', 'surface-raised': '#ffffff', 'line': '#d5ded6', 'accent-wash': '#e5f0e6',
|
|
22
|
-
'accent-line': '#acc9b4', 'press': '#d8e9db', 'chip': '#edf3ec', 'ink': '#
|
|
23
|
-
'ink-faint': '#
|
|
24
|
-
'tone-urgent': '#
|
|
25
|
+
'accent-line': '#acc9b4', 'press': '#d8e9db', 'chip': '#edf3ec', 'ink': '#111a14', 'ink-dim': '#4a5661',
|
|
26
|
+
'ink-faint': '#6f7b87', 'accent': '#1f6f3f', 'on-accent': '#ffffff', 'on-accent-urgent': '#ffd3cc',
|
|
27
|
+
'tone-urgent': '#c41c1c', 'tone-attention': '#a04d00', 'tone-working': '#1a56c4', 'tone-done': '#0d7338',
|
|
25
28
|
'scrim': '#1a222b80', 'shadow': '0 -16px 80px #0003'},
|
|
26
29
|
}
|
|
27
30
|
# 16-colour fallback: the user's own palette resolves these. Selection is achromatic (bold) there, dim is bright-black.
|
|
28
31
|
ANSI16 = {'ink': '39', 'ink-dim': '90', 'ink-faint': '90', 'accent': '1;39',
|
|
29
32
|
'tone-urgent': '31', 'tone-attention': '33', 'tone-working': '34', 'tone-done': '32'}
|
|
33
|
+
# Painted roles that are a BACKGROUND, by the token they take their colour from. `selected` is the cursor row (cockpit
|
|
34
|
+
# v3 P2 paints it): the accent-wash the Zellij themes' *_selected components and the phone's current row
|
|
35
|
+
# (--lanes-accent-wash) already use, with ink on it. Not a CSS variable of its own: the phone needs none, and a copy
|
|
36
|
+
# per variant would cost 58 B of the 37 B mobile.css still has under spec §8's cap.
|
|
37
|
+
BACKGROUNDS = {'selected': 'accent-wash'}
|
|
38
|
+
PAINTED = tuple(ANSI16) + tuple(BACKGROUNDS) # the roles the TUI ever emits
|
|
39
|
+
# The colour modes whose colours Lanes defines (cockpit v3 P2/P3): the sidebar spends the palette in them, and view.sh
|
|
40
|
+
# (`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.
|
|
41
|
+
VIVID = ('truecolor', '256')
|
|
42
|
+
# The 256-colour set: one xterm index per painted role, always from 16-255 (fixed RGB; 0-15 are the user's palette).
|
|
43
|
+
# Each is the nearest entry (RGB distance) in its token's hue family — greys for the inks — that keeps every text role
|
|
44
|
+
# >= 4.2:1 on surface, surface-raised and the 256 `selected` band; `selected` is the nearest entry to the wash. The
|
|
45
|
+
# floor is 4.2, not 4.5, for the cube's six levels alone (test_tokens.Xterm256 re-derives every index). Light accent
|
|
46
|
+
# and done share 22 (#005f00): the nearest passing entry, 23, is a teal.
|
|
47
|
+
XTERM256 = {
|
|
48
|
+
'dark': {'ink': 255, 'ink-dim': 249, 'ink-faint': 243, 'accent': 115, 'tone-urgent': 209, 'tone-attention': 215,
|
|
49
|
+
'tone-working': 75, 'tone-done': 78, 'selected': 235},
|
|
50
|
+
'light': {'ink': 233, 'ink-dim': 240, 'ink-faint': 243, 'accent': 22, 'tone-urgent': 160, 'tone-attention': 94,
|
|
51
|
+
'tone-working': 26, 'tone-done': 22, 'selected': 254},
|
|
52
|
+
}
|
|
30
53
|
# Lane-identity palette for the two places that DEFINE a palette: Zellij's `web_client { theme {} }` (the phone's
|
|
31
54
|
# 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
|
|
55
|
+
# 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
|
|
56
|
+
# (PALETTE16_ROLES); v2 re-saturates the rest (more chroma than v1 in every chromatic slot). Every slot >= 4.5:1.
|
|
33
57
|
PALETTE16 = {
|
|
34
|
-
'dark': ['#
|
|
35
|
-
'#
|
|
36
|
-
'light': ['#
|
|
37
|
-
'#
|
|
58
|
+
'dark': ['#8792a0', '#ff7b6e', '#5fdc8a', '#ffc233', '#6fb6ff', '#c58bff', '#3fd9cf', '#a6b1be',
|
|
59
|
+
'#97a3b1', '#ff9a8e', '#86f0ad', '#ffd866', '#98cbff', '#dcb2ff', '#7deee6', '#f2f5f8'],
|
|
60
|
+
'light': ['#2f3b34', '#c41c1c', '#0d7338', '#a04d00', '#1a56c4', '#8228c4', '#006f73', '#4a5661',
|
|
61
|
+
'#55625b', '#d0221a', '#0e7a3a', '#a85200', '#1f5fd8', '#9133d0', '#00797c', '#111a14'],
|
|
38
62
|
}
|
|
63
|
+
PALETTE16_ROLES = {1: 'tone-urgent', 2: 'tone-done', 3: 'tone-attention', 4: 'tone-working', 7: 'ink-dim', 15: 'ink'}
|
|
39
64
|
# key -> (unicode, ascii twin, tone). Position disambiguates: agent glyphs in column 2, stream glyphs in column 33.
|
|
40
65
|
AGENT_GLYPHS = {'needs_approval': ('◆', '!', 'tone-urgent'), 'needs_input': ('◇', '?', 'tone-attention'),
|
|
41
66
|
'working': ('▸', '>', 'tone-working'), 'done': ('✓', 'v', 'tone-done'),
|
|
@@ -52,11 +77,27 @@ MARKS = {'cursor': ('▌', '|', 'accent'), 'viewing': ('›', '=', 'accent'), 'v
|
|
|
52
77
|
# Tone-less punctuation (spec §3 completeness rule): every ` · ` and every quote the sidebar draws is a lookup, so
|
|
53
78
|
# LANES_GLYPHS=ascii yields a seven-bit frame. `sep` shares `-` with `rule` on purpose: neither carries state.
|
|
54
79
|
'sep': ('·', '-', 'ink-dim'), 'quote-open': ('‘', "'", 'ink'), 'quote-close': ('’', "'", 'ink'),
|
|
80
|
+
# The confirmation's dash (` › close ‘sh’ — y⏎ confirm · Esc cancel`, cockpit v3 S3/S6); `-` like `sep`.
|
|
81
|
+
'dash': ('—', '-', 'ink-dim'),
|
|
82
|
+
# The focus legend's arrow (` ▌ keys go to sidebar · Esc → lane`, cockpit v3 S4).
|
|
83
|
+
'arrow': ('→', '->', 'accent'),
|
|
84
|
+
# Column 3 of a row whose registration is on its way (`P`, cockpit v3 S8): the session stays listed, dimmed
|
|
85
|
+
# mark, until the registry lists its lane. The truncation pair on purpose — position disambiguates (column 3,
|
|
86
|
+
# never the end of a name) — and seven-bit `~` is free in column 3 (`=` `2` `3` are the other marks there).
|
|
87
|
+
'pending': ('…', '~', 'ink-dim'),
|
|
88
|
+
# Column 3 of a lane its owner pinned (`p`, cockpit v3 R5): it leads its section and never falls below `⋮ N
|
|
89
|
+
# more`. The lowest of column 3's marks: pending, the viewports and the viewing mark win over it. Seven-bit
|
|
90
|
+
# `^` is free in column 3 (`=` `2` `3` `~` are the others; the upload strip's `^` is no column-3 mark).
|
|
91
|
+
'pinned': ('▲', '^', 'accent'),
|
|
55
92
|
'ok': ('✓', 'OK', 'tone-done'), 'fail': ('✗', 'X', 'tone-urgent')}
|
|
56
93
|
ECONOMY_TAIL = {'starving': ('⚠', '!', 'tone-attention'), 'downgraded': ('↓', 'v', 'tone-attention'), 'paid': ('$', '$', 'ink-dim')}
|
|
57
94
|
MOBILE_AGENT_LABELS = {'needs_approval': 'Needs approval', 'needs_input': 'Needs you', 'working': 'Working',
|
|
58
95
|
'done': 'Done', 'errored': 'Error', 'shell': 'Shell', 'idle': 'Idle'}
|
|
59
|
-
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def hex_of(variant, role):
|
|
99
|
+
"""The colour a role paints: its own token, or for a background role the token it takes (`selected` -> wash)."""
|
|
100
|
+
return TOKENS[variant][BACKGROUNDS.get(role, role)]
|
|
60
101
|
|
|
61
102
|
|
|
62
103
|
def luminance(hexval):
|
|
@@ -74,17 +115,24 @@ def rgb(hexval):
|
|
|
74
115
|
return tuple(int(hexval[i:i + 2], 16) for i in (1, 3, 5))
|
|
75
116
|
|
|
76
117
|
|
|
77
|
-
def sgr_table(mode, variant):
|
|
78
|
-
"""Complete escape per painted role. `mode` truecolor|16|none; `variant` dark|light. Resolved ONCE per process.
|
|
118
|
+
def sgr_table(mode, variant, dim16=None):
|
|
119
|
+
"""Complete escape per painted role. `mode` truecolor|256|16|none; `variant` dark|light. Resolved ONCE per process.
|
|
120
|
+
Foreground roles are `38;…`, `selected` (the cursor row) `48;…`; in 16/none it is empty — the palette there is the
|
|
121
|
+
user's, and the cursor stays the bold bar. `dim16` overrides the 16-colour dim (default: `LANES_DIM=2` -> SGR 2,
|
|
122
|
+
else 90); the shell renderer pins 90 so the committed block never depends on the generator's environment."""
|
|
79
123
|
table = {'bold': '\033[1m', 'reset': '\033[0m'}
|
|
80
|
-
|
|
124
|
+
if dim16 is None:
|
|
125
|
+
dim16 = '\033[2m' if os.environ.get('LANES_DIM') == '2' else '\033[90m'
|
|
81
126
|
for role in PAINTED:
|
|
127
|
+
bold, layer = '\033[1m' if role == 'accent' else '', 48 if role in BACKGROUNDS else 38
|
|
82
128
|
if mode == 'none':
|
|
83
|
-
table[role] =
|
|
129
|
+
table[role] = bold
|
|
84
130
|
elif mode == '16':
|
|
85
|
-
table[role] = dim16 if role in ('ink-dim', 'ink-faint') else f'\033[{ANSI16[role]}m'
|
|
131
|
+
table[role] = '' if role in BACKGROUNDS else dim16 if role in ('ink-dim', 'ink-faint') else f'\033[{ANSI16[role]}m'
|
|
132
|
+
elif mode == '256':
|
|
133
|
+
table[role] = bold + '\033[%d;5;%dm' % (layer, XTERM256[variant][role])
|
|
86
134
|
else:
|
|
87
|
-
table[role] =
|
|
135
|
+
table[role] = bold + '\033[%d;2;%d;%d;%dm' % ((layer,) + rgb(hex_of(variant, role)))
|
|
88
136
|
return table
|
|
89
137
|
|
|
90
138
|
|
|
@@ -194,18 +242,61 @@ def render_css():
|
|
|
194
242
|
return '\n'.join(out) + '\n'
|
|
195
243
|
|
|
196
244
|
|
|
245
|
+
# The painted roles as the viewer's shell variables (`tokens.sh`), in PAINTED order. LZ_DIM kept its v1 name.
|
|
246
|
+
SH_NAMES = {'ink': 'LZ_INK', 'ink-dim': 'LZ_DIM', 'ink-faint': 'LZ_FAINT', 'accent': 'LZ_ACCENT',
|
|
247
|
+
'tone-urgent': 'LZ_URGENT', 'tone-attention': 'LZ_ATTENTION', 'tone-working': 'LZ_WORKING',
|
|
248
|
+
'tone-done': 'LZ_DONE', 'selected': 'LZ_SELECTED'}
|
|
249
|
+
SH_SETS = (('256', 'dark'), ('256', 'light'), ('truecolor', 'dark'), ('truecolor', 'light'))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _sh_set(mode, variant, indent):
|
|
253
|
+
"""One set's assignments: every painted role, then `LZ_VIVID` (`1` in a VIVID mode, else empty) — what view.sh asks
|
|
254
|
+
instead of re-matching the mode words itself (cockpit v3 P3)."""
|
|
255
|
+
table = sgr_table(mode, variant, dim16='\033[90m')
|
|
256
|
+
lines = [indent + "%s='%s'" % (SH_NAMES[role], table[role].replace('\033', '\\033')) for role in PAINTED]
|
|
257
|
+
return lines + [indent + "LZ_VIVID='%s'" % ('1' if mode in VIVID else '')]
|
|
258
|
+
|
|
259
|
+
|
|
197
260
|
def render_sh():
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
261
|
+
"""The viewer's colour sets as POSIX-shell literals holding `\\033` as text (they go into printf's FORMAT). A plain
|
|
262
|
+
`. tokens.sh` assigns the 16-colour set — the user's own palette; `lz_tones MODE VARIANT` re-points every LZ_* tone
|
|
263
|
+
variable to the 256 / truecolor set of dark / light, `lz_tones none` (one argument) to none, and anything else —
|
|
264
|
+
`lz_tones 16`, no argument, an unknown mode or variant — back to 16 colours. Every set also says `LZ_VIVID` (`1`
|
|
265
|
+
in truecolor and 256). The `case` word reads `${1:-}:${2:-}`: view.sh runs `set -u`, where a bare `$2` aborts the
|
|
266
|
+
shell on a one-argument call (DEF-1772). Assignments and one `case` only: sourcing or calling it forks nothing
|
|
267
|
+
(cockpit v3 P1). view.sh calls `lz_tones "$color" "$theme"` with its device's `.color`/`.theme` stamps on every
|
|
268
|
+
status screen (cockpit v3 P3)."""
|
|
269
|
+
lines = ['# BEGIN lanes tokens (generated by libexec/tokens.py --sh; do not edit by hand)',
|
|
270
|
+
"LZ_BOLD='\\033[1m'", "LZ_RESET='\\033[0m'"]
|
|
271
|
+
lines += _sh_set('16', 'dark', '')
|
|
272
|
+
lines += ['lz_tones() {', ' case "${1:-}:${2:-}" in']
|
|
273
|
+
for mode, variant in SH_SETS:
|
|
274
|
+
lines += [f' {mode}:{variant})'] + _sh_set(mode, variant, ' ') + [' ;;']
|
|
275
|
+
lines += [' none:*)'] + _sh_set('none', 'dark', ' ') + [' ;;']
|
|
276
|
+
lines += [' *)'] + _sh_set('16', 'dark', ' ') + [' ;;']
|
|
277
|
+
lines += [' esac', '}', '# END lanes tokens']
|
|
278
|
+
return '\n'.join(lines) + '\n'
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _md_role(role):
|
|
282
|
+
"""One row of the role table: hex per variant, the 256 index per variant, the 16-colour SGR, the CSS variable."""
|
|
283
|
+
painted = role in PAINTED
|
|
284
|
+
x256 = [f'`{XTERM256[v][role]}`' if painted else '`—`' for v in ('dark', 'light')]
|
|
285
|
+
ansi = '—' if role in BACKGROUNDS else ANSI16.get(role, '—')
|
|
286
|
+
return (f'| {role} | `{hex_of("dark", role)}` | `{hex_of("light", role)}` | {x256[0]} | {x256[1]} | `{ansi}` | '
|
|
287
|
+
f'`--lanes-{BACKGROUNDS.get(role, role)}` |')
|
|
203
288
|
|
|
204
289
|
|
|
205
290
|
def render_md():
|
|
206
291
|
rows = ['<!-- BEGIN lanes tokens (generated by libexec/tokens.py --md; do not edit by hand) -->',
|
|
207
|
-
'| Role | Dark | Light | ANSI-16 | CSS variable |', '
|
|
208
|
-
|
|
292
|
+
'| Role | Dark | Light | 256 dark | 256 light | ANSI-16 | CSS variable |', '|---|---|---|---|---|---|---|']
|
|
293
|
+
for role in ROLES:
|
|
294
|
+
rows.append(_md_role(role))
|
|
295
|
+
rows += [_md_role(bg) for bg, source in BACKGROUNDS.items() if source == role] # `selected` under its wash
|
|
296
|
+
rows += ['', '| Slot | ANSI | Dark | Light | Role |', '|---|---|---|---|---|']
|
|
297
|
+
names = ANSI_NAMES + tuple('bright ' + n for n in ANSI_NAMES)
|
|
298
|
+
rows += [f'| {i} | {names[i]} | `{PALETTE16["dark"][i]}` | `{PALETTE16["light"][i]}` | {PALETTE16_ROLES.get(i, "—")} |'
|
|
299
|
+
for i in range(16)]
|
|
209
300
|
rows += ['', '| Glyph | Codepoint | ASCII | Tone | Meaning |', '|---|---|---|---|---|']
|
|
210
301
|
for name, table in (('agent', AGENT_GLYPHS), ('stream', GLYPHS), ('mark', MARKS), ('economy', ECONOMY_TAIL)):
|
|
211
302
|
for key, (glyph, ascii_twin, tone) in table.items():
|
|
@@ -1,11 +1,95 @@
|
|
|
1
|
-
# Lanes
|
|
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
|
-
|
|
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
|