Skip to content

Commit 4be39f2

Browse files
authored
Restore terminal tabs (#1592)
* change default font size and inactive style of terminal * feat: save pid of opened terminal and restore it save the pid of terminal tab in localstorage and restore when app reopened * fix: font issue with terminal * fix(terminal): keep previous active tab when restoring sessions
1 parent 2b5c958 commit 4be39f2

File tree

4 files changed

+160
-28
lines changed

4 files changed

+160
-28
lines changed

src/components/terminal/terminalDefaults.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import appSettings from "lib/settings";
22

33
export const DEFAULT_TERMINAL_SETTINGS = {
4-
fontSize: 14,
4+
fontSize: 12,
55
fontFamily: "MesloLGS NF Regular",
66
fontWeight: "normal",
77
cursorBlink: true,
88
cursorStyle: "block",
9-
cursorInactiveStyle: "underline",
9+
cursorInactiveStyle: "outline",
1010
scrollback: 1000,
1111
theme: "dark",
1212
tabStopWidth: 4,

src/components/terminal/terminalManager.js

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,145 @@ import EditorFile from "lib/editorFile";
77
import TerminalComponent from "./terminal";
88
import "@xterm/xterm/css/xterm.css";
99
import toast from "components/toast";
10+
import helpers from "utils/helpers";
11+
12+
const TERMINAL_SESSION_STORAGE_KEY = "acodeTerminalSessions";
1013

1114
class TerminalManager {
1215
constructor() {
1316
this.terminals = new Map();
1417
this.terminalCounter = 0;
1518
}
1619

20+
getPersistedSessions() {
21+
try {
22+
const stored = helpers.parseJSON(
23+
localStorage.getItem(TERMINAL_SESSION_STORAGE_KEY),
24+
);
25+
if (!Array.isArray(stored)) return [];
26+
return stored
27+
.map((entry) => {
28+
if (!entry) return null;
29+
if (typeof entry === "string") {
30+
return { pid: entry, name: `Terminal ${entry}` };
31+
}
32+
if (typeof entry === "object" && entry.pid) {
33+
const pid = String(entry.pid);
34+
return {
35+
pid,
36+
name: entry.name || `Terminal ${pid}`,
37+
};
38+
}
39+
return null;
40+
})
41+
.filter(Boolean);
42+
} catch (error) {
43+
console.error("Failed to read persisted terminal sessions:", error);
44+
return [];
45+
}
46+
}
47+
48+
savePersistedSessions(sessions) {
49+
try {
50+
localStorage.setItem(
51+
TERMINAL_SESSION_STORAGE_KEY,
52+
JSON.stringify(sessions),
53+
);
54+
} catch (error) {
55+
console.error("Failed to persist terminal sessions:", error);
56+
}
57+
}
58+
59+
persistTerminalSession(pid, name) {
60+
if (!pid) return;
61+
62+
const pidStr = String(pid);
63+
const sessions = this.getPersistedSessions();
64+
const existingIndex = sessions.findIndex(
65+
(session) => session.pid === pidStr,
66+
);
67+
const sessionData = {
68+
pid: pidStr,
69+
name: name || `Terminal ${pidStr}`,
70+
};
71+
72+
if (existingIndex >= 0) {
73+
sessions[existingIndex] = {
74+
...sessions[existingIndex],
75+
...sessionData,
76+
};
77+
} else {
78+
sessions.push(sessionData);
79+
}
80+
81+
this.savePersistedSessions(sessions);
82+
}
83+
84+
removePersistedSession(pid) {
85+
if (!pid) return;
86+
87+
const pidStr = String(pid);
88+
const sessions = this.getPersistedSessions();
89+
const nextSessions = sessions.filter((session) => session.pid !== pidStr);
90+
91+
if (nextSessions.length !== sessions.length) {
92+
this.savePersistedSessions(nextSessions);
93+
}
94+
}
95+
96+
async restorePersistedSessions() {
97+
const sessions = this.getPersistedSessions();
98+
if (!sessions.length) return;
99+
100+
const manager = window.editorManager;
101+
const activeFileId = manager?.activeFile?.id;
102+
const restoredTerminals = [];
103+
104+
for (const session of sessions) {
105+
if (!session?.pid) continue;
106+
if (this.terminals.has(session.pid)) continue;
107+
108+
try {
109+
const instance = await this.createServerTerminal({
110+
pid: session.pid,
111+
name: session.name,
112+
reconnecting: true,
113+
render: false,
114+
});
115+
if (instance) restoredTerminals.push(instance);
116+
} catch (error) {
117+
console.error(
118+
`Failed to restore terminal session ${session.pid}:`,
119+
error,
120+
);
121+
this.removePersistedSession(session.pid);
122+
}
123+
}
124+
125+
if (activeFileId && manager?.getFile) {
126+
const fileToRestore = manager.getFile(activeFileId, "id");
127+
fileToRestore?.makeActive();
128+
} else if (!manager?.activeFile && restoredTerminals.length) {
129+
restoredTerminals[0]?.file?.makeActive();
130+
}
131+
}
132+
17133
/**
18134
* Create a new terminal session
19135
* @param {object} options - Terminal options
20136
* @returns {Promise<object>} Terminal instance info
21137
*/
22138
async createTerminal(options = {}) {
23139
try {
140+
const { render, serverMode, ...terminalOptions } = options;
141+
const shouldRender = render !== false;
142+
const isServerMode = serverMode !== false;
143+
24144
const terminalId = `terminal_${++this.terminalCounter}`;
25145
const terminalName = options.name || `Terminal ${this.terminalCounter}`;
26146

27147
// Check if terminal is installed before proceeding
28-
if (options.serverMode !== false) {
148+
if (isServerMode) {
29149
const installationResult = await this.checkAndInstallTerminal();
30150
if (!installationResult.success) {
31151
throw new Error(installationResult.error);
@@ -34,8 +154,8 @@ class TerminalManager {
34154

35155
// Create terminal component
36156
const terminalComponent = new TerminalComponent({
37-
serverMode: options.serverMode !== false,
38-
...options,
157+
serverMode: isServerMode,
158+
...terminalOptions,
39159
});
40160

41161
// Create container
@@ -59,7 +179,7 @@ class TerminalManager {
59179
type: "terminal",
60180
content: terminalContainer,
61181
tabIcon: "licons terminal",
62-
render: true,
182+
render: shouldRender,
63183
});
64184

65185
// Wait for tab creation and setup
@@ -71,7 +191,7 @@ class TerminalManager {
71191

72192
// Connect to session if in server mode
73193
if (terminalComponent.serverMode) {
74-
await terminalComponent.connectToSession();
194+
await terminalComponent.connectToSession(terminalOptions.pid);
75195
} else {
76196
// For local mode, just write a welcome message
77197
terminalComponent.write(
@@ -98,6 +218,10 @@ class TerminalManager {
98218
};
99219

100220
this.terminals.set(uniqueId, instance);
221+
222+
if (terminalComponent.serverMode && terminalComponent.pid) {
223+
this.persistTerminalSession(terminalComponent.pid, terminalName);
224+
}
101225
resolve(instance);
102226
} catch (error) {
103227
console.error("Failed to initialize terminal:", error);
@@ -368,6 +492,10 @@ class TerminalManager {
368492
const formattedTitle = `Terminal ${this.terminalCounter} - ${title}`;
369493
terminalFile.filename = formattedTitle;
370494

495+
if (terminalComponent.serverMode && terminalComponent.pid) {
496+
this.persistTerminalSession(terminalComponent.pid, formattedTitle);
497+
}
498+
371499
// Refresh the header subtitle if this terminal is active
372500
if (
373501
editorManager.activeFile &&
@@ -421,6 +549,10 @@ class TerminalManager {
421549
if (!terminal) return;
422550

423551
try {
552+
if (terminal.component.serverMode && terminal.component.pid) {
553+
this.removePersistedSession(terminal.component.pid);
554+
}
555+
424556
// Cleanup resize observer
425557
if (terminal.file._resizeObserver) {
426558
terminal.file._resizeObserver.disconnect();

src/lib/fonts.js

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import robotoMono from "../res/fonts/RobotoMono.ttf";
99
const fonts = new Map();
1010
const customFontNames = new Set();
1111
const CUSTOM_FONTS_KEY = "custom_fonts";
12+
const FONT_FACE_STYLE_ID = "font-face-style";
13+
const EDITOR_STYLE_ID = "editor-font-style";
1214

1315
add(
1416
"Fira Code",
@@ -188,29 +190,14 @@ function has(name) {
188190
async function setFont(name) {
189191
loader.showTitleLoader();
190192
try {
191-
const $style = tag.get("style#font-style") ?? (
192-
<style id="font-style"></style>
193-
);
194-
let css = get(name);
195-
196-
// Get all URL font references
197-
const urls = [...css.matchAll(/url\((.*?)\)/g)].map((match) => match[1]);
198-
199-
urls?.map(async (url) => {
200-
if (!/^https?/.test(url)) return;
201-
if (/^https?:\/\/localhost/.test(url)) return;
202-
const fontFile = await downloadFont(name, url);
203-
const internalUrl = await helpers.toInternalUri(fontFile);
204-
css = css.replace(url, internalUrl);
205-
}),
206-
($style.textContent = `${css}
207-
.editor-container.ace_editor{
193+
await loadFont(name);
194+
const $style = ensureStyleElement(EDITOR_STYLE_ID);
195+
$style.textContent = `.editor-container.ace_editor{
208196
font-family: "${name}", NotoMono, Monaco, MONOSPACE !important;
209197
}
210198
.ace_text{
211199
font-family: inherit !important;
212-
}`);
213-
document.head.append($style);
200+
}`;
214201
} catch (error) {
215202
toast(`${name} font not found`, "error");
216203
setFont("Roboto Mono");
@@ -238,7 +225,7 @@ async function downloadFont(name, link) {
238225
}
239226

240227
async function loadFont(name) {
241-
const $style = tag.get("style#font-style") ?? <style id="font-style"></style>;
228+
const $style = ensureStyleElement(FONT_FACE_STYLE_ID);
242229
let css = get(name);
243230

244231
if (!css) {
@@ -260,12 +247,20 @@ async function loadFont(name) {
260247
// Add font face to document if not already present
261248
if (!$style.textContent.includes(`font-family: '${name}'`)) {
262249
$style.textContent = `${$style.textContent}\n${css}`;
263-
document.head.append($style);
264250
}
265251

266252
return css;
267253
}
268254

255+
function ensureStyleElement(id) {
256+
const selector = `style#${id}`;
257+
const $style = tag.get(selector) ?? <style id={id}></style>;
258+
if (!$style.isConnected) {
259+
document.head.append($style);
260+
}
261+
return $style;
262+
}
263+
269264
export default {
270265
add,
271266
addCustom,

src/main.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { setKeyBindings } from "ace/commands";
1818
import { initModes } from "ace/modelist";
1919
import Contextmenu from "components/contextmenu";
2020
import Sidebar from "components/sidebar";
21+
import { TerminalManager } from "components/terminal";
2122
import tile from "components/tile";
2223
import toast from "components/toast";
2324
import tutorial from "components/tutorial";
@@ -487,6 +488,10 @@ async function loadApp() {
487488

488489
initFileList();
489490

491+
TerminalManager.restorePersistedSessions().catch((error) => {
492+
console.error("Terminal restoration failed:", error);
493+
});
494+
490495
/**
491496
*
492497
* @param {MouseEvent} e

0 commit comments

Comments
 (0)