use std::cell::RefCell; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::Duration; use ::settings::{Setting, ToggleableSetting}; use galaxy_core::channel::ChannelState; use galaxy_core::context_flag::ContextFlag; use galaxy_core::semantic_selection::{ SemanticSelection, SemanticSelectionChangedEvent, SmartSelectEnabled, }; use galaxyui::elements::{ Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Dismiss, DispatchEventResult, Element, Empty, EventHandler, Fill, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseState, MouseStateHandle, ParentElement, Radius, Shrinkable, Text, }; use galaxyui::keymap::{ContextPredicate, FixedBinding, Keystroke}; use galaxyui::platform::{Cursor, GraphicsBackend}; use galaxyui::rendering::GPUPowerPreference; use galaxyui::ui_components::button::ButtonVariant; use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles}; use galaxyui::ui_components::switch::SwitchStateHandle; use galaxyui::{ Action, AppContext, DisplayIdx, Entity, EventContext, ModelHandle, SingletonEntity, Tracked, TypedActionView, View, ViewContext, ViewHandle, WindowId, }; use lazy_static::lazy_static; use strum::IntoEnumIterator; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use { crate::settings::ForceX11, crate::settings::LinuxAppConfiguration, galaxyui::platform::linux::windowing_system_is_customizable, }; use super::keybindings::KeyBindingModifyingState; #[cfg(feature = "local_tty")] use super::settings_page::render_sub_sub_header; use super::settings_page::{ add_setting, build_reset_button, build_toggle_element, render_body_item, render_body_item_label, render_dropdown_item, render_dropdown_item_label, render_local_only_icon, AdditionalInfo, Category, LocalOnlyIconState, MatchData, PageType, SettingsPageMeta, SettingsPageViewHandle, SettingsWidget, ToggleState, CONTENT_FONT_SIZE, HEADER_PADDING, TOGGLE_BUTTON_RIGHT_PADDING, }; use super::{ features, flags, render_beta_chip, DisplayCount, SettingsAction, SettingsSection, ToggleSettingActionPair, }; use crate::appearance::Appearance; use crate::default_terminal::DefaultTerminal; use crate::editor::{ EditorView, Event as EditorEvent, SingleLineEditorOptions, TextOptions, ACCEPT_AUTOSUGGESTION_KEYBINDING_NAME, }; use crate::features::FeatureFlag; use crate::gpu_state::{GPUState, GPUStateEvent}; use crate::root_view::QuakeModePinPosition; use crate::search::command_search::settings::{ CommandSearchSettings, ShowGlobalWorkflowsInUniversalSearch, }; use crate::server::telemetry::TelemetryEvent; use crate::settings::ai::AISettings; use crate::settings::native_preference::{NativePreferenceSettings, UserNativePreference}; use crate::settings::{ AISettingsChangedEvent, AliasExpansionEnabled, AliasExpansionSettings, AppEditorSettings, AtContextMenuInTerminalMode, AutocompleteSymbols, AutosuggestionKeybindingHint, ChangelogSettings, CloudPreferencesSettings, CodeEditorLineNumberMode, CodeEditorLineNumberModeSetting, CodeSettings, CommandCorrections, CompletionsOpenWhileTyping, CopyOnSelect, CtrlTabBehavior, DefaultSessionMode, EnableSlashCommandsInTerminal, ErrorUnderliningEnabled, ExtraMetaKeys, GPUSettings, GlobalHotkeyMode, InputSettings, InputSettingsChangedEvent, LinuxSelectionClipboard, MiddleClickPasteEnabled, MouseScrollMultiplier, OutlineCodebaseSymbolsForAtContextMenu, PreferLowPowerGPU, PreferredGraphicsBackend, QuakeModeSettings, ScrollSettings, ScrollSettingsChangedEvent, SelectionSettings, ShowAutosuggestionIgnoreButton, ShowChangelogAfterUpdate, ShowTerminalInputMessageBar, SshSettings, SyntaxHighlighting, TabBehavior, UserNativeRedirectPreference, VimModeEnabled, VimStatusBar, VimUnnamedSystemClipboard, DEFAULT_QUAKE_MODE_SIZE_PERCENTAGES, QUAKE_WINDOW_AUTOHIDE_SUPPORTED, }; use crate::terminal::alt_screen_reporting::{ AltScreenReporting, FocusReportingEnabled, MouseReportingEnabled, ScrollReportingEnabled, }; use crate::terminal::general_settings::{ AutoOpenCodeReviewPaneOnFirstAgentChange, GeneralSettings, LinkTooltip, LoginItem, QuitOnLastWindowClosed, RestoreSession, ShowWarningBeforeQuitting, }; use crate::terminal::input::OPEN_COMPLETIONS_KEYBINDING_NAME; use crate::terminal::keys_settings::{ ActivationHotkeyEnabled, CtrlTabBehaviorSetting, KeysSettings, KeysSettingsChangedEvent, }; #[cfg(feature = "local_tty")] use crate::terminal::session_settings::StartupShellOverride; #[cfg(feature = "local_tty")] use crate::terminal::session_settings::WorkingDirectoryConfig; use crate::terminal::session_settings::{ Notifications, NotificationsMode, NotificationsSettings, SessionSettings, SessionSettingsChangedEvent, ShouldConfirmCloseSession, }; use crate::terminal::settings::{ AsyncFindEnabled, MaximumGridSize, Osc52ClipboardAccess, Osc52ClipboardAccessSetting, ShowTerminalZeroStateBlock, TerminalSettings, TerminalSettingsChangedEvent, UseAudibleBell, }; use crate::terminal::{BlockListSettings, PreserveInputFocusOnBlockSelection, SnackbarEnabled}; use crate::undo_close::UndoCloseSettings; use crate::user_config::{GalaxyConfig, GalaxyConfigUpdateEvent}; use crate::util::bindings::{ keybinding_name_to_display_string, reset_keybinding_to_default, set_custom_keybinding, }; use crate::view_components::{Dropdown, DropdownItem, FilterableDropdown}; use crate::workspace::tab_settings::{NewTabPlacement, TabSettings, TabSettingsChangedEvent}; use crate::workspace::WorkspaceAction; use crate::{report_if_error, send_telemetry_from_ctx, themes, GlobalResourceHandles}; cfg_if::cfg_if! { if #[cfg(target_os = "macos")] { static EXTRA_META_KEYS_LEFT_TEXT: &str = "Left Option key is Meta"; static EXTRA_META_KEYS_RIGHT_TEXT: &str = "Right Option key is Meta"; } else { static EXTRA_META_KEYS_LEFT_TEXT: &str = "Left Alt key is Meta"; static EXTRA_META_KEYS_RIGHT_TEXT: &str = "Right Alt key is Meta"; } } pub fn init_actions_from_parent_view( app: &mut AppContext, context: &ContextPredicate, builder: fn(SettingsAction) -> T, ) { use galaxyui::keymap::macros::*; // Add all of the toggle settings from the Features Page that you want to show up on the Command Palette here. let mut toggle_binding_pairs = vec![ ToggleSettingActionPair::new( "copy on select within the terminal", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCopyOnSelect, )), context, flags::COPY_ON_SELECT_CONTEXT_FLAG, ), ToggleSettingActionPair::new( "linux selection clipboard", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLinuxClipboardSelection, )), context, flags::LINUX_SELECTION_CLIPBOARD_FLAG, ) .is_supported_on_current_platform( SelectionSettings::as_ref(app) .linux_selection_clipboard .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( "autocomplete quotes, parentheses, and brackets", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAutocompleteSymbols, )), context, flags::AUTOCOMPLETE_SYMBOLS_CONTEXT_FLAG, ) .is_supported_on_current_platform( AppEditorSettings::as_ref(app) .autocomplete_symbols .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( "restore windows, tabs, and panes on startup", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleRestoreSession, )), context, flags::RESTORE_SESSION_CONTEXT_FLAG, ), ToggleSettingActionPair::new( EXTRA_META_KEYS_LEFT_TEXT, builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLeftMetaKey, )), context, flags::EXTRA_META_KEYS_LEFT_CONTEXT_FLAG, ) .is_supported_on_current_platform( KeysSettings::as_ref(app) .extra_meta_keys .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( EXTRA_META_KEYS_RIGHT_TEXT, builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleRightMetaKey, )), context, flags::EXTRA_META_KEYS_RIGHT_CONTEXT_FLAG, ) .is_supported_on_current_platform( KeysSettings::as_ref(app) .extra_meta_keys .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( "scroll reporting", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleScrollReporting, )), context, flags::SCROLL_REPORTING_CONTEXT_FLAG, ) .is_supported_on_current_platform( AltScreenReporting::as_ref(app) .scroll_reporting_enabled .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( "completions while typing", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCompletionsOpenWhileTyping, )), context, flags::COMPLETIONS_OPEN_WHILE_TYPING_CONTEXT_FLAG, ) .is_supported_on_current_platform( InputSettings::as_ref(app) .completions_open_while_typing .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( "command corrections", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCommandCorrections, )), context, flags::COMMAND_CORRECTIONS_CONTEXT_FLAG, ) .is_supported_on_current_platform( InputSettings::as_ref(app) .command_corrections .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( "error underlining", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleErrorUnderlining, )), context, flags::ERROR_UNDERLINING_FLAG, ) .is_supported_on_current_platform( InputSettings::as_ref(app) .error_underlining .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( "syntax highlighting", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSyntaxHighlighting, )), context, flags::SYNTAX_HIGHLIGHTING_FLAG, ) .is_supported_on_current_platform( InputSettings::as_ref(app) .syntax_highlighting .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( "audible terminal bell", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleUseAudibleBell, )), context, flags::USE_AUDIBLE_BELL_CONTEXT_FLAG, ) .is_supported_on_current_platform( TerminalSettings::as_ref(app) .use_audible_bell .is_supported_on_current_platform(), ), ToggleSettingActionPair::new( "autosuggestions", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAutosuggestions, )), context, flags::AUTOSUGGESTIONS_ENABLED_FLAG, ), ToggleSettingActionPair::new( "autosuggestion keybinding hint", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAutosuggestionKeybindingHint, )), context, flags::AUTOSUGGESTION_KEYBINDING_HINT_FLAG, ), ToggleSettingActionPair::new( "autosuggestion ignore button", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowAutosuggestionIgnoreButton, )), context, flags::SHOW_AUTOSUGGESTION_IGNORE_BUTTON_FLAG, ) .with_enabled(|| FeatureFlag::AllowIgnoringInputSuggestions.is_enabled()), ]; toggle_binding_pairs.push(ToggleSettingActionPair::new( "reuse existing SSH ControlMaster in the Galaxy SSH wrapper", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSshReuseControlMaster, )), context, flags::SSH_REUSE_CONTROL_MASTER_CONTEXT_FLAG, )); toggle_binding_pairs.push(ToggleSettingActionPair::new( "show tooltip on click on links", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLinkTooltip, )), context, flags::LINK_TOOLTIP_CONTEXT_FLAG, )); toggle_binding_pairs.push( ToggleSettingActionPair::new( "long-running command notifications", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleLongRunningNotifications, )), &(context.to_owned() & id!(flags::NOTIFICATIONS_CONTEXT_FLAG)), flags::LONG_RUNNING_NOTIFICATIONS_FLAG, ) .is_supported_on_current_platform( SessionSettings::as_ref(app) .notifications .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "agent task completion notifications", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAgentTaskCompletedNotifications, )), &(context.to_owned() & id!(flags::NOTIFICATIONS_CONTEXT_FLAG)), flags::AGENT_TASK_COMPLETED_NOTIFICATIONS_FLAG, ) .is_supported_on_current_platform( SessionSettings::as_ref(app) .notifications .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "needs-attention notifications", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleNeedsAttentionNotifications, )), &(context.to_owned() & id!(flags::NOTIFICATIONS_CONTEXT_FLAG)), flags::NEEDS_ATTENTION_NOTIFICATIONS_FLAG, ) .is_supported_on_current_platform( SessionSettings::as_ref(app) .notifications .is_supported_on_current_platform(), ), ); #[cfg(target_os = "macos")] toggle_binding_pairs.push( ToggleSettingActionPair::new( "notification sounds", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleNotificationSound, )), &(context.to_owned() & id!(flags::NOTIFICATIONS_CONTEXT_FLAG)), flags::NOTIFICATION_SOUND_FLAG, ) .is_supported_on_current_platform( SessionSettings::as_ref(app) .notifications .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "in-app agent notifications", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAgentInAppNotifications, )), context, flags::AGENT_IN_APP_NOTIFICATIONS_FLAG, ) .with_enabled(|| FeatureFlag::HOANotifications.is_enabled()), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "quit warning modal", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowWarningBeforeQuitting, )), context, flags::QUIT_WARNING_MODAL, ) .is_supported_on_current_platform( GeneralSettings::as_ref(app) .show_warning_before_quitting .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "mouse reporting", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleMouseReporting, )), context, flags::MOUSE_REPORTING_CONTEXT_FLAG, ) .is_supported_on_current_platform( AltScreenReporting::as_ref(app) .mouse_reporting_enabled .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "alias expansion", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAliasExpansion, )), context, flags::ALIAS_EXPANSION_FLAG, ) .is_supported_on_current_platform( AliasExpansionSettings::as_ref(app) .alias_expansion_enabled .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "middle-click paste", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleMiddleClickPaste, )), context, flags::MIDDLE_CLICK_PASTE_FLAG, ) .is_supported_on_current_platform( SelectionSettings::as_ref(app) .middle_click_paste_enabled .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "code as default editor", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleCodeAsDefaultEditor, )), context, flags::CODE_AS_DEFAULT_EDITOR, ) .is_supported_on_current_platform( CodeSettings::as_ref(app) .code_as_default_editor .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "input hint text", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowInputHintText, )), context, flags::SHOW_INPUT_HINT_TEXT_CONTEXT_FLAG, ) .is_supported_on_current_platform( InputSettings::as_ref(app) .show_hint_text .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "editing commands with Vim keybindings", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleVimMode, )), context, flags::VIM_MODE_CONTEXT_FLAG, ) .is_supported_on_current_platform( AppEditorSettings::as_ref(app) .vim_mode .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "Vim unnamed register as system clipboard", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleVimUnnamedSystemClipboard, )), &(context.to_owned() & id!(flags::VIM_MODE_CONTEXT_FLAG)), flags::VIM_UNNAMED_SYSTEM_CLIPBOARD, ) .is_supported_on_current_platform( AppEditorSettings::as_ref(app) .vim_unnamed_system_clipboard .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "Vim status bar", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleVimStatusBar, )), &(context.to_owned() & id!(flags::VIM_MODE_CONTEXT_FLAG)), flags::VIM_SHOW_STATUS_BAR, ) .is_supported_on_current_platform( AppEditorSettings::as_ref(app) .vim_status_bar .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "focus reporting", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleFocusReporting, )), context, flags::FOCUS_REPORTING_CONTEXT_FLAG, ) .is_supported_on_current_platform( AltScreenReporting::as_ref(app) .focus_reporting_enabled .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push(ToggleSettingActionPair::new( "smart select", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSmartSelection, )), context, flags::SMART_SELECT_FLAG, )); if FeatureFlag::AgentView.is_enabled() && AISettings::as_ref(app).is_any_ai_enabled(app) { toggle_binding_pairs.push( ToggleSettingActionPair::new( "help block in new sessions", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowTerminalZeroStateBlock, )), context, flags::SHOW_TERMINAL_ZERO_STATE_BLOCK_FLAG, ) .is_supported_on_current_platform( TerminalSettings::as_ref(app) .show_terminal_zero_state_block .is_supported_on_current_platform(), ), ); } toggle_binding_pairs.push( ToggleSettingActionPair::new( "terminal input message line", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleShowTerminalInputMessageLine, )), context, flags::SHOW_TERMINAL_INPUT_MESSAGE_LINE_FLAG, ) .with_enabled(|| FeatureFlag::AgentView.is_enabled()), ); toggle_binding_pairs.push( ToggleSettingActionPair::new( "'@' context menu in terminal mode", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleAtContextMenuInTerminalMode, )), context, flags::AT_CONTEXT_MENU_IN_TERMINAL_FLAG, ) .is_supported_on_current_platform( InputSettings::as_ref(app) .at_context_menu_in_terminal_mode .is_supported_on_current_platform(), ), ); toggle_binding_pairs.push(ToggleSettingActionPair::new( "preserve input focus on block selection", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::TogglePreserveInputFocusOnBlockSelection, )), context, flags::PRESERVE_INPUT_FOCUS_ON_BLOCK_SELECTION_FLAG, )); if FeatureFlag::AgentView.is_enabled() && AISettings::as_ref(app).is_any_ai_enabled(app) { toggle_binding_pairs.push( ToggleSettingActionPair::new( "slash commands in terminal mode", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleSlashCommandsInTerminalMode, )), context, flags::SLASH_COMMANDS_IN_TERMINAL_FLAG, ) .is_supported_on_current_platform( InputSettings::as_ref(app) .enable_slash_commands_in_terminal .is_supported_on_current_platform(), ), ); } if FeatureFlag::AIContextMenuCode.is_enabled() { toggle_binding_pairs.push( ToggleSettingActionPair::new( "codebase symbols in the '@' context menu", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleOutlineCodebaseSymbolsForAtContextMenu, )), context, flags::OUTLINE_CODEBASE_SYMBOLS_FOR_AT_CONTEXT_MENU_FLAG, ) .is_supported_on_current_platform( InputSettings::as_ref(app) .outline_codebase_symbols_for_at_context_menu .is_supported_on_current_platform(), ), ); } toggle_binding_pairs.push( ToggleSettingActionPair::new( "global workflows in Command Search", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleGlobalWorkflowsInUniversalSearch, )), context, flags::GLOBAL_WORKFLOWS_IN_COMMAND_SEARCH_FLAG, ) .is_supported_on_current_platform( CommandSearchSettings::as_ref(app) .show_global_workflows_in_universal_search .is_supported_on_current_platform(), ), ); if GPUState::as_ref(app).is_low_power_gpu_available() { toggle_binding_pairs.push( ToggleSettingActionPair::new( "integrated GPU rendering (low power)", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::TogglePreferLowPowerGPU, )), context, flags::PREFER_LOW_POWER_GPU_FLAG, ) .is_supported_on_current_platform( GPUSettings::as_ref(app) .prefer_low_power_gpu .is_supported_on_current_platform(), ), ); } #[cfg(any(target_os = "linux", target_os = "freebsd"))] { if windowing_system_is_customizable(app) { toggle_binding_pairs.push( ToggleSettingActionPair::new( "Wayland for window management", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::ToggleForceX11, )), context, flags::ALLOW_NATIVE_WAYLAND, ) .is_supported_on_current_platform(cfg!(any( target_os = "linux", target_os = "freebsd" ))), ); } } ToggleSettingActionPair::add_toggle_setting_action_pairs_as_bindings(toggle_binding_pairs, app); app.register_fixed_bindings([FixedBinding::empty( "Configure Global Hotkey", WorkspaceAction::ScrollToSettingsWidget { page: SettingsSection::Features, widget_id: GlobalHotkeyWidget::static_widget_id(), }, id!("Workspace"), )]); if DefaultTerminal::can_warp_become_default() { app.register_fixed_bindings([FixedBinding::empty( "Make Galaxy the default terminal", builder(SettingsAction::FeaturesPageToggle( FeaturesPageAction::MakeWarpDefaultTerminal, )), context.to_owned() & !id!(flags::WARP_IS_DEFAULT_TERMINAL), )]); } } #[derive(Clone, Debug, PartialEq)] pub enum FeaturesPageAction { ToggleCopyOnSelect, ToggleAsyncFind, ToggleNotifications, ToggleRestoreSession, ToggleAutocompleteSymbols, ToggleLinuxClipboardSelection, ToggleOpenLinksInDesktopApp, ToggleSshReuseControlMaster, ToggleSnackbar, ToggleLinkTooltip, ToggleCompletionsOpenWhileTyping, ToggleCommandCorrections, ToggleErrorUnderlining, ToggleSyntaxHighlighting, ToggleAliasExpansion, ToggleMiddleClickPaste, ToggleCodeAsDefaultEditor, ToggleShowInputHintText, ToggleUseAudibleBell, ToggleShowTerminalZeroStateBlock, TogglePreferLowPowerGPU, ToggleVimMode, ToggleVimUnnamedSystemClipboard, ToggleVimStatusBar, ActivationKeybindEditorClicked, ActivationKeybindEditorCancel, ActivationKeybindEditorSave, ActivationKeystrokeDefined(Keystroke), QuakeKeystrokeDefined(Keystroke), QuakeKeybindEditorClicked, QuakeKeybindEditorCancel, QuakeKeybindEditorSave, QuakeEditorSetPinPosition(QuakeModePinPosition), QuakeEditorSetPinScreen(Option), QuakeEditorSetWidthPercentage, QuakeEditorSetHeightPercentage, QuakeEditorResetWidthHeight, QuakeEditorTogglePinWindow, OpenUrl(String), SetExtraMetaKeys(ExtraMetaKeys), ToggleLeftMetaKey, ToggleRightMetaKey, ToggleMouseReporting, ToggleGlobalWorkflowsInUniversalSearch, ToggleScrollReporting, ToggleFocusReporting, ToggleLongRunningNotifications, SetLongRunningNotificationThreshold, /// Legacy. To be combined with `ToggleNeedsAttentionNotifications` when desktop notifs are unflagged. TogglePasswordPromptNotifications, ToggleAgentTaskCompletedNotifications, ToggleNeedsAttentionNotifications, ToggleNotificationSound, SetNotificationToastDuration, ToggleShowWarningBeforeQuitting, ToggleLoginItem, ToggleQuitOnLastWindowClosed, ToggleSmartSelection, SetWordCharAllowlist, ResetWordCharAllowlist, SetGlobalHotkeyMode(GlobalHotkeyMode), SetTabBehavior(TabBehavior), SetCtrlTabBehavior(CtrlTabBehavior), SetPreferredGraphicsBackend(Option), SetNewTabPlacement(NewTabPlacement), SetOsc52ClipboardAccess(Osc52ClipboardAccess), SetDefaultSessionMode(DefaultSessionMode), SetDefaultTabConfig(String), SearchForKeybinding(String), ToggleAutosuggestions, ToggleConfirmCloseSession, ToggleShowChangelogAfterUpdate, #[cfg(any(target_os = "linux", target_os = "freebsd"))] ToggleForceX11, ToggleAutosuggestionKeybindingHint, ToggleShowAutosuggestionIgnoreButton, ToggleAtContextMenuInTerminalMode, ToggleSlashCommandsInTerminalMode, ToggleOutlineCodebaseSymbolsForAtContextMenu, ToggleAutoOpenCodeReviewPane, ToggleShowTerminalInputMessageLine, TogglePreserveInputFocusOnBlockSelection, ToggleAgentInAppNotifications, MakeWarpDefaultTerminal, SetCodeEditorLineNumberMode(CodeEditorLineNumberMode), } lazy_static! { static ref TAB_KEYSTROKE: Keystroke = Keystroke { key: "tab".to_string(), ..Default::default() }; static ref CTRL_SPACE_KEYSTROKE: Keystroke = Keystroke { key: " ".into(), ctrl: true, ..Default::default() }; } /// Used for styling notification settings const NOTIFICATION_CHECKBOX_MARGIN_RIGHT: f32 = 5.; const NOTIFICATION_EDITOR_MARGIN: f32 = 5.; const NOTIFICATIONS_DOCS_URL: &str = "https://docs.warp.dev/terminal/more-features/notifications"; /// WARNING: this constant was computed manually by determining the pixel width /// of the quake mode dropdowns based on the number of expanded items in the flex row. /// This should be adjusted if the flex row is changed in any way! const QUAKE_DROPDOWN_WIDTH: f32 = 130.; const MAX_BLOCK_SIZE_INPUT_BOX_WIDTH: f32 = 80.; const MIN_MAX_GRID_SIZE: usize = 100; const MOUSE_SCROLL_EDITOR_WIDTH: f32 = 40.; const MIN_MOUSE_SCROLL_MULTIPLIER: f32 = 1.0; const MAX_MOUSE_SCROLL_MULTIPLIER: f32 = 20.0; const TAB_KEYSTROKE_STR: &str = "Tab"; /// Function to get maximum value for max grid size: 10 million for dogfood/dev builds, /// 1 million for release builds. /// /// TODO: address the use of f32 in blocklist rendering code that leads to precision errors /// when the number of lines gets too high. fn max_max_grid_size() -> usize { if ChannelState::enable_debug_features() { 10_000_000 } else { 1_000_000 } } fn block_maximum_rows_description() -> String { let max_rows = if ChannelState::enable_debug_features() { "10 million" } else { "1 million" }; format!( "Setting the limit above 100k lines may impact performance. Maximum rows supported is {max_rows}." ) } fn to_string(b: bool) -> String { format!("{b}") } impl FeaturesPageAction { fn telemetry_event(&self, ctx: &AppContext) -> TelemetryEvent { let workflow_settings = CommandSearchSettings::as_ref(ctx); let reporting_settings = AltScreenReporting::as_ref(ctx); let selection_settings = SelectionSettings::as_ref(ctx); let input_settings = InputSettings::as_ref(ctx); let keys_settings = KeysSettings::as_ref(ctx); match self { Self::ToggleCopyOnSelect => TelemetryEvent::FeaturesPageAction { action: "ToggleCopyOnSelect".to_string(), value: to_string(selection_settings.copy_on_select_enabled()), }, Self::ToggleOpenLinksInDesktopApp => TelemetryEvent::FeaturesPageAction { action: "ToggleOpenLinksInDesktopApp".to_string(), value: to_string(matches!( NativePreferenceSettings::as_ref(ctx) .user_native_redirect_preference .value(), UserNativePreference::Desktop )), }, Self::ToggleSnackbar => { let settings = BlockListSettings::as_ref(ctx); TelemetryEvent::FeaturesPageAction { action: "ToggleSnackbar".to_string(), value: to_string(*settings.snackbar_enabled), } } Self::ToggleGlobalWorkflowsInUniversalSearch => TelemetryEvent::FeaturesPageAction { action: "ToggleGlobalWorkflowsInUniversalSearch".to_string(), value: to_string(*workflow_settings.show_global_workflows_in_universal_search), }, Self::ToggleNotifications => TelemetryEvent::FeaturesPageAction { action: "ToggleNotifications".to_string(), value: to_string(matches!( SessionSettings::as_ref(ctx).notifications.mode, NotificationsMode::Enabled )), }, Self::ToggleRestoreSession => { TelemetryEvent::ToggleRestoreSession(*GeneralSettings::as_ref(ctx).restore_session) } Self::ToggleAutocompleteSymbols => TelemetryEvent::FeaturesPageAction { action: "ToggleAutocompleteSymbols".to_string(), value: to_string(*AppEditorSettings::as_ref(ctx).autocomplete_symbols), }, Self::ToggleSshReuseControlMaster => TelemetryEvent::FeaturesPageAction { action: "ToggleSshReuseControlMaster".to_string(), value: to_string( *SshSettings::as_ref(ctx) .reuse_existing_control_master .value(), ), }, Self::SetGlobalHotkeyMode(mode) => TelemetryEvent::FeaturesPageAction { action: "SetGlobalHotkeyMode".to_string(), value: format!("{mode:?}"), }, Self::ToggleLinkTooltip => TelemetryEvent::FeaturesPageAction { action: "ToggleLinkTooltip".to_string(), value: to_string(*GeneralSettings::as_ref(ctx).link_tooltip), }, Self::ToggleCompletionsOpenWhileTyping => TelemetryEvent::FeaturesPageAction { action: "ToggleCompletionsOpenWhileTyping".to_string(), value: to_string(*input_settings.completions_open_while_typing.value()), }, Self::ToggleCommandCorrections => TelemetryEvent::FeaturesPageAction { action: "ToggleCommandCorrections".to_string(), value: to_string(*input_settings.command_corrections.value()), }, Self::ToggleErrorUnderlining => TelemetryEvent::FeaturesPageAction { action: "ToggleErrorUnderlining".to_string(), value: to_string(*input_settings.error_underlining.value()), }, Self::ToggleSyntaxHighlighting => TelemetryEvent::FeaturesPageAction { action: "ToggleSyntaxHighlighting".to_string(), value: to_string(*input_settings.syntax_highlighting.value()), }, Self::ToggleAliasExpansion => { let settings = AliasExpansionSettings::as_ref(ctx); TelemetryEvent::FeaturesPageAction { action: "ToggleAliasExpansion".to_string(), value: to_string(*settings.alias_expansion_enabled), } } Self::ToggleMiddleClickPaste => { let settings = SelectionSettings::as_ref(ctx); TelemetryEvent::FeaturesPageAction { action: "ToggleMiddleClickPaste".to_string(), value: to_string(*settings.middle_click_paste_enabled), } } Self::ToggleCodeAsDefaultEditor => TelemetryEvent::FeaturesPageAction { action: "ToggleCodeAsDefaultEditor".to_string(), value: to_string(*CodeSettings::as_ref(ctx).code_as_default_editor.value()), }, Self::ToggleShowInputHintText => { let settings = InputSettings::as_ref(ctx); TelemetryEvent::FeaturesPageAction { action: "ToggleShowInputHintText".to_string(), value: to_string(*settings.show_hint_text), } } Self::ToggleShowTerminalInputMessageLine => { let settings = InputSettings::as_ref(ctx); TelemetryEvent::FeaturesPageAction { action: "ToggleShowTerminalInputMessageLine".to_string(), value: to_string(settings.is_terminal_input_message_bar_enabled()), } } Self::ActivationKeybindEditorClicked => TelemetryEvent::FeaturesPageAction { action: "ActivationKeybindEditorClicked".to_string(), value: String::new(), }, Self::ActivationKeybindEditorCancel => TelemetryEvent::FeaturesPageAction { action: "ActivationKeybindEditorCancel".to_string(), value: String::new(), }, Self::ActivationKeybindEditorSave => TelemetryEvent::FeaturesPageAction { action: "ActivationKeybindEditorSave".to_string(), value: String::new(), }, Self::ActivationKeystrokeDefined(keystroke) => TelemetryEvent::FeaturesPageAction { action: "ActivationKeystrokeDefined".to_string(), value: keystroke.normalized(), }, Self::QuakeKeybindEditorClicked => TelemetryEvent::FeaturesPageAction { action: "QuakeKeybindEditorClicked".to_string(), value: String::new(), }, Self::QuakeKeystrokeDefined(keystroke) => TelemetryEvent::FeaturesPageAction { action: "QuakeKeystrokeDefined".to_string(), value: keystroke.normalized(), }, Self::QuakeKeybindEditorCancel => TelemetryEvent::FeaturesPageAction { action: "QuakeKeybindEditorCancel".to_string(), value: String::new(), }, Self::QuakeKeybindEditorSave => TelemetryEvent::FeaturesPageAction { action: "QuakeKeybindEditorSave".to_string(), value: String::new(), }, Self::OpenUrl(url) => TelemetryEvent::FeaturesPageAction { action: "OpenUrl".to_string(), value: url.clone(), }, Self::SetExtraMetaKeys(extra_metas) => TelemetryEvent::FeaturesPageAction { action: "SetExtraMetaKeys".to_string(), value: format!("{extra_metas:?}"), }, Self::ToggleLeftMetaKey => TelemetryEvent::FeaturesPageAction { action: "ToggleLeftMetaKey".to_string(), value: to_string(keys_settings.extra_meta_keys.left_alt), }, Self::ToggleRightMetaKey => TelemetryEvent::FeaturesPageAction { action: "ToggleRightMetaKey".to_string(), value: to_string(keys_settings.extra_meta_keys.right_alt), }, Self::ToggleMouseReporting => TelemetryEvent::FeaturesPageAction { action: "ToggleMouseReporting".to_string(), value: to_string(*reporting_settings.mouse_reporting_enabled), }, Self::ToggleScrollReporting => TelemetryEvent::FeaturesPageAction { action: "ToggleScrollReporting".to_string(), value: to_string(*reporting_settings.scroll_reporting_enabled), }, Self::ToggleFocusReporting => TelemetryEvent::FeaturesPageAction { action: "ToggleFocusReporting".to_string(), value: to_string(*reporting_settings.focus_reporting_enabled), }, Self::QuakeEditorSetPinPosition(position) => TelemetryEvent::FeaturesPageAction { action: "QuakeEditorSetPinPosition".to_string(), value: format!("{position:?}"), }, Self::QuakeEditorSetPinScreen(screen) => TelemetryEvent::FeaturesPageAction { action: "QuakeEditorSetPinScreen".to_string(), value: screen .map(|idx| format!("{idx}")) .unwrap_or_else(|| "Active Screen".into()), }, Self::QuakeEditorResetWidthHeight => TelemetryEvent::FeaturesPageAction { action: "QuakeEditorResetWidthHeight".to_string(), value: String::new(), }, Self::QuakeEditorSetWidthPercentage | Self::QuakeEditorSetHeightPercentage => { TelemetryEvent::FeaturesPageAction { action: "QuakeEditorSetSizePercentage".to_string(), value: format!( "width: {:?}, height: {:?}", KeysSettings::handle(ctx) .as_ref(ctx) .quake_mode_settings .width_percentage(), KeysSettings::handle(ctx) .as_ref(ctx) .quake_mode_settings .height_percentage() ), } } Self::QuakeEditorTogglePinWindow => TelemetryEvent::FeaturesPageAction { action: "QuakeEditorTogglePinWindow".to_string(), value: to_string( KeysSettings::as_ref(ctx) .quake_mode_settings .hide_window_when_unfocused, ), }, Self::ToggleLongRunningNotifications => TelemetryEvent::FeaturesPageAction { action: "ToggleLongRunningNotifications".to_string(), value: to_string( SessionSettings::as_ref(ctx) .notifications .is_long_running_enabled, ), }, Self::SetLongRunningNotificationThreshold => TelemetryEvent::FeaturesPageAction { action: "SetLongRunningNotificationThreshold".to_string(), value: format!( "{}s", SessionSettings::handle(ctx) .as_ref(ctx) .notifications .long_running_threshold .as_secs_f32() ), }, Self::TogglePasswordPromptNotifications => TelemetryEvent::FeaturesPageAction { action: "TogglePasswordPromptNotifications".to_string(), value: to_string( SessionSettings::as_ref(ctx) .notifications .is_password_prompt_enabled, ), }, Self::ToggleAgentTaskCompletedNotifications => TelemetryEvent::FeaturesPageAction { action: "ToggleAgentTaskCompletedNotifications".to_string(), value: to_string( SessionSettings::as_ref(ctx) .notifications .is_agent_task_completed_enabled, ), }, Self::ToggleNeedsAttentionNotifications => TelemetryEvent::FeaturesPageAction { action: "ToggleNeedsAttentionNotifications".to_string(), value: to_string( SessionSettings::as_ref(ctx) .notifications .is_needs_attention_enabled, ), }, Self::ToggleNotificationSound => TelemetryEvent::FeaturesPageAction { action: "ToggleNotificationSound".to_string(), value: to_string( SessionSettings::as_ref(ctx) .notifications .play_notification_sound, ), }, Self::ToggleShowWarningBeforeQuitting => TelemetryEvent::FeaturesPageAction { action: "ToggleShowWarningBeforeQuitting".to_string(), value: to_string( *GeneralSettings::as_ref(ctx) .show_warning_before_quitting .value(), ), }, Self::ToggleLoginItem => TelemetryEvent::FeaturesPageAction { action: "ToggleLoginItem".to_string(), value: to_string(*GeneralSettings::as_ref(ctx).add_app_as_login_item.value()), }, Self::ToggleQuitOnLastWindowClosed => TelemetryEvent::FeaturesPageAction { action: "ToggleQuitOnLastWindowClosed".to_string(), value: to_string( *GeneralSettings::as_ref(ctx) .quit_on_last_window_closed .value(), ), }, Self::ToggleSmartSelection => TelemetryEvent::FeaturesPageAction { action: "ToggleSmartSelection".to_string(), value: to_string(SemanticSelection::as_ref(ctx).smart_select_enabled()), }, Self::SetWordCharAllowlist => TelemetryEvent::FeaturesPageAction { action: "SetWordCharAllowlist".to_string(), value: SemanticSelection::as_ref(ctx).word_char_allowlist_string(), }, Self::ResetWordCharAllowlist => TelemetryEvent::FeaturesPageAction { action: "ResetWordCharAllowlist".to_string(), value: String::new(), }, Self::ToggleUseAudibleBell => { let terminal_settings = TerminalSettings::as_ref(ctx); TelemetryEvent::FeaturesPageAction { action: "ToggleUseAudibleBell".to_string(), value: to_string(*terminal_settings.use_audible_bell), } } Self::ToggleVimMode => TelemetryEvent::FeaturesPageAction { action: "ToggleVimMode".to_string(), value: to_string(*AppEditorSettings::as_ref(ctx).vim_mode.value()), }, Self::ToggleVimUnnamedSystemClipboard => TelemetryEvent::FeaturesPageAction { action: "ToggleVimUnnamedSystemClipboard".to_string(), value: to_string( *AppEditorSettings::as_ref(ctx) .vim_unnamed_system_clipboard .value(), ), }, Self::ToggleVimStatusBar => TelemetryEvent::FeaturesPageAction { action: "ToggleVimStatusBar".to_string(), value: to_string(*AppEditorSettings::as_ref(ctx).vim_status_bar.value()), }, Self::SetCodeEditorLineNumberMode(mode) => TelemetryEvent::FeaturesPageAction { action: "SetCodeEditorLineNumberMode".to_string(), value: format!("{mode:?}"), }, Self::SetTabBehavior(tab_behavior) => TelemetryEvent::FeaturesPageAction { action: "SetTabBehavior".to_string(), value: format!("{tab_behavior:?}"), }, Self::SetCtrlTabBehavior(ctrl_tab_behavior) => TelemetryEvent::FeaturesPageAction { action: "SetCtrlTabBehavior".to_string(), value: format!("{ctrl_tab_behavior:?}"), }, Self::SetNewTabPlacement(new_tab_placement) => TelemetryEvent::FeaturesPageAction { action: "SetNewTabPlacement".to_string(), value: format!("{new_tab_placement:?}"), }, Self::SetOsc52ClipboardAccess(access) => TelemetryEvent::FeaturesPageAction { action: "SetOsc52ClipboardAccess".to_string(), value: format!("{access:?}"), }, Self::SetDefaultSessionMode(mode) => TelemetryEvent::FeaturesPageAction { action: "SetDefaultSessionMode".to_string(), value: format!("{mode:?}"), }, Self::SetDefaultTabConfig(path) => TelemetryEvent::FeaturesPageAction { action: "SetDefaultTabConfig".to_string(), value: path.clone(), }, Self::SearchForKeybinding(page_name) => TelemetryEvent::FeaturesPageAction { action: "SearchForKeybinding".to_string(), value: page_name.clone(), }, Self::ToggleAutosuggestions => TelemetryEvent::FeaturesPageAction { action: "ToggleAutosuggestions".to_string(), value: to_string(*AppEditorSettings::as_ref(ctx).enable_autosuggestions), }, Self::ToggleAutosuggestionKeybindingHint => TelemetryEvent::FeaturesPageAction { action: "ToggleHideAutosuggestionKeybindingHint".to_string(), value: to_string( *AppEditorSettings::as_ref(ctx) .autosuggestion_keybinding_hint .value(), ), }, Self::ToggleShowAutosuggestionIgnoreButton => TelemetryEvent::FeaturesPageAction { action: "ToggleShowAutosuggestionIgnoreButton".to_string(), value: to_string( *AppEditorSettings::as_ref(ctx) .show_autosuggestion_ignore_button .value(), ), }, Self::TogglePreferLowPowerGPU => { let gpu_settings = GPUSettings::as_ref(ctx); TelemetryEvent::FeaturesPageAction { action: "TogglePreferLowPowerGPU".to_string(), value: to_string(*gpu_settings.prefer_low_power_gpu.value()), } } Self::SetPreferredGraphicsBackend(backend) => TelemetryEvent::FeaturesPageAction { action: "SetPreferredGraphicsBackend".to_string(), value: format!("{backend:?}"), }, Self::ToggleConfirmCloseSession => TelemetryEvent::FeaturesPageAction { action: "ToggleConfirmCloseSession".to_string(), value: to_string(*SessionSettings::as_ref(ctx).should_confirm_close_session), }, Self::ToggleShowTerminalZeroStateBlock => TelemetryEvent::FeaturesPageAction { action: "ToggleShowTerminalZeroStateBlock".to_string(), value: to_string(*TerminalSettings::as_ref(ctx).show_terminal_zero_state_block), }, Self::ToggleShowChangelogAfterUpdate => { let changelog_settings = ChangelogSettings::as_ref(ctx); TelemetryEvent::FeaturesPageAction { action: "ToggleShowChangelogAfterUpdate".to_string(), value: to_string(*changelog_settings.show_changelog_after_update), } } Self::ToggleLinuxClipboardSelection => { let selection_setting = SelectionSettings::as_ref(ctx).linux_selection_clipboard_enabled(); TelemetryEvent::FeaturesPageAction { action: "ToggleLinuxClipboardSelection".to_string(), value: to_string(selection_setting), } } #[cfg(any(target_os = "linux", target_os = "freebsd"))] Self::ToggleForceX11 => { let setting = *LinuxAppConfiguration::as_ref(ctx).force_x11.value(); TelemetryEvent::FeaturesPageAction { action: "ToggleForceX11".to_string(), value: to_string(setting), } } Self::ToggleAtContextMenuInTerminalMode => TelemetryEvent::FeaturesPageAction { action: "ToggleAtContextMenuInTerminalMode".to_string(), value: to_string( *InputSettings::as_ref(ctx) .at_context_menu_in_terminal_mode .value(), ), }, Self::ToggleSlashCommandsInTerminalMode => TelemetryEvent::FeaturesPageAction { action: "ToggleSlashCommandsInTerminalMode".to_string(), value: to_string( *InputSettings::as_ref(ctx) .enable_slash_commands_in_terminal .value(), ), }, Self::ToggleOutlineCodebaseSymbolsForAtContextMenu => { TelemetryEvent::FeaturesPageAction { action: "ToggleOutlineCodebaseSymbolsForAtContextMenu".to_string(), value: to_string( *InputSettings::as_ref(ctx) .outline_codebase_symbols_for_at_context_menu .value(), ), } } Self::MakeWarpDefaultTerminal => TelemetryEvent::FeaturesPageAction { action: "MakeWarpDefaultTerminal".to_string(), value: to_string(DefaultTerminal::as_ref(ctx).is_warp_default()), }, Self::ToggleAutoOpenCodeReviewPane => TelemetryEvent::FeaturesPageAction { action: "ToggleAutoOpenCodeReviewPane".to_string(), value: to_string( *GeneralSettings::as_ref(ctx).auto_open_code_review_pane_on_first_agent_change, ), }, Self::TogglePreserveInputFocusOnBlockSelection => { let settings = BlockListSettings::as_ref(ctx); TelemetryEvent::FeaturesPageAction { action: "TogglePreserveInputFocusOnBlockSelection".to_string(), value: to_string(*settings.preserve_input_focus_on_block_selection), } } Self::SetNotificationToastDuration => TelemetryEvent::FeaturesPageAction { action: "SetNotificationToastDuration".to_string(), value: format!( "{}s", *SessionSettings::as_ref(ctx).notification_toast_duration_secs ), }, Self::ToggleAgentInAppNotifications => TelemetryEvent::FeaturesPageAction { action: "ToggleAgentInAppNotifications".to_string(), value: to_string(*AISettings::as_ref(ctx).show_agent_notifications), }, Self::ToggleAsyncFind => TelemetryEvent::FeaturesPageAction { action: "ToggleAsyncFind".to_string(), value: to_string(*TerminalSettings::as_ref(ctx).async_find_enabled), }, } } } #[derive(Default)] struct MouseStateHandles { local_only_icon_tooltip_states: RefCell>, tab_behavior_local_only_icon: MouseStateHandle, activation_hotkey_keybinding_editor: MouseStateHandle, activation_hotkey_save: MouseStateHandle, activation_hotkey_cancel: MouseStateHandle, quake_mode_keybinding_editor: MouseStateHandle, quake_mode_save: MouseStateHandle, quake_mode_cancel: MouseStateHandle, quake_mode_width_height_reset: MouseStateHandle, quake_mode_pin_window_check: MouseStateHandle, long_running_notifications_checkbox: MouseStateHandle, agent_task_completed_notifications_checkbox: MouseStateHandle, agent_needs_attention_notifications_checkbox: MouseStateHandle, agent_in_app_notifications_switch: SwitchStateHandle, #[cfg(target_os = "macos")] notification_sound_checkbox: MouseStateHandle, change_keybinding: MouseStateHandle, global_hotkey_link: MouseStateHandle, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum KeybindingEditorState { /// The editor needs to be clicked first before you can record a keybinding Idle, /// The editor is active and currently recording a keybinding Recording, } pub struct FeaturesPageView { page: PageType, global_resource_handles: GlobalResourceHandles, button_mouse_states: MouseStateHandles, ctrl_tab_behavior_dropdown: ViewHandle>, code_editor_line_number_mode_dropdown: ViewHandle>, global_hotkey_dropdown: ViewHandle>, activation_hotkey_keybinding_editor_state: KeybindingEditorState, activation_hotkey_keybinding: KeyBindingModifyingState, quake_mode_keybinding_editor_state: KeybindingEditorState, quake_mode_keybinding: KeyBindingModifyingState, quake_mode_pin_position_dropdown: ViewHandle>, quake_mode_pin_screen_dropdown: ViewHandle>, quake_mode_width_editor: ViewHandle, quake_mode_height_editor: ViewHandle, notifications_long_running_threshold_editor: ViewHandle, notification_toast_duration_editor: ViewHandle, #[cfg(feature = "local_tty")] working_directory_view: ViewHandle, #[cfg(feature = "local_tty")] startup_shell_view: ViewHandle, undo_close_view: ViewHandle, max_block_size_input_editor: ViewHandle, valid_max_block_size: bool, mouse_scroll_input_editor: ViewHandle, valid_mouse_scroll_multiplier: bool, #[cfg(feature = "local_fs")] external_editor_view: ViewHandle, word_boundary_editor: ViewHandle, tab_behavior_dropdown: ViewHandle>, graphics_backend_dropdown: ViewHandle>, new_tab_placement_dropdown: ViewHandle>, osc52_clipboard_access_dropdown: ViewHandle>, default_session_mode_dropdown: ViewHandle>, tab_behavior: Tracked, completions_keystroke: Tracked, autosuggestions_keystroke: Tracked, window_id: WindowId, #[cfg(any(target_os = "linux", target_os = "freebsd"))] force_x11_changed: bool, gpu_power_preference_changed: bool, graphics_backend_preference_changed: bool, } pub enum FeaturesSettingsPageEvent { SearchForKeybinding(String), FocusModal, } impl Entity for FeaturesPageView { type Event = FeaturesSettingsPageEvent; } impl TypedActionView for FeaturesPageView { type Action = FeaturesPageAction; fn handle_action(&mut self, action: &FeaturesPageAction, ctx: &mut ViewContext) { use FeaturesPageAction::*; match action { SetCtrlTabBehavior(ctrl_tab_behavior) => { KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { report_if_error!(keys_settings .ctrl_tab_behavior .set_value(*ctrl_tab_behavior, ctx)); }); } ToggleCopyOnSelect => { SelectionSettings::handle(ctx).update(ctx, |selection_settings, ctx| { report_if_error!(selection_settings.copy_on_select.toggle_and_save_value(ctx)); }); } ToggleSnackbar => { BlockListSettings::handle(ctx).update(ctx, |blocklist_settings, ctx| { report_if_error!(blocklist_settings .snackbar_enabled .toggle_and_save_value(ctx)); }); } ToggleGlobalWorkflowsInUniversalSearch => { CommandSearchSettings::handle(ctx).update(ctx, |workflow_settings, ctx| { report_if_error!(workflow_settings .show_global_workflows_in_universal_search .toggle_and_save_value(ctx)); }); } ToggleCodeAsDefaultEditor => { CodeSettings::handle(ctx).update(ctx, |code_settings, ctx| { report_if_error!(code_settings .code_as_default_editor .toggle_and_save_value(ctx)); }) } ToggleOpenLinksInDesktopApp => { NativePreferenceSettings::handle(ctx).update( ctx, |native_preference_settings, ctx| { let new_value = match native_preference_settings .user_native_redirect_preference .value() { UserNativePreference::Desktop => UserNativePreference::Web, UserNativePreference::NotSelected | UserNativePreference::Web => { UserNativePreference::Desktop } }; report_if_error!(native_preference_settings .user_native_redirect_preference .set_value(new_value, ctx)); }, ); } ToggleNotifications => { ctx.dispatch_typed_action(&WorkspaceAction::ToggleNotifications); } ToggleRestoreSession => { GeneralSettings::handle(ctx).update(ctx, |general_settings, ctx| { report_if_error!(general_settings.restore_session.toggle_and_save_value(ctx)) }) } ToggleAutocompleteSymbols => { AppEditorSettings::handle(ctx).update(ctx, |editor_settings, ctx| { report_if_error!(editor_settings .autocomplete_symbols .toggle_and_save_value(ctx)); }) } ToggleSshReuseControlMaster => { SshSettings::handle(ctx).update(ctx, |ssh_settings, ctx| { report_if_error!(ssh_settings .reuse_existing_control_master .toggle_and_save_value(ctx)); }); } OpenUrl(url) => { ctx.open_url(url.as_str()); } SetGlobalHotkeyMode(mode) => self.set_global_hotkey_mode(mode, ctx), ActivationKeybindEditorClicked => { ctx.disable_key_bindings_dispatching(); self.activation_hotkey_keybinding_editor_state = KeybindingEditorState::Recording; ctx.notify(); } ActivationKeybindEditorCancel => { self.reset_activation_hotkey_keybinding_editor(); ctx.enable_key_bindings_dispatching(); ctx.notify(); } ActivationKeybindEditorSave => { ctx.enable_key_bindings_dispatching(); if !self.activation_hotkey_keybinding.is_dirty() { self.activation_hotkey_keybinding_editor_state = KeybindingEditorState::Idle; ctx.notify(); return; } self.disable_activation_hotkey_global_shortcut(ctx); self.activation_hotkey_keybinding .current_binding .clone_from(&self.activation_hotkey_keybinding.unsaved_binding); self.activation_hotkey_keybinding_editor_state = KeybindingEditorState::Idle; self.enable_activation_hotkey_global_shortcut(ctx); KeysSettings::handle(ctx).update(ctx, |key_settings, ctx| { key_settings.set_activation_hotkey_keybinding_and_write_to_user_defaults( self.activation_hotkey_keybinding.current_binding.clone(), ctx, ) }); ctx.notify(); } ActivationKeystrokeDefined(keystroke) => { self.activation_hotkey_keybinding.unsaved_binding = Some(keystroke.clone()); ctx.notify(); } QuakeKeybindEditorClicked => { ctx.disable_key_bindings_dispatching(); self.quake_mode_keybinding_editor_state = KeybindingEditorState::Recording; ctx.notify(); } QuakeKeystrokeDefined(keystroke) => { self.quake_mode_keybinding.unsaved_binding = Some(keystroke.clone()); ctx.notify(); } QuakeKeybindEditorCancel => { self.reset_quake_mode_keybinding_editor(); ctx.enable_key_bindings_dispatching(); ctx.notify(); } QuakeKeybindEditorSave => { ctx.enable_key_bindings_dispatching(); if !self.quake_mode_keybinding.is_dirty() { self.quake_mode_keybinding_editor_state = KeybindingEditorState::Idle; ctx.notify(); return; } self.disable_quake_mode_global_shortcut(ctx); self.quake_mode_keybinding .current_binding .clone_from(&self.quake_mode_keybinding.unsaved_binding); self.quake_mode_keybinding_editor_state = KeybindingEditorState::Idle; self.enable_quake_mode_global_shortcut(ctx); KeysSettings::handle(ctx).update(ctx, |key_settings, ctx| { key_settings.set_quake_mode_keybinding_and_write_to_user_defaults( self.quake_mode_keybinding.current_binding.clone(), ctx, ) }); ctx.notify(); } QuakeEditorSetPinPosition(pin_position) => { KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { keys_settings .set_quake_mode_pin_position_and_write_to_user_defaults(*pin_position, ctx) }); let new_size = KeysSettings::as_ref(ctx) .quake_mode_settings .size_percentages_for_pin_position(pin_position); self.quake_mode_width_editor.update(ctx, |editor, ctx| { editor.set_buffer_text(&format!("{}", new_size.width), ctx); }); self.quake_mode_height_editor.update(ctx, |editor, ctx| { editor.set_buffer_text(&format!("{}", new_size.height), ctx); }); ctx.notify(); } QuakeEditorSetPinScreen(pin_screen) => { KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { keys_settings .set_quake_mode_pin_screen_and_write_to_user_defaults(*pin_screen, ctx) }); ctx.notify(); } QuakeEditorSetHeightPercentage => self.set_height_ratio(ctx), QuakeEditorSetWidthPercentage => self.set_width_ratio(ctx), QuakeEditorResetWidthHeight => self.reset_size_percentage(ctx), QuakeEditorTogglePinWindow => { KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { keys_settings .toggle_hide_quake_mode_window_when_unfocused_and_write_to_user_defaults( ctx, ) }); } SetExtraMetaKeys(extra_meta_keys) => { KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { report_if_error!(keys_settings .extra_meta_keys .set_value(*extra_meta_keys, ctx)) }); } ToggleLeftMetaKey => { let current_meta_keys = *KeysSettings::as_ref(ctx).extra_meta_keys; KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { report_if_error!(keys_settings .extra_meta_keys .set_value(current_meta_keys.toggle_left_key(), ctx)) }); } ToggleRightMetaKey => { let current_meta_keys = *KeysSettings::as_ref(ctx).extra_meta_keys; KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { report_if_error!(keys_settings .extra_meta_keys .set_value(current_meta_keys.toggle_right_key(), ctx)) }); } ToggleMouseReporting => { AltScreenReporting::handle(ctx).update(ctx, |reporting, ctx| { reporting .mouse_reporting_enabled .toggle_and_save_value(ctx) .expect("MouseReportingEnabled failed to serialize"); }); ctx.notify(); } ToggleScrollReporting => { AltScreenReporting::handle(ctx).update(ctx, |reporting, ctx| { reporting .scroll_reporting_enabled .toggle_and_save_value(ctx) .expect("ScrollReportingEnabled failed to serialize"); }); ctx.notify(); } ToggleFocusReporting => { AltScreenReporting::handle(ctx).update(ctx, |reporting, ctx| { reporting .focus_reporting_enabled .toggle_and_save_value(ctx) .expect("FocusReportingEnabled failed to serialize"); }); ctx.notify(); } ToggleLongRunningNotifications => { let current_settings = SessionSettings::as_ref(ctx).notifications.value().clone(); let is_long_running_enabled = !current_settings.is_long_running_enabled; SessionSettings::handle(ctx).update(ctx, |settings, ctx| { let new_settings = NotificationsSettings { is_long_running_enabled, ..current_settings }; if let Err(e) = settings.notifications.set_value(new_settings, ctx) { log::error!("Failed to persist Notifications setting: {e}"); } }); ctx.notify(); } SetLongRunningNotificationThreshold => { let user_input = self .notifications_long_running_threshold_editor .as_ref(ctx) .buffer_text(ctx); if let Ok(long_running_threshold) = user_input.parse::() { if long_running_threshold > 0.0 { // TODO: use try_from_secs_32 in the future to avoid previous cmp let current_settings = SessionSettings::as_ref(ctx).notifications.value().clone(); SessionSettings::handle(ctx).update(ctx, |settings, ctx| { let new_settings = NotificationsSettings { long_running_threshold: Duration::from_secs_f32( long_running_threshold, ), ..current_settings }; if let Err(e) = settings.notifications.set_value(new_settings, ctx) { log::error!("Error persisting notifications setting: {e}"); } }); } } } TogglePasswordPromptNotifications => { let current_settings = SessionSettings::as_ref(ctx).notifications.value().clone(); let is_password_prompt_enabled = !current_settings.is_password_prompt_enabled; SessionSettings::handle(ctx).update(ctx, |settings, ctx| { let new_settings = NotificationsSettings { is_password_prompt_enabled, ..current_settings }; if let Err(e) = settings.notifications.set_value(new_settings, ctx) { log::error!("Error persisting notifications setting: {e}"); } }); ctx.notify(); } ToggleAgentTaskCompletedNotifications => { let current_settings = SessionSettings::as_ref(ctx).notifications.value().clone(); let is_agent_task_completed_enabled = !current_settings.is_agent_task_completed_enabled; SessionSettings::handle(ctx).update(ctx, |settings, ctx| { let new_settings = NotificationsSettings { is_agent_task_completed_enabled, ..current_settings }; if let Err(e) = settings.notifications.set_value(new_settings, ctx) { log::error!("Error persisting notifications setting: {e}"); } }); ctx.notify(); } ToggleNeedsAttentionNotifications => { let current_settings = SessionSettings::as_ref(ctx).notifications.value().clone(); let is_agent_needs_attention_enabled = !current_settings.is_needs_attention_enabled; SessionSettings::handle(ctx).update(ctx, |settings, ctx| { let new_settings = NotificationsSettings { is_needs_attention_enabled: is_agent_needs_attention_enabled, ..current_settings }; if let Err(e) = settings.notifications.set_value(new_settings, ctx) { log::error!("Error persisting notifications setting: {e}"); } }); ctx.notify(); } ToggleNotificationSound => { let current_settings = SessionSettings::as_ref(ctx).notifications.value().clone(); let play_notification_sound = !current_settings.play_notification_sound; SessionSettings::handle(ctx).update(ctx, |settings, ctx| { let new_settings = NotificationsSettings { play_notification_sound, ..current_settings }; if let Err(e) = settings.notifications.set_value(new_settings, ctx) { log::error!("Error persisting notification sound setting: {e}"); } }); ctx.notify(); } ToggleAgentInAppNotifications => { AISettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings.show_agent_notifications.toggle_and_save_value(ctx)); }); ctx.notify(); } ToggleCompletionsOpenWhileTyping => { InputSettings::handle(ctx).update(ctx, |input_settings, ctx| { report_if_error!(input_settings .completions_open_while_typing .toggle_and_save_value(ctx)); }); } ToggleCommandCorrections => { InputSettings::handle(ctx).update(ctx, |input_settings, ctx| { report_if_error!(input_settings .command_corrections .toggle_and_save_value(ctx)); }); } ToggleErrorUnderlining => { InputSettings::handle(ctx).update(ctx, |input_settings, ctx| { report_if_error!(input_settings.error_underlining.toggle_and_save_value(ctx)); }); } ToggleSyntaxHighlighting => { InputSettings::handle(ctx).update(ctx, |input_settings, ctx| { report_if_error!(input_settings .syntax_highlighting .toggle_and_save_value(ctx)); }); } ToggleAliasExpansion => { AliasExpansionSettings::handle(ctx).update(ctx, |alias_expansion_settings, ctx| { report_if_error!(alias_expansion_settings .alias_expansion_enabled .toggle_and_save_value(ctx)); }); } ToggleMiddleClickPaste => { SelectionSettings::handle(ctx).update(ctx, |selection_settings, ctx| { report_if_error!(selection_settings .middle_click_paste_enabled .toggle_and_save_value(ctx)); }); } ToggleShowInputHintText => { InputSettings::handle(ctx).update(ctx, |input_settings, ctx| { report_if_error!(input_settings.show_hint_text.toggle_and_save_value(ctx)); }); } ToggleShowTerminalInputMessageLine => { InputSettings::handle(ctx).update(ctx, |input_settings, ctx| { report_if_error!(input_settings .show_terminal_input_message_bar .toggle_and_save_value(ctx)); }); } ToggleLinkTooltip => { GeneralSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings.link_tooltip.toggle_and_save_value(ctx)); }); } ToggleShowWarningBeforeQuitting => { GeneralSettings::handle(ctx).update(ctx, |warning_settings, ctx| { report_if_error!(warning_settings .show_warning_before_quitting .toggle_and_save_value(ctx)); }) } ToggleSmartSelection => { SemanticSelection::handle(ctx).update(ctx, |selection, ctx| { report_if_error!(selection.smart_select_enabled.toggle_and_save_value(ctx)); }); } SetWordCharAllowlist => { let word_boundary_allowlist = self .word_boundary_editor .read(ctx, |editor, ctx| editor.buffer_text(ctx)); SemanticSelection::handle(ctx).update(ctx, |selection, ctx| { report_if_error!(selection .word_char_allowlist .set_value(word_boundary_allowlist, ctx)); }); } ResetWordCharAllowlist => { SemanticSelection::handle(ctx).update(ctx, |selection, ctx| { report_if_error!(selection.word_char_allowlist.set_value_to_default(ctx)); }); } ToggleUseAudibleBell => { TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| { report_if_error!(terminal_settings .use_audible_bell .toggle_and_save_value(ctx)); }) } ToggleVimMode => AppEditorSettings::handle(ctx).update(ctx, |editor_settings, ctx| { editor_settings .vim_mode .toggle_and_save_value(ctx) .expect("failed to serialize VimMode"); ctx.notify(); }), ToggleVimUnnamedSystemClipboard => { AppEditorSettings::handle(ctx).update(ctx, |editor_settings, ctx| { editor_settings .vim_unnamed_system_clipboard .toggle_and_save_value(ctx) .expect("failed to serialize VimUnnamedSystemClipboard"); ctx.notify(); }) } ToggleVimStatusBar => { AppEditorSettings::handle(ctx).update(ctx, |editor_settings, ctx| { editor_settings .vim_status_bar .toggle_and_save_value(ctx) .expect("failed to serialize VimStatusBar"); ctx.notify(); }) } SetTabBehavior(tab_behavior) => self.set_tab_behavior_setting(tab_behavior, ctx), SetNewTabPlacement(new_tab_placement) => { self.set_new_tab_placement(new_tab_placement, ctx) } SetOsc52ClipboardAccess(access) => { TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| { report_if_error!(terminal_settings .osc52_clipboard_access .set_value(*access, ctx)); }); } SetDefaultSessionMode(mode) => self.set_default_session_mode(mode, ctx), SetDefaultTabConfig(path) => { AISettings::handle(ctx).update(ctx, |ai_settings, ctx| { report_if_error!(ai_settings .default_session_mode_internal .set_value(DefaultSessionMode::TabConfig, ctx)); report_if_error!(ai_settings .default_tab_config_path .set_value(path.clone(), ctx)); }); } SearchForKeybinding(query) => { ctx.emit(FeaturesSettingsPageEvent::SearchForKeybinding( query.clone(), )); ctx.notify(); } ToggleAutosuggestions => { AppEditorSettings::handle(ctx).update(ctx, |editor_settings, ctx| { editor_settings .enable_autosuggestions .toggle_and_save_value(ctx) .expect("failed to serialize EnableAutosuggestions"); ctx.notify(); }) } ToggleAutosuggestionKeybindingHint => { AppEditorSettings::handle(ctx).update(ctx, |editor_settings, ctx| { editor_settings .autosuggestion_keybinding_hint .toggle_and_save_value(ctx) .expect("failed to serialize HideAutosuggestionKeybindingHint"); ctx.notify(); }) } ToggleShowAutosuggestionIgnoreButton => { AppEditorSettings::handle(ctx).update(ctx, |editor_settings, ctx| { editor_settings .show_autosuggestion_ignore_button .toggle_and_save_value(ctx) .expect("failed to serialize ShowAutosuggestionIgnoreButton"); ctx.notify(); }) } TogglePreferLowPowerGPU => { let new_value = GPUSettings::handle(ctx).update(ctx, |gpu_settings, ctx| { report_if_error!(gpu_settings.prefer_low_power_gpu.toggle_and_save_value(ctx)); *gpu_settings.prefer_low_power_gpu.value() }); ctx.update_rendering_config(|config| { config.gpu_power_preference = if new_value { GPUPowerPreference::LowPower } else { GPUPowerPreference::default() } }); self.gpu_power_preference_changed = true; } SetPreferredGraphicsBackend(graphics_backend) => { GPUSettings::handle(ctx).update(ctx, |gpu_settings, ctx| { report_if_error!(gpu_settings .preferred_backend .set_value(*graphics_backend, ctx)); }); ctx.update_rendering_config(|config| config.backend_preference = *graphics_backend); self.graphics_backend_preference_changed = true; } ToggleConfirmCloseSession => { SessionSettings::handle(ctx).update(ctx, |session_settings, ctx| { session_settings .should_confirm_close_session .toggle_and_save_value(ctx) .expect("failed to serialize ShouldConfirmCloseSession"); ctx.notify(); }) } ToggleShowTerminalZeroStateBlock => { TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| { report_if_error!(terminal_settings .show_terminal_zero_state_block .toggle_and_save_value(ctx)); }); } ToggleShowChangelogAfterUpdate => { ChangelogSettings::handle(ctx).update(ctx, |changelog_settings, ctx| { report_if_error!(changelog_settings .show_changelog_after_update .toggle_and_save_value(ctx)); }) } ToggleLinuxClipboardSelection => { SelectionSettings::handle(ctx).update(ctx, |selection_settings, ctx| { report_if_error!(selection_settings .linux_selection_clipboard .toggle_and_save_value(ctx)); }); } #[cfg(any(target_os = "linux", target_os = "freebsd"))] ToggleForceX11 => { LinuxAppConfiguration::handle(ctx).update(ctx, |linux_app_configuration, ctx| { report_if_error!(linux_app_configuration.force_x11.toggle_and_save_value(ctx)); }); self.force_x11_changed = true; // This is a workaround to make sure the user sees the new text that is added to the description after changing the setting. // Without scrolling, the new description text gets cut off. self.page.scroll_by(galaxyui::units::Pixels::new(40.)); ctx.notify(); } ToggleQuitOnLastWindowClosed => { GeneralSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings .quit_on_last_window_closed .toggle_and_save_value(ctx)); }) } ToggleLoginItem => GeneralSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings.add_app_as_login_item.toggle_and_save_value(ctx)); }), ToggleAtContextMenuInTerminalMode => { InputSettings::handle(ctx).update(ctx, |input_settings, ctx| { report_if_error!(input_settings .at_context_menu_in_terminal_mode .toggle_and_save_value(ctx)); }); } ToggleSlashCommandsInTerminalMode => { InputSettings::handle(ctx).update(ctx, |input_settings, ctx| { report_if_error!(input_settings .enable_slash_commands_in_terminal .toggle_and_save_value(ctx)); }); } ToggleOutlineCodebaseSymbolsForAtContextMenu => { InputSettings::handle(ctx).update(ctx, |input_settings, ctx| { report_if_error!(input_settings .outline_codebase_symbols_for_at_context_menu .toggle_and_save_value(ctx)); }); } ToggleAutoOpenCodeReviewPane => { GeneralSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings .auto_open_code_review_pane_on_first_agent_change .toggle_and_save_value(ctx)); }) } TogglePreserveInputFocusOnBlockSelection => { BlockListSettings::handle(ctx).update(ctx, |blocklist_settings, ctx| { report_if_error!(blocklist_settings .preserve_input_focus_on_block_selection .toggle_and_save_value(ctx)); }); } SetNotificationToastDuration => { let user_input = self .notification_toast_duration_editor .as_ref(ctx) .buffer_text(ctx); if let Ok(duration_secs) = user_input.parse::() { if duration_secs > 0 { SessionSettings::handle(ctx).update(ctx, |settings, ctx| { if let Err(e) = settings .notification_toast_duration_secs .set_value(duration_secs, ctx) { log::error!("Error persisting notification toast duration: {e}"); } }); } } } MakeWarpDefaultTerminal => { DefaultTerminal::handle(ctx).update(ctx, |default_terminal, ctx| { default_terminal.make_warp_default(ctx); }); } SetCodeEditorLineNumberMode(mode) => { AppEditorSettings::handle(ctx).update(ctx, |editor_settings, ctx| { report_if_error!(editor_settings .code_editor_line_number_mode .set_value(*mode, ctx)); ctx.notify(); }); } ToggleAsyncFind => { TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| { report_if_error!(terminal_settings .async_find_enabled .toggle_and_save_value(ctx)); }); } } send_telemetry_from_ctx!(action.telemetry_event(ctx), ctx); } } impl View for FeaturesPageView { fn ui_name() -> &'static str { "FeaturesPage" } fn render(&self, app: &AppContext) -> Box { self.page.render(self, app) } } impl FeaturesPageView { pub fn new( global_resource_handles: GlobalResourceHandles, ctx: &mut ViewContext, ) -> Self { ctx.observe( &DisplayCount::handle(ctx), Self::on_display_count_model_changed, ); // Listen for model changes on all the settings that are used in this view. ctx.subscribe_to_model(&AppEditorSettings::handle(ctx), |me, _, _, ctx| { Self::update_code_editor_line_number_mode_dropdown( me.code_editor_line_number_mode_dropdown.clone(), ctx, ); ctx.notify(); }); ctx.subscribe_to_model(&SelectionSettings::handle(ctx), |_, _, _, ctx| ctx.notify()); ctx.subscribe_to_model(&AltScreenReporting::handle(ctx), |_, _, _, ctx| { ctx.notify() }); ctx.subscribe_to_model(&BlockListSettings::handle(ctx), |_, _, _, ctx| ctx.notify()); ctx.subscribe_to_model(&AliasExpansionSettings::handle(ctx), |_, _, _, ctx| { ctx.notify() }); ctx.subscribe_to_model(&ChangelogSettings::handle(ctx), |_, _, _, ctx| ctx.notify()); ctx.subscribe_to_model(&CommandSearchSettings::handle(ctx), |_, _, _, ctx| { ctx.notify() }); ctx.subscribe_to_model(&GPUSettings::handle(ctx), |me, _, _, ctx| { me.refresh_preferred_graphics_backend_dropdown(ctx); ctx.notify(); }); ctx.subscribe_to_model(&InputSettings::handle(ctx), |me, _, event, ctx| { if matches!( event, InputSettingsChangedEvent::CompletionsOpenWhileTyping { .. } ) { me.refresh_tab_behavior_dropdown(ctx); } ctx.notify(); }); ctx.subscribe_to_model( &ScrollSettings::handle(ctx), |me, scroll_settings, event, ctx| { if matches!( event, ScrollSettingsChangedEvent::MouseScrollMultiplier { .. } ) { me.mouse_scroll_input_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( format!("{}", *scroll_settings.as_ref(ctx).mouse_scroll_multiplier) .as_str(), ctx, ); }) } ctx.notify() }, ); ctx.subscribe_to_model(&GeneralSettings::handle(ctx), |_, _, _, ctx| ctx.notify()); ctx.subscribe_to_model(&KeysSettings::handle(ctx), |me, _, _, ctx| { me.handle_hotkey_settings_update(ctx); }); ctx.subscribe_to_model(&SessionSettings::handle(ctx), |me, _, event, ctx| { match event { SessionSettingsChangedEvent::Notifications { .. } => { // Update the value of the notifications threshold input to match the new setting me.notifications_long_running_threshold_editor .update(ctx, |editor, ctx| { editor.set_buffer_text( &format!( "{}", SessionSettings::handle(ctx) .as_ref(ctx) .notifications .long_running_threshold .as_secs_f32() ), ctx, ); }); } SessionSettingsChangedEvent::NotificationToastDurationSecs { .. } => { me.notification_toast_duration_editor .update(ctx, |editor, ctx| { editor.set_buffer_text( &format!( "{}", *SessionSettings::as_ref(ctx).notification_toast_duration_secs ), ctx, ); }); } SessionSettingsChangedEvent::NewSessionShellOverride { .. } => { #[cfg(feature = "local_tty")] { use super::features::startup_shell::NewSessionShellAction; use crate::terminal::session_settings::StartupShell; me.startup_shell_view.update(ctx, |_, ctx| { if matches!( *SessionSettings::as_ref(ctx).startup_shell_override.value(), StartupShell::Custom(_), ) { ctx.dispatch_typed_action( &NewSessionShellAction::ShowCustomPathInput, ); } ctx.notify(); }); } } _ => {} } ctx.notify() }); ctx.subscribe_to_model( &TerminalSettings::handle(ctx), |me, terminal_settings, event, ctx| { if matches!(event, TerminalSettingsChangedEvent::MaximumGridSize { .. }) { me.max_block_size_input_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( &format!("{}", *terminal_settings.as_ref(ctx).maximum_grid_size), ctx, ); }); } if matches!( event, TerminalSettingsChangedEvent::Osc52ClipboardAccessSetting { .. } ) { Self::update_osc52_clipboard_access_dropdown( me.osc52_clipboard_access_dropdown.clone(), ctx, ); } ctx.notify() }, ); ctx.subscribe_to_model(&UndoCloseSettings::handle(ctx), |_, _, _, ctx| ctx.notify()); ctx.subscribe_to_model(&DefaultTerminal::handle(ctx), |_, _, _, ctx| { ctx.notify(); }); ctx.subscribe_to_model(&AISettings::handle(ctx), |me, _, event, ctx| { if matches!( event, AISettingsChangedEvent::IsAnyAIEnabled { .. } | AISettingsChangedEvent::DefaultSessionMode { .. } ) { Self::update_default_session_mode_dropdown( me.default_session_mode_dropdown.clone(), ctx, ); ctx.notify(); } }); let pin_position_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); let top = DropdownItem::new( "Pin to top", FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Top), ); let bottom = DropdownItem::new( "Pin to bottom", FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Bottom), ); let left = DropdownItem::new( "Pin to left", FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Left), ); let right = DropdownItem::new( "Pin to right", FeaturesPageAction::QuakeEditorSetPinPosition(QuakeModePinPosition::Right), ); // Note that the index here has to correspond to the ordering of the items. let selected_index = match KeysSettings::as_ref(ctx) .quake_mode_settings .active_pin_position { QuakeModePinPosition::Top => 0, QuakeModePinPosition::Bottom => 1, QuakeModePinPosition::Left => 2, QuakeModePinPosition::Right => 3, }; dropdown.add_items(vec![top, bottom, left, right], ctx); dropdown.set_selected_by_index(selected_index, ctx); dropdown }); let pin_screen_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); init_display_count_dropdown( &KeysSettings::as_ref(ctx) .quake_mode_settings .value() .clone(), DisplayCount::as_ref(ctx).num_displays(), &mut dropdown, ctx, ); dropdown }); let new_tab_placement_dropdown = ctx.add_typed_action_view(Dropdown::new); Self::update_new_tab_placement_dropdown(new_tab_placement_dropdown.clone(), ctx); let osc52_clipboard_access_dropdown = ctx.add_typed_action_view(Dropdown::new); Self::update_osc52_clipboard_access_dropdown(osc52_clipboard_access_dropdown.clone(), ctx); ctx.subscribe_to_model(&TabSettings::handle(ctx), |me, _, event, ctx| { if matches!(event, TabSettingsChangedEvent::NewTabPlacement { .. }) { Self::update_new_tab_placement_dropdown(me.new_tab_placement_dropdown.clone(), ctx); } ctx.notify(); }); let default_session_mode_dropdown = ctx.add_typed_action_view(FilterableDropdown::new); Self::update_default_session_mode_dropdown(default_session_mode_dropdown.clone(), ctx); ctx.subscribe_to_model(&GalaxyConfig::handle(ctx), |me, _, event, ctx| { if matches!(event, GalaxyConfigUpdateEvent::TabConfigs) { Self::update_default_session_mode_dropdown( me.default_session_mode_dropdown.clone(), ctx, ); ctx.notify(); } }); #[cfg(feature = "local_fs")] let external_editor_view = ctx.add_typed_action_view(features::ExternalEditorView::new); let global_hotkey_mode = KeysSettings::handle(ctx).read(ctx, |settings, ctx| settings.global_hotkey_mode(ctx)); let global_hotkey_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); init_global_hotkey_dropdown(global_hotkey_mode, &mut dropdown, ctx); dropdown }); // The state for this dropdown is initialized by `refresh_tab_behavior_state`. let tab_behavior_dropdown = ctx.add_typed_action_view(Dropdown::new); let ctrl_tab_behavior_dropdown = ctx.add_typed_action_view(Dropdown::new); Self::update_ctrl_tab_behavior_dropdown(ctrl_tab_behavior_dropdown.clone(), ctx); let code_editor_line_number_mode_dropdown = ctx.add_typed_action_view(Dropdown::new); Self::update_code_editor_line_number_mode_dropdown( code_editor_line_number_mode_dropdown.clone(), ctx, ); ctx.subscribe_to_model(&KeysSettings::handle(ctx), |me, _, event, ctx| { if matches!( event, KeysSettingsChangedEvent::CtrlTabBehaviorSetting { .. } ) { Self::update_ctrl_tab_behavior_dropdown(me.ctrl_tab_behavior_dropdown.clone(), ctx); } ctx.notify(); }); let graphics_backend_dropdown = ctx.add_typed_action_view(Dropdown::new); #[cfg(feature = "local_tty")] let working_directory_view = ctx.add_typed_action_view(features::WorkingDirectoryView::new); #[cfg(feature = "local_tty")] let startup_shell_view = ctx.add_typed_action_view(features::StartupShellView::new); let undo_close_view = ctx.add_typed_action_view(features::UndoCloseView::new); let appearance_handle = Appearance::handle(ctx); let width_and_height_editor_options = SingleLineEditorOptions { text: TextOptions::ui_font_size(appearance_handle.as_ref(ctx)), ..Default::default() }; let width_editor = { ctx.add_typed_action_view(|ctx| { EditorView::single_line(width_and_height_editor_options.clone(), ctx) }) }; width_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( &format!( "{}", KeysSettings::handle(ctx) .as_ref(ctx) .quake_mode_settings .value() .width_percentage() ), ctx, ); }); ctx.subscribe_to_view(&width_editor, move |me, _, event, ctx| { me.handle_width_editor_event(event, ctx); }); let word_boundary_editor = ctx.add_typed_action_view(|ctx| { EditorView::single_line(width_and_height_editor_options.clone(), ctx) }); word_boundary_editor.update(ctx, |editor, ctx| { let word_char_allowlist = SemanticSelection::as_ref(ctx).word_char_allowlist_string(); editor.set_buffer_text(&word_char_allowlist, ctx); }); ctx.subscribe_to_model(&SemanticSelection::handle(ctx), |me, _, event, ctx| { if matches!( event, SemanticSelectionChangedEvent::WordCharAllowlist { .. } ) { me.word_boundary_editor.update(ctx, |editor, ctx| { let word_char_allowlist = SemanticSelection::as_ref(ctx).word_char_allowlist_string(); editor.set_buffer_text(&word_char_allowlist, ctx); }); } ctx.notify(); }); let height_editor = { ctx.add_typed_action_view(|ctx| { EditorView::single_line(width_and_height_editor_options.clone(), ctx) }) }; height_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( &format!( "{}", KeysSettings::handle(ctx) .as_ref(ctx) .quake_mode_settings .value() .height_percentage() ), ctx, ); }); ctx.subscribe_to_view(&height_editor, move |me, _, event, ctx| { me.handle_height_editor_event(event, ctx); }); // Editor for configuring max block size let block_size_editor = { ctx.add_typed_action_view(|ctx| { EditorView::single_line(width_and_height_editor_options.clone(), ctx) }) }; block_size_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( &format!( "{}", *TerminalSettings::handle(ctx) .as_ref(ctx) .maximum_grid_size .value() ), ctx, ); }); ctx.subscribe_to_view(&block_size_editor, move |me, _, event, ctx| { me.handle_block_size_editor_event(event, ctx); }); let mouse_scroll_input_editor = ctx.add_typed_action_view(|ctx| { EditorView::single_line(width_and_height_editor_options.clone(), ctx) }); mouse_scroll_input_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( format!( "{}", ScrollSettings::handle(ctx) .as_ref(ctx) .mouse_scroll_multiplier .value() ) .as_str(), ctx, ); }); ctx.subscribe_to_view(&mouse_scroll_input_editor, |me, _, event, ctx| { me.handle_mouse_scroll_input_editor_event(event, ctx); }); let notifications_long_running_threshold_editor = { ctx.add_typed_action_view(|ctx| { let options = SingleLineEditorOptions { text: TextOptions { font_size_override: Some(appearance_handle.as_ref(ctx).ui_font_size() - 2.), ..Default::default() }, ..Default::default() }; EditorView::single_line(options, ctx) }) }; notifications_long_running_threshold_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( &format!( "{}", SessionSettings::handle(ctx) .as_ref(ctx) .notifications .long_running_threshold .as_secs_f32() ), ctx, ); }); let notification_toast_duration_editor = ctx.add_typed_action_view(|ctx| { let options = SingleLineEditorOptions { text: TextOptions { font_size_override: Some(appearance_handle.as_ref(ctx).ui_font_size() - 2.), ..Default::default() }, ..Default::default() }; EditorView::single_line(options, ctx) }); notification_toast_duration_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( &format!( "{}", *SessionSettings::as_ref(ctx).notification_toast_duration_secs ), ctx, ); }); ctx.subscribe_to_model(&GPUState::handle(ctx), |me, _, event, ctx| { if matches!(event, GPUStateEvent::LowPowerGPUAvailable) { me.page = Self::build_page(ctx); ctx.notify(); } }); let mut features_page_view = FeaturesPageView { page: Self::build_page(ctx), global_resource_handles, button_mouse_states: Default::default(), activation_hotkey_keybinding_editor_state: KeybindingEditorState::Idle, completions_keystroke: Default::default(), autosuggestions_keystroke: Default::default(), activation_hotkey_keybinding: KeyBindingModifyingState::new( KeysSettings::as_ref(ctx) .activation_hotkey_keybinding .value() .clone(), ), quake_mode_keybinding_editor_state: KeybindingEditorState::Idle, quake_mode_keybinding: KeyBindingModifyingState::new( KeysSettings::as_ref(ctx) .quake_mode_settings .value() .keybinding .clone(), ), quake_mode_pin_position_dropdown: pin_position_dropdown, quake_mode_pin_screen_dropdown: pin_screen_dropdown, quake_mode_width_editor: width_editor, quake_mode_height_editor: height_editor, notifications_long_running_threshold_editor, notification_toast_duration_editor, #[cfg(feature = "local_tty")] working_directory_view, #[cfg(feature = "local_tty")] startup_shell_view, undo_close_view, max_block_size_input_editor: block_size_editor, valid_max_block_size: true, #[cfg(feature = "local_fs")] external_editor_view, word_boundary_editor, global_hotkey_dropdown, tab_behavior_dropdown, ctrl_tab_behavior_dropdown, code_editor_line_number_mode_dropdown, graphics_backend_dropdown, new_tab_placement_dropdown, osc52_clipboard_access_dropdown, default_session_mode_dropdown, tab_behavior: Default::default(), window_id: ctx.window_id(), mouse_scroll_input_editor, valid_mouse_scroll_multiplier: true, #[cfg(any(target_os = "linux", target_os = "freebsd"))] force_x11_changed: false, gpu_power_preference_changed: false, graphics_backend_preference_changed: false, }; features_page_view.refresh_tab_behavior_state(ctx); features_page_view.refresh_tab_behavior_dropdown(ctx); features_page_view.refresh_preferred_graphics_backend_dropdown(ctx); features_page_view } fn build_page(ctx: &mut ViewContext) -> PageType { let mut general_widgets: Vec>> = vec![Box::new(DefaultSessionModeWidget::default())]; let native_preference_settings = NativePreferenceSettings::as_ref(ctx); if native_preference_settings .user_native_redirect_preference .is_supported_on_current_platform() { general_widgets.push(Box::new(NativeRedirectWidget::default())); } let general_settings = &GeneralSettings::as_ref(ctx); if general_settings .restore_session .is_supported_on_current_platform() { general_widgets.push(Box::new(SessionRestorationWidget::default())) } general_widgets.push(Box::new(SnackbarHeaderWidget::default())); general_widgets.push(Box::new(LinkTooltipWidget::default())); #[cfg(feature = "local_fs")] { if !FeatureFlag::OpenWarpNewSettingsModes.is_enabled() { let external_editor_settings = crate::util::file::external_editor::EditorSettings::as_ref(ctx); if external_editor_settings .open_file_editor .is_supported_on_current_platform() { general_widgets.push(Box::new(ExternalEditorWidget::default())); } } } if general_settings .show_warning_before_quitting .is_supported_on_current_platform() { general_widgets.push(Box::new(QuitWarningModalWidget::default())); } if general_settings .quit_on_last_window_closed .is_supported_on_current_platform() { general_widgets.push(Box::new(QuitWhenAllWindowsClosedWidget::default())); } if general_settings .add_app_as_login_item .is_supported_on_current_platform() { general_widgets.push(Box::new(LoginItemWidget::default())); } let changelog_settings = ChangelogSettings::as_ref(ctx); if changelog_settings .show_changelog_after_update .is_supported_on_current_platform() { general_widgets.push(Box::new(ShowChangelogWidget::default())); } let scroll_settings = ScrollSettings::as_ref(ctx); if scroll_settings .mouse_scroll_multiplier .is_supported_on_current_platform() { general_widgets.push(Box::new(MouseScrollMultiplierWidget::default())); } if FeatureFlag::AutoOpenCodeReviewPane.is_enabled() && !FeatureFlag::OpenWarpNewSettingsModes.is_enabled() { general_widgets.push(Box::new(AutoOpenCodeReviewPaneWidget::default())); } if DefaultTerminal::can_warp_become_default() { general_widgets.push(Box::new(DefaultTerminalWidget::default())); } // The widget is the opt-in surface for channels where `FeatureFlag::AsyncFind` // is off. Channels with the flag on force the feature on and hide the toggle // entirely; see `TerminalSettings::is_async_find_enabled`. general_widgets.push(Box::new(AsyncFindWidget::default())); let app_editor_settings = AppEditorSettings::as_ref(ctx); let notifications_widgets: Vec>> = vec![Box::new(DesktopNotificationsWidget::default())]; let mut session_widgets: Vec>> = vec![]; session_widgets.push(Box::new(BlockLimitWidget::default())); let session_settings = SessionSettings::as_ref(ctx); #[cfg(feature = "local_tty")] { if session_settings .startup_shell_override .is_supported_on_current_platform() { session_widgets.push(Box::new(StartupShellWidget::default())); } if session_settings .working_directory_config .is_supported_on_current_platform() { session_widgets.push(Box::new(WorkingDirectoryWidget::default())); } } let undo_close_settings = UndoCloseSettings::as_ref(ctx); if undo_close_settings .enabled .is_supported_on_current_platform() { session_widgets.push(Box::new(UndoCloseWidget::default())); } if FeatureFlag::CreatingSharedSessions.is_enabled() && ContextFlag::CreateSharedSession.is_enabled() && session_settings .should_confirm_close_session .is_supported_on_current_platform() { session_widgets.push(Box::new(ConfirmCloseSharedSessionWidget::default())); } let mut keys_widgets: Vec>> = vec![]; let keys_settings = KeysSettings::as_ref(ctx); if keys_settings .extra_meta_keys .is_supported_on_current_platform() { keys_widgets.push(Box::new(ExtraMetaKeysWidget::default())) } if keys_settings .ctrl_tab_behavior .is_supported_on_current_platform() { keys_widgets.push(Box::new(CtrlTabBehaviorWidget::default())); } if keys_settings .activation_hotkey_enabled .is_supported_on_current_platform() { keys_widgets.push(Box::new(GlobalHotkeyWidget::default())); } let mut text_editing_widgets: Vec>> = vec![Box::new(AutocompleteSymbolsWidget::default())]; if app_editor_settings .code_editor_line_number_mode .is_supported_on_current_platform() { text_editing_widgets.push(Box::new(CodeEditorLineNumberModeWidget::default())); } if app_editor_settings .vim_mode .is_supported_on_current_platform() { text_editing_widgets.push(Box::new(VimModeWidget::default())); } let mut editor_widgets: Vec>> = vec![]; let input_settings = InputSettings::as_ref(ctx); if input_settings .error_underlining .is_supported_on_current_platform() { editor_widgets.push(Box::new(ErrorUnderliningWidget::default())) } if input_settings .syntax_highlighting .is_supported_on_current_platform() { editor_widgets.push(Box::new(SyntaxHighlightingWidget::default())) } if input_settings .completions_open_while_typing .is_supported_on_current_platform() { editor_widgets.push(Box::new(CompletionsMenuWhileTypingWidget::default())); } if input_settings .command_corrections .is_supported_on_current_platform() { editor_widgets.push(Box::new(CommandCorrectionsWidget::default())); } let alias_expansion_settings = AliasExpansionSettings::as_ref(ctx); if alias_expansion_settings .alias_expansion_enabled .is_supported_on_current_platform() { editor_widgets.push(Box::new(AliasExpansionWidget::default())); } let selection_settings = SelectionSettings::as_ref(ctx); if selection_settings .middle_click_paste_enabled .is_supported_on_current_platform() { editor_widgets.push(Box::new(MiddleClickPasteWidget::default())); } editor_widgets.push(Box::new(AutosuggestionKeybindingHintWidget::default())); if FeatureFlag::AllowIgnoringInputSuggestions.is_enabled() { editor_widgets.push(Box::new(AutosuggestionIgnoreButtonWidget::default())); } if input_settings .at_context_menu_in_terminal_mode .is_supported_on_current_platform() { editor_widgets.push(Box::new(AtContextMenuInTerminalModeWidget::default())); } if FeatureFlag::AgentView.is_enabled() && input_settings .enable_slash_commands_in_terminal .is_supported_on_current_platform() { editor_widgets.push(Box::new(SlashCommandsInTerminalModeWidget::default())); } if input_settings .outline_codebase_symbols_for_at_context_menu .is_supported_on_current_platform() && FeatureFlag::AIContextMenuCode.is_enabled() { editor_widgets.push(Box::new( OutlineCodebaseSymbolsForAtContextMenuWidget::default(), )); } if FeatureFlag::AgentView.is_enabled() { editor_widgets.push(Box::new(ShowTerminalInputMessageLineWidget::default())); } editor_widgets.push(Box::new(TabKeyBehaviorWidget::default())); let blocklist_settings = BlockListSettings::as_ref(ctx); if blocklist_settings .preserve_input_focus_on_block_selection .is_supported_on_current_platform() { editor_widgets.push(Box::new(PreserveInputFocusOnBlockSelectionWidget::default())); } let mut terminal_widgets: Vec>> = vec![]; let reporting_settings = AltScreenReporting::as_ref(ctx); if reporting_settings .mouse_reporting_enabled .is_supported_on_current_platform() { terminal_widgets.push(Box::new(MouseReportingWidget::default())); } if reporting_settings .scroll_reporting_enabled .is_supported_on_current_platform() { terminal_widgets.push(Box::new(ScrollReportingWidget::default())); } if reporting_settings .focus_reporting_enabled .is_supported_on_current_platform() { terminal_widgets.push(Box::new(FocusReportingWidget::default())); } let terminal_settings = TerminalSettings::as_ref(ctx); if terminal_settings .use_audible_bell .is_supported_on_current_platform() { terminal_widgets.push(Box::new(AudibleBellWidget::default())); } if FeatureFlag::AgentView.is_enabled() { terminal_widgets.push(Box::new(ShowTerminalZeroStateBlockWidget::default())); } terminal_widgets.push(Box::new(SmartSelectWidget::default())); terminal_widgets.push(Box::new(CopyOnSelectWidget::default())); terminal_widgets.push(Box::new(Osc52ClipboardAccessWidget::default())); terminal_widgets.push(Box::new(NewTabPlacementWidget::default())); let mut system_widgets: Vec>> = vec![]; let selection_settings = SelectionSettings::as_ref(ctx); if selection_settings .linux_selection_clipboard .is_supported_on_current_platform() { system_widgets.push(Box::new(LinuxSelectionClipboardWidget::default())); } let gpu_settings = GPUSettings::as_ref(ctx); if gpu_settings .prefer_low_power_gpu .is_supported_on_current_platform() && GPUState::as_ref(ctx).is_low_power_gpu_available() { system_widgets.push(Box::new(GPUWidget::default())); } if gpu_settings .preferred_backend .is_supported_on_current_platform() { system_widgets.push(Box::new(GraphicsBackendWidget::default())); } #[cfg(any(target_os = "linux", target_os = "freebsd"))] { if windowing_system_is_customizable(ctx) { system_widgets.push(Box::new(WindowSystemWidget::default())); } } let categories = vec![ Category::new("General", general_widgets), Category::new("Session", session_widgets), Category::new("Keys", keys_widgets), Category::new("Text Editing", text_editing_widgets), Category::new("Terminal Input", editor_widgets), Category::new("Terminal", terminal_widgets), Category::new("Notifications", notifications_widgets), Category::new( "Workflows", vec![Box::new(WorkflowsInCommandSearch::default())], ), Category::new("System", system_widgets), ]; PageType::new_categorized(categories, None) } fn update_code_editor_line_number_mode_dropdown( dropdown: ViewHandle>, ctx: &mut ViewContext, ) { dropdown.update(ctx, |dropdown, ctx| { let values = [ CodeEditorLineNumberMode::Absolute, CodeEditorLineNumberMode::Relative, ]; let current_value = *AppEditorSettings::as_ref(ctx) .code_editor_line_number_mode .value(); let selected_index = values .iter() .position(|val| *val == current_value) .unwrap_or(0); dropdown.set_items( values .into_iter() .map(|val| { DropdownItem::new( val.dropdown_item_label(), FeaturesPageAction::SetCodeEditorLineNumberMode(val), ) }) .collect(), ctx, ); dropdown.set_selected_by_index(selected_index, ctx); }); } fn update_ctrl_tab_behavior_dropdown( dropdown: ViewHandle>, ctx: &mut ViewContext, ) { dropdown.update(ctx, |dropdown, ctx| { let values = vec![ CtrlTabBehavior::ActivatePrevNextTab, CtrlTabBehavior::CycleMostRecentSession, CtrlTabBehavior::CycleMostRecentTab, ]; let current_value = *KeysSettings::as_ref(ctx).ctrl_tab_behavior; let selected_index = values .iter() .position(|val| *val == current_value) .unwrap_or_else(|| { log::error!( "Could not find current Ctrl-Tab behavior value in dropdown option list" ); 0 }); dropdown.set_items( values .into_iter() .map(|val| { DropdownItem::new( val.as_dropdown_label(), FeaturesPageAction::SetCtrlTabBehavior(val), ) }) .collect(), ctx, ); dropdown.set_selected_by_index(selected_index, ctx); }); } fn update_new_tab_placement_dropdown( dropdown: ViewHandle>, ctx: &mut ViewContext, ) { dropdown.update(ctx, |dropdown, ctx| { let values = vec![ NewTabPlacement::AfterCurrentTab, NewTabPlacement::AfterAllTabs, ]; let current_value = TabSettings::as_ref(ctx).new_tab_placement; let selected_index = values .iter() .position(|val| *val == current_value) .unwrap_or_else(|| { log::error!( "Could not find current NewTabPlacement value in dropdown option list" ); 0 }); dropdown.set_items( values .into_iter() .map(|val| { DropdownItem::new( Self::new_tab_placement_dropdown_item_label(val), FeaturesPageAction::SetNewTabPlacement(val), ) }) .collect(), ctx, ); dropdown.set_selected_by_index(selected_index, ctx); }); } fn handle_hotkey_settings_update(&mut self, ctx: &mut ViewContext) { let global_hotkey_mode = KeysSettings::handle(ctx).read(ctx, |settings, ctx| settings.global_hotkey_mode(ctx)); // Update the keybinding editor if it is already open. match global_hotkey_mode { GlobalHotkeyMode::Disabled => (), GlobalHotkeyMode::QuakeMode => { self.quake_mode_keybinding .current_binding .clone_from(&KeysSettings::as_ref(ctx).quake_mode_settings.keybinding); self.reset_quake_mode_keybinding_editor(); } GlobalHotkeyMode::ActivationHotkey => { self.activation_hotkey_keybinding .current_binding .clone_from(&KeysSettings::as_ref(ctx).activation_hotkey_keybinding); self.reset_activation_hotkey_keybinding_editor(); } } // the selected item needs to update if the settings gets changed via the command palette self.global_hotkey_dropdown.update(ctx, |dropdown, ctx| { dropdown.set_selected_by_name(global_hotkey_mode.as_dropdown_label(), ctx); ctx.notify(); }); ctx.notify(); } fn on_display_count_model_changed( &mut self, display_count: ModelHandle, ctx: &mut ViewContext, ) { self.quake_mode_pin_screen_dropdown .update(ctx, |dropdown, ctx| { init_display_count_dropdown( &KeysSettings::as_ref(ctx) .quake_mode_settings .value() .clone(), display_count.as_ref(ctx).num_displays(), dropdown, ctx, ); ctx.notify(); }); } pub fn handle_height_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext) { match event { EditorEvent::Blurred | EditorEvent::Enter => self.set_height_ratio(ctx), EditorEvent::Escape => ctx.emit(FeaturesSettingsPageEvent::FocusModal), _ => {} } } pub fn handle_width_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext) { match event { EditorEvent::Blurred | EditorEvent::Enter => self.set_width_ratio(ctx), EditorEvent::Escape => ctx.emit(FeaturesSettingsPageEvent::FocusModal), _ => {} } } pub fn handle_block_size_editor_event( &mut self, event: &EditorEvent, ctx: &mut ViewContext, ) { match event { EditorEvent::Edited(_) => { let buffer_text = self .max_block_size_input_editor .as_ref(ctx) .buffer_text(ctx); if let Ok(input) = buffer_text.parse::() { if (MIN_MAX_GRID_SIZE..=max_max_grid_size()).contains(&input) { self.valid_max_block_size = true; ctx.notify(); return; } } self.valid_max_block_size = false; ctx.notify(); } EditorEvent::Enter | EditorEvent::Blurred => self.set_max_block_size(ctx), EditorEvent::Escape => ctx.emit(FeaturesSettingsPageEvent::FocusModal), _ => {} } } fn set_max_block_size(&mut self, ctx: &mut ViewContext) { let buffer_text = self .max_block_size_input_editor .as_ref(ctx) .buffer_text(ctx); if let Ok(input) = buffer_text.parse::() { // Block sizes need to fall within the range limit let new_size = input.clamp(MIN_MAX_GRID_SIZE, max_max_grid_size()); if new_size != input { self.max_block_size_input_editor.update(ctx, |editor, ctx| { editor.set_buffer_text(&format!("{new_size}"), ctx); }); }; // If the input is not a new value, do nothing let current_size = *TerminalSettings::as_ref(ctx).maximum_grid_size.value(); if current_size == new_size { return; } TerminalSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings.maximum_grid_size.set_value(new_size, ctx)); }); } else { // Any invalid input should reset the input back to the last known value self.max_block_size_input_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( &format!( "{}", *TerminalSettings::handle(ctx) .as_ref(ctx) .maximum_grid_size .value() ), ctx, ); }); } } fn handle_mouse_scroll_input_editor_event( &mut self, event: &EditorEvent, ctx: &mut ViewContext, ) { match event { EditorEvent::Edited(_) => { let buffer_text = self.mouse_scroll_input_editor.as_ref(ctx).buffer_text(ctx); if buffer_text.parse::().is_ok_and(|parsed_multiplier| { (MIN_MOUSE_SCROLL_MULTIPLIER..=MAX_MOUSE_SCROLL_MULTIPLIER) .contains(&parsed_multiplier) }) { self.valid_mouse_scroll_multiplier = true; ctx.notify(); } else { self.valid_mouse_scroll_multiplier = false; ctx.notify(); } } EditorEvent::Enter | EditorEvent::Blurred => self.set_mouse_scroll_multiplier(ctx), EditorEvent::Escape => ctx.emit(FeaturesSettingsPageEvent::FocusModal), _ => {} } } fn set_mouse_scroll_multiplier(&mut self, ctx: &mut ViewContext) { let user_input = self.mouse_scroll_input_editor.as_ref(ctx).buffer_text(ctx); let scroll_settings = ScrollSettings::handle(ctx); if let Ok(new_multiplier) = user_input.parse::() { let constrained_multiplier = new_multiplier.clamp(MIN_MOUSE_SCROLL_MULTIPLIER, MAX_MOUSE_SCROLL_MULTIPLIER); if constrained_multiplier != new_multiplier { self.mouse_scroll_input_editor.update(ctx, |editor, ctx| { editor.set_buffer_text(format!("{constrained_multiplier}").as_str(), ctx); }); } if *scroll_settings.as_ref(ctx).mouse_scroll_multiplier.value() == constrained_multiplier { return; } scroll_settings.update(ctx, |settings, ctx| { let _ = settings .mouse_scroll_multiplier .set_value(constrained_multiplier, ctx); }); } else { // Fall back to the current setting value. self.mouse_scroll_input_editor.update(ctx, |editor, ctx| { editor.set_buffer_text( format!( "{}", scroll_settings.as_ref(ctx).mouse_scroll_multiplier.value() ) .as_str(), ctx, ); }) } } fn set_height_ratio(&mut self, ctx: &mut ViewContext) { let user_input = self.quake_mode_height_editor.as_ref(ctx).buffer_text(ctx); if let Ok(num) = user_input.parse() { KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { keys_settings.set_quake_mode_width_or_height_and_write_to_user_defaults( None, Some(num), ctx, ) }) } } fn set_width_ratio(&mut self, ctx: &mut ViewContext) { let user_input = self.quake_mode_width_editor.as_ref(ctx).buffer_text(ctx); if let Ok(num) = user_input.parse::() { KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { keys_settings.set_quake_mode_width_or_height_and_write_to_user_defaults( Some(num as u8), None, ctx, ); }) } } fn reset_size_percentage(&mut self, ctx: &mut ViewContext) { let default_size_percentage = DEFAULT_QUAKE_MODE_SIZE_PERCENTAGES .get( &KeysSettings::as_ref(ctx) .quake_mode_settings .value() .active_pin_position, ) .expect("Pin position should exist in default size percentages"); self.quake_mode_height_editor.update(ctx, |editor, ctx| { editor.set_buffer_text(&format!("{}", default_size_percentage.height), ctx); }); self.quake_mode_width_editor.update(ctx, |editor, ctx| { editor.set_buffer_text(&format!("{}", default_size_percentage.width), ctx); }); KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { keys_settings.set_quake_mode_width_or_height_and_write_to_user_defaults( Some(default_size_percentage.width), /*width percentages*/ Some(default_size_percentage.height), /*height percentages*/ ctx, ); }); ctx.notify(); } fn reset_quake_mode_keybinding_editor(&mut self) { self.quake_mode_keybinding .unsaved_binding .clone_from(&self.quake_mode_keybinding.current_binding); self.quake_mode_keybinding_editor_state = KeybindingEditorState::Idle; } fn enable_quake_mode_global_shortcut(&self, app: &mut AppContext) { if let Some(shortcut) = &self.quake_mode_keybinding.current_binding { app.register_global_shortcut( shortcut.clone(), "root_view:toggle_quake_mode_window", self.global_resource_handles.clone(), ); } } fn disable_quake_mode_global_shortcut(&self, app: &mut AppContext) { if let Some(shortcut) = &self.quake_mode_keybinding.current_binding { app.unregister_global_shortcut(shortcut); } } fn reset_activation_hotkey_keybinding_editor(&mut self) { self.activation_hotkey_keybinding .unsaved_binding .clone_from(&self.activation_hotkey_keybinding.current_binding); self.activation_hotkey_keybinding_editor_state = KeybindingEditorState::Idle; } fn enable_activation_hotkey_global_shortcut(&self, app: &mut AppContext) { if let Some(shortcut) = &self.activation_hotkey_keybinding.current_binding { app.register_global_shortcut( shortcut.clone(), "root_view:show_or_hide_non_quake_mode_windows", (), ); } } fn disable_activation_hotkey_global_shortcut(&self, app: &mut AppContext) { if let Some(shortcut) = &self.activation_hotkey_keybinding.current_binding { app.unregister_global_shortcut(shortcut); } } /// A lot of state needs to get updated here: /// 1. Settings /// 2. AppContext::disabled_key_bindings_windows /// 3. AppContext::global_shortcuts /// 4. self.quake_mode_keybinding and self.activation_hotkey_keybinding /// 5. quake_mode_keybinding_editor_state and activation_hotkey_keybinding_editor_state fn set_global_hotkey_mode(&mut self, mode: &GlobalHotkeyMode, ctx: &mut ViewContext) { ctx.enable_key_bindings_dispatching(); self.reset_quake_mode_keybinding_editor(); self.reset_activation_hotkey_keybinding_editor(); ctx.notify(); KeysSettings::handle(ctx).update(ctx, |keys_settings, ctx| { keys_settings.set_global_hotkey_mode_and_write_to_user_defaults(mode, ctx); }); self.disable_quake_mode_global_shortcut(ctx); self.disable_activation_hotkey_global_shortcut(ctx); match mode { GlobalHotkeyMode::Disabled => {} GlobalHotkeyMode::QuakeMode => { self.enable_quake_mode_global_shortcut(ctx); } GlobalHotkeyMode::ActivationHotkey => { self.enable_activation_hotkey_global_shortcut(ctx); } } } pub(super) fn refresh_preferred_graphics_backend_dropdown( &mut self, ctx: &mut ViewContext, ) { self.graphics_backend_dropdown.update(ctx, |dropdown, ctx| { if let Some(window) = ctx.windows().platform_window(ctx.window_id()) { let mut items = vec![DropdownItem::new( "Default", FeaturesPageAction::SetPreferredGraphicsBackend(None), )]; items.extend(window.supported_backends().into_iter().map(|backend| { DropdownItem::new( backend.to_label(), FeaturesPageAction::SetPreferredGraphicsBackend(Some(backend)), ) })); dropdown.set_items(items, ctx); } let gpu_settings = GPUSettings::as_ref(ctx); dropdown.set_selected_by_name( gpu_settings .preferred_backend .map(|backend| backend.to_label()) .unwrap_or("Default"), ctx, ); }); } /// Updates the state of the tab behavior dropdown based on the current tab behavior. fn refresh_tab_behavior_dropdown(&mut self, ctx: &mut ViewContext) { self.tab_behavior_dropdown.update(ctx, |dropdown, ctx| { let mut items = vec![ DropdownItem::new( TabBehavior::Completions.dropdown_item_label(), FeaturesPageAction::SetTabBehavior(TabBehavior::Completions), ), DropdownItem::new( TabBehavior::Autosuggestions.dropdown_item_label(), FeaturesPageAction::SetTabBehavior(TabBehavior::Autosuggestions), ), ]; // We only show the "User defined" label if the user has manually edited their // Tab keybinding to neither completions nor autosuggestions. It's not a // selectable option from the dropdown. if matches!(*self.tab_behavior, TabBehavior::UserDefined) { items.push(DropdownItem::new( TabBehavior::UserDefined.dropdown_item_label(), FeaturesPageAction::SetTabBehavior(TabBehavior::UserDefined), )); } dropdown.set_items(items, ctx); dropdown.set_selected_by_name(self.tab_behavior.dropdown_item_label(), ctx); }); } /// Retrieves the current completions/autosuggestions keybindings, determines the /// current tab behavior, and updates the state on the view. /// /// Note that this method does not update the state of the tab behavior dropdown. /// To update that, use `refresh_tab_behavior_dropdown`. fn refresh_tab_behavior_state(&mut self, ctx: &mut ViewContext) { // Get the current keybindings for opening the completions menu and inserting // autosuggestions. *self.completions_keystroke = keybinding_name_to_display_string(OPEN_COMPLETIONS_KEYBINDING_NAME, ctx) .unwrap_or_default(); *self.autosuggestions_keystroke = keybinding_name_to_display_string(ACCEPT_AUTOSUGGESTION_KEYBINDING_NAME, ctx) .unwrap_or_default(); // Determine the current tab behavior based on the user's keybindings. *self.tab_behavior = if self.completions_keystroke.to_uppercase() == TAB_KEYSTROKE_STR { TabBehavior::Completions } else if self.autosuggestions_keystroke.to_uppercase() == TAB_KEYSTROKE_STR { TabBehavior::Autosuggestions } else { TabBehavior::UserDefined }; } /// Sets the keybindings for opening the completions menu and inserting autosuggestions /// when the user selects an option from the tab key behavior dropdown. fn set_tab_behavior_setting( &mut self, tab_behavior: &TabBehavior, ctx: &mut ViewContext, ) { match tab_behavior { TabBehavior::Completions => { // Set the binding for opening completions to Tab. If accepting autosuggestions // was previously bound to Tab, unbind it. We unbind because autosuggestions // are accepted via right arrow by default, so we don't need another binding. if *self.autosuggestions_keystroke == TAB_KEYSTROKE_STR { reset_keybinding_to_default(ACCEPT_AUTOSUGGESTION_KEYBINDING_NAME, ctx); } set_custom_keybinding(OPEN_COMPLETIONS_KEYBINDING_NAME, &TAB_KEYSTROKE, ctx); } TabBehavior::Autosuggestions => { // Set the binding for accepting autosuggestions to Tab. If opening completions // was previously bound to Tab, set it to ctrl-space. if *self.completions_keystroke == TAB_KEYSTROKE_STR { set_custom_keybinding( OPEN_COMPLETIONS_KEYBINDING_NAME, &CTRL_SPACE_KEYSTROKE, ctx, ); } set_custom_keybinding(ACCEPT_AUTOSUGGESTION_KEYBINDING_NAME, &TAB_KEYSTROKE, ctx); } // No-op if the user selects "User defined" from the dropdown. TabBehavior::UserDefined => {} } self.refresh_tab_behavior_state(ctx); } fn new_tab_placement_dropdown_item_label(val: NewTabPlacement) -> &'static str { match val { NewTabPlacement::AfterAllTabs => "After all tabs", NewTabPlacement::AfterCurrentTab => "After current tab", } } fn set_new_tab_placement(&mut self, value: &NewTabPlacement, ctx: &mut ViewContext) { let _ = TabSettings::handle(ctx).update(ctx, |tab_settings, ctx| { tab_settings.new_tab_placement.set_value(*value, ctx) }); } fn update_osc52_clipboard_access_dropdown( dropdown: ViewHandle>, ctx: &mut ViewContext, ) { dropdown.update(ctx, |dropdown, ctx| { let values = vec![ Osc52ClipboardAccess::Deny, Osc52ClipboardAccess::WriteOnly, Osc52ClipboardAccess::ReadWrite, ]; let current_value = *TerminalSettings::as_ref(ctx).osc52_clipboard_access; let selected_index = values .iter() .position(|val| *val == current_value) .unwrap_or(0); dropdown.set_items( values .into_iter() .map(|val| { DropdownItem::new( val.as_dropdown_label(), FeaturesPageAction::SetOsc52ClipboardAccess(val), ) }) .collect(), ctx, ); dropdown.set_selected_by_index(selected_index, ctx); }); } fn update_default_session_mode_dropdown( dropdown: ViewHandle>, ctx: &mut ViewContext, ) { dropdown.update( ctx, |dropdown: &mut FilterableDropdown, ctx| { let is_ai_enabled = AISettings::as_ref(ctx).is_any_ai_enabled(ctx); if is_ai_enabled { dropdown.set_enabled(ctx); } else { dropdown.set_disabled(ctx); } let ai_settings = AISettings::as_ref(ctx); let current_mode = ai_settings.default_session_mode(ctx); let current_tab_config_path = ai_settings.default_tab_config_path().to_string(); // Build items: built-in modes (skip TabConfig since configs are listed individually, // and skip DockerSandbox when its feature flag is disabled). let docker_sandbox_enabled = FeatureFlag::LocalDockerSandbox.is_enabled(); let mut items: Vec> = DefaultSessionMode::iter() .filter(|val| *val != DefaultSessionMode::TabConfig) .filter(|val| { *val != DefaultSessionMode::DockerSandbox || docker_sandbox_enabled }) .map(|val| { DropdownItem::new( val.display_name(), FeaturesPageAction::SetDefaultSessionMode(val), ) }) .collect(); // Append each loaded tab config let tab_configs = GalaxyConfig::as_ref(ctx).tab_configs().to_vec(); for config in &tab_configs { if let Some(path) = &config.source_path { items.push(DropdownItem::new( config.name.clone(), FeaturesPageAction::SetDefaultTabConfig( path.to_string_lossy().into_owned(), ), )); } } dropdown.set_items(items, ctx); // Select the currently active item. let selected_name = match current_mode { DefaultSessionMode::TabConfig => tab_configs .iter() .find(|c| { c.source_path .as_ref() .is_some_and(|p| p.to_string_lossy() == current_tab_config_path) }) .map(|c| c.name.clone()) .unwrap_or_else(|| DefaultSessionMode::Terminal.display_name().to_string()), other => other.display_name().to_string(), }; dropdown.set_selected_by_name(&selected_name, ctx); }, ); } fn set_default_session_mode( &mut self, value: &DefaultSessionMode, ctx: &mut ViewContext, ) { AISettings::handle(ctx).update(ctx, |ai_settings, ctx| { report_if_error!(ai_settings .default_session_mode_internal .set_value(*value, ctx)); }); } /// This function renders the component that allows the user to record a keybinding for the /// global hotkey. The default is to display the current keybinding, called the "summary". /// Once clicked, it tells the view to disable keybinding dispatching in order to record a /// keybinding. From there it can be saved or cancelled. #[allow(clippy::too_many_arguments)] fn render_keybinding_editor_row( &self, outer_button_mouse_state: Arc>, cancel_button_mouse_state: Arc>, save_button_mouse_state: Arc>, keybinding_editor_state: KeybindingEditorState, keybinding: &KeyBindingModifyingState, editor_clicked_action: FeaturesPageAction, cancel_action: FeaturesPageAction, save_action: FeaturesPageAction, record_keystroke: T, appearance: &Appearance, ) -> Box { Hoverable::new(outer_button_mouse_state, |state| { let background: Option = if state.is_hovered() { Some(appearance.theme().accent().with_opacity(40).into()) } else { None }; let container = Container::new(if keybinding_editor_state == KeybindingEditorState::Idle { self.render_summary( keybinding, keybinding_editor_state, record_keystroke, appearance, ) } else { self.render_clicked( cancel_button_mouse_state, save_button_mouse_state, keybinding, keybinding_editor_state, cancel_action, save_action, record_keystroke, appearance, ) }) .with_padding_left(10.) .with_padding_right(10.) .with_padding_top(10.) .with_margin_right(4.) .with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.))); if let Some(background) = background { container.with_background(background).finish() } else { container.finish() } }) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(editor_clicked_action.clone()); }) .finish() } fn render_dropdown( &self, drop_down: &ViewHandle>, width: f32, ) -> Box { ConstrainedBox::new(ChildView::new(drop_down).finish()) .with_width(width) .finish() } fn render_quake_width_height_editor( &self, quake_mode_settings: &QuakeModeSettings, appearance: &Appearance, ) -> Box { let theme = appearance.theme(); let editor_style = UiComponentStyles { width: Some(40.), padding: Some(Coords::uniform(5.)), background: Some(theme.surface_2().into()), ..Default::default() }; Flex::column() .with_child( Container::new( Flex::row() .with_child( Container::new( Text::new_inline( "Width %", appearance.ui_font_family(), appearance.ui_font_size(), ) .with_color(theme.active_ui_text_color().into()) .finish(), ) .with_padding_right(8.5) .finish(), ) .with_child( Dismiss::new( appearance .ui_builder() .text_input(self.quake_mode_width_editor.clone()) .with_style(editor_style) .build() .finish(), ) .on_dismiss(|ctx, _app| { ctx.dispatch_typed_action( FeaturesPageAction::QuakeEditorSetWidthPercentage, ) }) .finish(), ) .with_cross_axis_alignment(CrossAxisAlignment::Center) .finish(), ) .with_padding_top(5.) .finish(), ) .with_child( Container::new( Flex::row() .with_child( Container::new( Text::new_inline( "Height %", appearance.ui_font_family(), appearance.ui_font_size(), ) .with_color(theme.active_ui_text_color().into()) .finish(), ) .with_padding_right(5.) .finish(), ) .with_child( Dismiss::new( appearance .ui_builder() .text_input(self.quake_mode_height_editor.clone()) .with_style(editor_style) .build() .finish(), ) .on_dismiss(|ctx, _app| { ctx.dispatch_typed_action( FeaturesPageAction::QuakeEditorSetHeightPercentage, ) }) .finish(), ) .with_cross_axis_alignment(CrossAxisAlignment::Center) .finish(), ) .with_padding_top(5.) .finish(), ) .with_child({ let button = build_reset_button( appearance, self.button_mouse_states .quake_mode_width_height_reset .clone(), quake_mode_settings.size_changed_from_default(), ); button .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::QuakeEditorResetWidthHeight); }) .finish() }) .with_cross_axis_alignment(CrossAxisAlignment::Start) .finish() } fn render_quake_mode_pin_window_toggle_row( &self, quake_mode_settings: &QuakeModeSettings, appearance: &Appearance, ) -> Box { Container::new( Flex::row() .with_child( appearance .ui_builder() .checkbox( self.button_mouse_states.quake_mode_pin_window_check.clone(), None, ) .check(quake_mode_settings.hide_window_when_unfocused) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::QuakeEditorTogglePinWindow, ) }) .finish(), ) .with_child( appearance .ui_builder() .span("Autohides on loss of keyboard focus") .build() .with_margin_left(5.) .finish(), ) .with_cross_axis_alignment(CrossAxisAlignment::Center) .finish(), ) .with_margin_bottom(2.) .finish() } fn render_quake_mode_position_row( &self, quake_mode_settings: &QuakeModeSettings, appearance: &Appearance, ) -> Box { Flex::row() .with_child( Container::new( self.render_dropdown( &self.quake_mode_pin_position_dropdown, QUAKE_DROPDOWN_WIDTH, ), ) .with_padding_right(30.) .finish(), ) .with_child( Container::new( self.render_dropdown( &self.quake_mode_pin_screen_dropdown, QUAKE_DROPDOWN_WIDTH, ), ) .with_padding_right(30.) .finish(), ) .with_child(self.render_quake_width_height_editor(quake_mode_settings, appearance)) .with_cross_axis_alignment(CrossAxisAlignment::Start) .finish() } fn render_long_running_notifications_setting( &self, notification_settings: &Notifications, appearance: &Appearance, ) -> Box { let theme = appearance.theme(); let font_size = appearance.ui_font_size() - 2.; let font_color = if notification_settings.is_long_running_enabled { theme.active_ui_text_color() } else { theme.nonactive_ui_text_color() }; let editor_style = UiComponentStyles { width: Some(appearance.ui_font_size() * 3.), height: Some(appearance.ui_font_size() * 2.), padding: Some(Coords::uniform(5.)), background: Some(theme.surface_2().into()), ..Default::default() }; Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Container::new( Align::new( appearance .ui_builder() .checkbox( self.button_mouse_states .long_running_notifications_checkbox .clone(), None, ) .check(notification_settings.is_long_running_enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::ToggleLongRunningNotifications, ); }) .finish(), ) .top_left() .finish(), ) .with_margin_right(NOTIFICATION_CHECKBOX_MARGIN_RIGHT) .finish(), ) .with_child( Container::new( Align::new( Text::new_inline( "When a command takes longer than", appearance.ui_font_family(), font_size, ) .with_color(font_color.into()) .finish(), ) .finish(), ) .finish(), ) .with_child( Container::new( Dismiss::new( appearance .ui_builder() .text_input(self.notifications_long_running_threshold_editor.clone()) .with_style(editor_style) .build() .finish(), ) .on_dismiss(|ctx, _app| { ctx.dispatch_typed_action( FeaturesPageAction::SetLongRunningNotificationThreshold, ) }) .finish(), ) .with_margin_right(NOTIFICATION_EDITOR_MARGIN) .with_margin_left(NOTIFICATION_EDITOR_MARGIN) .finish(), ) .with_child( Container::new( Align::new( Text::new_inline( "seconds to complete", appearance.ui_font_family(), font_size, ) .with_color(font_color.into()) .finish(), ) .finish(), ) .finish(), ) .finish() } fn render_notification_toggle( &self, is_enabled: bool, text: &str, toggle_action: FeaturesPageAction, mouse_state: Arc>, appearance: &Appearance, ) -> Box { let text = text.to_string(); let font_size = appearance.ui_font_size() - 2.; let font_color = if is_enabled { appearance.theme().active_ui_text_color() } else { appearance.theme().nonactive_ui_text_color() }; Container::new( Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Container::new( Align::new( appearance .ui_builder() .checkbox(mouse_state.clone(), None) .check(is_enabled) .build() .on_click(move |ctx, _, _| { let toggle_action = toggle_action.clone(); ctx.dispatch_typed_action(toggle_action); }) .finish(), ) .top_left() .finish(), ) .with_margin_right(NOTIFICATION_CHECKBOX_MARGIN_RIGHT) .finish(), ) .with_child( Container::new( Align::new( Text::new_inline(text, appearance.ui_font_family(), font_size) .with_color(font_color.into()) .finish(), ) .finish(), ) .finish(), ) .finish(), ) .finish() } fn render_summary( &self, keybinding: &KeyBindingModifyingState, keybinding_editor_state: KeybindingEditorState, record_keystroke: T, appearance: &Appearance, ) -> Box { let element = Container::new( Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Shrinkable::new( 2., Align::new( Text::new_inline("Keybinding", appearance.ui_font_family(), 13.) .with_color(appearance.theme().active_ui_text_color().into()) .finish(), ) .left() .finish(), ) .finish(), ) .with_child( Shrinkable::new( 1., Align::new(if let Some(keybinding) = &keybinding.unsaved_binding { appearance .ui_builder() .keyboard_shortcut(keybinding) .build() .finish() } else { appearance .ui_builder() .paragraph("Click to set global hotkey".to_string()) .build() .finish() }) .finish(), ) .finish(), ) .finish(), ) .with_padding_bottom(10.) .finish(); match keybinding_editor_state { KeybindingEditorState::Recording => { EventHandler::new(element) .on_keydown(move |ctx, _, keystroke| { let mut keystroke = keystroke.clone(); // For global hotkey we don't support meta keys. if keystroke.meta { keystroke.alt = true; keystroke.meta = false; } record_keystroke(ctx, keystroke); DispatchEventResult::StopPropagation }) .finish() } _ => element, } } /// This renders the keybinding editor once it's clicked, and is listening to record a new /// keybinding. #[allow(clippy::too_many_arguments)] fn render_clicked( &self, cancel_button_mouse_state: Arc>, save_button_mouse_state: Arc>, keybinding: &KeyBindingModifyingState, keybinding_editor_state: KeybindingEditorState, cancel_action: FeaturesPageAction, save_action: FeaturesPageAction, record_keystroke: T, appearance: &Appearance, ) -> Box { let cancel_button = appearance .ui_builder() .button(ButtonVariant::Text, cancel_button_mouse_state) .with_style(UiComponentStyles { padding: Some(Coords::default().right(10.)), ..Default::default() }) .with_text_label("Cancel".to_string()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(cancel_action.clone()); }) .finish(); let save_button = Shrinkable::new( 1., appearance .ui_builder() .button(ButtonVariant::Text, save_button_mouse_state) .with_text_label("Save".to_string()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(save_action.clone()); }) .finish(), ) .finish(); Container::new( Flex::column() .with_child(self.render_summary( keybinding, keybinding_editor_state, record_keystroke, appearance, )) .with_child( Flex::row() .with_child( Shrinkable::new( 2., Align::new( Text::new_inline( "Press new keyboard shortcut", appearance.ui_font_family(), 13., ) .with_color(appearance.theme().active_ui_text_color().into()) .finish(), ) .left() .finish(), ) .finish(), ) .with_child( Shrinkable::new( 1., Align::new( Flex::row() .with_child(cancel_button) .with_child(save_button) .finish(), ) .left() .finish(), ) .finish(), ) .finish(), ) .finish(), ) .with_padding_bottom(10.) .finish() } fn render_change_keybinding_button( &self, keybinding_name: &str, appearance: &Appearance, ) -> Box { let keybinding_name = keybinding_name.to_string(); Hoverable::new( self.button_mouse_states.change_keybinding.clone(), |state| { let button_color = appearance.theme().accent().into_solid(); let mut border = Border::bottom(1.); if state.is_hovered() { border = border.with_border_color(button_color); } Container::new( Text::new_inline("Change keybinding", appearance.ui_font_family(), 12.) .with_color(button_color) .finish(), ) .with_border(border) .finish() }, ) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::SearchForKeybinding( keybinding_name.clone(), )); }) .with_cursor(Cursor::PointingHand) .finish() } fn render_setting_subgroup_item( &self, appearance: &Appearance, local_only_icon_state: LocalOnlyIconState, switch: Box, label_text: String, ) -> Box { Container::new( Flex::row() .with_child( Shrinkable::new( 1.0, Container::new( Align::new(render_body_item_label::( label_text, None, None, local_only_icon_state, ToggleState::Enabled, appearance, )) .left() .finish(), ) .with_padding_top(5.) .finish(), ) .finish(), ) .with_child( Container::new(switch) .with_padding_right(TOGGLE_BUTTON_RIGHT_PADDING) .finish(), ) .finish(), ) .with_padding_top(4.) .with_padding_bottom(4.) .finish() } } impl SettingsPageMeta for FeaturesPageView { fn section() -> SettingsSection { SettingsSection::Features } fn should_render(&self, _ctx: &AppContext) -> bool { true } fn on_page_selected(&mut self, _: bool, ctx: &mut ViewContext) { // On MacOS, we rely on [`galaxyui::platform::AppCallbacks::on_screen_changed`] to update and // notify on the [`DisplayCount`] model. However, no mechanism exists on Linux to trigger // that callback. As a workaround, we check for updates here where quake mode is // configured. #[cfg(any(target_os = "linux", target_os = "freebsd"))] DisplayCount::handle(ctx).update(ctx, |display_count, ctx| { display_count.0 = ctx.windows().display_count(); ctx.notify(); }); // Fetch the latest tab behavior state in case the user changed their keybindings // since we last loaded this page. self.refresh_tab_behavior_state(ctx); self.refresh_tab_behavior_dropdown(ctx); self.refresh_preferred_graphics_backend_dropdown(ctx); } fn update_filter(&mut self, query: &str, ctx: &mut ViewContext) -> MatchData { self.page.update_filter(query, ctx) } fn scroll_to_widget(&mut self, widget_id: &'static str) { self.page.scroll_to_widget(widget_id) } fn clear_highlighted_widget(&mut self) { self.page.clear_highlighted_widget(); } } impl From> for SettingsPageViewHandle { fn from(view_handle: ViewHandle) -> Self { SettingsPageViewHandle::Features(view_handle) } } pub(super) fn render_group( children: impl IntoIterator>, appearance: &Appearance, ) -> Box { let bar = Container::new( ConstrainedBox::new(Empty::new().finish()) .with_width(4.) .finish(), ) .with_background(appearance.theme().outline()) .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.))) .with_margin_right(8.) .with_margin_left(8.) .finish(); Container::new( Flex::row() .with_main_axis_size(MainAxisSize::Max) .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_child(bar) .with_child( Shrinkable::new(1., Flex::column().with_children(children).finish()).finish(), ) .finish(), ) .with_margin_top(-4.) .with_margin_bottom(HEADER_PADDING) .finish() } fn init_global_hotkey_dropdown( hotkey_mode: GlobalHotkeyMode, dropdown: &mut Dropdown, ctx: &mut ViewContext>, ) { let items = vec![ DropdownItem::new( GlobalHotkeyMode::Disabled.as_dropdown_label(), FeaturesPageAction::SetGlobalHotkeyMode(GlobalHotkeyMode::Disabled), ), DropdownItem::new( GlobalHotkeyMode::QuakeMode.as_dropdown_label(), FeaturesPageAction::SetGlobalHotkeyMode(GlobalHotkeyMode::QuakeMode), ), DropdownItem::new( GlobalHotkeyMode::ActivationHotkey.as_dropdown_label(), FeaturesPageAction::SetGlobalHotkeyMode(GlobalHotkeyMode::ActivationHotkey), ), ]; dropdown.set_items(items, ctx); dropdown.set_selected_by_name(hotkey_mode.as_dropdown_label(), ctx); } fn init_display_count_dropdown( quake_mode_settings: &QuakeModeSettings, display_count: usize, dropdown: &mut Dropdown, ctx: &mut ViewContext>, ) { let no_preference = DropdownItem::new( "Active Screen", //|| { FeaturesPageAction::QuakeEditorSetPinScreen(None), //} ); let mut items = vec![no_preference]; items.push(DropdownItem::new( format!("{}", DisplayIdx::Primary), //move || FeaturesPageAction::QuakeEditorSetPinScreen(Some(DisplayIdx::Primary)), )); (1..display_count).for_each(|idx| { items.push(DropdownItem::new( format!("{}", DisplayIdx::External(idx - 1)), //move || { FeaturesPageAction::QuakeEditorSetPinScreen(Some(DisplayIdx::External(idx - 1))), //}, )); }); dropdown.set_items(items, ctx); match quake_mode_settings.pin_screen { Some(idx) if idx.is_valid_given_display_count(display_count) => { dropdown.set_selected_by_name(format!("{idx}"), ctx) } _ => dropdown.set_selected_by_name("Active Screen", ctx), }; } #[derive(Default)] struct NativeRedirectWidget { additional_info_link: MouseStateHandle, switch_state: SwitchStateHandle, } impl SettingsWidget for NativeRedirectWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "link open desktop native redirect url intent deep link deeplink" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Open links in desktop app".into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, tooltip_override_text: Some( "Automatically open links in desktop app whenever possible.".into(), ), }), LocalOnlyIconState::for_setting( UserNativeRedirectPreference::storage_key(), UserNativeRedirectPreference::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(matches!( NativePreferenceSettings::as_ref(app) .user_native_redirect_preference .value(), UserNativePreference::Desktop )) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleOpenLinksInDesktopApp); }) .finish(), None, ) } } #[derive(Default)] struct SessionRestorationWidget { switch_state: SwitchStateHandle, additional_info_link: MouseStateHandle, docs_link: MouseStateHandle, } impl SettingsWidget for SessionRestorationWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "restore session window tab pane startup" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let switch = ui_builder .switch(self.switch_state.clone()) .check(*GeneralSettings::as_ref(app).restore_session) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleRestoreSession); }) .finish(); let labeled_switch = render_body_item::( "Restore windows, tabs, and panes on startup".into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( "https://docs.warp.dev/terminal/sessions/session-restoration".into(), )), secondary_text: None, tooltip_override_text: None, }), LocalOnlyIconState::for_setting( RestoreSession::storage_key(), RestoreSession::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, switch, None, ); if app.is_wayland() { let message = Text::new_inline( "Window positions won't be restored on Wayland. ", appearance.ui_font_family(), CONTENT_FONT_SIZE, ) .with_color(appearance.theme().disabled_ui_text_color().into()) .finish(); let link = ui_builder .link( "See docs.".to_owned(), Some("https://docs.warp.dev/terminal/sessions/session-restoration".to_owned()), None, self.docs_link.clone(), ) .soft_wrap(false) .build() .finish(); Flex::column() .with_children([ labeled_switch, Container::new( Flex::row() .with_children([message, link]) .with_main_axis_alignment(MainAxisAlignment::End) .finish(), ) .with_padding_bottom(HEADER_PADDING) .finish(), ]) .with_main_axis_size(MainAxisSize::Min) .finish() } else { labeled_switch } } } #[derive(Default)] struct SnackbarHeaderWidget { switch_state: SwitchStateHandle, additional_info_link: MouseStateHandle, } impl SettingsWidget for SnackbarHeaderWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "sticky command block header snackbar" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Show sticky command header".into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( "https://docs.warp.dev/terminal/blocks/sticky-command-header".into(), )), secondary_text: None, tooltip_override_text: None, }), LocalOnlyIconState::for_setting( SnackbarEnabled::storage_key(), SnackbarEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*BlockListSettings::as_ref(app).snackbar_enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleSnackbar) }) .finish(), None, ) } } #[derive(Default)] struct LinkTooltipWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for LinkTooltipWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "link tooltip click open" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Show tooltip on click on links".into(), None, LocalOnlyIconState::for_setting( LinkTooltip::storage_key(), LinkTooltip::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*GeneralSettings::as_ref(app).link_tooltip) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleLinkTooltip); }) .finish(), None, ) } } #[cfg(feature = "local_fs")] #[derive(Default)] struct ExternalEditorWidget {} #[cfg(feature = "local_fs")] impl SettingsWidget for ExternalEditorWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "editor open files markdown AI conversations layout pane tab" } fn render( &self, view: &Self::View, _appearance: &Appearance, _app: &AppContext, ) -> Box { ChildView::new(&view.external_editor_view).finish() } } #[derive(Default)] struct QuitWarningModalWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for QuitWarningModalWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "warning popup modal dialog quit logout log out close" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( "Show warning before quitting/logging out".into(), None, LocalOnlyIconState::for_setting( ShowWarningBeforeQuitting::storage_key(), ShowWarningBeforeQuitting::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*general_settings.show_warning_before_quitting) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleShowWarningBeforeQuitting); }) .finish(), None, ) } } #[derive(Default)] struct LoginItemWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for LoginItemWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "login item startup start mac windows app restart automatic" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); #[cfg(target_os = "macos")] let label = "Start Galaxy at login (requires macOS 13+)"; #[cfg(not(target_os = "macos"))] let label = "Start Galaxy at login"; render_body_item::( label.into(), None, LocalOnlyIconState::for_setting( LoginItem::storage_key(), LoginItem::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*general_settings.add_app_as_login_item) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleLoginItem); }) .finish(), None, ) } } #[derive(Default)] struct QuitWhenAllWindowsClosedWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for QuitWhenAllWindowsClosedWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "quit all windows closed" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( "Quit when all windows are closed".into(), None, LocalOnlyIconState::for_setting( QuitOnLastWindowClosed::storage_key(), QuitOnLastWindowClosed::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*general_settings.quit_on_last_window_closed) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleQuitOnLastWindowClosed); }) .finish(), None, ) } } #[derive(Default)] struct ShowChangelogWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for ShowChangelogWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "changelog updates" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let changelog_settings = ChangelogSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( "Show changelog toast after updates".into(), None, LocalOnlyIconState::for_setting( ShowChangelogAfterUpdate::storage_key(), ShowChangelogAfterUpdate::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*changelog_settings.show_changelog_after_update) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleShowChangelogAfterUpdate); }) .finish(), None, ) } } #[derive(Default)] struct MouseScrollMultiplierWidget { additional_info_link: MouseStateHandle, } impl SettingsWidget for MouseScrollMultiplierWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "mouse scroll wheel multiplier lines" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let border_color = match view.valid_mouse_scroll_multiplier { false => Some(themes::theme::Fill::error().into()), true => None, }; let input_field = appearance .ui_builder() .text_input(view.mouse_scroll_input_editor.clone()) .with_style(UiComponentStyles { width: Some(MOUSE_SCROLL_EDITOR_WIDTH), padding: Some(Coords { top: 4., bottom: 4., left: 6., right: 6., }), background: Some(appearance.theme().surface_2().into()), border_color, ..Default::default() }) .build() .finish(); let input_column = Flex::column() .with_children([ input_field, if view.valid_mouse_scroll_multiplier { Empty::new().finish() } else { appearance .ui_builder() .wrappable_text("Allowed Values: 1-20", true) .with_style(UiComponentStyles { font_color: Some(themes::theme::Fill::error().into_solid()), ..Default::default() }) .build() .finish() }, ]) .with_cross_axis_alignment(CrossAxisAlignment::End) .finish(); render_body_item::( "Lines scrolled by mouse wheel interval".into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, tooltip_override_text: Some( "Supports floating point values between 1 and 20.".to_string(), ), }), LocalOnlyIconState::for_setting( MouseScrollMultiplier::storage_key(), MouseScrollMultiplier::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, input_column, None, ) } } #[derive(Default)] struct AutoOpenCodeReviewPaneWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for AutoOpenCodeReviewPaneWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "oz auto open code review pane panel agent mode change first time accepted diff view conversation" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let general_settings = GeneralSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( "Auto open code review panel".into(), None, LocalOnlyIconState::for_setting( AutoOpenCodeReviewPaneOnFirstAgentChange::storage_key(), AutoOpenCodeReviewPaneOnFirstAgentChange::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*general_settings.auto_open_code_review_pane_on_first_agent_change) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleAutoOpenCodeReviewPane); }) .finish(), Some("When this setting is on, the code review panel will open on the first accepted diff of a conversation".into()), ) } } #[derive(Default)] struct DefaultTerminalWidget { link_state: MouseStateHandle, } impl SettingsWidget for DefaultTerminalWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "warp default terminal application" } fn render( &self, _view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let default_terminal = DefaultTerminal::as_ref(app); if default_terminal.is_warp_default() { ui_builder .wrappable_text("Galaxy is the default terminal", true) .with_style(UiComponentStyles { font_color: Some(appearance.theme().disabled_ui_text_color().into()), margin: Some(Coords::default().bottom(16.)), ..Default::default() }) .build() .finish() } else { ui_builder .link( "Make Galaxy the default terminal".to_string(), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action(FeaturesPageAction::MakeWarpDefaultTerminal); })), self.link_state.clone(), ) .build() .with_margin_bottom(16.) .finish() } } } #[derive(Default)] struct BlockLimitWidget {} impl SettingsWidget for BlockLimitWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "max block size lines maximum limit memory" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let border_color: Option = match view.valid_max_block_size { false => Some(themes::theme::Fill::error().into()), true => Default::default(), }; let input_field = appearance .ui_builder() .text_input(view.max_block_size_input_editor.clone()) .with_style(UiComponentStyles { width: Some(MAX_BLOCK_SIZE_INPUT_BOX_WIDTH), padding: Some(Coords { top: 4., bottom: 4., left: 6., right: 6., }), background: Some(appearance.theme().surface_2().into()), border_color, ..Default::default() }) .build() .finish(); render_body_item::( "Maximum rows in a block".into(), None, LocalOnlyIconState::for_setting( MaximumGridSize::storage_key(), MaximumGridSize::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, input_field, Some(block_maximum_rows_description()), ) } } #[derive(Default)] struct DesktopNotificationsWidget { additional_info_link: MouseStateHandle, switch_state: SwitchStateHandle, } impl SettingsWidget for DesktopNotificationsWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "desktop notifications" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let session_settings = SessionSettings::as_ref(app); let ui_builder = appearance.ui_builder(); let mut column = Flex::column(); column.add_child(render_body_item::( "Receive desktop notifications from Galaxy".into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl(NOTIFICATIONS_DOCS_URL.into())), secondary_text: None, tooltip_override_text: None, }), LocalOnlyIconState::for_setting( Notifications::storage_key(), Notifications::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(matches!( session_settings.notifications.mode, NotificationsMode::Enabled )) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleNotifications); }) .finish(), None, )); if matches!( session_settings.notifications.mode, NotificationsMode::Enabled ) { let toggles = vec![ view.render_notification_toggle( session_settings .notifications .is_agent_task_completed_enabled, "Notify when an agent completes a task", FeaturesPageAction::ToggleAgentTaskCompletedNotifications, view.button_mouse_states .agent_task_completed_notifications_checkbox .clone(), appearance, ), view.render_long_running_notifications_setting( &session_settings.notifications, appearance, ), view.render_notification_toggle( session_settings.notifications.is_needs_attention_enabled, "Notify when a command or agent needs your attention to continue", FeaturesPageAction::ToggleNeedsAttentionNotifications, view.button_mouse_states .agent_needs_attention_notifications_checkbox .clone(), appearance, ), // Add notification sound toggle only on macOS #[cfg(target_os = "macos")] { view.render_notification_toggle( session_settings.notifications.play_notification_sound, "Play notification sounds", FeaturesPageAction::ToggleNotificationSound, view.button_mouse_states.notification_sound_checkbox.clone(), appearance, ) }, ]; column.add_child(render_group(toggles, appearance)); } if FeatureFlag::HOANotifications.is_enabled() { let ai_settings = AISettings::as_ref(app); let show_agent_notifications = *ai_settings.show_agent_notifications; column.add_child(render_body_item::( "Show in-app agent notifications".into(), None, LocalOnlyIconState::Hidden, ToggleState::Enabled, appearance, ui_builder .switch( view.button_mouse_states .agent_in_app_notifications_switch .clone(), ) .check(show_agent_notifications) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::ToggleAgentInAppNotifications, ); }) .finish(), None, )); if show_agent_notifications { let theme = appearance.theme(); let font_size = appearance.ui_font_size() - 2.; let font_color = theme.active_ui_text_color(); let editor_style = UiComponentStyles { width: Some(appearance.ui_font_size() * 3.), height: Some(appearance.ui_font_size() * 2.), padding: Some(Coords::uniform(5.)), background: Some(theme.surface_2().into()), ..Default::default() }; let toast_duration_row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Text::new_inline( "Toast notifications stay visible for", appearance.ui_font_family(), font_size, ) .with_color(font_color.into()) .finish(), ) .with_child( Container::new( Dismiss::new( appearance .ui_builder() .text_input(view.notification_toast_duration_editor.clone()) .with_style(editor_style) .build() .finish(), ) .on_dismiss(|ctx, _app| { ctx.dispatch_typed_action( FeaturesPageAction::SetNotificationToastDuration, ) }) .finish(), ) .with_margin_right(NOTIFICATION_EDITOR_MARGIN) .with_margin_left(NOTIFICATION_EDITOR_MARGIN) .finish(), ) .with_child( Text::new_inline("seconds", appearance.ui_font_family(), font_size) .with_color(font_color.into()) .finish(), ) .finish(); column.add_child(render_group(vec![toast_duration_row], appearance)); } } column.finish() } } #[cfg(feature = "local_tty")] #[derive(Default)] struct StartupShellWidget {} #[cfg(feature = "local_tty")] impl SettingsWidget for StartupShellWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "startup shell session" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { Flex::column() .with_children([ render_sub_sub_header( appearance, "Default shell for new sessions".to_string(), Some(LocalOnlyIconState::for_setting( StartupShellOverride::storage_key(), StartupShellOverride::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, )), ), ChildView::new(&view.startup_shell_view).finish(), ]) .finish() } } #[cfg(feature = "local_tty")] #[derive(Default)] struct WorkingDirectoryWidget {} #[cfg(feature = "local_tty")] impl SettingsWidget for WorkingDirectoryWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "starting working directory pwd session" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { Flex::column() .with_children([ render_sub_sub_header( appearance, "Working directory for new sessions".to_string(), Some(LocalOnlyIconState::for_setting( WorkingDirectoryConfig::storage_key(), WorkingDirectoryConfig::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, )), ), ChildView::new(&view.working_directory_view).finish(), ]) .finish() } } #[derive(Default)] struct UndoCloseWidget {} impl SettingsWidget for UndoCloseWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "reopen restore recover closed tab session" } fn render( &self, view: &Self::View, _appearance: &Appearance, _app: &AppContext, ) -> Box { ChildView::new(&view.undo_close_view).finish() } } #[derive(Default)] struct ConfirmCloseSharedSessionWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for ConfirmCloseSharedSessionWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "warning popup modal dialog shared session close" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let session_settings = SessionSettings::as_ref(app); render_body_item::( "Confirm before closing shared session".into(), None, LocalOnlyIconState::for_setting( ShouldConfirmCloseSession::storage_key(), ShouldConfirmCloseSession::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*session_settings.should_confirm_close_session) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleConfirmCloseSession); }) .finish(), None, ) } } #[derive(Default)] struct ExtraMetaKeysWidget { left_switch_state: SwitchStateHandle, right_switch_state: SwitchStateHandle, } impl SettingsWidget for ExtraMetaKeysWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "extra meta key alt option" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let key_settings = KeysSettings::as_ref(app); let mut tooltip_states = view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(); Flex::column() .with_child(render_body_item::( EXTRA_META_KEYS_LEFT_TEXT.into(), None, LocalOnlyIconState::for_setting( crate::terminal::keys_settings::ExtraMetaKeys::storage_key(), crate::terminal::keys_settings::ExtraMetaKeys::sync_to_cloud(), &mut tooltip_states, app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.left_switch_state.clone()) .check(key_settings.extra_meta_keys.left_alt) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleLeftMetaKey); }) .finish(), None, )) .with_child(render_body_item::( EXTRA_META_KEYS_RIGHT_TEXT.into(), None, LocalOnlyIconState::for_setting( crate::terminal::keys_settings::ExtraMetaKeys::storage_key(), crate::terminal::keys_settings::ExtraMetaKeys::sync_to_cloud(), &mut tooltip_states, app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.right_switch_state.clone()) .check(key_settings.extra_meta_keys.right_alt) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleRightMetaKey); }) .finish(), None, )) .finish() } } /// Stable `&'static str` id for the global-hotkey settings widget, exposed for /// the `warp://settings?widget=global_hotkey` deeplink (see /// `settings_widget_deeplink_target`). pub(crate) fn global_hotkey_widget_id() -> &'static str { GlobalHotkeyWidget::static_widget_id() } #[derive(Default)] struct GlobalHotkeyWidget {} impl SettingsWidget for GlobalHotkeyWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "global hotkey quake mode keybinding quick terminal" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let mut column = Flex::column(); let ui_builder = appearance.ui_builder(); if app.is_wayland() { column.add_child(render_body_item::( "Global hotkey:".to_owned(), None, // Fine not to show local only icon state for this, as it's not a supported setting. LocalOnlyIconState::Hidden, ToggleState::Disabled, appearance, Flex::row() .with_children([ ui_builder .span("Not supported on Wayland. ") .build() .finish(), ui_builder .link( "See docs.".to_owned(), Some( "https://docs.warp.dev/terminal/windows/global-hotkey" .to_owned(), ), None, view.button_mouse_states.global_hotkey_link.clone(), ) .soft_wrap(false) .build() .finish(), ]) .finish(), None, )) } else { add_setting( &mut column, &KeysSettings::as_ref(app).activation_hotkey_enabled, || { render_dropdown_item( appearance, "Global hotkey:", None, None, LocalOnlyIconState::for_setting( ActivationHotkeyEnabled::storage_key(), ActivationHotkeyEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), None, &view.global_hotkey_dropdown, ) }, ); } let global_hotkey_mode = KeysSettings::handle(app).read(app, |settings, ctx| settings.global_hotkey_mode(ctx)); match global_hotkey_mode { GlobalHotkeyMode::QuakeMode => { column.add_child(render_group( [ view.render_keybinding_editor_row( view.button_mouse_states .quake_mode_keybinding_editor .clone(), view.button_mouse_states.quake_mode_cancel.clone(), view.button_mouse_states.quake_mode_save.clone(), view.quake_mode_keybinding_editor_state, &view.quake_mode_keybinding, FeaturesPageAction::QuakeKeybindEditorClicked, FeaturesPageAction::QuakeKeybindEditorCancel, FeaturesPageAction::QuakeKeybindEditorSave, |ctx, keystroke| { ctx.dispatch_typed_action( FeaturesPageAction::QuakeKeystrokeDefined(keystroke), ) }, appearance, ), view.render_quake_mode_position_row( KeysSettings::as_ref(app).quake_mode_settings.value(), appearance, ), // This feature is only supported on MacOS. if QUAKE_WINDOW_AUTOHIDE_SUPPORTED { view.render_quake_mode_pin_window_toggle_row( KeysSettings::as_ref(app).quake_mode_settings.value(), appearance, ) } else { Empty::new().finish() }, ], appearance, )); } GlobalHotkeyMode::ActivationHotkey => column.add_child(render_group( [view.render_keybinding_editor_row( view.button_mouse_states .activation_hotkey_keybinding_editor .clone(), view.button_mouse_states.activation_hotkey_cancel.clone(), view.button_mouse_states.activation_hotkey_save.clone(), view.activation_hotkey_keybinding_editor_state, &view.activation_hotkey_keybinding, FeaturesPageAction::ActivationKeybindEditorClicked, FeaturesPageAction::ActivationKeybindEditorCancel, FeaturesPageAction::ActivationKeybindEditorSave, |ctx, keystroke| { ctx.dispatch_typed_action(FeaturesPageAction::ActivationKeystrokeDefined( keystroke, )) }, appearance, )], appearance, )), GlobalHotkeyMode::Disabled => {} } column.finish() } } #[derive(Default)] struct AutocompleteSymbolsWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for AutocompleteSymbolsWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "autocomplete autoclose symbol bracket quote parentheses braces" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Autocomplete quotes, parentheses, and brackets".into(), None, LocalOnlyIconState::for_setting( AutocompleteSymbols::storage_key(), AutocompleteSymbols::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*AppEditorSettings::as_ref(app).autocomplete_symbols) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleAutocompleteSymbols); }) .finish(), None, ) } } #[derive(Default)] struct CodeEditorLineNumberModeWidget {} impl SettingsWidget for CodeEditorLineNumberModeWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "line number relative line vim gutter code editor" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let mut column = Flex::column(); add_setting( &mut column, &AppEditorSettings::as_ref(app).code_editor_line_number_mode, || { render_dropdown_item( appearance, "Code editor line numbers:", None, None, LocalOnlyIconState::for_setting( CodeEditorLineNumberModeSetting::storage_key(), CodeEditorLineNumberModeSetting::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), None, &view.code_editor_line_number_mode_dropdown, ) }, ); column.finish() } } #[derive(Default)] struct ErrorUnderliningWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for ErrorUnderliningWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "error underline editor" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Error underlining for commands".into(), None, LocalOnlyIconState::for_setting( ErrorUnderliningEnabled::storage_key(), ErrorUnderliningEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*InputSettings::as_ref(app).error_underlining.value()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleErrorUnderlining); }) .finish(), None, ) } } #[derive(Default)] struct SyntaxHighlightingWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for SyntaxHighlightingWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "syntax highlighting editor" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Syntax highlighting for commands".into(), None, LocalOnlyIconState::for_setting( SyntaxHighlighting::storage_key(), SyntaxHighlighting::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*InputSettings::as_ref(app).syntax_highlighting.value()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleSyntaxHighlighting); }) .finish(), None, ) } } #[derive(Default)] struct CompletionsMenuWhileTypingWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for CompletionsMenuWhileTypingWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "completions menu type typing" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Open completions menu as you type".into(), None, LocalOnlyIconState::for_setting( CompletionsOpenWhileTyping::storage_key(), CompletionsOpenWhileTyping::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check( *InputSettings::as_ref(app) .completions_open_while_typing .value(), ) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleCompletionsOpenWhileTyping); }) .finish(), None, ) } } #[derive(Default)] struct CommandCorrectionsWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for CommandCorrectionsWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "suggest command corrections" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Suggest corrected commands".into(), None, LocalOnlyIconState::for_setting( CommandCorrections::storage_key(), CommandCorrections::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*InputSettings::as_ref(app).command_corrections.value()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleCommandCorrections); }) .finish(), None, ) } } #[derive(Default)] struct AliasExpansionWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for AliasExpansionWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "expand alias expansion" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let alias_expansion_settings = AliasExpansionSettings::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( "Expand aliases as you type".into(), None, LocalOnlyIconState::for_setting( AliasExpansionEnabled::storage_key(), AliasExpansionEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*alias_expansion_settings.alias_expansion_enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleAliasExpansion); }) .finish(), None, ) } } #[derive(Default)] struct MiddleClickPasteWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for MiddleClickPasteWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "middle click paste clipboard" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let selection_settings = SelectionSettings::as_ref(app); render_body_item::( "Middle-click to paste".into(), None, LocalOnlyIconState::for_setting( MiddleClickPasteEnabled::storage_key(), MiddleClickPasteEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*selection_settings.middle_click_paste_enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleMiddleClickPaste); }) .finish(), None, ) } } #[derive(Default)] struct VimModeWidget { enabled_switch_state: SwitchStateHandle, clipboard_switch_state: SwitchStateHandle, status_bar_switch_state: SwitchStateHandle, } impl SettingsWidget for VimModeWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "vim mode keybindings" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let mut column = Flex::column(); let app_editor_settings = AppEditorSettings::as_ref(app); let vim_mode_enabled = *app_editor_settings.vim_mode.value(); column.add_child(render_body_item::( "Edit code and commands with Vim keybindings".into(), None, LocalOnlyIconState::for_setting( VimModeEnabled::storage_key(), VimModeEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.enabled_switch_state.clone()) .check(vim_mode_enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleVimMode); }) .finish(), None, )); if vim_mode_enabled { let unnamed_system_clipboard = *app_editor_settings.vim_unnamed_system_clipboard.value(); let clipboard_switch = ui_builder .switch(self.clipboard_switch_state.clone()) .check(unnamed_system_clipboard) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleVimUnnamedSystemClipboard); }) .finish(); let clipboard_setting = view.render_setting_subgroup_item( appearance, LocalOnlyIconState::for_setting( VimUnnamedSystemClipboard::storage_key(), VimUnnamedSystemClipboard::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), clipboard_switch, "Set unnamed register as system clipboard".into(), ); let vim_status_bar = *app_editor_settings.vim_status_bar.value(); let status_bar_switch = ui_builder .switch(self.status_bar_switch_state.clone()) .check(vim_status_bar) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleVimStatusBar); }) .finish(); let status_bar_setting = view.render_setting_subgroup_item( appearance, LocalOnlyIconState::for_setting( VimStatusBar::storage_key(), VimStatusBar::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), status_bar_switch, "Show Vim status bar".into(), ); column.add_child(render_group( [clipboard_setting, status_bar_setting], appearance, )); } column.finish() } } #[derive(Default)] struct AtContextMenuInTerminalModeWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for AtContextMenuInTerminalModeWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "@ at sign context menu terminal mode AI assistant" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Enable '@' context menu in terminal mode".into(), None, LocalOnlyIconState::for_setting( AtContextMenuInTerminalMode::storage_key(), AtContextMenuInTerminalMode::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check( *InputSettings::as_ref(app) .at_context_menu_in_terminal_mode .value(), ) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::ToggleAtContextMenuInTerminalMode, ); }) .finish(), None, ) } } #[derive(Default)] struct SlashCommandsInTerminalModeWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for SlashCommandsInTerminalModeWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "slash commands terminal mode input menu" } fn should_render(&self, app: &AppContext) -> bool { AISettings::as_ref(app).is_any_ai_enabled(app) } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Enable slash commands in terminal mode".into(), None, LocalOnlyIconState::for_setting( EnableSlashCommandsInTerminal::storage_key(), EnableSlashCommandsInTerminal::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check( *InputSettings::as_ref(app) .enable_slash_commands_in_terminal .value(), ) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::ToggleSlashCommandsInTerminalMode, ); }) .finish(), None, ) } } #[derive(Default)] struct OutlineCodebaseSymbolsForAtContextMenuWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for OutlineCodebaseSymbolsForAtContextMenuWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "outline codebase symbols context menu code indexing" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Outline codebase symbols for '@' context menu".into(), None, LocalOnlyIconState::for_setting( OutlineCodebaseSymbolsForAtContextMenu::storage_key(), OutlineCodebaseSymbolsForAtContextMenu::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check( *InputSettings::as_ref(app) .outline_codebase_symbols_for_at_context_menu .value(), ) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::ToggleOutlineCodebaseSymbolsForAtContextMenu, ); }) .finish(), None, ) } } #[derive(Default)] struct ShowTerminalInputMessageLineWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for ShowTerminalInputMessageLineWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "terminal input message line bar agent" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Show terminal input message line".into(), None, LocalOnlyIconState::for_setting( ShowTerminalInputMessageBar::storage_key(), ShowTerminalInputMessageBar::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(InputSettings::as_ref(app).is_terminal_input_message_bar_enabled()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::ToggleShowTerminalInputMessageLine, ); }) .finish(), None, ) } } #[derive(Default)] struct PreserveInputFocusOnBlockSelectionWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for PreserveInputFocusOnBlockSelectionWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "preserve input focus block selection navigate arrow keys" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); render_body_item::( "Preserve input focus on block selection".into(), None, LocalOnlyIconState::for_setting( PreserveInputFocusOnBlockSelection::storage_key(), PreserveInputFocusOnBlockSelection::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*BlockListSettings::as_ref(app).preserve_input_focus_on_block_selection) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::TogglePreserveInputFocusOnBlockSelection, ); }) .finish(), None, ) } } #[derive(Default)] struct AutosuggestionKeybindingHintWidget { enabled_switch_state: SwitchStateHandle, } impl SettingsWidget for AutosuggestionKeybindingHintWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "autosuggestion keybinding hint" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let mut column = Flex::column(); let app_editor_settings = AppEditorSettings::as_ref(app); let autosuggestion_keybinding_hint = *app_editor_settings.autosuggestion_keybinding_hint.value(); column.add_child(render_body_item::( "Show autosuggestion keybinding hint".into(), None, LocalOnlyIconState::for_setting( AutosuggestionKeybindingHint::storage_key(), AutosuggestionKeybindingHint::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.enabled_switch_state.clone()) .check(autosuggestion_keybinding_hint) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::ToggleAutosuggestionKeybindingHint, ); }) .finish(), None, )); column.finish() } } #[derive(Default)] struct AutosuggestionIgnoreButtonWidget { enabled_switch_state: SwitchStateHandle, } impl SettingsWidget for AutosuggestionIgnoreButtonWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "autosuggestion ignore button hide" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let mut column = Flex::column(); let app_editor_settings = AppEditorSettings::as_ref(app); let show_autosuggestion_ignore_button = *app_editor_settings .show_autosuggestion_ignore_button .value(); column.add_child(render_body_item::( "Show autosuggestion ignore button".into(), None, LocalOnlyIconState::for_setting( ShowAutosuggestionIgnoreButton::storage_key(), ShowAutosuggestionIgnoreButton::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.enabled_switch_state.clone()) .check(show_autosuggestion_ignore_button) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::ToggleShowAutosuggestionIgnoreButton, ); }) .finish(), None, )); column.finish() } } #[derive(Default)] struct TabKeyBehaviorWidget {} impl TabKeyBehaviorWidget { fn render_tab_behavior_setting_secondary_row( &self, view: &FeaturesPageView, appearance: &Appearance, app: &AppContext, ) -> Box { let input_settings = InputSettings::as_ref(app); let other_keybinding_info = match *view.tab_behavior { TabBehavior::Completions if view.autosuggestions_keystroke.is_empty() => { // If the "Accept autosuggestions" keybinding is unbound, the // user can always still accept with right arrow. Some("→ accepts autosuggestions.".into()) } TabBehavior::Completions => Some(format!( "{} accepts autosuggestions.", *view.autosuggestions_keystroke )), TabBehavior::Autosuggestions if *input_settings.completions_open_while_typing.value() => { if view.completions_keystroke.is_empty() { Some("Completions open as you type.".into()) } else { Some(format!( "Completions open as you type (or {}).", *view.completions_keystroke )) } } TabBehavior::Autosuggestions if view.completions_keystroke.is_empty() => { Some("Opening the completion menu is unbound.".into()) } TabBehavior::Autosuggestions => Some(format!( "{} opens completion menu.", *view.completions_keystroke )), TabBehavior::UserDefined => None, }; let other_keybinding_name = match *view.tab_behavior { TabBehavior::Completions => Some("Accept Autosuggestion"), TabBehavior::Autosuggestions => Some("Open Completions Menu"), TabBehavior::UserDefined => None, }; if let (Some(other_keybinding_info), Some(other_keybinding_name)) = (other_keybinding_info, other_keybinding_name) { Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child(Shrinkable::new(1.0, Empty::new().finish()).finish()) .with_child( appearance .ui_builder() .span(other_keybinding_info) .with_style(UiComponentStyles { font_color: Some(appearance.theme().nonactive_ui_text_color().into()), ..Default::default() }) .build() .finish(), ) .with_child( Container::new( view.render_change_keybinding_button(other_keybinding_name, appearance), ) .with_margin_left(4.) .finish(), ) .finish() } else { Empty::new().finish() } } } impl SettingsWidget for TabKeyBehaviorWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "completions autosuggestions tab keybinding arrow accept" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let mut tab_key_span = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Start) .with_child( appearance .ui_builder() .span("Tab key behavior") .with_style(UiComponentStyles { font_size: Some(CONTENT_FONT_SIZE + 1.), ..Default::default() }) .build() .finish(), ); if *CloudPreferencesSettings::as_ref(app).settings_sync_enabled { tab_key_span.add_child(render_local_only_icon( appearance, view.button_mouse_states .tab_behavior_local_only_icon .clone(), None, )); } let main_row = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Shrinkable::new(1.0, Align::new(tab_key_span.finish()).left().finish()).finish(), ) .with_child(ChildView::new(&view.tab_behavior_dropdown).finish()) .finish(); Container::new( Flex::column() .with_child(main_row) .with_child(self.render_tab_behavior_setting_secondary_row(view, appearance, app)) .finish(), ) .with_margin_bottom(10.) .finish() } } #[derive(Default)] struct CtrlTabBehaviorWidget {} impl SettingsWidget for CtrlTabBehaviorWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "ctrl tab behavior pane switch session recent" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let mut column = Flex::column(); add_setting( &mut column, &KeysSettings::as_ref(app).ctrl_tab_behavior, || { render_dropdown_item( appearance, "Ctrl+Tab behavior:", None, None, LocalOnlyIconState::for_setting( CtrlTabBehaviorSetting::storage_key(), CtrlTabBehaviorSetting::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), None, &view.ctrl_tab_behavior_dropdown, ) }, ); column.finish() } } #[derive(Default)] struct MouseReportingWidget { additional_info_link: MouseStateHandle, switch_state: SwitchStateHandle, } impl SettingsWidget for MouseReportingWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "mouse reporting" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let reporting_settings = AltScreenReporting::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( "Enable Mouse Reporting".into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( "https://docs.warp.dev/terminal/more-features/full-screen-apps#mouse-and-scroll-reporting" .into(), )), secondary_text: None, tooltip_override_text: None, }), LocalOnlyIconState::for_setting( MouseReportingEnabled::storage_key(), MouseReportingEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*reporting_settings.mouse_reporting_enabled.value()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleMouseReporting) }) .finish(), None, ) } } #[derive(Default)] struct ScrollReportingWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for ScrollReportingWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "scroll reporting" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let reporting_settings = AltScreenReporting::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( "Enable Scroll Reporting".into(), None, LocalOnlyIconState::for_setting( ScrollReportingEnabled::storage_key(), ScrollReportingEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), if *reporting_settings.mouse_reporting_enabled.value() { ToggleState::Enabled } else { ToggleState::Disabled }, appearance, { let switch = ui_builder .switch(self.switch_state.clone()) .check(*reporting_settings.scroll_reporting_enabled.value()); if *reporting_settings.mouse_reporting_enabled.value() { switch .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleScrollReporting) }) .finish() } else { switch.disable().build().finish() } }, None, ) } } #[derive(Default)] struct FocusReportingWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for FocusReportingWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "focus reporting" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let reporting_settings = AltScreenReporting::as_ref(app); let ui_builder = appearance.ui_builder(); render_body_item::( "Enable Focus Reporting".into(), None, LocalOnlyIconState::for_setting( FocusReportingEnabled::storage_key(), FocusReportingEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*reporting_settings.focus_reporting_enabled.value()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleFocusReporting) }) .finish(), None, ) } } #[derive(Default)] struct AudibleBellWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for AudibleBellWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "audible bell" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let terminal_settings = TerminalSettings::as_ref(app); render_body_item::( "Use Audible Bell".into(), None, LocalOnlyIconState::for_setting( UseAudibleBell::storage_key(), UseAudibleBell::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*terminal_settings.use_audible_bell) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleUseAudibleBell) }) .finish(), None, ) } } #[derive(Default)] struct SmartSelectWidget { additional_info_link: MouseStateHandle, switch_state: SwitchStateHandle, word_char_allowlist_reset_state: MouseStateHandle, } impl SmartSelectWidget { fn render_word_char_config( &self, view: &FeaturesPageView, appearance: &Appearance, non_default: bool, ) -> Box { let ui_builder = appearance.ui_builder(); Flex::column() .with_child( ui_builder .label("Characters considered part of a word".to_string()) .with_style(UiComponentStyles { margin: Some(Coords { top: 10.0, bottom: 5.0, ..Default::default() }), ..Default::default() }) .build() .finish(), ) .with_child( Dismiss::new( ui_builder .text_input(view.word_boundary_editor.clone()) .with_style(UiComponentStyles { width: Some(240.0), ..Default::default() }) .build() .finish(), ) .on_dismiss(|ctx, _app| { ctx.dispatch_typed_action(FeaturesPageAction::SetWordCharAllowlist) }) .finish(), ) .with_child( build_reset_button( appearance, self.word_char_allowlist_reset_state.clone(), non_default, ) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ResetWordCharAllowlist); }) .finish(), ) .finish() } } impl SettingsWidget for SmartSelectWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "word smart select semantic separator" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let selection = SemanticSelection::as_ref(app); let mut column = Flex::column(); column.add_child(render_body_item::( "Double-click smart selection".into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( "https://docs.warp.dev/terminal/more-features/text-selection".into(), )), secondary_text: None, tooltip_override_text: None, }), LocalOnlyIconState::for_setting( SmartSelectEnabled::storage_key(), SmartSelectEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(selection.smart_select_enabled()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleSmartSelection) }) .finish(), None, )); if !selection.smart_select_enabled() { column.add_child(render_group( [self.render_word_char_config( view, appearance, selection.word_char_allowlist_changed_from_default(), )], appearance, )); } column.finish() } } #[derive(Default)] struct CopyOnSelectWidget { switch_state: SwitchStateHandle, } #[derive(Default)] struct ShowTerminalZeroStateBlockWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for ShowTerminalZeroStateBlockWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "zero state new conversation terminal block welcome output first" } fn should_render(&self, app: &AppContext) -> bool { AISettings::as_ref(app).is_any_ai_enabled(app) } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let terminal_settings = TerminalSettings::as_ref(app); render_body_item::( "Show help block in new sessions".into(), None, LocalOnlyIconState::for_setting( ShowTerminalZeroStateBlock::storage_key(), ShowTerminalZeroStateBlock::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*terminal_settings.show_terminal_zero_state_block) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleShowTerminalZeroStateBlock) }) .finish(), None, ) } } impl SettingsWidget for CopyOnSelectWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "copy on select" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let copy_on_select_enabled = SelectionSettings::as_ref(app).copy_on_select_enabled(); render_body_item::( "Copy on select".into(), None, LocalOnlyIconState::for_setting( CopyOnSelect::storage_key(), CopyOnSelect::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(copy_on_select_enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleCopyOnSelect); }) .finish(), None, ) } } #[derive(Default)] struct Osc52ClipboardAccessWidget {} impl SettingsWidget for Osc52ClipboardAccessWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "clipboard osc 52 osc52 paste copy access terminal program" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { render_dropdown_item( appearance, "Clipboard access (OSC 52)", Some("Controls whether programs running in the terminal can read or write your system clipboard."), None, LocalOnlyIconState::for_setting( Osc52ClipboardAccessSetting::storage_key(), Osc52ClipboardAccessSetting::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), None, &view.osc52_clipboard_access_dropdown, ) } } #[derive(Default)] struct NewTabPlacementWidget {} impl SettingsWidget for NewTabPlacementWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "new tab placement" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { render_dropdown_item( appearance, "New tab placement", None, None, LocalOnlyIconState::for_setting( NewTabPlacement::storage_key(), NewTabPlacement::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), None, &view.new_tab_placement_dropdown, ) } } #[derive(Default)] struct DefaultSessionModeWidget {} impl SettingsWidget for DefaultSessionModeWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "default session mode agent terminal new pane tab open config" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let label = render_dropdown_item_label( "Default mode for new sessions".to_string(), None, LocalOnlyIconState::for_setting( DefaultSessionMode::storage_key(), DefaultSessionMode::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), None, appearance, ); Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child( Shrinkable::new( 1.0, Container::new(Align::new(label).left().finish()) .with_margin_bottom(4.) .with_padding_right(16.) .finish(), ) .finish(), ) .with_child(ChildView::new(&view.default_session_mode_dropdown).finish()) .finish() } } #[derive(Default)] struct WorkflowsInCommandSearch { additional_info_link: MouseStateHandle, switch_state: SwitchStateHandle, } impl SettingsWidget for WorkflowsInCommandSearch { type View = FeaturesPageView; fn search_terms(&self) -> &str { "global workflows command search" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let workflow_settings = CommandSearchSettings::as_ref(app); render_body_item::( "Show Global Workflows in Command Search (ctrl-r)".into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: Some(FeaturesPageAction::OpenUrl( "https://docs.warp.dev/terminal/entry/yaml-workflows".into(), )), secondary_text: None, tooltip_override_text: None, }), LocalOnlyIconState::for_setting( ShowGlobalWorkflowsInUniversalSearch::storage_key(), ShowGlobalWorkflowsInUniversalSearch::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ui_builder .switch(self.switch_state.clone()) .check(*workflow_settings.show_global_workflows_in_universal_search) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action( FeaturesPageAction::ToggleGlobalWorkflowsInUniversalSearch, ) }) .finish(), None, ) } } #[derive(Default)] struct LinuxSelectionClipboardWidget { additional_info_link: MouseStateHandle, switch_state: SwitchStateHandle, } impl SettingsWidget for LinuxSelectionClipboardWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "linux selection clipboard middle click" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { render_body_item::( "Honor linux selection clipboard".into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, tooltip_override_text: Some( "Whether the Linux primary clipboard should be supported.".into(), ), }), LocalOnlyIconState::for_setting( LinuxSelectionClipboard::storage_key(), LinuxSelectionClipboard::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(SelectionSettings::as_ref(app).linux_selection_clipboard_enabled()) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleLinuxClipboardSelection); }) .finish(), None, ) } } #[derive(Default)] struct GPUWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for GPUWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "render integrated discrete gpu graphics" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let gpu_settings = GPUSettings::as_ref(app); let mut col = Flex::column().with_child(render_body_item::( "Prefer rendering new windows with integrated GPU (low power)".into(), None, LocalOnlyIconState::for_setting( PreferLowPowerGPU::storage_key(), PreferLowPowerGPU::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(*gpu_settings.prefer_low_power_gpu) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::TogglePreferLowPowerGPU) }) .finish(), None, )); if view.gpu_power_preference_changed { let theme = appearance.theme(); col.add_child( Container::new( appearance .ui_builder() .wrappable_text("Changes will apply to new windows.", true) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(theme.background()).into_solid()), ..Default::default() }) .build() .finish(), ) .with_margin_bottom(10.) .finish(), ); } col.finish() } } #[cfg(any(target_os = "linux", target_os = "freebsd"))] #[derive(Default)] struct WindowSystemWidget { additional_info_link: MouseStateHandle, switch_state: SwitchStateHandle, } #[cfg(any(target_os = "linux", target_os = "freebsd"))] impl SettingsWidget for WindowSystemWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "wayland x11 window system compositor" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let mut children = Flex::column(); let force_x11 = *LinuxAppConfiguration::as_ref(app).force_x11.value(); children.add_child(render_body_item::( "Use Wayland for window management".into(), Some(AdditionalInfo { mouse_state: self.additional_info_link.clone(), on_click_action: None, secondary_text: None, tooltip_override_text: Some("Enables the use of Wayland".to_string()), }), LocalOnlyIconState::for_setting( ForceX11::storage_key(), ForceX11::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.switch_state.clone()) .check(!force_x11) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleForceX11) }) .finish(), None, )); let mut secondary_text = "Enabling this setting disables global hotkey support. When disabled, text \ may be blurry if your Wayland compositor is using fraction scaling (ex: 125%)." .to_string(); if view.force_x11_changed { secondary_text.push_str("\n\nRestart Galaxy for changes to take effect."); } let warp_theme = appearance.theme(); children.add_child( appearance .ui_builder() .wrappable_text(secondary_text, true) .with_style(UiComponentStyles { font_color: Some( warp_theme .sub_text_color(warp_theme.background()) .into_solid(), ), ..Default::default() }) .build() .finish(), ); children.finish() } } #[derive(Default)] struct GraphicsBackendWidget {} impl SettingsWidget for GraphicsBackendWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "gpu graphics backend vulkan dx12 directx12 opengl driver" } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let theme = appearance.theme(); let dropdown = render_dropdown_item( appearance, "Preferred graphics backend", None, None, LocalOnlyIconState::for_setting( PreferredGraphicsBackend::storage_key(), PreferredGraphicsBackend::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), None, &view.graphics_backend_dropdown, ); let mut col = Flex::column().with_child(dropdown); if let Some(window) = app.windows().platform_window(view.window_id) { let backend = window.graphics_backend(); col.add_child( appearance .ui_builder() .wrappable_text(format!("Current backend: {}", backend.to_label()), true) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(theme.background()).into_solid()), ..Default::default() }) .build() .finish(), ); } if view.graphics_backend_preference_changed { col.add_child( Container::new( appearance .ui_builder() .wrappable_text("Changes will apply to new windows.", true) .with_style(UiComponentStyles { font_color: Some(theme.sub_text_color(theme.background()).into_solid()), ..Default::default() }) .build() .finish(), ) .with_margin_bottom(10.) .finish(), ); } col.finish() } } #[derive(Default)] struct AsyncFindWidget { switch_state: SwitchStateHandle, } impl SettingsWidget for AsyncFindWidget { type View = FeaturesPageView; fn search_terms(&self) -> &str { "async asynchronous fast find search" } fn should_render(&self, _app: &AppContext) -> bool { // Here, the feature flag being enabled means the feature is force-enabled, // so we don't need to render the toggle. !FeatureFlag::AsyncFind.is_enabled() } fn render( &self, view: &Self::View, appearance: &Appearance, app: &AppContext, ) -> Box { let ui_builder = appearance.ui_builder(); let label = render_body_item_label::( "Asynchronous find".into(), None, None, LocalOnlyIconState::for_setting( AsyncFindEnabled::storage_key(), AsyncFindEnabled::sync_to_cloud(), &mut view .button_mouse_states .local_only_icon_tooltip_states .borrow_mut(), app, ), ToggleState::Enabled, appearance, ); let label_with_chip = Flex::row() .with_cross_axis_alignment(CrossAxisAlignment::Center) .with_child(label) .with_child(render_beta_chip(appearance)) .finish(); let switch = ui_builder .switch(self.switch_state.clone()) .check(*TerminalSettings::as_ref(app).async_find_enabled) .build() .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(FeaturesPageAction::ToggleAsyncFind); }) .finish(); build_toggle_element( label_with_chip, switch, appearance, Some( "Use an improved implementation of find to keep the UI responsive while searching for matches on large outputs." .into(), ), ) } }