numux 1.4.0 → 1.5.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/README.md +1 -1
- package/package.json +2 -2
- package/src/ui/app.ts +35 -47
- package/src/ui/status-bar.ts +3 -1
- package/src/utils/logger.ts +11 -3
package/README.md
CHANGED
|
@@ -264,7 +264,7 @@ Persistent processes that crash are auto-restarted with exponential backoff (1s
|
|
|
264
264
|
| `Alt+L` | Clear active pane output |
|
|
265
265
|
| `Alt+1`–`Alt+9` | Jump to tab |
|
|
266
266
|
| `Alt+Left/Right` | Cycle tabs |
|
|
267
|
-
| `Up/Down` |
|
|
267
|
+
| `Up/Down` | Navigate between tabs |
|
|
268
268
|
| `PageUp/PageDown` | Scroll output by page (non-interactive panes) |
|
|
269
269
|
| `Home/End` | Scroll to top/bottom (non-interactive panes) |
|
|
270
270
|
| `Alt+PageUp/PageDown` | Scroll output up/down |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "numux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Terminal multiplexer with dependency orchestration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
".": "./src/config.ts"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"dev": "bun run
|
|
30
|
+
"dev": "cd example && bun run dev --debug",
|
|
31
31
|
"test": "bun test",
|
|
32
32
|
"typecheck": "bunx tsc --noEmit",
|
|
33
33
|
"lint": "biome check .",
|
package/src/ui/app.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { BoxRenderable, type CliRenderer, createCliRenderer } from '@opentui/cor
|
|
|
2
2
|
import type { ProcessManager } from '../process/manager'
|
|
3
3
|
import type { ResolvedNumuxConfig } from '../types'
|
|
4
4
|
import { buildProcessHexColorMap } from '../utils/color'
|
|
5
|
+
import { log } from '../utils/logger'
|
|
5
6
|
import { Pane, type SearchMatch } from './pane'
|
|
6
7
|
import { StatusBar } from './status-bar'
|
|
7
8
|
import { TabBar } from './tabs'
|
|
@@ -149,6 +150,8 @@ export class App {
|
|
|
149
150
|
this.renderer.keyInput.on(
|
|
150
151
|
'keypress',
|
|
151
152
|
(key: { ctrl: boolean; shift: boolean; meta: boolean; name: string; sequence: string }) => {
|
|
153
|
+
log(key)
|
|
154
|
+
|
|
152
155
|
// Ctrl+C: quit (always works)
|
|
153
156
|
if (key.ctrl && key.name === 'c') {
|
|
154
157
|
if (this.searchMode) {
|
|
@@ -167,30 +170,34 @@ export class App {
|
|
|
167
170
|
return
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
if (!this.activePane) return
|
|
174
|
+
|
|
175
|
+
const isInteractive = this.config.processes[this.activePane]?.interactive === true
|
|
176
|
+
|
|
177
|
+
// Non-interactive panes: plain keys act as shortcuts
|
|
178
|
+
if (!isInteractive) {
|
|
179
|
+
const name = key.name.toLowerCase()
|
|
180
|
+
|
|
181
|
+
// Shift+R: restart all processes
|
|
182
|
+
if (key.shift && name === 'r') {
|
|
174
183
|
this.manager.restartAll(this.termCols, this.termRows)
|
|
175
184
|
return
|
|
176
185
|
}
|
|
177
|
-
}
|
|
178
186
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (key.name === 'f' && this.activePane) {
|
|
187
|
+
// F: enter search mode
|
|
188
|
+
if (name === 'f') {
|
|
182
189
|
this.enterSearch()
|
|
183
190
|
return
|
|
184
191
|
}
|
|
185
192
|
|
|
186
|
-
//
|
|
187
|
-
if (
|
|
193
|
+
// R: restart active process
|
|
194
|
+
if (name === 'r') {
|
|
188
195
|
this.manager.restart(this.activePane, this.termCols, this.termRows)
|
|
189
196
|
return
|
|
190
197
|
}
|
|
191
198
|
|
|
192
|
-
//
|
|
193
|
-
if (
|
|
199
|
+
// S: stop/start active process
|
|
200
|
+
if (name === 's') {
|
|
194
201
|
const state = this.manager.getState(this.activePane)
|
|
195
202
|
if (state?.status === 'stopped' || state?.status === 'finished' || state?.status === 'failed') {
|
|
196
203
|
this.manager.start(this.activePane, this.termCols, this.termRows)
|
|
@@ -200,80 +207,61 @@ export class App {
|
|
|
200
207
|
return
|
|
201
208
|
}
|
|
202
209
|
|
|
203
|
-
//
|
|
204
|
-
if (
|
|
210
|
+
// L: clear active pane
|
|
211
|
+
if (name === 'l') {
|
|
205
212
|
this.panes.get(this.activePane)?.clear()
|
|
206
213
|
return
|
|
207
214
|
}
|
|
208
215
|
|
|
209
|
-
//
|
|
210
|
-
const num = Number.parseInt(
|
|
216
|
+
// 1-9: jump to tab (uses display order from tab bar)
|
|
217
|
+
const num = Number.parseInt(name, 10)
|
|
211
218
|
if (num >= 1 && num <= 9 && num <= this.tabBar.count) {
|
|
212
219
|
this.tabBar.setSelectedIndex(num - 1)
|
|
213
220
|
this.switchPane(this.tabBar.getNameAtIndex(num - 1))
|
|
214
221
|
return
|
|
215
222
|
}
|
|
216
223
|
|
|
217
|
-
//
|
|
218
|
-
if (
|
|
224
|
+
// Left/Right: cycle tabs
|
|
225
|
+
if (name === 'left' || name === 'right') {
|
|
219
226
|
const current = this.tabBar.getSelectedIndex()
|
|
220
227
|
const count = this.tabBar.count
|
|
221
|
-
const next =
|
|
228
|
+
const next = name === 'right' ? (current + 1) % count : (current - 1 + count) % count
|
|
222
229
|
this.tabBar.setSelectedIndex(next)
|
|
223
230
|
this.switchPane(this.tabBar.getNameAtIndex(next))
|
|
224
231
|
return
|
|
225
232
|
}
|
|
226
233
|
|
|
227
|
-
//
|
|
228
|
-
if (
|
|
234
|
+
// PageUp/PageDown: scroll by page
|
|
235
|
+
if (name === 'pageup' || name === 'pagedown') {
|
|
229
236
|
const pane = this.panes.get(this.activePane)
|
|
230
237
|
const delta = this.termRows - 2
|
|
231
|
-
pane?.scrollBy(
|
|
238
|
+
pane?.scrollBy(name === 'pageup' ? -delta : delta)
|
|
232
239
|
return
|
|
233
240
|
}
|
|
234
241
|
|
|
235
|
-
//
|
|
236
|
-
if (
|
|
242
|
+
// Home/End: scroll to top/bottom
|
|
243
|
+
if (name === 'home') {
|
|
237
244
|
this.panes.get(this.activePane)?.scrollToTop()
|
|
238
245
|
return
|
|
239
246
|
}
|
|
240
|
-
if (
|
|
247
|
+
if (name === 'end') {
|
|
241
248
|
this.panes.get(this.activePane)?.scrollToBottom()
|
|
242
249
|
return
|
|
243
250
|
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (!this.activePane) return
|
|
247
|
-
|
|
248
|
-
const isInteractive = this.config.processes[this.activePane]?.interactive === true
|
|
249
|
-
|
|
250
|
-
// Non-interactive panes: arrow keys scroll, all other input is dropped
|
|
251
|
-
if (!isInteractive) {
|
|
252
|
-
if (key.name === 'up' || key.name === 'down') {
|
|
253
|
-
const pane = this.panes.get(this.activePane)
|
|
254
|
-
pane?.scrollBy(key.name === 'up' ? -1 : 1)
|
|
255
|
-
} else if (key.name === 'pageup' || key.name === 'pagedown') {
|
|
256
|
-
const pane = this.panes.get(this.activePane)
|
|
257
|
-
const delta = this.termRows - 2
|
|
258
|
-
pane?.scrollBy(key.name === 'pageup' ? -delta : delta)
|
|
259
|
-
} else if (key.name === 'home') {
|
|
260
|
-
this.panes.get(this.activePane)?.scrollToTop()
|
|
261
|
-
} else if (key.name === 'end') {
|
|
262
|
-
this.panes.get(this.activePane)?.scrollToBottom()
|
|
263
|
-
}
|
|
264
251
|
return
|
|
265
252
|
}
|
|
266
253
|
|
|
267
|
-
// Forward all other input to the active process
|
|
254
|
+
// Forward all other input to the active process (interactive mode)
|
|
268
255
|
if (key.sequence) {
|
|
269
256
|
this.manager.write(this.activePane, key.sequence)
|
|
270
257
|
}
|
|
271
258
|
}
|
|
272
259
|
)
|
|
273
260
|
|
|
274
|
-
// Show first pane
|
|
261
|
+
// Show first pane and focus sidebar for keyboard navigation
|
|
275
262
|
if (this.names.length > 0) {
|
|
276
263
|
this.switchPane(this.names[0])
|
|
264
|
+
this.tabBar.focus()
|
|
277
265
|
}
|
|
278
266
|
|
|
279
267
|
// Start all processes
|
package/src/ui/status-bar.ts
CHANGED
|
@@ -34,7 +34,9 @@ export class StatusBar {
|
|
|
34
34
|
if (this._searchMode) {
|
|
35
35
|
return this.buildSearchContent()
|
|
36
36
|
}
|
|
37
|
-
return new StyledText([
|
|
37
|
+
return new StyledText([
|
|
38
|
+
plain('\u2190\u2192/1-9: tabs R: restart S: stop/start F: search L: clear Ctrl+C: quit')
|
|
39
|
+
])
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
private buildSearchContent(): StyledText {
|
package/src/utils/logger.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve } from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
let enabled = false
|
|
5
5
|
let logFile = ''
|
|
6
|
+
let debugCallback: ((line: string) => void) | null = null
|
|
6
7
|
|
|
7
8
|
export function enableDebugLog(dir?: string): void {
|
|
8
9
|
const logDir = dir ?? resolve(process.cwd(), '.numux')
|
|
@@ -13,12 +14,18 @@ export function enableDebugLog(dir?: string): void {
|
|
|
13
14
|
enabled = true
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function
|
|
17
|
+
export function setDebugCallback(cb: ((line: string) => void) | null): void {
|
|
18
|
+
debugCallback = cb
|
|
19
|
+
}
|
|
20
|
+
export function log(...args: unknown[]): void {
|
|
17
21
|
if (!enabled) return
|
|
18
22
|
try {
|
|
19
23
|
const timestamp = new Date().toISOString()
|
|
20
|
-
const formatted =
|
|
21
|
-
|
|
24
|
+
const formatted =
|
|
25
|
+
args.length > 0 ? `${args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')}` : ''
|
|
26
|
+
const line = `[${timestamp}] ${formatted}`
|
|
27
|
+
appendFileSync(logFile, `${line}\n`)
|
|
28
|
+
debugCallback?.(line)
|
|
22
29
|
} catch {
|
|
23
30
|
// Disk errors in debug logging should not crash the app
|
|
24
31
|
enabled = false
|
|
@@ -29,4 +36,5 @@ export function log(message: string, ...args: unknown[]): void {
|
|
|
29
36
|
export function _resetLogger(): void {
|
|
30
37
|
enabled = false
|
|
31
38
|
logFile = ''
|
|
39
|
+
debugCallback = null
|
|
32
40
|
}
|