diff --git a/Cargo.lock b/Cargo.lock index 97523a0..2722d8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,12 @@ dependencies = [ "libc", ] +[[package]] +name = "any_ascii" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" + [[package]] name = "apply" version = "0.3.0" @@ -954,7 +960,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -971,7 +977,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "quote", "syn 1.0.109", @@ -989,20 +995,22 @@ dependencies = [ "i18n-embed-fl", "indexmap", "lazy_static", + "lexical-sort", "libcosmic", "log", "palette", "paste", "rust-embed", "serde", + "shlex", "smol_str", "tokio", ] [[package]] name = "cosmic-text" -version = "0.11.1" -source = "git+https://github.com/pop-os/cosmic-text.git#cb447ea8c6717d558994575b93a00baa549d01f8" +version = "0.11.2" +source = "git+https://github.com/pop-os/cosmic-text.git#0cb6eba6e708e2743313ee0016162de7a0146353" dependencies = [ "bitflags 2.4.2", "fontdb", @@ -1024,7 +1032,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "almost", "cosmic-config", @@ -1638,9 +1646,9 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98b88c54a38407f7352dd2c4238830115a6377741098ffd1f997c813d0e088a6" +checksum = "3890d0893c8253d3eb98337af18b3e1a10a9b2958f2a164b53a93fb3a3049e72" dependencies = [ "fontconfig-parser", "log", @@ -2238,7 +2246,7 @@ dependencies = [ [[package]] name = "iced" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "iced_accessibility", "iced_core", @@ -2253,7 +2261,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "accesskit", "accesskit_winit", @@ -2262,7 +2270,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "bitflags 1.3.2", "log", @@ -2279,7 +2287,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "futures", "iced_core", @@ -2292,7 +2300,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2316,7 +2324,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2328,7 +2336,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "iced_core", "iced_futures", @@ -2338,7 +2346,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "iced_core", "once_cell", @@ -2348,7 +2356,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "bytemuck", "cosmic-text", @@ -2365,7 +2373,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2384,7 +2392,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "iced_renderer", "iced_runtime", @@ -2398,7 +2406,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "iced_graphics", "iced_runtime", @@ -2684,6 +2692,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +[[package]] +name = "lexical-sort" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09e4591611e231daf4d4c685a66cb0410cc1e502027a20ae55f2bb9e997207a" +dependencies = [ + "any_ascii", +] + [[package]] name = "libc" version = "0.2.151" @@ -2692,7 +2709,7 @@ source = "git+https://gitlab.redox-os.org/redox-os/liblibc.git?branch=redox_0.2. [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#f9d2e5832791d1d30350411bd9b5beb4b1b26e49" +source = "git+https://github.com/pop-os/libcosmic.git#5738ac20559ff3c327fd9bcf3bcf323281a4c504" dependencies = [ "apply", "ashpd", @@ -4095,6 +4112,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" diff --git a/Cargo.toml b/Cargo.toml index 6224540..2426e1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,10 @@ alacritty_terminal = "0.20" env_logger = "0.10" lazy_static = "1" indexmap = "2" +lexical-sort = "0.3.1" log = "0.4" serde = { version = "1", features = ["serde_derive"] } +shlex = "1" tokio = { version = "1", features = ["sync"] } # Internationalization i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] } diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index 31de374..cc36317 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -1,5 +1,15 @@ # Context Pages +## Profiles +profiles = Profiles +name = Name +command-line = Command line +command-line-description = Custom command line to run, if set +tab-title = Tab title +tab-title-description = Override the default tab title +add-profile = Add profile +new-profile = New profile + ## Settings settings = Settings @@ -44,6 +54,8 @@ find-next = Find next file = File new-tab = New tab new-window = New window +profile = Profile +menu-profiles = Profiles... close-tab = Close tab quit = Quit diff --git a/res/icons/edit-delete-symbolic.svg b/res/icons/edit-delete-symbolic.svg new file mode 100644 index 0000000..22efd68 --- /dev/null +++ b/res/icons/edit-delete-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/icons/list-add-symbolic.svg b/res/icons/list-add-symbolic.svg new file mode 100644 index 0000000..59b2fb0 --- /dev/null +++ b/res/icons/list-add-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/config.rs b/src/config.rs index d2c4680..95c788d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::sync::OnceLock; +use crate::fl; + pub const CONFIG_VERSION: u64 = 1; #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -29,6 +31,35 @@ impl AppTheme { } } +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(transparent)] +pub struct ProfileId(pub u64); + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct Profile { + pub name: String, + #[serde(default)] + pub command: String, + #[serde(default)] + pub syntax_theme_dark: String, + #[serde(default)] + pub syntax_theme_light: String, + #[serde(default)] + pub tab_title: String, +} + +impl Default for Profile { + fn default() -> Self { + Self { + name: fl!("new-profile"), + command: String::new(), + syntax_theme_dark: "COSMIC Dark".to_string(), + syntax_theme_light: "COSMIC Light".to_string(), + tab_title: String::new(), + } + } +} + #[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Config { pub app_theme: AppTheme, @@ -39,6 +70,7 @@ pub struct Config { pub bold_font_weight: u16, pub font_stretch: u16, pub font_size_zoom_step_mul_100: u16, + pub profiles: BTreeMap, pub show_headerbar: bool, pub use_bright_bold: bool, pub syntax_theme_dark: String, @@ -50,18 +82,19 @@ impl Default for Config { fn default() -> Self { Self { app_theme: AppTheme::System, + bold_font_weight: Weight::BOLD.0, + dim_font_weight: Weight::NORMAL.0, + focus_follow_mouse: false, font_name: "Fira Mono".to_string(), font_size: 14, - font_weight: Weight::NORMAL.0, - dim_font_weight: Weight::NORMAL.0, - bold_font_weight: Weight::BOLD.0, - font_stretch: Stretch::Normal.to_number(), font_size_zoom_step_mul_100: 100, + font_stretch: Stretch::Normal.to_number(), + font_weight: Weight::NORMAL.0, + profiles: BTreeMap::new(), show_headerbar: true, - use_bright_bold: false, syntax_theme_dark: "COSMIC Dark".to_string(), syntax_theme_light: "COSMIC Light".to_string(), - focus_follow_mouse: false, + use_bright_bold: false, } } } @@ -81,13 +114,42 @@ impl Config { Metrics::new(font_size, line_height) } + // Get a sorted and adjusted for duplicates list of profiles names and ids + pub fn profile_names(&self) -> Vec<(String, ProfileId)> { + let mut profile_names = Vec::<(String, ProfileId)>::with_capacity(self.profiles.len()); + for (profile_id, profile) in self.profiles.iter() { + let mut name = profile.name.clone(); + + let mut copies = 1; + while profile_names.iter().find(|x| x.0 == name).is_some() { + copies += 1; + name = format!("{} ({})", profile.name, copies); + } + + profile_names.push((name, *profile_id)); + } + profile_names.sort_by(|a, b| lexical_sort::natural_lexical_cmp(&a.0, &b.0)); + profile_names + } + // Get current syntax theme based on dark mode - pub fn syntax_theme(&self) -> &str { + pub fn syntax_theme(&self, profile_id_opt: Option) -> &str { let dark = self.app_theme.theme().theme_type.is_dark(); - if dark { - &self.syntax_theme_dark - } else { - &self.syntax_theme_light + match profile_id_opt.and_then(|profile_id| self.profiles.get(&profile_id)) { + Some(profile) => { + if dark { + &profile.syntax_theme_dark + } else { + &profile.syntax_theme_light + } + } + None => { + if dark { + &self.syntax_theme_dark + } else { + &self.syntax_theme_light + } + } } } diff --git a/src/icon_cache.rs b/src/icon_cache.rs index 24d5024..a06f3fa 100644 --- a/src/icon_cache.rs +++ b/src/icon_cache.rs @@ -31,6 +31,8 @@ impl IconCache { } bundle!("edit-clear-symbolic", 16); + bundle!("edit-delete-symbolic", 16); + bundle!("list-add-symbolic", 16); bundle!("go-down-symbolic", 16); bundle!("go-up-symbolic", 16); bundle!("window-close-symbolic", 16); diff --git a/src/main.rs b/src/main.rs index e9f4a7d..1c3d7b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use alacritty_terminal::{ }; use cosmic::{ app::{message, Command, Core, Settings}, - cosmic_config::{self, CosmicConfigEntry}, + cosmic_config::{self, ConfigSet, CosmicConfigEntry}, cosmic_theme, executor, iced::{ advanced::graphics::text::font_system, @@ -29,7 +29,7 @@ use std::{ }; use tokio::sync::mpsc; -use config::{AppTheme, Config, CONFIG_VERSION}; +use config::{AppTheme, Config, Profile, ProfileId, CONFIG_VERSION}; mod config; mod mouse_reporter; @@ -175,6 +175,8 @@ pub enum Action { PaneSplitVertical, PaneToggleMaximized, Paste, + ProfileOpen(ProfileId), + Profiles, SelectAll, Settings, ShowHeaderBar(bool), @@ -211,6 +213,8 @@ impl Action { Action::PaneSplitVertical => Message::PaneSplit(pane_grid::Axis::Vertical), Action::PaneToggleMaximized => Message::PaneToggleMaximized, Action::Paste => Message::Paste(entity_opt), + Action::ProfileOpen(profile_id) => Message::ProfileOpen(profile_id), + Action::Profiles => Message::ToggleContextPage(ContextPage::Profiles), Action::SelectAll => Message::SelectAll(entity_opt), Action::Settings => Message::ToggleContextPage(ContextPage::Settings), Action::ShowHeaderBar(show_headerbar) => Message::ShowHeaderBar(show_headerbar), @@ -264,6 +268,15 @@ pub enum Message { MouseEnter(pane_grid::Pane), Paste(Option), PasteValue(Option, String), + ProfileCollapse(ProfileId), + ProfileCommand(ProfileId, String), + ProfileExpand(ProfileId), + ProfileName(ProfileId, String), + ProfileNew, + ProfileOpen(ProfileId), + ProfileRemove(ProfileId), + ProfileSyntaxTheme(ProfileId, usize, bool), + ProfileTabTitle(ProfileId, String), SelectAll(Option), UseBrightBold(bool), ShowHeaderBar(bool), @@ -291,12 +304,14 @@ pub enum Message { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ContextPage { + Profiles, Settings, } impl ContextPage { fn title(&self) -> String { match self { + Self::Profiles => fl!("profiles"), Self::Settings => fl!("settings"), } } @@ -333,6 +348,7 @@ pub struct App { term_event_tx_opt: Option>, startup_options: Option, term_config: TermConfig, + profile_expanded: Option, show_advanced_font_settings: bool, modifiers: Modifiers, } @@ -367,6 +383,19 @@ impl App { self.update_config() } + fn save_profiles(&mut self) -> Command { + // Optimized for just saving profiles + if let Some(ref config_handler) = self.config_handler { + match config_handler.set("profiles", &self.config.profiles) { + Ok(()) => {} + Err(err) => { + log::error!("failed to save config: {}", err); + } + } + } + Command::none() + } + fn update_focus(&self) -> Command { if self.find { widget::text_input::focus(self.find_search_id.clone()) @@ -476,6 +505,147 @@ impl App { } } + fn profiles(&self) -> Element { + let cosmic_theme::Spacing { + space_s, + space_xs, + space_xxs, + space_xxxs, + .. + } = self.core().system_theme().cosmic().spacing; + + let mut sections = Vec::with_capacity(2); + + if !self.config.profiles.is_empty() { + let mut profiles_section = widget::settings::view_section(""); + for (profile_name, profile_id) in self.config.profile_names() { + let profile = match self.config.profiles.get(&profile_id) { + Some(some) => some, + None => continue, + }; + + let expanded = self.profile_expanded == Some(profile_id); + + profiles_section = profiles_section.add( + widget::settings::item::builder(profile_name).control( + widget::row::with_children(vec![ + widget::button(icon_cache_get("edit-delete-symbolic", 16)) + .on_press(Message::ProfileRemove(profile_id)) + .style(style::Button::Icon) + .into(), + if expanded { + widget::button(icon_cache_get("go-up-symbolic", 16)) + .on_press(Message::ProfileCollapse(profile_id)) + } else { + widget::button(icon_cache_get("go-down-symbolic", 16)) + .on_press(Message::ProfileExpand(profile_id)) + } + .style(style::Button::Icon) + .into(), + ]) + .align_items(Alignment::Center) + .spacing(space_xxs), + ), + ); + + if expanded { + let dark_selected = self + .theme_names + .iter() + .position(|theme_name| theme_name == &profile.syntax_theme_dark); + let light_selected = self + .theme_names + .iter() + .position(|theme_name| theme_name == &profile.syntax_theme_light); + + let expanded_section = widget::settings::view_section("") + .add( + widget::column::with_children(vec![ + widget::column::with_children(vec![ + widget::text(fl!("name")).into(), + widget::text_input("", &profile.name) + .on_input(move |text| { + Message::ProfileName(profile_id, text) + }) + .into(), + ]) + .spacing(space_xxxs) + .into(), + widget::column::with_children(vec![ + widget::text(fl!("command-line")).into(), + widget::text_input("", &profile.command) + .on_input(move |text| { + Message::ProfileCommand(profile_id, text) + }) + .into(), + widget::text::caption(fl!("command-line-description")).into(), + ]) + .spacing(space_xxxs) + .into(), + widget::column::with_children(vec![ + widget::text(fl!("tab-title")).into(), + widget::text_input("", &profile.tab_title) + .on_input(move |text| { + Message::ProfileTabTitle(profile_id, text) + }) + .into(), + widget::text::caption(fl!("tab-title-description")).into(), + ]) + .spacing(space_xxxs) + .into(), + ]) + .padding([0, space_s]) + .spacing(space_xs), + ) + .add( + //TODO: rename to color-scheme-dark? + widget::settings::item::builder(fl!("syntax-dark")).control( + widget::dropdown( + &self.theme_names, + dark_selected, + move |theme_i| { + Message::ProfileSyntaxTheme(profile_id, theme_i, true) + }, + ), + ), + ) + .add( + //TODO: rename to color-scheme-light? + widget::settings::item::builder(fl!("syntax-light")).control( + widget::dropdown( + &self.theme_names, + light_selected, + move |theme_i| { + Message::ProfileSyntaxTheme(profile_id, theme_i, false) + }, + ), + ), + ); + + let padding = Padding { + top: 0.0, + bottom: 0.0, + left: space_s as f32, + right: space_s as f32, + }; + profiles_section = + profiles_section.add(widget::container(expanded_section).padding(padding)) + } + } + sections.push(profiles_section.into()); + } + + let add_profile = widget::row::with_children(vec![ + widget::horizontal_space(Length::Fill).into(), + widget::button(widget::text(fl!("add-profile"))) + .on_press(Message::ProfileNew) + .into(), + ]); + sections.push(add_profile.into()); + + widget::settings::view_column(sections).into() + } + fn settings(&self) -> Element { let app_theme_selected = match self.config.app_theme { AppTheme::Dark => 1, @@ -658,48 +828,85 @@ impl App { .into() } - fn create_and_focus_new_terminal(&mut self, pane: pane_grid::Pane) { + fn create_and_focus_new_terminal( + &mut self, + pane: pane_grid::Pane, + profile_id_opt: Option, + ) -> Command { self.pane_model.focus = pane; match &self.term_event_tx_opt { - Some(term_event_tx) => match self.themes.get(self.config.syntax_theme()) { - Some(colors) => { - let current_pane = self.pane_model.focus; - if let Some(tab_model) = self.pane_model.active_mut() { - let entity = tab_model - .insert() - .text("New Terminal") - .closable() - .activate() - .id(); - // Use the startup options, or defaults - let options = self.startup_options.take().unwrap_or_default(); - let mut terminal = Terminal::new( - current_pane, - entity, - term_event_tx.clone(), - self.term_config.clone(), - options, - &self.config, - *colors, + Some(term_event_tx) => { + match self.themes.get(self.config.syntax_theme(profile_id_opt)) { + Some(colors) => { + let current_pane = self.pane_model.focus; + if let Some(tab_model) = self.pane_model.active_mut() { + let entity = tab_model + .insert() + .text("New Terminal") + .closable() + .activate() + .id(); + // Use the profile options, startup options, or defaults + let options = match profile_id_opt + .and_then(|profile_id| self.config.profiles.get(&profile_id)) + { + Some(profile) => { + let mut shell = None; + if let Some(mut args) = shlex::split(&profile.command) { + if !args.is_empty() { + let command = args.remove(0); + shell = Some(tty::Shell::new(command, args)); + } + } + tty::Options { + shell, + //TODO: configurable working directory? + working_directory: None, + //TODO: configurable hold (keep open when child exits)? + hold: false, + } + } + None => self.startup_options.take().unwrap_or_default(), + }; + match Terminal::new( + current_pane, + entity, + term_event_tx.clone(), + self.term_config.clone(), + options, + &self.config, + *colors, + profile_id_opt, + ) { + Ok(mut terminal) => { + terminal.set_config(&self.config, &self.themes, self.zoom_adj); + tab_model + .data_set::>(entity, Mutex::new(terminal)); + } + Err(err) => { + log::error!("failed to open terminal: {}", err); + // Clean up partially created tab + return self.update(Message::TabClose(Some(entity))); + } + } + } else { + log::error!("Found no active pane"); + } + } + None => { + log::error!( + "failed to find terminal theme {:?}", + self.config.syntax_theme(profile_id_opt) ); - terminal.set_config(&self.config, &self.themes, self.zoom_adj); - tab_model.data_set::>(entity, Mutex::new(terminal)); - } else { - log::error!("Found no active pane"); + //TODO: fall back to known good theme } } - None => { - log::error!( - "failed to find terminal theme {:?}", - self.config.syntax_theme() - ); - //TODO: fall back to known good theme - } - }, + } None => { log::warn!("tried to create new tab before having event channel"); } } + return self.update_title(Some(pane)); } } @@ -864,6 +1071,7 @@ impl Application for App { startup_options: flags.startup_options, term_config: flags.term_config, term_event_tx_opt: None, + profile_expanded: None, show_advanced_font_settings: false, modifiers: Modifiers::empty(), }; @@ -1096,9 +1304,9 @@ impl Application for App { ); if let Some((pane, _)) = result { self.terminal_ids.insert(pane, widget::Id::unique()); - self.create_and_focus_new_terminal(pane); + let command = self.create_and_focus_new_terminal(pane, None); self.pane_model.panes_created += 1; - return self.update_title(Some(pane)); + return command; } } Message::PaneToggleMaximized => { @@ -1142,6 +1350,77 @@ impl Application for App { } return self.update_focus(); } + Message::ProfileCollapse(_profile_id) => { + self.profile_expanded = None; + } + Message::ProfileCommand(profile_id, text) => { + if let Some(profile) = self.config.profiles.get_mut(&profile_id) { + profile.command = text; + return self.save_profiles(); + } + } + Message::ProfileExpand(profile_id) => { + self.profile_expanded = Some(profile_id); + } + Message::ProfileName(profile_id, text) => { + if let Some(profile) = self.config.profiles.get_mut(&profile_id) { + profile.name = text; + return self.save_profiles(); + } + } + Message::ProfileNew => { + // Get next profile ID + let profile_id = self + .config + .profiles + .last_key_value() + .map(|(id, _)| ProfileId(id.0 + 1)) + .unwrap_or_default(); + self.config.profiles.insert(profile_id, Profile::default()); + self.profile_expanded = Some(profile_id); + return self.save_profiles(); + } + Message::ProfileOpen(profile_id) => { + return self.create_and_focus_new_terminal(self.pane_model.focus, Some(profile_id)); + } + Message::ProfileRemove(profile_id) => { + // Reset matching terminals to default profile + for (_pane, tab_model) in self.pane_model.panes.iter() { + for entity in tab_model.iter() { + if let Some(terminal) = tab_model.data::>(entity) { + let mut terminal = terminal.lock().unwrap(); + if terminal.profile_id_opt == Some(profile_id) { + terminal.profile_id_opt = None; + } + } + } + } + self.config.profiles.remove(&profile_id); + return self.save_profiles(); + } + Message::ProfileSyntaxTheme(profile_id, theme_i, dark) => { + match self.theme_names.get(theme_i) { + Some(theme_name) => { + if let Some(profile) = self.config.profiles.get_mut(&profile_id) { + if dark { + profile.syntax_theme_dark = theme_name.to_string(); + } else { + profile.syntax_theme_light = theme_name.to_string(); + } + return self.save_profiles(); + } + } + None => { + log::warn!("failed to find syntax theme with index {}", theme_i); + } + } + } + Message::ProfileTabTitle(profile_id, text) => { + if let Some(profile) = self.config.profiles.get_mut(&profile_id) { + profile.tab_title = text; + return self.save_profiles(); + } + } Message::SelectAll(entity_opt) => { if let Some(tab_model) = self.pane_model.active() { let entity = entity_opt.unwrap_or_else(|| tab_model.active()); @@ -1280,7 +1559,9 @@ impl Application for App { self.pane_model.focus = pane; return self.update_title(Some(pane)); } - Message::TabNew => self.create_and_focus_new_terminal(self.pane_model.focus), + Message::TabNew => { + return self.create_and_focus_new_terminal(self.pane_model.focus, None) + } Message::TabNext => { if let Some(tab_model) = self.pane_model.active() { let len = tab_model.iter().count(); @@ -1449,17 +1730,18 @@ impl Application for App { } Some(match self.context_page { + ContextPage::Profiles => self.profiles(), ContextPage::Settings => self.settings(), }) } fn header_start(&self) -> Vec> { - vec![menu_bar(&self.key_binds)] + vec![menu_bar(&self.config, &self.key_binds)] } fn header_end(&self) -> Vec> { let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing; - vec![widget::button(widget::icon::from_name("list-add-symbolic")) + vec![widget::button(icon_cache_get("list-add-symbolic", 16)) .on_press(Message::TabNew) .padding(space_xxs) .style(style::Button::Icon) diff --git a/src/menu.rs b/src/menu.rs index a1477bd..6aed4dd 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -108,7 +108,7 @@ pub fn context_menu<'a>( .into() } -pub fn menu_bar<'a>(key_binds: &HashMap) -> Element<'a, Message> { +pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap) -> Element<'a, Message> { //TODO: port to libcosmic let menu_root = |label| { widget::button(widget::text(label)) @@ -116,6 +116,9 @@ pub fn menu_bar<'a>(key_binds: &HashMap) -> Element<'a, Message .style(theme::Button::MenuRoot) }; + let menu_folder = + |label| menu_button!(widget::text(label), horizontal_space(Length::Fill), ">"); + let find_key = |action: &Action| -> String { for (key_bind, key_action) in key_binds.iter() { if action == key_action { @@ -137,6 +140,12 @@ pub fn menu_bar<'a>(key_binds: &HashMap) -> Element<'a, Message ) }; + let mut profile_items = Vec::with_capacity(config.profiles.len()); + for (name, id) in config.profile_names() { + profile_items.push(menu_item(name, Action::ProfileOpen(id))); + } + //TODO: what to do if there are no profiles? + MenuBar::new(vec![ MenuTree::with_children( menu_root(fl!("file")), @@ -144,6 +153,9 @@ pub fn menu_bar<'a>(key_binds: &HashMap) -> Element<'a, Message menu_item(fl!("new-tab"), Action::TabNew), menu_item(fl!("new-window"), Action::WindowNew), MenuTree::new(horizontal_rule(1)), + MenuTree::with_children(menu_folder(fl!("profile")), profile_items), + menu_item(fl!("menu-profiles"), Action::Profiles), + MenuTree::new(horizontal_rule(1)), menu_item(fl!("close-tab"), Action::TabClose), MenuTree::new(horizontal_rule(1)), menu_item(fl!("quit"), Action::WindowClose), diff --git a/src/terminal.rs b/src/terminal.rs index 6b225c1..ba5ecc7 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -27,7 +27,7 @@ use indexmap::IndexSet; use std::{ borrow::Cow, collections::HashMap, - mem, + io, mem, sync::{ atomic::{AtomicU32, Ordering}, Arc, Weak, @@ -38,7 +38,10 @@ use tokio::sync::mpsc; pub use alacritty_terminal::grid::Scroll as TerminalScroll; -use crate::{config::Config as AppConfig, mouse_reporter::MouseReporter}; +use crate::{ + config::{Config as AppConfig, ProfileId}, + mouse_reporter::MouseReporter, +}; #[derive(Clone, Copy, Debug)] pub struct Size { @@ -187,21 +190,22 @@ impl Metadata { } pub struct Terminal { - default_attrs: Attrs<'static>, - buffer: Arc, - size: Size, - pub term: Arc>>, - colors: Colors, - dim_font_weight: Weight, - bold_font_weight: Weight, - use_bright_bold: bool, - notifier: Notifier, pub context_menu: Option, + pub metadata_set: IndexSet, pub needs_update: bool, + pub profile_id_opt: Option, + pub term: Arc>>, + bold_font_weight: Weight, + buffer: Arc, + colors: Colors, + default_attrs: Attrs<'static>, + dim_font_weight: Weight, + mouse_reporter: MouseReporter, + notifier: Notifier, search_regex_opt: Option, search_value: String, - pub metadata_set: IndexSet, - mouse_reporter: MouseReporter, + size: Size, + use_bright_bold: bool, } impl Terminal { @@ -214,7 +218,8 @@ impl Terminal { options: Options, app_config: &AppConfig, colors: Colors, - ) -> Self { + profile_id_opt: Option, + ) -> Result { let font_stretch = app_config.typed_font_stretch(); let font_weight = app_config.font_weight; let dim_font_weight = app_config.dim_font_weight; @@ -267,29 +272,30 @@ impl Terminal { ))); let window_id = 0; - let pty = tty::new(&options, size.into(), window_id).unwrap(); + let pty = tty::new(&options, size.into(), window_id)?; let pty_event_loop = EventLoop::new(term.clone(), event_proxy, pty, options.hold, false); let notifier = Notifier(pty_event_loop.channel()); let _pty_join_handle = pty_event_loop.spawn(); - Self { - colors, - dim_font_weight: Weight(dim_font_weight), + Ok(Self { bold_font_weight: Weight(bold_font_weight), - use_bright_bold, - default_attrs, buffer: Arc::new(buffer), - size, - term, - notifier, + colors, context_menu: None, - needs_update: true, - search_regex_opt: None, - search_value: String::new(), + default_attrs, + dim_font_weight: Weight(dim_font_weight), metadata_set, mouse_reporter: Default::default(), - } + needs_update: true, + notifier, + profile_id_opt, + search_regex_opt: None, + search_value: String::new(), + size, + term, + use_bright_bold, + }) } pub fn buffer_weak(&self) -> Weak { @@ -550,7 +556,7 @@ impl Terminal { update_cell_size = true; } - if let Some(colors) = themes.get(config.syntax_theme()) { + if let Some(colors) = themes.get(config.syntax_theme(self.profile_id_opt)) { let mut changed = false; for i in 0..color::COUNT { if self.colors[i] != colors[i] {