keep working on cli shortcuts, unify visibility management

This commit is contained in:
Joseph Montanaro 2023-09-21 10:44:35 -07:00
parent 47a3e1cfef
commit 4b06dce7f4
10 changed files with 190 additions and 92 deletions

View File

@ -19,6 +19,7 @@ use crate::{
ipc, ipc,
server::Server, server::Server,
errors::*, errors::*,
shortcuts,
state::AppState, state::AppState,
tray, tray,
}; };
@ -99,7 +100,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
if let Err(_e) = config::set_auto_launch(conf.start_on_login) { if let Err(_e) = config::set_auto_launch(conf.start_on_login) {
setup_errors.push("Error: Failed to manage autolaunch.".into()); setup_errors.push("Error: Failed to manage autolaunch.".into());
} }
if let Err(e) = config::register_hotkeys(&conf.hotkeys) { if let Err(e) = shortcuts::register_hotkeys(&conf.hotkeys) {
setup_errors.push(format!("{e}")); setup_errors.push(format!("{e}"));
} }

View File

@ -21,7 +21,8 @@ fn main() {
None | Some(("run", _)) => launch_gui(), None | Some(("run", _)) => launch_gui(),
Some(("get", m)) => cli::get(m), Some(("get", m)) => cli::get(m),
Some(("exec", m)) => cli::exec(m), Some(("exec", m)) => cli::exec(m),
_ => unreachable!(), Some(("shortcut", m)) => cli::invoke_shortcut(m),
_ => unreachable!("Unknown subcommand"),
}; };
if let Err(e) = res { if let Err(e) = res {

View File

@ -6,13 +6,15 @@ use clap::{
Command, Command,
Arg, Arg,
ArgMatches, ArgMatches,
ArgAction ArgAction,
builder::PossibleValuesParser,
}; };
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::credentials::Credentials; use crate::credentials::Credentials;
use crate::errors::*; use crate::errors::*;
use crate::server::{Request, Response}; use crate::server::{Request, Response};
use crate::shortcuts::ShortcutAction;
#[cfg(unix)] #[cfg(unix)]
use { use {
@ -63,6 +65,16 @@ pub fn parser() -> Command<'static> {
.multiple_values(true) .multiple_values(true)
) )
) )
.subcommand(
Command::new("shortcut")
.about("Invoke an action normally trigged by hotkey (e.g. launch terminal)")
.arg(
Arg::new("action")
.value_parser(
PossibleValuesParser::new(["show_window", "launch_terminal"])
)
)
)
} }
@ -129,10 +141,35 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
} }
#[tokio::main] pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
async fn get_credentials(base: bool) -> Result<Credentials, RequestError> { let action = match args.get_one::<String>("action").map(|s| s.as_str()) {
Some("show_window") => ShortcutAction::ShowWindow,
Some("launch_terminal") => ShortcutAction::LaunchTerminal,
Some(&_) | None => unreachable!("Unknown shortcut action"), // guaranteed by clap
};
let req = Request::InvokeShortcut(action);
match make_request(&req) {
Ok(Response::Empty) => Ok(()),
Ok(r) => Err(RequestError::Unexpected(r).into()),
Err(e) => Err(e.into()),
}
}
fn get_credentials(base: bool) -> Result<Credentials, RequestError> {
let req = Request::GetAwsCredentials { base }; let req = Request::GetAwsCredentials { base };
let mut data = serde_json::to_string(&req).unwrap(); match make_request(&req) {
Ok(Response::Aws(creds)) => Ok(creds),
Ok(r) => Err(RequestError::Unexpected(r)),
Err(e) => Err(e),
}
}
#[tokio::main]
async fn make_request(req: &Request) -> Result<Response, RequestError> {
let mut data = serde_json::to_string(req).unwrap();
// server expects newline marking end of request // server expects newline marking end of request
data.push('\n'); data.push('\n');
@ -142,12 +179,7 @@ async fn get_credentials(base: bool) -> Result<Credentials, RequestError> {
let mut buf = Vec::with_capacity(1024); let mut buf = Vec::with_capacity(1024);
stream.read_to_end(&mut buf).await?; stream.read_to_end(&mut buf).await?;
let res: Result<Response, ServerError> = serde_json::from_slice(&buf)?; let res: Result<Response, ServerError> = serde_json::from_slice(&buf)?;
match res { Ok(res?)
Ok(Response::Aws(creds)) => Ok(creds),
// Eventually we will want this
// Ok(r) => Err(RequestError::Unexpected(r)),
Err(e) => Err(RequestError::Server(e)),
}
} }

View File

@ -26,12 +26,14 @@ use serde::{
}; };
pub trait ErrorPopup { pub trait ShowError {
fn error_popup(self, title: &str); fn error_popup(self, title: &str);
fn error_popup_nowait(self, title: &str); fn error_popup_nowait(self, title: &str);
fn error_print(self);
fn error_print_prefix(self, prefix: &str);
} }
impl<E: std::fmt::Display> ErrorPopup for Result<(), E> { impl<E: std::fmt::Display> ShowError for Result<(), E> {
fn error_popup(self, title: &str) { fn error_popup(self, title: &str) {
if let Err(e) = self { if let Err(e) = self {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
@ -50,6 +52,18 @@ impl<E: std::fmt::Display> ErrorPopup for Result<(), E> {
.show(|_| {}) .show(|_| {})
} }
} }
fn error_print(self) {
if let Err(e) = self {
eprintln!("{e}");
}
}
fn error_print_prefix(self, prefix: &str) {
if let Err(e) = self {
eprintln!("{prefix}: {e}");
}
}
} }
@ -164,6 +178,15 @@ pub enum HandlerError {
} }
#[derive(Debug, ThisError, AsRefStr)]
pub enum WindowError {
#[error("Failed to find main application window")]
NoMainWindow,
#[error(transparent)]
ManageFailure(#[from] tauri::Error),
}
#[derive(Debug, ThisError, AsRefStr)] #[derive(Debug, ThisError, AsRefStr)]
pub enum GetCredentialsError { pub enum GetCredentialsError {
#[error("Credentials are currently locked")] #[error("Credentials are currently locked")]
@ -324,6 +347,7 @@ impl Serialize for SerializeWrapper<&GetSessionTokenError> {
impl_serialize_basic!(SetupError); impl_serialize_basic!(SetupError);
impl_serialize_basic!(GetCredentialsError); impl_serialize_basic!(GetCredentialsError);
impl_serialize_basic!(ClientInfoError); impl_serialize_basic!(ClientInfoError);
impl_serialize_basic!(WindowError);
impl Serialize for HandlerError { impl Serialize for HandlerError {

View File

@ -7,5 +7,6 @@ mod clientinfo;
mod ipc; mod ipc;
mod state; mod state;
mod server; mod server;
mod shortcuts;
mod terminal; mod terminal;
mod tray; mod tray;

View File

@ -6,7 +6,7 @@
use creddy::{ use creddy::{
app, app,
cli, cli,
errors::ErrorPopup, errors::ShowError,
}; };

View File

@ -1,5 +1,3 @@
use std::time::Duration;
#[cfg(windows)] #[cfg(windows)]
use tokio::net::windows::named_pipe::{ use tokio::net::windows::named_pipe::{
NamedPipeServer, NamedPipeServer,
@ -21,6 +19,7 @@ use crate::clientinfo::{self, Client};
use crate::credentials::Credentials; use crate::credentials::Credentials;
use crate::ipc::{Approval, AwsRequestNotification}; use crate::ipc::{Approval, AwsRequestNotification};
use crate::state::AppState; use crate::state::AppState;
use crate::shortcuts::{self, ShortcutAction};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -28,12 +27,14 @@ pub enum Request {
GetAwsCredentials{ GetAwsCredentials{
base: bool, base: bool,
}, },
InvokeShortcut(ShortcutAction),
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum Response { pub enum Response {
Aws(Credentials) Aws(Credentials),
Empty,
} }
@ -102,17 +103,25 @@ async fn handle(stream: &mut NamedPipeServer, app_handle: AppHandle) -> Result<R
let req: Request = serde_json::from_slice(&buf)?; let req: Request = serde_json::from_slice(&buf)?;
match req { match req {
Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await, Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await,
// etc Request::InvokeShortcut(action) => invoke_shortcut(action).await,
} }
} }
async fn invoke_shortcut(action: ShortcutAction) -> Result<Response, HandlerError> {
shortcuts::exec_shortcut(action);
Ok(Response::Empty)
}
async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) -> Result<Response, HandlerError> { async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) -> Result<Response, HandlerError> {
let state = app_handle.state::<AppState>(); let state = app_handle.state::<AppState>();
let rehide_ms = {
let main_window = app_handle.get_window("main").ok_or(HandlerError::NoMainWindow)?; let config = state.config.read().await;
let is_currently_visible = main_window.is_visible()?; config.rehide_ms
let rehide_after = state.get_or_set_rehide(!is_currently_visible).await; };
let lease = state.acquire_visibility_lease(rehide_ms).await
.map_err(|_e| HandlerError::NoMainWindow)?; // automate this conversion eventually?
let (chan_send, chan_recv) = oneshot::channel(); let (chan_send, chan_recv) = oneshot::channel();
let request_id = state.register_request(chan_send).await; let request_id = state.register_request(chan_send).await;
@ -124,12 +133,6 @@ async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle)
let notification = AwsRequestNotification {id: request_id, client, base}; let notification = AwsRequestNotification {id: request_id, client, base};
app_handle.emit_all("credentials-request", &notification)?; app_handle.emit_all("credentials-request", &notification)?;
if !main_window.is_visible()? {
main_window.unminimize()?;
main_window.show()?;
}
main_window.set_focus()?;
match chan_recv.await { match chan_recv.await {
Ok(Approval::Approved) => { Ok(Approval::Approved) => {
if base { if base {
@ -154,31 +157,6 @@ async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle)
} }
}; };
rt::spawn( lease.release();
handle_rehide(rehide_after, app_handle.app_handle())
);
result result
} }
async fn handle_rehide(rehide_after: bool, app_handle: AppHandle) {
let state = app_handle.state::<AppState>();
let delay = {
let config = state.config.read().await;
Duration::from_millis(config.rehide_ms)
};
tokio::time::sleep(delay).await;
// if there are no other pending requests, set rehide status back to None
if state.req_count().await == 0 {
state.clear_rehide().await;
// and hide the window if necessary
if rehide_after {
app_handle.get_window("main").map(|w| {
if let Err(e) = w.hide() {
eprintln!("{e}");
}
});
}
}
}

View File

@ -1,12 +1,14 @@
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use tauri::{ use tauri::{
AppHandle, GlobalShortcutManager,
Manager, Manager,
async_runtime as rt,
}; };
use crate::app::APP; use crate::app::APP;
use crate::config::HotkeysConfig; use crate::config::HotkeysConfig;
use crate::errors::*;
use crate::terminal; use crate::terminal;
@ -19,11 +21,18 @@ pub enum ShortcutAction {
pub fn exec_shortcut(action: ShortcutAction) { pub fn exec_shortcut(action: ShortcutAction) {
match action { match action {
ShowWindow => { ShortcutAction::ShowWindow => {
let app = APP.get().unwrap(); let app = APP.get().unwrap();
app.get_window("main").map(|w| w.show()); app.get_window("main")
.ok_or("Couldn't find application main window")
.map(|w| w.show().error_popup("Failed to show window"))
.error_popup("Failed to show window");
},
ShortcutAction::LaunchTerminal => {
rt::spawn(async {
terminal::launch(false).await.error_popup("Failed to launch terminal");
});
}, },
LaunchTerminal => terminal::launch(false),
} }
} }
@ -35,7 +44,7 @@ pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> {
if hotkeys.show_window.enabled { if hotkeys.show_window.enabled {
manager.register( manager.register(
hotkeys.show_window.keys, &hotkeys.show_window.keys,
|| exec_shortcut(ShortcutAction::ShowWindow) || exec_shortcut(ShortcutAction::ShowWindow)
)?; )?;
} }

View File

@ -1,10 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration;
use tokio::{ use tokio::{
sync::RwLock, sync::RwLock,
sync::oneshot::Sender, sync::oneshot::{self, Sender},
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tauri::{
Manager,
async_runtime as rt,
};
use crate::credentials::{ use crate::credentials::{
Session, Session,
@ -14,6 +19,73 @@ use crate::credentials::{
use crate::{config, config::AppConfig}; use crate::{config, config::AppConfig};
use crate::ipc::{self, Approval}; use crate::ipc::{self, Approval};
use crate::errors::*; use crate::errors::*;
use crate::shortcuts;
#[derive(Debug)]
struct Visibility {
leases: usize,
original: Option<bool>,
}
impl Visibility {
fn new() -> Self {
Visibility { leases: 0, original: None }
}
fn acquire(&mut self, delay_ms: u64) -> Result<VisibilityLease, WindowError> {
let app = crate::app::APP.get().unwrap();
let window = app.get_window("main")
.ok_or(WindowError::NoMainWindow)?;
self.leases += 1;
if self.original.is_none() {
let is_visible = window.is_visible()?;
self.original = Some(is_visible);
if !is_visible {
window.show()?;
}
}
window.set_focus()?;
let (tx, rx) = oneshot::channel();
let lease = VisibilityLease { notify: tx };
let delay = Duration::from_millis(delay_ms);
let handle = app.app_handle();
rt::spawn(async move {
// We don't care if it's an error; lease being dropped should be handled identically
let _ = rx.await;
tokio::time::sleep(delay).await;
// we can't use `self` here because we would have to move it into the async block
let state = handle.state::<AppState>();
let mut visibility = state.visibility.write().await;
visibility.leases -= 1;
if visibility.leases == 0 {
if let Some(false) = visibility.original {
window.hide().error_print();
}
visibility.original = None;
}
});
Ok(lease)
}
}
pub struct VisibilityLease {
notify: Sender<()>,
}
impl VisibilityLease {
pub fn release(self) {
rt::spawn(async move {
if let Err(_) = self.notify.send(()) {
eprintln!("Error releasing visibility lease")
}
});
}
}
#[derive(Debug)] #[derive(Debug)]
@ -22,11 +94,11 @@ pub struct AppState {
pub session: RwLock<Session>, pub session: RwLock<Session>,
pub request_count: RwLock<u64>, pub request_count: RwLock<u64>,
pub waiting_requests: RwLock<HashMap<u64, Sender<Approval>>>, pub waiting_requests: RwLock<HashMap<u64, Sender<Approval>>>,
pub current_rehide_status: RwLock<Option<bool>>,
pub pending_terminal_request: RwLock<bool>, pub pending_terminal_request: RwLock<bool>,
// setup_errors is never modified and so doesn't need to be wrapped in RwLock // setup_errors is never modified and so doesn't need to be wrapped in RwLock
pub setup_errors: Vec<String>, pub setup_errors: Vec<String>,
pool: sqlx::SqlitePool, pool: sqlx::SqlitePool,
visibility: RwLock<Visibility>,
} }
impl AppState { impl AppState {
@ -41,10 +113,10 @@ impl AppState {
session: RwLock::new(session), session: RwLock::new(session),
request_count: RwLock::new(0), request_count: RwLock::new(0),
waiting_requests: RwLock::new(HashMap::new()), waiting_requests: RwLock::new(HashMap::new()),
current_rehide_status: RwLock::new(None),
pending_terminal_request: RwLock::new(false), pending_terminal_request: RwLock::new(false),
setup_errors, setup_errors,
pool, pool,
visibility: RwLock::new(Visibility::new()),
} }
} }
@ -69,7 +141,7 @@ impl AppState {
if new_config.hotkeys.show_window != live_config.hotkeys.show_window if new_config.hotkeys.show_window != live_config.hotkeys.show_window
|| new_config.hotkeys.launch_terminal != live_config.hotkeys.launch_terminal || new_config.hotkeys.launch_terminal != live_config.hotkeys.launch_terminal
{ {
config::register_hotkeys(&new_config.hotkeys)?; shortcuts::register_hotkeys(&new_config.hotkeys)?;
} }
new_config.save(&self.pool).await?; new_config.save(&self.pool).await?;
@ -94,25 +166,9 @@ impl AppState {
waiting_requests.remove(&id); waiting_requests.remove(&id);
} }
pub async fn req_count(&self) -> usize { pub async fn acquire_visibility_lease(&self, delay: u64) -> Result<VisibilityLease, WindowError> {
let waiting_requests = self.waiting_requests.read().await; let mut visibility = self.visibility.write().await;
waiting_requests.len() visibility.acquire(delay)
}
pub async fn get_or_set_rehide(&self, new_value: bool) -> bool {
let mut rehide = self.current_rehide_status.write().await;
match *rehide {
Some(original) => original,
None => {
*rehide = Some(new_value);
new_value
}
}
}
pub async fn clear_rehide(&self) {
let mut rehide = self.current_rehide_status.write().await;
*rehide = None;
} }
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> { pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {

View File

@ -26,13 +26,8 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
// if session is unlocked or empty, wait for credentials from frontend // if session is unlocked or empty, wait for credentials from frontend
if !state.is_unlocked().await { if !state.is_unlocked().await {
app.emit_all("launch-terminal-request", ())?; app.emit_all("launch-terminal-request", ())?;
let window = app.get_window("main") let lease = state.acquire_visibility_lease(0).await
.ok_or(LaunchTerminalError::NoMainWindow)?; .map_err(|_e| LaunchTerminalError::NoMainWindow)?; // automate conversion eventually?
if !window.is_visible()? {
window.unminimize()?;
window.show()?;
}
window.set_focus()?;
let (tx, rx) = tokio::sync::oneshot::channel(); let (tx, rx) = tokio::sync::oneshot::channel();
app.once_global("credentials-event", move |e| { app.once_global("credentials-event", move |e| {
@ -47,6 +42,7 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
state.unregister_terminal_request().await; state.unregister_terminal_request().await;
return Ok(()); // request was canceled by user return Ok(()); // request was canceled by user
} }
lease.release();
} }
// more lock-management // more lock-management