@jx0/jmux 0.3.3 → 0.3.5
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/src/__tests__/sidebar.test.ts +121 -0
- package/src/input-router.ts +7 -0
- package/src/main.ts +48 -9
- package/src/renderer.ts +2 -1
- package/src/sidebar.ts +173 -125
package/package.json
CHANGED
|
@@ -235,4 +235,125 @@ describe("Sidebar", () => {
|
|
|
235
235
|
}
|
|
236
236
|
expect(found).toBe(true);
|
|
237
237
|
});
|
|
238
|
+
|
|
239
|
+
test("scrolls to show active session when it overflows", () => {
|
|
240
|
+
// Height 10 = 2 header rows + 8 viewport rows
|
|
241
|
+
// Each session = 2 rows + 1 spacer = 3 rows, so 8 rows fits ~2.6 sessions
|
|
242
|
+
const sidebar = new Sidebar(SIDEBAR_WIDTH, 10);
|
|
243
|
+
sidebar.updateSessions(
|
|
244
|
+
makeSessions([
|
|
245
|
+
{ name: "a" },
|
|
246
|
+
{ name: "b" },
|
|
247
|
+
{ name: "c" },
|
|
248
|
+
{ name: "d" },
|
|
249
|
+
]),
|
|
250
|
+
);
|
|
251
|
+
// Activate last session and scroll to it
|
|
252
|
+
sidebar.setActiveSession("$3");
|
|
253
|
+
sidebar.scrollToActive();
|
|
254
|
+
const grid = sidebar.getGrid();
|
|
255
|
+
// "d" should be visible somewhere in the grid
|
|
256
|
+
let found = false;
|
|
257
|
+
for (let r = 2; r < 10; r++) {
|
|
258
|
+
const text = Array.from(
|
|
259
|
+
{ length: SIDEBAR_WIDTH },
|
|
260
|
+
(_, i) => grid.cells[r][i].char,
|
|
261
|
+
).join("");
|
|
262
|
+
if (text.includes("d")) {
|
|
263
|
+
found = true;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
expect(found).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("scrollBy moves viewport and clamps to bounds", () => {
|
|
271
|
+
const sidebar = new Sidebar(SIDEBAR_WIDTH, 10);
|
|
272
|
+
sidebar.updateSessions(
|
|
273
|
+
makeSessions([
|
|
274
|
+
{ name: "a" },
|
|
275
|
+
{ name: "b" },
|
|
276
|
+
{ name: "c" },
|
|
277
|
+
{ name: "d" },
|
|
278
|
+
]),
|
|
279
|
+
);
|
|
280
|
+
// First session visible at start
|
|
281
|
+
let grid = sidebar.getGrid();
|
|
282
|
+
const row2 = Array.from(
|
|
283
|
+
{ length: SIDEBAR_WIDTH },
|
|
284
|
+
(_, i) => grid.cells[2][i].char,
|
|
285
|
+
).join("");
|
|
286
|
+
expect(row2).toContain("a");
|
|
287
|
+
|
|
288
|
+
// Scroll down
|
|
289
|
+
sidebar.scrollBy(3);
|
|
290
|
+
grid = sidebar.getGrid();
|
|
291
|
+
// "a" should no longer be on row 2
|
|
292
|
+
const row2After = Array.from(
|
|
293
|
+
{ length: SIDEBAR_WIDTH },
|
|
294
|
+
(_, i) => grid.cells[2][i].char,
|
|
295
|
+
).join("");
|
|
296
|
+
expect(row2After).not.toContain("a");
|
|
297
|
+
|
|
298
|
+
// Scroll way past the top — should clamp to 0
|
|
299
|
+
sidebar.scrollBy(-100);
|
|
300
|
+
grid = sidebar.getGrid();
|
|
301
|
+
const row2Reset = Array.from(
|
|
302
|
+
{ length: SIDEBAR_WIDTH },
|
|
303
|
+
(_, i) => grid.cells[2][i].char,
|
|
304
|
+
).join("");
|
|
305
|
+
expect(row2Reset).toContain("a");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("shows scroll indicators when content overflows", () => {
|
|
309
|
+
const sidebar = new Sidebar(SIDEBAR_WIDTH, 10);
|
|
310
|
+
sidebar.updateSessions(
|
|
311
|
+
makeSessions([
|
|
312
|
+
{ name: "a" },
|
|
313
|
+
{ name: "b" },
|
|
314
|
+
{ name: "c" },
|
|
315
|
+
{ name: "d" },
|
|
316
|
+
]),
|
|
317
|
+
);
|
|
318
|
+
// At top: should show down indicator but not up
|
|
319
|
+
let grid = sidebar.getGrid();
|
|
320
|
+
expect(grid.cells[2][SIDEBAR_WIDTH - 1].char).not.toBe("\u25b2");
|
|
321
|
+
expect(grid.cells[9][SIDEBAR_WIDTH - 1].char).toBe("\u25bc");
|
|
322
|
+
|
|
323
|
+
// Scroll to middle: should show both
|
|
324
|
+
sidebar.scrollBy(3);
|
|
325
|
+
grid = sidebar.getGrid();
|
|
326
|
+
expect(grid.cells[2][SIDEBAR_WIDTH - 1].char).toBe("\u25b2");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("scrollToActive snaps back after manual scroll", () => {
|
|
330
|
+
const sidebar = new Sidebar(SIDEBAR_WIDTH, 10);
|
|
331
|
+
sidebar.updateSessions(
|
|
332
|
+
makeSessions([
|
|
333
|
+
{ name: "a" },
|
|
334
|
+
{ name: "b" },
|
|
335
|
+
{ name: "c" },
|
|
336
|
+
{ name: "d" },
|
|
337
|
+
]),
|
|
338
|
+
);
|
|
339
|
+
sidebar.setActiveSession("$0"); // "a" is active
|
|
340
|
+
// Scroll away from active session
|
|
341
|
+
sidebar.scrollBy(6);
|
|
342
|
+
// Snap back
|
|
343
|
+
sidebar.scrollToActive();
|
|
344
|
+
const grid = sidebar.getGrid();
|
|
345
|
+
// "a" should be visible
|
|
346
|
+
let found = false;
|
|
347
|
+
for (let r = 2; r < 10; r++) {
|
|
348
|
+
const text = Array.from(
|
|
349
|
+
{ length: SIDEBAR_WIDTH },
|
|
350
|
+
(_, i) => grid.cells[r][i].char,
|
|
351
|
+
).join("");
|
|
352
|
+
if (text.includes("a")) {
|
|
353
|
+
found = true;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
expect(found).toBe(true);
|
|
358
|
+
});
|
|
238
359
|
});
|
package/src/input-router.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface InputRouterOptions {
|
|
|
33
33
|
sidebarCols: number;
|
|
34
34
|
onPtyData: (data: string) => void;
|
|
35
35
|
onSidebarClick: (row: number) => void;
|
|
36
|
+
onSidebarScroll?: (delta: number) => void;
|
|
36
37
|
onSessionPrev?: () => void;
|
|
37
38
|
onSessionNext?: () => void;
|
|
38
39
|
}
|
|
@@ -65,6 +66,12 @@ export class InputRouter {
|
|
|
65
66
|
const mouse = parseSgrMouse(data);
|
|
66
67
|
if (mouse && this.sidebarVisible) {
|
|
67
68
|
if (mouse.x <= this.opts.sidebarCols) {
|
|
69
|
+
// Wheel events: button 64 = up, 65 = down
|
|
70
|
+
if ((mouse.button & 64) !== 0) {
|
|
71
|
+
const delta = (mouse.button & 1) ? 3 : -3;
|
|
72
|
+
this.opts.onSidebarScroll?.(delta);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
68
75
|
// Click in sidebar region (ignore drags — button bit 5 = motion)
|
|
69
76
|
if (!mouse.release && (mouse.button & 32) === 0) {
|
|
70
77
|
this.opts.onSidebarClick(mouse.y - 1); // 0-indexed row
|
package/src/main.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { homedir } from "os";
|
|
|
12
12
|
|
|
13
13
|
// --- CLI commands (run and exit before TUI) ---
|
|
14
14
|
|
|
15
|
-
const VERSION = "0.3.
|
|
15
|
+
const VERSION = "0.3.5";
|
|
16
16
|
|
|
17
17
|
const HELP = `jmux — a persistent session sidebar for tmux
|
|
18
18
|
|
|
@@ -255,6 +255,7 @@ async function switchSession(sessionId: string): Promise<void> {
|
|
|
255
255
|
sidebar.setActivity(sessionId, false);
|
|
256
256
|
currentSessionId = sessionId;
|
|
257
257
|
sidebar.setActiveSession(sessionId);
|
|
258
|
+
sidebar.scrollToActive();
|
|
258
259
|
renderFrame();
|
|
259
260
|
|
|
260
261
|
// Clear attention flag if set
|
|
@@ -298,6 +299,10 @@ const inputRouter = new InputRouter(
|
|
|
298
299
|
const session = sidebar.getSessionByRow(row);
|
|
299
300
|
if (session) switchSession(session.id);
|
|
300
301
|
},
|
|
302
|
+
onSidebarScroll: (delta) => {
|
|
303
|
+
sidebar.scrollBy(delta);
|
|
304
|
+
scheduleRender();
|
|
305
|
+
},
|
|
301
306
|
onSessionPrev: () => switchByOffset(-1),
|
|
302
307
|
onSessionNext: () => switchByOffset(1),
|
|
303
308
|
},
|
|
@@ -308,17 +313,51 @@ const inputRouter = new InputRouter(
|
|
|
308
313
|
|
|
309
314
|
let writesPending = 0;
|
|
310
315
|
|
|
311
|
-
// OSC 52 clipboard
|
|
312
|
-
const
|
|
316
|
+
// OSC 52 clipboard passthrough — buffers across split chunks
|
|
317
|
+
const OSC52_START = "\x1b]52;";
|
|
318
|
+
let osc52Pending = "";
|
|
319
|
+
|
|
320
|
+
function forwardOsc52(data: string): void {
|
|
321
|
+
let search = osc52Pending ? osc52Pending + data : data;
|
|
322
|
+
osc52Pending = "";
|
|
323
|
+
|
|
324
|
+
let pos = 0;
|
|
325
|
+
while (pos < search.length) {
|
|
326
|
+
const start = search.indexOf(OSC52_START, pos);
|
|
327
|
+
if (start < 0) break;
|
|
328
|
+
|
|
329
|
+
// Find terminator: BEL (\x07) or ST (\x1b\\)
|
|
330
|
+
let end = -1;
|
|
331
|
+
let endLen = 0;
|
|
332
|
+
for (let i = start + OSC52_START.length; i < search.length; i++) {
|
|
333
|
+
if (search[i] === "\x07") {
|
|
334
|
+
end = i;
|
|
335
|
+
endLen = 1;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (search[i] === "\x1b" && i + 1 < search.length && search[i + 1] === "\\") {
|
|
339
|
+
end = i;
|
|
340
|
+
endLen = 2;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
313
344
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
345
|
+
if (end >= 0) {
|
|
346
|
+
process.stdout.write(search.slice(start, end + endLen));
|
|
347
|
+
pos = end + endLen;
|
|
348
|
+
} else {
|
|
349
|
+
// Incomplete — buffer for next chunk (cap at 512KB to avoid leaks)
|
|
350
|
+
const remainder = search.slice(start);
|
|
351
|
+
if (remainder.length < 512 * 1024) {
|
|
352
|
+
osc52Pending = remainder;
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
320
355
|
}
|
|
321
356
|
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
pty.onData((data: string) => {
|
|
360
|
+
forwardOsc52(data);
|
|
322
361
|
|
|
323
362
|
writesPending++;
|
|
324
363
|
bridge.write(data).then(() => {
|
package/src/renderer.ts
CHANGED
package/src/sidebar.ts
CHANGED
|
@@ -9,10 +9,14 @@ const ACCENT_ATTRS: CellAttrs = {
|
|
|
9
9
|
fg: 2,
|
|
10
10
|
fgMode: ColorMode.Palette,
|
|
11
11
|
};
|
|
12
|
+
// #1e2a35 as packed RGB for subtle active row background
|
|
13
|
+
const ACTIVE_BG = (0x1e << 16) | (0x2a << 8) | 0x35;
|
|
12
14
|
const ACTIVE_MARKER_ATTRS: CellAttrs = {
|
|
13
15
|
fg: 2,
|
|
14
16
|
fgMode: ColorMode.Palette,
|
|
15
17
|
bold: true,
|
|
18
|
+
bg: ACTIVE_BG,
|
|
19
|
+
bgMode: ColorMode.RGB,
|
|
16
20
|
};
|
|
17
21
|
const ACTIVITY_ATTRS: CellAttrs = {
|
|
18
22
|
fg: 2,
|
|
@@ -24,9 +28,16 @@ const ATTENTION_ATTRS: CellAttrs = {
|
|
|
24
28
|
bold: true,
|
|
25
29
|
};
|
|
26
30
|
const ACTIVE_NAME_ATTRS: CellAttrs = {
|
|
27
|
-
fg:
|
|
31
|
+
fg: 2,
|
|
28
32
|
fgMode: ColorMode.Palette,
|
|
29
33
|
bold: true,
|
|
34
|
+
bg: ACTIVE_BG,
|
|
35
|
+
bgMode: ColorMode.RGB,
|
|
36
|
+
};
|
|
37
|
+
const ACTIVE_DETAIL_ATTRS: CellAttrs = {
|
|
38
|
+
dim: true,
|
|
39
|
+
bg: ACTIVE_BG,
|
|
40
|
+
bgMode: ColorMode.RGB,
|
|
30
41
|
};
|
|
31
42
|
const INACTIVE_NAME_ATTRS: CellAttrs = {
|
|
32
43
|
fg: 7,
|
|
@@ -145,6 +156,10 @@ function buildRenderPlan(sessions: SessionInfo[]): {
|
|
|
145
156
|
return { items, displayOrder };
|
|
146
157
|
}
|
|
147
158
|
|
|
159
|
+
function itemHeight(item: RenderItem): number {
|
|
160
|
+
return item.type === "session" ? 2 : 1;
|
|
161
|
+
}
|
|
162
|
+
|
|
148
163
|
// --- Sidebar class ---
|
|
149
164
|
|
|
150
165
|
export class Sidebar {
|
|
@@ -152,9 +167,11 @@ export class Sidebar {
|
|
|
152
167
|
private height: number;
|
|
153
168
|
private sessions: SessionInfo[] = [];
|
|
154
169
|
private activeSessionId: string | null = null;
|
|
170
|
+
private items: RenderItem[] = [];
|
|
155
171
|
private displayOrder: number[] = [];
|
|
156
172
|
private rowToSessionIndex = new Map<number, number>();
|
|
157
173
|
private activitySet = new Set<string>();
|
|
174
|
+
private scrollOffset = 0;
|
|
158
175
|
|
|
159
176
|
constructor(width: number, height: number) {
|
|
160
177
|
this.width = width;
|
|
@@ -163,8 +180,10 @@ export class Sidebar {
|
|
|
163
180
|
|
|
164
181
|
updateSessions(sessions: SessionInfo[]): void {
|
|
165
182
|
this.sessions = sessions;
|
|
166
|
-
const { displayOrder } = buildRenderPlan(sessions);
|
|
183
|
+
const { items, displayOrder } = buildRenderPlan(sessions);
|
|
184
|
+
this.items = items;
|
|
167
185
|
this.displayOrder = displayOrder;
|
|
186
|
+
this.clampScroll();
|
|
168
187
|
}
|
|
169
188
|
|
|
170
189
|
setActiveSession(id: string): void {
|
|
@@ -194,6 +213,41 @@ export class Sidebar {
|
|
|
194
213
|
resize(width: number, height: number): void {
|
|
195
214
|
this.width = width;
|
|
196
215
|
this.height = height;
|
|
216
|
+
this.clampScroll();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
scrollBy(delta: number): void {
|
|
220
|
+
this.scrollOffset += delta;
|
|
221
|
+
this.clampScroll();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
scrollToActive(): void {
|
|
225
|
+
if (!this.activeSessionId) return;
|
|
226
|
+
const viewportHeight = this.height - HEADER_ROWS;
|
|
227
|
+
let vRow = 0;
|
|
228
|
+
for (const item of this.items) {
|
|
229
|
+
const h = itemHeight(item);
|
|
230
|
+
if (item.type === "session") {
|
|
231
|
+
const session = this.sessions[item.sessionIndex];
|
|
232
|
+
if (session?.id === this.activeSessionId) {
|
|
233
|
+
if (vRow < this.scrollOffset) {
|
|
234
|
+
this.scrollOffset = vRow;
|
|
235
|
+
} else if (vRow + h > this.scrollOffset + viewportHeight) {
|
|
236
|
+
this.scrollOffset = vRow + h - viewportHeight;
|
|
237
|
+
}
|
|
238
|
+
this.clampScroll();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
vRow += h;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private clampScroll(): void {
|
|
247
|
+
const totalRows = this.items.reduce((sum, item) => sum + itemHeight(item), 0);
|
|
248
|
+
const viewportHeight = this.height - HEADER_ROWS;
|
|
249
|
+
const maxOffset = Math.max(0, totalRows - viewportHeight);
|
|
250
|
+
this.scrollOffset = Math.max(0, Math.min(maxOffset, this.scrollOffset));
|
|
197
251
|
}
|
|
198
252
|
|
|
199
253
|
getGrid(): CellGrid {
|
|
@@ -204,152 +258,146 @@ export class Sidebar {
|
|
|
204
258
|
writeString(grid, 0, 1, "jmux", { ...ACCENT_ATTRS, bold: true });
|
|
205
259
|
writeString(grid, 1, 0, "\u2500".repeat(this.width), DIM_ATTRS);
|
|
206
260
|
|
|
207
|
-
const
|
|
208
|
-
let
|
|
261
|
+
const viewportHeight = this.height - HEADER_ROWS;
|
|
262
|
+
let vRow = 0;
|
|
263
|
+
let totalRows = 0;
|
|
209
264
|
|
|
210
|
-
for (const item of items) {
|
|
211
|
-
|
|
265
|
+
for (const item of this.items) {
|
|
266
|
+
const h = itemHeight(item);
|
|
267
|
+
const screenRow = HEADER_ROWS + vRow - this.scrollOffset;
|
|
212
268
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
writeString(grid, row, 1, label, GROUP_HEADER_ATTRS);
|
|
219
|
-
row++;
|
|
269
|
+
// Skip items entirely above viewport
|
|
270
|
+
if (screenRow + h <= HEADER_ROWS) {
|
|
271
|
+
vRow += h;
|
|
272
|
+
totalRows += h;
|
|
220
273
|
continue;
|
|
221
274
|
}
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
275
|
+
// Track total rows even after viewport
|
|
276
|
+
if (screenRow >= this.height) {
|
|
277
|
+
vRow += h;
|
|
278
|
+
totalRows += h;
|
|
225
279
|
continue;
|
|
226
280
|
}
|
|
227
281
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const nameRow = row;
|
|
233
|
-
const isActive = session.id === this.activeSessionId;
|
|
234
|
-
const hasActivity = this.activitySet.has(session.id);
|
|
235
|
-
|
|
236
|
-
if (item.grouped) {
|
|
237
|
-
// Grouped: two rows — name + window count, then subdirectory + branch
|
|
238
|
-
const detailRow = row + 1;
|
|
239
|
-
|
|
240
|
-
this.rowToSessionIndex.set(nameRow, sessionIdx);
|
|
241
|
-
if (detailRow < this.height) {
|
|
242
|
-
this.rowToSessionIndex.set(detailRow, sessionIdx);
|
|
282
|
+
if (item.type === "group-header") {
|
|
283
|
+
let label = item.label;
|
|
284
|
+
if (label.length > this.width - 2) {
|
|
285
|
+
label = label.slice(0, this.width - 3) + "\u2026";
|
|
243
286
|
}
|
|
287
|
+
writeString(grid, screenRow, 1, label, GROUP_HEADER_ATTRS);
|
|
288
|
+
} else if (item.type === "spacer") {
|
|
289
|
+
// nothing to render
|
|
290
|
+
} else {
|
|
291
|
+
this.renderSession(grid, screenRow, item);
|
|
292
|
+
}
|
|
244
293
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
writeString(grid, detailRow, 0, "\u258e", ACTIVE_MARKER_ATTRS);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
294
|
+
vRow += h;
|
|
295
|
+
totalRows += h;
|
|
296
|
+
}
|
|
251
297
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
298
|
+
// Scroll indicators
|
|
299
|
+
if (this.scrollOffset > 0) {
|
|
300
|
+
writeString(grid, HEADER_ROWS, this.width - 1, "\u25b2", DIM_ATTRS);
|
|
301
|
+
}
|
|
302
|
+
if (this.scrollOffset + viewportHeight < totalRows) {
|
|
303
|
+
writeString(grid, this.height - 1, this.width - 1, "\u25bc", DIM_ATTRS);
|
|
304
|
+
}
|
|
257
305
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const nameStart = 3;
|
|
261
|
-
const nameMaxLen = windowCountCol - 1 - nameStart;
|
|
262
|
-
let displayName = session.name;
|
|
263
|
-
if (displayName.length > nameMaxLen) {
|
|
264
|
-
displayName = displayName.slice(0, nameMaxLen - 1) + "\u2026";
|
|
265
|
-
}
|
|
306
|
+
return grid;
|
|
307
|
+
}
|
|
266
308
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
309
|
+
private renderSession(
|
|
310
|
+
grid: CellGrid,
|
|
311
|
+
nameRow: number,
|
|
312
|
+
item: Extract<RenderItem, { type: "session" }>,
|
|
313
|
+
): void {
|
|
314
|
+
const sessionIdx = item.sessionIndex;
|
|
315
|
+
const session = this.sessions[sessionIdx];
|
|
316
|
+
if (!session) return;
|
|
317
|
+
|
|
318
|
+
const detailRow = nameRow + 1;
|
|
319
|
+
const isActive = session.id === this.activeSessionId;
|
|
320
|
+
const hasActivity = this.activitySet.has(session.id);
|
|
321
|
+
|
|
322
|
+
// Map rows to session for click handling
|
|
323
|
+
this.rowToSessionIndex.set(nameRow, sessionIdx);
|
|
324
|
+
if (detailRow < this.height) {
|
|
325
|
+
this.rowToSessionIndex.set(detailRow, sessionIdx);
|
|
326
|
+
}
|
|
271
327
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
328
|
+
// Paint active background across both rows
|
|
329
|
+
if (isActive) {
|
|
330
|
+
const bgFill = " ".repeat(this.width);
|
|
331
|
+
const bgAttrs: CellAttrs = { bg: ACTIVE_BG, bgMode: ColorMode.RGB };
|
|
332
|
+
writeString(grid, nameRow, 0, bgFill, bgAttrs);
|
|
333
|
+
writeString(grid, detailRow, 0, bgFill, bgAttrs);
|
|
334
|
+
}
|
|
275
335
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (branch.length > maxLen) {
|
|
282
|
-
branch = branch.slice(0, maxLen - 1) + "\u2026";
|
|
283
|
-
}
|
|
284
|
-
writeString(grid, detailRow, detailStart, branch, DIM_ATTRS);
|
|
285
|
-
}
|
|
336
|
+
// Active marker
|
|
337
|
+
if (isActive) {
|
|
338
|
+
writeString(grid, nameRow, 0, "\u258e", ACTIVE_MARKER_ATTRS);
|
|
339
|
+
writeString(grid, detailRow, 0, "\u258e", ACTIVE_MARKER_ATTRS);
|
|
340
|
+
}
|
|
286
341
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
342
|
+
// Indicator
|
|
343
|
+
if (session.attention) {
|
|
344
|
+
writeString(grid, nameRow, 1, "!", ATTENTION_ATTRS);
|
|
345
|
+
} else if (hasActivity) {
|
|
346
|
+
writeString(grid, nameRow, 1, "\u25CF", ACTIVITY_ATTRS);
|
|
347
|
+
}
|
|
291
348
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
349
|
+
// Name row: name + window count
|
|
350
|
+
const windowCountStr = `${session.windowCount}w`;
|
|
351
|
+
const windowCountCol = this.width - windowCountStr.length - 1;
|
|
352
|
+
const nameStart = 3;
|
|
353
|
+
const nameMaxLen = windowCountCol - 1 - nameStart;
|
|
354
|
+
let displayName = session.name;
|
|
355
|
+
if (displayName.length > nameMaxLen) {
|
|
356
|
+
displayName = displayName.slice(0, nameMaxLen - 1) + "\u2026";
|
|
357
|
+
}
|
|
296
358
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
}
|
|
359
|
+
const nameAttrs: CellAttrs = isActive
|
|
360
|
+
? { ...ACTIVE_NAME_ATTRS }
|
|
361
|
+
: { ...INACTIVE_NAME_ATTRS };
|
|
362
|
+
writeString(grid, nameRow, nameStart, displayName, nameAttrs);
|
|
303
363
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
364
|
+
const wcAttrs: CellAttrs = isActive
|
|
365
|
+
? { ...DIM_ATTRS, bg: ACTIVE_BG, bgMode: ColorMode.RGB }
|
|
366
|
+
: DIM_ATTRS;
|
|
367
|
+
if (windowCountCol > nameStart) {
|
|
368
|
+
writeString(grid, nameRow, windowCountCol, windowCountStr, wcAttrs);
|
|
369
|
+
}
|
|
309
370
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
371
|
+
// Detail line
|
|
372
|
+
const detailAttrs: CellAttrs = isActive ? ACTIVE_DETAIL_ATTRS : DIM_ATTRS;
|
|
373
|
+
if (item.grouped) {
|
|
374
|
+
if (session.gitBranch) {
|
|
375
|
+
const detailStart = 3;
|
|
376
|
+
const maxLen = this.width - detailStart - 1;
|
|
377
|
+
let branch = session.gitBranch;
|
|
378
|
+
if (branch.length > maxLen) {
|
|
379
|
+
branch = branch.slice(0, maxLen - 1) + "\u2026";
|
|
317
380
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
381
|
+
writeString(grid, detailRow, detailStart, branch, detailAttrs);
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
const detailStart = 3;
|
|
385
|
+
let branchCols = 0;
|
|
386
|
+
if (session.gitBranch) {
|
|
387
|
+
const branchCol = this.width - session.gitBranch.length - 1;
|
|
388
|
+
if (branchCol > detailStart + 1) {
|
|
389
|
+
writeString(grid, detailRow, branchCol, session.gitBranch, detailAttrs);
|
|
390
|
+
branchCols = session.gitBranch.length + 2;
|
|
326
391
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const branchCol = this.width - session.gitBranch.length - 1;
|
|
334
|
-
if (branchCol > detailStart + 1) {
|
|
335
|
-
writeString(grid, detailRow, branchCol, session.gitBranch, DIM_ATTRS);
|
|
336
|
-
branchCols = session.gitBranch.length + 2;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (session.directory !== undefined) {
|
|
340
|
-
const dirMaxLen = this.width - detailStart - branchCols - 1;
|
|
341
|
-
let displayDir = session.directory;
|
|
342
|
-
if (displayDir.length > dirMaxLen) {
|
|
343
|
-
displayDir = displayDir.slice(0, dirMaxLen - 1) + "\u2026";
|
|
344
|
-
}
|
|
345
|
-
writeString(grid, detailRow, detailStart, displayDir, DIM_ATTRS);
|
|
346
|
-
}
|
|
392
|
+
}
|
|
393
|
+
if (session.directory !== undefined) {
|
|
394
|
+
const dirMaxLen = this.width - detailStart - branchCols - 1;
|
|
395
|
+
let displayDir = session.directory;
|
|
396
|
+
if (displayDir.length > dirMaxLen) {
|
|
397
|
+
displayDir = displayDir.slice(0, dirMaxLen - 1) + "\u2026";
|
|
347
398
|
}
|
|
348
|
-
|
|
349
|
-
row += 2;
|
|
399
|
+
writeString(grid, detailRow, detailStart, displayDir, detailAttrs);
|
|
350
400
|
}
|
|
351
401
|
}
|
|
352
|
-
|
|
353
|
-
return grid;
|
|
354
402
|
}
|
|
355
403
|
}
|