From 6fa65432d31f33e4682b7ed0834d1fb986ee993e Mon Sep 17 00:00:00 2001 From: SquitchYT Date: Sun, 4 Feb 2024 00:30:00 +0100 Subject: [PATCH 1/9] :sparkles: Add title formatting :sparkles: Add progress tracking :sparkles: Add unread content mark :bug: Fix terminal overloading --- src-tauri/Cargo.lock | 104 +++++++++-- src-tauri/Cargo.toml | 10 +- src-tauri/src/common/commands/pty.rs | 68 +++++--- src-tauri/src/common/consts.rs | 1 + src-tauri/src/common/mod.rs | 2 + src-tauri/src/common/payloads.rs | 6 + src-tauri/src/common/title_formatter.rs | 184 ++++++++++++++++++++ src-tauri/src/configuration/deserialized.rs | 68 ++++---- src-tauri/src/configuration/partial.rs | 15 +- src-tauri/src/configuration/types.rs | 2 +- src-tauri/src/main.rs | 33 ++-- src-tauri/src/pty/pty.rs | 171 +++++++++++++++--- src-tauri/src/pty/utils.rs | 65 ++++--- src-tauri/tauri.conf.json | 8 +- src/style/animation.scss | 14 ++ src/style/constants.scss | 1 + src/style/tab.scss | 21 ++- src/ts/app.ts | 16 +- src/ts/class/panes.ts | 4 +- src/ts/class/tab.ts | 8 + src/ts/class/terminal.ts | 28 ++- src/ts/class/views.ts | 24 ++- src/ts/manager/tabs.ts | 19 ++ src/ts/schema/option.ts | 3 +- src/ts/schema/term.ts | 5 + 25 files changed, 706 insertions(+), 174 deletions(-) create mode 100644 src-tauri/src/common/consts.rs create mode 100644 src-tauri/src/common/title_formatter.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 96c9336..1de9e5a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -47,6 +47,12 @@ version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "atk" version = "0.15.1" @@ -1073,7 +1079,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.4", + "itoa 1.0.10", ] [[package]] @@ -1193,9 +1199,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "javascriptcore-rs" @@ -1353,7 +1359,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -1364,9 +1370,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" @@ -1553,9 +1559,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "overload" @@ -1979,13 +1985,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.4" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick 1.0.2", "memchr", - "regex-syntax 0.7.2", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -1997,6 +2004,23 @@ dependencies = [ "regex-syntax 0.6.28", ] +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick 1.0.2", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-lite" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" + [[package]] name = "regex-syntax" version = "0.6.28" @@ -2005,9 +2029,9 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "remove_dir_all" @@ -2148,7 +2172,7 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ - "itoa 1.0.4", + "itoa 1.0.10", "ryu", "serde", ] @@ -2742,14 +2766,14 @@ dependencies = [ [[package]] name = "tess" -version = "0.7.0-alpha.5" +version = "0.7.0-alpha.9" dependencies = [ "dirs-next", "futures", "infer 0.12.0", "lazy_static", "portable-pty", - "regex", + "regex-lite", "serde", "serde_json", "signal-hook", @@ -2759,6 +2783,7 @@ dependencies = [ "thiserror", "tokio", "uuid 1.3.0", + "vt100", "window-vibrancy", "windows 0.52.0", ] @@ -2804,7 +2829,7 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ - "itoa 1.0.4", + "itoa 1.0.10", "serde", "time-core", "time-macros", @@ -2990,6 +3015,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "url" version = "2.3.1" @@ -3008,6 +3039,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "0.8.2" @@ -3047,6 +3084,39 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vt100" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de" +dependencies = [ + "itoa 1.0.10", + "log", + "unicode-width", + "vte", +] + +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "walkdir" version = "2.3.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ee95295..0d2478c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tess" -version = "0.7.0-alpha.5" +version = "0.7.0-alpha.9" description = "Fast. Lightweight. Famous. Terminal for the new era." authors = ["Squitch"] repository = "https://github.com/SquitchYT/Tess" @@ -23,15 +23,17 @@ tokio = { version = "1.25.0", features = ["full"] } dirs-next = "2.0.0" uuid = "1.3.0" thiserror = "1.0.50" +lazy_static = "1.4.0" +regex-lite = "0.1.5" +vt100 = "0.15.2" +futures = "0.3.29" + [target.'cfg(unix)'.dependencies] signal-hook = "0.3.17" signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } -futures = "0.3.29" [target.'cfg(windows)'.dependencies] -lazy_static = "1.4.0" -regex = "1.8.4" window-vibrancy = "0.3.2" windows = { version = "0.52.0", features = ["Win32_Foundation", "Win32_System", "Win32_System_Threading", "Win32_System_ProcessStatus", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Diagnostics"] } diff --git a/src-tauri/src/common/commands/pty.rs b/src-tauri/src/common/commands/pty.rs index e941b2a..32b85ec 100644 --- a/src-tauri/src/common/commands/pty.rs +++ b/src-tauri/src/common/commands/pty.rs @@ -1,4 +1,4 @@ -use crate::common::payloads::{PtySendData, PtyTitleChanged}; +use crate::common::payloads::{PtyProgressUpdated, PtySendData, PtyTitleChanged}; use crate::common::states::Ptys; use crate::configuration::deserialized::Option; use std::sync::Arc; @@ -17,11 +17,14 @@ pub async fn pty_open( option: tauri::State<'_, Arc>>, ptys: tauri::State<'_, Ptys>, ) -> Result<(), PtyError> { - let id_cloned = id.clone(); - let id_cloned_twice = id.clone(); - let id_cloned_thrice = id.clone(); - let app_cloned = app.clone(); - let app_cloned_twice = app.clone(); + let id_title_update = id.clone(); + let id_progress_update = id.clone(); + let id_displayed_content_update = id.clone(); + let id_pty_closed = id.clone(); + let app_title_update = app.clone(); + let app_progress_update = app.clone(); + let app_displayed_content_update = app.clone(); + let app_pty_closed = app.clone(); let locked_option = option.lock().await; let opening_profile = locked_option @@ -30,42 +33,55 @@ pub async fn pty_open( .find(|profile| profile.uuid == profile_uuid) .ok_or(PtyError::UnknownPty)?; - let should_update_title = opening_profile.terminal_options.title_is_running_process; - ptys.0.lock().await.insert( id.clone(), Pty::build_and_run( &opening_profile.command, + opening_profile.title_format.clone(), + opening_profile.terminal_options.progress_tracking, move |readed| { app.emit_all( "js_pty_data", PtySendData { data: readed, - id: &id_cloned, + id: &id, }, ) .ok(); }, move |tab_title| { - if should_update_title { - app_cloned - .emit_all( - "js_pty_title_update", - PtyTitleChanged { - id: &id_cloned_twice, - title: tab_title, - }, - ) - .ok(); - } + app_title_update + .emit_all( + "js_pty_title_update", + PtyTitleChanged { + id: &id_title_update, + title: tab_title, + }, + ) + .ok(); + }, + move |progress| { + app_progress_update + .emit_all( + "js_pty_progress_update", + PtyProgressUpdated { + id: &id_progress_update, + progress, + }, + ) + .ok(); + }, + move || { + app_displayed_content_update + .emit_all("js_pty_display_content_update", &id_pty_closed) + .ok(); }, move || { - app_cloned_twice - .emit_all("js_pty_closed", id_cloned_thrice) + app_pty_closed + .emit_all("js_pty_closed", id_displayed_content_update) .ok(); }, - ) - .await?, + )?, ); Ok(()) @@ -128,7 +144,7 @@ pub async fn pty_get_closable( || app_config .close_confirmation .excluded_process - .contains(&*pty.title.lock().await)) + .contains(&*pty.leader_name.lock().await)) } else { Ok(true) } @@ -142,7 +158,7 @@ pub async fn pty_get_title(ptys: tauri::State<'_, Ptys>, id: String) -> Result { pub id: &'a str, pub title: &'a str, } + +#[derive(serde::Serialize, Clone)] +pub struct PtyProgressUpdated<'a> { + pub id: &'a str, + pub progress: u8, +} diff --git a/src-tauri/src/common/title_formatter.rs b/src-tauri/src/common/title_formatter.rs new file mode 100644 index 0000000..9e1adf4 --- /dev/null +++ b/src-tauri/src/common/title_formatter.rs @@ -0,0 +1,184 @@ +#[derive(Debug, Clone)] +enum TitlePart { + Static(String), + Dynamic([String; 3]), +} +#[derive(Debug, Default, Clone, Copy)] +pub struct FormatterOptions { + pub pwd: bool, + pub leader_process: bool, + pub action_progress: bool, + pub shell_title: bool, +} +#[derive(Debug, Default, Clone)] +pub struct FormatterParams { + pub pwd: Option, + pub leader_process: Option, + pub progress: Option, + pub shell_title: Option, +} + +#[derive(Debug, Clone)] +pub struct Formatter { + parts: Vec, + pub options: FormatterOptions, +} + +impl Formatter { + #[must_use] + pub fn new(title: &str, profile_name: &str) -> Self { + let mut parts = Vec::new(); + let mut current_static_part = String::new(); + let mut current_placeholder_parts: [String; 3] = Default::default(); + let mut current_placeholder_part = 0; + let mut in_placeholder = false; + let mut escaped = false; + let mut format_option = FormatterOptions::default(); + + for char in title.chars() { + if escaped { + escaped = false; + if in_placeholder { + current_placeholder_parts[current_placeholder_part].push(char); + } else { + current_static_part.push(char); + } + } else { + match char { + '\\' => { + escaped = true; + } + '%' => { + if in_placeholder { + match current_placeholder_parts[1].clone().as_str() { + "profile_name" => { + current_static_part.push_str(¤t_placeholder_parts[0]); + current_static_part.push_str(profile_name); + current_static_part.push_str(¤t_placeholder_parts[2]); + + parts.push(TitlePart::Static(std::mem::take( + &mut current_static_part, + ))); + + current_placeholder_parts = Default::default(); + } + placeholder @ ("pwd" | "leader_process" | "action_progress" | "shell_title") => { + parts.push(TitlePart::Static(std::mem::take( + &mut current_static_part, + ))); + parts.push(TitlePart::Dynamic(std::mem::take( + &mut current_placeholder_parts, + ))); + + match placeholder { + "pwd" => format_option.pwd = true, + "leader_process" => format_option.leader_process = true, + "action_progress" => format_option.action_progress = true, + "shell_title" => format_option.shell_title = true, + _ => unreachable!(), + } + } + _ => { + current_static_part.push('%'); + current_static_part + .push_str(¤t_placeholder_parts.join("|")); + current_static_part.push('%'); + + parts.push(TitlePart::Static(std::mem::take( + &mut current_static_part, + ))); + + current_placeholder_parts = Default::default(); + } + } + + current_placeholder_part = 0; + } + + in_placeholder = !in_placeholder; + } + '|' if in_placeholder && current_placeholder_part < 2 => { + current_placeholder_part += 1; + } + char => { + if in_placeholder { + current_placeholder_parts[current_placeholder_part].push(char); + } else { + current_static_part.push(char); + } + } + } + } + } + + if in_placeholder { + parts.push(TitlePart::Static(format!( + "{}%{}", + current_static_part, + current_placeholder_parts[0..=current_placeholder_part].join("|") + ))); + } else if !current_static_part.is_empty() { + parts.push(TitlePart::Static(current_static_part)); + } + + Self { + parts, + options: format_option, + } + } + + #[must_use] + pub fn format(&self, params: &FormatterParams) -> String { + self.parts + .iter() + .map(|part| match part { + TitlePart::Static(content) => content.to_owned(), + TitlePart::Dynamic(content) => { + let mut output = String::new(); + match (content[1].as_str(), params) { + ( + "pwd", + FormatterParams { + pwd: Some(value), .. + }, + ) + | ( + "leader_process", + FormatterParams { + leader_process: Some(value), + .. + }, + ) + | ( + "shell_title", + FormatterParams { + shell_title: Some(value), + .. + }, + ) => { + output.push_str(&content[0]); + output.push_str(&value); + output.push_str(&content[2]); + + output + } + ( + "action_progress", + FormatterParams { + progress: Some(value), + .. + }, + ) => { + output.push_str(&content[0]); + output.push_str(&value.to_string()); + output.push_str(&content[2]); + + output + } + _ => output, + } + } + }) + .collect::() + } +} diff --git a/src-tauri/src/configuration/deserialized.rs b/src-tauri/src/configuration/deserialized.rs index 72b6f10..eba1cca 100644 --- a/src-tauri/src/configuration/deserialized.rs +++ b/src-tauri/src/configuration/deserialized.rs @@ -1,3 +1,4 @@ +use crate::common::title_formatter::Formatter; use crate::configuration::partial::PartialOption; use crate::configuration::types::CursorType; use crate::configuration::types::RangedInt; @@ -7,8 +8,10 @@ use serde::{ser::SerializeSeq, Serialize, Serializer}; use crate::common::utils::parse_theme; +use super::partial::default_title_format; + #[derive(Debug, Serialize, Clone)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all(serialize = "camelCase"))] pub struct Option { pub app_theme: String, pub terminal_theme: TerminalTheme, @@ -35,12 +38,12 @@ impl Default for Option { terminal_theme: TerminalTheme::default(), background: BackgroundType::default(), custom_titlebar: true, // TODO: Set false on linux - profiles: vec![default_profile(uuid.clone())], + profiles: vec![default_profile(uuid.clone(), &default_title_format())], terminal: TerminalOption::default(), background_transparency: RangedInt::default(), shortcuts: default_shortcuts(), macros: Vec::default(), - default_profile: default_profile(uuid), + default_profile: default_profile(uuid, &default_title_format()), close_confirmation: CloseConfirmation::default(), @@ -51,7 +54,7 @@ impl Default for Option { impl<'de> serde::Deserialize<'de> for Option { fn deserialize>(deserializer: D) -> Result { - let partial_option = PartialOption::deserialize(deserializer).unwrap_or_default(); + let partial_option = PartialOption::deserialize(deserializer)?; let (app_theme, terminal_theme) = parse_theme(&partial_option.theme); let app_theme = app_theme.unwrap_or_default(); @@ -60,20 +63,10 @@ impl<'de> serde::Deserialize<'de> for Option { let mut profiles = vec![]; if partial_option.profiles.is_empty() { - profiles.push(Profile { - name: String::from("Default profile"), - terminal_options: partial_option.terminal.clone(), - theme: terminal_theme.clone(), - background_transparency: partial_option.background_transparency, - uuid: uuid::Uuid::new_v4().to_string(), - #[cfg(target_family = "unix")] - command: String::from("sh -c $SHELL"), - #[cfg(target_os = "windows")] - command: String::from( - "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", - ), - background: None, - }); + profiles.push(default_profile( + uuid::Uuid::new_v4().to_string(), + &partial_option.title_format, + )); } else { for partial_profile in partial_option.profiles { let profile_option = TerminalOption { @@ -99,9 +92,9 @@ impl<'de> serde::Deserialize<'de> for Option { draw_bold_in_bright: partial_profile .draw_bold_in_bright .unwrap_or(partial_option.terminal.draw_bold_in_bright), - show_unread_data_indicator: partial_profile - .show_unread_data_indicator - .unwrap_or(partial_option.terminal.show_unread_data_indicator), + show_unread_data_mark: partial_profile + .show_unread_data_mark + .unwrap_or(partial_option.terminal.show_unread_data_mark), line_height: partial_profile .line_height .unwrap_or(partial_option.terminal.line_height), @@ -114,9 +107,9 @@ impl<'de> serde::Deserialize<'de> for Option { font_weight_bold: partial_profile .font_weight_bold .unwrap_or(partial_option.terminal.font_weight_bold), - title_is_running_process: partial_profile - .title_is_running_process - .unwrap_or(partial_option.terminal.title_is_running_process), + progress_tracking: partial_profile + .progress_tracking + .unwrap_or(partial_option.terminal.progress_tracking), }; let profile_theme = partial_profile.theme.map_or_else( @@ -129,6 +122,12 @@ impl<'de> serde::Deserialize<'de> for Option { ); profiles.push(Profile { + title_format: Formatter::new( + &partial_profile + .title_format + .unwrap_or_else(|| partial_option.title_format.clone()), + &partial_profile.name, + ), name: partial_profile.name, terminal_options: profile_option, theme: profile_theme, @@ -221,7 +220,7 @@ impl<'de> serde::Deserialize<'de> for Option { } #[derive(Deserialize, Debug, Serialize, Clone)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all(serialize = "camelCase"))] pub struct TerminalOption { #[serde(default)] buffer_size: RangedInt<500, 5000, 3000>, @@ -240,7 +239,7 @@ pub struct TerminalOption { #[serde(default)] draw_bold_in_bright: bool, #[serde(default = "default_to_true")] - show_unread_data_indicator: bool, + pub show_unread_data_mark: bool, #[serde(default)] line_height: RangedInt<100, 200, 100>, #[serde(default)] @@ -249,8 +248,8 @@ pub struct TerminalOption { font_weight: RangedInt<1, 9, 4>, #[serde(default)] font_weight_bold: RangedInt<1, 9, 6>, - #[serde(default = "default_to_true")] - pub title_is_running_process: bool, + #[serde(default)] + pub progress_tracking: bool, } impl Default for TerminalOption { @@ -264,12 +263,12 @@ impl Default for TerminalOption { bell: false, cursor_blink: false, draw_bold_in_bright: false, - show_unread_data_indicator: true, + show_unread_data_mark: true, line_height: RangedInt::default(), letter_spacing: RangedInt::default(), font_weight: RangedInt::default(), font_weight_bold: RangedInt::default(), - title_is_running_process: true, + progress_tracking: false, } } } @@ -340,16 +339,18 @@ impl Serialize for ShortcutAction { } #[derive(Debug, Serialize, Clone)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all(serialize = "camelCase"))] pub struct Profile { // TODO: Add Icon - name: String, + pub name: String, pub terminal_options: TerminalOption, theme: TerminalTheme, background_transparency: RangedInt<0, 100, 100>, background: std::option::Option, pub uuid: String, pub command: String, + #[serde(skip_serializing)] + pub title_format: Formatter, } #[derive(Debug, Clone, Serialize)] @@ -619,7 +620,7 @@ const fn default_to_true() -> bool { true } -fn default_profile(uuid: String) -> Profile { +fn default_profile(uuid: String, title_format: &str) -> Profile { Profile { name: String::from("Default profile"), terminal_options: TerminalOption::default(), @@ -631,6 +632,7 @@ fn default_profile(uuid: String) -> Profile { #[cfg(target_os = "windows")] command: String::from("%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"), background: None, + title_format: Formatter::new(title_format, "Default profile"), } } diff --git a/src-tauri/src/configuration/partial.rs b/src-tauri/src/configuration/partial.rs index 933e256..1ffc0f9 100644 --- a/src-tauri/src/configuration/partial.rs +++ b/src-tauri/src/configuration/partial.rs @@ -1,3 +1,4 @@ +use crate::common::consts; use crate::configuration::deserialized::CloseConfirmation; use crate::configuration::deserialized::ShortcutAction; use crate::configuration::deserialized::TerminalOption; @@ -32,6 +33,9 @@ pub struct PartialOption { #[serde(default)] pub default_profile: String, + + #[serde(default = "default_title_format")] + pub title_format: String, } impl Default for PartialOption { @@ -47,6 +51,7 @@ impl Default for PartialOption { macros: Some(Vec::default()), default_profile: String::new(), close_confirmation: CloseConfirmation::default(), + title_format: default_title_format(), } } } @@ -66,12 +71,13 @@ pub struct PartialProfile { pub background_transparency: std::option::Option>, pub cursor_blink: std::option::Option, pub draw_bold_in_bright: std::option::Option, - pub show_unread_data_indicator: std::option::Option, + pub show_unread_data_mark: std::option::Option, pub line_height: std::option::Option>, pub letter_spacing: std::option::Option>, pub font_weight: std::option::Option>, pub font_weight_bold: std::option::Option>, - pub title_is_running_process: std::option::Option, + pub title_format: std::option::Option, + pub progress_tracking: std::option::Option, #[serde(deserialize_with = "deserialize_profile_background")] #[serde(default)] pub background: std::option::Option, @@ -109,3 +115,8 @@ where }), ) } + +#[must_use] +pub fn default_title_format() -> String { + String::from(consts::DEFAULT_PROFILE_TITLE) +} diff --git a/src-tauri/src/configuration/types.rs b/src-tauri/src/configuration/types.rs index e3cba36..94b44d7 100644 --- a/src-tauri/src/configuration/types.rs +++ b/src-tauri/src/configuration/types.rs @@ -44,7 +44,7 @@ impl serde::Serialize for Ranged } #[derive(Debug, Serialize, Clone)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all(serialize = "camelCase"))] pub enum BackgroundType { Opaque, Media(BackgroundMedia), diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 553dd7a..f5e4391 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -38,14 +38,12 @@ async fn main() { let option = Arc::from(Mutex::from(if let Ok(config_file) = config_file { let parsed_option = serde_json::from_str(&config_file); - if parsed_option.is_err() { - logger.warn(&format!( - "Malformed configuration file: {}.", - parsed_option.as_ref().err().unwrap() - )); + if let Err(err) = &parsed_option { + logger.warn(&format!("Malformed configuration file: {err}.")); } parsed_option.unwrap_or_default() } else { + logger.warn("Cannot read configuration file."); Option::default() })); @@ -144,21 +142,18 @@ async fn main() { label, event: WindowEvent::CloseRequested { api, .. }, .. - } => { - let option_cloned = option.clone(); - tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async { - if option_cloned.lock().await.close_confirmation.window { - app.get_window(&label) - .unwrap() - .emit("js_window_request_closing", "") - .ok(); - - api.prevent_close() - } - }) + } => tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + if option.lock().await.close_confirmation.window { + app.get_window(&label) + .unwrap() + .emit("js_window_request_closing", ()) + .ok(); + + api.prevent_close() + } }) - } + }), _ => (), }) } diff --git a/src-tauri/src/pty/pty.rs b/src-tauri/src/pty/pty.rs index 95fb0cb..880f517 100644 --- a/src-tauri/src/pty/pty.rs +++ b/src-tauri/src/pty/pty.rs @@ -3,20 +3,25 @@ use std::io::{Read, Write}; use std::sync::{mpsc, Arc}; use std::time::Duration; +use std::pin::Pin; + use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize}; + use tokio::sync::Mutex; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use crate::common::error::PtyError; +use futures::future::join_all; -#[cfg(target_os = "windows")] -use regex::Regex; +use regex_lite::Regex; + +use crate::common::title_formatter::{Formatter, FormatterParams}; #[cfg(target_os = "windows")] use crate::pty::utils; #[cfg(target_os = "windows")] -use regex::Captures; +use regex_lite::Captures; pub struct Pty { writer: Box, @@ -24,7 +29,7 @@ pub struct Pty { master: Arc>>, paused: Arc, - pub title: Arc>, + pub leader_name: Arc>, pub closed: Arc, } @@ -32,10 +37,14 @@ unsafe impl Send for Pty {} unsafe impl Sync for Pty {} impl Pty { - pub async fn build_and_run( + pub fn build_and_run( command: &str, + title_formatter: Formatter, + progress_report: bool, on_read: impl Fn(&str) + std::marker::Send + 'static, on_tab_title_update: impl Fn(&str) + std::marker::Send + 'static, + on_action_progress: impl Fn(u8) + Send + 'static, + on_displayed_content_updated: impl Fn() + Send + 'static, once_exit: impl FnOnce() + std::marker::Send + 'static, ) -> Result { #[cfg(target_os = "windows")] @@ -88,8 +97,8 @@ impl Pty { )); let cloned_child = child.clone(); - let title = Arc::new(Mutex::from(String::new())); - let cloned_title = title.clone(); + let leader_name = Arc::new(Mutex::from(String::new())); + let cloned_leader_process = leader_name.clone(); let closed = Arc::new(AtomicBool::new(false)); let closed_cloned = closed.clone(); @@ -101,11 +110,27 @@ impl Pty { #[cfg(target_family = "unix")] let cloned_master = master.clone(); #[cfg(target_os = "windows")] - let shell_pid = child.lock().await.process_id().ok_or(PtyError::Creation("PID not found".to_owned()))?; + let shell_pid = child + .lock() + .await + .process_id() + .ok_or(PtyError::Creation("PID not found".to_owned()))?; + + let progress_tracking = progress_report | title_formatter.options.action_progress; + let current_progress = Arc::new(AtomicU8::new(0)); + let cloned_current_progress = current_progress.clone(); + + let sync_shell_title = Arc::new(std::sync::Mutex::from(None)); + let cloned_sync_shell_title = sync_shell_title.clone(); std::thread::spawn(move || { let mut buf = [0; 4096]; let mut remaining = 0; + let mut pre_parser = vt100::Parser::new(8, 144, 0); + + lazy_static::lazy_static! { + static ref PROGRESS_PARSING_REGEX: Regex = Regex::new(r"(([0-9]*[.])?[0-9]+%)|(\d+/\d+)").unwrap(); + } loop { if !paused_cloned.load(Ordering::Relaxed) { @@ -115,12 +140,15 @@ impl Pty { .read(&mut buf[remaining..]) .is_ok_and(|bytes| bytes > 0) { + let previous_cached_content = pre_parser.screen().contents(); match std::str::from_utf8(&buf) { Ok(parsed_buf) => { + pre_parser.process(parsed_buf.as_bytes()); on_read(parsed_buf); remaining = 0; } Err(utf8) => { + pre_parser.process(&buf[..utf8.valid_up_to()]); on_read(unsafe { std::str::from_utf8_unchecked(&buf[..utf8.valid_up_to()]) }); @@ -128,6 +156,62 @@ impl Pty { buf.rotate_left(utf8.valid_up_to()); } } + + if pre_parser.screen().contents() != previous_cached_content { + on_displayed_content_updated(); + + if progress_tracking && !pre_parser.screen().alternate_screen() { + let fetched_progress = PROGRESS_PARSING_REGEX + .find_iter(&pre_parser.screen().contents()) + .last() + .map(|m| { + if m.as_str().ends_with('%') { + m.as_str() + .split_once('%') + .and_then(|(number, _)| number.parse::().ok()) + .map(|parsed_number| { + (parsed_number.ceil() as u64 % 100) as u8 + }) + .unwrap_or_default() + } else { + let splitted = + m.as_str().split_once('/').unwrap_or_default(); + + let numerator = + splitted.0.parse::().unwrap_or(0f64); + let denominator = + splitted.1.parse::().unwrap_or(1f64); + + ((((numerator / denominator) * 100f64).ceil() as u64) + % 100) + as u8 + } + }) + .unwrap_or_default(); + + if fetched_progress != current_progress.load(Ordering::Relaxed) { + current_progress.store(fetched_progress, Ordering::Relaxed); + + if progress_report { + on_action_progress(fetched_progress); + } + } + } else if current_progress.load(Ordering::Relaxed) != 0 + && progress_tracking + { + current_progress.store(0, Ordering::Relaxed); + + if progress_report { + on_action_progress(0); + } + } + } + + if title_formatter.options.shell_title { + if let Ok(mut lock) = cloned_sync_shell_title.lock() { + *lock = Some(pre_parser.screen().title().to_owned()); + } + } } } @@ -139,6 +223,9 @@ impl Pty { tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_millis(20)); + let mut current_title = String::new(); + + on_tab_title_update(&title_formatter.format(&FormatterParams::default())); loop { interval.tick().await; @@ -158,22 +245,58 @@ impl Pty { let process_leader_pid = Some(utils::get_leader_pid(shell_pid)); if let Some(fetched_leader_pid) = process_leader_pid { - let fetched_title = tokio::task::spawn_blocking(move || { - super::utils::get_process_title(fetched_leader_pid) - }) - .await - .unwrap(); + let mut fetched_leader_process = Option::None; + let mut fetched_pwd = Option::None; + let current_progress = cloned_current_progress.load(Ordering::Relaxed); + let fetched_progress = if current_progress > 0 { + Some(current_progress) + } else { + None + }; + let fetched_shell_title = if title_formatter.options.shell_title { + let sync_fetched_shell_title = sync_shell_title.clone(); + tokio::task::spawn_blocking(move || { + sync_fetched_shell_title.lock().unwrap().clone() + }) + .await + .unwrap() + .map(|title| title.to_string()) + } else { + None + }; + + let mut fetchers: Vec + Send>>> = + vec![Box::pin(super::utils::get_process_title( + fetched_leader_pid, + &mut fetched_leader_process, + ))]; + + if title_formatter.options.pwd { + fetchers.push(Box::pin(super::utils::get_process_working_dir( + fetched_leader_pid, + &mut fetched_pwd, + ))); + } - let mut current_process_title = cloned_title.lock().await; + let fetched_data_count = fetchers.len(); + join_all(fetchers).await; - if fetched_title - .as_ref() - .is_some_and(|fetched_title| fetched_title != &*current_process_title) - { - let fetched_title = fetched_title.unwrap(); + if fetched_data_count > 1 || title_formatter.options.action_progress || title_formatter.options.shell_title { + let generated_title = title_formatter.format(&FormatterParams { + pwd: fetched_pwd, + leader_process: fetched_leader_process.clone(), + progress: fetched_progress, + shell_title: fetched_shell_title, + }); + + if generated_title != current_title { + on_tab_title_update(&generated_title); + current_title = generated_title; + } + } - on_tab_title_update(&fetched_title); - *current_process_title = fetched_title; + if let Some(fetched_leader_process) = fetched_leader_process { + *cloned_leader_process.lock().await = fetched_leader_process; } } } @@ -184,7 +307,7 @@ impl Pty { child, master, paused, - title, + leader_name, closed, }) } @@ -200,12 +323,12 @@ impl Pty { if self.closed.load(Ordering::Relaxed) { Ok(()) } else { - self.closed.store(true, Ordering::Relaxed); self.child .lock() .await .kill() .map_err(|err| PtyError::Kill(err.to_string())) + .map(|()| self.closed.store(true, Ordering::Relaxed)) } } diff --git a/src-tauri/src/pty/utils.rs b/src-tauri/src/pty/utils.rs index 4135ffb..83450ea 100644 --- a/src-tauri/src/pty/utils.rs +++ b/src-tauri/src/pty/utils.rs @@ -28,39 +28,44 @@ pub fn get_leader_pid(shell_pid: u32) -> u32 { leader_pid } + #[cfg(target_os = "windows")] -pub fn get_process_title(pid: u32) -> Option { +pub async fn get_process_title(pid: u32, fetched_title: &mut Option) { use std::{ffi::OsString, os::windows::ffi::OsStringExt}; use windows::Win32::{ Foundation::CloseHandle, System::Threading::{PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}, }; - unsafe { - windows::Win32::System::Threading::OpenProcess( - PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, - false, - pid, - ) - } - .map_or(None, |handle| { - let mut path = [0; 4096]; - let path_len = unsafe { - windows::Win32::System::ProcessStatus::GetModuleFileNameExW(handle, None, &mut path) - }; + *fetched_title = tokio::task::spawn_blocking(move || { + unsafe { + windows::Win32::System::Threading::OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, + false, + pid, + ) + } + .map_or(None, |handle| { + let mut path = [0; 4096]; + let path_len = unsafe { + windows::Win32::System::ProcessStatus::GetModuleFileNameExW(handle, None, &mut path) + }; - unsafe { CloseHandle(handle).ok() }; + unsafe { CloseHandle(handle).ok() }; - std::path::PathBuf::from(OsString::from_wide(&path[..path_len as usize])) - .file_name() - .and_then(|filename| filename.to_os_string().into_string().ok()) + std::path::PathBuf::from(OsString::from_wide(&path[..path_len as usize])) + .file_name() + .and_then(|filename| filename.to_os_string().into_string().ok()) + }) }) + .await + .unwrap(); } + #[cfg(target_family = "unix")] -pub fn get_process_title(pid: i32) -> Option { - #[cfg(target_family = "unix")] - { +pub async fn get_process_title(pid: i32, fetched_title: &mut Option) { + *fetched_title = tokio::task::spawn_blocking(move || { std::fs::read_to_string(format!("/proc/{pid}/comm")).map_or( None, |mut process_leader_title| { @@ -73,5 +78,23 @@ pub fn get_process_title(pid: i32) -> Option { } }, ) - } + }) + .await + .unwrap(); +} + + +#[cfg(target_family = "unix")] +pub async fn get_process_working_dir(pid: i32, fetched_pwd: &mut Option) { + *fetched_pwd = tokio::task::spawn_blocking(move || { + std::fs::read_link(format!("/proc/{pid}/cwd")) + .map_or(None, |path| path.into_os_string().into_string().ok()) + }) + .await + .unwrap(); +} + +#[cfg(target_os = "windows")] +pub async fn get_process_working_dir(pid: u32, fetched_pwd: &mut Option) { + todo!() } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 229a8bd..e19077e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -60,11 +60,13 @@ }, "windows": [ { + "title": "Tess", + "width": 1440, + "height": 810, + "minWidth": 640, + "minHeight": 360, "fullscreen": false, - "height": 720, "resizable": true, - "title": "Tess", - "width": 1280, "transparent": true, "decorations": false } diff --git a/src/style/animation.scss b/src/style/animation.scss index 7d665c8..e5c03bd 100644 --- a/src/style/animation.scss +++ b/src/style/animation.scss @@ -104,4 +104,18 @@ -webkit-backdrop-filter: blur(0); background: transparent; } +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 60%, transparent); + } + + 14% { + box-shadow: 0 0 0 4px transparent; + } + + 25% { + box-shadow: 0 0 0 0 transparent; + } } \ No newline at end of file diff --git a/src/style/constants.scss b/src/style/constants.scss index e03f41d..24f09ac 100644 --- a/src/style/constants.scss +++ b/src/style/constants.scss @@ -13,6 +13,7 @@ --tab-progressbar-color: #deeaf8; --tab-progressbar-background: #050a19; --tab-action-button-background: var(--tab-focused-background); + --tab-highlight-color: #deeaf8; --toast-background: #050a19; --toast-button-hovered-background: #192033; diff --git a/src/style/tab.scss b/src/style/tab.scss index 6f83aa3..21652b4 100644 --- a/src/style/tab.scss +++ b/src/style/tab.scss @@ -11,7 +11,6 @@ -webkit-user-select: none; transition: transform 200ms, background 140ms; font-size: 12px; - text-transform: capitalize; position: relative; overflow: hidden; background: var(--tabs-background); @@ -115,12 +114,28 @@ height: 20px; width: 20px; border-radius: 3px; - left: 5px; - position: absolute; + left: 6px; + position: absolute; img { max-height: 100%; max-width: 100%; } } + + .ping { + position: absolute; + color: var(--tab-highlight-color); + background: currentColor; + border-radius: 100%; + top: 4px; + left: 21px; + height: 7px; + width: 7px; + animation: pulse 4s infinite; + + &.hidden { + display: none; + } + } } \ No newline at end of file diff --git a/src/ts/app.ts b/src/ts/app.ts index a71397d..308ea7f 100644 --- a/src/ts/app.ts +++ b/src/ts/app.ts @@ -3,7 +3,7 @@ import { listen, Event } from '@tauri-apps/api/event' import { v4 as uuid } from 'uuid'; import { invoke } from '@tauri-apps/api/tauri' -import { terminalTitleChangedPayload } from "./schema/term"; +import {terminalTitleChangedPayload } from "./schema/term"; import { View } from "./class/views"; import { Toaster } from "./manager/toast"; @@ -39,7 +39,7 @@ export class App { this.shortcutsManager = new ShortcutsManager(option.shortcuts, (action) => { this.onShortcutExecuted(action) }); - listen("js_pty_title_update", (e) => { this.onTerminalTitleChanged(e); }) + listen("js_pty_title_update", (e) => { this.onTerminalTitleUpdated(e); }) listen("js_pty_closed", (e) => { this.onTerminalProcessExited(e); }); listen("js_window_request_closing", () => { this.closeViews(); }); @@ -98,7 +98,7 @@ export class App { this.tabsManager.closeTab(uuid); } - private onTerminalTitleChanged(e: Event) { + private onTerminalTitleUpdated(e: Event) { this.views.forEach((view) => { if (view.getTerm(e.payload.id)) { view.updatePaneTitle(e.payload.id, e.payload.title) @@ -206,7 +206,7 @@ export class App { let profile = this.option.profiles.find(profile => profile.uuid == profileId); if (profile) { - let view = new View(viewId, this.popupManager, this.toaster, (id) => { this.onViewsClosed(id); }, (title) => { this.tabsManager.setTitle(viewId, title); }) + let view = new View(viewId, this.popupManager, this.toaster, (id) => { this.onViewsClosed(id); }, (title) => { this.tabsManager.setTitle(viewId, title); }, (viewId) => { this.onViewGotUnreadData(viewId) }, (viewId, progress) => { this.onViewGotProgressUpdate(viewId, progress) }) view.openPane(paneId, profile, (e, term) => { return this.shortcutsManager.onKeyPress(e, term); }).then(() => { this.views.push(view); @@ -237,6 +237,14 @@ export class App { } } + private onViewGotUnreadData(viewId: string) { + this.tabsManager.setHightlight(viewId, true); + } + + private onViewGotProgressUpdate(viewId: string, progress: number) { + this.tabsManager.setprogress(viewId, progress); + } + /*openInternalPage(pageName: string) { // TODO: Implement diff --git a/src/ts/class/panes.ts b/src/ts/class/panes.ts index b3a880f..0761219 100644 --- a/src/ts/class/panes.ts +++ b/src/ts/class/panes.ts @@ -47,8 +47,8 @@ export class TerminalPane { return element } - async initializeTerm(customKeyEventHanlder: ((e: KeyboardEvent, term: Xterm) => boolean), toaster: Toaster) { - let terminal = new Terminal(this.id, this.profile.terminalOptions, this.profile.theme, customKeyEventHanlder, toaster); + async initializeTerm(customKeyEventHanlder: ((e: KeyboardEvent, term: Xterm) => boolean), toaster: Toaster, onUnreadData: (() => void), onProgressUpdate: ((progress: number) => void)) { + let terminal = new Terminal(this.id, this.profile.terminalOptions, this.profile.theme, customKeyEventHanlder, toaster, onUnreadData, onProgressUpdate); await terminal.launch(this.element.querySelector(".internal-term")!, this.profile.uuid); diff --git a/src/ts/class/tab.ts b/src/ts/class/tab.ts index ee333eb..bbd1472 100644 --- a/src/ts/class/tab.ts +++ b/src/ts/class/tab.ts @@ -47,6 +47,10 @@ export class Tab { } } + setHighlight(visible: boolean) { + (this.element.querySelector(".ping")! as HTMLElement).classList.toggle("hidden", !visible); + } + private generateComponents() : HTMLElement { let tab = document.createElement("div"); @@ -81,10 +85,14 @@ export class Tab { progress.classList.add("progress"); progress.appendChild(progressValue) + let pingMark = document.createElement("div"); + pingMark.classList.add("ping", "hidden"); + tab.appendChild(icon); tab.appendChild(title); tab.appendChild(closeButton); tab.appendChild(progress); + tab.appendChild(pingMark); return tab; } diff --git a/src/ts/class/terminal.ts b/src/ts/class/terminal.ts index 7bdad22..de0fd52 100644 --- a/src/ts/class/terminal.ts +++ b/src/ts/class/terminal.ts @@ -4,7 +4,7 @@ import { Terminal as Xterm } from "xterm"; import { invoke } from '@tauri-apps/api/tauri' import { TerminalOptions, TerminalTheme } from "ts/schema/option"; import { Toaster } from "ts/manager/toast"; -import { terminalDataPayload } from "ts/schema/term"; +import { terminalDataPayload, terminalProgressUpdatedPayload } from "ts/schema/term"; import { UnlistenFn, listen } from "@tauri-apps/api/event"; export class Terminal { @@ -14,9 +14,11 @@ export class Terminal { canvasResizeObserver: ResizeObserver | undefined; toaster: Toaster; unlisten: UnlistenFn | undefined = undefined; + disposeContentUpdate: UnlistenFn | undefined = undefined; + disposeProgressTracker: UnlistenFn | undefined = undefined; - constructor(id: string, options: TerminalOptions, theme: TerminalTheme, customKeyEventHandler: ((e: KeyboardEvent, term: Xterm) => boolean), toaster: Toaster) { + constructor(id: string, options: TerminalOptions, theme: TerminalTheme, customKeyEventHandler: ((e: KeyboardEvent, term: Xterm) => boolean), toaster: Toaster, onNewDisplayedDataReceived: (() => void), onProgressUpdate: ((progress: number) => void)) { theme = Object.assign({}, theme); theme.background = "rgba(0,0,0,0)"; @@ -59,13 +61,13 @@ export class Terminal { this.term.write(e.payload.data, () => { bufferedBytes = Math.max(bufferedBytes - e.payload.data.length, 0); - if (bufferedBytes < 16384 && paused) { + if (bufferedBytes < 2048 && paused) { invoke("pty_resume", {id: id}); paused = false; } }) - if (bufferedBytes > 131072 && !paused) { + if (bufferedBytes > 65536 && !paused) { invoke("pty_pause", {id: id}); paused = true; } @@ -73,6 +75,22 @@ export class Terminal { })).then((unlisten) => { this.unlisten = unlisten; }) + + listen("js_pty_display_content_update", ((e) => { + if (e.payload == this.id && options.showUnreadDataMark) { + onNewDisplayedDataReceived(); + } + })).then((disposeTodo) => { + this.disposeContentUpdate = disposeTodo; + }) + + listen("js_pty_progress_update", ((e) => { + if (e.payload.id == this.id) { + onProgressUpdate(e.payload.progress); + } + })).then((disposeProgressTracker) => { + this.disposeProgressTracker = disposeProgressTracker; + }) } async launch(target: HTMLElement, profile_id: string) { @@ -128,6 +146,8 @@ export class Terminal { close() { this.unlisten!(); + this.disposeContentUpdate!(); + this.disposeProgressTracker!(); this.canvasResizeObserver!.disconnect(); try { this.term.dispose() } catch (_) {} } diff --git a/src/ts/class/views.ts b/src/ts/class/views.ts index 2cfd085..44d6613 100644 --- a/src/ts/class/views.ts +++ b/src/ts/class/views.ts @@ -9,28 +9,34 @@ import { Toaster } from "ts/manager/toast"; export class View { // TODO: Implement pane page type - id: string | undefined; - element: HTMLElement | undefined; + id: string; + element: HTMLElement; panes: (TerminalPane|PagePane)[] = []; - closedEvent: ((id: string) => void) | undefined; - focusedPaneTitleChangedEvent: ((title: string) => void) | undefined; + closedEvent: ((id: string) => void); + focusedPaneTitleChangedEvent: ((title: string) => void); + gotUnreadDataEvent: ((viewId: string) => void); + gotProgressChangeEvent: ((viewId: string, progress: number) => void); focusedPane: TerminalPane | PagePane | undefined = undefined; - popupManager: PopupManager | undefined; + popupManager: PopupManager; closingAllRequested: boolean = false; toaster: Toaster; - constructor (viewId: string, popupManager: PopupManager, toaster: Toaster, closedEvent: ((id: string) => void), focusedPaneTitleChangedEvent: ((title: string) => void)) { + constructor(viewId: string, popupManager: PopupManager, toaster: Toaster, closedEvent: ((id: string) => void), focusedPaneTitleChangedEvent: ((title: string) => void), gotUnreadDataEvent: ((viewId: string) => void), gotProgressChange: ((viewid: string, progress: number) => void)) { this.id = viewId; this.element = this.generateComponents(); + this.closedEvent = closedEvent; this.focusedPaneTitleChangedEvent = focusedPaneTitleChangedEvent; + this.gotUnreadDataEvent = gotUnreadDataEvent; + this.gotProgressChangeEvent = gotProgressChange; + this.popupManager = popupManager; this.toaster = toaster; } @@ -40,10 +46,10 @@ export class View { async openPane(paneId: string, profile?: Profile, customKeyEventHandler?: ((e: KeyboardEvent, term: Xterm) => boolean)) { if (profile) { let pane = new TerminalPane(paneId, profile); - await pane.initializeTerm(customKeyEventHandler!, this.toaster); + await pane.initializeTerm(customKeyEventHandler!, this.toaster, (() => { this.gotUnreadDataEvent(this.id) }), ((progress) => { this.gotProgressChangeEvent(this.id, progress) })); this.panes.push(pane) - this.element!.appendChild(pane.element); + this.element.appendChild(pane.element); this.focusedPane = pane; } @@ -129,7 +135,7 @@ export class View { let pane = this.panes.find((pane) => pane.id == id); if (pane) { pane.title = title; - this.focusedPaneTitleChangedEvent!(title) + this.focusedPaneTitleChangedEvent(title) } } } \ No newline at end of file diff --git a/src/ts/manager/tabs.ts b/src/ts/manager/tabs.ts index bd5ec9e..ee06a76 100644 --- a/src/ts/manager/tabs.ts +++ b/src/ts/manager/tabs.ts @@ -72,6 +72,24 @@ export class TabsManager { } } + setprogress(uuid: string, progress: number) { + let tmp = this.tabs.find(tab => tab.id === uuid); + if (tmp) { + tmp.setProgress(progress); + } + } + + setHightlight(uuid: string, visible: boolean) { + let tmp = this.tabs.find(tab => tab.id === uuid); + if (tmp) { + if (visible && this.selectedTab != tmp) { + tmp.setHighlight(true); + } else { + tmp.setHighlight(false); + } + } + } + requestTabClosing(uuid: string) { let tab = this.tabs.find(tab => tab.id === uuid); @@ -175,6 +193,7 @@ export class TabsManager { } this.selectedTab = target; + this.selectedTab.setHighlight(false); this.selectedTab.element.classList.add("selected"); this.tabs.forEach((el) => { diff --git a/src/ts/schema/option.ts b/src/ts/schema/option.ts index 8648468..a18bfea 100644 --- a/src/ts/schema/option.ts +++ b/src/ts/schema/option.ts @@ -45,8 +45,7 @@ export type TerminalOptions = { letterSpacing: number, lineHeight: number, showPicture: boolean, - showUnreadDataIndicator: boolean, - titleIsRunningProcess: boolean + showUnreadDataMark: boolean } export type Profile = { diff --git a/src/ts/schema/term.ts b/src/ts/schema/term.ts index 3951d21..639f046 100644 --- a/src/ts/schema/term.ts +++ b/src/ts/schema/term.ts @@ -6,4 +6,9 @@ export type terminalDataPayload = { export type terminalTitleChangedPayload = { title: string, id: string +} + +export type terminalProgressUpdatedPayload = { + progress: number, + id: string } \ No newline at end of file From dabf3b6b757678b8eaa1f92ddff9951d91eb05f3 Mon Sep 17 00:00:00 2001 From: Squitch Date: Sun, 4 Feb 2024 16:34:43 +0100 Subject: [PATCH 2/9] :bug: Fix compilation on Windows --- src-tauri/src/common/title_formatter.rs | 3 ++- src-tauri/src/pty/pty.rs | 34 +++++++++++++++++-------- src-tauri/src/pty/utils.rs | 5 +--- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src-tauri/src/common/title_formatter.rs b/src-tauri/src/common/title_formatter.rs index 9e1adf4..6098ab3 100644 --- a/src-tauri/src/common/title_formatter.rs +++ b/src-tauri/src/common/title_formatter.rs @@ -62,7 +62,8 @@ impl Formatter { current_placeholder_parts = Default::default(); } - placeholder @ ("pwd" | "leader_process" | "action_progress" | "shell_title") => { + placeholder @ ("pwd" | "leader_process" | "action_progress" + | "shell_title") => { parts.push(TitlePart::Static(std::mem::take( &mut current_static_part, ))); diff --git a/src-tauri/src/pty/pty.rs b/src-tauri/src/pty/pty.rs index 880f517..b30272b 100644 --- a/src-tauri/src/pty/pty.rs +++ b/src-tauri/src/pty/pty.rs @@ -89,10 +89,29 @@ impl Pty { .map_err(|err| PtyError::Creation(err.to_string()))?; let master = Arc::new(Mutex::from(pty_pair.master)); + #[cfg(target_family = "unix")] + let cloned_master = master.clone(); + #[cfg(target_family = "unix")] + let child = Arc::from(Mutex::new( + pty_pair + .slave + .spawn_command(builded_command) + .map_err(|err| PtyError::Creation(err.to_string()))?, + )); + + #[cfg(target_os = "windows")] + let mut shell_pid = 0; + #[cfg(target_os = "windows")] let child = Arc::from(Mutex::new( pty_pair .slave .spawn_command(builded_command) + .and_then(|child| { + shell_pid = child + .process_id() + .ok_or(PtyError::Creation("PID not found".to_owned()))?; + Ok(child) + }) .map_err(|err| PtyError::Creation(err.to_string()))?, )); @@ -107,15 +126,6 @@ impl Pty { let paused = Arc::from(AtomicBool::new(false)); let paused_cloned = paused.clone(); - #[cfg(target_family = "unix")] - let cloned_master = master.clone(); - #[cfg(target_os = "windows")] - let shell_pid = child - .lock() - .await - .process_id() - .ok_or(PtyError::Creation("PID not found".to_owned()))?; - let progress_tracking = progress_report | title_formatter.options.action_progress; let current_progress = Arc::new(AtomicU8::new(0)); let cloned_current_progress = current_progress.clone(); @@ -281,7 +291,11 @@ impl Pty { let fetched_data_count = fetchers.len(); join_all(fetchers).await; - if fetched_data_count > 1 || title_formatter.options.action_progress || title_formatter.options.shell_title { + if fetched_data_count > 1 + || title_formatter.options.action_progress + || title_formatter.options.shell_title + || title_formatter.options.leader_process + { let generated_title = title_formatter.format(&FormatterParams { pwd: fetched_pwd, leader_process: fetched_leader_process.clone(), diff --git a/src-tauri/src/pty/utils.rs b/src-tauri/src/pty/utils.rs index 83450ea..c42d681 100644 --- a/src-tauri/src/pty/utils.rs +++ b/src-tauri/src/pty/utils.rs @@ -28,7 +28,6 @@ pub fn get_leader_pid(shell_pid: u32) -> u32 { leader_pid } - #[cfg(target_os = "windows")] pub async fn get_process_title(pid: u32, fetched_title: &mut Option) { use std::{ffi::OsString, os::windows::ffi::OsStringExt}; @@ -59,10 +58,9 @@ pub async fn get_process_title(pid: u32, fetched_title: &mut Option) { }) }) .await - .unwrap(); + .unwrap_or(None); } - #[cfg(target_family = "unix")] pub async fn get_process_title(pid: i32, fetched_title: &mut Option) { *fetched_title = tokio::task::spawn_blocking(move || { @@ -83,7 +81,6 @@ pub async fn get_process_title(pid: i32, fetched_title: &mut Option) { .unwrap(); } - #[cfg(target_family = "unix")] pub async fn get_process_working_dir(pid: i32, fetched_pwd: &mut Option) { *fetched_pwd = tokio::task::spawn_blocking(move || { From 977cd6d8b8fced47b443f64247600a6c51842a0c Mon Sep 17 00:00:00 2001 From: SquitchYT Date: Mon, 5 Feb 2024 23:34:33 +0100 Subject: [PATCH 3/9] :sparkles: Add dynamic titles --- src-tauri/src/common/commands/window.rs | 5 ++ src-tauri/src/configuration/deserialized.rs | 68 ++++++++++++++++++--- src-tauri/src/configuration/partial.rs | 8 ++- src-tauri/src/main.rs | 3 +- src/ts/app.ts | 18 +++++- src/ts/class/tab.ts | 5 ++ src/ts/class/terminal.ts | 4 +- src/ts/manager/tabs.ts | 9 ++- src/ts/schema/option.ts | 7 ++- 9 files changed, 109 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/common/commands/window.rs b/src-tauri/src/common/commands/window.rs index b9ab679..a9098b3 100644 --- a/src-tauri/src/common/commands/window.rs +++ b/src-tauri/src/common/commands/window.rs @@ -2,3 +2,8 @@ pub fn window_close(window: tauri::Window) { window.close().ok(); } + +#[tauri::command] +pub fn window_set_title(window: tauri::Window, title: &str) { + window.set_title(&format!("Tess - {title}")).ok(); +} diff --git a/src-tauri/src/configuration/deserialized.rs b/src-tauri/src/configuration/deserialized.rs index eba1cca..fe3b4e3 100644 --- a/src-tauri/src/configuration/deserialized.rs +++ b/src-tauri/src/configuration/deserialized.rs @@ -17,13 +17,13 @@ pub struct Option { pub terminal_theme: TerminalTheme, pub background: BackgroundType, pub background_transparency: RangedInt<0, 100, 100>, - pub custom_titlebar: bool, pub profiles: Vec, pub terminal: TerminalOption, pub shortcuts: Vec, pub macros: Vec, pub default_profile: Profile, pub close_confirmation: CloseConfirmation, + pub desktop_integration: DesktopIntegration, #[serde(skip_serializing)] theme: String, @@ -37,7 +37,6 @@ impl Default for Option { app_theme: String::default(), terminal_theme: TerminalTheme::default(), background: BackgroundType::default(), - custom_titlebar: true, // TODO: Set false on linux profiles: vec![default_profile(uuid.clone(), &default_title_format())], terminal: TerminalOption::default(), background_transparency: RangedInt::default(), @@ -46,6 +45,7 @@ impl Default for Option { default_profile: default_profile(uuid, &default_title_format()), close_confirmation: CloseConfirmation::default(), + desktop_integration: DesktopIntegration::default(), theme: String::default(), } @@ -203,7 +203,6 @@ impl<'de> serde::Deserialize<'de> for Option { terminal_theme, app_theme, background: partial_option.background, - custom_titlebar: partial_option.custom_titlebar, terminal: partial_option.terminal, profiles: profiles.clone(), background_transparency: partial_option.background_transparency, @@ -215,6 +214,7 @@ impl<'de> serde::Deserialize<'de> for Option { .unwrap_or(&profiles[0]) .clone(), close_confirmation: partial_option.close_confirmation, + desktop_integration: partial_option.desktop_integration, }) } } @@ -545,10 +545,10 @@ impl<'de> Deserialize<'de> for CloseConfirmation { } match Representation::deserialize(deserializer)? { - Representation::Simple(close_confirmation_toggled) => Ok(Self { - tab: close_confirmation_toggled, - window: close_confirmation_toggled, - app: close_confirmation_toggled, + Representation::Simple(enable) => Ok(Self { + tab: enable, + window: enable, + app: enable, #[cfg(target_family = "unix")] excluded_process: vec![ "sh".to_owned(), @@ -616,6 +616,60 @@ impl Default for CloseConfirmation { } } +#[derive(Debug, Clone, Serialize)] +pub struct DesktopIntegration { + pub custom_titlebar: bool, + pub dynamic_title: bool, +} + +impl Default for DesktopIntegration { + fn default() -> Self { + Self { + #[cfg(target_family = "unix")] + custom_titlebar: false, + #[cfg(target_os = "windows")] + custom_titlebar: true, + dynamic_title: true, + } + } +} + +impl<'de> Deserialize<'de> for DesktopIntegration { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct PartialDesktopIntegration { + custom_titlebar: std::option::Option, + dynamic_title: std::option::Option, + } + + #[derive(Deserialize)] + #[serde(untagged)] + enum Representation { + Simple(bool), + Complex(PartialDesktopIntegration), + } + + let representation = Representation::deserialize(deserializer)?; + + Ok(match representation { + Representation::Simple(enable) => Self { + custom_titlebar: enable, + dynamic_title: enable, + }, + Representation::Complex(partial_desktop_integration) => Self { + dynamic_title: partial_desktop_integration.dynamic_title.unwrap_or(true), + #[cfg(target_family = "unix")] + custom_titlebar: partial_desktop_integration.custom_titlebar.unwrap_or(false), + #[cfg(target_os = "windows")] + custom_titlebar: partial_desktop_integration.custom_titlebar.unwrap_or(true), + }, + }) + } +} + const fn default_to_true() -> bool { true } diff --git a/src-tauri/src/configuration/partial.rs b/src-tauri/src/configuration/partial.rs index 1ffc0f9..e968c22 100644 --- a/src-tauri/src/configuration/partial.rs +++ b/src-tauri/src/configuration/partial.rs @@ -8,6 +8,8 @@ use crate::configuration::types::{BackgroundMedia, BackgroundType}; use serde::Deserialize; use serde::Deserializer; +use super::deserialized::DesktopIntegration; + #[derive(Deserialize, Debug)] pub struct PartialOption { #[serde(default)] @@ -15,8 +17,6 @@ pub struct PartialOption { #[serde(default)] pub background: BackgroundType, #[serde(default)] - pub custom_titlebar: bool, // TODO: Set correct default value - #[serde(default)] pub profiles: Vec, #[serde(default, flatten)] pub terminal: TerminalOption, @@ -30,6 +30,8 @@ pub struct PartialOption { #[serde(default)] pub close_confirmation: CloseConfirmation, + #[serde(default)] + pub desktop_integration: DesktopIntegration, #[serde(default)] pub default_profile: String, @@ -43,7 +45,6 @@ impl Default for PartialOption { Self { theme: String::default(), background: BackgroundType::default(), - custom_titlebar: true, // TODO: Set false on linux profiles: Vec::default(), terminal: TerminalOption::default(), background_transparency: RangedInt::default(), @@ -51,6 +52,7 @@ impl Default for PartialOption { macros: Some(Vec::default()), default_profile: String::new(), close_confirmation: CloseConfirmation::default(), + desktop_integration: DesktopIntegration::default(), title_format: default_title_format(), } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f5e4391..4605315 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -62,7 +62,8 @@ async fn main() { commands::pty_pause, commands::utils_close_app, commands::utils_get_configuration, - commands::window_close + commands::window_close, + commands::window_set_title ]) .build(tauri::generate_context!()) .unwrap(); diff --git a/src/ts/app.ts b/src/ts/app.ts index 308ea7f..5d0670b 100644 --- a/src/ts/app.ts +++ b/src/ts/app.ts @@ -35,6 +35,7 @@ export class App { this.tabsManager = new TabsManager(tabsTarget, (id) => { this.onTabRequestClose(id); }); this.tabsManager.addEventListener("tabFocused", (id) => { this.onTabFocused(id); }); + this.tabsManager.addEventListener("titleUpdated", (id) => { this.onTabTitleUpdated(id); }); this.popupManager = new PopupManager(); this.shortcutsManager = new ShortcutsManager(option.shortcuts, (action) => { this.onShortcutExecuted(action) }); @@ -62,10 +63,12 @@ export class App { private onTabFocused(id: string) { let view = this.views.find((view) => view.id! == id); + let tab = this.tabsManager.getTab(id); - if (view) { + if (view && tab) { this.focusedView = view; view.focus(); + invoke("window_set_title", {title: tab.title}); this.views.forEach((view) => { if (view.id != id) { @@ -75,6 +78,15 @@ export class App { } } + + private onTabTitleUpdated(id: string) { + let tab = this.tabsManager.getTab(id); + + if (this.focusedView?.id == id && this.option.desktopIntegration.dynamic_title && tab) { + invoke("window_set_title", {title: tab.title}); + } + } + private async onTabRequestClose(id: string) { let view = this.views.find((view) => view.id == id); if (view) { @@ -91,7 +103,7 @@ export class App { view.element!.remove(); this.views.splice(this.views.indexOf(view), 1); if (this.views.length == 0) { - invoke("window_close") + invoke("window_close"); } } @@ -108,7 +120,7 @@ export class App { private onTerminalPaneInput(id: string, data: string) { invoke("pty_write", {content: data, id: id}).catch((err) => { - this.toaster.toast("Interaction error", err, "error") + this.toaster.toast("Interaction error", err, "error"); }); } diff --git a/src/ts/class/tab.ts b/src/ts/class/tab.ts index bbd1472..891d4b7 100644 --- a/src/ts/class/tab.ts +++ b/src/ts/class/tab.ts @@ -9,12 +9,16 @@ export class Tab { removedProgressTimeout1: number = 0; + title: string; + constructor(index: number, id: string, onClose:((id: string) => void)) { this.id = id; this.index = index; this.element = this.generateComponents(); + this.title = ""; + new ResizeObserver(() => { if (this.element.clientWidth <= 34) { this.element.classList.add("small") @@ -27,6 +31,7 @@ export class Tab { } setTitle(title: string) { + this.title = title; (this.element.querySelector(".title")! as HTMLSpanElement).innerText = title; } diff --git a/src/ts/class/terminal.ts b/src/ts/class/terminal.ts index de0fd52..729cd19 100644 --- a/src/ts/class/terminal.ts +++ b/src/ts/class/terminal.ts @@ -80,8 +80,8 @@ export class Terminal { if (e.payload == this.id && options.showUnreadDataMark) { onNewDisplayedDataReceived(); } - })).then((disposeTodo) => { - this.disposeContentUpdate = disposeTodo; + })).then((disposeContentUpdate) => { + this.disposeContentUpdate = disposeContentUpdate; }) listen("js_pty_progress_update", ((e) => { diff --git a/src/ts/manager/tabs.ts b/src/ts/manager/tabs.ts index ee06a76..403a4f8 100644 --- a/src/ts/manager/tabs.ts +++ b/src/ts/manager/tabs.ts @@ -18,6 +18,7 @@ export class TabsManager { private tabFocusedListener: ((id: string) => void)[] = []; private tabAddedListener: ((id: string) => void)[] = []; + private titleUpdatedListener: ((id: string) => void)[] = []; private requestedTabClosingListener: (id: string) => void; // Max Index is tabs.lenght @@ -69,6 +70,9 @@ export class TabsManager { let tmp = this.tabs.find(tab => tab.id === uuid); if (tmp) { tmp.setTitle(title); + this.titleUpdatedListener.forEach((listener) => { + listener(uuid); + }) } } @@ -176,7 +180,7 @@ export class TabsManager { return this.tabs.find(tab => tab.id === uuid) } - addEventListener(event: "tabFocused" | "tabAdded", listener: ((id: string) => void)) { + addEventListener(event: "tabFocused" | "tabAdded" | "titleUpdated", listener: ((id: string) => void)) { switch (event) { case "tabFocused": this.tabFocusedListener.push(listener); @@ -184,6 +188,9 @@ export class TabsManager { case "tabAdded": this.tabAddedListener.push(listener); break; + case "titleUpdated": + this.titleUpdatedListener.push(listener); + break; } } diff --git a/src/ts/schema/option.ts b/src/ts/schema/option.ts index a18bfea..9f5e548 100644 --- a/src/ts/schema/option.ts +++ b/src/ts/schema/option.ts @@ -1,6 +1,7 @@ export type Option = { appTheme : string, - closeConfirmation: boolean, + closeConfirmation: CloseConfirmation, + desktopIntegration: DesktopInetgration, customTitlebar: boolean, profiles: Profile[], macros: Macro[], @@ -91,4 +92,8 @@ export type CloseConfirmation = { window: boolean, app: boolean, excludedProcess: string[] +} + +export type DesktopInetgration = { + dynamic_title: boolean } \ No newline at end of file From bd3f88000fe827a88b933f677be9c194c68f2004 Mon Sep 17 00:00:00 2001 From: Squitch Date: Tue, 6 Feb 2024 22:06:27 +0100 Subject: [PATCH 4/9] :sparkles: Add pwd support as tab title format on Windows --- src-tauri/Cargo.lock | 66 ++++++++++++++++------------- src-tauri/Cargo.toml | 3 +- src-tauri/src/pty/utils.rs | 85 +++++++++++++++++++++++++++++++++++++- 3 files changed, 124 insertions(+), 30 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1de9e5a..97b082b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2785,7 +2785,8 @@ dependencies = [ "uuid 1.3.0", "vt100", "window-vibrancy", - "windows 0.52.0", + "windows 0.51.1", + "windows-native", ] [[package]] @@ -3284,9 +3285,9 @@ dependencies = [ [[package]] name = "windows" -version = "0.52.0" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" dependencies = [ "windows-core", "windows-targets", @@ -3304,9 +3305,9 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ "windows-targets", ] @@ -3327,6 +3328,15 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" +[[package]] +name = "windows-native" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239ad23b2cb80fddb2075952b37fac98a33adacc0d38fe9a39f4ba1a1937adf4" +dependencies = [ + "windows 0.51.1", +] + [[package]] name = "windows-sys" version = "0.42.0" @@ -3344,17 +3354,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -3371,9 +3381,9 @@ checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -3389,9 +3399,9 @@ checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -3407,9 +3417,9 @@ checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -3425,9 +3435,9 @@ checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -3443,9 +3453,9 @@ checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -3455,9 +3465,9 @@ checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -3473,9 +3483,9 @@ checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winreg" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0d2478c..d54dabf 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,7 +35,8 @@ signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } [target.'cfg(windows)'.dependencies] window-vibrancy = "0.3.2" -windows = { version = "0.52.0", features = ["Win32_Foundation", "Win32_System", "Win32_System_Threading", "Win32_System_ProcessStatus", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Diagnostics"] } +windows = { version = "^0.51.0", features = ["Win32_Foundation", "Win32_System", "Win32_System_Threading", "Win32_System_ProcessStatus", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Diagnostics"] } +windows-native = "1.0.40" [features] default = ["custom-protocol"] diff --git a/src-tauri/src/pty/utils.rs b/src-tauri/src/pty/utils.rs index c42d681..64ee933 100644 --- a/src-tauri/src/pty/utils.rs +++ b/src-tauri/src/pty/utils.rs @@ -93,5 +93,88 @@ pub async fn get_process_working_dir(pid: i32, fetched_pwd: &mut Option) #[cfg(target_os = "windows")] pub async fn get_process_working_dir(pid: u32, fetched_pwd: &mut Option) { - todo!() + use std::mem::size_of; + use std::{os::raw::c_void, ptr}; + + use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; + use windows::Win32::System::Threading::PEB; + use windows::{ + Wdk::System::Threading::{NtQueryInformationProcess, ProcessBasicInformation}, + Win32::{ + Foundation::CloseHandle, + System::Threading::{ + PROCESS_BASIC_INFORMATION, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, + }, + }, + }; + use windows_native::ntrtl::RTL_USER_PROCESS_PARAMETERS; + + if let Ok(handle) = unsafe { + windows::Win32::System::Threading::OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, + false, + pid, + ) + } { + let pbi = PROCESS_BASIC_INFORMATION::default(); + + *fetched_pwd = unsafe { + NtQueryInformationProcess( + handle, + ProcessBasicInformation, + &pbi as *const _ as *mut c_void, + size_of::() as u32, + ptr::null_mut(), + ) + .map(|()| (handle, pbi)) + } + .and_then(|(handle, pbi)| { + let peb = PEB::default(); + + unsafe { + ReadProcessMemory( + handle, + pbi.PebBaseAddress as *const c_void, + &peb as *const _ as *mut c_void, + size_of::(), + None, + ) + .map(|()| (handle, peb)) + } + }) + .and_then(|(handle, peb)| { + let upp = RTL_USER_PROCESS_PARAMETERS::default(); + + unsafe { + ReadProcessMemory( + handle, + peb.ProcessParameters as *const c_void, + &upp as *const _ as *mut c_void, + size_of::(), + None, + ) + .map(|()| (handle, upp)) + } + }) + .and_then(|(handle, upp)| { + let mut path: Vec = vec![0; (upp.CurrentDirectory.DosPath.Length / 2) as usize]; + let mut path_len = 0; + + unsafe { + ReadProcessMemory( + handle, + upp.CurrentDirectory.DosPath.Buffer.as_ptr() as *mut c_void, + path.as_mut_ptr() as *mut c_void, + upp.CurrentDirectory.DosPath.Length as usize, + Some(&mut path_len), + ) + .map(|()| { + String::from_utf16_lossy(&path) + }) + } + }) + .ok(); + + unsafe { CloseHandle(handle).ok() }; + } } From 4a8e0ae18eb1ea566eb60fbe83d46b425a7259ff Mon Sep 17 00:00:00 2001 From: SquitchYT Date: Wed, 7 Feb 2024 21:31:51 +0100 Subject: [PATCH 5/9] :pencil2: Fix a typo :bento: Reduce default tab icon size --- src/assets/default-tab.png | Bin 92677 -> 2898 bytes src/ts/class/tab.ts | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/assets/default-tab.png b/src/assets/default-tab.png index 3ff87b3baa0b331e25e83bc6487dd5ce4ec6b85d..907e842228db197f9005c817dbd77eec72b9e247 100644 GIT binary patch literal 2898 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 diff --git a/src/ts/class/tab.ts b/src/ts/class/tab.ts index 891d4b7..afc998c 100644 --- a/src/ts/class/tab.ts +++ b/src/ts/class/tab.ts @@ -7,7 +7,7 @@ export class Tab { onClose: ((id: string) => void) | null = null; - removedProgressTimeout1: number = 0; + removedProgressTimeout: number = 0; title: string; @@ -37,14 +37,14 @@ export class Tab { setProgress(value: number) { if (value > 0 && value < 100) { - clearTimeout(this.removedProgressTimeout1); + clearTimeout(this.removedProgressTimeout); (this.element.querySelector(".progress")! as HTMLElement).style.animation = "tab-with-progress-added-progress-bar 100ms forwards"; (this.element.querySelector(".progress")! as HTMLElement).style.animationDelay = "70ms"; (this.element.querySelector(".title")! as HTMLElement).style.animation = "tab-with-progress-added-title 120ms forwards"; (this.element.querySelector(".value")! as HTMLElement).style.width = `${value}%`; } else { - this.removedProgressTimeout1 = setTimeout(() => { + this.removedProgressTimeout = setTimeout(() => { (this.element.querySelector(".title")! as HTMLElement).style.animation = "tab-with-progress-removed-title 120ms forwards"; }, 70); (this.element.querySelector(".progress")! as HTMLElement).style.animationDelay = "0ms"; From 9604d34ea60059ae5798d09f6e7c3fd45bf686e7 Mon Sep 17 00:00:00 2001 From: SquitchYT Date: Mon, 12 Feb 2024 20:36:30 +0100 Subject: [PATCH 6/9] :sparkles: Add short_pwd support as a tab title format :zap: Remove transparency for terminal panes with an opaque background :bug: Fix pty UTF-8 parsing :rotating_light: Apply clippy suggestions :art: Clean codebase --- package.json | 3 +- src-tauri/src/common/title_formatter.rs | 22 +- src-tauri/src/configuration/deserialized.rs | 6 +- src-tauri/src/configuration/types.rs | 16 +- src-tauri/src/main.rs | 10 +- src-tauri/src/pty/pty.rs | 371 +++++++++++--------- src-tauri/src/pty/utils.rs | 40 ++- src/style/tab.scss | 2 +- src/ts/class/panes.ts | 2 +- src/ts/class/terminal.ts | 9 +- 10 files changed, 287 insertions(+), 194 deletions(-) diff --git a/package.json b/package.json index a0cdf73..46ace06 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build --emptyOutDir", - "preview": "vite preview" + "build": "vite build --emptyOutDir" }, "devDependencies": { "@types/node": "^18.11.9", diff --git a/src-tauri/src/common/title_formatter.rs b/src-tauri/src/common/title_formatter.rs index 6098ab3..f46586f 100644 --- a/src-tauri/src/common/title_formatter.rs +++ b/src-tauri/src/common/title_formatter.rs @@ -1,18 +1,20 @@ -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] enum TitlePart { Static(String), Dynamic([String; 3]), } -#[derive(Debug, Default, Clone, Copy)] +#[derive(Default, Clone, Copy, Debug)] pub struct FormatterOptions { pub pwd: bool, + pub short_pwd: bool, pub leader_process: bool, pub action_progress: bool, pub shell_title: bool, } -#[derive(Debug, Default, Clone)] +#[derive(Default)] pub struct FormatterParams { pub pwd: Option, + pub short_pwd: Option, pub leader_process: Option, pub progress: Option, pub shell_title: Option, @@ -62,8 +64,8 @@ impl Formatter { current_placeholder_parts = Default::default(); } - placeholder @ ("pwd" | "leader_process" | "action_progress" - | "shell_title") => { + placeholder @ ("pwd" | "short_pwd" | "leader_process" + | "action_progress" | "shell_title") => { parts.push(TitlePart::Static(std::mem::take( &mut current_static_part, ))); @@ -73,6 +75,7 @@ impl Formatter { match placeholder { "pwd" => format_option.pwd = true, + "short_pwd" => format_option.short_pwd = true, "leader_process" => format_option.leader_process = true, "action_progress" => format_option.action_progress = true, "shell_title" => format_option.shell_title = true, @@ -143,6 +146,13 @@ impl Formatter { pwd: Some(value), .. }, ) + | ( + "short_pwd", + FormatterParams { + short_pwd: Some(value), + .. + }, + ) | ( "leader_process", FormatterParams { @@ -158,7 +168,7 @@ impl Formatter { }, ) => { output.push_str(&content[0]); - output.push_str(&value); + output.push_str(value); output.push_str(&content[2]); output diff --git a/src-tauri/src/configuration/deserialized.rs b/src-tauri/src/configuration/deserialized.rs index fe3b4e3..e268bcf 100644 --- a/src-tauri/src/configuration/deserialized.rs +++ b/src-tauri/src/configuration/deserialized.rs @@ -54,7 +54,11 @@ impl Default for Option { impl<'de> serde::Deserialize<'de> for Option { fn deserialize>(deserializer: D) -> Result { - let partial_option = PartialOption::deserialize(deserializer)?; + let mut partial_option = PartialOption::deserialize(deserializer)?; + + if matches!(partial_option.background, BackgroundType::Opaque) { + partial_option.background_transparency = RangedInt(100); + } let (app_theme, terminal_theme) = parse_theme(&partial_option.theme); let app_theme = app_theme.unwrap_or_default(); diff --git a/src-tauri/src/configuration/types.rs b/src-tauri/src/configuration/types.rs index 94b44d7..5c4f584 100644 --- a/src-tauri/src/configuration/types.rs +++ b/src-tauri/src/configuration/types.rs @@ -3,13 +3,11 @@ use serde::Deserialize; use serde::{Serialize, Serializer}; #[derive(Debug, Clone, Copy)] -pub struct RangedInt { - value: u32, -} +pub struct RangedInt(pub u32); impl Default for RangedInt { fn default() -> Self { - Self { value: DEF } + Self(DEF) } } @@ -21,13 +19,11 @@ impl<'de, const MIN: u32, const MAX: u32, const DEF: u32> serde::Deserialize<'de |_| Self::default(), |deserialized_value| { if deserialized_value < MIN { - Self { value: MIN } + Self(MIN) } else if deserialized_value > MAX { - Self { value: MAX } + Self(MAX) } else { - Self { - value: deserialized_value, - } + Self(deserialized_value) } }, )) @@ -39,7 +35,7 @@ impl serde::Serialize for Ranged where S: Serializer, { - serializer.serialize_u32(self.value) + serializer.serialize_u32(self.0) } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4605315..37536f4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -110,22 +110,20 @@ async fn main() { #[cfg(target_family = "unix")] { - let app_cloned = app.clone(); + let app = app.clone(); tokio::spawn(async move { if let Ok(mut signals_stream) = signal_hook_tokio::Signals::new([SIGQUIT, SIGTERM]) { while signals_stream.next().await.is_some() { - let windows_count = app_cloned.windows().len(); + let windows_count = app.windows().len(); if windows_count > 1 { - app_cloned - .get_window("main") + app.get_window("main") .unwrap() .emit("js_app_request_exit", windows_count) .ok(); } else { - app_cloned - .get_window("main") + app.get_window("main") .unwrap() .emit("js_window_request_closing", ()) .ok(); diff --git a/src-tauri/src/pty/pty.rs b/src-tauri/src/pty/pty.rs index b30272b..db6d140 100644 --- a/src-tauri/src/pty/pty.rs +++ b/src-tauri/src/pty/pty.rs @@ -23,11 +23,14 @@ use crate::pty::utils; #[cfg(target_os = "windows")] use regex_lite::Captures; +const PTY_BUFFER_SIZE: usize = 4096; + pub struct Pty { writer: Box, child: Arc>>, master: Arc>>, paused: Arc, + pre_parser: Arc>, pub leader_name: Arc>, pub closed: Arc, @@ -89,8 +92,6 @@ impl Pty { .map_err(|err| PtyError::Creation(err.to_string()))?; let master = Arc::new(Mutex::from(pty_pair.master)); - #[cfg(target_family = "unix")] - let cloned_master = master.clone(); #[cfg(target_family = "unix")] let child = Arc::from(Mutex::new( pty_pair @@ -115,213 +116,256 @@ impl Pty { .map_err(|err| PtyError::Creation(err.to_string()))?, )); - let cloned_child = child.clone(); - let leader_name = Arc::new(Mutex::from(String::new())); - let cloned_leader_process = leader_name.clone(); + let leader_process = Arc::new(Mutex::from(String::new())); let closed = Arc::new(AtomicBool::new(false)); - let closed_cloned = closed.clone(); let (exit_sender, exit_receiver) = mpsc::channel::<()>(); let paused = Arc::from(AtomicBool::new(false)); - let paused_cloned = paused.clone(); let progress_tracking = progress_report | title_formatter.options.action_progress; let current_progress = Arc::new(AtomicU8::new(0)); - let cloned_current_progress = current_progress.clone(); - - let sync_shell_title = Arc::new(std::sync::Mutex::from(None)); - let cloned_sync_shell_title = sync_shell_title.clone(); - - std::thread::spawn(move || { - let mut buf = [0; 4096]; - let mut remaining = 0; - let mut pre_parser = vt100::Parser::new(8, 144, 0); - - lazy_static::lazy_static! { - static ref PROGRESS_PARSING_REGEX: Regex = Regex::new(r"(([0-9]*[.])?[0-9]+%)|(\d+/\d+)").unwrap(); - } - - loop { - if !paused_cloned.load(Ordering::Relaxed) { - buf[remaining..].fill(0); - - if reader - .read(&mut buf[remaining..]) - .is_ok_and(|bytes| bytes > 0) - { - let previous_cached_content = pre_parser.screen().contents(); - match std::str::from_utf8(&buf) { - Ok(parsed_buf) => { - pre_parser.process(parsed_buf.as_bytes()); - on_read(parsed_buf); - remaining = 0; - } - Err(utf8) => { - pre_parser.process(&buf[..utf8.valid_up_to()]); - on_read(unsafe { - std::str::from_utf8_unchecked(&buf[..utf8.valid_up_to()]) - }); - remaining = buf[utf8.valid_up_to()..].len(); - buf.rotate_left(utf8.valid_up_to()); + + let shell_title = Arc::new(std::sync::Mutex::from(None)); + let pre_parser = Arc::new(std::sync::Mutex::from(vt100::Parser::new(7, 144, 0))); + + { + let shell_title = shell_title.clone(); + let pre_parser = pre_parser.clone(); + let current_progress = current_progress.clone(); + let paused = paused.clone(); + + std::thread::spawn(move || { + let mut buf = [0; PTY_BUFFER_SIZE]; + let mut remaining = 0; + + lazy_static::lazy_static! { + static ref PROGRESS_PARSING_PERCENT_REGEX: Regex = Regex::new(r"(([0-9]*[.])?[0-9]+%)").unwrap(); + static ref PROGRESS_PARSING_FRAC_REGEX: Regex = Regex::new(r"(\d+/\d+)").unwrap(); + } + + loop { + if !paused.load(Ordering::Relaxed) { + buf[remaining..].fill(0); + + if reader + .read(&mut buf[remaining..]) + .is_ok_and(|bytes| bytes > 0) + { + let mut pre_parser = pre_parser.lock().unwrap(); + let previous_cached_content = pre_parser.screen().contents(); + match std::str::from_utf8(&buf) { + Ok(parsed_buf) => { + pre_parser.process(parsed_buf.as_bytes()); + on_read(parsed_buf); + remaining = 0; + } + Err(utf8) => { + pre_parser.process(&buf[..utf8.valid_up_to()]); + on_read(unsafe { + std::str::from_utf8_unchecked(&buf[..utf8.valid_up_to()]) + }); + remaining = buf[utf8.valid_up_to()..].len() + - (PTY_BUFFER_SIZE + - utf8.valid_up_to() + - utf8 + .error_len() + .unwrap_or(PTY_BUFFER_SIZE - utf8.valid_up_to())); + buf.rotate_left(utf8.valid_up_to()); + } } - } - if pre_parser.screen().contents() != previous_cached_content { - on_displayed_content_updated(); + if pre_parser.screen().contents() != previous_cached_content { + on_displayed_content_updated(); - if progress_tracking && !pre_parser.screen().alternate_screen() { - let fetched_progress = PROGRESS_PARSING_REGEX - .find_iter(&pre_parser.screen().contents()) - .last() - .map(|m| { - if m.as_str().ends_with('%') { + if progress_tracking && !pre_parser.screen().alternate_screen() { + let fetched_progress = PROGRESS_PARSING_PERCENT_REGEX + .find_iter(&pre_parser.screen().contents()) + .map(|m| { m.as_str() .split_once('%') .and_then(|(number, _)| number.parse::().ok()) - .map(|parsed_number| { - (parsed_number.ceil() as u64 % 100) as u8 + .map(|progress| { + (progress.ceil() as u64 % 100) as u8 }) .unwrap_or_default() - } else { - let splitted = - m.as_str().split_once('/').unwrap_or_default(); - - let numerator = - splitted.0.parse::().unwrap_or(0f64); - let denominator = - splitted.1.parse::().unwrap_or(1f64); - - ((((numerator / denominator) * 100f64).ceil() as u64) - % 100) - as u8 + }) + .filter(|progress| *progress > 0) + .last() + .map_or_else( + || { + PROGRESS_PARSING_FRAC_REGEX + .find_iter(&pre_parser.screen().contents()) + .map(|m| { + let parts = m + .as_str() + .split_once('/') + .unwrap_or_default(); + + let numerator = + parts.0.parse::().unwrap_or(0); + let denominator = + parts.1.parse::().unwrap_or(1); + + if numerator == 0 + || numerator >= denominator + { + 0 + } else { + u8::try_from( + numerator * 100 / denominator, + ) + .unwrap_or_default() + .max(1) + } + }) + .filter(|progress| *progress > 0) + .last() + }, + Some, + ) + .unwrap_or_default(); + + if fetched_progress != current_progress.load(Ordering::Relaxed) + { + current_progress.store(fetched_progress, Ordering::Relaxed); + + if progress_report { + on_action_progress(fetched_progress); } - }) - .unwrap_or_default(); - - if fetched_progress != current_progress.load(Ordering::Relaxed) { - current_progress.store(fetched_progress, Ordering::Relaxed); + } + } else if current_progress.load(Ordering::Relaxed) != 0 + && progress_tracking + { + current_progress.store(0, Ordering::Relaxed); if progress_report { - on_action_progress(fetched_progress); + on_action_progress(0); } } - } else if current_progress.load(Ordering::Relaxed) != 0 - && progress_tracking - { - current_progress.store(0, Ordering::Relaxed); + } - if progress_report { - on_action_progress(0); + if title_formatter.options.shell_title { + if let Ok(mut lock) = shell_title.lock() { + *lock = Some(pre_parser.screen().title().to_owned()); } } } + } - if title_formatter.options.shell_title { - if let Ok(mut lock) = cloned_sync_shell_title.lock() { - *lock = Some(pre_parser.screen().title().to_owned()); - } - } + if exit_receiver.try_recv().is_ok() { + break; } } + }); + } - if exit_receiver.try_recv().is_ok() { - break; - } - } - }); + { + let closed = closed.clone(); + let child = child.clone(); + let leader_process = leader_process.clone(); - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_millis(20)); - let mut current_title = String::new(); + #[cfg(target_family = "unix")] + let master = master.clone(); - on_tab_title_update(&title_formatter.format(&FormatterParams::default())); + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_millis(20)); + let mut current_title = String::new(); - loop { - interval.tick().await; + on_tab_title_update(&title_formatter.format(&FormatterParams::default())); - if matches!(cloned_child.lock().await.try_wait(), Ok(Some(_))) { - exit_sender.send(()).ok(); + loop { + interval.tick().await; - closed_cloned.store(true, Ordering::Relaxed); - once_exit(); + if matches!(child.lock().await.try_wait(), Ok(Some(_))) { + exit_sender.send(()).ok(); - break; - } + closed.store(true, Ordering::Relaxed); + once_exit(); - #[cfg(target_family = "unix")] - let process_leader_pid = cloned_master.lock().await.process_group_leader(); - #[cfg(target_os = "windows")] - let process_leader_pid = Some(utils::get_leader_pid(shell_pid)); - - if let Some(fetched_leader_pid) = process_leader_pid { - let mut fetched_leader_process = Option::None; - let mut fetched_pwd = Option::None; - let current_progress = cloned_current_progress.load(Ordering::Relaxed); - let fetched_progress = if current_progress > 0 { - Some(current_progress) - } else { - None - }; - let fetched_shell_title = if title_formatter.options.shell_title { - let sync_fetched_shell_title = sync_shell_title.clone(); - tokio::task::spawn_blocking(move || { - sync_fetched_shell_title.lock().unwrap().clone() - }) - .await - .unwrap() - .map(|title| title.to_string()) - } else { - None - }; - - let mut fetchers: Vec + Send>>> = - vec![Box::pin(super::utils::get_process_title( - fetched_leader_pid, - &mut fetched_leader_process, - ))]; - - if title_formatter.options.pwd { - fetchers.push(Box::pin(super::utils::get_process_working_dir( - fetched_leader_pid, - &mut fetched_pwd, - ))); + break; } - let fetched_data_count = fetchers.len(); - join_all(fetchers).await; - - if fetched_data_count > 1 - || title_formatter.options.action_progress - || title_formatter.options.shell_title - || title_formatter.options.leader_process - { - let generated_title = title_formatter.format(&FormatterParams { - pwd: fetched_pwd, - leader_process: fetched_leader_process.clone(), - progress: fetched_progress, - shell_title: fetched_shell_title, - }); - - if generated_title != current_title { - on_tab_title_update(&generated_title); - current_title = generated_title; + #[cfg(target_family = "unix")] + let process_leader_pid = master.lock().await.process_group_leader(); + #[cfg(target_os = "windows")] + let process_leader_pid = Some(utils::get_leader_pid(shell_pid)); + + if let Some(fetched_leader_pid) = process_leader_pid { + let mut fetched_leader_process = None; + let mut fetched_pwd = None; + let mut fetched_short_pwd = None; + let fetched_progress = match current_progress.load(Ordering::Relaxed) { + 0 => None, + value => Some(value), + }; + let fetched_shell_title = if title_formatter.options.shell_title { + let sync_fetched_shell_title = shell_title.clone(); + tokio::task::spawn_blocking(move || { + sync_fetched_shell_title.lock().unwrap().clone() + }) + .await + .unwrap() + } else { + None + }; + + let mut fetchers: Vec + Send>>> = + vec![Box::pin(super::utils::get_process_title( + fetched_leader_pid, + &mut fetched_leader_process, + ))]; + + if title_formatter.options.pwd { + fetchers.push(Box::pin(super::utils::get_process_working_dir( + fetched_leader_pid, + &mut fetched_pwd, + ))); + } + if title_formatter.options.short_pwd { + fetchers.push(Box::pin(super::utils::get_process_short_working_dir( + fetched_leader_pid, + &mut fetched_short_pwd, + ))); } - } - if let Some(fetched_leader_process) = fetched_leader_process { - *cloned_leader_process.lock().await = fetched_leader_process; + let fetched_data_count = fetchers.len(); + join_all(fetchers).await; + + if fetched_data_count > 1 + || title_formatter.options.action_progress + || title_formatter.options.shell_title + || title_formatter.options.leader_process + { + let generated_title = title_formatter.format(&FormatterParams { + pwd: fetched_pwd, + short_pwd: fetched_short_pwd, + leader_process: fetched_leader_process.clone(), + progress: fetched_progress, + shell_title: fetched_shell_title, + }); + + if generated_title != current_title { + on_tab_title_update(&generated_title); + current_title = generated_title; + } + } + + if let Some(fetched_leader_process) = fetched_leader_process { + *leader_process.lock().await = fetched_leader_process; + } } } - } - }); + }); + } Ok(Self { writer, child, master, paused, - leader_name, + pre_parser, + leader_name: leader_process, closed, }) } @@ -347,6 +391,11 @@ impl Pty { } pub async fn resize(&self, cols: u16, rows: u16) -> Result<(), PtyError> { + let pre_parser = self.pre_parser.clone(); + tokio::task::spawn_blocking(move || pre_parser.lock().unwrap().set_size(8, cols)) + .await + .ok(); + self.master .lock() .await diff --git a/src-tauri/src/pty/utils.rs b/src-tauri/src/pty/utils.rs index 64ee933..8ce1134 100644 --- a/src-tauri/src/pty/utils.rs +++ b/src-tauri/src/pty/utils.rs @@ -71,6 +71,13 @@ pub async fn get_process_title(pid: i32, fetched_title: &mut Option) { if process_leader_title == "tokio-runtime-w" { None + } else if process_leader_title == "sudo" { + std::fs::read_to_string(format!("/proc/{pid}/cmdline")).map_or( + Some(process_leader_title), + |cmdline| { + Some(cmdline.split('\0').take(2).collect::>().join(" ")) + }, + ) } else { Some(process_leader_title) } @@ -168,9 +175,7 @@ pub async fn get_process_working_dir(pid: u32, fetched_pwd: &mut Option) upp.CurrentDirectory.DosPath.Length as usize, Some(&mut path_len), ) - .map(|()| { - String::from_utf16_lossy(&path) - }) + .map(|()| String::from_utf16_lossy(&path)) } }) .ok(); @@ -178,3 +183,32 @@ pub async fn get_process_working_dir(pid: u32, fetched_pwd: &mut Option) unsafe { CloseHandle(handle).ok() }; } } + +#[cfg(target_family = "unix")] +pub async fn get_process_short_working_dir(pid: i32, fetched_short_pwd: &mut Option) { + use std::env; + + *fetched_short_pwd = tokio::task::spawn_blocking(move || { + std::fs::read_link(format!("/proc/{pid}/cwd")).map_or(None, |path| { + Some( + if env::var_os("HOME").is_some_and(|home| home == path.as_os_str()) { + String::from("~") + } else { + path.file_name().map_or_else( + || String::from("/"), + |dir| { + dir.to_os_string().to_string_lossy().to_string() + }, + ) + }, + ) + }) + }) + .await + .unwrap(); +} + +#[cfg(target_os = "windows")] +pub async fn get_process_short_working_dir(pid: u32, fetched_pwd: &mut Option) { + todo!() +} diff --git a/src/style/tab.scss b/src/style/tab.scss index 21652b4..633776d 100644 --- a/src/style/tab.scss +++ b/src/style/tab.scss @@ -44,7 +44,7 @@ width: calc(100% - 64px); text-align: center; font-weight: 600; - line-height: 12px; + line-height: 14px; } .close { diff --git a/src/ts/class/panes.ts b/src/ts/class/panes.ts index 0761219..f357447 100644 --- a/src/ts/class/panes.ts +++ b/src/ts/class/panes.ts @@ -48,7 +48,7 @@ export class TerminalPane { } async initializeTerm(customKeyEventHanlder: ((e: KeyboardEvent, term: Xterm) => boolean), toaster: Toaster, onUnreadData: (() => void), onProgressUpdate: ((progress: number) => void)) { - let terminal = new Terminal(this.id, this.profile.terminalOptions, this.profile.theme, customKeyEventHanlder, toaster, onUnreadData, onProgressUpdate); + let terminal = new Terminal(this.id, this.profile.terminalOptions, this.profile.theme, this.profile.backgroundTransparency < 100, customKeyEventHanlder, toaster, onUnreadData, onProgressUpdate); await terminal.launch(this.element.querySelector(".internal-term")!, this.profile.uuid); diff --git a/src/ts/class/terminal.ts b/src/ts/class/terminal.ts index 729cd19..1dd87b5 100644 --- a/src/ts/class/terminal.ts +++ b/src/ts/class/terminal.ts @@ -18,15 +18,18 @@ export class Terminal { disposeProgressTracker: UnlistenFn | undefined = undefined; - constructor(id: string, options: TerminalOptions, theme: TerminalTheme, customKeyEventHandler: ((e: KeyboardEvent, term: Xterm) => boolean), toaster: Toaster, onNewDisplayedDataReceived: (() => void), onProgressUpdate: ((progress: number) => void)) { + constructor(id: string, options: TerminalOptions, theme: TerminalTheme, transparency: boolean, customKeyEventHandler: ((e: KeyboardEvent, term: Xterm) => boolean), toaster: Toaster, onNewDisplayedDataReceived: (() => void), onProgressUpdate: ((progress: number) => void)) { theme = Object.assign({}, theme); - theme.background = "rgba(0,0,0,0)"; + + if (transparency) { + theme.background = "rgba(0,0,0,0)"; + } this.id = id; this.term = new Xterm({ allowProposedApi: true, fontFamily: "Fira Code, monospace", - allowTransparency: true, + allowTransparency: transparency, fontSize: options.fontSize, drawBoldTextInBrightColors: options.drawBoldInBright, cursorBlink: options.cursorBlink, From 248e5df900ade949c7c0f963e8a3951d4b1f7f9f Mon Sep 17 00:00:00 2001 From: Squitch Date: Tue, 13 Feb 2024 18:55:33 +0100 Subject: [PATCH 7/9] :sparkles: Add short_pwd support as a tab title format on Windows :bug: Remove trailing slashes for pwd format of tab title --- src-tauri/src/pty/utils.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/pty/utils.rs b/src-tauri/src/pty/utils.rs index 8ce1134..4845611 100644 --- a/src-tauri/src/pty/utils.rs +++ b/src-tauri/src/pty/utils.rs @@ -165,7 +165,6 @@ pub async fn get_process_working_dir(pid: u32, fetched_pwd: &mut Option) }) .and_then(|(handle, upp)| { let mut path: Vec = vec![0; (upp.CurrentDirectory.DosPath.Length / 2) as usize]; - let mut path_len = 0; unsafe { ReadProcessMemory( @@ -173,9 +172,9 @@ pub async fn get_process_working_dir(pid: u32, fetched_pwd: &mut Option) upp.CurrentDirectory.DosPath.Buffer.as_ptr() as *mut c_void, path.as_mut_ptr() as *mut c_void, upp.CurrentDirectory.DosPath.Length as usize, - Some(&mut path_len), + None, ) - .map(|()| String::from_utf16_lossy(&path)) + .map(|()| String::from_utf16_lossy(&path[..path.len() - 1])) } }) .ok(); @@ -196,9 +195,7 @@ pub async fn get_process_short_working_dir(pid: i32, fetched_short_pwd: &mut Opt } else { path.file_name().map_or_else( || String::from("/"), - |dir| { - dir.to_os_string().to_string_lossy().to_string() - }, + |dir| dir.to_os_string().to_string_lossy().to_string(), ) }, ) @@ -209,6 +206,19 @@ pub async fn get_process_short_working_dir(pid: i32, fetched_short_pwd: &mut Opt } #[cfg(target_os = "windows")] -pub async fn get_process_short_working_dir(pid: u32, fetched_pwd: &mut Option) { - todo!() +pub async fn get_process_short_working_dir(pid: u32, fetched_short_pwd: &mut Option) { + let mut fetched_pwd = None; + get_process_working_dir(pid, &mut fetched_pwd).await; + + *fetched_short_pwd = fetched_pwd.map(|path| { + let path = std::path::PathBuf::from(&path); + if dirs_next::home_dir().is_some_and(|home| path == home) { + String::from("~") + } else { + path.file_name().map_or_else( + || path.to_str().unwrap_or("\\").to_owned(), + |dir| dir.to_os_string().to_string_lossy().to_string(), + ) + } + }); } From afa0fdd97fc0d1df60b2d330926598586629cec6 Mon Sep 17 00:00:00 2001 From: SquitchYT Date: Tue, 20 Feb 2024 20:57:57 +0100 Subject: [PATCH 8/9] :rotating_light: Apply clippy suggestions --- src-tauri/src/pty/pty.rs | 12 ++++++------ src-tauri/src/pty/utils.rs | 20 ++++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src-tauri/src/pty/pty.rs b/src-tauri/src/pty/pty.rs index db6d140..c5223d9 100644 --- a/src-tauri/src/pty/pty.rs +++ b/src-tauri/src/pty/pty.rs @@ -23,8 +23,6 @@ use crate::pty::utils; #[cfg(target_os = "windows")] use regex_lite::Captures; -const PTY_BUFFER_SIZE: usize = 4096; - pub struct Pty { writer: Box, child: Arc>>, @@ -136,7 +134,7 @@ impl Pty { let paused = paused.clone(); std::thread::spawn(move || { - let mut buf = [0; PTY_BUFFER_SIZE]; + let mut buf = [0; 4096]; let mut remaining = 0; lazy_static::lazy_static! { @@ -166,11 +164,11 @@ impl Pty { std::str::from_utf8_unchecked(&buf[..utf8.valid_up_to()]) }); remaining = buf[utf8.valid_up_to()..].len() - - (PTY_BUFFER_SIZE + - (buf.len() - utf8.valid_up_to() - utf8 .error_len() - .unwrap_or(PTY_BUFFER_SIZE - utf8.valid_up_to())); + .unwrap_or(buf.len() - utf8.valid_up_to())); buf.rotate_left(utf8.valid_up_to()); } } @@ -186,7 +184,9 @@ impl Pty { .split_once('%') .and_then(|(number, _)| number.parse::().ok()) .map(|progress| { - (progress.ceil() as u64 % 100) as u8 + (progress.ceil() as u64 % 100) + .try_into() + .unwrap_or_default() }) .unwrap_or_default() }) diff --git a/src-tauri/src/pty/utils.rs b/src-tauri/src/pty/utils.rs index 4845611..3f7403a 100644 --- a/src-tauri/src/pty/utils.rs +++ b/src-tauri/src/pty/utils.rs @@ -185,20 +185,16 @@ pub async fn get_process_working_dir(pid: u32, fetched_pwd: &mut Option) #[cfg(target_family = "unix")] pub async fn get_process_short_working_dir(pid: i32, fetched_short_pwd: &mut Option) { - use std::env; - *fetched_short_pwd = tokio::task::spawn_blocking(move || { std::fs::read_link(format!("/proc/{pid}/cwd")).map_or(None, |path| { - Some( - if env::var_os("HOME").is_some_and(|home| home == path.as_os_str()) { - String::from("~") - } else { - path.file_name().map_or_else( - || String::from("/"), - |dir| dir.to_os_string().to_string_lossy().to_string(), - ) - }, - ) + Some(if dirs_next::home_dir().is_some_and(|home| home == path) { + String::from("~") + } else { + path.file_name().map_or_else( + || String::from("/"), + |dir| dir.to_os_string().to_string_lossy().to_string(), + ) + }) }) }) .await From 5e75b6572f2e24f2714a713f4190d733571fe30f Mon Sep 17 00:00:00 2001 From: SquitchYT Date: Mon, 26 Feb 2024 19:56:32 +0100 Subject: [PATCH 9/9] :bug: Rename shortcut action in snake_case to match other option properties :art: Clean codebase --- src-tauri/src/configuration/deserialized.rs | 1 + src/style/style.scss | 2 - src/ts/app.ts | 2 +- src/ts/class/tab.ts | 7 ++-- src/ts/manager/tabs.ts | 46 ++++++++------------- 5 files changed, 24 insertions(+), 34 deletions(-) diff --git a/src-tauri/src/configuration/deserialized.rs b/src-tauri/src/configuration/deserialized.rs index e268bcf..feaa217 100644 --- a/src-tauri/src/configuration/deserialized.rs +++ b/src-tauri/src/configuration/deserialized.rs @@ -290,6 +290,7 @@ pub struct Shortcut { } #[derive(Debug, Clone, Deserialize)] +#[serde(rename_all(deserialize = "snake_case"))] pub enum ShortcutAction { CloseFocusedTab, CloseAllTabs, diff --git a/src/style/style.scss b/src/style/style.scss index 1464b4c..6d6ae70 100644 --- a/src/style/style.scss +++ b/src/style/style.scss @@ -6,8 +6,6 @@ @import "term"; @import "tab"; -@import "../../node_modules/xterm/css/xterm.css"; - @font-face { font-family: "Inter"; diff --git a/src/ts/app.ts b/src/ts/app.ts index 5d0670b..f0b6cbc 100644 --- a/src/ts/app.ts +++ b/src/ts/app.ts @@ -228,7 +228,7 @@ export class App { this.onTerminalPaneInput(paneId, content); }); - this.tabsManager.openNewTab(profile!.name, viewId); + this.tabsManager.openNewTab(viewId); if (focus) { this.tabsManager.select(viewId); } }).catch((err) => { diff --git a/src/ts/class/tab.ts b/src/ts/class/tab.ts index afc998c..751a047 100644 --- a/src/ts/class/tab.ts +++ b/src/ts/class/tab.ts @@ -1,4 +1,5 @@ import tabIcon from "../../assets/default-tab.png"; +import "xterm/css/xterm.css"; export class Tab { element: HTMLElement; @@ -9,7 +10,7 @@ export class Tab { removedProgressTimeout: number = 0; - title: string; + title: string = ""; constructor(index: number, id: string, onClose:((id: string) => void)) { @@ -17,7 +18,7 @@ export class Tab { this.index = index; this.element = this.generateComponents(); - this.title = ""; + this.setTitle(""); new ResizeObserver(() => { if (this.element.clientWidth <= 34) { @@ -32,7 +33,7 @@ export class Tab { setTitle(title: string) { this.title = title; - (this.element.querySelector(".title")! as HTMLSpanElement).innerText = title; + (this.element.querySelector(".title")! as HTMLSpanElement).innerText = title != "" ? title : "Untitled tab"; } setProgress(value: number) { diff --git a/src/ts/manager/tabs.ts b/src/ts/manager/tabs.ts index 403a4f8..17234e9 100644 --- a/src/ts/manager/tabs.ts +++ b/src/ts/manager/tabs.ts @@ -32,7 +32,7 @@ export class TabsManager { document.addEventListener("mouseup", (_) => this.stopDragging()); } - openNewTab(title: string, id: string) : string { + openNewTab(id: string) : string { let tab = new Tab(this.tabs.length + 1, id, (id) => this.requestTabClosing(id)) tab.element.addEventListener("mousedown", (e) => {this.focusAndStartDragging(e, tab)}); @@ -49,7 +49,6 @@ export class TabsManager { } tab.element.style.order = String(this.tabs.length + 1); - tab.setTitle(title); this.tabs.push(tab); this.target.appendChild(tab.element); @@ -67,9 +66,9 @@ export class TabsManager { setTitle(uuid: string, title: string) { - let tmp = this.tabs.find(tab => tab.id === uuid); - if (tmp) { - tmp.setTitle(title); + let tab = this.tabs.find(tab => tab.id === uuid); + if (tab) { + tab.setTitle(title); this.titleUpdatedListener.forEach((listener) => { listener(uuid); }) @@ -77,19 +76,19 @@ export class TabsManager { } setprogress(uuid: string, progress: number) { - let tmp = this.tabs.find(tab => tab.id === uuid); - if (tmp) { - tmp.setProgress(progress); + let tab = this.tabs.find(tab => tab.id === uuid); + if (tab) { + tab.setProgress(progress); } } setHightlight(uuid: string, visible: boolean) { - let tmp = this.tabs.find(tab => tab.id === uuid); - if (tmp) { - if (visible && this.selectedTab != tmp) { - tmp.setHighlight(true); + let tab = this.tabs.find(tab => tab.id === uuid); + if (tab) { + if (visible && this.selectedTab != tab) { + tab.setHighlight(true); } else { - tmp.setHighlight(false); + tab.setHighlight(false); } } } @@ -149,25 +148,16 @@ export class TabsManager { select(uuid: string) : void; select(index: number) : void; - select(tab: unknown) { - let tabToFocus: Tab | undefined = undefined; - - if (typeof tab === "string") { - let uuid = tab; - tabToFocus = this.tabs.find(tab => tab.id === uuid); - - } else { - let index = tab; - tabToFocus = this.tabs.find(tab => tab.index === index); - } + select(selector: string | number) { + let tab = this.tabs.find(tab => tab.id === selector || tab.index == selector); - if (tabToFocus) { + if (tab) { this.selectedTab?.element.classList.remove("selected"); - this.selectedTab = tabToFocus; - tabToFocus.element.classList.add("selected"); + this.selectedTab = tab; + tab.element.classList.add("selected"); this.tabFocusedListener.forEach((listener) => { - listener(tabToFocus!.id); + listener(tab!.id); }) } }