@doriandev/devcc 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +659 -0
- package/bin/devcc.mjs +66 -0
- package/dist/devcc/app/App.d.ts +178 -0
- package/dist/devcc/app/App.d.ts.map +1 -0
- package/dist/devcc/app/App.js +1255 -0
- package/dist/devcc/app/App.js.map +1 -0
- package/dist/devcc/app/builtinCommands.d.ts +17 -0
- package/dist/devcc/app/builtinCommands.d.ts.map +1 -0
- package/dist/devcc/app/builtinCommands.js +389 -0
- package/dist/devcc/app/builtinCommands.js.map +1 -0
- package/dist/devcc/app/keymap.d.ts +21 -0
- package/dist/devcc/app/keymap.d.ts.map +1 -0
- package/dist/devcc/app/keymap.js +162 -0
- package/dist/devcc/app/keymap.js.map +1 -0
- package/dist/devcc/app/theme.d.ts +49 -0
- package/dist/devcc/app/theme.d.ts.map +1 -0
- package/dist/devcc/app/theme.js +68 -0
- package/dist/devcc/app/theme.js.map +1 -0
- package/dist/devcc/cli.d.ts +3 -0
- package/dist/devcc/cli.d.ts.map +1 -0
- package/dist/devcc/cli.js +113 -0
- package/dist/devcc/cli.js.map +1 -0
- package/dist/devcc/components/CommandPalette.d.ts +26 -0
- package/dist/devcc/components/CommandPalette.d.ts.map +1 -0
- package/dist/devcc/components/CommandPalette.js +113 -0
- package/dist/devcc/components/CommandPalette.js.map +1 -0
- package/dist/devcc/components/ConfirmDialog.d.ts +31 -0
- package/dist/devcc/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/devcc/components/ConfirmDialog.js +147 -0
- package/dist/devcc/components/ConfirmDialog.js.map +1 -0
- package/dist/devcc/components/Footer.d.ts +22 -0
- package/dist/devcc/components/Footer.d.ts.map +1 -0
- package/dist/devcc/components/Footer.js +72 -0
- package/dist/devcc/components/Footer.js.map +1 -0
- package/dist/devcc/components/Header.d.ts +12 -0
- package/dist/devcc/components/Header.d.ts.map +1 -0
- package/dist/devcc/components/Header.js +84 -0
- package/dist/devcc/components/Header.js.map +1 -0
- package/dist/devcc/components/HelpOverlay.d.ts +18 -0
- package/dist/devcc/components/HelpOverlay.d.ts.map +1 -0
- package/dist/devcc/components/HelpOverlay.js +48 -0
- package/dist/devcc/components/HelpOverlay.js.map +1 -0
- package/dist/devcc/components/Overlay.d.ts +31 -0
- package/dist/devcc/components/Overlay.d.ts.map +1 -0
- package/dist/devcc/components/Overlay.js +60 -0
- package/dist/devcc/components/Overlay.js.map +1 -0
- package/dist/devcc/components/ProjectPicker.d.ts +26 -0
- package/dist/devcc/components/ProjectPicker.d.ts.map +1 -0
- package/dist/devcc/components/ProjectPicker.js +114 -0
- package/dist/devcc/components/ProjectPicker.js.map +1 -0
- package/dist/devcc/components/Sidebar.d.ts +42 -0
- package/dist/devcc/components/Sidebar.d.ts.map +1 -0
- package/dist/devcc/components/Sidebar.js +127 -0
- package/dist/devcc/components/Sidebar.js.map +1 -0
- package/dist/devcc/components/StatusBadge.d.ts +16 -0
- package/dist/devcc/components/StatusBadge.d.ts.map +1 -0
- package/dist/devcc/components/StatusBadge.js +30 -0
- package/dist/devcc/components/StatusBadge.js.map +1 -0
- package/dist/devcc/components/primitives.d.ts +62 -0
- package/dist/devcc/components/primitives.d.ts.map +1 -0
- package/dist/devcc/components/primitives.js +169 -0
- package/dist/devcc/components/primitives.js.map +1 -0
- package/dist/devcc/components/styled.d.ts +16 -0
- package/dist/devcc/components/styled.d.ts.map +1 -0
- package/dist/devcc/components/styled.js +35 -0
- package/dist/devcc/components/styled.js.map +1 -0
- package/dist/devcc/core/commands.d.ts +89 -0
- package/dist/devcc/core/commands.d.ts.map +1 -0
- package/dist/devcc/core/commands.js +68 -0
- package/dist/devcc/core/commands.js.map +1 -0
- package/dist/devcc/core/config.d.ts +81 -0
- package/dist/devcc/core/config.d.ts.map +1 -0
- package/dist/devcc/core/config.js +313 -0
- package/dist/devcc/core/config.js.map +1 -0
- package/dist/devcc/core/events.d.ts +21 -0
- package/dist/devcc/core/events.d.ts.map +1 -0
- package/dist/devcc/core/events.js +27 -0
- package/dist/devcc/core/events.js.map +1 -0
- package/dist/devcc/core/state.d.ts +104 -0
- package/dist/devcc/core/state.d.ts.map +1 -0
- package/dist/devcc/core/state.js +139 -0
- package/dist/devcc/core/state.js.map +1 -0
- package/dist/devcc/deps/DependencyService.d.ts +50 -0
- package/dist/devcc/deps/DependencyService.d.ts.map +1 -0
- package/dist/devcc/deps/DependencyService.js +109 -0
- package/dist/devcc/deps/DependencyService.js.map +1 -0
- package/dist/devcc/deps/parse.d.ts +49 -0
- package/dist/devcc/deps/parse.d.ts.map +1 -0
- package/dist/devcc/deps/parse.js +158 -0
- package/dist/devcc/deps/parse.js.map +1 -0
- package/dist/devcc/git/GitService.d.ts +60 -0
- package/dist/devcc/git/GitService.d.ts.map +1 -0
- package/dist/devcc/git/GitService.js +160 -0
- package/dist/devcc/git/GitService.js.map +1 -0
- package/dist/devcc/git/parse.d.ts +57 -0
- package/dist/devcc/git/parse.d.ts.map +1 -0
- package/dist/devcc/git/parse.js +174 -0
- package/dist/devcc/git/parse.js.map +1 -0
- package/dist/devcc/index.d.ts +37 -0
- package/dist/devcc/index.d.ts.map +1 -0
- package/dist/devcc/index.js +36 -0
- package/dist/devcc/index.js.map +1 -0
- package/dist/devcc/logs/LogBuffer.d.ts +86 -0
- package/dist/devcc/logs/LogBuffer.d.ts.map +1 -0
- package/dist/devcc/logs/LogBuffer.js +200 -0
- package/dist/devcc/logs/LogBuffer.js.map +1 -0
- package/dist/devcc/logs/merge.d.ts +36 -0
- package/dist/devcc/logs/merge.d.ts.map +1 -0
- package/dist/devcc/logs/merge.js +71 -0
- package/dist/devcc/logs/merge.js.map +1 -0
- package/dist/devcc/processes/ManagedProcess.d.ts +129 -0
- package/dist/devcc/processes/ManagedProcess.d.ts.map +1 -0
- package/dist/devcc/processes/ManagedProcess.js +80 -0
- package/dist/devcc/processes/ManagedProcess.js.map +1 -0
- package/dist/devcc/processes/ProcessManager.d.ts +112 -0
- package/dist/devcc/processes/ProcessManager.d.ts.map +1 -0
- package/dist/devcc/processes/ProcessManager.js +525 -0
- package/dist/devcc/processes/ProcessManager.js.map +1 -0
- package/dist/devcc/processes/graph.d.ts +35 -0
- package/dist/devcc/processes/graph.d.ts.map +1 -0
- package/dist/devcc/processes/graph.js +68 -0
- package/dist/devcc/processes/graph.js.map +1 -0
- package/dist/devcc/projects/ProjectRegistry.d.ts +52 -0
- package/dist/devcc/projects/ProjectRegistry.d.ts.map +1 -0
- package/dist/devcc/projects/ProjectRegistry.js +177 -0
- package/dist/devcc/projects/ProjectRegistry.js.map +1 -0
- package/dist/devcc/services/ServiceManager.d.ts +48 -0
- package/dist/devcc/services/ServiceManager.d.ts.map +1 -0
- package/dist/devcc/services/ServiceManager.js +146 -0
- package/dist/devcc/services/ServiceManager.js.map +1 -0
- package/dist/devcc/services/docker.d.ts +37 -0
- package/dist/devcc/services/docker.d.ts.map +1 -0
- package/dist/devcc/services/docker.js +179 -0
- package/dist/devcc/services/docker.js.map +1 -0
- package/dist/devcc/services/node.d.ts +40 -0
- package/dist/devcc/services/node.d.ts.map +1 -0
- package/dist/devcc/services/node.js +107 -0
- package/dist/devcc/services/node.js.map +1 -0
- package/dist/devcc/services/ollama.d.ts +27 -0
- package/dist/devcc/services/ollama.d.ts.map +1 -0
- package/dist/devcc/services/ollama.js +66 -0
- package/dist/devcc/services/ollama.js.map +1 -0
- package/dist/devcc/services/postgres.d.ts +30 -0
- package/dist/devcc/services/postgres.d.ts.map +1 -0
- package/dist/devcc/services/postgres.js +47 -0
- package/dist/devcc/services/postgres.js.map +1 -0
- package/dist/devcc/services/redis.d.ts +20 -0
- package/dist/devcc/services/redis.d.ts.map +1 -0
- package/dist/devcc/services/redis.js +39 -0
- package/dist/devcc/services/redis.js.map +1 -0
- package/dist/devcc/services/types.d.ts +51 -0
- package/dist/devcc/services/types.d.ts.map +1 -0
- package/dist/devcc/services/types.js +10 -0
- package/dist/devcc/services/types.js.map +1 -0
- package/dist/devcc/system/SystemMonitor.d.ts +87 -0
- package/dist/devcc/system/SystemMonitor.d.ts.map +1 -0
- package/dist/devcc/system/SystemMonitor.js +142 -0
- package/dist/devcc/system/SystemMonitor.js.map +1 -0
- package/dist/devcc/utils/errors.d.ts +31 -0
- package/dist/devcc/utils/errors.d.ts.map +1 -0
- package/dist/devcc/utils/errors.js +52 -0
- package/dist/devcc/utils/errors.js.map +1 -0
- package/dist/devcc/utils/exec.d.ts +41 -0
- package/dist/devcc/utils/exec.d.ts.map +1 -0
- package/dist/devcc/utils/exec.js +112 -0
- package/dist/devcc/utils/exec.js.map +1 -0
- package/dist/devcc/utils/format.d.ts +18 -0
- package/dist/devcc/utils/format.d.ts.map +1 -0
- package/dist/devcc/utils/format.js +85 -0
- package/dist/devcc/utils/format.js.map +1 -0
- package/dist/devcc/utils/fuzzy.d.ts +12 -0
- package/dist/devcc/utils/fuzzy.d.ts.map +1 -0
- package/dist/devcc/utils/fuzzy.js +20 -0
- package/dist/devcc/utils/fuzzy.js.map +1 -0
- package/dist/devcc/utils/logger.d.ts +41 -0
- package/dist/devcc/utils/logger.d.ts.map +1 -0
- package/dist/devcc/utils/logger.js +117 -0
- package/dist/devcc/utils/logger.js.map +1 -0
- package/dist/devcc/utils/packageManager.d.ts +39 -0
- package/dist/devcc/utils/packageManager.d.ts.map +1 -0
- package/dist/devcc/utils/packageManager.js +85 -0
- package/dist/devcc/utils/packageManager.js.map +1 -0
- package/dist/devcc/utils/paths.d.ts +12 -0
- package/dist/devcc/utils/paths.d.ts.map +1 -0
- package/dist/devcc/utils/paths.js +39 -0
- package/dist/devcc/utils/paths.js.map +1 -0
- package/dist/devcc/views/DependenciesView.d.ts +34 -0
- package/dist/devcc/views/DependenciesView.d.ts.map +1 -0
- package/dist/devcc/views/DependenciesView.js +170 -0
- package/dist/devcc/views/DependenciesView.js.map +1 -0
- package/dist/devcc/views/GitView.d.ts +29 -0
- package/dist/devcc/views/GitView.d.ts.map +1 -0
- package/dist/devcc/views/GitView.js +226 -0
- package/dist/devcc/views/GitView.js.map +1 -0
- package/dist/devcc/views/LogsView.d.ts +39 -0
- package/dist/devcc/views/LogsView.d.ts.map +1 -0
- package/dist/devcc/views/LogsView.js +249 -0
- package/dist/devcc/views/LogsView.js.map +1 -0
- package/dist/devcc/views/OverviewView.d.ts +16 -0
- package/dist/devcc/views/OverviewView.d.ts.map +1 -0
- package/dist/devcc/views/OverviewView.js +135 -0
- package/dist/devcc/views/OverviewView.js.map +1 -0
- package/dist/devcc/views/ProcessesView.d.ts +26 -0
- package/dist/devcc/views/ProcessesView.d.ts.map +1 -0
- package/dist/devcc/views/ProcessesView.js +161 -0
- package/dist/devcc/views/ProcessesView.js.map +1 -0
- package/dist/devcc/views/ScriptsView.d.ts +30 -0
- package/dist/devcc/views/ScriptsView.d.ts.map +1 -0
- package/dist/devcc/views/ScriptsView.js +168 -0
- package/dist/devcc/views/ScriptsView.js.map +1 -0
- package/dist/devcc/views/ServicesView.d.ts +28 -0
- package/dist/devcc/views/ServicesView.d.ts.map +1 -0
- package/dist/devcc/views/ServicesView.js +153 -0
- package/dist/devcc/views/ServicesView.js.map +1 -0
- package/dist/devcc/views/SettingsView.d.ts +20 -0
- package/dist/devcc/views/SettingsView.d.ts.map +1 -0
- package/dist/devcc/views/SettingsView.js +114 -0
- package/dist/devcc/views/SettingsView.js.map +1 -0
- package/dist/devcc/views/SystemView.d.ts +13 -0
- package/dist/devcc/views/SystemView.d.ts.map +1 -0
- package/dist/devcc/views/SystemView.js +82 -0
- package/dist/devcc/views/SystemView.js.map +1 -0
- package/dist/devcc/views/View.d.ts +35 -0
- package/dist/devcc/views/View.d.ts.map +1 -0
- package/dist/devcc/views/View.js +42 -0
- package/dist/devcc/views/View.js.map +1 -0
- package/package.json +79 -0
- package/src/devcc/app/App.ts +1463 -0
- package/src/devcc/app/builtinCommands.ts +429 -0
- package/src/devcc/app/keymap.ts +167 -0
- package/src/devcc/app/theme.ts +83 -0
- package/src/devcc/cli.ts +122 -0
- package/src/devcc/components/CommandPalette.ts +136 -0
- package/src/devcc/components/ConfirmDialog.ts +174 -0
- package/src/devcc/components/Footer.ts +85 -0
- package/src/devcc/components/Header.ts +93 -0
- package/src/devcc/components/HelpOverlay.ts +62 -0
- package/src/devcc/components/Overlay.ts +91 -0
- package/src/devcc/components/ProjectPicker.ts +138 -0
- package/src/devcc/components/Sidebar.ts +163 -0
- package/src/devcc/components/StatusBadge.ts +36 -0
- package/src/devcc/components/primitives.ts +213 -0
- package/src/devcc/components/styled.ts +44 -0
- package/src/devcc/core/commands.ts +140 -0
- package/src/devcc/core/config.ts +405 -0
- package/src/devcc/core/events.ts +41 -0
- package/src/devcc/core/state.ts +236 -0
- package/src/devcc/deps/DependencyService.ts +152 -0
- package/src/devcc/deps/parse.ts +210 -0
- package/src/devcc/git/GitService.ts +205 -0
- package/src/devcc/git/parse.ts +203 -0
- package/src/devcc/index.ts +115 -0
- package/src/devcc/logs/LogBuffer.ts +237 -0
- package/src/devcc/logs/merge.ts +87 -0
- package/src/devcc/processes/ManagedProcess.ts +195 -0
- package/src/devcc/processes/ProcessManager.ts +625 -0
- package/src/devcc/processes/graph.ts +98 -0
- package/src/devcc/projects/ProjectRegistry.ts +219 -0
- package/src/devcc/services/ServiceManager.ts +180 -0
- package/src/devcc/services/docker.ts +220 -0
- package/src/devcc/services/node.ts +137 -0
- package/src/devcc/services/ollama.ts +80 -0
- package/src/devcc/services/postgres.ts +57 -0
- package/src/devcc/services/redis.ts +47 -0
- package/src/devcc/services/types.ts +59 -0
- package/src/devcc/system/SystemMonitor.ts +207 -0
- package/src/devcc/utils/errors.ts +64 -0
- package/src/devcc/utils/exec.ts +165 -0
- package/src/devcc/utils/format.ts +83 -0
- package/src/devcc/utils/fuzzy.ts +44 -0
- package/src/devcc/utils/logger.ts +140 -0
- package/src/devcc/utils/packageManager.ts +112 -0
- package/src/devcc/utils/paths.ts +39 -0
- package/src/devcc/views/DependenciesView.ts +206 -0
- package/src/devcc/views/GitView.ts +259 -0
- package/src/devcc/views/LogsView.ts +300 -0
- package/src/devcc/views/OverviewView.ts +187 -0
- package/src/devcc/views/ProcessesView.ts +177 -0
- package/src/devcc/views/ScriptsView.ts +187 -0
- package/src/devcc/views/ServicesView.ts +188 -0
- package/src/devcc/views/SettingsView.ts +146 -0
- package/src/devcc/views/SystemView.ts +110 -0
- package/src/devcc/views/View.ts +63 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { KeyEvent } from "@opentui/core"
|
|
2
|
+
import { theme } from "../app/theme.js"
|
|
3
|
+
import type { KeyHint } from "../components/Footer.js"
|
|
4
|
+
import { RowList, type ListRow } from "../components/primitives.js"
|
|
5
|
+
import { chunk, dim, styled } from "../components/styled.js"
|
|
6
|
+
import { setSelection, type AppState, type ScriptRun, type ViewId } from "../core/state.js"
|
|
7
|
+
import { errorMessage } from "../utils/errors.js"
|
|
8
|
+
import { fit, formatDuration, singleLine } from "../utils/format.js"
|
|
9
|
+
import { fuzzyFilter } from "../utils/fuzzy.js"
|
|
10
|
+
import { View, type ViewDeps } from "./View.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* package.json scripts, run through the detected package manager.
|
|
14
|
+
*
|
|
15
|
+
* devcc does not reimplement script running: it shells out to `pnpm run x` (or
|
|
16
|
+
* the npm/yarn/bun equivalent) as a managed process so the output lands in the
|
|
17
|
+
* log viewer like anything else.
|
|
18
|
+
*/
|
|
19
|
+
export class ScriptsView extends View {
|
|
20
|
+
readonly id: ViewId = "scripts"
|
|
21
|
+
readonly title = "Scripts"
|
|
22
|
+
|
|
23
|
+
private readonly list: RowList
|
|
24
|
+
private filter = ""
|
|
25
|
+
private visible: { name: string; command: string }[] = []
|
|
26
|
+
|
|
27
|
+
constructor(deps: ViewDeps) {
|
|
28
|
+
super(deps)
|
|
29
|
+
this.list = new RowList(deps.ctx, { flexGrow: 1 })
|
|
30
|
+
this.box.add(this.list.box)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
update(state: AppState): void {
|
|
34
|
+
const width = Math.max(40, state.ui.width - (state.ui.sidebarVisible ? 26 : 4))
|
|
35
|
+
const nameWidth = Math.min(24, Math.max(12, Math.floor(width * 0.24)))
|
|
36
|
+
|
|
37
|
+
this.visible =
|
|
38
|
+
this.filter.trim() === ""
|
|
39
|
+
? state.scripts.available
|
|
40
|
+
: fuzzyFilter(state.scripts.available, this.filter, (script) => script.name).map(
|
|
41
|
+
(match) => match.item,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const rows: ListRow[] = this.visible.map((script) => {
|
|
45
|
+
const run = state.scripts.runs[script.name]
|
|
46
|
+
return {
|
|
47
|
+
key: script.name,
|
|
48
|
+
content: styled(
|
|
49
|
+
runGlyph(run),
|
|
50
|
+
" ",
|
|
51
|
+
chunk(fit(script.name, nameWidth), theme.textBright),
|
|
52
|
+
dim(fit(singleLine(script.command), Math.max(10, width - nameWidth - 24))),
|
|
53
|
+
runDetail(run),
|
|
54
|
+
),
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (rows.length === 0) {
|
|
59
|
+
rows.push({
|
|
60
|
+
key: "__empty",
|
|
61
|
+
disabled: true,
|
|
62
|
+
content: styled(
|
|
63
|
+
dim(this.filter ? `No scripts match "${this.filter}"` : "No scripts in package.json"),
|
|
64
|
+
),
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.list.setRows(rows, state.ui.selection.scripts)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override handleKey(key: KeyEvent, state: AppState): boolean {
|
|
72
|
+
const count = this.visible.length
|
|
73
|
+
const selected = state.ui.selection.scripts
|
|
74
|
+
|
|
75
|
+
switch (key.name) {
|
|
76
|
+
case "up":
|
|
77
|
+
case "k":
|
|
78
|
+
if (count > 0) setSelection(this.deps.app.store, "scripts", Math.max(0, selected - 1))
|
|
79
|
+
return true
|
|
80
|
+
case "down":
|
|
81
|
+
case "j":
|
|
82
|
+
if (count > 0) setSelection(this.deps.app.store, "scripts", Math.min(count - 1, selected + 1))
|
|
83
|
+
return true
|
|
84
|
+
case "return":
|
|
85
|
+
case "s":
|
|
86
|
+
void this.run(selected)
|
|
87
|
+
return true
|
|
88
|
+
case "x":
|
|
89
|
+
void this.stop(selected)
|
|
90
|
+
return true
|
|
91
|
+
case "f":
|
|
92
|
+
void this.promptFilter()
|
|
93
|
+
return true
|
|
94
|
+
case "c":
|
|
95
|
+
this.setFilter("")
|
|
96
|
+
return true
|
|
97
|
+
case "l":
|
|
98
|
+
this.openLogs(selected)
|
|
99
|
+
return false
|
|
100
|
+
default:
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override hints(): KeyHint[] {
|
|
106
|
+
return [
|
|
107
|
+
{ keys: "↑↓/jk", label: "select" },
|
|
108
|
+
{ keys: "enter", label: "run" },
|
|
109
|
+
{ keys: "x", label: "stop" },
|
|
110
|
+
{ keys: "f", label: "filter" },
|
|
111
|
+
{ keys: "c", label: "clear filter" },
|
|
112
|
+
{ keys: "l", label: "logs" },
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async promptFilter(): Promise<void> {
|
|
117
|
+
const value = await this.deps.app.prompt("Filter scripts", this.filter || "e.g. test")
|
|
118
|
+
if (value !== null) this.setFilter(value)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private setFilter(value: string): void {
|
|
122
|
+
this.filter = value
|
|
123
|
+
setSelection(this.deps.app.store, "scripts", 0)
|
|
124
|
+
this.update(this.deps.app.store.getState())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async run(index: number): Promise<void> {
|
|
128
|
+
const script = this.visible[index]
|
|
129
|
+
if (!script) return
|
|
130
|
+
try {
|
|
131
|
+
await this.deps.app.runScript(script.name)
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.deps.app.notify("error", errorMessage(error))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async stop(index: number): Promise<void> {
|
|
138
|
+
const script = this.visible[index]
|
|
139
|
+
if (!script) return
|
|
140
|
+
const processId = scriptProcessId(script.name)
|
|
141
|
+
if (!this.deps.app.processes.isRunning(processId)) {
|
|
142
|
+
this.deps.app.notify("warn", `${script.name} is not running`)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await this.deps.app.processes.stop(processId)
|
|
147
|
+
this.deps.app.notify("info", `Stopped ${script.name}`)
|
|
148
|
+
} catch (error) {
|
|
149
|
+
this.deps.app.notify("error", errorMessage(error))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private openLogs(index: number): void {
|
|
154
|
+
const script = this.visible[index]
|
|
155
|
+
if (!script) return
|
|
156
|
+
this.deps.app.store.setState((state) => ({
|
|
157
|
+
...state,
|
|
158
|
+
ui: { ...state.ui, logSource: scriptProcessId(script.name) },
|
|
159
|
+
}))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Log/process id used for a script run. */
|
|
164
|
+
export function scriptProcessId(scriptName: string): string {
|
|
165
|
+
return `script:${scriptName}`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function runGlyph(run: ScriptRun | undefined) {
|
|
169
|
+
if (!run) return dim("·")
|
|
170
|
+
switch (run.status) {
|
|
171
|
+
case "running":
|
|
172
|
+
return chunk("◐", theme.warning)
|
|
173
|
+
case "succeeded":
|
|
174
|
+
return chunk("✓", theme.success)
|
|
175
|
+
case "failed":
|
|
176
|
+
return chunk("✖", theme.danger)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function runDetail(run: ScriptRun | undefined) {
|
|
181
|
+
if (!run) return dim("")
|
|
182
|
+
const elapsed = (run.finishedAt ?? new Date()).getTime() - run.startedAt.getTime()
|
|
183
|
+
if (run.status === "running") return chunk(fit(`running ${formatDuration(elapsed)}`, 20), theme.warning)
|
|
184
|
+
const code = run.exitCode ?? 0
|
|
185
|
+
const label = `exit ${code} · ${formatDuration(elapsed)}`
|
|
186
|
+
return chunk(fit(label, 20), run.status === "failed" ? theme.danger : theme.textDim)
|
|
187
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { KeyEvent } from "@opentui/core"
|
|
2
|
+
import { theme } from "../app/theme.js"
|
|
3
|
+
import type { KeyHint } from "../components/Footer.js"
|
|
4
|
+
import { RowList, type ListRow } from "../components/primitives.js"
|
|
5
|
+
import { serviceBadge, serviceGlyph } from "../components/StatusBadge.js"
|
|
6
|
+
import { chunk, dim, styled } from "../components/styled.js"
|
|
7
|
+
import { setSelection, type AppState, type ViewId } from "../core/state.js"
|
|
8
|
+
import type { ServiceSnapshot } from "../services/types.js"
|
|
9
|
+
import { errorMessage } from "../utils/errors.js"
|
|
10
|
+
import { fit } from "../utils/format.js"
|
|
11
|
+
import { View, type ViewDeps } from "./View.js"
|
|
12
|
+
|
|
13
|
+
interface Entry {
|
|
14
|
+
kind: "service" | "item" | "heading"
|
|
15
|
+
service?: ServiceSnapshot
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detected services, grouped by category.
|
|
20
|
+
*
|
|
21
|
+
* Actions are only offered when the adapter implements them; a database devcc
|
|
22
|
+
* merely observes has no start/stop keys.
|
|
23
|
+
*/
|
|
24
|
+
export class ServicesView extends View {
|
|
25
|
+
readonly id: ViewId = "services"
|
|
26
|
+
readonly title = "Services"
|
|
27
|
+
|
|
28
|
+
private readonly list: RowList
|
|
29
|
+
private entries: Entry[] = []
|
|
30
|
+
|
|
31
|
+
constructor(deps: ViewDeps) {
|
|
32
|
+
super(deps)
|
|
33
|
+
this.list = new RowList(deps.ctx, { flexGrow: 1 })
|
|
34
|
+
this.box.add(this.list.box)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
update(state: AppState): void {
|
|
38
|
+
const rows: ListRow[] = []
|
|
39
|
+
const entries: Entry[] = []
|
|
40
|
+
const width = Math.max(40, state.ui.width - (state.ui.sidebarVisible ? 26 : 4))
|
|
41
|
+
|
|
42
|
+
const byCategory = new Map<string, ServiceSnapshot[]>()
|
|
43
|
+
for (const service of state.services) {
|
|
44
|
+
const bucket = byCategory.get(service.category) ?? []
|
|
45
|
+
bucket.push(service)
|
|
46
|
+
byCategory.set(service.category, bucket)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (state.services.length === 0) {
|
|
50
|
+
rows.push({
|
|
51
|
+
key: "__empty",
|
|
52
|
+
disabled: true,
|
|
53
|
+
content: styled(dim("No services detected yet. Detection runs in the background at startup.")),
|
|
54
|
+
})
|
|
55
|
+
entries.push({ kind: "heading" })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const [category, services] of [...byCategory.entries()].sort()) {
|
|
59
|
+
rows.push({
|
|
60
|
+
key: `heading-${category}`,
|
|
61
|
+
disabled: true,
|
|
62
|
+
content: styled(chunk(category.toUpperCase(), theme.accent)),
|
|
63
|
+
})
|
|
64
|
+
entries.push({ kind: "heading" })
|
|
65
|
+
|
|
66
|
+
for (const service of services) {
|
|
67
|
+
rows.push({
|
|
68
|
+
key: service.id,
|
|
69
|
+
content: styled(
|
|
70
|
+
...serviceBadge(service.status.state).chunks,
|
|
71
|
+
chunk(fit(service.name, 20), theme.textBright),
|
|
72
|
+
dim(fit(service.status.detail ?? "", Math.max(10, width - 56))),
|
|
73
|
+
dim(service.status.endpoint ?? ""),
|
|
74
|
+
),
|
|
75
|
+
})
|
|
76
|
+
entries.push({ kind: "service", service })
|
|
77
|
+
|
|
78
|
+
for (const item of service.status.items ?? []) {
|
|
79
|
+
rows.push({
|
|
80
|
+
key: `${service.id}-${item.name}`,
|
|
81
|
+
disabled: true,
|
|
82
|
+
content: styled(
|
|
83
|
+
" ",
|
|
84
|
+
serviceGlyph(item.state).chunks[0] ?? chunk(" "),
|
|
85
|
+
" ",
|
|
86
|
+
dim(fit(item.name, 20)),
|
|
87
|
+
dim(item.detail ?? ""),
|
|
88
|
+
),
|
|
89
|
+
})
|
|
90
|
+
entries.push({ kind: "item" })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.entries = entries
|
|
96
|
+
this.list.setRows(rows, state.ui.selection.services)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override handleKey(key: KeyEvent, state: AppState): boolean {
|
|
100
|
+
const selected = state.ui.selection.services
|
|
101
|
+
|
|
102
|
+
switch (key.name) {
|
|
103
|
+
case "up":
|
|
104
|
+
case "k":
|
|
105
|
+
this.moveSelection(selected, -1)
|
|
106
|
+
return true
|
|
107
|
+
case "down":
|
|
108
|
+
case "j":
|
|
109
|
+
this.moveSelection(selected, 1)
|
|
110
|
+
return true
|
|
111
|
+
case "s":
|
|
112
|
+
void this.toggle(selected)
|
|
113
|
+
return true
|
|
114
|
+
case "r":
|
|
115
|
+
void this.action(selected, "restart")
|
|
116
|
+
return true
|
|
117
|
+
case "d":
|
|
118
|
+
void this.composeDown(selected)
|
|
119
|
+
return true
|
|
120
|
+
default:
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
override hints(): KeyHint[] {
|
|
126
|
+
return [
|
|
127
|
+
{ keys: "↑↓/jk", label: "select" },
|
|
128
|
+
{ keys: "s", label: "start/stop" },
|
|
129
|
+
{ keys: "r", label: "restart" },
|
|
130
|
+
{ keys: "d", label: "compose down" },
|
|
131
|
+
{ keys: "/", label: "commands" },
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Skip headings and sub-items when moving. */
|
|
136
|
+
private moveSelection(from: number, direction: 1 | -1): void {
|
|
137
|
+
let index = from
|
|
138
|
+
for (let steps = 0; steps < this.entries.length; steps += 1) {
|
|
139
|
+
index += direction
|
|
140
|
+
if (index < 0 || index >= this.entries.length) return
|
|
141
|
+
if (this.entries[index]?.kind === "service") {
|
|
142
|
+
setSelection(this.deps.app.store, "services", index)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private selectedService(index: number): ServiceSnapshot | undefined {
|
|
149
|
+
return this.entries[index]?.service
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async toggle(index: number): Promise<void> {
|
|
153
|
+
const service = this.selectedService(index)
|
|
154
|
+
if (!service) return
|
|
155
|
+
const running = service.status.state === "running" || service.status.state === "degraded"
|
|
156
|
+
await this.action(index, running ? "stop" : "start")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async action(index: number, action: "start" | "stop" | "restart"): Promise<void> {
|
|
160
|
+
const service = this.selectedService(index)
|
|
161
|
+
if (!service) return
|
|
162
|
+
|
|
163
|
+
const supported =
|
|
164
|
+
action === "start" ? service.canStart : action === "stop" ? service.canStop : service.canRestart
|
|
165
|
+
if (!supported) {
|
|
166
|
+
this.deps.app.notify("warn", `${service.name} cannot be ${action}ed from devcc`)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
this.deps.app.notify("info", `${action}ing ${service.name}…`)
|
|
172
|
+
await this.deps.app.services.runAction(service.id, action, { confirm: this.deps.app.confirm })
|
|
173
|
+
this.deps.app.notify("success", `${service.name} ${action} complete`)
|
|
174
|
+
} catch (error) {
|
|
175
|
+
this.deps.app.notify("error", errorMessage(error))
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** `docker compose down` removes containers; always confirmed. */
|
|
180
|
+
private async composeDown(index: number): Promise<void> {
|
|
181
|
+
const service = this.selectedService(index)
|
|
182
|
+
if (!service || service.id !== "docker") {
|
|
183
|
+
this.deps.app.notify("warn", "Select the Docker Compose service first")
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
await this.deps.commands.run("docker.down", this.deps.app)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { KeyEvent } from "@opentui/core"
|
|
2
|
+
import { theme } from "../app/theme.js"
|
|
3
|
+
import type { KeyHint } from "../components/Footer.js"
|
|
4
|
+
import { RowList, type ListRow } from "../components/primitives.js"
|
|
5
|
+
import { chunk, dim, styled } from "../components/styled.js"
|
|
6
|
+
import type { AppState, Notification, ViewId } from "../core/state.js"
|
|
7
|
+
import { fit, formatTimestamp, singleLine } from "../utils/format.js"
|
|
8
|
+
import { tildify } from "../utils/paths.js"
|
|
9
|
+
import { View, type ViewDeps } from "./View.js"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configuration, detection results and the diagnostics log.
|
|
13
|
+
*
|
|
14
|
+
* This is where errors go to be inspected: the footer only shows the newest
|
|
15
|
+
* message, the full history (with detail) lives here.
|
|
16
|
+
*/
|
|
17
|
+
export class SettingsView extends View {
|
|
18
|
+
readonly id: ViewId = "settings"
|
|
19
|
+
readonly title = "Settings"
|
|
20
|
+
|
|
21
|
+
private readonly list: RowList
|
|
22
|
+
|
|
23
|
+
constructor(deps: ViewDeps) {
|
|
24
|
+
super(deps)
|
|
25
|
+
this.list = new RowList(deps.ctx, { flexGrow: 1, scrollPadding: 0 })
|
|
26
|
+
this.box.add(this.list.box)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
update(state: AppState): void {
|
|
30
|
+
const rows: ListRow[] = []
|
|
31
|
+
|
|
32
|
+
rows.push(heading("Project"))
|
|
33
|
+
rows.push(row("name", state.project.name))
|
|
34
|
+
rows.push(row("root", tildify(state.project.rootDir)))
|
|
35
|
+
rows.push(
|
|
36
|
+
row(
|
|
37
|
+
"package manager",
|
|
38
|
+
`${state.project.packageManager.manager} (${state.project.packageManager.source}${
|
|
39
|
+
state.project.packageManager.evidence ? `: ${state.project.packageManager.evidence}` : ""
|
|
40
|
+
})`,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
rows.push(
|
|
44
|
+
row("config", state.project.configPath ? tildify(state.project.configPath) : "none (using defaults)"),
|
|
45
|
+
)
|
|
46
|
+
rows.push(row("log file", tildify(logPath(state))))
|
|
47
|
+
|
|
48
|
+
if (state.project.configIssues.length > 0) {
|
|
49
|
+
rows.push(blank())
|
|
50
|
+
rows.push(heading("Config issues"))
|
|
51
|
+
for (const issue of state.project.configIssues) {
|
|
52
|
+
rows.push({
|
|
53
|
+
key: `issue-${issue}`,
|
|
54
|
+
disabled: true,
|
|
55
|
+
content: styled(chunk(`⚠ ${issue}`, theme.warning)),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
rows.push(blank())
|
|
61
|
+
rows.push(heading("Counts"))
|
|
62
|
+
rows.push(row("managed processes", String(state.processes.length)))
|
|
63
|
+
rows.push(row("detected services", String(state.services.length)))
|
|
64
|
+
rows.push(row("package scripts", String(state.scripts.available.length)))
|
|
65
|
+
rows.push(row("log sources", String(state.logSources.length)))
|
|
66
|
+
|
|
67
|
+
rows.push(blank())
|
|
68
|
+
rows.push(heading(`Diagnostics (${state.notifications.length})`))
|
|
69
|
+
if (state.notifications.length === 0) {
|
|
70
|
+
rows.push({ key: "no-diag", disabled: true, content: styled(dim(" nothing to report")) })
|
|
71
|
+
}
|
|
72
|
+
for (const notification of state.notifications.slice(0, 40)) {
|
|
73
|
+
rows.push({
|
|
74
|
+
key: `notification-${notification.id}`,
|
|
75
|
+
content: styled(
|
|
76
|
+
dim(fit(formatTimestamp(notification.createdAt), 13)),
|
|
77
|
+
chunk(fit(notification.level, 8), levelColor(notification.level)),
|
|
78
|
+
chunk(singleLine(notification.message, 90), theme.text),
|
|
79
|
+
),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.list.setRows(rows, state.ui.selection.settings)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override handleKey(key: KeyEvent, _state: AppState): boolean {
|
|
87
|
+
switch (key.name) {
|
|
88
|
+
case "up":
|
|
89
|
+
case "k":
|
|
90
|
+
this.list.scrollBy(-1)
|
|
91
|
+
return true
|
|
92
|
+
case "down":
|
|
93
|
+
case "j":
|
|
94
|
+
this.list.scrollBy(1)
|
|
95
|
+
return true
|
|
96
|
+
case "c":
|
|
97
|
+
this.deps.app.store.setState((state) => ({ ...state, notifications: [] }))
|
|
98
|
+
return true
|
|
99
|
+
default:
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override hints(): KeyHint[] {
|
|
105
|
+
return [
|
|
106
|
+
{ keys: "j/k", label: "scroll" },
|
|
107
|
+
{ keys: "c", label: "clear diagnostics" },
|
|
108
|
+
{ keys: "/", label: "commands" },
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const LABEL_WIDTH = 20
|
|
114
|
+
|
|
115
|
+
function logPath(state: AppState): string {
|
|
116
|
+
return `${state.project.rootDir}/.devcc/devcc.log`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function heading(title: string): ListRow {
|
|
120
|
+
return { key: `h-${title}`, disabled: true, content: styled(chunk(title.toUpperCase(), theme.accent)) }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function blank(): ListRow {
|
|
124
|
+
return { key: `b-${Math.random().toString(36).slice(2)}`, disabled: true, content: "" }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function row(label: string, value: string): ListRow {
|
|
128
|
+
return {
|
|
129
|
+
key: `r-${label}`,
|
|
130
|
+
disabled: true,
|
|
131
|
+
content: styled(dim(fit(label, LABEL_WIDTH)), chunk(value, theme.text)),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function levelColor(level: Notification["level"]): string {
|
|
136
|
+
switch (level) {
|
|
137
|
+
case "error":
|
|
138
|
+
return theme.danger
|
|
139
|
+
case "warn":
|
|
140
|
+
return theme.warning
|
|
141
|
+
case "success":
|
|
142
|
+
return theme.success
|
|
143
|
+
case "info":
|
|
144
|
+
return theme.info
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { theme } from "../app/theme.js"
|
|
2
|
+
import type { KeyHint } from "../components/Footer.js"
|
|
3
|
+
import { RowList, type ListRow } from "../components/primitives.js"
|
|
4
|
+
import { chunk, dim, styled } from "../components/styled.js"
|
|
5
|
+
import type { AppState, ViewId } from "../core/state.js"
|
|
6
|
+
import { fit, formatBytes, formatDuration, meter, toGigabytes } from "../utils/format.js"
|
|
7
|
+
import { View, type ViewDeps } from "./View.js"
|
|
8
|
+
|
|
9
|
+
/** Host information and live resource usage. Read-only. */
|
|
10
|
+
export class SystemView extends View {
|
|
11
|
+
readonly id: ViewId = "system"
|
|
12
|
+
readonly title = "System"
|
|
13
|
+
|
|
14
|
+
private readonly list: RowList
|
|
15
|
+
|
|
16
|
+
constructor(deps: ViewDeps) {
|
|
17
|
+
super(deps)
|
|
18
|
+
this.list = new RowList(deps.ctx, { flexGrow: 1, scrollPadding: 0 })
|
|
19
|
+
this.box.add(this.list.box)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
update(state: AppState): void {
|
|
23
|
+
const host = state.project.host
|
|
24
|
+
const tools = state.project.tools
|
|
25
|
+
const metrics = state.system
|
|
26
|
+
const rows: ListRow[] = []
|
|
27
|
+
|
|
28
|
+
rows.push(heading("Host"))
|
|
29
|
+
rows.push(row("hostname", host.hostname))
|
|
30
|
+
rows.push(row("os", `${host.platform} ${host.release}`))
|
|
31
|
+
rows.push(row("arch", host.arch))
|
|
32
|
+
rows.push(row("cpu", `${host.cpuModel} (${host.cpuCount} cores)`))
|
|
33
|
+
rows.push(row("memory", `${toGigabytes(host.totalMemory)} GB total`))
|
|
34
|
+
|
|
35
|
+
rows.push(blank())
|
|
36
|
+
rows.push(heading("Toolchain"))
|
|
37
|
+
rows.push(row("runtime", host.runtime))
|
|
38
|
+
rows.push(row("node", host.nodeVersion))
|
|
39
|
+
rows.push(
|
|
40
|
+
row("package manager", `${state.project.packageManager.manager} ${tools.packageManager ?? ""}`.trim()),
|
|
41
|
+
)
|
|
42
|
+
rows.push(row("git", tools.git ?? "not found"))
|
|
43
|
+
rows.push(row("docker", tools.docker ?? "not found"))
|
|
44
|
+
|
|
45
|
+
rows.push(blank())
|
|
46
|
+
rows.push(heading("Usage"))
|
|
47
|
+
if (!metrics) {
|
|
48
|
+
rows.push(row("", "sampling…"))
|
|
49
|
+
} else {
|
|
50
|
+
rows.push(gauge("cpu", metrics.cpu, `${(metrics.cpu * 100).toFixed(1)}%`))
|
|
51
|
+
rows.push(
|
|
52
|
+
gauge(
|
|
53
|
+
"memory",
|
|
54
|
+
metrics.memoryRatio,
|
|
55
|
+
`${toGigabytes(metrics.memoryUsed)} / ${toGigabytes(metrics.memoryTotal)} GB`,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
if (metrics.disk) {
|
|
59
|
+
rows.push(
|
|
60
|
+
gauge(
|
|
61
|
+
"disk",
|
|
62
|
+
metrics.disk.ratio,
|
|
63
|
+
`${formatBytes(metrics.disk.used)} / ${formatBytes(metrics.disk.total)}`,
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
rows.push(row("load average", metrics.loadAverage.map((value) => value.toFixed(2)).join(" ")))
|
|
68
|
+
rows.push(row("host uptime", formatDuration(metrics.uptimeMs)))
|
|
69
|
+
rows.push(row("devcc uptime", formatDuration(metrics.processUptimeMs)))
|
|
70
|
+
rows.push(row("sampled", metrics.sampledAt.toLocaleTimeString()))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.list.setRows(rows, -1)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override hints(): KeyHint[] {
|
|
77
|
+
return [
|
|
78
|
+
{ keys: "↑↓", label: "scroll" },
|
|
79
|
+
{ keys: "/", label: "commands" },
|
|
80
|
+
{ keys: "?", label: "help" },
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const LABEL_WIDTH = 18
|
|
86
|
+
|
|
87
|
+
function heading(title: string): ListRow {
|
|
88
|
+
return { key: `h-${title}`, disabled: true, content: styled(chunk(title.toUpperCase(), theme.accent)) }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function blank(): ListRow {
|
|
92
|
+
return { key: `b-${Math.random().toString(36).slice(2)}`, disabled: true, content: "" }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function row(label: string, value: string): ListRow {
|
|
96
|
+
return {
|
|
97
|
+
key: `r-${label}`,
|
|
98
|
+
disabled: true,
|
|
99
|
+
content: styled(dim(fit(label, LABEL_WIDTH)), chunk(value, theme.text)),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function gauge(label: string, ratio: number, value: string): ListRow {
|
|
104
|
+
const color = ratio > 0.85 ? theme.danger : ratio > 0.6 ? theme.warning : theme.success
|
|
105
|
+
return {
|
|
106
|
+
key: `g-${label}`,
|
|
107
|
+
disabled: true,
|
|
108
|
+
content: styled(dim(fit(label, LABEL_WIDTH)), chunk(meter(ratio, 16), color), dim(` ${value}`)),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { BoxRenderable, type KeyEvent, type RenderContext } from "@opentui/core"
|
|
2
|
+
import { theme } from "../app/theme.js"
|
|
3
|
+
import type { KeyHint } from "../components/Footer.js"
|
|
4
|
+
import type { CommandContext, CommandRegistry } from "../core/commands.js"
|
|
5
|
+
import type { AppState, ViewId } from "../core/state.js"
|
|
6
|
+
|
|
7
|
+
export interface ViewDeps {
|
|
8
|
+
ctx: RenderContext
|
|
9
|
+
/** The same context commands receive; views and commands share one surface. */
|
|
10
|
+
app: CommandContext
|
|
11
|
+
commands: CommandRegistry
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base class for the content panes.
|
|
16
|
+
*
|
|
17
|
+
* Views own a subtree and are told when state changes; they never poll. Key
|
|
18
|
+
* handling is view-first: a view may claim a key (returning `true`) before the
|
|
19
|
+
* global keymap sees it.
|
|
20
|
+
*/
|
|
21
|
+
export abstract class View {
|
|
22
|
+
abstract readonly id: ViewId
|
|
23
|
+
abstract readonly title: string
|
|
24
|
+
|
|
25
|
+
readonly box: BoxRenderable
|
|
26
|
+
|
|
27
|
+
constructor(protected readonly deps: ViewDeps) {
|
|
28
|
+
this.box = new BoxRenderable(deps.ctx, {
|
|
29
|
+
flexGrow: 1,
|
|
30
|
+
flexDirection: "column",
|
|
31
|
+
paddingLeft: 1,
|
|
32
|
+
paddingRight: 1,
|
|
33
|
+
paddingTop: 1,
|
|
34
|
+
backgroundColor: "transparent",
|
|
35
|
+
visible: false,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Called whenever application state changes while this view is active. */
|
|
40
|
+
abstract update(state: AppState): void
|
|
41
|
+
|
|
42
|
+
/** Return true when the key was handled and must not fall through. */
|
|
43
|
+
handleKey(_key: KeyEvent, _state: AppState): boolean {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Footer hints for this view. */
|
|
48
|
+
hints(): KeyHint[] {
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onActivate(_state: AppState): void {}
|
|
53
|
+
|
|
54
|
+
onDeactivate(): void {}
|
|
55
|
+
|
|
56
|
+
dispose(): void {
|
|
57
|
+
this.box.destroyRecursively()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected get accentColor(): string {
|
|
61
|
+
return theme.accent
|
|
62
|
+
}
|
|
63
|
+
}
|