「选中复制」打开后,鼠标选中的文字在松开时进入系统剪贴板,单击没选中东西时剪贴板不变; 默认关闭。「右键行为」可选无动作、粘贴、显示菜单,默认显示菜单;右键从不交给要求鼠标的 程序。设置存在 settings.json 的 terminal_interaction,终端经全局量 TerminalInteraction 读取,改动立即作用于已打开的终端。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
733 lines
26 KiB
Rust
733 lines
26 KiB
Rust
use gpui_kit::component::ThemeMode;
|
|
use gpui_kit::{SharedString, WindowAppearance};
|
|
use serde::{Deserialize, Deserializer, Serialize};
|
|
|
|
use crate::app::ShortcutOverrides;
|
|
use crate::explorer::{FileSizeFormat, ShowHiddenFiles};
|
|
use crate::i18n::t;
|
|
use crate::terminal::{
|
|
DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT, FONT_SIZE_RANGE, HighlightRule, LINE_HEIGHT_RANGE,
|
|
RightClick, TerminalInteraction, TerminalTheme, default_rules,
|
|
};
|
|
use crate::update::Channel;
|
|
|
|
/// Everything the settings page changes. Each field falls back to its
|
|
/// default when the file does not mention it, so a file written by an older
|
|
/// version still loads.
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct AppSettings {
|
|
pub language: InterfaceLanguage,
|
|
pub appearance: Appearance,
|
|
/// 外观 → 终端主题.
|
|
pub terminal_theme: TerminalThemeSettings,
|
|
pub terminal_font: TerminalFontSettings,
|
|
/// 终端 → 终端交互.
|
|
pub terminal_interaction: TerminalInteraction,
|
|
pub external_cli: ExternalCliSettings,
|
|
pub update: UpdateSettings,
|
|
/// 关于 → 隐私.
|
|
pub analytics: AnalyticsSettings,
|
|
/// The SFTP 大小 column, switched from the column title's menu rather
|
|
/// than the settings page, as in WinSCP.
|
|
pub file_size_format: FileSizeFormat,
|
|
/// 显示隐藏文件 in the SFTP lists, each side on its own, switched from
|
|
/// their toolbars and menus.
|
|
pub show_hidden: ShowHiddenFiles,
|
|
/// 终端 → 通知.
|
|
pub notifications: NotificationSettings,
|
|
/// 关键字高亮.
|
|
pub terminal_highlight: TerminalHighlightSettings,
|
|
/// 应用 → 窗口.
|
|
pub window: WindowSettings,
|
|
/// 键盘快捷键: what the user changed of them.
|
|
pub shortcuts: ShortcutOverrides,
|
|
}
|
|
|
|
/// 应用 → 窗口: what the next launch restores of the main window. Both on
|
|
/// until the user turns them off.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct WindowSettings {
|
|
/// 记住窗口大小: its size, and whether it was maximized.
|
|
pub remember_size: bool,
|
|
/// 记住窗口位置: where it was, on which screen.
|
|
pub remember_position: bool,
|
|
}
|
|
|
|
impl Default for WindowSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
remember_size: true,
|
|
remember_position: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 外观 → 主题: by key, the theme used while the app is light and the one
|
|
/// while it is dark, and whether the app takes its colors or only the
|
|
/// terminals do.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct TerminalThemeSettings {
|
|
pub light: String,
|
|
pub dark: String,
|
|
/// 界面跟随主题. Off until the user turns it on: the app keeps
|
|
/// gpui-kit's own neutral light or dark colors.
|
|
pub app_follows: bool,
|
|
}
|
|
|
|
impl Default for TerminalThemeSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
light: TerminalTheme::default_for(ThemeMode::Light).key().into(),
|
|
dark: TerminalTheme::default_for(ThemeMode::Dark).key().into(),
|
|
app_follows: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TerminalThemeSettings {
|
|
/// The theme the app's colors come from for `mode`: the chosen one, or
|
|
/// the default, ShellRS Light or Dark, when the app does not follow.
|
|
pub fn app_theme(&self, mode: ThemeMode) -> &'static TerminalTheme {
|
|
if self.app_follows {
|
|
self.theme(mode)
|
|
} else {
|
|
TerminalTheme::default_for(mode)
|
|
}
|
|
}
|
|
|
|
/// Whether both columns have their defaults, the switch aside.
|
|
pub fn is_default_choice(&self) -> bool {
|
|
let default = Self::default();
|
|
self.light == default.light && self.dark == default.dark
|
|
}
|
|
|
|
/// Both columns back to their defaults, the switch left as it is.
|
|
pub fn reset_choice(&mut self) {
|
|
let default = Self::default();
|
|
self.light = default.light;
|
|
self.dark = default.dark;
|
|
}
|
|
|
|
/// The theme for `mode`. A key this version does not know (from a newer
|
|
/// one, or a typo), or one of a theme for the other appearance, gives
|
|
/// way to the default rather than failing the file.
|
|
pub fn theme(&self, mode: ThemeMode) -> &'static TerminalTheme {
|
|
let key = match mode {
|
|
ThemeMode::Light => &self.light,
|
|
ThemeMode::Dark => &self.dark,
|
|
};
|
|
TerminalTheme::find(key)
|
|
.filter(|theme| theme.mode() == mode)
|
|
.unwrap_or_else(|| TerminalTheme::default_for(mode))
|
|
}
|
|
|
|
/// Use `theme` for the appearance it is made for.
|
|
pub fn choose(&mut self, theme: &TerminalTheme) {
|
|
let key = theme.key().to_string();
|
|
match theme.mode() {
|
|
ThemeMode::Light => self.light = key,
|
|
ThemeMode::Dark => self.dark = key,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 关键字高亮: whether terminals color their text by the rules, and the
|
|
/// rules, in the order they win where they overlap. Off until the user turns
|
|
/// it on. A file without the rules gets the examples a new installation
|
|
/// starts with, ready once it is on; one the user emptied stays empty.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct TerminalHighlightSettings {
|
|
/// 启用关键字高亮. Off, no rule colors anything or notifies.
|
|
pub enabled: bool,
|
|
#[serde(deserialize_with = "readable_rules")]
|
|
pub rules: Vec<HighlightRule>,
|
|
}
|
|
|
|
impl Default for TerminalHighlightSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: false,
|
|
rules: default_rules(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The rules the file holds that this version can read. One it cannot (a
|
|
/// color from a newer version, a hand-made typo) is dropped on its own:
|
|
/// failing the whole file would put every setting back to its default.
|
|
fn readable_rules<'de, D: Deserializer<'de>>(
|
|
deserializer: D,
|
|
) -> Result<Vec<HighlightRule>, D::Error> {
|
|
let values = Vec::<serde_json::Value>::deserialize(deserializer)?;
|
|
Ok(values
|
|
.into_iter()
|
|
.filter_map(|value| serde_json::from_value(value).ok())
|
|
.collect())
|
|
}
|
|
|
|
/// 终端 → 通知: what a terminal may tell the user about while they look
|
|
/// elsewhere. Both on until the user turns them off.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct NotificationSettings {
|
|
/// 程序发送的通知: OSC 9 and 777.
|
|
pub programs: bool,
|
|
/// 响铃时通知.
|
|
pub bell: bool,
|
|
}
|
|
|
|
impl Default for NotificationSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
programs: true,
|
|
bell: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 外部 CLI.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct ExternalCliSettings {
|
|
/// Whether the `shellrs` command may use the saved hosts. Off until
|
|
/// the user turns it on.
|
|
pub enabled: bool,
|
|
}
|
|
|
|
/// 关于 → 应用更新.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct UpdateSettings {
|
|
/// 自动升级: look for a newer ShellRS in the background and download
|
|
/// it. On unless the user turns it off.
|
|
pub automatic: bool,
|
|
/// 更新渠道. A release build starts on the channel it was built for.
|
|
pub channel: Channel,
|
|
}
|
|
|
|
impl Default for UpdateSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
automatic: true,
|
|
channel: Channel::of_this_build().unwrap_or(Channel::Stable),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 关于 → 隐私.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct AnalyticsSettings {
|
|
/// 发送匿名使用统计. On unless the user turns it off.
|
|
pub enabled: bool,
|
|
}
|
|
|
|
impl Default for AnalyticsSettings {
|
|
fn default() -> Self {
|
|
Self { enabled: true }
|
|
}
|
|
}
|
|
|
|
impl Choice for Channel {
|
|
const ALL: &'static [Self] = &[Self::Stable, Self::Beta];
|
|
|
|
fn key(self) -> &'static str {
|
|
Channel::key(self)
|
|
}
|
|
|
|
fn label(self) -> SharedString {
|
|
match self {
|
|
Self::Stable => t!("settings.channel.stable"),
|
|
Self::Beta => "Beta".into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Choice for RightClick {
|
|
const ALL: &'static [Self] = &[Self::Nothing, Self::Paste, Self::Menu];
|
|
|
|
fn key(self) -> &'static str {
|
|
match self {
|
|
Self::Nothing => "nothing",
|
|
Self::Paste => "paste",
|
|
Self::Menu => "menu",
|
|
}
|
|
}
|
|
|
|
fn label(self) -> SharedString {
|
|
match self {
|
|
Self::Nothing => t!("settings.right_click.nothing"),
|
|
Self::Paste => t!("settings.right_click.paste"),
|
|
Self::Menu => t!("settings.right_click.menu"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AppSettings {
|
|
/// The settings with every number in the range the app accepts. Applied
|
|
/// to what the file holds and to every change, so neither a hand-edited
|
|
/// file nor a typed value can give the terminal a size of zero.
|
|
pub fn normalized(mut self) -> Self {
|
|
self.terminal_font = self.terminal_font.normalized();
|
|
self
|
|
}
|
|
}
|
|
|
|
/// 终端 → 字体配置.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct TerminalFontSettings {
|
|
/// `None` is the theme's monospace family (Menlo on macOS).
|
|
pub family: Option<String>,
|
|
/// Pixels.
|
|
pub size: f32,
|
|
/// A multiple of `size`.
|
|
pub line_height: f32,
|
|
}
|
|
|
|
impl Default for TerminalFontSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
family: None,
|
|
size: DEFAULT_FONT_SIZE,
|
|
line_height: DEFAULT_LINE_HEIGHT,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TerminalFontSettings {
|
|
/// Whole pixels, and line heights to two decimals, each within the
|
|
/// terminal's range. A value that is not a number falls back to its
|
|
/// default.
|
|
fn normalized(mut self) -> Self {
|
|
let clamp = |value: f32, range: std::ops::RangeInclusive<f32>, default: f32| {
|
|
if value.is_finite() {
|
|
value.clamp(*range.start(), *range.end())
|
|
} else {
|
|
default
|
|
}
|
|
};
|
|
self.size = clamp(self.size, FONT_SIZE_RANGE, DEFAULT_FONT_SIZE).round();
|
|
self.line_height =
|
|
(clamp(self.line_height, LINE_HEIGHT_RANGE, DEFAULT_LINE_HEIGHT) * 100.).round() / 100.;
|
|
self.family = self.family.filter(|family| !family.trim().is_empty());
|
|
self
|
|
}
|
|
}
|
|
|
|
/// A setting with a fixed set of values, shown as a dropdown.
|
|
pub trait Choice: Copy + PartialEq + 'static {
|
|
/// Every value, in the order the dropdown lists them.
|
|
const ALL: &'static [Self];
|
|
/// How the value is written in the settings file.
|
|
fn key(self) -> &'static str;
|
|
fn label(self) -> SharedString;
|
|
|
|
fn from_key(key: &str) -> Option<Self> {
|
|
Self::ALL.iter().copied().find(|choice| choice.key() == key)
|
|
}
|
|
}
|
|
|
|
/// 界面语言, for ShellRS's text and gpui-kit's own. It changes the window
|
|
/// at once.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum InterfaceLanguage {
|
|
// A settings file written before there was English says `zh-CN`, as
|
|
// the default was then, and stays Chinese.
|
|
#[default]
|
|
#[serde(rename = "system")]
|
|
System,
|
|
#[serde(rename = "zh-CN")]
|
|
SimplifiedChinese,
|
|
#[serde(rename = "en")]
|
|
English,
|
|
}
|
|
|
|
impl Choice for InterfaceLanguage {
|
|
const ALL: &'static [Self] = &[Self::System, Self::SimplifiedChinese, Self::English];
|
|
|
|
fn key(self) -> &'static str {
|
|
match self {
|
|
Self::System => "system",
|
|
Self::SimplifiedChinese => "zh-CN",
|
|
Self::English => "en",
|
|
}
|
|
}
|
|
|
|
fn label(self) -> SharedString {
|
|
match self {
|
|
Self::System => t!("settings.choice.system"),
|
|
// A language is named in itself, so it can be found by someone
|
|
// who cannot read the rest of the menu.
|
|
Self::SimplifiedChinese => "简体中文".into(), // i18n: keep
|
|
Self::English => "English".into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl InterfaceLanguage {
|
|
/// The locale the interface runs in. `system` is the operating system's
|
|
/// preferred language tag, such as `zh-Hans-CN` or `en-US`; any Chinese
|
|
/// gets Simplified Chinese, the only Chinese there is, and anything else
|
|
/// English. When the system does not say, the interface stays Chinese.
|
|
pub fn locale(self, system: Option<&str>) -> &'static str {
|
|
match self {
|
|
Self::SimplifiedChinese => "zh-CN",
|
|
Self::English => "en",
|
|
Self::System => match system {
|
|
Some(tag) => {
|
|
let language = tag.split(['-', '_']).next().unwrap_or_default();
|
|
if language.eq_ignore_ascii_case("zh") {
|
|
"zh-CN"
|
|
} else {
|
|
"en"
|
|
}
|
|
}
|
|
None => "zh-CN",
|
|
},
|
|
}
|
|
}
|
|
|
|
/// `locale` with the operating system's preferred language.
|
|
pub fn resolved(self) -> &'static str {
|
|
self.locale(sys_locale::get_locale().as_deref())
|
|
}
|
|
}
|
|
|
|
/// 应用外观.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum Appearance {
|
|
#[serde(rename = "dark")]
|
|
Dark,
|
|
#[serde(rename = "light")]
|
|
Light,
|
|
#[default]
|
|
#[serde(rename = "system")]
|
|
System,
|
|
}
|
|
|
|
impl Choice for Appearance {
|
|
const ALL: &'static [Self] = &[Self::Dark, Self::Light, Self::System];
|
|
|
|
fn key(self) -> &'static str {
|
|
match self {
|
|
Self::Dark => "dark",
|
|
Self::Light => "light",
|
|
Self::System => "system",
|
|
}
|
|
}
|
|
|
|
fn label(self) -> SharedString {
|
|
match self {
|
|
Self::Dark => t!("settings.appearance.dark"),
|
|
Self::Light => t!("settings.appearance.light"),
|
|
Self::System => t!("settings.choice.system"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Appearance {
|
|
/// The theme to use while the window has `window` appearance.
|
|
pub fn theme_mode(self, window: WindowAppearance) -> ThemeMode {
|
|
match self {
|
|
Self::Dark => ThemeMode::Dark,
|
|
Self::Light => ThemeMode::Light,
|
|
Self::System => window.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn the_file_spells_each_value_as_its_key() {
|
|
fn check<T: Choice + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug>() {
|
|
for &choice in T::ALL {
|
|
let json = serde_json::to_string(&choice).unwrap();
|
|
assert_eq!(json, format!("\"{}\"", choice.key()));
|
|
assert_eq!(serde_json::from_str::<T>(&json).unwrap(), choice);
|
|
assert_eq!(T::from_key(choice.key()), Some(choice));
|
|
}
|
|
}
|
|
check::<InterfaceLanguage>();
|
|
check::<Appearance>();
|
|
check::<RightClick>();
|
|
assert_eq!(Appearance::from_key("sepia"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn a_file_missing_a_setting_keeps_its_default() {
|
|
let settings: AppSettings = serde_json::from_str(r#"{"appearance":"dark"}"#).unwrap();
|
|
assert_eq!(settings.appearance, Appearance::Dark);
|
|
assert_eq!(settings.language, InterfaceLanguage::default());
|
|
let settings: AppSettings = serde_json::from_str(r#"{"later":1}"#).unwrap();
|
|
assert_eq!(settings, AppSettings::default());
|
|
assert_eq!(settings.file_size_format, FileSizeFormat::Kilobytes);
|
|
let settings: AppSettings =
|
|
serde_json::from_str(r#"{"file_size_format":"short"}"#).unwrap();
|
|
assert_eq!(settings.file_size_format, FileSizeFormat::Short);
|
|
// Hidden files are left out unless the file says otherwise, for
|
|
// each side on its own.
|
|
assert_eq!(
|
|
AppSettings::default().show_hidden,
|
|
ShowHiddenFiles::default()
|
|
);
|
|
let settings: AppSettings =
|
|
serde_json::from_str(r#"{"show_hidden":{"remote":true}}"#).unwrap();
|
|
assert!(settings.show_hidden.remote);
|
|
assert!(!settings.show_hidden.local);
|
|
}
|
|
|
|
#[test]
|
|
fn notifications_are_on_unless_the_file_says_otherwise() {
|
|
let settings: AppSettings = serde_json::from_str("{}").unwrap();
|
|
assert!(settings.notifications.programs && settings.notifications.bell);
|
|
let settings: AppSettings =
|
|
serde_json::from_str(r#"{"notifications":{"bell":false}}"#).unwrap();
|
|
assert!(settings.notifications.programs && !settings.notifications.bell);
|
|
}
|
|
|
|
#[test]
|
|
fn highlight_rules_start_as_examples_and_stay_as_the_user_leaves_them() {
|
|
// Off, with the examples each on and quiet, ready for when it is.
|
|
let settings: AppSettings = serde_json::from_str("{}").unwrap();
|
|
assert!(!settings.terminal_highlight.enabled);
|
|
assert_eq!(settings.terminal_highlight.rules, default_rules());
|
|
assert!(
|
|
default_rules()
|
|
.iter()
|
|
.all(|rule| rule.enabled && !rule.notify)
|
|
);
|
|
let settings: AppSettings =
|
|
serde_json::from_str(r#"{"terminal_highlight":{"enabled":true,"rules":[]}}"#).unwrap();
|
|
assert!(settings.terminal_highlight.enabled);
|
|
assert!(settings.terminal_highlight.rules.is_empty());
|
|
let json = serde_json::to_value(AppSettings::default()).unwrap();
|
|
assert_eq!(
|
|
json["terminal_highlight"]["rules"][0],
|
|
serde_json::json!({
|
|
"enabled": true,
|
|
"pattern": "ERROR",
|
|
"note": "错误",
|
|
"color": "#e5484d",
|
|
"notify": false
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_rule_this_version_cannot_read_costs_only_itself() {
|
|
let settings: AppSettings = serde_json::from_str(
|
|
r##"{
|
|
"appearance": "dark",
|
|
"terminal_highlight": {"rules": [
|
|
{"pattern": "FATAL", "color": "orange"},
|
|
{"pattern": "OOM", "color": "#D946EF", "notify": true},
|
|
{"pattern": ""},
|
|
7
|
|
]}
|
|
}"##,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(settings.appearance, Appearance::Dark);
|
|
let rules = &settings.terminal_highlight.rules;
|
|
assert_eq!(rules.len(), 2);
|
|
assert_eq!(rules[0].pattern, "OOM");
|
|
assert_eq!(rules[0].color.to_hex(), "#d946ef");
|
|
assert!(rules[0].enabled && rules[0].notify);
|
|
// A row added and not filled in yet stays, to be filled in.
|
|
assert_eq!(rules[1], HighlightRule::default());
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_themes_fall_back_to_the_default_for_their_appearance() {
|
|
let settings: AppSettings = serde_json::from_str("{}").unwrap();
|
|
let themes = &settings.terminal_theme;
|
|
assert_eq!(themes.light, "shellrs-light");
|
|
assert_eq!(themes.dark, "shellrs-dark");
|
|
for mode in [ThemeMode::Light, ThemeMode::Dark] {
|
|
assert_eq!(themes.theme(mode), TerminalTheme::default_for(mode));
|
|
}
|
|
|
|
// A key from a newer version, and a dark theme where a light one
|
|
// belongs, each give way on their own side only.
|
|
let settings: AppSettings = serde_json::from_str(
|
|
r#"{"appearance":"dark","terminal_theme":{"light":"shellrs-dark","dark":"sepia"}}"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(settings.appearance, Appearance::Dark);
|
|
let themes = &settings.terminal_theme;
|
|
assert_eq!(
|
|
themes.theme(ThemeMode::Light),
|
|
TerminalTheme::default_for(ThemeMode::Light)
|
|
);
|
|
assert_eq!(
|
|
themes.theme(ThemeMode::Dark),
|
|
TerminalTheme::default_for(ThemeMode::Dark)
|
|
);
|
|
|
|
// Choosing a theme sets the side it is made for, by key.
|
|
let mut themes = TerminalThemeSettings::default();
|
|
let dark = TerminalTheme::for_mode(ThemeMode::Dark).last().unwrap();
|
|
themes.choose(dark);
|
|
assert_eq!(themes.theme(ThemeMode::Dark), dark);
|
|
assert_eq!(
|
|
themes.theme(ThemeMode::Light),
|
|
TerminalTheme::default_for(ThemeMode::Light)
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_value(&themes).unwrap(),
|
|
serde_json::json!({"light": "shellrs-light", "dark": dark.key(), "app_follows": false})
|
|
);
|
|
|
|
// The app stays neutral, the default's colors, gpui-kit's own,
|
|
// unless told to follow; the terminals have the theme either way.
|
|
assert_eq!(
|
|
themes.app_theme(ThemeMode::Dark),
|
|
TerminalTheme::default_for(ThemeMode::Dark)
|
|
);
|
|
let settings: AppSettings =
|
|
serde_json::from_str(r#"{"terminal_theme":{"dark":"nord","app_follows":true}}"#)
|
|
.unwrap();
|
|
let themes = &settings.terminal_theme;
|
|
assert_eq!(themes.theme(ThemeMode::Dark).key(), "nord");
|
|
assert_eq!(themes.app_theme(ThemeMode::Dark).key(), "nord");
|
|
assert!(!themes.is_default_choice());
|
|
}
|
|
|
|
#[test]
|
|
fn the_window_is_remembered_unless_the_file_says_otherwise() {
|
|
let settings: AppSettings = serde_json::from_str("{}").unwrap();
|
|
assert!(settings.window.remember_size && settings.window.remember_position);
|
|
let settings: AppSettings =
|
|
serde_json::from_str(r#"{"window":{"remember_position":false}}"#).unwrap();
|
|
assert!(settings.window.remember_size && !settings.window.remember_position);
|
|
}
|
|
|
|
#[test]
|
|
fn terminals_show_the_menu_and_copy_only_on_command_unless_the_file_says_otherwise() {
|
|
let settings: AppSettings = serde_json::from_str("{}").unwrap();
|
|
assert!(!settings.terminal_interaction.copy_on_select);
|
|
assert_eq!(settings.terminal_interaction.right_click, RightClick::Menu);
|
|
let settings: AppSettings =
|
|
serde_json::from_str(r#"{"terminal_interaction":{"right_click":"paste"}}"#).unwrap();
|
|
assert!(!settings.terminal_interaction.copy_on_select);
|
|
assert_eq!(settings.terminal_interaction.right_click, RightClick::Paste);
|
|
}
|
|
|
|
#[test]
|
|
fn automatic_updates_are_on_unless_the_file_says_otherwise() {
|
|
let settings: AppSettings = serde_json::from_str("{}").unwrap();
|
|
assert!(settings.update.automatic);
|
|
let settings: AppSettings = serde_json::from_str(r#"{"update":{}}"#).unwrap();
|
|
assert!(settings.update.automatic);
|
|
let settings: AppSettings =
|
|
serde_json::from_str(r#"{"update":{"automatic":false}}"#).unwrap();
|
|
assert!(!settings.update.automatic);
|
|
}
|
|
|
|
#[test]
|
|
fn usage_statistics_are_sent_unless_the_file_says_otherwise() {
|
|
let settings: AppSettings = serde_json::from_str("{}").unwrap();
|
|
assert!(settings.analytics.enabled);
|
|
let settings: AppSettings = serde_json::from_str(r#"{"analytics":{}}"#).unwrap();
|
|
assert!(settings.analytics.enabled);
|
|
let settings: AppSettings =
|
|
serde_json::from_str(r#"{"analytics":{"enabled":false}}"#).unwrap();
|
|
assert!(!settings.analytics.enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn the_update_channel_is_the_builds_until_one_is_chosen() {
|
|
// A development build has no channel of its own.
|
|
let settings: AppSettings = serde_json::from_str(r#"{"update":{}}"#).unwrap();
|
|
assert_eq!(settings.update.channel, Channel::Stable);
|
|
let settings: AppSettings =
|
|
serde_json::from_str(r#"{"update":{"channel":"beta"}}"#).unwrap();
|
|
assert_eq!(settings.update.channel, Channel::Beta);
|
|
assert_eq!(
|
|
serde_json::to_value(&settings.update).unwrap()["channel"],
|
|
"beta"
|
|
);
|
|
assert_eq!(Channel::from_key("beta"), Some(Channel::Beta));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_font_numbers_are_kept_in_range_and_rounded() {
|
|
let font = |size, line_height| TerminalFontSettings {
|
|
family: Some("Menlo".into()),
|
|
size,
|
|
line_height,
|
|
};
|
|
let normalized = AppSettings {
|
|
// What stepping 1.54 by 0.1 leaves behind.
|
|
terminal_font: font(13.4, 1.54 + 0.1),
|
|
..AppSettings::default()
|
|
}
|
|
.normalized()
|
|
.terminal_font;
|
|
assert_eq!((normalized.size, normalized.line_height), (13., 1.64));
|
|
|
|
let normalized = font(2., 9.).normalized();
|
|
assert_eq!(
|
|
(normalized.size, normalized.line_height),
|
|
(*FONT_SIZE_RANGE.start(), *LINE_HEIGHT_RANGE.end())
|
|
);
|
|
let normalized = font(f32::NAN, f32::INFINITY).normalized();
|
|
assert_eq!(
|
|
(normalized.size, normalized.line_height),
|
|
(DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT)
|
|
);
|
|
let blank = TerminalFontSettings {
|
|
family: Some(" ".into()),
|
|
..TerminalFontSettings::default()
|
|
};
|
|
assert_eq!(blank.normalized().family, None);
|
|
// The default already is what the terminal had before.
|
|
assert_eq!(
|
|
TerminalFontSettings::default().normalized(),
|
|
TerminalFontSettings::default()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn following_the_system_language_picks_chinese_or_english() {
|
|
let system = InterfaceLanguage::System;
|
|
assert_eq!(system.locale(Some("zh-Hans-CN")), "zh-CN");
|
|
assert_eq!(system.locale(Some("zh_TW")), "zh-CN");
|
|
assert_eq!(system.locale(Some("ZH")), "zh-CN");
|
|
assert_eq!(system.locale(Some("en-US")), "en");
|
|
assert_eq!(system.locale(Some("ja-JP")), "en");
|
|
assert_eq!(system.locale(None), "zh-CN");
|
|
assert_eq!(InterfaceLanguage::English.locale(Some("zh-CN")), "en");
|
|
assert_eq!(
|
|
InterfaceLanguage::SimplifiedChinese.locale(Some("en-US")),
|
|
"zh-CN"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn following_the_system_appearance_uses_the_window() {
|
|
let system = Appearance::System;
|
|
assert_eq!(system.theme_mode(WindowAppearance::Dark), ThemeMode::Dark);
|
|
assert_eq!(
|
|
system.theme_mode(WindowAppearance::VibrantLight),
|
|
ThemeMode::Light
|
|
);
|
|
assert_eq!(
|
|
Appearance::Light.theme_mode(WindowAppearance::Dark),
|
|
ThemeMode::Light
|
|
);
|
|
assert_eq!(
|
|
Appearance::Dark.theme_mode(WindowAppearance::Light),
|
|
ThemeMode::Dark
|
|
);
|
|
}
|
|
}
|