summaryrefslogtreecommitdiffstats
path: root/src/detection/terminalfont/terminalfont.c
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/detection/terminalfont/terminalfont.c
Add the files
Diffstat (limited to 'src/detection/terminalfont/terminalfont.c')
-rw-r--r--src/detection/terminalfont/terminalfont.c407
1 files changed, 407 insertions, 0 deletions
diff --git a/src/detection/terminalfont/terminalfont.c b/src/detection/terminalfont/terminalfont.c
new file mode 100644
index 0000000..7ef7120
--- /dev/null
+++ b/src/detection/terminalfont/terminalfont.c
@@ -0,0 +1,407 @@
+#include "terminalfont.h"
+#include "common/io.h"
+#include "common/properties.h"
+#include "common/processing.h"
+#include "common/debug.h"
+#include "common/strutil.h"
+#include "detection/terminalshell/terminalshell.h"
+
+static void detectAlacritty(FFTerminalFontResult* terminalFont) {
+ // Maybe using a toml parser to read the config file is better?
+ // https://github.com/cktan/tomlc17
+
+ // Doc: https://alacritty.org/config-alacritty.html#s26
+ FF_STRBUF_AUTO_DESTROY fontNormal = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY fontFamily = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY fontStyle = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate();
+
+ do {
+ FFpropquery fontQueryToml[] = {
+ { "normal =", &fontNormal },
+ { "size =", &fontSize },
+ };
+
+ // alacritty parses config files in this order
+ if (ffParsePropFileConfigValues("alacritty/alacritty.toml", 2, fontQueryToml)) {
+ break;
+ }
+ if (ffParsePropFileConfigValues("alacritty.toml", 2, fontQueryToml)) {
+ break;
+ }
+ if (ffParsePropFileConfigValues(".alacritty.toml", 2, fontQueryToml)) {
+ break;
+ }
+ } while (false);
+
+ if (fontNormal.length > 0) {
+ // { family = "Fira Code", style = "Medium" }
+ ffStrbufTrimSpace(&fontNormal);
+ ffStrbufTrimRight(&fontNormal, '}');
+ ffStrbufTrimLeft(&fontNormal, '{');
+ ffStrbufTrimSpace(&fontNormal);
+
+ // family = "Fira Code", style = "Medium"
+ ffStrbufReplaceAllC(&fontNormal, ',', '\n'); // Assume no commas in font names
+ ffParsePropLines(fontNormal.chars, "family =", &fontFamily);
+ ffParsePropLines(fontNormal.chars, "style =", &fontStyle);
+ }
+
+ if (fontFamily.length == 0) {
+#if __APPLE__
+ ffStrbufSetStatic(&fontFamily, "Menlo");
+#elif _WIN32
+ ffStrbufSetStatic(&fontFamily, "Consolas");
+#else
+ ffStrbufSetStatic(&fontFamily, "monospace");
+#endif
+ }
+ if (fontStyle.length == 0) {
+ ffStrbufSetStatic(&fontStyle, "Regular");
+ }
+
+ if (fontSize.length == 0) {
+ ffStrbufSetStatic(&fontSize, "11.25");
+ }
+
+ ffFontInitMoveValues(&terminalFont->font, &fontFamily, &fontSize, &fontStyle);
+}
+
+static bool parseGhosttyConfig(FFstrbuf* path, FFstrbuf* fontName, FFstrbuf* fontNameFallback, FFstrbuf* fontSize) {
+ FF_DEBUG("parsing config: %s", path->chars);
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate();
+ if (!ffAppendFileBuffer(path->chars, &buffer)) {
+ FF_DEBUG("cannot read config: %s", path->chars);
+ return false;
+ }
+
+ char* line = NULL;
+ size_t len = 0;
+ while (ffStrbufGetline(&line, &len, &buffer)) {
+ if (ffParsePropLine(line, "font-family =", &temp)) {
+ FF_DEBUG("found font-family='%s' in %s", temp.chars, path->chars);
+ if (fontName->length > 0) {
+ ffStrbufDestroy(fontNameFallback);
+ ffStrbufInitMove(fontNameFallback, fontName);
+ }
+ ffStrbufDestroy(fontName);
+ ffStrbufInitMove(fontName, &temp);
+ } else if (ffParsePropLine(line, "font-size =", fontSize)) {
+ FF_DEBUG("found font-size='%s' in %s", temp.chars, path->chars);
+ // Latter overrides former
+ ffStrbufDestroy(fontSize);
+ ffStrbufInitMove(fontSize, &temp);
+ }
+ }
+ return true;
+}
+
+static void detectGhostty(const FFstrbuf* exe, FFTerminalFontResult* terminalFont, const char* configPathMac, const char* configPathUnix) {
+ FF_DEBUG("detectGhostty: start");
+ FF_STRBUF_AUTO_DESTROY configPath = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY fontNameFallback = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate();
+
+ if (configPathMac && configPathUnix) {
+#if __APPLE__
+ ffStrbufSet(&configPath, &instance.state.platform.homeDir);
+ ffStrbufAppendS(&configPath, "Library/Application Support/");
+ ffStrbufAppendS(&configPath, configPathMac); // com.mitchellh.ghostty/config
+ parseGhosttyConfig(&configPath, &fontName, &fontNameFallback, &fontSize);
+#endif
+
+ if (instance.state.platform.configDirs.length > 0) {
+ ffStrbufSet(&configPath, FF_LIST_FIRST(FFstrbuf, instance.state.platform.configDirs));
+ ffStrbufAppendS(&configPath, configPathUnix); // ghostty/config
+ parseGhosttyConfig(&configPath, &fontName, &fontNameFallback, &fontSize);
+ }
+ } else {
+ // Try ghostty +show-config first
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ const char* error = ffProcessAppendStdOut(&buffer, (char* const[]){
+ exe->chars,
+ "+show-config",
+ NULL,
+ });
+ if (error == NULL) {
+ char* line = NULL;
+ size_t len = 0;
+ while (ffStrbufGetline(&line, &len, &buffer)) {
+ if (ffStrStartsWith(line, "font-family = ")) {
+ FF_DEBUG("found %s", line);
+ if (fontName.length > 0) {
+ ffStrbufDestroy(&fontNameFallback);
+ ffStrbufInitMove(&fontNameFallback, &fontName);
+ }
+ ffStrbufSetNS(&fontName, (uint32_t) (len - strlen("font-family = ")), line + strlen("font-family = "));
+ } else if (ffStrStartsWith(line, "font-size = ")) {
+ FF_DEBUG("found %s", line);
+ // `ghostty +show-config` reports only one font size even if the config has multiple font sizes
+ ffStrbufSetNS(&fontSize, (uint32_t) (len - strlen("font-size = ")), line + strlen("font-size = "));
+ }
+ }
+ } else {
+ FF_DEBUG("`ghostty +show-config` failed: %s", error);
+ }
+ }
+
+ if (fontName.length == 0) {
+ ffStrbufAppendS(&fontName, "JetBrainsMono Nerd Font");
+ FF_DEBUG("using default family='%s'", fontName.chars);
+ }
+
+ if (fontSize.length == 0) {
+ ffStrbufAppendS(&fontSize,
+#if __APPLE__
+ "13"
+#else
+ "12"
+#endif
+ );
+ FF_DEBUG("using default size='%s'", fontSize.chars);
+ }
+
+ ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars);
+ if (fontNameFallback.length > 0) {
+ FF_DEBUG("applying fallback family='%s'", fontNameFallback.chars);
+ ffFontInitValues(&terminalFont->fallback, fontNameFallback.chars, NULL);
+ }
+ FF_DEBUG("result family='%s' size='%s'%s", fontName.chars, fontSize.chars, fontNameFallback.length ? " (with fallback)" : "");
+ FF_DEBUG("detectGhostty: end");
+}
+
+FF_A_UNUSED static void detectTTY(FFTerminalFontResult* terminalFont) {
+ FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
+
+ ffParsePropFile(FASTFETCH_TARGET_DIR_ETC "/vconsole.conf", "Font =", &fontName);
+
+ if (fontName.length == 0) {
+ ffStrbufAppendS(&fontName, "VGA default kernel font ");
+ ffProcessAppendStdOut(&fontName, (char* const[]){ "showconsolefont", "--info", NULL });
+
+ ffStrbufTrimRight(&fontName, ' ');
+ }
+
+ if (fontName.length > 0) {
+ ffFontInitCopy(&terminalFont->font, fontName.chars);
+ } else {
+ ffStrbufAppendS(&terminalFont->error, "Couldn't find Font in " FASTFETCH_TARGET_DIR_ETC "/vconsole.conf");
+ }
+}
+
+FF_A_UNUSED static bool detectKitty(const FFstrbuf* exe, FFTerminalFontResult* result) {
+ FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate();
+
+ char fontHex[512] = "", sizeHex[512] = "";
+ // https://github.com/fastfetch-cli/fastfetch/discussions/1030#discussioncomment-9845233
+ if (ffGetTerminalResponse(
+ "\eP+q6b697474792d71756572792d666f6e745f66616d696c79;6b697474792d71756572792d666f6e745f73697a65\e\\", // kitty-query-font_family;kitty-query-font_size
+ 2,
+ "\eP1+r%*[^=]=%511[^\e]\e\\\eP1+r%*[^=]=%511[^\e]\e\\",
+ fontHex,
+ sizeHex) == NULL &&
+ *fontHex && *sizeHex) {
+ // decode hex string
+ for (const char* p = fontHex; p[0] && p[1]; p += 2) {
+ unsigned value;
+ if (sscanf(p, "%2x", &value) == 1) {
+ ffStrbufAppendC(&fontName, (char) value);
+ }
+ }
+ for (const char* p = sizeHex; p[0] && p[1]; p += 2) {
+ unsigned value;
+ if (sscanf(p, "%2x", &value) == 1) {
+ ffStrbufAppendC(&fontSize, (char) value);
+ }
+ }
+ } else {
+ FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
+ if (!ffProcessAppendStdOut(&buf, (char* const[]){
+ exe->chars,
+ "+kitten",
+ "query-terminal",
+ NULL,
+ })) {
+ ffParsePropLines(buf.chars, "font_family: ", &fontName);
+ ffParsePropLines(buf.chars, "font_size: ", &fontSize);
+ } else {
+ FFpropquery fontQuery[] = {
+ { "font_family ", &fontName },
+ { "font_size ", &fontSize },
+ };
+
+ ffParsePropFileConfigValues("kitty/kitty.conf", 2, fontQuery);
+
+ if (fontName.length == 0) {
+ ffStrbufSetS(&fontName, "monospace");
+ }
+ if (fontSize.length == 0) {
+ ffStrbufSetS(&fontSize, "11.0");
+ }
+ }
+ }
+
+ ffFontInitValues(&result->font, fontName.chars, fontSize.chars);
+
+ return true;
+}
+
+static bool detectWezterm(const FFstrbuf* exe, FFTerminalFontResult* result) {
+ FF_STRBUF_AUTO_DESTROY cli = ffStrbufCreateCopy(exe);
+ ffStrbufSubstrBeforeLastC(&cli, '-');
+
+#ifdef _WIN32
+ ffStrbufAppendS(&cli, ".exe");
+#endif
+
+ FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
+
+ ffStrbufSetS(&result->error, ffProcessAppendStdOut(&fontName, (char* const[]){ cli.chars, "ls-fonts", "--text", "a", NULL }));
+ if (result->error.length) {
+ return false;
+ }
+
+ // LeftToRight
+ // 0 a \u{61} x_adv=7 cells=1 glyph=a,180 wezterm.font("JetBrains Mono", {weight="Regular", stretch="Normal", style="Normal"})
+ // <built-in>, BuiltIn
+ ffStrbufSubstrAfterFirstC(&fontName, '"');
+ ffStrbufSubstrBeforeFirstC(&fontName, '"');
+
+ if (!fontName.length) {
+ return false;
+ }
+
+ ffFontInitCopy(&result->font, fontName.chars);
+ return true;
+}
+
+static bool detectTabby(FFTerminalFontResult* result) {
+ FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate();
+
+ FFpropquery fontQuery[] = {
+ { "font: ", &fontName },
+ { "fontSize: ", &fontSize },
+ };
+
+ if (!ffParsePropFileConfigValues("tabby/config.yaml", 2, fontQuery)) {
+ return false;
+ }
+
+ if (fontName.length == 0) {
+ ffStrbufSetS(&fontName, "monospace");
+ }
+ if (fontSize.length == 0) {
+ ffStrbufSetS(&fontSize, "14");
+ }
+
+ ffFontInitValues(&result->font, fontName.chars, fontSize.chars);
+
+ return true;
+}
+
+static bool detectContour(const FFstrbuf* exe, FFTerminalFontResult* result) {
+ FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
+ if (ffProcessAppendStdOut(&buf, (char* const[]){ exe->chars, "font-locator", NULL })) {
+ ffStrbufAppendS(&result->error, "`contour font-locator` failed");
+ return false;
+ }
+
+ //[error] Missing key .logging.enabled. Using default: false.
+ //[error] ...
+ // Matching fonts using : Fontconfig
+ // Font description : (family=Sarasa Term SC Nerd weight=Regular slant=Roman spacing=Monospace, strict_spacing=yes)
+ // Number of fonts found : 49
+ // path /usr/share/fonts/google-noto/NotoSansMono-Regular.ttf Regular Roman
+ // path ...
+
+ uint32_t index = ffStrbufFirstIndexS(&buf, "Font description : (family=");
+ if (index >= buf.length) {
+ return false;
+ }
+ index += (uint32_t) strlen("Font description : (family=");
+ ffStrbufSubstrBefore(&buf, ffStrbufNextIndexS(&buf, index, " weight="));
+ ffFontInitCopy(&result->font, buf.chars + index);
+ return true;
+}
+
+static bool detectRio(FFTerminalFontResult* terminalFont) {
+ FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate();
+
+ FFpropquery fontQueryToml[] = {
+ { "family =", &fontName },
+ { "size =", &fontSize },
+ };
+
+ ffParsePropFileConfigValues("rio/config.toml", 2, fontQueryToml);
+
+ if (fontName.length == 0) {
+ ffStrbufAppendS(&fontName, "Cascadia Code");
+ }
+
+ if (fontSize.length == 0) {
+ ffStrbufAppendS(&fontSize, "18");
+ }
+
+ ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars);
+
+ return true;
+}
+
+bool ffDetectTerminalFontPlatform(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont);
+
+static bool detectTerminalFontCommon(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) {
+ if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "alacritty")) {
+ detectAlacritty(terminalFont);
+ } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "wezterm-gui")) {
+ detectWezterm(&terminal->exe, terminalFont);
+ } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "tabby")) {
+ detectTabby(terminalFont);
+ } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "contour")) {
+ detectContour(&terminal->exe, terminalFont);
+ } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "ghostty")) {
+ detectGhostty(&terminal->exe, terminalFont, NULL, NULL);
+ } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "Muxy")) {
+ detectGhostty(&terminal->exe, terminalFont, "Muxy/ghostty.conf", "muxy/ghostty.conf");
+ } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "rio")) {
+ detectRio(terminalFont);
+ }
+#ifndef _WIN32
+ else if (ffStrbufStartsWithIgnCaseS(&terminal->exe, "/dev/pts/")) {
+ ffStrbufAppendS(&terminalFont->error, "Terminal font detection is not supported on PTS");
+ } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "kitty")) {
+ detectKitty(&terminal->exe, terminalFont);
+ } else if (ffStrbufStartsWithIgnCaseS(&terminal->exe, "/dev/tty")) {
+ detectTTY(terminalFont);
+ }
+#endif
+
+ else {
+ return false;
+ }
+
+ return true;
+}
+
+bool ffDetectTerminalFont(FFTerminalFontResult* result) {
+ const FFTerminalResult* terminal = ffDetectTerminal();
+
+ if (terminal->processName.length == 0) {
+ ffStrbufAppendS(&result->error, "Terminal font needs successful terminal detection");
+ }
+
+ else if (!detectTerminalFontCommon(terminal, result)) {
+ ffDetectTerminalFontPlatform(terminal, result);
+ }
+
+ if (result->error.length == 0 && result->font.pretty.length == 0) {
+ ffStrbufAppendF(&result->error, "Unknown terminal: %s", terminal->processName.chars);
+ }
+
+ return result->error.length == 0;
+}