tui: F10 offers Quit, Reset (home screen) and Cancel; :reset and :home
Quit ends the program after the unsaved-form or running-process warning. Reset closes every dialog and screen above Home after the same warning, refreshes the metadata, and returns to Home with the Apps tree focused, keeping the session and profile; also :reset, :home and the F9 command center. Esc cancels. Test, smoke script and docs updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
99befbb2ef
commit
165c002d3b
6 files changed
+60
-15
No files matched your search
@@ -50,7 +50,7 @@ Keys (the function-key bar is the truth; F1 or `?` lists them; every action is a
|
||||
| F7 `n` | new (a child on a child list) | `x` `g` `v` | export, go to by key, view settings |
|
||||
| F8 `d` | delete, asks | Ctrl+S | save a form or continue a process step |
|
||||
| F9 | menu: every command, searchable | `:` | command line, history and completion |
|
||||
| F10 | quit, asks | Ctrl+C | quit now |
|
||||
| F10 | Quit, Reset to home, or Cancel (`:reset`) | Ctrl+C | quit now |
|
||||
|
||||
Things to try: `:table query person`, Enter on a row, PgDn through records, Esc (same row); F6, add a
|
||||
condition, Tab to the grammar and type `AND (fi` then Tab; `:table create placeAddress` (country, state, city
|
||||
|
||||
@@ -32,7 +32,7 @@ bun run packages/tui/src/main.tsx --profile sample # the same without the
|
||||
| F7, `n` | new record (on a child list: a child of this record) | `s` | sort by the column under the cursor |
|
||||
| F8, `d` | delete, after a question | `]` `[`, Ctrl+N Ctrl+P, PgDn PgUp in a record | next and previous record of the list it came from |
|
||||
| F9 | the command center: every action, searchable | `b` | run a table process on the selection |
|
||||
| F10, `:quit` | quit, after a question | `x`, `g`, `v` | export, go to by key, view settings |
|
||||
| F10, `:quit` | Quit, Reset (home screen, metadata refreshed, session kept) or Cancel; `:reset` and `:home` reset | `x`, `g`, `v` | export, go to by key, view settings |
|
||||
| `:` | command line (history ↑↓, Ctrl+R, Tab completes) | Ctrl+S | save a form, continue a process step |
|
||||
| Ctrl+C | quit at once (the terminal is restored) | `c` | copy a record |
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ expect 12-help 'Help (F1' 'Keys'
|
||||
keys Escape T:: 'T:app SampleWidgetsDashboard' Enter
|
||||
expect 13-dashboard 'Sample Widgets Dashboard'
|
||||
keys F10
|
||||
expect 14-quit-confirm 'Quit qqq?'
|
||||
keys y
|
||||
expect 14-quit-confirm 'Quit qqq?' 'Reset: go to the home screen'
|
||||
keys Enter
|
||||
sleep 1
|
||||
echo "captures in $out"
|
||||
[[ $fails == 0 ]] && echo "smoke: ok" || { echo "smoke: $fails failed"; exit 1; }
|
||||
@@ -36,12 +36,12 @@ const NOUNS: Record<string, string[]> = {
|
||||
apps: [], whoami: [], search: [], 'help-search': [],
|
||||
}
|
||||
|
||||
const TUI_COMMANDS = ['quit', 'home', 'back', 'help', 'menu', 'goto', 'view', 'theme', 'ascii', 'linear', 'shortcuts', 'lock', 'pin', 'idle', 'refresh', 'filter', 'export', 'jobs', 'profile', 'server', 'signout', 'connection', 'open', 'record', 'app']
|
||||
const TUI_COMMANDS = ['quit', 'home', 'reset', 'back', 'help', 'menu', 'goto', 'view', 'theme', 'ascii', 'linear', 'shortcuts', 'lock', 'pin', 'idle', 'refresh', 'filter', 'export', 'jobs', 'profile', 'server', 'signout', 'connection', 'open', 'record', 'app']
|
||||
|
||||
/** Global keys for the help overlay. */
|
||||
export const GLOBAL_KEYS: Array<[string, string]> = [
|
||||
['F1 ?', 'help (this)'], ['F2 / .', 'go to anything'], ['F3 Enter', 'view / open'], ['F4 e', 'edit'], ['F5 r', 'refresh'],
|
||||
['F6 f', 'filter (grid)'], ['F7 n', 'new record'], ['F8 d', 'delete (asks)'], ['F9', 'menu: every command'], ['F10', 'quit (asks)'],
|
||||
['F6 f', 'filter (grid)'], ['F7 n', 'new record'], ['F8 d', 'delete (asks)'], ['F9', 'menu: every command'], ['F10', 'quit, reset or cancel'],
|
||||
['Esc', 'back, cancel'], ['Tab', 'Apps tree / content'], ['↑↓ PgUp PgDn', 'move, page'], ['Ctrl+F Ctrl+B', 'page (always works)'], ['Space S+Space', 'page lists'], ['Home End', 'first, last'], ['← →', 'columns, tree'],
|
||||
['s', 'sort by column'], ['Space (grid)', 'select row'], ['b', 'run a process on selection'], ['x', 'export'], ['g', 'go to by key'], ['v', 'view settings'],
|
||||
['] [', 'next / previous record'], ['Ctrl+S', 'save a form'], [':', 'command line'], ['Ctrl+K', 'palette'], ['Ctrl+C', 'quit now'],
|
||||
@@ -183,7 +183,7 @@ export class Commands implements CommandsLike {
|
||||
try {
|
||||
switch (cmd) {
|
||||
case 'quit': case 'q': case 'exit': return s.requestQuit()
|
||||
case 'home': return s.home()
|
||||
case 'home': case 'reset': return s.reset()
|
||||
case 'back': return s.back()
|
||||
case 'help': return this.help()
|
||||
case 'menu': return this.menu()
|
||||
@@ -427,7 +427,7 @@ export class Commands implements CommandsLike {
|
||||
const out: MenuEntry[] = [...(s.screen?.menu?.() ?? [])]
|
||||
out.push({ section: 'Context', label: 'Back', key: 'Esc', command: 'back', run: () => s.back() })
|
||||
// Go to
|
||||
out.push({ section: 'Go to', label: 'Home', command: 'home', run: () => s.home() })
|
||||
out.push({ section: 'Go to', label: 'Reset: go to the home screen', key: 'F10', command: 'reset', detail: 'refreshes metadata, keeps the session', run: () => s.reset() })
|
||||
out.push({ section: 'Go to', label: 'Go to anything…', key: 'F2 / .', command: 'goto', run: () => this.palette() })
|
||||
for (const n of (meta?.appTree ?? []) as AppTreeNode[]) if (n.type === 'APP') out.push({ section: 'Go to', label: `App: ${n.label}`, command: `app ${n.name}`, run: () => openApp(s, n.name) })
|
||||
for (const t of Object.values(meta?.tables ?? {}).filter((x) => !x.isHidden).sort((a, b) => a.label.localeCompare(b.label))) out.push({ section: 'Go to', label: `Table: ${t.label}`, command: `table query ${t.name}`, run: () => openTable(s, t.name) })
|
||||
|
||||
@@ -14,7 +14,7 @@ import { openRealtimeStream } from '@qrun-io/qqq-ui-api-client'
|
||||
import { History, addRecent, currentCredential, readIdleMinutes, type MetaDataDocument, type RecentItem } from '@qrun-io/qqq-core'
|
||||
import { TUI_CHANNEL, TuiClient, type VersionInfo } from './client'
|
||||
import { charOf, isPrintable, type Key } from './keys'
|
||||
import { ConfirmOverlay, MessageOverlay } from './overlays/basic'
|
||||
import { ChoiceOverlay, ConfirmOverlay, MessageOverlay } from './overlays/basic'
|
||||
import { Extensions } from './extensions'
|
||||
import { collaborationPresence, presenceSource, setPresenceSource } from './presence'
|
||||
import { setAscii, setTheme, theme } from './theme'
|
||||
@@ -314,11 +314,46 @@ export class AppStore {
|
||||
|
||||
// ---- quitting ----
|
||||
|
||||
/** F10: a small dialog with Quit, Reset (home screen) and Cancel. */
|
||||
requestQuit(): void {
|
||||
this.open(new ChoiceOverlay<'quit' | 'reset' | 'cancel'>(this, 'Quit qqq?', [
|
||||
{ label: 'Quit', value: 'quit', detail: 'end the program' },
|
||||
{ label: 'Reset: go to the home screen', value: 'reset', detail: 'keeps the session' },
|
||||
{ label: 'Cancel', value: 'cancel', detail: 'Esc' },
|
||||
], (choice) => {
|
||||
if (choice === 'quit') this.guard('quit', () => this.quit())
|
||||
else if (choice === 'reset') this.reset()
|
||||
}, 'Enter chooses'))
|
||||
}
|
||||
|
||||
/** Warns about unsaved forms or running processes before `then`. */
|
||||
private guard(what: 'quit' | 'reset', then: () => void): void {
|
||||
const dirty = this.routes.some((s) => s.dirty?.())
|
||||
const busy = this.routes.some((s) => s.busy?.())
|
||||
const warn = dirty ? ' A form has unsaved changes that will be lost.' : busy ? ' A process is still running; it continues on the server.' : ''
|
||||
this.confirm(`Quit qqq?${warn}`, () => this.quit())
|
||||
if (!dirty && !busy) return then()
|
||||
const warn = dirty ? 'A form has unsaved changes that will be lost.' : 'A process is still running; it continues on the server.'
|
||||
this.confirm(`${warn} ${what === 'quit' ? 'Quit' : 'Reset'} anyway?`, then)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset: closes dialogs and every screen above Home (after the same warning), refreshes the
|
||||
* metadata, and returns to Home with the Apps tree focused. The session and profile stay.
|
||||
*/
|
||||
reset(): void {
|
||||
this.guard('reset', () => {
|
||||
this.overlays = []
|
||||
while (this.routes.length > 1) this.routes.pop()?.dispose?.()
|
||||
const done = () => {
|
||||
this.sidebar = this.sidebarFactory?.(this) ?? this.sidebar
|
||||
this.screen?.resume?.()
|
||||
this.focus = this.showSidebar ? 'sidebar' : 'content'
|
||||
this.flash('reset: home screen, metadata refreshed')
|
||||
this.bump()
|
||||
}
|
||||
if (!this.client) return done()
|
||||
this.client.clearCaches()
|
||||
this.client.metaData(true).then((m) => { this.meta = m }).catch((e) => this.error(e)).finally(done)
|
||||
})
|
||||
}
|
||||
|
||||
quit(): void {
|
||||
|
||||
@@ -100,10 +100,20 @@ test('the process wizard: step form, asynchronous job with progress, summary', a
|
||||
expect(JSON.parse(String((step.body as { values: string }).values))).toEqual({ millis: '250' })
|
||||
})
|
||||
|
||||
test('F10 asks before quitting and n keeps the TUI', async () => {
|
||||
test('F10 offers Quit, Reset and Cancel; Reset returns to Home with the Apps tree focused', async () => {
|
||||
h = await startTui()
|
||||
const f = await press(h, () => h!.setup.mockInput.pressKey(KeyCodes.F10))
|
||||
await command(h, 'table query person')
|
||||
await waitFor(h, (x) => x.includes('30 records'))
|
||||
let f = await press(h, () => h!.setup.mockInput.pressKey(KeyCodes.F10))
|
||||
expect(f).toContain('Quit qqq?')
|
||||
const after = await press(h, () => h!.setup.mockInput.pressKey('n'))
|
||||
expect(after).not.toContain('Quit qqq?')
|
||||
expect(f).toContain('Reset: go to the home screen')
|
||||
f = await press(h, () => h!.setup.mockInput.pressEscape())
|
||||
expect(f).not.toContain('Quit qqq?')
|
||||
await press(h, () => h!.setup.mockInput.pressKey(KeyCodes.F10))
|
||||
await press(h, () => h!.setup.mockInput.pressArrow('down'))
|
||||
await press(h, () => h!.setup.mockInput.pressEnter())
|
||||
await waitFor(h, (x) => x.includes('reset: home screen'))
|
||||
expect(h.store.routes.length).toBe(1)
|
||||
expect(h.store.screen?.kind).toBe('home')
|
||||
expect(h.store.focus).toBe('sidebar')
|
||||
})
|
||||
Reference in new issue
Block a user