diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index b1058cf..88ed838 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -86,6 +86,9 @@ select-all = Select all find = Find clear-scrollback = Clear scrollback +## Open +open-link = Open Link + ## View view = View zoom-in = Larger text diff --git a/i18n/sv-SE/cosmic_term.ftl b/i18n/sv-SE/cosmic_term.ftl index b162c82..5389ebe 100644 --- a/i18n/sv-SE/cosmic_term.ftl +++ b/i18n/sv-SE/cosmic_term.ftl @@ -94,6 +94,9 @@ paste = Klistra in select-all = Välj alla find = Sök +## Öppna +open-link = Öppna länk + ## Visa view = Visa diff --git a/src/main.rs b/src/main.rs index 7268a00..194b4d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use cosmic::{ cosmic_config::{self, ConfigSet, CosmicConfigEntry}, cosmic_theme, executor, iced::{ - self, Alignment, Color, Event, Length, Limits, Padding, Point, Subscription, + self, Alignment, Color, Event, Length, Limits, Padding, Subscription, advanced::graphics::text::font_system, clipboard, event, futures::SinkExt, @@ -63,6 +63,7 @@ mod terminal; use terminal_box::terminal_box; use crate::dnd::DndDrop; +use crate::menu::MenuState; mod terminal_box; #[cfg(feature = "password_manager")] @@ -225,6 +226,7 @@ pub enum Action { CopyOrSigint, CopyPrimary, Find, + LaunchUrlByMenu, PaneFocusDown, PaneFocusLeft, PaneFocusRight, @@ -275,6 +277,7 @@ impl Action { Self::CopyOrSigint => Message::CopyOrSigint(entity_opt), Self::CopyPrimary => Message::CopyPrimary(entity_opt), Self::Find => Message::Find(true), + Self::LaunchUrlByMenu => Message::LaunchUrlByMenu, Self::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down), Self::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left), Self::PaneFocusRight => Message::PaneFocusAdjacent(pane_grid::Direction::Right), @@ -359,6 +362,7 @@ pub enum Message { FocusFollowMouse(bool), Key(Modifiers, Key), LaunchUrl(String), + LaunchUrlByMenu, Modifiers(Modifiers), MouseEnter(pane_grid::Pane), Opacity(u8), @@ -396,7 +400,7 @@ pub enum Message { TabActivateJump(usize), TabClose(Option), TabContextAction(segmented_button::Entity, Action), - TabContextMenu(pane_grid::Pane, Option), + TabContextMenu(pane_grid::Pane, Option), TabNew, TabNewNoProfile, TabNext, @@ -2118,6 +2122,23 @@ impl Application for App { log::warn!("failed to open {:?}: {}", url, err); } } + Message::LaunchUrlByMenu => { + if let Some(tab_model) = self.pane_model.active() { + let entity = tab_model.active(); + if let Some(terminal) = tab_model.data::>(entity) { + // Update context menu position + let mut terminal = terminal.lock().unwrap(); + if let Some(url) = + terminal.context_menu.as_ref().and_then(|m| m.link.as_ref()) + { + if let Err(err) = open::that_detached(url) { + log::warn!("failed to open {:?}: {}", url, err); + } + } + terminal.context_menu = None; + } + } + } Message::Modifiers(modifiers) => { self.modifiers = modifiers; } @@ -2413,14 +2434,25 @@ impl Application for App { // Close context menu { let mut terminal = terminal.lock().unwrap(); - terminal.context_menu = None; + //Some actions need the menu_state, + //so only clear the position for them. + match action { + Action::LaunchUrlByMenu => { + if let Some(context_menu) = terminal.context_menu.as_mut() { + context_menu.position = None; + } + } + _ => { + terminal.context_menu = None; + } + } } // Run action's message return self.update(action.message(Some(entity))); } } } - Message::TabContextMenu(pane, position_opt) => { + Message::TabContextMenu(pane, menu_state) => { // Close any existing context menues let panes: Vec<_> = self.pane_model.panes.iter().collect(); for (_pane, tab_model) in panes { @@ -2437,7 +2469,7 @@ impl Application for App { if let Some(terminal) = tab_model.data::>(entity) { // Update context menu position let mut terminal = terminal.lock().unwrap(); - terminal.context_menu = position_opt; + terminal.context_menu = menu_state; } } @@ -2797,9 +2829,7 @@ impl Application for App { if let Some(terminal) = tab_model.data::>(entity) { let mut terminal_box = terminal_box(terminal) .id(terminal_id) - .on_context_menu(move |position_opt| { - Message::TabContextMenu(pane, position_opt) - }) + .on_context_menu(move |menu_state| Message::TabContextMenu(pane, menu_state)) .on_middle_click(move || Message::MiddleClick(pane, Some(entity_middle_click))) .on_open_hyperlink(Some(Box::new(Message::LaunchUrl))) .on_window_focused(|| Message::WindowFocused) @@ -2815,14 +2845,22 @@ impl Application for App { let context_menu = { let terminal = terminal.lock().unwrap(); - terminal.context_menu + terminal.context_menu.clone() }; let tab_element: Element<'_, Message> = match context_menu { - Some(point) => widget::popover(terminal_box.context_menu(point)) - .popup(menu::context_menu(&self.config, &self.key_binds, entity)) - .position(widget::popover::Position::Point(point)) - .into(), + Some(menu_state) => match menu_state.position { + Some(point) => widget::popover(terminal_box.context_menu(point)) + .popup(menu::context_menu( + &self.config, + &self.key_binds, + entity, + menu_state.link, + )) + .position(widget::popover::Position::Point(point)) + .into(), + None => terminal_box.into(), + }, None => terminal_box.into(), }; tab_column = tab_column.push(tab_element); diff --git a/src/menu.rs b/src/menu.rs index cf8459a..5ae4535 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-only +use cosmic::iced::Point; +use cosmic::widget::Column; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::{Item as MenuItem, menu_button}; use cosmic::{ Element, app::Core, iced::{ - Background, Length, - advanced::widget::text::Style as TextStyle, - widget::{column, horizontal_space}, + Background, Length, advanced::widget::text::Style as TextStyle, widget::horizontal_space, }, iced_core::Border, theme, @@ -25,10 +25,17 @@ use crate::{Action, ColorSchemeId, ColorSchemeKind, Config, Message, fl}; static MENU_ID: LazyLock = LazyLock::new(|| cosmic::widget::Id::new("responsive-menu")); +#[derive(Debug, Clone)] +pub struct MenuState { + pub position: Option, + pub link: Option, +} + pub fn context_menu<'a>( config: &Config, key_binds: &HashMap, entity: segmented_button::Entity, + link: Option, ) -> Element<'a, Message> { let find_key = |action: &Action| -> String { for (key_bind, key_action) in key_binds { @@ -70,32 +77,49 @@ pub fn context_menu<'a>( .on_press(Message::TabContextAction(entity, action)) }; - let mut content = column!( - menu_item(fl!("copy"), Action::Copy), - menu_item(fl!("paste"), Action::Paste), - menu_item(fl!("select-all"), Action::SelectAll), - divider::horizontal::light(), - menu_item(fl!("clear-scrollback"), Action::ClearScrollback), - divider::horizontal::light(), - menu_item(fl!("split-horizontal"), Action::PaneSplitHorizontal), - menu_item(fl!("split-vertical"), Action::PaneSplitVertical), - menu_item(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized), - divider::horizontal::light(), - menu_item(fl!("new-tab"), Action::TabNew), - menu_item(fl!("menu-settings"), Action::Settings), - ); + let mut rows = vec![ + Element::from(menu_item(fl!("copy"), Action::Copy)), + Element::from(menu_item(fl!("paste"), Action::Paste)), + Element::from(menu_item(fl!("select-all"), Action::SelectAll)), + Element::from(divider::horizontal::light()), + Element::from(menu_item(fl!("clear-scrollback"), Action::ClearScrollback)), + Element::from(divider::horizontal::light()), + Element::from(menu_item( + fl!("split-horizontal"), + Action::PaneSplitHorizontal, + )), + Element::from(menu_item(fl!("split-vertical"), Action::PaneSplitVertical)), + Element::from(menu_item( + fl!("pane-toggle-maximize"), + Action::PaneToggleMaximized, + )), + Element::from(divider::horizontal::light()), + Element::from(menu_item(fl!("new-tab"), Action::TabNew)), + Element::from(menu_item(fl!("menu-settings"), Action::Settings)), + ]; #[cfg(feature = "password_manager")] { - content = content.push(menu_item( + rows.push(Element::from(menu_item( fl!("menu-password-manager"), Action::PasswordManager, - )); + ))); } - content = content.push(menu_checkbox( + rows.push(Element::from(menu_checkbox( fl!("show-headerbar"), config.show_headerbar, Action::ShowHeaderBar(!config.show_headerbar), - )); + ))); + + //If we have a link + //prepend the Open Link item + if link.is_some() { + rows.insert( + 0, + Element::from(menu_item(fl!("open-link"), Action::LaunchUrlByMenu)), + ); + rows.insert(1, Element::from(divider::horizontal::light())); + } + let content = Column::with_children(rows); widget::container(content) .padding(1) //TODO: move style to libcosmic diff --git a/src/terminal.rs b/src/terminal.rs index d6fafed..9430d59 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -41,6 +41,7 @@ pub use alacritty_terminal::grid::Scroll as TerminalScroll; use crate::{ config::{ColorSchemeKind, Config as AppConfig, ProfileId}, + menu::MenuState, mouse_reporter::MouseReporter, }; @@ -236,7 +237,7 @@ impl Metadata { } pub struct Terminal { - pub context_menu: Option, + pub context_menu: Option, pub metadata_set: IndexSet, pub needs_update: bool, pub profile_id_opt: Option, diff --git a/src/terminal_box.rs b/src/terminal_box.rs index b2576c5..f27749e 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -44,8 +44,8 @@ use std::{ }; use crate::{ - Action, Terminal, TerminalScroll, key_bind::key_binds, mouse_reporter::MouseReporter, - terminal::Metadata, + Action, Terminal, TerminalScroll, key_bind::key_binds, menu::MenuState, + mouse_reporter::MouseReporter, terminal::Metadata, }; pub struct TerminalBox<'a, Message> { @@ -56,7 +56,7 @@ pub struct TerminalBox<'a, Message> { show_headerbar: bool, click_timing: Duration, context_menu: Option, - on_context_menu: Option) -> Message + 'a>>, + on_context_menu: Option) -> Message + 'a>>, on_mouse_enter: Option Message + 'a>>, opacity: Option, mouse_inside_boundary: Option, @@ -126,7 +126,7 @@ where pub fn on_context_menu( mut self, - on_context_menu: impl Fn(Option) -> Message + 'a, + on_context_menu: impl Fn(Option) -> Message + 'a, ) -> Self { self.on_context_menu = Some(Box::new(on_context_menu)); self @@ -927,7 +927,7 @@ where } else { None }; - update_active_regex_match(&mut terminal, location, &state.modifiers); + update_active_regex_match(&mut terminal, location, Some(&state.modifiers)); } } Event::Keyboard(KeyEvent::KeyPressed { @@ -1100,13 +1100,35 @@ where } // Update context menu state if let Some(on_context_menu) = &self.on_context_menu { - shell.publish((on_context_menu)(match self.context_menu { - Some(_) => None, - None => match button { - Button::Right => Some(p), - _ => None, - }, - })); + match self.context_menu { + Some(_) => { + shell.publish(on_context_menu(None)); + } + None => { + if button == Button::Right { + let x = p.x - self.padding.left; + let y = p.y - self.padding.top; + //TODO: better calculation of position + let col = x / terminal.size().cell_width; + let row = y / terminal.size().cell_height; + + let location = terminal.viewport_to_point(TermPoint::new( + row as usize, + TermColumn(col as usize), + )); + update_active_regex_match( + &mut terminal, + Some(location), + None, + ); + let link = get_hyperlink(&terminal, location); + shell.publish(on_context_menu(Some(MenuState { + position: Some(p), + link, + }))); + } + } + } } status = Status::Captured; } @@ -1125,14 +1147,7 @@ where .viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize))); if state.modifiers.control() { if let Some(on_open_hyperlink) = &self.on_open_hyperlink { - if let Some(match_) = terminal - .regex_matches - .iter() - .find(|bounds| bounds.contains(&location)) - { - let term = terminal.term.lock(); - let hyperlink = - term.bounds_to_string(*match_.start(), *match_.end()); + if let Some(hyperlink) = get_hyperlink(&terminal, location) { shell.publish(on_open_hyperlink(hyperlink)); status = Status::Captured; } @@ -1182,7 +1197,11 @@ where let row = y / terminal.size().cell_height; let location = terminal .viewport_to_point(TermPoint::new(row as usize, TermColumn(col as usize))); - update_active_regex_match(&mut terminal, Some(location), &state.modifiers); + update_active_regex_match( + &mut terminal, + Some(location), + Some(&state.modifiers), + ); if is_mouse_mode { terminal.report_mouse(event, &state.modifiers, col as u32, row as u32); @@ -1283,7 +1302,11 @@ where row as usize, TermColumn(col as usize), )); - update_active_regex_match(&mut terminal, Some(location), &state.modifiers); + update_active_regex_match( + &mut terminal, + Some(location), + Some(&state.modifiers), + ); } } } @@ -1294,18 +1317,44 @@ where } } +fn get_hyperlink( + terminal: &std::sync::MutexGuard<'_, Terminal>, + location: TermPoint, +) -> Option { + if let Some(match_) = terminal + .regex_matches + .iter() + .find(|bounds| bounds.contains(&location)) + { + let term = terminal.term.lock(); + Some(term.bounds_to_string(*match_.start(), *match_.end())) + } else { + None + } +} + fn update_active_regex_match( terminal: &mut std::sync::MutexGuard<'_, Terminal>, location: Option, - modifiers: &Modifiers, + modifiers: Option<&Modifiers>, ) { - if !modifiers.contains(Modifiers::CTRL) { - if terminal.active_regex_match.is_some() { - terminal.active_regex_match = None; - terminal.needs_update = true; - } + //Do not update any highlights if + //there is a context_menu shown + //to the user + if terminal.context_menu.is_some() { return; } + + //Require CTRL for keyboard and mouse interaction + if let Some(modifiers) = modifiers { + if !modifiers.contains(Modifiers::CTRL) { + if terminal.active_regex_match.is_some() { + terminal.active_regex_match = None; + terminal.needs_update = true; + } + return; + } + } let Some(location) = location else { if terminal.active_regex_match.is_some() { terminal.active_regex_match = None;