diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index 93d5e71..686e9b5 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -104,8 +104,6 @@ type-to-search = Type to search... find-placeholder = Find... find-previous = Find previous find-next = Find next -find-case-sensitive = Case sensitive -find-regex = Regex # Menu @@ -115,12 +113,6 @@ new-tab = New tab new-window = New window profile = Profile menu-profiles = Profiles... -ssh = SSH -save-layout = Save layout... -load-layout = Load layout... -load-layout-title = Load layout -load-layout-body = Loading a layout replaces all tabs and splits in this window. -load = Load close-tab = Close tab quit = Quit @@ -131,12 +123,6 @@ paste = Paste select-all = Select all find = Find clear-scrollback = Clear scrollback -save-output = Save output as text... - -## Monitors -monitor-activity = Monitor for activity -monitor-silence = Monitor for silence -monitor-process-finished = Monitor for process finishing ## Open open-link = Open Link diff --git a/i18n/fr/cosmic_term.ftl b/i18n/fr/cosmic_term.ftl index 992b18e..8351497 100644 --- a/i18n/fr/cosmic_term.ftl +++ b/i18n/fr/cosmic_term.ftl @@ -72,8 +72,6 @@ show-header-description = Révéler l'en-tête du menu contextuel find-placeholder = Rechercher... find-previous = Chercher précédent find-next = Chercher prochain -find-case-sensitive = Respecter la casse -find-regex = Regex # Menu @@ -85,12 +83,6 @@ new-tab = Nouvel onglet new-window = Nouvelle fenêtre profile = Profil menu-profiles = Profils... -ssh = SSH -save-layout = Enregistrer la disposition... -load-layout = Charger une disposition... -load-layout-title = Charger une disposition -load-layout-body = Charger une disposition remplace tous les onglets et divisions de cette fenêtre. -load = Charger close-tab = Fermer l'onglet quit = Quitter @@ -102,12 +94,6 @@ paste = Coller select-all = Sélectionner tout find = Rechercher clear-scrollback = Effacer l'historique -save-output = Enregistrer la sortie comme texte... - -## Monitors -monitor-activity = Surveiller l'activité -monitor-silence = Surveiller le silence -monitor-process-finished = Surveiller la fin du processus ## View diff --git a/src/layout.rs b/src/layout.rs deleted file mode 100644 index 68a5f57..0000000 --- a/src/layout.rs +++ /dev/null @@ -1,147 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only - -//! Serializable description of a window layout: the pane grid split tree -//! and, for each pane, the list of tabs to restore. - -use cosmic::widget::pane_grid; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum LayoutAxis { - Horizontal, - Vertical, -} - -impl From for LayoutAxis { - fn from(axis: pane_grid::Axis) -> Self { - match axis { - pane_grid::Axis::Horizontal => Self::Horizontal, - pane_grid::Axis::Vertical => Self::Vertical, - } - } -} - -impl From for pane_grid::Axis { - fn from(axis: LayoutAxis) -> Self { - match axis { - LayoutAxis::Horizontal => Self::Horizontal, - LayoutAxis::Vertical => Self::Vertical, - } - } -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct LayoutTab { - pub profile: Option, - pub working_directory: Option, - pub title: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum LayoutNode { - Split { - axis: LayoutAxis, - ratio: f32, - a: Box, - b: Box, - }, - Pane { - tabs: Vec, - }, -} - -impl LayoutNode { - /// Leaf tab lists in depth-first order (a before b), matching the pane - /// creation order of `pane_grid::State::with_configuration`. - pub fn leaves(&self) -> Vec<&Vec> { - let mut leaves = Vec::new(); - self.collect_leaves(&mut leaves); - leaves - } - - fn collect_leaves<'a>(&'a self, leaves: &mut Vec<&'a Vec>) { - match self { - Self::Split { a, b, .. } => { - a.collect_leaves(leaves); - b.collect_leaves(leaves); - } - Self::Pane { tabs } => leaves.push(tabs), - } - } - - /// Build a pane grid configuration with one empty tab model per leaf. - pub fn configuration T>( - &self, - new_model: &mut F, - ) -> pane_grid::Configuration { - match self { - Self::Split { axis, ratio, a, b } => pane_grid::Configuration::Split { - axis: (*axis).into(), - ratio: *ratio, - a: Box::new(a.configuration(new_model)), - b: Box::new(b.configuration(new_model)), - }, - Self::Pane { .. } => pane_grid::Configuration::Pane(new_model()), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn sample() -> LayoutNode { - LayoutNode::Split { - axis: LayoutAxis::Vertical, - ratio: 0.6, - a: Box::new(LayoutNode::Pane { - tabs: vec![ - LayoutTab { - profile: Some(1), - working_directory: Some(PathBuf::from("/tmp")), - title: Some("build".to_string()), - }, - LayoutTab::default(), - ], - }), - b: Box::new(LayoutNode::Split { - axis: LayoutAxis::Horizontal, - ratio: 0.5, - a: Box::new(LayoutNode::Pane { - tabs: vec![LayoutTab::default()], - }), - b: Box::new(LayoutNode::Pane { - tabs: vec![LayoutTab::default()], - }), - }), - } - } - - #[test] - fn ron_round_trip() { - let layout = sample(); - let ron = ron::ser::to_string_pretty(&layout, ron::ser::PrettyConfig::new()).unwrap(); - let parsed: LayoutNode = ron::from_str(&ron).unwrap(); - let leaves = parsed.leaves(); - assert_eq!(leaves.len(), 3); - assert_eq!(leaves[0].len(), 2); - assert_eq!(leaves[0][0].profile, Some(1)); - assert_eq!(leaves[0][0].title.as_deref(), Some("build")); - assert_eq!( - leaves[0][0].working_directory.as_deref(), - Some(std::path::Path::new("/tmp")) - ); - } - - #[test] - fn leaves_depth_first_order() { - let layout = sample(); - // Depth-first: pane a first (2 tabs), then the two nested panes - let leaves = layout.leaves(); - assert_eq!( - leaves.iter().map(|tabs| tabs.len()).collect::>(), - vec![2, 1, 1] - ); - } -} diff --git a/src/main.rs b/src/main.rs index 4f13fe6..005cbc3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,9 +58,6 @@ mod icon_cache; use key_bind::key_binds; mod key_bind; -use layout::{LayoutNode, LayoutTab}; -mod layout; - mod shortcuts; mod localize; @@ -68,7 +65,7 @@ mod localize; use menu::menu_bar; mod menu; -use terminal::{MONITOR_SILENCE_SECONDS, MonitorKind, Terminal, TerminalPaneGrid, TerminalScroll}; +use terminal::{Terminal, TerminalPaneGrid, TerminalScroll}; mod terminal; use terminal_box::terminal_box; @@ -92,66 +89,6 @@ pub fn icon_cache_get(name: &'static str, size: u16) -> widget::icon::Icon { icon_cache.get(name, size) } -/// Tab bar indicator for the monitor state of a terminal: -/// an alert icon if one fired, an "armed" icon while monitoring, else none. -fn monitor_icon_name(terminal: &Terminal) -> Option<&'static str> { - match terminal.monitor_alert { - Some(MonitorKind::Activity) => Some("dialog-information-symbolic"), - Some(MonitorKind::Silence) => Some("alarm-symbolic"), - Some(MonitorKind::ProcessFinished) => Some("emblem-ok-symbolic"), - None if terminal.monitoring_enabled() => Some("view-reveal-symbolic"), - None => None, - } -} - -fn monitor_update_tab_icon( - tab_model: &mut segmented_button::Model, - entity: segmented_button::Entity, - icon_name: Option<&'static str>, -) { - match icon_name { - Some(name) => { - tab_model.icon_set(entity, icon_cache_get(name, 16)); - } - None => { - tab_model.icon_remove(entity); - } - } -} - -/// Concrete host names from `~/.ssh/config` (Host entries without wildcards -/// or negations), sorted and deduplicated. -fn ssh_config_hosts() -> Vec { - let Some(home) = env::var_os("HOME") else { - return Vec::new(); - }; - let path = PathBuf::from(home).join(".ssh").join("config"); - let Ok(content) = fs::read_to_string(&path) else { - return Vec::new(); - }; - let mut hosts = BTreeSet::new(); - for line in content.lines() { - let line = line.trim(); - if line.starts_with('#') { - continue; - } - let mut parts = line.split_whitespace(); - let Some(keyword) = parts.next() else { - continue; - }; - if !keyword.eq_ignore_ascii_case("host") { - continue; - } - for token in parts { - if token.contains(['*', '?']) || token.starts_with('!') { - continue; - } - hosts.insert(token.to_string()); - } - } - hosts.into_iter().collect() -} - /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { @@ -162,8 +99,6 @@ fn main() -> Result<(), Box> { let mut shell_args = Vec::new(); let mut daemonize = true; let mut working_directory = None; - let mut profile_name_opt = None; - let mut list_profiles = false; // Parse the arguments using clap_lex while let Some(arg) = raw_args.next_os(&mut cursor) { match arg.to_str() { @@ -186,17 +121,6 @@ fn main() -> Result<(), Box> { process::exit(1); } } - Some(arg_str @ "--profile") | Some(arg_str @ "-p") => { - if let Some(profile_arg) = raw_args.next_os(&mut cursor) { - profile_name_opt = Some(profile_arg.to_string_lossy().to_string()); - } else { - eprintln!("Missing argument for {arg_str}"); - process::exit(1); - } - } - Some("--list-profiles") => { - list_profiles = true; - } Some("--no-daemon") => { daemonize = false; } @@ -219,8 +143,22 @@ fn main() -> Result<(), Box> { } } - // Load config before daemonizing so profile CLI options can print - // errors and listings to the launching terminal + // Platform-specific daemonization logic + + #[cfg(all(unix, not(target_os = "redox")))] + if daemonize { + match fork::daemon(true, true) { + Ok(fork::Fork::Child) => (), + Ok(fork::Fork::Parent(_child_pid)) => process::exit(0), + Err(err) => { + eprintln!("failed to daemonize: {:?}", err); + process::exit(1); + } + } + } + + localize::localize(); + let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) { Ok(config_handler) => { let config = match Config::get_entry(&config_handler) { @@ -238,53 +176,6 @@ fn main() -> Result<(), Box> { } }; - if list_profiles { - for (name, profile_id) in config.profile_names() { - println!("{}\t{}", profile_id.0, name); - } - return Ok(()); - } - - let startup_profile_id = match &profile_name_opt { - Some(profile_name) => { - let profile_id_opt = config - .profile_names() - .into_iter() - .find(|(name, _)| name == profile_name) - .map(|(_, profile_id)| profile_id) - .or_else(|| { - // Fall back to matching a numeric profile ID - let id = profile_name.parse::().ok()?; - config.profiles.contains_key(&ProfileId(id)).then_some(ProfileId(id)) - }); - if profile_id_opt.is_none() { - eprintln!("Profile {profile_name:?} not found. Available profiles:"); - for (name, profile_id) in config.profile_names() { - eprintln!("{}\t{}", profile_id.0, name); - } - process::exit(1); - } - profile_id_opt - } - None => None, - }; - - // Platform-specific daemonization logic - - #[cfg(all(unix, not(target_os = "redox")))] - if daemonize { - match fork::daemon(true, true) { - Ok(fork::Fork::Child) => (), - Ok(fork::Fork::Parent(_child_pid)) => process::exit(0), - Err(err) => { - eprintln!("failed to daemonize: {:?}", err); - process::exit(1); - } - } - } - - localize::localize(); - let shortcuts_config = shortcuts::ShortcutsConfig::new(config.shortcuts_custom.clone()); let shell = shell_program_opt.map(|shell_program| tty::Shell::new(shell_program, shell_args)); @@ -317,7 +208,6 @@ fn main() -> Result<(), Box> { config, shortcuts_config, startup_options, - startup_profile_id, term_config, }; @@ -336,13 +226,7 @@ Project home page: https://github.com/pop-os/cosmic-term Options: --help Show this message --version Show the version of cosmic-term - -w, --working-directory Set the working directory for the terminal - -p, --profile Open the first tab with the given profile - --list-profiles List profiles (ID and name) and exit - --no-daemon Do not detach from the launching terminal - -e, --command Run a command instead of the default shell; - everything after this flag (or after `--`) - is treated as the command and its arguments"# + -w, --working-directory Set the working directory for the terminal"# ); } @@ -352,7 +236,6 @@ pub struct Flags { config: Config, shortcuts_config: shortcuts::ShortcutsConfig, startup_options: Option, - startup_profile_id: Option, term_config: term::Config, } @@ -368,9 +251,6 @@ pub enum Action { Find, KeyboardShortcuts, LaunchUrlByMenu, - LayoutLoad, - LayoutSave, - Monitor(MonitorKind), PaneFocusDown, PaneFocusLeft, PaneFocusRight, @@ -382,10 +262,8 @@ pub enum Action { PastePrimary, ProfileOpen(ProfileId), Profiles, - SaveOutput, SelectAll, Settings, - SshConnect(usize), #[cfg(feature = "password_manager")] PasswordManager, ShowHeaderBar(bool), @@ -427,9 +305,6 @@ impl Action { Self::Find => Message::Find(true), Self::KeyboardShortcuts => Message::ToggleContextPage(ContextPage::KeyboardShortcuts), Self::LaunchUrlByMenu => Message::LaunchUrlByMenu, - Self::LayoutLoad => Message::LayoutLoad, - Self::LayoutSave => Message::LayoutSave, - Self::Monitor(monitor_kind) => Message::MonitorToggle(*monitor_kind, entity_opt), Self::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down), Self::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left), Self::PaneFocusRight => Message::PaneFocusAdjacent(pane_grid::Direction::Right), @@ -443,10 +318,8 @@ impl Action { Self::PastePrimary => Message::PastePrimary(entity_opt), Self::ProfileOpen(profile_id) => Message::ProfileOpen(*profile_id), Self::Profiles => Message::ToggleContextPage(ContextPage::Profiles), - Self::SaveOutput => Message::SaveOutput(entity_opt), Self::SelectAll => Message::SelectAll(entity_opt), Self::Settings => Message::ToggleContextPage(ContextPage::Settings), - Self::SshConnect(index) => Message::SshConnect(*index), Self::ShowHeaderBar(show_headerbar) => Message::ShowHeaderBar(*show_headerbar), Self::TabActivate0 => Message::TabActivateJump(0), Self::TabActivate1 => Message::TabActivateJump(1), @@ -511,22 +384,14 @@ pub enum Message { DialogMessage(Box), // DialogMessage is huge, so we use a box to make the size of this enum smaller on the stack Drop(Option<(pane_grid::Pane, segmented_button::Entity, DndDrop)>), Find(bool), - FindCaseSensitive(bool), FindNext, FindPrevious, - FindRegex(bool), FindSearchValueChanged(String), MiddleClick(pane_grid::Pane, Option), FocusFollowMouse(bool), Key(Modifiers, Physical, Key), LaunchUrl(String), LaunchUrlByMenu, - LayoutLoad, - LayoutLoadApply, - LayoutLoadCancel, - LayoutLoadResult(DialogResult), - LayoutSave, - LayoutSaveResult(DialogResult), Modifiers(Modifiers), ShortcutCaptureCancel, ShortcutCaptureStart(shortcuts::KeyBindAction), @@ -535,8 +400,6 @@ pub enum Message { ShortcutRemove(shortcuts::Binding, shortcuts::BindingSource), ShortcutReset(shortcuts::KeyBindAction), ShortcutSearch(String), - MonitorTick, - MonitorToggle(MonitorKind, Option), MouseEnter(pane_grid::Pane), Opacity(u8), PaneClicked(pane_grid::Pane), @@ -564,13 +427,10 @@ pub enum Message { ProfileSyntaxTheme(ProfileId, ColorSchemeKind, usize), ProfileTabTitle(ProfileId, String), ReorderTab(Pane, ReorderEvent), - SaveOutput(Option), - SaveOutputResult(pane_grid::Pane, segmented_button::Entity, DialogResult), Surface(surface::Action), SelectAll(Option), ShowAdvancedFontSettings(bool), ShowHeaderBar(bool), - SshConnect(usize), SyntaxTheme(ColorSchemeKind, usize), SystemThemeChange, TabNewInheritWorkingDirectory(bool), @@ -652,14 +512,11 @@ pub struct App { dialog_opt: Option>, terminal_ids: HashMap, find: bool, - find_case_sensitive: bool, - find_regex: bool, find_search_id: widget::Id, find_search_value: String, term_event_tx_opt: Option>, startup_options: Option, - startup_profile_id: Option, term_config: term::Config, color_scheme_errors: Vec, color_scheme_expanded: Option<(ColorSchemeKind, Option)>, @@ -667,8 +524,6 @@ pub struct App { color_scheme_rename_id: widget::Id, rename_tab: Option<(segmented_button::Entity, String)>, rename_tab_id: widget::Id, - layout_load_pending: Option, - ssh_hosts: Vec, color_scheme_tab_model: widget::segmented_button::SingleSelectModel, profile_expanded: Option, show_advanced_font_settings: bool, @@ -1699,151 +1554,6 @@ impl App { terminal.working_directory() } - /// Build the regex pattern for the find bar based on the current options. - fn find_pattern(&self) -> String { - let mut pattern = if self.find_regex { - self.find_search_value.clone() - } else { - regex::escape(&self.find_search_value) - }; - if !self.find_case_sensitive { - pattern = format!("(?i){pattern}"); - } - pattern - } - - fn terminal_monitors( - &self, - pane: pane_grid::Pane, - entity: segmented_button::Entity, - ) -> (bool, bool, bool) { - if let Some(tab_model) = self.pane_model.panes.get(pane) - && let Some(terminal) = tab_model.data::>(entity) - { - let terminal = terminal.lock().unwrap(); - ( - terminal.monitor_activity, - terminal.monitor_silence, - terminal.monitor_process_finished, - ) - } else { - (false, false, false) - } - } - - fn active_terminal_monitors(&self) -> (bool, bool, bool) { - let pane = self.pane_model.focused(); - if let Some(tab_model) = self.pane_model.panes.get(pane) { - self.terminal_monitors(pane, tab_model.active()) - } else { - (false, false, false) - } - } - - fn any_monitor_needs_tick(&self) -> bool { - self.pane_model.panes.iter().any(|(_pane, tab_model)| { - tab_model.iter().any(|entity| { - tab_model - .data::>(entity) - .is_some_and(|terminal| { - let terminal = terminal.lock().unwrap(); - terminal.monitor_silence || terminal.monitor_process_finished - }) - }) - }) - } - - /// Snapshot the current pane grid and tabs as a serializable layout. - fn layout_from_state(&self, node: &pane_grid::Node) -> LayoutNode { - match node { - pane_grid::Node::Split { - axis, ratio, a, b, .. - } => LayoutNode::Split { - axis: (*axis).into(), - ratio: *ratio, - a: Box::new(self.layout_from_state(a)), - b: Box::new(self.layout_from_state(b)), - }, - pane_grid::Node::Pane(pane) => { - let mut tabs = Vec::new(); - if let Some(tab_model) = self.pane_model.panes.get(*pane) { - for entity in tab_model.iter() { - if let Some(terminal) = tab_model.data::>(entity) { - let terminal = terminal.lock().unwrap(); - tabs.push(LayoutTab { - profile: terminal.profile_id_opt.map(|profile_id| profile_id.0), - working_directory: terminal.working_directory(), - title: terminal.tab_title_override.clone(), - }); - } - } - } - if tabs.is_empty() { - tabs.push(LayoutTab::default()); - } - LayoutNode::Pane { tabs } - } - } - } - - /// Replace the window content with the given layout: rebuild the pane - /// grid and spawn a terminal per saved tab. - fn layout_apply(&mut self, layout: &LayoutNode) -> Task { - self.pane_model.panes.restore(); - - // Rebuild the pane grid; leaf order matches `LayoutNode::leaves` - self.pane_model.panes = pane_grid::State::with_configuration( - layout.configuration(&mut || segmented_button::ModelBuilder::default().build()), - ); - self.terminal_ids.clear(); - let panes: Vec = self - .pane_model - .panes - .iter() - .map(|(pane, _)| *pane) - .collect(); - for pane in &panes { - self.terminal_ids.insert(*pane, widget::Id::unique()); - } - self.pane_model.panes_created = panes.len(); - - let mut tasks = Vec::new(); - let default_tabs = vec![LayoutTab::default()]; - for (pane, tabs) in panes.iter().zip(layout.leaves()) { - // Guard against hand-edited layouts with an empty tab list - let tabs = if tabs.is_empty() { &default_tabs } else { tabs }; - for tab in tabs { - let profile_id_opt = tab - .profile - .map(ProfileId) - .filter(|profile_id| self.config.profiles.contains_key(profile_id)); - self.startup_options = Some(tty::Options { - working_directory: tab.working_directory.clone(), - ..tty::Options::default() - }); - tasks.push(self.create_and_focus_new_terminal(*pane, profile_id_opt, false)); - - // Restore custom tab titles - if let Some(title) = &tab.title - && let Some(tab_model) = self.pane_model.panes.get_mut(*pane) - { - let entity = tab_model.active(); - tab_model.text_set(entity, title.clone()); - if let Some(terminal) = tab_model.data::>(entity) { - terminal.lock().unwrap().tab_title_override = Some(title.clone()); - } - } - } - } - - if let Some(first) = panes.first() { - self.pane_model.set_focus(*first); - } - tasks.push(self.update_title(None)); - tasks.push(self.update_focus()); - Task::batch(tasks) - } - fn create_and_focus_new_terminal( &mut self, pane: pane_grid::Pane, @@ -2177,12 +1887,9 @@ impl Application for App { dialog_opt: None, terminal_ids, find: false, - find_case_sensitive: false, - find_regex: false, find_search_id: widget::Id::unique(), find_search_value: String::new(), startup_options: flags.startup_options, - startup_profile_id: flags.startup_profile_id, term_config: flags.term_config, term_event_tx_opt: None, color_scheme_errors: Vec::new(), @@ -2191,8 +1898,6 @@ impl Application for App { color_scheme_rename_id: widget::Id::unique(), rename_tab: None, rename_tab_id: widget::Id::unique(), - layout_load_pending: None, - ssh_hosts: ssh_config_hosts(), color_scheme_tab_model: widget::segmented_button::Model::default(), profile_expanded: None, show_advanced_font_settings: false, @@ -2696,19 +2401,14 @@ impl Application for App { // Focus correct input return self.update_focus(); } - Message::FindCaseSensitive(case_sensitive) => { - self.find_case_sensitive = case_sensitive; - return self.update_focus(); - } Message::FindNext => { - if !self.find_search_value.is_empty() { - let pattern = self.find_pattern(); - if let Some(tab_model) = self.pane_model.active() { - let entity = tab_model.active(); - if let Some(terminal) = tab_model.data::>(entity) { - let mut terminal = terminal.lock().unwrap(); - terminal.search(&pattern, true); - } + if !self.find_search_value.is_empty() + && let Some(tab_model) = self.pane_model.active() + { + let entity = tab_model.active(); + if let Some(terminal) = tab_model.data::>(entity) { + let mut terminal = terminal.lock().unwrap(); + terminal.search(&self.find_search_value, true); } } @@ -2716,24 +2416,19 @@ impl Application for App { return self.update_focus(); } Message::FindPrevious => { - if !self.find_search_value.is_empty() { - let pattern = self.find_pattern(); - if let Some(tab_model) = self.pane_model.active() { - let entity = tab_model.active(); - if let Some(terminal) = tab_model.data::>(entity) { - let mut terminal = terminal.lock().unwrap(); - terminal.search(&pattern, false); - } + if !self.find_search_value.is_empty() + && let Some(tab_model) = self.pane_model.active() + { + let entity = tab_model.active(); + if let Some(terminal) = tab_model.data::>(entity) { + let mut terminal = terminal.lock().unwrap(); + terminal.search(&self.find_search_value, false); } } // Focus correct input return self.update_focus(); } - Message::FindRegex(regex) => { - self.find_regex = regex; - return self.update_focus(); - } Message::FindSearchValueChanged(value) => { self.find_search_value = value; } @@ -2835,154 +2530,9 @@ impl Application for App { } } } - Message::LayoutLoad => { - if self.dialog_opt.is_none() { - let (dialog, command) = Dialog::new( - DialogSettings::new().kind(DialogKind::OpenFile), - |msg| Message::DialogMessage(Box::new(msg)), - Message::LayoutLoadResult, - ); - self.dialog_opt = Some(dialog); - return command; - } - } - Message::LayoutLoadApply => { - if let Some(layout) = self.layout_load_pending.take() { - return self.layout_apply(&layout); - } - } - Message::LayoutLoadCancel => { - self.layout_load_pending = None; - self.pane_model.update_terminal_focus(); - return self.update_focus(); - } - Message::LayoutLoadResult(result) => { - self.dialog_opt = None; - if let DialogResult::Open(paths) = result { - let path = &paths[0]; - match fs::read_to_string(path) { - Ok(content) => match ron::from_str::(&content) { - Ok(layout) => { - // Ask for confirmation before replacing sessions - self.layout_load_pending = Some(layout); - self.pane_model.unfocus_all_terminals(); - } - Err(err) => { - log::error!("failed to parse layout {:?}: {}", path, err); - } - }, - Err(err) => { - log::error!("failed to read layout {:?}: {}", path, err); - } - } - } - } - Message::LayoutSave => { - if self.dialog_opt.is_none() { - let (dialog, command) = Dialog::new( - DialogSettings::new().kind(DialogKind::SaveFile { - filename: String::from("cosmic-term-layout.ron"), - }), - |msg| Message::DialogMessage(Box::new(msg)), - Message::LayoutSaveResult, - ); - self.dialog_opt = Some(dialog); - return command; - } - } - Message::LayoutSaveResult(result) => { - self.dialog_opt = None; - if let DialogResult::Open(paths) = result { - let path = &paths[0]; - let layout = self.layout_from_state(self.pane_model.panes.layout()); - match ron::ser::to_string_pretty(&layout, ron::ser::PrettyConfig::new()) { - Ok(ron) => { - if let Err(err) = fs::write(path, ron) { - log::error!("failed to save layout to {:?}: {}", path, err); - } - } - Err(err) => { - log::error!("failed to serialize layout: {}", err); - } - } - } - } Message::Modifiers(modifiers) => { self.modifiers = modifiers; } - Message::MonitorTick => { - let panes: Vec = self - .pane_model - .panes - .iter() - .map(|(pane, _)| *pane) - .collect(); - for pane in panes { - let Some(tab_model) = self.pane_model.panes.get_mut(pane) else { - continue; - }; - let entities: Vec = tab_model.iter().collect(); - for entity in entities { - let mut icon_update = None; - if let Some(terminal) = tab_model.data::>(entity) { - let mut terminal = terminal.lock().unwrap(); - - // Silence: alert when no output for the configured delay - if terminal.monitor_silence - && terminal.monitor_alert.is_none() - && terminal.monitor_last_output.elapsed().as_secs() - >= MONITOR_SILENCE_SECONDS - { - terminal.monitor_alert = Some(MonitorKind::Silence); - icon_update = Some(monitor_icon_name(&terminal)); - } - - // Process finished: alert when the foreground job goes away - if terminal.monitor_process_finished { - let job_running = - terminal.foreground_job_running().unwrap_or(false); - if terminal.monitor_job_running && !job_running { - terminal.monitor_alert = Some(MonitorKind::ProcessFinished); - icon_update = Some(monitor_icon_name(&terminal)); - } - terminal.monitor_job_running = job_running; - } - } - if let Some(icon_name) = icon_update { - monitor_update_tab_icon(tab_model, entity, icon_name); - } - } - } - } - Message::MonitorToggle(monitor_kind, entity_opt) => { - if let Some(tab_model) = self.pane_model.active_mut() { - let entity = entity_opt.unwrap_or_else(|| tab_model.active()); - let mut icon_update = None; - if let Some(terminal) = tab_model.data::>(entity) { - let mut terminal = terminal.lock().unwrap(); - match monitor_kind { - MonitorKind::Activity => { - terminal.monitor_activity = !terminal.monitor_activity; - } - MonitorKind::Silence => { - terminal.monitor_silence = !terminal.monitor_silence; - terminal.monitor_last_output = std::time::Instant::now(); - } - MonitorKind::ProcessFinished => { - terminal.monitor_process_finished = - !terminal.monitor_process_finished; - terminal.monitor_job_running = - terminal.foreground_job_running().unwrap_or(false); - } - } - terminal.monitor_alert = None; - icon_update = Some(monitor_icon_name(&terminal)); - } - if let Some(icon_name) = icon_update { - monitor_update_tab_icon(tab_model, entity, icon_name); - } - } - } Message::MouseEnter(pane) => { self.pane_model.set_focus(pane); return self.update_focus(); @@ -3222,57 +2772,6 @@ impl Application for App { return self.save_profiles(); } } - Message::SaveOutput(entity_opt) => { - if self.dialog_opt.is_none() { - let pane = self.pane_model.focused(); - if let Some(tab_model) = self.pane_model.panes.get(pane) { - let entity = entity_opt.unwrap_or_else(|| tab_model.active()); - let filename = format!( - "{}.txt", - tab_model - .text(entity) - .map(|title| title.replace(['/', '\0'], "-")) - .unwrap_or_else(|| fl!("new-terminal")) - ); - let (dialog, command) = Dialog::new( - DialogSettings::new().kind(DialogKind::SaveFile { filename }), - |msg| Message::DialogMessage(Box::new(msg)), - move |result| Message::SaveOutputResult(pane, entity, result), - ); - self.dialog_opt = Some(dialog); - return command; - } - } - } - Message::SaveOutputResult(pane, entity, result) => { - self.dialog_opt = None; - if let DialogResult::Open(paths) = result { - let path = paths[0].clone(); - if let Some(tab_model) = self.pane_model.panes.get(pane) - && let Some(terminal) = tab_model.data::>(entity) - { - let text = terminal.lock().unwrap().scrollback_text(); - // Write in a blocking task to avoid stalling the UI on - // large scrollbacks or slow storage - return Task::perform( - async move { - match tokio::task::spawn_blocking(move || fs::write(&path, text)) - .await - { - Ok(Ok(())) => {} - Ok(Err(err)) => { - log::error!("failed to save terminal output: {}", err); - } - Err(err) => { - log::error!("failed to save terminal output: {}", err); - } - } - }, - |()| action::none(), - ); - } - } - } Message::SelectAll(entity_opt) => { if let Some(tab_model) = self.pane_model.active() { let entity = entity_opt.unwrap_or_else(|| tab_model.active()); @@ -3301,26 +2800,6 @@ impl Application for App { return self.update_config(); } } - Message::SshConnect(index) => { - if let Some(host) = self.ssh_hosts.get(index).cloned() { - self.startup_options = Some(tty::Options { - shell: Some(tty::Shell::new("ssh".to_string(), vec![host.clone()])), - ..tty::Options::default() - }); - let task = - self.create_and_focus_new_terminal(self.pane_model.focused(), None, false); - - // Name the tab after the host - if let Some(tab_model) = self.pane_model.active_mut() { - let entity = tab_model.active(); - tab_model.text_set(entity, host.clone()); - if let Some(terminal) = tab_model.data::>(entity) { - terminal.lock().unwrap().tab_title_override = Some(host); - } - } - return Task::batch([task, self.update_title(None)]); - } - } Message::ShowAdvancedFontSettings(show) => { self.show_advanced_font_settings = show; } @@ -3348,19 +2827,6 @@ impl Application for App { Message::TabActivate(entity) => { if let Some(tab_model) = self.pane_model.active_mut() { tab_model.activate(entity); - - // Viewing the tab acknowledges any monitor alert - let mut icon_update = None; - if let Some(terminal) = tab_model.data::>(entity) { - let mut terminal = terminal.lock().unwrap(); - if terminal.monitor_alert.is_some() { - terminal.monitor_alert = None; - icon_update = Some(monitor_icon_name(&terminal)); - } - } - if let Some(icon_name) = icon_update { - monitor_update_tab_icon(tab_model, entity, icon_name); - } } return self.update_title(None); } @@ -3540,14 +3006,9 @@ impl Application for App { return cosmic::Task::batch(tasks); } Message::TabNew => { - // The profile from `--profile` only applies to the first tab - let profile_id_opt = self - .startup_profile_id - .take() - .or_else(|| self.get_default_profile()); return self.create_and_focus_new_terminal( self.pane_model.focused(), - profile_id_opt, + self.get_default_profile(), self.config.tab_new_inherit_working_directory, ); } @@ -3738,7 +3199,7 @@ impl Application for App { } return self.update_title(Some(pane)); } - TermEvent::MouseCursorDirty => { + TermEvent::MouseCursorDirty | TermEvent::Wakeup => { if let Some(tab_model) = self.pane_model.panes.get(pane) && let Some(terminal) = tab_model.data::>(entity) { @@ -3746,33 +3207,6 @@ impl Application for App { terminal.needs_update = true; } } - TermEvent::Wakeup => { - if let Some(tab_model) = self.pane_model.panes.get_mut(pane) { - let is_active_tab = tab_model.active() == entity; - let mut icon_update = None; - if let Some(terminal) = tab_model.data::>(entity) { - let mut terminal = terminal.lock().unwrap(); - terminal.needs_update = true; - if terminal.monitoring_enabled() { - terminal.monitor_last_output = std::time::Instant::now(); - if terminal.monitor_alert == Some(MonitorKind::Silence) { - terminal.monitor_alert = None; - icon_update = Some(monitor_icon_name(&terminal)); - } - if terminal.monitor_activity - && !is_active_tab - && terminal.monitor_alert.is_none() - { - terminal.monitor_alert = Some(MonitorKind::Activity); - icon_update = Some(monitor_icon_name(&terminal)); - } - } - } - if let Some(icon_name) = icon_update { - monitor_update_tab_icon(tab_model, entity, icon_name); - } - } - } TermEvent::ChildExit(_error_code) => { //Ignore this for now } @@ -3996,21 +3430,6 @@ impl Application for App { } fn dialog(&self) -> Option> { - if self.layout_load_pending.is_some() { - return Some( - widget::dialog() - .title(fl!("load-layout-title")) - .body(fl!("load-layout-body")) - .primary_action( - widget::button::destructive(fl!("load")).on_press(Message::LayoutLoadApply), - ) - .secondary_action( - widget::button::standard(fl!("cancel")).on_press(Message::LayoutLoadCancel), - ) - .into(), - ); - } - if let Some((_, buffer)) = self.rename_tab.as_ref() { let input = widget::text_input(fl!("tab-name-placeholder"), buffer) .id(self.rename_tab_id.clone()) @@ -4064,13 +3483,7 @@ impl Application for App { } fn header_start(&self) -> Vec> { - vec![menu_bar( - &self.core, - &self.config, - &self.key_binds, - self.active_terminal_monitors(), - &self.ssh_hosts, - )] + vec![menu_bar(&self.core, &self.config, &self.key_binds)] } fn header_end(&self) -> Vec> { @@ -4093,18 +3506,12 @@ impl Application for App { } fn view_window(&self, window_id: window::Id) -> Element<'_, Message> { - if let Some((popup_id, pane, entity, ref link, ref autosize_id, _)) = + if let Some((popup_id, _pane, entity, ref link, ref autosize_id, _)) = self.context_menu_popup && window_id == popup_id { return widget::autosize::autosize( - menu::context_menu( - &self.config, - &self.key_binds, - entity, - link.clone(), - self.terminal_monitors(pane, entity), - ), + menu::context_menu(&self.config, &self.key_binds, entity, link.clone()), autosize_id.clone(), ) .into(); @@ -4139,9 +3546,7 @@ impl Application for App { cosmic::iced::widget::container::Style { icon_color: Some(Color::from(cosmic.background(false).on)), text_color: Some(Color::from(cosmic.background(false).on)), - background: Some(iced::Background::Color( - cosmic.background(false).base.into(), - )), + background: Some(iced::Background::Color(cosmic.background(false).base.into())), border: iced::Border::default(), shadow: iced::Shadow::default(), snap: true, @@ -4206,7 +3611,6 @@ impl Application for App { &self.key_binds, popup_entity, link.clone(), - self.terminal_monitors(popup_pane, popup_entity), )) .position(widget::popover::Position::Point(point)); popover.into() @@ -4266,14 +3670,6 @@ impl Application for App { widget::tooltip::Position::Top, ) .into(), - widget::checkbox(self.find_case_sensitive) - .label(fl!("find-case-sensitive")) - .on_toggle(Message::FindCaseSensitive) - .into(), - widget::checkbox(self.find_regex) - .label(fl!("find-regex")) - .on_toggle(Message::FindRegex) - .into(), widget::space::horizontal().into(), button::custom(icon_cache_get("window-close-symbolic", 16)) .on_press(Message::Find(false)) @@ -4327,7 +3723,7 @@ impl Application for App { struct ConfigSubscription; struct TerminalEventSubscription; - let mut subscriptions = vec![ + Subscription::batch([ event::listen_with(|event, _status, _window_id| match event { Event::Keyboard(KeyEvent::KeyPressed { key, @@ -4380,16 +3776,7 @@ impl Application for App { Some(dialog) => dialog.subscription(), None => Subscription::none(), }, - ]; - - // Timer driving silence and process-finished monitors - if self.any_monitor_needs_tick() { - subscriptions.push( - iced::time::every(std::time::Duration::from_secs(1)).map(|_| Message::MonitorTick), - ); - } - - Subscription::batch(subscriptions) + ]) } } diff --git a/src/menu.rs b/src/menu.rs index 4473f93..57c03ca 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -18,7 +18,7 @@ use cosmic::{ }; use std::{collections::HashMap, sync::LazyLock}; -use crate::{Action, ColorSchemeId, ColorSchemeKind, Config, Message, fl, terminal::MonitorKind}; +use crate::{Action, ColorSchemeId, ColorSchemeKind, Config, Message, fl}; static MENU_ID: LazyLock = LazyLock::new(|| cosmic::widget::Id::new("responsive-menu")); @@ -35,7 +35,6 @@ pub fn context_menu<'a>( key_binds: &HashMap, entity: segmented_button::Entity, link: Option, - monitors: (bool, bool, bool), ) -> Element<'a, Message> { let find_key = |action: &Action| -> String { for (key_bind, key_action) in key_binds { @@ -84,23 +83,6 @@ pub fn context_menu<'a>( 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(menu_item(fl!("save-output"), Action::SaveOutput)), - Element::from(divider::horizontal::light()), - Element::from(menu_checkbox( - fl!("monitor-activity"), - monitors.0, - Action::Monitor(MonitorKind::Activity), - )), - Element::from(menu_checkbox( - fl!("monitor-silence"), - monitors.1, - Action::Monitor(MonitorKind::Silence), - )), - Element::from(menu_checkbox( - fl!("monitor-process-finished"), - monitors.2, - Action::Monitor(MonitorKind::ProcessFinished), - )), Element::from(divider::horizontal::light()), Element::from(menu_item( fl!("split-horizontal"), @@ -218,8 +200,6 @@ pub fn menu_bar<'a>( core: &Core, config: &Config, key_binds: &HashMap, - monitors: (bool, bool, bool), - ssh_hosts: &[String], ) -> Element<'a, Message> { let mut profile_items = Vec::with_capacity(config.profiles.len()); for (name, id) in config.profile_names() { @@ -228,32 +208,6 @@ pub fn menu_bar<'a>( //TODO: what to do if there are no profiles? - let mut file_items = vec![ - MenuItem::Button(fl!("new-tab"), None, Action::TabNew), - MenuItem::Button(fl!("new-window"), None, Action::WindowNew), - MenuItem::Divider, - MenuItem::Folder(fl!("profile"), profile_items), - MenuItem::Button(fl!("menu-profiles"), None, Action::Profiles), - MenuItem::Divider, - ]; - if !ssh_hosts.is_empty() { - let ssh_items = ssh_hosts - .iter() - .enumerate() - .map(|(index, host)| MenuItem::Button(host.clone(), None, Action::SshConnect(index))) - .collect(); - file_items.push(MenuItem::Folder(fl!("ssh"), ssh_items)); - } - file_items.extend([ - MenuItem::Button(fl!("save-layout"), None, Action::LayoutSave), - MenuItem::Button(fl!("load-layout"), None, Action::LayoutLoad), - MenuItem::Divider, - MenuItem::Button(fl!("rename-tab"), None, Action::TabRename), - MenuItem::Button(fl!("close-tab"), None, Action::TabClose), - MenuItem::Divider, - MenuItem::Button(fl!("quit"), None, Action::WindowClose), - ]); - let color_scheme_kind = config.color_scheme_kind(core.system_theme()); responsive_menu_bar() @@ -266,7 +220,21 @@ pub fn menu_bar<'a>( MENU_ID.clone(), Message::Surface, vec![ - (fl!("file"), file_items), + ( + fl!("file"), + vec![ + MenuItem::Button(fl!("new-tab"), None, Action::TabNew), + MenuItem::Button(fl!("new-window"), None, Action::WindowNew), + MenuItem::Divider, + MenuItem::Folder(fl!("profile"), profile_items), + MenuItem::Button(fl!("menu-profiles"), None, Action::Profiles), + MenuItem::Divider, + MenuItem::Button(fl!("rename-tab"), None, Action::TabRename), + MenuItem::Button(fl!("close-tab"), None, Action::TabClose), + MenuItem::Divider, + MenuItem::Button(fl!("quit"), None, Action::WindowClose), + ], + ), ( fl!("edit"), vec![ @@ -275,7 +243,6 @@ pub fn menu_bar<'a>( MenuItem::Button(fl!("select-all"), None, Action::SelectAll), MenuItem::Divider, MenuItem::Button(fl!("clear-scrollback"), None, Action::ClearScrollback), - MenuItem::Button(fl!("save-output"), None, Action::SaveOutput), MenuItem::Divider, MenuItem::Button(fl!("find"), None, Action::Find), ], @@ -302,25 +269,6 @@ pub fn menu_bar<'a>( Action::PaneToggleMaximized, ), MenuItem::Divider, - MenuItem::CheckBox( - fl!("monitor-activity"), - None, - monitors.0, - Action::Monitor(MonitorKind::Activity), - ), - MenuItem::CheckBox( - fl!("monitor-silence"), - None, - monitors.1, - Action::Monitor(MonitorKind::Silence), - ), - MenuItem::CheckBox( - fl!("monitor-process-finished"), - None, - monitors.2, - Action::Monitor(MonitorKind::ProcessFinished), - ), - MenuItem::Divider, MenuItem::Button( fl!("menu-color-schemes"), None, diff --git a/src/shortcuts.rs b/src/shortcuts.rs index a04a103..e02ee00 100644 --- a/src/shortcuts.rs +++ b/src/shortcuts.rs @@ -72,7 +72,6 @@ pub enum KeyBindAction { PastePrimary, #[cfg_attr(not(feature = "password_manager"), allow(dead_code))] PasswordManager, - SaveOutput, SelectAll, Settings, TabActivate0, @@ -114,7 +113,6 @@ impl KeyBindAction { Self::PaneToggleMaximized => Some(Action::PaneToggleMaximized), Self::Paste => Some(Action::Paste), Self::PastePrimary => Some(Action::PastePrimary), - Self::SaveOutput => Some(Action::SaveOutput), Self::SelectAll => Some(Action::SelectAll), Self::Settings => Some(Action::Settings), Self::TabActivate0 => Some(Action::TabActivate0), @@ -277,7 +275,6 @@ pub fn action_label(action: KeyBindAction) -> String { KeyBindAction::Paste => fl!("paste"), KeyBindAction::PastePrimary => fl!("paste-primary"), KeyBindAction::PasswordManager => fl!("password-manager"), - KeyBindAction::SaveOutput => fl!("save-output"), KeyBindAction::SelectAll => fl!("select-all"), KeyBindAction::Settings => fl!("settings"), KeyBindAction::TabActivate0 => fl!("tab-activate", number = 1), @@ -369,7 +366,7 @@ pub fn shortcut_groups() -> Vec { KeyBindAction::ZoomReset, ], }); - let mut other_actions = vec![KeyBindAction::ClearScrollback, KeyBindAction::SaveOutput]; + let mut other_actions = vec![KeyBindAction::ClearScrollback]; #[cfg(feature = "password_manager")] other_actions.push(KeyBindAction::PasswordManager); groups.push(ShortcutGroup { @@ -462,7 +459,6 @@ fn fallback_shortcuts() -> Shortcuts { bind!([Shift], "Insert", PastePrimary); bind!([Ctrl, Shift], "W", TabClose); bind!([Ctrl, Shift], "R", TabRename); - bind!([Ctrl, Shift], "S", SaveOutput); bind!([Ctrl], ",", Settings); bind!([], "F11", ToggleFullscreen); diff --git a/src/terminal.rs b/src/terminal.rs index 1da3269..99ea621 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -54,17 +54,6 @@ pub const MIN_CURSOR_CONTRAST: f64 = 1.5; /// A regex expression can start or end outside the visible screen. Therefore, without this constant, some regular expressions would not match at the top and bottom. pub const MAX_SEARCH_LINES: usize = 100; -/// Kinds of session monitoring, modeled after Konsole's monitor options. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum MonitorKind { - Activity, - Silence, - ProcessFinished, -} - -/// Seconds without output before a silence alert fires (Konsole default). -pub const MONITOR_SILENCE_SECONDS: u64 = 10; - /// https://github.com/alacritty/alacritty/blob/4a7728bf7fac06a35f27f6c4f31e0d9214e5152b/alacritty/src/config/ui_config.rs#L36-L39 fn url_regex_search() -> RegexSearch { let url_regex = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)\ @@ -251,12 +240,6 @@ impl Metadata { pub struct Terminal { pub context_menu: Option, pub metadata_set: IndexSet, - pub monitor_activity: bool, - pub monitor_silence: bool, - pub monitor_process_finished: bool, - pub monitor_alert: Option, - pub monitor_last_output: Instant, - pub monitor_job_running: bool, pub needs_update: bool, pub profile_id_opt: Option, pub tab_title_override: Option, @@ -370,12 +353,6 @@ impl Terminal { default_attrs, dim_font_weight: Weight(dim_font_weight), metadata_set, - monitor_activity: false, - monitor_silence: false, - monitor_process_finished: false, - monitor_alert: None, - monitor_last_output: Instant::now(), - monitor_job_running: false, mouse_reporter: Default::default(), needs_update: true, notifier, @@ -643,50 +620,6 @@ impl Terminal { self.update(); } - /// Extract the entire scrollback plus visible screen as plain text, - /// trimming trailing empty lines. - pub fn scrollback_text(&self) -> String { - let term = self.term.lock(); - let grid = term.grid(); - let start = Point::new(Line(-(grid.history_size() as i32)), Column(0)); - let mut end_line = grid.bottommost_line(); - while end_line.0 > 0 { - if !grid[end_line].is_clear() { - break; - } - end_line.0 -= 1; - } - let end = Point::new(end_line, Column(grid.columns() - 1)); - term.bounds_to_string(start, end) - } - - pub fn monitoring_enabled(&self) -> bool { - self.monitor_activity || self.monitor_silence || self.monitor_process_finished - } - - /// Whether a foreground job (other than the shell itself) is running on - /// the terminal. Compares the process group of the shell with the - /// foreground process group of the controlling terminal. - pub fn foreground_job_running(&self) -> Option { - #[cfg(target_os = "linux")] - { - let shell_pid = self.shell_pid?; - let stat = fs::read_to_string(format!("/proc/{shell_pid}/stat")).ok()?; - // Fields after the last ')' to handle process names with spaces: - // state ppid pgrp session tty_nr tpgid ... - let after_comm = stat.rsplit_once(')')?.1; - let mut fields = after_comm.split_whitespace(); - let pgrp = fields.nth(2)?; - let tpgid = fields.nth(2)?; - Some(pgrp != tpgid) - } - - #[cfg(not(target_os = "linux"))] - { - None - } - } - pub fn select_all(&mut self) { { let mut term = self.term.lock();