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 @@