keep working on cli shortcuts, unify visibility management
This commit is contained in:
parent
47a3e1cfef
commit
4b06dce7f4
@ -19,6 +19,7 @@ use crate::{
|
||||
ipc,
|
||||
server::Server,
|
||||
errors::*,
|
||||
shortcuts,
|
||||
state::AppState,
|
||||
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) {
|
||||
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}"));
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,8 @@ fn main() {
|
||||
None | Some(("run", _)) => launch_gui(),
|
||||
Some(("get", m)) => cli::get(m),
|
||||
Some(("exec", m)) => cli::exec(m),
|
||||
_ => unreachable!(),
|
||||
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
||||
_ => unreachable!("Unknown subcommand"),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
|
@ -6,13 +6,15 @@ use clap::{
|
||||
Command,
|
||||
Arg,
|
||||
ArgMatches,
|
||||
ArgAction
|
||||
ArgAction,
|
||||
builder::PossibleValuesParser,
|
||||
};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::credentials::Credentials;
|
||||
use crate::errors::*;
|
||||
use crate::server::{Request, Response};
|
||||
use crate::shortcuts::ShortcutAction;
|
||||
|
||||
#[cfg(unix)]
|
||||
use {
|
||||
@ -63,6 +65,16 @@ pub fn parser() -> Command<'static> {
|
||||
.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]
|
||||
async fn get_credentials(base: bool) -> Result<Credentials, RequestError> {
|
||||
pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
|
||||
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 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
|
||||
data.push('\n');
|
||||
|
||||
@ -142,12 +179,7 @@ async fn get_credentials(base: bool) -> Result<Credentials, RequestError> {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
stream.read_to_end(&mut buf).await?;
|
||||
let res: Result<Response, ServerError> = serde_json::from_slice(&buf)?;
|
||||
match res {
|
||||
Ok(Response::Aws(creds)) => Ok(creds),
|
||||
// Eventually we will want this
|
||||
// Ok(r) => Err(RequestError::Unexpected(r)),
|
||||
Err(e) => Err(RequestError::Server(e)),
|
||||
}
|
||||
Ok(res?)
|
||||
}
|
||||
|
||||
|
||||
|
@ -26,12 +26,14 @@ use serde::{
|
||||
};
|
||||
|
||||
|
||||
pub trait ErrorPopup {
|
||||
pub trait ShowError {
|
||||
fn error_popup(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) {
|
||||
if let Err(e) = self {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
@ -50,6 +52,18 @@ impl<E: std::fmt::Display> ErrorPopup for Result<(), E> {
|
||||
.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)]
|
||||
pub enum GetCredentialsError {
|
||||
#[error("Credentials are currently locked")]
|
||||
@ -324,6 +347,7 @@ impl Serialize for SerializeWrapper<&GetSessionTokenError> {
|
||||
impl_serialize_basic!(SetupError);
|
||||
impl_serialize_basic!(GetCredentialsError);
|
||||
impl_serialize_basic!(ClientInfoError);
|
||||
impl_serialize_basic!(WindowError);
|
||||
|
||||
|
||||
impl Serialize for HandlerError {
|
||||
|
@ -7,5 +7,6 @@ mod clientinfo;
|
||||
mod ipc;
|
||||
mod state;
|
||||
mod server;
|
||||
mod shortcuts;
|
||||
mod terminal;
|
||||
mod tray;
|
||||
|
@ -6,7 +6,7 @@
|
||||
use creddy::{
|
||||
app,
|
||||
cli,
|
||||
errors::ErrorPopup,
|
||||
errors::ShowError,
|
||||
};
|
||||
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::windows::named_pipe::{
|
||||
NamedPipeServer,
|
||||
@ -21,6 +19,7 @@ use crate::clientinfo::{self, Client};
|
||||
use crate::credentials::Credentials;
|
||||
use crate::ipc::{Approval, AwsRequestNotification};
|
||||
use crate::state::AppState;
|
||||
use crate::shortcuts::{self, ShortcutAction};
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -28,12 +27,14 @@ pub enum Request {
|
||||
GetAwsCredentials{
|
||||
base: bool,
|
||||
},
|
||||
InvokeShortcut(ShortcutAction),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
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)?;
|
||||
match req {
|
||||
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> {
|
||||
let state = app_handle.state::<AppState>();
|
||||
|
||||
let main_window = app_handle.get_window("main").ok_or(HandlerError::NoMainWindow)?;
|
||||
let is_currently_visible = main_window.is_visible()?;
|
||||
let rehide_after = state.get_or_set_rehide(!is_currently_visible).await;
|
||||
let rehide_ms = {
|
||||
let config = state.config.read().await;
|
||||
config.rehide_ms
|
||||
};
|
||||
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 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};
|
||||
app_handle.emit_all("credentials-request", ¬ification)?;
|
||||
|
||||
if !main_window.is_visible()? {
|
||||
main_window.unminimize()?;
|
||||
main_window.show()?;
|
||||
}
|
||||
main_window.set_focus()?;
|
||||
|
||||
match chan_recv.await {
|
||||
Ok(Approval::Approved) => {
|
||||
if base {
|
||||
@ -154,31 +157,6 @@ async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle)
|
||||
}
|
||||
};
|
||||
|
||||
rt::spawn(
|
||||
handle_rehide(rehide_after, app_handle.app_handle())
|
||||
);
|
||||
lease.release();
|
||||
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}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use tauri::{
|
||||
AppHandle,
|
||||
GlobalShortcutManager,
|
||||
Manager,
|
||||
async_runtime as rt,
|
||||
};
|
||||
|
||||
use crate::app::APP;
|
||||
use crate::config::HotkeysConfig;
|
||||
use crate::errors::*;
|
||||
use crate::terminal;
|
||||
|
||||
|
||||
@ -19,11 +21,18 @@ pub enum ShortcutAction {
|
||||
|
||||
pub fn exec_shortcut(action: ShortcutAction) {
|
||||
match action {
|
||||
ShowWindow => {
|
||||
ShortcutAction::ShowWindow => {
|
||||
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 {
|
||||
manager.register(
|
||||
hotkeys.show_window.keys,
|
||||
&hotkeys.show_window.keys,
|
||||
|| exec_shortcut(ShortcutAction::ShowWindow)
|
||||
)?;
|
||||
}
|
||||
|
@ -1,10 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::{
|
||||
sync::RwLock,
|
||||
sync::oneshot::Sender,
|
||||
sync::oneshot::{self, Sender},
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
use tauri::{
|
||||
Manager,
|
||||
async_runtime as rt,
|
||||
};
|
||||
|
||||
use crate::credentials::{
|
||||
Session,
|
||||
@ -14,6 +19,73 @@ use crate::credentials::{
|
||||
use crate::{config, config::AppConfig};
|
||||
use crate::ipc::{self, Approval};
|
||||
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)]
|
||||
@ -22,11 +94,11 @@ pub struct AppState {
|
||||
pub session: RwLock<Session>,
|
||||
pub request_count: RwLock<u64>,
|
||||
pub waiting_requests: RwLock<HashMap<u64, Sender<Approval>>>,
|
||||
pub current_rehide_status: RwLock<Option<bool>>,
|
||||
pub pending_terminal_request: RwLock<bool>,
|
||||
// setup_errors is never modified and so doesn't need to be wrapped in RwLock
|
||||
pub setup_errors: Vec<String>,
|
||||
pool: sqlx::SqlitePool,
|
||||
visibility: RwLock<Visibility>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@ -41,10 +113,10 @@ impl AppState {
|
||||
session: RwLock::new(session),
|
||||
request_count: RwLock::new(0),
|
||||
waiting_requests: RwLock::new(HashMap::new()),
|
||||
current_rehide_status: RwLock::new(None),
|
||||
pending_terminal_request: RwLock::new(false),
|
||||
setup_errors,
|
||||
pool,
|
||||
visibility: RwLock::new(Visibility::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,7 +141,7 @@ impl AppState {
|
||||
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)?;
|
||||
shortcuts::register_hotkeys(&new_config.hotkeys)?;
|
||||
}
|
||||
|
||||
new_config.save(&self.pool).await?;
|
||||
@ -94,25 +166,9 @@ impl AppState {
|
||||
waiting_requests.remove(&id);
|
||||
}
|
||||
|
||||
pub async fn req_count(&self) -> usize {
|
||||
let waiting_requests = self.waiting_requests.read().await;
|
||||
waiting_requests.len()
|
||||
}
|
||||
|
||||
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 acquire_visibility_lease(&self, delay: u64) -> Result<VisibilityLease, WindowError> {
|
||||
let mut visibility = self.visibility.write().await;
|
||||
visibility.acquire(delay)
|
||||
}
|
||||
|
||||
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
|
||||
|
@ -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 !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 lease = state.acquire_visibility_lease(0).await
|
||||
.map_err(|_e| LaunchTerminalError::NoMainWindow)?; // automate conversion eventually?
|
||||
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
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;
|
||||
return Ok(()); // request was canceled by user
|
||||
}
|
||||
lease.release();
|
||||
}
|
||||
|
||||
// more lock-management
|
||||
|
Loading…
x
Reference in New Issue
Block a user