Skip to content

Commit 486bbb4

Browse files
author
shmuel hizmi
committed
master
1 parent b9ab849 commit 486bbb4

File tree

4 files changed

+100
-13
lines changed

4 files changed

+100
-13
lines changed

packages/wmux-client-terminal/src/components/FocusContext.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,35 @@ interface PrefixContextValue {
99
/** True when Ctrl+B was pressed and control mode is active */
1010
readonly prefixRef: MutableRefObject<boolean>;
1111
readonly searchOpenRef: MutableRefObject<boolean>;
12+
readonly copyModeRef: MutableRefObject<boolean>;
1213
readonly activeTabId: string;
14+
readonly copyMode: boolean;
1315
}
1416

1517
const PrefixContext = createContext<PrefixContextValue>({
1618
prefixRef: { current: false },
1719
searchOpenRef: { current: false },
20+
copyModeRef: { current: false },
1821
activeTabId: "",
22+
copyMode: false,
1923
});
2024

2125
export const PrefixProvider = ({
2226
prefixRef,
2327
searchOpenRef,
28+
copyModeRef,
2429
activeTabId,
30+
copyMode,
2531
children,
2632
}: {
2733
readonly prefixRef: MutableRefObject<boolean>;
2834
readonly searchOpenRef: MutableRefObject<boolean>;
35+
readonly copyModeRef: MutableRefObject<boolean>;
2936
readonly activeTabId: string;
37+
readonly copyMode: boolean;
3038
readonly children: ReactNode;
3139
}): ReactNode => (
32-
<PrefixContext.Provider value={{ prefixRef, searchOpenRef, activeTabId }}>
40+
<PrefixContext.Provider value={{ prefixRef, searchOpenRef, copyModeRef, activeTabId, copyMode }}>
3341
{children}
3442
</PrefixContext.Provider>
3543
);

packages/wmux-client-terminal/src/components/StatusBar.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,24 @@ const WARN = "#ffd60a";
77

88
interface StatusBarProps {
99
readonly prefixActive: boolean;
10+
readonly copyMode: boolean;
1011
}
1112

12-
export const StatusBar = ({ prefixActive }: StatusBarProps): ReactNode => (
13+
export const StatusBar = ({ prefixActive, copyMode }: StatusBarProps): ReactNode => (
1314
<box height={1} flexDirection="row" paddingX={1} gap={2}>
14-
{prefixActive ? (
15+
{copyMode ? (
16+
<>
17+
<text fg={WARN}>
18+
<strong>COPY</strong>
19+
</text>
20+
<text fg={MUTED}>
21+
select text with mouse
22+
</text>
23+
<text fg={MUTED}>
24+
<span fg={ACCENT}>esc</span> exit
25+
</text>
26+
</>
27+
) : prefixActive ? (
1528
<>
1629
<text fg={WARN}>
1730
<strong>wmux</strong>
@@ -34,6 +47,9 @@ export const StatusBar = ({ prefixActive }: StatusBarProps): ReactNode => (
3447
<text fg={MUTED}>
3548
<span fg={ACCENT}>w</span> web
3649
</text>
50+
<text fg={MUTED}>
51+
<span fg={ACCENT}>c</span> copy
52+
</text>
3753
<text fg={MUTED}>
3854
<span fg={ACCENT}>{"\u23ce"}</span> exit
3955
</text>
@@ -42,9 +58,14 @@ export const StatusBar = ({ prefixActive }: StatusBarProps): ReactNode => (
4258
</text>
4359
</>
4460
) : (
45-
<text fg={MUTED}>
46-
<span fg={ACCENT}>^B</span> enter wmux controls
47-
</text>
61+
<>
62+
<text fg={MUTED}>
63+
<span fg={ACCENT}>^B</span> controls
64+
</text>
65+
<text fg={MUTED}>
66+
<span fg={ACCENT}>^Cc</span> copy
67+
</text>
68+
</>
4869
)}
4970
</box>
5071
);

packages/wmux-client-terminal/src/components/WmuxApp.tsx

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export const WmuxApp = (props: {
8181
const [prefixVisible, setPrefixVisible] = useState(false);
8282
const [searchOpen, setSearchOpen] = useState(false);
8383
const searchOpenRef = useRef(false);
84+
const [copyMode, setCopyMode] = useState(false);
85+
const copyModeRef = useRef(false);
86+
const copyPrefixRef = useRef(false);
87+
const copyPrefixTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
8488

8589
const activatePrefix = useCallback(() => {
8690
prefixRef.current = true;
@@ -97,6 +101,21 @@ export const WmuxApp = (props: {
97101
setSearchOpen(false);
98102
}, []);
99103

104+
const enterCopyMode = useCallback(() => {
105+
copyModeRef.current = true;
106+
setCopyMode(true);
107+
copyPrefixRef.current = false;
108+
if (copyPrefixTimerRef.current) {
109+
clearTimeout(copyPrefixTimerRef.current);
110+
copyPrefixTimerRef.current = null;
111+
}
112+
}, []);
113+
114+
const exitCopyMode = useCallback(() => {
115+
copyModeRef.current = false;
116+
setCopyMode(false);
117+
}, []);
118+
100119
const allNavItems = useMemo(() => buildAllNavigationItems(categories), [categories]);
101120

102121
const registry = useMemo(() => {
@@ -131,6 +150,37 @@ export const WmuxApp = (props: {
131150
// ── Search overlay handles its own keys ──────────────
132151
if (searchOpenRef.current) return;
133152

153+
// ── Copy mode: only Escape exits ────────────────────
154+
if (copyModeRef.current) {
155+
if (key.name === "escape") exitCopyMode();
156+
return;
157+
}
158+
159+
// ── Copy prefix: Ctrl+C was pressed, waiting for 'c' ──
160+
if (copyPrefixRef.current) {
161+
copyPrefixRef.current = false;
162+
if (copyPrefixTimerRef.current) {
163+
clearTimeout(copyPrefixTimerRef.current);
164+
copyPrefixTimerRef.current = null;
165+
}
166+
if (key.name === "c" && !key.ctrl) {
167+
enterCopyMode();
168+
return;
169+
}
170+
// Non-'c' key: fall through to normal handling
171+
}
172+
173+
// ── Ctrl+C: set copy prefix (deferred so Ctrl+C reaches PTY) ──
174+
if (key.ctrl && key.name === "c" && !prefixRef.current) {
175+
setTimeout(() => { copyPrefixRef.current = true; }, 0);
176+
if (copyPrefixTimerRef.current) clearTimeout(copyPrefixTimerRef.current);
177+
copyPrefixTimerRef.current = setTimeout(() => {
178+
copyPrefixRef.current = false;
179+
copyPrefixTimerRef.current = null;
180+
}, 500);
181+
return;
182+
}
183+
134184
// ── Ctrl+B: toggle control mode ─────────────────────
135185
if (key.ctrl && key.name === "b") {
136186
if (prefixRef.current) {
@@ -214,6 +264,13 @@ export const WmuxApp = (props: {
214264
return;
215265
}
216266

267+
// Copy mode
268+
if (key.name === "c") {
269+
consumePrefix();
270+
enterCopyMode();
271+
return;
272+
}
273+
217274
// Exit control mode (+ start process if idle)
218275
if (key.name === "enter" || key.name === "return") {
219276
const activeCat = categories.find((c) => c.name === activeCategory);
@@ -244,7 +301,7 @@ export const WmuxApp = (props: {
244301
const activeChild = registry.get(activeTabId);
245302

246303
return (
247-
<PrefixProvider prefixRef={prefixRef} searchOpenRef={searchOpenRef} activeTabId={activeTabId}>
304+
<PrefixProvider prefixRef={prefixRef} searchOpenRef={searchOpenRef} copyModeRef={copyModeRef} activeTabId={activeTabId} copyMode={copyMode}>
248305
<box
249306
flexDirection="column"
250307
width={width}
@@ -299,7 +356,7 @@ export const WmuxApp = (props: {
299356
<box height={1} backgroundColor={BORDER_COLOR} />
300357

301358
{/* Status bar */}
302-
<StatusBar prefixActive={prefixVisible} />
359+
<StatusBar prefixActive={prefixVisible} copyMode={copyMode} />
303360
</box>
304361
</PrefixProvider>
305362
);

packages/wmux-client-terminal/src/components/WmuxTerminal.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ const renderSegment = (seg: StyledSegment, key: number): ReactNode => {
3232
return <span key={key} fg={seg.fg} bg={seg.bg}>{inner}</span>;
3333
};
3434

35-
const renderLine = (line: StyledLine, key: number): ReactNode => (
36-
<text key={key}>
35+
const renderLine = (line: StyledLine, key: number, selectable?: boolean): ReactNode => (
36+
<text key={key} selectable={selectable}>
3737
{line.segments.length === 0
3838
? " "
3939
: line.segments.map((seg, j) => renderSegment(seg, j))}
@@ -44,7 +44,7 @@ export const WmuxTerminal = (props: WmuxTerminalProps): ReactNode => {
4444
const { id, output, status } = props;
4545
const sendInput = props.onInput.mutate;
4646
const sendResize = props.onResize.mutate;
47-
const { prefixRef, searchOpenRef, activeTabId } = usePrefixContext();
47+
const { prefixRef, searchOpenRef, copyModeRef, activeTabId, copyMode } = usePrefixContext();
4848

4949
const [lines, setLines] = useState<readonly StyledLine[]>([]);
5050
const { width, height } = useTerminalDimensions();
@@ -67,6 +67,7 @@ export const WmuxTerminal = (props: WmuxTerminalProps): ReactNode => {
6767
if (key.ctrl && key.name === "b") return; // prefix key, handled by WmuxApp
6868
if (prefixRef.current) return; // control mode active, handled by WmuxApp
6969
if (searchOpenRef.current) return; // search overlay is open
70+
if (copyModeRef.current) return; // copy mode active
7071

7172
const data = key.sequence;
7273
if (data) {
@@ -116,8 +117,8 @@ export const WmuxTerminal = (props: WmuxTerminalProps): ReactNode => {
116117
</text>
117118
</box>
118119
) : null}
119-
<scrollbox flexGrow={1} stickyScroll stickyStart="bottom">
120-
{lines.map((line, i) => renderLine(line, i))}
120+
<scrollbox flexGrow={1} stickyScroll={!copyMode} stickyStart="bottom">
121+
{lines.map((line, i) => renderLine(line, i, copyMode || undefined))}
121122
</scrollbox>
122123
</box>
123124
);

0 commit comments

Comments
 (0)