opentui(v6): arrows + enter navigate every completion menu (paths, args)
This commit is contained in:
parent
72118b049f
commit
e1edbb0e89
@ -47,12 +47,17 @@ function clampSelected(ctx: MenuKeyContext): number {
|
|||||||
* Route one key press against the open menu. `modified` is Ctrl/Alt/Option —
|
* Route one key press against the open menu. `modified` is Ctrl/Alt/Option —
|
||||||
* modified arrows/Enter never belong to the menu (Tab/Esc keep their
|
* modified arrows/Enter never belong to the menu (Tab/Esc keep their
|
||||||
* pre-existing modifier-blind accept/dismiss semantics).
|
* pre-existing modifier-blind accept/dismiss semantics).
|
||||||
|
*
|
||||||
|
* ANY open menu owns plain arrows/Enter (glitch, 2026-06-10): @-path and
|
||||||
|
* arg menus navigate exactly like the slash menu — standard editor-
|
||||||
|
* autocomplete behavior; Esc dismisses to hand the cursor keys back.
|
||||||
|
* (`ctx.slashMenu` still feeds the hint text + suggestion rows.)
|
||||||
*/
|
*/
|
||||||
export function routeMenuKey(name: string, modified: boolean, ctx: MenuKeyContext): MenuKeyAction {
|
export function routeMenuKey(name: string, modified: boolean, ctx: MenuKeyContext): MenuKeyAction {
|
||||||
if (ctx.count <= 0) return PASS
|
if (ctx.count <= 0) return PASS
|
||||||
if (name === 'tab') return { index: clampSelected(ctx), kind: 'accept' }
|
if (name === 'tab') return { index: clampSelected(ctx), kind: 'accept' }
|
||||||
if (name === 'escape') return { kind: 'dismiss' }
|
if (name === 'escape') return { kind: 'dismiss' }
|
||||||
if (modified || !ctx.slashMenu) return PASS
|
if (modified) return PASS
|
||||||
const sel = clampSelected(ctx)
|
const sel = clampSelected(ctx)
|
||||||
if (name === 'up') return { kind: 'move', selected: (sel - 1 + ctx.count) % ctx.count }
|
if (name === 'up') return { kind: 'move', selected: (sel - 1 + ctx.count) % ctx.count }
|
||||||
if (name === 'down') return { kind: 'move', selected: (sel + 1) % ctx.count }
|
if (name === 'down') return { kind: 'move', selected: (sel + 1) % ctx.count }
|
||||||
|
|||||||
@ -47,9 +47,17 @@ describe('routeMenuKey — key-routing precedence table', () => {
|
|||||||
['Esc dismisses', 'escape', false, ctx({ selected: 2 }), { kind: 'dismiss' }],
|
['Esc dismisses', 'escape', false, ctx({ selected: 2 }), { kind: 'dismiss' }],
|
||||||
// NOT the slash menu (path/@-mention dropdown): arrows + Enter keep their
|
// NOT the slash menu (path/@-mention dropdown): arrows + Enter keep their
|
||||||
// existing meanings (history / cursor / textarea submit) …
|
// existing meanings (history / cursor / textarea submit) …
|
||||||
['Down on a path menu passes', 'down', false, ctx({ slashMenu: false }), { kind: 'pass' }],
|
// glitch 2026-06-10: ANY open menu owns plain arrows/Enter (path/arg menus
|
||||||
['Up on a path menu passes', 'up', false, ctx({ slashMenu: false }), { kind: 'pass' }],
|
// navigate like the slash menu; Esc hands the cursor keys back).
|
||||||
['Enter on a path menu passes (submits as today)', 'return', false, ctx({ slashMenu: false }), { kind: 'pass' }],
|
['Down on a path menu moves', 'down', false, ctx({ slashMenu: false }), { kind: 'move', selected: 1 }],
|
||||||
|
['Up on a path menu moves (wraps)', 'up', false, ctx({ slashMenu: false }), { kind: 'move', selected: 2 }],
|
||||||
|
[
|
||||||
|
'Enter on a path menu accepts the highlighted item',
|
||||||
|
'return',
|
||||||
|
false,
|
||||||
|
ctx({ slashMenu: false }),
|
||||||
|
{ index: 0, kind: 'accept' }
|
||||||
|
],
|
||||||
// … but Tab/Esc keep working on ANY menu (pre-Epic-8 semantics)
|
// … but Tab/Esc keep working on ANY menu (pre-Epic-8 semantics)
|
||||||
['Tab on a path menu still accepts', 'tab', false, ctx({ slashMenu: false }), { index: 0, kind: 'accept' }],
|
['Tab on a path menu still accepts', 'tab', false, ctx({ slashMenu: false }), { index: 0, kind: 'accept' }],
|
||||||
['Esc on a path menu still dismisses', 'escape', false, ctx({ slashMenu: false }), { kind: 'dismiss' }],
|
['Esc on a path menu still dismisses', 'escape', false, ctx({ slashMenu: false }), { kind: 'dismiss' }],
|
||||||
|
|||||||
@ -234,7 +234,6 @@ export function Composer(props: {
|
|||||||
// Whether the composer text starts with `/` (slash menu vs path menu) — a
|
// Whether the composer text starts with `/` (slash menu vs path menu) — a
|
||||||
// signal so the dropdown hint stays reactive; the key handler re-checks
|
// signal so the dropdown hint stays reactive; the key handler re-checks
|
||||||
// `ta.plainText` directly.
|
// `ta.plainText` directly.
|
||||||
const [slashText, setSlashText] = createSignal(false)
|
|
||||||
|
|
||||||
/** Replace the textarea content and park the cursor at the end (history recall). */
|
/** Replace the textarea content and park the cursor at the end (history recall). */
|
||||||
const setBuffer = (text: string) => {
|
const setBuffer = (text: string) => {
|
||||||
@ -416,7 +415,7 @@ export function Composer(props: {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<text selectable={false} fg={theme().color.muted}>
|
<text selectable={false} fg={theme().color.muted}>
|
||||||
{slashText() ? '↑/↓ select · Enter/Tab accept · Esc dismiss' : 'Tab complete · Esc dismiss'}
|
{'↑/↓ select · Enter/Tab accept · Esc dismiss'}
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
@ -460,7 +459,6 @@ export function Composer(props: {
|
|||||||
}}
|
}}
|
||||||
onContentChange={() => {
|
onContentChange={() => {
|
||||||
const text = ta?.plainText ?? ''
|
const text = ta?.plainText ?? ''
|
||||||
setSlashText(text.startsWith('/'))
|
|
||||||
setBufText(text) // drives the token analysis (highlight + suggestion)
|
setBufText(text) // drives the token analysis (highlight + suggestion)
|
||||||
props.onType?.(text)
|
props.onType?.(text)
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user