mod accessibility; pub mod ai; mod alias_expansion; pub mod app_icon; pub mod app_installation_detection; mod block_visibility; mod changelog; pub mod cloud_preferences; pub mod cloud_preferences_syncer; mod code; mod debug; mod editor; mod emacs_bindings; pub mod font; mod gpu; pub mod import; mod init; pub mod initializer; mod input; mod input_mode; #[cfg(any(target_os = "linux", target_os = "freebsd"))] mod linux; mod local_control; pub mod macros; pub mod manager; pub mod native_preference; mod onboarding; mod pane; mod privacy; mod same_line_prompt_block; mod scroll; mod select; mod ssh; mod theme; mod vim_banner; #[cfg(test)] #[path = "schema_validation_tests.rs"] mod schema_validation_tests; pub use accessibility::*; pub use ai::*; pub use alias_expansion::*; pub use block_visibility::*; pub use changelog::*; pub use cloud_preferences::*; pub use code::*; pub use debug::*; pub use editor::*; pub use emacs_bindings::*; pub use font::*; use galaxy_core::user_preferences::GetUserPreferences as _; pub use gpu::*; pub use init::*; pub use input::*; pub use input_mode::*; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub use linux::*; pub use local_control::*; pub use native_preference::*; pub use onboarding::*; pub use pane::*; pub use privacy::*; pub use same_line_prompt_block::*; pub use scroll::*; pub use select::*; pub use ssh::*; pub use theme::*; pub use vim_banner::*; /// Describes errors encountered when loading settings from `settings.toml`. #[derive(Clone, Debug, PartialEq, Eq)] pub enum SettingsFileError { /// The entire file failed to parse as valid TOML. FileParseFailed(String), /// Individual setting values failed to deserialize. Contains the storage /// keys of the settings that could not be loaded. InvalidSettings(Vec), } impl std::fmt::Display for SettingsFileError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::FileParseFailed(_) => { write!(f, "Couldn't parse due to invalid syntax") } Self::InvalidSettings(keys) => match keys.as_slice() { [key] => write!(f, "Invalid value for '{key}'"), _ => write!(f, "Invalid values for: {}", keys.join(", ")), }, } } } impl SettingsFileError { /// Returns the user-facing `(heading, description)` pair used to present /// this error. Shared between the workspace-level banner /// (`Workspace::render_settings_error_banner`) and the settings nav rail /// footer (`render_settings_error_alert`) so the two UIs stay in sync. pub fn heading_and_description(&self) -> (String, String) { match self { Self::FileParseFailed(_) => ( "Your settings file contains an error.".to_owned(), format!("{self}. Open the file to fix it."), ), Self::InvalidSettings(keys) => match keys.len() { 1 => ( "Your settings file contains an error.".to_owned(), format!("{self}. The default value is being used."), ), _ => ( "Your settings file contains errors.".to_owned(), format!("{self}. Default values are being used."), ), }, } } } use std::collections::HashMap; use std::ops::Mul; use std::path::PathBuf; use galaxy_core::features::FeatureFlag; use galaxyui::elements::DEFAULT_UI_LINE_HEIGHT_RATIO; use galaxyui::keymap::Keystroke; use galaxyui::{AppContext, DisplayIdx, SingletonEntity}; use lazy_static::lazy_static; use pathfinder_geometry::rect::RectF; use pathfinder_geometry::vector::Vector2F; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use settings::Setting as _; use crate::root_view::QuakeModePinPosition; use crate::terminal::{BlockListSettings, BlockPadding}; use crate::themes::theme::{GalaxyTheme, ThemeKind}; use crate::user_config::GalaxyConfig; // The following are user preferences keys. pub const CHANGELOG_VERSIONS: &str = "ChangelogVersions"; pub const RESTORE_SESSION: &str = "RestoreSession"; pub const INPUT_MODE: &str = "InputMode"; pub const ACTIVATION_HOTKEY_ENABLED: &str = "ActivationHotkeyEnabled"; pub const ACTIVATION_HOTKEY_KEYBINDING: &str = "ActivationHotkeyKeybinding"; pub const DISMISSED_AI_ASSISTANT_WELCOME_KEY: &str = "DismissedWarpAIWarmWelcome"; pub const TIMES_TO_SHOW_AUTOSUGGESTION_HINT: i8 = 2; pub const QUAKE_WINDOW_AUTOHIDE_SUPPORTED: bool = cfg!(any(target_os = "macos", windows)); lazy_static! { pub static ref DEFAULT_QUAKE_MODE_SIZE_PERCENTAGES: HashMap = HashMap::from_iter([ ( QuakeModePinPosition::Top, SizePercentages { width: 100, height: 30 } ), ( QuakeModePinPosition::Bottom, SizePercentages { width: 100, height: 30 } ), ( QuakeModePinPosition::Left, SizePercentages { width: 40, height: 100 } ), ( QuakeModePinPosition::Right, SizePercentages { width: 40, height: 100 } ) ]); } /// Keys which may be interpreted as the meta key. #[derive( Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars(description = "Additional keys that act as the meta key.")] pub struct ExtraMetaKeys { #[schemars(description = "Whether the left Alt key acts as meta.")] pub left_alt: bool, #[schemars(description = "Whether the right Alt key acts as meta.")] pub right_alt: bool, } #[derive( Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars(description = "What Ctrl+Tab does.", rename_all = "snake_case")] pub enum CtrlTabBehavior { #[default] ActivatePrevNextTab, CycleMostRecentSession, CycleMostRecentTab, } impl CtrlTabBehavior { pub fn as_dropdown_label(&self) -> &str { match self { Self::ActivatePrevNextTab => "Activate previous/next tab", Self::CycleMostRecentSession => "Cycle most recent session", Self::CycleMostRecentTab => "Cycle most recent tab", } } } impl ExtraMetaKeys { pub fn toggle_left_key(&self) -> Self { ExtraMetaKeys { left_alt: !self.left_alt, right_alt: self.right_alt, } } pub fn toggle_right_key(&self) -> Self { ExtraMetaKeys { left_alt: self.left_alt, right_alt: !self.right_alt, } } } /// App-wide UI settings. /// /// DO NOT ADD ANYTHING NEW HERE! /// /// This struct is deprecated; all new settings should make use of the /// macros in app/src/settings/macros.rs. #[derive(Clone, Debug)] pub struct Settings; /// This enum is used to enforce a ternary option with a dropdown in the features page. We may /// later allow users to have both quake mode and activation mode enabled simultaneously. If/when /// that happens we'll remove this enum. These options are not modeled as a ternary option in the /// serialized user-defaults, but as independent options. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum GlobalHotkeyMode { #[default] Disabled, /// "Quake mode" shows a dedicated window with special properties (thanks to it using an Appkit /// NSPanel). QuakeMode, /// "Activation hotkey" shows/hides all of the normal windows ActivationHotkey, } impl GlobalHotkeyMode { pub fn as_dropdown_label(&self) -> &str { match self { Self::Disabled => "Disabled", Self::QuakeMode => "Dedicated hotkey window", Self::ActivationHotkey => "Show/hide all windows", } } } #[derive( Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars(description = "Window size as width and height percentages of the screen.")] pub struct SizePercentages { #[schemars(description = "Width as a percentage of screen width (0–100).")] pub width: u8, #[schemars(description = "Height as a percentage of screen height (0–100).")] pub height: u8, } impl SizePercentages { pub fn width_decimal(&self) -> f32 { (self.width as f32 / 100.).min(1.) } pub fn height_decimal(&self) -> f32 { (self.height as f32 / 100.).min(1.) } } impl Mul for SizePercentages { type Output = Vector2F; fn mul(self, rhs: Vector2F) -> Vector2F { Vector2F::new( self.width_decimal() * rhs.x(), self.height_decimal() * rhs.y(), ) } } #[derive( Clone, Debug, Serialize, Deserialize, PartialEq, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars(description = "Configuration for the hotkey window.")] pub struct QuakeModeSettings { #[schemars( description = "Keyboard shortcut to toggle the hotkey window. Format: modifiers (cmd, ctrl, alt, shift, meta) and a key joined by '-', e.g. \"cmd-shift-a\" or \"alt-enter\". Bindings are case-sensitive: when shift is present, the key must be its shifted form (e.g., \"ctrl-shift-E\", not \"ctrl-shift-e\")." )] pub keybinding: Option, #[schemars(description = "Screen edge where the hotkey window is pinned.")] pub active_pin_position: QuakeModePinPosition, #[schemars(description = "Window size percentages for each pin position.")] pub pin_position_to_size_percentages: HashMap, #[schemars(description = "Display to pin the hotkey window to.")] pub pin_screen: Option, /// Whether we should hide quake mode window when it loses focus, this could happen either when /// user focuses on another warp window or another app. #[schemars(description = "Whether to hide the hotkey window when it loses focus.")] pub hide_window_when_unfocused: bool, } impl Default for QuakeModeSettings { fn default() -> Self { Self { keybinding: Default::default(), active_pin_position: Default::default(), pin_position_to_size_percentages: DEFAULT_QUAKE_MODE_SIZE_PERCENTAGES.clone(), pin_screen: Default::default(), // Defaults to `true` only when it's supported on this platform. hide_window_when_unfocused: QUAKE_WINDOW_AUTOHIDE_SUPPORTED, } } } impl QuakeModeSettings { pub fn width_percentage(&self) -> u8 { self.size_percentages_for_pin_position(&self.active_pin_position) .width } pub fn height_percentage(&self) -> u8 { self.size_percentages_for_pin_position(&self.active_pin_position) .height } pub fn size_changed_from_default(&self) -> bool { self.size_percentages_for_pin_position(&self.active_pin_position) != *DEFAULT_QUAKE_MODE_SIZE_PERCENTAGES .get(&self.active_pin_position) .expect("Default should have every pin position") } pub fn size_percentages_for_pin_position( &self, pin_position: &QuakeModePinPosition, ) -> SizePercentages { *self .pin_position_to_size_percentages .get(pin_position) .unwrap_or_else(|| { DEFAULT_QUAKE_MODE_SIZE_PERCENTAGES .get(&self.active_pin_position) .expect("Default should have every pin position") }) } /// Resolves the display bounds for quake mode (respecting the pinned screen setting) /// and calculates the window bounds. pub fn resolve_quake_mode_bounds(&self, ctx: &mut AppContext) -> RectF { let display_bounds = self .pin_screen .and_then(|display_idx| ctx.windows().bounds_for_display_idx(display_idx)) .unwrap_or_else(|| ctx.windows().active_display_bounds()); self.calculate_quake_mode_bounds_from_settings(display_bounds) } pub fn calculate_quake_mode_bounds_from_settings(&self, display_bounds: RectF) -> RectF { let size_percentages = self.size_percentages_for_pin_position(&self.active_pin_position); let quake_window_size = size_percentages * display_bounds.size(); match self.active_pin_position { QuakeModePinPosition::Top => { // Position the frame in the center of the display on x-axis. let x_axis_offset = display_bounds.size().x() * (1. - size_percentages.width_decimal()) / 2.; let quake_window_origin = Vector2F::new( display_bounds.origin().x() + x_axis_offset, display_bounds.origin().y(), ); RectF::new(quake_window_origin, quake_window_size) } QuakeModePinPosition::Bottom => { // Position the frame in the center of the display on x-axis. let x_axis_offset = display_bounds.size().x() * (1. - size_percentages.width_decimal()) / 2.; let quake_window_origin = Vector2F::new( display_bounds.origin().x() + x_axis_offset, display_bounds.lower_left().y() - quake_window_size.y(), ); RectF::new(quake_window_origin, quake_window_size) } QuakeModePinPosition::Left => { // Position the frame in the center of the display on y-axis. let y_axis_offset = display_bounds.size().y() * (1. - size_percentages.height_decimal()) / 2.; let quake_window_origin = Vector2F::new( display_bounds.origin().x(), display_bounds.origin().y() + y_axis_offset, ); RectF::new(quake_window_origin, quake_window_size) } QuakeModePinPosition::Right => { // Position the frame in the center of the display on y-axis. let y_axis_offset = display_bounds.size().y() * (1. - size_percentages.height_decimal()) / 2.; let quake_window_origin = Vector2F::new( display_bounds.upper_right().x() - quake_window_size.x(), display_bounds.origin().y() + y_axis_offset, ); RectF::new(quake_window_origin, quake_window_size) } } } } /// Circumstances when FG color can be automatically changed to increase contrast with BG color #[derive( Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema, settings_value::SettingsValue, )] #[schemars( description = "When to adjust foreground color to ensure readability against the background.", rename_all = "snake_case" )] pub enum EnforceMinimumContrast { /// Never change FG color Never, /// FG color can be changed, but only if the FG is specified with default colors #[default] OnlyNamedColors, /// FG color is changed regardless of how FG was specified Always, } impl Settings { pub fn has_changelog_been_shown(changelog_version: &str, ctx: &mut AppContext) -> bool { let changelog_versions = ctx .private_user_preferences() .read_value(CHANGELOG_VERSIONS) .unwrap_or_default(); changelog_versions.is_some_and(|versions| -> bool { let res = serde_json::from_str::(&versions); match res { Ok(versions) => versions[&changelog_version].as_bool().unwrap_or(false), Err(e) => { log::warn!("Error deserializing changelog user default {e}"); false } } }) } pub fn mark_changelog_shown(changelog_version: &str, ctx: &mut AppContext) -> bool { ctx.private_user_preferences() .read_value(CHANGELOG_VERSIONS) .unwrap_or_default() .map_or(Ok(json!({})), |versions| { serde_json::from_str::(&versions) }) .is_ok_and(|mut versions| { log::info!( "Marking changelog {changelog_version} as shown in versions {versions:?}" ); versions[&changelog_version] = Value::Bool(true); let _ = ctx.private_user_preferences().write_value( CHANGELOG_VERSIONS, serde_json::to_string(&versions).expect("changelog versions should serialize"), ); true }) } pub fn theme_for_theme_kind(theme_kind: &ThemeKind, ctx: &mut AppContext) -> GalaxyTheme { match theme_kind { ThemeKind::InMemory(in_memory_theme) => in_memory_theme.theme(), _ => GalaxyConfig::as_ref(ctx).theme_config().theme(theme_kind), } } } /// Terminal Spacing settings. BlockPadding and inline_separator_height values are measured in grid /// cells, not pixels. #[derive(Clone, Debug, PartialEq)] pub struct TerminalSpacing { pub block_padding: BlockPadding, pub prompt_to_editor_padding: f32, pub editor_bottom_padding: f32, pub block_borders_enabled: bool, pub overflow_offset: f32, pub subshell_separator_height: f32, } impl TerminalSpacing { pub fn normal(line_height_ratio: f32, ctx: &AppContext) -> Self { Self { block_padding: BlockPadding { padding_top: 1.1 * (DEFAULT_UI_LINE_HEIGHT_RATIO / line_height_ratio).min(1.0), command_padding_top: 0.19 * (DEFAULT_UI_LINE_HEIGHT_RATIO / line_height_ratio).min(1.0), middle: 0.5 * (DEFAULT_UI_LINE_HEIGHT_RATIO / line_height_ratio).min(1.0), bottom: 1. * (DEFAULT_UI_LINE_HEIGHT_RATIO / line_height_ratio).min(1.0), }, prompt_to_editor_padding: 10., editor_bottom_padding: 20., block_borders_enabled: *BlockListSettings::as_ref(ctx).show_block_dividers.value() || !FeatureFlag::MinimalistUI.is_enabled(), overflow_offset: 12., // Subshell separators are actually hidden in normal spacing b/c they are meant to be // shown inside the block padding instead. subshell_separator_height: 0., } } pub fn compact(line_height_ratio: f32, ctx: &AppContext) -> Self { Self { block_padding: BlockPadding { padding_top: 0.3 * (DEFAULT_UI_LINE_HEIGHT_RATIO / line_height_ratio).min(1.0), command_padding_top: 0., middle: 0., bottom: 0.2 * (DEFAULT_UI_LINE_HEIGHT_RATIO / line_height_ratio).min(1.0), }, prompt_to_editor_padding: 0., editor_bottom_padding: 4., block_borders_enabled: *BlockListSettings::as_ref(ctx).show_block_dividers.value() || !FeatureFlag::MinimalistUI.is_enabled(), overflow_offset: 6., subshell_separator_height: 1.1, } } } /// The argument type for set_extra_meta_keys action. #[derive(Clone)] pub struct ExtraMetaKeysChangedArg { pub keys: ExtraMetaKeys, } /// Returns the path to the user preferences file. pub fn user_preferences_file_path() -> PathBuf { galaxy_core::paths::config_local_dir().join("user_preferences.json") } /// Returns the path to the TOML settings file. pub fn user_preferences_toml_file_path() -> PathBuf { galaxy_core::paths::config_local_dir().join("settings.toml") }