numux 2.2.1 → 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.1",
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,
@@ -2273,9 +2309,25 @@ class Pane {
2273
2309
  if (this.terminal.lineCount > MAX_SCROLLBACK_LINES || this.bytesFed > MAX_BUFFER_BYTES) {
2274
2310
  this.terminal.reset();
2275
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
+ }
2276
2325
  }
2277
2326
  const text = this.decoder.decode(data, { stream: true });
2278
2327
  this.terminal.feed(text);
2328
+ if (this._timestampFormat) {
2329
+ this.updateTimestampSigns();
2330
+ }
2279
2331
  }
2280
2332
  resize(cols, rows) {
2281
2333
  this.terminal.cols = cols;
@@ -2352,6 +2404,60 @@ class Pane {
2352
2404
  clear() {
2353
2405
  this.terminal.reset();
2354
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);
2355
2461
  }
2356
2462
  destroy() {
2357
2463
  this.terminal.destroy();
@@ -2959,6 +3065,9 @@ class App {
2959
3065
  for (const name of this.names) {
2960
3066
  const interactive = this.config.processes[name].interactive === true;
2961
3067
  const pane = new Pane(this.renderer, name, termCols, termRows, interactive);
3068
+ if (this.config.timestamps) {
3069
+ pane.setTimestamps(this.config.timestamps);
3070
+ }
2962
3071
  pane.onCopy((text) => {
2963
3072
  this.copyToClipboard(text);
2964
3073
  this.statusBar.showTemporaryMessage("Copied!");
@@ -3072,6 +3181,18 @@ class App {
3072
3181
  this.logWriter.truncate(this.activePane);
3073
3182
  return;
3074
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
+ }
3075
3196
  const num = Number.parseInt(name, 10);
3076
3197
  if (num >= 1 && num <= 9 && num <= this.tabBar.count) {
3077
3198
  this.tabBar.setSelectedIndex(num - 1);
@@ -3229,7 +3350,7 @@ class PrefixDisplay {
3229
3350
  logWriter;
3230
3351
  killOthers;
3231
3352
  killOthersOnFail;
3232
- timestamps;
3353
+ timestampFormat;
3233
3354
  stopping = false;
3234
3355
  startTime = 0;
3235
3356
  constructor(manager, config, options = {}) {
@@ -3237,7 +3358,7 @@ class PrefixDisplay {
3237
3358
  this.logWriter = options.logWriter;
3238
3359
  this.killOthers = options.killOthers ?? false;
3239
3360
  this.killOthersOnFail = options.killOthersOnFail ?? false;
3240
- this.timestamps = options.timestamps ?? false;
3361
+ this.timestampFormat = resolveTimestampFormat(options.timestamps);
3241
3362
  this.noColor = "NO_COLOR" in process.env;
3242
3363
  const names = manager.getProcessNames();
3243
3364
  this.colors = buildProcessColorMap(names, config);
@@ -3306,16 +3427,10 @@ class PrefixDisplay {
3306
3427
  }
3307
3428
  }
3308
3429
  handleStatus(_name, _status) {}
3309
- formatTimestamp() {
3310
- const now = new Date;
3311
- const h = String(now.getHours()).padStart(2, "0");
3312
- const m = String(now.getMinutes()).padStart(2, "0");
3313
- const s = String(now.getSeconds()).padStart(2, "0");
3314
- return `${h}:${m}:${s}`;
3315
- }
3316
3430
  printLine(name, line) {
3317
- const ts = this.timestamps ? `${DIM}[${this.formatTimestamp()}]${RESET} ` : "";
3318
- 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)}] ` : "";
3319
3434
  if (this.noColor) {
3320
3435
  process.stdout.write(`${tsPlain}[${name}] ${stripAnsi(line)}
3321
3436
  `);
@@ -3852,16 +3967,19 @@ async function main() {
3852
3967
  const logDir = parsed.logDir ?? config.logDir;
3853
3968
  const logWriter = logDir ? LogWriter.createPersistent(logDir) : LogWriter.createTemp();
3854
3969
  printWarnings(warnings);
3970
+ const timestamps = parsed.timestamps || config.timestamps;
3855
3971
  const usePrefix = parsed.prefix || config.prefix;
3856
3972
  if (usePrefix) {
3857
3973
  const display = new PrefixDisplay(manager, config, {
3858
3974
  logWriter,
3859
3975
  killOthers: parsed.killOthers || config.killOthers,
3860
3976
  killOthersOnFail: parsed.killOthersOnFail || config.killOthersOnFail,
3861
- timestamps: parsed.timestamps || config.timestamps
3977
+ timestamps
3862
3978
  });
3863
3979
  await display.start();
3864
3980
  } else {
3981
+ if (timestamps)
3982
+ config.timestamps = timestamps;
3865
3983
  manager.on(logWriter.handleEvent);
3866
3984
  const app = new App(manager, config, logWriter);
3867
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.1",
3
+ "version": "2.3.0",
4
4
  "description": "Terminal multiplexer with dependency orchestration",
5
5
  "type": "module",
6
6
  "license": "MIT",