Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@diskpush/rsync-core": "workspace:*",
"@diskpush/schemas": "workspace:*",
"@diskpush/ssh-core": "workspace:*",
"@profullstack/hqtui": "^0.2.0",
"zod": "^3.24.1"
}
}
55 changes: 18 additions & 37 deletions apps/cli/src/commands/tui.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { createApp } from '@profullstack/hqtui'
import type { DiskPushStore } from '@diskpush/database'
import { EXIT } from '../exit-codes.js'
import { failure, type Output } from '../output.js'
import { flagValue, type ParsedArgv } from '../parse-argv.js'
import { type ParsedArgv } from '../parse-argv.js'
import { resolveEndpoint, sshConfigHosts } from '../resolve.js'
import { blankPane, buildEndpointChoices, defaultLocalPath, Tui } from '../tui/app.js'
import { parseKeys } from '../tui/keys.js'
import { ansi } from '../tui/render.js'

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

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

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

const onResize = () => tui.render()
process.stdout.on('resize', onResize)
app.render(({ ui, theme, width, height }) => {
tui.view(ui, theme, width, height)
})

try {
await tui.loadBoth()
tui.render()

await new Promise<void>((resolve) => {
const onData = (chunk: string) => {
// Several keys can arrive in one chunk, and an arrow key is three
// bytes; parseKeys turns the raw bytes into logical keys first.
void (async () => {
for (const key of parseKeys(chunk)) {
const keepGoing = await tui.onKey(key)
if (!keepGoing) {
process.stdin.off('data', onData)
resolve()
return
}
}
tui.render()
})()
}
process.stdin.on('data', onData)
})
void tui.loadBoth()
await app.start()
} finally {
process.stdout.off('resize', onResize)
tui.close()
restore()
}

return EXIT.ok
Expand Down
Loading
Loading