github-issue-155-taktno-moodo (#158)

* caffeinate に -d フラグを追加し、ディスプレイスリープ中の App Nap によるプロセス凍結を防止

* takt 対話モードの save_task を takt add と同じ worktree 設定フローに統一

takt 対話モードで Save Task を選択した際に worktree/branch/auto_pr の
設定プロンプトがスキップされ、takt run で clone なしに実行されて成果物が
消失するバグを修正。promptWorktreeSettings() を共通関数として抽出し、
saveTaskFromInteractive() と addTask() の両方から使用するようにした。

* Release v0.9.0

* takt: github-issue-155-taktno-moodo
This commit is contained in:
nrs 2026-02-09 00:18:07 +09:00 committed by GitHub
parent cdedb4326e
commit c542dc0896
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 524 additions and 53 deletions

View File

@ -131,9 +131,11 @@ describe('readMultilineInput cursor navigation', () => {
let savedStdinRemoveListener: typeof process.stdin.removeListener;
let savedStdinResume: typeof process.stdin.resume;
let savedStdinPause: typeof process.stdin.pause;
let savedColumns: number | undefined;
let columnsOverridden = false;
let stdoutCalls: string[];
function setupRawStdin(rawInputs: string[]): void {
function setupRawStdin(rawInputs: string[], termColumns?: number): void {
savedIsTTY = process.stdin.isTTY;
savedIsRaw = process.stdin.isRaw;
savedSetRawMode = process.stdin.setRawMode;
@ -142,6 +144,13 @@ describe('readMultilineInput cursor navigation', () => {
savedStdinRemoveListener = process.stdin.removeListener;
savedStdinResume = process.stdin.resume;
savedStdinPause = process.stdin.pause;
savedColumns = process.stdout.columns;
columnsOverridden = false;
if (termColumns !== undefined) {
Object.defineProperty(process.stdout, 'columns', { value: termColumns, configurable: true, writable: true });
columnsOverridden = true;
}
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
Object.defineProperty(process.stdin, 'isRaw', { value: false, configurable: true, writable: true });
@ -197,6 +206,10 @@ describe('readMultilineInput cursor navigation', () => {
if (savedStdinRemoveListener) process.stdin.removeListener = savedStdinRemoveListener;
if (savedStdinResume) process.stdin.resume = savedStdinResume;
if (savedStdinPause) process.stdin.pause = savedStdinPause;
if (columnsOverridden) {
Object.defineProperty(process.stdout, 'columns', { value: savedColumns, configurable: true, writable: true });
columnsOverridden = false;
}
}
beforeEach(() => {
@ -611,4 +624,338 @@ describe('readMultilineInput cursor navigation', () => {
expect(result).toBe('abc\ndef\nghiX');
});
});
describe('soft-wrap: arrow up within wrapped line', () => {
it('should move to previous display row within same logical line', async () => {
// Given: termWidth=20, prompt "> " (2 cols), first display row = 18 chars, second = 20 chars
// Type 30 chars "abcdefghijklmnopqrstuvwxyz1234" → wraps at pos 18
// Display row 1: "abcdefghijklmnopqr" (18 chars, cols 3-20 with prompt)
// Display row 2: "stuvwxyz1234" (12 chars, cols 1-12)
// Cursor at end (pos 30, display col 12), press ↑ → display col 12 in row 1 → pos 12
// Insert "X" → "abcdefghijklXmnopqrstuvwxyz1234"
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x1B[AX\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcdefghijklXmnopqrstuvwxyz1234');
});
it('should do nothing when on first display row of first logical line', async () => {
// Given: termWidth=20, prompt "> " (2 cols), type "abcdefghij" (10 chars, fits in first row of 18 cols)
// Cursor at end (pos 10, first display row), press ↑ → no previous row, nothing happens
// Insert "X" → "abcdefghijX"
setupRawStdin([
'abcdefghij\x1B[AX\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcdefghijX');
});
});
describe('soft-wrap: arrow down within wrapped line', () => {
it('should move to next display row within same logical line', async () => {
// Given: termWidth=20, prompt "> " (2 cols), first row = 18 chars
// Type 30 chars, Home → pos 0, then ↓ → display col 0 in row 2 → pos 18
// Insert "X" → "abcdefghijklmnopqrXstuvwxyz1234"
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\x1B[BX\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcdefghijklmnopqrXstuvwxyz1234');
});
it('should do nothing when on last display row of last logical line', async () => {
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars (wraps into 2 display rows)
// Cursor at end (last display row), press ↓ → nothing happens
// Insert "X" → "abcdefghijklmnopqrstuvwxyz1234X"
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x1B[BX\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcdefghijklmnopqrstuvwxyz1234X');
});
});
describe('soft-wrap: Ctrl+A moves to display row start', () => {
it('should move to display row start on wrapped second row', async () => {
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars
// Cursor at end (pos 30), Ctrl+A → display row start (pos 18), insert "X"
// Result: "abcdefghijklmnopqrXstuvwxyz1234"
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x01X\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcdefghijklmnopqrXstuvwxyz1234');
});
it('should move to display row start on first row', async () => {
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars
// Move cursor to middle of first display row (Home, Right*5 → pos 5)
// Ctrl+A → pos 0, insert "X"
// Result: "Xabcdefghijklmnopqrstuvwxyz1234"
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\x1B[C\x1B[C\x1B[C\x1B[C\x1B[C\x01X\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('Xabcdefghijklmnopqrstuvwxyz1234');
});
});
describe('soft-wrap: Ctrl+E moves to display row end', () => {
it('should move to display row end on first row', async () => {
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars
// Home → pos 0, Ctrl+E → end of first display row (pos 18), insert "X"
// Result: "abcdefghijklmnopqrXstuvwxyz1234"
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\x05X\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcdefghijklmnopqrXstuvwxyz1234');
});
});
describe('soft-wrap: Home moves to logical line start', () => {
it('should move from wrapped second row to logical line start', async () => {
// Given: termWidth=20, prompt "> " (2 cols), first row = 18 chars
// Type 30 chars, cursor at end (pos 30, second display row)
// Home → logical line start (pos 0), insert "X"
// Result: "Xabcdefghijklmnopqrstuvwxyz1234"
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x1B[HX\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('Xabcdefghijklmnopqrstuvwxyz1234');
});
it('should emit cursor up sequence when crossing display rows', async () => {
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars (wraps into 2 rows)
// Cursor at end (second display row), Home → pos 0 (first display row)
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\r',
], 20);
// When
await callReadMultilineInput('> ');
// Then: should contain \x1B[{n}A for moving up display rows
const hasUpMove = stdoutCalls.some(c => /^\x1B\[\d+A$/.test(c));
expect(hasUpMove).toBe(true);
});
});
describe('soft-wrap: End moves to logical line end', () => {
it('should move from first display row to logical line end', async () => {
// Given: termWidth=20, prompt "> " (2 cols), first row = 18 chars
// Type 30 chars, Home → pos 0, End → logical line end (pos 30), insert "X"
// Result: "abcdefghijklmnopqrstuvwxyz1234X"
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\x1B[FX\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcdefghijklmnopqrstuvwxyz1234X');
});
it('should emit cursor down sequence when crossing display rows', async () => {
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars (wraps into 2 rows)
// Home → pos 0 (first display row), End → pos 30 (second display row)
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\x1B[F\r',
], 20);
// When
await callReadMultilineInput('> ');
// Then: should contain \x1B[{n}B for moving down display rows
const hasDownMove = stdoutCalls.some(c => /^\x1B\[\d+B$/.test(c));
expect(hasDownMove).toBe(true);
});
it('should stay at end when already at logical line end on last display row', async () => {
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars
// Cursor at end (pos 30, already at logical line end), End → nothing changes, insert "X"
// Result: "abcdefghijklmnopqrstuvwxyz1234X"
setupRawStdin([
'abcdefghijklmnopqrstuvwxyz1234\x1B[FX\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcdefghijklmnopqrstuvwxyz1234X');
});
});
describe('soft-wrap: non-wrapped text retains original behavior', () => {
it('should not affect arrow up on short single-line text', async () => {
// Given: termWidth=80, short text "abc" (no wrap), ↑ does nothing
setupRawStdin([
'abc\x1B[AX\r',
], 80);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcX');
});
it('should not affect arrow down on short single-line text', async () => {
// Given: termWidth=80, short text "abc" (no wrap), ↓ does nothing
setupRawStdin([
'abc\x1B[BX\r',
], 80);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcX');
});
it('should still navigate between logical lines with arrow up', async () => {
// Given: termWidth=80, "abcde\nfgh" (no wrap), cursor at end of "fgh", ↑ → "abcde" at col 3
setupRawStdin([
'abcde\x1B[13;2ufgh\x1B[AX\r',
], 80);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcXde\nfgh');
});
});
describe('soft-wrap: full-width characters', () => {
it('should calculate display row boundaries with full-width chars', async () => {
// Given: termWidth=10, prompt "> " (2 cols), first row available = 8 cols
// Type "あいうえ" (4 full-width chars = 8 display cols = fills first row exactly)
// Then type "お" (2 cols, starts second row)
// Cursor at end (after "お"), Ctrl+A → display row start (pos 4, start of "お")
// Insert "X"
// Result: "あいうえXお"
setupRawStdin([
'あいうえお\x01X\r',
], 10);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('あいうえXお');
});
it('should push full-width char to next row when only 1 column remains', async () => {
// Given: termWidth=10, prompt "> " (2 cols), first row available = 8 cols
// Type "abcdefg" (7 cols) then "あ" (2 cols) → 7+2=9 > 8, "あ" goes to row 2
// Cursor at end (after "あ"), Ctrl+A → display row start at "あ" (pos 7)
// Insert "X"
// Result: "abcdefgXあ"
setupRawStdin([
'abcdefgあ\x01X\r',
], 10);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcdefgXあ');
});
});
describe('soft-wrap: prompt width consideration', () => {
it('should account for prompt width in first display row', async () => {
// Given: termWidth=10, prompt "> " (2 cols), first row = 8 chars
// Type "12345678" (8 chars = fills first row) then "9" (starts row 2)
// Cursor at "9" (pos 9), ↑ → row 1 at display col 1, but only 8 chars available
// Display col 1 → pos 1
// Insert "X" → "1X234567890" ... wait, let me recalculate.
// Actually: cursor at end of "123456789" (pos 9, display col 1 in row 2)
// ↑ → display col 1 in row 1 → pos 1
// Insert "X" → "1X23456789"
setupRawStdin([
'123456789\x1B[AX\r',
], 10);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('1X23456789');
});
it('should not add prompt offset for second logical line', async () => {
// Given: termWidth=10, prompt "> " (2 cols)
// Type "ab\n123456789" → second logical line "123456789" (9 chars, fits in 10 col row)
// Cursor at end (pos 12), ↑ → "ab" at display col 9 → clamped to col 2 → pos 2 (end of "ab")
// Insert "X" → "abX\n123456789"
setupRawStdin([
'ab\x1B[13;2u123456789\x1B[AX\r',
], 10);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abX\n123456789');
});
});
describe('soft-wrap: cross logical line with display rows', () => {
it('should move from wrapped logical line to previous logical line last display row', async () => {
// Given: termWidth=20, prompt "> " (2 cols)
// Line 1: "abcdefghijklmnopqrstuvwx" (24 chars) → wraps: row 1 (18 chars) + row 2 (6 chars)
// Line 2: "123"
// Cursor at end of "123" (display col 3), ↑ → last display row of line 1 (row 2: "uvwx", 6 chars)
// Display col 3 → pos 21 ("v" position... let me calculate)
// Row 2 of line 1 starts at pos 18 ("stuvwx"), display col 3 → pos 21
// Insert "X" → "abcdefghijklmnopqrstuXvwx\n123"
setupRawStdin([
'abcdefghijklmnopqrstuvwx\x1B[13;2u123\x1B[AX\r',
], 20);
// When
const result = await callReadMultilineInput('> ');
// Then
expect(result).toBe('abcdefghijklmnopqrstuXvwx\n123');
});
});
});

View File

@ -250,46 +250,103 @@ export function readMultilineInput(prompt: string): Promise<string | null> {
// --- Buffer position helpers ---
function getLineStart(): number {
const lastNl = buffer.lastIndexOf('\n', cursorPos - 1);
return lastNl + 1;
}
function getLineEnd(): number {
const nextNl = buffer.indexOf('\n', cursorPos);
return nextNl >= 0 ? nextNl : buffer.length;
}
function getLineStartAt(pos: number): number {
const lastNl = buffer.lastIndexOf('\n', pos - 1);
return lastNl + 1;
}
function getLineStart(): number {
return getLineStartAt(cursorPos);
}
function getLineEndAt(pos: number): number {
const nextNl = buffer.indexOf('\n', pos);
return nextNl >= 0 ? nextNl : buffer.length;
}
/** Display width from line start to cursor */
function getDisplayColumn(): number {
return getDisplayWidth(buffer.slice(getLineStart(), cursorPos));
function getLineEnd(): number {
return getLineEndAt(cursorPos);
}
const promptWidth = getDisplayWidth(stripAnsi(prompt));
/** Terminal column (1-based) for a given buffer position */
function getTerminalColumn(pos: number): number {
const lineStart = getLineStartAt(pos);
const col = getDisplayWidth(buffer.slice(lineStart, pos));
const isFirstLine = lineStart === 0;
return isFirstLine ? promptWidth + col + 1 : col + 1;
// --- Display row helpers (soft-wrap awareness) ---
function getTermWidth(): number {
return process.stdout.columns || 80;
}
/** Find the buffer position in a line that matches a target display column */
function findPositionByDisplayColumn(lineStart: number, lineEnd: number, targetDisplayCol: number): number {
/** Buffer position of the display row start that contains `pos` */
function getDisplayRowStart(pos: number): number {
const logicalStart = getLineStartAt(pos);
const termWidth = getTermWidth();
const isFirstLogicalLine = logicalStart === 0;
let firstRowWidth = isFirstLogicalLine ? termWidth - promptWidth : termWidth;
if (firstRowWidth <= 0) firstRowWidth = 1;
let rowStart = logicalStart;
let accumulated = 0;
let available = firstRowWidth;
let i = logicalStart;
for (const ch of buffer.slice(logicalStart, pos)) {
const w = getDisplayWidth(ch);
if (accumulated + w > available) {
rowStart = i;
accumulated = w;
available = termWidth;
} else {
accumulated += w;
// Row exactly filled — next position starts a new display row
if (accumulated === available) {
rowStart = i + ch.length;
accumulated = 0;
available = termWidth;
}
}
i += ch.length;
}
return rowStart;
}
/** Buffer position of the display row end that contains `pos` */
function getDisplayRowEnd(pos: number): number {
const logicalEnd = getLineEndAt(pos);
const rowStart = getDisplayRowStart(pos);
const termWidth = getTermWidth();
// The first display row of the first logical line has reduced width
const isFirstDisplayRow = rowStart === 0;
const available = isFirstDisplayRow ? termWidth - promptWidth : termWidth;
let accumulated = 0;
let i = rowStart;
for (const ch of buffer.slice(rowStart, logicalEnd)) {
const w = getDisplayWidth(ch);
if (accumulated + w > available) return i;
accumulated += w;
i += ch.length;
}
return logicalEnd;
}
/** Display column (0-based) within the display row that contains `pos` */
function getDisplayRowColumn(pos: number): number {
return getDisplayWidth(buffer.slice(getDisplayRowStart(pos), pos));
}
/** Terminal column (1-based) for a given buffer position */
function getTerminalColumn(pos: number): number {
const displayRowStart = getDisplayRowStart(pos);
const col = getDisplayWidth(buffer.slice(displayRowStart, pos));
// Only the first display row of the first logical line has the prompt offset
const isFirstDisplayRow = displayRowStart === 0;
return isFirstDisplayRow ? promptWidth + col + 1 : col + 1;
}
/** Find the buffer position in a range that matches a target display column */
function findPositionByDisplayColumn(rangeStart: number, rangeEnd: number, targetDisplayCol: number): number {
let displayCol = 0;
let pos = lineStart;
for (const ch of buffer.slice(lineStart, lineEnd)) {
let pos = rangeStart;
for (const ch of buffer.slice(rangeStart, rangeEnd)) {
const w = getDisplayWidth(ch);
if (displayCol + w > targetDisplayCol) break;
displayCol += w;
@ -322,23 +379,77 @@ export function readMultilineInput(prompt: string): Promise<string | null> {
// --- Cursor movement ---
function moveCursorToLineStart(): void {
const displayOffset = getDisplayColumn();
function moveCursorToDisplayRowStart(): void {
const displayRowStart = getDisplayRowStart(cursorPos);
const displayOffset = getDisplayRowColumn(cursorPos);
if (displayOffset > 0) {
cursorPos = getLineStart();
cursorPos = displayRowStart;
process.stdout.write(`\x1B[${displayOffset}D`);
}
}
function moveCursorToLineEnd(): void {
const lineEnd = getLineEnd();
const displayOffset = getDisplayWidth(buffer.slice(cursorPos, lineEnd));
function moveCursorToDisplayRowEnd(): void {
const displayRowEnd = getDisplayRowEnd(cursorPos);
const displayOffset = getDisplayWidth(buffer.slice(cursorPos, displayRowEnd));
if (displayOffset > 0) {
cursorPos = lineEnd;
cursorPos = displayRowEnd;
process.stdout.write(`\x1B[${displayOffset}C`);
}
}
/** Move cursor to a target display row, positioning at the given display column */
function moveCursorToDisplayRow(
targetRowStart: number,
targetRowEnd: number,
displayCol: number,
direction: 'A' | 'B',
): void {
cursorPos = findPositionByDisplayColumn(targetRowStart, targetRowEnd, displayCol);
const termCol = getTerminalColumn(cursorPos);
process.stdout.write(`\x1B[${direction}`);
process.stdout.write(`\x1B[${termCol}G`);
}
/** Count how many display rows lie between two buffer positions in the same logical line */
function countDisplayRowsBetween(from: number, to: number): number {
if (from === to) return 0;
const start = Math.min(from, to);
const end = Math.max(from, to);
let count = 0;
let pos = start;
while (pos < end) {
const nextRowStart = getDisplayRowEnd(pos);
if (nextRowStart >= end) break;
pos = nextRowStart;
count++;
}
return count;
}
function moveCursorToLogicalLineStart(): void {
const lineStart = getLineStart();
if (cursorPos === lineStart) return;
const rowDiff = countDisplayRowsBetween(lineStart, cursorPos);
cursorPos = lineStart;
if (rowDiff > 0) {
process.stdout.write(`\x1B[${rowDiff}A`);
}
const termCol = getTerminalColumn(cursorPos);
process.stdout.write(`\x1B[${termCol}G`);
}
function moveCursorToLogicalLineEnd(): void {
const lineEnd = getLineEnd();
if (cursorPos === lineEnd) return;
const rowDiff = countDisplayRowsBetween(cursorPos, lineEnd);
cursorPos = lineEnd;
if (rowDiff > 0) {
process.stdout.write(`\x1B[${rowDiff}B`);
}
const termCol = getTerminalColumn(cursorPos);
process.stdout.write(`\x1B[${termCol}G`);
}
// --- Buffer editing ---
function insertAt(pos: number, text: string): void {
@ -461,27 +572,40 @@ export function readMultilineInput(prompt: string): Promise<string | null> {
},
onArrowUp() {
if (state !== 'normal') return;
const lineStart = getLineStart();
if (lineStart === 0) return;
const displayCol = getDisplayColumn();
const prevLineStart = getLineStartAt(lineStart - 1);
const prevLineEnd = lineStart - 1;
cursorPos = findPositionByDisplayColumn(prevLineStart, prevLineEnd, displayCol);
const termCol = getTerminalColumn(cursorPos);
process.stdout.write('\x1B[A');
process.stdout.write(`\x1B[${termCol}G`);
const logicalLineStart = getLineStart();
const displayRowStart = getDisplayRowStart(cursorPos);
const displayCol = getDisplayRowColumn(cursorPos);
if (displayRowStart > logicalLineStart) {
// Move to previous display row within the same logical line
const prevRowStart = getDisplayRowStart(displayRowStart - 1);
const prevRowEnd = getDisplayRowEnd(displayRowStart - 1);
moveCursorToDisplayRow(prevRowStart, prevRowEnd, displayCol, 'A');
} else if (logicalLineStart > 0) {
// Move to the last display row of the previous logical line
const prevLogicalLineEnd = logicalLineStart - 1;
const prevRowStart = getDisplayRowStart(prevLogicalLineEnd);
const prevRowEnd = getDisplayRowEnd(prevLogicalLineEnd);
moveCursorToDisplayRow(prevRowStart, prevRowEnd, displayCol, 'A');
}
},
onArrowDown() {
if (state !== 'normal') return;
const lineEnd = getLineEnd();
if (lineEnd >= buffer.length) return;
const displayCol = getDisplayColumn();
const nextLineStart = lineEnd + 1;
const nextLineEnd = getLineEndAt(nextLineStart);
cursorPos = findPositionByDisplayColumn(nextLineStart, nextLineEnd, displayCol);
const termCol = getTerminalColumn(cursorPos);
process.stdout.write('\x1B[B');
process.stdout.write(`\x1B[${termCol}G`);
const logicalLineEnd = getLineEnd();
const displayRowEnd = getDisplayRowEnd(cursorPos);
const displayCol = getDisplayRowColumn(cursorPos);
if (displayRowEnd < logicalLineEnd) {
// Move to next display row within the same logical line
const nextRowStart = displayRowEnd;
const nextRowEnd = getDisplayRowEnd(displayRowEnd);
moveCursorToDisplayRow(nextRowStart, nextRowEnd, displayCol, 'B');
} else if (logicalLineEnd < buffer.length) {
// Move to the first display row of the next logical line
const nextLineStart = logicalLineEnd + 1;
const nextRowEnd = getDisplayRowEnd(nextLineStart);
moveCursorToDisplayRow(nextLineStart, nextRowEnd, displayCol, 'B');
}
},
onWordLeft() {
if (state !== 'normal') return;
@ -507,11 +631,11 @@ export function readMultilineInput(prompt: string): Promise<string | null> {
},
onHome() {
if (state !== 'normal') return;
moveCursorToLineStart();
moveCursorToLogicalLineStart();
},
onEnd() {
if (state !== 'normal') return;
moveCursorToLineEnd();
moveCursorToLogicalLineEnd();
},
onChar(ch: string) {
if (state === 'paste') {
@ -543,8 +667,8 @@ export function readMultilineInput(prompt: string): Promise<string | null> {
}
// Editing
if (ch === '\x7F' || ch === '\x08') { deleteCharBefore(); return; }
if (ch === '\x01') { moveCursorToLineStart(); return; }
if (ch === '\x05') { moveCursorToLineEnd(); return; }
if (ch === '\x01') { moveCursorToDisplayRowStart(); return; }
if (ch === '\x05') { moveCursorToDisplayRowEnd(); return; }
if (ch === '\x0B') { deleteToLineEnd(); return; }
if (ch === '\x15') { deleteToLineStart(); return; }
if (ch === '\x17') { deleteWord(); return; }