@boba-cli/timer 0.1.0-alpha.1

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 ADDED
@@ -0,0 +1,69 @@
1
+ # @boba-cli/timer
2
+
3
+ Countdown timer component for Boba terminal UIs. Port of Charmbracelet Bubbles timer.
4
+
5
+ <img src="../../examples/timer-demo.gif" width="950" alt="Timer component demo" />
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @boba-cli/timer
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ```ts
16
+ import { TimerModel, TickMsg, TimeoutMsg } from '@boba-cli/timer'
17
+ import type { Cmd, Msg, Model } from '@boba-cli/tea'
18
+
19
+ const timer = TimerModel.new({ timeout: 30_000 }) // 30 seconds
20
+
21
+ function init(): Cmd<Msg> {
22
+ return timer.init()
23
+ }
24
+
25
+ function update(msg: Msg): [Model, Cmd<Msg>] {
26
+ if (msg instanceof TickMsg || msg instanceof TimeoutMsg) {
27
+ const [nextTimer, cmd] = timer.update(msg)
28
+ return [{ ...model, timer: nextTimer }, cmd]
29
+ }
30
+ return [model, null]
31
+ }
32
+
33
+ function view(): string {
34
+ return `Remaining ${timer.view()}`
35
+ }
36
+ ```
37
+
38
+ ## API
39
+
40
+ | Export | Description |
41
+ | -------------- | ----------------------------------------- |
42
+ | `TimerModel` | Countdown timer model |
43
+ | `TimerOptions` | Options for creating a timer |
44
+ | `TickMsg` | Tick message carrying ID/tag/timeout flag |
45
+ | `TimeoutMsg` | Message emitted when timer expires |
46
+ | `StartStopMsg` | Message to start/stop the timer |
47
+
48
+ ### TimerModel methods
49
+
50
+ | Method | Description |
51
+ | ------------------------- | --------------------------------------- |
52
+ | `id()` | Unique ID for message routing |
53
+ | `running()` | Whether the timer is active |
54
+ | `timedOut()` | Whether the timer has expired |
55
+ | `init()` | Start ticking on init |
56
+ | `update(msg)` | Handle messages, returns `[model, cmd]` |
57
+ | `view()` | Render remaining time |
58
+ | `start()/stop()/toggle()` | Control commands |
59
+
60
+ ## Scripts
61
+
62
+ - `pnpm -C packages/timer build`
63
+ - `pnpm -C packages/timer test`
64
+ - `pnpm -C packages/timer lint`
65
+ - `pnpm -C packages/timer generate:api-report`
66
+
67
+ ## License
68
+
69
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ var tea = require('@boba-cli/tea');
4
+
5
+ // src/messages.ts
6
+ var TickMsg = class {
7
+ constructor(id, tag, timeout) {
8
+ this.id = id;
9
+ this.tag = tag;
10
+ this.timeout = timeout;
11
+ }
12
+ _tag = "timer-tick";
13
+ };
14
+ var TimeoutMsg = class {
15
+ constructor(id) {
16
+ this.id = id;
17
+ }
18
+ _tag = "timer-timeout";
19
+ };
20
+ var StartStopMsg = class {
21
+ constructor(id, running) {
22
+ this.id = id;
23
+ this.running = running;
24
+ }
25
+ _tag = "timer-start-stop";
26
+ };
27
+ var lastId = 0;
28
+ function nextId() {
29
+ return ++lastId;
30
+ }
31
+ var TimerModel = class _TimerModel {
32
+ timeout;
33
+ interval;
34
+ #id;
35
+ #tag;
36
+ #running;
37
+ constructor(options) {
38
+ this.timeout = options.timeout;
39
+ this.interval = options.interval;
40
+ this.#running = options.running;
41
+ this.#id = options.id;
42
+ this.#tag = options.tag;
43
+ }
44
+ /** Create a new timer with the given options. */
45
+ static new(options) {
46
+ const interval = options.interval ?? 1e3;
47
+ return new _TimerModel({
48
+ timeout: options.timeout,
49
+ interval,
50
+ running: true,
51
+ id: nextId(),
52
+ tag: 0
53
+ });
54
+ }
55
+ /** Create a new timer with explicit timeout and interval. */
56
+ static withInterval(timeout, interval) {
57
+ return _TimerModel.new({ timeout, interval });
58
+ }
59
+ /** Unique ID for message routing. */
60
+ id() {
61
+ return this.#id;
62
+ }
63
+ /** Whether the timer is currently running (false once timed out). */
64
+ running() {
65
+ if (this.timedOut()) {
66
+ return false;
67
+ }
68
+ return this.#running;
69
+ }
70
+ /** Whether the timer has expired. */
71
+ timedOut() {
72
+ return this.timeout <= 0;
73
+ }
74
+ /** Start ticking. */
75
+ init() {
76
+ return this.tick();
77
+ }
78
+ /** Update the timer in response to a message. */
79
+ update(msg) {
80
+ if (msg instanceof StartStopMsg) {
81
+ if (msg.id !== 0 && msg.id !== this.#id) {
82
+ return [this, null];
83
+ }
84
+ const next = new _TimerModel({
85
+ timeout: this.timeout,
86
+ interval: this.interval,
87
+ running: msg.running,
88
+ id: this.#id,
89
+ tag: this.#tag
90
+ });
91
+ return [next, next.tick()];
92
+ }
93
+ if (msg instanceof TickMsg) {
94
+ if (!this.running() || msg.id !== 0 && msg.id !== this.#id) {
95
+ return [this, null];
96
+ }
97
+ if (msg.tag > 0 && msg.tag !== this.#tag) {
98
+ return [this, null];
99
+ }
100
+ const nextTimeout = this.timeout - this.interval;
101
+ const nextTag = this.#tag + 1;
102
+ const next = new _TimerModel({
103
+ timeout: nextTimeout,
104
+ interval: this.interval,
105
+ running: this.#running,
106
+ id: this.#id,
107
+ tag: nextTag
108
+ });
109
+ const timeoutCmd = next.timedOut() ? tea.msg(new TimeoutMsg(this.#id)) : null;
110
+ const tickCmd = next.timedOut() ? null : next.tick();
111
+ return [next, tea.batch(tickCmd, timeoutCmd)];
112
+ }
113
+ return [this, null];
114
+ }
115
+ /** Render remaining time as a human-readable string. */
116
+ view() {
117
+ return formatDuration(Math.max(0, this.timeout));
118
+ }
119
+ /** Command to start the timer. */
120
+ start() {
121
+ return this.startStop(true);
122
+ }
123
+ /** Command to stop/pause the timer. */
124
+ stop() {
125
+ return this.startStop(false);
126
+ }
127
+ /** Command to toggle running state. */
128
+ toggle() {
129
+ return this.startStop(!this.running());
130
+ }
131
+ tick() {
132
+ const id = this.#id;
133
+ const tag = this.#tag;
134
+ return tea.tick(this.interval, () => new TickMsg(id, tag, this.timedOut()));
135
+ }
136
+ startStop(running) {
137
+ return tea.msg(new StartStopMsg(this.#id, running));
138
+ }
139
+ };
140
+ function formatDuration(ms) {
141
+ const seconds = Math.floor(ms / 1e3) % 60;
142
+ const minutes = Math.floor(ms / 6e4) % 60;
143
+ const hours = Math.floor(ms / 36e5);
144
+ if (hours > 0) return `${hours}h${minutes}m${seconds}s`;
145
+ if (minutes > 0) return `${minutes}m${seconds}s`;
146
+ return `${seconds}s`;
147
+ }
148
+
149
+ exports.StartStopMsg = StartStopMsg;
150
+ exports.TickMsg = TickMsg;
151
+ exports.TimeoutMsg = TimeoutMsg;
152
+ exports.TimerModel = TimerModel;
153
+ //# sourceMappingURL=index.cjs.map
154
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/messages.ts","../src/model.ts"],"names":["lift","batch","tick"],"mappings":"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","file":"index.cjs","sourcesContent":["/** Tick message for timer countdown. @public */\nexport class TickMsg {\n readonly _tag = 'timer-tick'\n\n constructor(\n /** Unique timer ID */\n public readonly id: number,\n /** Internal tag for deduplication */\n public readonly tag: number,\n /** Whether this tick indicates the timer expired */\n public readonly timeout: boolean,\n ) {}\n}\n\n/** Message emitted once when the timer times out. @public */\nexport class TimeoutMsg {\n readonly _tag = 'timer-timeout'\n\n constructor(public readonly id: number) {}\n}\n\n/** Message that starts or stops the timer. @public */\nexport class StartStopMsg {\n readonly _tag = 'timer-start-stop'\n\n constructor(\n /** Unique timer ID */\n public readonly id: number,\n /** True to run, false to pause */\n public readonly running: boolean,\n ) {}\n}\n","import {\n batch,\n msg as lift,\n tick,\n type Cmd,\n type Msg as TeaMsg,\n type Model as TeaModel,\n} from '@boba-cli/tea'\nimport { StartStopMsg, TickMsg, TimeoutMsg } from './messages.js'\n\n// Module-level ID counter for unique timers\nlet lastId = 0\nfunction nextId(): number {\n return ++lastId\n}\n\n/** Options for creating a timer. @public */\nexport interface TimerOptions {\n /** Milliseconds until the timer expires. */\n timeout: number\n /** Tick interval in milliseconds (default: 1000). */\n interval?: number\n}\n\n/** Timer messages. @public */\nexport type TimerMsg = TickMsg | TimeoutMsg | StartStopMsg\n\n/** Countdown timer model. @public */\nexport class TimerModel implements TeaModel<TimerMsg, TimerModel> {\n readonly timeout: number\n readonly interval: number\n readonly #id: number\n readonly #tag: number\n readonly #running: boolean\n\n private constructor(options: {\n timeout: number\n interval: number\n running: boolean\n id: number\n tag: number\n }) {\n this.timeout = options.timeout\n this.interval = options.interval\n this.#running = options.running\n this.#id = options.id\n this.#tag = options.tag\n }\n\n /** Create a new timer with the given options. */\n static new(options: TimerOptions): TimerModel {\n const interval = options.interval ?? 1000\n return new TimerModel({\n timeout: options.timeout,\n interval,\n running: true,\n id: nextId(),\n tag: 0,\n })\n }\n\n /** Create a new timer with explicit timeout and interval. */\n static withInterval(timeout: number, interval: number): TimerModel {\n return TimerModel.new({ timeout, interval })\n }\n\n /** Unique ID for message routing. */\n id(): number {\n return this.#id\n }\n\n /** Whether the timer is currently running (false once timed out). */\n running(): boolean {\n if (this.timedOut()) {\n return false\n }\n return this.#running\n }\n\n /** Whether the timer has expired. */\n timedOut(): boolean {\n return this.timeout <= 0\n }\n\n /** Start ticking. */\n init(): Cmd<TimerMsg> {\n return this.tick()\n }\n\n /** Update the timer in response to a message. */\n update(msg: TeaMsg): [TimerModel, Cmd<TimerMsg>] {\n if (msg instanceof StartStopMsg) {\n if (msg.id !== 0 && msg.id !== this.#id) {\n return [this, null]\n }\n const next = new TimerModel({\n timeout: this.timeout,\n interval: this.interval,\n running: msg.running,\n id: this.#id,\n tag: this.#tag,\n })\n return [next, next.tick()]\n }\n\n if (msg instanceof TickMsg) {\n if (!this.running() || (msg.id !== 0 && msg.id !== this.#id)) {\n return [this, null]\n }\n\n if (msg.tag > 0 && msg.tag !== this.#tag) {\n return [this, null]\n }\n\n const nextTimeout = this.timeout - this.interval\n const nextTag = this.#tag + 1\n\n const next = new TimerModel({\n timeout: nextTimeout,\n interval: this.interval,\n running: this.#running,\n id: this.#id,\n tag: nextTag,\n })\n\n const timeoutCmd = next.timedOut() ? lift(new TimeoutMsg(this.#id)) : null\n const tickCmd = next.timedOut() ? null : next.tick()\n\n return [next, batch(tickCmd, timeoutCmd)]\n }\n\n return [this, null]\n }\n\n /** Render remaining time as a human-readable string. */\n view(): string {\n return formatDuration(Math.max(0, this.timeout))\n }\n\n /** Command to start the timer. */\n start(): Cmd<TimerMsg> {\n return this.startStop(true)\n }\n\n /** Command to stop/pause the timer. */\n stop(): Cmd<TimerMsg> {\n return this.startStop(false)\n }\n\n /** Command to toggle running state. */\n toggle(): Cmd<TimerMsg> {\n return this.startStop(!this.running())\n }\n\n private tick(): Cmd<TimerMsg> {\n const id = this.#id\n const tag = this.#tag\n return tick(this.interval, () => new TickMsg(id, tag, this.timedOut()))\n }\n\n private startStop(running: boolean): Cmd<TimerMsg> {\n return lift(new StartStopMsg(this.#id, running))\n }\n}\n\n// Simple duration formatter (e.g., 1h2m3s)\nfunction formatDuration(ms: number): string {\n const seconds = Math.floor(ms / 1000) % 60\n const minutes = Math.floor(ms / 60000) % 60\n const hours = Math.floor(ms / 3600000)\n\n if (hours > 0) return `${hours}h${minutes}m${seconds}s`\n if (minutes > 0) return `${minutes}m${seconds}s`\n return `${seconds}s`\n}\n"]}
@@ -0,0 +1,81 @@
1
+ import { Model, Cmd, Msg } from '@boba-cli/tea';
2
+
3
+ /** Tick message for timer countdown. @public */
4
+ declare class TickMsg {
5
+ /** Unique timer ID */
6
+ readonly id: number;
7
+ /** Internal tag for deduplication */
8
+ readonly tag: number;
9
+ /** Whether this tick indicates the timer expired */
10
+ readonly timeout: boolean;
11
+ readonly _tag = "timer-tick";
12
+ constructor(
13
+ /** Unique timer ID */
14
+ id: number,
15
+ /** Internal tag for deduplication */
16
+ tag: number,
17
+ /** Whether this tick indicates the timer expired */
18
+ timeout: boolean);
19
+ }
20
+ /** Message emitted once when the timer times out. @public */
21
+ declare class TimeoutMsg {
22
+ readonly id: number;
23
+ readonly _tag = "timer-timeout";
24
+ constructor(id: number);
25
+ }
26
+ /** Message that starts or stops the timer. @public */
27
+ declare class StartStopMsg {
28
+ /** Unique timer ID */
29
+ readonly id: number;
30
+ /** True to run, false to pause */
31
+ readonly running: boolean;
32
+ readonly _tag = "timer-start-stop";
33
+ constructor(
34
+ /** Unique timer ID */
35
+ id: number,
36
+ /** True to run, false to pause */
37
+ running: boolean);
38
+ }
39
+
40
+ /** Options for creating a timer. @public */
41
+ interface TimerOptions {
42
+ /** Milliseconds until the timer expires. */
43
+ timeout: number;
44
+ /** Tick interval in milliseconds (default: 1000). */
45
+ interval?: number;
46
+ }
47
+ /** Timer messages. @public */
48
+ type TimerMsg = TickMsg | TimeoutMsg | StartStopMsg;
49
+ /** Countdown timer model. @public */
50
+ declare class TimerModel implements Model<TimerMsg, TimerModel> {
51
+ #private;
52
+ readonly timeout: number;
53
+ readonly interval: number;
54
+ private constructor();
55
+ /** Create a new timer with the given options. */
56
+ static new(options: TimerOptions): TimerModel;
57
+ /** Create a new timer with explicit timeout and interval. */
58
+ static withInterval(timeout: number, interval: number): TimerModel;
59
+ /** Unique ID for message routing. */
60
+ id(): number;
61
+ /** Whether the timer is currently running (false once timed out). */
62
+ running(): boolean;
63
+ /** Whether the timer has expired. */
64
+ timedOut(): boolean;
65
+ /** Start ticking. */
66
+ init(): Cmd<TimerMsg>;
67
+ /** Update the timer in response to a message. */
68
+ update(msg: Msg): [TimerModel, Cmd<TimerMsg>];
69
+ /** Render remaining time as a human-readable string. */
70
+ view(): string;
71
+ /** Command to start the timer. */
72
+ start(): Cmd<TimerMsg>;
73
+ /** Command to stop/pause the timer. */
74
+ stop(): Cmd<TimerMsg>;
75
+ /** Command to toggle running state. */
76
+ toggle(): Cmd<TimerMsg>;
77
+ private tick;
78
+ private startStop;
79
+ }
80
+
81
+ export { StartStopMsg, TickMsg, TimeoutMsg, TimerModel, type TimerMsg, type TimerOptions };
@@ -0,0 +1,81 @@
1
+ import { Model, Cmd, Msg } from '@boba-cli/tea';
2
+
3
+ /** Tick message for timer countdown. @public */
4
+ declare class TickMsg {
5
+ /** Unique timer ID */
6
+ readonly id: number;
7
+ /** Internal tag for deduplication */
8
+ readonly tag: number;
9
+ /** Whether this tick indicates the timer expired */
10
+ readonly timeout: boolean;
11
+ readonly _tag = "timer-tick";
12
+ constructor(
13
+ /** Unique timer ID */
14
+ id: number,
15
+ /** Internal tag for deduplication */
16
+ tag: number,
17
+ /** Whether this tick indicates the timer expired */
18
+ timeout: boolean);
19
+ }
20
+ /** Message emitted once when the timer times out. @public */
21
+ declare class TimeoutMsg {
22
+ readonly id: number;
23
+ readonly _tag = "timer-timeout";
24
+ constructor(id: number);
25
+ }
26
+ /** Message that starts or stops the timer. @public */
27
+ declare class StartStopMsg {
28
+ /** Unique timer ID */
29
+ readonly id: number;
30
+ /** True to run, false to pause */
31
+ readonly running: boolean;
32
+ readonly _tag = "timer-start-stop";
33
+ constructor(
34
+ /** Unique timer ID */
35
+ id: number,
36
+ /** True to run, false to pause */
37
+ running: boolean);
38
+ }
39
+
40
+ /** Options for creating a timer. @public */
41
+ interface TimerOptions {
42
+ /** Milliseconds until the timer expires. */
43
+ timeout: number;
44
+ /** Tick interval in milliseconds (default: 1000). */
45
+ interval?: number;
46
+ }
47
+ /** Timer messages. @public */
48
+ type TimerMsg = TickMsg | TimeoutMsg | StartStopMsg;
49
+ /** Countdown timer model. @public */
50
+ declare class TimerModel implements Model<TimerMsg, TimerModel> {
51
+ #private;
52
+ readonly timeout: number;
53
+ readonly interval: number;
54
+ private constructor();
55
+ /** Create a new timer with the given options. */
56
+ static new(options: TimerOptions): TimerModel;
57
+ /** Create a new timer with explicit timeout and interval. */
58
+ static withInterval(timeout: number, interval: number): TimerModel;
59
+ /** Unique ID for message routing. */
60
+ id(): number;
61
+ /** Whether the timer is currently running (false once timed out). */
62
+ running(): boolean;
63
+ /** Whether the timer has expired. */
64
+ timedOut(): boolean;
65
+ /** Start ticking. */
66
+ init(): Cmd<TimerMsg>;
67
+ /** Update the timer in response to a message. */
68
+ update(msg: Msg): [TimerModel, Cmd<TimerMsg>];
69
+ /** Render remaining time as a human-readable string. */
70
+ view(): string;
71
+ /** Command to start the timer. */
72
+ start(): Cmd<TimerMsg>;
73
+ /** Command to stop/pause the timer. */
74
+ stop(): Cmd<TimerMsg>;
75
+ /** Command to toggle running state. */
76
+ toggle(): Cmd<TimerMsg>;
77
+ private tick;
78
+ private startStop;
79
+ }
80
+
81
+ export { StartStopMsg, TickMsg, TimeoutMsg, TimerModel, type TimerMsg, type TimerOptions };
package/dist/index.js ADDED
@@ -0,0 +1,149 @@
1
+ import { msg, batch, tick } from '@boba-cli/tea';
2
+
3
+ // src/messages.ts
4
+ var TickMsg = class {
5
+ constructor(id, tag, timeout) {
6
+ this.id = id;
7
+ this.tag = tag;
8
+ this.timeout = timeout;
9
+ }
10
+ _tag = "timer-tick";
11
+ };
12
+ var TimeoutMsg = class {
13
+ constructor(id) {
14
+ this.id = id;
15
+ }
16
+ _tag = "timer-timeout";
17
+ };
18
+ var StartStopMsg = class {
19
+ constructor(id, running) {
20
+ this.id = id;
21
+ this.running = running;
22
+ }
23
+ _tag = "timer-start-stop";
24
+ };
25
+ var lastId = 0;
26
+ function nextId() {
27
+ return ++lastId;
28
+ }
29
+ var TimerModel = class _TimerModel {
30
+ timeout;
31
+ interval;
32
+ #id;
33
+ #tag;
34
+ #running;
35
+ constructor(options) {
36
+ this.timeout = options.timeout;
37
+ this.interval = options.interval;
38
+ this.#running = options.running;
39
+ this.#id = options.id;
40
+ this.#tag = options.tag;
41
+ }
42
+ /** Create a new timer with the given options. */
43
+ static new(options) {
44
+ const interval = options.interval ?? 1e3;
45
+ return new _TimerModel({
46
+ timeout: options.timeout,
47
+ interval,
48
+ running: true,
49
+ id: nextId(),
50
+ tag: 0
51
+ });
52
+ }
53
+ /** Create a new timer with explicit timeout and interval. */
54
+ static withInterval(timeout, interval) {
55
+ return _TimerModel.new({ timeout, interval });
56
+ }
57
+ /** Unique ID for message routing. */
58
+ id() {
59
+ return this.#id;
60
+ }
61
+ /** Whether the timer is currently running (false once timed out). */
62
+ running() {
63
+ if (this.timedOut()) {
64
+ return false;
65
+ }
66
+ return this.#running;
67
+ }
68
+ /** Whether the timer has expired. */
69
+ timedOut() {
70
+ return this.timeout <= 0;
71
+ }
72
+ /** Start ticking. */
73
+ init() {
74
+ return this.tick();
75
+ }
76
+ /** Update the timer in response to a message. */
77
+ update(msg$1) {
78
+ if (msg$1 instanceof StartStopMsg) {
79
+ if (msg$1.id !== 0 && msg$1.id !== this.#id) {
80
+ return [this, null];
81
+ }
82
+ const next = new _TimerModel({
83
+ timeout: this.timeout,
84
+ interval: this.interval,
85
+ running: msg$1.running,
86
+ id: this.#id,
87
+ tag: this.#tag
88
+ });
89
+ return [next, next.tick()];
90
+ }
91
+ if (msg$1 instanceof TickMsg) {
92
+ if (!this.running() || msg$1.id !== 0 && msg$1.id !== this.#id) {
93
+ return [this, null];
94
+ }
95
+ if (msg$1.tag > 0 && msg$1.tag !== this.#tag) {
96
+ return [this, null];
97
+ }
98
+ const nextTimeout = this.timeout - this.interval;
99
+ const nextTag = this.#tag + 1;
100
+ const next = new _TimerModel({
101
+ timeout: nextTimeout,
102
+ interval: this.interval,
103
+ running: this.#running,
104
+ id: this.#id,
105
+ tag: nextTag
106
+ });
107
+ const timeoutCmd = next.timedOut() ? msg(new TimeoutMsg(this.#id)) : null;
108
+ const tickCmd = next.timedOut() ? null : next.tick();
109
+ return [next, batch(tickCmd, timeoutCmd)];
110
+ }
111
+ return [this, null];
112
+ }
113
+ /** Render remaining time as a human-readable string. */
114
+ view() {
115
+ return formatDuration(Math.max(0, this.timeout));
116
+ }
117
+ /** Command to start the timer. */
118
+ start() {
119
+ return this.startStop(true);
120
+ }
121
+ /** Command to stop/pause the timer. */
122
+ stop() {
123
+ return this.startStop(false);
124
+ }
125
+ /** Command to toggle running state. */
126
+ toggle() {
127
+ return this.startStop(!this.running());
128
+ }
129
+ tick() {
130
+ const id = this.#id;
131
+ const tag = this.#tag;
132
+ return tick(this.interval, () => new TickMsg(id, tag, this.timedOut()));
133
+ }
134
+ startStop(running) {
135
+ return msg(new StartStopMsg(this.#id, running));
136
+ }
137
+ };
138
+ function formatDuration(ms) {
139
+ const seconds = Math.floor(ms / 1e3) % 60;
140
+ const minutes = Math.floor(ms / 6e4) % 60;
141
+ const hours = Math.floor(ms / 36e5);
142
+ if (hours > 0) return `${hours}h${minutes}m${seconds}s`;
143
+ if (minutes > 0) return `${minutes}m${seconds}s`;
144
+ return `${seconds}s`;
145
+ }
146
+
147
+ export { StartStopMsg, TickMsg, TimeoutMsg, TimerModel };
148
+ //# sourceMappingURL=index.js.map
149
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/messages.ts","../src/model.ts"],"names":["msg","lift"],"mappings":"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","file":"index.js","sourcesContent":["/** Tick message for timer countdown. @public */\nexport class TickMsg {\n readonly _tag = 'timer-tick'\n\n constructor(\n /** Unique timer ID */\n public readonly id: number,\n /** Internal tag for deduplication */\n public readonly tag: number,\n /** Whether this tick indicates the timer expired */\n public readonly timeout: boolean,\n ) {}\n}\n\n/** Message emitted once when the timer times out. @public */\nexport class TimeoutMsg {\n readonly _tag = 'timer-timeout'\n\n constructor(public readonly id: number) {}\n}\n\n/** Message that starts or stops the timer. @public */\nexport class StartStopMsg {\n readonly _tag = 'timer-start-stop'\n\n constructor(\n /** Unique timer ID */\n public readonly id: number,\n /** True to run, false to pause */\n public readonly running: boolean,\n ) {}\n}\n","import {\n batch,\n msg as lift,\n tick,\n type Cmd,\n type Msg as TeaMsg,\n type Model as TeaModel,\n} from '@boba-cli/tea'\nimport { StartStopMsg, TickMsg, TimeoutMsg } from './messages.js'\n\n// Module-level ID counter for unique timers\nlet lastId = 0\nfunction nextId(): number {\n return ++lastId\n}\n\n/** Options for creating a timer. @public */\nexport interface TimerOptions {\n /** Milliseconds until the timer expires. */\n timeout: number\n /** Tick interval in milliseconds (default: 1000). */\n interval?: number\n}\n\n/** Timer messages. @public */\nexport type TimerMsg = TickMsg | TimeoutMsg | StartStopMsg\n\n/** Countdown timer model. @public */\nexport class TimerModel implements TeaModel<TimerMsg, TimerModel> {\n readonly timeout: number\n readonly interval: number\n readonly #id: number\n readonly #tag: number\n readonly #running: boolean\n\n private constructor(options: {\n timeout: number\n interval: number\n running: boolean\n id: number\n tag: number\n }) {\n this.timeout = options.timeout\n this.interval = options.interval\n this.#running = options.running\n this.#id = options.id\n this.#tag = options.tag\n }\n\n /** Create a new timer with the given options. */\n static new(options: TimerOptions): TimerModel {\n const interval = options.interval ?? 1000\n return new TimerModel({\n timeout: options.timeout,\n interval,\n running: true,\n id: nextId(),\n tag: 0,\n })\n }\n\n /** Create a new timer with explicit timeout and interval. */\n static withInterval(timeout: number, interval: number): TimerModel {\n return TimerModel.new({ timeout, interval })\n }\n\n /** Unique ID for message routing. */\n id(): number {\n return this.#id\n }\n\n /** Whether the timer is currently running (false once timed out). */\n running(): boolean {\n if (this.timedOut()) {\n return false\n }\n return this.#running\n }\n\n /** Whether the timer has expired. */\n timedOut(): boolean {\n return this.timeout <= 0\n }\n\n /** Start ticking. */\n init(): Cmd<TimerMsg> {\n return this.tick()\n }\n\n /** Update the timer in response to a message. */\n update(msg: TeaMsg): [TimerModel, Cmd<TimerMsg>] {\n if (msg instanceof StartStopMsg) {\n if (msg.id !== 0 && msg.id !== this.#id) {\n return [this, null]\n }\n const next = new TimerModel({\n timeout: this.timeout,\n interval: this.interval,\n running: msg.running,\n id: this.#id,\n tag: this.#tag,\n })\n return [next, next.tick()]\n }\n\n if (msg instanceof TickMsg) {\n if (!this.running() || (msg.id !== 0 && msg.id !== this.#id)) {\n return [this, null]\n }\n\n if (msg.tag > 0 && msg.tag !== this.#tag) {\n return [this, null]\n }\n\n const nextTimeout = this.timeout - this.interval\n const nextTag = this.#tag + 1\n\n const next = new TimerModel({\n timeout: nextTimeout,\n interval: this.interval,\n running: this.#running,\n id: this.#id,\n tag: nextTag,\n })\n\n const timeoutCmd = next.timedOut() ? lift(new TimeoutMsg(this.#id)) : null\n const tickCmd = next.timedOut() ? null : next.tick()\n\n return [next, batch(tickCmd, timeoutCmd)]\n }\n\n return [this, null]\n }\n\n /** Render remaining time as a human-readable string. */\n view(): string {\n return formatDuration(Math.max(0, this.timeout))\n }\n\n /** Command to start the timer. */\n start(): Cmd<TimerMsg> {\n return this.startStop(true)\n }\n\n /** Command to stop/pause the timer. */\n stop(): Cmd<TimerMsg> {\n return this.startStop(false)\n }\n\n /** Command to toggle running state. */\n toggle(): Cmd<TimerMsg> {\n return this.startStop(!this.running())\n }\n\n private tick(): Cmd<TimerMsg> {\n const id = this.#id\n const tag = this.#tag\n return tick(this.interval, () => new TickMsg(id, tag, this.timedOut()))\n }\n\n private startStop(running: boolean): Cmd<TimerMsg> {\n return lift(new StartStopMsg(this.#id, running))\n }\n}\n\n// Simple duration formatter (e.g., 1h2m3s)\nfunction formatDuration(ms: number): string {\n const seconds = Math.floor(ms / 1000) % 60\n const minutes = Math.floor(ms / 60000) % 60\n const hours = Math.floor(ms / 3600000)\n\n if (hours > 0) return `${hours}h${minutes}m${seconds}s`\n if (minutes > 0) return `${minutes}m${seconds}s`\n return `${seconds}s`\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@boba-cli/timer",
3
+ "description": "Countdown timer component for Boba terminal UIs",
4
+ "version": "0.1.0-alpha.1",
5
+ "dependencies": {
6
+ "@boba-cli/tea": "0.1.0-alpha.1"
7
+ },
8
+ "devDependencies": {
9
+ "typescript": "5.8.2",
10
+ "vitest": "^4.0.16"
11
+ },
12
+ "engines": {
13
+ "node": ">=20.0.0"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "import": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "require": {
22
+ "types": "./dist/index.d.cts",
23
+ "default": "./dist/index.cjs"
24
+ }
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "main": "./dist/index.cjs",
32
+ "module": "./dist/index.js",
33
+ "type": "module",
34
+ "types": "./dist/index.d.ts",
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "check:api-report": "pnpm run generate:api-report",
38
+ "check:eslint": "pnpm run lint",
39
+ "generate:api-report": "api-extractor run --local",
40
+ "lint": "eslint \"{src,test}/**/*.{ts,tsx}\"",
41
+ "test": "vitest run"
42
+ }
43
+ }