feat(settings): shared nebula-settings crate; GPUI lab reads runtime settings + theme term colors
New zero-dependency workspace crate owning nebula_settings.txt path resolution, key semantics and the per-theme terminal color tables (background, light-theme foreground/ANSI-16 replacement, powerline slots 16..=23) - values transcribed verbatim from the legacy shell's theme.rs, which stays frozen per P4. The GPUI lab now honors the user's actual runtime intent: theme (SilverLight etc.) applied over toml colors in the same order as apply_term_colors, font family/size from the settings UI, default cursor shape/blink wired into the engine Term config, copy_on_select exposed for the next slice. Cargo.lock intentionally not committed: it currently carries the user's in-flight gpui-fork switch.
This commit is contained in:
1 parent
7727bcb779
commit
189e5ac4ff
7 files changed
+491
-17
No files matched your search
@@ -7,6 +7,7 @@ members = [
|
||||
"nebula-completions",
|
||||
"nebula_hook",
|
||||
"nebula_gpui",
|
||||
"nebula_settings",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ gpui-component-assets = "=0.5.1"
|
||||
# 终端核心是纯 Rust(vte/grid/ConPTY),没有 GUI 原生依赖,因此可以跨 workspace
|
||||
# 边界直接 path 引用而不会重新引入 fontconfig links 冲突。
|
||||
nebula_terminal = { path = "../nebula_terminal" }
|
||||
# 运行时用户设置(nebula_settings.txt + 主题终端色表)的共享权威实现。
|
||||
nebula-settings = { path = "../nebula_settings" }
|
||||
futures = "0.3"
|
||||
# 读取用户 nebula.toml 的字体/配色子集;版本与主仓保持一致。
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
//! 读取用户的 `nebula.toml`,与 `nebula_app` 共享同一份配置文件。
|
||||
//! 读取用户配置:`nebula.toml` + `nebula_settings.txt`,与 `nebula_app`
|
||||
//! 共享同一套文件与语义。
|
||||
//!
|
||||
//! 只解析 GPUI 壳当前消费的子集(字体、终端配色),解析全程宽容:未知字段
|
||||
//! 忽略、非法值回退默认,保证任何用户配置都不会阻止启动。查找路径与主应用
|
||||
//! `config::installed_config` 完全一致;`general.import`(及顶层 `import`)
|
||||
//! 按主应用语义做值级合并:imports 先加载,主文件最后覆盖。
|
||||
//! 两个来源、一个优先级:`nebula_settings.txt`(设置界面持久化的运行时
|
||||
//! 意图,经共享 crate `nebula-settings` 读取)覆盖 `nebula.toml` 的对应
|
||||
//! 字段;主题对终端色表的影响永远在最后叠加(镜像旧壳 `apply_term_colors`
|
||||
//! 的次序:用户配色是 defaults,主题裁定背景/浅色替换/Powerline 槽位)。
|
||||
//!
|
||||
//! 解析全程宽容:未知字段忽略、非法值回退默认,任何用户配置都不会阻止
|
||||
//! 启动。toml 查找路径与主应用 `config::installed_config` 一致;
|
||||
//! `general.import`(及顶层 `import`)按主应用语义值级合并。
|
||||
//!
|
||||
//! 尚未对齐的字段(记录在案,后续步骤处理):
|
||||
//! - `font.offset` / `font.glyph_offset`(cell 度量微调)
|
||||
//! - `colors.cursor` 的 `CellForeground`/`CellBackground` 反色语义
|
||||
//! - `follow_system_theme`(系统外观联动切主题)
|
||||
//! - 配置热重载(主应用用 notify 监视;本壳目前启动读一次)
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use gpui::Global;
|
||||
use nebula_settings::{CursorShapeName, RuntimeSettings};
|
||||
use nebula_terminal::vte::ansi::CursorShape;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::terminal::colors::Palette;
|
||||
@@ -26,7 +34,11 @@ pub struct Settings {
|
||||
/// GPUI 逻辑像素(配置里是 pt,1pt = 4/3 px @96dpi)。
|
||||
pub font_size_px: f32,
|
||||
pub palette: Palette,
|
||||
/// 实际加载的配置文件(诊断用;None 表示全默认)。
|
||||
pub cursor_shape: Option<CursorShape>,
|
||||
pub cursor_blink: Option<bool>,
|
||||
/// 选区完成即复制(旧壳 `copy_on_select` 设置)。
|
||||
pub copy_on_select: bool,
|
||||
/// 实际加载的 toml 配置文件(诊断用;None 表示无 toml)。
|
||||
pub source_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -34,6 +46,7 @@ impl Global for Settings {}
|
||||
|
||||
impl Settings {
|
||||
pub fn load() -> Self {
|
||||
let runtime = RuntimeSettings::load();
|
||||
let path = find_config_file();
|
||||
let raw = path
|
||||
.as_deref()
|
||||
@@ -41,26 +54,82 @@ impl Settings {
|
||||
.unwrap_or_else(|| toml::Value::Table(Default::default()));
|
||||
let raw: RawConfig = raw.try_into().unwrap_or_default();
|
||||
|
||||
let normal_family = raw
|
||||
.font
|
||||
.normal
|
||||
.family
|
||||
// 字体:settings.txt(设置界面)覆盖 toml,最后落内置默认。
|
||||
let normal_family = runtime
|
||||
.font_family
|
||||
.clone()
|
||||
.or_else(|| raw.font.normal.family.clone())
|
||||
.unwrap_or_else(|| default_font_family().to_string());
|
||||
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);
|
||||
|
||||
// 配色:toml 覆盖内置默认,主题最后裁定(与旧壳同序)。
|
||||
let mut palette = build_palette(&raw.colors);
|
||||
apply_theme(&mut palette, runtime.theme);
|
||||
|
||||
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: raw.font.size.unwrap_or(11.25).clamp(4.0, 96.0) * 4.0 / 3.0,
|
||||
palette: build_palette(&raw.colors),
|
||||
font_size_px: font_size_pt * 4.0 / 3.0,
|
||||
palette,
|
||||
cursor_shape: runtime.cursor_shape.map(|shape| match shape {
|
||||
CursorShapeName::Block => CursorShape::Block,
|
||||
CursorShapeName::Beam => CursorShape::Beam,
|
||||
CursorShapeName::Underline => CursorShape::Underline,
|
||||
}),
|
||||
cursor_blink: runtime.cursor_blink,
|
||||
copy_on_select: runtime.copy_on_select,
|
||||
font_family: normal_family,
|
||||
source_path: path,
|
||||
}
|
||||
}
|
||||
|
||||
/// 引擎 Term 的启动配置(默认光标形状/闪烁来自运行时设置)。
|
||||
pub fn term_config(&self) -> nebula_terminal::term::Config {
|
||||
let mut config = nebula_terminal::term::Config::default();
|
||||
if let Some(shape) = self.cursor_shape {
|
||||
config.default_cursor_style.shape = shape;
|
||||
}
|
||||
if let Some(blinking) = self.cursor_blink {
|
||||
config.default_cursor_style.blinking = blinking;
|
||||
}
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
/// 主题叠加在 toml 配色之上(镜像旧壳 `apply_term_colors` 的范围与次序):
|
||||
/// 背景永远替换;Powerline 槽位 16..=23 替换;浅色主题另替换前景与
|
||||
/// ANSI-16。dim 表与 bright_foreground 有意不动——与旧壳逐字段一致。
|
||||
fn apply_theme(palette: &mut Palette, theme: nebula_settings::ThemeName) {
|
||||
let term = theme.term_theme();
|
||||
palette.background = rgba8(term.background);
|
||||
for (i, color) in term.powerline.into_iter().enumerate() {
|
||||
set_indexed(palette, nebula_settings::POWERLINE_SLOT0 + i as u8, rgba8(color));
|
||||
}
|
||||
if term.is_light {
|
||||
palette.foreground = rgba8(nebula_settings::LIGHT_FOREGROUND);
|
||||
for (i, color) in nebula_settings::LIGHT_ANSI.into_iter().enumerate() {
|
||||
palette.ansi[i] = rgba8(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_indexed(palette: &mut Palette, index: u8, color: gpui::Rgba) {
|
||||
palette.indexed.retain(|(existing, _)| *existing != index);
|
||||
palette.indexed.push((index, color));
|
||||
}
|
||||
|
||||
fn rgba8(color: nebula_settings::Rgb8) -> gpui::Rgba {
|
||||
gpui::Rgba {
|
||||
r: color[0] as f32 / 255.0,
|
||||
g: color[1] as f32 / 255.0,
|
||||
b: color[2] as f32 / 255.0,
|
||||
a: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_font_family() -> &'static str {
|
||||
|
||||
@@ -50,9 +50,11 @@ pub struct TerminalSession {
|
||||
pub notifier: Notifier,
|
||||
}
|
||||
|
||||
/// 启动一个本地 shell 会话。尺寸随后由首帧 prepaint 按真实布局重设。
|
||||
/// 启动一个本地 shell 会话。尺寸随后由首帧 prepaint 按真实布局重设;
|
||||
/// `term_config` 携带运行时设置(默认光标形状/闪烁等)。
|
||||
pub fn spawn(
|
||||
window_size: WindowSize,
|
||||
term_config: Config,
|
||||
) -> std::io::Result<(TerminalSession, UnboundedReceiver<Event>)> {
|
||||
let (tx, rx) = unbounded();
|
||||
let proxy = EventProxy(tx);
|
||||
@@ -61,7 +63,7 @@ pub fn spawn(
|
||||
columns: window_size.num_cols as usize,
|
||||
screen_lines: window_size.num_lines as usize,
|
||||
};
|
||||
let term = Arc::new(FairMutex::new(Term::new(Config::default(), &grid, proxy.clone())));
|
||||
let term = Arc::new(FairMutex::new(Term::new(term_config, &grid, proxy.clone())));
|
||||
|
||||
let options = tty::Options::default();
|
||||
tty::setup_env();
|
||||
|
||||
@@ -62,8 +62,9 @@ pub struct TerminalView {
|
||||
|
||||
impl TerminalView {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
// 字体与调色板来自用户 nebula.toml(bootstrap 时装载为全局 Settings)。
|
||||
let (families, font_size, palette) = match cx.try_global::<Settings>() {
|
||||
// 字体、调色板与终端启动配置来自用户配置(nebula.toml +
|
||||
// nebula_settings.txt,bootstrap 时装载为全局 Settings)。
|
||||
let (families, font_size, palette, term_config) = match cx.try_global::<Settings>() {
|
||||
Some(settings) => (
|
||||
[
|
||||
settings.font_family.clone(),
|
||||
@@ -73,11 +74,13 @@ impl TerminalView {
|
||||
],
|
||||
px(settings.font_size_px),
|
||||
Arc::new(settings.palette.clone()),
|
||||
settings.term_config(),
|
||||
),
|
||||
None => (
|
||||
std::array::from_fn(|_| String::from("Cascadia Mono")),
|
||||
px(15.0),
|
||||
Arc::new(Palette::default()),
|
||||
nebula_terminal::term::Config::default(),
|
||||
),
|
||||
};
|
||||
// to_string:SharedString 的 &str 转换要求 'static,字体名是运行时值。
|
||||
@@ -88,7 +91,7 @@ impl TerminalView {
|
||||
};
|
||||
|
||||
let initial = WindowSize { num_lines: 30, num_cols: 100, cell_width: 9, cell_height: 18 };
|
||||
let (session, error) = match session::spawn(initial) {
|
||||
let (session, error) = match session::spawn(initial, term_config) {
|
||||
Ok((session, mut rx)) => {
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some(event) = rx.next().await {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "nebula-settings"
|
||||
version = "1.0.0"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "Shared reader for Nebula runtime user settings (nebula_settings.txt) and theme terminal-color tables"
|
||||
publish = false
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
# 刻意零依赖:任何壳(winit 旧壳 / GPUI)与任何渲染后端都能免费引用,
|
||||
# 不给依赖图带来任何新节点。
|
||||
[dependencies]
|
||||
@@ -0,0 +1,385 @@
|
||||
//! Nebula 运行时用户设置(`nebula_settings.txt`)与主题终端色表的共享读取。
|
||||
//!
|
||||
//! 这是两个 UI 壳同读的权威实现:设置文件的路径解析、键值语义,以及每个
|
||||
//! 主题对终端色表的影响(背景替换、浅色主题的前景/ANSI-16 替换、Powerline
|
||||
//! 提示符槽位)。设置界面写入的 `nebula_settings.txt` 是运行时用户意图的
|
||||
//! 权威来源,优先级高于 `nebula.toml` 的对应字段。
|
||||
//!
|
||||
//! 同步事实(记录在案):
|
||||
//! - `nebula_app/src/display/ui/theme.rs` 是旧壳体内的既有实现;本 crate 的
|
||||
//! 色值与其逐字一致。旧壳按 P4 裁定冻结保留,新 UI 一律读这里;改主题
|
||||
//! 色值时两处同步,直到 P3 接入完成后旧壳引用此 crate。
|
||||
//! - `nebula_terminal::tty::windows` 里有一份私有的同款路径/键值读取;引擎
|
||||
//! 不能依赖 UI 侧 crate(依赖方向是铁律),属有意保留的最小重复。
|
||||
//! - `follow_system_theme` 的系统外观联动暂未在此实现(消费方自行处理)。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 设置目录:`NEBULA_CONFIG_DIR` 覆盖(便携/测试隔离,指向目录本身)→
|
||||
/// `%APPDATA%\Nebula` → `$HOME/.config/Nebula` → 临时目录。
|
||||
/// 与引擎 tty 层、旧壳、注入的 shell 提示符脚本使用同一套解析。
|
||||
pub fn settings_dir() -> PathBuf {
|
||||
if let Some(path) = std::env::var_os("NEBULA_CONFIG_DIR").filter(|p| !p.is_empty()) {
|
||||
return PathBuf::from(path);
|
||||
}
|
||||
std::env::var_os("APPDATA")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join("Nebula")
|
||||
}
|
||||
|
||||
pub fn settings_path() -> PathBuf {
|
||||
settings_dir().join("nebula_settings.txt")
|
||||
}
|
||||
|
||||
/// `nebula_settings.txt` 的键值视图。行格式 `key=value`;键大小写不敏感,
|
||||
/// 值两端修剪;空值视为未设置。
|
||||
#[derive(Default)]
|
||||
pub struct RawSettings {
|
||||
values: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl RawSettings {
|
||||
pub fn load() -> Self {
|
||||
std::fs::read_to_string(settings_path())
|
||||
.map(|text| Self::from_text(&text))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn from_text(text: &str) -> Self {
|
||||
let mut values = HashMap::new();
|
||||
for line in text.lines() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
values.insert(key.trim().to_ascii_lowercase(), value.trim().to_owned());
|
||||
}
|
||||
}
|
||||
Self { values }
|
||||
}
|
||||
|
||||
pub fn value(&self, key: &str) -> Option<&str> {
|
||||
self.values
|
||||
.get(&key.to_ascii_lowercase())
|
||||
.map(String::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub fn f32(&self, key: &str) -> Option<f32> {
|
||||
self.value(key)?.parse().ok()
|
||||
}
|
||||
|
||||
/// `1/true/yes/on` → true;`0/false/no/off` → false;其余 `None`。
|
||||
pub fn bool_on(&self, key: &str) -> Option<bool> {
|
||||
match self.value(key)?.to_ascii_lowercase().as_str() {
|
||||
"1" | "true" | "yes" | "on" => Some(true),
|
||||
"0" | "false" | "no" | "off" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Rgb8 = [u8; 3];
|
||||
|
||||
/// 主题标识;`nebula_settings.txt` 里 `theme=` 持久化 [`Self::prompt_name`]。
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ThemeName {
|
||||
#[default]
|
||||
Nebula,
|
||||
SilverLight,
|
||||
SteelDark,
|
||||
LimestoneLight,
|
||||
CoalDark,
|
||||
LinenLight,
|
||||
MossDark,
|
||||
}
|
||||
|
||||
impl ThemeName {
|
||||
pub fn from_prompt_name(name: &str) -> Option<Self> {
|
||||
Some(match name {
|
||||
"Nebula" => Self::Nebula,
|
||||
"SilverLight" => Self::SilverLight,
|
||||
"SteelDark" => Self::SteelDark,
|
||||
"LimestoneLight" => Self::LimestoneLight,
|
||||
"CoalDark" => Self::CoalDark,
|
||||
"LinenLight" => Self::LinenLight,
|
||||
"MossDark" => Self::MossDark,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prompt_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Nebula => "Nebula",
|
||||
Self::SilverLight => "SilverLight",
|
||||
Self::SteelDark => "SteelDark",
|
||||
Self::LimestoneLight => "LimestoneLight",
|
||||
Self::CoalDark => "CoalDark",
|
||||
Self::LinenLight => "LinenLight",
|
||||
Self::MossDark => "MossDark",
|
||||
}
|
||||
}
|
||||
|
||||
/// 主题对终端色表的全部影响(旧壳 `apply_term_colors` 的数据形态)。
|
||||
pub fn term_theme(self) -> TermTheme {
|
||||
// 背景与 is_light 来自各主题 palette();powerline 为提示符段色
|
||||
// (icon bg/fg、path bg/fg、branch bg/fg、time bg/fg)。
|
||||
match self {
|
||||
Self::Nebula => TermTheme {
|
||||
background: [15, 17, 26],
|
||||
is_light: false,
|
||||
powerline: [
|
||||
[57, 75, 112],
|
||||
[192, 202, 245],
|
||||
[41, 52, 82],
|
||||
[169, 177, 214],
|
||||
[47, 79, 79],
|
||||
[139, 213, 202],
|
||||
[29, 33, 46],
|
||||
[100, 116, 139],
|
||||
],
|
||||
},
|
||||
Self::SilverLight => TermTheme {
|
||||
background: [255, 255, 255],
|
||||
is_light: true,
|
||||
powerline: [
|
||||
[229, 231, 235],
|
||||
[55, 65, 81],
|
||||
[243, 244, 246],
|
||||
[55, 65, 81],
|
||||
[224, 242, 254],
|
||||
[3, 105, 161],
|
||||
[249, 250, 251],
|
||||
[107, 114, 128],
|
||||
],
|
||||
},
|
||||
Self::SteelDark => TermTheme {
|
||||
background: [26, 28, 36],
|
||||
is_light: false,
|
||||
powerline: [
|
||||
[71, 85, 105],
|
||||
[241, 245, 249],
|
||||
[51, 65, 85],
|
||||
[203, 213, 225],
|
||||
[59, 82, 73],
|
||||
[163, 184, 153],
|
||||
[40, 44, 56],
|
||||
[148, 163, 184],
|
||||
],
|
||||
},
|
||||
Self::LimestoneLight => TermTheme {
|
||||
background: [255, 255, 255],
|
||||
is_light: true,
|
||||
powerline: [
|
||||
[214, 211, 209],
|
||||
[250, 250, 249],
|
||||
[231, 229, 228],
|
||||
[68, 64, 60],
|
||||
[200, 198, 167],
|
||||
[41, 37, 36],
|
||||
[235, 233, 230],
|
||||
[163, 160, 151],
|
||||
],
|
||||
},
|
||||
Self::CoalDark => TermTheme {
|
||||
background: [23, 23, 23],
|
||||
is_light: false,
|
||||
powerline: [
|
||||
[82, 82, 82],
|
||||
[245, 245, 245],
|
||||
[64, 64, 64],
|
||||
[212, 212, 212],
|
||||
[74, 79, 65],
|
||||
[181, 181, 166],
|
||||
[48, 48, 48],
|
||||
[115, 115, 115],
|
||||
],
|
||||
},
|
||||
Self::LinenLight => TermTheme {
|
||||
background: [255, 255, 255],
|
||||
is_light: true,
|
||||
powerline: [
|
||||
[212, 212, 208],
|
||||
[255, 255, 255],
|
||||
[229, 229, 223],
|
||||
[63, 63, 63],
|
||||
[181, 196, 177],
|
||||
[45, 45, 45],
|
||||
[236, 236, 230],
|
||||
[176, 179, 176],
|
||||
],
|
||||
},
|
||||
Self::MossDark => TermTheme {
|
||||
background: [30, 33, 30],
|
||||
is_light: false,
|
||||
powerline: [
|
||||
[75, 85, 72],
|
||||
[240, 253, 244],
|
||||
[59, 66, 56],
|
||||
[220, 252, 231],
|
||||
[60, 79, 60],
|
||||
[187, 247, 208],
|
||||
[42, 47, 42],
|
||||
[107, 114, 107],
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个主题对终端色表的影响。浅色主题额外用 [`LIGHT_FOREGROUND`] 与
|
||||
/// [`LIGHT_ANSI`] 替换前景与 ANSI-16(配置的暗底配色在浅底上不可读)。
|
||||
/// dim 表与 bright_foreground 有意不动——与旧壳逐字段一致。
|
||||
pub struct TermTheme {
|
||||
pub background: Rgb8,
|
||||
pub is_light: bool,
|
||||
/// Powerline 提示符段色,发布到 256 色表 [`POWERLINE_SLOT0`]`..+8`。
|
||||
pub powerline: [Rgb8; 8],
|
||||
}
|
||||
|
||||
/// Powerline 槽位起点(索引色 16..=23)。
|
||||
pub const POWERLINE_SLOT0: u8 = 16;
|
||||
|
||||
/// 浅色主题的替换前景(#24292f)。
|
||||
pub const LIGHT_FOREGROUND: Rgb8 = [36, 41, 47];
|
||||
|
||||
/// 浅色主题的 ANSI-16 替换表(GitHub Primer Light 派生;BrightWhite 刻意
|
||||
/// 用灰——纯白会消失在白底终端上)。
|
||||
pub const LIGHT_ANSI: [Rgb8; 16] = [
|
||||
[36, 41, 47], // black #24292f
|
||||
[207, 34, 46], // red #cf222e
|
||||
[26, 127, 55], // green #1a7f37
|
||||
[154, 103, 0], // yellow #9a6700
|
||||
[9, 105, 218], // blue #0969da
|
||||
[130, 80, 223], // magenta #8250df
|
||||
[27, 124, 131], // cyan #1b7c83
|
||||
[110, 119, 129], // white #6e7781
|
||||
[87, 96, 106], // bright black #57606a
|
||||
[164, 14, 38], // bright red #a40e26
|
||||
[45, 164, 78], // bright green #2da44e
|
||||
[191, 135, 0], // bright yellow #bf8700
|
||||
[33, 139, 255], // bright blue #218bff
|
||||
[164, 117, 249], // bright magenta #a475f9
|
||||
[49, 146, 170], // bright cyan #3192aa
|
||||
[140, 149, 159], // bright white #8c959f
|
||||
];
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CursorShapeName {
|
||||
Block,
|
||||
Beam,
|
||||
Underline,
|
||||
}
|
||||
|
||||
/// 新 UI 消费的运行时设置子集。字段随消费方增长;未设置的键保持 `None`
|
||||
/// 让调用方自选回退。
|
||||
pub struct RuntimeSettings {
|
||||
pub theme: ThemeName,
|
||||
pub font_family: Option<String>,
|
||||
/// pt(与设置界面一致)。
|
||||
pub font_size_pt: Option<f32>,
|
||||
pub cursor_shape: Option<CursorShapeName>,
|
||||
pub cursor_blink: Option<bool>,
|
||||
pub copy_on_select: bool,
|
||||
pub powerline: bool,
|
||||
}
|
||||
|
||||
impl RuntimeSettings {
|
||||
pub fn load() -> Self {
|
||||
Self::from_raw(&RawSettings::load())
|
||||
}
|
||||
|
||||
pub fn from_raw(raw: &RawSettings) -> Self {
|
||||
Self {
|
||||
theme: raw.value("theme").and_then(ThemeName::from_prompt_name).unwrap_or_default(),
|
||||
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,
|
||||
}
|
||||
}),
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_real_settings_shape() {
|
||||
let raw = RawSettings::from_text(
|
||||
"language=system\n\
|
||||
theme=SilverLight\n\
|
||||
follow_system_theme=0\n\
|
||||
shell=powershell\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\
|
||||
powerline=0\n\
|
||||
startup_directory=\n",
|
||||
);
|
||||
let settings = RuntimeSettings::from_raw(&raw);
|
||||
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));
|
||||
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!(raw.value("startup_directory"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_when_file_content_is_absent_or_junk() {
|
||||
let settings = RuntimeSettings::from_raw(&RawSettings::from_text("theme=NoSuchTheme\n"));
|
||||
assert_eq!(settings.theme, ThemeName::Nebula);
|
||||
assert_eq!(settings.font_family, None);
|
||||
assert!(!settings.copy_on_select);
|
||||
assert!(settings.powerline);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_names_roundtrip() {
|
||||
for theme in [
|
||||
ThemeName::Nebula,
|
||||
ThemeName::SilverLight,
|
||||
ThemeName::SteelDark,
|
||||
ThemeName::LimestoneLight,
|
||||
ThemeName::CoalDark,
|
||||
ThemeName::LinenLight,
|
||||
ThemeName::MossDark,
|
||||
] {
|
||||
assert_eq!(ThemeName::from_prompt_name(theme.prompt_name()), Some(theme));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_themes_replace_foreground_dark_themes_do_not() {
|
||||
assert!(ThemeName::SilverLight.term_theme().is_light);
|
||||
assert!(ThemeName::LimestoneLight.term_theme().is_light);
|
||||
assert!(ThemeName::LinenLight.term_theme().is_light);
|
||||
assert!(!ThemeName::Nebula.term_theme().is_light);
|
||||
assert!(!ThemeName::SteelDark.term_theme().is_light);
|
||||
// 浅色主题终端底全白,背景由主题裁定而非用户配色。
|
||||
assert_eq!(ThemeName::SilverLight.term_theme().background, [255, 255, 255]);
|
||||
assert_eq!(ThemeName::Nebula.term_theme().background, [15, 17, 26]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_are_case_insensitive_and_trimmed() {
|
||||
let raw = RawSettings::from_text(" Theme = SilverLight \nFONT_SIZE=12\n");
|
||||
assert_eq!(raw.value("theme"), Some("SilverLight"));
|
||||
assert_eq!(raw.f32("font_size"), Some(12.0));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user