Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2da4115
feat(terminal): add natural-font line height preference
WilliamWang1721 Oct 3, 2026
840c37d
fix(settings): cancel line height edit before restoring auto
WilliamWang1721 Oct 3, 2026
6d8c6f5
test(terminal): consume queued resize and isolate pointer preferences
WilliamWang1721 Oct 3, 2026
0a5fa9f
Merge 6d8c6f50b5eff98941b2fa57a224d74af7a2bf5c into b29c04b5c8c52f021…
WilliamWang1721 Oct 4, 2026
c66a230
Merge upstream main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 4, 2026
d5edff3
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 6, 2026
2c31475
Merge main into codex/issue-439-terminal-line-height
claude Oct 9, 2026
c4c6939
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 11, 2026
9d69edb
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 11, 2026
24054c5
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 13, 2026
1a28964
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 13, 2026
0448581
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 14, 2026
c7974fa
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 14, 2026
17cfadf
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 14, 2026
c553fa8
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 15, 2026
4644c9f
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 15, 2026
05920c4
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 16, 2026
4b74dcb
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 17, 2026
99a79e1
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 19, 2026
9e9586b
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 21, 2026
3157c4f
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 21, 2026
d3095ae
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 22, 2026
158f70d
Merge main into codex/issue-439-terminal-line-height
WilliamWang1721 Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Natural terminal line-height override

## Status
Proposed in the Issue #439 implementation.

## Context
Users need independent terminal spacing without editing a custom theme.

## Evidence
Terminal typography previously used shaped ascent plus descent, or the custom
theme's font-size multiplier, followed by the configured device-pixel offset.

## Decision
`terminal_line_height` is optional, finite, clamped to 0.50–5.00, and rounded to
two decimal places by nebula-settings. Explicit values multiply natural shaped
font height and override theme line height. Auto removes the setting and preserves
existing theme/natural geometry. The editor starts at 1.00; no default migration
changes existing spacing. Device-pixel offsets and rounding remain unchanged.

## Rejected alternatives
A default 1.40 would silently expand existing grids. Reusing the theme key would
change its established font-size semantics. Separate pointer/selection geometry
would duplicate the terminal cell-height authority.

## Consequences
Startup and live layout call the same typography function. Prepaint writes the
new cell height into the grid, PTY viewport and pointer/selection layout together.
Existing settings reset removes the explicit override.

## Validation
Regression coverage checks persistence, precision/range, reset, editor commit/
cancel/blur, theme precedence and device-pixel geometry. Execution is delegated
to GitHub Actions; native visual acceptance remains separate.

## Supersedes
None.

## Revisit when
Font metrics or theme line-height semantics change.
5 changes: 5 additions & 0 deletions nebula_app/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,11 @@
"changed": "The package changed after inspection. Choose the file again before installing."
}
},
"terminal_line_height": {
"label": "Line height",
"auto": "Auto",
"description": "0.50–5.00; 1.00 uses the font’s natural height. An explicit value overrides theme line height. Auto preserves the theme or natural height."
},
"ssh.ports.button": "Ports",
"ssh.ports.title": "SSH port forwarding",
"ssh.ports.empty": "No forwarded ports",
Expand Down
5 changes: 5 additions & 0 deletions nebula_app/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,11 @@
"changed": "主题包在检查后发生了变化。请重新选择文件,再确认安装。"
}
},
"terminal_line_height": {
"label": "行高",
"auto": "自动",
"description": "0.50–5.00;1.00 为字体自然高度。显式值覆盖主题行高,自动保留主题或自然高度。"
},
"ssh.ports.button": "端口",
"ssh.ports.title": "SSH 端口转发",
"ssh.ports.empty": "暂无端口转发",
Expand Down
2 changes: 2 additions & 0 deletions nebula_app/src/gpui_shell/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub struct Settings {
/// Optional theme line-height multiplier. `None` keeps the shaped font
/// metrics, while `Some(multiplier)` is resolved as multiplier * font size.
pub(crate) theme_line_height: Option<f32>,
pub(crate) terminal_line_height: Option<f32>,
/// Effective window material values after user settings and theme defaults
/// have been merged once during settings loading.
pub(crate) visual_opacity: f32,
Expand Down Expand Up @@ -282,6 +283,7 @@ impl Settings {
font_offset_x: f32::from(offset.x),
font_offset_y: f32::from(offset.y),
theme_line_height,
terminal_line_height: runtime.terminal_line_height,
visual_opacity,
visual_blur,
palette,
Expand Down
1 change: 1 addition & 0 deletions nebula_app/src/gpui_shell/settings_pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ pub struct SettingsPane {
font_family_cjk_input: Entity<InputState>,
font_size_input: Entity<InputState>,
font_size_editing: Option<bool>,
line_height_editing: bool,
/// 字体输入框上一帧的窗口坐标。字体目录是宽弹层,不能把整条设置行当
/// 锚点;否则输入框在右侧、菜单却会从正文左缘展开。
font_picker_trigger_bounds: Option<gpui::Bounds<gpui::Pixels>>,
Expand Down
116 changes: 113 additions & 3 deletions nebula_app/src/gpui_shell/settings_pane/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ impl SettingsPane {
cx: &mut Context<Self>,
) {
let Some(ui) = self.font_size_editing.take() else { return };
let line_height = std::mem::take(&mut self.line_height_editing);
if apply {
let value = self.font_size_input.read(cx).value();
if let Ok(size) = value.trim().parse::<f32>() {
if size.is_finite() {
let (key, min, max) =
if ui { ("ui_font_size", 10.0, 24.0) } else { ("font_size", 4.0, 96.0) };
self.persist(&[(key, format!("{:.2}", size.clamp(min, max)))], cx);
if line_height {
let height = nebula_settings::normalize_terminal_line_height(size);
self.persist(&[("terminal_line_height", format!("{height:.2}"))], cx);
} else {
self.persist(&[(key, format!("{:.2}", size.clamp(min, max)))], cx);
}
}
}
}
Expand Down Expand Up @@ -147,7 +153,7 @@ impl SettingsPane {
this.persist(&[(key, format!("{next:.2}"))], cx);
})),
)
.child(if self.font_size_editing == Some(ui) {
.child(if self.font_size_editing == Some(ui) && !self.line_height_editing {
Input::new(&self.font_size_input)
.appearance(false)
.focus_bordered(false)
Expand All @@ -168,6 +174,7 @@ impl SettingsPane {
.h(px(34.0))
.label(format!("{size} px"))
.on_click(cx.listener(move |this, _, window, cx| {
this.line_height_editing = false;
this.font_size_editing = Some(ui);
this.font_size_input.update(cx, |input, cx| {
let value = size.to_string();
Expand Down Expand Up @@ -210,6 +217,67 @@ impl SettingsPane {
.into_any_element()
}

pub(super) fn terminal_line_height_row(&self, cx: &Context<Self>) -> gpui::AnyElement {
use crate::i18n::Message;
let language = crate::gpui_shell::config::ui_language(cx);
let value = self.runtime.terminal_line_height;
let control = h_flex()
.gap(px(4.0))
.child(if self.line_height_editing {
Input::new(&self.font_size_input)
.appearance(false)
.cleanable(false)
.w(px(90.0))
.h(px(34.0))
.aria_label(language.text(Message::TerminalLineHeightLabel))
.into_any_element()
} else {
Button::new("terminal-line-height-edit")
.debug_selector(|| "terminal-line-height-edit".to_owned())
.ghost()
.w(px(90.0))
.h(px(34.0))
.label(value.map(|value| format!("{value:.2}")).unwrap_or_else(|| {
language.text(Message::TerminalLineHeightAuto).to_owned()
}))
.on_click(cx.listener(move |this, _, window, cx| {
this.font_size_editing = Some(false);
this.line_height_editing = true;
this.font_size_input.update(cx, |input, cx| {
let value = format!(
"{:.2}",
value.unwrap_or(nebula_settings::DEFAULT_TERMINAL_LINE_HEIGHT)
);
input.set_value(value.clone(), window, cx);
input.focus(window, cx);
input.set_selected_range(0..value.len(), cx);
});
cx.notify();
}))
.into_any_element()
})
.child(
Button::new("terminal-line-height-reset")
.debug_selector(|| "terminal-line-height-reset".to_owned())
.ghost()
.label(language.text(Message::TerminalLineHeightAuto))
.disabled(value.is_none())
.on_click(cx.listener(|this, _, window, cx| {
if this.line_height_editing {
this.finish_font_size_edit(false, window, cx);
}
this.persist(&[("terminal_line_height", String::new())], cx);
})),
);
self.row(
language.text(Message::TerminalLineHeightLabel),
language.text(Message::TerminalLineHeightDescription),
control,
cx,
)
.into_any_element()
}

pub(super) fn section_appearance(
&mut self,
window: &mut Window,
Expand Down Expand Up @@ -289,11 +357,20 @@ mod tests {
("ui_font_size", "17", "tab", 17.0),
("ui_font_size", "18", "shift-tab", 18.0),
("ui_font_size", "16", "blur", 16.0),
("terminal_line_height", "1.456", "enter", 1.46),
("terminal_line_height", "2", "escape", 1.46),
("terminal_line_height", "NaN", "enter", 1.46),
("terminal_line_height", "99", "tab", 5.0),
("terminal_line_height", "0", "blur", 0.5),
] {
cx.update(|window, cx| {
let _ = window.draw(cx);
});
let selector = if key == "font_size" { "font_size-edit" } else { "ui_font_size-edit" };
let selector = match key {
"font_size" => "font_size-edit",
"terminal_line_height" => "terminal-line-height-edit",
_ => "ui_font_size-edit",
};
let bounds = cx.debug_bounds(selector).unwrap();
// Increasing the interface size can move this row below the fold.
// Reveal it with the same wheel path a user takes before clicking.
Expand Down Expand Up @@ -359,6 +436,8 @@ mod tests {
);
let actual = if key == "font_size" {
pane.terminal_font_size_px(cx)
} else if key == "terminal_line_height" {
pane.runtime.terminal_line_height.unwrap()
} else {
pane.font_size_px(cx)
};
Expand All @@ -368,10 +447,41 @@ mod tests {
if key == "font_size" {
assert_eq!(saved.font_size_px, Some(expected));
assert_eq!(saved.ui_font_size_px, Some(14.0));
} else if key == "terminal_line_height" {
assert_eq!(saved.terminal_line_height, Some(expected));
assert_eq!(saved.font_size_px, Some(4.0));
} else {
assert_eq!(saved.font_size_px, Some(4.0));
assert_eq!(saved.ui_font_size_px, Some(expected));
}
}
cx.update(|window, cx| {
let _ = window.draw(cx);
});
let edit = cx.debug_bounds("terminal-line-height-edit").unwrap();
cx.simulate_click(edit.center(), gpui::Modifiers::default());
cx.run_until_parked();
cx.update(|window, cx| {
let _ = window.draw(cx);
});
cx.simulate_input("2");
cx.update(|window, cx| {
let _ = window.draw(cx);
});
let bounds = cx.debug_bounds("terminal-line-height-reset").unwrap();
cx.simulate_click(bounds.center(), gpui::Modifiers::default());
cx.run_until_parked();
assert!(RuntimeSettings::load().terminal_line_height.is_none());
cx.simulate_keystrokes("enter");
cx.update(|window, cx| {
pane.read(cx).settings_search_input.read(cx).focus_handle(cx).focus(window, cx);
let _ = window.draw(cx);
});
cx.run_until_parked();
assert!(RuntimeSettings::load().terminal_line_height.is_none());
pane.read_with(cx, |pane, _| {
assert!(pane.font_size_editing.is_none());
assert!(!pane.line_height_editing);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ impl SettingsPane {
cx,
))
.child(self.font_size_row(false, cx))
.child(self.terminal_line_height_row(cx))
.child(self.switch_row(
"ctrl_wheel_font_zoom",
language.text(crate::i18n::Message::SettingsFontCtrlWheelZoom),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,7 @@ impl SettingsPane {
font_family_cjk_input,
font_size_input,
font_size_editing: None,
line_height_editing: false,
font_picker_trigger_bounds: None,
font_picker_cjk_bounds: None,
backup_selection: backup_remote.selection,
Expand Down
3 changes: 3 additions & 0 deletions nebula_app/src/gpui_shell/terminal/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ pub struct TerminalView {
/// Optional theme line-height multiplier. `None` preserves the shaped
/// font metrics; a value is applied to the logical font size.
line_height_multiplier: Option<f32>,
terminal_line_height: Option<f32>,
pub palette: Arc<Palette>,
/// 把**应用写死的**颜色按当前主题矫正(最低对比度 + 旧主题表面重映射)。
///
Expand Down Expand Up @@ -488,6 +489,7 @@ impl TerminalView {
typography::line_height_for_view(
self.font_size,
self.line_height_multiplier,
self.terminal_line_height,
natural_height,
self.font_offset_y,
scale,
Expand Down Expand Up @@ -976,6 +978,7 @@ impl TerminalView {
self.font_offset_x = settings.font_offset_x;
self.font_offset_y = settings.font_offset_y;
self.line_height_multiplier = settings.theme_line_height;
self.terminal_line_height = settings.terminal_line_height;
// 底色换了就把矫正缓存作废,并记下「旧底色 → 新底色」这一跳:应用当初
// 按旧主题底色画的连续表面(面板、状态栏)要跟着搬过去,否则浅色主题上
// 会留一整块旧的深色板。旧壳 `apply_nebula_theme` 同一时机做同一件事。
Expand Down
33 changes: 33 additions & 0 deletions nebula_app/src/gpui_shell/terminal/view/activity_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,36 @@ fn claude_shift_enter_reaches_pty_without_committing_shell_history(cx: &mut Test
.collect();
assert_eq!(bytes, b"\n");
}

#[gpui::test]
fn line_height_hot_apply_updates_grid_and_pointer_geometry(cx: &mut TestAppContext) {
let (view, window, receiver) = open(cx);
view.update(window, |view, cx| {
view.grid_synced = true;
let content = gpui::size(px(800.0), px(400.0));
for multiplier in [1.0, 2.0, 0.5] {
cx.global_mut::<Settings>().terminal_line_height = Some(multiplier);
view.apply_settings(cx);
let height = view.line_height_for_metrics(20.0, 1.0);
view.set_layout(gpui::point(px(0.0), px(0.0)), px(10.0), height, content, 1.0, cx);
assert_eq!(view.rows, (400.0 / height.as_f32()).floor() as usize);
assert_eq!(view.line_height, height);
let size = receiver
.try_iter()
.find_map(|message| match message {
Msg::ResizeGrid(size) => Some(size),
_ => None,
})
.expect("changed geometry must reach the terminal event loop");
assert_eq!(size.num_lines as usize, view.rows);
assert_eq!(size.num_cols as usize, view.cols);
// This fixture has no I/O pump; consume its queued resize as the event loop does.
view.session.as_ref().unwrap().term.lock().resize(size);
let (point, _) = view.grid_point(gpui::point(px(15.0), height * 2.5));
assert_eq!(point.line, nebula_terminal::index::Line(2));
assert_eq!(point.column, nebula_terminal::index::Column(1));
let term = view.session.as_ref().unwrap().term.lock();
assert_eq!(term.screen_lines(), view.rows);
}
});
}
6 changes: 5 additions & 1 deletion nebula_app/src/gpui_shell/terminal/view/pointer/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ impl Render for Probe {
fn open(cx: &mut TestAppContext) -> (Entity<Probe>, VisualTestContext) {
cx.update(|cx| {
gpui_component::init(cx);
let mut settings = Settings::load(nebula_settings::ThemeName::Nord);
// Settings UI fixtures write real preferences concurrently under nextest.
let runtime = nebula_settings::RuntimeSettings::from_raw(
&nebula_settings::RawSettings::from_text("font_size=15\nfollow_system_theme=0\n"),
);
let mut settings = Settings::load_with_runtime(nebula_settings::ThemeName::Nord, runtime);
settings.focus_follows_mouse = false;
cx.set_global(settings);
});
Expand Down
4 changes: 4 additions & 0 deletions nebula_app/src/gpui_shell/terminal/view/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ impl TerminalView {
font_offset_x,
font_offset_y,
line_height_multiplier,
terminal_line_height,
palette,
term_config,
copy_on_select,
Expand All @@ -57,6 +58,7 @@ impl TerminalView {
settings.font_offset_x,
settings.font_offset_y,
settings.theme_line_height,
settings.terminal_line_height,
Arc::new(settings.palette.clone()),
settings.term_config(),
settings.copy_on_select,
Expand All @@ -76,6 +78,7 @@ impl TerminalView {
0.0,
0.0,
None,
None,
Arc::new(Palette::default()),
nebula_terminal::term::Config::default(),
// 旧壳的出厂默认即开。
Expand Down Expand Up @@ -289,6 +292,7 @@ impl TerminalView {
font_offset_x,
font_offset_y,
line_height_multiplier,
terminal_line_height,
palette,
color_resolver: Default::default(),
marked_text: None,
Expand Down
Loading
Loading