feat: add context-aware ask keymaps

This commit is contained in:
Erik Kotsiuba
2026-05-06 12:44:37 +02:00
parent 3672ada13f
commit 808b62cc77
18 changed files with 1158 additions and 444 deletions
+16
View File
@@ -172,6 +172,7 @@ function renderController(
}
function handleControllerInput(controller: AskFlowController, data: string) {
controller.editor.disableSubmit = !isNativeEditorSubmitEnabled(controller);
const command = getInputCommand(
controller.state,
controller.config,
@@ -185,6 +186,16 @@ function handleControllerInput(controller: AskFlowController, data: string) {
handleNavigationCommand(controller, command);
}
function isNativeEditorSubmitEnabled(controller: AskFlowController): boolean {
if (controller.state.view.kind === "input") {
return controller.config.keymaps.editor.submit.includes("enter");
}
if (controller.state.view.kind === "note") {
return controller.config.keymaps.noteEditor.save.includes("enter");
}
return true;
}
function handleEditingCommand(
controller: AskFlowController,
command: AskInputCommand,
@@ -210,6 +221,10 @@ function handleEditingCommand(
closeEditor(controller);
return;
}
if (command.kind === "editSubmit") {
submitEditor(controller, controller.editor.getText());
return;
}
if (command.kind === "delegateToEditor") {
controller.editor.handleInput(data);
refresh(controller);
@@ -270,6 +285,7 @@ function handleNavigationCommand(
case "editMoveTab":
case "editMoveOption":
case "editClose":
case "editSubmit":
case "delegateToEditor":
return;
default:
+63 -15
View File
@@ -1,6 +1,9 @@
import { matchesKey } from "@mariozechner/pi-tui";
import type { AskConfig } from "../config/schema.ts";
import {
getAskKeyBindings,
type AskKeyBinding,
getAskContextBindings,
getGlobalBindings,
matchesBinding,
matchesDigitShortcut,
} from "../constants/keymaps.ts";
@@ -20,6 +23,7 @@ export type AskInputCommand =
| { kind: "editMoveTab"; delta: 1 | -1 }
| { kind: "editMoveOption"; delta: 1 | -1 }
| { kind: "editClose" }
| { kind: "editSubmit" }
| { kind: "delegateToEditor" }
| { kind: "ignore" };
@@ -29,50 +33,94 @@ export function getInputCommand(
data: string,
editingText = ""
): AskInputCommand {
const bindings = getAskKeyBindings(config);
if (matchesBinding(data, bindings.dismiss)) {
const global = getGlobalBindings(config);
if (matchesBinding(data, global.dismiss)) {
return { kind: "dismiss" };
}
if (matchesBinding(data, bindings.settings) && editingText.length === 0) {
if (matchesBinding(data, global.settings) && editingText.length === 0) {
return { kind: "showSettings" };
}
if (state.view.kind === "input" || state.view.kind === "note") {
return getEditingInputCommand(bindings, data, editingText);
if (state.view.kind === "input") {
return getAnswerEditorInputCommand(config, data, editingText);
}
if (state.view.kind === "note") {
return getNoteEditorInputCommand(config, data, editingText);
}
return getNavigationInputCommand(bindings, data);
return getNavigationInputCommand(config, data);
}
function getEditingInputCommand(
bindings: ReturnType<typeof getAskKeyBindings>,
function getAnswerEditorInputCommand(
config: AskConfig,
data: string,
editingText: string
): AskInputCommand {
if (matchesBinding(data, bindings.cancel)) {
const bindings = getAskContextBindings(config, "editor");
if (matchesBinding(data, bindings.submit)) {
return isNativeEditorSubmit(data)
? { kind: "delegateToEditor" }
: { kind: "editSubmit" };
}
if (matchesBinding(data, bindings.close)) {
return { kind: "editClose" };
}
return getEmptyEditorNavigationCommand(bindings, data, editingText);
}
function getNoteEditorInputCommand(
config: AskConfig,
data: string,
editingText: string
): AskInputCommand {
const bindings = getAskContextBindings(config, "noteEditor");
if (matchesBinding(data, bindings.save)) {
return isNativeEditorSubmit(data)
? { kind: "delegateToEditor" }
: { kind: "editSubmit" };
}
if (matchesBinding(data, bindings.close)) {
return { kind: "editClose" };
}
return getEmptyEditorNavigationCommand(bindings, data, editingText);
}
function getEmptyEditorNavigationCommand(
bindings: {
nextTabWhenEmpty: AskKeyBinding;
previousTabWhenEmpty: AskKeyBinding;
previousOptionWhenEmpty: AskKeyBinding;
nextOptionWhenEmpty: AskKeyBinding;
},
data: string,
editingText: string
): AskInputCommand {
if (editingText.length === 0) {
if (matchesBinding(data, bindings.nextTab)) {
if (matchesBinding(data, bindings.nextTabWhenEmpty)) {
return { kind: "editMoveTab", delta: 1 };
}
if (matchesBinding(data, bindings.previousTab)) {
if (matchesBinding(data, bindings.previousTabWhenEmpty)) {
return { kind: "editMoveTab", delta: -1 };
}
if (matchesBinding(data, bindings.previousOption)) {
if (matchesBinding(data, bindings.previousOptionWhenEmpty)) {
return { kind: "editMoveOption", delta: -1 };
}
if (matchesBinding(data, bindings.nextOption)) {
if (matchesBinding(data, bindings.nextOptionWhenEmpty)) {
return { kind: "editMoveOption", delta: 1 };
}
}
return { kind: "delegateToEditor" };
}
function isNativeEditorSubmit(data: string): boolean {
return matchesKey(data, "enter");
}
function getNavigationInputCommand(
bindings: ReturnType<typeof getAskKeyBindings>,
config: AskConfig,
data: string
): AskInputCommand {
const bindings = getAskContextBindings(config, "main");
if (matchesBinding(data, bindings.nextTab)) {
return { kind: "moveTab", delta: 1 };
}
+15 -15
View File
@@ -1,12 +1,15 @@
import {
Key,
matchesKey,
truncateToWidth,
visibleWidth,
wrapTextWithAnsi,
} from "@mariozechner/pi-tui";
import type { AskConfig } from "../config/schema.ts";
import type { AskConfigNotice } from "../config/store.ts";
import {
getAskContextBindings,
matchesBinding,
renderSettingsFooterKeymaps,
} from "../constants/keymaps.ts";
interface Theme {
bg(color: string, text: string): string;
@@ -86,26 +89,23 @@ export class AskSettingsList {
}
handleInput(data: string): void {
if (
data === "?" ||
matchesKey(data, Key.ctrl("c")) ||
matchesKey(data, Key.escape)
) {
const bindings = getAskContextBindings(this.config, "settingsModal");
if (matchesBinding(data, bindings.close)) {
this.close();
return;
}
if (matchesKey(data, Key.up)) {
if (matchesBinding(data, bindings.previousOption)) {
this.focusIndex =
this.focusIndex === 0 ? SETTINGS.length - 1 : this.focusIndex - 1;
this.tui.requestRender();
return;
}
if (matchesKey(data, Key.down)) {
if (matchesBinding(data, bindings.nextOption)) {
this.focusIndex = (this.focusIndex + 1) % SETTINGS.length;
this.tui.requestRender();
return;
}
if (matchesKey(data, Key.enter) || data === " ") {
if (matchesBinding(data, bindings.toggle)) {
const setting = SETTINGS[this.focusIndex];
if (setting) {
this.saveSetting(setting.key, !this.getSettingValue(setting.key));
@@ -175,15 +175,15 @@ export class AskSettingsList {
private appendFooter(lines: string[], innerWidth: number): void {
if (innerWidth < COMPACT_WIDTH) {
lines.push(
this.line(this.theme.fg("dim", " Enter/Space to change"), innerWidth)
);
lines.push(this.line(this.theme.fg("dim", " Esc to close"), innerWidth));
const footer = renderSettingsFooterKeymaps(this.config);
for (const line of wrapTextWithAnsi(footer, innerWidth - 2)) {
lines.push(this.line(this.theme.fg("dim", ` ${line}`), innerWidth));
}
return;
}
this.appendWrapped(
lines,
this.theme.fg("dim", "Enter/Space to change · Esc to close"),
this.theme.fg("dim", renderSettingsFooterKeymaps(this.config)),
innerWidth
);
}