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
29 changes: 28 additions & 1 deletion test/tui-progress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { describe, expect, it } from 'vitest'

import { cells, clock, fit, progressBar } from '../tui/src/progress.ts'
import { cells, clock, fit, playedS, progressBar } from '../tui/src/progress.ts'

// Wide glyphs are the SUBJECT of these cases, so they ride in as code points:
// the source-language gate (DESIGN 0) keeps CJK out of v1 sources, and a test
Expand Down Expand Up @@ -79,6 +79,33 @@ describe('progressBar', () => {
})
})

describe('playedS', () => {
const started = 1_700_000_000_000

it('advances on the clock the front-end keeps itself', () => {
expect(playedS(started, 174, started)).toBe(0)
expect(playedS(started, 174, started + 63_500)).toBe(63.5)
})

it('stops at the length of the track instead of outrunning it', () => {
// A coda riding the outro (spec 04 3.3) keeps the state on music after the
// song itself has ended, and the wall clock would otherwise print 3:03 of
// a 2:54 track. The rail already clamps; the readout has to agree with it.
expect(playedS(started, 174, started + 189_000)).toBe(174)
expect(progressBar(playedS(started, 174, started + 189_000), 174, 10).played).toHaveLength(10)
})

it('reads an unknown length as no ceiling at all', () => {
// durationS absent is "no rail, just the title" (spec 10 3.3): nothing to
// clamp to, and a zero ceiling would freeze the clock at 0:00.
expect(playedS(started, 0, started + 42_000)).toBe(42)
})

it('never reads back before the start', () => {
expect(playedS(started, 174, started - 5_000)).toBe(0)
})
})

describe('cells', () => {
it('counts terminal cells, not code points — CJK is twice as wide', () => {
expect(cells('Calgary')).toBe(7)
Expand Down
4 changes: 2 additions & 2 deletions tui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import { encodeWavePng, waveGeomFor, waveRowsFor, WAVE_FPS } from './wave-image.
import { IDENT_LINE, identSize, TAGLINE, WORDMARK } from './logo.ts'
import { busyLine, COMPOSER_KEYS, composerRows, floorFace } from './floor.ts'
import { accentFor, CARD, CARD_INK, CHIP, EMBER, hush, INK, mix, PERIWINKLE, QUIET, WARM, type Accent } from './palette.ts'
import { cells, clock, fit, progressBar } from './progress.ts'
import { cells, clock, fit, playedS, progressBar } from './progress.ts'
import { adjust, languagePatch, paneFacts, paneItems } from './settings-pane.ts'
import {
awayGreeting,
Expand Down Expand Up @@ -769,7 +769,7 @@ export function App({ subscribe, wire }: { subscribe: Subscribe; wire: Wire }):
// The identity of the TRACK, not of the object: a re-emit during the song
// (a typed line refreshing presence) must not restart the interval.
}, [track?.startedAt, track?.durationS])
const elapsedS = track === null ? 0 : (now - track.startedAt) / 1000
const elapsedS = track === null ? 0 : playedS(track.startedAt, track.durationS, now)

// The alive band's composition follows the live pet setting (spec 12 §3.7),
// with the env override resolved inside bandLayout.
Expand Down
11 changes: 11 additions & 0 deletions tui/src/progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ export function clock(seconds: number): string {
return `${h > 0 ? `${h}:` : ''}${mm}:${String(s).padStart(2, '0')}`
}

// How far into the track the strip should read, on the clock the front-end keeps
// (spec 10 3.3). Bounded by the length, because the state stays on music after
// the audio has ended -- a coda riding the outro (spec 04 3.3) holds it there
// for as long as it takes to say, and unbounded wall clock prints 3:03 of a
// 2:54 song. A length of 0 is "unknown" (no rail), so there is nothing to bound
// it with.
export function playedS(startedAt: number, durationS: number, now: number): number {
const played = Math.max(0, (now - startedAt) / 1000)
return durationS > 0 ? Math.min(played, durationS) : played
}

// The rail split in two so the caller can ink them differently: what has played
// (full cells plus the eighth-cell leading edge) and what has not. The two
// always add up to exactly `width` cells.
Expand Down
Loading