@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,91 @@
|
|
|
1
|
+
import { BoxRenderable, type RenderContext } from "@opentui/core"
|
|
2
|
+
import { theme } from "../app/theme.js"
|
|
3
|
+
|
|
4
|
+
export interface OverlayOptions {
|
|
5
|
+
title: string
|
|
6
|
+
/** Fraction of the terminal width, clamped by min/max. */
|
|
7
|
+
widthRatio?: number
|
|
8
|
+
minWidth?: number
|
|
9
|
+
maxWidth?: number
|
|
10
|
+
heightRatio?: number
|
|
11
|
+
minHeight?: number
|
|
12
|
+
maxHeight?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Base class for modal panels.
|
|
17
|
+
*
|
|
18
|
+
* Overlays are absolutely positioned children of the root, so they float above
|
|
19
|
+
* the layout without disturbing it, and they re-centre themselves on resize.
|
|
20
|
+
*/
|
|
21
|
+
export class Overlay {
|
|
22
|
+
readonly box: BoxRenderable
|
|
23
|
+
protected terminalWidth = 80
|
|
24
|
+
protected terminalHeight = 24
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
protected readonly ctx: RenderContext,
|
|
28
|
+
protected readonly options: OverlayOptions,
|
|
29
|
+
) {
|
|
30
|
+
this.box = new BoxRenderable(ctx, {
|
|
31
|
+
position: "absolute",
|
|
32
|
+
zIndex: 100,
|
|
33
|
+
visible: false,
|
|
34
|
+
flexDirection: "column",
|
|
35
|
+
border: true,
|
|
36
|
+
borderStyle: "rounded",
|
|
37
|
+
borderColor: theme.borderFocus,
|
|
38
|
+
backgroundColor: theme.panel,
|
|
39
|
+
title: options.title,
|
|
40
|
+
titleColor: theme.accent,
|
|
41
|
+
paddingLeft: 1,
|
|
42
|
+
paddingRight: 1,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get visible(): boolean {
|
|
47
|
+
return this.box.visible
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
show(): void {
|
|
51
|
+
this.box.visible = true
|
|
52
|
+
this.layout(this.terminalWidth, this.terminalHeight)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
hide(): void {
|
|
56
|
+
this.box.visible = false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Re-centre for the given terminal size. Safe to call on every resize. */
|
|
60
|
+
layout(terminalWidth: number, terminalHeight: number): void {
|
|
61
|
+
this.terminalWidth = terminalWidth
|
|
62
|
+
this.terminalHeight = terminalHeight
|
|
63
|
+
|
|
64
|
+
const {
|
|
65
|
+
widthRatio = 0.6,
|
|
66
|
+
minWidth = 30,
|
|
67
|
+
maxWidth = 100,
|
|
68
|
+
heightRatio = 0.5,
|
|
69
|
+
minHeight = 5,
|
|
70
|
+
maxHeight = 30,
|
|
71
|
+
} = this.options
|
|
72
|
+
|
|
73
|
+
const width = clampSize(Math.round(terminalWidth * widthRatio), minWidth, maxWidth, terminalWidth - 2)
|
|
74
|
+
const height = clampSize(
|
|
75
|
+
Math.round(terminalHeight * heightRatio),
|
|
76
|
+
minHeight,
|
|
77
|
+
maxHeight,
|
|
78
|
+
terminalHeight - 2,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
this.box.width = width
|
|
82
|
+
this.box.height = height
|
|
83
|
+
this.box.left = Math.max(0, Math.floor((terminalWidth - width) / 2))
|
|
84
|
+
this.box.top = Math.max(0, Math.floor((terminalHeight - height) / 2))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function clampSize(preferred: number, min: number, max: number, available: number): number {
|
|
89
|
+
const upper = Math.max(1, Math.min(max, available))
|
|
90
|
+
return Math.max(1, Math.min(upper, Math.max(min, preferred)))
|
|
91
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { BoxRenderable, InputRenderable, TextRenderable, type RenderContext } from "@opentui/core"
|
|
2
|
+
import { theme } from "../app/theme.js"
|
|
3
|
+
import type { ProjectEntry } from "../projects/ProjectRegistry.js"
|
|
4
|
+
import { fit } from "../utils/format.js"
|
|
5
|
+
import { tildify, truncatePath } from "../utils/paths.js"
|
|
6
|
+
import { Overlay } from "./Overlay.js"
|
|
7
|
+
import { RowList, type ListRow } from "./primitives.js"
|
|
8
|
+
import { chunk, dim, styled } from "./styled.js"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Project switcher (`ctrl+p`).
|
|
12
|
+
*
|
|
13
|
+
* Reattaches the whole dashboard to another checkout: the current project,
|
|
14
|
+
* recently opened ones, anything named in config, and sibling directories.
|
|
15
|
+
*/
|
|
16
|
+
export class ProjectPicker extends Overlay {
|
|
17
|
+
readonly input: InputRenderable
|
|
18
|
+
private readonly list: RowList
|
|
19
|
+
private readonly footer: TextRenderable
|
|
20
|
+
private results: ProjectEntry[] = []
|
|
21
|
+
private selectedIndex = 0
|
|
22
|
+
private currentPath = ""
|
|
23
|
+
|
|
24
|
+
constructor(ctx: RenderContext) {
|
|
25
|
+
super(ctx, {
|
|
26
|
+
title: " Switch project ",
|
|
27
|
+
widthRatio: 0.7,
|
|
28
|
+
minWidth: 44,
|
|
29
|
+
maxWidth: 100,
|
|
30
|
+
heightRatio: 0.6,
|
|
31
|
+
minHeight: 9,
|
|
32
|
+
maxHeight: 24,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const inputWrapper = new BoxRenderable(ctx, {
|
|
36
|
+
height: 1,
|
|
37
|
+
flexShrink: 0,
|
|
38
|
+
flexDirection: "row",
|
|
39
|
+
marginTop: 1,
|
|
40
|
+
backgroundColor: theme.panelAlt,
|
|
41
|
+
paddingLeft: 1,
|
|
42
|
+
paddingRight: 1,
|
|
43
|
+
})
|
|
44
|
+
inputWrapper.add(
|
|
45
|
+
new TextRenderable(ctx, { content: "› ", fg: theme.accent, width: 2, selectable: false }),
|
|
46
|
+
)
|
|
47
|
+
this.input = new InputRenderable(ctx, {
|
|
48
|
+
flexGrow: 1,
|
|
49
|
+
backgroundColor: theme.panelAlt,
|
|
50
|
+
textColor: theme.textBright,
|
|
51
|
+
placeholder: "Filter projects…",
|
|
52
|
+
placeholderColor: theme.textDim,
|
|
53
|
+
})
|
|
54
|
+
inputWrapper.add(this.input)
|
|
55
|
+
|
|
56
|
+
// Column widths depend on the overlay width, which is only known after
|
|
57
|
+
// layout, so recompute the rows when the viewport changes.
|
|
58
|
+
this.list = new RowList(ctx, { marginTop: 1, flexGrow: 1, onViewportChange: () => this.paint() })
|
|
59
|
+
this.footer = new TextRenderable(ctx, {
|
|
60
|
+
content: "",
|
|
61
|
+
fg: theme.textDim,
|
|
62
|
+
height: 1,
|
|
63
|
+
flexShrink: 0,
|
|
64
|
+
selectable: false,
|
|
65
|
+
truncate: true,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
this.box.add(inputWrapper)
|
|
69
|
+
this.box.add(this.list.box)
|
|
70
|
+
this.box.add(this.footer)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
open(currentPath: string): void {
|
|
74
|
+
this.currentPath = currentPath
|
|
75
|
+
this.input.value = ""
|
|
76
|
+
this.selectedIndex = 0
|
|
77
|
+
this.show()
|
|
78
|
+
this.input.focus()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
close(): void {
|
|
82
|
+
this.input.blur()
|
|
83
|
+
this.hide()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get query(): string {
|
|
87
|
+
return this.input.value
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setResults(results: ProjectEntry[]): void {
|
|
91
|
+
this.results = results
|
|
92
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, results.length - 1))
|
|
93
|
+
this.paint()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
move(delta: number): void {
|
|
97
|
+
if (this.results.length === 0) return
|
|
98
|
+
const next = this.selectedIndex + delta
|
|
99
|
+
this.selectedIndex = ((next % this.results.length) + this.results.length) % this.results.length
|
|
100
|
+
this.paint()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get selected(): ProjectEntry | undefined {
|
|
104
|
+
return this.results[this.selectedIndex]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private paint(): void {
|
|
108
|
+
const width = Math.max(24, this.box.width - 4)
|
|
109
|
+
const nameWidth = Math.min(28, Math.max(12, Math.floor(width * 0.3)))
|
|
110
|
+
|
|
111
|
+
const rows: ListRow[] = this.results.map((entry) => {
|
|
112
|
+
const isCurrent = entry.path === this.currentPath
|
|
113
|
+
const markers = [entry.hasGit ? "git" : null, entry.hasPackageJson ? "pkg" : null]
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.join(" ")
|
|
116
|
+
const pathWidth = Math.max(12, width - nameWidth - 12)
|
|
117
|
+
return {
|
|
118
|
+
key: entry.path,
|
|
119
|
+
content: styled(
|
|
120
|
+
chunk(isCurrent ? "● " : " ", theme.accent),
|
|
121
|
+
chunk(fit(entry.name, nameWidth), isCurrent ? theme.accent : theme.textBright),
|
|
122
|
+
dim(fit(truncatePath(tildify(entry.path), pathWidth - 1), pathWidth)),
|
|
123
|
+
dim(fit(markers, 8)),
|
|
124
|
+
),
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (rows.length === 0) {
|
|
129
|
+
rows.push({ key: "__empty", disabled: true, content: styled(dim("No matching projects")) })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.list.setRows(rows, this.selectedIndex)
|
|
133
|
+
this.footer.content = styled(
|
|
134
|
+
dim(`${this.results.length} project${this.results.length === 1 ? "" : "s"}`),
|
|
135
|
+
dim(" ↑↓ move enter switch esc cancel"),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { BoxRenderable, TextRenderable, type RenderContext } from "@opentui/core"
|
|
2
|
+
import { shortcutHintFor } from "../app/keymap.js"
|
|
3
|
+
import { theme } from "../app/theme.js"
|
|
4
|
+
import type { ViewId } from "../core/state.js"
|
|
5
|
+
import { fit } from "../utils/format.js"
|
|
6
|
+
import { chunk, dim, styled } from "./styled.js"
|
|
7
|
+
|
|
8
|
+
export interface SidebarItem {
|
|
9
|
+
id: ViewId
|
|
10
|
+
label: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SIDEBAR_ITEMS: readonly SidebarItem[] = [
|
|
14
|
+
{ id: "overview", label: "Overview" },
|
|
15
|
+
{ id: "processes", label: "Processes" },
|
|
16
|
+
{ id: "services", label: "Services" },
|
|
17
|
+
{ id: "scripts", label: "Scripts" },
|
|
18
|
+
{ id: "git", label: "Git" },
|
|
19
|
+
{ id: "logs", label: "Logs" },
|
|
20
|
+
{ id: "dependencies", label: "Dependencies" },
|
|
21
|
+
{ id: "system", label: "System" },
|
|
22
|
+
{ id: "settings", label: "Settings" },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
/** What the rail needs to know about an open project. */
|
|
26
|
+
export interface SidebarProject {
|
|
27
|
+
path: string
|
|
28
|
+
name: string
|
|
29
|
+
/** Processes currently active in that project. */
|
|
30
|
+
running: number
|
|
31
|
+
/** True when a process there has failed or exhausted its restarts. */
|
|
32
|
+
attention: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SIDEBAR_WIDTH = 23
|
|
36
|
+
|
|
37
|
+
/** Widest label ("Dependencies") plus a space before the shortcut column. */
|
|
38
|
+
const LABEL_WIDTH = 13
|
|
39
|
+
/** Room for the project name; a space and a 3-column badge follow it. */
|
|
40
|
+
const PROJECT_WIDTH = 14
|
|
41
|
+
/** Keep the rail readable on short terminals. */
|
|
42
|
+
const MAX_PROJECT_ROWS = 6
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Left navigation rail.
|
|
46
|
+
*
|
|
47
|
+
* With one project open it is the plain section list. Once a second project is
|
|
48
|
+
* opened a PROJECTS block appears above it - progressive disclosure, so the
|
|
49
|
+
* single-project case looks exactly as it always did.
|
|
50
|
+
*/
|
|
51
|
+
export class Sidebar {
|
|
52
|
+
readonly box: BoxRenderable
|
|
53
|
+
private readonly projectsBlock: BoxRenderable
|
|
54
|
+
private readonly projectRows: TextRenderable[] = []
|
|
55
|
+
private readonly sectionHeading: TextRenderable
|
|
56
|
+
private readonly rows = new Map<ViewId, TextRenderable>()
|
|
57
|
+
private activeView: ViewId = "overview"
|
|
58
|
+
|
|
59
|
+
constructor(private readonly ctx: RenderContext) {
|
|
60
|
+
this.box = new BoxRenderable(ctx, {
|
|
61
|
+
width: SIDEBAR_WIDTH,
|
|
62
|
+
flexShrink: 0,
|
|
63
|
+
flexDirection: "column",
|
|
64
|
+
paddingLeft: 1,
|
|
65
|
+
paddingRight: 1,
|
|
66
|
+
paddingTop: 1,
|
|
67
|
+
border: ["right"],
|
|
68
|
+
borderColor: theme.border,
|
|
69
|
+
backgroundColor: theme.panel,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
this.projectsBlock = new BoxRenderable(ctx, {
|
|
73
|
+
flexDirection: "column",
|
|
74
|
+
flexShrink: 0,
|
|
75
|
+
visible: false,
|
|
76
|
+
backgroundColor: "transparent",
|
|
77
|
+
})
|
|
78
|
+
this.projectsBlock.add(
|
|
79
|
+
new TextRenderable(ctx, { content: "PROJECTS", fg: theme.textDim, height: 1, selectable: false }),
|
|
80
|
+
)
|
|
81
|
+
this.box.add(this.projectsBlock)
|
|
82
|
+
|
|
83
|
+
this.sectionHeading = new TextRenderable(ctx, {
|
|
84
|
+
content: "PROJECT",
|
|
85
|
+
fg: theme.textDim,
|
|
86
|
+
height: 1,
|
|
87
|
+
selectable: false,
|
|
88
|
+
})
|
|
89
|
+
this.box.add(this.sectionHeading)
|
|
90
|
+
this.box.add(new BoxRenderable(ctx, { height: 1, backgroundColor: "transparent" }))
|
|
91
|
+
|
|
92
|
+
for (const item of SIDEBAR_ITEMS) {
|
|
93
|
+
const row = new TextRenderable(ctx, { content: "", height: 1, selectable: false, truncate: true })
|
|
94
|
+
this.rows.set(item.id, row)
|
|
95
|
+
this.box.add(row)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.setActive("overview")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setActive(view: ViewId): void {
|
|
102
|
+
this.activeView = view
|
|
103
|
+
for (const item of SIDEBAR_ITEMS) {
|
|
104
|
+
const row = this.rows.get(item.id)
|
|
105
|
+
if (!row) continue
|
|
106
|
+
const active = item.id === view
|
|
107
|
+
// Shortcut hints are derived from the keymap, never hand-written here.
|
|
108
|
+
const hint = shortcutHintFor(item.id)
|
|
109
|
+
row.content = styled(
|
|
110
|
+
chunk(active ? "● " : "○ ", active ? theme.accent : theme.textDim),
|
|
111
|
+
chunk(fit(item.label, LABEL_WIDTH), active ? theme.textBright : theme.text),
|
|
112
|
+
dim(hint),
|
|
113
|
+
)
|
|
114
|
+
row.bg = active ? theme.selectionBackground : "transparent"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Show the open projects. The block hides itself with a single project, and
|
|
120
|
+
* the section heading switches from "PROJECT" to "SECTIONS" when it shows.
|
|
121
|
+
*/
|
|
122
|
+
setProjects(projects: readonly SidebarProject[], activePath: string): void {
|
|
123
|
+
const multi = projects.length > 1
|
|
124
|
+
this.projectsBlock.visible = multi
|
|
125
|
+
this.sectionHeading.content = multi ? "SECTIONS" : "PROJECT"
|
|
126
|
+
if (!multi) return
|
|
127
|
+
|
|
128
|
+
const visible = projects.slice(0, MAX_PROJECT_ROWS)
|
|
129
|
+
while (this.projectRows.length < visible.length + 1) {
|
|
130
|
+
const row = new TextRenderable(this.ctx, { content: "", height: 1, selectable: false, truncate: true })
|
|
131
|
+
this.projectRows.push(row)
|
|
132
|
+
this.projectsBlock.add(row)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const [index, row] of this.projectRows.entries()) {
|
|
136
|
+
const project = visible[index]
|
|
137
|
+
if (!project) {
|
|
138
|
+
// The last pooled row doubles as the spacer under the block.
|
|
139
|
+
row.visible = index === visible.length
|
|
140
|
+
row.content =
|
|
141
|
+
index === visible.length && projects.length > visible.length
|
|
142
|
+
? styled(dim(` +${projects.length - visible.length} more`))
|
|
143
|
+
: ""
|
|
144
|
+
row.bg = "transparent"
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
row.visible = true
|
|
148
|
+
const active = project.path === activePath
|
|
149
|
+
const badge = project.running > 0 ? `${project.running}▸` : ""
|
|
150
|
+
row.content = styled(
|
|
151
|
+
chunk(active ? "● " : "○ ", active ? theme.accent : theme.textDim),
|
|
152
|
+
chunk(fit(project.name, PROJECT_WIDTH), active ? theme.textBright : theme.text),
|
|
153
|
+
" ",
|
|
154
|
+
project.attention ? chunk(fit("!", 3), theme.danger) : chunk(fit(badge, 3), theme.success),
|
|
155
|
+
)
|
|
156
|
+
row.bg = active ? theme.selectionBackground : "transparent"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get active(): ViewId {
|
|
161
|
+
return this.activeView
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { fg, t, type StyledText } from "@opentui/core"
|
|
2
|
+
import { processStatusStyle, serviceStateStyle, theme } from "../app/theme.js"
|
|
3
|
+
import type { ProcessStatus } from "../processes/ManagedProcess.js"
|
|
4
|
+
import type { ServiceState } from "../services/types.js"
|
|
5
|
+
import { fit } from "../utils/format.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Status indicators.
|
|
9
|
+
*
|
|
10
|
+
* A single glyph plus a colour is enough to scan a column at a glance; the
|
|
11
|
+
* word is included at a fixed width so the columns after it stay aligned.
|
|
12
|
+
*/
|
|
13
|
+
export function processBadge(status: ProcessStatus, width = 9): StyledText {
|
|
14
|
+
const style = processStatusStyle(status)
|
|
15
|
+
return t`${fg(style.color)(style.glyph)} ${fg(style.color)(fit(style.label, width))}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function serviceBadge(state: ServiceState, width = 12): StyledText {
|
|
19
|
+
const style = serviceStateStyle(state)
|
|
20
|
+
return t`${fg(style.color)(style.glyph)} ${fg(style.color)(fit(style.label, width))}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function processGlyph(status: ProcessStatus): StyledText {
|
|
24
|
+
const style = processStatusStyle(status)
|
|
25
|
+
return t`${fg(style.color)(style.glyph)}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function serviceGlyph(state: ServiceState): StyledText {
|
|
29
|
+
const style = serviceStateStyle(state)
|
|
30
|
+
return t`${fg(style.color)(style.glyph)}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `label value` pair with a dim label, aligned to `labelWidth`. */
|
|
34
|
+
export function keyValue(label: string, value: string, labelWidth = 16): StyledText {
|
|
35
|
+
return t`${fg(theme.textDim)(fit(label, labelWidth))}${fg(theme.text)(value)}`
|
|
36
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoxRenderable,
|
|
3
|
+
TextRenderable,
|
|
4
|
+
type BoxOptions,
|
|
5
|
+
type RenderContext,
|
|
6
|
+
type StyledText,
|
|
7
|
+
} from "@opentui/core"
|
|
8
|
+
import { theme } from "../app/theme.js"
|
|
9
|
+
|
|
10
|
+
/** Shorthand for a plain text row. */
|
|
11
|
+
export function createText(
|
|
12
|
+
ctx: RenderContext,
|
|
13
|
+
content: StyledText | string,
|
|
14
|
+
options: { fg?: string; bg?: string; height?: number } = {},
|
|
15
|
+
): TextRenderable {
|
|
16
|
+
return new TextRenderable(ctx, {
|
|
17
|
+
content,
|
|
18
|
+
fg: options.fg ?? theme.text,
|
|
19
|
+
bg: options.bg ?? "transparent",
|
|
20
|
+
height: options.height ?? 1,
|
|
21
|
+
truncate: true,
|
|
22
|
+
selectable: false,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A section heading inside a panel. */
|
|
27
|
+
export function createHeading(ctx: RenderContext, label: string): TextRenderable {
|
|
28
|
+
return new TextRenderable(ctx, {
|
|
29
|
+
content: label.toUpperCase(),
|
|
30
|
+
fg: theme.accent,
|
|
31
|
+
height: 1,
|
|
32
|
+
selectable: false,
|
|
33
|
+
truncate: true,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createBox(ctx: RenderContext, options: BoxOptions = {}): BoxRenderable {
|
|
38
|
+
return new BoxRenderable(ctx, {
|
|
39
|
+
backgroundColor: "transparent",
|
|
40
|
+
...options,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A vertical spacer. */
|
|
45
|
+
export function createSpacer(ctx: RenderContext, height = 1): BoxRenderable {
|
|
46
|
+
return new BoxRenderable(ctx, { height, backgroundColor: "transparent" })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ListRow {
|
|
50
|
+
/** Stable identity, used to preserve selection across refreshes. */
|
|
51
|
+
key: string
|
|
52
|
+
content: StyledText | string
|
|
53
|
+
/** Rendered as a non-selectable row (group headings, empty states). */
|
|
54
|
+
disabled?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RowListOptions extends BoxOptions {
|
|
58
|
+
/** Rows kept visible above/below the selection while scrolling. */
|
|
59
|
+
scrollPadding?: number
|
|
60
|
+
/**
|
|
61
|
+
* Called when the viewport height changes. Views that window their own data
|
|
62
|
+
* (the log viewer) use this to recompute the slice they hand over.
|
|
63
|
+
*/
|
|
64
|
+
onViewportChange?: () => void
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A scrolling, selectable list of single-line rows.
|
|
69
|
+
*
|
|
70
|
+
* Renderables are recycled: refreshing the data updates existing
|
|
71
|
+
* `TextRenderable` contents instead of rebuilding the subtree, and only the rows
|
|
72
|
+
* that fit the current viewport are materialised.
|
|
73
|
+
*/
|
|
74
|
+
export class RowList {
|
|
75
|
+
readonly box: BoxRenderable
|
|
76
|
+
private readonly rowRenderables: TextRenderable[] = []
|
|
77
|
+
private rows: ListRow[] = []
|
|
78
|
+
private selectedIndex = 0
|
|
79
|
+
private scrollOffset = 0
|
|
80
|
+
private focused = true
|
|
81
|
+
private readonly scrollPadding: number
|
|
82
|
+
private readonly onViewportChange: (() => void) | undefined
|
|
83
|
+
private lastVisibleCount = 0
|
|
84
|
+
|
|
85
|
+
constructor(
|
|
86
|
+
private readonly ctx: RenderContext,
|
|
87
|
+
options: RowListOptions = {},
|
|
88
|
+
) {
|
|
89
|
+
const { scrollPadding, onViewportChange, ...boxOptions } = options
|
|
90
|
+
this.scrollPadding = scrollPadding ?? 1
|
|
91
|
+
this.onViewportChange = onViewportChange
|
|
92
|
+
this.box = new BoxRenderable(ctx, {
|
|
93
|
+
flexDirection: "column",
|
|
94
|
+
flexGrow: 1,
|
|
95
|
+
overflow: "hidden",
|
|
96
|
+
backgroundColor: "transparent",
|
|
97
|
+
...boxOptions,
|
|
98
|
+
})
|
|
99
|
+
this.box.onSizeChange = () => {
|
|
100
|
+
const visible = this.visibleCount
|
|
101
|
+
this.paint()
|
|
102
|
+
if (visible !== this.lastVisibleCount) {
|
|
103
|
+
this.lastVisibleCount = visible
|
|
104
|
+
this.onViewportChange?.()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get length(): number {
|
|
110
|
+
return this.rows.length
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get visibleCount(): number {
|
|
114
|
+
return Math.max(1, this.box.height)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get offset(): number {
|
|
118
|
+
return this.scrollOffset
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setFocused(focused: boolean): void {
|
|
122
|
+
if (this.focused === focused) return
|
|
123
|
+
this.focused = focused
|
|
124
|
+
this.paint()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setRows(rows: ListRow[], selectedIndex: number): void {
|
|
128
|
+
this.rows = rows
|
|
129
|
+
this.selectedIndex = clamp(selectedIndex, 0, Math.max(0, rows.length - 1))
|
|
130
|
+
this.ensureVisible()
|
|
131
|
+
this.paint()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setSelectedIndex(index: number): void {
|
|
135
|
+
const next = clamp(index, 0, Math.max(0, this.rows.length - 1))
|
|
136
|
+
if (next === this.selectedIndex) return
|
|
137
|
+
this.selectedIndex = next
|
|
138
|
+
this.ensureVisible()
|
|
139
|
+
this.paint()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
scrollBy(delta: number): void {
|
|
143
|
+
const maxOffset = Math.max(0, this.rows.length - this.visibleCount)
|
|
144
|
+
this.scrollOffset = clamp(this.scrollOffset + delta, 0, maxOffset)
|
|
145
|
+
this.paint()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
scrollToEnd(): void {
|
|
149
|
+
this.scrollOffset = Math.max(0, this.rows.length - this.visibleCount)
|
|
150
|
+
this.paint()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private ensureVisible(): void {
|
|
154
|
+
const visible = this.visibleCount
|
|
155
|
+
const padding = Math.min(this.scrollPadding, Math.floor((visible - 1) / 2))
|
|
156
|
+
const maxOffset = Math.max(0, this.rows.length - visible)
|
|
157
|
+
|
|
158
|
+
if (this.selectedIndex < this.scrollOffset + padding) {
|
|
159
|
+
this.scrollOffset = this.selectedIndex - padding
|
|
160
|
+
} else if (this.selectedIndex > this.scrollOffset + visible - 1 - padding) {
|
|
161
|
+
this.scrollOffset = this.selectedIndex - visible + 1 + padding
|
|
162
|
+
}
|
|
163
|
+
this.scrollOffset = clamp(this.scrollOffset, 0, maxOffset)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private paint(): void {
|
|
167
|
+
const visible = this.visibleCount
|
|
168
|
+
this.ensurePool(Math.min(visible, Math.max(this.rows.length, 1)))
|
|
169
|
+
|
|
170
|
+
for (const [index, renderable] of this.rowRenderables.entries()) {
|
|
171
|
+
const rowIndex = this.scrollOffset + index
|
|
172
|
+
const row = this.rows[rowIndex]
|
|
173
|
+
if (!row) {
|
|
174
|
+
renderable.visible = false
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
renderable.visible = true
|
|
178
|
+
renderable.content = row.content
|
|
179
|
+
const selected = rowIndex === this.selectedIndex && !row.disabled
|
|
180
|
+
renderable.bg = selected ? (this.focused ? theme.selectionBackground : theme.panelAlt) : "transparent"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private ensurePool(size: number): void {
|
|
185
|
+
while (this.rowRenderables.length < size) {
|
|
186
|
+
const renderable = new TextRenderable(this.ctx, {
|
|
187
|
+
content: "",
|
|
188
|
+
fg: theme.text,
|
|
189
|
+
height: 1,
|
|
190
|
+
flexShrink: 0,
|
|
191
|
+
truncate: true,
|
|
192
|
+
selectable: false,
|
|
193
|
+
})
|
|
194
|
+
this.rowRenderables.push(renderable)
|
|
195
|
+
this.box.add(renderable)
|
|
196
|
+
}
|
|
197
|
+
while (this.rowRenderables.length > size) {
|
|
198
|
+
const renderable = this.rowRenderables.pop()
|
|
199
|
+
if (!renderable) break
|
|
200
|
+
this.box.remove(renderable)
|
|
201
|
+
renderable.destroy()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
destroy(): void {
|
|
206
|
+
this.box.destroyRecursively()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
211
|
+
if (max < min) return min
|
|
212
|
+
return Math.max(min, Math.min(max, value))
|
|
213
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { bold as boldChunk, dim as dimChunk, fg, StyledText, type TextChunk } from "@opentui/core"
|
|
2
|
+
import { theme } from "../app/theme.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Helpers for composing `StyledText` programmatically.
|
|
6
|
+
*
|
|
7
|
+
* The `t` template literal is convenient for fixed strings; these helpers cover
|
|
8
|
+
* the dynamic case (variable numbers of coloured segments) without fighting the
|
|
9
|
+
* template syntax.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function chunk(text: string, color: string = theme.text): TextChunk {
|
|
13
|
+
return fg(color)(text)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function bold(text: string, color: string = theme.textBright): TextChunk {
|
|
17
|
+
return boldChunk(fg(color)(text))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function dim(text: string): TextChunk {
|
|
21
|
+
return dimChunk(fg(theme.textDim)(text))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function styled(...parts: (TextChunk | string)[]): StyledText {
|
|
25
|
+
return new StyledText(parts.map((part) => (typeof part === "string" ? chunk(part) : part)))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function joinStyled(
|
|
29
|
+
parts: StyledText[],
|
|
30
|
+
separator: string,
|
|
31
|
+
separatorColor = theme.textDim,
|
|
32
|
+
): StyledText {
|
|
33
|
+
const chunks: TextChunk[] = []
|
|
34
|
+
for (const [index, part] of parts.entries()) {
|
|
35
|
+
if (index > 0) chunks.push(chunk(separator, separatorColor))
|
|
36
|
+
chunks.push(...part.chunks)
|
|
37
|
+
}
|
|
38
|
+
return new StyledText(chunks)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Total display length of a styled line, for right-alignment maths. */
|
|
42
|
+
export function styledLength(text: StyledText): number {
|
|
43
|
+
return text.chunks.reduce((total, part) => total + part.text.length, 0)
|
|
44
|
+
}
|