From 89bc74e644d2fd5d5add36543f464b84d44ec93a Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Wed, 2 Aug 2023 19:57:37 -0700 Subject: [PATCH 1/9] start working on terminal launcher --- src-tauri/Cargo.lock | 12 +++++ src-tauri/Cargo.toml | 1 + src-tauri/src/app.rs | 1 + src-tauri/src/config.rs | 12 +++++ src-tauri/src/ipc.rs | 10 ++++ src-tauri/src/lib.rs | 1 + src-tauri/src/terminal.rs | 97 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 134 insertions(+) create mode 100644 src-tauri/src/terminal.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 09746c8..1579058 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1066,6 +1066,7 @@ dependencies = [ "tauri-plugin-single-instance", "thiserror", "tokio", + "which", ] [[package]] @@ -5145,6 +5146,17 @@ dependencies = [ "windows-metadata", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bbd2b7e..e1e2a4f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -46,6 +46,7 @@ clap = { version = "3.2.23", features = ["derive"] } is-terminal = "0.4.7" argon2 = { version = "0.5.0", features = ["std"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] } +which = "4.4.0" [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index 603190d..0d2fe0a 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -42,6 +42,7 @@ pub fn run() -> tauri::Result<()> { ipc::save_credentials, ipc::get_config, ipc::save_config, + ipc::launch_terminal, ]) .setup(|app| rt::block_on(setup(app))) .build(tauri::generate_context!())? diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index cd82565..ead3552 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -9,6 +9,16 @@ use sqlx::SqlitePool; use crate::errors::*; +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TermConfig { + pub name: String, + // we call it exec because it isn't always the actual path, + // in some cases it's just the name and relies on path-searching + pub exec: PathBuf, + pub args: Vec, +} + + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppConfig { #[serde(default = "default_listen_addr")] @@ -21,6 +31,7 @@ pub struct AppConfig { pub start_minimized: bool, #[serde(default = "default_start_on_login")] pub start_on_login: bool, + pub terminal: Option, } @@ -32,6 +43,7 @@ impl Default for AppConfig { rehide_ms: default_rehide_ms(), start_minimized: default_start_minimized(), start_on_login: default_start_on_login(), + terminal: None, } } } diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index ea6a62c..8cd3b36 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -6,6 +6,7 @@ use crate::credentials::{Session,BaseCredentials}; use crate::errors::*; use crate::clientinfo::Client; use crate::state::AppState; +use crate::terminal; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -78,3 +79,12 @@ pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> R .map_err(|e| format!("Error saving config: {e}"))?; Ok(()) } + + +#[tauri::command] +pub async fn launch_terminal(base: bool) -> bool { + match terminal::launch(base).await { + Ok(_) => true, + Err(_) => false, + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a594d9b..2c1fbeb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,4 +7,5 @@ mod clientinfo; mod ipc; mod state; mod server; +mod terminal; mod tray; diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs new file mode 100644 index 0000000..de539a6 --- /dev/null +++ b/src-tauri/src/terminal.rs @@ -0,0 +1,97 @@ +use std::process::Command; + +use tauri::Manager; + +use crate::app::APP; +use crate::config::TermConfig; +use crate::credentials::Session; +use crate::errors::*; +use crate::state::AppState; + + +pub async fn launch(use_base: bool) -> Result<(), ExecError> { + // we may have multiple candidates, because we might be on unix and + // we don't have a good way of detecting for sure what default to use + let state = APP.get().unwrap().state::(); + let config = state.config.read().await; + let _ = match config.terminal { + Some(ref term) => launch_term(term, use_base).await, + None => launch_default(use_base).await, + }?; + Ok(()) +} + + +async fn launch_term(term: &TermConfig, use_base: bool) -> Result<(), std::io::Error> { + // do all this in a block so we don't hold the lock any longer than necessary + let mut cmd = Command::new(&term.exec); + cmd.args(&term.args); + { + // note: if called from launch(), there is already a lock being held on state.config + // don't read that here or we will deadlock + let state = APP.get().unwrap().state::(); + let app_session = state.session.read().await; + let (base_creds, session_creds) = match *app_session { + Session::Locked(_) | Session::Empty => todo!(), + Session::Unlocked{ref base, ref session} => (base, session), + }; + + if use_base { + cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id); + cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key); + } + else { + cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id); + cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key); + cmd.env("AWS_SESSION_TOKEN", &session_creds.token); + } + } + + let _ = cmd.spawn()?; + Ok(()) +} + + +async fn launch_default(use_base: bool) -> Result<(), std::io::Error> { + let defaults = default_terms(); + let last_idx = defaults.len() - 1; + for (i, candidate) in defaults.iter().enumerate() { + match launch_term(candidate, use_base).await { + Ok(_) => return Ok(()), + Err(e) => { + if std::io::ErrorKind::NotFound == e.kind() && i < last_idx { + continue; + } + return Err(e); + } + } + } + // we only continue the loop if there are further iterations left, so this is safe + unreachable!() +} + + +pub fn default_terms() -> Vec { + #[cfg(windows)] + return vec![ + TermConfig { + name: "powershell.exe".into(), + exec: "conhost.exe".into(), + args: vec!["powershell.exe".into()] + } + ]; + + #[cfg(unix)] + return vec![ + TermConfig { + name: "gnome-terminal".into(), + exec: "gnome-terminal".into(), + args: vec![], + }, + TermConfig { + name: "konsole".into(), + exec: "konsole".into(), + args: vec![], + }, + ]; +} \ No newline at end of file From 890f715388e5ab461368c700951a555ebedfd90a Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Thu, 3 Aug 2023 16:35:15 -0700 Subject: [PATCH 2/9] usable backend for terminal launch --- src-tauri/src/config.rs | 46 +++++++++++++++++-- src-tauri/src/credentials.rs | 10 +++++ src-tauri/src/errors.rs | 30 ++++++++++++- src-tauri/src/ipc.rs | 7 +-- src-tauri/src/terminal.rs | 87 ++++++++---------------------------- 5 files changed, 103 insertions(+), 77 deletions(-) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index ead3552..3f7587e 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,4 +1,5 @@ use std::net::Ipv4Addr; +use std::ffi::OsString; use std::path::PathBuf; use auto_launch::AutoLaunchBuilder; @@ -15,7 +16,7 @@ pub struct TermConfig { // we call it exec because it isn't always the actual path, // in some cases it's just the name and relies on path-searching pub exec: PathBuf, - pub args: Vec, + pub args: Vec, } @@ -31,7 +32,8 @@ pub struct AppConfig { pub start_minimized: bool, #[serde(default = "default_start_on_login")] pub start_on_login: bool, - pub terminal: Option, + #[serde(default = "default_term_config")] + pub terminal: TermConfig, } @@ -43,7 +45,7 @@ impl Default for AppConfig { rehide_ms: default_rehide_ms(), start_minimized: default_start_minimized(), start_on_login: default_start_on_login(), - terminal: None, + terminal: default_term_config(), } } } @@ -128,6 +130,44 @@ fn default_listen_port() -> u16 { } } + +fn default_term_config() -> TermConfig { + #[cfg(windows)] + { + if let Ok(path) = which::which("pwsh.exe") { + return TermConfig { + name: exe.into(), + exec: "conhost.exe".into(), + args: vec![path.into_os_string()] + }; + } + return TermConfig { + name: "powershell.exe".into(), + exec: "conhost.exe".into(), + args: vec!["powershell.exe".into()] + }; + } + + #[cfg(unix)] + { + for bin in ["gnome-terminal", "konsole"] { + if let Ok(_) = which::which(bin) { + return TermConfig { + name: bin.into(), + exec: bin.into(), + args: vec![], + } + } + } + return TermConfig { + name: "gnome-terminal".into(), + exec: "gnome-terminal".into(), + args: vec![], + }; + } +} + + fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST } fn default_rehide_ms() -> u64 { 1000 } // start minimized and on login only in production mode diff --git a/src-tauri/src/credentials.rs b/src-tauri/src/credentials.rs index 946c907..91b1588 100644 --- a/src-tauri/src/credentials.rs +++ b/src-tauri/src/credentials.rs @@ -81,6 +81,16 @@ impl Session { Session::Empty => Err(GetSessionError::CredentialsEmpty), } } + + pub fn try_get( + &self + ) -> Result<(&BaseCredentials, &SessionCredentials), GetCredentialsError> { + match self { + Self::Empty => Err(GetCredentialsError::Empty), + Self::Locked(_) => Err(GetCredentialsError::Locked), + Self::Unlocked{ ref base, ref session } => Ok((base, session)) + } + } } diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index d3cc9f3..b74c3de 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -1,5 +1,6 @@ use std::error::Error; use std::convert::AsRef; +use std::ffi::OsString; use std::sync::mpsc; use strum_macros::AsRefStr; @@ -216,7 +217,7 @@ pub enum RequestError { } -// Errors encountered while running a subprocess (via creddy exec) +// Errors encountered while running a subprocess via creddy exec #[derive(Debug, ThisError, AsRefStr)] pub enum ExecError { #[error("Please specify a command")] @@ -237,6 +238,18 @@ pub enum CliError { } +// Errors encountered while trying to launch a child process +#[derive(Debug, ThisError, AsRefStr)] +pub enum LaunchError { + #[error("Executable not found: {0:?}")] + ExeNotFound(OsString), + #[error("Failed to execute command: {0}")] + ExecutionFailed(#[from] std::io::Error), + #[error(transparent)] + GetCredentials(#[from] GetCredentialsError), +} + + // ========================= // Serialize implementations // ========================= @@ -327,3 +340,18 @@ impl Serialize for UnlockError { map.end() } } + + +impl Serialize for LaunchError { + fn serialize(&self, serializer: S) -> Result { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("code", self.as_ref())?; + map.serialize_entry("msg", &format!("{self}"))?; + + match self { + LaunchError::GetCredentials(src) => map.serialize_entry("source", &src)?, + _ => serialize_upstream_err(self, &mut map)?, + } + map.end() + } +} diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index 8cd3b36..a6f0c7e 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -82,9 +82,6 @@ pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> R #[tauri::command] -pub async fn launch_terminal(base: bool) -> bool { - match terminal::launch(base).await { - Ok(_) => true, - Err(_) => false, - } +pub async fn launch_terminal(base: bool) -> Result<(), LaunchError> { + terminal::launch(base).await } diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs index de539a6..18afe4e 100644 --- a/src-tauri/src/terminal.rs +++ b/src-tauri/src/terminal.rs @@ -3,39 +3,25 @@ use std::process::Command; use tauri::Manager; use crate::app::APP; -use crate::config::TermConfig; -use crate::credentials::Session; use crate::errors::*; use crate::state::AppState; -pub async fn launch(use_base: bool) -> Result<(), ExecError> { - // we may have multiple candidates, because we might be on unix and - // we don't have a good way of detecting for sure what default to use +pub async fn launch(use_base: bool) -> Result<(), LaunchError> { let state = APP.get().unwrap().state::(); - let config = state.config.read().await; - let _ = match config.terminal { - Some(ref term) => launch_term(term, use_base).await, - None => launch_default(use_base).await, - }?; - Ok(()) -} - - -async fn launch_term(term: &TermConfig, use_base: bool) -> Result<(), std::io::Error> { // do all this in a block so we don't hold the lock any longer than necessary - let mut cmd = Command::new(&term.exec); - cmd.args(&term.args); + let mut cmd = { + let config = state.config.read().await; + let mut cmd = Command::new(&config.terminal.exec); + cmd.args(&config.terminal.args); + cmd + }; + + // similarly { - // note: if called from launch(), there is already a lock being held on state.config - // don't read that here or we will deadlock let state = APP.get().unwrap().state::(); let app_session = state.session.read().await; - let (base_creds, session_creds) = match *app_session { - Session::Locked(_) | Session::Empty => todo!(), - Session::Unlocked{ref base, ref session} => (base, session), - }; - + let (base_creds, session_creds) = app_session.try_get()?; if use_base { cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id); cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key); @@ -47,51 +33,16 @@ async fn launch_term(term: &TermConfig, use_base: bool) -> Result<(), std::io::E } } - let _ = cmd.spawn()?; - Ok(()) -} - - -async fn launch_default(use_base: bool) -> Result<(), std::io::Error> { - let defaults = default_terms(); - let last_idx = defaults.len() - 1; - for (i, candidate) in defaults.iter().enumerate() { - match launch_term(candidate, use_base).await { - Ok(_) => return Ok(()), - Err(e) => { - if std::io::ErrorKind::NotFound == e.kind() && i < last_idx { - continue; - } - return Err(e); + match cmd.spawn() { + Ok(_) => Ok(()), + Err(e) => { + use std::io::ErrorKind::*; + if let NotFound = e.kind() { + Err(LaunchError::ExeNotFound(cmd.get_program().to_owned())) + } + else { + Err(LaunchError::from(e)) } } } - // we only continue the loop if there are further iterations left, so this is safe - unreachable!() } - - -pub fn default_terms() -> Vec { - #[cfg(windows)] - return vec![ - TermConfig { - name: "powershell.exe".into(), - exec: "conhost.exe".into(), - args: vec!["powershell.exe".into()] - } - ]; - - #[cfg(unix)] - return vec![ - TermConfig { - name: "gnome-terminal".into(), - exec: "gnome-terminal".into(), - args: vec![], - }, - TermConfig { - name: "konsole".into(), - exec: "konsole".into(), - args: vec![], - }, - ]; -} \ No newline at end of file From a51b20add712f76dd705fee7e3370d30abcf3c44 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Thu, 3 Aug 2023 21:57:55 -0700 Subject: [PATCH 3/9] combine ExecError with LaunchError and use Session::try_get() instead of matching --- src-tauri/src/cli.rs | 24 +++++++++++++++++++----- src-tauri/src/config.rs | 2 +- src-tauri/src/errors.rs | 20 ++++++-------------- src-tauri/src/ipc.rs | 2 +- src-tauri/src/state.rs | 18 ++++++------------ src-tauri/src/terminal.rs | 15 +++++---------- 6 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src-tauri/src/cli.rs b/src-tauri/src/cli.rs index 17bce76..d60a56c 100644 --- a/src-tauri/src/cli.rs +++ b/src-tauri/src/cli.rs @@ -1,3 +1,4 @@ +use std::ffi::OsString; use std::process::Command as ChildCommand; #[cfg(unix)] use std::os::unix::process::CommandExt; @@ -90,15 +91,28 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> { #[cfg(unix)] { - let e = cmd.exec(); // never returns if successful - Err(ExecError::ExecutionFailed(e))?; - Ok(()) + // cmd.exec() never returns if successful + let e = cmd.exec(); + match e.kind() { + std::io::ErrorKind::NotFound => { + let name: OsString = cmd_name.into(); + Err(ExecError::NotFound(name).into()) + } + e => Err(ExecError::ExecutionFailed(e).into()), + } } #[cfg(windows)] { - let mut child = cmd.spawn() - .map_err(|e| ExecError::ExecutionFailed(e))?; + let mut child = match cmd.spawn() { + Ok(c) => c, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + let name: OsString = cmd_name.into(); + return Err(ExecError::NotFound(name).into()); + } + Err(e) => return Err(ExecError::ExecutionFailed(e).into()), + }; + let status = child.wait() .map_err(|e| ExecError::ExecutionFailed(e))?; std::process::exit(status.code().unwrap_or(1)); diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 3f7587e..69f8813 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -136,7 +136,7 @@ fn default_term_config() -> TermConfig { { if let Ok(path) = which::which("pwsh.exe") { return TermConfig { - name: exe.into(), + name: "pwsh.exe".into(), exec: "conhost.exe".into(), args: vec![path.into_os_string()] }; diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index b74c3de..53f0c62 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -217,16 +217,6 @@ pub enum RequestError { } -// Errors encountered while running a subprocess via creddy exec -#[derive(Debug, ThisError, AsRefStr)] -pub enum ExecError { - #[error("Please specify a command")] - NoCommand, - #[error("Failed to execute command: {0}")] - ExecutionFailed(#[from] std::io::Error) -} - - #[derive(Debug, ThisError, AsRefStr)] pub enum CliError { #[error(transparent)] @@ -240,9 +230,11 @@ pub enum CliError { // Errors encountered while trying to launch a child process #[derive(Debug, ThisError, AsRefStr)] -pub enum LaunchError { +pub enum ExecError { + #[error("Please specify a command")] + NoCommand, #[error("Executable not found: {0:?}")] - ExeNotFound(OsString), + NotFound(OsString), #[error("Failed to execute command: {0}")] ExecutionFailed(#[from] std::io::Error), #[error(transparent)] @@ -342,14 +334,14 @@ impl Serialize for UnlockError { } -impl Serialize for LaunchError { +impl Serialize for ExecError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; match self { - LaunchError::GetCredentials(src) => map.serialize_entry("source", &src)?, + ExecError::GetCredentials(src) => map.serialize_entry("source", &src)?, _ => serialize_upstream_err(self, &mut map)?, } map.end() diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index a6f0c7e..b34584d 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -82,6 +82,6 @@ pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> R #[tauri::command] -pub async fn launch_terminal(base: bool) -> Result<(), LaunchError> { +pub async fn launch_terminal(base: bool) -> Result<(), ExecError> { terminal::launch(base).await } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index eaaaed2..5d30d5d 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -142,21 +142,15 @@ impl AppState { } pub async fn serialize_base_creds(&self) -> Result { - let session = self.session.read().await; - match *session { - Session::Unlocked{ref base, ..} => Ok(serde_json::to_string(base).unwrap()), - Session::Locked(_) => Err(GetCredentialsError::Locked), - Session::Empty => Err(GetCredentialsError::Empty), - } + let app_session = self.session.read().await; + let (base, _session) = app_session.try_get()?; + Ok(serde_json::to_string(base).unwrap()) } pub async fn serialize_session_creds(&self) -> Result { - let session = self.session.read().await; - match *session { - Session::Unlocked{ref session, ..} => Ok(serde_json::to_string(session).unwrap()), - Session::Locked(_) => Err(GetCredentialsError::Locked), - Session::Empty => Err(GetCredentialsError::Empty), - } + let app_session = self.session.read().await; + let (_bsae, session) = app_session.try_get()?; + Ok(serde_json::to_string(session).unwrap()) } async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> { diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs index 18afe4e..f0544fa 100644 --- a/src-tauri/src/terminal.rs +++ b/src-tauri/src/terminal.rs @@ -7,7 +7,7 @@ use crate::errors::*; use crate::state::AppState; -pub async fn launch(use_base: bool) -> Result<(), LaunchError> { +pub async fn launch(use_base: bool) -> Result<(), ExecError> { let state = APP.get().unwrap().state::(); // do all this in a block so we don't hold the lock any longer than necessary let mut cmd = { @@ -35,14 +35,9 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchError> { match cmd.spawn() { Ok(_) => Ok(()), - Err(e) => { - use std::io::ErrorKind::*; - if let NotFound = e.kind() { - Err(LaunchError::ExeNotFound(cmd.get_program().to_owned())) - } - else { - Err(LaunchError::from(e)) - } - } + Err(e) if std::io::ErrorKind::NotFound == e.kind() => { + Err(ExecError::NotFound(cmd.get_program().to_owned())) + }, + Err(e) => Err(e.into()), } } From e7e0f9d33e2164a1cafcd3bc9aab94a8ca97d8d4 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Thu, 3 Aug 2023 22:08:24 -0700 Subject: [PATCH 4/9] very basic launch button --- src/views/Home.svelte | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/views/Home.svelte b/src/views/Home.svelte index 0322c1f..785927e 100644 --- a/src/views/Home.svelte +++ b/src/views/Home.svelte @@ -10,13 +10,11 @@ import vaultDoorSvg from '../assets/vault_door.svg?raw'; - - // onMount(async () => { - // // will block until a request comes in - // let req = await $appState.pendingRequests.get(); - // $appState.currentRequest = req; - // navigate('Approve'); - // }); + let launchBase = false; + function launchTerminal() { + invoke('launch_terminal', {base: launchBase}); + launchBase = false; + } @@ -25,6 +23,7 @@
+
{#await invoke('get_session_status') then status} {#if status === 'locked'} @@ -37,6 +36,13 @@ {:else if status === 'unlocked'} {@html vaultDoorSvg}

Waiting for requests

+ + {:else if status === 'empty'} {@html vaultDoorSvg} @@ -46,4 +52,5 @@ {/if} {/await} +
\ No newline at end of file From e46c3d2b4dce1998c56d00a9ee8324edabb5b9a7 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Tue, 5 Sep 2023 06:12:26 -0700 Subject: [PATCH 5/9] tweak home screen --- src/views/Home.svelte | 48 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/views/Home.svelte b/src/views/Home.svelte index 785927e..c9b24cf 100644 --- a/src/views/Home.svelte +++ b/src/views/Home.svelte @@ -24,33 +24,31 @@
- {#await invoke('get_session_status') then status} - {#if status === 'locked'} + {@html vaultDoorSvg} + {#await invoke('get_session_status') then status} + {#if status === 'locked'} - {@html vaultDoorSvg} -

Creddy is locked

- - - +

Creddy is locked

+ + + - {:else if status === 'unlocked'} - {@html vaultDoorSvg} -

Waiting for requests

- - + {:else if status === 'unlocked'} +

Waiting for requests

+ + - {:else if status === 'empty'} - {@html vaultDoorSvg} -

No credentials found

- - - - {/if} - {/await} + {:else if status === 'empty'} +

No credentials found

+ + + + {/if} + {/await}
\ No newline at end of file From c98a0655870ed7e8810cc7c92849d3a6d91d6618 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Sat, 9 Sep 2023 06:30:19 -0700 Subject: [PATCH 6/9] make terminal emulator configurable --- src-tauri/Cargo.toml | 2 +- src-tauri/src/config.rs | 28 +++++++++++++------------ src-tauri/tauri.conf.json | 3 ++- src/ui/settings/FileSetting.svelte | 28 +++++++++++++++++++++++++ src/ui/settings/NumericSetting.svelte | 3 ++- src/ui/settings/Setting.svelte | 9 +++++--- src/ui/settings/TextSetting.svelte | 23 ++++++++++++++++++++ src/ui/settings/ToggleSetting.svelte | 3 ++- src/ui/settings/index.js | 2 ++ src/views/Settings.svelte | 30 +++++++++++++++++++++++---- 10 files changed, 107 insertions(+), 24 deletions(-) create mode 100644 src/ui/settings/FileSetting.svelte create mode 100644 src/ui/settings/TextSetting.svelte diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e1e2a4f..564e032 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,7 +25,7 @@ tauri-build = { version = "1.0.4", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2", features = ["dialog", "os-all", "system-tray"] } +tauri = { version = "1.2", features = ["dialog", "dialog-open", "os-all", "system-tray"] } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } sodiumoxide = "0.2.7" tokio = { version = ">=1.19", features = ["full"] } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 69f8813..ffb6bc8 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,5 +1,4 @@ use std::net::Ipv4Addr; -use std::ffi::OsString; use std::path::PathBuf; use auto_launch::AutoLaunchBuilder; @@ -15,8 +14,9 @@ pub struct TermConfig { pub name: String, // we call it exec because it isn't always the actual path, // in some cases it's just the name and relies on path-searching - pub exec: PathBuf, - pub args: Vec, + // it's a string because it can come from the frontend as json + pub exec: String, + pub args: Vec, } @@ -134,18 +134,20 @@ fn default_listen_port() -> u16 { fn default_term_config() -> TermConfig { #[cfg(windows)] { - if let Ok(path) = which::which("pwsh.exe") { - return TermConfig { - name: "pwsh.exe".into(), - exec: "conhost.exe".into(), - args: vec![path.into_os_string()] - }; + let shell = if which::which("pwsh.exe").is_ok() { + "pwsh.exe".to_string() } - return TermConfig { - name: "powershell.exe".into(), - exec: "conhost.exe".into(), - args: vec!["powershell.exe".into()] + else { + "powershell.exe".to_string() }; + + let (exec, args) = if cfg!(debug_assertions) { + ("conhost.exe".to_string(), vec![shell.clone()]) + } else { + (shell.clone(), vec![]) + }; + + TermConfig { name: shell, exec, args } } #[cfg(unix)] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e3d14c4..5e575ba 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -12,7 +12,8 @@ }, "tauri": { "allowlist": { - "os": {"all": true} + "os": {"all": true}, + "dialog": {"open": true} }, "bundle": { "active": true, diff --git a/src/ui/settings/FileSetting.svelte b/src/ui/settings/FileSetting.svelte new file mode 100644 index 0000000..0eef6e6 --- /dev/null +++ b/src/ui/settings/FileSetting.svelte @@ -0,0 +1,28 @@ + + + + +
+ dispatch('update', {value})} + > + +
+ +
diff --git a/src/ui/settings/NumericSetting.svelte b/src/ui/settings/NumericSetting.svelte index 5c81d75..161b923 100644 --- a/src/ui/settings/NumericSetting.svelte +++ b/src/ui/settings/NumericSetting.svelte @@ -4,6 +4,7 @@ import Setting from './Setting.svelte'; export let title; + export let divider = true; export let value; export let unit = ''; export let min = null; @@ -59,7 +60,7 @@ - +
{#if unit} {unit}: diff --git a/src/ui/settings/Setting.svelte b/src/ui/settings/Setting.svelte index baec200..d7f0b9a 100644 --- a/src/ui/settings/Setting.svelte +++ b/src/ui/settings/Setting.svelte @@ -3,12 +3,15 @@ import ErrorAlert from '../ErrorAlert.svelte'; export let title; + export let divider = true; -
-
-

{title}

+{#if divider} +
+{/if} +
+

{title}

diff --git a/src/ui/settings/TextSetting.svelte b/src/ui/settings/TextSetting.svelte new file mode 100644 index 0000000..1bdbfb4 --- /dev/null +++ b/src/ui/settings/TextSetting.svelte @@ -0,0 +1,23 @@ + + + + +
+ dispatch('update', {value})} + > +
+ +
diff --git a/src/ui/settings/ToggleSetting.svelte b/src/ui/settings/ToggleSetting.svelte index 329c677..0363083 100644 --- a/src/ui/settings/ToggleSetting.svelte +++ b/src/ui/settings/ToggleSetting.svelte @@ -4,13 +4,14 @@ import Setting from './Setting.svelte'; export let title; + export let divider = true; // passed through to Setting export let value; const dispatch = createEventDispatcher(); - + osType = t); + + console.log($appState.config.terminal); + window.term = $appState.config.terminal; {#await invoke('get_config') then config} -
+
- +
+

General

+
+ + Start Creddy when you log in to your computer. @@ -76,6 +83,21 @@ Update or re-enter your encrypted credentials. + +
+

Terminal

+
+ + + + Choose your preferred terminal emulator (e.g. gnome-terminal, wt.exe.) May be an absolute path or an executable discoverable on $PATH. + +
{/await} From 568594860815502286505cc5be1518fd1316c162 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Sat, 9 Sep 2023 07:29:57 -0700 Subject: [PATCH 7/9] add hotkeys to show window and launch terminal --- src-tauri/Cargo.toml | 2 +- src-tauri/src/app.rs | 2 ++ src-tauri/src/config.rs | 58 +++++++++++++++++++++++++++++++++++++++++ src-tauri/src/errors.rs | 2 ++ src-tauri/src/state.rs | 6 +++++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 564e032..9c78901 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,7 +25,7 @@ tauri-build = { version = "1.0.4", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2", features = ["dialog", "dialog-open", "os-all", "system-tray"] } +tauri = { version = "1.2", features = ["dialog", "dialog-open", "os-all", "system-tray", "global-shortcut"] } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } sodiumoxide = "0.2.7" tokio = { version = ">=1.19", features = ["full"] } diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index 0d2fe0a..4702646 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -10,6 +10,7 @@ use tauri::{ App, AppHandle, Manager, + GlobalShortcutManager, async_runtime as rt, }; @@ -83,6 +84,7 @@ async fn setup(app: &mut App) -> Result<(), Box> { let srv = Server::new(conf.listen_addr, conf.listen_port, app.handle()).await?; config::set_auto_launch(conf.start_on_login)?; + config::register_hotkeys(&conf.hotkeys)?; // if session is empty, this is probably the first launch, so don't autohide if !conf.start_minimized || is_first_launch { app.get_window("main") diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index ffb6bc8..dc20103 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -5,6 +5,12 @@ use auto_launch::AutoLaunchBuilder; use is_terminal::IsTerminal; use serde::{Serialize, Deserialize}; use sqlx::SqlitePool; +use tauri::{ + AppHandle, + Manager, + GlobalShortcutManager, + async_runtime as rt, +}; use crate::errors::*; @@ -20,6 +26,14 @@ pub struct TermConfig { } +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct HotkeyConfig { + // tauri uses strings to represent keybinds, so we will as well + pub show_window: String, + pub launch_terminal: String, +} + + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppConfig { #[serde(default = "default_listen_addr")] @@ -34,6 +48,8 @@ pub struct AppConfig { pub start_on_login: bool, #[serde(default = "default_term_config")] pub terminal: TermConfig, + #[serde(default = "default_hotkey_config")] + pub hotkeys: HotkeyConfig, } @@ -46,6 +62,7 @@ impl Default for AppConfig { start_minimized: default_start_minimized(), start_on_login: default_start_on_login(), terminal: default_term_config(), + hotkeys: default_hotkey_config(), } } } @@ -170,6 +187,47 @@ fn default_term_config() -> TermConfig { } +fn default_hotkey_config() -> HotkeyConfig { + HotkeyConfig { + show_window: "alt+shift+C".into(), + launch_terminal: "alt+shift+T".into(), + } +} + +// note: will panic if called before APP is set +pub fn register_hotkeys(hotkeys: &HotkeyConfig) -> tauri::Result<()> { + let app = crate::app::APP.get().unwrap(); + + let mut manager = app.global_shortcut_manager(); + manager.unregister_all()?; + + let h = app.app_handle(); + manager.register( + &hotkeys.show_window, + move || { + h.get_window("main") + .map(|w| w.show().error_popup("Failed to show")) + .ok_or(HandlerError::NoMainWindow) + .error_popup("No main window"); + }, + )?; + + // register() doesn't take an async fn, so we have to use spawn + manager.register( + &hotkeys.launch_terminal, + || { + rt::spawn(async { + crate::terminal::launch(false) + .await + .error_popup("Failed to launch"); + }); + } + )?; + + Ok(()) +} + + fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST } fn default_rehide_ms() -> u64 { 1000 } // start minimized and on login only in production mode diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index 53f0c62..f564917 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -95,6 +95,8 @@ pub enum SetupError { ServerSetupError(#[from] std::io::Error), #[error("Failed to resolve data directory: {0}")] DataDir(#[from] DataDirError), + #[error("Failed to register hotkeys: {0}")] + RegisterHotkeys(#[from] tauri::Error), } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 5d30d5d..ffb2c9f 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -59,15 +59,21 @@ impl AppState { pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> { let mut live_config = self.config.write().await; + // update autostart if necessary if new_config.start_on_login != live_config.start_on_login { config::set_auto_launch(new_config.start_on_login)?; } + // rebind socket if necessary if new_config.listen_addr != live_config.listen_addr || new_config.listen_port != live_config.listen_port { let mut sv = self.server.write().await; sv.rebind(new_config.listen_addr, new_config.listen_port).await?; } + // re-register hotkeys if necessary + if new_config.hotkeys != live_config.hotkeys { + config::register_hotkeys(&new_config.hotkeys)?; + } new_config.save(&self.pool).await?; *live_config = new_config; From 8d7b01629d525d42902491c68aab6722a53fe7af Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Sun, 10 Sep 2023 14:04:09 -0700 Subject: [PATCH 8/9] make keybinds configurable --- src-tauri/src/config.rs | 72 +++++++------- src-tauri/src/state.rs | 4 +- src/ui/KeyCombo.svelte | 11 +++ src/ui/settings/FileSetting.svelte | 3 +- src/ui/settings/Keybind.svelte | 61 ++++++++++++ src/ui/settings/NumericSetting.svelte | 4 +- src/ui/settings/Setting.svelte | 26 +++--- src/ui/settings/SettingsGroup.svelte | 14 +++ src/ui/settings/TextSetting.svelte | 3 +- src/ui/settings/ToggleSetting.svelte | 3 +- src/views/Approve.svelte | 11 +-- src/views/Settings.svelte | 129 +++++++++++++------------- 12 files changed, 220 insertions(+), 121 deletions(-) create mode 100644 src/ui/KeyCombo.svelte create mode 100644 src/ui/settings/Keybind.svelte create mode 100644 src/ui/settings/SettingsGroup.svelte diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index dc20103..f75c1f7 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -6,7 +6,6 @@ use is_terminal::IsTerminal; use serde::{Serialize, Deserialize}; use sqlx::SqlitePool; use tauri::{ - AppHandle, Manager, GlobalShortcutManager, async_runtime as rt, @@ -27,10 +26,17 @@ pub struct TermConfig { #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct HotkeyConfig { +pub struct HotkeysConfig { // tauri uses strings to represent keybinds, so we will as well - pub show_window: String, - pub launch_terminal: String, + pub show_window: Hotkey, + pub launch_terminal: Hotkey, +} + + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct Hotkey { + pub keys: String, + pub enabled: bool, } @@ -49,7 +55,7 @@ pub struct AppConfig { #[serde(default = "default_term_config")] pub terminal: TermConfig, #[serde(default = "default_hotkey_config")] - pub hotkeys: HotkeyConfig, + pub hotkeys: HotkeysConfig, } @@ -187,42 +193,46 @@ fn default_term_config() -> TermConfig { } -fn default_hotkey_config() -> HotkeyConfig { - HotkeyConfig { - show_window: "alt+shift+C".into(), - launch_terminal: "alt+shift+T".into(), +fn default_hotkey_config() -> HotkeysConfig { + HotkeysConfig { + show_window: Hotkey {keys: "alt+shift+C".into(), enabled: true}, + launch_terminal: Hotkey {keys: "alt+shift+T".into(), enabled: true}, } } // note: will panic if called before APP is set -pub fn register_hotkeys(hotkeys: &HotkeyConfig) -> tauri::Result<()> { +pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> { let app = crate::app::APP.get().unwrap(); let mut manager = app.global_shortcut_manager(); manager.unregister_all()?; - let h = app.app_handle(); - manager.register( - &hotkeys.show_window, - move || { - h.get_window("main") - .map(|w| w.show().error_popup("Failed to show")) - .ok_or(HandlerError::NoMainWindow) - .error_popup("No main window"); - }, - )?; + if hotkeys.show_window.enabled { + let handle = app.app_handle(); + manager.register( + &hotkeys.show_window.keys, + move || { + handle.get_window("main") + .map(|w| w.show().error_popup("Failed to show")) + .ok_or(HandlerError::NoMainWindow) + .error_popup("No main window"); + }, + )?; + } - // register() doesn't take an async fn, so we have to use spawn - manager.register( - &hotkeys.launch_terminal, - || { - rt::spawn(async { - crate::terminal::launch(false) - .await - .error_popup("Failed to launch"); - }); - } - )?; + if hotkeys.launch_terminal.enabled { + // register() doesn't take an async fn, so we have to use spawn + manager.register( + &hotkeys.launch_terminal.keys, + || { + rt::spawn(async { + crate::terminal::launch(false) + .await + .error_popup("Failed to launch"); + }); + } + )?; + } Ok(()) } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index ffb2c9f..633a1f6 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -71,7 +71,9 @@ impl AppState { sv.rebind(new_config.listen_addr, new_config.listen_port).await?; } // re-register hotkeys if necessary - if new_config.hotkeys != live_config.hotkeys { + if new_config.hotkeys.show_window != live_config.hotkeys.show_window + || new_config.hotkeys.launch_terminal != live_config.hotkeys.launch_terminal + { config::register_hotkeys(&new_config.hotkeys)?; } diff --git a/src/ui/KeyCombo.svelte b/src/ui/KeyCombo.svelte new file mode 100644 index 0000000..99b01e1 --- /dev/null +++ b/src/ui/KeyCombo.svelte @@ -0,0 +1,11 @@ + + + +
+ {#each keys as key, i} + {#if i > 0} + {/if} + {key} + {/each} +
diff --git a/src/ui/settings/FileSetting.svelte b/src/ui/settings/FileSetting.svelte index 0eef6e6..c3c0bde 100644 --- a/src/ui/settings/FileSetting.svelte +++ b/src/ui/settings/FileSetting.svelte @@ -4,14 +4,13 @@ import Setting from './Setting.svelte'; export let title; - export let divider = true; export let value; const dispatch = createEventDispatcher(); - +
+ import { createEventDispatcher } from 'svelte'; + import KeyCombo from '../KeyCombo.svelte'; + + export let description; + export let value; + + const id = Math.random().toString().slice(2); + const dispatch = createEventDispatcher(); + let listening = false; + + function listen() { + // don't re-listen if we already are + if (listening) return; + + listening = true; + window.addEventListener('keyup', setKeybind, {once: true}); + // setTimeout avoids reacting to the click event that we are currently processing + setTimeout(() => window.addEventListener('click', cancel, {once: true}), 0); + } + + function setKeybind(event) { + console.log(event); + let keys = []; + if (event.ctrlKey) keys.push('ctrl'); + if (event.altKey) keys.push('alt'); + if (event.metaKey) keys.push('meta'); + if (event.shiftKey) keys.push('shift'); + keys.push(event.key); + + value.keys = keys.join('+'); + dispatch('update', {value}); + listening = false; + window.removeEventListener('click', cancel, {once: true}); + event.preventDefault(); + event.stopPropagation(); + } + + function cancel() { + listening = false; + window.removeEventListener('keyup', setKeybind, {once: true}); + } + + + + dispatch('update', {value})} +> + + + diff --git a/src/ui/settings/NumericSetting.svelte b/src/ui/settings/NumericSetting.svelte index 161b923..4c8e4c2 100644 --- a/src/ui/settings/NumericSetting.svelte +++ b/src/ui/settings/NumericSetting.svelte @@ -4,8 +4,8 @@ import Setting from './Setting.svelte'; export let title; - export let divider = true; export let value; + export let unit = ''; export let min = null; export let max = null; @@ -60,7 +60,7 @@ - +
{#if unit} {unit}: diff --git a/src/ui/settings/Setting.svelte b/src/ui/settings/Setting.svelte index d7f0b9a..e506312 100644 --- a/src/ui/settings/Setting.svelte +++ b/src/ui/settings/Setting.svelte @@ -3,20 +3,20 @@ import ErrorAlert from '../ErrorAlert.svelte'; export let title; - export let divider = true; -{#if divider} -
-{/if} -
-

{title}

- -
+
+
+

{title}

+ {#if $$slots.input} + + {/if} +
-{#if $$slots.description} -

- -

-{/if} + {#if $$slots.description} +

+ +

+ {/if} +
diff --git a/src/ui/settings/SettingsGroup.svelte b/src/ui/settings/SettingsGroup.svelte new file mode 100644 index 0000000..97b4d3f --- /dev/null +++ b/src/ui/settings/SettingsGroup.svelte @@ -0,0 +1,14 @@ + + + +
+
+

{name}

+
+ +
+ +
+
diff --git a/src/ui/settings/TextSetting.svelte b/src/ui/settings/TextSetting.svelte index 1bdbfb4..bcb9917 100644 --- a/src/ui/settings/TextSetting.svelte +++ b/src/ui/settings/TextSetting.svelte @@ -3,14 +3,13 @@ import Setting from './Setting.svelte'; export let title; - export let divider = true; export let value; const dispatch = createEventDispatcher(); - +
- +
diff --git a/src/views/Settings.svelte b/src/views/Settings.svelte index eb3a248..602d8f4 100644 --- a/src/views/Settings.svelte +++ b/src/views/Settings.svelte @@ -1,11 +1,18 @@ + + + @@ -36,68 +38,71 @@ {#await invoke('get_config') then config} -
- +
+ + + + Start Creddy when you log in to your computer. + + -
-

General

-
- - - - Start Creddy when you log in to your computer. - - + + + Minimize to the system tray at startup. + + - - - Minimize to the system tray at startup. - - + + + How long to wait after a request is approved/denied before minimizing + the window to tray. Only applicable if the window was minimized + to tray before the request was received. + + - - - How long to wait after a request is approved/denied before minimizing - the window to tray. Only applicable if the window was minimized - to tray before the request was received. - - + + + Listen for credentials requests on this port. + (Should be used with $AWS_CONTAINER_CREDENTIALS_FULL_URI) + + - - - Listen for credentials requests on this port. - (Should be used with $AWS_CONTAINER_CREDENTIALS_FULL_URI) - - + + + + + + Update or re-enter your encrypted credentials. + + - - - - - - Update or re-enter your encrypted credentials. - - + + + Choose your preferred terminal emulator (e.g. gnome-terminal or wt.exe.) May be an absolute path or an executable discoverable on $PATH. + + +
-
-

Terminal

-
+ +
+

Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.

+ +
+ + +
+
+
- - - Choose your preferred terminal emulator (e.g. gnome-terminal, wt.exe.) May be an absolute path or an executable discoverable on $PATH. - -
{/await} From 61d9acc7c60336577727eb4f57c0f8c72c96fb9e Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Mon, 11 Sep 2023 16:00:58 -0700 Subject: [PATCH 9/9] request unlock/credentials when terminal is launched from locked/empty state --- src-tauri/Cargo.toml | 2 +- src-tauri/src/app.rs | 1 - src-tauri/src/config.rs | 14 ++++---- src-tauri/src/errors.rs | 28 ++++++++++++++++ src-tauri/src/ipc.rs | 2 +- src-tauri/src/state.rs | 24 ++++++++++++++ src-tauri/src/terminal.rs | 55 ++++++++++++++++++++++++++----- src/App.svelte | 14 ++++++++ src/lib/queue.js | 4 +++ src/ui/KeyCombo.svelte | 4 ++- src/views/EnterCredentials.svelte | 8 ++++- src/views/Unlock.svelte | 11 +++++-- 12 files changed, 145 insertions(+), 22 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9c78901..b2a9407 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,7 +25,7 @@ tauri-build = { version = "1.0.4", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2", features = ["dialog", "dialog-open", "os-all", "system-tray", "global-shortcut"] } +tauri = { version = "1.2", features = ["dialog", "dialog-open", "global-shortcut", "os-all", "system-tray"] } tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } sodiumoxide = "0.2.7" tokio = { version = ">=1.19", features = ["full"] } diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index 4702646..812eb4c 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -10,7 +10,6 @@ use tauri::{ App, AppHandle, Manager, - GlobalShortcutManager, async_runtime as rt, }; diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index f75c1f7..37e2891 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -26,17 +26,17 @@ pub struct TermConfig { #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct HotkeysConfig { - // tauri uses strings to represent keybinds, so we will as well - pub show_window: Hotkey, - pub launch_terminal: Hotkey, +pub struct Hotkey { + pub keys: String, + pub enabled: bool, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct Hotkey { - pub keys: String, - pub enabled: bool, +pub struct HotkeysConfig { + // tauri uses strings to represent keybinds, so we will as well + pub show_window: Hotkey, + pub launch_terminal: Hotkey, } diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index f564917..f8cd51e 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -244,6 +244,19 @@ pub enum ExecError { } +#[derive(Debug, ThisError, AsRefStr)] +pub enum LaunchTerminalError { + #[error("Could not discover main window")] + NoMainWindow, + #[error("Failed to communicate with main Creddy window")] + IpcFailed(#[from] tauri::Error), + #[error("Failed to launch terminal: {0}")] + Exec(#[from] ExecError), + #[error(transparent)] + GetCredentials(#[from] GetCredentialsError), +} + + // ========================= // Serialize implementations // ========================= @@ -349,3 +362,18 @@ impl Serialize for ExecError { map.end() } } + + +impl Serialize for LaunchTerminalError { + fn serialize(&self, serializer: S) -> Result { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("code", self.as_ref())?; + map.serialize_entry("msg", &format!("{self}"))?; + + match self { + LaunchTerminalError::Exec(src) => map.serialize_entry("source", &src)?, + _ => serialize_upstream_err(self, &mut map)?, + } + map.end() + } +} diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index b34584d..eef4754 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -82,6 +82,6 @@ pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> R #[tauri::command] -pub async fn launch_terminal(base: bool) -> Result<(), ExecError> { +pub async fn launch_terminal(base: bool) -> Result<(), LaunchTerminalError> { terminal::launch(base).await } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 633a1f6..337b6ae 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -29,6 +29,7 @@ pub struct AppState { pub session: RwLock, pub request_count: RwLock, pub open_requests: RwLock>>, + pub pending_terminal_request: RwLock, pub bans: RwLock>>, server: RwLock, pool: sqlx::SqlitePool, @@ -41,6 +42,7 @@ impl AppState { session: RwLock::new(session), request_count: RwLock::new(0), open_requests: RwLock::new(HashMap::new()), + pending_terminal_request: RwLock::new(false), bans: RwLock::new(HashSet::new()), server: RwLock::new(server), pool, @@ -149,6 +151,11 @@ impl AppState { Ok(()) } + pub async fn is_unlocked(&self) -> bool { + let session = self.session.read().await; + matches!(*session, Session::Unlocked{..}) + } + pub async fn serialize_base_creds(&self) -> Result { let app_session = self.session.read().await; let (base, _session) = app_session.try_get()?; @@ -167,4 +174,21 @@ impl AppState { *app_session = Session::Unlocked {base, session}; Ok(()) } + + pub async fn register_terminal_request(&self) -> Result<(), ()> { + let mut req = self.pending_terminal_request.write().await; + if *req { + // if a request is already pending, we can't register a new one + Err(()) + } + else { + *req = true; + Ok(()) + } + } + + pub async fn unregister_terminal_request(&self) { + let mut req = self.pending_terminal_request.write().await; + *req = false; + } } diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs index f0544fa..fcac4db 100644 --- a/src-tauri/src/terminal.rs +++ b/src-tauri/src/terminal.rs @@ -7,9 +7,15 @@ use crate::errors::*; use crate::state::AppState; -pub async fn launch(use_base: bool) -> Result<(), ExecError> { - let state = APP.get().unwrap().state::(); - // do all this in a block so we don't hold the lock any longer than necessary +pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> { + let app = APP.get().unwrap(); + let state = app.state::(); + + // register_terminal_request() returns Err if there is another request pending + if state.register_terminal_request().await.is_err() { + return Ok(()); + } + let mut cmd = { let config = state.config.read().await; let mut cmd = Command::new(&config.terminal.exec); @@ -17,10 +23,38 @@ pub async fn launch(use_base: bool) -> Result<(), ExecError> { cmd }; - // similarly + // if session is unlocked or empty, wait for credentials from frontend + if !state.is_unlocked().await { + app.emit_all("launch-terminal-request", ())?; + let window = app.get_window("main") + .ok_or(LaunchTerminalError::NoMainWindow)?; + if !window.is_visible()? { + window.unminimize()?; + window.show()?; + } + window.set_focus()?; + + let (tx, rx) = tokio::sync::oneshot::channel(); + app.once_global("credentials-event", move |e| { + let success = match e.payload() { + Some("\"unlocked\"") | Some("\"entered\"") => true, + _ => false, + }; + let _ = tx.send(success); + }); + + if !rx.await.unwrap_or(false) { + state.unregister_terminal_request().await; + return Ok(()); // request was canceled by user + } + } + + // more lock-management { - let state = APP.get().unwrap().state::(); let app_session = state.session.read().await; + // session should really be unlocked at this point, but if the frontend misbehaves + // (i.e. lies about unlocking) we could end up here with a locked session + // this will result in an error popup to the user (see main hotkey handler) let (base_creds, session_creds) = app_session.try_get()?; if use_base { cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id); @@ -33,11 +67,16 @@ pub async fn launch(use_base: bool) -> Result<(), ExecError> { } } - match cmd.spawn() { + let res = match cmd.spawn() { Ok(_) => Ok(()), Err(e) if std::io::ErrorKind::NotFound == e.kind() => { Err(ExecError::NotFound(cmd.get_program().to_owned())) }, - Err(e) => Err(e.into()), - } + Err(e) => Err(ExecError::ExecutionFailed(e)), + }; + + state.unregister_terminal_request().await; + + res?; // ? auto-conversion is more liberal than .into() + Ok(()) } diff --git a/src/App.svelte b/src/App.svelte index 4f0d979..891ff8a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -16,6 +16,20 @@ listen('credentials-request', (tauriEvent) => { $appState.pendingRequests.put(tauriEvent.payload); }); +listen('launch-terminal-request', async (tauriEvent) => { + if ($appState.currentRequest === null) { + let status = await invoke('get_session_status'); + if (status === 'locked') { + navigate('Unlock'); + } + else if (status === 'empty') { + navigate('EnterCredentials'); + } + // else, session is unlocked, so do nothing + // (although we shouldn't even get the event in that case) + } +}) + acceptRequest(); diff --git a/src/lib/queue.js b/src/lib/queue.js index 702970f..15816af 100644 --- a/src/lib/queue.js +++ b/src/lib/queue.js @@ -9,6 +9,10 @@ export default function() { resolvers: [], + size() { + return this.items.length; + }, + put(item) { this.items.push(item); let resolver = this.resolvers.shift(); diff --git a/src/ui/KeyCombo.svelte b/src/ui/KeyCombo.svelte index 99b01e1..cd331eb 100644 --- a/src/ui/KeyCombo.svelte +++ b/src/ui/KeyCombo.svelte @@ -5,7 +5,9 @@
{#each keys as key, i} - {#if i > 0} + {/if} + {#if i > 0} + + + {/if} {key} {/each}
diff --git a/src/views/EnterCredentials.svelte b/src/views/EnterCredentials.svelte index e62a441..878ea79 100644 --- a/src/views/EnterCredentials.svelte +++ b/src/views/EnterCredentials.svelte @@ -31,6 +31,7 @@ try { saving = true; await invoke('save_credentials', {credentials, passphrase}); + emit('credentials-event', 'entered'); if ($appState.currentRequest) { navigate('Approve'); } @@ -56,6 +57,11 @@ saving = false; } } + + function cancel() { + emit('credentials-event', 'enter-canceled'); + navigate('Home'); + } @@ -79,7 +85,7 @@ Submit {/if} - + diff --git a/src/views/Unlock.svelte b/src/views/Unlock.svelte index 5a14a16..ea08496 100644 --- a/src/views/Unlock.svelte +++ b/src/views/Unlock.svelte @@ -1,5 +1,6 @@