ohmu
·
2026-10-15
1#include "terminalfont.h"
2#include "common/io.h"
3#include "common/properties.h"
4#include "common/processing.h"
5#include "common/debug.h"
6#include "detection/terminalshell/terminalshell.h"
7
8static void detectAlacritty(FFTerminalFontResult* terminalFont) {
9 // Maybe using a toml parser to read the config file is better?
10 // https://github.com/cktan/tomlc17
11
12 // Doc: https://alacritty.org/config-alacritty.html#s26
13 FF_STRBUF_AUTO_DESTROY fontNormal = ffStrbufCreate();
14 FF_STRBUF_AUTO_DESTROY fontFamily = ffStrbufCreate();
15 FF_STRBUF_AUTO_DESTROY fontStyle = ffStrbufCreate();
16 FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate();
17
18 do {
19 FFpropquery fontQueryToml[] = {
20 { "normal =", &fontNormal },
21 { "family =", &fontFamily },
22 { "size =", &fontSize },
23 };
24
25 // alacritty parses config files in this order
26 if (ffParsePropFileConfigValues("alacritty/alacritty.toml", 3, fontQueryToml)) {
27 break;
28 }
29 if (ffParsePropFileConfigValues("alacritty.toml", 3, fontQueryToml)) {
30 break;
31 }
32 if (ffParsePropFileConfigValues(".alacritty.toml", 3, fontQueryToml)) {
33 break;
34 }
35 } while (false);
36
37 if (fontNormal.length > 0) {
38 ffStrbufClear(&fontFamily); // If both `normal` and `family` are specified, `normal` takes precedence
39
40 // { family = "Fira Code", style = "Medium" }
41 ffStrbufTrimSpace(&fontNormal);
42 ffStrbufTrimRight(&fontNormal, '}');
43 ffStrbufTrimLeft(&fontNormal, '{');
44 ffStrbufTrimSpace(&fontNormal);
45
46 // family = "Fira Code", style = "Medium"
47 ffStrbufReplaceAllC(&fontNormal, ',', '\n'); // Assume no commas in font names
48 ffParsePropLines(fontNormal.chars, "family =", &fontFamily);
49 ffParsePropLines(fontNormal.chars, "style =", &fontStyle);
50 }
51
52 if (fontFamily.length == 0) {
53#if __APPLE__
54 ffStrbufSetStatic(&fontFamily, "Menlo");
55#elif _WIN32
56 ffStrbufSetStatic(&fontFamily, "Consolas");
57#else
58 ffStrbufSetStatic(&fontFamily, "monospace");
59#endif
60 }
61 if (fontStyle.length == 0) {
62 ffStrbufSetStatic(&fontStyle, "Regular");
63 }
64
65 if (fontSize.length == 0) {
66 ffStrbufSetStatic(&fontSize, "11.25");
67 }
68
69 ffFontInitMoveValues(&terminalFont->font, &fontFamily, &fontSize, &fontStyle);
70}
71
72static void parseGhosttyConfig(FFstrbuf* path, FFstrbuf* fontName, FFstrbuf* fontNameFallback, FFstrbuf* fontSize, FFlist* configFiles /* list of FFstrbuf */) {
73 // Maximum number of `config-file` directives to follow, guarding against runaway includes
74 enum { FF_GHOSTTY_MAX_CONFIG_FILES = 16 };
75
76 FF_DEBUG("parsing config: %s", path->chars);
77
78 FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
79 FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate();
80 if (!ffAppendFileBuffer(path->chars, &buffer)) {
81 FF_DEBUG("cannot read config: %s", path->chars);
82 if (ffStrbufEndsWithS(path, ".ghostty")) {
83 FF_DEBUG("Trying to load config without .ghostty extension");
84 path->chars[path->length - strlen(".ghostty")] = '\0';
85 bool ok = ffAppendFileBuffer(path->chars, &buffer);
86 path->chars[path->length - strlen(".ghostty")] = '.';
87 if (!ok) {
88 FF_DEBUG("cannot read config: %s", path->chars);
89 return;
90 }
91 } else {
92 return;
93 }
94 }
95
96 char* line = nullptr;
97 size_t len = 0;
98 while (ffStrbufGetline(&line, &len, &buffer)) {
99 if (ffParsePropLine(line, "font-family =", &temp)) {
100 FF_DEBUG("found font-family='%s' in %s", temp.chars, path->chars);
101 // Ghostty appends to a fallback list; the first entry is the primary font.
102 // An empty value resets the list.
103 if (temp.length == 0) {
104 ffStrbufClear(fontName);
105 ffStrbufClear(fontNameFallback);
106 } else if (fontName->length == 0) {
107 ffStrbufSet(fontName, &temp);
108 } else if (fontNameFallback->length == 0) {
109 ffStrbufSet(fontNameFallback, &temp);
110 }
111 ffStrbufClear(&temp);
112 } else if (ffParsePropLine(line, "font-size =", &temp)) {
113 FF_DEBUG("found font-size='%s' in %s", temp.chars, path->chars);
114 // Latter overrides former
115 ffStrbufDestroy(fontSize);
116 ffStrbufInitMove(fontSize, &temp);
117 } else if (ffParsePropLine(line, "config-file =", &temp)) {
118 // Doc: https://ghostty.org/docs/config/reference#config-file
119 // A leading `?` suppresses errors if the file doesn't exist; missing files are skipped here either way
120 ffStrbufTrimLeft(&temp, '?');
121 ffStrbufTrim(&temp, '"');
122 if (temp.length == 0) {
123 continue;
124 }
125
126 if (!ffStrbufStartsWithC(&temp, '/')) {
127 // Relative paths are relative to the file containing the `config-file` directive
128 FF_STRBUF_AUTO_DESTROY absolutePath = ffStrbufCreateCopy(path);
129 ffStrbufSubstrBeforeLastC(&absolutePath, '/');
130 ffStrbufAppendC(&absolutePath, '/');
131 ffStrbufAppend(&absolutePath, &temp);
132 ffStrbufDestroy(&temp);
133 ffStrbufInitMove(&temp, &absolutePath);
134 }
135
136 // Each unique file is only loaded once, which also prevents include cycles
137 bool loaded = ffStrbufEqual(&temp, path);
138 if (!loaded) {
139 FF_LIST_FOR_EACH(FFstrbuf, it, *configFiles) {
140 if (ffStrbufEqual(it, &temp)) {
141 loaded = true;
142 break;
143 }
144 }
145 }
146
147 if (loaded) {
148 FF_DEBUG("config-file '%s' was already loaded, skipping to avoid cycles", temp.chars);
149 } else if (configFiles->length >= FF_GHOSTTY_MAX_CONFIG_FILES) {
150 FF_DEBUG("too many config-file directives, ignoring '%s'", temp.chars);
151 } else {
152 FF_DEBUG("found config-file='%s' in %s", temp.chars, path->chars);
153 ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *configFiles), &temp);
154 }
155 ffStrbufClear(&temp);
156 }
157 }
158}
159
160static void detectGhostty(FFTerminalFontResult* terminalFont, [[maybe_unused]] const char* configPathMac, const char* configPathUnix) {
161 FF_DEBUG("detectGhostty: start");
162 FF_STRBUF_AUTO_DESTROY configPath = ffStrbufCreate();
163 FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
164 FF_STRBUF_AUTO_DESTROY fontNameFallback = ffStrbufCreate();
165 FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate();
166 FF_LIST_AUTO_DESTROY configFiles = ffListCreate(); // list of FFstrbuf
167
168 // Ghostty loads the XDG config first, then (on macOS) the Application Support config,
169 // so values in the latter override the former
170 if (instance.state.platform.configDirs.length > 0) {
171 ffStrbufSet(&configPath, FF_LIST_FIRST(FFstrbuf, instance.state.platform.configDirs));
172 ffStrbufAppendS(&configPath, configPathUnix); // ghostty/config
173 parseGhosttyConfig(&configPath, &fontName, &fontNameFallback, &fontSize, &configFiles);
174 }
175
176#if __APPLE__
177 ffStrbufSet(&configPath, &instance.state.platform.homeDir);
178 ffStrbufAppendS(&configPath, "Library/Application Support/");
179 ffStrbufAppendS(&configPath, configPathMac); // com.mitchellh.ghostty/config
180 parseGhosttyConfig(&configPath, &fontName, &fontNameFallback, &fontSize, &configFiles);
181#endif
182
183 // Files referenced by `config-file` don't take effect until the whole configuration is loaded,
184 // so they are parsed after all root config files, in the order they were found
185 for (uint32_t i = 0; i < configFiles.length; ++i) {
186 // Copy the path as parseGhosttyConfig may grow the list, invalidating pointers into it
187 ffStrbufSet(&configPath, FF_LIST_GET(FFstrbuf, configFiles, i));
188 parseGhosttyConfig(&configPath, &fontName, &fontNameFallback, &fontSize, &configFiles);
189 }
190
191 FF_LIST_FOR_EACH(FFstrbuf, it, configFiles) {
192 ffStrbufDestroy(it);
193 }
194
195 if (fontName.length == 0) {
196 ffStrbufAppendS(&fontName, "JetBrainsMono Nerd Font");
197 FF_DEBUG("using default family='%s'", fontName.chars);
198 }
199
200 if (fontSize.length == 0) {
201 ffStrbufAppendS(&fontSize,
202#if __APPLE__
203 "13"
204#else
205 "12"
206#endif
207 );
208 FF_DEBUG("using default size='%s'", fontSize.chars);
209 }
210
211 ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars);
212 if (fontNameFallback.length > 0) {
213 FF_DEBUG("applying fallback family='%s'", fontNameFallback.chars);
214 ffFontInitValues(&terminalFont->fallback, fontNameFallback.chars, nullptr);
215 }
216 FF_DEBUG("result family='%s' size='%s'%s", fontName.chars, fontSize.chars, fontNameFallback.length ? " (with fallback)" : "");
217 FF_DEBUG("detectGhostty: end");
218}
219
220[[maybe_unused]] static void detectTTY(FFTerminalFontResult* terminalFont) {
221 FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
222
223 ffParsePropFile(FASTFETCH_TARGET_DIR_ETC "/vconsole.conf", "Font =", &fontName);
224
225 if (fontName.length == 0) {
226 ffStrbufAppendS(&fontName, "VGA default kernel font ");
227 ffProcessAppendStdOut(&fontName, (char* const[]){ "showconsolefont", "--info", nullptr });
228
229 ffStrbufTrimRight(&fontName, ' ');
230 }
231
232 if (fontName.length > 0) {
233 ffFontInitCopy(&terminalFont->font, fontName.chars);
234 } else {
235 ffStrbufAppendS(&terminalFont->error, "Couldn't find Font in " FASTFETCH_TARGET_DIR_ETC "/vconsole.conf");
236 }
237}
238
239[[maybe_unused]] static bool detectKitty(const FFstrbuf* exe, FFTerminalFontResult* result) {
240 FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
241 FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate();
242
243 char fontHex[512] = "", sizeHex[512] = "";
244 // https://github.com/fastfetch-cli/fastfetch/discussions/1030#discussioncomment-9845233
245 if (ffGetTerminalResponse(
246 "\eP+q6b697474792d71756572792d666f6e745f66616d696c79;6b697474792d71756572792d666f6e745f73697a65\e\\", // kitty-query-font_family;kitty-query-font_size
247 2,
248 "\eP1+r%*[^=]=%511[^\e]\e\\\eP1+r%*[^=]=%511[^\e]\e\\",
249 fontHex,
250 sizeHex) == nullptr &&
251 *fontHex && *sizeHex) {
252 // decode hex string
253 for (const char* p = fontHex; p[0] && p[1]; p += 2) {
254 unsigned value;
255 if (sscanf(p, "%2x", &value) == 1) {
256 ffStrbufAppendC(&fontName, (char) value);
257 }
258 }
259 for (const char* p = sizeHex; p[0] && p[1]; p += 2) {
260 unsigned value;
261 if (sscanf(p, "%2x", &value) == 1) {
262 ffStrbufAppendC(&fontSize, (char) value);
263 }
264 }
265 } else {
266 FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
267 if (!ffProcessAppendStdOut(&buf, (char* const[]){
268 exe->chars,
269 "+kitten",
270 "query-terminal",
271 nullptr,
272 })) {
273 ffParsePropLines(buf.chars, "font_family: ", &fontName);
274 ffParsePropLines(buf.chars, "font_size: ", &fontSize);
275 } else {
276 FFpropquery fontQuery[] = {
277 { "font_family ", &fontName },
278 { "font_size ", &fontSize },
279 };
280
281 ffParsePropFileConfigValues("kitty/kitty.conf", 2, fontQuery);
282
283 if (fontName.length == 0) {
284 ffStrbufSetS(&fontName, "monospace");
285 }
286 if (fontSize.length == 0) {
287 ffStrbufSetS(&fontSize, "11.0");
288 }
289 }
290 }
291
292 ffFontInitValues(&result->font, fontName.chars, fontSize.chars);
293
294 return true;
295}
296
297static bool detectWezterm(const FFstrbuf* exe, FFTerminalFontResult* result) {
298 FF_STRBUF_AUTO_DESTROY cli = ffStrbufCreateCopy(exe);
299 ffStrbufSubstrBeforeLastC(&cli, '-');
300
301#ifdef _WIN32
302 ffStrbufAppendS(&cli, ".exe");
303#endif
304
305 FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
306
307 ffStrbufSetS(&result->error, ffProcessAppendStdOut(&fontName, (char* const[]){ cli.chars, "ls-fonts", "--text", "a", nullptr }));
308 if (result->error.length) {
309 return false;
310 }
311
312 // LeftToRight
313 // 0 a \u{61} x_adv=7 cells=1 glyph=a,180 wezterm.font("JetBrains Mono", {weight="Regular", stretch="Normal", style="Normal"})
314 // <built-in>, BuiltIn
315 ffStrbufSubstrAfterFirstC(&fontName, '"');
316 ffStrbufSubstrBeforeFirstC(&fontName, '"');
317
318 if (!fontName.length) {
319 return false;
320 }
321
322 ffFontInitCopy(&result->font, fontName.chars);
323 return true;
324}
325
326static bool detectTabby(FFTerminalFontResult* result) {
327 FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
328 FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate();
329
330 FFpropquery fontQuery[] = {
331 { "font: ", &fontName },
332 { "fontSize: ", &fontSize },
333 };
334
335 if (!ffParsePropFileConfigValues("tabby/config.yaml", 2, fontQuery)) {
336 return false;
337 }
338
339 if (fontName.length == 0) {
340 ffStrbufSetS(&fontName, "monospace");
341 }
342 if (fontSize.length == 0) {
343 ffStrbufSetS(&fontSize, "14");
344 }
345
346 ffFontInitValues(&result->font, fontName.chars, fontSize.chars);
347
348 return true;
349}
350
351static bool detectContour(const FFstrbuf* exe, FFTerminalFontResult* result) {
352 FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
353 if (ffProcessAppendStdOut(&buf, (char* const[]){ exe->chars, "font-locator", nullptr })) {
354 ffStrbufAppendS(&result->error, "`contour font-locator` failed");
355 return false;
356 }
357
358 //[error] Missing key .logging.enabled. Using default: false.
359 //[error] ...
360 // Matching fonts using : Fontconfig
361 // Font description : (family=Sarasa Term SC Nerd weight=Regular slant=Roman spacing=Monospace, strict_spacing=yes)
362 // Number of fonts found : 49
363 // path /usr/share/fonts/google-noto/NotoSansMono-Regular.ttf Regular Roman
364 // path ...
365
366 uint32_t index = ffStrbufFirstIndexS(&buf, "Font description : (family=");
367 if (index >= buf.length) {
368 return false;
369 }
370 index += (uint32_t) strlen("Font description : (family=");
371 ffStrbufSubstrBefore(&buf, ffStrbufNextIndexS(&buf, index, " weight="));
372 ffFontInitCopy(&result->font, buf.chars + index);
373 return true;
374}
375
376static bool detectRio(FFTerminalFontResult* terminalFont) {
377 FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
378 FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate();
379
380 FFpropquery fontQueryToml[] = {
381 { "family =", &fontName },
382 { "size =", &fontSize },
383 };
384
385 ffParsePropFileConfigValues("rio/config.toml", 2, fontQueryToml);
386
387 if (fontName.length == 0) {
388 ffStrbufAppendS(&fontName, "Cascadia Code");
389 }
390
391 if (fontSize.length == 0) {
392 ffStrbufAppendS(&fontSize, "18");
393 }
394
395 ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars);
396
397 return true;
398}
399
400bool ffDetectTerminalFontPlatform(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont);
401
402static bool detectTerminalFontCommon(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) {
403 if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "alacritty")) {
404 detectAlacritty(terminalFont);
405 } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "wezterm-gui")) {
406 detectWezterm(&terminal->exe, terminalFont);
407 } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "tabby")) {
408 detectTabby(terminalFont);
409 } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "contour")) {
410 detectContour(&terminal->exe, terminalFont);
411 } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "ghostty")) {
412 detectGhostty(terminalFont, "com.mitchellh.ghostty/config.ghostty", "ghostty/config.ghostty");
413 } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "Muxy")) {
414 detectGhostty(terminalFont, "Muxy/ghostty.conf", "muxy/ghostty.conf");
415 } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "rio")) {
416 detectRio(terminalFont);
417 }
418#ifndef _WIN32
419 else if (ffStrbufStartsWithIgnCaseS(&terminal->exe, "/dev/pts/")) {
420 ffStrbufAppendS(&terminalFont->error, "Terminal font detection is not supported on PTS");
421 } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "kitty")) {
422 detectKitty(&terminal->exe, terminalFont);
423 } else if (ffStrbufStartsWithIgnCaseS(&terminal->exe, "/dev/tty")) {
424 detectTTY(terminalFont);
425 }
426#endif
427
428 else {
429 return false;
430 }
431
432 return true;
433}
434
435bool ffDetectTerminalFont(FFTerminalFontResult* result) {
436 const FFTerminalResult* terminal = ffDetectTerminal();
437
438 if (terminal->processName.length == 0) {
439 ffStrbufAppendS(&result->error, "Terminal font needs successful terminal detection");
440 }
441
442 else if (!detectTerminalFontCommon(terminal, result)) {
443 ffDetectTerminalFontPlatform(terminal, result);
444 }
445
446 if (result->error.length == 0 && result->font.pretty.length == 0) {
447 ffStrbufAppendF(&result->error, "Unknown terminal: %s", terminal->processName.chars);
448 }
449
450 return result->error.length == 0;
451}