Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
use std::{cell::RefCell, collections::HashMap};
|
||||
|
||||
use settings::{Setting, ToggleableSetting};
|
||||
use warp_core::features::FeatureFlag;
|
||||
use warpui::{
|
||||
elements::{Flex, MouseStateHandle, ParentElement},
|
||||
ui_components::{components::UiComponent, switch::SwitchStateHandle},
|
||||
Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
report_if_error, send_telemetry_from_ctx,
|
||||
server::telemetry::TelemetryEvent,
|
||||
settings_view::settings_page::{
|
||||
render_body_item, render_dropdown_item, AdditionalInfo, LocalOnlyIconState, ToggleState,
|
||||
},
|
||||
util::file::external_editor::{
|
||||
settings::{
|
||||
EditorChoice, EditorLayout, OpenCodePanelsFileEditor, OpenFileEditor, OpenFileLayout,
|
||||
PreferMarkdownViewer, PreferTabbedEditorView,
|
||||
},
|
||||
EditorSettings, SUPPORTED_EDITORS,
|
||||
},
|
||||
view_components::{Dropdown, DropdownItem},
|
||||
};
|
||||
|
||||
const TABBED_FILE_VIEWER_TOGGLE_HEADER: &str = "Group files into single editor pane";
|
||||
const TABBED_FILE_VIEWER_TOGGLE_DESCRIPTION: &str = "When this setting is on, any files opened in the same tab will be automatically grouped into a single editor pane.";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExternalEditorAction {
|
||||
SetEditor(EditorChoice),
|
||||
SetCodePanelsEditor(EditorChoice),
|
||||
SetLayout(EditorLayout),
|
||||
TogglePreferMarkdownViewer,
|
||||
ToggleTabbedEditorView,
|
||||
OpenUrl(String),
|
||||
}
|
||||
|
||||
pub struct ExternalEditorView {
|
||||
editor_dropdown: ViewHandle<Dropdown<ExternalEditorAction>>,
|
||||
code_panels_editor_dropdown: ViewHandle<Dropdown<ExternalEditorAction>>,
|
||||
layout_dropdown: ViewHandle<Dropdown<ExternalEditorAction>>,
|
||||
tabbed_editor_view_mouse_state: SwitchStateHandle,
|
||||
prefer_markdown_viewer_switch: SwitchStateHandle,
|
||||
markdown_viewer_mouse_state: MouseStateHandle,
|
||||
local_only_icon_states: RefCell<HashMap<String, MouseStateHandle>>,
|
||||
}
|
||||
|
||||
impl ExternalEditorView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let settings = EditorSettings::handle(ctx);
|
||||
let editor_to_open_files = *settings.as_ref(ctx).open_file_editor;
|
||||
let code_panels_editor_to_open_files = *settings.as_ref(ctx).open_code_panels_file_editor;
|
||||
let layout_to_open_files = *settings.as_ref(ctx).open_file_layout;
|
||||
|
||||
let editor_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
Self::init_editor_dropdown(
|
||||
&editor_to_open_files,
|
||||
&mut dropdown,
|
||||
ExternalEditorAction::SetEditor,
|
||||
ctx,
|
||||
);
|
||||
dropdown
|
||||
});
|
||||
let code_panels_editor_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
Self::init_editor_dropdown(
|
||||
&code_panels_editor_to_open_files,
|
||||
&mut dropdown,
|
||||
ExternalEditorAction::SetCodePanelsEditor,
|
||||
ctx,
|
||||
);
|
||||
dropdown
|
||||
});
|
||||
let layout_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
Self::init_layout_dropdown(&layout_to_open_files, &mut dropdown, ctx);
|
||||
dropdown
|
||||
});
|
||||
ctx.subscribe_to_model(
|
||||
&EditorSettings::handle(ctx),
|
||||
|me, editor_settings, _, ctx| {
|
||||
me.editor_dropdown.update(ctx, |dropdown, ctx| {
|
||||
let editor = *editor_settings.as_ref(ctx).open_file_editor;
|
||||
Self::init_editor_dropdown(
|
||||
&editor,
|
||||
dropdown,
|
||||
ExternalEditorAction::SetEditor,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
me.code_panels_editor_dropdown.update(ctx, |dropdown, ctx| {
|
||||
let editor = *editor_settings.as_ref(ctx).open_code_panels_file_editor;
|
||||
Self::init_editor_dropdown(
|
||||
&editor,
|
||||
dropdown,
|
||||
ExternalEditorAction::SetCodePanelsEditor,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
ctx.notify()
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
editor_dropdown,
|
||||
code_panels_editor_dropdown,
|
||||
layout_dropdown,
|
||||
tabbed_editor_view_mouse_state: Default::default(),
|
||||
prefer_markdown_viewer_switch: Default::default(),
|
||||
markdown_viewer_mouse_state: Default::default(),
|
||||
local_only_icon_states: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_layout_dropdown(
|
||||
layout_to_open_files: &EditorLayout,
|
||||
dropdown: &mut Dropdown<ExternalEditorAction>,
|
||||
ctx: &mut ViewContext<Dropdown<ExternalEditorAction>>,
|
||||
) {
|
||||
let default_option_text = "Split Pane";
|
||||
let default_app = DropdownItem::new(
|
||||
default_option_text,
|
||||
ExternalEditorAction::SetLayout(EditorLayout::SplitPane),
|
||||
);
|
||||
|
||||
let mut items = vec![default_app];
|
||||
items.push(DropdownItem::new(
|
||||
"New Tab",
|
||||
ExternalEditorAction::SetLayout(EditorLayout::NewTab),
|
||||
));
|
||||
|
||||
dropdown.set_items(items, ctx);
|
||||
match layout_to_open_files {
|
||||
EditorLayout::SplitPane => dropdown.set_selected_by_name(default_option_text, ctx),
|
||||
EditorLayout::NewTab => dropdown.set_selected_by_name("New Tab", ctx),
|
||||
};
|
||||
}
|
||||
|
||||
fn init_editor_dropdown(
|
||||
editor_to_open_files: &EditorChoice,
|
||||
dropdown: &mut Dropdown<ExternalEditorAction>,
|
||||
mut make_action: impl FnMut(EditorChoice) -> ExternalEditorAction,
|
||||
ctx: &mut ViewContext<Dropdown<ExternalEditorAction>>,
|
||||
) {
|
||||
let default_option_text = "Default App";
|
||||
let default_app = DropdownItem::new(
|
||||
default_option_text,
|
||||
make_action(EditorChoice::SystemDefault),
|
||||
);
|
||||
|
||||
let mut items = vec![default_app];
|
||||
|
||||
items.push(DropdownItem::new("Warp", make_action(EditorChoice::Warp)));
|
||||
if FeatureFlag::AllowOpeningFileLinksUsingEditorEnv.is_enabled() {
|
||||
items.push(DropdownItem::new(
|
||||
"$EDITOR",
|
||||
make_action(EditorChoice::EnvEditor),
|
||||
));
|
||||
}
|
||||
for editor in SUPPORTED_EDITORS {
|
||||
if editor.is_installed(ctx) {
|
||||
let editor_name = format!("{editor}");
|
||||
items.push(DropdownItem::new(
|
||||
editor_name,
|
||||
make_action(EditorChoice::ExternalEditor(*editor)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
dropdown.set_items(items, ctx);
|
||||
match editor_to_open_files {
|
||||
EditorChoice::ExternalEditor(editor) => {
|
||||
dropdown.set_selected_by_name(format!("{editor}"), ctx)
|
||||
}
|
||||
EditorChoice::Warp => dropdown.set_selected_by_name("Warp", ctx),
|
||||
EditorChoice::EnvEditor => dropdown.set_selected_by_name("$EDITOR", ctx),
|
||||
EditorChoice::SystemDefault => dropdown.set_selected_by_name(default_option_text, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
/// Handles [`ExternalEditorAction::SetEditor`] by updating the external editor settings.
|
||||
fn set_editor(&mut self, editor: &EditorChoice, ctx: &mut ViewContext<Self>) {
|
||||
EditorSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings.open_file_editor.set_value(*editor, ctx));
|
||||
});
|
||||
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::FeaturesPageAction {
|
||||
action: "SetEditor".to_string(),
|
||||
value: format!("{editor:?}")
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
|
||||
fn set_code_panels_editor(&mut self, editor: &EditorChoice, ctx: &mut ViewContext<Self>) {
|
||||
EditorSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings
|
||||
.open_code_panels_file_editor
|
||||
.set_value(*editor, ctx));
|
||||
});
|
||||
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::FeaturesPageAction {
|
||||
action: "SetCodePanelsEditor".to_string(),
|
||||
value: format!("{editor:?}")
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
|
||||
// Handles [`ExternalEditorAction::SetLayout`] by updating the external editor layout settings.
|
||||
fn set_layout(&mut self, layout: &EditorLayout, ctx: &mut ViewContext<Self>) {
|
||||
EditorSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings.open_file_layout.set_value(*layout, ctx));
|
||||
});
|
||||
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::FeaturesPageAction {
|
||||
action: "SetLayout".to_string(),
|
||||
value: format!("{layout:?}")
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles [`ExternalEditorAction::TogglePreferMarkdownViewer`]
|
||||
/// preference.
|
||||
fn toggle_prefer_markdown_viewer(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let new_value = EditorSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
let new_value = settings.prefer_markdown_viewer.toggle_and_save_value(ctx);
|
||||
report_if_error!(new_value);
|
||||
new_value.unwrap_or(PreferMarkdownViewer::default_value())
|
||||
});
|
||||
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::FeaturesPageAction {
|
||||
action: "TogglePreferMarkdownViewer".to_string(),
|
||||
value: new_value.to_string()
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles [`ExternalEditorAction::TogglePreferTabbedEditorView`] by updating the tabbed file viewer preference.
|
||||
fn toggle_prefer_tabbed_editor_view(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let new_value = EditorSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
let new_value = settings
|
||||
.prefer_tabbed_editor_view
|
||||
.toggle_and_save_value(ctx);
|
||||
report_if_error!(new_value);
|
||||
new_value.unwrap_or(PreferTabbedEditorView::default_value())
|
||||
});
|
||||
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::FeaturesPageAction {
|
||||
action: "ToggleTabbedEditorView".to_string(),
|
||||
value: new_value.to_string()
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ExternalEditorView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ExternalEditorView {
|
||||
fn ui_name() -> &'static str {
|
||||
"ExternalEditorView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &warpui::AppContext) -> Box<dyn warpui::Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
|
||||
let default_editor = render_dropdown_item(
|
||||
appearance,
|
||||
"Choose an editor to open file links",
|
||||
None,
|
||||
None,
|
||||
LocalOnlyIconState::for_setting(
|
||||
OpenFileEditor::storage_key(),
|
||||
OpenFileEditor::sync_to_cloud(),
|
||||
&mut self.local_only_icon_states.borrow_mut(),
|
||||
app,
|
||||
),
|
||||
None,
|
||||
&self.editor_dropdown,
|
||||
);
|
||||
|
||||
let code_panels_editor = render_dropdown_item(
|
||||
appearance,
|
||||
"Choose an editor to open files from the code review panel, project explorer, and global search",
|
||||
None,
|
||||
None,
|
||||
LocalOnlyIconState::for_setting(
|
||||
OpenCodePanelsFileEditor::storage_key(),
|
||||
OpenCodePanelsFileEditor::sync_to_cloud(),
|
||||
&mut self.local_only_icon_states.borrow_mut(),
|
||||
app,
|
||||
),
|
||||
None,
|
||||
&self.code_panels_editor_dropdown,
|
||||
);
|
||||
|
||||
let default_layout = render_dropdown_item(
|
||||
appearance,
|
||||
"Choose a layout to open files in Warp",
|
||||
None,
|
||||
None,
|
||||
LocalOnlyIconState::for_setting(
|
||||
OpenFileLayout::storage_key(),
|
||||
OpenFileLayout::sync_to_cloud(),
|
||||
&mut self.local_only_icon_states.borrow_mut(),
|
||||
app,
|
||||
),
|
||||
None,
|
||||
&self.layout_dropdown,
|
||||
);
|
||||
|
||||
let mut column = Flex::column()
|
||||
.with_child(default_editor)
|
||||
.with_child(code_panels_editor)
|
||||
.with_child(default_layout);
|
||||
|
||||
if FeatureFlag::TabbedEditorView.is_enabled() {
|
||||
column.add_child(render_body_item::<ExternalEditorAction>(
|
||||
TABBED_FILE_VIEWER_TOGGLE_HEADER.into(),
|
||||
None,
|
||||
LocalOnlyIconState::for_setting(
|
||||
PreferTabbedEditorView::storage_key(),
|
||||
PreferTabbedEditorView::sync_to_cloud(),
|
||||
&mut self.local_only_icon_states.borrow_mut(),
|
||||
app,
|
||||
),
|
||||
ToggleState::Enabled,
|
||||
appearance,
|
||||
appearance
|
||||
.ui_builder()
|
||||
.switch(self.tabbed_editor_view_mouse_state.clone())
|
||||
.check(
|
||||
*EditorSettings::as_ref(app)
|
||||
.prefer_tabbed_editor_view
|
||||
.value(),
|
||||
)
|
||||
.build()
|
||||
.on_click(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(ExternalEditorAction::ToggleTabbedEditorView);
|
||||
})
|
||||
.finish(),
|
||||
Some(TABBED_FILE_VIEWER_TOGGLE_DESCRIPTION.into()),
|
||||
));
|
||||
}
|
||||
|
||||
column.add_child(render_body_item::<ExternalEditorAction>(
|
||||
"Open Markdown files in Warp's Markdown Viewer by default".to_string(),
|
||||
Some(AdditionalInfo {
|
||||
mouse_state: self.markdown_viewer_mouse_state.clone(),
|
||||
on_click_action: Some(ExternalEditorAction::OpenUrl(
|
||||
"https://docs.warp.dev/terminal/more-features/markdown-viewer".to_string(),
|
||||
)),
|
||||
secondary_text: None,
|
||||
tooltip_override_text: None,
|
||||
}),
|
||||
LocalOnlyIconState::for_setting(
|
||||
PreferMarkdownViewer::storage_key(),
|
||||
PreferMarkdownViewer::sync_to_cloud(),
|
||||
&mut self.local_only_icon_states.borrow_mut(),
|
||||
app,
|
||||
),
|
||||
ToggleState::Enabled,
|
||||
appearance,
|
||||
appearance
|
||||
.ui_builder()
|
||||
.switch(self.prefer_markdown_viewer_switch.clone())
|
||||
.check(*EditorSettings::as_ref(app).prefer_markdown_viewer.value())
|
||||
.build()
|
||||
.on_click(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(ExternalEditorAction::TogglePreferMarkdownViewer);
|
||||
})
|
||||
.finish(),
|
||||
None,
|
||||
));
|
||||
|
||||
column.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for ExternalEditorView {
|
||||
type Action = ExternalEditorAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
ExternalEditorAction::SetEditor(editor) => self.set_editor(editor, ctx),
|
||||
ExternalEditorAction::SetCodePanelsEditor(editor) => {
|
||||
self.set_code_panels_editor(editor, ctx)
|
||||
}
|
||||
ExternalEditorAction::SetLayout(layout) => self.set_layout(layout, ctx),
|
||||
ExternalEditorAction::TogglePreferMarkdownViewer => {
|
||||
self.toggle_prefer_markdown_viewer(ctx)
|
||||
}
|
||||
ExternalEditorAction::ToggleTabbedEditorView => {
|
||||
self.toggle_prefer_tabbed_editor_view(ctx);
|
||||
}
|
||||
ExternalEditorAction::OpenUrl(url) => {
|
||||
ctx.open_url(url.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
pub mod undo_close;
|
||||
pub use undo_close::UndoCloseView;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "local_fs")] {
|
||||
pub mod external_editor;
|
||||
pub use external_editor::ExternalEditorView;
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "local_tty")] {
|
||||
pub mod startup_shell;
|
||||
pub use startup_shell::StartupShellView;
|
||||
|
||||
pub mod working_directory;
|
||||
pub use working_directory::WorkingDirectoryView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
use warpui::{
|
||||
elements::{CrossAxisAlignment, Fill, Flex, ParentElement, Shrinkable},
|
||||
presenter::ChildView,
|
||||
ui_components::components::{Coords, UiComponent, UiComponentStyles},
|
||||
Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
editor::{EditorView, Event, SingleLineEditorOptions, TextOptions},
|
||||
report_if_error, send_telemetry_from_ctx,
|
||||
server::telemetry::TelemetryEvent,
|
||||
terminal::{
|
||||
available_shells::{AvailableShell, AvailableShells},
|
||||
local_tty::shell::is_valid_path_or_command_for_supported_shell,
|
||||
session_settings::{SessionSettings, SessionSettingsChangedEvent},
|
||||
},
|
||||
view_components::{dropdown::TOP_MENU_BAR_HEIGHT, Dropdown, DropdownItem},
|
||||
};
|
||||
|
||||
/// A view for configuring the initial shell for new sessions. This can be the
|
||||
/// user's login shell, the default installed version of zsh, bash, or fish,
|
||||
/// or an arbitrary user-provided path.
|
||||
pub struct StartupShellView {
|
||||
/// This dropdown is for selecting between the login shell, supported shells,
|
||||
/// and a custom shell.
|
||||
shell_dropdown: ViewHandle<Dropdown<NewSessionShellAction>>,
|
||||
/// This flags whether or not to show the custom path editor. It's toggled
|
||||
/// when the user chooses different dropdown options.
|
||||
should_display_editor: bool,
|
||||
/// If the user chose a custom shell path, they enter it in this editor.
|
||||
custom_path_editor: ViewHandle<EditorView>,
|
||||
/// This holds the current validity of the user's custom shell path, for
|
||||
/// drawing an error border if it's invalid.
|
||||
is_custom_path_valid: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NewSessionShellAction {
|
||||
/// Changes the user's startup shell to the given option. This also hides
|
||||
/// the custom shell path editor if a non-custom shell was chosen.
|
||||
Set(AvailableShell),
|
||||
/// Displays the custom shell path editor.
|
||||
ShowCustomPathInput,
|
||||
}
|
||||
|
||||
impl NewSessionShellAction {
|
||||
/// Produces a [`TelemetryEvent`] that corresponds to this UI action.
|
||||
///
|
||||
/// This tracks both high-level information about which shells users select
|
||||
/// and when they switch to the custom path UI (so we can see if they're
|
||||
/// trying to use a custom shell but are unable to).
|
||||
fn telemetry_event(&self) -> TelemetryEvent {
|
||||
match self {
|
||||
NewSessionShellAction::Set(option) => TelemetryEvent::FeaturesPageAction {
|
||||
action: "NewSessionShellOverride".to_string(),
|
||||
value: option.telemetry_value(),
|
||||
},
|
||||
NewSessionShellAction::ShowCustomPathInput => TelemetryEvent::FeaturesPageAction {
|
||||
action: "ShowCustomPathInput".to_string(),
|
||||
value: String::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StartupShellView {
|
||||
/// Creates a new `StartupShellView`. The UI is initialized with the user's
|
||||
/// current startup shell setting.
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let custom_shell_text = AvailableShells::handle(ctx).read(ctx, |shells, ctx| {
|
||||
shells.get_user_preferred_shell(ctx).get_custom_path()
|
||||
});
|
||||
|
||||
ctx.subscribe_to_model(&SessionSettings::handle(ctx), |me, _, event, ctx| {
|
||||
if matches!(
|
||||
event,
|
||||
SessionSettingsChangedEvent::StartupShellOverride { .. }
|
||||
) {
|
||||
Self::update_dropdown_state(me.shell_dropdown.clone(), ctx);
|
||||
me.maybe_update_editor_state(ctx);
|
||||
}
|
||||
ctx.notify()
|
||||
});
|
||||
|
||||
let shell_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
dropdown.set_top_bar_max_width(200.);
|
||||
dropdown
|
||||
});
|
||||
|
||||
Self::update_dropdown_state(shell_dropdown.clone(), ctx);
|
||||
|
||||
let shell_editor = ctx.add_typed_action_view(|ctx| {
|
||||
let appearance_handle = Appearance::handle(ctx);
|
||||
let options = SingleLineEditorOptions {
|
||||
text: TextOptions::ui_font_size(appearance_handle.as_ref(ctx)),
|
||||
..Default::default()
|
||||
};
|
||||
let mut editor = EditorView::single_line(options, ctx);
|
||||
editor.set_placeholder_text("Executable path", ctx);
|
||||
|
||||
if let Some(shell) = custom_shell_text.as_ref() {
|
||||
editor.set_buffer_text(shell, ctx);
|
||||
}
|
||||
|
||||
editor
|
||||
});
|
||||
|
||||
ctx.subscribe_to_view(&shell_editor, move |me, _, event, ctx| {
|
||||
me.handle_editor_event(event, ctx);
|
||||
});
|
||||
|
||||
Self {
|
||||
shell_dropdown,
|
||||
custom_path_editor: shell_editor,
|
||||
is_custom_path_valid: true,
|
||||
should_display_editor: custom_shell_text.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_update_editor_state(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let custom_shell_path = AvailableShells::handle(ctx).read(ctx, |shells, ctx| {
|
||||
shells.get_user_preferred_shell(ctx).get_custom_path()
|
||||
});
|
||||
if let Some(custom_shell_path) = custom_shell_path {
|
||||
self.should_display_editor = true;
|
||||
self.custom_path_editor.update(ctx, |editor_view, ctx| {
|
||||
editor_view.set_buffer_text(&custom_shell_path, ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn update_dropdown_state(
|
||||
dropdown: ViewHandle<Dropdown<NewSessionShellAction>>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
dropdown.update(ctx, |dropdown, ctx| {
|
||||
let mut items = vec![DropdownItem::new(
|
||||
"Default",
|
||||
NewSessionShellAction::Set(AvailableShell::default()),
|
||||
)];
|
||||
let shell_to_index = AvailableShells::handle(ctx).read(ctx, |model, _| {
|
||||
let mut shell_to_index = std::collections::HashMap::new();
|
||||
// Iterate over each shell in the model and add it to the dropdown if it's valid.
|
||||
for shell_entry in model.get_available_shells() {
|
||||
items.push(DropdownItem::new(
|
||||
model.display_name_for_shell(shell_entry),
|
||||
NewSessionShellAction::Set(shell_entry.clone()),
|
||||
));
|
||||
shell_to_index.insert(shell_entry.clone(), items.len() - 1);
|
||||
}
|
||||
|
||||
shell_to_index
|
||||
});
|
||||
|
||||
items.push(DropdownItem::new(
|
||||
"Custom",
|
||||
NewSessionShellAction::ShowCustomPathInput,
|
||||
));
|
||||
let custom_index = items.len() - 1;
|
||||
dropdown.set_items(items, ctx);
|
||||
|
||||
let selected_shell = AvailableShells::as_ref(ctx).get_user_preferred_shell(ctx);
|
||||
|
||||
let selected_index = if selected_shell.get_custom_path().is_some() {
|
||||
custom_index
|
||||
} else {
|
||||
shell_to_index.get(&selected_shell).copied().unwrap_or(0)
|
||||
};
|
||||
|
||||
dropdown.set_selected_by_index(selected_index, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
/// This callback updates the startup shell override setting based on user
|
||||
/// input. If the user hits Enter or the input loses focus, the new setting
|
||||
/// is saved (they can also save it by clicking outside of the text field).
|
||||
fn handle_editor_event(&mut self, event: &Event, ctx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
Event::Edited(_) => {
|
||||
let buffer_text = self.custom_path_editor.as_ref(ctx).buffer_text(ctx);
|
||||
let new_validity = is_valid_path_or_command_for_supported_shell(&buffer_text);
|
||||
if new_validity != self.is_custom_path_valid {
|
||||
self.is_custom_path_valid = new_validity;
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
Event::Blurred | Event::Enter => {
|
||||
let buffer_text = self.custom_path_editor.as_ref(ctx).buffer_text(ctx);
|
||||
if let Ok(shell) = AvailableShell::try_from(buffer_text.as_str()) {
|
||||
self.handle_action(&NewSessionShellAction::Set(shell), ctx);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for StartupShellView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for StartupShellView {
|
||||
fn ui_name() -> &'static str {
|
||||
"StartupShellView"
|
||||
}
|
||||
|
||||
/// Renders controls to change the default shell for new sessions.
|
||||
fn render(&self, app: &warpui::AppContext) -> Box<dyn warpui::Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let ui_builder = appearance.ui_builder();
|
||||
let theme = appearance.theme();
|
||||
|
||||
let mut row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
row.add_child(ChildView::new(&self.shell_dropdown).finish());
|
||||
|
||||
if self.should_display_editor {
|
||||
let border_color: Option<Fill> = if self.is_custom_path_valid {
|
||||
None
|
||||
} else {
|
||||
Some(crate::themes::theme::Fill::error().into())
|
||||
};
|
||||
|
||||
row.add_child(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
ui_builder
|
||||
.text_input(self.custom_path_editor.clone())
|
||||
.with_style(UiComponentStyles {
|
||||
border_color,
|
||||
// Make sure the editor is the same height as the dropdown it's next to.
|
||||
height: Some(TOP_MENU_BAR_HEIGHT),
|
||||
padding: Some(Coords::uniform(7.)),
|
||||
margin: Some(Coords::default().left(8.).right(8.)),
|
||||
font_size: Some(appearance.ui_font_size()),
|
||||
background: Some(theme.surface_2().into()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
}
|
||||
|
||||
row.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for StartupShellView {
|
||||
type Action = NewSessionShellAction;
|
||||
|
||||
/// Handles a `NewSessionShellAction`, either triggered by the shell dropdown
|
||||
/// or by custom path editor events.
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
NewSessionShellAction::ShowCustomPathInput => {
|
||||
self.should_display_editor = true;
|
||||
ctx.notify();
|
||||
}
|
||||
NewSessionShellAction::Set(shell) => {
|
||||
if shell.get_custom_path().is_none() && self.should_display_editor {
|
||||
self.should_display_editor = false;
|
||||
ctx.notify();
|
||||
}
|
||||
AvailableShells::handle(ctx).update(ctx, |shells, ctx| {
|
||||
report_if_error!(shells.set_user_preferred_shell(shell.clone(), ctx));
|
||||
});
|
||||
}
|
||||
}
|
||||
send_telemetry_from_ctx!(action.telemetry_event(), ctx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
use std::{cell::RefCell, collections::HashMap, time::Duration};
|
||||
|
||||
use settings::{Setting, ToggleableSetting};
|
||||
use warpui::{
|
||||
elements::{
|
||||
Container, CrossAxisAlignment, Flex, MainAxisAlignment, MouseStateHandle, ParentElement,
|
||||
Text,
|
||||
},
|
||||
ui_components::{
|
||||
components::{Coords, UiComponent, UiComponentStyles},
|
||||
switch::SwitchStateHandle,
|
||||
},
|
||||
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
editor::{self, EditorView, SingleLineEditorOptions, TextOptions},
|
||||
report_if_error,
|
||||
settings_view::{
|
||||
features_page::render_group,
|
||||
settings_page::{render_body_item, LocalOnlyIconState, ToggleState},
|
||||
},
|
||||
undo_close::{settings::UndoCloseEnabled, UndoCloseSettings},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Action {
|
||||
ToggleUndoCloseEnabled,
|
||||
UpdateGracePeriod,
|
||||
}
|
||||
|
||||
/// A view containing settings relating to the undo close feature.
|
||||
pub struct UndoCloseView {
|
||||
/// State for the enable/disable toggle switch.
|
||||
switch_state: SwitchStateHandle,
|
||||
/// An editor for modifying the undo close grace period.
|
||||
grace_period_editor: ViewHandle<EditorView>,
|
||||
/// Whether or not the grace period value is valid.
|
||||
is_grace_period_valid: bool,
|
||||
/// State for the local only icon tooltip.
|
||||
local_only_icon_states: RefCell<HashMap<String, MouseStateHandle>>,
|
||||
}
|
||||
|
||||
impl UndoCloseView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let grace_period_editor = ctx.add_typed_action_view(|ctx| {
|
||||
EditorView::single_line(
|
||||
SingleLineEditorOptions {
|
||||
text: TextOptions::ui_font_size(Appearance::as_ref(ctx)),
|
||||
..Default::default()
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
|
||||
ctx.subscribe_to_model(&UndoCloseSettings::handle(ctx), |me, _, _, ctx| {
|
||||
// Update the value of the grace period input to match the new setting
|
||||
me.grace_period_editor.update(ctx, |editor, ctx| {
|
||||
editor.set_buffer_text(
|
||||
&format!(
|
||||
"{}",
|
||||
UndoCloseSettings::handle(ctx)
|
||||
.as_ref(ctx)
|
||||
.grace_period
|
||||
.as_secs_f32()
|
||||
),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
ctx.notify()
|
||||
});
|
||||
|
||||
ctx.subscribe_to_view(&grace_period_editor, move |me, _, event, ctx| {
|
||||
me.handle_editor_event(event, ctx);
|
||||
});
|
||||
|
||||
let grace_period = UndoCloseSettings::as_ref(ctx).grace_period.as_secs();
|
||||
grace_period_editor.update(ctx, |editor, ctx| {
|
||||
editor.set_buffer_text(&grace_period.to_string(), ctx);
|
||||
});
|
||||
Self {
|
||||
switch_state: Default::default(),
|
||||
local_only_icon_states: Default::default(),
|
||||
grace_period_editor,
|
||||
is_grace_period_valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// This callback updates the undo close setting based on user input. If the
|
||||
/// user hits Enter or the input loses focus, the new setting is saved (they
|
||||
/// can also save it by clicking outside of the text field).
|
||||
fn handle_editor_event(&mut self, event: &editor::Event, ctx: &mut ViewContext<Self>) {
|
||||
use editor::Event;
|
||||
match event {
|
||||
Event::Edited(_) => {
|
||||
let buffer_text = self.grace_period_editor.as_ref(ctx).buffer_text(ctx);
|
||||
let new_validity = Self::parse_grace_period(&buffer_text).is_some();
|
||||
if new_validity != self.is_grace_period_valid {
|
||||
self.is_grace_period_valid = new_validity;
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
Event::Blurred | Event::Enter => {
|
||||
self.handle_action(&Action::UpdateGracePeriod, ctx);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses user-entered text into a grace period duration, returning
|
||||
/// None if the text isn't a valid grace period.
|
||||
fn parse_grace_period(text: &str) -> Option<Duration> {
|
||||
text.parse::<u64>().ok().map(Duration::from_secs)
|
||||
}
|
||||
|
||||
/// Renders the editor for the grace period duration.
|
||||
fn render_grace_period_editor(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
|
||||
let border_color = if self.is_grace_period_valid {
|
||||
None
|
||||
} else {
|
||||
Some(crate::themes::theme::Fill::error().into())
|
||||
};
|
||||
|
||||
let editor_style = UiComponentStyles {
|
||||
border_color,
|
||||
width: Some(40.),
|
||||
padding: Some(Coords::uniform(5.)),
|
||||
background: Some(theme.surface_2().into()),
|
||||
..Default::default()
|
||||
};
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"Grace period (seconds)",
|
||||
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(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.text_input(self.grace_period_editor.clone())
|
||||
.with_style(editor_style)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for UndoCloseView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for UndoCloseView {
|
||||
fn ui_name() -> &'static str {
|
||||
"UndoCloseView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let ui_builder = appearance.ui_builder();
|
||||
|
||||
let settings = UndoCloseSettings::as_ref(app);
|
||||
let enabled = *settings.enabled;
|
||||
|
||||
let mut column = Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(render_body_item::<Action>(
|
||||
"Enable reopening of closed sessions".into(),
|
||||
None,
|
||||
LocalOnlyIconState::for_setting(
|
||||
UndoCloseEnabled::storage_key(),
|
||||
UndoCloseEnabled::sync_to_cloud(),
|
||||
&mut self.local_only_icon_states.borrow_mut(),
|
||||
app,
|
||||
),
|
||||
ToggleState::Enabled,
|
||||
appearance,
|
||||
ui_builder
|
||||
.switch(self.switch_state.clone())
|
||||
.check(enabled)
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(Action::ToggleUndoCloseEnabled);
|
||||
})
|
||||
.finish(),
|
||||
None,
|
||||
));
|
||||
|
||||
if enabled {
|
||||
column.add_child(render_group(
|
||||
[self.render_grace_period_editor(appearance)],
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
|
||||
column.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for UndoCloseView {
|
||||
type Action = Action;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut warpui::ViewContext<Self>) {
|
||||
match action {
|
||||
Action::ToggleUndoCloseEnabled => {
|
||||
UndoCloseSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings.enabled.toggle_and_save_value(ctx));
|
||||
})
|
||||
}
|
||||
Action::UpdateGracePeriod => {
|
||||
let grace_period_secs = self
|
||||
.grace_period_editor
|
||||
.read(ctx, |editor, ctx| editor.buffer_text(ctx));
|
||||
let Some(grace_period) = Self::parse_grace_period(&grace_period_secs) else {
|
||||
self.is_grace_period_valid = false;
|
||||
return;
|
||||
};
|
||||
|
||||
self.is_grace_period_valid = true;
|
||||
UndoCloseSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings.grace_period.set_value(grace_period, ctx));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
use itertools::Itertools;
|
||||
use warpui::{
|
||||
elements::{Container, CrossAxisAlignment, Flex, ParentElement, Shrinkable},
|
||||
presenter::ChildView,
|
||||
ui_components::components::{Coords, UiComponent, UiComponentStyles},
|
||||
Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
editor::{EditorView, Event as EditorEvent, SingleLineEditorOptions, TextOptions},
|
||||
report_if_error, send_telemetry_from_ctx,
|
||||
server::telemetry::TelemetryEvent,
|
||||
settings_view::features_page::render_group,
|
||||
terminal::session_settings::*,
|
||||
view_components::{dropdown::TOP_MENU_BAR_HEIGHT, Dropdown, DropdownItem},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum WorkingDirectoryAction {
|
||||
/// Sets the mode that should be used for all new sessions, independent of
|
||||
/// source. A value of None indicates that the mode should be configured
|
||||
/// per-source instead of globally (i.e.: "advanced" mode).
|
||||
SetGlobalWorkingDirectoryMode(Option<WorkingDirectoryMode>),
|
||||
/// Sets the mode that should be used for new sessions spawned from the
|
||||
/// given source (e.g.: new tab/window/split pane).
|
||||
SetPerSourceWorkingDirectoryMode(NewSessionSource, WorkingDirectoryMode),
|
||||
/// Sets the path that will be used for [`WorkingDirectoryMode::CustomDir`]
|
||||
/// for the given source (where None represents global configuration).
|
||||
SetCustomWorkingDirectoryValue(Option<NewSessionSource>, String),
|
||||
}
|
||||
|
||||
/// A view for configuring the initial working directory for new sessions,
|
||||
/// either globally or on a per-source (new tab/window/split pane) basis.
|
||||
pub struct WorkingDirectoryView {
|
||||
working_directory_dropdown: ViewHandle<Dropdown<WorkingDirectoryAction>>,
|
||||
working_directory_editor: ViewHandle<EditorView>,
|
||||
new_window_working_directory_dropdown: ViewHandle<Dropdown<WorkingDirectoryAction>>,
|
||||
new_window_working_directory_editor: ViewHandle<EditorView>,
|
||||
new_tab_working_directory_dropdown: ViewHandle<Dropdown<WorkingDirectoryAction>>,
|
||||
new_tab_working_directory_editor: ViewHandle<EditorView>,
|
||||
split_pane_working_directory_dropdown: ViewHandle<Dropdown<WorkingDirectoryAction>>,
|
||||
split_pane_working_directory_editor: ViewHandle<EditorView>,
|
||||
}
|
||||
|
||||
impl WorkingDirectoryView {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let working_directory_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
init_top_level_dropdown(&mut dropdown, ctx);
|
||||
dropdown
|
||||
});
|
||||
let working_directory_editor = create_editor(None, ctx);
|
||||
|
||||
let new_window_working_directory_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
init_per_source_dropdown(&mut dropdown, NewSessionSource::Window, ctx);
|
||||
dropdown
|
||||
});
|
||||
let new_window_working_directory_editor =
|
||||
create_editor(Some(NewSessionSource::Window), ctx);
|
||||
|
||||
let new_tab_working_directory_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
init_per_source_dropdown(&mut dropdown, NewSessionSource::Tab, ctx);
|
||||
dropdown
|
||||
});
|
||||
let new_tab_working_directory_editor = create_editor(Some(NewSessionSource::Tab), ctx);
|
||||
|
||||
let split_pane_working_directory_dropdown = ctx.add_typed_action_view(|ctx| {
|
||||
let mut dropdown = Dropdown::new(ctx);
|
||||
init_per_source_dropdown(&mut dropdown, NewSessionSource::SplitPane, ctx);
|
||||
dropdown
|
||||
});
|
||||
let split_pane_working_directory_editor =
|
||||
create_editor(Some(NewSessionSource::SplitPane), ctx);
|
||||
|
||||
ctx.subscribe_to_model(&SessionSettings::handle(ctx), |me, _, event, ctx| {
|
||||
if matches!(
|
||||
event,
|
||||
SessionSettingsChangedEvent::WorkingDirectoryConfig { .. }
|
||||
) {
|
||||
me.working_directory_dropdown.update(ctx, |dropdown, ctx| {
|
||||
init_top_level_dropdown(dropdown, ctx);
|
||||
ctx.notify();
|
||||
});
|
||||
me.new_window_working_directory_dropdown
|
||||
.update(ctx, |dropdown, ctx| {
|
||||
init_per_source_dropdown(dropdown, NewSessionSource::Window, ctx);
|
||||
ctx.notify();
|
||||
});
|
||||
me.new_tab_working_directory_dropdown
|
||||
.update(ctx, |dropdown, ctx| {
|
||||
init_per_source_dropdown(dropdown, NewSessionSource::Tab, ctx);
|
||||
ctx.notify();
|
||||
});
|
||||
me.split_pane_working_directory_dropdown
|
||||
.update(ctx, |dropdown, ctx| {
|
||||
init_per_source_dropdown(dropdown, NewSessionSource::SplitPane, ctx);
|
||||
ctx.notify();
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
working_directory_dropdown,
|
||||
working_directory_editor,
|
||||
new_window_working_directory_dropdown,
|
||||
new_window_working_directory_editor,
|
||||
new_tab_working_directory_dropdown,
|
||||
new_tab_working_directory_editor,
|
||||
split_pane_working_directory_dropdown,
|
||||
split_pane_working_directory_editor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for WorkingDirectoryView {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for WorkingDirectoryView {
|
||||
fn ui_name() -> &'static str {
|
||||
"WorkingDirectoryView"
|
||||
}
|
||||
|
||||
fn render(&self, app: &warpui::AppContext) -> Box<dyn warpui::Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let ui_builder = appearance.ui_builder();
|
||||
|
||||
let settings = SessionSettings::as_ref(app);
|
||||
let config = &settings.working_directory_config;
|
||||
|
||||
let mut column = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
|
||||
column.add_child(render_row(
|
||||
&self.working_directory_dropdown,
|
||||
&self.working_directory_editor,
|
||||
config.global.mode == WorkingDirectoryMode::CustomDir && !config.advanced_mode,
|
||||
appearance,
|
||||
));
|
||||
|
||||
if config.advanced_mode {
|
||||
let items = Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_children([
|
||||
ui_builder.label("New window").build().finish(),
|
||||
render_row(
|
||||
&self.new_window_working_directory_dropdown,
|
||||
&self.new_window_working_directory_editor,
|
||||
config.new_window.mode == WorkingDirectoryMode::CustomDir,
|
||||
appearance,
|
||||
),
|
||||
ui_builder.label("New tab").build().finish(),
|
||||
render_row(
|
||||
&self.new_tab_working_directory_dropdown,
|
||||
&self.new_tab_working_directory_editor,
|
||||
config.new_tab.mode == WorkingDirectoryMode::CustomDir,
|
||||
appearance,
|
||||
),
|
||||
ui_builder.label("Split pane").build().finish(),
|
||||
render_row(
|
||||
&self.split_pane_working_directory_dropdown,
|
||||
&self.split_pane_working_directory_editor,
|
||||
config.split_pane.mode == WorkingDirectoryMode::CustomDir,
|
||||
appearance,
|
||||
),
|
||||
])
|
||||
.finish();
|
||||
column.add_child(
|
||||
Container::new(render_group(
|
||||
[Container::new(items)
|
||||
.with_margin_top(4.)
|
||||
.with_margin_bottom(2.)
|
||||
.finish()],
|
||||
appearance,
|
||||
))
|
||||
.with_margin_top(8.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
column.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for WorkingDirectoryView {
|
||||
type Action = WorkingDirectoryAction;
|
||||
|
||||
fn handle_action(&mut self, action: &WorkingDirectoryAction, ctx: &mut ViewContext<Self>) {
|
||||
use WorkingDirectoryAction::*;
|
||||
|
||||
match action {
|
||||
SetGlobalWorkingDirectoryMode(mode) => {
|
||||
SessionSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings.working_directory_config.update_and_save_value(
|
||||
|config| {
|
||||
if let Some(mode) = mode {
|
||||
config.advanced_mode = false;
|
||||
config.global.mode = *mode;
|
||||
} else {
|
||||
config.advanced_mode = true;
|
||||
}
|
||||
},
|
||||
ctx,
|
||||
));
|
||||
});
|
||||
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::InitialWorkingDirectoryConfigurationChanged {
|
||||
advanced_mode_enabled: mode.is_none()
|
||||
},
|
||||
ctx
|
||||
);
|
||||
|
||||
// Redraw settings in case we switched in or out of advanced mode.
|
||||
ctx.notify();
|
||||
}
|
||||
SetPerSourceWorkingDirectoryMode(source, mode) => {
|
||||
SessionSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings.working_directory_config.update_and_save_value(
|
||||
|config| match source {
|
||||
NewSessionSource::SplitPane => config.split_pane.mode = *mode,
|
||||
NewSessionSource::Tab => config.new_tab.mode = *mode,
|
||||
NewSessionSource::Window => config.new_window.mode = *mode,
|
||||
},
|
||||
ctx,
|
||||
));
|
||||
});
|
||||
// Redraw settings in case we changed a mode to/from "custom directory".
|
||||
ctx.notify();
|
||||
}
|
||||
SetCustomWorkingDirectoryValue(source, value) => {
|
||||
SessionSettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings.working_directory_config.update_and_save_value(
|
||||
|config| match source {
|
||||
Some(NewSessionSource::SplitPane) => {
|
||||
config.split_pane.custom_dir.clone_from(value)
|
||||
}
|
||||
Some(NewSessionSource::Tab) => {
|
||||
config.new_tab.custom_dir.clone_from(value)
|
||||
}
|
||||
Some(NewSessionSource::Window) => {
|
||||
config.new_window.custom_dir.clone_from(value)
|
||||
}
|
||||
None => config.global.custom_dir.clone_from(value),
|
||||
},
|
||||
ctx,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a single row, containing a dropdown view and editor.
|
||||
///
|
||||
/// `show_editor` controls whether the editor is currently visible.
|
||||
fn render_row(
|
||||
dropdown: &ViewHandle<Dropdown<WorkingDirectoryAction>>,
|
||||
editor: &ViewHandle<EditorView>,
|
||||
show_editor: bool,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
let mut row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
row.add_child(ChildView::new(dropdown).finish());
|
||||
if show_editor {
|
||||
row.add_child(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
appearance
|
||||
.ui_builder()
|
||||
.text_input(editor.clone())
|
||||
.with_style(UiComponentStyles {
|
||||
height: Some(TOP_MENU_BAR_HEIGHT),
|
||||
font_color: Some(pathfinder_color::ColorU::black()),
|
||||
font_size: Some(appearance.ui_font_size()),
|
||||
padding: Some(Coords::uniform(7.)),
|
||||
margin: Some(Coords::default().left(8.).right(8.)),
|
||||
background: Some(appearance.theme().surface_2().into()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
row.finish()
|
||||
}
|
||||
|
||||
/// Initializes the top-level dropdown (global configuration vs. advanced mode).
|
||||
fn init_top_level_dropdown(
|
||||
dropdown: &mut Dropdown<WorkingDirectoryAction>,
|
||||
ctx: &mut ViewContext<Dropdown<WorkingDirectoryAction>>,
|
||||
) {
|
||||
let mut items = [
|
||||
WorkingDirectoryMode::HomeDir,
|
||||
WorkingDirectoryMode::PreviousDir,
|
||||
WorkingDirectoryMode::CustomDir,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|mode| {
|
||||
DropdownItem::new(
|
||||
mode.dropdown_item_label(),
|
||||
WorkingDirectoryAction::SetGlobalWorkingDirectoryMode(Some(mode)),
|
||||
)
|
||||
})
|
||||
.collect_vec();
|
||||
items.push(DropdownItem::new(
|
||||
"Advanced".to_string(),
|
||||
WorkingDirectoryAction::SetGlobalWorkingDirectoryMode(None),
|
||||
));
|
||||
let advanced_item_index = items.len() - 1;
|
||||
dropdown.set_items(items, ctx);
|
||||
dropdown.set_top_bar_max_width(200.);
|
||||
|
||||
let config = &SessionSettings::as_ref(ctx).working_directory_config;
|
||||
if config.advanced_mode {
|
||||
dropdown.set_selected_by_index(advanced_item_index, ctx);
|
||||
} else {
|
||||
dropdown.set_selected_by_name(config.global.mode.dropdown_item_label(), ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a dropdown that relates to a particular new session source.
|
||||
fn init_per_source_dropdown(
|
||||
dropdown: &mut Dropdown<WorkingDirectoryAction>,
|
||||
source: NewSessionSource,
|
||||
ctx: &mut ViewContext<Dropdown<WorkingDirectoryAction>>,
|
||||
) {
|
||||
let items = [
|
||||
WorkingDirectoryMode::HomeDir,
|
||||
WorkingDirectoryMode::PreviousDir,
|
||||
WorkingDirectoryMode::CustomDir,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|mode| {
|
||||
DropdownItem::new(
|
||||
mode.dropdown_item_label(),
|
||||
WorkingDirectoryAction::SetPerSourceWorkingDirectoryMode(source, mode),
|
||||
)
|
||||
})
|
||||
.collect_vec();
|
||||
dropdown.set_items(items, ctx);
|
||||
dropdown.set_top_bar_max_width(200.);
|
||||
|
||||
let config = &SessionSettings::as_ref(ctx).working_directory_config;
|
||||
let source_config = match source {
|
||||
NewSessionSource::SplitPane => &config.split_pane,
|
||||
NewSessionSource::Tab => &config.new_tab,
|
||||
NewSessionSource::Window => &config.new_window,
|
||||
};
|
||||
dropdown.set_selected_by_name(source_config.mode.dropdown_item_label(), ctx);
|
||||
}
|
||||
|
||||
/// Creates a new editor view for entering a custom initial directory path.
|
||||
fn create_editor(
|
||||
source: Option<NewSessionSource>,
|
||||
ctx: &mut ViewContext<WorkingDirectoryView>,
|
||||
) -> ViewHandle<EditorView> {
|
||||
let editor = {
|
||||
let appearance = Appearance::as_ref(ctx);
|
||||
let options = SingleLineEditorOptions {
|
||||
text: TextOptions::ui_font_size(appearance),
|
||||
..Default::default()
|
||||
};
|
||||
ctx.add_typed_action_view(|ctx| {
|
||||
let mut editor = EditorView::single_line(options, ctx);
|
||||
editor.set_placeholder_text("Directory path", ctx);
|
||||
editor
|
||||
})
|
||||
};
|
||||
let initial_value = {
|
||||
let config = &SessionSettings::as_ref(ctx).working_directory_config;
|
||||
let source_config = match source {
|
||||
None => &config.global,
|
||||
Some(NewSessionSource::SplitPane) => &config.split_pane,
|
||||
Some(NewSessionSource::Tab) => &config.new_tab,
|
||||
Some(NewSessionSource::Window) => &config.new_window,
|
||||
};
|
||||
source_config.custom_dir.clone()
|
||||
};
|
||||
editor.update(ctx, |editor, ctx| {
|
||||
editor.set_buffer_text(&initial_value, ctx);
|
||||
});
|
||||
let editor_handle = editor.clone();
|
||||
ctx.subscribe_to_view(&editor, move |me, _, event, ctx| match event {
|
||||
// If the user presses enter or focus moves out of the editor view,
|
||||
// update our configuration to match the current value.
|
||||
EditorEvent::Blurred | EditorEvent::Enter => {
|
||||
let editor_contents = editor_handle.as_ref(ctx).buffer_text(ctx);
|
||||
me.handle_action(
|
||||
&WorkingDirectoryAction::SetCustomWorkingDirectoryValue(source, editor_contents),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
editor
|
||||
}
|
||||
Reference in New Issue
Block a user