@vibelord/lanes 1.0.0 → 1.0.2
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/package.json +1 -1
- package/payload/lanes/README.md +92 -7
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +928 -150
- package/payload/lanes/client/ghostty/common.conf +3 -0
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +25 -0
- package/payload/lanes/client/ghostty/lanes-palette.conf +25 -0
- package/payload/lanes/client/ghostty/omarchy.conf +3 -2
- package/payload/lanes/client/lanes-launch +62 -5
- package/payload/lanes/client/lanes-window +17 -1
- package/payload/lanes/container/env.sh +8 -0
- package/payload/lanes/container/hooks/claude-settings.json +10 -0
- package/payload/lanes/container/hooks/codex-hooks.json +22 -0
- package/payload/lanes/container/lane +326 -25
- package/payload/lanes/container/lane-signal +173 -0
- package/payload/lanes/docs/ARCHITECTURE.md +158 -6
- package/payload/lanes/docs/DESIGN.md +250 -0
- package/payload/lanes/docs/INSTALL.md +39 -0
- package/payload/lanes/docs/RUNBOOK.md +507 -24
- package/payload/lanes/docs/SHARING.md +1 -1
- package/payload/lanes/holders/exec.sh +2 -5
- package/payload/lanes/holders/ssh.sh +18 -0
- package/payload/lanes/holders.lock +3 -0
- package/payload/lanes/install/enable-phone.sh +15 -6
- package/payload/lanes/install/enable-web.sh +19 -6
- package/payload/lanes/install/host-install.sh +10 -1
- package/payload/lanes/install/services.py +30 -3
- package/payload/lanes/install.sh +108 -3
- package/payload/lanes/libexec/agent_state.py +246 -0
- package/payload/lanes/libexec/connection.py +178 -0
- package/payload/lanes/libexec/cursor_sync.py +147 -6
- package/payload/lanes/libexec/device_sync.py +134 -0
- package/payload/lanes/libexec/economy_poll.py +685 -0
- package/payload/lanes/libexec/guard.py +27 -1
- package/payload/lanes/libexec/lanes_lib.py +436 -36
- package/payload/lanes/libexec/notify.py +317 -0
- package/payload/lanes/libexec/registrations.py +148 -0
- package/payload/lanes/libexec/session_actions.py +53 -22
- package/payload/lanes/libexec/sidebar.py +1190 -186
- package/payload/lanes/libexec/statusd.py +352 -90
- package/payload/lanes/libexec/tokens.py +310 -0
- package/payload/lanes/libexec/tokens.sh +11 -0
- package/payload/lanes/libexec/view.sh +22 -11
- package/payload/lanes/systemd/lanes-economy.service +20 -0
- package/payload/lanes/systemd/lanes-notify.service +19 -0
- package/payload/lanes/systemd/lanes-sync.service +3 -0
- package/payload/lanes/web/README.md +96 -2
- package/payload/lanes/web/browser.css +22 -0
- package/payload/lanes/web/browser.js +73 -0
- package/payload/lanes/web/index.html +2 -1
- package/payload/lanes/web/mobile.css +108 -221
- package/payload/lanes/web/mobile.js +105 -55
- package/payload/lanes/workspaces.example/README.md +9 -3
- package/payload/lanes/zellij/config.kdl +128 -19
- package/payload/manifest.json +7 -3
- package/payload/lanes/libexec/legacy_sessions.py +0 -130
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
/* Lanes
|
|
2
|
-
|
|
1
|
+
/* Lanes browser shell; see web/README.md, Code notes */
|
|
2
|
+
function laneShellMode({userAgent, coarse, width, height} = {}) {
|
|
3
|
+
if (/iPhone|iPod|Android.*Mobile/i.test(userAgent) || (coarse && (width <= 600 || height <= 500))) return 'phone';
|
|
4
|
+
return coarse ? 'tablet' : 'desktop';
|
|
5
|
+
}
|
|
6
|
+
if (typeof module === 'object') module.exports = {laneShellMode};
|
|
3
7
|
(() => {
|
|
4
8
|
'use strict';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
if (typeof document === 'undefined') return;
|
|
10
|
+
const mode = laneShellMode({userAgent: navigator.userAgent, coarse: matchMedia('(pointer: coarse)').matches,
|
|
11
|
+
width: innerWidth, height: innerHeight});
|
|
8
12
|
const root = document.documentElement;
|
|
13
|
+
// BEGIN lanes tokens (generated by libexec/tokens.py --js; do not edit by hand)
|
|
14
|
+
const LANES_ANSI = {dark: ['#7d8896', '#f28b7d', '#6fd39a', '#e5b84a', '#7fb3f2', '#c9a6ff', '#6fd6d0', '#9ca7b5', '#8a95a3', '#ff9d96', '#8fe0a3', '#f7cf7f', '#a5cbff', '#dcc6ff', '#9ae6e0', '#edf1f5'], light: ['#3b4a42', '#b7322a', '#1b7142', '#8a5a00', '#2456a8', '#7a3fb8', '#0f7b7b', '#56616d', '#5f6d66', '#c2332a', '#1b7a38', '#8f5a00', '#2860c4', '#8a4dc8', '#0f7272', '#17241b']};
|
|
15
|
+
// END lanes tokens
|
|
16
|
+
root.dataset.lanesShell = mode;
|
|
17
|
+
if (mode !== 'phone') {
|
|
18
|
+
const add = (tag, props) => document.head.append(Object.assign(document.createElement(tag), props));
|
|
19
|
+
add('link', {rel: 'stylesheet', href: '/lanes-assets/browser.css'});
|
|
20
|
+
add('script', {src: '/lanes-assets/browser.js'});
|
|
21
|
+
}
|
|
22
|
+
if (mode === 'desktop') return;
|
|
9
23
|
root.classList.add('lanes-mobile');
|
|
24
|
+
if (mode === 'tablet') {
|
|
25
|
+
root.classList.add('lanes-tablet');
|
|
26
|
+
try {localStorage.getItem('zellij:mobile-ui') || localStorage.setItem('zellij:mobile-ui', 'on');} catch (_) {}
|
|
27
|
+
}
|
|
10
28
|
|
|
11
|
-
// Only a browser's native tap/focus on an input may summon the keyboard. Zellij
|
|
12
|
-
// otherwise refocuses a hidden input on touchstart, touchend, click and server
|
|
13
|
-
// SetSoftKeyboard messages. Install before its first terminal or login modal.
|
|
14
29
|
const elementFocus = HTMLElement.prototype.focus;
|
|
15
30
|
HTMLElement.prototype.focus = function(...args) {
|
|
16
31
|
const textInput = this instanceof HTMLInputElement &&
|
|
@@ -18,11 +33,7 @@
|
|
|
18
33
|
if (textInput || this instanceof HTMLTextAreaElement || this.isContentEditable) return;
|
|
19
34
|
return elementFocus.apply(this, args);
|
|
20
35
|
};
|
|
21
|
-
Object.defineProperty(window, '__zjSoftKbdEnabled', {
|
|
22
|
-
configurable: false,
|
|
23
|
-
get: () => false,
|
|
24
|
-
set: () => {},
|
|
25
|
-
});
|
|
36
|
+
Object.defineProperty(window, '__zjSoftKbdEnabled', {configurable: false, get: () => false, set: () => {}});
|
|
26
37
|
try {
|
|
27
38
|
const theme = localStorage.getItem('zellij:mobile-theme') || 'dark';
|
|
28
39
|
localStorage.setItem('zellij:mobile-theme', theme);
|
|
@@ -56,7 +67,7 @@
|
|
|
56
67
|
el.addEventListener('click', action);
|
|
57
68
|
return el;
|
|
58
69
|
};
|
|
59
|
-
let shell, heading, subtitle, dock, sheet, list, search, count, lanesButton, typingField;
|
|
70
|
+
let shell, heading, subtitle, dock, sheet, list, search, count, lanesButton, typingField, quickRow, quickOn;
|
|
60
71
|
let laneFilterBar, laneFilterInput, laneFilterPrev = '';
|
|
61
72
|
let api, term, previousCards = '', sheetOpen = false, returnFocus;
|
|
62
73
|
let fontSize = 16;
|
|
@@ -75,9 +86,7 @@
|
|
|
75
86
|
root.style.setProperty('--lanes-vw', `${vv?.width ?? innerWidth}px`);
|
|
76
87
|
root.style.setProperty('--lanes-offset-top', `${vv?.offsetTop ?? 0}px`);
|
|
77
88
|
root.style.setProperty('--lanes-offset-left', `${vv?.offsetLeft ?? 0}px`);
|
|
78
|
-
// Browser toolbars alone must not put the UI into keyboard mode.
|
|
79
89
|
const occluded = !!vv && innerHeight - vv.height - vv.offsetTop > 120;
|
|
80
|
-
// Zellij's keyboard input lives in a closed shadow root, so activeElement is its host.
|
|
81
90
|
const focused = window.__zjSoftKbdCapture?.isFocused || document.activeElement?.matches('input, textarea');
|
|
82
91
|
setKeyboard(occluded && focused);
|
|
83
92
|
}
|
|
@@ -97,19 +106,14 @@
|
|
|
97
106
|
label.innerHTML = svg('keyboard');
|
|
98
107
|
label.append(make('span', '', 'Type here'));
|
|
99
108
|
label.setAttribute('aria-hidden', 'true');
|
|
100
|
-
// Keep the original input and its event handlers inside its closed shadow
|
|
101
|
-
// root (including its protection from password-manager extensions). Make it
|
|
102
|
-
// a real, tappable field instead of a 1px invisible focus target. Keystrokes
|
|
103
|
-
// appear in the terminal; its internal padding/history stays invisible.
|
|
104
109
|
host.removeAttribute('aria-hidden');
|
|
105
110
|
host.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;opacity:1;pointer-events:auto;';
|
|
106
111
|
capture.removeAttribute('aria-hidden');
|
|
107
112
|
capture.setAttribute('aria-label', 'Type in terminal');
|
|
108
113
|
capture.tabIndex = 0;
|
|
109
|
-
capture.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;' +
|
|
110
|
-
'
|
|
111
|
-
'
|
|
112
|
-
'pointer-events:auto;user-select:text;-webkit-user-select:text;';
|
|
114
|
+
capture.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;font-size:16px;' +
|
|
115
|
+
'border:0;border-radius:12px;padding:0;margin:0;opacity:1;background:transparent;color:transparent;' +
|
|
116
|
+
'caret-color:transparent;outline:none;pointer-events:auto;user-select:text;-webkit-user-select:text;';
|
|
113
117
|
field.append(label, host);
|
|
114
118
|
capture.addEventListener('focus', () => field.classList.add('is-typing'));
|
|
115
119
|
capture.addEventListener('blur', () => field.classList.remove('is-typing'));
|
|
@@ -124,6 +128,20 @@
|
|
|
124
128
|
function currentTab() {
|
|
125
129
|
return data()?.tabs.find(t => t.position === data()?.active_pane?.tab_position);
|
|
126
130
|
}
|
|
131
|
+
const GLYPHS = '◆◇▸✓', TONES = ['urgent', 'attention', 'working', 'done'];
|
|
132
|
+
function parseCounts(title) {
|
|
133
|
+
const c = {approval: 0, input: 0, working: 0, done: 0}, keys = Object.keys(c);
|
|
134
|
+
let found = 0;
|
|
135
|
+
for (const [, g, n] of String(title || '').matchAll(/([◆◇▸✓])(\d+)/g)) { c[keys[GLYPHS.indexOf(g)]] = +n; found = 1; }
|
|
136
|
+
return found ? c : null;
|
|
137
|
+
}
|
|
138
|
+
window.__lanesParseCounts = parseCounts;
|
|
139
|
+
const countParts = c => c ? Object.values(c).map((n, i) => [GLYPHS[i], n, TONES[i]]).filter(p => p[1]) : [];
|
|
140
|
+
const pickerTab = name => data()?.tabs.find(t => t.name === `${name} ☰`);
|
|
141
|
+
function pickerTitle(name) {
|
|
142
|
+
const picker = pickerTab(name);
|
|
143
|
+
return picker ? data()?.panes.find(p => p.tab_position === picker.position && !p.is_plugin)?.title || '' : '';
|
|
144
|
+
}
|
|
127
145
|
function workspaces() {
|
|
128
146
|
const tabs = data()?.tabs || [];
|
|
129
147
|
const pickers = tabs.filter(t => t.name.endsWith(' ☰'));
|
|
@@ -159,12 +177,14 @@
|
|
|
159
177
|
const items = workspaces();
|
|
160
178
|
const activeName = (currentTab()?.name || '').replace(/ ☰$/, '');
|
|
161
179
|
const query = search.value.trim().toLocaleLowerCase();
|
|
162
|
-
const
|
|
180
|
+
const titles = items.map(w => w.picker ? pickerTitle(w.name) : '');
|
|
181
|
+
const signature = JSON.stringify([items, titles, activeName, query]);
|
|
163
182
|
if (signature === previousCards) return;
|
|
164
183
|
previousCards = signature;
|
|
165
184
|
list.replaceChildren();
|
|
166
185
|
text(count, `${items.length} workspaces`);
|
|
167
|
-
for (const item of items.
|
|
186
|
+
for (const [index, item] of items.entries()) {
|
|
187
|
+
if (!item.name.toLocaleLowerCase().includes(query)) continue;
|
|
168
188
|
const active = item.name === activeName;
|
|
169
189
|
const card = make('button', `lanes-workspace${active ? ' is-current' : ''}`);
|
|
170
190
|
card.type = 'button';
|
|
@@ -172,7 +192,10 @@
|
|
|
172
192
|
if (active) card.setAttribute('aria-current', 'true');
|
|
173
193
|
const monogram = make('span', 'lanes-monogram', Array.from(item.name)[0]?.toUpperCase() || 'L');
|
|
174
194
|
const copy = make('span', 'lanes-workspace-copy');
|
|
175
|
-
|
|
195
|
+
const small = make('small'), parts = countParts(parseCounts(titles[index]));
|
|
196
|
+
parts.forEach(([g, n, tone], i) => small.append(i ? ' · ' : '', make('span', `lanes-tone-${tone}`, `${g} ${n}`)));
|
|
197
|
+
if (!parts.length) small.textContent = item.picker ? 'Choose a lane' : 'Open workspace';
|
|
198
|
+
copy.append(make('strong', '', item.name), small);
|
|
176
199
|
const arrow = make('span', 'lanes-workspace-arrow');
|
|
177
200
|
arrow.innerHTML = svg(active ? 'check' : 'arrow');
|
|
178
201
|
card.append(monogram, copy, arrow);
|
|
@@ -197,8 +220,7 @@
|
|
|
197
220
|
dock = make('nav', 'lanes-dock'); dock.setAttribute('aria-label', 'Workspace controls');
|
|
198
221
|
const spaces = button('Workspaces', 'workspaces', openSheet);
|
|
199
222
|
lanesButton = button('Lanes', 'lanes', () => {
|
|
200
|
-
const
|
|
201
|
-
const picker = data()?.tabs.find(t => t.name === `${name} ☰`);
|
|
223
|
+
const picker = pickerTab((currentTab()?.name || '').replace(/ ☰$/, ''));
|
|
202
224
|
if (picker) focusTab(picker); else openSheet();
|
|
203
225
|
});
|
|
204
226
|
const panes = button('Panes', 'panes', () => nativeClick('.zj-pane'));
|
|
@@ -233,11 +255,10 @@
|
|
|
233
255
|
if (ev.shiftKey && document.activeElement === first) {ev.preventDefault(); last.focus();}
|
|
234
256
|
else if (!ev.shiftKey && document.activeElement === last) {ev.preventDefault(); first.focus();}
|
|
235
257
|
});
|
|
236
|
-
|
|
258
|
+
quickRow = buildQuickRow();
|
|
259
|
+
shell.append(header, quickRow, dock, sheet); document.body.append(shell);
|
|
260
|
+
if (mode === 'tablet') more.before(dock);
|
|
237
261
|
root.classList.add('lanes-ready');
|
|
238
|
-
// Upstream cancels pointerdown on extra keys, which also suppresses their
|
|
239
|
-
// touch click in Safari. Cancel mousedown instead: keep typing focus while
|
|
240
|
-
// allowing the native click to activate CTRL, arrows, and the other keys.
|
|
241
262
|
for (const key of document.querySelectorAll('.zj-mobile-modbar button')) {
|
|
242
263
|
key.addEventListener('pointerdown', ev => ev.stopImmediatePropagation(), true);
|
|
243
264
|
key.addEventListener('mousedown', ev => ev.preventDefault());
|
|
@@ -246,31 +267,39 @@
|
|
|
246
267
|
resize();
|
|
247
268
|
}
|
|
248
269
|
function sendToTerminal(bytes) {
|
|
249
|
-
|
|
250
|
-
// then fall back to the keyboard capture element.
|
|
251
|
-
if (window.__zjSendControl) {
|
|
252
|
-
try { window.__zjSendControl({type: 'Write', bytes}); return; } catch (_) {}
|
|
253
|
-
}
|
|
254
|
-
const el = window.__zjSoftKbdCapture?.element;
|
|
255
|
-
if (!el) return;
|
|
256
|
-
const text = String.fromCharCode(...bytes);
|
|
257
|
-
el.focus({preventScroll: true});
|
|
258
|
-
try { document.execCommand('insertText', false, text); } catch (_) {}
|
|
270
|
+
try { window.__zjSendControl?.({type: 'Write', bytes}); } catch (_) {}
|
|
259
271
|
}
|
|
260
272
|
function applyLaneFilter(query) {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
273
|
+
const bytes = [...new TextEncoder().encode(query)].filter(b => b >= 0x20 && b !== 0x7f);
|
|
274
|
+
sendToTerminal([0x1b, 0x2f, ...bytes]);
|
|
275
|
+
}
|
|
276
|
+
function buildQuickRow() {
|
|
277
|
+
const row = make('div', 'lanes-quick');
|
|
278
|
+
row.hidden = true;
|
|
279
|
+
for (const [label, bytes, name, hold] of [['⏎', [13], 'Enter'], ['y', [121]], ['n', [110], 0, 1], ['Esc', [27]],
|
|
280
|
+
['^C', [3], 'Control C', 1], ['↑', [27, 91, 65], 'Up'], ['↓', [27, 91, 66], 'Down']]) {
|
|
281
|
+
const key = make('button', hold ? 'is-destructive' : '', label);
|
|
282
|
+
let down = -1;
|
|
283
|
+
key.type = 'button'; if (name) key.setAttribute('aria-label', name);
|
|
284
|
+
key.addEventListener('mousedown', ev => ev.preventDefault());
|
|
285
|
+
key.addEventListener('pointerdown', ev => { down = ev.timeStamp; });
|
|
286
|
+
key.addEventListener('pointerup', ev => {
|
|
287
|
+
if (!hold || (down >= 0 && ev.timeStamp - down >= 400)) sendToTerminal(bytes); else text(subtitle, 'Hold to send');
|
|
288
|
+
down = -1;
|
|
289
|
+
});
|
|
290
|
+
row.append(key);
|
|
291
|
+
}
|
|
292
|
+
return row;
|
|
264
293
|
}
|
|
265
294
|
function clearLaneFilter() {
|
|
266
|
-
sendToTerminal([0x1b]);
|
|
295
|
+
sendToTerminal([0x1b]);
|
|
267
296
|
laneFilterPrev = '';
|
|
268
297
|
if (laneFilterInput) laneFilterInput.value = '';
|
|
269
298
|
}
|
|
270
299
|
function buildLaneFilterBar() {
|
|
271
300
|
const bar = make('div', 'lanes-filter-bar');
|
|
272
301
|
bar.setAttribute('aria-label', 'Filter lanes');
|
|
273
|
-
bar.
|
|
302
|
+
bar.hidden = true;
|
|
274
303
|
bar.innerHTML = svg('search');
|
|
275
304
|
laneFilterInput = make('input');
|
|
276
305
|
laneFilterInput.type = 'search'; laneFilterInput.placeholder = 'Filter lanes';
|
|
@@ -299,6 +328,17 @@
|
|
|
299
328
|
text(document.getElementById('lanes-font-value'), `${fontSize} px`);
|
|
300
329
|
resize();
|
|
301
330
|
}
|
|
331
|
+
function lanesTheme() {
|
|
332
|
+
const css = getComputedStyle(root), v = p => css.getPropertyValue(p).trim();
|
|
333
|
+
const t = {background: v('--lanes-surface'), foreground: v('--lanes-ink'), cursor: v('--lanes-accent'),
|
|
334
|
+
selectionBackground: v('--lanes-accent-wash'), selectionForeground: v('--lanes-ink')};
|
|
335
|
+
LANES_ANSI[root.dataset.zjTheme === 'light' ? 'light' : 'dark'].forEach((c, i) => {
|
|
336
|
+
const k = 'Black Red Green Yellow Blue Magenta Cyan White'.split(' ')[i % 8];
|
|
337
|
+
t[i < 8 ? k.toLowerCase() : 'bright' + k] = c;
|
|
338
|
+
});
|
|
339
|
+
return t;
|
|
340
|
+
}
|
|
341
|
+
const applyTheme = () => { if (term) term.options.theme = lanesTheme(); };
|
|
302
342
|
function installFontControls() {
|
|
303
343
|
const row = make('div', 'lanes-font-controls');
|
|
304
344
|
row.append(make('span', '', 'Text size'));
|
|
@@ -320,20 +360,32 @@
|
|
|
320
360
|
const name = tab?.name.replace(/ ☰$/, '') || 'Lanes';
|
|
321
361
|
const picker = tab?.name.endsWith(' ☰');
|
|
322
362
|
const pane = data()?.panes.find(p => p.pane_id === data()?.active_pane?.pane_id && !p.is_plugin);
|
|
363
|
+
const counts = parseCounts(pickerTitle(name));
|
|
364
|
+
const chips = countParts(counts).map(([glyph, n]) => glyph + n).join(' ');
|
|
323
365
|
text(heading, name);
|
|
324
|
-
text(subtitle, picker ? 'Choose a lane to continue' : pane?.title || 'Your workspace');
|
|
366
|
+
text(subtitle, picker ? chips || 'Choose a lane to continue' : pane?.title || 'Your workspace');
|
|
325
367
|
lanesButton.setAttribute('aria-pressed', String(!!picker));
|
|
368
|
+
const needs = counts ? counts.approval + counts.input : 0;
|
|
369
|
+
if (needs) lanesButton.dataset.count = String(needs); else delete lanesButton.dataset.count;
|
|
326
370
|
updateLaneFilterBar(picker);
|
|
327
|
-
|
|
371
|
+
const quick = active && !picker;
|
|
372
|
+
if (quick !== quickOn) {
|
|
373
|
+
quickOn = quick;
|
|
374
|
+
quickRow.hidden = !quick;
|
|
375
|
+
root.style.setProperty('--lanes-quick', quick ? '48px' : '0px');
|
|
376
|
+
resize();
|
|
377
|
+
}
|
|
378
|
+
document.title = `${chips ? chips + ' · ' : ''}${name} · Lanes`;
|
|
328
379
|
renderCards();
|
|
329
380
|
}
|
|
330
381
|
function connect() {
|
|
331
382
|
if (api || !window.__zjMobileUi || !window.term || !window.__zjSoftKbdCapture?.element) return;
|
|
332
|
-
// Wait for server state: resizing before the control socket opens makes 0.45.1
|
|
333
|
-
// throw InvalidStateError and leaves the server with the old terminal size.
|
|
334
383
|
if (!window.__zjMobileUi.getState()?.data?.tabs?.length) return;
|
|
335
384
|
api = window.__zjMobileUi; term = window.term;
|
|
336
|
-
build(); applyFont();
|
|
385
|
+
build(); applyFont(); applyTheme();
|
|
386
|
+
const sync = window.__zjSyncInactiveCursorStyle;
|
|
387
|
+
window.__zjSyncInactiveCursorStyle = () => { sync?.(); applyTheme(); };
|
|
388
|
+
new MutationObserver(applyTheme).observe(root, {attributes: true, attributeFilter: ['data-zj-theme']});
|
|
337
389
|
const setData = api.setData;
|
|
338
390
|
api.setData = function(value) {setData.call(api, value); refresh();};
|
|
339
391
|
const nativeRoot = document.getElementById('zj-mobile-root');
|
|
@@ -354,14 +406,12 @@
|
|
|
354
406
|
window.visualViewport?.addEventListener('scroll', viewport);
|
|
355
407
|
window.addEventListener('zellij:soft-keyboard-visibility', () => {
|
|
356
408
|
viewport();
|
|
357
|
-
// A rotation also shrinks the viewport. Correct upstream's height-only keyboard heuristic.
|
|
358
409
|
requestAnimationFrame(() => window.__zjSendControl?.({
|
|
359
410
|
type: 'SoftKeyboardVisibilityChanged', visible: root.classList.contains('lanes-keyboard'),
|
|
360
411
|
}));
|
|
361
412
|
});
|
|
362
413
|
document.addEventListener('focusout', () => requestAnimationFrame(viewport));
|
|
363
414
|
new MutationObserver(() => {styleLogin(); connect();}).observe(document.body, {childList: true});
|
|
364
|
-
// The upstream window hooks are installed just after it appends its mobile root.
|
|
365
415
|
const awaitingClient = setInterval(() => {connect(); if (api) clearInterval(awaitingClient);}, 200);
|
|
366
416
|
styleLogin(); connect();
|
|
367
417
|
})();
|
|
@@ -13,10 +13,16 @@ Copy this folder to `~/.config/lanes/workspaces/` and edit it.
|
|
|
13
13
|
| `catalog.json` | The boards, in tab order: `id` (also the uploads/watch-folder name), `title` (the tab), `manifest`. |
|
|
14
14
|
| `workspaces.<board>.json` | Where the board's lanes come from: one or more `repositories`, each naming the workspace **container** (`host`) and the board's terminal registry (`registry`: the repository's `.vscode/terminals.json`). Optional per repository: `prefix` (prepended to lane titles), `displayNames`, `bindingOverrides` (tmux socket/session for a terminal whose registry entry has none), `cursor` (a second file to project the board into, for boards whose registry lives outside a repository). `monitor` adds the full-pane Throttle monitor tab. |
|
|
15
15
|
| `commit-targets.json` | Only for `lanes cursor-commit`: repository, branch and author for committing the normalized registries. |
|
|
16
|
+
| `registered-sessions.json` | Optional, created by **Add to workspace** or `lanes register`: `version: 1`, plus `sessions` records with `id`, `workspace`, `name`, `container`, `tmux: {socket, session}`, and `folder`. Preserve this file in site backups. |
|
|
16
17
|
|
|
17
|
-
A lane is whatever the registry says it is: `lanes sync` (every two minutes, read-only) turns each
|
|
18
|
-
lane with a stable id `<board>--<slug of the terminal name
|
|
19
|
-
|
|
18
|
+
A lane is whatever the registry says it is: `lanes sync` (every two minutes, read-only) turns each repository
|
|
19
|
+
terminal into a lane with a stable id `<board>--<slug of the terminal name>` (with the repository `prefix`). A
|
|
20
|
+
`displayNames` entry, which the sidebar `R` key and `lanes rename <lane-id> <name>` write into the repository's block
|
|
21
|
+
of `workspaces.<board>.json` (first write keeps a `.bak-<UTC>` copy beside it), changes only the shown name, never the
|
|
22
|
+
id. Add a terminal to `.vscode/terminals.json` for an editor-defined lane.
|
|
23
|
+
Alternatively, use **+** beside a discovered session to register its existing binding and ID in a workspace
|
|
24
|
+
using the same container. Sync includes these local registrations; they never run a create command.
|
|
25
|
+
Repository declarations take precedence when they declare the same binding. Legacy Docker shells cannot be adopted.
|
|
20
26
|
|
|
21
27
|
After editing: `lanes sync && lanes doctor`.
|
|
22
28
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// lanes host view configuration. Generated from the user's coder-launch keybinding scheme
|
|
2
2
|
// (clear-defaults, Ctrl-Alt prefix: every ordinary key reaches the agent). Edit here; this file is versioned.
|
|
3
|
+
// Ctrl-g is never bound (plan Task 19): it reaches the lane (Claude Code opens its editor on it); the lock is
|
|
4
|
+
// Ctrl-Alt-g, in and out, and the LOCKED ribbon turns on-accent-urgent while it is on (spec §5.2).
|
|
3
5
|
keybinds clear-defaults=true {
|
|
4
6
|
normal {
|
|
5
|
-
bind "Ctrl g" { SwitchToMode "locked"; }
|
|
7
|
+
bind "Ctrl Alt g" { SwitchToMode "locked"; }
|
|
6
8
|
bind "Ctrl Alt p" { SwitchToMode "pane"; }
|
|
7
9
|
bind "Ctrl Alt t" { SwitchToMode "tab"; }
|
|
8
10
|
bind "Ctrl Alt n" { SwitchToMode "resize"; }
|
|
@@ -12,7 +14,7 @@ keybinds clear-defaults=true {
|
|
|
12
14
|
bind "Ctrl Alt f" { SwitchToMode "scroll"; }
|
|
13
15
|
}
|
|
14
16
|
locked {
|
|
15
|
-
bind "Ctrl g" { SwitchToMode "normal"; }
|
|
17
|
+
bind "Ctrl Alt g" { SwitchToMode "normal"; }
|
|
16
18
|
}
|
|
17
19
|
pane {
|
|
18
20
|
bind "left" { MoveFocus "left"; }
|
|
@@ -164,19 +166,40 @@ keybinds clear-defaults=true {
|
|
|
164
166
|
bind "Ctrl Alt End" { ScrollToBottom; SwitchToMode "normal"; }
|
|
165
167
|
bind "Ctrl Alt c" { Copy; }
|
|
166
168
|
bind "Ctrl Alt left" { GoToPreviousTab; }
|
|
167
|
-
// lanes: workspaces are tabs (Ctrl-Alt-←/→); lanes live in the sidebar.
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
// lanes: workspaces are tabs (Ctrl-Alt-←/→); lanes live in the sidebar. Next / previous lane (plan Task 19)
|
|
170
|
+
// types nothing: `lanes next|prev --focused` points the FOCUSED viewport (the first one when the sidebar has
|
|
171
|
+
// the focus) at the neighbouring lane of its board, exactly as Enter in the sidebar does, from a floating pane
|
|
172
|
+
// that closes when it exits. The focus stays where it was. On a tab without a board (Host, Throttle) it does
|
|
173
|
+
// nothing — the old WriteChars bind typed its "J"/"K" into that tab's pane. "lanes" is found on the view
|
|
174
|
+
// server's PATH (bin/lanes clean_env), as for Ctrl-Alt-m below.
|
|
175
|
+
bind "Ctrl Alt down" {
|
|
176
|
+
Run "lanes" "next" "--focused" {
|
|
177
|
+
floating true
|
|
178
|
+
close_on_exit true
|
|
179
|
+
name "Lanes"
|
|
180
|
+
x "0%"
|
|
181
|
+
y "0%"
|
|
182
|
+
width "1%"
|
|
183
|
+
height "1%"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
bind "Ctrl Alt up" {
|
|
187
|
+
Run "lanes" "prev" "--focused" {
|
|
188
|
+
floating true
|
|
189
|
+
close_on_exit true
|
|
190
|
+
name "Lanes"
|
|
191
|
+
x "0%"
|
|
192
|
+
y "0%"
|
|
193
|
+
width "1%"
|
|
194
|
+
height "1%"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// These two still hand a key to the SIDEBAR (a filter and the upload box live there, so the focus must end
|
|
198
|
+
// on it). WHY THREE MoveFocus "left": a workspace tab is [sidebar | view main | view b | view c] (wide views,
|
|
199
|
+
// at most 3 viewports) and WriteChars types into whichever pane holds the focus BY THEN; three lefts reach
|
|
200
|
+
// the sidebar from every viewport and zellij does not wrap at the left edge (tests/view.py). Known limit: a
|
|
201
|
+
// tab WITHOUT a sidebar (Host, Throttle, the mobile view tab) has nothing to the left, so there the
|
|
202
|
+
// character still goes to the pane itself.
|
|
180
203
|
bind "Ctrl Alt /" { MoveFocus "left"; MoveFocus "left"; MoveFocus "left"; WriteChars "/"; }
|
|
181
204
|
// Arm the sidebar's upload box, then drop files anywhere on the window (a drop = a paste into the focused pane).
|
|
182
205
|
bind "Ctrl Alt u" { MoveFocus "left"; MoveFocus "left"; MoveFocus "left"; WriteChars "U"; }
|
|
@@ -188,6 +211,24 @@ keybinds clear-defaults=true {
|
|
|
188
211
|
}
|
|
189
212
|
}
|
|
190
213
|
}
|
|
214
|
+
// Markdown preview (spec §5.4): the newest markdown file the focused lane named, in a floating pane that closes
|
|
215
|
+
// when the pager quits. Both Run forms parse on Zellij 0.45.1 (measured 2026-10-04, DEF-1453); the multi-line
|
|
216
|
+
// children are the pinned style, and test_ctrl_alt_m_is_the_multi_line_floating_run_block pins them. "lanes"
|
|
217
|
+
// is found on the view server's PATH
|
|
218
|
+
// (bin/lanes clean_env: this checkout's bin/), because this file is read in place, never rendered per host.
|
|
219
|
+
shared_except "locked" {
|
|
220
|
+
bind "Ctrl Alt m" {
|
|
221
|
+
Run "lanes" "preview" "--focused" {
|
|
222
|
+
floating true
|
|
223
|
+
close_on_exit true
|
|
224
|
+
name "Preview"
|
|
225
|
+
x "10%"
|
|
226
|
+
y "5%"
|
|
227
|
+
width "80%"
|
|
228
|
+
height "90%"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
191
232
|
shared_except "normal" "locked" {
|
|
192
233
|
bind "Alt left" { MoveFocusOrTab "left"; }
|
|
193
234
|
bind "Alt down" { MoveFocus "down"; }
|
|
@@ -199,7 +240,7 @@ keybinds clear-defaults=true {
|
|
|
199
240
|
bind "Alt [" { PreviousSwapLayout; }
|
|
200
241
|
bind "Alt ]" { NextSwapLayout; }
|
|
201
242
|
bind "Alt f" { ToggleFloatingPanes; }
|
|
202
|
-
bind "Ctrl g" { SwitchToMode "locked"; }
|
|
243
|
+
bind "Ctrl Alt g" { SwitchToMode "locked"; }
|
|
203
244
|
bind "Alt h" { MoveFocusOrTab "left"; }
|
|
204
245
|
bind "Alt i" { MoveTab "left"; }
|
|
205
246
|
bind "Alt j" { MoveFocus "down"; }
|
|
@@ -331,12 +372,80 @@ env {
|
|
|
331
372
|
FORCE_COLOR "3"
|
|
332
373
|
FORCE_HYPERLINK "1"
|
|
333
374
|
}
|
|
375
|
+
osc8_hyperlinks true
|
|
376
|
+
// BEGIN lanes tokens (generated by libexec/tokens.py --kdl; do not edit by hand)
|
|
377
|
+
themes {
|
|
378
|
+
lanes-dark {
|
|
379
|
+
text_unselected { base "#edf1f5"; background "#101318"; emphasis_0 "#e5b84a"; emphasis_1 "#6fd39a"; emphasis_2 "#b4e3bf"; emphasis_3 "#7fb3f2"; }
|
|
380
|
+
text_selected { base "#edf1f5"; background "#1a2721"; emphasis_0 "#e5b84a"; emphasis_1 "#6fd39a"; emphasis_2 "#b4e3bf"; emphasis_3 "#7fb3f2"; }
|
|
381
|
+
ribbon_selected { base "#101318"; background "#b4e3bf"; emphasis_0 "#7a1a12"; emphasis_1 "#101318"; emphasis_2 "#101318"; emphasis_3 "#101318"; }
|
|
382
|
+
ribbon_unselected { base "#9ca7b5"; background "#191e25"; emphasis_0 "#f28b7d"; emphasis_1 "#edf1f5"; emphasis_2 "#e5b84a"; emphasis_3 "#7fb3f2"; }
|
|
383
|
+
table_title { base "#b4e3bf"; background "#101318"; emphasis_0 "#e5b84a"; emphasis_1 "#6fd39a"; emphasis_2 "#b4e3bf"; emphasis_3 "#7fb3f2"; }
|
|
384
|
+
table_cell_selected { base "#edf1f5"; background "#1a2721"; emphasis_0 "#e5b84a"; emphasis_1 "#6fd39a"; emphasis_2 "#b4e3bf"; emphasis_3 "#7fb3f2"; }
|
|
385
|
+
table_cell_unselected { base "#edf1f5"; background "#101318"; emphasis_0 "#e5b84a"; emphasis_1 "#6fd39a"; emphasis_2 "#b4e3bf"; emphasis_3 "#7fb3f2"; }
|
|
386
|
+
list_selected { base "#edf1f5"; background "#1a2721"; emphasis_0 "#e5b84a"; emphasis_1 "#6fd39a"; emphasis_2 "#b4e3bf"; emphasis_3 "#7fb3f2"; }
|
|
387
|
+
list_unselected { base "#edf1f5"; background "#101318"; emphasis_0 "#e5b84a"; emphasis_1 "#6fd39a"; emphasis_2 "#b4e3bf"; emphasis_3 "#7fb3f2"; }
|
|
388
|
+
frame_selected { base "#b4e3bf"; background "#101318"; emphasis_0 "#e5b84a"; emphasis_1 "#6fd39a"; emphasis_2 "#7fb3f2"; emphasis_3 "#f28b7d"; }
|
|
389
|
+
frame_highlight { base "#e5b84a"; background "#101318"; emphasis_0 "#f28b7d"; emphasis_1 "#6fd39a"; emphasis_2 "#b4e3bf"; emphasis_3 "#7fb3f2"; }
|
|
390
|
+
exit_code_success { base "#6fd39a"; background "#101318"; emphasis_0 "#6fd39a"; emphasis_1 "#6fd39a"; emphasis_2 "#6fd39a"; emphasis_3 "#6fd39a"; }
|
|
391
|
+
exit_code_error { base "#f28b7d"; background "#101318"; emphasis_0 "#f28b7d"; emphasis_1 "#f28b7d"; emphasis_2 "#f28b7d"; emphasis_3 "#f28b7d"; }
|
|
392
|
+
multiplayer_user_colors { player_1 "#7fb3f2"; player_2 "#e5b84a"; player_3 "#6fd39a"; player_4 "#f28b7d"; player_5 "#b4e3bf"; player_6 "#c9a6ff"; player_7 "#6fd6d0"; player_8 "#f7cf7f"; player_9 "#8fe0a3"; player_10 "#ff9d96"; }
|
|
393
|
+
}
|
|
394
|
+
lanes-light {
|
|
395
|
+
text_unselected { base "#17241b"; background "#f3f5f2"; emphasis_0 "#8a5a00"; emphasis_1 "#1b7142"; emphasis_2 "#2f6b45"; emphasis_3 "#2456a8"; }
|
|
396
|
+
text_selected { base "#17241b"; background "#e5f0e6"; emphasis_0 "#8a5a00"; emphasis_1 "#1b7142"; emphasis_2 "#2f6b45"; emphasis_3 "#2456a8"; }
|
|
397
|
+
ribbon_selected { base "#ffffff"; background "#2f6b45"; emphasis_0 "#ffd3cc"; emphasis_1 "#ffffff"; emphasis_2 "#ffffff"; emphasis_3 "#ffffff"; }
|
|
398
|
+
ribbon_unselected { base "#56616d"; background "#ffffff"; emphasis_0 "#b7322a"; emphasis_1 "#17241b"; emphasis_2 "#8a5a00"; emphasis_3 "#2456a8"; }
|
|
399
|
+
table_title { base "#2f6b45"; background "#f3f5f2"; emphasis_0 "#8a5a00"; emphasis_1 "#1b7142"; emphasis_2 "#2f6b45"; emphasis_3 "#2456a8"; }
|
|
400
|
+
table_cell_selected { base "#17241b"; background "#e5f0e6"; emphasis_0 "#8a5a00"; emphasis_1 "#1b7142"; emphasis_2 "#2f6b45"; emphasis_3 "#2456a8"; }
|
|
401
|
+
table_cell_unselected { base "#17241b"; background "#f3f5f2"; emphasis_0 "#8a5a00"; emphasis_1 "#1b7142"; emphasis_2 "#2f6b45"; emphasis_3 "#2456a8"; }
|
|
402
|
+
list_selected { base "#17241b"; background "#e5f0e6"; emphasis_0 "#8a5a00"; emphasis_1 "#1b7142"; emphasis_2 "#2f6b45"; emphasis_3 "#2456a8"; }
|
|
403
|
+
list_unselected { base "#17241b"; background "#f3f5f2"; emphasis_0 "#8a5a00"; emphasis_1 "#1b7142"; emphasis_2 "#2f6b45"; emphasis_3 "#2456a8"; }
|
|
404
|
+
frame_selected { base "#2f6b45"; background "#f3f5f2"; emphasis_0 "#8a5a00"; emphasis_1 "#1b7142"; emphasis_2 "#2456a8"; emphasis_3 "#b7322a"; }
|
|
405
|
+
frame_highlight { base "#8a5a00"; background "#f3f5f2"; emphasis_0 "#b7322a"; emphasis_1 "#1b7142"; emphasis_2 "#2f6b45"; emphasis_3 "#2456a8"; }
|
|
406
|
+
exit_code_success { base "#1b7142"; background "#f3f5f2"; emphasis_0 "#1b7142"; emphasis_1 "#1b7142"; emphasis_2 "#1b7142"; emphasis_3 "#1b7142"; }
|
|
407
|
+
exit_code_error { base "#b7322a"; background "#f3f5f2"; emphasis_0 "#b7322a"; emphasis_1 "#b7322a"; emphasis_2 "#b7322a"; emphasis_3 "#b7322a"; }
|
|
408
|
+
multiplayer_user_colors { player_1 "#2456a8"; player_2 "#8a5a00"; player_3 "#1b7142"; player_4 "#b7322a"; player_5 "#2f6b45"; player_6 "#7a3fb8"; player_7 "#0f7b7b"; player_8 "#8f5a00"; player_9 "#1b7a38"; player_10 "#c2332a"; }
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
theme_dark "lanes-dark"
|
|
412
|
+
theme_light "lanes-light"
|
|
334
413
|
web_client {
|
|
335
414
|
font "monospace"
|
|
415
|
+
theme {
|
|
416
|
+
background "#101318"
|
|
417
|
+
foreground "#edf1f5"
|
|
418
|
+
black "#7d8896"
|
|
419
|
+
red "#f28b7d"
|
|
420
|
+
green "#6fd39a"
|
|
421
|
+
yellow "#e5b84a"
|
|
422
|
+
blue "#7fb3f2"
|
|
423
|
+
magenta "#c9a6ff"
|
|
424
|
+
cyan "#6fd6d0"
|
|
425
|
+
white "#9ca7b5"
|
|
426
|
+
bright_black "#8a95a3"
|
|
427
|
+
bright_red "#ff9d96"
|
|
428
|
+
bright_green "#8fe0a3"
|
|
429
|
+
bright_yellow "#f7cf7f"
|
|
430
|
+
bright_blue "#a5cbff"
|
|
431
|
+
bright_magenta "#dcc6ff"
|
|
432
|
+
bright_cyan "#9ae6e0"
|
|
433
|
+
bright_white "#edf1f5"
|
|
434
|
+
selection_background "#1a2721"
|
|
435
|
+
selection_foreground "#edf1f5"
|
|
436
|
+
}
|
|
336
437
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
438
|
+
// END lanes tokens
|
|
439
|
+
// Chrome: straight frames to match the sidebar's ─ rules; the session name is noise in a 14-tab bar; simplified_ui
|
|
440
|
+
// removes the compact-bar's Private-Use-Area chevrons — the one glyph the cockpit needed a Nerd Font for. Tabs
|
|
441
|
+
// render as [ Sakio ] ribbons: accent-on-dark for the active one (ribbon_selected), dim-on-raised for the rest.
|
|
442
|
+
ui {
|
|
443
|
+
pane_frames {
|
|
444
|
+
rounded_corners false
|
|
445
|
+
hide_session_name true
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
simplified_ui true
|
|
340
449
|
default_mode "normal"
|
|
341
450
|
mouse_mode true
|
|
342
451
|
copy_clipboard "system"
|
package/payload/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibelord/lanes",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"commit": "
|
|
5
|
-
"digest": "sha256:
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"commit": "a8c1fcdf78e24125b9d4c0b32618a16448445acc",
|
|
5
|
+
"digest": "sha256:162bffbf04a2c3a478fe1fce82b9953e609b5450edd106bba28e265df9ba464a",
|
|
6
6
|
"executables": [
|
|
7
7
|
"bin/lanes",
|
|
8
8
|
"client/install.sh",
|
|
@@ -12,15 +12,19 @@
|
|
|
12
12
|
"client/omarchy/discover.sh",
|
|
13
13
|
"container/lane",
|
|
14
14
|
"container/lane-run",
|
|
15
|
+
"container/lane-signal",
|
|
15
16
|
"holders/exec.sh",
|
|
17
|
+
"holders/ssh.sh",
|
|
16
18
|
"holders/tmux.sh",
|
|
17
19
|
"holders/zmx.sh",
|
|
18
20
|
"install.sh",
|
|
19
21
|
"install/enable-web.sh",
|
|
20
22
|
"install/host-install.sh",
|
|
23
|
+
"libexec/connection.py",
|
|
21
24
|
"libexec/sidebar",
|
|
22
25
|
"libexec/sidebar.py",
|
|
23
26
|
"libexec/statusd.py",
|
|
27
|
+
"libexec/tokens.py",
|
|
24
28
|
"libexec/view.sh",
|
|
25
29
|
"sudo/host-guard.sh",
|
|
26
30
|
"sudo/one-time-root.sh"
|