Add shortcut settings page
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
mod services;
|
mod services;
|
||||||
mod shell;
|
mod shell;
|
||||||
|
mod shortcuts;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Application, Bounds, Focusable, KeyBinding, Menu, MenuItem, SystemMenuType,
|
App, AppContext, Application, Bounds, Focusable, Menu, MenuItem, SystemMenuType, WindowBounds,
|
||||||
WindowBounds, WindowOptions, actions, px, size,
|
WindowOptions, actions, px, size,
|
||||||
};
|
};
|
||||||
use gpui_component_assets::Assets;
|
use gpui_component_assets::Assets;
|
||||||
use shell::ElyShell;
|
use shell::ElyShell;
|
||||||
|
use shortcuts::bind_shortcuts;
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
ely_app,
|
ely_app,
|
||||||
@@ -33,35 +35,7 @@ fn main() {
|
|||||||
Application::new().with_assets(Assets).run(|cx: &mut App| {
|
Application::new().with_assets(Assets).run(|cx: &mut App| {
|
||||||
gpui_component::init(cx);
|
gpui_component::init(cx);
|
||||||
cx.on_action(quit);
|
cx.on_action(quit);
|
||||||
cx.bind_keys([
|
bind_shortcuts(cx);
|
||||||
KeyBinding::new("cmd-t", OpenNewTab, None),
|
|
||||||
KeyBinding::new("ctrl-t", OpenNewTab, None),
|
|
||||||
KeyBinding::new("cmd-\\", SplitRight, None),
|
|
||||||
KeyBinding::new("ctrl-\\", SplitRight, None),
|
|
||||||
KeyBinding::new("cmd-shift-j", OpenDownloads, None),
|
|
||||||
KeyBinding::new("ctrl-shift-j", OpenDownloads, None),
|
|
||||||
KeyBinding::new("cmd-y", OpenHistory, None),
|
|
||||||
KeyBinding::new("ctrl-h", OpenHistory, None),
|
|
||||||
KeyBinding::new("cmd-escape", OpenTaskManager, None),
|
|
||||||
KeyBinding::new("shift-escape", OpenTaskManager, None),
|
|
||||||
KeyBinding::new("cmd-,", OpenSettings, None),
|
|
||||||
KeyBinding::new("ctrl-,", OpenSettings, None),
|
|
||||||
KeyBinding::new("cmd-l", FocusAddressBar, None),
|
|
||||||
KeyBinding::new("ctrl-l", FocusAddressBar, None),
|
|
||||||
KeyBinding::new("cmd-w", CloseCurrentTab, None),
|
|
||||||
KeyBinding::new("ctrl-w", CloseCurrentTab, None),
|
|
||||||
KeyBinding::new("cmd-shift-t", RestoreClosedTab, None),
|
|
||||||
KeyBinding::new("ctrl-shift-t", RestoreClosedTab, None),
|
|
||||||
KeyBinding::new("cmd-shift-f", ToggleFavoriteTab, None),
|
|
||||||
KeyBinding::new("ctrl-shift-f", ToggleFavoriteTab, None),
|
|
||||||
KeyBinding::new("cmd-shift-p", FocusCommandMode, None),
|
|
||||||
KeyBinding::new("ctrl-shift-p", FocusCommandMode, None),
|
|
||||||
KeyBinding::new("cmd-shift-]", SelectNextTab, None),
|
|
||||||
KeyBinding::new("ctrl-tab", SelectNextTab, None),
|
|
||||||
KeyBinding::new("cmd-shift-[", SelectPreviousTab, None),
|
|
||||||
KeyBinding::new("ctrl-shift-tab", SelectPreviousTab, None),
|
|
||||||
KeyBinding::new("cmd-q", Quit, None),
|
|
||||||
]);
|
|
||||||
cx.set_menus(vec![
|
cx.set_menus(vec![
|
||||||
Menu {
|
Menu {
|
||||||
name: "ELY Browser".into(),
|
name: "ELY Browser".into(),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mod plugins;
|
|||||||
mod profiles;
|
mod profiles;
|
||||||
mod reading_list;
|
mod reading_list;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
mod shortcuts;
|
||||||
mod sidebar_tabs;
|
mod sidebar_tabs;
|
||||||
mod site_settings;
|
mod site_settings;
|
||||||
mod sync;
|
mod sync;
|
||||||
@@ -49,6 +50,7 @@ impl ElyShell {
|
|||||||
"ely://about" => self.render_about_page(snapshot),
|
"ely://about" => self.render_about_page(snapshot),
|
||||||
"ely://settings" => self.render_settings_page(snapshot, cx),
|
"ely://settings" => self.render_settings_page(snapshot, cx),
|
||||||
"ely://settings/sidebar-tabs" => self.render_sidebar_tabs_page(snapshot, cx),
|
"ely://settings/sidebar-tabs" => self.render_sidebar_tabs_page(snapshot, cx),
|
||||||
|
"ely://settings/shortcuts" => self.render_shortcuts_page(snapshot),
|
||||||
"ely://settings/plugins" => self.render_plugins_page(snapshot, cx),
|
"ely://settings/plugins" => self.render_plugins_page(snapshot, cx),
|
||||||
"ely://settings/profiles" => self.render_profiles_page(snapshot, cx),
|
"ely://settings/profiles" => self.render_profiles_page(snapshot, cx),
|
||||||
"ely://settings/sync" => self.render_sync_page(snapshot),
|
"ely://settings/sync" => self.render_sync_page(snapshot),
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ const SETTINGS_ROUTES: &[SettingsRoute] = &[
|
|||||||
detail: "Profile identity, color, and download policy.",
|
detail: "Profile identity, color, and download policy.",
|
||||||
route: "ely://settings/profiles",
|
route: "ely://settings/profiles",
|
||||||
},
|
},
|
||||||
|
SettingsRoute {
|
||||||
|
icon: IconName::SquareTerminal,
|
||||||
|
title: "Shortcuts",
|
||||||
|
detail: "Registered key bindings and conflict state.",
|
||||||
|
route: "ely://settings/shortcuts",
|
||||||
|
},
|
||||||
SettingsRoute {
|
SettingsRoute {
|
||||||
icon: IconName::Globe,
|
icon: IconName::Globe,
|
||||||
title: "Sync",
|
title: "Sync",
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
use ely_browser_core::BrowserSnapshot;
|
||||||
|
use ely_design_system::colors;
|
||||||
|
use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb};
|
||||||
|
use gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
|
||||||
|
|
||||||
|
use crate::shortcuts::{
|
||||||
|
SHORTCUT_ACTIONS, ShortcutAction, ShortcutConflict, ShortcutPlatform, bindings_for_action,
|
||||||
|
shortcut_conflicts,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{ElyShell, render_canvas_surface};
|
||||||
|
|
||||||
|
const SHORTCUT_CATEGORIES: &[&str] = &["Command", "Tabs", "Library", "System", "Application"];
|
||||||
|
|
||||||
|
impl ElyShell {
|
||||||
|
pub(super) fn render_shortcuts_page(&mut self, snapshot: &BrowserSnapshot) -> AnyElement {
|
||||||
|
let conflicts = shortcut_conflicts();
|
||||||
|
|
||||||
|
render_canvas_surface(
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.p_8()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_5()
|
||||||
|
.child(render_shortcuts_header(snapshot, conflicts.len()))
|
||||||
|
.child(render_conflict_panel(&conflicts))
|
||||||
|
.child(render_shortcut_categories(&conflicts)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_shortcuts_header(snapshot: &BrowserSnapshot, conflict_count: usize) -> AnyElement {
|
||||||
|
let status = if conflict_count == 0 {
|
||||||
|
"Ready".to_string()
|
||||||
|
} else {
|
||||||
|
format!("{conflict_count} conflicts")
|
||||||
|
};
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_end()
|
||||||
|
.justify_between()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Shortcuts"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.truncate()
|
||||||
|
.text_color(rgb(colors::MUTED))
|
||||||
|
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(shortcut_status_color(conflict_count)))
|
||||||
|
.child(shortcut_status_icon(conflict_count))
|
||||||
|
.child(status),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_conflict_panel(conflicts: &[ShortcutConflict]) -> AnyElement {
|
||||||
|
let (icon, title, detail, color) = if conflicts.is_empty() {
|
||||||
|
(
|
||||||
|
IconName::CircleCheck,
|
||||||
|
"No shortcut conflicts",
|
||||||
|
"Every registered browser shortcut maps to a single action.",
|
||||||
|
colors::SUCCESS,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
IconName::TriangleAlert,
|
||||||
|
"Shortcut conflicts",
|
||||||
|
"Conflicting bindings need a new key before customization is enabled.",
|
||||||
|
colors::ERROR,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
div()
|
||||||
|
.rounded_md()
|
||||||
|
.border_1()
|
||||||
|
.border_color(rgb(colors::HAIRLINE))
|
||||||
|
.bg(rgb(colors::CANVAS_SOFT))
|
||||||
|
.px_4()
|
||||||
|
.py_3()
|
||||||
|
.flex()
|
||||||
|
.items_start()
|
||||||
|
.justify_between()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.items_start()
|
||||||
|
.gap_3()
|
||||||
|
.child(div().text_color(rgb(color)).child(icon))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child(title),
|
||||||
|
)
|
||||||
|
.child(div().text_xs().text_color(rgb(colors::MUTED)).child(detail)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(color))
|
||||||
|
.child(conflicts.len().to_string()),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_shortcut_categories(conflicts: &[ShortcutConflict]) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_h_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.overflow_y_scrollbar()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(rgb(colors::HAIRLINE))
|
||||||
|
.children(
|
||||||
|
SHORTCUT_CATEGORIES
|
||||||
|
.iter()
|
||||||
|
.map(|category| render_shortcut_category(category, conflicts)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_shortcut_category(category: &'static str, conflicts: &[ShortcutConflict]) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.child(render_category_header(category))
|
||||||
|
.children(
|
||||||
|
SHORTCUT_ACTIONS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(move |action| action.category() == category)
|
||||||
|
.map(|action| render_shortcut_row(action, conflicts)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_category_header(category: &'static str) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.pt_4()
|
||||||
|
.pb_2()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(colors::MUTED))
|
||||||
|
.child(category)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_shortcut_row(action: ShortcutAction, conflicts: &[ShortcutConflict]) -> AnyElement {
|
||||||
|
let has_conflict = conflicts
|
||||||
|
.iter()
|
||||||
|
.any(|conflict| conflict.actions.iter().any(|conflict_action| conflict_action == &action));
|
||||||
|
|
||||||
|
div()
|
||||||
|
.py_3()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(rgb(colors::HAIRLINE))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_3()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(shortcut_row_icon_color(has_conflict)))
|
||||||
|
.child(shortcut_row_icon(has_conflict)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.truncate()
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child(action.label()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.truncate()
|
||||||
|
.text_color(rgb(colors::MUTED))
|
||||||
|
.child(action.command().unwrap_or("Key binding only")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_end()
|
||||||
|
.gap_3()
|
||||||
|
.text_xs()
|
||||||
|
.child(shortcut_platform_label(action, ShortcutPlatform::Macos))
|
||||||
|
.child(shortcut_platform_label(action, ShortcutPlatform::WindowsLinux))
|
||||||
|
.child(shortcut_row_status(has_conflict)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shortcut_platform_label(action: ShortcutAction, platform: ShortcutPlatform) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.min_w(px(170.0))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(div().text_xs().text_color(rgb(colors::MUTED_SOFT)).child(platform.label()))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child(shortcut_keys_label(action, platform)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shortcut_row_status(has_conflict: bool) -> AnyElement {
|
||||||
|
let (label, color) =
|
||||||
|
if has_conflict { ("Conflict", colors::ERROR) } else { ("Ready", colors::SUCCESS) };
|
||||||
|
|
||||||
|
div().min_w(px(72.0)).font_semibold().text_color(rgb(color)).child(label).into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shortcut_keys_label(action: ShortcutAction, platform: ShortcutPlatform) -> String {
|
||||||
|
let bindings = bindings_for_action(action, platform)
|
||||||
|
.map(|binding| binding.display_keystroke())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if bindings.is_empty() {
|
||||||
|
return "Unassigned".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings.join(" / ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shortcut_status_color(conflict_count: usize) -> u32 {
|
||||||
|
if conflict_count == 0 { colors::SUCCESS } else { colors::ERROR }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shortcut_status_icon(conflict_count: usize) -> IconName {
|
||||||
|
if conflict_count == 0 { IconName::CircleCheck } else { IconName::TriangleAlert }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shortcut_row_icon(has_conflict: bool) -> IconName {
|
||||||
|
if has_conflict { IconName::TriangleAlert } else { IconName::SquareTerminal }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shortcut_row_icon_color(has_conflict: bool) -> u32 {
|
||||||
|
if has_conflict { colors::ERROR } else { colors::MUTED_SOFT }
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use gpui::{App, KeyBinding};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
||||||
|
OpenSettings, OpenTaskManager, Quit, RestoreClosedTab, SelectNextTab, SelectPreviousTab,
|
||||||
|
SplitRight, ToggleFavoriteTab,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub(crate) enum ShortcutPlatform {
|
||||||
|
Macos,
|
||||||
|
WindowsLinux,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShortcutPlatform {
|
||||||
|
pub(crate) fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Macos => "macOS",
|
||||||
|
Self::WindowsLinux => "Windows/Linux",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub(crate) enum ShortcutAction {
|
||||||
|
FocusAddressBar,
|
||||||
|
FocusCommandMode,
|
||||||
|
OpenNewTab,
|
||||||
|
CloseCurrentTab,
|
||||||
|
RestoreClosedTab,
|
||||||
|
SelectNextTab,
|
||||||
|
SelectPreviousTab,
|
||||||
|
SplitRight,
|
||||||
|
ToggleFavoriteTab,
|
||||||
|
OpenDownloads,
|
||||||
|
OpenHistory,
|
||||||
|
OpenSettings,
|
||||||
|
OpenTaskManager,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShortcutAction {
|
||||||
|
pub(crate) fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::FocusAddressBar => "Command Bar",
|
||||||
|
Self::FocusCommandMode => "Command Mode",
|
||||||
|
Self::OpenNewTab => "New Tab",
|
||||||
|
Self::CloseCurrentTab => "Close Tab",
|
||||||
|
Self::RestoreClosedTab => "Restore Closed Tab",
|
||||||
|
Self::SelectNextTab => "Next Tab",
|
||||||
|
Self::SelectPreviousTab => "Previous Tab",
|
||||||
|
Self::SplitRight => "Split Right",
|
||||||
|
Self::ToggleFavoriteTab => "Toggle Favorite",
|
||||||
|
Self::OpenDownloads => "Open Downloads",
|
||||||
|
Self::OpenHistory => "Open History",
|
||||||
|
Self::OpenSettings => "Open Settings",
|
||||||
|
Self::OpenTaskManager => "Task Manager",
|
||||||
|
Self::Quit => "Quit ELY Browser",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn category(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::FocusAddressBar | Self::FocusCommandMode => "Command",
|
||||||
|
Self::OpenNewTab
|
||||||
|
| Self::CloseCurrentTab
|
||||||
|
| Self::RestoreClosedTab
|
||||||
|
| Self::SelectNextTab
|
||||||
|
| Self::SelectPreviousTab
|
||||||
|
| Self::SplitRight
|
||||||
|
| Self::ToggleFavoriteTab => "Tabs",
|
||||||
|
Self::OpenDownloads | Self::OpenHistory => "Library",
|
||||||
|
Self::OpenSettings | Self::OpenTaskManager => "System",
|
||||||
|
Self::Quit => "Application",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn command(self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
Self::FocusAddressBar => None,
|
||||||
|
Self::FocusCommandMode => None,
|
||||||
|
Self::OpenNewTab => Some(">new-tab"),
|
||||||
|
Self::CloseCurrentTab => Some(">close-tab"),
|
||||||
|
Self::RestoreClosedTab => Some(">restore-tab"),
|
||||||
|
Self::SelectNextTab => None,
|
||||||
|
Self::SelectPreviousTab => None,
|
||||||
|
Self::SplitRight => Some(">split-right"),
|
||||||
|
Self::ToggleFavoriteTab => Some(">favorite"),
|
||||||
|
Self::OpenDownloads => Some(">open-downloads"),
|
||||||
|
Self::OpenHistory => Some(">open-history"),
|
||||||
|
Self::OpenSettings => Some(">open-settings"),
|
||||||
|
Self::OpenTaskManager => Some(">open-task-manager"),
|
||||||
|
Self::Quit => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct ShortcutBinding {
|
||||||
|
action: ShortcutAction,
|
||||||
|
platform: ShortcutPlatform,
|
||||||
|
keystroke: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShortcutBinding {
|
||||||
|
pub(crate) fn display_keystroke(self) -> String {
|
||||||
|
display_keystroke(self.keystroke, self.platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct ShortcutConflict {
|
||||||
|
pub(crate) platform: ShortcutPlatform,
|
||||||
|
pub(crate) keystroke: &'static str,
|
||||||
|
pub(crate) actions: Vec<ShortcutAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const SHORTCUT_ACTIONS: &[ShortcutAction] = &[
|
||||||
|
ShortcutAction::FocusAddressBar,
|
||||||
|
ShortcutAction::FocusCommandMode,
|
||||||
|
ShortcutAction::OpenNewTab,
|
||||||
|
ShortcutAction::CloseCurrentTab,
|
||||||
|
ShortcutAction::RestoreClosedTab,
|
||||||
|
ShortcutAction::SelectNextTab,
|
||||||
|
ShortcutAction::SelectPreviousTab,
|
||||||
|
ShortcutAction::SplitRight,
|
||||||
|
ShortcutAction::ToggleFavoriteTab,
|
||||||
|
ShortcutAction::OpenDownloads,
|
||||||
|
ShortcutAction::OpenHistory,
|
||||||
|
ShortcutAction::OpenSettings,
|
||||||
|
ShortcutAction::OpenTaskManager,
|
||||||
|
ShortcutAction::Quit,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) const SHORTCUT_BINDINGS: &[ShortcutBinding] = &[
|
||||||
|
shortcut(ShortcutAction::OpenNewTab, ShortcutPlatform::Macos, "cmd-t"),
|
||||||
|
shortcut(ShortcutAction::OpenNewTab, ShortcutPlatform::WindowsLinux, "ctrl-t"),
|
||||||
|
shortcut(ShortcutAction::SplitRight, ShortcutPlatform::Macos, "cmd-\\"),
|
||||||
|
shortcut(ShortcutAction::SplitRight, ShortcutPlatform::WindowsLinux, "ctrl-\\"),
|
||||||
|
shortcut(ShortcutAction::OpenDownloads, ShortcutPlatform::Macos, "cmd-shift-j"),
|
||||||
|
shortcut(ShortcutAction::OpenDownloads, ShortcutPlatform::WindowsLinux, "ctrl-shift-j"),
|
||||||
|
shortcut(ShortcutAction::OpenHistory, ShortcutPlatform::Macos, "cmd-y"),
|
||||||
|
shortcut(ShortcutAction::OpenHistory, ShortcutPlatform::WindowsLinux, "ctrl-h"),
|
||||||
|
shortcut(ShortcutAction::OpenTaskManager, ShortcutPlatform::Macos, "cmd-escape"),
|
||||||
|
shortcut(ShortcutAction::OpenTaskManager, ShortcutPlatform::WindowsLinux, "shift-escape"),
|
||||||
|
shortcut(ShortcutAction::OpenSettings, ShortcutPlatform::Macos, "cmd-,"),
|
||||||
|
shortcut(ShortcutAction::OpenSettings, ShortcutPlatform::WindowsLinux, "ctrl-,"),
|
||||||
|
shortcut(ShortcutAction::FocusAddressBar, ShortcutPlatform::Macos, "cmd-l"),
|
||||||
|
shortcut(ShortcutAction::FocusAddressBar, ShortcutPlatform::WindowsLinux, "ctrl-l"),
|
||||||
|
shortcut(ShortcutAction::CloseCurrentTab, ShortcutPlatform::Macos, "cmd-w"),
|
||||||
|
shortcut(ShortcutAction::CloseCurrentTab, ShortcutPlatform::WindowsLinux, "ctrl-w"),
|
||||||
|
shortcut(ShortcutAction::RestoreClosedTab, ShortcutPlatform::Macos, "cmd-shift-t"),
|
||||||
|
shortcut(ShortcutAction::RestoreClosedTab, ShortcutPlatform::WindowsLinux, "ctrl-shift-t"),
|
||||||
|
shortcut(ShortcutAction::ToggleFavoriteTab, ShortcutPlatform::Macos, "cmd-shift-f"),
|
||||||
|
shortcut(ShortcutAction::ToggleFavoriteTab, ShortcutPlatform::WindowsLinux, "ctrl-shift-f"),
|
||||||
|
shortcut(ShortcutAction::FocusCommandMode, ShortcutPlatform::Macos, "cmd-shift-p"),
|
||||||
|
shortcut(ShortcutAction::FocusCommandMode, ShortcutPlatform::WindowsLinux, "ctrl-shift-p"),
|
||||||
|
shortcut(ShortcutAction::SelectNextTab, ShortcutPlatform::Macos, "cmd-shift-]"),
|
||||||
|
shortcut(ShortcutAction::SelectNextTab, ShortcutPlatform::Macos, "cmd-alt-down"),
|
||||||
|
shortcut(ShortcutAction::SelectNextTab, ShortcutPlatform::WindowsLinux, "ctrl-tab"),
|
||||||
|
shortcut(ShortcutAction::SelectNextTab, ShortcutPlatform::WindowsLinux, "ctrl-alt-down"),
|
||||||
|
shortcut(ShortcutAction::SelectPreviousTab, ShortcutPlatform::Macos, "cmd-shift-["),
|
||||||
|
shortcut(ShortcutAction::SelectPreviousTab, ShortcutPlatform::Macos, "cmd-alt-up"),
|
||||||
|
shortcut(ShortcutAction::SelectPreviousTab, ShortcutPlatform::WindowsLinux, "ctrl-shift-tab"),
|
||||||
|
shortcut(ShortcutAction::SelectPreviousTab, ShortcutPlatform::WindowsLinux, "ctrl-alt-up"),
|
||||||
|
shortcut(ShortcutAction::Quit, ShortcutPlatform::Macos, "cmd-q"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const fn shortcut(
|
||||||
|
action: ShortcutAction,
|
||||||
|
platform: ShortcutPlatform,
|
||||||
|
keystroke: &'static str,
|
||||||
|
) -> ShortcutBinding {
|
||||||
|
ShortcutBinding { action, platform, keystroke }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bind_shortcuts(cx: &mut App) {
|
||||||
|
cx.bind_keys(SHORTCUT_BINDINGS.iter().map(|binding| binding.key_binding()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bindings_for_action(
|
||||||
|
action: ShortcutAction,
|
||||||
|
platform: ShortcutPlatform,
|
||||||
|
) -> impl Iterator<Item = ShortcutBinding> {
|
||||||
|
SHORTCUT_BINDINGS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(move |binding| binding.action == action && binding.platform == platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn shortcut_conflicts() -> Vec<ShortcutConflict> {
|
||||||
|
let mut bindings_by_key: BTreeMap<(ShortcutPlatform, &'static str), Vec<ShortcutAction>> =
|
||||||
|
BTreeMap::new();
|
||||||
|
|
||||||
|
for binding in SHORTCUT_BINDINGS {
|
||||||
|
bindings_by_key
|
||||||
|
.entry((binding.platform, binding.keystroke))
|
||||||
|
.or_default()
|
||||||
|
.push(binding.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings_by_key
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|((platform, keystroke), actions)| {
|
||||||
|
(actions.len() > 1).then_some(ShortcutConflict { platform, keystroke, actions })
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShortcutBinding {
|
||||||
|
fn key_binding(self) -> KeyBinding {
|
||||||
|
match self.action {
|
||||||
|
ShortcutAction::CloseCurrentTab => {
|
||||||
|
KeyBinding::new(self.keystroke, CloseCurrentTab, None)
|
||||||
|
}
|
||||||
|
ShortcutAction::FocusAddressBar => {
|
||||||
|
KeyBinding::new(self.keystroke, FocusAddressBar, None)
|
||||||
|
}
|
||||||
|
ShortcutAction::FocusCommandMode => {
|
||||||
|
KeyBinding::new(self.keystroke, FocusCommandMode, None)
|
||||||
|
}
|
||||||
|
ShortcutAction::OpenDownloads => KeyBinding::new(self.keystroke, OpenDownloads, None),
|
||||||
|
ShortcutAction::OpenHistory => KeyBinding::new(self.keystroke, OpenHistory, None),
|
||||||
|
ShortcutAction::OpenNewTab => KeyBinding::new(self.keystroke, OpenNewTab, None),
|
||||||
|
ShortcutAction::OpenSettings => KeyBinding::new(self.keystroke, OpenSettings, None),
|
||||||
|
ShortcutAction::OpenTaskManager => {
|
||||||
|
KeyBinding::new(self.keystroke, OpenTaskManager, None)
|
||||||
|
}
|
||||||
|
ShortcutAction::Quit => KeyBinding::new(self.keystroke, Quit, None),
|
||||||
|
ShortcutAction::RestoreClosedTab => {
|
||||||
|
KeyBinding::new(self.keystroke, RestoreClosedTab, None)
|
||||||
|
}
|
||||||
|
ShortcutAction::SelectNextTab => KeyBinding::new(self.keystroke, SelectNextTab, None),
|
||||||
|
ShortcutAction::SelectPreviousTab => {
|
||||||
|
KeyBinding::new(self.keystroke, SelectPreviousTab, None)
|
||||||
|
}
|
||||||
|
ShortcutAction::SplitRight => KeyBinding::new(self.keystroke, SplitRight, None),
|
||||||
|
ShortcutAction::ToggleFavoriteTab => {
|
||||||
|
KeyBinding::new(self.keystroke, ToggleFavoriteTab, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_keystroke(keystroke: &str, platform: ShortcutPlatform) -> String {
|
||||||
|
keystroke
|
||||||
|
.split('-')
|
||||||
|
.map(|part| display_key_part(part, platform))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" + ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_key_part(part: &str, platform: ShortcutPlatform) -> String {
|
||||||
|
match part {
|
||||||
|
"cmd" => "Cmd".to_string(),
|
||||||
|
"ctrl" => "Ctrl".to_string(),
|
||||||
|
"alt" if platform == ShortcutPlatform::Macos => "Option".to_string(),
|
||||||
|
"alt" => "Alt".to_string(),
|
||||||
|
"shift" => "Shift".to_string(),
|
||||||
|
"escape" => "Esc".to_string(),
|
||||||
|
value => value.to_ascii_uppercase(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
SHORTCUT_ACTIONS, SHORTCUT_BINDINGS, ShortcutAction, ShortcutPlatform, bindings_for_action,
|
||||||
|
shortcut_conflicts,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registered_shortcuts_have_no_conflicts() {
|
||||||
|
assert_eq!(shortcut_conflicts(), Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_settings_shortcut_has_platform_bindings() {
|
||||||
|
let bindings = bindings_for_action(ShortcutAction::OpenSettings, ShortcutPlatform::Macos)
|
||||||
|
.chain(bindings_for_action(
|
||||||
|
ShortcutAction::OpenSettings,
|
||||||
|
ShortcutPlatform::WindowsLinux,
|
||||||
|
))
|
||||||
|
.map(|binding| binding.display_keystroke())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(bindings, vec!["Cmd + ,".to_string(), "Ctrl + ,".to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_declared_action_has_a_binding() {
|
||||||
|
for action in SHORTCUT_ACTIONS {
|
||||||
|
assert!(SHORTCUT_BINDINGS.iter().any(|binding| binding.action == *action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ fn internal_page_title(url: &str) -> Option<&'static str> {
|
|||||||
"ely://about" => Some("About ELY Browser"),
|
"ely://about" => Some("About ELY Browser"),
|
||||||
"ely://settings" => Some("Settings"),
|
"ely://settings" => Some("Settings"),
|
||||||
"ely://settings/sidebar-tabs" => Some("Sidebar & Tabs Settings"),
|
"ely://settings/sidebar-tabs" => Some("Sidebar & Tabs Settings"),
|
||||||
|
"ely://settings/shortcuts" => Some("Shortcut Settings"),
|
||||||
"ely://settings/plugins" => Some("Plugin Settings"),
|
"ely://settings/plugins" => Some("Plugin Settings"),
|
||||||
"ely://settings/profiles" => Some("Profile Settings"),
|
"ely://settings/profiles" => Some("Profile Settings"),
|
||||||
"ely://settings/sync" => Some("Sync Settings"),
|
"ely://settings/sync" => Some("Sync Settings"),
|
||||||
@@ -137,6 +138,10 @@ pub(crate) fn settings_url() -> Result<UrlText, CoreError> {
|
|||||||
internal_page_url("ely://settings")
|
internal_page_url("ely://settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn shortcut_settings_url() -> Result<UrlText, CoreError> {
|
||||||
|
internal_page_url("ely://settings/shortcuts")
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn sync_status_url() -> Result<UrlText, CoreError> {
|
pub(crate) fn sync_status_url() -> Result<UrlText, CoreError> {
|
||||||
internal_page_url("ely://sync/status")
|
internal_page_url("ely://sync/status")
|
||||||
}
|
}
|
||||||
@@ -157,6 +162,9 @@ fn settings_page_route(query: &str) -> Option<&'static str> {
|
|||||||
"sidebar" | "tabs" | "sidebar tabs" | "sidebar & tabs" => {
|
"sidebar" | "tabs" | "sidebar tabs" | "sidebar & tabs" => {
|
||||||
Some("ely://settings/sidebar-tabs")
|
Some("ely://settings/sidebar-tabs")
|
||||||
}
|
}
|
||||||
|
"shortcut" | "shortcuts" | "keyboard" | "keyboard shortcuts" => {
|
||||||
|
Some("ely://settings/shortcuts")
|
||||||
|
}
|
||||||
"sync" | "sync settings" => Some("ely://settings/sync"),
|
"sync" | "sync settings" => Some("ely://settings/sync"),
|
||||||
"profile" | "profiles" | "profile settings" | "profiles settings" => {
|
"profile" | "profiles" | "profile settings" | "profiles settings" => {
|
||||||
Some("ely://settings/profiles")
|
Some("ely://settings/profiles")
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use crate::{
|
|||||||
about_url, archive_idle_days, archive_url, bookmarks_url, downloads_url, history_url,
|
about_url, archive_idle_days, archive_url, bookmarks_url, downloads_url, history_url,
|
||||||
move_tab_space_name, new_private_profile_name, new_profile_name, new_space_name,
|
move_tab_space_name, new_private_profile_name, new_profile_name, new_space_name,
|
||||||
plugin_detail_url, plugins_url, reading_list_url, search_url, settings_page_url,
|
plugin_detail_url, plugins_url, reading_list_url, search_url, settings_page_url,
|
||||||
settings_url, space_icon, switch_profile_name, sync_status_url, task_manager_url,
|
settings_url, shortcut_settings_url, space_icon, switch_profile_name, sync_status_url,
|
||||||
|
task_manager_url,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -181,6 +182,10 @@ impl BrowserCore {
|
|||||||
self.open_tab(settings_url()?);
|
self.open_tab(settings_url()?);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
"shortcuts" | "open-shortcuts" | "open shortcuts" => {
|
||||||
|
self.open_tab(shortcut_settings_url()?);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
"sync" | "open-sync-status" | "open sync status" => {
|
"sync" | "open-sync-status" | "open sync status" => {
|
||||||
self.open_tab(sync_status_url()?);
|
self.open_tab(sync_status_url()?);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|||||||
@@ -141,6 +141,21 @@ fn open_settings_command_opens_settings_page() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_shortcuts_command_opens_shortcuts_page() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.set_command_query(">open-shortcuts");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
let active_tab = core.active_tab()?;
|
||||||
|
|
||||||
|
assert_eq!(intent, Some(CommandIntent::Command("open-shortcuts".to_string())));
|
||||||
|
assert_eq!(active_tab.title(), "Shortcut Settings");
|
||||||
|
assert_eq!(active_tab.url().as_str(), "ely://settings/shortcuts");
|
||||||
|
assert_eq!(core.snapshot()?.command_query, "");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open_sync_status_command_opens_sync_status_page() -> Result<(), Box<dyn Error>> {
|
fn open_sync_status_command_opens_sync_status_page() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|||||||
@@ -23,3 +23,24 @@ fn settings_scoped_search_opens_sidebar_tabs_page() -> Result<(), Box<dyn Error>
|
|||||||
assert_eq!(core.snapshot()?.command_query, "");
|
assert_eq!(core.snapshot()?.command_query, "");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_scoped_search_opens_shortcuts_page() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.set_command_query("@settings shortcuts");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
let active_tab = core.active_tab()?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
intent,
|
||||||
|
Some(CommandIntent::ScopedSearch {
|
||||||
|
scope: CommandScope::Settings,
|
||||||
|
query: "shortcuts".to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(active_tab.title(), "Shortcut Settings");
|
||||||
|
assert_eq!(active_tab.url().as_str(), "ely://settings/shortcuts");
|
||||||
|
assert_eq!(core.snapshot()?.command_query, "");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user