@pellux/goodvibes-tui 0.20.0 → 0.20.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.
@@ -1,7 +1,7 @@
1
- import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
2
- import { getDisplayWidth } from '../utils/terminal-width.ts';
1
+ import { createStyledCell, type Line } from '../types/grid.ts';
3
2
  import { getOverlayMaxWidth } from './overlay-viewport.ts';
4
3
  import { GLYPHS, UI_TONES } from './ui-primitives.ts';
4
+ import { fillWidth, makeLine, writeText } from './fullscreen-primitives.ts';
5
5
 
6
6
  export interface OverlayBoxPalette {
7
7
  readonly borderFg: string;
@@ -60,29 +60,12 @@ export function putOverlayText(
60
60
  text: string,
61
61
  style: OverlayTextStyle,
62
62
  ): void {
63
- let x = startX;
64
- let used = 0;
65
- for (const ch of text) {
66
- const cellWidth = getDisplayWidth(ch);
67
- if (cellWidth <= 0) continue;
68
- if (used + cellWidth > maxWidth || x >= line.length) break;
69
- line[x] = createStyledCell(ch, {
70
- fg: style.fg,
71
- bg: style.bg ?? '',
72
- bold: style.bold ?? false,
73
- dim: style.dim ?? false,
74
- });
75
- if (cellWidth > 1 && x + 1 < line.length) {
76
- line[x + 1] = createStyledCell(' ', {
77
- fg: style.fg,
78
- bg: style.bg ?? '',
79
- bold: style.bold ?? false,
80
- dim: style.dim ?? false,
81
- });
82
- }
83
- x += cellWidth;
84
- used += cellWidth;
85
- }
63
+ writeText(line, startX, maxWidth, text, {
64
+ fg: style.fg,
65
+ bg: style.bg ?? '',
66
+ bold: style.bold ?? false,
67
+ dim: style.dim ?? false,
68
+ });
86
69
  }
87
70
 
88
71
  export function createOverlayBorderLine(
@@ -93,7 +76,7 @@ export function createOverlayBorderLine(
93
76
  right: string,
94
77
  fg: string = DEFAULT_OVERLAY_PALETTE.borderFg,
95
78
  ): Line {
96
- const line = createEmptyLine(terminalWidth);
79
+ const line = makeLine(terminalWidth);
97
80
  const leftX = layout.margin;
98
81
  const rightX = layout.margin + layout.width - 1;
99
82
  line[leftX] = createStyledCell(left, { fg });
@@ -110,13 +93,11 @@ export function createOverlayContentLine(
110
93
  borderFg: string = DEFAULT_OVERLAY_PALETTE.borderFg,
111
94
  bg = '',
112
95
  ): Line {
113
- const line = createEmptyLine(terminalWidth);
96
+ const line = makeLine(terminalWidth);
114
97
  const leftX = layout.margin;
115
98
  const rightX = layout.margin + layout.width - 1;
116
99
  line[leftX] = createStyledCell('│', { fg: borderFg });
117
- for (let x = leftX + 1; x < rightX; x++) {
118
- line[x] = createStyledCell(' ', { bg });
119
- }
100
+ fillWidth(line, leftX + 1, rightX - leftX - 1, bg);
120
101
  line[rightX] = createStyledCell('│', { fg: borderFg });
121
102
  return line;
122
103
  }
@@ -130,7 +111,7 @@ export function createOverlayFilledBorderLine(
130
111
  fg: string = DEFAULT_OVERLAY_PALETTE.borderFg,
131
112
  bg = '',
132
113
  ): Line {
133
- const line = createEmptyLine(terminalWidth);
114
+ const line = makeLine(terminalWidth);
134
115
  const leftX = layout.margin;
135
116
  const rightX = layout.margin + layout.width - 1;
136
117
  line[leftX] = createStyledCell(left, { fg, bg });
@@ -6,31 +6,21 @@
6
6
  */
7
7
 
8
8
  import type { Line } from '../types/grid.ts';
9
- import { createEmptyLine, createStyledCell } from '../types/grid.ts';
10
9
  import type { SettingsModal, SettingEntry, FlagEntry, McpEntry, SubscriptionEntry, SettingsCategory } from '../input/settings-modal.ts';
11
10
  import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings-modal.ts';
12
11
  import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
13
12
  import { CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
14
13
  import { isSecretConfigKey } from '../config/secret-config.ts';
15
- import { GLYPHS, UI_TONES } from './ui-primitives.ts';
16
-
17
- const PALETTE = {
18
- border: '#64748b',
19
- title: '#67e8f9',
20
- subtitle: '#93c5fd',
21
- text: '#e2e8f0',
22
- muted: '#94a3b8',
23
- dim: '#64748b',
24
- selectedBg: '#223049',
25
- categoryBg: '#141b25',
26
- contextBg: '#121923',
27
- controlsBg: '#0f141d',
28
- footerBg: '#111827',
29
- good: UI_TONES.state.good,
30
- warn: UI_TONES.state.warn,
31
- bad: UI_TONES.state.bad,
32
- info: UI_TONES.state.info,
33
- };
14
+ import { GLYPHS } from './ui-primitives.ts';
15
+ import {
16
+ clamp,
17
+ getFullscreenWorkspaceMetrics,
18
+ padDisplay,
19
+ renderFullscreenWorkspace,
20
+ stableWindow,
21
+ WORKSPACE_PALETTE as PALETTE,
22
+ type WorkspaceRow,
23
+ } from './fullscreen-workspace.ts';
34
24
 
35
25
  const CATEGORY_INFO: Record<SettingsCategory, string> = {
36
26
  display: 'Presentation settings for the terminal transcript: streaming, line numbers, thinking visibility, reasoning summaries, token speed, and tool previews.',
@@ -131,77 +121,6 @@ const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
131
121
  },
132
122
  };
133
123
 
134
- function clamp(value: number, min: number, max: number): number {
135
- return Math.max(min, Math.min(max, value));
136
- }
137
-
138
- function fillRange(line: Line, startX: number, endX: number, bg: string): void {
139
- for (let x = Math.max(0, startX); x <= Math.min(line.length - 1, endX); x += 1) {
140
- const cell = line[x] ?? createStyledCell(' ');
141
- line[x] = createStyledCell(cell.char, {
142
- fg: cell.fg,
143
- bg,
144
- bold: cell.bold,
145
- dim: cell.dim,
146
- underline: cell.underline,
147
- italic: cell.italic,
148
- strikethrough: cell.strikethrough,
149
- link: cell.link,
150
- });
151
- }
152
- }
153
-
154
- function writeText(line: Line, startX: number, maxWidth: number, text: string, style: Partial<Omit<Line[number], 'char'>> = {}): void {
155
- let x = startX;
156
- let used = 0;
157
- for (const ch of text) {
158
- const width = getDisplayWidth(ch);
159
- if (width <= 0) continue;
160
- if (used + width > maxWidth || x >= line.length) break;
161
- line[x] = createStyledCell(ch, style);
162
- if (width > 1 && x + 1 < line.length) {
163
- line[x + 1] = createStyledCell(' ', style);
164
- }
165
- x += width;
166
- used += width;
167
- }
168
- }
169
-
170
- function makeLine(width: number, bg = ''): Line {
171
- const line = createEmptyLine(width);
172
- if (bg) fillRange(line, 0, width - 1, bg);
173
- return line;
174
- }
175
-
176
- function borderLine(width: number, left: string, fill: string, right: string): Line {
177
- const line = makeLine(width);
178
- if (width <= 0) return line;
179
- line[0] = createStyledCell(left, { fg: PALETTE.border });
180
- for (let x = 1; x < width - 1; x += 1) {
181
- line[x] = createStyledCell(fill, { fg: PALETTE.border });
182
- }
183
- if (width > 1) line[width - 1] = createStyledCell(right, { fg: PALETTE.border });
184
- return line;
185
- }
186
-
187
- function contentLine(width: number, bg: string): Line {
188
- const line = makeLine(width, bg);
189
- if (width > 0) line[0] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border });
190
- if (width > 1) line[width - 1] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border });
191
- return line;
192
- }
193
-
194
- function drawVertical(line: Line, x: number, bg = ''): void {
195
- if (x <= 0 || x >= line.length - 1) return;
196
- line[x] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border, bg });
197
- }
198
-
199
- function drawHorizontalRange(line: Line, startX: number, endX: number, bg = ''): void {
200
- for (let x = Math.max(1, startX); x <= Math.min(line.length - 2, endX); x += 1) {
201
- line[x] = createStyledCell(GLYPHS.frame.horizontal, { fg: PALETTE.border, bg });
202
- }
203
- }
204
-
205
124
  function paddedWrapped(text: string, width: number, prefix = ''): string[] {
206
125
  const safeWidth = Math.max(1, width - getDisplayWidth(prefix));
207
126
  const wrapped = wrapText(text, safeWidth);
@@ -209,34 +128,6 @@ function paddedWrapped(text: string, width: number, prefix = ''): string[] {
209
128
  return wrapped.map((line, index) => `${index === 0 ? prefix : ' '.repeat(getDisplayWidth(prefix))}${line}`);
210
129
  }
211
130
 
212
- function clipDisplay(text: string, width: number): string {
213
- if (width <= 0) return '';
214
- let used = 0;
215
- let output = '';
216
- for (const ch of text) {
217
- const chWidth = getDisplayWidth(ch);
218
- if (chWidth <= 0) continue;
219
- if (used + chWidth > width) break;
220
- output += ch;
221
- used += chWidth;
222
- }
223
- return output;
224
- }
225
-
226
- function padDisplay(text: string, width: number): string {
227
- const clipped = clipDisplay(text, width);
228
- return `${clipped}${' '.repeat(Math.max(0, width - getDisplayWidth(clipped)))}`;
229
- }
230
-
231
- function stableWindow(total: number, selectedIndex: number, visibleCount: number): { start: number; end: number } {
232
- if (total <= 0 || visibleCount <= 0) return { start: 0, end: 0 };
233
- if (total <= visibleCount) return { start: 0, end: total };
234
- const selected = clamp(selectedIndex, 0, total - 1);
235
- const half = Math.floor(visibleCount / 2);
236
- const start = clamp(selected - half, 0, total - visibleCount);
237
- return { start, end: start + visibleCount };
238
- }
239
-
240
131
  function formatDefaultValue(value: unknown): string {
241
132
  if (value === '') return '(empty)';
242
133
  if (value === null || value === undefined) return '(unset)';
@@ -596,99 +487,51 @@ export function renderSettingsModal(
596
487
  width: number,
597
488
  viewportHeight = 24,
598
489
  ): Line[] {
599
- const safeWidth = Math.max(1, width);
600
- const safeHeight = Math.max(12, viewportHeight);
601
- const lines: Line[] = [];
602
- const leftWidth = safeWidth < 80
603
- ? clamp(Math.round(safeWidth * 0.32), 14, Math.max(14, safeWidth - 24))
604
- : clamp(Math.round(safeWidth * 0.22), 24, 34);
605
- const centerWidth = Math.max(20, safeWidth - leftWidth - 3);
606
- const leftStart = 1;
607
- const dividerX = leftWidth + 1;
608
- const centerStart = dividerX + 1;
609
- const centerEnd = safeWidth - 2;
610
- const bodyTop = 3;
611
- const footerY = safeHeight - 2;
612
- const bodyRows = Math.max(4, footerY - bodyTop);
613
- const contextWidth = Math.max(10, centerWidth - 2);
614
- const contextLines = buildContextLines(modal, contextWidth);
615
- const maxContextRows = Math.max(3, bodyRows - 4);
616
- const contextRows = clamp(Math.round(bodyRows * 0.4), Math.min(10, maxContextRows), maxContextRows);
617
- const controlsRows = Math.max(3, bodyRows - contextRows - 1);
618
- const separatorY = bodyTop + contextRows;
619
-
620
- const top = borderLine(safeWidth, GLYPHS.frame.topLeft, GLYPHS.frame.horizontal, GLYPHS.frame.topRight);
621
- writeText(top, 2, safeWidth - 4, ` Configuration Workspace / Settings `, { fg: PALETTE.title, bold: true });
622
- lines.push(top);
623
-
624
- const header = contentLine(safeWidth, PALETTE.footerBg);
625
- drawVertical(header, dividerX, PALETTE.footerBg);
626
- writeText(header, leftStart + 1, leftWidth - 2, 'Categories', { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
627
490
  const notices = [
628
491
  ...(modal.lastSaveTriggeredRestart ? [`Restarting ${modal.lastSaveTriggeredRestart}`] : []),
629
492
  ...(modal.lastSettingEffectMessage ? [modal.lastSettingEffectMessage] : []),
630
493
  ];
631
- const headerText = `${CATEGORY_LABELS[modal.currentCategory]} (${categoryItemCount(modal, modal.currentCategory)})${notices.length > 0 ? ` · ${notices.join(' · ')}` : ''}`;
632
- writeText(header, centerStart + 1, centerWidth - 2, headerText, { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
633
- lines.push(header);
634
-
635
- const headerSep = contentLine(safeWidth, '');
636
- drawVertical(headerSep, dividerX);
637
- drawHorizontalRange(headerSep, 1, safeWidth - 2);
638
- lines.push(headerSep);
639
-
640
- const categoryRows = renderCategories(modal, leftWidth - 2, bodyRows);
641
- const controlRows = renderControlRows(modal, contextWidth, controlsRows);
642
-
643
- for (let row = 0; row < bodyRows; row += 1) {
644
- const y = bodyTop + row;
645
- const inContext = y < separatorY;
646
- const inSeparator = y === separatorY;
647
- const bg = inSeparator ? '' : inContext ? PALETTE.contextBg : PALETTE.controlsBg;
648
- const line = contentLine(safeWidth, bg);
649
- fillRange(line, 1, dividerX - 1, PALETTE.categoryBg);
650
- drawVertical(line, dividerX, bg);
651
-
652
- const categoryRow = categoryRows[row] ?? { text: '', type: 'empty' as const, selected: false };
653
- if (categoryRow.selected) fillRange(line, leftStart, dividerX - 1, PALETTE.selectedBg);
654
- writeText(line, leftStart + 1, leftWidth - 3, categoryRow.text, {
655
- fg: categoryRow.selected ? PALETTE.text : categoryRow.type === 'group' ? PALETTE.subtitle : PALETTE.muted,
656
- bg: categoryRow.selected ? PALETTE.selectedBg : PALETTE.categoryBg,
657
- bold: categoryRow.selected || categoryRow.type === 'group',
658
- });
659
-
660
- if (inSeparator) {
661
- drawHorizontalRange(line, centerStart, centerEnd);
662
- } else if (inContext) {
663
- const contextText = contextLines[row] ?? '';
664
- const selectedSetting = modal.getSelected();
665
- const isTitle = row === 0 || (selectedSetting !== null && contextText === getSettingLabel(selectedSetting));
666
- writeText(line, centerStart + 1, contextWidth, contextText, {
667
- fg: row === 0 ? PALETTE.title : contextText.endsWith(':') ? PALETTE.subtitle : PALETTE.text,
668
- bg,
669
- bold: isTitle,
670
- dim: contextText.length === 0,
671
- });
672
- } else {
673
- const controlText = controlRows[row - contextRows - 1] ?? '';
674
- const selected = controlText.startsWith(GLYPHS.navigation.selected);
675
- if (selected) fillRange(line, centerStart, centerEnd, PALETTE.selectedBg);
676
- writeText(line, centerStart + 1, contextWidth, controlText, {
677
- fg: selected ? PALETTE.text : controlText.startsWith('value:') || controlText.trimStart().startsWith('value:') ? PALETTE.info : rowColorForSetting(modal, controlText),
678
- bg: selected ? PALETTE.selectedBg : bg,
679
- bold: selected,
680
- dim: controlText.length === 0,
681
- });
682
- }
683
- lines.push(line);
684
- }
685
-
686
- const footer = contentLine(safeWidth, PALETTE.footerBg);
687
- writeText(footer, 2, safeWidth - 4, footerText(modal), { fg: PALETTE.muted, bg: PALETTE.footerBg });
688
- lines.push(footer);
689
- const bottom = borderLine(safeWidth, GLYPHS.frame.bottomLeft, GLYPHS.frame.horizontal, GLYPHS.frame.bottomRight);
690
- lines.push(bottom);
691
-
692
- while (lines.length < safeHeight) lines.unshift(makeLine(safeWidth));
693
- return lines.slice(-safeHeight);
494
+ const metrics = getFullscreenWorkspaceMetrics({ width, height: viewportHeight });
495
+ const categoryRows = renderCategories(modal, metrics.leftWidth - 2, metrics.bodyRows);
496
+ const contextRows = buildContextLines(modal, metrics.contextWidth).map((text, row): WorkspaceRow => {
497
+ const selectedSetting = modal.getSelected();
498
+ const isTitle = row === 0 || (selectedSetting !== null && text === getSettingLabel(selectedSetting));
499
+ return {
500
+ text,
501
+ fg: row === 0 ? PALETTE.title : text.endsWith(':') ? PALETTE.subtitle : PALETTE.text,
502
+ bold: isTitle,
503
+ dim: text.length === 0,
504
+ };
505
+ });
506
+ const controlRows = renderControlRows(modal, metrics.contextWidth, metrics.controlRows).map((text): WorkspaceRow => {
507
+ const selected = text.startsWith(GLYPHS.navigation.selected);
508
+ return {
509
+ text,
510
+ selected,
511
+ fg: selected
512
+ ? PALETTE.text
513
+ : text.startsWith('value:') || text.trimStart().startsWith('value:')
514
+ ? PALETTE.info
515
+ : rowColorForSetting(modal, text),
516
+ bold: selected,
517
+ dim: text.length === 0,
518
+ };
519
+ });
520
+
521
+ return renderFullscreenWorkspace({
522
+ width,
523
+ height: viewportHeight,
524
+ title: 'Configuration Workspace / Settings',
525
+ leftHeader: 'Categories',
526
+ mainHeader: `${CATEGORY_LABELS[modal.currentCategory]} (${categoryItemCount(modal, modal.currentCategory)})${notices.length > 0 ? ` · ${notices.join(' · ')}` : ''}`,
527
+ leftRows: categoryRows.map((row): WorkspaceRow => ({
528
+ text: row.text,
529
+ selected: row.selected,
530
+ kind: row.type === 'group' ? 'group' : row.type === 'more' ? 'more' : row.type === 'empty' ? 'empty' : 'item',
531
+ bold: row.selected || row.type === 'group',
532
+ })),
533
+ contextRows,
534
+ controlRows,
535
+ footer: footerText(modal),
536
+ });
694
537
  }
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.20.0';
9
+ let _version = '0.20.2';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;