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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 { type CliRenderer, SelectRenderable, SelectRenderableEvents } from '@opentui/core'
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: SelectRenderable
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 SelectRenderable(renderer, {
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 {