1189 lines
44 KiB
Rust
1189 lines
44 KiB
Rust
use std::borrow::Cow;
|
|
use std::fs::File;
|
|
use std::path::PathBuf;
|
|
|
|
use ai::workspace::WorkspaceMetadata;
|
|
use csv::Writer;
|
|
use enclose::enclose;
|
|
use galaxy_core::context_flag::ContextFlag;
|
|
use itertools::Itertools;
|
|
use settings::manager::SettingsManager;
|
|
use settings::Setting as _;
|
|
use warp_util::path::user_friendly_path;
|
|
use warpui::actions::StandardAction;
|
|
use warpui::keymap::{Keystroke, Trigger};
|
|
use warpui::platform::menu::{
|
|
CustomMenuItem, Menu, MenuBar, MenuItem, MenuItemProperties, MenuItemPropertyChanges,
|
|
};
|
|
use warpui::windowing::WindowManager;
|
|
use warpui::{AppContext, SingletonEntity};
|
|
|
|
use crate::ai::persisted_workspace::PersistedWorkspace;
|
|
use crate::auth::AuthStateProvider;
|
|
use crate::default_terminal::DefaultTerminal;
|
|
use crate::features::{runtime_flags_menu_items, FeatureFlag};
|
|
use crate::root_view::OpenLaunchConfigArg;
|
|
use crate::server::telemetry::LaunchConfigUiLocation;
|
|
use crate::settings::{
|
|
AISettings, BlockVisibilitySettings, DebugSettings, DefaultSessionMode, SelectionSettings,
|
|
};
|
|
use crate::terminal::alt_screen_reporting::AltScreenReporting;
|
|
use crate::terminal::session_settings::SessionSettings;
|
|
use crate::terminal::settings::{SpacingMode, TerminalSettings};
|
|
use crate::undo_close::UndoCloseStack;
|
|
use crate::user_config::GalaxyConfig;
|
|
use crate::util::bindings::{self, trigger_to_keystroke, CustomAction};
|
|
use crate::util::links;
|
|
use crate::workspace::sync_inputs::SyncedInputState;
|
|
use crate::{auth, report_if_error};
|
|
|
|
type CheckmarkStatusGetter = dyn 'static + Fn(&mut AppContext) -> bool;
|
|
|
|
const ENABLE_SHELL_DEBUG_MODE_MENU_ITEM_NAME: &str =
|
|
"Enable Shell Debug Mode (-x) for New Sessions";
|
|
const DISABLE_SHELL_DEBUG_MODE_MENU_ITEM_NAME: &str =
|
|
"Disable Shell Debug Mode (-x) for New Sessions";
|
|
const ENABLE_IN_BAND_GENERATORS_MENU_ITEM_NAME: &str = "Enable In-band Generators for New Sessions";
|
|
const DISABLE_IN_BAND_GENERATORS_MENU_ITEM_NAME: &str =
|
|
"Disable in-band generators for new sessions";
|
|
const ENABLE_PTY_RECORDING: &str = "Enable PTY Recording Mode (warp.pty.recording)";
|
|
const DISABLE_PTY_RECORDING: &str = "Disable PTY Recording Mode (warp.pty.recording)";
|
|
const SHOW_BOOTSTRAP_BLOCK_MENU_ITEM_NAME: &str = "Show Initialization Block";
|
|
const HIDE_BOOTSTRAP_BLOCK_MENU_ITEM_NAME: &str = "Hide Initialization Block";
|
|
const SHOW_IN_BAND_COMMAND_BLOCKS_MENU_ITEM_NAME: &str = "Show In-band Command Blocks";
|
|
const HIDE_IN_BAND_COMMAND_BLOCKS_MENU_ITEM_NAME: &str = "Hide In-band Command Blocks";
|
|
const SHOW_SSH_COMMAND_BLOCKS_MENU_ITEM_NAME: &str = "Show Enhanced SSH Blocks";
|
|
const HIDE_SSH_COMMAND_BLOCKS_MENU_ITEM_NAME: &str = "Hide Enhanced SSH Blocks";
|
|
const EXPORT_DEFAULT_SETTINGS_CSV_MENU_ITEM_NAME: &str =
|
|
"Export Default Settings as CSV to home dir";
|
|
|
|
const SETTINGS_CSV_FILE_NAME: &str = "warp_default_settings.csv";
|
|
const MAX_RECENT_REPOS_IN_MENU: usize = 10;
|
|
|
|
/// Creates the root app menu bar
|
|
pub fn menu_bar(ctx: &mut AppContext) -> MenuBar {
|
|
MenuBar::new(vec![
|
|
make_new_app_menu(ctx),
|
|
make_new_file_menu(ctx),
|
|
make_new_edit_menu(ctx),
|
|
make_new_view_menu(ctx),
|
|
make_new_tab_menu(ctx),
|
|
make_new_blocks_menu(ctx),
|
|
make_new_ai_menu(ctx),
|
|
make_new_drive_menu(ctx),
|
|
make_new_window_menu(),
|
|
make_new_help_menu(),
|
|
])
|
|
}
|
|
|
|
// Creates the app dock menu
|
|
// Menu here instead of MenuBar, since we only need one vec<MenuItem> in the dock
|
|
// To create submenus, we could use MenuItem::Custom(CustomMenuItem::new_with_submenu(...))
|
|
pub fn dock_menu() -> Menu {
|
|
Menu::new(
|
|
"New Window",
|
|
vec![MenuItem::Custom(CustomMenuItem::new(
|
|
"New Window",
|
|
move |ctx| {
|
|
ctx.dispatch_global_action("root_view:open_new", &());
|
|
ctx.dispatch_global_action("workspace:save_app", &());
|
|
},
|
|
no_updates,
|
|
Some(Keystroke::parse("cmd-n").expect("Valid keystroke")),
|
|
))],
|
|
)
|
|
}
|
|
|
|
fn custom_shortcut(action: CustomAction) -> Option<Keystroke> {
|
|
trigger_to_keystroke(&Trigger::Custom(action.into()))
|
|
}
|
|
|
|
fn default_name(action: CustomAction, ctx: &AppContext) -> String {
|
|
ctx.description_for_custom_action(action.into(), bindings::MAC_MENUS_CONTEXT)
|
|
.unwrap_or_else(|| {
|
|
debug_assert!(false, "action should have a name: {action:?}");
|
|
"<NO DESCRIPTION>".into()
|
|
})
|
|
}
|
|
|
|
fn non_updateable_custom_item(action: CustomAction, ctx: &AppContext) -> MenuItem {
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
&default_name(action, ctx),
|
|
custom_action_dispatcher(action),
|
|
no_updates,
|
|
custom_shortcut(action),
|
|
))
|
|
}
|
|
|
|
/// Return a Custom Menu Item whose CustomAction can be updated and
|
|
/// whose checkmark status is determined by
|
|
/// the bool result of should_be_checked, a provided closure
|
|
fn updateable_custom_item_with_checkmark(
|
|
action: CustomAction,
|
|
ctx: &AppContext,
|
|
should_be_checked: Box<CheckmarkStatusGetter>,
|
|
) -> MenuItem {
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
&default_name(action, ctx),
|
|
custom_action_dispatcher(action),
|
|
custom_action_updater(action, should_be_checked),
|
|
custom_shortcut(action),
|
|
))
|
|
}
|
|
|
|
/// Return a Custom Menu Item whose CustomAction can be updated
|
|
/// and is always unchecked
|
|
fn updateable_custom_item_without_checkmark(action: CustomAction, ctx: &AppContext) -> MenuItem {
|
|
updateable_custom_item_with_checkmark(action, ctx, Box::new(|_| false))
|
|
}
|
|
|
|
fn make_new_app_menu(ctx: &AppContext) -> Menu {
|
|
let mut menu_items = vec![updateable_custom_item_without_checkmark(
|
|
CustomAction::ShowAboutGalaxy,
|
|
ctx,
|
|
)];
|
|
|
|
if !FeatureFlag::AvatarInTabBar.is_enabled() {
|
|
menu_items.push(updateable_custom_item_without_checkmark(
|
|
CustomAction::ToggleResourceCenter,
|
|
ctx,
|
|
))
|
|
}
|
|
|
|
menu_items.extend([
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::ReferAFriend, ctx),
|
|
MenuItem::Separator,
|
|
]);
|
|
|
|
let preferences_menu_items = vec![
|
|
updateable_custom_item_without_checkmark(CustomAction::ShowSettings, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::ToggleKeybindingsPage, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::ConfigureKeybindings, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::ShowAppearance, ctx),
|
|
MenuItem::Separator,
|
|
];
|
|
|
|
menu_items.push(MenuItem::Custom(CustomMenuItem::new_with_submenu(
|
|
"Preferences",
|
|
|_| (),
|
|
no_updates,
|
|
None,
|
|
preferences_menu_items,
|
|
)));
|
|
|
|
if FeatureFlag::Changelog.is_enabled() {
|
|
menu_items.push(updateable_custom_item_without_checkmark(
|
|
CustomAction::ViewChangelog,
|
|
ctx,
|
|
));
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
menu_items.push(MenuItem::Services);
|
|
}
|
|
|
|
menu_items.push(MenuItem::Separator);
|
|
menu_items.push(link_menu_item(
|
|
"Privacy Policy...",
|
|
links::PRIVACY_POLICY_URL.into(),
|
|
));
|
|
|
|
let debug_menu_items = debug_menu_items();
|
|
if !debug_menu_items.is_empty() {
|
|
menu_items.push(MenuItem::Custom(CustomMenuItem::new_with_submenu(
|
|
"Debug",
|
|
|_| (),
|
|
no_updates,
|
|
None,
|
|
debug_menu_items,
|
|
)));
|
|
}
|
|
|
|
menu_items.push(MenuItem::Separator);
|
|
menu_items.push(MenuItem::Standard(StandardAction::Hide));
|
|
menu_items.push(MenuItem::Standard(StandardAction::HideOtherApps));
|
|
menu_items.push(MenuItem::Standard(StandardAction::ShowAllApps));
|
|
menu_items.push(MenuItem::Separator);
|
|
menu_items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
"Set Galaxy as Default Terminal",
|
|
move |ctx| {
|
|
DefaultTerminal::handle(ctx).update(ctx, |default_terminal, ctx| {
|
|
default_terminal.make_warp_default(ctx)
|
|
});
|
|
},
|
|
move |_props, ctx| {
|
|
let default_terminal = DefaultTerminal::handle(ctx).as_ref(ctx);
|
|
MenuItemPropertyChanges {
|
|
disabled: Some(
|
|
!DefaultTerminal::can_warp_become_default()
|
|
|| default_terminal.is_warp_default(),
|
|
),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
)));
|
|
menu_items.push(MenuItem::Separator);
|
|
menu_items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
"Log out",
|
|
auth::maybe_log_out,
|
|
move |_, ctx| {
|
|
let is_anonymous = AuthStateProvider::handle(ctx)
|
|
.as_ref(ctx)
|
|
.get()
|
|
.is_anonymous_or_logged_out();
|
|
MenuItemPropertyChanges {
|
|
disabled: Some(is_anonymous),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
)));
|
|
menu_items.push(MenuItem::Standard(StandardAction::Quit));
|
|
Menu::new("Galaxy", menu_items)
|
|
}
|
|
|
|
fn make_new_file_menu(ctx: &AppContext) -> Menu {
|
|
let mut file_menu_options = make_new_elements_menu_items(ctx);
|
|
file_menu_options.extend([
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::OpenRepository, ctx),
|
|
MenuItem::Custom(CustomMenuItem::new_with_submenu(
|
|
"Open Recent",
|
|
|_| (),
|
|
|_props, ctx| {
|
|
let recent_repos = generate_recent_repos_for_menu(ctx);
|
|
MenuItemPropertyChanges {
|
|
submenu: Some(Some(make_recent_repos_menu_items(ctx))),
|
|
disabled: Some(recent_repos.is_empty()),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
vec![],
|
|
)),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::CloseCurrentSession, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::CloseWindow, ctx),
|
|
]);
|
|
|
|
Menu::new("File", file_menu_options)
|
|
}
|
|
|
|
fn make_new_edit_menu(ctx: &AppContext) -> Menu {
|
|
let mut edit_menu_items = vec![];
|
|
|
|
let group_1 = vec![
|
|
updateable_custom_item_without_checkmark(CustomAction::Undo, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::Redo, ctx),
|
|
];
|
|
let group_2 = vec![
|
|
updateable_custom_item_without_checkmark(CustomAction::Cut, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::Copy, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::Paste, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::SelectAll, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::ClearEditor, ctx),
|
|
];
|
|
let group_3 = vec![
|
|
updateable_custom_item_without_checkmark(CustomAction::AddNextOccurrence, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::AddCursorAbove, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::AddCursorBelow, ctx),
|
|
];
|
|
let group_4 = vec![
|
|
updateable_custom_item_without_checkmark(CustomAction::Find, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::GoToLine, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::FocusInput, ctx),
|
|
];
|
|
let group_5 = vec![
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
"Use Galaxy's Prompt",
|
|
move |ctx| ctx.dispatch_global_action("app:toggle_user_ps1", &()),
|
|
move |_props, ctx| MenuItemPropertyChanges {
|
|
checked: Some(
|
|
SessionSettings::handle(ctx).read(ctx, |session_settings, _ctx| {
|
|
!session_settings.honor_ps1.value()
|
|
}),
|
|
),
|
|
..Default::default()
|
|
},
|
|
None,
|
|
)),
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
"Copy on Select within the Terminal",
|
|
move |ctx| {
|
|
ctx.dispatch_global_action("app:toggle_copy_on_select", &());
|
|
},
|
|
move |_props, ctx| MenuItemPropertyChanges {
|
|
checked: Some(
|
|
SelectionSettings::handle(ctx)
|
|
.as_ref(ctx)
|
|
.copy_on_select_enabled(),
|
|
),
|
|
..Default::default()
|
|
},
|
|
None,
|
|
)),
|
|
];
|
|
|
|
edit_menu_items.extend(group_1);
|
|
edit_menu_items.push(MenuItem::Separator);
|
|
edit_menu_items.extend(group_2);
|
|
edit_menu_items.push(MenuItem::Separator);
|
|
edit_menu_items.extend(group_3);
|
|
edit_menu_items.push(MenuItem::Separator);
|
|
edit_menu_items.extend(group_4);
|
|
edit_menu_items.push(MenuItem::Separator);
|
|
|
|
edit_menu_items.push(MenuItem::Custom(CustomMenuItem::new_with_submenu(
|
|
"Synchronize Inputs",
|
|
|_| (),
|
|
no_updates,
|
|
None,
|
|
vec![
|
|
updateable_custom_item_without_checkmark(
|
|
CustomAction::ToggleSyncAllTerminalInputsInAllTabs,
|
|
ctx,
|
|
),
|
|
updateable_custom_item_without_checkmark(
|
|
CustomAction::ToggleSyncTerminalInputsInCurrentTab,
|
|
ctx,
|
|
),
|
|
updateable_custom_item_with_checkmark(
|
|
CustomAction::DisableSyncTerminalInputs,
|
|
ctx,
|
|
Box::new(|ctx| {
|
|
if let Some(window_id) = WindowManager::handle(ctx).as_ref(ctx).active_window()
|
|
{
|
|
SyncedInputState::handle(ctx)
|
|
.read(ctx, |status, _| !status.is_syncing_any_inputs(window_id))
|
|
} else {
|
|
false
|
|
}
|
|
}),
|
|
),
|
|
],
|
|
)));
|
|
edit_menu_items.push(MenuItem::Separator);
|
|
|
|
edit_menu_items.extend(group_5);
|
|
|
|
Menu::new("Edit", edit_menu_items)
|
|
}
|
|
|
|
fn make_new_view_menu(ctx: &AppContext) -> Menu {
|
|
let mut items = vec![
|
|
updateable_custom_item_without_checkmark(CustomAction::ToggleGalaxyDrive, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::CommandPalette, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::NavigationPalette, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::LaunchConfigPalette, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::FilesPalette, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::ToggleProjectExplorer, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::ToggleConversationListView, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::ToggleGlobalSearch, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::History, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::CommandSearch, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::Workflows, ctx),
|
|
MenuItem::Separator,
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
"Toggle Mouse Reporting",
|
|
move |ctx| {
|
|
ctx.dispatch_global_action("workspace:toggle_mouse_reporting", &());
|
|
},
|
|
move |_props, ctx| {
|
|
let mouse_reporting_enabled = AltScreenReporting::handle(ctx)
|
|
.as_ref(ctx)
|
|
.mouse_reporting_enabled
|
|
.value();
|
|
MenuItemPropertyChanges {
|
|
checked: Some(*mouse_reporting_enabled),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
)),
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
"Toggle Scroll Reporting",
|
|
move |ctx| {
|
|
ctx.dispatch_global_action("workspace:toggle_scroll_reporting", &());
|
|
},
|
|
move |_props, ctx| {
|
|
let reporting = AltScreenReporting::handle(ctx).as_ref(ctx);
|
|
MenuItemPropertyChanges {
|
|
disabled: Some(!*reporting.mouse_reporting_enabled.value()),
|
|
checked: Some(*reporting.scroll_reporting_enabled.value()),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
)),
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
"Toggle Focus Reporting",
|
|
move |ctx| {
|
|
ctx.dispatch_global_action("workspace:toggle_focus_reporting", &());
|
|
},
|
|
move |_props, ctx| {
|
|
let reporting = AltScreenReporting::handle(ctx).as_ref(ctx);
|
|
MenuItemPropertyChanges {
|
|
checked: Some(*reporting.focus_reporting_enabled.value()),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
)),
|
|
];
|
|
|
|
let is_compact_mode = matches!(
|
|
TerminalSettings::handle(ctx)
|
|
.as_ref(ctx)
|
|
.spacing_mode
|
|
.value(),
|
|
SpacingMode::Compact
|
|
);
|
|
|
|
items.extend([
|
|
MenuItem::Separator,
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
"Compact Mode",
|
|
move |ctx| {
|
|
TerminalSettings::handle(ctx).update(ctx, |terminal_settings, ctx| {
|
|
let current_value = *terminal_settings.spacing_mode;
|
|
report_if_error!(terminal_settings
|
|
.spacing_mode
|
|
.set_value(current_value.other_mode(), ctx));
|
|
});
|
|
},
|
|
move |_props, _| MenuItemPropertyChanges {
|
|
checked: Some(is_compact_mode),
|
|
..Default::default()
|
|
},
|
|
None,
|
|
)),
|
|
MenuItem::Separator,
|
|
]);
|
|
|
|
if FeatureFlag::UIZoom.is_enabled() {
|
|
items.extend([
|
|
updateable_custom_item_without_checkmark(CustomAction::IncreaseZoom, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::DecreaseZoom, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::ResetZoom, ctx),
|
|
MenuItem::Separator,
|
|
]);
|
|
} else {
|
|
items.extend([
|
|
updateable_custom_item_without_checkmark(CustomAction::IncreaseFontSize, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::DecreaseFontSize, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::ResetFontSize, ctx),
|
|
MenuItem::Separator,
|
|
]);
|
|
}
|
|
|
|
Menu::new("View", items)
|
|
}
|
|
|
|
fn make_new_tab_menu(ctx: &AppContext) -> Menu {
|
|
let items = vec![
|
|
updateable_custom_item_without_checkmark(CustomAction::RenameTab, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::SplitPaneRight, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::SplitPaneLeft, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::SplitPaneDown, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::SplitPaneUp, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::MoveTabLeft, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::MoveTabRight, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::CycleNextSession, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::CyclePrevSession, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::ActivateNextPane, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::ActivatePreviousPane, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::ToggleMaximizePane, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::CloseTab, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::CloseOtherTabs, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::CloseTabsRight, ctx),
|
|
];
|
|
Menu::new("Tab", items)
|
|
}
|
|
|
|
fn make_new_ai_menu(ctx: &AppContext) -> Menu {
|
|
let mut items = vec![updateable_custom_item_without_checkmark(
|
|
CustomAction::NewAgentModePane,
|
|
ctx,
|
|
)];
|
|
|
|
items.push(updateable_custom_item_without_checkmark(
|
|
CustomAction::AttachSelectionAsAgentModeContext,
|
|
ctx,
|
|
));
|
|
|
|
items.extend([
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::AISearch, ctx),
|
|
]);
|
|
|
|
if FeatureFlag::AIRules.is_enabled() {
|
|
items.extend([
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::OpenAIFactCollection, ctx),
|
|
]);
|
|
}
|
|
|
|
if FeatureFlag::McpServer.is_enabled() && ContextFlag::ShowMCPServers.is_enabled() {
|
|
items.push(updateable_custom_item_without_checkmark(
|
|
CustomAction::OpenMCPServerCollection,
|
|
ctx,
|
|
));
|
|
}
|
|
|
|
Menu::new("AI", items)
|
|
}
|
|
|
|
fn make_new_blocks_menu(ctx: &AppContext) -> Menu {
|
|
let mut items = vec![
|
|
updateable_custom_item_without_checkmark(CustomAction::ClearBlocks, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::SelectBlockAbove, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::SelectBlockBelow, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::SelectAllBlocks, ctx),
|
|
MenuItem::Separator,
|
|
];
|
|
items.push(updateable_custom_item_without_checkmark(
|
|
CustomAction::ScrollToTopOfSelectedBlocks,
|
|
ctx,
|
|
));
|
|
items.push(updateable_custom_item_without_checkmark(
|
|
CustomAction::ScrollToBottomOfSelectedBlocks,
|
|
ctx,
|
|
));
|
|
items.push(MenuItem::Separator);
|
|
items.extend([
|
|
updateable_custom_item_without_checkmark(CustomAction::CreateBlockPermalink, ctx),
|
|
non_updateable_custom_item(CustomAction::ViewSharedBlocks, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::ToggleBookmarkBlock, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::FindWithinBlock, ctx),
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::CopyBlock, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::CopyBlockCommand, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::CopyBlockOutput, ctx),
|
|
]);
|
|
|
|
let debug_items = block_menu_debug_items();
|
|
if !debug_items.is_empty() {
|
|
items.push(MenuItem::Separator);
|
|
items.extend(debug_items);
|
|
}
|
|
|
|
Menu::new("Blocks", items)
|
|
}
|
|
|
|
fn make_new_drive_menu(ctx: &AppContext) -> Menu {
|
|
let mut items = vec![
|
|
updateable_custom_item_without_checkmark(CustomAction::NewPersonalWorkflow, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::NewPersonalNotebook, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::NewPersonalAIPrompt, ctx),
|
|
];
|
|
items.push(updateable_custom_item_without_checkmark(
|
|
CustomAction::NewPersonalEnvVars,
|
|
ctx,
|
|
));
|
|
items.extend([
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::NewTeamWorkflow, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::NewTeamNotebook, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::NewTeamAIPrompt, ctx),
|
|
]);
|
|
items.push(updateable_custom_item_without_checkmark(
|
|
CustomAction::NewTeamEnvVars,
|
|
ctx,
|
|
));
|
|
items.extend([
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::ToggleGalaxyDrive, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::SearchDrive, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::OpenTeamSettings, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::OpenAIFactCollection, ctx),
|
|
updateable_custom_item_without_checkmark(CustomAction::OpenMCPServerCollection, ctx),
|
|
]);
|
|
|
|
items.push(updateable_custom_item_without_checkmark(
|
|
CustomAction::SharePaneContents,
|
|
ctx,
|
|
));
|
|
|
|
if FeatureFlag::CreatingSharedSessions.is_enabled() {
|
|
items.extend([
|
|
MenuItem::Separator,
|
|
updateable_custom_item_without_checkmark(CustomAction::ShareCurrentSession, ctx),
|
|
])
|
|
}
|
|
|
|
Menu::new("Drive", items)
|
|
}
|
|
|
|
/// Returns [`MenuItem`]s that aid debugging to be included in the Block menu.
|
|
fn block_menu_debug_items() -> Vec<MenuItem> {
|
|
let mut items = vec![];
|
|
if FeatureFlag::ToggleBootstrapBlock.is_enabled() {
|
|
items.push(toggle_bootstrap_block_menu_item());
|
|
}
|
|
|
|
items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
SHOW_IN_BAND_COMMAND_BLOCKS_MENU_ITEM_NAME,
|
|
move |ctx| {
|
|
let handle = BlockVisibilitySettings::handle(ctx);
|
|
handle.update(ctx, |block_visibility_settings, ctx| {
|
|
let new_value = !block_visibility_settings
|
|
.should_show_in_band_command_blocks
|
|
.value();
|
|
if let Err(e) = block_visibility_settings
|
|
.should_show_in_band_command_blocks
|
|
.set_value(new_value, ctx)
|
|
{
|
|
log::error!("Failed to persist 'Show in-band command blocks' setting: {e}");
|
|
}
|
|
});
|
|
},
|
|
move |_props, ctx| {
|
|
let name = if BlockVisibilitySettings::handle(ctx).read(ctx, |settings, _ctx| {
|
|
*settings.should_show_in_band_command_blocks.value()
|
|
}) {
|
|
HIDE_IN_BAND_COMMAND_BLOCKS_MENU_ITEM_NAME.to_owned()
|
|
} else {
|
|
SHOW_IN_BAND_COMMAND_BLOCKS_MENU_ITEM_NAME.to_owned()
|
|
};
|
|
|
|
MenuItemPropertyChanges {
|
|
name: Some(name),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
)));
|
|
|
|
items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
SHOW_SSH_COMMAND_BLOCKS_MENU_ITEM_NAME,
|
|
move |ctx| {
|
|
let handle = BlockVisibilitySettings::handle(ctx);
|
|
handle.update(ctx, |block_visibility_settings, ctx| {
|
|
let new_value = !block_visibility_settings.should_show_ssh_block.value();
|
|
if let Err(e) = block_visibility_settings
|
|
.should_show_ssh_block
|
|
.set_value(new_value, ctx)
|
|
{
|
|
log::error!("Failed to persist 'Show ssh command blocks' setting: {e}");
|
|
}
|
|
});
|
|
},
|
|
move |_props, ctx| {
|
|
let name = if BlockVisibilitySettings::handle(ctx).read(ctx, |settings, _ctx| {
|
|
*settings.should_show_ssh_block.value()
|
|
}) {
|
|
HIDE_SSH_COMMAND_BLOCKS_MENU_ITEM_NAME.to_owned()
|
|
} else {
|
|
SHOW_SSH_COMMAND_BLOCKS_MENU_ITEM_NAME.to_owned()
|
|
};
|
|
|
|
MenuItemPropertyChanges {
|
|
name: Some(name),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
)));
|
|
|
|
items
|
|
}
|
|
|
|
fn toggle_bootstrap_block_menu_item() -> MenuItem {
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
SHOW_BOOTSTRAP_BLOCK_MENU_ITEM_NAME,
|
|
move |ctx| {
|
|
BlockVisibilitySettings::handle(ctx).update(ctx, |block_visibility_settings, ctx| {
|
|
let new_value = !block_visibility_settings
|
|
.should_show_bootstrap_block
|
|
.value();
|
|
if let Err(e) = block_visibility_settings
|
|
.should_show_bootstrap_block
|
|
.set_value(new_value, ctx)
|
|
{
|
|
log::error!("Failed to persist 'Show bootstrap block' setting: {e}");
|
|
}
|
|
});
|
|
},
|
|
move |_props, ctx| {
|
|
let name = if BlockVisibilitySettings::handle(ctx).read(ctx, |settings, _ctx| {
|
|
*settings.should_show_bootstrap_block.value()
|
|
}) {
|
|
HIDE_BOOTSTRAP_BLOCK_MENU_ITEM_NAME.to_owned()
|
|
} else {
|
|
SHOW_BOOTSTRAP_BLOCK_MENU_ITEM_NAME.to_owned()
|
|
};
|
|
|
|
MenuItemPropertyChanges {
|
|
name: Some(name),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
))
|
|
}
|
|
|
|
fn make_new_window_menu() -> Menu {
|
|
Menu::new(
|
|
"Window",
|
|
vec![
|
|
MenuItem::Standard(StandardAction::Minimize),
|
|
MenuItem::Standard(StandardAction::Zoom),
|
|
MenuItem::Standard(StandardAction::ToggleFullScreen),
|
|
MenuItem::Separator,
|
|
MenuItem::Standard(StandardAction::BringAllToFront),
|
|
],
|
|
)
|
|
}
|
|
|
|
fn debug_menu_items() -> Vec<MenuItem> {
|
|
let mut debug_menu_items = vec![];
|
|
|
|
if FeatureFlag::DebugMode.is_enabled() {
|
|
debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
ENABLE_SHELL_DEBUG_MODE_MENU_ITEM_NAME,
|
|
move |ctx| {
|
|
DebugSettings::handle(ctx).update(ctx, |debug_settings, ctx| {
|
|
let new_value = !debug_settings.is_shell_debug_mode_enabled.value();
|
|
if let Err(e) = debug_settings
|
|
.is_shell_debug_mode_enabled
|
|
.set_value(new_value, ctx)
|
|
{
|
|
log::error!("Failed to persist 'Debug mode' setting: {e}");
|
|
}
|
|
});
|
|
},
|
|
move |_props, ctx| {
|
|
let name = if DebugSettings::handle(ctx).read(ctx, |settings, _ctx| {
|
|
*settings.is_shell_debug_mode_enabled.value()
|
|
}) {
|
|
DISABLE_SHELL_DEBUG_MODE_MENU_ITEM_NAME.to_owned()
|
|
} else {
|
|
ENABLE_SHELL_DEBUG_MODE_MENU_ITEM_NAME.to_owned()
|
|
};
|
|
|
|
MenuItemPropertyChanges {
|
|
name: Some(name),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
)));
|
|
|
|
debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
ENABLE_PTY_RECORDING,
|
|
move |ctx| {
|
|
DebugSettings::handle(ctx).update(ctx, |debug_settings, ctx| {
|
|
let new_value = !debug_settings.recording_mode.value();
|
|
let _ = debug_settings.recording_mode.set_value(new_value, ctx);
|
|
});
|
|
},
|
|
move |_props, ctx| {
|
|
let name = if DebugSettings::handle(ctx)
|
|
.read(ctx, |settings, _ctx| *settings.recording_mode.value())
|
|
{
|
|
DISABLE_PTY_RECORDING.to_owned()
|
|
} else {
|
|
ENABLE_PTY_RECORDING.to_owned()
|
|
};
|
|
|
|
MenuItemPropertyChanges {
|
|
name: Some(name),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
)));
|
|
|
|
debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
ENABLE_IN_BAND_GENERATORS_MENU_ITEM_NAME,
|
|
move |ctx| {
|
|
DebugSettings::handle(ctx).update(ctx, |debug_settings, ctx| {
|
|
let new_value = !debug_settings
|
|
.are_in_band_generators_for_all_sessions_enabled
|
|
.value();
|
|
if let Err(e) = debug_settings
|
|
.are_in_band_generators_for_all_sessions_enabled
|
|
.set_value(new_value, ctx)
|
|
{
|
|
log::error!("Failed to persist 'Enable in-band generators' setting: {e}");
|
|
}
|
|
});
|
|
},
|
|
move |_props, ctx| {
|
|
let name = if DebugSettings::handle(ctx).read(ctx, |settings, _ctx| {
|
|
*settings
|
|
.are_in_band_generators_for_all_sessions_enabled
|
|
.value()
|
|
}) {
|
|
DISABLE_IN_BAND_GENERATORS_MENU_ITEM_NAME.to_owned()
|
|
} else {
|
|
ENABLE_IN_BAND_GENERATORS_MENU_ITEM_NAME.to_owned()
|
|
};
|
|
|
|
MenuItemPropertyChanges {
|
|
name: Some(name),
|
|
..Default::default()
|
|
}
|
|
},
|
|
None,
|
|
)));
|
|
|
|
if !FeatureFlag::ToggleBootstrapBlock.is_enabled() {
|
|
debug_menu_items.push(toggle_bootstrap_block_menu_item());
|
|
}
|
|
|
|
debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
"Manually Toggle Network Status",
|
|
move |ctx| ctx.dispatch_global_action("workspace:toggle_debug_network_status", &()),
|
|
no_updates,
|
|
None,
|
|
)));
|
|
|
|
debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
EXPORT_DEFAULT_SETTINGS_CSV_MENU_ITEM_NAME,
|
|
move |ctx| {
|
|
let default_settings = SettingsManager::handle(ctx).as_ref(ctx).default_values();
|
|
let mut writer = Writer::from_writer(
|
|
File::create(
|
|
dirs::home_dir()
|
|
.unwrap_or_default()
|
|
.join(SETTINGS_CSV_FILE_NAME),
|
|
)
|
|
.expect("Failed to create settings csv file"),
|
|
);
|
|
writer
|
|
.write_record(["storage_key", "value"])
|
|
.expect("Failed to write header record");
|
|
for (storage_key, value) in default_settings {
|
|
log::debug!("Writing setting: {storage_key} = {value}");
|
|
writer
|
|
.write_record(&[storage_key, value])
|
|
.expect("Failed to write settings record");
|
|
}
|
|
let _ = writer.flush();
|
|
},
|
|
no_updates,
|
|
None,
|
|
)));
|
|
|
|
debug_menu_items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
"Create anonymous user",
|
|
move |ctx| ctx.dispatch_global_action("workspace:debug_create_anonymous_user", &()),
|
|
no_updates,
|
|
None,
|
|
)));
|
|
}
|
|
|
|
if FeatureFlag::RuntimeFeatureFlags.is_enabled() {
|
|
debug_menu_items.extend(runtime_flags_menu_items());
|
|
}
|
|
|
|
debug_menu_items
|
|
}
|
|
|
|
fn link_menu_item(title: &'static str, link: Cow<'static, str>) -> MenuItem {
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
title,
|
|
move |ctx| {
|
|
ctx.open_url(&link);
|
|
},
|
|
no_updates,
|
|
None,
|
|
))
|
|
}
|
|
|
|
fn feedback_menu_item() -> MenuItem {
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
"Send Feedback...",
|
|
move |ctx| {
|
|
// Route through the root-view action so workspace windows can open the
|
|
// guided AI flow, while non-workspace windows still fall back to the
|
|
// browser-based feedback form.
|
|
ctx.dispatch_global_action("root_view:send_feedback", &());
|
|
},
|
|
no_updates,
|
|
None,
|
|
))
|
|
}
|
|
|
|
fn make_new_help_menu() -> Menu {
|
|
Menu::new(
|
|
"Help",
|
|
vec![
|
|
feedback_menu_item(),
|
|
link_menu_item("Galaxy Documentation...", links::USER_DOCS_URL.into()),
|
|
link_menu_item("GitHub Issues...", links::GITHUB_ISSUES_URL.into()),
|
|
link_menu_item("Galaxy Slack Community...", links::SLACK_URL.into()),
|
|
],
|
|
)
|
|
}
|
|
|
|
fn make_launch_config_menu_items(ctx: &mut AppContext) -> Vec<MenuItem> {
|
|
let mut launch_config_menu_items = vec![];
|
|
|
|
let launch_configs = GalaxyConfig::handle(ctx).as_ref(ctx).launch_configs();
|
|
for config in launch_configs {
|
|
launch_config_menu_items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
&config.name,
|
|
enclose!((config) move |ctx| {
|
|
ctx.dispatch_global_action(
|
|
"root_view:open_launch_config",
|
|
&OpenLaunchConfigArg {
|
|
launch_config: config.clone(),
|
|
ui_location: LaunchConfigUiLocation::AppMenu,
|
|
open_in_active_window: false,
|
|
}
|
|
);
|
|
ctx.dispatch_global_action("workspace:save_app", &());
|
|
}),
|
|
no_updates,
|
|
None,
|
|
)));
|
|
}
|
|
|
|
if !launch_config_menu_items.is_empty() {
|
|
launch_config_menu_items.push(MenuItem::Separator);
|
|
}
|
|
|
|
// TODO(vorporeal): use non_updateable_custom_item() here instead
|
|
launch_config_menu_items.push(MenuItem::Custom(CustomMenuItem::new(
|
|
"Save New...",
|
|
custom_action_dispatcher(CustomAction::SaveCurrentConfig),
|
|
no_updates,
|
|
custom_shortcut(CustomAction::SaveCurrentConfig),
|
|
)));
|
|
|
|
launch_config_menu_items
|
|
}
|
|
|
|
fn make_new_elements_menu_items(ctx: &AppContext) -> Vec<MenuItem> {
|
|
// Dynamically assign the workspace:new_tab keystroke (cmd-t) to whichever item
|
|
// matches the user's "Default mode for new sessions" setting. The non-default item
|
|
// shows its dedicated keystroke instead.
|
|
let mut new_elements_menu = vec![
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
"New Window",
|
|
open_new_window,
|
|
no_updates,
|
|
Some(Keystroke::parse("cmd-n").expect("Valid keystroke")),
|
|
)),
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
"New Terminal Tab",
|
|
open_new_default_tab_or_window,
|
|
move |_props: &MenuItemProperties, ctx: &mut AppContext| {
|
|
let mut changes = MenuItemPropertyChanges::default();
|
|
let is_default_session_mode_agent =
|
|
AISettings::handle(ctx).read(ctx, |ai_settings, ctx| {
|
|
ai_settings.is_any_ai_enabled(ctx)
|
|
&& ai_settings.default_session_mode(ctx) == DefaultSessionMode::Agent
|
|
});
|
|
let trigger = if is_default_session_mode_agent {
|
|
Trigger::Custom(CustomAction::NewTerminalTab.into())
|
|
} else {
|
|
Trigger::Custom(CustomAction::NewTab.into())
|
|
};
|
|
let binding = ctx
|
|
.get_key_bindings()
|
|
.find(|b| b.trigger == &trigger || b.original_trigger == Some(&trigger));
|
|
if let Some(binding) = binding {
|
|
changes.keystroke = Some(bindings::trigger_to_keystroke(binding.trigger));
|
|
}
|
|
changes
|
|
},
|
|
Some(Keystroke::parse("cmd-t").expect("Valid keystroke")),
|
|
)),
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
"New Agent Tab",
|
|
open_new_agent_tab_or_window,
|
|
move |_props: &MenuItemProperties, ctx: &mut AppContext| {
|
|
let mut changes = MenuItemPropertyChanges::default();
|
|
let (is_any_ai_enabled, is_default_session_mode_agent) = AISettings::handle(ctx)
|
|
.read(ctx, |ai_settings, ctx| {
|
|
let enabled = ai_settings.is_any_ai_enabled(ctx);
|
|
let agent = enabled
|
|
&& ai_settings.default_session_mode(ctx) == DefaultSessionMode::Agent;
|
|
(enabled, agent)
|
|
});
|
|
if !is_any_ai_enabled {
|
|
changes.disabled = Some(true);
|
|
return changes;
|
|
}
|
|
let trigger = if is_default_session_mode_agent {
|
|
Trigger::Custom(CustomAction::NewTab.into())
|
|
} else {
|
|
Trigger::Custom(CustomAction::NewAgentTab.into())
|
|
};
|
|
let binding = ctx
|
|
.get_key_bindings()
|
|
.find(|b| b.trigger == &trigger || b.original_trigger == Some(&trigger));
|
|
if let Some(binding) = binding {
|
|
changes.keystroke = Some(bindings::trigger_to_keystroke(binding.trigger));
|
|
}
|
|
changes
|
|
},
|
|
None,
|
|
)),
|
|
non_updateable_custom_item(CustomAction::NewFile, ctx),
|
|
];
|
|
|
|
let reopen_session_action_updater =
|
|
custom_action_updater(CustomAction::ReopenClosedSession, Box::new(|_| false));
|
|
new_elements_menu.push(MenuItem::Custom(CustomMenuItem::new(
|
|
"Reopen closed session",
|
|
|ctx| {
|
|
UndoCloseStack::handle(ctx).update(ctx, |stack, ctx| {
|
|
stack.undo_close(ctx);
|
|
});
|
|
},
|
|
move |props, ctx| {
|
|
let mut changes = reopen_session_action_updater(props, ctx);
|
|
changes.disabled = Some(UndoCloseStack::handle(ctx).as_ref(ctx).is_empty());
|
|
changes
|
|
},
|
|
Some(Keystroke::parse("cmd-shift-T").expect("Valid keystroke")),
|
|
)));
|
|
|
|
new_elements_menu.push(MenuItem::Custom(CustomMenuItem::new_with_submenu(
|
|
"Launch Configurations",
|
|
|_| (),
|
|
|_props, ctx| MenuItemPropertyChanges {
|
|
submenu: Some(Some(make_launch_config_menu_items(ctx))),
|
|
..Default::default()
|
|
},
|
|
None,
|
|
vec![],
|
|
)));
|
|
|
|
new_elements_menu
|
|
}
|
|
|
|
/// \return a callback that dispatches a CustomAction, appropriate for plugging into a CustomMenuItem.
|
|
fn custom_action_dispatcher(action: CustomAction) -> impl Fn(&mut AppContext) + 'static {
|
|
move |ctx| {
|
|
if let Some(wid) = WindowManager::handle(ctx).as_ref(ctx).active_window() {
|
|
ctx.dispatch_custom_action(action, wid)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Dispatch events to open the user's default tab type in the active window
|
|
/// or make a new window if there is no active window.
|
|
fn open_new_default_tab_or_window(ctx: &mut AppContext) {
|
|
if let Some(wid) = WindowManager::handle(ctx).as_ref(ctx).active_window() {
|
|
ctx.dispatch_custom_action(CustomAction::NewTab, wid)
|
|
} else {
|
|
open_new_window(ctx)
|
|
}
|
|
}
|
|
|
|
/// Dispatch events to open an agent tab in the active window
|
|
/// or make a new window if there is no active window.
|
|
fn open_new_agent_tab_or_window(ctx: &mut AppContext) {
|
|
if let Some(wid) = WindowManager::handle(ctx).as_ref(ctx).active_window() {
|
|
ctx.dispatch_custom_action(CustomAction::NewAgentTab, wid)
|
|
} else {
|
|
open_new_window(ctx)
|
|
}
|
|
}
|
|
|
|
/// Dispatch event to open a new Warp window
|
|
fn open_new_window(ctx: &mut AppContext) {
|
|
ctx.dispatch_global_action("root_view:open_new", &());
|
|
ctx.dispatch_global_action("workspace:save_app", &());
|
|
}
|
|
|
|
/// No-op updater function for custom menu items that never change.
|
|
fn no_updates(_: &MenuItemProperties, _: &mut AppContext) -> MenuItemPropertyChanges {
|
|
Default::default()
|
|
}
|
|
|
|
fn make_recent_repos_menu_items(ctx: &AppContext) -> Vec<MenuItem> {
|
|
let recent_repos = generate_recent_repos_for_menu(ctx);
|
|
|
|
if recent_repos.is_empty() {
|
|
return vec![];
|
|
}
|
|
|
|
let home = dirs::home_dir().map(|p| p.display().to_string());
|
|
|
|
recent_repos
|
|
.into_iter()
|
|
.map(|path| {
|
|
let full_path = path.display().to_string();
|
|
let display_path = user_friendly_path(&full_path, home.as_deref()).into_owned();
|
|
|
|
MenuItem::Custom(CustomMenuItem::new(
|
|
&display_path,
|
|
move |ctx| {
|
|
ctx.dispatch_global_action("workspace:open_repository", &full_path);
|
|
},
|
|
no_updates,
|
|
None,
|
|
))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn generate_recent_repos_for_menu(ctx: &AppContext) -> Vec<PathBuf> {
|
|
PersistedWorkspace::handle(ctx)
|
|
.as_ref(ctx)
|
|
.workspaces()
|
|
.sorted_by(WorkspaceMetadata::most_recently_navigated)
|
|
.take(MAX_RECENT_REPOS_IN_MENU)
|
|
.map(|cbm| cbm.path)
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
/// \return a callback that updates a custom action based menu item based on the
|
|
/// current keybinding context:
|
|
/// 1) Whether the item should be enabled / disabled
|
|
/// 2) The current keystroke bound to the item (either the default or a custom one)
|
|
/// 3) The name of the item
|
|
///
|
|
/// This updated is called by the mac menu system before the menu is opened.
|
|
///
|
|
/// Note, that correctly disabling the key binding is important not just for limiting
|
|
/// what a user can do in a given context, but also because if the keybinding is not
|
|
/// disabled correctly, the mac menu system will capture key events when they should
|
|
/// be flowing into the underlying views (e.g. ctrl-c needs to flow directly to the
|
|
/// alt grid in emacs, whereas if our editor is active it's ok to dispatch a custom action)
|
|
fn custom_action_updater(
|
|
action: CustomAction,
|
|
checkmark_status: Box<CheckmarkStatusGetter>,
|
|
) -> impl Fn(&MenuItemProperties, &mut AppContext) -> MenuItemPropertyChanges + 'static {
|
|
move |_props, ctx| {
|
|
let mut changes = MenuItemPropertyChanges::default();
|
|
ctx.update_custom_action_binding(action.into(), |binding| {
|
|
changes.disabled = Some(binding.is_none());
|
|
if let Some(binding) = binding {
|
|
if let Some(description) = binding.description {
|
|
changes.name = Some(
|
|
description
|
|
.resolve(ctx, bindings::MAC_MENUS_CONTEXT)
|
|
.into_owned(),
|
|
);
|
|
}
|
|
changes.keystroke = Some(bindings::trigger_to_keystroke(binding.trigger));
|
|
}
|
|
});
|
|
|
|
changes.checked = Some(checkmark_status(ctx));
|
|
|
|
changes
|
|
}
|
|
}
|