Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions src/components/terminal/terminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import confirm from "dialogs/confirm";
import fonts from "lib/fonts";
import appSettings from "lib/settings";
import LigaturesAddon from "./ligatures";
import { getTerminalSettings } from "./terminalDefaults";
import {
DEFAULT_TERMINAL_SETTINGS,
getTerminalSettings,
} from "./terminalDefaults";
import TerminalThemeManager from "./terminalThemeManager";
import TerminalTouchSelection from "./terminalTouchSelection";

Expand Down Expand Up @@ -103,8 +106,13 @@ export default class TerminalComponent {
this.loadImageAddon();
}

// Load font if specified
this.loadTerminalFont();
// Load font in background - apply when ready without blocking render
this._fontReady = this.loadTerminalFont().then(() => {
if (this.terminal) {
this.terminal.options.fontFamily = this.options.fontFamily;
this.terminal.refresh(0, this.terminal.rows - 1);
}
});

// Set up terminal event handlers
this.setupEventHandlers();
Expand Down Expand Up @@ -570,6 +578,24 @@ export default class TerminalComponent {
this.setupTouchSelection();
}, 0);
}

// Safety: re-apply fontFamily on next frame to ensure xterm
// uses correct metrics even if font wasn't ready for first paint
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => {
if (this.terminal) {
this.terminal.options.fontFamily = this.options.fontFamily;
this.terminal.refresh(0, this.terminal.rows - 1);
}
});
} else {
setTimeout(() => {
if (this.terminal) {
this.terminal.options.fontFamily = this.options.fontFamily;
this.terminal.refresh(0, this.terminal.rows - 1);
}
}, 16);
}
Comment thread
deadlyjack marked this conversation as resolved.
} catch (error) {
console.error("Failed to mount terminal:", error);
}
Expand Down Expand Up @@ -1002,6 +1028,7 @@ export default class TerminalComponent {
const fontFamily = this.options.fontFamily;
if (fontFamily && fonts.get(fontFamily)) {
try {
fonts.injectFontFace(fontFamily);
await fonts.loadFont(fontFamily);
} catch (error) {
console.warn(`Failed to load terminal font ${fontFamily}:`, error);
Expand Down
27 changes: 27 additions & 0 deletions src/lib/fonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,23 @@ async function downloadFont(name, link) {
return FONT_FILE;
}

function injectFontFace(name) {
const $style = ensureStyleElement(FONT_FACE_STYLE_ID);
const css = get(name);

if (!css) return;

// Inject CSS if not already present (skip remote URL downloads - loadFont handles those)
if (!$style.textContent.includes(`font-family: '${name}'`)) {
$style.textContent = `${$style.textContent}\n${css}`;
}

// Kick off browser font loading without blocking
if (document.fonts?.load) {
document.fonts.load(`12px '${name}'`).catch(() => {});
}
}
Comment on lines +257 to +272
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Remote fonts bypass local caching when injectFontFace is called before loadFont

injectFontFace injects the raw @font-face CSS (containing the original https://acode.app/… URL) into the style element. When loadFont is subsequently called in loadTerminalFont() or terminalSettings.js, it downloads the font file to local storage and replaces the remote URL with an internal URI (content://…) — but then the "already present" guard ($style.textContent.includes(...)) is true, so the locally-URI'd CSS is never injected. The browser is left with the remote URL and the locally cached copy is never served.

For locally bundled fonts (like MesloLGS NF Regular) this is harmless, but for any remote font (e.g., JetBrains Mono Regular, Cascadia Code) this breaks offline/cached font loading — a regression from the pre-PR behaviour where only loadFont ran and always replaced the URL before injecting.


async function loadFont(name) {
const $style = ensureStyleElement(FONT_FACE_STYLE_ID);
let css = get(name);
Expand All @@ -279,6 +296,15 @@ async function loadFont(name) {
$style.textContent = `${$style.textContent}\n${css}`;
}

// Ensure the browser has actually parsed and loaded the font
if (document.fonts?.load) {
try {
await document.fonts.load(`12px '${name}'`);
} catch {
// document.fonts.load may reject if font is unavailable
}
}

return css;
}

Expand All @@ -303,4 +329,5 @@ export default {
setEditorFont,
setAppFont,
loadFont,
injectFontFace,
};
4 changes: 4 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import config from "lib/config";
import EditorFile from "lib/editorFile";
import EditorManager from "lib/editorManager";
import { initFileList } from "lib/fileList";
import fonts from "lib/fonts";
import lang from "lib/lang";
import loadPlugins from "lib/loadPlugins";
import Logger from "lib/logger";
Expand Down Expand Up @@ -254,6 +255,9 @@ async function onDeviceReady() {
themes.init();
initHighlighting();

// Inject default terminal font face early so browser preloads it
fonts.injectFontFace("MesloLGS NF Regular");

registerPrettierFormatter();

acode.setLoadingMessage("Loading language...");
Expand Down
1 change: 1 addition & 0 deletions src/settings/terminalSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ export async function updateActiveTerminals(key, value) {
case "fontFamily":
// Load font if it's not already loaded
try {
fonts.injectFontFace(value);
await fonts.loadFont(value);
} catch (error) {
console.warn(`Failed to load font ${value}:`, error);
Expand Down