From 2fdf4e0fc8584c4a136f3d2706b8f9cfe93db1a7 Mon Sep 17 00:00:00 2001 From: SquitchYT Date: Sat, 9 Dec 2023 22:52:12 +0100 Subject: [PATCH 1/4] :recycle: Refactor codebase :zap: Revamp toast notifications :zap: Improve error handling --- src-tauri/Cargo.lock | 11 +- src-tauri/Cargo.toml | 3 +- src-tauri/src/command/app.rs | 4 - src-tauri/src/command/mod.rs | 4 - src-tauri/src/command/option.rs | 7 - src-tauri/src/command/term.rs | 112 ------- src-tauri/src/command/window.rs | 6 - src-tauri/src/common/commands/mod.rs | 7 + src-tauri/src/common/commands/pty.rs | 148 +++++++++ src-tauri/src/common/commands/utils.rs | 15 + src-tauri/src/common/commands/window.rs | 4 + src-tauri/src/common/error.rs | 22 ++ src-tauri/src/common/errors.rs | 35 --- src-tauri/src/common/mod.rs | 4 +- src-tauri/src/common/payloads.rs | 12 +- src-tauri/src/common/states.rs | 6 + src-tauri/src/common/utils.rs | 99 ++---- src-tauri/src/configuration/deserialized.rs | 8 +- src-tauri/src/configuration/partial.rs | 9 +- src-tauri/src/configuration/types.rs | 1 + src-tauri/src/lib.rs | 2 - src-tauri/src/main.rs | 96 +++--- src-tauri/src/pty.rs | 317 -------------------- src-tauri/src/pty/mod.rs | 2 + src-tauri/src/pty/pty.rs | 175 +++++++++++ src-tauri/src/pty/utils.rs | 27 ++ src-tauri/src/state/mod.rs | 1 - src-tauri/src/state/pty_manager.rs | 80 ----- src/index.html | 5 +- src/style/constants.scss | 7 +- src/style/style.scss | 23 +- src/style/tab.scss | 5 +- src/style/toasts.scss | 102 ++++--- src/ts/{manager/view.ts => app.ts} | 80 ++--- src/ts/class/panes.ts | 15 +- src/ts/class/terminal.ts | 29 +- src/ts/class/views.ts | 20 +- src/ts/main.ts | 26 +- src/ts/manager/toast.ts | 49 +-- 39 files changed, 698 insertions(+), 880 deletions(-) delete mode 100644 src-tauri/src/command/app.rs delete mode 100644 src-tauri/src/command/mod.rs delete mode 100644 src-tauri/src/command/option.rs delete mode 100644 src-tauri/src/command/term.rs delete mode 100644 src-tauri/src/command/window.rs create mode 100644 src-tauri/src/common/commands/mod.rs create mode 100644 src-tauri/src/common/commands/pty.rs create mode 100644 src-tauri/src/common/commands/utils.rs create mode 100644 src-tauri/src/common/commands/window.rs create mode 100644 src-tauri/src/common/error.rs delete mode 100644 src-tauri/src/common/errors.rs create mode 100644 src-tauri/src/common/states.rs delete mode 100644 src-tauri/src/pty.rs create mode 100644 src-tauri/src/pty/mod.rs create mode 100644 src-tauri/src/pty/pty.rs create mode 100644 src-tauri/src/pty/utils.rs delete mode 100644 src-tauri/src/state/mod.rs delete mode 100644 src-tauri/src/state/pty_manager.rs rename src/ts/{manager/view.ts => app.ts} (75%) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index db04dd3..40a0752 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3151,6 +3151,7 @@ dependencies = [ "signal-hook-tokio", "tauri", "tauri-build", + "thiserror", "tokio", "uuid 1.3.0", "window-vibrancy", @@ -3170,22 +3171,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.105", + "syn 2.0.39", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6d0c8b7..247ada8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,7 +22,7 @@ infer = "0.12.0" tokio = { version = "1.25.0", features = ["full"] } dirs-next = "2.0.0" uuid = "1.3.0" -remoteprocess = "0.4.11" +thiserror = "1.0.50" [target.'cfg(unix)'.dependencies] signal-hook = "0.3.17" @@ -33,6 +33,7 @@ futures = "0.3.29" lazy_static = "1.4.0" regex = "1.8.4" window-vibrancy = "0.3.2" +remoteprocess = "0.4.11" [features] default = ["custom-protocol"] diff --git a/src-tauri/src/command/app.rs b/src-tauri/src/command/app.rs deleted file mode 100644 index 9382b81..0000000 --- a/src-tauri/src/command/app.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[tauri::command] -pub fn close_app(app: tauri::AppHandle) { - app.exit(0); -} diff --git a/src-tauri/src/command/mod.rs b/src-tauri/src/command/mod.rs deleted file mode 100644 index 7fd2f03..0000000 --- a/src-tauri/src/command/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod app; -pub mod option; -pub mod term; -pub mod window; diff --git a/src-tauri/src/command/option.rs b/src-tauri/src/command/option.rs deleted file mode 100644 index 0704e1a..0000000 --- a/src-tauri/src/command/option.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::configuration::deserialized::Option; -use std::sync::{Arc, Mutex}; - -#[tauri::command] -pub async fn get_configuration(option: tauri::State<'_, Arc>>) -> Result { - Ok(option.lock().unwrap().clone()) -} diff --git a/src-tauri/src/command/term.rs b/src-tauri/src/command/term.rs deleted file mode 100644 index cd918ce..0000000 --- a/src-tauri/src/command/term.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::configuration::deserialized::Option; -use crate::state::pty_manager::PtyManager; -use std::sync::{Arc, Mutex}; - -use crate::common::errors::PtyError; - -#[tauri::command] -pub async fn terminal_input( - pty_manager: tauri::State<'_, Mutex>, - id: String, - content: String, -) -> Result<(), PtyError> { - if let Ok(mut pty_manager) = pty_manager.lock() { - pty_manager.write(&id, &content)?; - Ok(()) - } else { - Err(PtyError::ManagerUnresponding) - } -} - -#[tauri::command] -pub async fn create_terminal( - app: tauri::AppHandle, - pty_manager: tauri::State<'_, Mutex>, - cols: u16, - rows: u16, - id: String, - profile_uuid: String, - option: tauri::State<'_, Arc>>, -) -> Result<(), PtyError> { - if let Some(profile) = option - .lock() - .unwrap() - .profiles - .iter() - .find(|profile| profile.uuid == profile_uuid) - { - if let Ok(mut pty_manager) = pty_manager.lock() { - if pty_manager.app.is_none() { - pty_manager.app = Some(Arc::new(app)); - } - - pty_manager.create_pty(cols, rows, id, profile.clone())?; - Ok(()) - } else { - Err(PtyError::ManagerUnresponding) - } - } else { - todo!() - } -} - -#[tauri::command] -pub async fn resize_terminal( - pty_manager: tauri::State<'_, Mutex>, - id: String, - cols: u16, - rows: u16, -) -> Result<(), PtyError> { - if let Ok(mut pty_manager) = pty_manager.lock() { - pty_manager.resize(&id, cols, rows)?; - Ok(()) - } else { - Err(PtyError::ManagerUnresponding) - } -} - -#[tauri::command] -pub async fn check_close_availability( - pty_manager: tauri::State<'_, Mutex>, - app_config: tauri::State<'_, Arc>>, - id: String, -) -> Result { - let app_config = app_config.lock().unwrap(); - - if app_config.close_confirmation.tab { - if let Ok(pty_manager) = pty_manager.lock() { - Ok(app_config - .close_confirmation - .excluded_process - .contains(&pty_manager.get_running_process(&id)?)) - } else { - Err(PtyError::ManagerUnresponding) - } - } else { - Ok(true) - } -} - -#[tauri::command] -pub async fn close_terminal( - pty_manager: tauri::State<'_, Mutex>, - id: String, -) -> Result<(), PtyError> { - if let Ok(mut pty_manager) = pty_manager.lock() { - pty_manager.close(&id)?; - Ok(()) - } else { - Err(PtyError::ManagerUnresponding) - } -} - -#[tauri::command] -pub async fn get_pty_title( - pty_manager: tauri::State<'_, Mutex>, - id: String, -) -> Result { - pty_manager - .lock() - .or(Err(PtyError::ManagerUnresponding))? - .get_running_process(&id) -} diff --git a/src-tauri/src/command/window.rs b/src-tauri/src/command/window.rs deleted file mode 100644 index cf39ea4..0000000 --- a/src-tauri/src/command/window.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[tauri::command] -pub fn close_window(window: tauri::Window) -> Result<(), ()> { - window.close().unwrap(); - - Ok(()) -} diff --git a/src-tauri/src/common/commands/mod.rs b/src-tauri/src/common/commands/mod.rs new file mode 100644 index 0000000..299d4b6 --- /dev/null +++ b/src-tauri/src/common/commands/mod.rs @@ -0,0 +1,7 @@ +mod pty; +mod utils; +mod window; + +pub use pty::*; +pub use utils::*; +pub use window::*; diff --git a/src-tauri/src/common/commands/pty.rs b/src-tauri/src/common/commands/pty.rs new file mode 100644 index 0000000..d3ddf76 --- /dev/null +++ b/src-tauri/src/common/commands/pty.rs @@ -0,0 +1,148 @@ +use crate::common::payloads::{PtySendData, PtyTitleChanged}; +use crate::common::states::Ptys; +use crate::configuration::deserialized::Option; +use std::sync::Arc; +use tauri::Manager; +use tokio::sync::Mutex; + +use crate::common::error::PtyError; + +use crate::pty::pty::Pty; + +#[tauri::command] +pub async fn pty_open( + app: tauri::AppHandle, + id: String, + profile_uuid: String, + 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 locked_option = option.lock().await; + let opening_profile = locked_option + .profiles + .iter() + .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, + move |readed| { + app.emit_all( + "js_pty_data", + PtySendData { + data: std::str::from_utf8(&readed).unwrap_or_default(), + id: &id_cloned, + }, + ) + .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(); + } + }, + move || { + app_cloned_twice + .emit_all("js_pty_closed", id_cloned_thrice) + .ok(); + }, + )?, + ); + + Ok(()) +} + +#[tauri::command] +pub async fn pty_close(ptys: tauri::State<'_, Ptys>, id: String) -> Result<(), PtyError> { + ptys.0 + .lock() + .await + .get(&id) + .ok_or(PtyError::UnknownPty)? + .kill() + .await +} + +#[tauri::command] +pub async fn pty_write( + id: String, + content: String, + ptys: tauri::State<'_, Ptys>, +) -> Result<(), PtyError> { + ptys.0 + .lock() + .await + .get_mut(&id) + .ok_or(PtyError::UnknownPty)? + .write(&content) +} + +#[tauri::command] +pub async fn pty_resize( + ptys: tauri::State<'_, Ptys>, + id: String, + cols: u16, + rows: u16, +) -> Result<(), PtyError> { + ptys.0 + .lock() + .await + .get(&id) + .ok_or(PtyError::UnknownPty)? + .resize(cols, rows) + .await +} + +#[tauri::command] +pub async fn pty_get_closable( + ptys: tauri::State<'_, Ptys>, + app_config: tauri::State<'_, Arc>>, + id: String, +) -> Result { + let app_config = app_config.lock().await; + + if app_config.close_confirmation.tab { + let locked_ptys = ptys.0.lock().await; + let pty = locked_ptys.get(&id).ok_or(PtyError::UnknownPty)?; + + Ok(pty.closed.load(std::sync::atomic::Ordering::Relaxed) + || app_config + .close_confirmation + .excluded_process + .contains(&*pty.title.lock().await)) + } else { + Ok(true) + } +} + +#[tauri::command] +pub async fn pty_get_title(ptys: tauri::State<'_, Ptys>, id: String) -> Result { + Ok(ptys + .0 + .lock() + .await + .get_mut(&id) + .ok_or(PtyError::UnknownPty)? + .title + .lock() + .await + .to_string()) +} diff --git a/src-tauri/src/common/commands/utils.rs b/src-tauri/src/common/commands/utils.rs new file mode 100644 index 0000000..e5df6fa --- /dev/null +++ b/src-tauri/src/common/commands/utils.rs @@ -0,0 +1,15 @@ +use crate::configuration::deserialized::Option; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[tauri::command] +pub fn utils_close_app(app: tauri::AppHandle) { + app.exit(0); +} + +#[tauri::command] +pub async fn utils_get_configuration( + option: tauri::State<'_, Arc>>, +) -> Result { + Ok(option.lock().await.clone()) +} diff --git a/src-tauri/src/common/commands/window.rs b/src-tauri/src/common/commands/window.rs new file mode 100644 index 0000000..b9ab679 --- /dev/null +++ b/src-tauri/src/common/commands/window.rs @@ -0,0 +1,4 @@ +#[tauri::command] +pub fn window_close(window: tauri::Window) { + window.close().ok(); +} diff --git a/src-tauri/src/common/error.rs b/src-tauri/src/common/error.rs new file mode 100644 index 0000000..f3bd342 --- /dev/null +++ b/src-tauri/src/common/error.rs @@ -0,0 +1,22 @@ +#[derive(Debug, thiserror::Error)] +pub enum PtyError { + #[error("There is no terminal corresponding to this ID.")] + UnknownPty, + #[error("A problem occurred while creating the terminal: {0}.")] + Creation(String), + #[error("A problem occurred while writing to the terminal: {0}.")] + Write(String), + #[error("A problem occurred while resizing the terminal: {0}.")] + Resize(String), + #[error("A problem occurred while closing the terminal: {0}.")] + Kill(String), +} + +impl serde::Serialize for PtyError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/src-tauri/src/common/errors.rs b/src-tauri/src/common/errors.rs deleted file mode 100644 index df6ba9d..0000000 --- a/src-tauri/src/common/errors.rs +++ /dev/null @@ -1,35 +0,0 @@ -#[derive(Debug)] -pub enum PtyError { - Kill(String), - Resize(String), - Write(String), - Create(String), - CloseableStatus(String), - ManagerUnresponding, -} - -impl serde::Serialize for PtyError { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} - -impl std::fmt::Display for PtyError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::Kill(r) => write!(f, "Unable to terminate the process. Reason: {}", r), - Self::Resize(r) => write!(f, "Unable to resize the given terminal. Reason: {}", r), - Self::Write(r) => write!(f, "Unable to write to the given terminal. Reason: {}", r), - Self::Create(r) => write!(f, "Unable to create a new terminal. Reason: {}", r), - Self::CloseableStatus(r) => write!( - f, - "Unable to verify if the terminal is closable. Reason: {}", - r - ), - Self::ManagerUnresponding => write!(f, "Terminal manager is unresponding."), - } - } -} diff --git a/src-tauri/src/common/mod.rs b/src-tauri/src/common/mod.rs index 34df27b..7a51d27 100644 --- a/src-tauri/src/common/mod.rs +++ b/src-tauri/src/common/mod.rs @@ -1,3 +1,5 @@ -pub mod errors; +pub mod commands; +pub mod error; pub mod payloads; +pub mod states; pub mod utils; diff --git a/src-tauri/src/common/payloads.rs b/src-tauri/src/common/payloads.rs index f451f11..01e32cd 100644 --- a/src-tauri/src/common/payloads.rs +++ b/src-tauri/src/common/payloads.rs @@ -1,11 +1,11 @@ #[derive(serde::Serialize, Clone)] -pub struct PtySendData { - pub id: String, - pub data: String, +pub struct PtySendData<'a> { + pub id: &'a str, + pub data: &'a str, } #[derive(serde::Serialize, Clone)] -pub struct PtyTitleChanged { - pub id: String, - pub title: String, +pub struct PtyTitleChanged<'a> { + pub id: &'a str, + pub title: &'a str, } diff --git a/src-tauri/src/common/states.rs b/src-tauri/src/common/states.rs new file mode 100644 index 0000000..6d150d2 --- /dev/null +++ b/src-tauri/src/common/states.rs @@ -0,0 +1,6 @@ +use crate::pty::pty::Pty; +use std::collections::HashMap; +use tokio::sync::Mutex; + +#[derive(Default)] +pub struct Ptys(pub Mutex>); diff --git a/src-tauri/src/common/utils.rs b/src-tauri/src/common/utils.rs index e625071..26aed7d 100644 --- a/src-tauri/src/common/utils.rs +++ b/src-tauri/src/common/utils.rs @@ -3,83 +3,30 @@ use crate::configuration::deserialized::TerminalTheme; #[cfg(target_os = "windows")] use std::ffi::OsString; -pub fn parse_theme(location: String) -> (Option, Option) { - let theme_path = if std::path::Path::new(&format!( - "{}/tess/themes/{}", - dirs_next::config_dir().unwrap_or_default().display(), - location - )) - .exists() - { - Some(format!( - "{}/tess/themes/{}", - dirs_next::config_dir().unwrap_or_default().display(), - location - )) - } else if std::path::Path::new(&location).exists() { - Some(location) - } else { - None - }; - - theme_path.map_or((None, None), |theme_path| { - let app_theme = if std::path::Path::new(&format!("{}/style.css", theme_path)).exists() { - Some(format!("{}/style.css", theme_path)) - } else { - None - }; - let terminal_theme = - if std::path::Path::new(&format!("{}/terminal.json", theme_path)).exists() { - Some( +#[must_use] +pub fn parse_theme(location: &str) -> (Option, Option) { + dirs_next::config_dir() + .unwrap_or_default() + .join("tess/themes") + .join(location) + .canonicalize() + .map_or((None, None), |theme_path| { + let app_theme = theme_path + .join("style.css") + .canonicalize() + .map_or(None, |app_theme_file| { + app_theme_file.to_str().map(String::from) + }); + let terminal_theme = theme_path.join("terminal.json").canonicalize().map_or( + None, + |terminal_theme_file| { serde_json::from_str( - &std::fs::read_to_string(std::path::Path::new(&format!( - "{}/terminal.json", - theme_path - ))) - .unwrap_or_default(), + &std::fs::read_to_string(terminal_theme_file).unwrap_or_default(), ) - .unwrap_or_default(), - ) - } else { - None - }; - - (app_theme, terminal_theme) - }) -} - -#[cfg(target_os = "windows")] -pub fn get_leader_process_name(process: remoteprocess::Process) -> Option { - if let Ok(childs) = process.child_processes() { - let mut childs = childs - .iter() - .filter(|tmp| { - tmp.1 == process.pid - && remoteprocess::Process::new(tmp.0) - .is_ok_and(|sub_process| sub_process.exe().is_ok()) - }) - .collect::>(); - - if childs.is_empty() { - std::path::Path::new(&process.exe().ok().unwrap_or_default()) - .file_name() - .map(|x| x.to_owned()) - } else { - childs.sort(); + .ok() + }, + ); - if let Ok(child_process) = - remoteprocess::Process::new(childs.as_slice().last().unwrap().0) - { - get_leader_process_name(child_process) - } else { - std::path::Path::new(&process.exe().ok().unwrap_or_default()) - .file_name() - .map(|x| x.to_owned()) - } - } - } else { - std::path::Path::new(&process.exe().ok().unwrap_or_default()) - .file_name() - .map(|x| x.to_owned()) - } + (app_theme, terminal_theme) + }) } diff --git a/src-tauri/src/configuration/deserialized.rs b/src-tauri/src/configuration/deserialized.rs index 19922bd..72b6f10 100644 --- a/src-tauri/src/configuration/deserialized.rs +++ b/src-tauri/src/configuration/deserialized.rs @@ -53,7 +53,7 @@ impl<'de> serde::Deserialize<'de> for Option { fn deserialize>(deserializer: D) -> Result { let partial_option = PartialOption::deserialize(deserializer).unwrap_or_default(); - let (app_theme, terminal_theme) = parse_theme(partial_option.theme.clone()); + let (app_theme, terminal_theme) = parse_theme(&partial_option.theme); let app_theme = app_theme.unwrap_or_default(); let terminal_theme = terminal_theme.unwrap_or_default(); @@ -122,7 +122,7 @@ impl<'de> serde::Deserialize<'de> for Option { let profile_theme = partial_profile.theme.map_or_else( || terminal_theme.clone(), |partial_profile_theme| { - parse_theme(partial_profile_theme) + parse_theme(&partial_profile_theme) .1 .unwrap_or_else(|| terminal_theme.clone()) }, @@ -135,7 +135,7 @@ impl<'de> serde::Deserialize<'de> for Option { background_transparency: partial_profile .background_transparency .unwrap_or(partial_option.background_transparency), - uuid: uuid::Uuid::parse_str(partial_profile.uuid.unwrap_or_default().as_str()) + uuid: uuid::Uuid::parse_str(&partial_profile.uuid.unwrap_or_default()) .unwrap_or_else(|_| uuid::Uuid::new_v4()) .to_string(), command: partial_profile.command, @@ -199,7 +199,7 @@ impl<'de> serde::Deserialize<'de> for Option { }); Ok(Self { - theme: partial_option.theme.clone(), + theme: partial_option.theme, terminal_theme, app_theme, diff --git a/src-tauri/src/configuration/partial.rs b/src-tauri/src/configuration/partial.rs index b437067..933e256 100644 --- a/src-tauri/src/configuration/partial.rs +++ b/src-tauri/src/configuration/partial.rs @@ -102,11 +102,10 @@ where Complex(BackgroundMedia), } - Ok(Representation::deserialize(data).map_or( - None, - |todo_name_to_find| match todo_name_to_find { + Ok( + Representation::deserialize(data).map_or(None, |representation| match representation { Representation::Simple(path) => BackgroundMedia::deserialize_from_string(path), Representation::Complex(background) => Some(background), - }, - )) + }), + ) } diff --git a/src-tauri/src/configuration/types.rs b/src-tauri/src/configuration/types.rs index 4c06e48..e3cba36 100644 --- a/src-tauri/src/configuration/types.rs +++ b/src-tauri/src/configuration/types.rs @@ -122,6 +122,7 @@ pub struct BackgroundMedia { } impl BackgroundMedia { + #[must_use] pub fn deserialize_from_string(value: String) -> Option { std::fs::read(&value).map_or(None, |file| { if infer::is_image(&file) { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3fae476..23cb129 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,4 @@ -pub mod command; pub mod common; pub mod configuration; pub mod logger; pub mod pty; -pub mod state; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index eb6a451..91234d0 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,17 +4,21 @@ )] use tauri::{Manager, WindowEvent}; -use tess::command::{app::*, option::*, term::*, window::*}; use tess::configuration::deserialized::Option; use tess::configuration::types::BackgroundType; use tess::logger::Logger; +use tess::common::commands; + #[cfg(target_family = "unix")] use futures::stream::StreamExt; #[cfg(target_family = "unix")] use signal_hook::consts::signal::*; -use std::sync::{Arc, Mutex}; +use tess::common::states::Ptys; + +use std::sync::Arc; +use tokio::sync::Mutex; #[tokio::main] async fn main() { @@ -33,34 +37,39 @@ async fn main() { )); let option = Arc::from(Mutex::from(if let Ok(config_file) = config_file { - // TODO: Log error - serde_json::from_str(&config_file).unwrap_or_default() + 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() + )); + } + parsed_option.unwrap_or_default() } else { Option::default() })); tauri::async_runtime::set(tokio::runtime::Handle::current()); let app = tauri::Builder::default() - .manage(Mutex::new(tess::state::pty_manager::PtyManager::new())) .manage(option.clone()) + .manage(Ptys::default()) .invoke_handler(tauri::generate_handler![ - create_terminal, - terminal_input, - resize_terminal, - close_terminal, - close_window, - close_app, - check_close_availability, - get_pty_title, - get_configuration + commands::pty_open, + commands::pty_close, + commands::pty_write, + commands::pty_resize, + commands::pty_get_title, + commands::pty_get_closable, + commands::utils_close_app, + commands::utils_get_configuration, + commands::window_close ]) - .build(tauri::generate_context!()); - - let app_handle = app.as_ref().unwrap().app_handle(); + .build(tauri::generate_context!()) + .unwrap(); - match &option.lock().unwrap().background { + match &option.lock().await.background { BackgroundType::Media(media) => { - app_handle.fs_scope().allow_file(&media.location).ok(); + app.fs_scope().allow_file(&media.location).ok(); } #[cfg(target_family = "unix")] BackgroundType::Blurred => { @@ -68,7 +77,7 @@ async fn main() { } #[cfg(target_os = "windows")] BackgroundType::Mica => { - if window_vibrancy::apply_mica(app_handle.get_window("main").unwrap()).is_err() { + if window_vibrancy::apply_mica(app.get_window("main").unwrap()).is_err() { logger.warn( "Cannot apply mica background effect. Switching back to transparent background", ); @@ -76,8 +85,7 @@ async fn main() { } #[cfg(target_os = "windows")] BackgroundType::Acrylic => { - if window_vibrancy::apply_acrylic(app_handle.get_window("main").unwrap(), None).is_err() - { + if window_vibrancy::apply_acrylic(app.get_window("main").unwrap(), None).is_err() { logger.warn("Cannot apply acrylic background effect. Switching back to transparent background"); } } @@ -88,18 +96,13 @@ async fn main() { _ => {} } - app_handle - .get_window("main") - .unwrap() - .set_decorations(true) - .ok(); + app.get_window("main").unwrap().set_decorations(true).ok(); - app_handle - .fs_scope() - .allow_file(&option.lock().unwrap().app_theme) + app.fs_scope() + .allow_file(&option.lock().await.app_theme) .ok(); - app.unwrap().run(move |app, event| match event { + app.run(move |app, event| match event { tauri::RunEvent::Ready => { #[cfg(debug_assertions)] app.get_window("main").unwrap().open_devtools(); @@ -108,20 +111,22 @@ async fn main() { { let app_cloned = app.clone(); tokio::spawn(async move { - if let Ok(mut signals_stream) = signal_hook_tokio::Signals::new(&[SIGQUIT, SIGTERM]) { - while let Some(_) = signals_stream.next().await { + 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(); if windows_count > 1 { app_cloned .get_window("main") .unwrap() - .emit("request_app_exit", windows_count) + .emit("js_app_request_exit", windows_count) .ok(); } else { app_cloned .get_window("main") .unwrap() - .emit("request_window_closing", ()) + .emit("js_window_request_closing", ()) .ok(); } } @@ -131,21 +136,26 @@ async fn main() { }); } - logger.info(&format!("Launched in {}ms", start.elapsed().as_millis())); + logger.info(&format!("Launched in {}ms.", start.elapsed().as_millis())); } tauri::RunEvent::WindowEvent { label, event: WindowEvent::CloseRequested { api, .. }, .. } => { - if option.lock().unwrap().close_confirmation.window { - app.get_window(&label) - .unwrap() - .emit("request_window_closing", "") - .ok(); + 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() - } + api.prevent_close() + } + }) + }) } _ => (), }) diff --git a/src-tauri/src/pty.rs b/src-tauri/src/pty.rs deleted file mode 100644 index fb2c08c..0000000 --- a/src-tauri/src/pty.rs +++ /dev/null @@ -1,317 +0,0 @@ -use portable_pty::Child; -use portable_pty::{native_pty_system, CommandBuilder, PtySize}; -use std::sync::{Arc, Mutex, RwLock}; - -use crate::common::errors::PtyError; -use crate::configuration::deserialized::Profile; -use tauri::{AppHandle, Manager}; - -use crate::common::payloads::{PtySendData, PtyTitleChanged}; - -use std::ffi::OsString; - -#[cfg(target_os = "windows")] -use crate::common::utils::get_leader_process_name; -#[cfg(target_os = "windows")] -use lazy_static::lazy_static; -#[cfg(target_os = "windows")] -use regex::{Captures, Regex}; -#[cfg(target_os = "windows")] -use remoteprocess::Process; - -pub struct Pty { - pub app: Arc, - pair: Arc>, - child: Option>>>, - #[cfg(target_family = "unix")] - current_leader: Arc>, - #[cfg(target_os = "windows")] - leader_programm_name: Arc>, - writer: Option>, - is_running: Arc>, - title_is_running_process: bool, - running_process: Arc>, -} - -unsafe impl Send for Pty {} -unsafe impl Sync for Pty {} - -impl Pty { - pub fn new( - app: Arc, - profile: Profile, - cols: u16, - rows: u16, - id: String, - ) -> Result { - let pty_system = native_pty_system(); - - if let Ok(pair) = pty_system.openpty(PtySize { - rows, - cols, - pixel_width: 0, - pixel_height: 0, - }) { - let mut pty = Self { - app, - pair: Arc::new(Mutex::from(pair)), - child: None, - #[cfg(target_family = "unix")] - current_leader: Arc::new(RwLock::new(0)), - #[cfg(target_os = "windows")] - leader_programm_name: Arc::new(Mutex::from(OsString::new())), - writer: None, - is_running: Arc::new(RwLock::new(true)), - title_is_running_process: profile.terminal_options.title_is_running_process, - running_process: Arc::new(Mutex::from(String::new())), - }; - - pty.run(profile, id)?; - - Ok(pty) - } else { - Err(PtyError::Create(String::from( - "An error occured when creating a new terminal.", - ))) - } - } - - pub fn run(&mut self, profile: Profile, id: String) -> Result<(), PtyError> { - #[cfg(target_os = "windows")] - lazy_static! { - static ref PROGRAMM_PARSING_REGEX: Regex = Regex::new("%([[:word:]]*)%").unwrap(); - } - - #[cfg(target_os = "windows")] - let cmd: String = PROGRAMM_PARSING_REGEX - .replace_all(&profile.command, |env_variable: &Captures| { - std::env::var(&env_variable[1]).unwrap_or_default() - }) - .into(); - - #[cfg(target_family = "unix")] - let cmd = profile.command; - - #[allow(unused_mut)] - let mut command_builder = CommandBuilder::from_argv( - cmd.split(' ') - .map(std::ffi::OsString::from) - .collect::>(), - ); - - #[cfg(target_family = "unix")] - command_builder.env("TERM", "xterm-256color"); - - let locked_pair = self.pair.lock().unwrap(); - - if let Ok(child) = locked_pair.slave.spawn_command(command_builder) { - #[cfg(target_os = "windows")] - let tmp_pty_pid = child.process_id().unwrap(); - - let child = Arc::from(Mutex::from(child)); - let child_clone = child.clone(); - - if let Ok(mut reader) = locked_pair.master.try_clone_reader() { - let app = self.app.as_ref().clone(); - let cloned_app = app.clone(); - - let id_cloned = id.clone(); - - let running_process_cloned = self.running_process.clone(); - - self.child = Some(child); - self.writer = Some(locked_pair.master.take_writer().unwrap()); - - let is_running = self.is_running.clone(); - let is_running_cloned = is_running.clone(); - - let title_is_running_process = self.title_is_running_process; - - #[cfg(target_os = "windows")] - let leader_programm_name = self.leader_programm_name.clone(); - - #[cfg(target_family = "unix")] - let cloned_pair = self.pair.clone(); - #[cfg(target_family = "unix")] - let current_process_leader_pid = self.current_leader.clone(); - - std::thread::spawn(move || { - while *is_running.read().unwrap() { - let mut buffer = [0; 4096]; - - if reader.read(&mut buffer).is_ok() { - app.emit_all( - "terminalData", - PtySendData { - data: String::from_utf8(buffer.to_vec()).unwrap_or_default(), - id: id.clone(), - }, - ) - .ok(); - } - } - }); - - std::thread::spawn(move || { - while *is_running_cloned.read().unwrap() { - if let Ok(Some(_)) = child_clone.as_ref().lock().unwrap().try_wait() { - *is_running_cloned.write().unwrap() = false; - - cloned_app - .emit_all("terminal_closed", id_cloned.clone()) - .ok(); - - break; - } - - #[cfg(target_os = "windows")] - if let Some(leader_process_name) = - get_leader_process_name(Process::new(tmp_pty_pid).unwrap()) - { - let mut leader_programm_name_locked = - leader_programm_name.lock().unwrap(); - - if leader_process_name != *leader_programm_name_locked { - *leader_programm_name_locked = leader_process_name; - - if title_is_running_process { - cloned_app - .emit_all( - "terminalTitleChanged", - PtyTitleChanged { - id: id_cloned.clone(), - title: leader_programm_name_locked - .clone() - .into_string() - .unwrap(), - }, - ) - .ok(); - } - - if let Ok(mut locked_running_process_clone) = - running_process_cloned.lock() - { - *locked_running_process_clone = - leader_programm_name_locked.clone().into_string().unwrap() - } - } - } - - #[cfg(target_family = "unix")] - { - if let Some(process_leader_pid) = - cloned_pair.lock().unwrap().master.process_group_leader() - { - if process_leader_pid != *current_process_leader_pid.read().unwrap() - { - if let Ok(mut process_leader_title) = std::fs::read_to_string( - format!("/proc/{}/comm", process_leader_pid), - ) { - process_leader_title.pop(); - - if process_leader_title != "tokio-runtime-w" { - *current_process_leader_pid.write().unwrap() = - process_leader_pid; - - if title_is_running_process { - cloned_app - .emit_all( - "terminalTitleChanged", - PtyTitleChanged { - id: id_cloned.clone(), - title: process_leader_title.clone(), - }, - ) - .ok(); - } - - if let Ok(mut locked_running_process_clone) = - running_process_cloned.lock() - { - *locked_running_process_clone = - process_leader_title; - } - } - } - } - } - } - - std::thread::sleep(std::time::Duration::from_millis(16)); - } - }); - }; - - Ok(()) - } else { - Err(PtyError::Create(String::from( - "An error occured when creating a new terminal.", - ))) - } - } - - pub fn write(&mut self, content: &str) -> Result<(), PtyError> { - if matches!(write!(self.writer.as_mut().unwrap(), "{}", content), Ok(())) { - Ok(()) - } else { - Err(PtyError::Write(String::from( - "pty doesn't accept the incoming data", - ))) - } - } - - pub fn resize(&self, cols: u16, rows: u16) -> Result<(), PtyError> { - if matches!( - self.pair - .lock() - .unwrap() - .master - .resize(portable_pty::PtySize { - cols, - rows, - pixel_height: 0, - pixel_width: 0 - }), - Ok(()) - ) { - Ok(()) - } else { - Err(PtyError::Resize(String::from( - "pty doesn't accept the resize operation.", - ))) - } - } - - pub fn close(&mut self) -> Result<(), PtyError> { - self.child.as_deref().map_or_else( - || todo!(), - |child| { - *self.is_running.write().unwrap() = false; - - child.lock().map_or_else( - |_| todo!(), - |mut child| { - if child.try_wait().is_ok_and(|x| x.is_some()) - || matches!(child.kill(), Ok(())) - { - Ok(()) - } else { - todo!() - } - }, - ) - }, - ) - } - - pub fn running_process(&self) -> Result { - Ok(self - .running_process - .lock() - .or(Err(PtyError::CloseableStatus(String::from( - "Cannot get running process.", - ))))? - .to_string()) - } -} diff --git a/src-tauri/src/pty/mod.rs b/src-tauri/src/pty/mod.rs new file mode 100644 index 0000000..91905e4 --- /dev/null +++ b/src-tauri/src/pty/mod.rs @@ -0,0 +1,2 @@ +pub mod pty; +mod utils; diff --git a/src-tauri/src/pty/pty.rs b/src-tauri/src/pty/pty.rs new file mode 100644 index 0000000..b785313 --- /dev/null +++ b/src-tauri/src/pty/pty.rs @@ -0,0 +1,175 @@ +use std::ffi::OsString; +use std::io::{Read, Write}; +use std::sync::{mpsc, Arc}; +use std::time::Duration; + +use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize}; +use tokio::sync::Mutex; + +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::common::error::PtyError; + +pub struct Pty { + writer: Box, + child: Arc>>, + master: Arc>>, + + pub title: Arc>, + pub closed: Arc, +} + +unsafe impl Send for Pty {} +unsafe impl Sync for Pty {} + +impl Pty { + pub fn build_and_run( + command: &str, + on_read: impl Fn([u8; 2048]) + std::marker::Send + 'static, + on_tab_title_update: impl Fn(&str) + std::marker::Send + 'static, + once_exit: impl FnOnce() + std::marker::Send + 'static, + ) -> Result { + #[cfg(target_os = "windows")] + lazy_static! { + static ref PROGRAMM_PARSING_REGEX: Regex = Regex::new("%([[:word:]]*)%").unwrap(); + } + + #[cfg(target_os = "windows")] + let builded_command: String = PROGRAMM_PARSING_REGEX + .replace_all(command, |env_variable: &Captures| { + std::env::var(&env_variable[1]).unwrap_or_default() + }) + .into(); + + #[allow(unused_mut)] + let mut builded_command = CommandBuilder::from_argv( + command + .split(' ') + .map(std::ffi::OsString::from) + .collect::>(), + ); + + #[cfg(target_family = "unix")] + builded_command.env("TERM", "xterm-256color"); + + let pty_pair = native_pty_system() + .openpty(PtySize::default()) + .map_err(|err| PtyError::Creation(err.to_string()))?; + + let writer = pty_pair + .master + .take_writer() + .map_err(|err| PtyError::Creation(err.to_string()))?; + let mut reader = pty_pair + .master + .try_clone_reader() + .map_err(|err| PtyError::Creation(err.to_string()))?; + let master = Arc::new(Mutex::from(pty_pair.master)); + + let child = Arc::from(Mutex::new( + pty_pair + .slave + .spawn_command(builded_command) + .map_err(|err| PtyError::Creation(err.to_string()))?, + )); + let cloned_child = child.clone(); + let cloned_master = master.clone(); + let title = Arc::new(Mutex::from(String::new())); + let cloned_title = title.clone(); + + let closed = Arc::new(AtomicBool::new(false)); + let closed_cloned = closed.clone(); + + let (exit_sender, exit_receiver) = mpsc::channel::<()>(); + + std::thread::spawn(move || loop { + let mut buf = [0; 2048]; + if reader.read(&mut buf).is_ok_and(|bytes| bytes > 0) { + on_read(buf); + } + + if exit_receiver.try_recv().is_ok() { + break; + } + }); + + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_millis(16)); + + loop { + interval.tick().await; + + if matches!(cloned_child.lock().await.try_wait(), Ok(Some(_))) { + exit_sender.send(()).ok(); + + closed_cloned.store(true, Ordering::Relaxed); + once_exit(); + + break; + } + + let process_leader_pid = cloned_master.lock().await.process_group_leader(); + + 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 current_process_title = cloned_title.lock().await; + + if fetched_title + .as_ref() + .is_some_and(|fetched_title| fetched_title != &*current_process_title) + { + let fetched_title = fetched_title.unwrap(); + + on_tab_title_update(&fetched_title); + *current_process_title = fetched_title; + } + } + } + }); + + Ok(Self { + writer, + child, + master, + title, + closed, + }) + } + + pub fn write(&mut self, content: &str) -> Result<(), PtyError> { + self.writer + .write(content.as_bytes()) + .map_err(|err| PtyError::Write(err.to_string())) + .map(|_| ()) + } + + pub async fn kill(&self) -> Result<(), PtyError> { + 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())) + } + } + + pub async fn resize(&self, cols: u16, rows: u16) -> Result<(), PtyError> { + self.master + .lock() + .await + .resize(PtySize { + rows, + cols, + ..Default::default() + }) + .map_err(|err| PtyError::Resize(err.to_string())) + } +} diff --git a/src-tauri/src/pty/utils.rs b/src-tauri/src/pty/utils.rs new file mode 100644 index 0000000..16eeb02 --- /dev/null +++ b/src-tauri/src/pty/utils.rs @@ -0,0 +1,27 @@ +#[cfg(target_os = "windows")] +pub fn get_leader_pid(foo: i32) -> i32 { + todo!() +} + +pub fn get_process_title(pid: i32) -> Option { + #[cfg(target_os = "windows")] + { + todo!() + } + + #[cfg(target_family = "unix")] + { + std::fs::read_to_string(format!("/proc/{pid}/comm")).map_or( + None, + |mut process_leader_title| { + process_leader_title.pop(); + + if process_leader_title == "tokio-runtime-w" { + None + } else { + Some(process_leader_title) + } + }, + ) + } +} diff --git a/src-tauri/src/state/mod.rs b/src-tauri/src/state/mod.rs deleted file mode 100644 index 2d4ce59..0000000 --- a/src-tauri/src/state/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod pty_manager; diff --git a/src-tauri/src/state/pty_manager.rs b/src-tauri/src/state/pty_manager.rs deleted file mode 100644 index 9815ee8..0000000 --- a/src-tauri/src/state/pty_manager.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use crate::configuration::deserialized::Profile; -use crate::pty::Pty; - -use crate::common::errors::PtyError; - -pub struct PtyManager { - ptys: HashMap, - pub app: Option>, -} - -impl Default for PtyManager { - fn default() -> Self { - Self::new() - } -} - -impl PtyManager { - #[must_use] - pub fn new() -> Self { - Self { - ptys: HashMap::new(), - app: None, - } - } - - pub fn create_pty( - &mut self, - cols: u16, - rows: u16, - id: String, - profile: Profile, - ) -> Result<(), PtyError> { - if let Some(app_ref) = self.app.as_ref() { - let pty = Pty::new(app_ref.clone(), profile, cols, rows, id.clone())?; - - self.ptys.insert(id, pty); - - Ok(()) - } else { - Err(PtyError::Create(String::from( - "Unable to give app access to the new process", - ))) - } - } - - pub fn write(&mut self, id: &str, content: &str) -> Result<(), PtyError> { - self.ptys - .get_mut(id) - .ok_or_else(|| PtyError::Write(String::from("Unable to access to the terminal.")))? - .write(content)?; - Ok(()) - } - - pub fn resize(&mut self, id: &str, cols: u16, rows: u16) -> Result<(), PtyError> { - self.ptys - .get_mut(id) - .ok_or_else(|| PtyError::Resize(String::from("Unable to access to the terminal.")))? - .resize(cols, rows)?; - Ok(()) - } - - pub fn close(&mut self, id: &str) -> Result<(), PtyError> { - self.ptys - .remove(id) - .ok_or_else(|| PtyError::Kill(String::from("Unable to access to the terminal.")))? - .close() - } - - pub fn get_running_process(&self, id: &str) -> Result { - self.ptys - .get(id) - .ok_or_else(|| { - PtyError::CloseableStatus(String::from("Unable to access to the terminal.")) - })? - .running_process() - } -} diff --git a/src/index.html b/src/index.html index 3f403d9..28d2efb 100644 --- a/src/index.html +++ b/src/index.html @@ -3,8 +3,9 @@ - Vite + TS + Tess +
@@ -23,7 +24,5 @@
- - diff --git a/src/style/constants.scss b/src/style/constants.scss index f891b01..e03f41d 100644 --- a/src/style/constants.scss +++ b/src/style/constants.scss @@ -14,7 +14,8 @@ --tab-progressbar-background: #050a19; --tab-action-button-background: var(--tab-focused-background); - --toast-background: #192033; + --toast-background: #050a19; + --toast-button-hovered-background: #192033; --popup-background: #050a19; --popup-button-background: #161e32; @@ -25,7 +26,5 @@ --popup-do-not-show-again-checkbox-checked-background: #156ce6; --action-button-background: transparent; - --action-button-hovered-background: #050a19; - - --shadow-color: #192033; // Delete this ?? + --action-button-hovered-background: #050a19; } \ No newline at end of file diff --git a/src/style/style.scss b/src/style/style.scss index 70d7bbf..1464b4c 100644 --- a/src/style/style.scss +++ b/src/style/style.scss @@ -9,12 +9,6 @@ @import "../../node_modules/xterm/css/xterm.css"; -@font-face { - font-family: "Inter"; - font-weight: 400; - src: url(../fonts/Inter-Regular.ttf); -} - @font-face { font-family: "Inter"; font-weight: 500; @@ -27,12 +21,6 @@ src: url(../fonts/Inter-SemiBold.ttf); } -/*@font-face { - font-family: "Inter"; - font-weight: 700; - src: url(../fonts/Inter-Bold.ttf); -}*/ - body { margin: 0; @@ -41,10 +29,13 @@ body { display: flex; flex-direction: column; overflow: hidden; - font-family: "Inter"; - letter-spacing: 0.6px; - word-spacing: 0.66px; + font-family: Inter; + letter-spacing: 0.8px; + word-spacing: 0.8px; + line-height: 16px; background: var(--app-background); + font-weight: 500; + color: var(--text-color); } @@ -54,7 +45,7 @@ body { } ::-webkit-scrollbar-thumb { - background:rgba(212, 212, 212, 0.14); + background:rgba(212, 212, 212, 0.15); border-radius: 6px; } diff --git a/src/style/tab.scss b/src/style/tab.scss index 35a379d..6f83aa3 100644 --- a/src/style/tab.scss +++ b/src/style/tab.scss @@ -4,7 +4,6 @@ width: 16rem; border-top-left-radius: 2px; border-top-right-radius: 2px; - color: var(--text-color); font-weight: 500; justify-content: center; align-items: center; @@ -12,10 +11,8 @@ -webkit-user-select: none; transition: transform 200ms, background 140ms; font-size: 12px; - letter-spacing: 1px; text-transform: capitalize; position: relative; - min-width: 0; overflow: hidden; background: var(--tabs-background); @@ -59,7 +56,7 @@ position: absolute; justify-content: center; align-items: center; - transition: background 100ms; + transition: background 140ms; opacity: 0; display: flex; color: var(--icon-color); diff --git a/src/style/toasts.scss b/src/style/toasts.scss index 45c50aa..14b7af4 100644 --- a/src/style/toasts.scss +++ b/src/style/toasts.scss @@ -1,107 +1,115 @@ .toasts { - width: calc(100% - 32px); position: absolute; - bottom: 0; + bottom: 18px; + right: 18px; z-index: 10; display: flex; align-items:end; flex-direction: column; row-gap: 12px; - padding-bottom: 16px; pointer-events: none; - margin-left: 16px; .toast { - padding-top: 9px; - padding-bottom: 9px; - padding-left: 10px; - padding-right: 10px; + padding-inline: 13px; + padding-block: 12px; border-radius: 4px; box-shadow: rgba(0, 0, 0, 0.28) 0px 5px 10px; - height: fit-content; background: var(--toast-background); animation: toast-inserted 140ms forwards; display: flex; justify-content: center; align-items: center; - color: var(--text-color); pointer-events: all; - max-width: 260px; + max-width: 270px; position: relative; .icon { position: absolute; - top: 4.5px; left: 0; fill: none; display: flex; justify-content: center; align-items: center; - padding-left: 10px; + padding-left: 13px; color: var(--icon-color); svg { - height: 16px; - width: 16px; + height: 20px; + width: 20px; } } .content { - padding-left: 24px; - display: flex; - justify-content: center; - row-gap: 2px; - flex-direction: column; + padding-left: 33px; overflow: hidden; - font-weight: 600; .title { font-size: 13px; + font-weight: 600; } .message { - font-size: 11px; - text-overflow: ellipsis; - max-height: 14px; - white-space: nowrap; - overflow: hidden; - transition: max-height 200ms; - -webkit-line-clamp: 13; - -webkit-box-orient: vertical; - display: -webkit-box; + margin-top: 4px; + font-size: 12px; + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 200ms; + + span { + text-align: justify; + min-height: 17px; + } } } - &.text-expanded .content { - .message { - max-height: 182px; - white-space: normal; + &.with-text { + .icon { + top: 20.5px; } - } - - &:hover .actions { - opacity: unset; - } - &.with-text { .actions { + top: 8px; + .close { box-shadow: unset; } .expand { - box-shadow: -9px 0 7px var(--shadow-color); + box-shadow: -9px 0 7px var(--toast-background); + + svg { + transform: rotate(-90deg); + transition: transform 140ms; + } } } + } + + &.text-expanded { + .actions .expand svg { + transform: rotate(0deg); + } - .icon { - top: 8.5px !important; + .content { + .message { + grid-template-rows: 1fr; + + span { + transition-delay: 200ms; + transition-property: white-space; + } + } } } + &:hover .actions { + opacity: unset; + } + .actions { - right: 7px; - top: 7px; + right: 8px; + margin-top: auto; + margin-bottom: auto; position: absolute; height: 20px; display: flex; @@ -121,12 +129,11 @@ cursor: pointer; &:hover { - background: var(--action-button-hovered-background) !important; + background: var(--toast-button-hovered-background) !important; } } .close { - background: var(--action-button-background); height: 20px; width: 20px; transition: background 100ms; @@ -139,7 +146,6 @@ } .expand { - background: var(--action-button-background); height: 20px; width: 20px; diff --git a/src/ts/manager/view.ts b/src/ts/app.ts similarity index 75% rename from src/ts/manager/view.ts rename to src/ts/app.ts index 6c859bb..ec4177a 100644 --- a/src/ts/manager/view.ts +++ b/src/ts/app.ts @@ -1,23 +1,21 @@ -import { TabsManager } from "./tabs"; +import { TabsManager } from "./manager/tabs"; import { listen, Event } from '@tauri-apps/api/event' import { v4 as uuid } from 'uuid'; import { invoke } from '@tauri-apps/api/tauri' -import { terminalDataPayload, terminalTitleChangedPayload } from "../schema/term"; -import { View } from "../class/views"; -import { Toaster } from "./toast"; +import { terminalDataPayload, terminalTitleChangedPayload } from "./schema/term"; +import { View } from "./class/views"; +import { Toaster } from "./manager/toast"; import { Option, ShortcutAction } from "ts/schema/option"; -import { PopupManager } from "./popup"; +import { PopupManager } from "./manager/popup"; import { TerminalPane } from "ts/class/panes"; -import { ShortcutsManager } from "./shortcuts"; +import { ShortcutsManager } from "./manager/shortcuts"; import { clipboard } from "@tauri-apps/api"; -import { PopupBuilder, PopupButton } from "../class/popup"; +import { PopupBuilder, PopupButton } from "./class/popup"; -export class ViewsManager { - // TODO: Finish - +export class App { private target: Element; private tabsManager: TabsManager; @@ -41,12 +39,12 @@ export class ViewsManager { this.shortcutsManager = new ShortcutsManager(option.shortcuts, (action) => { this.onShortcutExecuted(action) }); - listen("terminalData", (e) => { this.onTerminalReceiveData(e); }); - listen("terminalTitleChanged", (e) => { this.onTerminalTitleChanged(e); }) - listen("terminal_closed", (e) => { this.onTerminalProcessExited(e); }); + listen("js_pty_data", (e) => { this.onTerminalReceiveData(e); }); + listen("js_pty_title_update", (e) => { this.onTerminalTitleChanged(e); }) + listen("js_pty_closed", (e) => { this.onTerminalProcessExited(e); }); - listen("request_window_closing", () => { this.closeViews(); }); - listen("request_app_exit", (e) => { this.closeAllWindows(e); }); + listen("js_window_request_closing", () => { this.closeViews(); }); + listen("js_app_request_exit", (e) => { this.closeAllWindows(e); }); this.toaster = new Toaster(toastTarget); @@ -59,34 +57,32 @@ export class ViewsManager { let popupResult = await this.popupManager.sendPopup(new PopupBuilder(`Confirm close of ${e.payload} windows`).withMessage(`Are you sure to close the app?`).withButtons(confirmButton, cancelButton)); if (popupResult.action == "confirm") { - invoke("close_app"); + invoke("utils_close_app"); } } private onTabFocused(id: string) { - let viewToFocus = this.views.find((view) => view.id! == id); - - if (viewToFocus) { - this.focusedView = viewToFocus; + let view = this.views.find((view) => view.id! == id); - viewToFocus.focus(); + if (view) { + this.focusedView = view; + view.focus(); this.views.forEach((view) => { if (view.id != id) { view.unfocus(); } }) - } else { - // TODO: Handle unknown view and close the tab } } - private onTabRequestClose(id: string) { + private async onTabRequestClose(id: string) { let view = this.views.find((view) => view.id == id); if (view) { - view.requestClosingAll() - } else { - this.toaster.toast("Orphaned tab", "It looks like this tab is orphaned.") + view.requestClosingAll().catch((err) => { + this.toaster.toast("Interaction error", err, "error"); + throw err; + }) } } @@ -96,7 +92,7 @@ export class ViewsManager { view.element!.remove(); this.views.splice(this.views.indexOf(view), 1); if (this.views.length == 0) { - invoke("close_window") + invoke("window_close") } } @@ -121,7 +117,9 @@ export class ViewsManager { } private onTerminalPaneInput(id: string, data: string) { - invoke("terminal_input", {content: data, id: id}); + invoke("pty_write", {content: data, id: id}).catch((err) => { + this.toaster.toast("Interaction error", err, "error") + }); } private onTerminalProcessExited(e: Event) { @@ -146,11 +144,13 @@ export class ViewsManager { case "executeMacro": let macro = this.option.macros.find(macro => macro.uuid == action[1]); if (macro) { - invoke("terminal_input", {content: macro.content, id: this.focusedView!.focusedPane!.id}); + invoke("pty_write", {content: macro.content, id: this.focusedView!.focusedPane!.id}).catch((err) => { + this.toaster.toast("Macro error", err, "error"); + }); } break; default: - this.toaster.toast("Shortcut error", action[0] + " is not yet implemented"); + this.toaster.toast("Unknown shortcut", action[0] + " is not yet implemented"); } } else { switch (action) { @@ -160,7 +160,9 @@ export class ViewsManager { case "paste": let clipboardContent = await clipboard.readText(); if (clipboardContent) { - invoke("terminal_input", {content: clipboardContent, id: this.focusedView!.focusedPane!.id}); + invoke("pty_write", {content: clipboardContent, id: this.focusedView!.focusedPane!.id}).catch((err) => { + this.toaster.toast("Interaction error", err, "error"); + }); } break; case "openDefaultProfile": @@ -185,7 +187,7 @@ export class ViewsManager { this.closeViews() break; default: - this.toaster.toast("Shortcut error", action + " is not yet implemented"); + this.toaster.toast("Unknown shortcut", action + " is not yet implemented"); } } } @@ -203,7 +205,7 @@ export class ViewsManager { await view.closeAll() } - invoke("close_window"); + invoke("window_close"); } } } @@ -214,7 +216,7 @@ export class ViewsManager { let profile = this.option.profiles.find(profile => profile.uuid == profileId); if (profile) { - let view = new View(viewId, this.popupManager, (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); }) view.openPane(paneId, profile, (e, term) => { return this.shortcutsManager.onKeyPress(e, term); }).then(() => { this.views.push(view); @@ -228,10 +230,10 @@ export class ViewsManager { if (focus) { this.tabsManager.select(viewId); } }).catch((err) => { - this.toaster.toast("Unable to create view", err); + this.toaster.toast("Unable to create a view", err, "error"); }) } else { - this.toaster.toast("Unable to create view", `An error occur while opening a view. Reason: no profile corresponding to id ${profileId}`); + this.toaster.toast("Unable to create a view", `There is no profile corresponding to ID: '${profileId}'`, "error"); } } @@ -240,10 +242,8 @@ export class ViewsManager { if (view) { view.requestClosingOne(paneId).catch((err) => { - this.toaster.toast("Unable to close a view's pane", err); + this.toaster.toast("Unable to close a view's pane", err, "error"); }) - } else { - // TODO: Handle error } } diff --git a/src/ts/class/panes.ts b/src/ts/class/panes.ts index 746421a..02d8a13 100644 --- a/src/ts/class/panes.ts +++ b/src/ts/class/panes.ts @@ -5,6 +5,7 @@ import { PopupManager } from 'ts/manager/popup'; import { PopupBuilder, PopupButton } from './popup'; import { Terminal as Xterm } from "xterm"; +import { Toaster } from 'ts/manager/toast'; export class TerminalPane { element: Element; @@ -46,8 +47,8 @@ export class TerminalPane { return element } - async initializeTerm(customKeyEventHanlder: ((e: KeyboardEvent, term: Xterm) => boolean)) { - let terminal = new Terminal(this.id, this.profile.terminalOptions, this.profile.theme, customKeyEventHanlder); + async initializeTerm(customKeyEventHanlder: ((e: KeyboardEvent, term: Xterm) => boolean), toaster: Toaster) { + let terminal = new Terminal(this.id, this.profile.terminalOptions, this.profile.theme, customKeyEventHanlder, toaster); await terminal.launch(this.element.querySelector(".internal-term")!, this.profile.uuid); @@ -55,26 +56,26 @@ export class TerminalPane { } async requestClosing(popupManager: PopupManager, viewElement: HTMLElement) : Promise { - if (!await invoke("check_close_availability", {id: this.id})) { + if (!await invoke("pty_get_closable", {id: this.id})) { let cancelButton = new PopupButton("cancel", "dismiss"); let confirmButton = new PopupButton("confirm", "validate"); - let closeAuthorized = (await popupManager.sendPopup(new PopupBuilder(`Confirm close of ${await invoke("get_pty_title", {id: this.id})}`).withMessage("Are you sure to close this tab?").withButtons(cancelButton, confirmButton), viewElement)).action == "confirm"; + let closeAuthorized = (await popupManager.sendPopup(new PopupBuilder(`Confirm close of ${await invoke("pty_get_title", {id: this.id})}`).withMessage("Are you sure to close this tab?").withButtons(cancelButton, confirmButton), viewElement)).action == "confirm"; if (closeAuthorized) { - await invoke("close_terminal", {id: this.id}); + await invoke("pty_close", {id: this.id}); this.term!.close(); } return closeAuthorized; } else { - await invoke("close_terminal", {id: this.id}); + await invoke("pty_close", {id: this.id}); this.term!.close(); return true; } } async forceClosing() { - await invoke("close_terminal", {id: this.id}); + await invoke("pty_close", {id: this.id}); this.term!.close(); } diff --git a/src/ts/class/terminal.ts b/src/ts/class/terminal.ts index 6685c99..79e12a3 100644 --- a/src/ts/class/terminal.ts +++ b/src/ts/class/terminal.ts @@ -3,18 +3,17 @@ import { CanvasAddon } from 'xterm-addon-canvas'; 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"; export class Terminal { id: string; term: Xterm; fitAddon: FitAddon; canvasResizeObserver: ResizeObserver | undefined; + toaster: Toaster; - constructor(id: string, options: TerminalOptions, theme: TerminalTheme, customKeyEventHandler: ((e: KeyboardEvent, term: Xterm) => boolean)) { - // TODO: Finish - // TODO: Load all addons - + constructor(id: string, options: TerminalOptions, theme: TerminalTheme, customKeyEventHandler: ((e: KeyboardEvent, term: Xterm) => boolean), toaster: Toaster) { theme = Object.assign({}, theme); theme.background = "rgba(0,0,0,0)"; @@ -39,13 +38,15 @@ export class Terminal { this.term.attachCustomKeyEventHandler((e) => { if (e.key == "F10") { - invoke("terminal_input", {content: "\x1b[21~", id: id}); + invoke("pty_write", {content: "\x1b[21~", id: id}); return false; } else { return customKeyEventHandler(e, this.term); } }) + + this.toaster = toaster; } async launch(target: HTMLElement, profile_id: string) { @@ -54,13 +55,15 @@ export class Terminal { if (proposedDimensions && !isNaN(proposedDimensions.cols) && !isNaN(proposedDimensions.rows)) { this.term.resize(proposedDimensions.cols + 1, proposedDimensions.rows + 1); - await invoke("resize_terminal", {cols: this.term.cols, rows: this.term.rows, id: this.id}); + invoke("pty_resize", {cols: this.term.cols, rows: this.term.rows, id: this.id}).catch((err) => { + this.toaster.toast("Terminal error", err, "error"); + }); } }); this.canvasResizeObserver.observe(target); - await invoke("create_terminal", {cols: this.term.cols, rows: this.term.rows, id: this.id, profileUuid: profile_id}); + await invoke("pty_open", {id: this.id, profileUuid: profile_id}); this.term.open(target); @@ -76,11 +79,17 @@ export class Terminal { if (proposedDimensions && !isNaN(proposedDimensions.cols) && !isNaN(proposedDimensions.rows)) { this.term.resize(proposedDimensions.cols + 1, proposedDimensions.rows + 1); - await invoke("resize_terminal", {cols: this.term.cols, rows: this.term.rows, id: this.id}); - - onRenderDisposable.dispose() + invoke("pty_resize", {cols: this.term.cols, rows: this.term.rows, id: this.id}).then(() => { + onRenderDisposable.dispose() + }).catch((err) => { + this.toaster.toast("Terminal error", err, "error"); + }) } }) + + setTimeout(() => { + this.term.clearTextureAtlas() + }, 50) } focus() { diff --git a/src/ts/class/views.ts b/src/ts/class/views.ts index c900a54..0314ab3 100644 --- a/src/ts/class/views.ts +++ b/src/ts/class/views.ts @@ -4,6 +4,7 @@ import { Profile } from "ts/schema/option"; import { PopupManager } from "ts/manager/popup"; import { Terminal as Xterm } from "xterm"; +import { Toaster } from "ts/manager/toast"; export class View { // TODO: Implement pane page type @@ -22,13 +23,16 @@ export class View { closingAllRequested: boolean = false; + toaster: Toaster; - constructor (viewId: string, popupManager: PopupManager, closedEvent: ((id: string) => void), focusedPaneTitleChangedEvent: ((title: string) => void)) { + + constructor (viewId: string, popupManager: PopupManager, toaster: Toaster, closedEvent: ((id: string) => void), focusedPaneTitleChangedEvent: ((title: string) => void)) { this.id = viewId; this.element = this.generateComponents(); this.closedEvent = closedEvent; this.focusedPaneTitleChangedEvent = focusedPaneTitleChangedEvent; this.popupManager = popupManager; + this.toaster = toaster; } async openPane(paneId: string) : Promise; @@ -36,14 +40,12 @@ 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!); + await pane.initializeTerm(customKeyEventHandler!, this.toaster); this.panes.push(pane) this.element!.appendChild(pane.element); this.focusedPane = pane; - } else { - console.log("not yet implemented") } } @@ -72,11 +74,13 @@ export class View { if (!this.closingAllRequested) { this.closingAllRequested = true; - for await (let pane of this.panes) { - await this.requestClosingOne(pane.id) + try { + for await (let pane of this.panes) { + await this.requestClosingOne(pane.id) + } + } finally { + this.closingAllRequested = false; } - - this.closingAllRequested = false; } } diff --git a/src/ts/main.ts b/src/ts/main.ts index a5766ed..b672357 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -1,4 +1,4 @@ -import { ViewsManager } from './manager/view'; +import { App } from './app'; import { invoke, convertFileSrc } from '@tauri-apps/api/tauri'; import { Option } from './schema/option'; @@ -7,8 +7,7 @@ window.addEventListener("contextmenu", (e) => { e.preventDefault(); }) - -invoke