Add support for profiles (#131)

This commit is contained in:
Jeremy Soller 2024-02-09 15:45:46 -07:00 • committed by GitHub
parent 8fd3197cc4
commit 3a3e42110c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 508 additions and 100 deletions

63
Cargo.lock generated
View file

@ -202,6 +202,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "any_ascii"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e"
[[package]] [[package]]
name = "apply" name = "apply"
version = "0.3.0" version = "0.3.0"
@ -954,7 +960,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-config" name = "cosmic-config"
version = "0.1.0" 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 = [ dependencies = [
"atomicwrites", "atomicwrites",
"cosmic-config-derive", "cosmic-config-derive",
@ -971,7 +977,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-config-derive" name = "cosmic-config-derive"
version = "0.1.0" 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 = [ dependencies = [
"quote", "quote",
"syn 1.0.109", "syn 1.0.109",
@ -989,20 +995,22 @@ dependencies = [
"i18n-embed-fl", "i18n-embed-fl",
"indexmap", "indexmap",
"lazy_static", "lazy_static",
"lexical-sort",
"libcosmic", "libcosmic",
"log", "log",
"palette", "palette",
"paste", "paste",
"rust-embed", "rust-embed",
"serde", "serde",
"shlex",
"smol_str", "smol_str",
"tokio", "tokio",
] ]
[[package]] [[package]]
name = "cosmic-text" name = "cosmic-text"
version = "0.11.1" version = "0.11.2"
source = "git+https://github.com/pop-os/cosmic-text.git#cb447ea8c6717d558994575b93a00baa549d01f8" source = "git+https://github.com/pop-os/cosmic-text.git#0cb6eba6e708e2743313ee0016162de7a0146353"
dependencies = [ dependencies = [
"bitflags 2.4.2", "bitflags 2.4.2",
"fontdb", "fontdb",
@ -1024,7 +1032,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-theme" name = "cosmic-theme"
version = "0.1.0" 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 = [ dependencies = [
"almost", "almost",
"cosmic-config", "cosmic-config",
@ -1638,9 +1646,9 @@ dependencies = [
[[package]] [[package]]
name = "fontdb" name = "fontdb"
version = "0.16.0" version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98b88c54a38407f7352dd2c4238830115a6377741098ffd1f997c813d0e088a6" checksum = "3890d0893c8253d3eb98337af18b3e1a10a9b2958f2a164b53a93fb3a3049e72"
dependencies = [ dependencies = [
"fontconfig-parser", "fontconfig-parser",
"log", "log",
@ -2238,7 +2246,7 @@ dependencies = [
[[package]] [[package]]
name = "iced" name = "iced"
version = "0.12.0" 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 = [ dependencies = [
"iced_accessibility", "iced_accessibility",
"iced_core", "iced_core",
@ -2253,7 +2261,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_accessibility" name = "iced_accessibility"
version = "0.1.0" 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 = [ dependencies = [
"accesskit", "accesskit",
"accesskit_winit", "accesskit_winit",
@ -2262,7 +2270,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_core" name = "iced_core"
version = "0.12.0" 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 = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"log", "log",
@ -2279,7 +2287,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_futures" name = "iced_futures"
version = "0.12.0" 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 = [ dependencies = [
"futures", "futures",
"iced_core", "iced_core",
@ -2292,7 +2300,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_graphics" name = "iced_graphics"
version = "0.12.0" 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 = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"bytemuck", "bytemuck",
@ -2316,7 +2324,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_renderer" name = "iced_renderer"
version = "0.12.0" 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 = [ dependencies = [
"iced_graphics", "iced_graphics",
"iced_tiny_skia", "iced_tiny_skia",
@ -2328,7 +2336,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_runtime" name = "iced_runtime"
version = "0.12.0" 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 = [ dependencies = [
"iced_core", "iced_core",
"iced_futures", "iced_futures",
@ -2338,7 +2346,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_style" name = "iced_style"
version = "0.12.0" 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 = [ dependencies = [
"iced_core", "iced_core",
"once_cell", "once_cell",
@ -2348,7 +2356,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_tiny_skia" name = "iced_tiny_skia"
version = "0.12.0" 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 = [ dependencies = [
"bytemuck", "bytemuck",
"cosmic-text", "cosmic-text",
@ -2365,7 +2373,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_wgpu" name = "iced_wgpu"
version = "0.12.0" 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 = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"bytemuck", "bytemuck",
@ -2384,7 +2392,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_widget" name = "iced_widget"
version = "0.12.0" 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 = [ dependencies = [
"iced_renderer", "iced_renderer",
"iced_runtime", "iced_runtime",
@ -2398,7 +2406,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_winit" name = "iced_winit"
version = "0.12.0" 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 = [ dependencies = [
"iced_graphics", "iced_graphics",
"iced_runtime", "iced_runtime",
@ -2684,6 +2692,15 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" 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]] [[package]]
name = "libc" name = "libc"
version = "0.2.151" version = "0.2.151"
@ -2692,7 +2709,7 @@ source = "git+https://gitlab.redox-os.org/redox-os/liblibc.git?branch=redox_0.2.
[[package]] [[package]]
name = "libcosmic" name = "libcosmic"
version = "0.1.0" 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 = [ dependencies = [
"apply", "apply",
"ashpd", "ashpd",
@ -4095,6 +4112,12 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.17" version = "0.3.17"

View file

@ -11,8 +11,10 @@ alacritty_terminal = "0.20"
env_logger = "0.10" env_logger = "0.10"
lazy_static = "1" lazy_static = "1"
indexmap = "2" indexmap = "2"
lexical-sort = "0.3.1"
log = "0.4" log = "0.4"
serde = { version = "1", features = ["serde_derive"] } serde = { version = "1", features = ["serde_derive"] }
shlex = "1"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
# Internationalization # Internationalization
i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] } i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }

View file

@ -1,5 +1,15 @@
# Context Pages # 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 = Settings settings = Settings
@ -44,6 +54,8 @@ find-next = Find next
file = File file = File
new-tab = New tab new-tab = New tab
new-window = New window new-window = New window
profile = Profile
menu-profiles = Profiles...
close-tab = Close tab close-tab = Close tab
quit = Quit quit = Quit

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4.98999V13.99C2 14.514 2.476 14.99 3 14.99H12C12.524 14.99 13 14.514 13 13.99V4.98999H2Z" fill="#232323"/>
<path d="M1 2.99001V3.99001L14 3.98701V2.99001C14 1.99001 13 1.98701 13 1.98701H10C10 1.98701 10 0.987 9 0.987H6C5 0.987 5 1.98701 5 1.98701H2C2 1.98701 1 1.99001 1 2.99001Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 414 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 3C7.73478 3 7.48043 3.10536 7.29289 3.29289C7.10536 3.48043 7 3.73478 7 4V6.996L4 7C3.73478 7 3.48043 7.10536 3.29289 7.29289C3.10536 7.48043 3 7.73478 3 8C3 8.26522 3.10536 8.51957 3.29289 8.70711C3.48043 8.89464 3.73478 9 4 9L7 8.996V12C7 12.2652 7.10536 12.5196 7.29289 12.7071C7.48043 12.8946 7.73478 13 8 13C8.26522 13 8.51957 12.8946 8.70711 12.7071C8.89464 12.5196 9 12.2652 9 12V8.996L12 9C12.2652 9 12.5196 8.89464 12.7071 8.70711C12.8946 8.51957 13 8.26522 13 8C13 7.73478 12.8946 7.48043 12.7071 7.29289C12.5196 7.10536 12.2652 7 12 7L9 6.996V4C9 3.73478 8.89464 3.48043 8.70711 3.29289C8.51957 3.10536 8.26522 3 8 3Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 762 B

View file

@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::OnceLock; use std::sync::OnceLock;
use crate::fl;
pub const CONFIG_VERSION: u64 = 1; pub const CONFIG_VERSION: u64 = 1;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[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)] #[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Config { pub struct Config {
pub app_theme: AppTheme, pub app_theme: AppTheme,
@ -39,6 +70,7 @@ pub struct Config {
pub bold_font_weight: u16, pub bold_font_weight: u16,
pub font_stretch: u16, pub font_stretch: u16,
pub font_size_zoom_step_mul_100: u16, pub font_size_zoom_step_mul_100: u16,
pub profiles: BTreeMap<ProfileId, Profile>,
pub show_headerbar: bool, pub show_headerbar: bool,
pub use_bright_bold: bool, pub use_bright_bold: bool,
pub syntax_theme_dark: String, pub syntax_theme_dark: String,
@ -50,18 +82,19 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
app_theme: AppTheme::System, 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_name: "Fira Mono".to_string(),
font_size: 14, 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_size_zoom_step_mul_100: 100,
font_stretch: Stretch::Normal.to_number(),
font_weight: Weight::NORMAL.0,
profiles: BTreeMap::new(),
show_headerbar: true, show_headerbar: true,
use_bright_bold: false,
syntax_theme_dark: "COSMIC Dark".to_string(), syntax_theme_dark: "COSMIC Dark".to_string(),
syntax_theme_light: "COSMIC Light".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) 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 // Get current syntax theme based on dark mode
pub fn syntax_theme(&self) -> &str { pub fn syntax_theme(&self, profile_id_opt: Option<ProfileId>) -> &str {
let dark = self.app_theme.theme().theme_type.is_dark(); let dark = self.app_theme.theme().theme_type.is_dark();
if dark { match profile_id_opt.and_then(|profile_id| self.profiles.get(&profile_id)) {
&self.syntax_theme_dark Some(profile) => {
} else { if dark {
&self.syntax_theme_light &profile.syntax_theme_dark
} else {
&profile.syntax_theme_light
}
}
None => {
if dark {
&self.syntax_theme_dark
} else {
&self.syntax_theme_light
}
}
} }
} }

View file

@ -31,6 +31,8 @@ impl IconCache {
} }
bundle!("edit-clear-symbolic", 16); bundle!("edit-clear-symbolic", 16);
bundle!("edit-delete-symbolic", 16);
bundle!("list-add-symbolic", 16);
bundle!("go-down-symbolic", 16); bundle!("go-down-symbolic", 16);
bundle!("go-up-symbolic", 16); bundle!("go-up-symbolic", 16);
bundle!("window-close-symbolic", 16); bundle!("window-close-symbolic", 16);

View file

@ -6,7 +6,7 @@ use alacritty_terminal::{
}; };
use cosmic::{ use cosmic::{
app::{message, Command, Core, Settings}, app::{message, Command, Core, Settings},
cosmic_config::{self, CosmicConfigEntry}, cosmic_config::{self, ConfigSet, CosmicConfigEntry},
cosmic_theme, executor, cosmic_theme, executor,
iced::{ iced::{
advanced::graphics::text::font_system, advanced::graphics::text::font_system,
@ -29,7 +29,7 @@ use std::{
}; };
use tokio::sync::mpsc; use tokio::sync::mpsc;
use config::{AppTheme, Config, CONFIG_VERSION}; use config::{AppTheme, Config, Profile, ProfileId, CONFIG_VERSION};
mod config; mod config;
mod mouse_reporter; mod mouse_reporter;
@ -175,6 +175,8 @@ pub enum Action {
PaneSplitVertical, PaneSplitVertical,
PaneToggleMaximized, PaneToggleMaximized,
Paste, Paste,
ProfileOpen(ProfileId),
Profiles,
SelectAll, SelectAll,
Settings, Settings,
ShowHeaderBar(bool), ShowHeaderBar(bool),
@ -211,6 +213,8 @@ impl Action {
Action::PaneSplitVertical => Message::PaneSplit(pane_grid::Axis::Vertical), Action::PaneSplitVertical => Message::PaneSplit(pane_grid::Axis::Vertical),
Action::PaneToggleMaximized => Message::PaneToggleMaximized, Action::PaneToggleMaximized => Message::PaneToggleMaximized,
Action::Paste => Message::Paste(entity_opt), 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::SelectAll => Message::SelectAll(entity_opt),
Action::Settings => Message::ToggleContextPage(ContextPage::Settings), Action::Settings => Message::ToggleContextPage(ContextPage::Settings),
Action::ShowHeaderBar(show_headerbar) => Message::ShowHeaderBar(show_headerbar), Action::ShowHeaderBar(show_headerbar) => Message::ShowHeaderBar(show_headerbar),
@ -264,6 +268,15 @@ pub enum Message {
MouseEnter(pane_grid::Pane), MouseEnter(pane_grid::Pane),
Paste(Option<segmented_button::Entity>), Paste(Option<segmented_button::Entity>),
PasteValue(Option<segmented_button::Entity>, String), PasteValue(Option<segmented_button::Entity>, 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<segmented_button::Entity>), SelectAll(Option<segmented_button::Entity>),
UseBrightBold(bool), UseBrightBold(bool),
ShowHeaderBar(bool), ShowHeaderBar(bool),
@ -291,12 +304,14 @@ pub enum Message {
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ContextPage { pub enum ContextPage {
Profiles,
Settings, Settings,
} }
impl ContextPage { impl ContextPage {
fn title(&self) -> String { fn title(&self) -> String {
match self { match self {
Self::Profiles => fl!("profiles"),
Self::Settings => fl!("settings"), Self::Settings => fl!("settings"),
} }
} }
@ -333,6 +348,7 @@ pub struct App {
term_event_tx_opt: Option<mpsc::Sender<(pane_grid::Pane, segmented_button::Entity, TermEvent)>>, term_event_tx_opt: Option<mpsc::Sender<(pane_grid::Pane, segmented_button::Entity, TermEvent)>>,
startup_options: Option<tty::Options>, startup_options: Option<tty::Options>,
term_config: TermConfig, term_config: TermConfig,
profile_expanded: Option<ProfileId>,
show_advanced_font_settings: bool, show_advanced_font_settings: bool,
modifiers: Modifiers, modifiers: Modifiers,
} }
@ -367,6 +383,19 @@ impl App {
self.update_config() self.update_config()
} }
fn save_profiles(&mut self) -> Command<Message> {
// 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<Message> { fn update_focus(&self) -> Command<Message> {
if self.find { if self.find {
widget::text_input::focus(self.find_search_id.clone()) widget::text_input::focus(self.find_search_id.clone())
@ -476,6 +505,147 @@ impl App {
} }
} }
fn profiles(&self) -> Element<Message> {
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<Message> { fn settings(&self) -> Element<Message> {
let app_theme_selected = match self.config.app_theme { let app_theme_selected = match self.config.app_theme {
AppTheme::Dark => 1, AppTheme::Dark => 1,
@ -658,48 +828,85 @@ impl App {
.into() .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<ProfileId>,
) -> Command<Message> {
self.pane_model.focus = pane; self.pane_model.focus = pane;
match &self.term_event_tx_opt { match &self.term_event_tx_opt {
Some(term_event_tx) => match self.themes.get(self.config.syntax_theme()) { Some(term_event_tx) => {
Some(colors) => { match self.themes.get(self.config.syntax_theme(profile_id_opt)) {
let current_pane = self.pane_model.focus; Some(colors) => {
if let Some(tab_model) = self.pane_model.active_mut() { let current_pane = self.pane_model.focus;
let entity = tab_model if let Some(tab_model) = self.pane_model.active_mut() {
.insert() let entity = tab_model
.text("New Terminal") .insert()
.closable() .text("New Terminal")
.activate() .closable()
.id(); .activate()
// Use the startup options, or defaults .id();
let options = self.startup_options.take().unwrap_or_default(); // Use the profile options, startup options, or defaults
let mut terminal = Terminal::new( let options = match profile_id_opt
current_pane, .and_then(|profile_id| self.config.profiles.get(&profile_id))
entity, {
term_event_tx.clone(), Some(profile) => {
self.term_config.clone(), let mut shell = None;
options, if let Some(mut args) = shlex::split(&profile.command) {
&self.config, if !args.is_empty() {
*colors, 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::<Mutex<Terminal>>(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); //TODO: fall back to known good theme
tab_model.data_set::<Mutex<Terminal>>(entity, Mutex::new(terminal));
} else {
log::error!("Found no active pane");
} }
} }
None => { }
log::error!(
"failed to find terminal theme {:?}",
self.config.syntax_theme()
);
//TODO: fall back to known good theme
}
},
None => { None => {
log::warn!("tried to create new tab before having event channel"); 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, startup_options: flags.startup_options,
term_config: flags.term_config, term_config: flags.term_config,
term_event_tx_opt: None, term_event_tx_opt: None,
profile_expanded: None,
show_advanced_font_settings: false, show_advanced_font_settings: false,
modifiers: Modifiers::empty(), modifiers: Modifiers::empty(),
}; };
@ -1096,9 +1304,9 @@ impl Application for App {
); );
if let Some((pane, _)) = result { if let Some((pane, _)) = result {
self.terminal_ids.insert(pane, widget::Id::unique()); 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; self.pane_model.panes_created += 1;
return self.update_title(Some(pane)); return command;
} }
} }
Message::PaneToggleMaximized => { Message::PaneToggleMaximized => {
@ -1142,6 +1350,77 @@ impl Application for App {
} }
return self.update_focus(); 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::<Mutex<Terminal>>(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) => { Message::SelectAll(entity_opt) => {
if let Some(tab_model) = self.pane_model.active() { if let Some(tab_model) = self.pane_model.active() {
let entity = entity_opt.unwrap_or_else(|| tab_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; self.pane_model.focus = pane;
return self.update_title(Some(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 => { Message::TabNext => {
if let Some(tab_model) = self.pane_model.active() { if let Some(tab_model) = self.pane_model.active() {
let len = tab_model.iter().count(); let len = tab_model.iter().count();
@ -1449,17 +1730,18 @@ impl Application for App {
} }
Some(match self.context_page { Some(match self.context_page {
ContextPage::Profiles => self.profiles(),
ContextPage::Settings => self.settings(), ContextPage::Settings => self.settings(),
}) })
} }
fn header_start(&self) -> Vec<Element<Self::Message>> { fn header_start(&self) -> Vec<Element<Self::Message>> {
vec![menu_bar(&self.key_binds)] vec![menu_bar(&self.config, &self.key_binds)]
} }
fn header_end(&self) -> Vec<Element<Self::Message>> { fn header_end(&self) -> Vec<Element<Self::Message>> {
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing; 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) .on_press(Message::TabNew)
.padding(space_xxs) .padding(space_xxs)
.style(style::Button::Icon) .style(style::Button::Icon)

View file

@ -108,7 +108,7 @@ pub fn context_menu<'a>(
.into() .into()
} }
pub fn menu_bar<'a>(key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> { pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> {
//TODO: port to libcosmic //TODO: port to libcosmic
let menu_root = |label| { let menu_root = |label| {
widget::button(widget::text(label)) widget::button(widget::text(label))
@ -116,6 +116,9 @@ pub fn menu_bar<'a>(key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message
.style(theme::Button::MenuRoot) .style(theme::Button::MenuRoot)
}; };
let menu_folder =
|label| menu_button!(widget::text(label), horizontal_space(Length::Fill), ">");
let find_key = |action: &Action| -> String { let find_key = |action: &Action| -> String {
for (key_bind, key_action) in key_binds.iter() { for (key_bind, key_action) in key_binds.iter() {
if action == key_action { if action == key_action {
@ -137,6 +140,12 @@ pub fn menu_bar<'a>(key_binds: &HashMap<KeyBind, Action>) -> 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![ MenuBar::new(vec![
MenuTree::with_children( MenuTree::with_children(
menu_root(fl!("file")), menu_root(fl!("file")),
@ -144,6 +153,9 @@ pub fn menu_bar<'a>(key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message
menu_item(fl!("new-tab"), Action::TabNew), menu_item(fl!("new-tab"), Action::TabNew),
menu_item(fl!("new-window"), Action::WindowNew), menu_item(fl!("new-window"), Action::WindowNew),
MenuTree::new(horizontal_rule(1)), 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), menu_item(fl!("close-tab"), Action::TabClose),
MenuTree::new(horizontal_rule(1)), MenuTree::new(horizontal_rule(1)),
menu_item(fl!("quit"), Action::WindowClose), menu_item(fl!("quit"), Action::WindowClose),

View file

@ -27,7 +27,7 @@ use indexmap::IndexSet;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::HashMap, collections::HashMap,
mem, io, mem,
sync::{ sync::{
atomic::{AtomicU32, Ordering}, atomic::{AtomicU32, Ordering},
Arc, Weak, Arc, Weak,
@ -38,7 +38,10 @@ use tokio::sync::mpsc;
pub use alacritty_terminal::grid::Scroll as TerminalScroll; 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)] #[derive(Clone, Copy, Debug)]
pub struct Size { pub struct Size {
@ -187,21 +190,22 @@ impl Metadata {
} }
pub struct Terminal { pub struct Terminal {
default_attrs: Attrs<'static>,
buffer: Arc<Buffer>,
size: Size,
pub term: Arc<FairMutex<Term<EventProxy>>>,
colors: Colors,
dim_font_weight: Weight,
bold_font_weight: Weight,
use_bright_bold: bool,
notifier: Notifier,
pub context_menu: Option<cosmic::iced::Point>, pub context_menu: Option<cosmic::iced::Point>,
pub metadata_set: IndexSet<Metadata>,
pub needs_update: bool, pub needs_update: bool,
pub profile_id_opt: Option<ProfileId>,
pub term: Arc<FairMutex<Term<EventProxy>>>,
bold_font_weight: Weight,
buffer: Arc<Buffer>,
colors: Colors,
default_attrs: Attrs<'static>,
dim_font_weight: Weight,
mouse_reporter: MouseReporter,
notifier: Notifier,
search_regex_opt: Option<RegexSearch>, search_regex_opt: Option<RegexSearch>,
search_value: String, search_value: String,
pub metadata_set: IndexSet<Metadata>, size: Size,
mouse_reporter: MouseReporter, use_bright_bold: bool,
} }
impl Terminal { impl Terminal {
@ -214,7 +218,8 @@ impl Terminal {
options: Options, options: Options,
app_config: &AppConfig, app_config: &AppConfig,
colors: Colors, colors: Colors,
) -> Self { profile_id_opt: Option<ProfileId>,
) -> Result<Self, io::Error> {
let font_stretch = app_config.typed_font_stretch(); let font_stretch = app_config.typed_font_stretch();
let font_weight = app_config.font_weight; let font_weight = app_config.font_weight;
let dim_font_weight = app_config.dim_font_weight; let dim_font_weight = app_config.dim_font_weight;
@ -267,29 +272,30 @@ impl Terminal {
))); )));
let window_id = 0; 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 pty_event_loop = EventLoop::new(term.clone(), event_proxy, pty, options.hold, false);
let notifier = Notifier(pty_event_loop.channel()); let notifier = Notifier(pty_event_loop.channel());
let _pty_join_handle = pty_event_loop.spawn(); let _pty_join_handle = pty_event_loop.spawn();
Self { Ok(Self {
colors,
dim_font_weight: Weight(dim_font_weight),
bold_font_weight: Weight(bold_font_weight), bold_font_weight: Weight(bold_font_weight),
use_bright_bold,
default_attrs,
buffer: Arc::new(buffer), buffer: Arc::new(buffer),
size, colors,
term,
notifier,
context_menu: None, context_menu: None,
needs_update: true, default_attrs,
search_regex_opt: None, dim_font_weight: Weight(dim_font_weight),
search_value: String::new(),
metadata_set, metadata_set,
mouse_reporter: Default::default(), 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<Buffer> { pub fn buffer_weak(&self) -> Weak<Buffer> {
@ -550,7 +556,7 @@ impl Terminal {
update_cell_size = true; 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; let mut changed = false;
for i in 0..color::COUNT { for i in 0..color::COUNT {
if self.colors[i] != colors[i] { if self.colors[i] != colors[i] {