mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
feat(tui): add arrow key navigation and screen attach
- Add arrow key navigation in StartScreen (up/down to move) - Highlight selected session with blue background - Press Enter to view session in TUI mode - Press 'a' to attach directly to GNU screen session - Attach exits TUI and inherits stdio for full terminal control Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
||||
import { execSync, spawnSync } from 'child_process';
|
||||
import { StartScreen } from './components/StartScreen.js';
|
||||
import { TabBar } from './components/TabBar.js';
|
||||
import { TerminalView } from './components/TerminalView.js';
|
||||
@@ -176,6 +177,28 @@ export function App(): React.ReactElement {
|
||||
}
|
||||
}, [createSession]);
|
||||
|
||||
/**
|
||||
* Attach directly to a screen session
|
||||
* This exits the TUI and attaches to GNU screen
|
||||
*/
|
||||
const handleAttachSession = useCallback((session: ScreenSession) => {
|
||||
// Clear screen and restore terminal
|
||||
process.stdout.write('\x1b[2J\x1b[H');
|
||||
console.log(`Attaching to screen: ${session.screenName}`);
|
||||
console.log('Press Ctrl+A D to detach and return to terminal\n');
|
||||
|
||||
// Use spawnSync to attach to screen (inherits stdio)
|
||||
const result = spawnSync('screen', ['-r', session.screenName], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
// After detaching, exit the TUI
|
||||
if (result.status === 0) {
|
||||
console.log('\nDetached from screen. Run "claudeman tui" to return.');
|
||||
}
|
||||
exit();
|
||||
}, [exit]);
|
||||
|
||||
// Render help overlay if shown
|
||||
if (showHelp) {
|
||||
return <HelpOverlay onClose={() => setShowHelp(false)} />;
|
||||
@@ -187,6 +210,7 @@ export function App(): React.ReactElement {
|
||||
<StartScreen
|
||||
sessions={sessions}
|
||||
onSelectSession={handleSelectSession}
|
||||
onAttachSession={handleAttachSession}
|
||||
onCreateSession={handleCreateSession}
|
||||
onRefresh={refreshSessions}
|
||||
onExit={exit}
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
* Initial screen shown when TUI launches. Displays:
|
||||
* - List of existing screen sessions from ~/.claudeman/screens.json
|
||||
* - Session status (alive/dead), runtime, mode
|
||||
* - Options to select, create, or refresh sessions
|
||||
* - Arrow key navigation with highlighted selection
|
||||
* - Options to select, create, attach, or refresh sessions
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import type { ScreenSession } from '../../types.js';
|
||||
|
||||
interface StartScreenProps {
|
||||
sessions: ScreenSession[];
|
||||
onSelectSession: (session: ScreenSession) => void;
|
||||
onAttachSession: (session: ScreenSession) => void;
|
||||
onCreateSession: () => void;
|
||||
onRefresh: () => void;
|
||||
onExit: () => void;
|
||||
@@ -41,16 +43,43 @@ function formatDuration(ms: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start screen component showing session list
|
||||
* Start screen component showing session list with arrow key navigation
|
||||
*/
|
||||
export function StartScreen({
|
||||
sessions,
|
||||
onSelectSession,
|
||||
onAttachSession,
|
||||
onCreateSession,
|
||||
onRefresh,
|
||||
onExit,
|
||||
}: StartScreenProps): React.ReactElement {
|
||||
const now = Date.now();
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
// Handle keyboard input for navigation
|
||||
useInput((input, key) => {
|
||||
// Arrow key navigation
|
||||
if (key.upArrow && sessions.length > 0) {
|
||||
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : sessions.length - 1));
|
||||
return;
|
||||
}
|
||||
if (key.downArrow && sessions.length > 0) {
|
||||
setSelectedIndex((prev) => (prev < sessions.length - 1 ? prev + 1 : 0));
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter to view session in TUI
|
||||
if (key.return && sessions.length > 0) {
|
||||
onSelectSession(sessions[selectedIndex]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 'a' to attach directly to screen
|
||||
if (input === 'a' && sessions.length > 0 && sessions[selectedIndex].attached) {
|
||||
onAttachSession(sessions[selectedIndex]);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
@@ -82,7 +111,7 @@ export function StartScreen({
|
||||
{/* Table header */}
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>
|
||||
<Text color="gray">{' # '}</Text>
|
||||
<Text color="gray">{' '}</Text>
|
||||
<Text>{'NAME'.padEnd(22)}</Text>
|
||||
<Text>{'RUNTIME'.padEnd(12)}</Text>
|
||||
<Text>{'STATUS'.padEnd(10)}</Text>
|
||||
@@ -97,19 +126,31 @@ export function StartScreen({
|
||||
const statusIcon = session.attached ? '\u25CF' : '\u25CB';
|
||||
const statusText = session.attached ? 'alive' : 'dead';
|
||||
const name = (session.name || 'unnamed').slice(0, 20);
|
||||
const isSelected = index === selectedIndex;
|
||||
|
||||
return (
|
||||
<Box key={session.sessionId}>
|
||||
<Text>
|
||||
<Text color="cyan">[{index + 1}]</Text>
|
||||
<Text> </Text>
|
||||
<Text>{name.padEnd(22)}</Text>
|
||||
<Text dimColor>{runtime.padEnd(12)}</Text>
|
||||
<Text color={statusColor}>
|
||||
{statusIcon} {statusText.padEnd(8)}
|
||||
{isSelected ? (
|
||||
<Text backgroundColor="blue" color="white">
|
||||
<Text color="cyan" bold>{' \u25B6 '}</Text>
|
||||
<Text bold>{name.padEnd(22)}</Text>
|
||||
<Text>{runtime.padEnd(12)}</Text>
|
||||
<Text color={statusColor}>
|
||||
{statusIcon} {statusText.padEnd(8)}
|
||||
</Text>
|
||||
<Text>{session.mode.padEnd(10)}</Text>
|
||||
</Text>
|
||||
<Text>{session.mode.padEnd(10)}</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<Text>
|
||||
<Text color="gray">{' '}</Text>
|
||||
<Text>{name.padEnd(22)}</Text>
|
||||
<Text dimColor>{runtime.padEnd(12)}</Text>
|
||||
<Text color={statusColor}>
|
||||
{statusIcon} {statusText.padEnd(8)}
|
||||
</Text>
|
||||
<Text>{session.mode.padEnd(10)}</Text>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
@@ -121,15 +162,17 @@ export function StartScreen({
|
||||
<Box borderStyle="single" borderColor="gray" paddingX={1}>
|
||||
<Text>
|
||||
<Text color="green">[n]</Text>
|
||||
<Text> New session </Text>
|
||||
<Text color="green">[1-9]</Text>
|
||||
<Text> Select </Text>
|
||||
<Text> New </Text>
|
||||
<Text color="green">[{'\u2191\u2193'}]</Text>
|
||||
<Text> Navigate </Text>
|
||||
<Text color="green">[Enter]</Text>
|
||||
<Text> View </Text>
|
||||
<Text color="green">[a]</Text>
|
||||
<Text> Attach </Text>
|
||||
<Text color="green">[r]</Text>
|
||||
<Text> Refresh </Text>
|
||||
<Text color="yellow">[q]</Text>
|
||||
<Text> Quit </Text>
|
||||
<Text color="blue">[?]</Text>
|
||||
<Text> Help</Text>
|
||||
<Text> Quit</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user