File size: 3,297 Bytes
f500658 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | /**
* Multi-line editor component for extensions.
* Supports Ctrl+G for external editor.
*/
import {
Container,
Editor,
type EditorOptions,
type Focusable,
getKeybindings,
Spacer,
Text,
type TUI,
} from "@earendil-works/pi-tui";
import type { KeybindingsManager } from "../../../core/keybindings.ts";
import { editInExternalEditor } from "../external-editor.ts";
import { getEditorTheme, theme } from "../theme/theme.ts";
import { DynamicBorder } from "./dynamic-border.ts";
import { keyHint } from "./keybinding-hints.ts";
export class ExtensionEditorComponent extends Container implements Focusable {
private editor: Editor;
private onSubmitCallback: (value: string) => void;
private onCancelCallback: () => void;
private tui: TUI;
private keybindings: KeybindingsManager;
private externalEditorCommand: string;
private _focused = false;
get focused(): boolean {
return this._focused;
}
set focused(value: boolean) {
this._focused = value;
this.editor.focused = value;
}
constructor(
tui: TUI,
keybindings: KeybindingsManager,
title: string,
prefill: string | undefined,
onSubmit: (value: string) => void,
onCancel: () => void,
options?: EditorOptions,
externalEditorCommand?: string,
) {
super();
this.tui = tui;
this.keybindings = keybindings;
this.externalEditorCommand =
externalEditorCommand ||
process.env.VISUAL ||
process.env.EDITOR ||
(process.platform === "win32" ? "notepad" : "nano");
this.onSubmitCallback = onSubmit;
this.onCancelCallback = onCancel;
// Add top border
this.addChild(new DynamicBorder());
this.addChild(new Spacer(1));
// Add title
this.addChild(new Text(theme.fg("accent", title), 1, 0));
this.addChild(new Spacer(1));
// Create editor
this.editor = new Editor(tui, getEditorTheme(), options);
if (prefill) {
this.editor.setText(prefill);
}
// Wire up Enter to submit (Shift+Enter for newlines, like the main editor)
this.editor.onSubmit = (text: string) => {
this.onSubmitCallback(text);
};
this.addChild(this.editor);
this.addChild(new Spacer(1));
// Add hint
const hint =
keyHint("tui.select.confirm", "submit") +
" " +
keyHint("tui.input.newLine", "newline") +
" " +
keyHint("tui.select.cancel", "cancel") +
` ${keyHint("app.editor.external", "external editor")}`;
this.addChild(new Text(hint, 1, 0));
this.addChild(new Spacer(1));
// Add bottom border
this.addChild(new DynamicBorder());
}
handleInput(keyData: string): void {
const kb = getKeybindings();
// Escape or Ctrl+C to cancel
if (kb.matches(keyData, "tui.select.cancel")) {
this.onCancelCallback();
return;
}
// External editor (app keybinding)
if (this.keybindings.matches(keyData, "app.editor.external")) {
void this.handleOpenExternalEditor();
return;
}
// Forward to editor
this.editor.handleInput(keyData);
}
private async handleOpenExternalEditor(): Promise<void> {
const content = this.editor.getText();
this.tui.stop();
try {
const result = await editInExternalEditor({
command: this.externalEditorCommand,
content,
});
if (result.status === "complete") {
this.editor.setText(result.content);
}
} finally {
this.tui.start();
this.tui.requestRender(true);
}
}
}
|