@pokit/tabs-core 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Daniel Grant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @pokit/tabs-core
2
+
3
+ Shared logic for pok tabbed terminal UI adapters.
4
+
5
+ ## Purpose
6
+
7
+ This package provides framework-agnostic types, state management, and process handling used by tabs adapter implementations like `@pokit/tabs-ink`.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ bun add @pokit/tabs-core
13
+ ```
14
+
15
+ Note: This is typically a dependency of adapter packages, not installed directly.
16
+
17
+ ## Exports
18
+
19
+ ### State Management
20
+
21
+ ```typescript
22
+ import { createInitialState, reducer } from '@pokit/tabs-core';
23
+ ```
24
+
25
+ ### Process Manager
26
+
27
+ ```typescript
28
+ import { ProcessManager } from '@pokit/tabs-core';
29
+ ```
30
+
31
+ ### Types
32
+
33
+ ```typescript
34
+ import type { TabStatus, TabProcess, EventDrivenState } from '@pokit/tabs-core';
35
+ ```
36
+
37
+ ### Status Indicators
38
+
39
+ ```typescript
40
+ import { STATUS_INDICATORS, getStatusIndicator } from '@pokit/tabs-core';
41
+ ```
42
+
43
+ ## Documentation
44
+
45
+ See the [full documentation](https://github.com/openpok/pok/blob/main/docs/packages/tabs-core.md).
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared Help Content for CLI Tabs
3
+ *
4
+ * Keyboard shortcut definitions and help overlay content used by all adapters.
5
+ */
6
+ export type Shortcut = {
7
+ key: string;
8
+ description: string;
9
+ };
10
+ export type ShortcutGroup = {
11
+ title: string;
12
+ shortcuts: Shortcut[];
13
+ };
14
+ /**
15
+ * Standard keyboard shortcut help content.
16
+ * Used by both Ink and OpenTUI help overlays.
17
+ */
18
+ export declare const HELP_CONTENT: ShortcutGroup[];
19
+ //# sourceMappingURL=help-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help-content.d.ts","sourceRoot":"","sources":["../../src/constants/help-content.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,QAAQ,EAAE,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY,EAAE,aAAa,EA4BvC,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared Help Content for CLI Tabs
3
+ *
4
+ * Keyboard shortcut definitions and help overlay content used by all adapters.
5
+ */
6
+ /**
7
+ * Standard keyboard shortcut help content.
8
+ * Used by both Ink and OpenTUI help overlays.
9
+ */
10
+ export const HELP_CONTENT = [
11
+ {
12
+ title: 'Navigation',
13
+ shortcuts: [
14
+ { key: '\u2191/\u2193', description: 'Scroll output up/down' },
15
+ { key: 'Page Up/Dn', description: 'Scroll by page' },
16
+ { key: 'Tab', description: 'Next tab' },
17
+ { key: 'Shift+Tab', description: 'Previous tab' },
18
+ { key: '1-9', description: 'Jump to tab by number' },
19
+ { key: 'Meta+\u2190/\u2192', description: 'Previous/next tab' },
20
+ ],
21
+ },
22
+ {
23
+ title: 'Process Control',
24
+ shortcuts: [
25
+ { key: 'r', description: 'Restart current process' },
26
+ { key: 'k', description: 'Kill current process' },
27
+ { key: 'q', description: 'Quit (with confirmation)' },
28
+ { key: 'Ctrl+C', description: 'Force quit immediately' },
29
+ ],
30
+ },
31
+ {
32
+ title: 'Input Mode',
33
+ shortcuts: [
34
+ { key: 'i', description: 'Enter input mode' },
35
+ { key: 'Escape', description: 'Exit input mode' },
36
+ ],
37
+ },
38
+ ];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Constants Index
3
+ *
4
+ * Re-exports all shared constants for CLI tabs.
5
+ */
6
+ export type { Shortcut, ShortcutGroup } from './help-content.js';
7
+ export { HELP_CONTENT } from './help-content.js';
8
+ export { KEY_SEQUENCES, ctrlKeyToSequence, HELP_HINT_DURATION_MS } from './keyboard.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Constants Index
3
+ *
4
+ * Re-exports all shared constants for CLI tabs.
5
+ */
6
+ export { HELP_CONTENT } from './help-content.js';
7
+ export { KEY_SEQUENCES, ctrlKeyToSequence, HELP_HINT_DURATION_MS } from './keyboard.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Keyboard Constants for CLI Tabs
3
+ *
4
+ * Escape sequences and key mappings for terminal input handling.
5
+ */
6
+ /**
7
+ * Common terminal escape sequences for special keys.
8
+ * Used when forwarding input to child processes in focus mode.
9
+ */
10
+ export declare const KEY_SEQUENCES: {
11
+ readonly RETURN: "\n";
12
+ readonly TAB: "\t";
13
+ readonly BACKSPACE: "";
14
+ readonly DELETE: "\u001B[3~";
15
+ readonly ARROW_UP: "\u001B[A";
16
+ readonly ARROW_DOWN: "\u001B[B";
17
+ readonly ARROW_RIGHT: "\u001B[C";
18
+ readonly ARROW_LEFT: "\u001B[D";
19
+ };
20
+ /**
21
+ * Convert a Ctrl+key combination to its terminal escape sequence.
22
+ * Ctrl+A = 1, Ctrl+B = 2, ..., Ctrl+Z = 26
23
+ */
24
+ export declare function ctrlKeyToSequence(key: string): string | null;
25
+ /**
26
+ * Duration to show help hint on startup (in milliseconds).
27
+ */
28
+ export declare const HELP_HINT_DURATION_MS = 5000;
29
+ //# sourceMappingURL=keyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../../src/constants/keyboard.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;;;;;;CAShB,CAAC;AAEX;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM5D;AAED;;GAEG;AACH,eAAO,MAAM,qBAAqB,OAAO,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Keyboard Constants for CLI Tabs
3
+ *
4
+ * Escape sequences and key mappings for terminal input handling.
5
+ */
6
+ /**
7
+ * Common terminal escape sequences for special keys.
8
+ * Used when forwarding input to child processes in focus mode.
9
+ */
10
+ export const KEY_SEQUENCES = {
11
+ RETURN: '\n',
12
+ TAB: '\t',
13
+ BACKSPACE: '\x7f',
14
+ DELETE: '\x1b[3~',
15
+ ARROW_UP: '\x1b[A',
16
+ ARROW_DOWN: '\x1b[B',
17
+ ARROW_RIGHT: '\x1b[C',
18
+ ARROW_LEFT: '\x1b[D',
19
+ };
20
+ /**
21
+ * Convert a Ctrl+key combination to its terminal escape sequence.
22
+ * Ctrl+A = 1, Ctrl+B = 2, ..., Ctrl+Z = 26
23
+ */
24
+ export function ctrlKeyToSequence(key) {
25
+ const code = key.toUpperCase().charCodeAt(0) - 64;
26
+ if (code >= 1 && code <= 26) {
27
+ return String.fromCharCode(code);
28
+ }
29
+ return null;
30
+ }
31
+ /**
32
+ * Duration to show help hint on startup (in milliseconds).
33
+ */
34
+ export const HELP_HINT_DURATION_MS = 5000;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Hooks Index
3
+ *
4
+ * Re-exports all shared hooks for CLI tabs.
5
+ */
6
+ export type { UseTabsStateOptions, TabsState, TabsActions } from './use-tabs-state.js';
7
+ export { useTabsState } from './use-tabs-state.js';
8
+ export type { KeyboardCallbacks, KeyboardState, NormalizedKeyEvent, KeyboardAction, } from './use-keyboard-handler.js';
9
+ export { processKeyEvent, useKeyboardCallbackRefs, executeKeyboardAction, } from './use-keyboard-handler.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvF,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,cAAc,GACf,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,2BAA2B,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Hooks Index
3
+ *
4
+ * Re-exports all shared hooks for CLI tabs.
5
+ */
6
+ export { useTabsState } from './use-tabs-state.js';
7
+ export { processKeyEvent, useKeyboardCallbackRefs, executeKeyboardAction, } from './use-keyboard-handler.js';
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Shared Keyboard Handler Logic
3
+ *
4
+ * Framework-agnostic keyboard action handling for tabbed terminal interfaces.
5
+ * Provides normalized keyboard actions that can be called by framework-specific handlers.
6
+ */
7
+ /**
8
+ * Callbacks for keyboard actions in the tabbed view.
9
+ */
10
+ export type KeyboardCallbacks = {
11
+ onQuit: () => void;
12
+ onQuitRequest: () => void;
13
+ onRestart: (index: number) => void;
14
+ onKill: (index: number) => void;
15
+ onEnterFocusMode: () => void;
16
+ onExitFocusMode: () => void;
17
+ onSendInput: (data: string) => void;
18
+ onToggleHelp: () => void;
19
+ onCloseHelp: () => void;
20
+ };
21
+ /**
22
+ * Current state needed for keyboard handling decisions.
23
+ */
24
+ export type KeyboardState = {
25
+ helpVisible: boolean;
26
+ focusMode: boolean;
27
+ quitConfirmPending: boolean;
28
+ activeIndex: number;
29
+ tabCount: number;
30
+ };
31
+ /**
32
+ * Normalized key event structure.
33
+ * Provides a unified interface for different terminal keyboard APIs.
34
+ */
35
+ export type NormalizedKeyEvent = {
36
+ /** The character(s) typed, if any */
37
+ char?: string;
38
+ /** Named key (e.g., 'escape', 'tab', 'up', 'down') */
39
+ name?: string;
40
+ /** Whether Ctrl was held */
41
+ ctrl?: boolean;
42
+ /** Whether Shift was held */
43
+ shift?: boolean;
44
+ /** Whether Meta/Alt was held */
45
+ meta?: boolean;
46
+ };
47
+ /**
48
+ * Action results from keyboard handling.
49
+ */
50
+ export type KeyboardAction = {
51
+ type: 'toggle-help';
52
+ } | {
53
+ type: 'close-help';
54
+ } | {
55
+ type: 'exit-focus-mode';
56
+ } | {
57
+ type: 'send-input';
58
+ data: string;
59
+ } | {
60
+ type: 'quit';
61
+ } | {
62
+ type: 'quit-request';
63
+ } | {
64
+ type: 'cancel-quit';
65
+ } | {
66
+ type: 'enter-focus-mode';
67
+ } | {
68
+ type: 'restart';
69
+ index: number;
70
+ } | {
71
+ type: 'kill';
72
+ index: number;
73
+ } | {
74
+ type: 'switch-tab';
75
+ index: number;
76
+ } | {
77
+ type: 'next-tab';
78
+ } | {
79
+ type: 'prev-tab';
80
+ } | {
81
+ type: 'scroll';
82
+ delta: number;
83
+ } | {
84
+ type: 'none';
85
+ };
86
+ /**
87
+ * Process a normalized key event and return the appropriate action.
88
+ *
89
+ * This is the core keyboard handling logic, extracted to be framework-agnostic.
90
+ * Each adapter (Ink, OpenTUI) normalizes their keyboard events and calls this function.
91
+ */
92
+ export declare function processKeyEvent(event: NormalizedKeyEvent, state: KeyboardState, viewHeight: number): KeyboardAction;
93
+ /**
94
+ * Hook to create stable references for keyboard callbacks.
95
+ *
96
+ * This solves the common React issue where callbacks in keyboard handlers
97
+ * may have stale closures. The refs always point to the current callback.
98
+ */
99
+ export declare function useKeyboardCallbackRefs(callbacks: KeyboardCallbacks): {
100
+ onQuitRef: import("react").RefObject<() => void>;
101
+ onQuitRequestRef: import("react").RefObject<() => void>;
102
+ onRestartRef: import("react").RefObject<(index: number) => void>;
103
+ onKillRef: import("react").RefObject<(index: number) => void>;
104
+ onEnterFocusModeRef: import("react").RefObject<() => void>;
105
+ onExitFocusModeRef: import("react").RefObject<() => void>;
106
+ onSendInputRef: import("react").RefObject<(data: string) => void>;
107
+ onToggleHelpRef: import("react").RefObject<() => void>;
108
+ onCloseHelpRef: import("react").RefObject<() => void>;
109
+ };
110
+ /**
111
+ * Execute a keyboard action using the provided callback refs.
112
+ */
113
+ export declare function executeKeyboardAction(action: KeyboardAction, refs: ReturnType<typeof useKeyboardCallbackRefs>, scrollBy: (delta: number) => void, switchTab: (index: number) => void, nextTab: () => void, prevTab: () => void): void;
114
+ //# sourceMappingURL=use-keyboard-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-keyboard-handler.d.ts","sourceRoot":"","sources":["../../src/hooks/use-keyboard-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Shared Keyboard Handler Logic
3
+ *
4
+ * Framework-agnostic keyboard action handling for tabbed terminal interfaces.
5
+ * Provides normalized keyboard actions that can be called by framework-specific handlers.
6
+ */
7
+ import { useRef, useEffect } from 'react';
8
+ import { KEY_SEQUENCES, ctrlKeyToSequence } from '../constants/keyboard.js';
9
+ /**
10
+ * Process a normalized key event and return the appropriate action.
11
+ *
12
+ * This is the core keyboard handling logic, extracted to be framework-agnostic.
13
+ * Each adapter (Ink, OpenTUI) normalizes their keyboard events and calls this function.
14
+ */
15
+ export function processKeyEvent(event, state, viewHeight) {
16
+ const { char, name, ctrl, shift, meta } = event;
17
+ const { helpVisible, focusMode, quitConfirmPending, activeIndex, tabCount } = state;
18
+ // Help toggle takes priority
19
+ if (char === '?') {
20
+ return { type: 'toggle-help' };
21
+ }
22
+ // Escape closes help if visible
23
+ if (name === 'escape' && helpVisible) {
24
+ return { type: 'close-help' };
25
+ }
26
+ // Don't process other keys when help is visible
27
+ if (helpVisible) {
28
+ return { type: 'none' };
29
+ }
30
+ // Focus mode: forward most input to child process
31
+ if (focusMode) {
32
+ if (name === 'escape') {
33
+ return { type: 'exit-focus-mode' };
34
+ }
35
+ const rawInput = getFocusModeInput(event);
36
+ if (rawInput) {
37
+ return { type: 'send-input', data: rawInput };
38
+ }
39
+ return { type: 'none' };
40
+ }
41
+ // Normal mode: handle UI navigation
42
+ // Quit confirmation handling
43
+ if (quitConfirmPending) {
44
+ if (char === 'q' || name === 'q') {
45
+ return { type: 'quit' };
46
+ }
47
+ return { type: 'cancel-quit' };
48
+ }
49
+ // Quit request
50
+ if (char === 'q' || name === 'q') {
51
+ return { type: 'quit-request' };
52
+ }
53
+ // Ctrl+C for instant quit
54
+ if ((char === 'c' || name === 'c') && ctrl) {
55
+ return { type: 'quit' };
56
+ }
57
+ // Enter focus/input mode
58
+ if (char === 'i' || name === 'i') {
59
+ return { type: 'enter-focus-mode' };
60
+ }
61
+ // Restart current tab
62
+ if (char === 'r' || name === 'r') {
63
+ return { type: 'restart', index: activeIndex };
64
+ }
65
+ // Kill current tab's process
66
+ if (char === 'k' || name === 'k') {
67
+ return { type: 'kill', index: activeIndex };
68
+ }
69
+ // Number keys 1-9 for direct tab access
70
+ const input = char || name || '';
71
+ const num = parseInt(input, 10);
72
+ if (num >= 1 && num <= tabCount) {
73
+ return { type: 'switch-tab', index: num - 1 };
74
+ }
75
+ // Tab navigation
76
+ if (name === 'tab' && shift) {
77
+ return { type: 'prev-tab' };
78
+ }
79
+ if (name === 'tab') {
80
+ return { type: 'next-tab' };
81
+ }
82
+ // Arrow key tab navigation (with meta)
83
+ if ((name === 'left' || name === 'leftArrow') && meta) {
84
+ return { type: 'prev-tab' };
85
+ }
86
+ if ((name === 'right' || name === 'rightArrow') && meta) {
87
+ return { type: 'next-tab' };
88
+ }
89
+ // Scrolling
90
+ if (name === 'up' || name === 'upArrow') {
91
+ return { type: 'scroll', delta: -1 };
92
+ }
93
+ if (name === 'down' || name === 'downArrow') {
94
+ return { type: 'scroll', delta: 1 };
95
+ }
96
+ if (name === 'pageup' || name === 'pageUp') {
97
+ return { type: 'scroll', delta: -viewHeight };
98
+ }
99
+ if (name === 'pagedown' || name === 'pageDown') {
100
+ return { type: 'scroll', delta: viewHeight };
101
+ }
102
+ return { type: 'none' };
103
+ }
104
+ /**
105
+ * Get the raw input to send to child process when in focus mode.
106
+ */
107
+ function getFocusModeInput(event) {
108
+ const { char, name, ctrl } = event;
109
+ // Map special keys to escape sequences
110
+ if (name === 'return')
111
+ return KEY_SEQUENCES.RETURN;
112
+ if (name === 'tab')
113
+ return KEY_SEQUENCES.TAB;
114
+ if (name === 'backspace')
115
+ return KEY_SEQUENCES.BACKSPACE;
116
+ if (name === 'delete')
117
+ return KEY_SEQUENCES.DELETE;
118
+ if (name === 'up' || name === 'upArrow')
119
+ return KEY_SEQUENCES.ARROW_UP;
120
+ if (name === 'down' || name === 'downArrow')
121
+ return KEY_SEQUENCES.ARROW_DOWN;
122
+ if (name === 'right' || name === 'rightArrow')
123
+ return KEY_SEQUENCES.ARROW_RIGHT;
124
+ if (name === 'left' || name === 'leftArrow')
125
+ return KEY_SEQUENCES.ARROW_LEFT;
126
+ // Ctrl+key combinations
127
+ if (ctrl && name && name.length === 1) {
128
+ return ctrlKeyToSequence(name);
129
+ }
130
+ // Regular character input
131
+ if (char) {
132
+ return char;
133
+ }
134
+ return null;
135
+ }
136
+ /**
137
+ * Hook to create stable references for keyboard callbacks.
138
+ *
139
+ * This solves the common React issue where callbacks in keyboard handlers
140
+ * may have stale closures. The refs always point to the current callback.
141
+ */
142
+ export function useKeyboardCallbackRefs(callbacks) {
143
+ const onQuitRef = useRef(callbacks.onQuit);
144
+ const onQuitRequestRef = useRef(callbacks.onQuitRequest);
145
+ const onRestartRef = useRef(callbacks.onRestart);
146
+ const onKillRef = useRef(callbacks.onKill);
147
+ const onEnterFocusModeRef = useRef(callbacks.onEnterFocusMode);
148
+ const onExitFocusModeRef = useRef(callbacks.onExitFocusMode);
149
+ const onSendInputRef = useRef(callbacks.onSendInput);
150
+ const onToggleHelpRef = useRef(callbacks.onToggleHelp);
151
+ const onCloseHelpRef = useRef(callbacks.onCloseHelp);
152
+ useEffect(() => {
153
+ onQuitRef.current = callbacks.onQuit;
154
+ onQuitRequestRef.current = callbacks.onQuitRequest;
155
+ onRestartRef.current = callbacks.onRestart;
156
+ onKillRef.current = callbacks.onKill;
157
+ onEnterFocusModeRef.current = callbacks.onEnterFocusMode;
158
+ onExitFocusModeRef.current = callbacks.onExitFocusMode;
159
+ onSendInputRef.current = callbacks.onSendInput;
160
+ onToggleHelpRef.current = callbacks.onToggleHelp;
161
+ onCloseHelpRef.current = callbacks.onCloseHelp;
162
+ }, [callbacks]);
163
+ return {
164
+ onQuitRef,
165
+ onQuitRequestRef,
166
+ onRestartRef,
167
+ onKillRef,
168
+ onEnterFocusModeRef,
169
+ onExitFocusModeRef,
170
+ onSendInputRef,
171
+ onToggleHelpRef,
172
+ onCloseHelpRef,
173
+ };
174
+ }
175
+ /**
176
+ * Execute a keyboard action using the provided callback refs.
177
+ */
178
+ export function executeKeyboardAction(action, refs, scrollBy, switchTab, nextTab, prevTab) {
179
+ switch (action.type) {
180
+ case 'toggle-help':
181
+ refs.onToggleHelpRef.current();
182
+ break;
183
+ case 'close-help':
184
+ refs.onCloseHelpRef.current();
185
+ break;
186
+ case 'exit-focus-mode':
187
+ refs.onExitFocusModeRef.current();
188
+ break;
189
+ case 'send-input':
190
+ refs.onSendInputRef.current(action.data);
191
+ break;
192
+ case 'quit':
193
+ refs.onQuitRef.current();
194
+ break;
195
+ case 'quit-request':
196
+ case 'cancel-quit':
197
+ refs.onQuitRequestRef.current();
198
+ break;
199
+ case 'enter-focus-mode':
200
+ refs.onEnterFocusModeRef.current();
201
+ break;
202
+ case 'restart':
203
+ refs.onRestartRef.current(action.index);
204
+ break;
205
+ case 'kill':
206
+ refs.onKillRef.current(action.index);
207
+ break;
208
+ case 'switch-tab':
209
+ switchTab(action.index);
210
+ break;
211
+ case 'next-tab':
212
+ nextTab();
213
+ break;
214
+ case 'prev-tab':
215
+ prevTab();
216
+ break;
217
+ case 'scroll':
218
+ scrollBy(action.delta);
219
+ break;
220
+ case 'none':
221
+ // Do nothing
222
+ break;
223
+ }
224
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared Tab State Management Hook
3
+ *
4
+ * Framework-agnostic state management for tabbed terminal interfaces.
5
+ * Handles tab selection, scroll offsets, auto-scroll behavior, and help hint visibility.
6
+ */
7
+ import type { TabProcess } from '../types.js';
8
+ export type UseTabsStateOptions = {
9
+ /** List of tab processes */
10
+ tabs: TabProcess[];
11
+ /** Current active tab index */
12
+ activeIndex: number;
13
+ /** Callback when active index changes */
14
+ onActiveIndexChange: (index: number) => void;
15
+ /** Height of the output view in lines */
16
+ viewHeight: number;
17
+ };
18
+ export type TabsState = {
19
+ /** Whether to show the help hint */
20
+ showHelpHint: boolean;
21
+ /** Current scroll offset for the active tab */
22
+ activeScrollOffset: number;
23
+ /** Whether the user can scroll up */
24
+ canScrollUp: boolean;
25
+ /** Whether the user can scroll down */
26
+ canScrollDown: boolean;
27
+ };
28
+ export type TabsActions = {
29
+ /** Scroll by a given delta (positive = down, negative = up) */
30
+ scrollBy: (delta: number) => void;
31
+ /** Switch to a specific tab by index */
32
+ switchTab: (index: number) => void;
33
+ /** Switch to the next tab (wraps around) */
34
+ nextTab: () => void;
35
+ /** Switch to the previous tab (wraps around) */
36
+ prevTab: () => void;
37
+ };
38
+ /**
39
+ * Hook for managing tabbed interface state.
40
+ *
41
+ * Provides:
42
+ * - Per-tab scroll offsets with auto-scroll behavior
43
+ * - Tab navigation helpers
44
+ * - Help hint auto-dismiss timer
45
+ */
46
+ export declare function useTabsState({ tabs, activeIndex, onActiveIndexChange, viewHeight, }: UseTabsStateOptions): TabsState & TabsActions;
47
+ //# sourceMappingURL=use-tabs-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-tabs-state.d.ts","sourceRoot":"","sources":["../../src/hooks/use-tabs-state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,MAAM,MAAM,mBAAmB,GAAG;IAChC,4BAA4B;IAC5B,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,mBAAmB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,oCAAoC;IACpC,YAAY,EAAE,OAAO,CAAC;IACtB,+CAA+C;IAC/C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qCAAqC;IACrC,WAAW,EAAE,OAAO,CAAC;IACrB,uCAAuC;IACvC,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,+DAA+D;IAC/D,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,wCAAwC;IACxC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,4CAA4C;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gDAAgD;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,WAAW,EACX,mBAAmB,EACnB,UAAU,GACX,EAAE,mBAAmB,GAAG,SAAS,GAAG,WAAW,CA4F/C"}