feat(gpui): full settings page - legacy section nav, every key-value setting migrated
Settings page now mirrors the legacy shell's layout: left section nav (Appearance / Profiles / Providers / SSH / Network / Interaction / Keymap / Advanced / Backup, same names and order as NebulaSettingsSection) with single-section content on the right. nebula-settings grows the full key-value surface ported verbatim from nebula_settings_load - language, shell, startup_directory, ghost/accept/completion_style, cjk_bold_regular, tab_reveal, density, new_tab_position, cell_width_mode, fetch, keep_session, restore_session, resume_ai, tray, blur, opacity, background (hex), panel_resize, ssh proxy trio, quick_terminal_hotkey - with factory defaults asserted item-by-item in tests (9 passing). Fixes a real unit bug: the font_size key stores LOGICAL PIXELS (legacy persists size/scale), not points - the GPUI shell was multiplying by 4/3 and rendering every terminal a third too large. Editors that depend on unmigrated subsystems (keymap table, SSH hosts, AI providers, backup) show explicit placeholders pointing at the legacy page; both shells keep reading and writing the same nebula_settings.txt.
This commit is contained in:
1 parent
3d738f6bd8
commit
af41bae643
3 files changed
+975
-214
No files matched your search
@@ -63,23 +63,32 @@ impl Settings {
|
||||
let secondary = |desc: &RawFontDesc| -> String {
|
||||
desc.family.clone().unwrap_or_else(|| normal_family.clone())
|
||||
};
|
||||
let font_size_pt =
|
||||
runtime.font_size_pt.or(raw.font.size).unwrap_or(11.25).clamp(4.0, 96.0);
|
||||
// 字号语义(对齐旧壳写盘):settings.txt 的 font_size 是**逻辑像素**
|
||||
// (设置 spinner/Ctrl+滚轮持久化时已除 scale);toml 的 font.size
|
||||
// 才是 pt(1pt = 4/3 px @96dpi)。
|
||||
let font_size_px = runtime
|
||||
.font_size_px
|
||||
.unwrap_or_else(|| raw.font.size.unwrap_or(11.25).clamp(4.0, 96.0) * 4.0 / 3.0);
|
||||
|
||||
// 配色:toml 覆盖内置默认,主题最后裁定(与旧壳同序)。
|
||||
// 配色:toml 覆盖内置默认,主题裁定背景/浅色替换/Powerline 槽位,
|
||||
// 用户的背景覆盖色(设置页取色器)最后压轴——与旧壳同序。
|
||||
let mut palette = build_palette(&raw.colors);
|
||||
apply_theme(&mut palette, runtime.theme);
|
||||
if let Some(background) = runtime.background {
|
||||
palette.background = rgba8(background);
|
||||
}
|
||||
|
||||
Settings {
|
||||
font_bold_family: secondary(&raw.font.bold),
|
||||
font_italic_family: secondary(&raw.font.italic),
|
||||
font_bold_italic_family: secondary(&raw.font.bold_italic),
|
||||
font_size_px: font_size_pt * 4.0 / 3.0,
|
||||
font_size_px,
|
||||
palette,
|
||||
cursor_shape: runtime.cursor_shape.map(|shape| match shape {
|
||||
CursorShapeName::Block => CursorShape::Block,
|
||||
CursorShapeName::Beam => CursorShape::Beam,
|
||||
CursorShapeName::Underline => CursorShape::Underline,
|
||||
CursorShapeName::Hollow => CursorShape::HollowBlock,
|
||||
}),
|
||||
cursor_blink: runtime.cursor_blink,
|
||||
copy_on_select: runtime.copy_on_select,
|
||||
|
||||
@@ -1,150 +1,265 @@
|
||||
//! 设置页(GPUI 组件版)。
|
||||
//! 设置页(GPUI 组件版,覆盖旧壳设置的全部纯键值项)。
|
||||
//!
|
||||
//! 薄壳纪律:本文件不定义任何设置语义——键名、值域、主题色表、持久化
|
||||
//! 格式全部在共享 crate `nebula-settings`(从旧壳逐字迁移而来)。这里只
|
||||
//! 负责用组件库把字段摆出来;改动经 `persist_keys` 原地写回
|
||||
//! `nebula_settings.txt`,与旧壳读写同一份文件、同一套语义。
|
||||
//! 版面对齐旧壳:**左侧分区导航 + 右侧单分区内容**(分区名与顺序对照
|
||||
//! `NebulaSettingsSection`:外观/配置文件/供应商/SSH/网络/交互/按键映射/
|
||||
//! 高级/备份),默认落在外观。
|
||||
//!
|
||||
//! 生效时机(对齐旧壳):主题/配色、copy_on_select、字号即时生效(宿主
|
||||
//! 收 `Changed` 事件后热应用);字体族与默认光标形状对新标签页生效。
|
||||
//! 薄壳纪律:本文件不定义任何设置语义——键名、值域、出厂默认、主题色表、
|
||||
//! 持久化格式全部在共享 crate `nebula-settings`(从旧壳逐字迁移并有单测
|
||||
//! 锁定)。这里只负责用组件库把字段摆出来;改动经 `persist_keys` 原地
|
||||
//! 写回 `nebula_settings.txt`,与旧壳读写同一份文件、同一套语义。
|
||||
//!
|
||||
//! 生效时机(对齐旧壳):主题/配色/背景色/字号/copy_on_select 即时热应用;
|
||||
//! 字体族与默认光标形状对新标签页生效;窗口透明/模糊/托盘/快速终端热键
|
||||
//! 等依赖尚未迁移的窗口子系统,写盘后由旧壳消费(本页如实标注)。
|
||||
//!
|
||||
//! 未迁移的编辑器(依赖各自功能子系统):按键映射、SSH 主机管理、
|
||||
//! AI Provider、备份/同步。对应分区放明确的占位说明。
|
||||
|
||||
use gpui::{
|
||||
App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString, Styled as _,
|
||||
Subscription, Window, div, px,
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement as _, Styled as _, Subscription, Window, div, px,
|
||||
};
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui_component::select::SelectEvent;
|
||||
use nebula_settings::{CursorShapeName, RuntimeSettings, ThemeName, persist_keys};
|
||||
use nebula_settings::{RuntimeSettings, format_hex_rgb, persist_keys};
|
||||
|
||||
use crate::gpui_shell::prelude::*;
|
||||
|
||||
/// 主题下拉的选项顺序(展示名 = 持久化名,与旧壳一致)。
|
||||
const THEMES: [ThemeName; 7] = [
|
||||
ThemeName::Nebula,
|
||||
ThemeName::SilverLight,
|
||||
ThemeName::SteelDark,
|
||||
ThemeName::LimestoneLight,
|
||||
ThemeName::CoalDark,
|
||||
ThemeName::LinenLight,
|
||||
ThemeName::MossDark,
|
||||
/// 主题下拉(展示名 = 持久化名,与旧壳一致)。
|
||||
const THEME_VALUES: [&str; 7] = [
|
||||
"Nebula",
|
||||
"SilverLight",
|
||||
"SteelDark",
|
||||
"LimestoneLight",
|
||||
"CoalDark",
|
||||
"LinenLight",
|
||||
"MossDark",
|
||||
];
|
||||
|
||||
const CURSOR_SHAPES: [(&str, &str); 3] =
|
||||
[("block", "块状"), ("beam", "竖线"), ("underline", "下划线")];
|
||||
/// 左侧分区导航(名称与顺序对照旧壳 `NebulaSettingsSection`)。
|
||||
const SECTIONS: [&str; 9] =
|
||||
["外观", "配置文件", "供应商", "SSH", "网络", "交互", "按键映射", "高级", "备份"];
|
||||
|
||||
/// 宿主(workspace)监听:设置已写盘,全局 `Settings` 已重载。
|
||||
pub enum SettingsPaneEvent {
|
||||
Changed,
|
||||
}
|
||||
|
||||
type SharedSelect = Entity<SelectState<Vec<SharedString>>>;
|
||||
|
||||
pub struct SettingsPane {
|
||||
focus_handle: FocusHandle,
|
||||
theme_select: Entity<SelectState<Vec<SharedString>>>,
|
||||
cursor_select: Entity<SelectState<Vec<SharedString>>>,
|
||||
cursor_blink: bool,
|
||||
copy_on_select: bool,
|
||||
powerline: bool,
|
||||
font_family: SharedString,
|
||||
font_size_pt: f32,
|
||||
/// 渲染与写盘的单一事实源;每次 persist 后整体重载。
|
||||
runtime: RuntimeSettings,
|
||||
/// 当前分区(`SECTIONS` 下标);旧壳默认落在外观。
|
||||
active_section: usize,
|
||||
selects: Vec<(&'static str, SharedSelect)>,
|
||||
dir_input: Entity<InputState>,
|
||||
bg_input: Entity<InputState>,
|
||||
proxy_url_input: Entity<InputState>,
|
||||
proxy_bypass_input: Entity<InputState>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl SettingsPane {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let runtime = RuntimeSettings::load();
|
||||
let mut selects: Vec<(&'static str, SharedSelect)> = Vec::new();
|
||||
let mut subscriptions = Vec::new();
|
||||
|
||||
let theme_ix = THEMES.iter().position(|t| *t == runtime.theme).unwrap_or(0);
|
||||
let theme_select = cx.new(|cx| {
|
||||
SelectState::new(
|
||||
THEMES.iter().map(|t| SharedString::from(t.prompt_name())).collect::<Vec<_>>(),
|
||||
Some(IndexPath::default().row(theme_ix)),
|
||||
let mut add_select = |key: &'static str,
|
||||
labels: &'static [&'static str],
|
||||
values: &'static [&'static str],
|
||||
current: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>| {
|
||||
let ix = values.iter().position(|v| *v == current).unwrap_or(0);
|
||||
let select = cx.new(|cx| {
|
||||
SelectState::new(
|
||||
labels.iter().map(|l| SharedString::from(*l)).collect::<Vec<_>>(),
|
||||
Some(IndexPath::default().row(ix)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&select,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let cursor_ix = match runtime.cursor_shape {
|
||||
Some(CursorShapeName::Block) => 0,
|
||||
// 旧壳出厂默认 beam;未设置时展示 beam。
|
||||
Some(CursorShapeName::Beam) | None => 1,
|
||||
Some(CursorShapeName::Underline) => 2,
|
||||
move |this: &mut Self,
|
||||
entity: &SharedSelect,
|
||||
event: &SelectEvent<Vec<SharedString>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>| {
|
||||
if let SelectEvent::Confirm(Some(_)) = event {
|
||||
let row = entity.read(cx).selected_index(cx).map(|path| path.row);
|
||||
if let Some(value) = row.and_then(|row| values.get(row)) {
|
||||
this.persist(&[(key, (*value).to_string())], cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
selects.push((key, select));
|
||||
};
|
||||
let cursor_select = cx.new(|cx| {
|
||||
SelectState::new(
|
||||
CURSOR_SHAPES
|
||||
.iter()
|
||||
.map(|(_, label)| SharedString::from(*label))
|
||||
.collect::<Vec<_>>(),
|
||||
Some(IndexPath::default().row(cursor_ix)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(&theme_select, window, Self::on_theme_confirm),
|
||||
cx.subscribe_in(&cursor_select, window, Self::on_cursor_confirm),
|
||||
];
|
||||
let cursor_current =
|
||||
runtime.cursor_shape.map(|shape| shape.settings_value()).unwrap_or("beam");
|
||||
let shell_current = runtime.shell.clone().unwrap_or_else(|| "powershell".into());
|
||||
|
||||
add_select(
|
||||
"language",
|
||||
&["跟随系统", "简体中文", "English"],
|
||||
&["system", "zh-CN", "en-US"],
|
||||
runtime.language.settings_value(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
add_select(
|
||||
"shell",
|
||||
&["PowerShell", "Bash (Git Bash)"],
|
||||
&["powershell", "bash"],
|
||||
&shell_current,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
add_select("theme", &THEME_VALUES, &THEME_VALUES, runtime.theme.prompt_name(), window, cx);
|
||||
add_select(
|
||||
"cursor_shape",
|
||||
&["块状", "竖线", "下划线", "空心块"],
|
||||
&["block", "beam", "underline", "hollow"],
|
||||
cursor_current,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
add_select(
|
||||
"tab_reveal",
|
||||
&["滑动", "立即"],
|
||||
&["slide", "instant"],
|
||||
runtime.tab_reveal.settings_value(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
add_select(
|
||||
"density",
|
||||
&["标准", "紧凑"],
|
||||
&["standard", "compact"],
|
||||
runtime.density.settings_value(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
add_select(
|
||||
"new_tab_position",
|
||||
&["当前标签之后", "列表末尾"],
|
||||
&["after_current", "end"],
|
||||
runtime.new_tab_position.settings_value(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
add_select(
|
||||
"cell_width_mode",
|
||||
&["紧凑", "宽松"],
|
||||
&["compact", "relaxed"],
|
||||
runtime.cell_width_mode.settings_value(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
add_select(
|
||||
"accept",
|
||||
&["→ 方向键", "Tab", "两者皆可"],
|
||||
&["right", "tab", "both"],
|
||||
runtime.accept.settings_value(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
add_select(
|
||||
"completion_style",
|
||||
&["内联 ghost", "弹出列表"],
|
||||
&["inline", "popup"],
|
||||
runtime.completion_style.settings_value(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
add_select(
|
||||
"ssh_proxy_mode",
|
||||
&["关闭", "系统代理", "自定义"],
|
||||
&["off", "system", "custom"],
|
||||
runtime.ssh_proxy_mode.settings_value(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let dir_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("留空 = 继承当前窗格目录")
|
||||
.default_value(runtime.startup_directory.clone().unwrap_or_default())
|
||||
});
|
||||
let bg_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("#rrggbb,留空跟随主题")
|
||||
.default_value(runtime.background.map(format_hex_rgb).unwrap_or_default())
|
||||
});
|
||||
let proxy_url_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("socks5://127.0.0.1:7890")
|
||||
.default_value(runtime.ssh_proxy_url.clone())
|
||||
});
|
||||
let proxy_bypass_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("逗号分隔,如 10.0.0.0/8,*.internal")
|
||||
.default_value(runtime.ssh_proxy_no_proxy.clone())
|
||||
});
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
theme_select,
|
||||
cursor_select,
|
||||
cursor_blink: runtime.cursor_blink.unwrap_or(true),
|
||||
copy_on_select: runtime.copy_on_select,
|
||||
powerline: runtime.powerline,
|
||||
font_family: runtime.font_family.unwrap_or_default().into(),
|
||||
font_size_pt: runtime.font_size_pt.unwrap_or(11.25),
|
||||
runtime,
|
||||
active_section: 0,
|
||||
selects,
|
||||
dir_input,
|
||||
bg_input,
|
||||
proxy_url_input,
|
||||
proxy_bypass_input,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_theme_confirm(
|
||||
&mut self,
|
||||
_: &Entity<SelectState<Vec<SharedString>>>,
|
||||
event: &SelectEvent<Vec<SharedString>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let SelectEvent::Confirm(Some(value)) = event {
|
||||
if let Some(theme) = ThemeName::from_prompt_name(value.as_ref()) {
|
||||
self.persist(&[("theme", theme.prompt_name().to_owned())], cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_cursor_confirm(
|
||||
&mut self,
|
||||
_: &Entity<SelectState<Vec<SharedString>>>,
|
||||
event: &SelectEvent<Vec<SharedString>>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let SelectEvent::Confirm(Some(value)) = event {
|
||||
if let Some((key, _)) = CURSOR_SHAPES.iter().find(|(_, label)| *label == value.as_ref())
|
||||
{
|
||||
self.persist(&[("cursor_shape", (*key).to_owned())], cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 写盘 → 重载全局 `Settings` → 通知宿主热应用。
|
||||
/// 写盘 → 重载单一事实源与全局 `Settings` → 通知宿主热应用。
|
||||
fn persist(&mut self, updates: &[(&str, String)], cx: &mut Context<Self>) {
|
||||
if let Err(err) = persist_keys(updates) {
|
||||
eprintln!("[nebula:gpui] failed to persist settings: {err}");
|
||||
}
|
||||
self.runtime = RuntimeSettings::load();
|
||||
let settings = crate::gpui_shell::config::Settings::load();
|
||||
cx.set_global(settings);
|
||||
cx.emit(SettingsPaneEvent::Changed);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_font_size(&mut self, size: f32, cx: &mut Context<Self>) {
|
||||
self.font_size_pt = size.clamp(4.0, 96.0);
|
||||
self.persist(&[("font_size", format!("{:.2}", self.font_size_pt))], cx);
|
||||
fn toggle(&mut self, key: &'static str, value: bool, cx: &mut Context<Self>) {
|
||||
self.persist(&[(key, (value as u8).to_string())], cx);
|
||||
}
|
||||
|
||||
/// 分组卡片(对齐设置页的分组式版面)。
|
||||
fn font_size_px(&self, cx: &App) -> f32 {
|
||||
self.runtime
|
||||
.font_size_px
|
||||
.unwrap_or_else(|| cx.global::<crate::gpui_shell::config::Settings>().font_size_px)
|
||||
}
|
||||
|
||||
fn set_font_size(&mut self, size: f32, cx: &mut Context<Self>) {
|
||||
let size = size.clamp(4.0, 96.0);
|
||||
self.persist(&[("font_size", format!("{size:.2}"))], cx);
|
||||
}
|
||||
|
||||
fn set_opacity(&mut self, opacity: f32, cx: &mut Context<Self>) {
|
||||
let opacity = opacity.clamp(0.2, 1.0);
|
||||
self.persist(&[("opacity", format!("{opacity:.2}"))], cx);
|
||||
}
|
||||
|
||||
fn select_of(&self, key: &str) -> Option<SharedSelect> {
|
||||
self.selects.iter().find(|(k, _)| *k == key).map(|(_, entity)| entity.clone())
|
||||
}
|
||||
|
||||
/// 分组卡片(右侧内容区的分组式版面,对齐旧壳)。
|
||||
fn group(&self, title: &'static str, cx: &Context<Self>) -> gpui::Div {
|
||||
v_flex()
|
||||
.w_full()
|
||||
@@ -165,6 +280,327 @@ impl SettingsPane {
|
||||
.child(div().flex_1().text_sm().child(label))
|
||||
.child(control)
|
||||
}
|
||||
|
||||
fn select_row(&self, key: &'static str, label: &'static str) -> impl IntoElement {
|
||||
let select = self.select_of(key);
|
||||
Self::row(label, div().w(px(220.0)).children(select.map(|state| Select::new(&state))))
|
||||
}
|
||||
|
||||
fn checkbox_row(
|
||||
&self,
|
||||
key: &'static str,
|
||||
label: &'static str,
|
||||
checked: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
Self::row(
|
||||
label,
|
||||
Checkbox::new(key).checked(checked).on_click(cx.listener(
|
||||
move |this, checked: &bool, _, cx| {
|
||||
this.toggle(key, *checked, cx);
|
||||
},
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
/// 文本输入 + 保存按钮(保存时读值写盘)。
|
||||
fn input_row(
|
||||
&self,
|
||||
label: &'static str,
|
||||
key: &'static str,
|
||||
input: &Entity<InputState>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let state = input.clone();
|
||||
Self::row(
|
||||
label,
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.child(div().w(px(280.0)).child(Input::new(input)))
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("save-{key}")))
|
||||
.label("保存")
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
let value = state.read(cx).value().to_string();
|
||||
this.persist(&[(key, value)], cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn stepper_row(
|
||||
&self,
|
||||
label: &'static str,
|
||||
id: &'static str,
|
||||
display: SharedString,
|
||||
on_minus: impl Fn(&mut Self, &mut Context<Self>) + 'static,
|
||||
on_plus: impl Fn(&mut Self, &mut Context<Self>) + 'static,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
Self::row(
|
||||
label,
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("minus-{id}")))
|
||||
.label("−")
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
on_minus(this, cx);
|
||||
})),
|
||||
)
|
||||
.child(div().text_sm().min_w(px(64.0)).child(display))
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("plus-{id}")))
|
||||
.label("+")
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
on_plus(this, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// 未迁移分区的占位说明。
|
||||
fn pending_group(
|
||||
&self,
|
||||
title: &'static str,
|
||||
note: &'static str,
|
||||
cx: &Context<Self>,
|
||||
) -> gpui::Div {
|
||||
self.group(title, cx)
|
||||
.child(div().text_sm().text_color(cx.theme().muted_foreground).child(note))
|
||||
}
|
||||
|
||||
// ---- 分区内容(归属对照旧壳各 section)----
|
||||
|
||||
fn section_appearance(&mut self, cx: &mut Context<Self>) -> gpui::Div {
|
||||
let font_size: SharedString = format!("{:.1} px", self.font_size_px(cx)).into();
|
||||
let opacity: SharedString = format!("{:.0}%", self.runtime.opacity * 100.0).into();
|
||||
self.group("外观", cx)
|
||||
.child(self.select_row("theme", "主题"))
|
||||
.child(self.checkbox_row(
|
||||
"follow_system_theme",
|
||||
"跟随系统外观自动切换深浅",
|
||||
self.runtime.follow_system_theme,
|
||||
cx,
|
||||
))
|
||||
.child(self.input_row("终端背景色(覆盖主题)", "background", &self.bg_input.clone(), cx))
|
||||
.child(self.stepper_row(
|
||||
"窗口透明度(旧壳窗口生效)",
|
||||
"opacity",
|
||||
opacity,
|
||||
|this, cx| {
|
||||
let opacity = this.runtime.opacity - 0.05;
|
||||
this.set_opacity(opacity, cx);
|
||||
},
|
||||
|this, cx| {
|
||||
let opacity = this.runtime.opacity + 0.05;
|
||||
this.set_opacity(opacity, cx);
|
||||
},
|
||||
cx,
|
||||
))
|
||||
.child(self.checkbox_row("blur", "背景模糊(旧壳窗口生效)", self.runtime.blur, cx))
|
||||
.child(self.select_row("density", "界面密度"))
|
||||
.child(self.stepper_row(
|
||||
"字号",
|
||||
"font-size",
|
||||
font_size,
|
||||
|this, cx| {
|
||||
let size = this.font_size_px(cx) - 0.5;
|
||||
this.set_font_size(size, cx);
|
||||
},
|
||||
|this, cx| {
|
||||
let size = this.font_size_px(cx) + 0.5;
|
||||
this.set_font_size(size, cx);
|
||||
},
|
||||
cx,
|
||||
))
|
||||
.child(self.select_row("cursor_shape", "光标形状(新标签页生效)"))
|
||||
.child(self.checkbox_row(
|
||||
"cursor_blink",
|
||||
"光标闪烁",
|
||||
self.runtime.cursor_blink.unwrap_or(true),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn section_profiles(&mut self, cx: &mut Context<Self>) -> gpui::Div {
|
||||
let font_label: SharedString = self
|
||||
.runtime
|
||||
.font_family
|
||||
.clone()
|
||||
.unwrap_or_else(|| "(跟随 nebula.toml / 内置默认)".into())
|
||||
.into();
|
||||
self.group("配置文件", cx)
|
||||
.child(self.select_row("language", "界面语言"))
|
||||
.child(self.select_row("shell", "默认 Shell"))
|
||||
.child(self.input_row("新标签启动目录", "startup_directory", &self.dir_input.clone(), cx))
|
||||
.child(Self::row(
|
||||
"字体(选择器待迁移)",
|
||||
div().text_sm().text_color(cx.theme().muted_foreground).child(font_label),
|
||||
))
|
||||
.child(self.checkbox_row("ghost", "启用命令补全", self.runtime.ghost, cx))
|
||||
.child(self.select_row("accept", "补全接受键"))
|
||||
.child(self.select_row("completion_style", "补全样式"))
|
||||
}
|
||||
|
||||
fn section_network(&mut self, cx: &mut Context<Self>) -> gpui::Div {
|
||||
self.group("网络(SSH 出站代理)", cx)
|
||||
.child(self.select_row("ssh_proxy_mode", "代理模式"))
|
||||
.child(self.input_row("代理 URL", "ssh_proxy_url", &self.proxy_url_input.clone(), cx))
|
||||
.child(self.input_row(
|
||||
"绕过列表",
|
||||
"ssh_proxy_no_proxy",
|
||||
&self.proxy_bypass_input.clone(),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn section_interaction(&mut self, cx: &mut Context<Self>) -> gpui::Div {
|
||||
self.group("交互", cx)
|
||||
.child(self.checkbox_row(
|
||||
"copy_on_select",
|
||||
"选中即复制(关 = 右键复制/粘贴)",
|
||||
self.runtime.copy_on_select,
|
||||
cx,
|
||||
))
|
||||
.child(self.select_row("tab_reveal", "标签展开动效"))
|
||||
.child(self.select_row("new_tab_position", "新标签位置"))
|
||||
.child(self.select_row("cell_width_mode", "单元格宽度"))
|
||||
.child(self.checkbox_row(
|
||||
"panel_resize",
|
||||
"拖拽调节侧栏宽度",
|
||||
self.runtime.panel_resize,
|
||||
cx,
|
||||
))
|
||||
.child(self.checkbox_row(
|
||||
"cjk_bold_regular",
|
||||
"CJK 粗体使用常规字形(提亮不加粗)",
|
||||
self.runtime.cjk_bold_regular,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn section_keymap(&mut self, cx: &mut Context<Self>) -> gpui::Div {
|
||||
let hotkey: SharedString = self.runtime.quick_terminal_hotkey.clone().into();
|
||||
self.group("按键映射", cx)
|
||||
.child(Self::row(
|
||||
"快速终端热键",
|
||||
div().text_sm().text_color(cx.theme().muted_foreground).child(hotkey),
|
||||
))
|
||||
.child(div().text_sm().text_color(cx.theme().muted_foreground).child(
|
||||
"快捷键编辑器待迁移:绑定表当前请在旧壳设置页修改,\
|
||||
存储于 nebula_settings.txt 的 keybind= 行,两壳同读。",
|
||||
))
|
||||
}
|
||||
|
||||
fn section_advanced(&mut self, cx: &mut Context<Self>) -> gpui::Div {
|
||||
self.group("高级", cx)
|
||||
.child(self.checkbox_row("fetch", "新会话欢迎屏 fastfetch", self.runtime.fetch, cx))
|
||||
.child(self.checkbox_row(
|
||||
"powerline",
|
||||
"Powerline 提示符(新会话生效)",
|
||||
self.runtime.powerline,
|
||||
cx,
|
||||
))
|
||||
.child(self.checkbox_row(
|
||||
"keep_session",
|
||||
"关窗后保留后台会话",
|
||||
self.runtime.keep_session,
|
||||
cx,
|
||||
))
|
||||
.child(self.checkbox_row(
|
||||
"restore_session",
|
||||
"启动时恢复上次标签",
|
||||
self.runtime.restore_session,
|
||||
cx,
|
||||
))
|
||||
.child(self.checkbox_row(
|
||||
"resume_ai",
|
||||
"恢复会话时自动接续 AI 对话",
|
||||
self.runtime.resume_ai,
|
||||
cx,
|
||||
))
|
||||
.child(self.checkbox_row(
|
||||
"tray",
|
||||
"常驻系统托盘图标(旧壳生效)",
|
||||
self.runtime.tray,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn section_content(&mut self, cx: &mut Context<Self>) -> gpui::AnyElement {
|
||||
use gpui::IntoElement as _;
|
||||
match self.active_section {
|
||||
0 => self.section_appearance(cx),
|
||||
1 => self.section_profiles(cx),
|
||||
2 => self.pending_group(
|
||||
"供应商",
|
||||
"AI Provider 编辑器待迁移(API 地址/密钥托管在系统凭据库)。\
|
||||
当前请在旧壳设置页修改。",
|
||||
cx,
|
||||
),
|
||||
3 => self.pending_group(
|
||||
"SSH",
|
||||
"SSH 主机管理(保存/置顶/隐藏恢复)待迁移。当前请在旧壳侧栏\
|
||||
与设置页管理,数据两壳同读。",
|
||||
cx,
|
||||
),
|
||||
4 => self.section_network(cx),
|
||||
5 => self.section_interaction(cx),
|
||||
6 => self.section_keymap(cx),
|
||||
7 => self.section_advanced(cx),
|
||||
_ => self.pending_group(
|
||||
"备份",
|
||||
"加密备份/远程同步(文件夹/WebDAV/S3/SFTP)编辑器待迁移。\
|
||||
当前请在旧壳设置页操作。",
|
||||
cx,
|
||||
),
|
||||
}
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_nav(&self, cx: &mut Context<Self>) -> gpui::AnyElement {
|
||||
use gpui::IntoElement as _;
|
||||
let theme = cx.theme();
|
||||
let muted = theme.muted_foreground;
|
||||
let active_bg = theme.sidebar_accent;
|
||||
let active_fg = theme.sidebar_accent_foreground;
|
||||
let hover_bg = theme.list_hover;
|
||||
|
||||
v_flex()
|
||||
.w(px(150.0))
|
||||
.h_full()
|
||||
.flex_shrink_0()
|
||||
.p_2()
|
||||
.gap_1()
|
||||
.border_r_1()
|
||||
.border_color(theme.border)
|
||||
.children(SECTIONS.iter().enumerate().map(|(ix, label)| {
|
||||
let active = ix == self.active_section;
|
||||
div()
|
||||
.id(("settings-nav", ix))
|
||||
.px_3()
|
||||
.h(px(30.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
.when(active, |item| item.bg(active_bg).text_color(active_fg))
|
||||
.when(!active, |item| item.text_color(muted).hover(|s| s.bg(hover_bg)))
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.active_section = ix;
|
||||
cx.notify();
|
||||
}))
|
||||
.child(*label)
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<SettingsPaneEvent> for SettingsPane {}
|
||||
@@ -177,113 +613,38 @@ impl Focusable for SettingsPane {
|
||||
|
||||
impl Render for SettingsPane {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let font_label: SharedString = if self.font_family.is_empty() {
|
||||
"(跟随 nebula.toml / 内置默认)".into()
|
||||
} else {
|
||||
self.font_family.clone()
|
||||
};
|
||||
let font_size: SharedString = format!("{:.1} pt", self.font_size_pt).into();
|
||||
let nav = self.render_nav(cx);
|
||||
let content = self.section_content(cx);
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.track_focus(&self.focus_handle)
|
||||
.bg(cx.theme().background)
|
||||
.text_color(cx.theme().foreground)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(nav)
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.p_8()
|
||||
.gap_5()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.h_full()
|
||||
.p_6()
|
||||
.gap_4()
|
||||
.overflow_y_scrollbar()
|
||||
.child(
|
||||
div().text_xl().font_weight(gpui::FontWeight::SEMIBOLD).child("设置"),
|
||||
)
|
||||
.child(
|
||||
self.group("外观", cx)
|
||||
.child(Self::row(
|
||||
"主题",
|
||||
div().w(px(220.0)).child(Select::new(&self.theme_select)),
|
||||
))
|
||||
.child(Self::row(
|
||||
"字号",
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.child(
|
||||
Button::new("font-size-down")
|
||||
.label("−")
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.set_font_size(this.font_size_pt - 0.5, cx);
|
||||
})),
|
||||
)
|
||||
.child(div().text_sm().min_w(px(64.0)).child(font_size))
|
||||
.child(
|
||||
Button::new("font-size-up")
|
||||
.label("+")
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.set_font_size(this.font_size_pt + 0.5, cx);
|
||||
})),
|
||||
),
|
||||
))
|
||||
.child(Self::row(
|
||||
"字体",
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(font_label),
|
||||
))
|
||||
.child(Self::row(
|
||||
"光标形状(新标签页生效)",
|
||||
div().w(px(220.0)).child(Select::new(&self.cursor_select)),
|
||||
))
|
||||
.child(Self::row(
|
||||
"光标闪烁",
|
||||
Checkbox::new("cursor-blink").checked(self.cursor_blink).on_click(
|
||||
cx.listener(|this, checked: &bool, _, cx| {
|
||||
this.cursor_blink = *checked;
|
||||
this.persist(
|
||||
&[("cursor_blink", (*checked as u8).to_string())],
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
self.group("交互", cx)
|
||||
.child(Self::row(
|
||||
"选中即复制(关 = 右键复制/粘贴)",
|
||||
Checkbox::new("copy-on-select")
|
||||
.checked(self.copy_on_select)
|
||||
.on_click(cx.listener(|this, checked: &bool, _, cx| {
|
||||
this.copy_on_select = *checked;
|
||||
this.persist(
|
||||
&[("copy_on_select", (*checked as u8).to_string())],
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
))
|
||||
.child(Self::row(
|
||||
"Powerline 提示符(新会话生效)",
|
||||
Checkbox::new("powerline").checked(self.powerline).on_click(
|
||||
cx.listener(|this, checked: &bool, _, cx| {
|
||||
this.powerline = *checked;
|
||||
this.persist(
|
||||
&[("powerline", (*checked as u8).to_string())],
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("写入 nebula_settings.txt,与旧壳共享同一份设置。"),
|
||||
),
|
||||
.text_xl()
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.child(SharedString::from(format!(
|
||||
"设置 · {}",
|
||||
SECTIONS[self.active_section]
|
||||
))),
|
||||
)
|
||||
.child(content)
|
||||
.child(div().text_xs().text_color(cx.theme().muted_foreground).child(
|
||||
"写入 nebula_settings.txt,与旧壳共享同一份设置;两边可交替修改。",
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
+407
-16
@@ -313,21 +313,285 @@ pub enum CursorShapeName {
|
||||
Block,
|
||||
Beam,
|
||||
Underline,
|
||||
Hollow,
|
||||
}
|
||||
|
||||
/// 新 UI 消费的运行时设置子集。字段随消费方增长;未设置的键保持 `None`
|
||||
/// 让调用方自选回退。
|
||||
impl CursorShapeName {
|
||||
pub fn from_settings(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"block" => Some(Self::Block),
|
||||
"beam" | "bar" => Some(Self::Beam),
|
||||
"underline" => Some(Self::Underline),
|
||||
"hollow" => Some(Self::Hollow),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Block => "block",
|
||||
Self::Beam => "beam",
|
||||
Self::Underline => "underline",
|
||||
Self::Hollow => "hollow",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 下面的小枚举都是 `nebula_settings.txt` 的键值域,取值与旧壳设置页
|
||||
/// 逐字一致(parse 宽容:未知值回退默认)。
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum LanguagePref {
|
||||
#[default]
|
||||
System,
|
||||
ZhCn,
|
||||
EnUs,
|
||||
}
|
||||
|
||||
impl LanguagePref {
|
||||
pub fn from_settings(value: &str) -> Option<Self> {
|
||||
match value.trim() {
|
||||
"system" => Some(Self::System),
|
||||
"zh-CN" => Some(Self::ZhCn),
|
||||
"en-US" => Some(Self::EnUs),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::System => "system",
|
||||
Self::ZhCn => "zh-CN",
|
||||
Self::EnUs => "en-US",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum AcceptKeyName {
|
||||
Right,
|
||||
Tab,
|
||||
#[default]
|
||||
Both,
|
||||
}
|
||||
|
||||
impl AcceptKeyName {
|
||||
pub fn from_settings(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"right" => Some(Self::Right),
|
||||
"tab" => Some(Self::Tab),
|
||||
"both" => Some(Self::Both),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Right => "right",
|
||||
Self::Tab => "tab",
|
||||
Self::Both => "both",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum CompletionStyleName {
|
||||
#[default]
|
||||
Inline,
|
||||
Popup,
|
||||
}
|
||||
|
||||
impl CompletionStyleName {
|
||||
pub fn from_settings(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"inline" | "ghost" => Some(Self::Inline),
|
||||
"popup" | "menu" | "list" => Some(Self::Popup),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Inline => "inline",
|
||||
Self::Popup => "popup",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum TabRevealName {
|
||||
#[default]
|
||||
Slide,
|
||||
Instant,
|
||||
}
|
||||
|
||||
impl TabRevealName {
|
||||
pub fn from_settings(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"slide" => Some(Self::Slide),
|
||||
"instant" => Some(Self::Instant),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Slide => "slide",
|
||||
Self::Instant => "instant",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum DensityName {
|
||||
#[default]
|
||||
Standard,
|
||||
Compact,
|
||||
}
|
||||
|
||||
impl DensityName {
|
||||
pub fn from_settings(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"standard" => Some(Self::Standard),
|
||||
"compact" => Some(Self::Compact),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Standard => "standard",
|
||||
Self::Compact => "compact",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum NewTabPositionName {
|
||||
#[default]
|
||||
AfterCurrent,
|
||||
End,
|
||||
}
|
||||
|
||||
impl NewTabPositionName {
|
||||
pub fn from_settings(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"after_current" => Some(Self::AfterCurrent),
|
||||
"end" => Some(Self::End),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::AfterCurrent => "after_current",
|
||||
Self::End => "end",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum CellWidthModeName {
|
||||
#[default]
|
||||
Compact,
|
||||
Relaxed,
|
||||
}
|
||||
|
||||
impl CellWidthModeName {
|
||||
pub fn from_settings(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"compact" => Some(Self::Compact),
|
||||
"relaxed" => Some(Self::Relaxed),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Compact => "compact",
|
||||
Self::Relaxed => "relaxed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ProxyModeName {
|
||||
#[default]
|
||||
Off,
|
||||
System,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl ProxyModeName {
|
||||
pub fn from_settings(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"off" => Some(Self::Off),
|
||||
"system" => Some(Self::System),
|
||||
"custom" => Some(Self::Custom),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Off => "off",
|
||||
Self::System => "system",
|
||||
Self::Custom => "custom",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 新 UI 消费的运行时设置。字段与出厂默认逐项对照旧壳
|
||||
/// `nebula_settings_load`;`Option` 字段的 `None` = 键未设置,调用方自选
|
||||
/// 回退(如 font_family 回落 nebula.toml)。
|
||||
pub struct RuntimeSettings {
|
||||
pub language: LanguagePref,
|
||||
pub theme: ThemeName,
|
||||
/// 系统外观变化时自动在主题家族的深浅成员间切换(默认关,尊重显式选择)。
|
||||
pub follow_system_theme: bool,
|
||||
pub font_family: Option<String>,
|
||||
/// pt(与设置界面一致)。
|
||||
pub font_size_pt: Option<f32>,
|
||||
/// **逻辑像素**(旧壳写盘语义:设置页 spinner 与 Ctrl+滚轮缩放持久化时
|
||||
/// 已除以 scale factor)。`None` = 跟随 nebula.toml 的 `font.size`(pt)。
|
||||
pub font_size_px: Option<f32>,
|
||||
pub cursor_shape: Option<CursorShapeName>,
|
||||
pub cursor_blink: Option<bool>,
|
||||
pub copy_on_select: bool,
|
||||
pub powerline: bool,
|
||||
/// 默认 shell 的原始 id(`shell=` 原文:powershell/bash/cmd/pwsh/WSL
|
||||
/// 发行版等)。解析归 shell 检测层,这里只做持久化往返。
|
||||
pub shell: Option<String>,
|
||||
pub startup_directory: Option<String>,
|
||||
/// AI 内联补全(ghost text)。
|
||||
pub ghost: bool,
|
||||
pub accept: AcceptKeyName,
|
||||
pub completion_style: CompletionStyleName,
|
||||
/// 全宽字形(CJK 等)bold run 用 Regular 字形(粗体提亮不加粗)。
|
||||
pub cjk_bold_regular: bool,
|
||||
pub tab_reveal: TabRevealName,
|
||||
pub density: DensityName,
|
||||
pub new_tab_position: NewTabPositionName,
|
||||
pub cell_width_mode: CellWidthModeName,
|
||||
/// 新会话欢迎屏 fastfetch(默认关:启动速度优先于观感,旧壳裁定)。
|
||||
pub fetch: bool,
|
||||
pub keep_session: bool,
|
||||
pub restore_session: bool,
|
||||
pub resume_ai: bool,
|
||||
/// 常驻系统托盘图标。
|
||||
pub tray: bool,
|
||||
pub blur: bool,
|
||||
pub opacity: f32,
|
||||
/// 终端背景覆盖色(设置页取色器写入,优先于主题背景)。
|
||||
pub background: Option<Rgb8>,
|
||||
pub panel_resize: bool,
|
||||
pub ssh_proxy_mode: ProxyModeName,
|
||||
pub ssh_proxy_url: String,
|
||||
pub ssh_proxy_no_proxy: String,
|
||||
pub quick_terminal_hotkey: String,
|
||||
}
|
||||
|
||||
/// 旧壳默认快速终端热键。
|
||||
pub const DEFAULT_QUICK_TERMINAL_HOTKEY: &str = "ctrl+`";
|
||||
|
||||
impl RuntimeSettings {
|
||||
pub fn load() -> Self {
|
||||
Self::from_raw(&RawSettings::load())
|
||||
@@ -335,24 +599,81 @@ impl RuntimeSettings {
|
||||
|
||||
pub fn from_raw(raw: &RawSettings) -> Self {
|
||||
Self {
|
||||
language: raw
|
||||
.value("language")
|
||||
.and_then(LanguagePref::from_settings)
|
||||
.unwrap_or_default(),
|
||||
theme: raw.value("theme").and_then(ThemeName::from_prompt_name).unwrap_or_default(),
|
||||
follow_system_theme: raw.bool_on("follow_system_theme").unwrap_or(false),
|
||||
font_family: raw.value("font_family").map(str::to_owned),
|
||||
font_size_pt: raw.f32("font_size").map(|size| size.clamp(4.0, 96.0)),
|
||||
cursor_shape: raw.value("cursor_shape").and_then(|value| {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"block" => Some(CursorShapeName::Block),
|
||||
"beam" | "bar" => Some(CursorShapeName::Beam),
|
||||
"underline" => Some(CursorShapeName::Underline),
|
||||
_ => None,
|
||||
}
|
||||
}),
|
||||
font_size_px: raw.f32("font_size").map(|size| size.clamp(4.0, 96.0)),
|
||||
cursor_shape: raw.value("cursor_shape").and_then(CursorShapeName::from_settings),
|
||||
cursor_blink: raw.bool_on("cursor_blink"),
|
||||
copy_on_select: raw.bool_on("copy_on_select").unwrap_or(false),
|
||||
powerline: raw.bool_on("powerline").unwrap_or(true),
|
||||
shell: raw.value("shell").or_else(|| raw.value("executor")).map(str::to_owned),
|
||||
startup_directory: raw.value("startup_directory").map(str::to_owned),
|
||||
ghost: raw.value("ghost").map(|v| v != "0").unwrap_or(true),
|
||||
accept: raw.value("accept").and_then(AcceptKeyName::from_settings).unwrap_or_default(),
|
||||
completion_style: raw
|
||||
.value("completion_style")
|
||||
.and_then(CompletionStyleName::from_settings)
|
||||
.unwrap_or_default(),
|
||||
cjk_bold_regular: raw.bool_on("cjk_bold_regular").unwrap_or(true),
|
||||
tab_reveal: raw
|
||||
.value("tab_reveal")
|
||||
.and_then(TabRevealName::from_settings)
|
||||
.unwrap_or_default(),
|
||||
density: raw.value("density").and_then(DensityName::from_settings).unwrap_or_default(),
|
||||
new_tab_position: raw
|
||||
.value("new_tab_position")
|
||||
.and_then(NewTabPositionName::from_settings)
|
||||
.unwrap_or_default(),
|
||||
cell_width_mode: raw
|
||||
.value("cell_width_mode")
|
||||
.and_then(CellWidthModeName::from_settings)
|
||||
.unwrap_or_default(),
|
||||
fetch: raw.bool_on("fetch").unwrap_or(false),
|
||||
keep_session: raw.bool_on("keep_session").unwrap_or(false),
|
||||
restore_session: raw.bool_on("restore_session").unwrap_or(true),
|
||||
resume_ai: raw.bool_on("resume_ai").unwrap_or(true),
|
||||
tray: raw.bool_on("tray").unwrap_or(true),
|
||||
blur: raw.bool_on("blur").unwrap_or(true),
|
||||
opacity: raw.f32("opacity").map(|o| o.clamp(0.0, 1.0)).unwrap_or(1.0),
|
||||
background: raw.value("background").and_then(parse_hex_rgb),
|
||||
panel_resize: raw.bool_on("panel_resize").unwrap_or(false),
|
||||
ssh_proxy_mode: raw
|
||||
.value("ssh_proxy_mode")
|
||||
.and_then(ProxyModeName::from_settings)
|
||||
.unwrap_or_default(),
|
||||
ssh_proxy_url: raw.value("ssh_proxy_url").unwrap_or_default().to_owned(),
|
||||
ssh_proxy_no_proxy: raw.value("ssh_proxy_no_proxy").unwrap_or_default().to_owned(),
|
||||
quick_terminal_hotkey: raw
|
||||
.value("quick_terminal_hotkey")
|
||||
.unwrap_or(DEFAULT_QUICK_TERMINAL_HOTKEY)
|
||||
.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 `#rrggbb`(旧壳 `parse_hex_rgb` 同款:# 前缀可省)。
|
||||
pub fn parse_hex_rgb(value: &str) -> Option<Rgb8> {
|
||||
let hex = value.trim();
|
||||
let hex = hex.strip_prefix('#').unwrap_or(hex);
|
||||
if hex.len() != 6 || !hex.is_ascii() {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||
Some([r, g, b])
|
||||
}
|
||||
|
||||
/// 格式化为 `#rrggbb`(写盘格式,与旧壳一致)。
|
||||
pub fn format_hex_rgb(rgb: Rgb8) -> String {
|
||||
format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -360,26 +681,67 @@ mod tests {
|
||||
#[test]
|
||||
fn parses_real_settings_shape() {
|
||||
let raw = RawSettings::from_text(
|
||||
"language=system\n\
|
||||
"language=zh-CN\n\
|
||||
theme=SilverLight\n\
|
||||
follow_system_theme=0\n\
|
||||
shell=powershell\n\
|
||||
ghost=1\n\
|
||||
accept=tab\n\
|
||||
completion_style=popup\n\
|
||||
shell=pwsh\n\
|
||||
font_family=Maple Mono Normal NF CN\n\
|
||||
font_size=16.3\n\
|
||||
cursor_shape=beam\n\
|
||||
cursor_blink=1\n\
|
||||
copy_on_select=1\n\
|
||||
cjk_bold_regular=1\n\
|
||||
tab_reveal=instant\n\
|
||||
density=compact\n\
|
||||
new_tab_position=end\n\
|
||||
cell_width_mode=relaxed\n\
|
||||
fetch=1\n\
|
||||
powerline=0\n\
|
||||
keep_session=1\n\
|
||||
restore_session=0\n\
|
||||
resume_ai=0\n\
|
||||
tray=0\n\
|
||||
blur=0\n\
|
||||
opacity=0.87\n\
|
||||
background=#101216\n\
|
||||
panel_resize=1\n\
|
||||
ssh_proxy_mode=custom\n\
|
||||
ssh_proxy_url=socks5://127.0.0.1:7890\n\
|
||||
quick_terminal_hotkey=alt+`\n\
|
||||
startup_directory=\n",
|
||||
);
|
||||
let settings = RuntimeSettings::from_raw(&raw);
|
||||
assert_eq!(settings.language, LanguagePref::ZhCn);
|
||||
assert_eq!(settings.theme, ThemeName::SilverLight);
|
||||
assert_eq!(settings.font_family.as_deref(), Some("Maple Mono Normal NF CN"));
|
||||
assert_eq!(settings.font_size_pt, Some(16.3));
|
||||
// font_size 键存的是逻辑像素(旧壳写盘语义),不做 pt 换算。
|
||||
assert_eq!(settings.font_size_px, Some(16.3));
|
||||
assert_eq!(settings.cursor_shape, Some(CursorShapeName::Beam));
|
||||
assert_eq!(settings.cursor_blink, Some(true));
|
||||
assert!(settings.copy_on_select);
|
||||
assert!(!settings.powerline);
|
||||
assert_eq!(settings.shell.as_deref(), Some("pwsh"));
|
||||
assert_eq!(settings.accept, AcceptKeyName::Tab);
|
||||
assert_eq!(settings.completion_style, CompletionStyleName::Popup);
|
||||
assert_eq!(settings.tab_reveal, TabRevealName::Instant);
|
||||
assert_eq!(settings.density, DensityName::Compact);
|
||||
assert_eq!(settings.new_tab_position, NewTabPositionName::End);
|
||||
assert_eq!(settings.cell_width_mode, CellWidthModeName::Relaxed);
|
||||
assert!(settings.fetch);
|
||||
assert!(settings.keep_session);
|
||||
assert!(!settings.restore_session);
|
||||
assert!(!settings.resume_ai);
|
||||
assert!(!settings.tray);
|
||||
assert!(!settings.blur);
|
||||
assert!((settings.opacity - 0.87).abs() < 1e-6);
|
||||
assert_eq!(settings.background, Some([0x10, 0x12, 0x16]));
|
||||
assert!(settings.panel_resize);
|
||||
assert_eq!(settings.ssh_proxy_mode, ProxyModeName::Custom);
|
||||
assert_eq!(settings.ssh_proxy_url, "socks5://127.0.0.1:7890");
|
||||
assert_eq!(settings.quick_terminal_hotkey, "alt+`");
|
||||
// 空值 = 未设置。
|
||||
assert_eq!(raw.value("startup_directory"), None);
|
||||
}
|
||||
@@ -387,10 +749,39 @@ mod tests {
|
||||
#[test]
|
||||
fn defaults_when_file_content_is_absent_or_junk() {
|
||||
let settings = RuntimeSettings::from_raw(&RawSettings::from_text("theme=NoSuchTheme\n"));
|
||||
// 出厂默认逐项对照旧壳 nebula_settings_load。
|
||||
assert_eq!(settings.language, LanguagePref::System);
|
||||
assert_eq!(settings.theme, ThemeName::Nebula);
|
||||
assert_eq!(settings.font_family, None);
|
||||
assert!(!settings.copy_on_select);
|
||||
assert!(settings.powerline);
|
||||
assert!(settings.ghost);
|
||||
assert_eq!(settings.accept, AcceptKeyName::Both);
|
||||
assert_eq!(settings.completion_style, CompletionStyleName::Inline);
|
||||
assert!(settings.cjk_bold_regular);
|
||||
assert_eq!(settings.tab_reveal, TabRevealName::Slide);
|
||||
assert_eq!(settings.density, DensityName::Standard);
|
||||
assert_eq!(settings.new_tab_position, NewTabPositionName::AfterCurrent);
|
||||
assert_eq!(settings.cell_width_mode, CellWidthModeName::Compact);
|
||||
assert!(!settings.fetch);
|
||||
assert!(!settings.keep_session);
|
||||
assert!(settings.restore_session);
|
||||
assert!(settings.resume_ai);
|
||||
assert!(settings.tray);
|
||||
assert!(settings.blur);
|
||||
assert!((settings.opacity - 1.0).abs() < 1e-6);
|
||||
assert_eq!(settings.background, None);
|
||||
assert!(!settings.panel_resize);
|
||||
assert_eq!(settings.ssh_proxy_mode, ProxyModeName::Off);
|
||||
assert_eq!(settings.quick_terminal_hotkey, DEFAULT_QUICK_TERMINAL_HOTKEY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_rgb_roundtrip() {
|
||||
assert_eq!(parse_hex_rgb("#8bd5ca"), Some([0x8b, 0xd5, 0xca]));
|
||||
assert_eq!(parse_hex_rgb("8bd5ca"), Some([0x8b, 0xd5, 0xca]));
|
||||
assert_eq!(parse_hex_rgb("#nothex"), None);
|
||||
assert_eq!(format_hex_rgb([0x8b, 0xd5, 0xca]), "#8bd5ca");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user