Skip to content
This repository was archived by the owner on Jan 6, 2025. It is now read-only.

Commit 5285593

Browse files
close tab when shell exits (#48)
1 parent fba7269 commit 5285593

File tree

8 files changed

+66
-17
lines changed

8 files changed

+66
-17
lines changed

apps/app/src/nativeBridge/modules/terminalModule.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ class PTYInstance {
7070
public onData(handler: (_: string) => void): void {
7171
this.ptyProcess.onData(handler);
7272
}
73+
74+
public onExit(handler: () => void): void {
75+
this.ptyProcess.onExit(handler);
76+
}
7377
}
7478

7579
@nativeBridgeModule('terminal')
@@ -78,7 +82,7 @@ export class TerminalModule extends NativeBridgeModule {
7882

7983
@moduleFunction()
8084
public async createTerminalIfNotExist(
81-
_mainWindow: BrowserWindow,
85+
mainWindow: BrowserWindow,
8286
id: string,
8387
cols: number,
8488
rows: number,
@@ -97,7 +101,10 @@ export class TerminalModule extends NativeBridgeModule {
97101
startupDirectory,
98102
);
99103
ptyInstance.onData((data: string) => {
100-
this.onData(_mainWindow, ptyInstance.id, data);
104+
this.onData(mainWindow, ptyInstance.id, data);
105+
});
106+
ptyInstance.onExit(() => {
107+
this.onExit(mainWindow, ptyInstance.id);
101108
});
102109

103110
this.ptyInstances[ptyInstance.id] = ptyInstance;
@@ -149,4 +156,9 @@ export class TerminalModule extends NativeBridgeModule {
149156
public onData(_mainWindow: BrowserWindow, _id: string, _data: string): void {
150157
return;
151158
}
159+
160+
@moduleEvent('on')
161+
public onExit(_mainWindow: BrowserWindow, _id: string): void {
162+
return;
163+
}
152164
}

apps/app/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"**/*.tsx"
1111
],
1212
"exclude": [
13+
"dist",
1314
"node_modules"
1415
]
1516
}

apps/terminal/app/page.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,21 @@ const Page = () => {
4949
setTabId(newTabId);
5050
}, [userTabs, config]);
5151

52-
const closeTab = useCallback(() => {
53-
const newTabs = userTabs.filter((t) => t.tabId !== tabId);
54-
setUserTabs(newTabs);
55-
setTabId(_.max(newTabs.map((t) => t.tabId)) || 0);
56-
}, [userTabs, tabId]);
52+
const closeTab = useCallback(
53+
(targetTabId: number) => {
54+
const newTabs = userTabs.filter((t) => t.tabId !== targetTabId);
55+
if (newTabs.length === 0) {
56+
window.TerminalOne?.app.quit();
57+
} else {
58+
setUserTabs(newTabs);
59+
setTabId(_.max(newTabs.map((t) => t.tabId)) || 0);
60+
}
61+
},
62+
[userTabs],
63+
);
64+
const closeCurrentTab = useCallback(() => {
65+
closeTab(tabId);
66+
}, [closeTab, tabId]);
5767

5868
const nextTab = useCallback(() => {
5969
const currentTabIndex = userTabs.findIndex((t) => t.tabId === tabId);
@@ -126,7 +136,7 @@ const Page = () => {
126136

127137
useEffect(() => {
128138
commands.on('createTab', createTab);
129-
commands.on('closeTab', closeTab);
139+
commands.on('closeTab', closeCurrentTab);
130140
commands.on('nextTab', nextTab);
131141
commands.on('previousTab', previousTab);
132142
commands.on('tab1', switchToTab1);
@@ -141,7 +151,7 @@ const Page = () => {
141151

142152
return () => {
143153
commands.off('createTab', createTab);
144-
commands.off('closeTab', closeTab);
154+
commands.off('closeTab', closeCurrentTab);
145155
commands.off('nextTab', nextTab);
146156
commands.off('previousTab', previousTab);
147157
commands.off('tab1', switchToTab1);
@@ -157,7 +167,7 @@ const Page = () => {
157167
}, [
158168
commands,
159169
createTab,
160-
closeTab,
170+
closeCurrentTab,
161171
nextTab,
162172
previousTab,
163173
switchToTab1,
@@ -229,7 +239,7 @@ const Page = () => {
229239
<button
230240
className="btn btn-ghost btn-square btn-xs opacity-50 hover:bg-transparent hover:opacity-100 ml-2"
231241
onClick={() => {
232-
closeTab();
242+
closeTab(tabId);
233243
}}
234244
>
235245
<FiX />
@@ -265,6 +275,9 @@ const Page = () => {
265275
key={userTab.tabId}
266276
active={tabId === userTab.tabId}
267277
tabId={userTab.tabId}
278+
close={() => {
279+
closeTab(userTab.tabId);
280+
}}
268281
/>
269282
);
270283
})}

apps/terminal/components/Tab/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@ import _ from 'lodash';
33
import { TabContextProvider } from '../../hooks/TabContext';
44
import { TerminalTreeNode } from '../TerminalTreeNode';
55

6-
const Tab = ({ tabId, active }: { tabId: number; active: boolean }) => {
6+
const Tab = ({
7+
tabId,
8+
active,
9+
close,
10+
}: {
11+
tabId: number;
12+
active: boolean;
13+
close: () => void;
14+
}) => {
715
return (
816
<div
917
className={`w-full h-full absolute ${active ? 'visible' : 'invisible'}`}
1018
>
11-
<TabContextProvider active={active} tabId={tabId}>
19+
<TabContextProvider active={active} tabId={tabId} close={close}>
1220
<TerminalTreeNode data={null} />
1321
</TabContextProvider>
1422
</div>

apps/terminal/components/Terminal/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const Terminal = ({
2424
const [shellName, setShellName] = useState<string | null | undefined>(
2525
undefined,
2626
);
27-
const { handleKey } = useKeybindContext();
27+
const { handleKey, commands } = useKeybindContext();
2828
const terminalRef = useRef<HTMLDivElement>(null);
2929
const xtermRef = useRef<XTerm | null>(null);
3030
const { root, activeTerminalId, onTerminalActive, onTerminalCreated } =
@@ -129,6 +129,12 @@ const Terminal = ({
129129
}
130130
xterm.write(data);
131131
});
132+
window.TerminalOne?.terminal?.onExit((_e, id: string) => {
133+
if (id !== terminalId) {
134+
return;
135+
}
136+
commands.emit('closePane', terminalId);
137+
});
132138

133139
// make backend pty size consistent with xterm on the frontend
134140
fitAddon.fit();
@@ -194,6 +200,7 @@ const Terminal = ({
194200
terminalId,
195201
config,
196202
handleKey,
203+
commands,
197204
onTerminalActive,
198205
onTerminalCreated,
199206
]);

apps/terminal/hooks/TabContext.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const lookupTerminalById = (root: TerminalTreeNodeData, terminalId: string) => {
3535
const createCloseHandler = (
3636
root: TerminalTreeNodeData,
3737
setRoot: (root: TerminalTreeNodeData) => void,
38+
closeRoot: () => void,
3839
) => {
3940
return (terminalId: string) => {
4041
const node = lookupTerminalById(root, terminalId);
@@ -44,6 +45,7 @@ const createCloseHandler = (
4445

4546
const replaceTarget = node.parent;
4647
if (!replaceTarget) {
48+
closeRoot();
4749
return;
4850
}
4951
if (replaceTarget.nodeType === 'terminal') {
@@ -248,6 +250,7 @@ export const TabContextProvider = (
248250
props: React.PropsWithChildren<{
249251
active: boolean;
250252
tabId: number;
253+
close: () => void;
251254
}>,
252255
) => {
253256
const { commands } = useKeybindContext();
@@ -293,7 +296,9 @@ export const TabContextProvider = (
293296
onTerminalActive,
294297
'right',
295298
);
296-
const closePane = createCloseHandler(root, setRoot);
299+
const closePane = createCloseHandler(root, setRoot, () => {
300+
props.close();
301+
});
297302

298303
commands.on('splitVertical', splitVertical);
299304
commands.on('splitHorizontal', splitHorizontal);
@@ -316,7 +321,7 @@ export const TabContextProvider = (
316321

317322
commands.off('closePane', closePane);
318323
};
319-
}, [commands, onTerminalActive, root]);
324+
}, [commands, onTerminalActive, root, props]);
320325

321326
return (
322327
<TabContext.Provider

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@terminalone/monorepo",
33
"productName": "Terminal One",
4-
"version": "1.4.2",
4+
"version": "1.5.0",
55
"description": "A fast, elegant and intelligent cross-platform terminal.",
66
"author": "atinylittleshell <shell@atinylittleshell.me>",
77
"license": "MIT",

packages/types/nativeBridge.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ export type INativeBridge = {
7676
_data: string,
7777
) => void,
7878
) => void;
79+
onExit: (
80+
_callback: (_event: ElectronOpaqueEvent, _id: string) => void,
81+
) => void;
7982
};
8083
config: {
8184
getConfig: () => Promise<ResolvedConfig>;

0 commit comments

Comments
 (0)