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] {