summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components/SettingsDialog.tsx
diff options
context:
space:
mode:
authorJacob Richman <[email protected]>2025-08-21 16:43:56 -0700
committerGitHub <[email protected]>2025-08-21 23:43:56 +0000
commit29699274bb0e8f70b9bedad40ca2d03739318853 (patch)
tree17ff82d54991e72deba93eb40e4b7d068b881cb5 /packages/cli/src/ui/components/SettingsDialog.tsx
parent10286934e6a549dcad557adecfc087552e13c983 (diff)
feat(settings) support editing string settings. (#6732)
Diffstat (limited to 'packages/cli/src/ui/components/SettingsDialog.tsx')
-rw-r--r--packages/cli/src/ui/components/SettingsDialog.tsx113
1 files changed, 77 insertions, 36 deletions
diff --git a/packages/cli/src/ui/components/SettingsDialog.tsx b/packages/cli/src/ui/components/SettingsDialog.tsx
index 8fa689d0..c9685cd5 100644
--- a/packages/cli/src/ui/components/SettingsDialog.tsx
+++ b/packages/cli/src/ui/components/SettingsDialog.tsx
@@ -35,7 +35,7 @@ import {
import { useVimMode } from '../contexts/VimModeContext.js';
import { useKeypress } from '../hooks/useKeypress.js';
import chalk from 'chalk';
-import { cpSlice, cpLen } from '../utils/textUtils.js';
+import { cpSlice, cpLen, stripUnsafeCharacters } from '../utils/textUtils.js';
interface SettingsDialogProps {
settings: LoadedSettings;
@@ -78,8 +78,8 @@ export function SettingsDialog({
new Set(),
);
- // Preserve pending changes across scope switches (boolean and number values only)
- type PendingValue = boolean | number;
+ // Preserve pending changes across scope switches
+ type PendingValue = boolean | number | string;
const [globalPendingChanges, setGlobalPendingChanges] = useState<
Map<string, PendingValue>
>(new Map());
@@ -99,7 +99,10 @@ export function SettingsDialog({
const def = getSettingDefinition(key);
if (def?.type === 'boolean' && typeof value === 'boolean') {
updated = setPendingSettingValue(key, value, updated);
- } else if (def?.type === 'number' && typeof value === 'number') {
+ } else if (
+ (def?.type === 'number' && typeof value === 'number') ||
+ (def?.type === 'string' && typeof value === 'string')
+ ) {
updated = setPendingSettingValueAny(key, value, updated);
}
newModified.add(key);
@@ -123,7 +126,7 @@ export function SettingsDialog({
type: definition?.type,
toggle: () => {
if (definition?.type !== 'boolean') {
- // For non-boolean (e.g., number) items, toggle will be handled via edit mode.
+ // For non-boolean items, toggle will be handled via edit mode.
return;
}
const currentValue = getSettingValue(key, pendingSettings, {});
@@ -220,7 +223,7 @@ export function SettingsDialog({
const items = generateSettingsItems();
- // Number edit state
+ // Generic edit state
const [editingKey, setEditingKey] = useState<string | null>(null);
const [editBuffer, setEditBuffer] = useState<string>('');
const [editCursorPos, setEditCursorPos] = useState<number>(0); // Cursor position within edit buffer
@@ -235,28 +238,39 @@ export function SettingsDialog({
return () => clearInterval(id);
}, [editingKey]);
- const startEditingNumber = (key: string, initial?: string) => {
+ const startEditing = (key: string, initial?: string) => {
setEditingKey(key);
const initialValue = initial ?? '';
setEditBuffer(initialValue);
setEditCursorPos(cpLen(initialValue)); // Position cursor at end of initial value
};
- const commitNumberEdit = (key: string) => {
- if (editBuffer.trim() === '') {
- // Nothing entered; cancel edit
+ const commitEdit = (key: string) => {
+ const definition = getSettingDefinition(key);
+ const type = definition?.type;
+
+ if (editBuffer.trim() === '' && type === 'number') {
+ // Nothing entered for a number; cancel edit
setEditingKey(null);
setEditBuffer('');
setEditCursorPos(0);
return;
}
- const parsed = Number(editBuffer.trim());
- if (Number.isNaN(parsed)) {
- // Invalid number; cancel edit
- setEditingKey(null);
- setEditBuffer('');
- setEditCursorPos(0);
- return;
+
+ let parsed: string | number;
+ if (type === 'number') {
+ const numParsed = Number(editBuffer.trim());
+ if (Number.isNaN(numParsed)) {
+ // Invalid number; cancel edit
+ setEditingKey(null);
+ setEditBuffer('');
+ setEditCursorPos(0);
+ return;
+ }
+ parsed = numParsed;
+ } else {
+ // For strings, use the buffer as is.
+ parsed = editBuffer;
}
// Update pending
@@ -347,10 +361,16 @@ export function SettingsDialog({
setFocusSection((prev) => (prev === 'settings' ? 'scope' : 'settings'));
}
if (focusSection === 'settings') {
- // If editing a number, capture numeric input and control keys
+ // If editing, capture input and control keys
if (editingKey) {
+ const definition = getSettingDefinition(editingKey);
+ const type = definition?.type;
+
if (key.paste && key.sequence) {
- const pasted = key.sequence.replace(/[^0-9\-+.]/g, '');
+ let pasted = key.sequence;
+ if (type === 'number') {
+ pasted = key.sequence.replace(/[^0-9\-+.]/g, '');
+ }
if (pasted) {
setEditBuffer((b) => {
const before = cpSlice(b, 0, editCursorPos);
@@ -380,16 +400,27 @@ export function SettingsDialog({
return;
}
if (name === 'escape') {
- commitNumberEdit(editingKey);
+ commitEdit(editingKey);
return;
}
if (name === 'return') {
- commitNumberEdit(editingKey);
+ commitEdit(editingKey);
return;
}
- // Allow digits, minus, plus, and dot
- const ch = key.sequence;
- if (/[0-9\-+.]/.test(ch)) {
+
+ let ch = key.sequence;
+ let isValidChar = false;
+ if (type === 'number') {
+ // Allow digits, minus, plus, and dot.
+ isValidChar = /[0-9\-+.]/.test(ch);
+ } else {
+ ch = stripUnsafeCharacters(ch);
+ // For strings, allow any single character that isn't a control
+ // sequence.
+ isValidChar = ch.length === 1;
+ }
+
+ if (isValidChar) {
setEditBuffer((currentBuffer) => {
const beforeCursor = cpSlice(currentBuffer, 0, editCursorPos);
const afterCursor = cpSlice(currentBuffer, editCursorPos);
@@ -398,6 +429,7 @@ export function SettingsDialog({
setEditCursorPos((pos) => pos + 1);
return;
}
+
// Arrow key navigation
if (name === 'left') {
setEditCursorPos((pos) => Math.max(0, pos - 1));
@@ -422,7 +454,7 @@ export function SettingsDialog({
if (name === 'up' || name === 'k') {
// If editing, commit first
if (editingKey) {
- commitNumberEdit(editingKey);
+ commitEdit(editingKey);
}
const newIndex =
activeSettingIndex > 0 ? activeSettingIndex - 1 : items.length - 1;
@@ -436,7 +468,7 @@ export function SettingsDialog({
} else if (name === 'down' || name === 'j') {
// If editing, commit first
if (editingKey) {
- commitNumberEdit(editingKey);
+ commitEdit(editingKey);
}
const newIndex =
activeSettingIndex < items.length - 1 ? activeSettingIndex + 1 : 0;
@@ -449,15 +481,18 @@ export function SettingsDialog({
}
} else if (name === 'return' || name === 'space') {
const currentItem = items[activeSettingIndex];
- if (currentItem?.type === 'number') {
- startEditingNumber(currentItem.value);
+ if (
+ currentItem?.type === 'number' ||
+ currentItem?.type === 'string'
+ ) {
+ startEditing(currentItem.value);
} else {
currentItem?.toggle();
}
} else if (/^[0-9]$/.test(key.sequence || '') && !editingKey) {
const currentItem = items[activeSettingIndex];
if (currentItem?.type === 'number') {
- startEditingNumber(currentItem.value, key.sequence);
+ startEditing(currentItem.value, key.sequence);
}
} else if (ctrl && (name === 'c' || name === 'l')) {
// Ctrl+C or Ctrl+L: Clear current setting and reset to default
@@ -475,8 +510,11 @@ export function SettingsDialog({
prev,
),
);
- } else if (defType === 'number') {
- if (typeof defaultValue === 'number') {
+ } else if (defType === 'number' || defType === 'string') {
+ if (
+ typeof defaultValue === 'number' ||
+ typeof defaultValue === 'string'
+ ) {
setPendingSettings((prev) =>
setPendingSettingValueAny(
currentSetting.value,
@@ -509,7 +547,8 @@ export function SettingsDialog({
? typeof defaultValue === 'boolean'
? defaultValue
: false
- : typeof defaultValue === 'number'
+ : typeof defaultValue === 'number' ||
+ typeof defaultValue === 'string'
? defaultValue
: undefined;
const immediateSettingsObject =
@@ -541,7 +580,9 @@ export function SettingsDialog({
(currentSetting.type === 'boolean' &&
typeof defaultValue === 'boolean') ||
(currentSetting.type === 'number' &&
- typeof defaultValue === 'number')
+ typeof defaultValue === 'number') ||
+ (currentSetting.type === 'string' &&
+ typeof defaultValue === 'string')
) {
setGlobalPendingChanges((prev) => {
const next = new Map(prev);
@@ -584,7 +625,7 @@ export function SettingsDialog({
}
if (name === 'escape') {
if (editingKey) {
- commitNumberEdit(editingKey);
+ commitEdit(editingKey);
} else {
onSelect(undefined, selectedScope);
}
@@ -637,8 +678,8 @@ export function SettingsDialog({
// Cursor not visible
displayValue = editBuffer;
}
- } else if (item.type === 'number') {
- // For numbers, get the actual current value from pending settings
+ } else if (item.type === 'number' || item.type === 'string') {
+ // For numbers/strings, get the actual current value from pending settings
const path = item.value.split('.');
const currentValue = getNestedValue(pendingSettings, path);