| import { Editor, type EditorOptions, type EditorTheme, type TUI, visibleWidth } from "@earendil-works/pi-tui"; |
| import type { AppKeybinding, KeybindingsManager } from "../../../core/keybindings.ts"; |
| import type { StatusIndicator } from "./status-indicator.ts"; |
|
|
| export type CustomEditorOptions = EditorOptions & { |
| |
| embedWorkingStatus?: boolean; |
| }; |
|
|
| |
| |
| |
| export class CustomEditor extends Editor { |
| private keybindings: KeybindingsManager; |
| private workingStatusIndicator: StatusIndicator | undefined; |
| public readonly embedWorkingStatus: boolean; |
| public actionHandlers: Map<AppKeybinding, () => void> = new Map(); |
|
|
| |
| public onEscape?: () => void; |
| public onCtrlD?: () => void; |
| public onPasteImage?: () => void; |
| |
| public onExtensionShortcut?: (data: string) => boolean; |
|
|
| constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: CustomEditorOptions) { |
| super(tui, theme, options); |
| this.keybindings = keybindings; |
| this.embedWorkingStatus = options?.embedWorkingStatus ?? false; |
| } |
|
|
| setWorkingStatusIndicator(indicator: StatusIndicator | undefined): void { |
| this.workingStatusIndicator = indicator; |
| } |
|
|
| protected override renderTopBorder(width: number, hiddenLineCount: number): string { |
| if (!this.embedWorkingStatus || !this.workingStatusIndicator || width <= 0) { |
| return super.renderTopBorder(width, hiddenLineCount); |
| } |
|
|
| let status = this.workingStatusIndicator.renderInBorder(Math.max(1, width - 5)); |
| let statusWidth = visibleWidth(status); |
| if (statusWidth === 0) return super.renderTopBorder(width, hiddenLineCount); |
|
|
| const overflowLabel = hiddenLineCount > 0 ? ` β ${hiddenLineCount} more ` : undefined; |
| const overflowLabelWidth = overflowLabel ? visibleWidth(overflowLabel) : 0; |
| const overflowStart = Math.floor((width - overflowLabelWidth) / 2); |
| const canFitOverflow = () => |
| overflowLabel !== undefined && overflowLabelWidth + 2 <= width && overflowStart - (3 + statusWidth + 1) >= 1; |
|
|
| if (overflowLabel && !canFitOverflow()) { |
| status = this.workingStatusIndicator.renderSpinnerInBorder(width); |
| statusWidth = visibleWidth(status); |
| } |
|
|
| if (canFitOverflow()) { |
| const leftBlockWidth = 3 + statusWidth + 1; |
| return ( |
| this.borderColor("ββ ") + |
| status + |
| this.borderColor( |
| ` ${"β".repeat(overflowStart - leftBlockWidth)}${overflowLabel}${"β".repeat(width - overflowStart - overflowLabelWidth)}`, |
| ) |
| ); |
| } |
|
|
| if (width >= statusWidth + 5) { |
| return this.borderColor("ββ ") + status + this.borderColor(` ${"β".repeat(width - statusWidth - 4)}`); |
| } |
|
|
| status = this.workingStatusIndicator.renderSpinnerInBorder(width); |
| statusWidth = visibleWidth(status); |
| const prefixWidth = Math.min(3, Math.max(0, width - statusWidth)); |
| return ( |
| this.borderColor("β".repeat(prefixWidth)) + |
| status + |
| this.borderColor("β".repeat(Math.max(0, width - prefixWidth - statusWidth))) |
| ); |
| } |
|
|
| |
| |
| |
| onAction(action: AppKeybinding, handler: () => void): void { |
| this.actionHandlers.set(action, handler); |
| } |
|
|
| handleInput(data: string): void { |
| |
| if (this.onExtensionShortcut?.(data)) { |
| return; |
| } |
|
|
| |
| if (this.keybindings.matches(data, "app.clipboard.pasteImage")) { |
| this.onPasteImage?.(); |
| return; |
| } |
|
|
| |
|
|
| |
| if (this.keybindings.matches(data, "app.interrupt")) { |
| if (!this.isShowingAutocomplete()) { |
| |
| const handler = this.onEscape ?? this.actionHandlers.get("app.interrupt"); |
| if (handler) { |
| handler(); |
| return; |
| } |
| } |
| |
| super.handleInput(data); |
| return; |
| } |
|
|
| |
| if (this.keybindings.matches(data, "app.exit")) { |
| if (this.getText().length === 0) { |
| const handler = this.onCtrlD ?? this.actionHandlers.get("app.exit"); |
| if (handler) handler(); |
| return; |
| } |
| |
| } |
|
|
| |
| |
| if ( |
| this.keybindings.matches(data, "tui.editor.historyPrevious") || |
| this.keybindings.matches(data, "tui.editor.historyNext") |
| ) { |
| super.handleInput(data); |
| return; |
| } |
|
|
| |
| for (const [action, handler] of this.actionHandlers) { |
| if (action !== "app.interrupt" && action !== "app.exit" && this.keybindings.matches(data, action)) { |
| handler(); |
| return; |
| } |
| } |
|
|
| |
| super.handleInput(data); |
| } |
| } |
|
|