ccmanager 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/README.md +85 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +57 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +24 -0
- package/dist/components/App.d.ts +3 -0
- package/dist/components/App.js +228 -0
- package/dist/components/ConfigureShortcuts.d.ts +6 -0
- package/dist/components/ConfigureShortcuts.js +139 -0
- package/dist/components/Confirmation.d.ts +12 -0
- package/dist/components/Confirmation.js +42 -0
- package/dist/components/DeleteWorktree.d.ts +7 -0
- package/dist/components/DeleteWorktree.js +116 -0
- package/dist/components/Menu.d.ts +9 -0
- package/dist/components/Menu.js +154 -0
- package/dist/components/MergeWorktree.d.ts +7 -0
- package/dist/components/MergeWorktree.js +142 -0
- package/dist/components/NewWorktree.d.ts +7 -0
- package/dist/components/NewWorktree.js +49 -0
- package/dist/components/Session.d.ts +10 -0
- package/dist/components/Session.js +121 -0
- package/dist/constants/statusIcons.d.ts +18 -0
- package/dist/constants/statusIcons.js +27 -0
- package/dist/services/sessionManager.d.ts +16 -0
- package/dist/services/sessionManager.js +190 -0
- package/dist/services/sessionManager.test.d.ts +1 -0
- package/dist/services/sessionManager.test.js +99 -0
- package/dist/services/shortcutManager.d.ts +17 -0
- package/dist/services/shortcutManager.js +167 -0
- package/dist/services/worktreeService.d.ts +24 -0
- package/dist/services/worktreeService.js +220 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/promptDetector.d.ts +1 -0
- package/dist/utils/promptDetector.js +20 -0
- package/dist/utils/promptDetector.test.d.ts +1 -0
- package/dist/utils/promptDetector.test.js +81 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# CCManager - Claude Code Worktree Manager
|
|
2
|
+
|
|
3
|
+
CCManager is a TUI application for managing multiple Claude Code sessions across Git worktrees.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Run multiple Claude Code sessions in parallel across different Git worktrees
|
|
8
|
+
- Switch between sessions seamlessly
|
|
9
|
+
- Visual status indicators for session states (busy, waiting, idle)
|
|
10
|
+
- Create, merge, and delete worktrees from within the app
|
|
11
|
+
- **Configurable keyboard shortcuts**
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
$ npm install
|
|
17
|
+
$ npm run build
|
|
18
|
+
$ npm start
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
$ npx ccmanager
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Keyboard Shortcuts
|
|
28
|
+
|
|
29
|
+
### Default Shortcuts
|
|
30
|
+
|
|
31
|
+
- **Ctrl+E**: Return to menu from active session (by default)
|
|
32
|
+
|
|
33
|
+
### Customizing Shortcuts
|
|
34
|
+
|
|
35
|
+
You can customize keyboard shortcuts in two ways:
|
|
36
|
+
|
|
37
|
+
1. **Through the UI**: Select "Configure Shortcuts" from the main menu
|
|
38
|
+
2. **Configuration file**: Edit `~/.config/ccmanager/shortcuts.json`
|
|
39
|
+
|
|
40
|
+
Example configuration:
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"returnToMenu": {
|
|
44
|
+
"ctrl": true,
|
|
45
|
+
"key": "r"
|
|
46
|
+
},
|
|
47
|
+
"exitApp": {
|
|
48
|
+
"ctrl": true,
|
|
49
|
+
"key": "x"
|
|
50
|
+
},
|
|
51
|
+
"cancel": {
|
|
52
|
+
"key": "escape"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Restrictions
|
|
58
|
+
|
|
59
|
+
- Shortcuts must use a modifier key (Ctrl) except for special keys like Escape
|
|
60
|
+
- The following key combinations are reserved and cannot be used:
|
|
61
|
+
- Ctrl+C
|
|
62
|
+
- Ctrl+D
|
|
63
|
+
- Ctrl+[ (equivalent to Escape)
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Install dependencies
|
|
69
|
+
npm install
|
|
70
|
+
|
|
71
|
+
# Run in development mode
|
|
72
|
+
npm run dev
|
|
73
|
+
|
|
74
|
+
# Build
|
|
75
|
+
npm run build
|
|
76
|
+
|
|
77
|
+
# Run tests
|
|
78
|
+
npm test
|
|
79
|
+
|
|
80
|
+
# Run linter
|
|
81
|
+
npm run lint
|
|
82
|
+
|
|
83
|
+
# Run type checker
|
|
84
|
+
npm run typecheck
|
|
85
|
+
```
|
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
3
|
+
import { spawn } from 'node-pty';
|
|
4
|
+
const App = ({ onReturnToMenu }) => {
|
|
5
|
+
const { exit } = useApp();
|
|
6
|
+
const { stdout } = useStdout();
|
|
7
|
+
const [pty, setPty] = useState(null);
|
|
8
|
+
const [showMenu, setShowMenu] = useState(false);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const ptyProcess = spawn('claude', [], {
|
|
11
|
+
name: 'xterm-color',
|
|
12
|
+
cols: process.stdout.columns || 80,
|
|
13
|
+
rows: process.stdout.rows || 24,
|
|
14
|
+
cwd: process.cwd(),
|
|
15
|
+
env: process.env,
|
|
16
|
+
});
|
|
17
|
+
ptyProcess.onData((data) => {
|
|
18
|
+
if (stdout) {
|
|
19
|
+
stdout.write(data);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
ptyProcess.onExit(() => {
|
|
23
|
+
exit();
|
|
24
|
+
});
|
|
25
|
+
setPty(ptyProcess);
|
|
26
|
+
if (stdout) {
|
|
27
|
+
stdout.on('resize', () => {
|
|
28
|
+
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return () => {
|
|
32
|
+
ptyProcess.kill();
|
|
33
|
+
};
|
|
34
|
+
}, [exit, stdout]);
|
|
35
|
+
useInput((char, key) => {
|
|
36
|
+
if (!pty)
|
|
37
|
+
return;
|
|
38
|
+
if (key.ctrl && char === 'e') {
|
|
39
|
+
if (onReturnToMenu) {
|
|
40
|
+
onReturnToMenu();
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
setShowMenu(true);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// if (char) {
|
|
48
|
+
// pty.write(char);
|
|
49
|
+
// }
|
|
50
|
+
});
|
|
51
|
+
if (showMenu) {
|
|
52
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
53
|
+
React.createElement(Text, { color: "green" }, "Press Ctrl+E to return to menu")));
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
export default App;
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import meow from 'meow';
|
|
5
|
+
import App from './components/App.js';
|
|
6
|
+
meow(`
|
|
7
|
+
Usage
|
|
8
|
+
$ ccmanager
|
|
9
|
+
|
|
10
|
+
Options
|
|
11
|
+
--help Show help
|
|
12
|
+
--version Show version
|
|
13
|
+
|
|
14
|
+
Examples
|
|
15
|
+
$ ccmanager
|
|
16
|
+
`, {
|
|
17
|
+
importMeta: import.meta,
|
|
18
|
+
});
|
|
19
|
+
// Check if we're in a TTY environment
|
|
20
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
21
|
+
console.error('Error: ccmanager must be run in an interactive terminal (TTY)');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
render(React.createElement(App, null));
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useApp, Box, Text } from 'ink';
|
|
3
|
+
import Menu from './Menu.js';
|
|
4
|
+
import Session from './Session.js';
|
|
5
|
+
import NewWorktree from './NewWorktree.js';
|
|
6
|
+
import DeleteWorktree from './DeleteWorktree.js';
|
|
7
|
+
import MergeWorktree from './MergeWorktree.js';
|
|
8
|
+
import ConfigureShortcuts from './ConfigureShortcuts.js';
|
|
9
|
+
import { SessionManager } from '../services/sessionManager.js';
|
|
10
|
+
import { WorktreeService } from '../services/worktreeService.js';
|
|
11
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
12
|
+
const App = () => {
|
|
13
|
+
const { exit } = useApp();
|
|
14
|
+
const [view, setView] = useState('menu');
|
|
15
|
+
const [sessionManager] = useState(() => new SessionManager());
|
|
16
|
+
const [worktreeService] = useState(() => new WorktreeService());
|
|
17
|
+
const [activeSession, setActiveSession] = useState(null);
|
|
18
|
+
const [error, setError] = useState(null);
|
|
19
|
+
const [menuKey, setMenuKey] = useState(0); // Force menu refresh
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// Listen for session exits to return to menu automatically
|
|
22
|
+
const handleSessionExit = (session) => {
|
|
23
|
+
// If the exited session is the active one, return to menu
|
|
24
|
+
setActiveSession(current => {
|
|
25
|
+
if (current && session.id === current.id) {
|
|
26
|
+
// Session that exited is the active one, trigger return to menu
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
setActiveSession(null);
|
|
29
|
+
setError(null);
|
|
30
|
+
setView('menu');
|
|
31
|
+
setMenuKey(prev => prev + 1);
|
|
32
|
+
if (process.stdout.isTTY) {
|
|
33
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
34
|
+
}
|
|
35
|
+
process.stdin.resume();
|
|
36
|
+
process.stdin.setEncoding('utf8');
|
|
37
|
+
}, 0);
|
|
38
|
+
}
|
|
39
|
+
return current;
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
sessionManager.on('sessionExit', handleSessionExit);
|
|
43
|
+
// Cleanup on unmount
|
|
44
|
+
return () => {
|
|
45
|
+
sessionManager.off('sessionExit', handleSessionExit);
|
|
46
|
+
sessionManager.destroy();
|
|
47
|
+
};
|
|
48
|
+
}, [sessionManager]);
|
|
49
|
+
const handleSelectWorktree = (worktree) => {
|
|
50
|
+
// Check if this is the new worktree option
|
|
51
|
+
if (worktree.path === '') {
|
|
52
|
+
setView('new-worktree');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Check if this is the delete worktree option
|
|
56
|
+
if (worktree.path === 'DELETE_WORKTREE') {
|
|
57
|
+
setView('delete-worktree');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Check if this is the merge worktree option
|
|
61
|
+
if (worktree.path === 'MERGE_WORKTREE') {
|
|
62
|
+
setView('merge-worktree');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Check if this is the configure shortcuts option
|
|
66
|
+
if (worktree.path === 'CONFIGURE_SHORTCUTS') {
|
|
67
|
+
setView('configure-shortcuts');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Check if this is the exit application option
|
|
71
|
+
if (worktree.path === 'EXIT_APPLICATION') {
|
|
72
|
+
sessionManager.destroy();
|
|
73
|
+
exit();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Get or create session for this worktree
|
|
77
|
+
let session = sessionManager.getSession(worktree.path);
|
|
78
|
+
if (!session) {
|
|
79
|
+
session = sessionManager.createSession(worktree.path);
|
|
80
|
+
}
|
|
81
|
+
setActiveSession(session);
|
|
82
|
+
setView('session');
|
|
83
|
+
};
|
|
84
|
+
const handleReturnToMenu = () => {
|
|
85
|
+
setActiveSession(null);
|
|
86
|
+
setError(null);
|
|
87
|
+
// Add a small delay to ensure Session cleanup completes
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
setView('menu');
|
|
90
|
+
setMenuKey(prev => prev + 1); // Force menu refresh
|
|
91
|
+
// Clear the screen when returning to menu
|
|
92
|
+
if (process.stdout.isTTY) {
|
|
93
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
94
|
+
}
|
|
95
|
+
// Ensure stdin is in a clean state for Ink components
|
|
96
|
+
if (process.stdin.isTTY) {
|
|
97
|
+
// Flush any pending input to prevent escape sequences from leaking
|
|
98
|
+
process.stdin.read();
|
|
99
|
+
process.stdin.setRawMode(false);
|
|
100
|
+
process.stdin.resume();
|
|
101
|
+
process.stdin.setEncoding('utf8');
|
|
102
|
+
}
|
|
103
|
+
}, 50); // Small delay to ensure proper cleanup
|
|
104
|
+
};
|
|
105
|
+
const handleCreateWorktree = async (path, branch) => {
|
|
106
|
+
setView('creating-worktree');
|
|
107
|
+
setError(null);
|
|
108
|
+
// Create the worktree
|
|
109
|
+
const result = worktreeService.createWorktree(path, branch);
|
|
110
|
+
if (result.success) {
|
|
111
|
+
// Success - return to menu
|
|
112
|
+
handleReturnToMenu();
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Show error
|
|
116
|
+
setError(result.error || 'Failed to create worktree');
|
|
117
|
+
setView('new-worktree');
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const handleCancelNewWorktree = () => {
|
|
121
|
+
handleReturnToMenu();
|
|
122
|
+
};
|
|
123
|
+
const handleDeleteWorktrees = async (worktreePaths) => {
|
|
124
|
+
setView('deleting-worktree');
|
|
125
|
+
setError(null);
|
|
126
|
+
// Delete the worktrees
|
|
127
|
+
let hasError = false;
|
|
128
|
+
for (const path of worktreePaths) {
|
|
129
|
+
const result = worktreeService.deleteWorktree(path);
|
|
130
|
+
if (!result.success) {
|
|
131
|
+
hasError = true;
|
|
132
|
+
setError(result.error || 'Failed to delete worktree');
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!hasError) {
|
|
137
|
+
// Success - return to menu
|
|
138
|
+
handleReturnToMenu();
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Show error
|
|
142
|
+
setView('delete-worktree');
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const handleCancelDeleteWorktree = () => {
|
|
146
|
+
handleReturnToMenu();
|
|
147
|
+
};
|
|
148
|
+
const handleMergeWorktree = async (sourceBranch, targetBranch, deleteAfterMerge, useRebase) => {
|
|
149
|
+
setView('merging-worktree');
|
|
150
|
+
setError(null);
|
|
151
|
+
// Perform the merge
|
|
152
|
+
const mergeResult = worktreeService.mergeWorktree(sourceBranch, targetBranch, useRebase);
|
|
153
|
+
if (mergeResult.success) {
|
|
154
|
+
// If user wants to delete the merged branch
|
|
155
|
+
if (deleteAfterMerge) {
|
|
156
|
+
const deleteResult = worktreeService.deleteWorktreeByBranch(sourceBranch);
|
|
157
|
+
if (!deleteResult.success) {
|
|
158
|
+
setError(deleteResult.error || 'Failed to delete merged worktree');
|
|
159
|
+
setView('merge-worktree');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Success - return to menu
|
|
164
|
+
handleReturnToMenu();
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// Show error
|
|
168
|
+
setError(mergeResult.error || 'Failed to merge branches');
|
|
169
|
+
setView('merge-worktree');
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const handleCancelMergeWorktree = () => {
|
|
173
|
+
handleReturnToMenu();
|
|
174
|
+
};
|
|
175
|
+
if (view === 'menu') {
|
|
176
|
+
return (React.createElement(Menu, { key: menuKey, sessionManager: sessionManager, onSelectWorktree: handleSelectWorktree }));
|
|
177
|
+
}
|
|
178
|
+
if (view === 'session' && activeSession) {
|
|
179
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
180
|
+
React.createElement(Session, { key: activeSession.id, session: activeSession, sessionManager: sessionManager, onReturnToMenu: handleReturnToMenu }),
|
|
181
|
+
React.createElement(Box, { marginTop: 1 },
|
|
182
|
+
React.createElement(Text, { dimColor: true },
|
|
183
|
+
"Press ",
|
|
184
|
+
shortcutManager.getShortcutDisplay('returnToMenu'),
|
|
185
|
+
" to return to menu"))));
|
|
186
|
+
}
|
|
187
|
+
if (view === 'new-worktree') {
|
|
188
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
189
|
+
error && (React.createElement(Box, { marginBottom: 1 },
|
|
190
|
+
React.createElement(Text, { color: "red" },
|
|
191
|
+
"Error: ",
|
|
192
|
+
error))),
|
|
193
|
+
React.createElement(NewWorktree, { onComplete: handleCreateWorktree, onCancel: handleCancelNewWorktree })));
|
|
194
|
+
}
|
|
195
|
+
if (view === 'creating-worktree') {
|
|
196
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
197
|
+
React.createElement(Text, { color: "green" }, "Creating worktree...")));
|
|
198
|
+
}
|
|
199
|
+
if (view === 'delete-worktree') {
|
|
200
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
201
|
+
error && (React.createElement(Box, { marginBottom: 1 },
|
|
202
|
+
React.createElement(Text, { color: "red" },
|
|
203
|
+
"Error: ",
|
|
204
|
+
error))),
|
|
205
|
+
React.createElement(DeleteWorktree, { onComplete: handleDeleteWorktrees, onCancel: handleCancelDeleteWorktree })));
|
|
206
|
+
}
|
|
207
|
+
if (view === 'deleting-worktree') {
|
|
208
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
209
|
+
React.createElement(Text, { color: "red" }, "Deleting worktrees...")));
|
|
210
|
+
}
|
|
211
|
+
if (view === 'merge-worktree') {
|
|
212
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
213
|
+
error && (React.createElement(Box, { marginBottom: 1 },
|
|
214
|
+
React.createElement(Text, { color: "red" },
|
|
215
|
+
"Error: ",
|
|
216
|
+
error))),
|
|
217
|
+
React.createElement(MergeWorktree, { onComplete: handleMergeWorktree, onCancel: handleCancelMergeWorktree })));
|
|
218
|
+
}
|
|
219
|
+
if (view === 'merging-worktree') {
|
|
220
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
221
|
+
React.createElement(Text, { color: "green" }, "Merging worktrees...")));
|
|
222
|
+
}
|
|
223
|
+
if (view === 'configure-shortcuts') {
|
|
224
|
+
return React.createElement(ConfigureShortcuts, { onComplete: handleReturnToMenu });
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
};
|
|
228
|
+
export default App;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
5
|
+
const ConfigureShortcuts = ({ onComplete, }) => {
|
|
6
|
+
const [step, setStep] = useState('menu');
|
|
7
|
+
const [shortcuts, setShortcuts] = useState(shortcutManager.getShortcuts());
|
|
8
|
+
const [editingShortcut, setEditingShortcut] = useState(null);
|
|
9
|
+
const [error, setError] = useState(null);
|
|
10
|
+
const getShortcutDisplayFromState = (key) => {
|
|
11
|
+
const shortcut = shortcuts[key];
|
|
12
|
+
if (!shortcut)
|
|
13
|
+
return 'Not set';
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (shortcut.ctrl)
|
|
16
|
+
parts.push('Ctrl');
|
|
17
|
+
if (shortcut.alt)
|
|
18
|
+
parts.push('Alt');
|
|
19
|
+
if (shortcut.shift)
|
|
20
|
+
parts.push('Shift');
|
|
21
|
+
if (shortcut.key === 'escape') {
|
|
22
|
+
parts.push('Esc');
|
|
23
|
+
}
|
|
24
|
+
else if (shortcut.key) {
|
|
25
|
+
parts.push(shortcut.key.toUpperCase());
|
|
26
|
+
}
|
|
27
|
+
return parts.join('+');
|
|
28
|
+
};
|
|
29
|
+
const shortcutItems = [
|
|
30
|
+
{
|
|
31
|
+
label: `Return to Menu: ${getShortcutDisplayFromState('returnToMenu')}`,
|
|
32
|
+
value: 'returnToMenu',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: '---',
|
|
36
|
+
value: 'separator',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'Save and Exit',
|
|
40
|
+
value: 'save',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: 'Exit without Saving',
|
|
44
|
+
value: 'exit',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
useInput((input, key) => {
|
|
48
|
+
if (step === 'capturing' && editingShortcut) {
|
|
49
|
+
// Capture the key combination
|
|
50
|
+
const newShortcut = {
|
|
51
|
+
key: key.escape ? 'escape' : input || '',
|
|
52
|
+
ctrl: key.ctrl || false,
|
|
53
|
+
alt: false, // Ink doesn't support alt
|
|
54
|
+
shift: false, // Ink doesn't support shift
|
|
55
|
+
};
|
|
56
|
+
// Check for reserved keys
|
|
57
|
+
if (key.ctrl && input === 'c') {
|
|
58
|
+
setError('Ctrl+C is reserved and cannot be used');
|
|
59
|
+
setStep('menu');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (key.ctrl && input === 'd') {
|
|
63
|
+
setError('Ctrl+D is reserved and cannot be used');
|
|
64
|
+
setStep('menu');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.ctrl && input === '[') {
|
|
68
|
+
setError('Ctrl+[ is reserved and cannot be used');
|
|
69
|
+
setStep('menu');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Validate that a modifier is used (except for escape)
|
|
73
|
+
if (!key.escape && !key.ctrl) {
|
|
74
|
+
setError('Shortcuts must use a modifier key (Ctrl)');
|
|
75
|
+
setStep('menu');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
setShortcuts({
|
|
79
|
+
...shortcuts,
|
|
80
|
+
[editingShortcut]: newShortcut,
|
|
81
|
+
});
|
|
82
|
+
setError(null);
|
|
83
|
+
setStep('menu');
|
|
84
|
+
}
|
|
85
|
+
else if (step === 'menu') {
|
|
86
|
+
if (key.escape) {
|
|
87
|
+
onComplete();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const handleSelect = (item) => {
|
|
92
|
+
if (item.value === 'separator') {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (item.value === 'save') {
|
|
96
|
+
const success = shortcutManager.saveShortcuts(shortcuts);
|
|
97
|
+
if (success) {
|
|
98
|
+
onComplete();
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
setError('Failed to save shortcuts');
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (item.value === 'exit') {
|
|
106
|
+
onComplete();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Start editing a shortcut
|
|
110
|
+
setEditingShortcut(item.value);
|
|
111
|
+
setStep('capturing');
|
|
112
|
+
setError(null);
|
|
113
|
+
};
|
|
114
|
+
if (step === 'capturing') {
|
|
115
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
116
|
+
React.createElement(Text, { bold: true, color: "green" },
|
|
117
|
+
"Configure Shortcut: ",
|
|
118
|
+
editingShortcut),
|
|
119
|
+
React.createElement(Box, { marginTop: 1 },
|
|
120
|
+
React.createElement(Text, null, "Press the key combination you want to use")),
|
|
121
|
+
React.createElement(Box, { marginTop: 1 },
|
|
122
|
+
React.createElement(Text, { dimColor: true }, "Note: Shortcuts must use Ctrl as a modifier key")),
|
|
123
|
+
React.createElement(Box, { marginTop: 1 },
|
|
124
|
+
React.createElement(Text, { dimColor: true }, "Reserved: Ctrl+C, Ctrl+D, Ctrl+[ (Esc)"))));
|
|
125
|
+
}
|
|
126
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
127
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
128
|
+
React.createElement(Text, { bold: true, color: "green" }, "Configure Keyboard Shortcuts")),
|
|
129
|
+
error && (React.createElement(Box, { marginBottom: 1 },
|
|
130
|
+
React.createElement(Text, { color: "red" },
|
|
131
|
+
"Error: ",
|
|
132
|
+
error))),
|
|
133
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
134
|
+
React.createElement(Text, { dimColor: true }, "Select a shortcut to change:")),
|
|
135
|
+
React.createElement(SelectInput, { items: shortcutItems, onSelect: handleSelect, isFocused: true }),
|
|
136
|
+
React.createElement(Box, { marginTop: 1 },
|
|
137
|
+
React.createElement(Text, { dimColor: true }, "Press Esc to exit without saving"))));
|
|
138
|
+
};
|
|
139
|
+
export default ConfigureShortcuts;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ConfirmationProps {
|
|
3
|
+
message: string | React.ReactNode;
|
|
4
|
+
onConfirm: () => void;
|
|
5
|
+
onCancel: () => void;
|
|
6
|
+
confirmText?: string;
|
|
7
|
+
cancelText?: string;
|
|
8
|
+
confirmColor?: string;
|
|
9
|
+
cancelColor?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const Confirmation: React.FC<ConfirmationProps>;
|
|
12
|
+
export default Confirmation;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
4
|
+
const Confirmation = ({ message, onConfirm, onCancel, confirmText = 'Yes', cancelText = 'No', confirmColor = 'green', cancelColor = 'red', }) => {
|
|
5
|
+
const [focused, setFocused] = useState(true); // true = confirm, false = cancel
|
|
6
|
+
useInput((input, key) => {
|
|
7
|
+
if (key.leftArrow || key.rightArrow) {
|
|
8
|
+
setFocused(!focused);
|
|
9
|
+
}
|
|
10
|
+
else if (key.return) {
|
|
11
|
+
if (focused) {
|
|
12
|
+
onConfirm();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
onCancel();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else if (shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
19
|
+
onCancel();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
23
|
+
React.createElement(Box, { marginBottom: 1 }, message),
|
|
24
|
+
React.createElement(Box, null,
|
|
25
|
+
React.createElement(Box, { marginRight: 2 },
|
|
26
|
+
React.createElement(Text, { color: focused ? confirmColor : 'white', inverse: focused },
|
|
27
|
+
' ',
|
|
28
|
+
confirmText,
|
|
29
|
+
' ')),
|
|
30
|
+
React.createElement(Box, null,
|
|
31
|
+
React.createElement(Text, { color: !focused ? cancelColor : 'white', inverse: !focused },
|
|
32
|
+
' ',
|
|
33
|
+
cancelText,
|
|
34
|
+
' '))),
|
|
35
|
+
React.createElement(Box, { marginTop: 1 },
|
|
36
|
+
React.createElement(Text, { dimColor: true },
|
|
37
|
+
"Use \u2190 \u2192 to navigate, Enter to select,",
|
|
38
|
+
' ',
|
|
39
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
40
|
+
" to cancel"))));
|
|
41
|
+
};
|
|
42
|
+
export default Confirmation;
|