@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 +69 -0
- package/dist/index.cjs +154 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|