Files
galaxy/app/src/settings_view/features_page.rs
T

7716 lines
282 KiB
Rust

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<T: Action + Clone>(
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<DisplayIdx>),
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<GraphicsBackend>),
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<HashMap<String, MouseStateHandle>>,
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<Self>,
global_resource_handles: GlobalResourceHandles,
button_mouse_states: MouseStateHandles,
ctrl_tab_behavior_dropdown: ViewHandle<Dropdown<FeaturesPageAction>>,
code_editor_line_number_mode_dropdown: ViewHandle<Dropdown<FeaturesPageAction>>,
global_hotkey_dropdown: ViewHandle<Dropdown<FeaturesPageAction>>,
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<Dropdown<FeaturesPageAction>>,
quake_mode_pin_screen_dropdown: ViewHandle<Dropdown<FeaturesPageAction>>,
quake_mode_width_editor: ViewHandle<EditorView>,
quake_mode_height_editor: ViewHandle<EditorView>,
notifications_long_running_threshold_editor: ViewHandle<EditorView>,
notification_toast_duration_editor: ViewHandle<EditorView>,
#[cfg(feature = "local_tty")]
working_directory_view: ViewHandle<features::WorkingDirectoryView>,
#[cfg(feature = "local_tty")]
startup_shell_view: ViewHandle<features::StartupShellView>,
undo_close_view: ViewHandle<features::UndoCloseView>,
max_block_size_input_editor: ViewHandle<EditorView>,
valid_max_block_size: bool,
mouse_scroll_input_editor: ViewHandle<EditorView>,
valid_mouse_scroll_multiplier: bool,
#[cfg(feature = "local_fs")]
external_editor_view: ViewHandle<features::ExternalEditorView>,
word_boundary_editor: ViewHandle<EditorView>,
tab_behavior_dropdown: ViewHandle<Dropdown<FeaturesPageAction>>,
graphics_backend_dropdown: ViewHandle<Dropdown<FeaturesPageAction>>,
new_tab_placement_dropdown: ViewHandle<Dropdown<FeaturesPageAction>>,
osc52_clipboard_access_dropdown: ViewHandle<Dropdown<FeaturesPageAction>>,
default_session_mode_dropdown: ViewHandle<FilterableDropdown<FeaturesPageAction>>,
tab_behavior: Tracked<TabBehavior>,
completions_keystroke: Tracked<String>,
autosuggestions_keystroke: Tracked<String>,
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<Self>) {
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::<f32>() {
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::<u64>() {
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<dyn Element> {
self.page.render(self, app)
}
}
impl FeaturesPageView {
pub fn new(
global_resource_handles: GlobalResourceHandles,
ctx: &mut ViewContext<FeaturesPageView>,
) -> 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<Self>) -> PageType<Self> {
let mut general_widgets: Vec<Box<dyn SettingsWidget<View = Self>>> =
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<Box<dyn SettingsWidget<View = Self>>> =
vec![Box::new(DesktopNotificationsWidget::default())];
let mut session_widgets: Vec<Box<dyn SettingsWidget<View = Self>>> = 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<Box<dyn SettingsWidget<View = Self>>> = 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<Box<dyn SettingsWidget<View = Self>>> =
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<Box<dyn SettingsWidget<View = Self>>> = 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<Box<dyn SettingsWidget<View = Self>>> = 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<Box<dyn SettingsWidget<View = Self>>> = 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<Dropdown<FeaturesPageAction>>,
ctx: &mut ViewContext<Self>,
) {
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<Dropdown<FeaturesPageAction>>,
ctx: &mut ViewContext<Self>,
) {
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<Dropdown<FeaturesPageAction>>,
ctx: &mut ViewContext<Self>,
) {
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<Self>) {
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<DisplayCount>,
ctx: &mut ViewContext<Self>,
) {
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<Self>) {
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<Self>) {
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<Self>,
) {
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::<usize>() {
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<Self>) {
let buffer_text = self
.max_block_size_input_editor
.as_ref(ctx)
.buffer_text(ctx);
if let Ok(input) = buffer_text.parse::<usize>() {
// 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<Self>,
) {
match event {
EditorEvent::Edited(_) => {
let buffer_text = self.mouse_scroll_input_editor.as_ref(ctx).buffer_text(ctx);
if buffer_text.parse::<f32>().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<Self>) {
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::<f32>() {
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<Self>) {
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<Self>) {
let user_input = self.quake_mode_width_editor.as_ref(ctx).buffer_text(ctx);
if let Ok(num) = user_input.parse::<usize>() {
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<Self>) {
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<Self>) {
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>,
) {
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>) {
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<Self>) {
// 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<Self>,
) {
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<Self>) {
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<Dropdown<FeaturesPageAction>>,
ctx: &mut ViewContext<Self>,
) {
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<FilterableDropdown<FeaturesPageAction>>,
ctx: &mut ViewContext<Self>,
) {
dropdown.update(
ctx,
|dropdown: &mut FilterableDropdown<FeaturesPageAction>, 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<DropdownItem<FeaturesPageAction>> = 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<Self>,
) {
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<T: Fn(&mut EventContext, Keystroke) + 'static>(
&self,
outer_button_mouse_state: Arc<Mutex<MouseState>>,
cancel_button_mouse_state: Arc<Mutex<MouseState>>,
save_button_mouse_state: Arc<Mutex<MouseState>>,
keybinding_editor_state: KeybindingEditorState,
keybinding: &KeyBindingModifyingState,
editor_clicked_action: FeaturesPageAction,
cancel_action: FeaturesPageAction,
save_action: FeaturesPageAction,
record_keystroke: T,
appearance: &Appearance,
) -> Box<dyn Element> {
Hoverable::new(outer_button_mouse_state, |state| {
let background: Option<Fill> = 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<Dropdown<FeaturesPageAction>>,
width: f32,
) -> Box<dyn Element> {
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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
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<Mutex<MouseState>>,
appearance: &Appearance,
) -> Box<dyn Element> {
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<T: Fn(&mut EventContext, Keystroke) + 'static>(
&self,
keybinding: &KeyBindingModifyingState,
keybinding_editor_state: KeybindingEditorState,
record_keystroke: T,
appearance: &Appearance,
) -> Box<dyn Element> {
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<T: Fn(&mut EventContext, Keystroke) + 'static>(
&self,
cancel_button_mouse_state: Arc<Mutex<MouseState>>,
save_button_mouse_state: Arc<Mutex<MouseState>>,
keybinding: &KeyBindingModifyingState,
keybinding_editor_state: KeybindingEditorState,
cancel_action: FeaturesPageAction,
save_action: FeaturesPageAction,
record_keystroke: T,
appearance: &Appearance,
) -> Box<dyn Element> {
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<dyn Element> {
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<dyn Element>,
label_text: String,
) -> Box<dyn Element> {
Container::new(
Flex::row()
.with_child(
Shrinkable::new(
1.0,
Container::new(
Align::new(render_body_item_label::<FeaturesPageAction>(
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<Self>) {
// 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<Self>) -> 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<ViewHandle<FeaturesPageView>> for SettingsPageViewHandle {
fn from(view_handle: ViewHandle<FeaturesPageView>) -> Self {
SettingsPageViewHandle::Features(view_handle)
}
}
pub(super) fn render_group(
children: impl IntoIterator<Item = Box<dyn Element>>,
appearance: &Appearance,
) -> Box<dyn Element> {
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<FeaturesPageAction>,
ctx: &mut ViewContext<Dropdown<FeaturesPageAction>>,
) {
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<FeaturesPageAction>,
ctx: &mut ViewContext<Dropdown<FeaturesPageAction>>,
) {
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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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<dyn Element> {
let general_settings = GeneralSettings::as_ref(app);
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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::<FeaturesPageAction>(
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<dyn Element> {
let general_settings = GeneralSettings::as_ref(app);
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let changelog_settings = ChangelogSettings::as_ref(app);
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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::<FeaturesPageAction>(
"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<dyn Element> {
let general_settings = GeneralSettings::as_ref(app);
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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<dyn Element> {
let border_color: Option<Fill> = 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::<FeaturesPageAction>(
"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<dyn Element> {
let session_settings = SessionSettings::as_ref(app);
let ui_builder = appearance.ui_builder();
let mut column = Flex::column();
column.add_child(render_body_item::<FeaturesPageAction>(
"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::<FeaturesPageAction>(
"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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
let ui_builder = appearance.ui_builder();
let session_settings = SessionSettings::as_ref(app);
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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::<FeaturesPageAction>(
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::<FeaturesPageAction>(
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<dyn Element> {
let mut column = Flex::column();
let ui_builder = appearance.ui_builder();
if app.is_wayland() {
column.add_child(render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let alias_expansion_settings = AliasExpansionSettings::as_ref(app);
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
let selection_settings = SelectionSettings::as_ref(app);
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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::<FeaturesPageAction>(
"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<dyn Element> {
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::<FeaturesPageAction>(
"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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
let reporting_settings = AltScreenReporting::as_ref(app);
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let reporting_settings = AltScreenReporting::as_ref(app);
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let reporting_settings = AltScreenReporting::as_ref(app);
let ui_builder = appearance.ui_builder();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
let terminal_settings = TerminalSettings::as_ref(app);
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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<dyn Element> {
let ui_builder = appearance.ui_builder();
let selection = SemanticSelection::as_ref(app);
let mut column = Flex::column();
column.add_child(render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
let terminal_settings = TerminalSettings::as_ref(app);
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let ui_builder = appearance.ui_builder();
let copy_on_select_enabled = SelectionSettings::as_ref(app).copy_on_select_enabled();
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
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<dyn Element> {
let ui_builder = appearance.ui_builder();
let workflow_settings = CommandSearchSettings::as_ref(app);
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let gpu_settings = GPUSettings::as_ref(app);
let mut col = Flex::column().with_child(render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
let mut children = Flex::column();
let force_x11 = *LinuxAppConfiguration::as_ref(app).force_x11.value();
children.add_child(render_body_item::<FeaturesPageAction>(
"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<dyn Element> {
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<dyn Element> {
let ui_builder = appearance.ui_builder();
let label = render_body_item_label::<FeaturesPageAction>(
"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(),
),
)
}
}