@boba-cli/stopwatch 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 +160 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +69 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +155 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @boba-cli/stopwatch
|
|
2
|
+
|
|
3
|
+
Stopwatch component for Boba terminal UIs. Port of Charmbracelet Bubbles stopwatch.
|
|
4
|
+
|
|
5
|
+
<img src="../../examples/stopwatch-demo.gif" width="950" alt="Stopwatch component demo" />
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @boba-cli/stopwatch
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { StopwatchModel, TickMsg } from '@boba-cli/stopwatch'
|
|
17
|
+
import type { Cmd, Msg, Model } from '@boba-cli/tea'
|
|
18
|
+
|
|
19
|
+
const stopwatch = StopwatchModel.new()
|
|
20
|
+
|
|
21
|
+
function init(): Cmd<Msg> {
|
|
22
|
+
return stopwatch.init()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function update(msg: Msg): [Model, Cmd<Msg>] {
|
|
26
|
+
if (msg instanceof TickMsg) {
|
|
27
|
+
const [next, cmd] = stopwatch.update(msg)
|
|
28
|
+
return [{ ...model, stopwatch: next }, cmd]
|
|
29
|
+
}
|
|
30
|
+
return [model, null]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function view(): string {
|
|
34
|
+
return `Elapsed ${stopwatch.view()}`
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
| Export | Description |
|
|
41
|
+
| ------------------ | ----------------------------------- |
|
|
42
|
+
| `StopwatchModel` | Stopwatch model |
|
|
43
|
+
| `StopwatchOptions` | Options for creating a stopwatch |
|
|
44
|
+
| `TickMsg` | Tick message carrying ID/tag |
|
|
45
|
+
| `StartStopMsg` | Message to start/stop the stopwatch |
|
|
46
|
+
| `ResetMsg` | Message to reset elapsed time |
|
|
47
|
+
|
|
48
|
+
### StopwatchModel methods
|
|
49
|
+
|
|
50
|
+
| Method | Description |
|
|
51
|
+
| --------------------------------- | --------------------------------------- |
|
|
52
|
+
| `id()` | Unique ID for message routing |
|
|
53
|
+
| `running()` | Whether the stopwatch is active |
|
|
54
|
+
| `elapsed()` | Milliseconds elapsed |
|
|
55
|
+
| `init()` | Start ticking on init |
|
|
56
|
+
| `update(msg)` | Handle messages, returns `[model, cmd]` |
|
|
57
|
+
| `view()` | Render elapsed time |
|
|
58
|
+
| `start()/stop()/toggle()/reset()` | Control commands |
|
|
59
|
+
|
|
60
|
+
## Scripts
|
|
61
|
+
|
|
62
|
+
- `pnpm -C packages/stopwatch build`
|
|
63
|
+
- `pnpm -C packages/stopwatch test`
|
|
64
|
+
- `pnpm -C packages/stopwatch lint`
|
|
65
|
+
- `pnpm -C packages/stopwatch generate:api-report`
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var tea = require('@boba-cli/tea');
|
|
4
|
+
|
|
5
|
+
// src/messages.ts
|
|
6
|
+
var TickMsg = class {
|
|
7
|
+
constructor(id, tag) {
|
|
8
|
+
this.id = id;
|
|
9
|
+
this.tag = tag;
|
|
10
|
+
}
|
|
11
|
+
_tag = "stopwatch-tick";
|
|
12
|
+
};
|
|
13
|
+
var StartStopMsg = class {
|
|
14
|
+
constructor(id, running) {
|
|
15
|
+
this.id = id;
|
|
16
|
+
this.running = running;
|
|
17
|
+
}
|
|
18
|
+
_tag = "stopwatch-start-stop";
|
|
19
|
+
};
|
|
20
|
+
var ResetMsg = class {
|
|
21
|
+
constructor(id) {
|
|
22
|
+
this.id = id;
|
|
23
|
+
}
|
|
24
|
+
_tag = "stopwatch-reset";
|
|
25
|
+
};
|
|
26
|
+
var lastId = 0;
|
|
27
|
+
function nextId() {
|
|
28
|
+
return ++lastId;
|
|
29
|
+
}
|
|
30
|
+
var StopwatchModel = class _StopwatchModel {
|
|
31
|
+
interval;
|
|
32
|
+
#elapsed;
|
|
33
|
+
#id;
|
|
34
|
+
#tag;
|
|
35
|
+
#running;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.interval = options.interval;
|
|
38
|
+
this.#elapsed = options.elapsed;
|
|
39
|
+
this.#running = options.running;
|
|
40
|
+
this.#id = options.id;
|
|
41
|
+
this.#tag = options.tag;
|
|
42
|
+
}
|
|
43
|
+
/** Create a new stopwatch. */
|
|
44
|
+
static new(options = {}) {
|
|
45
|
+
const interval = options.interval ?? 1e3;
|
|
46
|
+
return new _StopwatchModel({
|
|
47
|
+
elapsed: 0,
|
|
48
|
+
interval,
|
|
49
|
+
running: false,
|
|
50
|
+
id: nextId(),
|
|
51
|
+
tag: 0
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Create a new stopwatch with explicit interval. */
|
|
55
|
+
static withInterval(interval) {
|
|
56
|
+
return _StopwatchModel.new({ interval });
|
|
57
|
+
}
|
|
58
|
+
/** Unique ID for message routing. */
|
|
59
|
+
id() {
|
|
60
|
+
return this.#id;
|
|
61
|
+
}
|
|
62
|
+
/** Whether the stopwatch is running. */
|
|
63
|
+
running() {
|
|
64
|
+
return this.#running;
|
|
65
|
+
}
|
|
66
|
+
/** Milliseconds elapsed. */
|
|
67
|
+
elapsed() {
|
|
68
|
+
return this.#elapsed;
|
|
69
|
+
}
|
|
70
|
+
/** Start the stopwatch on init. */
|
|
71
|
+
init() {
|
|
72
|
+
return this.start();
|
|
73
|
+
}
|
|
74
|
+
/** Update the stopwatch in response to a message. */
|
|
75
|
+
update(msg) {
|
|
76
|
+
if (msg instanceof StartStopMsg) {
|
|
77
|
+
if (msg.id !== this.#id) {
|
|
78
|
+
return [this, null];
|
|
79
|
+
}
|
|
80
|
+
const next = new _StopwatchModel({
|
|
81
|
+
elapsed: this.#elapsed,
|
|
82
|
+
interval: this.interval,
|
|
83
|
+
running: msg.running,
|
|
84
|
+
id: this.#id,
|
|
85
|
+
tag: this.#tag
|
|
86
|
+
});
|
|
87
|
+
return [next, null];
|
|
88
|
+
}
|
|
89
|
+
if (msg instanceof ResetMsg) {
|
|
90
|
+
if (msg.id !== this.#id) {
|
|
91
|
+
return [this, null];
|
|
92
|
+
}
|
|
93
|
+
const next = new _StopwatchModel({
|
|
94
|
+
elapsed: 0,
|
|
95
|
+
interval: this.interval,
|
|
96
|
+
running: this.#running,
|
|
97
|
+
id: this.#id,
|
|
98
|
+
tag: this.#tag
|
|
99
|
+
});
|
|
100
|
+
return [next, null];
|
|
101
|
+
}
|
|
102
|
+
if (msg instanceof TickMsg) {
|
|
103
|
+
if (!this.running() || msg.id !== this.#id) {
|
|
104
|
+
return [this, null];
|
|
105
|
+
}
|
|
106
|
+
if (msg.tag !== this.#tag) {
|
|
107
|
+
return [this, null];
|
|
108
|
+
}
|
|
109
|
+
const next = new _StopwatchModel({
|
|
110
|
+
elapsed: this.#elapsed + this.interval,
|
|
111
|
+
interval: this.interval,
|
|
112
|
+
running: this.#running,
|
|
113
|
+
id: this.#id,
|
|
114
|
+
tag: this.#tag + 1
|
|
115
|
+
});
|
|
116
|
+
return [next, next.tick()];
|
|
117
|
+
}
|
|
118
|
+
return [this, null];
|
|
119
|
+
}
|
|
120
|
+
/** Render elapsed time as a human-readable string. */
|
|
121
|
+
view() {
|
|
122
|
+
return formatDuration(this.#elapsed);
|
|
123
|
+
}
|
|
124
|
+
/** Command to start the stopwatch. */
|
|
125
|
+
start() {
|
|
126
|
+
return tea.sequence(tea.msg(new StartStopMsg(this.#id, true)), this.tick());
|
|
127
|
+
}
|
|
128
|
+
/** Command to stop the stopwatch. */
|
|
129
|
+
stop() {
|
|
130
|
+
return tea.msg(new StartStopMsg(this.#id, false));
|
|
131
|
+
}
|
|
132
|
+
/** Command to toggle running state. */
|
|
133
|
+
toggle() {
|
|
134
|
+
return this.running() ? this.stop() : this.start();
|
|
135
|
+
}
|
|
136
|
+
/** Command to reset elapsed time. */
|
|
137
|
+
reset() {
|
|
138
|
+
return tea.msg(new ResetMsg(this.#id));
|
|
139
|
+
}
|
|
140
|
+
tick() {
|
|
141
|
+
const id = this.#id;
|
|
142
|
+
const tag = this.#tag;
|
|
143
|
+
return tea.tick(this.interval, () => new TickMsg(id, tag));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
function formatDuration(ms) {
|
|
147
|
+
const seconds = Math.floor(ms / 1e3) % 60;
|
|
148
|
+
const minutes = Math.floor(ms / 6e4) % 60;
|
|
149
|
+
const hours = Math.floor(ms / 36e5);
|
|
150
|
+
if (hours > 0) return `${hours}h${minutes}m${seconds}s`;
|
|
151
|
+
if (minutes > 0) return `${minutes}m${seconds}s`;
|
|
152
|
+
return `${seconds}s`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
exports.ResetMsg = ResetMsg;
|
|
156
|
+
exports.StartStopMsg = StartStopMsg;
|
|
157
|
+
exports.StopwatchModel = StopwatchModel;
|
|
158
|
+
exports.TickMsg = TickMsg;
|
|
159
|
+
//# sourceMappingURL=index.cjs.map
|
|
160
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/messages.ts","../src/model.ts"],"names":["sequence","lift","tick"],"mappings":"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","file":"index.cjs","sourcesContent":["/** Tick message for stopwatch increments. @public */\nexport class TickMsg {\n readonly _tag = 'stopwatch-tick'\n\n constructor(\n /** Unique stopwatch ID */\n public readonly id: number,\n /** Internal tag for deduplication */\n public readonly tag: number,\n ) {}\n}\n\n/** Message that starts or stops the stopwatch. @public */\nexport class StartStopMsg {\n readonly _tag = 'stopwatch-start-stop'\n\n constructor(\n public readonly id: number,\n public readonly running: boolean,\n ) {}\n}\n\n/** Message that resets the stopwatch. @public */\nexport class ResetMsg {\n readonly _tag = 'stopwatch-reset'\n\n constructor(public readonly id: number) {}\n}\n","import {\n msg as lift,\n sequence,\n tick,\n type Cmd,\n type Msg as TeaMsg,\n type Model as TeaModel,\n} from '@boba-cli/tea'\nimport { ResetMsg, StartStopMsg, TickMsg } from './messages.js'\n\n// Module-level ID counter for unique stopwatches\nlet lastId = 0\nfunction nextId(): number {\n return ++lastId\n}\n\n/** Options for creating a stopwatch. @public */\nexport interface StopwatchOptions {\n /** Tick interval in milliseconds (default: 1000). */\n interval?: number\n}\n\n/** Stopwatch messages. @public */\nexport type StopwatchMsg = TickMsg | StartStopMsg | ResetMsg\n\n/** Stopwatch model. @public */\nexport class StopwatchModel implements TeaModel<StopwatchMsg, StopwatchModel> {\n readonly interval: number\n readonly #elapsed: number\n readonly #id: number\n readonly #tag: number\n readonly #running: boolean\n\n private constructor(options: {\n elapsed: number\n interval: number\n running: boolean\n id: number\n tag: number\n }) {\n this.interval = options.interval\n this.#elapsed = options.elapsed\n this.#running = options.running\n this.#id = options.id\n this.#tag = options.tag\n }\n\n /** Create a new stopwatch. */\n static new(options: StopwatchOptions = {}): StopwatchModel {\n const interval = options.interval ?? 1000\n return new StopwatchModel({\n elapsed: 0,\n interval,\n running: false,\n id: nextId(),\n tag: 0,\n })\n }\n\n /** Create a new stopwatch with explicit interval. */\n static withInterval(interval: number): StopwatchModel {\n return StopwatchModel.new({ interval })\n }\n\n /** Unique ID for message routing. */\n id(): number {\n return this.#id\n }\n\n /** Whether the stopwatch is running. */\n running(): boolean {\n return this.#running\n }\n\n /** Milliseconds elapsed. */\n elapsed(): number {\n return this.#elapsed\n }\n\n /** Start the stopwatch on init. */\n init(): Cmd<StopwatchMsg> {\n return this.start()\n }\n\n /** Update the stopwatch in response to a message. */\n update(msg: TeaMsg): [StopwatchModel, Cmd<StopwatchMsg>] {\n if (msg instanceof StartStopMsg) {\n if (msg.id !== this.#id) {\n return [this, null]\n }\n const next = new StopwatchModel({\n elapsed: this.#elapsed,\n interval: this.interval,\n running: msg.running,\n id: this.#id,\n tag: this.#tag,\n })\n return [next, null]\n }\n\n if (msg instanceof ResetMsg) {\n if (msg.id !== this.#id) {\n return [this, null]\n }\n const next = new StopwatchModel({\n elapsed: 0,\n interval: this.interval,\n running: this.#running,\n id: this.#id,\n tag: this.#tag,\n })\n return [next, null]\n }\n\n if (msg instanceof TickMsg) {\n if (!this.running() || msg.id !== this.#id) {\n return [this, null]\n }\n\n if (msg.tag !== this.#tag) {\n return [this, null]\n }\n\n const next = new StopwatchModel({\n elapsed: this.#elapsed + this.interval,\n interval: this.interval,\n running: this.#running,\n id: this.#id,\n tag: this.#tag + 1,\n })\n\n return [next, next.tick()]\n }\n\n return [this, null]\n }\n\n /** Render elapsed time as a human-readable string. */\n view(): string {\n return formatDuration(this.#elapsed)\n }\n\n /** Command to start the stopwatch. */\n start(): Cmd<StopwatchMsg> {\n return sequence(lift(new StartStopMsg(this.#id, true)), this.tick())\n }\n\n /** Command to stop the stopwatch. */\n stop(): Cmd<StopwatchMsg> {\n return lift(new StartStopMsg(this.#id, false))\n }\n\n /** Command to toggle running state. */\n toggle(): Cmd<StopwatchMsg> {\n return this.running() ? this.stop() : this.start()\n }\n\n /** Command to reset elapsed time. */\n reset(): Cmd<StopwatchMsg> {\n return lift(new ResetMsg(this.#id))\n }\n\n private tick(): Cmd<StopwatchMsg> {\n const id = this.#id\n const tag = this.#tag\n return tick(this.interval, () => new TickMsg(id, tag))\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,69 @@
|
|
|
1
|
+
import { Model, Cmd, Msg } from '@boba-cli/tea';
|
|
2
|
+
|
|
3
|
+
/** Tick message for stopwatch increments. @public */
|
|
4
|
+
declare class TickMsg {
|
|
5
|
+
/** Unique stopwatch ID */
|
|
6
|
+
readonly id: number;
|
|
7
|
+
/** Internal tag for deduplication */
|
|
8
|
+
readonly tag: number;
|
|
9
|
+
readonly _tag = "stopwatch-tick";
|
|
10
|
+
constructor(
|
|
11
|
+
/** Unique stopwatch ID */
|
|
12
|
+
id: number,
|
|
13
|
+
/** Internal tag for deduplication */
|
|
14
|
+
tag: number);
|
|
15
|
+
}
|
|
16
|
+
/** Message that starts or stops the stopwatch. @public */
|
|
17
|
+
declare class StartStopMsg {
|
|
18
|
+
readonly id: number;
|
|
19
|
+
readonly running: boolean;
|
|
20
|
+
readonly _tag = "stopwatch-start-stop";
|
|
21
|
+
constructor(id: number, running: boolean);
|
|
22
|
+
}
|
|
23
|
+
/** Message that resets the stopwatch. @public */
|
|
24
|
+
declare class ResetMsg {
|
|
25
|
+
readonly id: number;
|
|
26
|
+
readonly _tag = "stopwatch-reset";
|
|
27
|
+
constructor(id: number);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Options for creating a stopwatch. @public */
|
|
31
|
+
interface StopwatchOptions {
|
|
32
|
+
/** Tick interval in milliseconds (default: 1000). */
|
|
33
|
+
interval?: number;
|
|
34
|
+
}
|
|
35
|
+
/** Stopwatch messages. @public */
|
|
36
|
+
type StopwatchMsg = TickMsg | StartStopMsg | ResetMsg;
|
|
37
|
+
/** Stopwatch model. @public */
|
|
38
|
+
declare class StopwatchModel implements Model<StopwatchMsg, StopwatchModel> {
|
|
39
|
+
#private;
|
|
40
|
+
readonly interval: number;
|
|
41
|
+
private constructor();
|
|
42
|
+
/** Create a new stopwatch. */
|
|
43
|
+
static new(options?: StopwatchOptions): StopwatchModel;
|
|
44
|
+
/** Create a new stopwatch with explicit interval. */
|
|
45
|
+
static withInterval(interval: number): StopwatchModel;
|
|
46
|
+
/** Unique ID for message routing. */
|
|
47
|
+
id(): number;
|
|
48
|
+
/** Whether the stopwatch is running. */
|
|
49
|
+
running(): boolean;
|
|
50
|
+
/** Milliseconds elapsed. */
|
|
51
|
+
elapsed(): number;
|
|
52
|
+
/** Start the stopwatch on init. */
|
|
53
|
+
init(): Cmd<StopwatchMsg>;
|
|
54
|
+
/** Update the stopwatch in response to a message. */
|
|
55
|
+
update(msg: Msg): [StopwatchModel, Cmd<StopwatchMsg>];
|
|
56
|
+
/** Render elapsed time as a human-readable string. */
|
|
57
|
+
view(): string;
|
|
58
|
+
/** Command to start the stopwatch. */
|
|
59
|
+
start(): Cmd<StopwatchMsg>;
|
|
60
|
+
/** Command to stop the stopwatch. */
|
|
61
|
+
stop(): Cmd<StopwatchMsg>;
|
|
62
|
+
/** Command to toggle running state. */
|
|
63
|
+
toggle(): Cmd<StopwatchMsg>;
|
|
64
|
+
/** Command to reset elapsed time. */
|
|
65
|
+
reset(): Cmd<StopwatchMsg>;
|
|
66
|
+
private tick;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { ResetMsg, StartStopMsg, StopwatchModel, type StopwatchMsg, type StopwatchOptions, TickMsg };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Model, Cmd, Msg } from '@boba-cli/tea';
|
|
2
|
+
|
|
3
|
+
/** Tick message for stopwatch increments. @public */
|
|
4
|
+
declare class TickMsg {
|
|
5
|
+
/** Unique stopwatch ID */
|
|
6
|
+
readonly id: number;
|
|
7
|
+
/** Internal tag for deduplication */
|
|
8
|
+
readonly tag: number;
|
|
9
|
+
readonly _tag = "stopwatch-tick";
|
|
10
|
+
constructor(
|
|
11
|
+
/** Unique stopwatch ID */
|
|
12
|
+
id: number,
|
|
13
|
+
/** Internal tag for deduplication */
|
|
14
|
+
tag: number);
|
|
15
|
+
}
|
|
16
|
+
/** Message that starts or stops the stopwatch. @public */
|
|
17
|
+
declare class StartStopMsg {
|
|
18
|
+
readonly id: number;
|
|
19
|
+
readonly running: boolean;
|
|
20
|
+
readonly _tag = "stopwatch-start-stop";
|
|
21
|
+
constructor(id: number, running: boolean);
|
|
22
|
+
}
|
|
23
|
+
/** Message that resets the stopwatch. @public */
|
|
24
|
+
declare class ResetMsg {
|
|
25
|
+
readonly id: number;
|
|
26
|
+
readonly _tag = "stopwatch-reset";
|
|
27
|
+
constructor(id: number);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Options for creating a stopwatch. @public */
|
|
31
|
+
interface StopwatchOptions {
|
|
32
|
+
/** Tick interval in milliseconds (default: 1000). */
|
|
33
|
+
interval?: number;
|
|
34
|
+
}
|
|
35
|
+
/** Stopwatch messages. @public */
|
|
36
|
+
type StopwatchMsg = TickMsg | StartStopMsg | ResetMsg;
|
|
37
|
+
/** Stopwatch model. @public */
|
|
38
|
+
declare class StopwatchModel implements Model<StopwatchMsg, StopwatchModel> {
|
|
39
|
+
#private;
|
|
40
|
+
readonly interval: number;
|
|
41
|
+
private constructor();
|
|
42
|
+
/** Create a new stopwatch. */
|
|
43
|
+
static new(options?: StopwatchOptions): StopwatchModel;
|
|
44
|
+
/** Create a new stopwatch with explicit interval. */
|
|
45
|
+
static withInterval(interval: number): StopwatchModel;
|
|
46
|
+
/** Unique ID for message routing. */
|
|
47
|
+
id(): number;
|
|
48
|
+
/** Whether the stopwatch is running. */
|
|
49
|
+
running(): boolean;
|
|
50
|
+
/** Milliseconds elapsed. */
|
|
51
|
+
elapsed(): number;
|
|
52
|
+
/** Start the stopwatch on init. */
|
|
53
|
+
init(): Cmd<StopwatchMsg>;
|
|
54
|
+
/** Update the stopwatch in response to a message. */
|
|
55
|
+
update(msg: Msg): [StopwatchModel, Cmd<StopwatchMsg>];
|
|
56
|
+
/** Render elapsed time as a human-readable string. */
|
|
57
|
+
view(): string;
|
|
58
|
+
/** Command to start the stopwatch. */
|
|
59
|
+
start(): Cmd<StopwatchMsg>;
|
|
60
|
+
/** Command to stop the stopwatch. */
|
|
61
|
+
stop(): Cmd<StopwatchMsg>;
|
|
62
|
+
/** Command to toggle running state. */
|
|
63
|
+
toggle(): Cmd<StopwatchMsg>;
|
|
64
|
+
/** Command to reset elapsed time. */
|
|
65
|
+
reset(): Cmd<StopwatchMsg>;
|
|
66
|
+
private tick;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { ResetMsg, StartStopMsg, StopwatchModel, type StopwatchMsg, type StopwatchOptions, TickMsg };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { sequence, msg, tick } from '@boba-cli/tea';
|
|
2
|
+
|
|
3
|
+
// src/messages.ts
|
|
4
|
+
var TickMsg = class {
|
|
5
|
+
constructor(id, tag) {
|
|
6
|
+
this.id = id;
|
|
7
|
+
this.tag = tag;
|
|
8
|
+
}
|
|
9
|
+
_tag = "stopwatch-tick";
|
|
10
|
+
};
|
|
11
|
+
var StartStopMsg = class {
|
|
12
|
+
constructor(id, running) {
|
|
13
|
+
this.id = id;
|
|
14
|
+
this.running = running;
|
|
15
|
+
}
|
|
16
|
+
_tag = "stopwatch-start-stop";
|
|
17
|
+
};
|
|
18
|
+
var ResetMsg = class {
|
|
19
|
+
constructor(id) {
|
|
20
|
+
this.id = id;
|
|
21
|
+
}
|
|
22
|
+
_tag = "stopwatch-reset";
|
|
23
|
+
};
|
|
24
|
+
var lastId = 0;
|
|
25
|
+
function nextId() {
|
|
26
|
+
return ++lastId;
|
|
27
|
+
}
|
|
28
|
+
var StopwatchModel = class _StopwatchModel {
|
|
29
|
+
interval;
|
|
30
|
+
#elapsed;
|
|
31
|
+
#id;
|
|
32
|
+
#tag;
|
|
33
|
+
#running;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.interval = options.interval;
|
|
36
|
+
this.#elapsed = options.elapsed;
|
|
37
|
+
this.#running = options.running;
|
|
38
|
+
this.#id = options.id;
|
|
39
|
+
this.#tag = options.tag;
|
|
40
|
+
}
|
|
41
|
+
/** Create a new stopwatch. */
|
|
42
|
+
static new(options = {}) {
|
|
43
|
+
const interval = options.interval ?? 1e3;
|
|
44
|
+
return new _StopwatchModel({
|
|
45
|
+
elapsed: 0,
|
|
46
|
+
interval,
|
|
47
|
+
running: false,
|
|
48
|
+
id: nextId(),
|
|
49
|
+
tag: 0
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Create a new stopwatch with explicit interval. */
|
|
53
|
+
static withInterval(interval) {
|
|
54
|
+
return _StopwatchModel.new({ interval });
|
|
55
|
+
}
|
|
56
|
+
/** Unique ID for message routing. */
|
|
57
|
+
id() {
|
|
58
|
+
return this.#id;
|
|
59
|
+
}
|
|
60
|
+
/** Whether the stopwatch is running. */
|
|
61
|
+
running() {
|
|
62
|
+
return this.#running;
|
|
63
|
+
}
|
|
64
|
+
/** Milliseconds elapsed. */
|
|
65
|
+
elapsed() {
|
|
66
|
+
return this.#elapsed;
|
|
67
|
+
}
|
|
68
|
+
/** Start the stopwatch on init. */
|
|
69
|
+
init() {
|
|
70
|
+
return this.start();
|
|
71
|
+
}
|
|
72
|
+
/** Update the stopwatch in response to a message. */
|
|
73
|
+
update(msg) {
|
|
74
|
+
if (msg instanceof StartStopMsg) {
|
|
75
|
+
if (msg.id !== this.#id) {
|
|
76
|
+
return [this, null];
|
|
77
|
+
}
|
|
78
|
+
const next = new _StopwatchModel({
|
|
79
|
+
elapsed: this.#elapsed,
|
|
80
|
+
interval: this.interval,
|
|
81
|
+
running: msg.running,
|
|
82
|
+
id: this.#id,
|
|
83
|
+
tag: this.#tag
|
|
84
|
+
});
|
|
85
|
+
return [next, null];
|
|
86
|
+
}
|
|
87
|
+
if (msg instanceof ResetMsg) {
|
|
88
|
+
if (msg.id !== this.#id) {
|
|
89
|
+
return [this, null];
|
|
90
|
+
}
|
|
91
|
+
const next = new _StopwatchModel({
|
|
92
|
+
elapsed: 0,
|
|
93
|
+
interval: this.interval,
|
|
94
|
+
running: this.#running,
|
|
95
|
+
id: this.#id,
|
|
96
|
+
tag: this.#tag
|
|
97
|
+
});
|
|
98
|
+
return [next, null];
|
|
99
|
+
}
|
|
100
|
+
if (msg instanceof TickMsg) {
|
|
101
|
+
if (!this.running() || msg.id !== this.#id) {
|
|
102
|
+
return [this, null];
|
|
103
|
+
}
|
|
104
|
+
if (msg.tag !== this.#tag) {
|
|
105
|
+
return [this, null];
|
|
106
|
+
}
|
|
107
|
+
const next = new _StopwatchModel({
|
|
108
|
+
elapsed: this.#elapsed + this.interval,
|
|
109
|
+
interval: this.interval,
|
|
110
|
+
running: this.#running,
|
|
111
|
+
id: this.#id,
|
|
112
|
+
tag: this.#tag + 1
|
|
113
|
+
});
|
|
114
|
+
return [next, next.tick()];
|
|
115
|
+
}
|
|
116
|
+
return [this, null];
|
|
117
|
+
}
|
|
118
|
+
/** Render elapsed time as a human-readable string. */
|
|
119
|
+
view() {
|
|
120
|
+
return formatDuration(this.#elapsed);
|
|
121
|
+
}
|
|
122
|
+
/** Command to start the stopwatch. */
|
|
123
|
+
start() {
|
|
124
|
+
return sequence(msg(new StartStopMsg(this.#id, true)), this.tick());
|
|
125
|
+
}
|
|
126
|
+
/** Command to stop the stopwatch. */
|
|
127
|
+
stop() {
|
|
128
|
+
return msg(new StartStopMsg(this.#id, false));
|
|
129
|
+
}
|
|
130
|
+
/** Command to toggle running state. */
|
|
131
|
+
toggle() {
|
|
132
|
+
return this.running() ? this.stop() : this.start();
|
|
133
|
+
}
|
|
134
|
+
/** Command to reset elapsed time. */
|
|
135
|
+
reset() {
|
|
136
|
+
return msg(new ResetMsg(this.#id));
|
|
137
|
+
}
|
|
138
|
+
tick() {
|
|
139
|
+
const id = this.#id;
|
|
140
|
+
const tag = this.#tag;
|
|
141
|
+
return tick(this.interval, () => new TickMsg(id, tag));
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
function formatDuration(ms) {
|
|
145
|
+
const seconds = Math.floor(ms / 1e3) % 60;
|
|
146
|
+
const minutes = Math.floor(ms / 6e4) % 60;
|
|
147
|
+
const hours = Math.floor(ms / 36e5);
|
|
148
|
+
if (hours > 0) return `${hours}h${minutes}m${seconds}s`;
|
|
149
|
+
if (minutes > 0) return `${minutes}m${seconds}s`;
|
|
150
|
+
return `${seconds}s`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { ResetMsg, StartStopMsg, StopwatchModel, TickMsg };
|
|
154
|
+
//# sourceMappingURL=index.js.map
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/messages.ts","../src/model.ts"],"names":["lift"],"mappings":"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","file":"index.js","sourcesContent":["/** Tick message for stopwatch increments. @public */\nexport class TickMsg {\n readonly _tag = 'stopwatch-tick'\n\n constructor(\n /** Unique stopwatch ID */\n public readonly id: number,\n /** Internal tag for deduplication */\n public readonly tag: number,\n ) {}\n}\n\n/** Message that starts or stops the stopwatch. @public */\nexport class StartStopMsg {\n readonly _tag = 'stopwatch-start-stop'\n\n constructor(\n public readonly id: number,\n public readonly running: boolean,\n ) {}\n}\n\n/** Message that resets the stopwatch. @public */\nexport class ResetMsg {\n readonly _tag = 'stopwatch-reset'\n\n constructor(public readonly id: number) {}\n}\n","import {\n msg as lift,\n sequence,\n tick,\n type Cmd,\n type Msg as TeaMsg,\n type Model as TeaModel,\n} from '@boba-cli/tea'\nimport { ResetMsg, StartStopMsg, TickMsg } from './messages.js'\n\n// Module-level ID counter for unique stopwatches\nlet lastId = 0\nfunction nextId(): number {\n return ++lastId\n}\n\n/** Options for creating a stopwatch. @public */\nexport interface StopwatchOptions {\n /** Tick interval in milliseconds (default: 1000). */\n interval?: number\n}\n\n/** Stopwatch messages. @public */\nexport type StopwatchMsg = TickMsg | StartStopMsg | ResetMsg\n\n/** Stopwatch model. @public */\nexport class StopwatchModel implements TeaModel<StopwatchMsg, StopwatchModel> {\n readonly interval: number\n readonly #elapsed: number\n readonly #id: number\n readonly #tag: number\n readonly #running: boolean\n\n private constructor(options: {\n elapsed: number\n interval: number\n running: boolean\n id: number\n tag: number\n }) {\n this.interval = options.interval\n this.#elapsed = options.elapsed\n this.#running = options.running\n this.#id = options.id\n this.#tag = options.tag\n }\n\n /** Create a new stopwatch. */\n static new(options: StopwatchOptions = {}): StopwatchModel {\n const interval = options.interval ?? 1000\n return new StopwatchModel({\n elapsed: 0,\n interval,\n running: false,\n id: nextId(),\n tag: 0,\n })\n }\n\n /** Create a new stopwatch with explicit interval. */\n static withInterval(interval: number): StopwatchModel {\n return StopwatchModel.new({ interval })\n }\n\n /** Unique ID for message routing. */\n id(): number {\n return this.#id\n }\n\n /** Whether the stopwatch is running. */\n running(): boolean {\n return this.#running\n }\n\n /** Milliseconds elapsed. */\n elapsed(): number {\n return this.#elapsed\n }\n\n /** Start the stopwatch on init. */\n init(): Cmd<StopwatchMsg> {\n return this.start()\n }\n\n /** Update the stopwatch in response to a message. */\n update(msg: TeaMsg): [StopwatchModel, Cmd<StopwatchMsg>] {\n if (msg instanceof StartStopMsg) {\n if (msg.id !== this.#id) {\n return [this, null]\n }\n const next = new StopwatchModel({\n elapsed: this.#elapsed,\n interval: this.interval,\n running: msg.running,\n id: this.#id,\n tag: this.#tag,\n })\n return [next, null]\n }\n\n if (msg instanceof ResetMsg) {\n if (msg.id !== this.#id) {\n return [this, null]\n }\n const next = new StopwatchModel({\n elapsed: 0,\n interval: this.interval,\n running: this.#running,\n id: this.#id,\n tag: this.#tag,\n })\n return [next, null]\n }\n\n if (msg instanceof TickMsg) {\n if (!this.running() || msg.id !== this.#id) {\n return [this, null]\n }\n\n if (msg.tag !== this.#tag) {\n return [this, null]\n }\n\n const next = new StopwatchModel({\n elapsed: this.#elapsed + this.interval,\n interval: this.interval,\n running: this.#running,\n id: this.#id,\n tag: this.#tag + 1,\n })\n\n return [next, next.tick()]\n }\n\n return [this, null]\n }\n\n /** Render elapsed time as a human-readable string. */\n view(): string {\n return formatDuration(this.#elapsed)\n }\n\n /** Command to start the stopwatch. */\n start(): Cmd<StopwatchMsg> {\n return sequence(lift(new StartStopMsg(this.#id, true)), this.tick())\n }\n\n /** Command to stop the stopwatch. */\n stop(): Cmd<StopwatchMsg> {\n return lift(new StartStopMsg(this.#id, false))\n }\n\n /** Command to toggle running state. */\n toggle(): Cmd<StopwatchMsg> {\n return this.running() ? this.stop() : this.start()\n }\n\n /** Command to reset elapsed time. */\n reset(): Cmd<StopwatchMsg> {\n return lift(new ResetMsg(this.#id))\n }\n\n private tick(): Cmd<StopwatchMsg> {\n const id = this.#id\n const tag = this.#tag\n return tick(this.interval, () => new TickMsg(id, tag))\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/stopwatch",
|
|
3
|
+
"description": "Stopwatch 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
|
+
}
|