Compare commits
5 Commits
47a3e1cfef
...
pipe
Author | SHA1 | Date | |
---|---|---|---|
1b749a857c | |||
2079f99d04 | |||
5e0ffc1155 | |||
d4fa8966b2 | |||
4b06dce7f4 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,6 +2,9 @@ dist
|
|||||||
**/node_modules
|
**/node_modules
|
||||||
src-tauri/target/
|
src-tauri/target/
|
||||||
**/creddy.db
|
**/creddy.db
|
||||||
|
# .env is system-specific
|
||||||
|
.env
|
||||||
|
.vscode
|
||||||
|
|
||||||
# just in case
|
# just in case
|
||||||
credentials*
|
credentials*
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "creddy",
|
"name": "creddy",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
@ -1 +0,0 @@
|
|||||||
DATABASE_URL=sqlite://C:/Users/Joe/AppData/Roaming/creddy/creddy.dev.db
|
|
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -1035,7 +1035,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "creddy"
|
name = "creddy"
|
||||||
version = "0.3.3"
|
version = "0.3.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"auto-launch",
|
"auto-launch",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "creddy"
|
name = "creddy"
|
||||||
version = "0.3.3"
|
version = "0.3.4"
|
||||||
description = "A friendly AWS credentials manager"
|
description = "A friendly AWS credentials manager"
|
||||||
authors = ["Joseph Montanaro"]
|
authors = ["Joseph Montanaro"]
|
||||||
license = ""
|
license = ""
|
||||||
|
@ -19,6 +19,7 @@ use crate::{
|
|||||||
ipc,
|
ipc,
|
||||||
server::Server,
|
server::Server,
|
||||||
errors::*,
|
errors::*,
|
||||||
|
shortcuts,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
tray,
|
tray,
|
||||||
};
|
};
|
||||||
@ -81,7 +82,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
|||||||
let pool = connect_db().await?;
|
let pool = connect_db().await?;
|
||||||
let mut setup_errors: Vec<String> = vec![];
|
let mut setup_errors: Vec<String> = vec![];
|
||||||
|
|
||||||
let conf = match AppConfig::load(&pool).await {
|
let mut conf = match AppConfig::load(&pool).await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(SetupError::ConfigParseError(_)) => {
|
Err(SetupError::ConfigParseError(_)) => {
|
||||||
setup_errors.push(
|
setup_errors.push(
|
||||||
@ -99,8 +100,12 @@ 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) {
|
|
||||||
setup_errors.push(format!("{e}"));
|
// if hotkeys fail to register, disable them so that this error doesn't have to keep showing up
|
||||||
|
if let Err(_e) = shortcuts::register_hotkeys(&conf.hotkeys) {
|
||||||
|
conf.hotkeys.disable_all();
|
||||||
|
conf.save(&pool).await?;
|
||||||
|
setup_errors.push("Failed to register hotkeys. Hotkey settings have been disabled.".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// if session is empty, this is probably the first launch, so don't autohide
|
// if session is empty, this is probably the first launch, so don't autohide
|
||||||
|
@ -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 {
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::process::Command as ChildCommand;
|
use std::process::Command as ChildCommand;
|
||||||
|
#[cfg(windows)]
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use clap::{
|
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 {
|
||||||
std::os::unix::process::CommandExt,
|
std::os::unix::process::CommandExt,
|
||||||
std::path::Path,
|
|
||||||
tokio::net::UnixStream,
|
tokio::net::UnixStream,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -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)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -167,7 +199,5 @@ async fn connect() -> Result<NamedPipeClient, std::io::Error> {
|
|||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
async fn connect() -> Result<UnixStream, std::io::Error> {
|
async fn connect() -> Result<UnixStream, std::io::Error> {
|
||||||
let path = Path::from("/tmp/creddy-requests");
|
UnixStream::connect("/tmp/creddy.sock").await
|
||||||
std::fs::remove_file(path)?;
|
|
||||||
UnixStream::connect(path)
|
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,6 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
|
use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use std::os::windows::io::AsRawHandle;
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use {
|
|
||||||
tokio::net::windows::named_pipe::NamedPipeServer,
|
|
||||||
windows::Win32::{
|
|
||||||
Foundation::HANDLE,
|
|
||||||
System::Pipes::GetNamedPipeClientProcessId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use tokio::net::UnixStream;
|
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
@ -26,25 +13,8 @@ pub struct Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
pub fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
|
||||||
pub fn get_client_parent(stream: &UnixStream) -> Result<Client, ClientInfoError> {
|
dbg!(pid);
|
||||||
let pid = stream.peer_cred()?;
|
|
||||||
get_process_parent_info(pid)?
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub fn get_client_parent(stream: &NamedPipeServer) -> Result<Client, ClientInfoError> {
|
|
||||||
let raw_handle = stream.as_raw_handle();
|
|
||||||
let mut pid = 0u32;
|
|
||||||
let handle = HANDLE(raw_handle as _);
|
|
||||||
unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? };
|
|
||||||
|
|
||||||
get_process_parent_info(pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
|
|
||||||
let sys_pid = Pid::from_u32(pid);
|
let sys_pid = Pid::from_u32(pid);
|
||||||
let mut sys = System::new();
|
let mut sys = System::new();
|
||||||
sys.refresh_process(sys_pid);
|
sys.refresh_process(sys_pid);
|
||||||
|
@ -4,11 +4,6 @@ use auto_launch::AutoLaunchBuilder;
|
|||||||
use is_terminal::IsTerminal;
|
use is_terminal::IsTerminal;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use tauri::{
|
|
||||||
Manager,
|
|
||||||
GlobalShortcutManager,
|
|
||||||
async_runtime as rt,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
@ -38,6 +33,13 @@ pub struct HotkeysConfig {
|
|||||||
pub launch_terminal: Hotkey,
|
pub launch_terminal: Hotkey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HotkeysConfig {
|
||||||
|
pub fn disable_all(&mut self) {
|
||||||
|
self.show_window.enabled = false;
|
||||||
|
self.launch_terminal.enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
@ -183,43 +185,6 @@ fn default_hotkey_config() -> HotkeysConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: will panic if called before APP is set
|
|
||||||
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()?;
|
|
||||||
|
|
||||||
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");
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn default_rehide_ms() -> u64 { 1000 }
|
fn default_rehide_ms() -> u64 { 1000 }
|
||||||
// start minimized and on login only in production mode
|
// start minimized and on login only in production mode
|
||||||
|
@ -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")]
|
||||||
@ -221,6 +244,7 @@ pub enum ClientInfoError {
|
|||||||
ParentPidNotFound,
|
ParentPidNotFound,
|
||||||
#[error("Found PID for parent process of client, but no corresponding process")]
|
#[error("Found PID for parent process of client, but no corresponding process")]
|
||||||
ParentProcessNotFound,
|
ParentProcessNotFound,
|
||||||
|
#[cfg(windows)]
|
||||||
#[error("Could not determine PID of connected client")]
|
#[error("Could not determine PID of connected client")]
|
||||||
WindowsError(#[from] windows::core::Error),
|
WindowsError(#[from] windows::core::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
@ -324,6 +348,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 {
|
||||||
|
@ -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;
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
use creddy::{
|
use creddy::{
|
||||||
app,
|
app,
|
||||||
cli,
|
cli,
|
||||||
errors::ErrorPopup,
|
errors::ShowError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ fn main() {
|
|||||||
},
|
},
|
||||||
Some(("get", m)) => cli::get(m),
|
Some(("get", m)) => cli::get(m),
|
||||||
Some(("exec", m)) => cli::exec(m),
|
Some(("exec", m)) => cli::exec(m),
|
||||||
|
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,184 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use tokio::net::windows::named_pipe::{
|
|
||||||
NamedPipeServer,
|
|
||||||
ServerOptions,
|
|
||||||
};
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::sync::oneshot;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
|
|
||||||
use tauri::{
|
|
||||||
AppHandle,
|
|
||||||
Manager,
|
|
||||||
async_runtime as rt,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::errors::*;
|
|
||||||
use crate::clientinfo::{self, Client};
|
|
||||||
use crate::credentials::Credentials;
|
|
||||||
use crate::ipc::{Approval, AwsRequestNotification};
|
|
||||||
use crate::state::AppState;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub enum Request {
|
|
||||||
GetAwsCredentials{
|
|
||||||
base: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub enum Response {
|
|
||||||
Aws(Credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub struct Server {
|
|
||||||
listener: tokio::net::windows::named_pipe::NamedPipeServer,
|
|
||||||
app_handle: AppHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Server {
|
|
||||||
pub fn start(app_handle: AppHandle) -> std::io::Result<()> {
|
|
||||||
let listener = ServerOptions::new()
|
|
||||||
.first_pipe_instance(true)
|
|
||||||
.create(r"\\.\pipe\creddy-requests")?;
|
|
||||||
|
|
||||||
let srv = Server {listener, app_handle};
|
|
||||||
rt::spawn(srv.serve());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn serve(mut self) {
|
|
||||||
loop {
|
|
||||||
if let Err(e) = self.try_serve().await {
|
|
||||||
eprintln!("Error accepting connection: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_serve(&mut self) -> std::io::Result<()> {
|
|
||||||
// connect() just waits for a client to connect, it doesn't return anything
|
|
||||||
self.listener.connect().await?;
|
|
||||||
|
|
||||||
// create a new pipe instance to listen for the next client, and swap it in
|
|
||||||
let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?;
|
|
||||||
let mut stream = std::mem::replace(&mut self.listener, new_listener);
|
|
||||||
let new_handle = self.app_handle.app_handle();
|
|
||||||
rt::spawn(async move {
|
|
||||||
let res = serde_json::to_string(
|
|
||||||
&handle(&mut stream, new_handle).await
|
|
||||||
).unwrap();
|
|
||||||
if let Err(e) = stream.write_all(res.as_bytes()).await {
|
|
||||||
eprintln!("Error responding to request: {e}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async fn handle(stream: &mut NamedPipeServer, app_handle: AppHandle) -> Result<Response, HandlerError> {
|
|
||||||
// read from stream until delimiter is reached
|
|
||||||
let mut buf: Vec<u8> = Vec::with_capacity(1024); // requests are small, 1KiB is more than enough
|
|
||||||
let mut n = 0;
|
|
||||||
loop {
|
|
||||||
n += stream.read_buf(&mut buf).await?;
|
|
||||||
if let Some(&b'\n') = buf.last() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if n >= 1024 {
|
|
||||||
return Err(HandlerError::RequestTooLarge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = clientinfo::get_client_parent(&stream)?;
|
|
||||||
|
|
||||||
let req: Request = serde_json::from_slice(&buf)?;
|
|
||||||
match req {
|
|
||||||
Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await,
|
|
||||||
// etc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 (chan_send, chan_recv) = oneshot::channel();
|
|
||||||
let request_id = state.register_request(chan_send).await;
|
|
||||||
|
|
||||||
// if an error occurs in any of the following, we want to abort the operation
|
|
||||||
// but ? returns immediately, and we want to unregister the request before returning
|
|
||||||
// so we bundle it all up in an async block and return a Result so we can handle errors
|
|
||||||
let proceed = async {
|
|
||||||
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 {
|
|
||||||
let creds = state.base_creds_cloned().await?;
|
|
||||||
Ok(Response::Aws(Credentials::Base(creds)))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let creds = state.session_creds_cloned().await?;
|
|
||||||
Ok(Response::Aws(Credentials::Session(creds)))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(Approval::Denied) => Err(HandlerError::Denied),
|
|
||||||
Err(_e) => Err(HandlerError::Internal),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = match proceed.await {
|
|
||||||
Ok(r) => Ok(r),
|
|
||||||
Err(e) => {
|
|
||||||
state.unregister_request(request_id).await;
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rt::spawn(
|
|
||||||
handle_rehide(rehide_after, app_handle.app_handle())
|
|
||||||
);
|
|
||||||
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}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
126
src-tauri/src/server/mod.rs
Normal file
126
src-tauri/src/server/mod.rs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::clientinfo::{self, Client};
|
||||||
|
use crate::credentials::Credentials;
|
||||||
|
use crate::ipc::{Approval, AwsRequestNotification};
|
||||||
|
use crate::state::AppState;
|
||||||
|
use crate::shortcuts::{self, ShortcutAction};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod server_win;
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub use server_win::Server;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use server_win::Stream;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod server_unix;
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub use server_unix::Server;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use server_unix::Stream;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum Request {
|
||||||
|
GetAwsCredentials{
|
||||||
|
base: bool,
|
||||||
|
},
|
||||||
|
InvokeShortcut(ShortcutAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Response {
|
||||||
|
Aws(Credentials),
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> Result<(), HandlerError>
|
||||||
|
{
|
||||||
|
// read from stream until delimiter is reached
|
||||||
|
let mut buf: Vec<u8> = Vec::with_capacity(1024); // requests are small, 1KiB is more than enough
|
||||||
|
let mut n = 0;
|
||||||
|
loop {
|
||||||
|
n += stream.read_buf(&mut buf).await?;
|
||||||
|
if let Some(&b'\n') = buf.last() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if n >= 1024 {
|
||||||
|
return Err(HandlerError::RequestTooLarge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = clientinfo::get_process_parent_info(client_pid)?;
|
||||||
|
|
||||||
|
let req: Request = serde_json::from_slice(&buf)?;
|
||||||
|
let res = match req {
|
||||||
|
Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await,
|
||||||
|
Request::InvokeShortcut(action) => invoke_shortcut(action).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = serde_json::to_vec(&res).unwrap();
|
||||||
|
stream.write_all(&res).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
// if an error occurs in any of the following, we want to abort the operation
|
||||||
|
// but ? returns immediately, and we want to unregister the request before returning
|
||||||
|
// so we bundle it all up in an async block and return a Result so we can handle errors
|
||||||
|
let proceed = async {
|
||||||
|
let notification = AwsRequestNotification {id: request_id, client, base};
|
||||||
|
app_handle.emit_all("credentials-request", ¬ification)?;
|
||||||
|
|
||||||
|
match chan_recv.await {
|
||||||
|
Ok(Approval::Approved) => {
|
||||||
|
if base {
|
||||||
|
let creds = state.base_creds_cloned().await?;
|
||||||
|
Ok(Response::Aws(Credentials::Base(creds)))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let creds = state.session_creds_cloned().await?;
|
||||||
|
Ok(Response::Aws(Credentials::Session(creds)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(Approval::Denied) => Err(HandlerError::Denied),
|
||||||
|
Err(_e) => Err(HandlerError::Internal),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match proceed.await {
|
||||||
|
Ok(r) => Ok(r),
|
||||||
|
Err(e) => {
|
||||||
|
state.unregister_request(request_id).await;
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
lease.release();
|
||||||
|
result
|
||||||
|
}
|
59
src-tauri/src/server/server_unix.rs
Normal file
59
src-tauri/src/server/server_unix.rs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
use std::io::ErrorKind;
|
||||||
|
use tokio::net::{UnixListener, UnixStream};
|
||||||
|
use tauri::{
|
||||||
|
AppHandle,
|
||||||
|
Manager,
|
||||||
|
async_runtime as rt,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
pub type Stream = UnixStream;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
listener: UnixListener,
|
||||||
|
app_handle: AppHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub fn start(app_handle: AppHandle) -> std::io::Result<()> {
|
||||||
|
match std::fs::remove_file("/tmp/creddy.sock") {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => (),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
let listener = UnixListener::bind("/tmp/creddy.sock")?;
|
||||||
|
let srv = Server { listener, app_handle };
|
||||||
|
rt::spawn(srv.serve());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve(self) {
|
||||||
|
loop {
|
||||||
|
self.try_serve()
|
||||||
|
.await
|
||||||
|
.error_print_prefix("Error accepting request: ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_serve(&self) -> Result<(), HandlerError> {
|
||||||
|
let (stream, _addr) = self.listener.accept().await?;
|
||||||
|
let new_handle = self.app_handle.app_handle();
|
||||||
|
let client_pid = get_client_pid(&stream)?;
|
||||||
|
rt::spawn(async move {
|
||||||
|
super::handle(stream, new_handle, client_pid)
|
||||||
|
.await
|
||||||
|
.error_print_prefix("Error responding to request: ");
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn get_client_pid(stream: &UnixStream) -> std::io::Result<u32> {
|
||||||
|
let cred = stream.peer_cred()?;
|
||||||
|
Ok(cred.pid().unwrap() as u32)
|
||||||
|
}
|
75
src-tauri/src/server/server_win.rs
Normal file
75
src-tauri/src/server/server_win.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use tokio::{
|
||||||
|
net::windows::named_pipe::{
|
||||||
|
NamedPipeServer,
|
||||||
|
ServerOptions,
|
||||||
|
},
|
||||||
|
sync::oneshot,
|
||||||
|
};
|
||||||
|
|
||||||
|
use windows::Win32:: {
|
||||||
|
Foundation::HANDLE,
|
||||||
|
System::Pipes::GetNamedPipeClientProcessId,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::os::windows::io::AsRawHandle;
|
||||||
|
|
||||||
|
use tauri::async_runtime as rt;
|
||||||
|
|
||||||
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
|
// used by parent module
|
||||||
|
pub type Stream = NamedPipeServer;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
listener: NamedPipeServer,
|
||||||
|
app_handle: AppHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub fn start(app_handle: AppHandle) -> std::io::Result<()> {
|
||||||
|
let listener = ServerOptions::new()
|
||||||
|
.first_pipe_instance(true)
|
||||||
|
.create(r"\\.\pipe\creddy-requests")?;
|
||||||
|
|
||||||
|
let srv = Server {listener, app_handle};
|
||||||
|
rt::spawn(srv.serve());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve(mut self) {
|
||||||
|
loop {
|
||||||
|
if let Err(e) = self.try_serve().await {
|
||||||
|
eprintln!("Error accepting connection: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_serve(&mut self) -> Result<(), HandlerError> {
|
||||||
|
// connect() just waits for a client to connect, it doesn't return anything
|
||||||
|
self.listener.connect().await?;
|
||||||
|
|
||||||
|
// create a new pipe instance to listen for the next client, and swap it in
|
||||||
|
let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?;
|
||||||
|
let mut stream = std::mem::replace(&mut self.listener, new_listener);
|
||||||
|
let new_handle = self.app_handle.app_handle();
|
||||||
|
let client_pid = get_client_pid(&stream)?;
|
||||||
|
rt::spawn(async move {
|
||||||
|
super::handle(stream, app_handle)
|
||||||
|
.await
|
||||||
|
.error_print_prefix("Error responding to request: ");
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn get_client_pid(pipe: &NamedPipeServer) -> Result<u32, ClientInfoError> {
|
||||||
|
let raw_handle = pipe.as_raw_handle();
|
||||||
|
let mut pid = 0u32;
|
||||||
|
let handle = HANDLE(raw_handle as _);
|
||||||
|
unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? };
|
||||||
|
pid
|
||||||
|
}
|
@ -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)
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
@ -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> {
|
||||||
|
@ -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
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "creddy",
|
"productName": "creddy",
|
||||||
"version": "0.3.3"
|
"version": "0.3.4"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
@ -10,15 +10,21 @@
|
|||||||
export let min = null;
|
export let min = null;
|
||||||
export let max = null;
|
export let max = null;
|
||||||
export let decimal = false;
|
export let decimal = false;
|
||||||
|
export let debounceInterval = 0;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
$: localValue = value.toString();
|
$: localValue = value.toString();
|
||||||
let lastInputTime = null;
|
let lastInputTime = null;
|
||||||
function debounce(event) {
|
function debounce(event) {
|
||||||
lastInputTime = Date.now();
|
|
||||||
localValue = localValue.replace(/[^-0-9.]/g, '');
|
localValue = localValue.replace(/[^-0-9.]/g, '');
|
||||||
|
|
||||||
|
if (debounceInterval === 0) {
|
||||||
|
updateValue(localValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastInputTime = Date.now();
|
||||||
const eventTime = lastInputTime;
|
const eventTime = lastInputTime;
|
||||||
const pendingValue = localValue;
|
const pendingValue = localValue;
|
||||||
window.setTimeout(
|
window.setTimeout(
|
||||||
@ -28,7 +34,7 @@
|
|||||||
updateValue(pendingValue);
|
updateValue(pendingValue);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
500
|
debounceInterval,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { getRootCause } from '../lib/errors.js';
|
import { getRootCause } from '../lib/errors.js';
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
|
@ -14,15 +14,18 @@
|
|||||||
import { backInOut } from 'svelte/easing';
|
import { backInOut } from 'svelte/easing';
|
||||||
|
|
||||||
|
|
||||||
|
// make an independent copy so it can differ from the main config object
|
||||||
|
let config = JSON.parse(JSON.stringify($appState.config));
|
||||||
|
$: configModified = JSON.stringify(config) !== JSON.stringify($appState.config);
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
async function save() {
|
async function save() {
|
||||||
console.log('updating config');
|
|
||||||
try {
|
try {
|
||||||
await invoke('save_config', {config: $appState.config});
|
await invoke('save_config', {config});
|
||||||
|
$appState.config = await invoke('get_config');
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
error = e;
|
error = e;
|
||||||
$appState.config = await invoke('get_config');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,62 +38,60 @@
|
|||||||
<h1 slot="title" class="text-2xl font-bold">Settings</h1>
|
<h1 slot="title" class="text-2xl font-bold">Settings</h1>
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
{#await invoke('get_config') then config}
|
<div class="max-w-lg mx-auto mt-1.5 mb-24 p-4 space-y-16">
|
||||||
<div class="max-w-lg mx-auto mt-1.5 p-4 space-y-16">
|
<SettingsGroup name="General">
|
||||||
<SettingsGroup name="General">
|
<ToggleSetting title="Start on login" bind:value={config.start_on_login}>
|
||||||
<ToggleSetting title="Start on login" bind:value={$appState.config.start_on_login} on:update={save}>
|
<svelte:fragment slot="description">
|
||||||
<svelte:fragment slot="description">
|
Start Creddy when you log in to your computer.
|
||||||
Start Creddy when you log in to your computer.
|
</svelte:fragment>
|
||||||
</svelte:fragment>
|
</ToggleSetting>
|
||||||
</ToggleSetting>
|
|
||||||
|
|
||||||
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
|
<ToggleSetting title="Start minimized" bind:value={config.start_minimized}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Minimize to the system tray at startup.
|
Minimize to the system tray at startup.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ToggleSetting>
|
</ToggleSetting>
|
||||||
|
|
||||||
<NumericSetting title="Re-hide delay" bind:value={$appState.config.rehide_ms} min={0} unit="Milliseconds" on:update={save}>
|
<NumericSetting title="Re-hide delay" bind:value={config.rehide_ms} min={0} unit="Milliseconds">
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
How long to wait after a request is approved/denied before minimizing
|
How long to wait after a request is approved/denied before minimizing
|
||||||
the window to tray. Only applicable if the window was minimized
|
the window to tray. Only applicable if the window was minimized
|
||||||
to tray before the request was received.
|
to tray before the request was received.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</NumericSetting>
|
</NumericSetting>
|
||||||
|
|
||||||
<Setting title="Update credentials">
|
<Setting title="Update credentials">
|
||||||
<Link slot="input" target="EnterCredentials">
|
<Link slot="input" target="EnterCredentials">
|
||||||
<button class="btn btn-sm btn-primary">Update</button>
|
<button class="btn btn-sm btn-primary">Update</button>
|
||||||
</Link>
|
</Link>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Update or re-enter your encrypted credentials.
|
Update or re-enter your encrypted credentials.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Setting>
|
</Setting>
|
||||||
|
|
||||||
<FileSetting
|
<FileSetting
|
||||||
title="Terminal emulator"
|
title="Terminal emulator"
|
||||||
bind:value={$appState.config.terminal.exec}
|
bind:value={config.terminal.exec}
|
||||||
on:update={save}
|
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Choose your preferred terminal emulator (e.g. <code>gnome-terminal</code> or <code>wt.exe</code>.) May be an absolute path or an executable discoverable on <code>$PATH</code>.
|
Choose your preferred terminal emulator (e.g. <code>gnome-terminal</code> or <code>wt.exe</code>.) May be an absolute path or an executable discoverable on <code>$PATH</code>.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</FileSetting>
|
</FileSetting>
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
|
|
||||||
<SettingsGroup name="Hotkeys">
|
<SettingsGroup name="Hotkeys">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p>Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.</p>
|
<p>Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.</p>
|
||||||
|
|
||||||
<div class="grid grid-cols-[auto_1fr_auto] gap-y-3 items-center">
|
<div class="grid grid-cols-[auto_1fr_auto] gap-y-3 items-center">
|
||||||
<Keybind description="Show Creddy" value={$appState.config.hotkeys.show_window} on:update={save} />
|
<Keybind description="Show Creddy" bind:value={config.hotkeys.show_window} />
|
||||||
<Keybind description="Launch terminal" value={$appState.config.hotkeys.launch_terminal} on:update={save} />
|
<Keybind description="Launch terminal" bind:value={config.hotkeys.launch_terminal} />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</SettingsGroup>
|
</div>
|
||||||
|
</SettingsGroup>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
|
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
|
||||||
@ -104,4 +105,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if configModified}
|
||||||
|
<div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
|
||||||
|
<div class="alert shadow-lg no-animation">
|
||||||
|
<span>You have unsaved changes.</span>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- <button class="btn btn-sm btn-ghost">Cancel</button> -->
|
||||||
|
<buton class="btn btn-sm btn-primary" on:click={save}>Save</buton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
Reference in New Issue
Block a user