numux 2.2.0 → 2.3.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 CHANGED
@@ -174,7 +174,7 @@ Template properties (color, env, dependsOn, etc.) are inherited by all matched p
174
174
  | `--max-restarts <n>` | Max auto-restarts for crashed processes |
175
175
  | `-s, --sort <mode>` | Tab display order: `config` (default), `alphabetical`, `topological` |
176
176
  | `--no-watch` | Disable file watching even if config has `watch` patterns |
177
- | `-t, --timestamps` | Add `[HH:MM:SS]` timestamps to prefixed output |
177
+ | `-t, --timestamps [format]` | Add timestamps (default `HH:mm:ss`). Works in both prefix and TUI mode. Pass a format string for custom output (e.g. `HH:mm:ss.SSS`). Toggle in TUI with `T` |
178
178
  | `--log-dir <path>` | Persist logs to timestamped subdirs (`<path>/<timestamp>/<name>.log`) with a `latest` symlink. Path is printed on exit |
179
179
  | `--debug` | Log to `.numux/debug.log` |
180
180
  | `-h, --help` | Show help |
package/dist/numux.js CHANGED
@@ -36,7 +36,7 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
36
36
  var require_package = __commonJS((exports, module) => {
37
37
  module.exports = {
38
38
  name: "numux",
39
- version: "2.2.0",
39
+ version: "2.3.0",
40
40
  description: "Terminal multiplexer with dependency orchestration",
41
41
  type: "module",
42
42
  license: "MIT",
@@ -224,11 +224,13 @@ var FLAGS = [
224
224
  description: "Disable file watching even if config has watch patterns"
225
225
  },
226
226
  {
227
- type: "boolean",
227
+ type: "optional-value",
228
228
  long: "--timestamps",
229
229
  short: "-t",
230
230
  key: "timestamps",
231
- description: "Add timestamps to prefixed output lines"
231
+ description: "Add timestamps to output (default HH:mm:ss, or pass a format string)",
232
+ valueName: "<format>",
233
+ completionHint: "none"
232
234
  },
233
235
  {
234
236
  type: "value",
@@ -330,6 +332,8 @@ function generateHelp() {
330
332
  parts.push(f.long);
331
333
  if (f.type === "value")
332
334
  parts.push(f.valueName);
335
+ if (f.type === "optional-value")
336
+ parts.push(`[${f.valueName}]`);
333
337
  const left = ` ${parts.join(" ")}`;
334
338
  lines.push(`${left.padEnd(29)}${f.description}`);
335
339
  }
@@ -375,6 +379,14 @@ function parseArgs(argv) {
375
379
  if (flag) {
376
380
  if (flag.type === "boolean") {
377
381
  result[flag.key] = true;
382
+ } else if (flag.type === "optional-value") {
383
+ const next = args[i + 1];
384
+ if (next !== undefined && !next.startsWith("-")) {
385
+ result[flag.key] = next;
386
+ i++;
387
+ } else {
388
+ result[flag.key] = true;
389
+ }
378
390
  } else {
379
391
  const next = args[++i];
380
392
  if (next === undefined) {
@@ -1099,7 +1111,7 @@ function validateConfig(raw, _warnings) {
1099
1111
  }
1100
1112
  const sort = validateSort(config.sort);
1101
1113
  const prefix = config.prefix === true ? true : undefined;
1102
- const timestamps = config.timestamps === true ? true : undefined;
1114
+ const timestamps = config.timestamps === true ? true : typeof config.timestamps === "string" ? config.timestamps : undefined;
1103
1115
  const killOthers = config.killOthers === true ? true : undefined;
1104
1116
  const killOthersOnFail = config.killOthersOnFail === true ? true : undefined;
1105
1117
  const noWatch = config.noWatch === true ? true : undefined;
@@ -2145,6 +2157,7 @@ var SHORTCUTS = {
2145
2157
  restart: { key: "r", label: "R", description: "restart" },
2146
2158
  stopStart: { key: "s", label: "S", description: "stop/start" },
2147
2159
  clear: { key: "l", label: "L", description: "clear" },
2160
+ timestamps: { key: "t", label: "T", description: "timestamps" },
2148
2161
  scrollToTop: { key: "g", label: "G", description: "top" },
2149
2162
  scrollToBottom: { key: "g", label: "Shift+G", description: "bottom", shift: true }
2150
2163
  };
@@ -2156,15 +2169,32 @@ var STATUS_HINTS = [
2156
2169
  [SHORTCUTS.search.label, SHORTCUTS.search.description],
2157
2170
  [SHORTCUTS.copy.label, SHORTCUTS.copy.description],
2158
2171
  [SHORTCUTS.clear.label, SHORTCUTS.clear.description],
2172
+ [SHORTCUTS.timestamps.label, SHORTCUTS.timestamps.description],
2159
2173
  ["Ctrl+Click", "open link"],
2160
2174
  ["Ctrl+C", "quit"]
2161
2175
  ];
2162
2176
  var STATUS_BAR_TEXT = STATUS_HINTS.map(([l, d]) => `${l}: ${d}`).join(" ");
2163
2177
 
2164
2178
  // src/ui/pane.ts
2165
- import { ScrollBoxRenderable } from "@opentui/core";
2179
+ import {
2180
+ LineNumberRenderable,
2181
+ ScrollBoxRenderable
2182
+ } from "@opentui/core";
2166
2183
  import { GhosttyTerminalRenderable } from "ghostty-opentui/terminal-buffer";
2167
2184
 
2185
+ // src/utils/timestamp.ts
2186
+ var DEFAULT_TIMESTAMP_FORMAT = "HH:mm:ss";
2187
+ function formatTimestamp(date, format) {
2188
+ const hours24 = date.getHours();
2189
+ const hours12 = hours24 % 12 || 12;
2190
+ return format.replace("YYYY", date.getFullYear().toString()).replace("MM", (date.getMonth() + 1).toString().padStart(2, "0")).replace("DD", date.getDate().toString().padStart(2, "0")).replace("HH", hours24.toString().padStart(2, "0")).replace("hh", hours12.toString().padStart(2, "0")).replace("mm", date.getMinutes().toString().padStart(2, "0")).replace("SSS", date.getMilliseconds().toString().padStart(3, "0")).replace("ss", date.getSeconds().toString().padStart(2, "0")).replace("A", hours24 < 12 ? "AM" : "PM");
2191
+ }
2192
+ function resolveTimestampFormat(timestamps) {
2193
+ if (!timestamps)
2194
+ return null;
2195
+ return typeof timestamps === "string" ? timestamps : DEFAULT_TIMESTAMP_FORMAT;
2196
+ }
2197
+
2168
2198
  // src/ui/url-handler.ts
2169
2199
  var URL_RE = /https?:\/\/[^\s<>"'`)\]},;]+/g;
2170
2200
  var FILE_PATH_RE = /(?:\.\.?\/|\/)[^\s:]+(?::(\d+)(?::(\d+))?)?/g;
@@ -2217,10 +2247,16 @@ class Pane {
2217
2247
  terminal;
2218
2248
  decoder = new TextDecoder;
2219
2249
  bytesFed = 0;
2250
+ renderer;
2251
+ timestampGutter = null;
2252
+ _timestampFormat = null;
2253
+ lineTimestamps = [];
2254
+ lineCounter = 0;
2220
2255
  _onScroll = null;
2221
2256
  _onCopy = null;
2222
2257
  _onLinkClick = null;
2223
2258
  constructor(renderer, name, cols, rows, interactive = false) {
2259
+ this.renderer = renderer;
2224
2260
  this.scrollBox = new ScrollBoxRenderable(renderer, {
2225
2261
  id: `pane-${name}`,
2226
2262
  flexGrow: 1,
@@ -2247,7 +2283,12 @@ class Pane {
2247
2283
  if (text) {
2248
2284
  this._onCopy?.(text);
2249
2285
  } else {
2250
- queueMicrotask(() => renderer.clearSelection());
2286
+ const stale = renderer.getSelection();
2287
+ queueMicrotask(() => {
2288
+ if (renderer.getSelection() === stale) {
2289
+ renderer.clearSelection();
2290
+ }
2291
+ });
2251
2292
  }
2252
2293
  }
2253
2294
  return result;
@@ -2268,9 +2309,25 @@ class Pane {
2268
2309
  if (this.terminal.lineCount > MAX_SCROLLBACK_LINES || this.bytesFed > MAX_BUFFER_BYTES) {
2269
2310
  this.terminal.reset();
2270
2311
  this.bytesFed = 0;
2312
+ this.lineTimestamps = [];
2313
+ this.lineCounter = 0;
2314
+ }
2315
+ const now = Date.now();
2316
+ if (this.lineCounter === 0) {
2317
+ this.lineTimestamps.push(now);
2318
+ this.lineCounter = 1;
2319
+ }
2320
+ for (let i = 0;i < data.length; i++) {
2321
+ if (data[i] === 10) {
2322
+ this.lineTimestamps.push(now);
2323
+ this.lineCounter++;
2324
+ }
2271
2325
  }
2272
2326
  const text = this.decoder.decode(data, { stream: true });
2273
2327
  this.terminal.feed(text);
2328
+ if (this._timestampFormat) {
2329
+ this.updateTimestampSigns();
2330
+ }
2274
2331
  }
2275
2332
  resize(cols, rows) {
2276
2333
  this.terminal.cols = cols;
@@ -2347,6 +2404,60 @@ class Pane {
2347
2404
  clear() {
2348
2405
  this.terminal.reset();
2349
2406
  this.bytesFed = 0;
2407
+ this.lineTimestamps = [];
2408
+ this.lineCounter = 0;
2409
+ if (this._timestampFormat) {
2410
+ this.timestampGutter?.clearAllLineSigns();
2411
+ }
2412
+ }
2413
+ setTimestamps(value) {
2414
+ const newFormat = !value ? null : typeof value === "string" ? value : DEFAULT_TIMESTAMP_FORMAT;
2415
+ const wasEnabled = this._timestampFormat !== null;
2416
+ const isEnabled = newFormat !== null;
2417
+ if (wasEnabled === isEnabled && this._timestampFormat === newFormat)
2418
+ return;
2419
+ this._timestampFormat = newFormat;
2420
+ if (isEnabled && !wasEnabled) {
2421
+ this.scrollBox.remove(this.terminal.id);
2422
+ const gutterWidth = (newFormat?.length ?? 8) + 1;
2423
+ this.timestampGutter = new LineNumberRenderable(this.renderer, {
2424
+ id: `ts-${this.terminal.id}`,
2425
+ target: this.terminal,
2426
+ showLineNumbers: false,
2427
+ minWidth: gutterWidth,
2428
+ paddingRight: 0,
2429
+ fg: "#666666"
2430
+ });
2431
+ this.scrollBox.add(this.timestampGutter);
2432
+ this.updateTimestampSigns();
2433
+ } else if (!isEnabled && wasEnabled) {
2434
+ if (this.timestampGutter) {
2435
+ this.timestampGutter.clearTarget();
2436
+ this.scrollBox.remove(this.timestampGutter.id);
2437
+ this.timestampGutter = null;
2438
+ }
2439
+ this.scrollBox.add(this.terminal);
2440
+ } else if (isEnabled) {
2441
+ this.updateTimestampSigns();
2442
+ }
2443
+ }
2444
+ get timestampsEnabled() {
2445
+ return this._timestampFormat !== null;
2446
+ }
2447
+ updateTimestampSigns() {
2448
+ if (!(this.timestampGutter && this._timestampFormat))
2449
+ return;
2450
+ const fmt = this._timestampFormat;
2451
+ const signs = new Map;
2452
+ let prevFormatted = "";
2453
+ for (let i = 0;i < this.lineTimestamps.length; i++) {
2454
+ const formatted = formatTimestamp(new Date(this.lineTimestamps[i]), fmt);
2455
+ if (formatted !== prevFormatted) {
2456
+ signs.set(i, { before: formatted });
2457
+ prevFormatted = formatted;
2458
+ }
2459
+ }
2460
+ this.timestampGutter.setLineSigns(signs);
2350
2461
  }
2351
2462
  destroy() {
2352
2463
  this.terminal.destroy();
@@ -2568,6 +2679,7 @@ class StatusBar {
2568
2679
  bg: "#1a1a1a",
2569
2680
  paddingX: 1
2570
2681
  });
2682
+ this.renderable.selectable = false;
2571
2683
  }
2572
2684
  setSearchMode(active, query = "", matchCount = 0, currentIndex = -1, crossProcessInfo) {
2573
2685
  this._searchMode = active;
@@ -2953,6 +3065,9 @@ class App {
2953
3065
  for (const name of this.names) {
2954
3066
  const interactive = this.config.processes[name].interactive === true;
2955
3067
  const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
3068
+ if (this.config.timestamps) {
3069
+ pane.setTimestamps(this.config.timestamps);
3070
+ }
2956
3071
  pane.onCopy((text) => {
2957
3072
  this.copyToClipboard(text);
2958
3073
  this.statusBar.showTemporaryMessage("Copied!");
@@ -3066,6 +3181,18 @@ class App {
3066
3181
  this.logWriter.truncate(this.activePane);
3067
3182
  return;
3068
3183
  }
3184
+ if (name === SHORTCUTS.timestamps.key) {
3185
+ const firstPane = this.panes.values().next().value;
3186
+ if (firstPane?.timestampsEnabled) {
3187
+ for (const pane of this.panes.values())
3188
+ pane.setTimestamps(false);
3189
+ } else {
3190
+ const fmt = this.config.timestamps ?? true;
3191
+ for (const pane of this.panes.values())
3192
+ pane.setTimestamps(fmt);
3193
+ }
3194
+ return;
3195
+ }
3069
3196
  const num = Number.parseInt(name, 10);
3070
3197
  if (num >= 1 && num <= 9 && num <= this.tabBar.count) {
3071
3198
  this.tabBar.setSelectedIndex(num - 1);
@@ -3208,9 +3335,10 @@ class App {
3208
3335
  // src/ui/prefix.ts
3209
3336
  var RESET = ANSI_RESET;
3210
3337
  var DIM = "\x1B[90m";
3338
+ var CHA_COL1_RE = /\x1b\[1?G/g;
3211
3339
  var CURSOR_SEQ_RE = /\x1b\[[\d;]*[ABCDEFGHJKLMSTdf]/g;
3212
3340
  function stripCursorSequences(text) {
3213
- return text.replace(CURSOR_SEQ_RE, "");
3341
+ return text.replace(CHA_COL1_RE, "\r").replace(CURSOR_SEQ_RE, "");
3214
3342
  }
3215
3343
 
3216
3344
  class PrefixDisplay {
@@ -3222,7 +3350,7 @@ class PrefixDisplay {
3222
3350
  logWriter;
3223
3351
  killOthers;
3224
3352
  killOthersOnFail;
3225
- timestamps;
3353
+ timestampFormat;
3226
3354
  stopping = false;
3227
3355
  startTime = 0;
3228
3356
  constructor(manager, config, options = {}) {
@@ -3230,7 +3358,7 @@ class PrefixDisplay {
3230
3358
  this.logWriter = options.logWriter;
3231
3359
  this.killOthers = options.killOthers ?? false;
3232
3360
  this.killOthersOnFail = options.killOthersOnFail ?? false;
3233
- this.timestamps = options.timestamps ?? false;
3361
+ this.timestampFormat = resolveTimestampFormat(options.timestamps);
3234
3362
  this.noColor = "NO_COLOR" in process.env;
3235
3363
  const names = manager.getProcessNames();
3236
3364
  this.colors = buildProcessColorMap(names, config);
@@ -3299,16 +3427,10 @@ class PrefixDisplay {
3299
3427
  }
3300
3428
  }
3301
3429
  handleStatus(_name, _status) {}
3302
- formatTimestamp() {
3303
- const now = new Date;
3304
- const h = String(now.getHours()).padStart(2, "0");
3305
- const m = String(now.getMinutes()).padStart(2, "0");
3306
- const s = String(now.getSeconds()).padStart(2, "0");
3307
- return `${h}:${m}:${s}`;
3308
- }
3309
3430
  printLine(name, line) {
3310
- const ts = this.timestamps ? `${DIM}[${this.formatTimestamp()}]${RESET} ` : "";
3311
- const tsPlain = this.timestamps ? `[${this.formatTimestamp()}] ` : "";
3431
+ const fmt = this.timestampFormat;
3432
+ const ts = fmt ? `${DIM}[${formatTimestamp(new Date, fmt)}]${RESET} ` : "";
3433
+ const tsPlain = fmt ? `[${formatTimestamp(new Date, fmt)}] ` : "";
3312
3434
  if (this.noColor) {
3313
3435
  process.stdout.write(`${tsPlain}[${name}] ${stripAnsi(line)}
3314
3436
  `);
@@ -3845,16 +3967,19 @@ async function main() {
3845
3967
  const logDir = parsed.logDir ?? config.logDir;
3846
3968
  const logWriter = logDir ? LogWriter.createPersistent(logDir) : LogWriter.createTemp();
3847
3969
  printWarnings(warnings);
3970
+ const timestamps = parsed.timestamps || config.timestamps;
3848
3971
  const usePrefix = parsed.prefix || config.prefix;
3849
3972
  if (usePrefix) {
3850
3973
  const display = new PrefixDisplay(manager, config, {
3851
3974
  logWriter,
3852
3975
  killOthers: parsed.killOthers || config.killOthers,
3853
3976
  killOthersOnFail: parsed.killOthersOnFail || config.killOthersOnFail,
3854
- timestamps: parsed.timestamps || config.timestamps
3977
+ timestamps
3855
3978
  });
3856
3979
  await display.start();
3857
3980
  } else {
3981
+ if (timestamps)
3982
+ config.timestamps = timestamps;
3858
3983
  manager.on(logWriter.handleEvent);
3859
3984
  const app = new App(manager, config, logWriter);
3860
3985
  setupShutdownHandlers(app, logWriter);
package/dist/types.d.ts CHANGED
@@ -95,8 +95,8 @@ export interface NumuxConfig<K extends string = string> {
95
95
  * @default false
96
96
  */
97
97
  prefix?: boolean;
98
- /** Add timestamps to prefixed output lines (only applies when `prefix` is true) */
99
- timestamps?: boolean;
98
+ /** Add timestamps to output lines. `true` uses default `HH:mm:ss` format, or pass a format string (e.g. `"HH:mm:ss.SSS"`) */
99
+ timestamps?: boolean | string;
100
100
  /**
101
101
  * Kill all processes when any one exits (regardless of exit code)
102
102
  * @default false
@@ -125,7 +125,7 @@ export interface ResolvedProcessConfig extends Omit<NumuxProcessConfig, 'depends
125
125
  export interface ResolvedNumuxConfig {
126
126
  sort?: SortOrder;
127
127
  prefix?: boolean;
128
- timestamps?: boolean;
128
+ timestamps?: boolean | string;
129
129
  killOthers?: boolean;
130
130
  killOthersOnFail?: boolean;
131
131
  noWatch?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "numux",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",