@vibelord/lanes 1.1.0 → 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 +47 -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 +625 -15
- package/payload/lanes/docs/DESIGN.md +122 -44
- package/payload/lanes/docs/INSTALL.md +157 -17
- package/payload/lanes/docs/RUNBOOK.md +653 -81
- package/payload/lanes/docs/SHARING.md +3 -2
- package/payload/lanes/docs/USERGUIDE.md +792 -140
- 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 +207 -25
- package/payload/lanes/web/browser.js +3 -5
- package/payload/lanes/web/mobile.css +17 -17
- package/payload/lanes/web/mobile.js +128 -76
- package/payload/lanes/workspaces.example/README.md +3 -1
- package/payload/lanes/zellij/config.kdl +106 -56
- package/payload/manifest.json +5 -3
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* Lanes browser shell; see web/README.md, Code notes */
|
|
2
1
|
function laneShellMode({userAgent, coarse, width, height} = {}) {
|
|
3
2
|
if (/iPhone|iPod|Android.*Mobile/i.test(userAgent) || (coarse && (width <= 600 || height <= 500))) return 'phone';
|
|
4
3
|
return coarse ? 'tablet' : 'desktop';
|
|
@@ -11,7 +10,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
11
10
|
width: innerWidth, height: innerHeight});
|
|
12
11
|
const root = document.documentElement;
|
|
13
12
|
// BEGIN lanes tokens (generated by libexec/tokens.py --js; do not edit by hand)
|
|
14
|
-
const LANES_ANSI = {dark: ['#
|
|
13
|
+
const LANES_ANSI = {dark: ['#8792a0', '#ff7b6e', '#5fdc8a', '#ffc233', '#6fb6ff', '#c58bff', '#3fd9cf', '#a6b1be', '#97a3b1', '#ff9a8e', '#86f0ad', '#ffd866', '#98cbff', '#dcb2ff', '#7deee6', '#f2f5f8'], light: ['#2f3b34', '#c41c1c', '#0d7338', '#a04d00', '#1a56c4', '#8228c4', '#006f73', '#4a5661', '#55625b', '#d0221a', '#0e7a3a', '#a85200', '#1f5fd8', '#9133d0', '#00797c', '#111a14']};
|
|
15
14
|
// END lanes tokens
|
|
16
15
|
root.dataset.lanesShell = mode;
|
|
17
16
|
if (mode !== 'phone') {
|
|
@@ -23,7 +22,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
23
22
|
root.classList.add('lanes-mobile');
|
|
24
23
|
if (mode === 'tablet') {
|
|
25
24
|
root.classList.add('lanes-tablet');
|
|
26
|
-
try {localStorage.getItem('zellij:mobile-ui') || localStorage.setItem('zellij:mobile-ui', 'on');} catch
|
|
25
|
+
try {localStorage.getItem('zellij:mobile-ui') || localStorage.setItem('zellij:mobile-ui', 'on');} catch {}
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
const elementFocus = HTMLElement.prototype.focus;
|
|
@@ -38,9 +37,9 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
38
37
|
const theme = localStorage.getItem('zellij:mobile-theme') || 'dark';
|
|
39
38
|
localStorage.setItem('zellij:mobile-theme', theme);
|
|
40
39
|
root.dataset.zjTheme = theme;
|
|
41
|
-
} catch
|
|
40
|
+
} catch {}
|
|
42
41
|
const icons = {
|
|
43
|
-
workspaces:
|
|
42
|
+
workspaces: [[3, 3], [14, 3], [3, 14], [14, 14]].map(([x, y]) => `<rect x="${x}" y="${y}" width="7" height="7" rx="2"/>`).join(''),
|
|
44
43
|
lanes: '<path d="M8 5h13M8 12h13M8 19h13"/><path d="M3 5h.01M3 12h.01M3 19h.01"/>',
|
|
45
44
|
panes: '<rect x="3" y="4" width="18" height="16" rx="3"/><path d="M12 4v16"/>',
|
|
46
45
|
keyboard: '<rect x="2" y="5" width="20" height="14" rx="3"/><path d="M6 9h.01M10 9h.01M14 9h.01M18 9h.01M6 13h.01M10 13h.01M14 13h.01M18 13h.01M8 16h8"/>',
|
|
@@ -49,6 +48,8 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
49
48
|
arrow: '<path d="m9 5 7 7-7 7"/>',
|
|
50
49
|
check: '<path d="m5 12 4 4L19 6"/>',
|
|
51
50
|
search: '<circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/>',
|
|
51
|
+
copy: '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5h10"/>',
|
|
52
|
+
paste: '<rect x="5" y="5" width="14" height="16" rx="2"/><path d="M9 3h6v4H9z"/>',
|
|
52
53
|
};
|
|
53
54
|
const svg = name => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icons[name]}</svg>`;
|
|
54
55
|
const make = (tag, cls, text) => {
|
|
@@ -61,33 +62,34 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
61
62
|
const button = (label, icon, action) => {
|
|
62
63
|
const el = make('button', 'lanes-button');
|
|
63
64
|
el.type = 'button';
|
|
64
|
-
el
|
|
65
|
+
aria(el, label);
|
|
65
66
|
el.innerHTML = svg(icon);
|
|
66
67
|
el.append(make('span', '', label));
|
|
67
68
|
el.addEventListener('click', action);
|
|
68
69
|
return el;
|
|
69
70
|
};
|
|
70
|
-
let shell, heading, subtitle, dock, sheet, list, search, count, lanesButton, typingField, quickRow, quickOn;
|
|
71
|
-
let laneFilterBar, laneFilterInput, laneFilterPrev = '';
|
|
72
|
-
let api, term, previousCards = '', sheetOpen = false, returnFocus;
|
|
71
|
+
let shell, heading, subtitle, dock, sheet, list, search, count, lanesButton, panesButton, pasteKey, typingField, quickRow, quickOn;
|
|
72
|
+
let laneFilterBar, laneFilterInput, laneFilterPrev = '', sentAt = 0, filterTab, owed = new Set();
|
|
73
|
+
let api, term, previousCards = '', sheetOpen = false, returnFocus, menuWas, hintN = 0, timer;
|
|
74
|
+
const queue = [];
|
|
73
75
|
let fontSize = 16;
|
|
74
76
|
try {
|
|
75
77
|
const stored = Number(localStorage.getItem('lanes:font-size'));
|
|
76
78
|
if (stored >= 14 && stored <= 24) fontSize = stored;
|
|
77
|
-
} catch
|
|
79
|
+
} catch {}
|
|
78
80
|
const state = () => api?.getState();
|
|
79
81
|
const data = () => state()?.data;
|
|
80
82
|
const nativeClick = selector => document.querySelector(selector)?.click();
|
|
81
83
|
const resize = () => window.dispatchEvent(new Event('zellij:rendering-resize'));
|
|
84
|
+
const kbd = () => window.__zjSoftKbdCapture, aria = (el, label) => el.setAttribute('aria-label', label);
|
|
82
85
|
|
|
83
86
|
function viewport() {
|
|
84
87
|
const vv = window.visualViewport;
|
|
85
|
-
root.style.setProperty(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
root.style.setProperty('--lanes-offset-left', `${vv?.offsetLeft ?? 0}px`);
|
|
88
|
+
const px = (k, v) => root.style.setProperty(`--lanes-${k}`, `${v}px`);
|
|
89
|
+
px('vh', vv?.height ?? innerHeight); px('vw', vv?.width ?? innerWidth);
|
|
90
|
+
px('offset-top', vv?.offsetTop ?? 0); px('offset-left', vv?.offsetLeft ?? 0);
|
|
89
91
|
const occluded = !!vv && innerHeight - vv.height - vv.offsetTop > 120;
|
|
90
|
-
const focused =
|
|
92
|
+
const focused = kbd()?.isFocused || document.activeElement?.matches('input, textarea');
|
|
91
93
|
setKeyboard(occluded && focused);
|
|
92
94
|
}
|
|
93
95
|
function setKeyboard(visible) {
|
|
@@ -95,10 +97,10 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
95
97
|
root.classList.toggle('lanes-keyboard', !!visible);
|
|
96
98
|
resize();
|
|
97
99
|
}
|
|
98
|
-
typingField?.classList.toggle('is-typing', !!
|
|
100
|
+
typingField?.classList.toggle('is-typing', !!kbd()?.isFocused);
|
|
99
101
|
}
|
|
100
102
|
function buildTypingField() {
|
|
101
|
-
const capture =
|
|
103
|
+
const capture = kbd()?.element;
|
|
102
104
|
const host = document.getElementById('zj-mobile-capture-host');
|
|
103
105
|
if (!capture || !host) return null;
|
|
104
106
|
const field = make('div', 'lanes-terminal-input');
|
|
@@ -107,16 +109,23 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
107
109
|
label.append(make('span', '', 'Type here'));
|
|
108
110
|
label.setAttribute('aria-hidden', 'true');
|
|
109
111
|
host.removeAttribute('aria-hidden');
|
|
110
|
-
|
|
112
|
+
const fill = 'position:absolute;inset:0;width:100%;height:100%;opacity:1;pointer-events:auto;';
|
|
113
|
+
host.style.cssText = fill;
|
|
111
114
|
capture.removeAttribute('aria-hidden');
|
|
112
|
-
capture
|
|
115
|
+
aria(capture, 'Type in terminal');
|
|
116
|
+
for (const a of ['type text', 'autocomplete off', 'autocapitalize none']) capture.setAttribute(...a.split(' '));
|
|
113
117
|
capture.tabIndex = 0;
|
|
114
|
-
capture.style.cssText = '
|
|
115
|
-
'
|
|
116
|
-
'caret-color:transparent;outline:none;pointer-events:auto;user-select:text;-webkit-user-select:text;';
|
|
118
|
+
capture.style.cssText = fill + 'box-sizing:border-box;font-size:16px;border:0;border-radius:12px;padding:0;margin:0;' +
|
|
119
|
+
'background:transparent;color:transparent;caret-color:transparent;outline:none;user-select:text;-webkit-user-select:text;';
|
|
117
120
|
field.append(label, host);
|
|
118
|
-
capture.addEventListener('
|
|
119
|
-
|
|
121
|
+
capture.addEventListener('beforeinput', ev => {
|
|
122
|
+
const t = ev.data ?? ev.dataTransfer?.getData('text'), r = ev.getTargetRanges?.()[0];
|
|
123
|
+
if (ev.cancelable && t && (ev.inputType == 'insertReplacementText' || ev.inputType == 'insertText' && t.length > 1)) {
|
|
124
|
+
ev.preventDefault();
|
|
125
|
+
send('\x7f'.repeat(r ? r.endOffset - r.startOffset : capture.selectionEnd - capture.selectionStart) + t);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
capture.addEventListener('paste', ev => { ev.preventDefault(); paste(ev.clipboardData.getData('text')); });
|
|
120
129
|
return field;
|
|
121
130
|
}
|
|
122
131
|
function focusTab(tab) {
|
|
@@ -153,7 +162,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
153
162
|
}
|
|
154
163
|
function openSheet() {
|
|
155
164
|
returnFocus = document.activeElement;
|
|
156
|
-
|
|
165
|
+
kbd()?.element.blur();
|
|
157
166
|
document.activeElement?.blur();
|
|
158
167
|
sheetOpen = true;
|
|
159
168
|
sheet.hidden = false;
|
|
@@ -188,7 +197,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
188
197
|
const active = item.name === activeName;
|
|
189
198
|
const card = make('button', `lanes-workspace${active ? ' is-current' : ''}`);
|
|
190
199
|
card.type = 'button';
|
|
191
|
-
card
|
|
200
|
+
aria(card, `${item.name}${active ? ', current workspace' : ''}`);
|
|
192
201
|
if (active) card.setAttribute('aria-current', 'true');
|
|
193
202
|
const monogram = make('span', 'lanes-monogram', Array.from(item.name)[0]?.toUpperCase() || 'L');
|
|
194
203
|
const copy = make('span', 'lanes-workspace-copy');
|
|
@@ -211,22 +220,26 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
211
220
|
const brand = make('img', 'lanes-brand');
|
|
212
221
|
brand.src = '/lanes-assets/icon.svg'; brand.alt = 'Lanes';
|
|
213
222
|
const context = make('button', 'lanes-context');
|
|
214
|
-
context.type = 'button'; context
|
|
223
|
+
context.type = 'button'; aria(context, 'Choose workspace');
|
|
215
224
|
heading = make('strong', '', 'Lanes'); subtitle = make('span', '', 'Connecting…');
|
|
216
225
|
context.append(heading, subtitle); context.addEventListener('click', openSheet);
|
|
217
|
-
|
|
218
|
-
more
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
addEventListener('pointerdown', () => { menuWas = document.querySelector('.zj-mobile-menu.zj-open'); }, true);
|
|
227
|
+
const more = button('More options', 'more', () => menuWas ? menuWas = null : nativeClick('.zj-hamburger'));
|
|
228
|
+
const copyKey = button('Copy screen', 'copy', () => clip(c => c.writeText(screen()), () => hint('Copied')));
|
|
229
|
+
pasteKey = button('Paste', 'paste', () => clip(c => c.readText(), t => paste(t) && hint('Pasted')));
|
|
230
|
+
for (const b of [copyKey, pasteKey, more]) b.classList.add('lanes-more');
|
|
231
|
+
for (const b of [copyKey, pasteKey]) b.addEventListener('mousedown', ev => ev.preventDefault());
|
|
232
|
+
header.append(brand, context, copyKey, pasteKey, more);
|
|
233
|
+
dock = make('nav', 'lanes-dock'); aria(dock, 'Workspace controls');
|
|
221
234
|
const spaces = button('Workspaces', 'workspaces', openSheet);
|
|
222
235
|
lanesButton = button('Lanes', 'lanes', () => {
|
|
223
236
|
const picker = pickerTab((currentTab()?.name || '').replace(/ ☰$/, ''));
|
|
224
237
|
if (picker) focusTab(picker); else openSheet();
|
|
225
238
|
});
|
|
226
|
-
|
|
239
|
+
panesButton = button('Panes', 'panes', () => nativeClick('.zj-pane'));
|
|
227
240
|
typingField = buildTypingField();
|
|
228
241
|
laneFilterBar = buildLaneFilterBar();
|
|
229
|
-
dock.append(spaces, lanesButton,
|
|
242
|
+
dock.append(spaces, lanesButton, panesButton, laneFilterBar, typingField);
|
|
230
243
|
sheet = make('div', 'lanes-sheet'); sheet.hidden = true;
|
|
231
244
|
sheet.setAttribute('role', 'dialog'); sheet.setAttribute('aria-labelledby', 'lanes-sheet-title');
|
|
232
245
|
const panel = make('section', 'lanes-sheet-panel');
|
|
@@ -240,7 +253,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
240
253
|
searchWrap.innerHTML = svg('search');
|
|
241
254
|
search = make('input'); search.type = 'search'; search.placeholder = 'Find a workspace';
|
|
242
255
|
search.autocomplete = 'off'; search.spellcheck = false;
|
|
243
|
-
search
|
|
256
|
+
aria(search, 'Find a workspace');
|
|
244
257
|
search.addEventListener('input', renderCards); searchWrap.append(search);
|
|
245
258
|
list = make('div', 'lanes-workspaces');
|
|
246
259
|
const sessionButton = make('button', 'lanes-session-link', 'Switch session');
|
|
@@ -266,65 +279,94 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
266
279
|
installFontControls();
|
|
267
280
|
resize();
|
|
268
281
|
}
|
|
269
|
-
function
|
|
270
|
-
|
|
282
|
+
function send(...frames) {
|
|
283
|
+
const f = kbd()?.sendFn;
|
|
284
|
+
queue.push(...frames);
|
|
285
|
+
if (f) try {
|
|
286
|
+
while (queue.length) {
|
|
287
|
+
if (queue[0] !== null) { f(queue.shift()); sentAt = Date.now(); }
|
|
288
|
+
else if (Date.now() - sentAt < 60) return arm();
|
|
289
|
+
else queue.shift();
|
|
290
|
+
}
|
|
291
|
+
} catch { queue.length = 0; }
|
|
292
|
+
}
|
|
293
|
+
const paste = t => currentTab()?.name.endsWith(' ☰') ? hint('Open a lane to paste') :
|
|
294
|
+
(send('\x1b[200~' + t.replace(/\x1b+(?=\[201~)/g, '').replace(/\r?\n/g, '\r') + '\x1b[201~'), 1);
|
|
295
|
+
function applyLaneFilter(value) {
|
|
296
|
+
const q = [...new TextDecoder().decode(new TextEncoder().encode(value).filter(b => b > 31 && b != 127))];
|
|
297
|
+
const p = [...laneFilterPrev], s = q.join('');
|
|
298
|
+
if (s === laneFilterPrev) return;
|
|
299
|
+
let i = 0;
|
|
300
|
+
while (i < p.length && p[i] === q[i]) i++;
|
|
301
|
+
laneFilterPrev = s;
|
|
302
|
+
if (!s) { clearLaneFilter(); refresh(); }
|
|
303
|
+
else if (!p.length) send('\x1b', null, '/', s);
|
|
304
|
+
else send('\x7f'.repeat(p.length - i) + q.slice(i).join(''));
|
|
305
|
+
}
|
|
306
|
+
async function clip(f, ok) {
|
|
307
|
+
try { ok(await f(navigator.clipboard)); } catch { hint('Clipboard blocked'); }
|
|
271
308
|
}
|
|
272
|
-
function
|
|
273
|
-
|
|
274
|
-
|
|
309
|
+
function screen() {
|
|
310
|
+
if (term.hasSelection()) return term.getSelection();
|
|
311
|
+
const b = term.buffer.active, rows = [];
|
|
312
|
+
for (let i = 0; i < term.rows; i++) rows.push(b.getLine(b.viewportY + i)?.translateToString(true));
|
|
313
|
+
return rows.join('\n').trimEnd();
|
|
275
314
|
}
|
|
315
|
+
function hint(s) { text(subtitle, s); hintN = 8; arm(); }
|
|
276
316
|
function buildQuickRow() {
|
|
277
317
|
const row = make('div', 'lanes-quick');
|
|
278
318
|
row.hidden = true;
|
|
279
|
-
for (const [label,
|
|
280
|
-
['^C',
|
|
281
|
-
const
|
|
319
|
+
for (const [label, key, name, hold] of [['⏎', '\r', 'Enter'], ['y'], ['n', 0, 0, 1], ['Esc', '\x1b'],
|
|
320
|
+
['^C', '\x03', 'Control C', 1], ['↑', '\x1b[A', 'Up'], ['↓', '\x1b[B', 'Down']]) {
|
|
321
|
+
const b = make('button', hold ? 'is-destructive' : '', label);
|
|
282
322
|
let down = -1;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
323
|
+
b.type = 'button'; if (name) aria(b, name);
|
|
324
|
+
b.addEventListener('mousedown', ev => ev.preventDefault());
|
|
325
|
+
b.addEventListener('pointerdown', ev => { down = ev.timeStamp; });
|
|
326
|
+
for (const t of ['pointercancel', 'pointerleave']) b.addEventListener(t, () => { down = -1; });
|
|
327
|
+
b.addEventListener('pointerup', ev => {
|
|
328
|
+
if (down >= 0) !hold || ev.timeStamp - down >= 400 ? send(key || label) : hint('Hold to send');
|
|
288
329
|
down = -1;
|
|
289
330
|
});
|
|
290
|
-
row.append(
|
|
331
|
+
row.append(b);
|
|
291
332
|
}
|
|
292
333
|
return row;
|
|
293
334
|
}
|
|
294
335
|
function clearLaneFilter() {
|
|
295
|
-
|
|
296
|
-
laneFilterPrev = '';
|
|
297
|
-
if (laneFilterInput) laneFilterInput.value = '';
|
|
336
|
+
owed.add(filterTab);
|
|
337
|
+
laneFilterPrev = laneFilterInput.value = '';
|
|
298
338
|
}
|
|
299
339
|
function buildLaneFilterBar() {
|
|
300
340
|
const bar = make('div', 'lanes-filter-bar');
|
|
301
|
-
bar
|
|
341
|
+
aria(bar, 'Filter lanes');
|
|
302
342
|
bar.hidden = true;
|
|
303
343
|
bar.innerHTML = svg('search');
|
|
304
344
|
laneFilterInput = make('input');
|
|
305
345
|
laneFilterInput.type = 'search'; laneFilterInput.placeholder = 'Filter lanes';
|
|
306
346
|
laneFilterInput.autocomplete = 'off'; laneFilterInput.spellcheck = false;
|
|
307
|
-
laneFilterInput
|
|
308
|
-
laneFilterInput.addEventListener('input', () =>
|
|
309
|
-
const q = laneFilterInput.value;
|
|
310
|
-
if (q !== laneFilterPrev) { applyLaneFilter(q); laneFilterPrev = q; }
|
|
311
|
-
});
|
|
347
|
+
aria(laneFilterInput, 'Filter lanes in this workspace');
|
|
348
|
+
laneFilterInput.addEventListener('input', () => applyLaneFilter(laneFilterInput.value));
|
|
312
349
|
laneFilterInput.addEventListener('keydown', ev => {
|
|
313
|
-
if (ev.key === 'Escape') {
|
|
314
|
-
if (ev.key
|
|
350
|
+
if (ev.key === 'Escape') { clearLaneFilter(); refresh(); }
|
|
351
|
+
if (/^E(scape|nter)$/.test(ev.key)) { ev.preventDefault(); laneFilterInput.blur(); }
|
|
315
352
|
});
|
|
316
353
|
bar.append(laneFilterInput);
|
|
317
354
|
return bar;
|
|
318
355
|
}
|
|
319
|
-
function updateLaneFilterBar(isPicker) {
|
|
320
|
-
if (!laneFilterBar) return;
|
|
356
|
+
function updateLaneFilterBar(tab, isPicker) {
|
|
321
357
|
laneFilterBar.hidden = !isPicker;
|
|
322
|
-
if (
|
|
358
|
+
if (tab !== filterTab) {
|
|
359
|
+
const wait = queue.indexOf(null);
|
|
360
|
+
if (wait >= 0) queue.length = wait;
|
|
361
|
+
if (laneFilterPrev) clearLaneFilter();
|
|
362
|
+
}
|
|
363
|
+
if (owed.delete(tab)) send('\x1b', null);
|
|
364
|
+
if (isPicker) filterTab = tab;
|
|
323
365
|
}
|
|
324
366
|
function applyFont() {
|
|
325
367
|
term.options.fontSize = fontSize;
|
|
326
368
|
term.options.lineHeight = 1.1;
|
|
327
|
-
try {localStorage.setItem('lanes:font-size', String(fontSize));} catch
|
|
369
|
+
try {localStorage.setItem('lanes:font-size', String(fontSize));} catch {}
|
|
328
370
|
text(document.getElementById('lanes-font-value'), `${fontSize} px`);
|
|
329
371
|
resize();
|
|
330
372
|
}
|
|
@@ -339,17 +381,23 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
339
381
|
return t;
|
|
340
382
|
}
|
|
341
383
|
const applyTheme = () => { if (term) term.options.theme = lanesTheme(); };
|
|
342
|
-
function
|
|
384
|
+
function menuRow(label) {
|
|
343
385
|
const row = make('div', 'lanes-font-controls');
|
|
344
|
-
row.append(make('span', '',
|
|
386
|
+
row.append(make('span', '', label));
|
|
387
|
+
document.querySelector('.zj-mobile-menu')?.prepend(row);
|
|
388
|
+
return row;
|
|
389
|
+
}
|
|
390
|
+
function installFontControls() {
|
|
391
|
+
const row = menuRow('Text size');
|
|
345
392
|
const smaller = make('button', '', 'A−'), larger = make('button', '', 'A+');
|
|
346
|
-
smaller
|
|
347
|
-
larger
|
|
393
|
+
aria(smaller, 'Smaller terminal text');
|
|
394
|
+
aria(larger, 'Larger terminal text');
|
|
348
395
|
const value = make('output', '', `${fontSize} px`); value.id = 'lanes-font-value';
|
|
349
396
|
smaller.addEventListener('click', () => {fontSize = Math.max(14, fontSize - 1); applyFont();});
|
|
350
397
|
larger.addEventListener('click', () => {fontSize = Math.min(24, fontSize + 1); applyFont();});
|
|
351
398
|
row.append(smaller, value, larger);
|
|
352
|
-
|
|
399
|
+
fetch('/enrol/site', {credentials: 'same-origin'}).then(r => r.json())
|
|
400
|
+
.then(({version: v}) => /^\d[\w.+-]{0,31}$/.test(v) && menuRow(`Lanes v${v}`)).catch(() => {});
|
|
353
401
|
}
|
|
354
402
|
function refresh() {
|
|
355
403
|
if (!api || !shell) return;
|
|
@@ -360,18 +408,21 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
360
408
|
const name = tab?.name.replace(/ ☰$/, '') || 'Lanes';
|
|
361
409
|
const picker = tab?.name.endsWith(' ☰');
|
|
362
410
|
const pane = data()?.panes.find(p => p.pane_id === data()?.active_pane?.pane_id && !p.is_plugin);
|
|
363
|
-
const
|
|
411
|
+
const title = pickerTitle(name), counts = parseCounts(title);
|
|
364
412
|
const chips = countParts(counts).map(([glyph, n]) => glyph + n).join(' ');
|
|
365
413
|
text(heading, name);
|
|
366
|
-
text(subtitle, picker ? chips || 'Choose a lane to continue' : pane?.title || 'Your workspace');
|
|
414
|
+
if (!hintN) text(subtitle, picker ? chips || 'Choose a lane to continue' : pane?.title || 'Your workspace');
|
|
367
415
|
lanesButton.setAttribute('aria-pressed', String(!!picker));
|
|
416
|
+
panesButton.hidden = pasteKey.hidden = !!picker;
|
|
368
417
|
const needs = counts ? counts.approval + counts.input : 0;
|
|
369
418
|
if (needs) lanesButton.dataset.count = String(needs); else delete lanesButton.dataset.count;
|
|
370
|
-
updateLaneFilterBar(picker);
|
|
371
|
-
|
|
419
|
+
updateLaneFilterBar(tab?.name, picker);
|
|
420
|
+
send();
|
|
421
|
+
const quick = active && (!picker || / · [›>] \w+$/.test(title) && 2);
|
|
372
422
|
if (quick !== quickOn) {
|
|
373
423
|
quickOn = quick;
|
|
374
424
|
quickRow.hidden = !quick;
|
|
425
|
+
for (const key of quickRow.children) key.hidden = quick > 1 && !['⏎', 'y', 'Esc'].includes(key.textContent);
|
|
375
426
|
root.style.setProperty('--lanes-quick', quick ? '48px' : '0px');
|
|
376
427
|
resize();
|
|
377
428
|
}
|
|
@@ -379,7 +430,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
379
430
|
renderCards();
|
|
380
431
|
}
|
|
381
432
|
function connect() {
|
|
382
|
-
if (api || !window.__zjMobileUi || !window.term || !
|
|
433
|
+
if (api || !window.__zjMobileUi || !window.term || !kbd()?.element) return;
|
|
383
434
|
if (!window.__zjMobileUi.getState()?.data?.tabs?.length) return;
|
|
384
435
|
api = window.__zjMobileUi; term = window.term;
|
|
385
436
|
build(); applyFont(); applyTheme();
|
|
@@ -396,7 +447,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
396
447
|
const token = document.getElementById('token');
|
|
397
448
|
if (!token || token.dataset.lanes) return;
|
|
398
449
|
token.dataset.lanes = 'true';
|
|
399
|
-
token.autocomplete = 'current-password'; token
|
|
450
|
+
token.autocomplete = 'current-password'; aria(token, 'Login token');
|
|
400
451
|
token.closest('.security-modal-content').querySelector('h3').textContent = 'Connect to Lanes';
|
|
401
452
|
token.closest('.security-modal-content').querySelector('.submit-btn').textContent = 'Connect';
|
|
402
453
|
}
|
|
@@ -412,6 +463,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
|
|
|
412
463
|
});
|
|
413
464
|
document.addEventListener('focusout', () => requestAnimationFrame(viewport));
|
|
414
465
|
new MutationObserver(() => {styleLogin(); connect();}).observe(document.body, {childList: true});
|
|
415
|
-
|
|
416
|
-
|
|
466
|
+
function tick() { connect(); send(); if (hintN && !--hintN) refresh(); if (api && !hintN && queue[0] !== null) timer = clearInterval(timer); }
|
|
467
|
+
function arm() { timer ||= setInterval(tick, 200); }
|
|
468
|
+
arm(); styleLogin(); connect();
|
|
417
469
|
})();
|
|
@@ -24,7 +24,9 @@ A lane is whatever the registry says it is: `lanes sync` (every two minutes, rea
|
|
|
24
24
|
terminal into a lane with a stable id `<board>--<slug of the terminal name>` (with the repository `prefix`). A
|
|
25
25
|
`displayNames` entry, which the sidebar `R` key and `lanes rename <lane-id> <name>` write into the repository's block
|
|
26
26
|
of `workspaces.<board>.json` (first write keeps a `.bak-<UTC>` copy beside it), changes only the shown name, never the
|
|
27
|
-
id.
|
|
27
|
+
id. The repository can also name a lane itself with `lanes.terminals[<name>].displayName` in its `terminals.json`;
|
|
28
|
+
the manifest's `displayNames` entry wins over it, and a `displayName` another lane on the board already answers to is
|
|
29
|
+
ignored (the lane keeps its title). Add a terminal to `.vscode/terminals.json` for an editor-defined lane.
|
|
28
30
|
Alternatively, use **+** beside a discovered session to register its existing binding and ID in a workspace
|
|
29
31
|
using the same container. Sync includes these local registrations; they never run a create command.
|
|
30
32
|
Repository declarations take precedence when they declare the same binding. Legacy Docker shells cannot be adopted.
|