dsh-neotui 0.0.3 → 0.0.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/panels.js +116 -4
- package/src/screen.js +8 -2
- package/src/views.js +63 -15
package/package.json
CHANGED
package/src/panels.js
CHANGED
|
@@ -272,7 +272,7 @@ export class WorkspacePanel extends Widget {
|
|
|
272
272
|
this.workspaces = items;
|
|
273
273
|
const tree = [];
|
|
274
274
|
for (const ws of items) {
|
|
275
|
-
tree.push({ depth: 0, name: `▣ ${ws.title}`, path: ws.path, isDir: true, open: false, ws: true });
|
|
275
|
+
tree.push({ depth: 0, name: `▣ ${ws.title}`, title: ws.title, path: ws.path, isDir: true, open: false, ws: true, workspaceId: ws.workspaceId });
|
|
276
276
|
}
|
|
277
277
|
this.tree = tree;
|
|
278
278
|
this.rebuildTree();
|
|
@@ -319,6 +319,25 @@ export class WorkspacePanel extends Widget {
|
|
|
319
319
|
} catch { node.children = []; }
|
|
320
320
|
}
|
|
321
321
|
onMouse(ev) {
|
|
322
|
+
if (ev.kind === "press" && ev.button === 2) {
|
|
323
|
+
// Right-click anywhere in the panel: workspace actions (add is the
|
|
324
|
+
// primary one; tree rows also offer move/rename).
|
|
325
|
+
const idx = this.treeScroll.scrollY + (ev.y - this.treeScroll.y);
|
|
326
|
+
const node = this.treeLinesNode(idx);
|
|
327
|
+
if (node?.ws) {
|
|
328
|
+
this.app.openMenu([
|
|
329
|
+
{ label: "添加工作区…", action: () => this.app.addWorkspace() },
|
|
330
|
+
{ label: "重命名工作区", action: () => this.app.renameWorkspace(node) },
|
|
331
|
+
{ label: "上移工作区", action: () => this.app.moveWorkspace(node, -1) },
|
|
332
|
+
{ label: "下移工作区", action: () => this.app.moveWorkspace(node, 1) },
|
|
333
|
+
], ev);
|
|
334
|
+
} else {
|
|
335
|
+
this.app.openMenu([
|
|
336
|
+
{ label: "添加工作区…", action: () => this.app.addWorkspace() },
|
|
337
|
+
], ev);
|
|
338
|
+
}
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
322
341
|
if (ev.x >= this.x + 1 && ev.x < this.x + Math.floor(this.w / 2)) {
|
|
323
342
|
const idx = this.treeScroll.scrollY + (ev.y - this.treeScroll.y);
|
|
324
343
|
const node = this.treeLinesNode(idx);
|
|
@@ -378,11 +397,11 @@ export class WorkspacePanel extends Widget {
|
|
|
378
397
|
}
|
|
379
398
|
}
|
|
380
399
|
render(screen) {
|
|
381
|
-
screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y + this.h - 1, " ", {});
|
|
400
|
+
screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y + this.h - 1, " ", { bg: T.BG2 });
|
|
382
401
|
const mid = this.x + Math.floor(this.w / 2);
|
|
383
|
-
screen.put(mid, this.y, "┬", { fg: T.BORDER });
|
|
402
|
+
screen.put(mid, this.y, "┬", { fg: T.BORDER, bg: T.BG2 });
|
|
384
403
|
screen.vline(mid, this.y + 1, this.y + this.h - 1);
|
|
385
|
-
screen.text(this.x + 1, this.y, ` 工作区 (${this.workspaces.length}) — 点击目录展开,/
|
|
404
|
+
screen.text(this.x + 1, this.y, ` 工作区 (${this.workspaces.length}) — 点击目录展开,/ 搜索文件,右键添加工作区`, { fg: K.DIM, bg: T.BG2 });
|
|
386
405
|
if (this.query) {
|
|
387
406
|
const results = [];
|
|
388
407
|
const walk = (nodes) => {
|
|
@@ -427,6 +446,99 @@ export class WorkspacePanel extends Widget {
|
|
|
427
446
|
}
|
|
428
447
|
}
|
|
429
448
|
|
|
449
|
+
// ---- DirPicker: yazi-style folder selection buffer ----
|
|
450
|
+
|
|
451
|
+
export class DirPicker extends Widget {
|
|
452
|
+
constructor(app, { startPath, onPick, onCancel }) {
|
|
453
|
+
const w = Math.min(60, app.screen.w - 4), h = Math.min(20, app.screen.h - 4);
|
|
454
|
+
super({ x: Math.floor((app.screen.w - w) / 2), y: Math.floor((app.screen.h - h) / 2), w, h });
|
|
455
|
+
this.app = app;
|
|
456
|
+
this.path = startPath ?? process.cwd();
|
|
457
|
+
this.onPick = onPick;
|
|
458
|
+
this.onCancel = onCancel;
|
|
459
|
+
this.entries = [];
|
|
460
|
+
this.sel = 0;
|
|
461
|
+
this.scroll = 0;
|
|
462
|
+
this.load();
|
|
463
|
+
}
|
|
464
|
+
load() {
|
|
465
|
+
try {
|
|
466
|
+
this.entries = readdirSync(this.path, { withFileTypes: true })
|
|
467
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
468
|
+
.map((d) => d.name)
|
|
469
|
+
.sort((a, b) => a.localeCompare(b));
|
|
470
|
+
} catch { this.entries = []; }
|
|
471
|
+
this.sel = 0;
|
|
472
|
+
this.scroll = 0;
|
|
473
|
+
}
|
|
474
|
+
#items() { return ["✓ 选择此目录", "..", ...this.entries]; }
|
|
475
|
+
render(screen) {
|
|
476
|
+
screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y + this.h - 1, " ", { bg: T.BG2 });
|
|
477
|
+
screen.box(this.x, this.y, this.x + this.w - 1, this.y + this.h - 1, { fg: K.ACCENT, bg: T.BG2 }, "选择文件夹");
|
|
478
|
+
screen.text(this.x + 2, this.y + 1, truncate("📁 " + this.path, this.w - 4), { fg: K.TXT, bg: T.BG2 });
|
|
479
|
+
const items = this.#items();
|
|
480
|
+
const lh = Math.max(1, this.h - 3);
|
|
481
|
+
if (this.sel < this.scroll) this.scroll = this.sel;
|
|
482
|
+
else if (this.sel >= this.scroll + lh) this.scroll = this.sel - lh + 1;
|
|
483
|
+
this.scroll = Math.max(0, Math.min(Math.max(0, items.length - lh), this.scroll));
|
|
484
|
+
for (let i = 0; i < lh; i++) {
|
|
485
|
+
const idx = this.scroll + i;
|
|
486
|
+
const it = items[idx];
|
|
487
|
+
const y = this.y + 2 + i;
|
|
488
|
+
if (it === undefined) { screen.hline(this.x + 1, this.x + this.w - 2, y, " ", { bg: T.BG2 }); continue; }
|
|
489
|
+
const sel = idx === this.sel;
|
|
490
|
+
const label = it === "✓ 选择此目录" ? it : it === ".." ? ".. (上级目录)" : "▸ " + it + "/";
|
|
491
|
+
const fg = it === "✓ 选择此目录" ? K.OK : it === ".." ? K.DIM : K.TXT;
|
|
492
|
+
screen.fillRect(this.x + 1, y, this.x + this.w - 2, y, " ", { bg: sel ? T.MENUSEL : T.BG2 });
|
|
493
|
+
screen.text(this.x + 2, y, truncate(label, this.w - 4), { fg: sel ? 0xffffff : fg, bg: sel ? T.MENUSEL : T.BG2, attrs: sel ? 1 : 0 });
|
|
494
|
+
}
|
|
495
|
+
screen.text(this.x + 2, this.y + this.h - 1, "↑↓/jk 移动 · Enter 进入/选择 · h/Backspace 上级 · Esc 取消", { fg: K.FAINT, bg: T.BG2 });
|
|
496
|
+
}
|
|
497
|
+
onKey(ev) {
|
|
498
|
+
if (ev.type !== "key") return false;
|
|
499
|
+
const items = this.#items();
|
|
500
|
+
switch (ev.name) {
|
|
501
|
+
case "up": this.sel = Math.max(0, this.sel - 1); return true;
|
|
502
|
+
case "down": this.sel = Math.min(items.length - 1, this.sel + 1); return true;
|
|
503
|
+
case "enter": {
|
|
504
|
+
const it = items[this.sel];
|
|
505
|
+
if (it === "✓ 选择此目录") { this.onPick?.(this.path); return true; }
|
|
506
|
+
if (it === "..") { this.path = dirname(this.path); this.load(); return true; }
|
|
507
|
+
this.path = join(this.path, it); this.load(); return true;
|
|
508
|
+
}
|
|
509
|
+
case "backspace": this.path = dirname(this.path); this.load(); return true;
|
|
510
|
+
case "char":
|
|
511
|
+
if (ev.key === "j" && !ev.ctrl) { this.sel = Math.min(items.length - 1, this.sel + 1); return true; }
|
|
512
|
+
if (ev.key === "k" && !ev.ctrl) { this.sel = Math.max(0, this.sel - 1); return true; }
|
|
513
|
+
if (ev.key === "h" && !ev.ctrl) { this.path = dirname(this.path); this.load(); return true; }
|
|
514
|
+
if (ev.key === "l" && !ev.ctrl) {
|
|
515
|
+
const it = items[this.sel];
|
|
516
|
+
if (it && it !== "✓ 选择此目录" && it !== "..") { this.path = join(this.path, it); this.load(); }
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
case "escape": this.onCancel?.(); return true;
|
|
521
|
+
}
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
onMouse(ev) {
|
|
525
|
+
if (ev.kind === "press" && ev.button === 0) {
|
|
526
|
+
const idx = this.scroll + (ev.y - this.y - 2);
|
|
527
|
+
const items = this.#items();
|
|
528
|
+
if (idx >= 0 && idx < items.length) {
|
|
529
|
+
const it = items[idx];
|
|
530
|
+
if (it === "✓ 选择此目录") { this.onPick?.(this.path); return true; }
|
|
531
|
+
if (it === "..") { this.path = dirname(this.path); this.load(); return true; }
|
|
532
|
+
this.path = join(this.path, it); this.load(); return true;
|
|
533
|
+
}
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
if (ev.kind === "wheel-up") { this.sel = Math.max(0, this.sel - 1); return true; }
|
|
537
|
+
if (ev.kind === "wheel-down") { this.sel = Math.min(this.#items().length - 1, this.sel + 1); return true; }
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
430
542
|
// ---- Trajectory view ----
|
|
431
543
|
|
|
432
544
|
export class TrajectoryPanel extends Widget {
|
package/src/screen.js
CHANGED
|
@@ -53,7 +53,13 @@ export class Screen {
|
|
|
53
53
|
const cell = this.cells[y][x];
|
|
54
54
|
const wide = wcwidth(ch.codePointAt(0)) === 2;
|
|
55
55
|
if (wide && x + 1 >= this.w) ch = " "; // clip wide char at the right edge (terminal wrap corruption)
|
|
56
|
-
cell.ch = ch; cell.fg = fg;
|
|
56
|
+
cell.ch = ch; cell.fg = fg;
|
|
57
|
+
// bg -1 = transparent: KEEP the cell's existing background (the layer
|
|
58
|
+
// underneath) instead of resetting it to the terminal default — this is
|
|
59
|
+
// what used to leave "unexplained dark blocks" wherever widgets drew
|
|
60
|
+
// text/fills without an explicit background.
|
|
61
|
+
if (bg !== -1) cell.bg = bg;
|
|
62
|
+
cell.attrs = attrs; cell.link = link;
|
|
57
63
|
cell.wide = wide && x + 1 < this.w;
|
|
58
64
|
if (cell.wide) {
|
|
59
65
|
const cont = this.cells[y][x + 1];
|
|
@@ -62,7 +68,7 @@ export class Screen {
|
|
|
62
68
|
const next = this.cells[y][x + 2];
|
|
63
69
|
next.ch = " "; next.wide = false; next.link = "";
|
|
64
70
|
}
|
|
65
|
-
cont.ch = ""; cont.fg = fg; cont.bg = bg; cont.attrs = attrs; cont.link = link; cont.wide = false;
|
|
71
|
+
cont.ch = ""; cont.fg = fg; cont.bg = cell.bg; cont.attrs = attrs; cont.link = link; cont.wide = false;
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
|
package/src/views.js
CHANGED
|
@@ -6,7 +6,7 @@ import { readFileSync } from "node:fs";
|
|
|
6
6
|
import { Widget, List, ScrollView, Input, Popup, Menu, StatusBar } from "./widgets.js";
|
|
7
7
|
import {
|
|
8
8
|
Picker, buildCommandPalette, buildModelPicker, buildModePicker, buildPermissionPicker,
|
|
9
|
-
modeName, permName, WorkspacePanel, TrajectoryPanel,
|
|
9
|
+
modeName, permName, WorkspacePanel, TrajectoryPanel, DirPicker,
|
|
10
10
|
ImagePopup, kittyCapable, buildJobsPopup, buildGoalPopup, SettingsPanel, SubagentPanel,
|
|
11
11
|
SkillsPanel, ControlPanel,
|
|
12
12
|
} from "./panels.js";
|
|
@@ -541,9 +541,10 @@ export class ChatView extends Widget {
|
|
|
541
541
|
this.input = new Input({
|
|
542
542
|
x: this.x, y: this.y + this.h - 2, w: this.w, h: 1,
|
|
543
543
|
multi: true, maxLines: 6,
|
|
544
|
+
bg: T.BG2, border: T.BORDER2,
|
|
544
545
|
placeholder: "输入消息…(Ctrl+J 换行,Enter 发送)",
|
|
545
546
|
onEnter: (v) => this.send(v),
|
|
546
|
-
onChange: () => this
|
|
547
|
+
onChange: () => this.inputChanged(),
|
|
547
548
|
});
|
|
548
549
|
this.contextNode = null;
|
|
549
550
|
this.rebuildQueued = false;
|
|
@@ -681,18 +682,18 @@ export class ChatView extends Widget {
|
|
|
681
682
|
}
|
|
682
683
|
|
|
683
684
|
/** Height of the collapsible todo block (0 when empty or collapsed). */
|
|
684
|
-
|
|
685
|
+
todoHeight() {
|
|
685
686
|
const todos = this.app.todos;
|
|
686
687
|
if (!this.todosVisible || !todos || todos.length === 0) return 0;
|
|
687
688
|
return Math.min(todos.length, 6) + 1; // 1 header row + up to 6 todos
|
|
688
689
|
}
|
|
689
690
|
|
|
690
|
-
|
|
691
|
+
inputChanged() {
|
|
691
692
|
// Multi-line input grew/shrunk → reflow view vs input, keep the tail visible.
|
|
692
693
|
const ih = this.input.height();
|
|
693
694
|
const prevIh = this.input.h;
|
|
694
695
|
this.input.h = ih;
|
|
695
|
-
const th = this
|
|
696
|
+
const th = this.todoHeight();
|
|
696
697
|
this.view.h = this.h - ih - th - 1;
|
|
697
698
|
this.input.y = this.y + this.h - ih;
|
|
698
699
|
if (ih !== prevIh) this.app.layout();
|
|
@@ -703,7 +704,7 @@ export class ChatView extends Widget {
|
|
|
703
704
|
this.x = x; this.y = y; this.w = w; this.h = h;
|
|
704
705
|
const ih = this.input.height();
|
|
705
706
|
this.input.h = ih;
|
|
706
|
-
const th = this
|
|
707
|
+
const th = this.todoHeight();
|
|
707
708
|
this.view.x = x; this.view.y = y; this.view.w = w; this.view.h = h - ih - th - 1;
|
|
708
709
|
this.input.x = x; this.input.y = y + h - ih; this.input.w = w;
|
|
709
710
|
this.cache.clear();
|
|
@@ -965,6 +966,7 @@ export class ChatView extends Widget {
|
|
|
965
966
|
{ t: open ? "▾ " : "▸ ", fg: K.ACCENT },
|
|
966
967
|
{ t: ` ${b.name ?? "tool"}`, fg: K.TXT, bold: true },
|
|
967
968
|
{ t: ` ${glyph}`, fg: status === "TOOLOK" ? K.OK : status === "TOOLERR" ? K.ERR : K.WARN },
|
|
969
|
+
{ t: open ? " [b 折叠]" : " [b 展开]", fg: K.FAINT },
|
|
968
970
|
]);
|
|
969
971
|
mark(realIdx, bi);
|
|
970
972
|
if (!open) {
|
|
@@ -1124,7 +1126,7 @@ export class ChatView extends Widget {
|
|
|
1124
1126
|
|
|
1125
1127
|
/** Collapsible todo block between the view and the input (Shift+T toggles). */
|
|
1126
1128
|
#renderTodos(screen) {
|
|
1127
|
-
const th = this
|
|
1129
|
+
const th = this.todoHeight();
|
|
1128
1130
|
if (th === 0) return;
|
|
1129
1131
|
const todos = this.app.todos ?? [];
|
|
1130
1132
|
const y = this.input.y - th - 1;
|
|
@@ -1243,7 +1245,7 @@ export class ChatView extends Widget {
|
|
|
1243
1245
|
if (ev.shift) {
|
|
1244
1246
|
this.todosVisible = !this.todosVisible;
|
|
1245
1247
|
this.app.toast(this.todosVisible ? "任务块:显示(Shift+T 折叠)" : "任务块:折叠(Shift+T 展开)");
|
|
1246
|
-
this
|
|
1248
|
+
this.inputChanged();
|
|
1247
1249
|
return true;
|
|
1248
1250
|
}
|
|
1249
1251
|
this.thinkMode = this.thinkMode === "collapsed" ? "expanded" : "collapsed";
|
|
@@ -1569,6 +1571,12 @@ export class App {
|
|
|
1569
1571
|
this.model = host.model ?? "";
|
|
1570
1572
|
} catch (e) { this.log(`[app] host.describe: ${e.message}`); }
|
|
1571
1573
|
await this.refreshSessions();
|
|
1574
|
+
// Open on a blank session at the launch directory (reusing an existing
|
|
1575
|
+
// draft when available) so the blank-session homepage shows immediately
|
|
1576
|
+
// instead of a fully empty chat area.
|
|
1577
|
+
if (!this.currentSession) {
|
|
1578
|
+
await this.newSessionIn(null);
|
|
1579
|
+
}
|
|
1572
1580
|
this.api.connectMux();
|
|
1573
1581
|
this.api.connectHost();
|
|
1574
1582
|
this.api.onFrame = (frame) => this.#onFrame(frame);
|
|
@@ -1621,6 +1629,8 @@ export class App {
|
|
|
1621
1629
|
case "session/projection":
|
|
1622
1630
|
this.projections[frame.key] = frame.value;
|
|
1623
1631
|
if (frame.key === "tokenUsage") this.tokenUsage = frame.value;
|
|
1632
|
+
// the todo block's height depends on the todos projection → reflow
|
|
1633
|
+
if (frame.key === "todos") this.chat.inputChanged();
|
|
1624
1634
|
break;
|
|
1625
1635
|
case "approval/resolved":
|
|
1626
1636
|
case "question/resolved":
|
|
@@ -1675,6 +1685,43 @@ export class App {
|
|
|
1675
1685
|
} catch (e) { this.toast(`移动失败: ${e.message}`); }
|
|
1676
1686
|
}
|
|
1677
1687
|
|
|
1688
|
+
/** Move a workspace up/down in the durable display order. */
|
|
1689
|
+
async moveWorkspace(node, delta) {
|
|
1690
|
+
const ws = this.workspaceItems?.find((w) => w.workspaceId === node.workspaceId);
|
|
1691
|
+
if (!ws || !this.workspaceItems) { this.toast("找不到工作区"); return; }
|
|
1692
|
+
const ids = this.workspaceItems.map((w) => w.workspaceId);
|
|
1693
|
+
const idx = ids.indexOf(ws.workspaceId);
|
|
1694
|
+
if (idx < 0) return;
|
|
1695
|
+
const target = idx + delta;
|
|
1696
|
+
if (target < 0 || target >= ids.length) return;
|
|
1697
|
+
const beforeWorkspaceId = delta === -1 ? ids[target] : (target + 1 < ids.length ? ids[target + 1] : undefined);
|
|
1698
|
+
try {
|
|
1699
|
+
await this.api.call("workspace.insertBefore", {
|
|
1700
|
+
workspaceId: ws.workspaceId,
|
|
1701
|
+
...(beforeWorkspaceId !== undefined ? { beforeWorkspaceId } : {}),
|
|
1702
|
+
});
|
|
1703
|
+
await this.refreshSessions();
|
|
1704
|
+
} catch (e) { this.toast(`移动工作区失败: ${e.message}`); }
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/** Yazi-style folder picker → workspace.create. */
|
|
1708
|
+
addWorkspace() {
|
|
1709
|
+
this.overlay = new DirPicker(this, {
|
|
1710
|
+
startPath: process.cwd(),
|
|
1711
|
+
onPick: async (path) => {
|
|
1712
|
+
this.overlay = null;
|
|
1713
|
+
try {
|
|
1714
|
+
await this.api.call("workspace.create", { path });
|
|
1715
|
+
await this.refreshSessions();
|
|
1716
|
+
this.toast(`已添加工作区: ${path}`);
|
|
1717
|
+
} catch (e) { this.toast(`添加失败: ${e.message}`); }
|
|
1718
|
+
this.redraw();
|
|
1719
|
+
},
|
|
1720
|
+
onCancel: () => { this.overlay = null; this.redraw(); },
|
|
1721
|
+
});
|
|
1722
|
+
this.redraw();
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1678
1725
|
renameSession(s) {
|
|
1679
1726
|
this.closeOverlay();
|
|
1680
1727
|
const input = new Input({ x: 2, y: this.screen.h - 3, w: this.screen.w - 4, h: 1, prompt: "标题: ", allowEmptyEnter: true, onEnter: () => this.#commitRename(s, input) });
|
|
@@ -1817,8 +1864,10 @@ export class App {
|
|
|
1817
1864
|
|
|
1818
1865
|
async newSessionIn(group = null) {
|
|
1819
1866
|
// Reuse an existing empty draft instead of minting a fresh blank session on
|
|
1820
|
-
// every "new session" click (this is how the meaningless blank sessions pile
|
|
1821
|
-
|
|
1867
|
+
// every "new session" click (this is how the meaningless blank sessions pile
|
|
1868
|
+
// up). Prefer a draft at the same directory (e.g. the launch cwd on open).
|
|
1869
|
+
const blanks = this.sessions.filter((s) => s.blank && !s.running && s.sessionId !== this.currentSession);
|
|
1870
|
+
const blank = blanks.find((s) => s.cwd === process.cwd()) ?? blanks[0];
|
|
1822
1871
|
if (blank && (group?.workspaceId == null || this.#sessionInWorkspace(blank.sessionId, group.workspaceId))) {
|
|
1823
1872
|
await this.refreshSessions();
|
|
1824
1873
|
this.openSession(blank.sessionId);
|
|
@@ -2327,7 +2376,7 @@ export class App {
|
|
|
2327
2376
|
renderFrame() {
|
|
2328
2377
|
this.chat.flushRebuild();
|
|
2329
2378
|
const s = this.screen;
|
|
2330
|
-
s.clear(T.BG);
|
|
2379
|
+
s.clear(-1, T.BG);
|
|
2331
2380
|
this.#renderTabBar(s);
|
|
2332
2381
|
if (this.sidebarVisible) {
|
|
2333
2382
|
if (this.searchActive) {
|
|
@@ -2434,12 +2483,11 @@ export class App {
|
|
|
2434
2483
|
if (this.overlay) this.overlay.render(s);
|
|
2435
2484
|
if (this.toastMsg) {
|
|
2436
2485
|
// Toasts land in the LOWER half (just above the input/footer) where the
|
|
2437
|
-
// user's attention is while pressing shortcuts — a solid color block
|
|
2438
|
-
// not a top-of-screen whisper.
|
|
2486
|
+
// user's attention is while pressing shortcuts — a solid color block.
|
|
2439
2487
|
const w = Math.min(s.w - 4, strWidth(this.toastMsg) + 6);
|
|
2440
2488
|
const x0 = Math.max(2, Math.floor((s.w - w) / 2));
|
|
2441
|
-
const y = Math.max(1,
|
|
2442
|
-
s.fillRect(x0
|
|
2489
|
+
const y = Math.max(1, this.chat.input.y - this.chat.todoHeight() - 2);
|
|
2490
|
+
s.fillRect(x0, y, x0 + w - 1, y, " ", { bg: T.ACCENT });
|
|
2443
2491
|
s.text(x0 + 1, y, truncate(this.toastMsg, w - 2), { fg: T.SELFG, bg: T.ACCENT, attrs: 1 });
|
|
2444
2492
|
}
|
|
2445
2493
|
if (this.renameInput && this.popup) this.renameInput.render(s);
|