Skip to content

Commit c47b210

Browse files
authored
feat(cli): rewrite the TUI on HQTUI (#35)
Replaces ~940 lines of hand-rolled ANSI with @profullstack/hqtui, split into model (state), view (a pure function of a snapshot) and app (effects only). New: a live transfer panel with progress, rate and the files as rsync reports them, cancellable with esc; / to filter a listing as you type; o/O to sort by name, size or time; . for dotfiles; ? for the key list; a filtering endpoint picker; modification times; and mouse click and wheel support. Three defects found by rendering frames rather than reading code: a panel header where one pane lost its title and the other its path over a one-column rounding difference, paths truncated from the wrong end, and a finished transfer showing rsync's last printed figure instead of 100%. TUI tests went 15 to 59, all asserting on real rendered frames.
1 parent 217f3b6 commit c47b210

18 files changed

Lines changed: 1756 additions & 790 deletions

‎apps/cli/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@diskpush/rsync-core": "workspace:*",
2222
"@diskpush/schemas": "workspace:*",
2323
"@diskpush/ssh-core": "workspace:*",
24+
"@profullstack/hqtui": "^0.2.0",
2425
"zod": "^3.24.1"
2526
}
2627
}

‎apps/cli/src/commands/tui.ts‎

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
import { createApp } from '@profullstack/hqtui'
12
import type { DiskPushStore } from '@diskpush/database'
23
import { EXIT } from '../exit-codes.js'
34
import { failure, type Output } from '../output.js'
4-
import { flagValue, type ParsedArgv } from '../parse-argv.js'
5+
import { type ParsedArgv } from '../parse-argv.js'
56
import { resolveEndpoint, sshConfigHosts } from '../resolve.js'
67
import { blankPane, buildEndpointChoices, defaultLocalPath, Tui } from '../tui/app.js'
7-
import { parseKeys } from '../tui/keys.js'
8-
import { ansi } from '../tui/render.js'
98

109
/**
1110
* `diskpush tui` — the two-pane browser, in a terminal.
@@ -42,46 +41,28 @@ export async function runTui(parsed: ParsedArgv, store: DiskPushStore, output: O
4241
const choices = buildEndpointChoices(await store.listConnections(), sshConfigHosts(), defaultLocalPath())
4342
const tui = new Tui(panes[0]!, panes[1]!, choices)
4443

45-
const restore = () => {
46-
process.stdout.write(ansi.showCursor + ansi.mainScreen)
47-
if (process.stdin.isTTY) process.stdin.setRawMode(false)
48-
process.stdin.pause()
49-
}
44+
// `q` is not a quit key to the app: inside the host-key prompt it has to
45+
// reach the Tui first, which is the only thing that knows a dialog is up.
46+
// Ctrl+C stays with the app so the terminal is restored however it dies.
47+
const app = await createApp({ quitKeys: ['ctrl+c'], collapseBorders: true, title: 'DiskPush' })
48+
tui.attach(app)
5049

51-
process.stdout.write(ansi.altScreen + ansi.hideCursor)
52-
process.stdin.setRawMode(true)
53-
process.stdin.resume()
54-
process.stdin.setEncoding('utf8')
50+
app.on('key', (event) => {
51+
void (async () => {
52+
if (!(await tui.onKey(event))) app.quit()
53+
else app.invalidate()
54+
})()
55+
})
5556

56-
const onResize = () => tui.render()
57-
process.stdout.on('resize', onResize)
57+
app.render(({ ui, theme, width, height }) => {
58+
tui.view(ui, theme, width, height)
59+
})
5860

5961
try {
60-
await tui.loadBoth()
61-
tui.render()
62-
63-
await new Promise<void>((resolve) => {
64-
const onData = (chunk: string) => {
65-
// Several keys can arrive in one chunk, and an arrow key is three
66-
// bytes; parseKeys turns the raw bytes into logical keys first.
67-
void (async () => {
68-
for (const key of parseKeys(chunk)) {
69-
const keepGoing = await tui.onKey(key)
70-
if (!keepGoing) {
71-
process.stdin.off('data', onData)
72-
resolve()
73-
return
74-
}
75-
}
76-
tui.render()
77-
})()
78-
}
79-
process.stdin.on('data', onData)
80-
})
62+
void tui.loadBoth()
63+
await app.start()
8164
} finally {
82-
process.stdout.off('resize', onResize)
8365
tui.close()
84-
restore()
8566
}
8667

8768
return EXIT.ok

0 commit comments

Comments
 (0)