Merge pull request #939 from l0ng-ai/feat/720-font-smoothing

feat(fonts): make macOS stroke thickening configurable
This commit is contained in:
l0ng-ai
2026-09-23 16:58:03 +08:00
committed by GitHub
11 changed files with 149 additions and 1 deletions
+4 -1
View File
@@ -222,7 +222,10 @@ objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSResponder", "
# `set_dock_icon_for_bare_binary` in main.rs.
# NSUserNotification is the click-to-reveal notification path — see
# `terminal::remote::macos_notify` for why it is driven directly.
objc2-foundation = { version = "0.3", features = ["NSData", "NSString", "NSUserNotification"] }
# NSUserDefaults (with NSDictionary / NSValue for the domain it takes) pins
# `AppleFontSmoothing` for this process when `font_thicken` is off — see
# `apply_font_thicken` in main.rs.
objc2-foundation = { version = "0.3", features = ["NSData", "NSDictionary", "NSString", "NSUserDefaults", "NSUserNotification", "NSValue"] }
# Dictionary-based Chinese word segmentation for double-click selection
# (`terminal::smart_select`), as a *fallback* where the OS has no tokenizer of
+23
View File
@@ -135,6 +135,16 @@ pub struct Config {
pub font_family_bold: Option<String>,
pub font_family_italic: Option<String>,
pub font_features: Option<FontFeatures>,
/// macOS only: let CoreGraphics font smoothing thicken glyph strokes — by
/// more the lighter the text, which is why light-on-dark looks bolder.
/// On by default, because it is what every macOS app draws with.
///
/// Off pins `AppleFontSmoothing` to `0` for this process alone, so glyphs
/// are drawn at the face's own weight. gpui reads that preference once, the
/// first time it rasterizes text, so a change applies at the next launch.
/// Ignored elsewhere, where there is no such dilation to turn off.
#[serde(default = "default_true")]
pub font_thicken: bool,
pub font_size: f32,
pub line_height: f32,
/// The interface's root font size, in pixels — everything outside the
@@ -629,6 +639,7 @@ impl Default for Config {
font_family_bold: None,
font_family_italic: None,
font_features: None,
font_thicken: true,
font_size: 15.0,
line_height: 1.4,
ui_font_size: default_ui_font_size(),
@@ -1497,6 +1508,18 @@ mod tests {
assert!(default_cfg.font_features.is_none());
}
#[test]
fn font_thicken_defaults_on_and_reads_an_explicit_off() {
// On is what tty7 drew before the key existed, so a config that never
// names it must keep rendering exactly as it did.
let cfg: Config = serde_json::from_str("{}").unwrap();
assert!(cfg.font_thicken);
assert!(Config::default().font_thicken);
let cfg: Config = serde_json::from_str(r#"{"font_thicken":false}"#).unwrap();
assert!(!cfg.font_thicken);
}
#[test]
fn font_features_round_trip_to_integer_valued_json() {
let features: FontFeatures =
+30
View File
@@ -15,6 +15,7 @@ description: "The bundled default, fallback chains, ligatures, and why CJK needs
| **Line height** | 1.4 | A multiple of the font size |
| **Bold font** / **Italic font** | — | Distinct faces, when you want them |
| **Font ligatures** | off | `calt`, `liga` and `clig` all stay off unless you ask |
| **Thicken strokes** | on | macOS only — see [Stroke weight on macOS](#stroke-weight-on-macos) |
## Hack is bundled
@@ -71,6 +72,35 @@ on. Name them explicitly if you want both:
}
```
## Stroke weight on macOS
macOS font smoothing thickens glyph strokes, and by how much depends on the
text colour: the lighter the text, the bolder it draws. That is why light text
on a dark theme can look heavier in tty7 than in a terminal that turns the
effect off, and why a thinner weight of the same family does not fully make up
for it.
**Settings → Appearance → Terminal text → Thicken strokes** (`font_thicken`)
turns it off, so glyphs draw at the face's own weight whatever their colour:
```json
{
"font_thicken": false
}
```
<Note>
The change applies the next time tty7 starts: the preference is read once,
when text is first drawn. It covers tty7 alone — no other app, and not the
system-wide setting.
</Note>
Leaving it on changes nothing, so an existing
`defaults write com.github.tty7 AppleFontSmoothing -int 0` keeps working. This
is the same switch as Ghostty's `font-thicken` and iTerm2's thin strokes, with
tty7 defaulting to the macOS look. Windows and Linux do not thicken strokes, so
the key does nothing there.
## CJK and the two-column grid
<Info>
+1
View File
@@ -35,6 +35,7 @@ failing the file.
| `font_family_bold` | string | — | A distinct bold face. |
| `font_family_italic` | string | — | A distinct italic face. |
| `font_features` | object | — | OpenType tags, e.g. `{"calt": true, "liga": 1}`. Four alphanumeric characters per tag. |
| `font_thicken` | bool | `true` | macOS: let font smoothing thicken strokes, light text most. `false` draws glyphs at their own weight. Applies after a restart. |
| `font_size` | number | `15` | Terminal text size in px (4–256). |
| `line_height` | number | `1.4` | Multiple of the font size (0.5–4). |
| `ui_font_size` | number | `16` | The interface's root size in px (12–24). |
+37
View File
@@ -441,6 +441,41 @@ fn set_dock_icon_for_bare_binary() {
}
}
/// Turns CoreGraphics' stroke thickening off for this process when
/// `font_thicken` is off; with it on there is nothing to do, and whatever the
/// system (or a hand-written `defaults write`) says stands.
///
/// gpui decides whether to dilate a glyph from `AppleFontSmoothing`, read with
/// `CFPreferencesCopyAppValue` the first time it rasterizes text and cached for
/// the life of the process. So this has to land before the application exists,
/// and a change waits for the next launch.
///
/// The value goes into the argument domain — the volatile one that
/// `-AppleFontSmoothing 0` on the command line would fill. It outranks both this
/// app's persisted defaults and the global domain, and it lives only in this
/// process's memory: nothing reaches disk, so no other app sees it and a later
/// launch with the key back on does not inherit it.
#[cfg(target_os = "macos")]
fn apply_font_thicken(thicken: bool) {
use objc2_foundation::{
NSArgumentDomain, NSMutableCopying, NSNumber, NSUserDefaults, ns_string,
};
if thicken {
return;
}
let defaults = NSUserDefaults::standardUserDefaults();
// SAFETY: a Foundation constant, initialized before `main` runs.
let domain = unsafe { NSArgumentDomain };
// Merged into, not replaced: the domain already holds any `-Key value`
// pairs the process was launched with.
let arguments = defaults.volatileDomainForName(domain).mutableCopy();
arguments.insert(ns_string!("AppleFontSmoothing"), &*NSNumber::new_i32(0));
// SAFETY: keys are `NSString` and values property-list objects, which is
// the shape a defaults domain requires.
unsafe { defaults.setVolatileDomain_forName(&arguments, domain) };
}
/// Delivers Finder document opens and LaunchServices URL opens after gpui has
/// created the application. The native callback queues requests; UI state is
/// then changed on gpui's application loop.
@@ -593,6 +628,8 @@ fn main() {
let (config, config_outcome) = crate::core::config::Config::load_with_outcome();
let gui_language = config.gui_language.clone();
#[cfg(target_os = "macos")]
apply_font_thicken(config.font_thicken);
// After the PATH enrichment above, which is what makes the candidate scan
// see the user's real PATH rather than the stub a Finder launch inherits —
+6
View File
@@ -3497,6 +3497,12 @@ impl Tty7App {
self.update_config(cx, |cfg| cfg.show_tray_icon = on);
}
/// Saved only: gpui reads the preference this drives once per process, so
/// it takes hold at the next launch (see `apply_font_thicken` in main.rs).
pub(crate) fn set_font_thicken(&mut self, on: bool, cx: &mut Context<Self>) {
self.update_config(cx, |cfg| cfg.font_thicken = on);
}
pub(crate) fn set_macos_option_as_alt(&mut self, on: bool, cx: &mut Context<Self>) {
self.update_config(cx, |cfg| cfg.macos_option_as_alt = on);
}
+7
View File
@@ -133,6 +133,10 @@ pub fn translate_en(key: L10nKey) -> &'static str {
L10nKey::SettingsFontLigaturesDesc => {
"Enable common programming ligature features for terminal text."
}
L10nKey::SettingsFontThicken => "Thicken strokes",
L10nKey::SettingsFontThickenDesc => {
"macOS font smoothing: draws text a little bolder, light text most. Takes effect after restarting tty7."
}
L10nKey::SettingsCursor => "Cursor",
L10nKey::SettingsCursorShape => "Cursor shape",
L10nKey::SettingsCursorShapeDesc => "How the terminal cursor is drawn.",
@@ -842,6 +846,9 @@ pub fn translate_en(key: L10nKey) -> &'static str {
L10nKey::SettingsSearchFocusFollowsMouseKeywords => "pane hover activate",
L10nKey::SettingsSearchFontFamilyKeywords => "typeface monospace typography",
L10nKey::SettingsSearchFontLigaturesKeywords => "typography glyph fira",
L10nKey::SettingsSearchFontThickenKeywords => {
"font smoothing thicken bold weight thin dilation antialiasing AppleFontSmoothing"
}
L10nKey::SettingsSearchFontSizeKeywords => "typography text bigger smaller zoom",
L10nKey::SettingsSearchForwardSshLoopbackLinksKeywords => {
"ssh remote port tunnel localhost forward links ports autoforward detect"
+7
View File
@@ -135,6 +135,10 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsFontLigaturesDesc => {
"ターミナルテキストで一般的なプログラミング用リガチャー(合字)を有効にする"
}
L10nKey::SettingsFontThicken => "ストロークを太くする",
L10nKey::SettingsFontThickenDesc => {
"macOS のフォントスムージング:文字をやや太く描画し、明るい文字ほど太くなる。tty7 の再起動後に反映"
}
L10nKey::SettingsCursor => "カーソル",
L10nKey::SettingsCursorShape => "カーソルの形状",
L10nKey::SettingsCursorShapeDesc => "ターミナルカーソルの描画方法",
@@ -876,6 +880,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsSearchFontLigaturesKeywords => {
"タイポグラフィ グリフ fira font ligatures typography glyph fira"
}
L10nKey::SettingsSearchFontThickenKeywords => {
"フォントスムージング 太字 細字 ウェイト font smoothing thicken bold weight thin AppleFontSmoothing"
}
L10nKey::SettingsSearchFontSizeKeywords => {
"タイポグラフィ 文字 拡大 縮小 ズーム font size typography text bigger smaller zoom"
}
+3
View File
@@ -181,6 +181,8 @@ l10n_keys! {
SettingsItalicFontDesc,
SettingsFontLigatures,
SettingsFontLigaturesDesc,
SettingsFontThicken,
SettingsFontThickenDesc,
SettingsCursor,
SettingsCursorShape,
SettingsCursorShapeDesc,
@@ -644,6 +646,7 @@ l10n_keys! {
SettingsSearchFocusFollowsMouseKeywords,
SettingsSearchFontFamilyKeywords,
SettingsSearchFontLigaturesKeywords,
SettingsSearchFontThickenKeywords,
SettingsSearchFontSizeKeywords,
SettingsSearchForwardSshLoopbackLinksKeywords,
SettingsSearchGeminiKeywords,
+7
View File
@@ -119,6 +119,10 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsItalicFontDesc => "斜体文字使用的字体;默认由主字体合成。",
L10nKey::SettingsFontLigatures => "字体连字",
L10nKey::SettingsFontLigaturesDesc => "为终端文字启用常见的编程连字特性。",
L10nKey::SettingsFontThicken => "笔画加粗",
L10nKey::SettingsFontThickenDesc => {
"macOS 字体平滑:文字画得稍粗,浅色文字最明显。重启 tty7 后生效。"
}
L10nKey::SettingsCursor => "光标",
L10nKey::SettingsCursorShape => "光标形状",
L10nKey::SettingsCursorShapeDesc => "终端光标的绘制方式。",
@@ -771,6 +775,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsSearchFontLigaturesKeywords => {
"字体连字 连字 字形 typography ligatures glyph fira"
}
L10nKey::SettingsSearchFontThickenKeywords => {
"字体平滑 加粗 变细 字重 笔画 font smoothing thicken bold weight thin AppleFontSmoothing"
}
L10nKey::SettingsSearchFontSizeKeywords => {
"字号 字体大小 文字 放大 缩小 typography font size bigger smaller zoom"
}
+24
View File
@@ -549,6 +549,12 @@ fn settings_search_entries() -> &'static [SearchEntry] {
title: SettingsFontLigatures,
keywords: SettingsSearchFontLigaturesKeywords,
},
#[cfg(target_os = "macos")]
SearchEntry {
section: Appearance,
title: SettingsFontThicken,
keywords: SettingsSearchFontThickenKeywords,
},
SearchEntry {
section: Appearance,
title: SettingsCursorShape,
@@ -920,6 +926,7 @@ impl SearchEntry {
L10nKey::SettingsItalicFont => "font_family_italic",
L10nKey::SettingsUiFontFamily => "ui_font_family",
L10nKey::SettingsFontLigatures => "font_features",
L10nKey::SettingsFontThicken => "font_thicken",
L10nKey::SettingsOpacity => "window_opacity",
L10nKey::SettingsBlur => "window_blur",
L10nKey::SettingsBackdrop => "window_backdrop",
@@ -956,6 +963,7 @@ impl SearchEntry {
L10nKey::SettingsBoldFont => t(L10nKey::SettingsBoldFontDesc),
L10nKey::SettingsItalicFont => t(L10nKey::SettingsItalicFontDesc),
L10nKey::SettingsFontLigatures => t(L10nKey::SettingsFontLigaturesDesc),
L10nKey::SettingsFontThicken => t(L10nKey::SettingsFontThickenDesc),
L10nKey::SettingsCursorShape => t(L10nKey::SettingsCursorShapeDesc),
L10nKey::SettingsCursorBlink => t(L10nKey::SettingsCursorBlinkDesc),
L10nKey::SettingsBackgroundImage => t(L10nKey::SettingsBackgroundImageDesc),
@@ -1123,6 +1131,7 @@ impl SearchEntry {
cfg.working_directory.path != defaults.working_directory.path
}
L10nKey::SettingsFontLigatures => cfg.font_features != defaults.font_features,
L10nKey::SettingsFontThicken => cfg.font_thicken != defaults.font_thicken,
_ => false,
}
}
@@ -3488,6 +3497,7 @@ impl Tty7App {
let cfg = cx.global::<Config>();
let cursor_style = cfg.cursor_style;
let cursor_blink = cfg.cursor_blink;
let font_thicken = cfg.font_thicken;
let font_ligatures = cfg.font_features.as_ref().is_some_and(|features| {
features.is_calt_enabled() == Some(true)
|| features
@@ -3591,6 +3601,19 @@ impl Tty7App {
.checked(font_ligatures)
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_font_ligatures(*on, cx)))
.into_any_element();
// macOS alone dilates glyph strokes, so elsewhere there is no row.
let thicken_row = cfg!(target_os = "macos").then(|| {
let thicken_switch = crate::ui::theme::switch("font-thicken", cx)
.checked(font_thicken)
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_font_thicken(*on, cx)))
.into_any_element();
self.settings_row(
t(L10nKey::SettingsFontThicken),
t(L10nKey::SettingsFontThickenDesc),
thicken_switch,
cx,
)
});
let cursor_idx = match cursor_style {
CursorStyle::Block => 0,
@@ -3682,6 +3705,7 @@ impl Tty7App {
ligature_switch,
cx,
))
.when_some(thicken_row, |v, row| v.child(row))
.child(self.section_rule(cx))
.child(self.section_header(t(L10nKey::SettingsCursor), cx))
.child(self.settings_row(