Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -1333,6 +1333,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 @@ -1333,6 +1333,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 @@ -281,6 +282,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 @@ -224,6 +224,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 @@ -143,6 +143,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 @@ -707,6 +707,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 @@ -290,6 +290,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 @@ -480,6 +481,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 @@ -944,6 +946,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 @@ -285,6 +288,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