mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat(settings): add font ligature toggle (#38)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
+23
-1
@@ -9,7 +9,7 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use gpui::{Global, Hsla, Rgba, rgb};
|
||||
use gpui::{FontFeatures, Global, Hsla, Rgba, rgb};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Top-level configuration. Stored as a GPUI global so any view can read it.
|
||||
@@ -26,6 +26,10 @@ pub struct Config {
|
||||
/// Optional distinct face for italic cells. `None` reuses `font_family` with a
|
||||
/// synthesized italic slant.
|
||||
pub font_family_italic: Option<String>,
|
||||
/// Optional OpenType font features for terminal text. When absent, tty7 keeps
|
||||
/// terminal-safe defaults and disables contextual ligatures; when present,
|
||||
/// this map is passed through to gpui as-is (for example `{ "calt": true }`).
|
||||
pub font_features: Option<FontFeatures>,
|
||||
/// Base font size in pixels.
|
||||
pub font_size: f32,
|
||||
/// Line height as a multiple of the font size (e.g. 1.35 → a 13px font gets
|
||||
@@ -230,6 +234,7 @@ impl Default for Config {
|
||||
],
|
||||
font_family_bold: None,
|
||||
font_family_italic: None,
|
||||
font_features: None,
|
||||
font_size: 15.0,
|
||||
line_height: 1.4,
|
||||
theme: "light".to_string(),
|
||||
@@ -667,6 +672,23 @@ mod tests {
|
||||
assert_eq!(color_or(&Some("#ffffff".to_string()), 0x000000), white);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_features_are_optional_and_parse_as_gpui_features() {
|
||||
let cfg: Config =
|
||||
serde_json::from_str(r#"{"font_features":{"calt":true,"liga":1}}"#).unwrap();
|
||||
let features = cfg.font_features.expect("font features should parse");
|
||||
assert_eq!(features.is_calt_enabled(), Some(true));
|
||||
assert!(
|
||||
features
|
||||
.tag_value_list()
|
||||
.iter()
|
||||
.any(|(tag, value)| tag == "liga" && *value == 1)
|
||||
);
|
||||
|
||||
let default_cfg = Config::default();
|
||||
assert!(default_cfg.font_features.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ansi_color_overrides_parse_and_default_independently() {
|
||||
let cfg: Config =
|
||||
|
||||
+24
-4
@@ -151,10 +151,13 @@ fn build_font(base: &Font, bold: bool, italic: bool) -> Font {
|
||||
FontStyle::Normal
|
||||
};
|
||||
// Batched runs shape several chars in one line, where a programming font's
|
||||
// contextual ligatures (`calt`, e.g. Fira Code's "->") would fuse cells into
|
||||
// one glyph and break `force_width`'s one-glyph-per-column snapping. A cell
|
||||
// grid can't ligate — Zed's terminal disables the same feature.
|
||||
f.features = gpui::FontFeatures::disable_ligatures();
|
||||
// contextual ligatures (`calt`, e.g. Fira Code's "->") can fuse cells into
|
||||
// one glyph and stress `force_width`'s one-glyph-per-column snapping. Keep
|
||||
// the terminal-safe default unless the user explicitly configured OpenType
|
||||
// features on the base font.
|
||||
if f.features.tag_value_list().is_empty() {
|
||||
f.features = gpui::FontFeatures::disable_ligatures();
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
@@ -1896,6 +1899,23 @@ mod tests {
|
||||
assert_eq!(seg_clip_width(true, 1, cell), px(20.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_font_disables_ligatures_unless_features_are_configured() {
|
||||
let font = build_font(&gpui::font("Test"), false, false);
|
||||
assert_eq!(font.features.is_calt_enabled(), Some(false));
|
||||
|
||||
let mut configured = gpui::font("Test");
|
||||
configured.features = serde_json::from_str(r#"{"calt":true,"liga":1}"#).unwrap();
|
||||
let font = build_font(&configured, false, false);
|
||||
assert_eq!(font.features.is_calt_enabled(), Some(true));
|
||||
assert!(
|
||||
font.features
|
||||
.tag_value_list()
|
||||
.iter()
|
||||
.any(|(tag, value)| tag == "liga" && *value == 1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn segment_row_keeps_powerline_separators_solo() {
|
||||
// The native-draw intercept lives in the Solo arm of `paint_glyphs`;
|
||||
|
||||
@@ -73,6 +73,9 @@ pub struct TerminalView {
|
||||
/// Optional distinct base face for italic cells (from `font_family_italic`).
|
||||
/// `None` → synthesize italic from `font`.
|
||||
pub font_italic: Option<Font>,
|
||||
/// User-configured OpenType features for terminal fonts. `None` preserves
|
||||
/// tty7's terminal-safe default (ligatures disabled); `Some` is opt-in.
|
||||
font_features: Option<gpui::FontFeatures>,
|
||||
pub font_size: Pixels,
|
||||
/// Line height as a multiple of `font_size`; the element turns it into the
|
||||
/// concrete row height each frame. Sourced from `Config::line_height`.
|
||||
@@ -438,14 +441,21 @@ impl TerminalView {
|
||||
let fallbacks = fallback_chain(&font_family, &config.font_fallbacks);
|
||||
let font_size = px(config.font_size);
|
||||
let line_height_mul = config.line_height;
|
||||
let font_features = config.font_features.clone();
|
||||
let mut font = gpui::font(font_family);
|
||||
font.fallbacks = Some(gpui::FontFallbacks::from_fonts(fallbacks.clone()));
|
||||
if let Some(features) = &font_features {
|
||||
font.features = features.clone();
|
||||
}
|
||||
// Optional distinct bold/italic faces, each carrying the same fallback
|
||||
// chain so glyph coverage matches the primary face.
|
||||
let alt_font = |family: &Option<String>| {
|
||||
family.as_ref().map(|f| {
|
||||
let mut af = gpui::font(f.clone());
|
||||
af.fallbacks = Some(gpui::FontFallbacks::from_fonts(fallbacks.clone()));
|
||||
if let Some(features) = &font_features {
|
||||
af.features = features.clone();
|
||||
}
|
||||
af
|
||||
})
|
||||
};
|
||||
@@ -591,6 +601,7 @@ impl TerminalView {
|
||||
font,
|
||||
font_bold,
|
||||
font_italic,
|
||||
font_features,
|
||||
font_size,
|
||||
line_height_mul,
|
||||
cell_width: px(8.),
|
||||
@@ -1647,6 +1658,9 @@ impl TerminalView {
|
||||
let fallbacks = self.font.fallbacks.clone();
|
||||
let mut font = gpui::font(family);
|
||||
font.fallbacks = fallbacks;
|
||||
if let Some(features) = &self.font_features {
|
||||
font.features = features.clone();
|
||||
}
|
||||
self.font = font;
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1664,12 +1678,37 @@ impl TerminalView {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Apply OpenType features to the live terminal fonts. `None` restores the
|
||||
/// terminal-safe default path, where the renderer disables contextual
|
||||
/// ligatures while building paint faces.
|
||||
pub fn set_font_features(
|
||||
&mut self,
|
||||
features: Option<gpui::FontFeatures>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.font_features = features.clone();
|
||||
let apply = |font: &mut Font| {
|
||||
font.features = features.clone().unwrap_or_default();
|
||||
};
|
||||
apply(&mut self.font);
|
||||
if let Some(font) = &mut self.font_bold {
|
||||
apply(font);
|
||||
}
|
||||
if let Some(font) = &mut self.font_italic {
|
||||
apply(font);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Build an alternate face from a family name, reusing the primary's
|
||||
/// fallbacks. `None` → `None` (fall back to synthesizing from `self.font`).
|
||||
fn alt_font(&self, family: Option<String>) -> Option<Font> {
|
||||
family.map(|f| {
|
||||
let mut af = gpui::font(f);
|
||||
af.fallbacks = self.font.fallbacks.clone();
|
||||
if let Some(features) = &self.font_features {
|
||||
af.features = features.clone();
|
||||
}
|
||||
af
|
||||
})
|
||||
}
|
||||
|
||||
+45
-2
@@ -10,6 +10,7 @@ use gpui_component::input::{InputEvent, InputState};
|
||||
use gpui_component::select::{SearchableVec, SelectEvent, SelectState};
|
||||
use gpui_component::slider::{SliderEvent, SliderState};
|
||||
use gpui_component::{ActiveTheme as _, IndexPath, TitleBar};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::core::actions::*;
|
||||
use crate::core::config::{Config, NewTabPosition, ShellConfig, color_or, hsla_to_hex6};
|
||||
@@ -100,6 +101,9 @@ pub struct Tty7App {
|
||||
/// tracked so the hot-reload observer can diff them like `font_family`.
|
||||
pub(crate) font_family_bold: Option<String>,
|
||||
pub(crate) font_family_italic: Option<String>,
|
||||
/// Currently-applied OpenType features for terminal fonts. `None` means the
|
||||
/// terminal-safe default (ligatures disabled).
|
||||
pub(crate) font_features: Option<gpui::FontFeatures>,
|
||||
/// Keeps the `observe_global::<Config>` subscription alive for the app's
|
||||
/// lifetime so external edits to `config.json` (swapped in by the watcher in
|
||||
/// `main.rs`) live-apply font size / line height / family. Never read.
|
||||
@@ -150,6 +154,7 @@ impl Tty7App {
|
||||
let font_family = cx.global::<Config>().font_family.clone();
|
||||
let font_family_bold = cx.global::<Config>().font_family_bold.clone();
|
||||
let font_family_italic = cx.global::<Config>().font_family_italic.clone();
|
||||
let font_features = cx.global::<Config>().font_features.clone();
|
||||
// Live-apply hot-reloaded config: the watcher in `main.rs` swaps the
|
||||
// `Config` global on every `config.json` change, which fires this. Theme
|
||||
// and colors are handled separately by `apply_theme`; here we cover the
|
||||
@@ -189,6 +194,7 @@ impl Tty7App {
|
||||
font_family,
|
||||
font_family_bold,
|
||||
font_family_italic,
|
||||
font_features,
|
||||
_config_watch: config_watch,
|
||||
_keystroke_watch: keystroke_watch,
|
||||
palette: None,
|
||||
@@ -545,6 +551,29 @@ impl Tty7App {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Toggle terminal font ligatures through the generic `font_features`
|
||||
/// config. On enables the common programming-font features; off restores
|
||||
/// tty7's terminal-safe default (contextual ligatures disabled).
|
||||
pub(crate) fn set_font_ligatures(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
let features = on.then(|| {
|
||||
gpui::FontFeatures(Arc::new(vec![
|
||||
("calt".to_string(), 1),
|
||||
("liga".to_string(), 1),
|
||||
]))
|
||||
});
|
||||
self.font_features = features.clone();
|
||||
for tab in &self.tabs {
|
||||
for leaf in tab.pane.leaves() {
|
||||
let features = features.clone();
|
||||
leaf.update(cx, |v, cx| v.set_font_features(features, cx));
|
||||
}
|
||||
}
|
||||
let cfg = cx.global_mut::<Config>();
|
||||
cfg.font_features = features;
|
||||
cfg.save();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Apply a picked ANSI color override and re-paint the terminal palette live.
|
||||
/// `None` clears the slot back to the active preset's ANSI value.
|
||||
pub(crate) fn set_ansi_color_override(
|
||||
@@ -1580,9 +1609,14 @@ impl Tty7App {
|
||||
/// writes it), and — because we never write the global or `save()` from here
|
||||
/// — closes the save → watch → reload loop that would otherwise oscillate.
|
||||
fn reload_from_config(&mut self, cx: &mut Context<Self>) {
|
||||
let (font_size, line_height, font_family) = {
|
||||
let (font_size, line_height, font_family, font_features) = {
|
||||
let cfg = cx.global::<Config>();
|
||||
(cfg.font_size, cfg.line_height, cfg.font_family.clone())
|
||||
(
|
||||
cfg.font_size,
|
||||
cfg.line_height,
|
||||
cfg.font_family.clone(),
|
||||
cfg.font_features.clone(),
|
||||
)
|
||||
};
|
||||
if font_size != self.font_size {
|
||||
self.font_size = font_size;
|
||||
@@ -1616,6 +1650,15 @@ impl Tty7App {
|
||||
}
|
||||
}
|
||||
}
|
||||
if font_features != self.font_features {
|
||||
self.font_features = font_features.clone();
|
||||
for tab in &self.tabs {
|
||||
for leaf in tab.pane.leaves() {
|
||||
let features = font_features.clone();
|
||||
leaf.update(cx, |v, cx| v.set_font_features(features, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (bold, italic) = {
|
||||
let cfg = cx.global::<Config>();
|
||||
(cfg.font_family_bold.clone(), cfg.font_family_italic.clone())
|
||||
|
||||
@@ -504,6 +504,13 @@ impl Tty7App {
|
||||
let cfg = cx.global::<Config>();
|
||||
let cursor_style = cfg.cursor_style;
|
||||
let cursor_blink = cfg.cursor_blink;
|
||||
let font_ligatures = cfg.font_features.as_ref().is_some_and(|features| {
|
||||
features.is_calt_enabled() == Some(true)
|
||||
|| features
|
||||
.tag_value_list()
|
||||
.iter()
|
||||
.any(|(tag, value)| tag == "liga" && *value != 0)
|
||||
});
|
||||
let colors = cfg.colors.clone();
|
||||
let ansi_colors = cfg.ansi_colors.clone();
|
||||
|
||||
@@ -614,6 +621,10 @@ impl Tty7App {
|
||||
let font_family_control = font_dropdown(&font_select);
|
||||
let font_bold_control = font_dropdown(&font_bold_select);
|
||||
let font_italic_control = font_dropdown(&font_italic_select);
|
||||
let ligature_switch = Switch::new("font-ligatures")
|
||||
.checked(font_ligatures)
|
||||
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_font_ligatures(*on, cx)))
|
||||
.into_any_element();
|
||||
|
||||
let cursor_idx = match cursor_style {
|
||||
CursorStyle::Block => 0,
|
||||
@@ -680,6 +691,12 @@ impl Tty7App {
|
||||
font_italic_control,
|
||||
cx,
|
||||
))
|
||||
.child(self.settings_row(
|
||||
"Font ligatures",
|
||||
"Enable common programming ligature features for terminal text.",
|
||||
ligature_switch,
|
||||
cx,
|
||||
))
|
||||
.child(self.section_rule(cx))
|
||||
.child(self.section_header("Cursor", cx))
|
||||
.child(self.settings_row(
|
||||
|
||||
Reference in New Issue
Block a user