diff options
Diffstat (limited to 'src/detection/terminalfont')
| -rw-r--r-- | src/detection/terminalfont/terminalfont.c | 407 | ||||
| -rw-r--r-- | src/detection/terminalfont/terminalfont.h | 13 | ||||
| -rw-r--r-- | src/detection/terminalfont/terminalfont_android.c | 71 | ||||
| -rw-r--r-- | src/detection/terminalfont/terminalfont_apple.m | 106 | ||||
| -rw-r--r-- | src/detection/terminalfont/terminalfont_linux.c | 545 | ||||
| -rw-r--r-- | src/detection/terminalfont/terminalfont_windows.c | 287 |
6 files changed, 1429 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; +} diff --git a/src/detection/terminalfont/terminalfont.h b/src/detection/terminalfont/terminalfont.h new file mode 100644 index 0000000..bdc416c --- /dev/null +++ b/src/detection/terminalfont/terminalfont.h @@ -0,0 +1,13 @@ +#pragma once + +#include "fastfetch.h" +#include "common/font.h" +#include "modules/font/option.h" + +typedef struct FFTerminalFontResult { + FFstrbuf error; + FFfont font; + FFfont fallback; +} FFTerminalFontResult; + +bool ffDetectTerminalFont(FFTerminalFontResult* result); diff --git a/src/detection/terminalfont/terminalfont_android.c b/src/detection/terminalfont/terminalfont_android.c new file mode 100644 index 0000000..a721078 --- /dev/null +++ b/src/detection/terminalfont/terminalfont_android.c @@ -0,0 +1,71 @@ +#include "fastfetch.h" +#include "terminalfont.h" +#include "detection/terminalshell/terminalshell.h" +#include "common/io.h" + +#ifdef FF_HAVE_FREETYPE + #include "common/library.h" + #include <ft2build.h> + #include FT_FREETYPE_H +#endif + +#define FF_TERMUX_FONT_PATH FASTFETCH_TARGET_DIR_HOME "/.termux/font.ttf" + +const char* detectTermux(FFTerminalFontResult* terminalFont) { + if (!ffPathExists(FF_TERMUX_FONT_PATH, FF_PATHTYPE_FILE)) { + ffFontInitCopy(&terminalFont->font, "monospace"); + return NULL; + } + +#ifdef FF_HAVE_FREETYPE + + FF_LIBRARY_LOAD_MESSAGE(freetype, "libfreetype" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(freetype, FT_Init_FreeType); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(freetype, FT_New_Face); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(freetype, FT_Done_Face); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(freetype, FT_Done_FreeType); + + FT_Library library = NULL; + FT_Face face = NULL; + const char* error = NULL; + + if (ffFT_Init_FreeType(&library)) { + error = "FT_Init_FreeType() failed"; + goto exit; + } + + if (ffFT_New_Face(library, FF_TERMUX_FONT_PATH, 0, &face)) { + error = "FT_NEW_Face(" FF_TERMUX_FONT_PATH ") failed"; + goto exit; + } + + ffFontInitCopy(&terminalFont->font, face->family_name); + +exit: + if (face) { + ffFT_Done_Face(face); + } + if (library) { + ffFT_Done_FreeType(library); + } + + return error; + +#else + + FF_UNUSED(terminalFont); + return "Fastfetch was built without freetype2 support"; + +#endif +} + +bool ffDetectTerminalFontPlatform(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) { + if (ffStrbufEqualS(&terminal->processName, "com.termux")) { + ffStrbufSetS(&terminalFont->error, detectTermux(terminalFont)); + } else { + bool ffDetectTerminalFontPlatformLinux(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont); + return ffDetectTerminalFontPlatformLinux(terminal, terminalFont); + } + + return true; +} diff --git a/src/detection/terminalfont/terminalfont_apple.m b/src/detection/terminalfont/terminalfont_apple.m new file mode 100644 index 0000000..126235f --- /dev/null +++ b/src/detection/terminalfont/terminalfont_apple.m @@ -0,0 +1,106 @@ +#include "terminalfont.h" +#include "common/font.h" +#include "detection/terminalshell/terminalshell.h" +#include "common/apple/osascript.h" + +#include <stdlib.h> +#include <string.h> +#import <Foundation/Foundation.h> + +static void detectIterm2(FFTerminalFontResult* terminalFont) +{ + const char* profile = getenv("ITERM_PROFILE"); + if (profile == NULL) + { + ffStrbufAppendS(&terminalFont->error, "environment variable ITERM_PROFILE not set"); + return; + } + + NSError* error; + NSString* fileName = [NSString stringWithFormat:@"file://%s/Library/Preferences/com.googlecode.iterm2.plist", instance.state.platform.homeDir.chars]; + NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:fileName] + error:&error]; + if(error) + { + ffStrbufAppendS(&terminalFont->error, error.localizedDescription.UTF8String); + return; + } + + for(NSDictionary* bookmark in dict[@"New Bookmarks"]) + { + if(![bookmark[@"Name"] isEqualToString:@(profile)]) + continue; + + NSString* normalFont = bookmark[@"Normal Font"]; + if(!normalFont) + { + ffStrbufAppendF(&terminalFont->error, "`Normal Font` key in profile `%s` doesn't exist", profile); + return; + } + ffFontInitWithSpace(&terminalFont->font, normalFont.UTF8String); + + NSNumber* useNonAsciiFont = bookmark[@"Use Non-ASCII Font"]; + if(useNonAsciiFont.boolValue) + { + NSString* nonAsciiFont = bookmark[@"Non Ascii Font"]; + if (nonAsciiFont) + ffFontInitWithSpace(&terminalFont->fallback, nonAsciiFont.UTF8String); + } + return; + } + + ffStrbufAppendF(&terminalFont->error, "find profile `%s` bookmark failed", profile); +} + +static void detectAppleTerminal(FFTerminalFontResult* terminalFont) +{ + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + ffOsascript("tell application \"Terminal\" to font name of window frontmost & \" \" & font size of window frontmost", &font); + + if(font.length == 0) + { + ffStrbufAppendS(&terminalFont->error, "executing osascript failed"); + return; + } + + ffFontInitWithSpace(&terminalFont->font, font.chars); +} + +static void detectWarpTerminal(FFTerminalFontResult* terminalFont) +{ + NSError* error; + NSString* fileName = [NSString stringWithFormat:@"file://%s/Library/Preferences/dev.warp.Warp-Stable.plist", instance.state.platform.homeDir.chars]; + NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:fileName] + error:&error]; + if(error) + { + ffStrbufAppendS(&terminalFont->error, error.localizedDescription.UTF8String); + return; + } + + NSString* fontName = dict[@"FontName"]; + if(!fontName) + fontName = @"Hack"; + else + fontName = [fontName stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\""]]; + + NSString* fontSize = dict[@"FontSize"]; + if(!fontSize) + fontSize = @"13"; + + ffFontInitValues(&terminalFont->font, fontName.UTF8String, fontSize.UTF8String); +} + +bool ffDetectTerminalFontPlatform(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) +{ + if(ffStrbufIgnCaseEqualS(&terminal->processName, "iterm.app") || + ffStrbufStartsWithIgnCaseS(&terminal->processName, "iTermServer-")) + detectIterm2(terminalFont); + else if(ffStrbufIgnCaseEqualS(&terminal->processName, "Apple_Terminal")) + detectAppleTerminal(terminalFont); + else if(ffStrbufIgnCaseEqualS(&terminal->processName, "WarpTerminal")) + detectWarpTerminal(terminalFont); + else + return false; + return true; +} diff --git a/src/detection/terminalfont/terminalfont_linux.c b/src/detection/terminalfont/terminalfont_linux.c new file mode 100644 index 0000000..bfc8959 --- /dev/null +++ b/src/detection/terminalfont/terminalfont_linux.c @@ -0,0 +1,545 @@ +#include "common/font.h" +#include "terminalfont.h" +#include "common/settings.h" +#include "common/properties.h" +#include "common/parsing.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/mallocHelper.h" +#include "common/strutil.h" +#include "common/binary.h" +#include "detection/terminalshell/terminalshell.h" +#include "detection/displayserver/displayserver.h" + +static const char* getSystemMonospaceFont(void) { + const FFDisplayServerResult* wmde = ffConnectDisplayServer(); + + if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, "Cinnamon")) { + const char* systemMonospaceFont = ffSettingsGetGnome("/org/cinnamon/desktop/interface/monospace-font-name", "org.cinnamon.desktop.interface", NULL, "monospace-font-name", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(systemMonospaceFont)) { + return systemMonospaceFont; + } + } else if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, "Mate")) { + const char* systemMonospaceFont = ffSettingsGetGnome("/org/mate/interface/monospace-font-name", "org.mate.interface", NULL, "monospace-font-name", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(systemMonospaceFont)) { + return systemMonospaceFont; + } + } + + return ffSettingsGetGnome("/org/gnome/desktop/interface/monospace-font-name", "org.gnome.desktop.interface", NULL, "monospace-font-name", FF_VARIANT_TYPE_STRING).strValue; +} + +static void detectKgx(FFTerminalFontResult* terminalFont) { + // kgx (gnome console) doesn't support profiles + if (!ffSettingsGetGnome("/org/gnome/Console/use-system-font", "org.gnome.Console", NULL, "use-system-font", FF_VARIANT_TYPE_BOOL).boolValue) { + FF_AUTO_FREE const char* fontName = ffSettingsGetGnome("/org/gnome/Console/custom-font", "org.gnome.Console", NULL, "custom-font", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendF(&terminalFont->error, "Couldn't get terminal font from GSettings (org.gnome.Console::custom-font)"); + } + } else { + FF_AUTO_FREE const char* fontName = getSystemMonospaceFont(); + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendS(&terminalFont->error, "Couldn't get system monospace font name from GSettings / DConf"); + } + } +} + +static void detectPtyxis(FFTerminalFontResult* terminalFont) { + if (!ffSettingsGetGnome("/org/gnome/Ptyxis/use-system-font", "org.gnome.Ptyxis", NULL, "use-system-font", FF_VARIANT_TYPE_BOOL).boolValue) { + FF_AUTO_FREE const char* fontName = ffSettingsGetGnome("/org/gnome/Ptyxis/font-name", "org.gnome.Ptyxis", NULL, "font-name", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendF(&terminalFont->error, "Couldn't get terminal font from GSettings (org.gnome.Ptyxis::font-name)"); + } + } else { + FF_AUTO_FREE const char* fontName = getSystemMonospaceFont(); + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendS(&terminalFont->error, "Couldn't get system monospace font name from GSettings / DConf"); + } + } +} + +static void detectFromGSettings(const char* profilePath, const char* profileList, const char* profile, const char* defaultProfileKey, FFTerminalFontResult* terminalFont) { + FF_AUTO_FREE const char* defaultProfile = ffSettingsGetGSettings(profileList, NULL, defaultProfileKey, FF_VARIANT_TYPE_STRING).strValue; + if (!ffStrSet(defaultProfile)) { + ffStrbufAppendF(&terminalFont->error, "Could not get default profile from gsettings: %s", profileList); + return; + } + + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateA(128); + ffStrbufAppendS(&path, profilePath); + ffStrbufAppendS(&path, defaultProfile); + ffStrbufAppendC(&path, '/'); + + if (!ffSettingsGetGSettings(profile, path.chars, "use-system-font", FF_VARIANT_TYPE_BOOL).boolValue) { + FF_AUTO_FREE const char* fontName = ffSettingsGetGSettings(profile, path.chars, "font", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendF(&terminalFont->error, "Couldn't get terminal font from GSettings (%s::%s::font)", profile, path.chars); + } + } else { + FF_AUTO_FREE const char* fontName = getSystemMonospaceFont(); + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendS(&terminalFont->error, "Couldn't get system monospace font name from GSettings / DConf"); + } + } +} + +static void detectFromConfigFile(const char* configFile, const char* start, FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + ffParsePropFileConfig(configFile, start, &fontName); + + if (fontName.length == 0) { + ffStrbufAppendF(&terminalFont->error, "Couldn't find %s in .config/%s", start, configFile); + } else { + ffFontInitPango(&terminalFont->font, fontName.chars); + } +} + +static void detectKonsole(FFTerminalFontResult* terminalFont, const char* rcFile) { + FF_STRBUF_AUTO_DESTROY profile = ffStrbufCreate(); + if (!ffParsePropFileConfig(rcFile, "DefaultProfile =", &profile)) { + ffStrbufAppendF(&terminalFont->error, "Configuration \".config/%s\" doesn't exist", rcFile); + return; + } + + if (profile.length == 0) { + ffStrbufAppendS(&terminalFont->error, "Built-in profile is used"); + return; + } + + FF_STRBUF_AUTO_DESTROY profilePath = ffStrbufCreateA(32); + ffStrbufAppendS(&profilePath, "konsole/"); + ffStrbufAppend(&profilePath, &profile); + + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + ffParsePropFileData(profilePath.chars, "Font =", &fontName); + + if (fontName.length == 0) { + ffStrbufAppendF(&terminalFont->error, "Couldn't find \"Font=%%[^\\n]\" in \"%s\"", profilePath.chars); + } else { + ffFontInitQt(&terminalFont->font, fontName.chars); + } +} + +static void detectXFCETerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY useSysFont = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + + const char* path = "xfce4/xfconf/xfce-perchannel-xml/xfce4-terminal.xml"; + bool configFound = ffParsePropFileConfigValues(path, 2, (FFpropquery[]) { { "<property name=\"font-use-system\" type=\"bool\" value=\"", &useSysFont }, { "<property name=\"font-name\" type=\"string\" value=\"", &fontName } }); + + if (configFound) { + ffStrbufSubstrBeforeLastC(&useSysFont, '"'); + ffStrbufSubstrBeforeLastC(&fontName, '"'); + } else { + path = "xfce4/terminal/terminalrc"; + configFound = ffParsePropFileConfigValues(path, 2, (FFpropquery[]) { { "FontUseSystem = ", &useSysFont }, { "FontName = ", &fontName } }); + } + + if (configFound && (useSysFont.length == 0 || ffStrbufIgnCaseEqualS(&useSysFont, "false"))) { + if (fontName.length == 0) { + ffStrbufAppendF(&terminalFont->error, "Couldn't find FontName in %s", path); + } else { + ffFontInitPango(&terminalFont->font, fontName.chars); + } + } else { + const char* systemFontName = ffSettingsGetXFConf("xsettings", "/Gtk/MonospaceFontName", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(systemFontName)) { + ffFontInitPango(&terminalFont->font, systemFontName); + } else { + ffStrbufAppendS(&terminalFont->error, "Couldn't find xsettings::/Gtk/MonospaceFontName in XFConf"); + } + } +} + +static void detectDeepinTerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + FF_STRBUF_AUTO_DESTROY profile = ffStrbufCreateA(64); + ffSearchUserConfigFile(&instance.state.platform.configDirs, "deepin/deepin-terminal/config.conf", &profile); + FILE* file = fopen(profile.chars, "r"); + + if (file) { + char* line = NULL; + size_t len = 0; + + for (int count = 0; getline(&line, &len, file) != -1 && count < 2;) { + if (ffStrEquals(line, "[basic.interface.font]\n")) { + if (getline(&line, &len, file) != -1) { + ffParsePropLine(line, "value=", &fontName); + } + ++count; + } else if (ffStrEquals(line, "[basic.interface.font_size]\n")) { + if (getline(&line, &len, file) != -1) { + ffParsePropLine(line, "value=", &fontSize); + } + ++count; + } + } + + free(line); + fclose(file); + } + + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "Noto Sans Mono"); + } + if (fontSize.length == 0) { + ffStrbufAppendS(&fontSize, "11"); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); +} + +static void detectFootTerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + + if (!ffParsePropFileConfig("foot/foot.ini", "font=", &font) || !ffStrSet(font.chars)) { + ffFontInitValues(&terminalFont->font, "monospace", "8"); + return; + } + + // Sarasa Term SC Nerd:size=8 + uint32_t colon = ffStrbufFirstIndexC(&font, ':'); + if (colon == font.length) { + ffFontInitValues(&terminalFont->font, font.chars, "8"); + return; + } + uint32_t equal = ffStrbufNextIndexS(&font, colon, "size="); + font.chars[colon] = '\0'; + if (equal == font.length) { + ffFontInitValues(&terminalFont->font, font.chars, "8"); + return; + } + uint32_t size = equal + (uint32_t) strlen("size="); + uint32_t comma = ffStrbufNextIndexC(&font, size, ','); + if (comma < font.length) { + font.chars[comma] = '\0'; + } + ffFontInitValues(&terminalFont->font, font.chars, &font.chars[size]); + if (comma < font.length) { + ffFontInitValues(&terminalFont->fallback, &font.chars[comma + 1], NULL); + } +} + +static void detectQTerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + ffParsePropFileConfigValues("qterminal.org/qterminal.ini", 2, (FFpropquery[]) { + { "fontFamily=", &fontName }, + { "fontSize=", &fontSize }, + }); + + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "monospace"); + } + if (fontSize.length == 0) { + ffStrbufAppendS(&fontSize, "12"); + } + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); +} + +static void detectXterm(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + ffParsePropFileHomeValues(".Xresources", 2, (FFpropquery[]) { + { "xterm*faceName:", &fontName }, + { "xterm*faceSize:", &fontSize }, + }); + + if (fontName.length == 0) { + ffParsePropFileHomeValues(".Xresources", 2, (FFpropquery[]) { + { "xterm.vt100.faceName:", &fontName }, + { "xterm.vt100.faceSize:", &fontSize }, + }); + } + + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "fixed"); + } + if (fontSize.length == 0) { + ffStrbufAppendS(&fontSize, "8.0"); + } + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); +} + +static bool extractStTermFont(const char* str, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrContains(str, "size=")) { + return true; + } + ffStrbufSetNS((FFstrbuf*) userdata, len, str); + return false; +} + +static void detectSt(FFTerminalFontResult* terminalFont, const FFTerminalResult* terminal) { + FF_STRBUF_AUTO_DESTROY size = ffStrbufCreateF("/proc/%u/cmdline", terminal->pid); + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + if (!ffAppendFileBuffer(size.chars, &font)) { + ffStrbufAppendF(&terminalFont->error, "Failed to open %s", size.chars); + return; + } + + const char* p = memmem(font.chars, font.length, "\0-f", sizeof("\0-f")); // find parameter of `-f` + if (p) { + // st was executed with `-f` parameter + ffStrbufSubstrAfter(&font, (uint32_t) (p + (sizeof("\0-f") - 1) - font.chars)); + ffStrbufRecalculateLength(&font); + } else { + ffStrbufClear(&font); + + const char* error = ffBinaryExtractStrings(terminal->exePath.chars, extractStTermFont, &font, (uint32_t) strlen("size=0")); + if (error) { + ffStrbufAppendS(&terminalFont->error, error); + return; + } + if (font.length == 0) { + ffStrbufAppendS(&terminalFont->error, "No font config found in st binary"); + return; + } + } + + // JetBrainsMono Nerd Font Mono:pixelsize=12:antialias=true:autohint=true + + uint32_t index = ffStrbufFirstIndexC(&font, ':'); + if (index != font.length) { + uint32_t sIndex = ffStrbufNextIndexS(&font, index + 1, "size="); + if (sIndex != font.length) { + sIndex += (uint32_t) strlen("size="); + uint32_t sIndexEnd = ffStrbufNextIndexC(&font, sIndex, ':'); + ffStrbufSetNS(&size, sIndexEnd - sIndex, font.chars + sIndex); + } + ffStrbufSubstrBefore(&font, index); + } else { + ffStrbufClear(&size); + } + ffFontInitValues(&terminalFont->font, font.chars, size.chars); +} + +static void detectWarp(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY baseDir = ffStrbufCreateA(64); + + FF_LIST_FOR_EACH (FFstrbuf, dirPrefix, instance.state.platform.configDirs) { + // We need to copy the dir each time, because it used by multiple threads, so we can't directly write to it. + ffStrbufSet(&baseDir, dirPrefix); + ffStrbufAppendS(&baseDir, "warp-terminal/user_preferences.json"); + + yyjson_doc* doc = yyjson_read_file(baseDir.chars, YYJSON_READ_INSITU | YYJSON_READ_ALLOW_TRAILING_COMMAS | YYJSON_READ_ALLOW_COMMENTS, NULL, NULL); + if (!doc) { + continue; + } + + yyjson_val* prefs = yyjson_obj_get(yyjson_doc_get_root(doc), "prefs"); + if (yyjson_is_obj(prefs)) { + const char* fontName = yyjson_get_str(yyjson_obj_get(prefs, "FontName")); + if (!fontName) { + fontName = "Hack"; + } + const char* fontSize = yyjson_get_str(yyjson_obj_get(prefs, "FontSize")); + if (!fontSize) { + fontSize = "13"; + } + + ffFontInitValues(&terminalFont->font, fontName, fontSize); + } + yyjson_doc_free(doc); + return; + } +} + +static void detectTerminator(FFTerminalFontResult* result) { + FF_STRBUF_AUTO_DESTROY useSystemFont = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + + if (!ffParsePropFileConfigValues("terminator/config", 2, (FFpropquery[]) { + { "use_system_font =", &useSystemFont }, + { "font =", &fontName }, + }) || + ffStrbufIgnCaseEqualS(&useSystemFont, "True")) { + FF_AUTO_FREE const char* fontName = getSystemMonospaceFont(); + if (ffStrSet(fontName)) { + ffFontInitPango(&result->font, fontName); + } else { + ffStrbufAppendS(&result->error, "Couldn't get system monospace font name from GSettings / DConf"); + } + return; + } + + if (fontName.length == 0) { + ffFontInitValues(&result->font, "Mono", "10"); + } else { + ffFontInitPango(&result->font, fontName.chars); + } +} + +static void detectWestonTerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY size = ffStrbufCreate(); + ffParsePropFileConfigValues("weston.ini", 2, (FFpropquery[]) { + { "font=", &font }, + { "font-size=", &size }, + }); + if (!font.length) { + ffStrbufSetStatic(&font, "DejaVu Sans Mono"); + } + if (!size.length) { + ffStrbufSetStatic(&size, "14"); + } + ffFontInitValues(&terminalFont->font, font.chars, size.chars); +} + +static void detectUrxvt(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + if (!(ffParsePropFileHomeValues(".Xresources", 1, (FFpropquery[]) { + { "URxvt.font:", &buffer }, + }) || + ffParsePropFileHomeValues(".Xdefaults", 1, (FFpropquery[]) { + { "URxvt.font:", &buffer }, + }))) { + ffStrbufAppendS(&terminalFont->error, "Could not find URxvt.font in .Xresources or .Xdefaults"); + return; + } + + uint32_t index = 0; + + char* line = NULL; + size_t len = 0; + while (ffStrbufGetdelim(&line, &len, ',', &buffer)) { + FFfont* font = index == 0 ? &terminalFont->font : &terminalFont->fallback; + if (line[0] == '-') { + ffFontInitXlfd(font, line); + } else if (ffStrStartsWith(line, "xft:")) { + ffFontInitXft(font, line + 4); + } else { + ffStrbufAppendF(&terminalFont->error, "Unknown URxvt font format: %s", line); + continue; + } + index++; + if (index > 1) { + break; + } + } +} + +static bool detectCosmicTerm(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(&instance.state.platform.homeDir); + ffStrbufAppendS(&path, ".config/cosmic/com.system76.CosmicTerm/v1/"); + uint32_t baseLen = path.length; + + ffStrbufAppendS(&path, "font_name"); + ffReadFileBuffer(path.chars, &fontName); + ffStrbufTrim(&path, '"'); + ffStrbufSubstrBefore(&path, baseLen); + if (fontName.length == 0) { + ffStrbufSetStatic(&fontName, "Noto Sans Mono"); + } + + ffStrbufAppendS(&path, "font_size"); + if (ffReadFileBuffer(path.chars, &fontSize)) { + ffStrbufAppendS(&fontSize, "px"); + } + ffStrbufSubstrBefore(&path, baseLen); + if (fontSize.length == 0) { + ffStrbufSetStatic(&fontSize, "14px"); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); + + return true; +} + +#ifdef __HAIKU__ +static void detectHaikuTerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY size = ffStrbufCreate(); + ffParsePropFileConfigValues("Terminal/Default", 2, (FFpropquery[]) { + { "\"Half Font Family\" , ", &font }, + { "\"Half Font Size\" , ", &size }, + }); + if (!font.length) { + ffStrbufSetStatic(&font, "Noto Sans Mono"); + } + if (!size.length) { + ffStrbufSetStatic(&size, "12"); + } + ffFontInitValues(&terminalFont->font, font.chars, size.chars); +} +#endif + +bool +#ifdef __ANDROID__ +ffDetectTerminalFontPlatformLinux +#else +ffDetectTerminalFontPlatform +#endif + (const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) { + if (ffStrbufIgnCaseEqualS(&terminal->processName, "konsole")) { + detectKonsole(terminalFont, "konsolerc"); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "yakuake")) { + detectKonsole(terminalFont, "yakuakerc"); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "xfce4-terminal")) { + detectXFCETerminal(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "lxterminal")) { + detectFromConfigFile("lxterminal/lxterminal.conf", "fontname =", terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "tilix")) { + detectFromGSettings("/com/gexperts/Tilix/profiles/", "com.gexperts.Tilix.ProfilesList", "com.gexperts.Tilix.Profile", "default", terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "gnome-terminal")) { + detectFromGSettings("/org/gnome/terminal/legacy/profiles:/:", "org.gnome.Terminal.ProfilesList", "org.gnome.Terminal.Legacy.Profile", "default", terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "ptyxis-agent")) { + detectPtyxis(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "kgx")) { + detectKgx(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "mate-terminal")) { + detectFromGSettings("/org/mate/terminal/profiles/", "org.mate.terminal.global", "org.mate.terminal.profile", "default-profile", terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "deepin-terminal")) { + detectDeepinTerminal(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "foot")) { + detectFootTerminal(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "qterminal")) { + detectQTerminal(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "xterm")) { + detectXterm(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "st")) { + detectSt(terminalFont, terminal); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "warp")) { + detectWarp(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "weston-terminal")) { + detectWestonTerminal(terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "terminator")) { + detectTerminator(terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "sakura")) { + detectFromConfigFile("sakura/sakura.conf", "font=", terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "cosmic-term")) { + detectCosmicTerm(terminalFont); + } +#ifdef __HAIKU__ + else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "Terminal")) { + detectHaikuTerminal(terminalFont); + } +#endif + else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "termite")) { + detectFromConfigFile("termite/config", "font =", terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "rxvt") || ffStrbufIgnCaseEqualS(&terminal->processName, "urxvt") || ffStrbufIgnCaseEqualS(&terminal->processName, "urxvtd")) { + detectUrxvt(terminalFont); + } else { + return false; + } + return true; +} diff --git a/src/detection/terminalfont/terminalfont_windows.c b/src/detection/terminalfont/terminalfont_windows.c new file mode 100644 index 0000000..87566af --- /dev/null +++ b/src/detection/terminalfont/terminalfont_windows.c @@ -0,0 +1,287 @@ +#include "common/library.h" +#include "common/io.h" +#include "common/path.h" +#include "common/processing.h" +#include "common/properties.h" +#include "common/windows/unicode.h" +#include "common/windows/registry.h" +#include "common/strutil.h" +#include "detection/terminalshell/terminalshell.h" +#include "terminalfont.h" + +#include <shlobj.h> +#include <windows.h> +#include <stdlib.h> + +static const char* detectWTProfile(yyjson_val* profile, FFstrbuf* name, double* size) { + yyjson_val* font = yyjson_obj_get(profile, "font"); + if (!font) { + return "yyjson_obj_get(profile, \"font\"); failed"; + } + + if (!yyjson_is_obj(font)) { + return "yyjson_is_obj(font) returns false"; + } + + if (name->length == 0) { + ffStrbufAppendJsonVal(name, yyjson_obj_get(font, "face")); + } + + if (*size < 0) { + yyjson_val* psize = yyjson_obj_get(font, "size"); + if (yyjson_is_num(psize)) { + *size = unsafe_yyjson_get_num(psize); + } + } + return NULL; +} + +static inline void wrapYyjsonFree(yyjson_doc** doc) { + assert(doc); + if (*doc) { + yyjson_doc_free(*doc); + } +} + +static const char* detectFromWTImpl(FFstrbuf* content, FFstrbuf* name, double* size) { + yyjson_doc* FF_A_CLEANUP(wrapYyjsonFree) doc = yyjson_read_opts(content->chars, content->length, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS, NULL, NULL); + if (!doc) { + return "Failed to parse WT JSON config file"; + } + + yyjson_val* const root = yyjson_doc_get_root(doc); + assert(root); + + yyjson_val* profiles = yyjson_obj_get(root, "profiles"); + if (!profiles) { + return "yyjson_obj_get(root, \"profiles\") failed"; + } + + FF_STRBUF_AUTO_DESTROY wtProfileId = ffStrbufCreateS(getenv("WT_PROFILE_ID")); + ffStrbufTrim(&wtProfileId, '\''); + if (wtProfileId.length > 0) { + yyjson_val* list = yyjson_obj_get(profiles, "list"); + if (yyjson_is_arr(list)) { + yyjson_val* profile; + size_t idx, max; + yyjson_arr_foreach (list, idx, max, profile) { + yyjson_val* guid = yyjson_obj_get(profile, "guid"); + + if (ffStrbufEqualS(&wtProfileId, yyjson_get_str(guid))) { + detectWTProfile(profile, name, size); + break; + } + } + } + } + + yyjson_val* defaults = yyjson_obj_get(profiles, "defaults"); + if (defaults) { + detectWTProfile(defaults, name, size); + } + + if (name->length == 0) { + ffStrbufSetS(name, "Cascadia Mono"); + } + if (*size < 0) { + *size = 12; + } + return NULL; +} + +static void detectFromWindowsTerminal(const FFstrbuf* terminalExe, FFTerminalFontResult* terminalFont) { + // https://learn.microsoft.com/en-us/windows/terminal/install#settings-json-file + FF_STRBUF_AUTO_DESTROY json = ffStrbufCreate(); + const char* error = NULL; + + if (terminalExe && ffIsAbsolutePath(terminalExe->chars)) { + FF_STRBUF_AUTO_DESTROY jsonPath = ffStrbufCreateA(MAX_PATH); + ffStrbufAppendNS(&jsonPath, ffStrbufLastIndexC(terminalExe, '\\') + 1, terminalExe->chars); + ffStrbufAppendS(&jsonPath, ".portable"); + + if (ffPathExists(jsonPath.chars, FF_PATHTYPE_ANY)) { + ffStrbufSubstrBefore(&jsonPath, jsonPath.length - strlen(".portable")); + ffStrbufAppendS(&jsonPath, "settings\\settings.json"); + if (!ffAppendFileBuffer(jsonPath.chars, &json)) { + error = "Error reading Windows Terminal portable settings JSON file"; + } + } else { + PWSTR localAppDataW = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, NULL, &localAppDataW))) { + ffStrbufSetWS(&jsonPath, localAppDataW); + CoTaskMemFree(localAppDataW); + + if (ffStrbufContainIgnCaseS(terminalExe, "_8wekyb3d8bbwe\\")) { + // Microsoft Store version + if (ffStrbufContainIgnCaseS(terminalExe, ".WindowsTerminalPreview_")) { + // Preview version + ffStrbufAppendS(&jsonPath, "\\Packages\\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\\LocalState\\settings.json"); + if (!ffAppendFileBuffer(jsonPath.chars, &json)) { + error = "Error reading Windows Terminal Preview settings JSON file"; + } + } else { + // Stable version + ffStrbufAppendS(&jsonPath, "\\Packages\\Microsoft.WindowsTerminal_8wekyb3d8bbwe\\LocalState\\settings.json"); + if (!ffAppendFileBuffer(jsonPath.chars, &json)) { + error = "Error reading Windows Terminal settings JSON file"; + } + } + } else { + ffStrbufAppendS(&jsonPath, "\\Microsoft\\Windows Terminal\\settings.json"); + if (!ffAppendFileBuffer(jsonPath.chars, &json)) { + error = "Error reading Windows Terminal settings JSON file"; + } + } + } + } + } + + if (!error && json.length == 0) { + error = ffProcessAppendStdOut(&json, (char* const[]) { "cmd.exe", "/c", + // print the file content directly, so we don't need to handle the difference of Windows and POSIX path + "if exist %LOCALAPPDATA%\\Packages\\Microsoft.WindowsTerminal_8wekyb3d8bbwe\\LocalState\\settings.json " + "( type %LOCALAPPDATA%\\Packages\\Microsoft.WindowsTerminal_8wekyb3d8bbwe\\LocalState\\settings.json ) " + "else if exist %LOCALAPPDATA%\\Packages\\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\\LocalState\\settings.json " + "( type %LOCALAPPDATA%\\Packages\\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\\LocalState\\settings.json ) " + "else if exist \"%LOCALAPPDATA%\\Microsoft\\Windows Terminal\\settings.json\" " + "( type %LOCALAPPDATA%\\Microsoft\\Windows Terminal\\settings.json ) " + "else ( call )", + NULL }); + } + + if (error) { + ffStrbufAppendS(&terminalFont->error, error); + return; + } + ffStrbufTrimRight(&json, '\n'); + if (json.length == 0) { + ffStrbufAppendS(&terminalFont->error, "Cannot find file \"settings.json\""); + return; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + double size = -1; + error = detectFromWTImpl(&json, &name, &size); + + if (error) { + ffStrbufAppendS(&terminalFont->error, error); + } else { + char sizeStr[16]; + snprintf(sizeStr, ARRAY_SIZE(sizeStr), "%g", size); + ffFontInitValues(&terminalFont->font, name.chars, sizeStr); + } +} + +static void detectMintty(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + if (!ffParsePropFileConfigValues("mintty/config", 2, (FFpropquery[]) { { "Font=", &fontName }, { "FontHeight=", &fontSize } })) { + ffParsePropFileConfigValues(".minttyrc", 2, (FFpropquery[]) { { "Font=", &fontName }, { "FontHeight=", &fontSize } }); + } + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "Lucida Console"); + } + if (fontSize.length == 0) { + ffStrbufAppendC(&fontSize, '9'); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); +} + +static void detectConhost(FFTerminalFontResult* terminalFont) { + CONSOLE_FONT_INFOEX cfi = { .cbSize = sizeof(cfi) }; + if (!GetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi)) { + ffStrbufAppendS(&terminalFont->error, "GetCurrentConsoleFontEx() failed"); + return; + } + + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreateWS(cfi.FaceName); + + char fontSize[16]; + _ultoa((unsigned long) (cfi.dwFontSize.Y), fontSize, 10); + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize); +} + +static void detectConEmu(FFTerminalFontResult* terminalFont) { + // https://conemu.github.io/en/ConEmuXml.html#search-sequence + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + const char* paths[] = { "ConEmuDir", "ConEmuBaseDir", "APPDATA" }; + for (uint32_t i = 0; i < ARRAY_SIZE(paths); ++i) { + ffStrbufSetS(&path, getenv(paths[i])); + if (path.length > 0) { + ffStrbufAppendS(&path, "/ConEmu.xml"); + if (ffParsePropFileValues(path.chars, 2, (FFpropquery[]) { { "<value name=\"FontName\" type=\"string\" data=\"", &fontName }, { "<value name=\"FontSize\" type=\"ulong\" data=\"", &fontSize } })) { + break; + } + } + } + + if (fontName.length == 0 && fontSize.length == 0) { + ffStrbufAppendS(&terminalFont->error, "Failed to parse ConEmu.xml"); + return; + } + + if (fontName.length > 0) { + ffStrbufSubstrBeforeLastC(&fontName, '"'); + } else { + ffStrbufAppendS(&fontName, "Consola"); + } + + if (fontSize.length > 0) { + ffStrbufSubstrBeforeLastC(&fontSize, '"'); + } else { + ffStrbufAppendS(&fontSize, "14"); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); +} + +static void detectWarp(FFTerminalFontResult* terminalFont) { + FF_AUTO_CLOSE_FD HANDLE key = NULL; + if (!ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Software\\Warp.dev\\Warp", &key, &terminalFont->error)) { + return; + } + + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + if (ffRegReadValues(key, 2, (FFRegValueArg[]) { FF_ARG(fontName, L"FontName"), FF_ARG(fontSize, L"FontSize") }, &terminalFont->error)) { + ffStrbufTrim(&fontName, '"'); + ffStrbufAppendS(&fontSize, "px"); + } else { + ffStrbufSetS(&fontName, "Hack"); + ffStrbufSetS(&fontSize, "13.0px"); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); + + FFstrbuf* fontWeight = FF_LIST_ADD(FFstrbuf, terminalFont->font.styles); + ffStrbufInit(fontWeight); + if (ffRegReadStrbuf(key, L"FontWeight", fontWeight, NULL)) { + ffStrbufTrim(fontWeight, '"'); + } else { + ffStrbufSetStatic(fontWeight, "Normal"); + } +} + +bool ffDetectTerminalFontPlatform(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) { + if (ffStrbufIgnCaseEqualS(&terminal->processName, "Windows Terminal") || + ffStrbufIgnCaseEqualS(&terminal->processName, "WindowsTerminal.exe")) { + detectFromWindowsTerminal(&terminal->exe, terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "mintty")) { + detectMintty(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "conhost.exe")) { + detectConhost(terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "ConEmu")) { + detectConEmu(terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "warp")) { + detectWarp(terminalFont); + } else { + return false; + } + return true; +} |