numux 1.0.0 → 1.1.0
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/package.json +1 -1
- package/src/ui/app.ts +1 -1
- package/src/ui/tabs.ts +90 -4
package/package.json
CHANGED
package/src/ui/app.ts
CHANGED
|
@@ -60,7 +60,7 @@ export class App {
|
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
// Tab bar (vertical sidebar)
|
|
63
|
-
this.tabBar = new TabBar(this.renderer, this.names)
|
|
63
|
+
this.tabBar = new TabBar(this.renderer, this.names, this.processHexColors)
|
|
64
64
|
|
|
65
65
|
// Content row: sidebar | pane
|
|
66
66
|
const contentRow = new BoxRenderable(this.renderer, {
|
package/src/ui/tabs.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type CliRenderer,
|
|
3
|
+
type OptimizedBuffer,
|
|
4
|
+
parseColor,
|
|
5
|
+
type RGBA,
|
|
6
|
+
SelectRenderable,
|
|
7
|
+
SelectRenderableEvents
|
|
8
|
+
} from '@opentui/core'
|
|
2
9
|
import type { ProcessStatus } from '../types'
|
|
3
10
|
|
|
4
11
|
const STATUS_ICONS: Record<ProcessStatus, string> = {
|
|
@@ -12,18 +19,82 @@ const STATUS_ICONS: Record<ProcessStatus, string> = {
|
|
|
12
19
|
skipped: '⊘'
|
|
13
20
|
}
|
|
14
21
|
|
|
22
|
+
/** Status-specific icon colors (override process colors) */
|
|
23
|
+
const STATUS_ICON_HEX: Partial<Record<ProcessStatus, string>> = {
|
|
24
|
+
ready: '#00cc00',
|
|
25
|
+
failed: '#ff5555',
|
|
26
|
+
stopped: '#888888',
|
|
27
|
+
skipped: '#888888'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface OptionColors {
|
|
31
|
+
icon: RGBA | null
|
|
32
|
+
name: RGBA | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* SelectRenderable subclass that supports per-option coloring.
|
|
37
|
+
* The base SelectRenderable draws all option text with a single color.
|
|
38
|
+
* This overrides renderSelf to repaint the icon and name with individual
|
|
39
|
+
* RGBA colors after the base render.
|
|
40
|
+
*/
|
|
41
|
+
class ColoredSelectRenderable extends SelectRenderable {
|
|
42
|
+
private _optionColors: OptionColors[] = []
|
|
43
|
+
|
|
44
|
+
setOptionColors(colors: OptionColors[]): void {
|
|
45
|
+
this._optionColors = colors
|
|
46
|
+
this.requestRender()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {
|
|
50
|
+
const wasDirty = this.isDirty
|
|
51
|
+
super.renderSelf(buffer, deltaTime)
|
|
52
|
+
if (wasDirty && this.frameBuffer && this._optionColors.length > 0) {
|
|
53
|
+
this.colorizeOptions()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private colorizeOptions(): void {
|
|
58
|
+
const fb = this.frameBuffer!
|
|
59
|
+
// Access internal layout state (private in TS, accessible at runtime)
|
|
60
|
+
const scrollOffset = (this as any).scrollOffset as number
|
|
61
|
+
const maxVisibleItems = (this as any).maxVisibleItems as number
|
|
62
|
+
const linesPerItem = (this as any).linesPerItem as number
|
|
63
|
+
const options = this.options
|
|
64
|
+
const visibleCount = Math.min(maxVisibleItems, options.length - scrollOffset)
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < visibleCount; i++) {
|
|
67
|
+
const actualIndex = scrollOffset + i
|
|
68
|
+
const colors = this._optionColors[actualIndex]
|
|
69
|
+
if (!colors) continue
|
|
70
|
+
const itemY = i * linesPerItem
|
|
71
|
+
// Layout: "▶ ○ name" or " ○ name" (drawText at x=1, prefix 2 chars)
|
|
72
|
+
// Icon at x=3, space at x=4, name starts at x=5
|
|
73
|
+
const optName = options[actualIndex].name
|
|
74
|
+
if (colors.icon) {
|
|
75
|
+
fb.drawText(optName.charAt(0), 3, itemY, colors.icon)
|
|
76
|
+
}
|
|
77
|
+
if (colors.name) {
|
|
78
|
+
fb.drawText(optName.slice(2), 5, itemY, colors.name)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
15
84
|
export class TabBar {
|
|
16
|
-
readonly renderable:
|
|
85
|
+
readonly renderable: ColoredSelectRenderable
|
|
17
86
|
private names: string[]
|
|
18
87
|
private statuses: Map<string, ProcessStatus>
|
|
19
88
|
private descriptions: Map<string, string>
|
|
89
|
+
private processColors: Map<string, string>
|
|
20
90
|
|
|
21
|
-
constructor(renderer: CliRenderer, names: string[]) {
|
|
91
|
+
constructor(renderer: CliRenderer, names: string[], colors?: Map<string, string>) {
|
|
22
92
|
this.names = names
|
|
23
93
|
this.statuses = new Map(names.map(n => [n, 'pending' as ProcessStatus]))
|
|
24
94
|
this.descriptions = new Map(names.map(n => [n, 'pending']))
|
|
95
|
+
this.processColors = colors ?? new Map()
|
|
25
96
|
|
|
26
|
-
this.renderable = new
|
|
97
|
+
this.renderable = new ColoredSelectRenderable(renderer, {
|
|
27
98
|
id: 'tab-bar',
|
|
28
99
|
width: '100%',
|
|
29
100
|
height: '100%',
|
|
@@ -37,6 +108,7 @@ export class TabBar {
|
|
|
37
108
|
showDescription: true,
|
|
38
109
|
wrapSelection: true
|
|
39
110
|
})
|
|
111
|
+
this.updateOptionColors()
|
|
40
112
|
}
|
|
41
113
|
|
|
42
114
|
onSelect(handler: (index: number, name: string) => void): void {
|
|
@@ -58,6 +130,20 @@ export class TabBar {
|
|
|
58
130
|
name: this.formatTab(n, this.statuses.get(n)!),
|
|
59
131
|
description: this.descriptions.get(n)!
|
|
60
132
|
}))
|
|
133
|
+
this.updateOptionColors()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private updateOptionColors(): void {
|
|
137
|
+
const colors = this.names.map(name => {
|
|
138
|
+
const status = this.statuses.get(name)!
|
|
139
|
+
const statusHex = STATUS_ICON_HEX[status]
|
|
140
|
+
const processHex = this.processColors.get(name)
|
|
141
|
+
return {
|
|
142
|
+
icon: parseColor(statusHex ?? processHex ?? '#888888'),
|
|
143
|
+
name: processHex ? parseColor(processHex) : null
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
this.renderable.setOptionColors(colors)
|
|
61
147
|
}
|
|
62
148
|
|
|
63
149
|
private formatDescription(status: ProcessStatus, exitCode?: number | null, restartCount?: number): string {
|