Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions src/cockpit/settings-render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
'use strict';

const DEFAULT_SETTINGS = {
theme: 'default',
sidebarWidth: 32,
refreshMs: 2000,
defaultAgent: 'codex',
defaultBase: 'main',
showLocks: true,
showWorktreePaths: true,
autopilotDefault: false,
editorCommand: '',
};

const SECTION_DEFINITIONS = [
{
title: 'Appearance',
fields: [
['theme', 'Theme', 'default, dim, high-contrast'],
],
},
{
title: 'Layout',
fields: [
['sidebarWidth', 'Sidebar width', '20-80 columns'],
['refreshMs', 'Refresh interval', '500-60000 ms'],
['showWorktreePaths', 'Show worktree paths', 'true, false'],
],
},
{
title: 'Agents',
fields: [
['defaultAgent', 'Default agent', 'codex, claude, opencode, cursor, gemini'],
['defaultBase', 'Default base', 'any branch name'],
['autopilotDefault', 'Autopilot default', 'true, false'],
],
},
{
title: 'Safety',
fields: [
['showLocks', 'Show locks', 'true, false'],
],
},
{
title: 'Editor',
fields: [
['editorCommand', 'Editor command', 'any shell command, blank'],
],
},
];

const KEYBINDINGS = [
'↑/↓ navigate',
'Enter edit',
'Esc back',
'q quit',
];

function normalizeSettings(settings) {
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
return { ...DEFAULT_SETTINGS };
}

return {
...DEFAULT_SETTINGS,
...settings,
};
}

function formatValue(value) {
if (value === '') {
return '(blank)';
}
if (value === undefined || value === null) {
return '-';
}
return String(value);
}

function fieldLine(field, label, available, settings, selectedField) {
const marker = field === selectedField ? '>' : ' ';
return `${marker} ${label}: ${formatValue(settings[field])} (available: ${available})`;
}

function resolveSelectedField(options) {
if (!options || typeof options !== 'object') {
return null;
}
if (typeof options.selectedField === 'string') {
return options.selectedField;
}

return null;
}

function renderSection(section, settings, selectedField) {
const lines = [`[${section.title}]`];
for (const [field, label, available] of section.fields) {
lines.push(fieldLine(field, label, available, settings, selectedField));
}
return lines.join('\n');
}

function renderSettingsScreen(settings, options = {}) {
const current = normalizeSettings(settings);
const selectedField = resolveSelectedField(options);
const lines = [
'gx cockpit settings',
'Plain terminal settings view',
'',
];

for (const section of SECTION_DEFINITIONS) {
lines.push(renderSection(section, current, selectedField));
lines.push('');
}

lines.push('[Keybindings]');
for (const keybinding of KEYBINDINGS) {
lines.push(` ${keybinding}`);
}

return `${lines.join('\n')}\n`;
}

module.exports = {
renderSettingsScreen,
};
54 changes: 54 additions & 0 deletions test/cockpit-settings-render.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use strict';

const assert = require('node:assert/strict');
const test = require('node:test');

const { renderSettingsScreen } = require('../src/cockpit/settings-render');

test('renderSettingsScreen shows settings sections and current values', () => {
const output = renderSettingsScreen({
theme: 'dim',
sidebarWidth: 44,
refreshMs: 3000,
defaultAgent: 'claude',
defaultBase: 'dev',
showLocks: false,
showWorktreePaths: true,
autopilotDefault: true,
editorCommand: 'code --reuse-window',
});

assert.match(output, /gx cockpit settings/);
assert.match(output, /\[Appearance\]/);
assert.match(output, /Theme: dim \(available: default, dim, high-contrast\)/);
assert.match(output, /\[Layout\]/);
assert.match(output, /Sidebar width: 44 \(available: 20-80 columns\)/);
assert.match(output, /Refresh interval: 3000 \(available: 500-60000 ms\)/);
assert.match(output, /Show worktree paths: true \(available: true, false\)/);
assert.match(output, /\[Agents\]/);
assert.match(output, /Default agent: claude \(available: codex, claude, opencode, cursor, gemini\)/);
assert.match(output, /Default base: dev \(available: any branch name\)/);
assert.match(output, /Autopilot default: true \(available: true, false\)/);
assert.match(output, /\[Safety\]/);
assert.match(output, /Show locks: false \(available: true, false\)/);
assert.match(output, /\[Editor\]/);
assert.match(output, /Editor command: code --reuse-window \(available: any shell command, blank\)/);
});

test('renderSettingsScreen includes fixed keyboard hints', () => {
const output = renderSettingsScreen({});

assert.match(output, /\[Keybindings\]/);
assert.match(output, /↑\/↓ navigate/);
assert.match(output, /Enter edit/);
assert.match(output, /Esc back/);
assert.match(output, /q quit/);
});

test('renderSettingsScreen uses defaults and can mark the selected setting', () => {
const output = renderSettingsScreen(null, { selectedField: 'theme' });

assert.match(output, /> Theme: default \(available: default, dim, high-contrast\)/);
assert.match(output, /Editor command: \(blank\) \(available: any shell command, blank\)/);
assert.equal(output.endsWith('\n'), true);
});
Loading