@boba-cli/cursor 0.1.0-alpha.2

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,83 @@
1
+ # @boba-cli/cursor
2
+
3
+ Cursor component for Boba terminal UIs. Handles blinking, focus, and hidden/static modes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @boba-cli/cursor
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```ts
14
+ import {
15
+ CursorModel,
16
+ CursorMode,
17
+ BlinkMsg,
18
+ InitialBlinkMsg,
19
+ } from '@boba-cli/cursor'
20
+ import type { Cmd, Msg } from '@boba-cli/tea'
21
+
22
+ // Create a cursor (defaults to blink mode)
23
+ let cursor = new CursorModel()
24
+
25
+ // In init, start blinking
26
+ function init(): Cmd<Msg> {
27
+ return cursor.initBlink()
28
+ }
29
+
30
+ // In update, handle focus/blur and blink messages
31
+ function update(msg: Msg): [MyModel, Cmd<Msg>] {
32
+ const [nextCursor, cmd] = cursor.update(msg)
33
+ cursor = nextCursor
34
+ return [model, cmd]
35
+ }
36
+
37
+ // In view, render the cursor at your desired position/character
38
+ function view(): string {
39
+ return cursor.view()
40
+ }
41
+ ```
42
+
43
+ ## Modes
44
+
45
+ | Mode | Behavior |
46
+ | ------------------- | ----------------------------------------------- |
47
+ | `CursorMode.Blink` | Toggles between block and text state on a timer |
48
+ | `CursorMode.Static` | Always shows the block |
49
+ | `CursorMode.Hidden` | Always shows text (cursor hidden) |
50
+
51
+ ## API
52
+
53
+ | Export | Description |
54
+ | ----------------- | ----------------------------------- |
55
+ | `CursorModel` | Main component model |
56
+ | `CursorMode` | Enum of `Blink`, `Static`, `Hidden` |
57
+ | `BlinkMsg` | Blink toggle message |
58
+ | `InitialBlinkMsg` | Kickoff message for blinking |
59
+
60
+ ### CursorModel helpers
61
+
62
+ | Method | Description |
63
+ | -------------------- | ---------------------------------------------- |
64
+ | `id()` | Unique ID for routing |
65
+ | `mode()` | Current mode |
66
+ | `initBlink()` | Command to send an initial blink message |
67
+ | `tickBlink()` | Command to schedule the next blink |
68
+ | `withMode(mode)` | Change mode (returns new model + optional cmd) |
69
+ | `withChar(char)` | Change the underlying character |
70
+ | `focus()` / `blur()` | Focus management (returns new model) |
71
+ | `update(msg)` | Handle messages; returns `[model, cmd]` |
72
+ | `view()` | Render the cursor |
73
+
74
+ ## Scripts
75
+
76
+ - `pnpm -C packages/cursor build`
77
+ - `pnpm -C packages/cursor test`
78
+ - `pnpm -C packages/cursor lint`
79
+ - `pnpm -C packages/cursor generate:api-report`
80
+
81
+ ## License
82
+
83
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,184 @@
1
+ 'use strict';
2
+
3
+ var tea = require('@boba-cli/tea');
4
+ var chapstick = require('@boba-cli/chapstick');
5
+
6
+ // src/model.ts
7
+
8
+ // src/messages.ts
9
+ var InitialBlinkMsg = class {
10
+ _tag = "cursor:init-blink";
11
+ };
12
+ var BlinkMsg = class {
13
+ constructor(id, tag, time) {
14
+ this.id = id;
15
+ this.tag = tag;
16
+ this.time = time;
17
+ }
18
+ _tag = "cursor:blink";
19
+ };
20
+
21
+ // src/model.ts
22
+ var defaultBlinkSpeed = 530;
23
+ var lastId = 0;
24
+ function nextId() {
25
+ return ++lastId;
26
+ }
27
+ var CursorMode = /* @__PURE__ */ ((CursorMode2) => {
28
+ CursorMode2["Blink"] = "blink";
29
+ CursorMode2["Static"] = "static";
30
+ CursorMode2["Hidden"] = "hidden";
31
+ return CursorMode2;
32
+ })(CursorMode || {});
33
+ var CursorModel = class _CursorModel {
34
+ blinkSpeed;
35
+ style;
36
+ textStyle;
37
+ char;
38
+ #id;
39
+ #tag;
40
+ #blink;
41
+ #focus;
42
+ #mode;
43
+ constructor(options = {}, state) {
44
+ this.blinkSpeed = options.blinkSpeed ?? defaultBlinkSpeed;
45
+ this.style = options.style ?? new chapstick.Style();
46
+ this.textStyle = options.textStyle ?? new chapstick.Style();
47
+ this.char = options.char ?? " ";
48
+ this.#id = state?.id ?? nextId();
49
+ this.#tag = state?.tag ?? 0;
50
+ this.#blink = state?.blink ?? true;
51
+ this.#focus = state?.focus ?? false;
52
+ this.#mode = state?.mode ?? options.mode ?? "blink" /* Blink */;
53
+ }
54
+ /** Unique ID for this cursor (for message routing). */
55
+ id() {
56
+ return this.#id;
57
+ }
58
+ /** Current cursor mode. */
59
+ mode() {
60
+ return this.#mode;
61
+ }
62
+ /** Whether the cursor is currently in the "text" state (hidden block). */
63
+ isBlinkHidden() {
64
+ return this.#blink;
65
+ }
66
+ /** Whether this cursor currently has focus. */
67
+ isFocused() {
68
+ return this.#focus;
69
+ }
70
+ /** Kick off blinking at init (emits InitialBlinkMsg). */
71
+ initBlink() {
72
+ return () => new InitialBlinkMsg();
73
+ }
74
+ /** Command to schedule the next blink toggle. */
75
+ tickBlink() {
76
+ const nextTag = this.#tag + 1;
77
+ const id = this.#id;
78
+ const speed = this.blinkSpeed;
79
+ const next = this.#withState({ ...this.#state(), tag: nextTag });
80
+ const cmd = tea.tick(
81
+ speed,
82
+ (time) => new BlinkMsg(id, nextTag, time)
83
+ );
84
+ return [next, cmd];
85
+ }
86
+ /** Set the character under the cursor. */
87
+ withChar(char) {
88
+ return this.#withState(this.#state(), { char });
89
+ }
90
+ /** Set the cursor mode. Returns a new model and optional blink command. */
91
+ withMode(mode) {
92
+ const bounded = mode === "blink" /* Blink */ || mode === "static" /* Static */ || mode === "hidden" /* Hidden */ ? mode : "blink" /* Blink */;
93
+ const blink = bounded === "hidden" /* Hidden */ || !this.#focus ? true : this.#blink;
94
+ const next = this.#withState({ ...this.#state(), blink, mode: bounded });
95
+ if (bounded === "blink" /* Blink */ && this.#focus) {
96
+ const [scheduled, cmd] = next.tickBlink();
97
+ return [scheduled, cmd];
98
+ }
99
+ return [next, null];
100
+ }
101
+ /** Focus the cursor. Returns new model and optional blink command. */
102
+ focus() {
103
+ const blink = this.#mode === "hidden" /* Hidden */ ? true : this.#blink;
104
+ const next = this.#withState({ ...this.#state(), blink, focus: true });
105
+ if (this.#mode === "blink" /* Blink */) {
106
+ const [scheduled, cmd] = next.tickBlink();
107
+ return [scheduled, cmd];
108
+ }
109
+ return [next, null];
110
+ }
111
+ /** Blur the cursor. */
112
+ blur() {
113
+ return this.#withState({ ...this.#state(), blink: true, focus: false });
114
+ }
115
+ /**
116
+ * Update the cursor model with an incoming message.
117
+ * Returns a new model and an optional command.
118
+ */
119
+ update(msg) {
120
+ if (msg instanceof InitialBlinkMsg) {
121
+ if (this.#mode !== "blink" /* Blink */ || !this.#focus) {
122
+ return [this, null];
123
+ }
124
+ const [next, cmd] = this.tickBlink();
125
+ return [next, cmd];
126
+ }
127
+ if (msg instanceof tea.FocusMsg) {
128
+ return this.focus();
129
+ }
130
+ if (msg instanceof tea.BlurMsg) {
131
+ const next = this.blur();
132
+ return [next, null];
133
+ }
134
+ if (msg instanceof BlinkMsg) {
135
+ if (this.#mode !== "blink" /* Blink */ || !this.#focus) {
136
+ return [this, null];
137
+ }
138
+ if (msg.id !== this.#id || msg.tag !== this.#tag) {
139
+ return [this, null];
140
+ }
141
+ const toggled = this.#withState({ ...this.#state(), blink: !this.#blink });
142
+ const [next, cmd] = toggled.tickBlink();
143
+ return [next, cmd];
144
+ }
145
+ return [this, null];
146
+ }
147
+ /** Render the cursor. */
148
+ view() {
149
+ const char = this.char;
150
+ if (this.#blink) {
151
+ return this.textStyle.inline(true).render(char);
152
+ }
153
+ return this.style.inline(true).render(char);
154
+ }
155
+ #state() {
156
+ return {
157
+ id: this.#id,
158
+ tag: this.#tag,
159
+ blink: this.#blink,
160
+ focus: this.#focus,
161
+ mode: this.#mode
162
+ };
163
+ }
164
+ #withState(state, overrides) {
165
+ return new _CursorModel(
166
+ {
167
+ blinkSpeed: this.blinkSpeed,
168
+ style: this.style,
169
+ textStyle: this.textStyle,
170
+ char: overrides?.char ?? this.char,
171
+ mode: state.mode,
172
+ focused: state.focus
173
+ },
174
+ state
175
+ );
176
+ }
177
+ };
178
+
179
+ exports.BlinkMsg = BlinkMsg;
180
+ exports.CursorMode = CursorMode;
181
+ exports.CursorModel = CursorModel;
182
+ exports.InitialBlinkMsg = InitialBlinkMsg;
183
+ //# sourceMappingURL=index.cjs.map
184
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/messages.ts","../src/model.ts"],"names":["CursorMode","Style","tick","FocusMsg","BlurMsg"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @public\n */\nexport class InitialBlinkMsg {\n readonly _tag = 'cursor:init-blink'\n}\n\n/**\n * @public\n */\nexport class BlinkMsg {\n readonly _tag = 'cursor:blink'\n constructor(\n public readonly id: number,\n public readonly tag: number,\n public readonly time: Date,\n ) {}\n}\n","import { tick, type Cmd, type Msg, FocusMsg, BlurMsg } from '@boba-cli/tea'\nimport { Style } from '@boba-cli/chapstick'\nimport { BlinkMsg, InitialBlinkMsg } from './messages.js'\n\nconst defaultBlinkSpeed = 530 // ms\n\n// Module-level ID counter for unique cursor identification\nlet lastId = 0\nfunction nextId(): number {\n return ++lastId\n}\n\n/** Available cursor behaviors. @public */\nexport enum CursorMode {\n Blink = 'blink',\n Static = 'static',\n Hidden = 'hidden',\n}\n\n/** Options for creating a CursorModel. @public */\nexport interface CursorOptions {\n blinkSpeed?: number\n style?: Style\n textStyle?: Style\n char?: string\n mode?: CursorMode\n focused?: boolean\n}\n\ntype CursorState = {\n id: number\n tag: number\n blink: boolean\n focus: boolean\n mode: CursorMode\n}\n\n/**\n * Cursor component model.\n *\n * Use `tickBlink()` to start blinking, or handle `InitialBlinkMsg` from `initBlink()`.\n * Handle `BlinkMsg`, `FocusMsg`, and `BlurMsg` in your update loop.\n *\n * @public\n */\nexport class CursorModel {\n readonly blinkSpeed: number\n readonly style: Style\n readonly textStyle: Style\n readonly char: string\n readonly #id: number\n readonly #tag: number\n readonly #blink: boolean\n readonly #focus: boolean\n readonly #mode: CursorMode\n\n constructor(options: CursorOptions = {}, state?: CursorState) {\n this.blinkSpeed = options.blinkSpeed ?? defaultBlinkSpeed\n this.style = options.style ?? new Style()\n this.textStyle = options.textStyle ?? new Style()\n this.char = options.char ?? ' '\n this.#id = state?.id ?? nextId()\n this.#tag = state?.tag ?? 0\n this.#blink = state?.blink ?? true\n this.#focus = state?.focus ?? false\n this.#mode = state?.mode ?? options.mode ?? CursorMode.Blink\n }\n\n /** Unique ID for this cursor (for message routing). */\n id(): number {\n return this.#id\n }\n\n /** Current cursor mode. */\n mode(): CursorMode {\n return this.#mode\n }\n\n /** Whether the cursor is currently in the \"text\" state (hidden block). */\n isBlinkHidden(): boolean {\n return this.#blink\n }\n\n /** Whether this cursor currently has focus. */\n isFocused(): boolean {\n return this.#focus\n }\n\n /** Kick off blinking at init (emits InitialBlinkMsg). */\n initBlink(): Cmd<InitialBlinkMsg> {\n return () => new InitialBlinkMsg()\n }\n\n /** Command to schedule the next blink toggle. */\n tickBlink(): [CursorModel, Cmd<BlinkMsg>] {\n const nextTag = this.#tag + 1\n const id = this.#id\n const speed = this.blinkSpeed\n const next = this.#withState({ ...this.#state(), tag: nextTag })\n const cmd: Cmd<BlinkMsg> = tick(\n speed,\n (time: Date) => new BlinkMsg(id, nextTag, time),\n )\n return [next, cmd]\n }\n\n /** Set the character under the cursor. */\n withChar(char: string): CursorModel {\n return this.#withState(this.#state(), { char })\n }\n\n /** Set the cursor mode. Returns a new model and optional blink command. */\n withMode(mode: CursorMode): [CursorModel, Cmd<Msg>] {\n const bounded =\n mode === CursorMode.Blink ||\n mode === CursorMode.Static ||\n mode === CursorMode.Hidden\n ? mode\n : CursorMode.Blink\n\n const blink =\n bounded === CursorMode.Hidden || !this.#focus ? true : this.#blink\n const next = this.#withState({ ...this.#state(), blink, mode: bounded })\n\n if (bounded === CursorMode.Blink && this.#focus) {\n const [scheduled, cmd] = next.tickBlink()\n return [scheduled, cmd]\n }\n return [next, null]\n }\n\n /** Focus the cursor. Returns new model and optional blink command. */\n focus(): [CursorModel, Cmd<Msg>] {\n const blink = this.#mode === CursorMode.Hidden ? true : this.#blink\n const next = this.#withState({ ...this.#state(), blink, focus: true })\n if (this.#mode === CursorMode.Blink) {\n const [scheduled, cmd] = next.tickBlink()\n return [scheduled, cmd]\n }\n return [next, null]\n }\n\n /** Blur the cursor. */\n blur(): CursorModel {\n return this.#withState({ ...this.#state(), blink: true, focus: false })\n }\n\n /**\n * Update the cursor model with an incoming message.\n * Returns a new model and an optional command.\n */\n update(msg: Msg): [CursorModel, Cmd<Msg>] {\n if (msg instanceof InitialBlinkMsg) {\n if (this.#mode !== CursorMode.Blink || !this.#focus) {\n return [this, null]\n }\n const [next, cmd] = this.tickBlink()\n return [next, cmd as Cmd<Msg>]\n }\n\n if (msg instanceof FocusMsg) {\n return this.focus()\n }\n\n if (msg instanceof BlurMsg) {\n const next = this.blur()\n return [next, null]\n }\n\n if (msg instanceof BlinkMsg) {\n // Is this model blink-able and expecting this tick?\n if (this.#mode !== CursorMode.Blink || !this.#focus) {\n return [this, null]\n }\n if (msg.id !== this.#id || msg.tag !== this.#tag) {\n return [this, null]\n }\n\n const toggled = this.#withState({ ...this.#state(), blink: !this.#blink })\n const [next, cmd] = toggled.tickBlink()\n return [next, cmd as Cmd<Msg>]\n }\n\n return [this, null]\n }\n\n /** Render the cursor. */\n view(): string {\n const char = this.char\n if (this.#blink) {\n return this.textStyle.inline(true).render(char)\n }\n return this.style.inline(true).render(char)\n }\n\n #state(): CursorState {\n return {\n id: this.#id,\n tag: this.#tag,\n blink: this.#blink,\n focus: this.#focus,\n mode: this.#mode,\n }\n }\n\n #withState(state: CursorState, overrides?: { char?: string }): CursorModel {\n return new CursorModel(\n {\n blinkSpeed: this.blinkSpeed,\n style: this.style,\n textStyle: this.textStyle,\n char: overrides?.char ?? this.char,\n mode: state.mode,\n focused: state.focus,\n },\n state,\n )\n }\n}\n"]}
@@ -0,0 +1,87 @@
1
+ import { Cmd, Msg } from '@boba-cli/tea';
2
+ import { Style } from '@boba-cli/chapstick';
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ declare class InitialBlinkMsg {
8
+ readonly _tag = "cursor:init-blink";
9
+ }
10
+ /**
11
+ * @public
12
+ */
13
+ declare class BlinkMsg {
14
+ readonly id: number;
15
+ readonly tag: number;
16
+ readonly time: Date;
17
+ readonly _tag = "cursor:blink";
18
+ constructor(id: number, tag: number, time: Date);
19
+ }
20
+
21
+ /** Available cursor behaviors. @public */
22
+ declare enum CursorMode {
23
+ Blink = "blink",
24
+ Static = "static",
25
+ Hidden = "hidden"
26
+ }
27
+ /** Options for creating a CursorModel. @public */
28
+ interface CursorOptions {
29
+ blinkSpeed?: number;
30
+ style?: Style;
31
+ textStyle?: Style;
32
+ char?: string;
33
+ mode?: CursorMode;
34
+ focused?: boolean;
35
+ }
36
+ type CursorState = {
37
+ id: number;
38
+ tag: number;
39
+ blink: boolean;
40
+ focus: boolean;
41
+ mode: CursorMode;
42
+ };
43
+ /**
44
+ * Cursor component model.
45
+ *
46
+ * Use `tickBlink()` to start blinking, or handle `InitialBlinkMsg` from `initBlink()`.
47
+ * Handle `BlinkMsg`, `FocusMsg`, and `BlurMsg` in your update loop.
48
+ *
49
+ * @public
50
+ */
51
+ declare class CursorModel {
52
+ #private;
53
+ readonly blinkSpeed: number;
54
+ readonly style: Style;
55
+ readonly textStyle: Style;
56
+ readonly char: string;
57
+ constructor(options?: CursorOptions, state?: CursorState);
58
+ /** Unique ID for this cursor (for message routing). */
59
+ id(): number;
60
+ /** Current cursor mode. */
61
+ mode(): CursorMode;
62
+ /** Whether the cursor is currently in the "text" state (hidden block). */
63
+ isBlinkHidden(): boolean;
64
+ /** Whether this cursor currently has focus. */
65
+ isFocused(): boolean;
66
+ /** Kick off blinking at init (emits InitialBlinkMsg). */
67
+ initBlink(): Cmd<InitialBlinkMsg>;
68
+ /** Command to schedule the next blink toggle. */
69
+ tickBlink(): [CursorModel, Cmd<BlinkMsg>];
70
+ /** Set the character under the cursor. */
71
+ withChar(char: string): CursorModel;
72
+ /** Set the cursor mode. Returns a new model and optional blink command. */
73
+ withMode(mode: CursorMode): [CursorModel, Cmd<Msg>];
74
+ /** Focus the cursor. Returns new model and optional blink command. */
75
+ focus(): [CursorModel, Cmd<Msg>];
76
+ /** Blur the cursor. */
77
+ blur(): CursorModel;
78
+ /**
79
+ * Update the cursor model with an incoming message.
80
+ * Returns a new model and an optional command.
81
+ */
82
+ update(msg: Msg): [CursorModel, Cmd<Msg>];
83
+ /** Render the cursor. */
84
+ view(): string;
85
+ }
86
+
87
+ export { BlinkMsg, CursorMode, CursorModel, type CursorOptions, InitialBlinkMsg };
@@ -0,0 +1,87 @@
1
+ import { Cmd, Msg } from '@boba-cli/tea';
2
+ import { Style } from '@boba-cli/chapstick';
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ declare class InitialBlinkMsg {
8
+ readonly _tag = "cursor:init-blink";
9
+ }
10
+ /**
11
+ * @public
12
+ */
13
+ declare class BlinkMsg {
14
+ readonly id: number;
15
+ readonly tag: number;
16
+ readonly time: Date;
17
+ readonly _tag = "cursor:blink";
18
+ constructor(id: number, tag: number, time: Date);
19
+ }
20
+
21
+ /** Available cursor behaviors. @public */
22
+ declare enum CursorMode {
23
+ Blink = "blink",
24
+ Static = "static",
25
+ Hidden = "hidden"
26
+ }
27
+ /** Options for creating a CursorModel. @public */
28
+ interface CursorOptions {
29
+ blinkSpeed?: number;
30
+ style?: Style;
31
+ textStyle?: Style;
32
+ char?: string;
33
+ mode?: CursorMode;
34
+ focused?: boolean;
35
+ }
36
+ type CursorState = {
37
+ id: number;
38
+ tag: number;
39
+ blink: boolean;
40
+ focus: boolean;
41
+ mode: CursorMode;
42
+ };
43
+ /**
44
+ * Cursor component model.
45
+ *
46
+ * Use `tickBlink()` to start blinking, or handle `InitialBlinkMsg` from `initBlink()`.
47
+ * Handle `BlinkMsg`, `FocusMsg`, and `BlurMsg` in your update loop.
48
+ *
49
+ * @public
50
+ */
51
+ declare class CursorModel {
52
+ #private;
53
+ readonly blinkSpeed: number;
54
+ readonly style: Style;
55
+ readonly textStyle: Style;
56
+ readonly char: string;
57
+ constructor(options?: CursorOptions, state?: CursorState);
58
+ /** Unique ID for this cursor (for message routing). */
59
+ id(): number;
60
+ /** Current cursor mode. */
61
+ mode(): CursorMode;
62
+ /** Whether the cursor is currently in the "text" state (hidden block). */
63
+ isBlinkHidden(): boolean;
64
+ /** Whether this cursor currently has focus. */
65
+ isFocused(): boolean;
66
+ /** Kick off blinking at init (emits InitialBlinkMsg). */
67
+ initBlink(): Cmd<InitialBlinkMsg>;
68
+ /** Command to schedule the next blink toggle. */
69
+ tickBlink(): [CursorModel, Cmd<BlinkMsg>];
70
+ /** Set the character under the cursor. */
71
+ withChar(char: string): CursorModel;
72
+ /** Set the cursor mode. Returns a new model and optional blink command. */
73
+ withMode(mode: CursorMode): [CursorModel, Cmd<Msg>];
74
+ /** Focus the cursor. Returns new model and optional blink command. */
75
+ focus(): [CursorModel, Cmd<Msg>];
76
+ /** Blur the cursor. */
77
+ blur(): CursorModel;
78
+ /**
79
+ * Update the cursor model with an incoming message.
80
+ * Returns a new model and an optional command.
81
+ */
82
+ update(msg: Msg): [CursorModel, Cmd<Msg>];
83
+ /** Render the cursor. */
84
+ view(): string;
85
+ }
86
+
87
+ export { BlinkMsg, CursorMode, CursorModel, type CursorOptions, InitialBlinkMsg };
package/dist/index.js ADDED
@@ -0,0 +1,179 @@
1
+ import { tick, FocusMsg, BlurMsg } from '@boba-cli/tea';
2
+ import { Style } from '@boba-cli/chapstick';
3
+
4
+ // src/model.ts
5
+
6
+ // src/messages.ts
7
+ var InitialBlinkMsg = class {
8
+ _tag = "cursor:init-blink";
9
+ };
10
+ var BlinkMsg = class {
11
+ constructor(id, tag, time) {
12
+ this.id = id;
13
+ this.tag = tag;
14
+ this.time = time;
15
+ }
16
+ _tag = "cursor:blink";
17
+ };
18
+
19
+ // src/model.ts
20
+ var defaultBlinkSpeed = 530;
21
+ var lastId = 0;
22
+ function nextId() {
23
+ return ++lastId;
24
+ }
25
+ var CursorMode = /* @__PURE__ */ ((CursorMode2) => {
26
+ CursorMode2["Blink"] = "blink";
27
+ CursorMode2["Static"] = "static";
28
+ CursorMode2["Hidden"] = "hidden";
29
+ return CursorMode2;
30
+ })(CursorMode || {});
31
+ var CursorModel = class _CursorModel {
32
+ blinkSpeed;
33
+ style;
34
+ textStyle;
35
+ char;
36
+ #id;
37
+ #tag;
38
+ #blink;
39
+ #focus;
40
+ #mode;
41
+ constructor(options = {}, state) {
42
+ this.blinkSpeed = options.blinkSpeed ?? defaultBlinkSpeed;
43
+ this.style = options.style ?? new Style();
44
+ this.textStyle = options.textStyle ?? new Style();
45
+ this.char = options.char ?? " ";
46
+ this.#id = state?.id ?? nextId();
47
+ this.#tag = state?.tag ?? 0;
48
+ this.#blink = state?.blink ?? true;
49
+ this.#focus = state?.focus ?? false;
50
+ this.#mode = state?.mode ?? options.mode ?? "blink" /* Blink */;
51
+ }
52
+ /** Unique ID for this cursor (for message routing). */
53
+ id() {
54
+ return this.#id;
55
+ }
56
+ /** Current cursor mode. */
57
+ mode() {
58
+ return this.#mode;
59
+ }
60
+ /** Whether the cursor is currently in the "text" state (hidden block). */
61
+ isBlinkHidden() {
62
+ return this.#blink;
63
+ }
64
+ /** Whether this cursor currently has focus. */
65
+ isFocused() {
66
+ return this.#focus;
67
+ }
68
+ /** Kick off blinking at init (emits InitialBlinkMsg). */
69
+ initBlink() {
70
+ return () => new InitialBlinkMsg();
71
+ }
72
+ /** Command to schedule the next blink toggle. */
73
+ tickBlink() {
74
+ const nextTag = this.#tag + 1;
75
+ const id = this.#id;
76
+ const speed = this.blinkSpeed;
77
+ const next = this.#withState({ ...this.#state(), tag: nextTag });
78
+ const cmd = tick(
79
+ speed,
80
+ (time) => new BlinkMsg(id, nextTag, time)
81
+ );
82
+ return [next, cmd];
83
+ }
84
+ /** Set the character under the cursor. */
85
+ withChar(char) {
86
+ return this.#withState(this.#state(), { char });
87
+ }
88
+ /** Set the cursor mode. Returns a new model and optional blink command. */
89
+ withMode(mode) {
90
+ const bounded = mode === "blink" /* Blink */ || mode === "static" /* Static */ || mode === "hidden" /* Hidden */ ? mode : "blink" /* Blink */;
91
+ const blink = bounded === "hidden" /* Hidden */ || !this.#focus ? true : this.#blink;
92
+ const next = this.#withState({ ...this.#state(), blink, mode: bounded });
93
+ if (bounded === "blink" /* Blink */ && this.#focus) {
94
+ const [scheduled, cmd] = next.tickBlink();
95
+ return [scheduled, cmd];
96
+ }
97
+ return [next, null];
98
+ }
99
+ /** Focus the cursor. Returns new model and optional blink command. */
100
+ focus() {
101
+ const blink = this.#mode === "hidden" /* Hidden */ ? true : this.#blink;
102
+ const next = this.#withState({ ...this.#state(), blink, focus: true });
103
+ if (this.#mode === "blink" /* Blink */) {
104
+ const [scheduled, cmd] = next.tickBlink();
105
+ return [scheduled, cmd];
106
+ }
107
+ return [next, null];
108
+ }
109
+ /** Blur the cursor. */
110
+ blur() {
111
+ return this.#withState({ ...this.#state(), blink: true, focus: false });
112
+ }
113
+ /**
114
+ * Update the cursor model with an incoming message.
115
+ * Returns a new model and an optional command.
116
+ */
117
+ update(msg) {
118
+ if (msg instanceof InitialBlinkMsg) {
119
+ if (this.#mode !== "blink" /* Blink */ || !this.#focus) {
120
+ return [this, null];
121
+ }
122
+ const [next, cmd] = this.tickBlink();
123
+ return [next, cmd];
124
+ }
125
+ if (msg instanceof FocusMsg) {
126
+ return this.focus();
127
+ }
128
+ if (msg instanceof BlurMsg) {
129
+ const next = this.blur();
130
+ return [next, null];
131
+ }
132
+ if (msg instanceof BlinkMsg) {
133
+ if (this.#mode !== "blink" /* Blink */ || !this.#focus) {
134
+ return [this, null];
135
+ }
136
+ if (msg.id !== this.#id || msg.tag !== this.#tag) {
137
+ return [this, null];
138
+ }
139
+ const toggled = this.#withState({ ...this.#state(), blink: !this.#blink });
140
+ const [next, cmd] = toggled.tickBlink();
141
+ return [next, cmd];
142
+ }
143
+ return [this, null];
144
+ }
145
+ /** Render the cursor. */
146
+ view() {
147
+ const char = this.char;
148
+ if (this.#blink) {
149
+ return this.textStyle.inline(true).render(char);
150
+ }
151
+ return this.style.inline(true).render(char);
152
+ }
153
+ #state() {
154
+ return {
155
+ id: this.#id,
156
+ tag: this.#tag,
157
+ blink: this.#blink,
158
+ focus: this.#focus,
159
+ mode: this.#mode
160
+ };
161
+ }
162
+ #withState(state, overrides) {
163
+ return new _CursorModel(
164
+ {
165
+ blinkSpeed: this.blinkSpeed,
166
+ style: this.style,
167
+ textStyle: this.textStyle,
168
+ char: overrides?.char ?? this.char,
169
+ mode: state.mode,
170
+ focused: state.focus
171
+ },
172
+ state
173
+ );
174
+ }
175
+ };
176
+
177
+ export { BlinkMsg, CursorMode, CursorModel, InitialBlinkMsg };
178
+ //# sourceMappingURL=index.js.map
179
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/messages.ts","../src/model.ts"],"names":["CursorMode"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @public\n */\nexport class InitialBlinkMsg {\n readonly _tag = 'cursor:init-blink'\n}\n\n/**\n * @public\n */\nexport class BlinkMsg {\n readonly _tag = 'cursor:blink'\n constructor(\n public readonly id: number,\n public readonly tag: number,\n public readonly time: Date,\n ) {}\n}\n","import { tick, type Cmd, type Msg, FocusMsg, BlurMsg } from '@boba-cli/tea'\nimport { Style } from '@boba-cli/chapstick'\nimport { BlinkMsg, InitialBlinkMsg } from './messages.js'\n\nconst defaultBlinkSpeed = 530 // ms\n\n// Module-level ID counter for unique cursor identification\nlet lastId = 0\nfunction nextId(): number {\n return ++lastId\n}\n\n/** Available cursor behaviors. @public */\nexport enum CursorMode {\n Blink = 'blink',\n Static = 'static',\n Hidden = 'hidden',\n}\n\n/** Options for creating a CursorModel. @public */\nexport interface CursorOptions {\n blinkSpeed?: number\n style?: Style\n textStyle?: Style\n char?: string\n mode?: CursorMode\n focused?: boolean\n}\n\ntype CursorState = {\n id: number\n tag: number\n blink: boolean\n focus: boolean\n mode: CursorMode\n}\n\n/**\n * Cursor component model.\n *\n * Use `tickBlink()` to start blinking, or handle `InitialBlinkMsg` from `initBlink()`.\n * Handle `BlinkMsg`, `FocusMsg`, and `BlurMsg` in your update loop.\n *\n * @public\n */\nexport class CursorModel {\n readonly blinkSpeed: number\n readonly style: Style\n readonly textStyle: Style\n readonly char: string\n readonly #id: number\n readonly #tag: number\n readonly #blink: boolean\n readonly #focus: boolean\n readonly #mode: CursorMode\n\n constructor(options: CursorOptions = {}, state?: CursorState) {\n this.blinkSpeed = options.blinkSpeed ?? defaultBlinkSpeed\n this.style = options.style ?? new Style()\n this.textStyle = options.textStyle ?? new Style()\n this.char = options.char ?? ' '\n this.#id = state?.id ?? nextId()\n this.#tag = state?.tag ?? 0\n this.#blink = state?.blink ?? true\n this.#focus = state?.focus ?? false\n this.#mode = state?.mode ?? options.mode ?? CursorMode.Blink\n }\n\n /** Unique ID for this cursor (for message routing). */\n id(): number {\n return this.#id\n }\n\n /** Current cursor mode. */\n mode(): CursorMode {\n return this.#mode\n }\n\n /** Whether the cursor is currently in the \"text\" state (hidden block). */\n isBlinkHidden(): boolean {\n return this.#blink\n }\n\n /** Whether this cursor currently has focus. */\n isFocused(): boolean {\n return this.#focus\n }\n\n /** Kick off blinking at init (emits InitialBlinkMsg). */\n initBlink(): Cmd<InitialBlinkMsg> {\n return () => new InitialBlinkMsg()\n }\n\n /** Command to schedule the next blink toggle. */\n tickBlink(): [CursorModel, Cmd<BlinkMsg>] {\n const nextTag = this.#tag + 1\n const id = this.#id\n const speed = this.blinkSpeed\n const next = this.#withState({ ...this.#state(), tag: nextTag })\n const cmd: Cmd<BlinkMsg> = tick(\n speed,\n (time: Date) => new BlinkMsg(id, nextTag, time),\n )\n return [next, cmd]\n }\n\n /** Set the character under the cursor. */\n withChar(char: string): CursorModel {\n return this.#withState(this.#state(), { char })\n }\n\n /** Set the cursor mode. Returns a new model and optional blink command. */\n withMode(mode: CursorMode): [CursorModel, Cmd<Msg>] {\n const bounded =\n mode === CursorMode.Blink ||\n mode === CursorMode.Static ||\n mode === CursorMode.Hidden\n ? mode\n : CursorMode.Blink\n\n const blink =\n bounded === CursorMode.Hidden || !this.#focus ? true : this.#blink\n const next = this.#withState({ ...this.#state(), blink, mode: bounded })\n\n if (bounded === CursorMode.Blink && this.#focus) {\n const [scheduled, cmd] = next.tickBlink()\n return [scheduled, cmd]\n }\n return [next, null]\n }\n\n /** Focus the cursor. Returns new model and optional blink command. */\n focus(): [CursorModel, Cmd<Msg>] {\n const blink = this.#mode === CursorMode.Hidden ? true : this.#blink\n const next = this.#withState({ ...this.#state(), blink, focus: true })\n if (this.#mode === CursorMode.Blink) {\n const [scheduled, cmd] = next.tickBlink()\n return [scheduled, cmd]\n }\n return [next, null]\n }\n\n /** Blur the cursor. */\n blur(): CursorModel {\n return this.#withState({ ...this.#state(), blink: true, focus: false })\n }\n\n /**\n * Update the cursor model with an incoming message.\n * Returns a new model and an optional command.\n */\n update(msg: Msg): [CursorModel, Cmd<Msg>] {\n if (msg instanceof InitialBlinkMsg) {\n if (this.#mode !== CursorMode.Blink || !this.#focus) {\n return [this, null]\n }\n const [next, cmd] = this.tickBlink()\n return [next, cmd as Cmd<Msg>]\n }\n\n if (msg instanceof FocusMsg) {\n return this.focus()\n }\n\n if (msg instanceof BlurMsg) {\n const next = this.blur()\n return [next, null]\n }\n\n if (msg instanceof BlinkMsg) {\n // Is this model blink-able and expecting this tick?\n if (this.#mode !== CursorMode.Blink || !this.#focus) {\n return [this, null]\n }\n if (msg.id !== this.#id || msg.tag !== this.#tag) {\n return [this, null]\n }\n\n const toggled = this.#withState({ ...this.#state(), blink: !this.#blink })\n const [next, cmd] = toggled.tickBlink()\n return [next, cmd as Cmd<Msg>]\n }\n\n return [this, null]\n }\n\n /** Render the cursor. */\n view(): string {\n const char = this.char\n if (this.#blink) {\n return this.textStyle.inline(true).render(char)\n }\n return this.style.inline(true).render(char)\n }\n\n #state(): CursorState {\n return {\n id: this.#id,\n tag: this.#tag,\n blink: this.#blink,\n focus: this.#focus,\n mode: this.#mode,\n }\n }\n\n #withState(state: CursorState, overrides?: { char?: string }): CursorModel {\n return new CursorModel(\n {\n blinkSpeed: this.blinkSpeed,\n style: this.style,\n textStyle: this.textStyle,\n char: overrides?.char ?? this.char,\n mode: state.mode,\n focused: state.focus,\n },\n state,\n )\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@boba-cli/cursor",
3
+ "description": "Cursor component for Boba terminal UIs",
4
+ "version": "0.1.0-alpha.2",
5
+ "dependencies": {
6
+ "@boba-cli/chapstick": "0.1.0-alpha.2",
7
+ "@boba-cli/tea": "0.1.0-alpha.1"
8
+ },
9
+ "devDependencies": {
10
+ "typescript": "5.8.2",
11
+ "vitest": "^4.0.16"
12
+ },
13
+ "engines": {
14
+ "node": ">=20.0.0"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "import": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "require": {
23
+ "types": "./dist/index.d.cts",
24
+ "default": "./dist/index.cjs"
25
+ }
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.js",
34
+ "type": "module",
35
+ "types": "./dist/index.d.ts",
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "check:api-report": "pnpm run generate:api-report",
39
+ "check:eslint": "pnpm run lint",
40
+ "generate:api-report": "api-extractor run --local",
41
+ "lint": "eslint \"{src,test}/**/*.{ts,tsx}\"",
42
+ "test": "vitest run"
43
+ }
44
+ }