Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ src/
- Panes are **readonly by default** — keyboard input is not forwarded to processes
- Arrow keys (Up/Down) navigate between tabs, PageUp/PageDown scroll by page, Home/End to top/bottom
- Mouse drag selects text and auto-copies to clipboard (OSC 52); `Y` key also copies selection
- `T` toggles an `HH:MM:SS` timestamp gutter in TUI mode; also enabled via `timestamps: true` config or `--timestamps` flag. Accepts a format string (e.g. `timestamps: "HH:mm:ss.SSS"`) with tokens: `YYYY`, `MM`, `DD`, `HH`, `hh`, `mm`, `ss`, `SSS`, `A`
- `T` toggles an `HH:mm:ss.SSS` timestamp gutter in TUI mode; also enabled via `timestamps: true` config or `--timestamps` flag. Accepts a format string (e.g. `timestamps: "HH:mm:ss"`) with tokens: `YYYY`, `MM`, `DD`, `HH`, `hh`, `mm`, `ss`, `SSS`, `A`
- Keybinding hints are shown in the status bar; config lives in `src/ui/keybindings.ts`
- Set `interactive: true` on processes that need stdin (REPLs, shells)
- Non-interactive panes hide the terminal cursor
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export default defineConfig({
| `--kill-others-on-fail` | Kill all processes when any exits with non-zero code |
| `--max-restarts` `<n>` | Max auto-restarts for crashed processes |
| `--no-watch` | Disable file watching even if config has watch patterns |
| `-t,` `--timestamps` `[<format>]` | Add timestamps to output (default HH:mm:ss, or pass a format string) |
| `-t,` `--timestamps` `[<format>]` | Add timestamps to output (default HH:mm:ss.SSS, or pass a format string) |
| `--log-dir` `<path>` | Write per-process logs to directory |
| `--debug` | Enable debug logging to .numux/debug.log |
| `-h,` `--help` | Show this help |
Expand Down Expand Up @@ -277,7 +277,7 @@ Top-level options apply to all processes (process-level settings override):
| `watch` | `string \| string[]` | Global watch patterns, inherited by processes without their own watch |
| `sort` | `'config' \| 'alphabetical' \| 'topological'` | Tab display order. `'config'` preserves definition order (package.json script order for wildcards), `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers. |
| `prefix` | `boolean` | Use prefixed output mode instead of TUI (for CI/scripts) |
| `timestamps` | `boolean \| string` | Add timestamps to output lines. `true` uses default `HH:mm:ss` format, or pass a format string (e.g. `"HH:mm:ss.SSS"`) |
| `timestamps` | `boolean \| string` | Add timestamps to output lines. `true` uses default `HH:mm:ss.SSS` format, or pass a format string (e.g. `"HH:mm:ss"`) |
| `killOthers` | `boolean` | Kill all processes when any one exits (regardless of exit code) |
| `killOthersOnFail` | `boolean` | Kill all processes when any one exits with a non-zero exit code |
| `noWatch` | `boolean` | Disable file watching even if processes have watch patterns |
Expand Down
2 changes: 1 addition & 1 deletion src/cli-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export const FLAGS: FlagDef[] = [
long: '--timestamps',
short: '-t',
key: 'timestamps',
description: 'Add timestamps to output (default HH:mm:ss, or pass a format string)',
description: 'Add timestamps to output (default HH:mm:ss.SSS, or pass a format string)',
valueName: '<format>',
completionHint: 'none'
},
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export interface NumuxConfig<K extends string = string> {
* @default false
*/
prefix?: boolean
/** Add timestamps to output lines. `true` uses default `HH:mm:ss` format, or pass a format string (e.g. `"HH:mm:ss.SSS"`) */
/** Add timestamps to output lines. `true` uses default `HH:mm:ss.SSS` format, or pass a format string (e.g. `"HH:mm:ss"`) */
timestamps?: boolean | string
/**
* Kill all processes when any one exits (regardless of exit code)
Expand Down
48 changes: 48 additions & 0 deletions src/ui/pane.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,51 @@ describe('Pane timestamp toggle', () => {
expect(pane.timestampsEnabled).toBe(true)
})
})

describe('Pane timestamp signs', () => {
test('every logical line gets a timestamp sign', async () => {
const pane = await createPane({ timestamps: true })
pane.feed(encoder.encode('line1\nline2\nline3\n'))
const signs = pane.getTimestampSigns()!
// 4 logical lines: line1, line2, line3, and trailing newline
expect(signs.size).toBe(4)
for (let i = 0; i < 4; i++) {
expect(signs.has(i)).toBe(true)
expect(signs.get(i)!.before).toBeDefined()
}
})

test('lines with identical timestamps still get individual signs', async () => {
const pane = await createPane({ timestamps: 'HH:mm:ss' })
// All lines fed in one call — same Date.now() — same formatted second
pane.feed(encoder.encode('a\nb\nc\n'))
const signs = pane.getTimestampSigns()!
expect(signs.size).toBe(4)
// All should have the same formatted value but still be present
const values = [...signs.values()].map(s => s.before)
expect(new Set(values).size).toBe(1) // same second
expect(values.length).toBe(4) // but every line has one
})

test('signs include milliseconds with default format', async () => {
const pane = await createPane({ timestamps: true })
pane.feed(encoder.encode('hello\n'))
const signs = pane.getTimestampSigns()!
const ts = signs.get(0)!.before!
// Default format is HH:mm:ss.SSS — 12 chars
expect(ts).toMatch(/^\d{2}:\d{2}:\d{2}\.\d{3}$/)
})

test('signs cleared after clear()', async () => {
const pane = await createPane({ timestamps: true })
pane.feed(encoder.encode('line1\nline2\n'))
expect(pane.getTimestampSigns()!.size).toBeGreaterThan(0)
pane.clear()
expect(pane.getTimestampSigns()!.size).toBe(0)
})

test('returns null when timestamps disabled', async () => {
const pane = await createPane()
expect(pane.getTimestampSigns()).toBeNull()
})
})
13 changes: 6 additions & 7 deletions src/ui/pane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,18 +268,17 @@ export class Pane {
return this._timestampFormat !== null
}

/** Returns the current line signs map from the timestamp gutter, or null if disabled. */
getTimestampSigns(): Map<number, LineSign> | null {
return this.timestampGutter?.getLineSigns() ?? null
}

private updateTimestampSigns(): void {
if (!(this.timestampGutter && this._timestampFormat)) return
const fmt = this._timestampFormat
const signs = new Map<number, LineSign>()
let prevFormatted = ''
for (let i = 0; i < this.lineTimestamps.length; i++) {
const formatted = formatTimestamp(new Date(this.lineTimestamps[i]), fmt)
// Only show timestamp when it changes from the previous line
if (formatted !== prevFormatted) {
signs.set(i, { before: formatted })
prevFormatted = formatted
}
signs.set(i, { before: formatTimestamp(new Date(this.lineTimestamps[i]), fmt) })
}
this.timestampGutter.setLineSigns(signs)
}
Expand Down
6 changes: 3 additions & 3 deletions src/ui/prefix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,16 @@ describe('PrefixDisplay (integration)', () => {
expect(exitCode).toBe(1)
}, 15000)

test('--timestamps prepends HH:MM:SS to output lines', async () => {
test('--timestamps prepends HH:mm:ss.SSS to output lines', async () => {
const config = writeConfig(
'timestamps.json',
JSON.stringify({
processes: { ts: { command: "echo 'hello'" } }
})
)
const { stdout, exitCode } = await runPrefix(config, ['--timestamps'])
// Should contain a timestamp like [12:34:56]
expect(stdout).toMatch(/\[\d{2}:\d{2}:\d{2}\]/)
// Should contain a timestamp like [12:34:56.789]
expect(stdout).toMatch(/\[\d{2}:\d{2}:\d{2}\.\d{3}\]/)
expect(stdout).toContain('hello')
expect(exitCode).toBe(0)
}, 10000)
Expand Down
2 changes: 1 addition & 1 deletion src/utils/timestamp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Default timestamp format */
export const DEFAULT_TIMESTAMP_FORMAT = 'HH:mm:ss'
export const DEFAULT_TIMESTAMP_FORMAT = 'HH:mm:ss.SSS'

/**
* Format a Date using a simple token-based format string.
Expand Down
Loading