Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
e1c2618dc8 | |||
a7df7adc8e | |||
03d164c9d3 | |||
f522674a1c | |||
51fcccafa2 | |||
e3913ab4c9 | |||
c16f21bba3 | |||
fa228acc3a |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "creddy",
|
"name": "creddy",
|
||||||
"version": "0.2.3",
|
"version": "0.3.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -1040,7 +1040,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "creddy"
|
name = "creddy"
|
||||||
version = "0.2.3"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"auto-launch",
|
"auto-launch",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "creddy"
|
name = "creddy"
|
||||||
version = "0.2.3"
|
version = "0.3.1"
|
||||||
description = "A friendly AWS credentials manager"
|
description = "A friendly AWS credentials manager"
|
||||||
authors = ["Joseph Montanaro"]
|
authors = ["Joseph Montanaro"]
|
||||||
license = ""
|
license = ""
|
||||||
|
@ -98,7 +98,7 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
let name: OsString = cmd_name.into();
|
let name: OsString = cmd_name.into();
|
||||||
Err(ExecError::NotFound(name).into())
|
Err(ExecError::NotFound(name).into())
|
||||||
}
|
}
|
||||||
e => Err(ExecError::ExecutionFailed(e).into()),
|
_ => Err(ExecError::ExecutionFailed(e).into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +116,8 @@ pub enum SendResponseError {
|
|||||||
NotFound,
|
NotFound,
|
||||||
#[error("The specified request was already closed by the client")]
|
#[error("The specified request was already closed by the client")]
|
||||||
Abandoned,
|
Abandoned,
|
||||||
|
#[error("A response has already been received for the specified request")]
|
||||||
|
Fulfilled,
|
||||||
#[error("Could not renew AWS sesssion: {0}")]
|
#[error("Could not renew AWS sesssion: {0}")]
|
||||||
SessionRenew(#[from] GetSessionError),
|
SessionRenew(#[from] GetSessionError),
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ use tokio::net::{
|
|||||||
TcpStream,
|
TcpStream,
|
||||||
};
|
};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot::{self, Sender, Receiver};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
@ -23,24 +23,55 @@ use crate::ipc::{Request, Approval};
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RequestWaiter {
|
||||||
|
pub rehide_after: bool,
|
||||||
|
pub sender: Option<Sender<Approval>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestWaiter {
|
||||||
|
pub fn notify(&mut self, approval: Approval) -> Result<(), SendResponseError> {
|
||||||
|
let chan = self.sender
|
||||||
|
.take()
|
||||||
|
.ok_or(SendResponseError::Fulfilled)?;
|
||||||
|
|
||||||
|
chan.send(approval)
|
||||||
|
.map_err(|_| SendResponseError::Abandoned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
struct Handler {
|
struct Handler {
|
||||||
request_id: u64,
|
request_id: u64,
|
||||||
stream: TcpStream,
|
stream: TcpStream,
|
||||||
receiver: Option<oneshot::Receiver<Approval>>,
|
rehide_after: bool,
|
||||||
|
receiver: Option<Receiver<Approval>>,
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler {
|
impl Handler {
|
||||||
async fn new(stream: TcpStream, app: AppHandle) -> Self {
|
async fn new(stream: TcpStream, app: AppHandle) -> Result<Self, HandlerError> {
|
||||||
let state = app.state::<AppState>();
|
let state = app.state::<AppState>();
|
||||||
|
|
||||||
|
// determine whether we should re-hide the window after handling this request
|
||||||
|
let is_currently_visible = app.get_window("main")
|
||||||
|
.ok_or(HandlerError::NoMainWindow)?
|
||||||
|
.is_visible()?;
|
||||||
|
let rehide_after = state.current_rehide_status()
|
||||||
|
.await
|
||||||
|
.unwrap_or(!is_currently_visible);
|
||||||
|
|
||||||
let (chan_send, chan_recv) = oneshot::channel();
|
let (chan_send, chan_recv) = oneshot::channel();
|
||||||
let request_id = state.register_request(chan_send).await;
|
let waiter = RequestWaiter {rehide_after, sender: Some(chan_send)};
|
||||||
Handler {
|
let request_id = state.register_request(waiter).await;
|
||||||
|
let handler = Handler {
|
||||||
request_id,
|
request_id,
|
||||||
stream,
|
stream,
|
||||||
|
rehide_after,
|
||||||
receiver: Some(chan_recv),
|
receiver: Some(chan_recv),
|
||||||
app
|
app
|
||||||
}
|
};
|
||||||
|
Ok(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(mut self) {
|
async fn handle(mut self) {
|
||||||
@ -62,7 +93,7 @@ impl Handler {
|
|||||||
|
|
||||||
let req = Request {id: self.request_id, clients, base};
|
let req = Request {id: self.request_id, clients, base};
|
||||||
self.app.emit_all("credentials-request", &req)?;
|
self.app.emit_all("credentials-request", &req)?;
|
||||||
let starting_visibility = self.show_window()?;
|
self.show_window()?;
|
||||||
|
|
||||||
match self.wait_for_response().await? {
|
match self.wait_for_response().await? {
|
||||||
Approval::Approved => {
|
Approval::Approved => {
|
||||||
@ -94,9 +125,11 @@ impl Handler {
|
|||||||
};
|
};
|
||||||
sleep(delay).await;
|
sleep(delay).await;
|
||||||
|
|
||||||
if !starting_visibility && state.req_count().await == 0 {
|
if self.rehide_after && state.req_count().await == 1 {
|
||||||
let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
|
self.app
|
||||||
window.hide()?;
|
.get_window("main")
|
||||||
|
.ok_or(HandlerError::NoMainWindow)?
|
||||||
|
.hide()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -143,15 +176,14 @@ impl Handler {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_window(&self) -> Result<bool, HandlerError> {
|
fn show_window(&self) -> Result<(), HandlerError> {
|
||||||
let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
|
let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
|
||||||
let starting_visibility = window.is_visible()?;
|
if !window.is_visible()? {
|
||||||
if !starting_visibility {
|
|
||||||
window.unminimize()?;
|
window.unminimize()?;
|
||||||
window.show()?;
|
window.show()?;
|
||||||
}
|
}
|
||||||
window.set_focus()?;
|
window.set_focus()?;
|
||||||
Ok(starting_visibility)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_response(&mut self) -> Result<Approval, HandlerError> {
|
async fn wait_for_response(&mut self) -> Result<Approval, HandlerError> {
|
||||||
@ -231,12 +263,12 @@ impl Server {
|
|||||||
loop {
|
loop {
|
||||||
match listener.accept().await {
|
match listener.accept().await {
|
||||||
Ok((stream, _)) => {
|
Ok((stream, _)) => {
|
||||||
let handler = Handler::new(stream, app_handle.app_handle()).await;
|
match Handler::new(stream, app_handle.app_handle()).await {
|
||||||
rt::spawn(handler.handle());
|
Ok(handler) => { rt::spawn(handler.handle()); }
|
||||||
|
Err(e) => { eprintln!("Error handling request: {e}"); }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => { eprintln!("Error accepting connection: {e}"); }
|
||||||
eprintln!("Error accepting connection: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::oneshot::Sender,
|
|
||||||
sync::RwLock,
|
sync::RwLock,
|
||||||
time::sleep,
|
time::sleep,
|
||||||
};
|
};
|
||||||
@ -20,7 +19,7 @@ use crate::{config, config::AppConfig};
|
|||||||
use crate::ipc::{self, Approval};
|
use crate::ipc::{self, Approval};
|
||||||
use crate::clientinfo::Client;
|
use crate::clientinfo::Client;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::server::Server;
|
use crate::server::{Server, RequestWaiter};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -28,7 +27,7 @@ pub struct AppState {
|
|||||||
pub config: RwLock<AppConfig>,
|
pub config: RwLock<AppConfig>,
|
||||||
pub session: RwLock<Session>,
|
pub session: RwLock<Session>,
|
||||||
pub request_count: RwLock<u64>,
|
pub request_count: RwLock<u64>,
|
||||||
pub open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
|
pub waiting_requests: RwLock<HashMap<u64, RequestWaiter>>,
|
||||||
pub pending_terminal_request: RwLock<bool>,
|
pub pending_terminal_request: RwLock<bool>,
|
||||||
pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
|
pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
|
||||||
server: RwLock<Server>,
|
server: RwLock<Server>,
|
||||||
@ -41,7 +40,7 @@ impl AppState {
|
|||||||
config: RwLock::new(config),
|
config: RwLock::new(config),
|
||||||
session: RwLock::new(session),
|
session: RwLock::new(session),
|
||||||
request_count: RwLock::new(0),
|
request_count: RwLock::new(0),
|
||||||
open_requests: RwLock::new(HashMap::new()),
|
waiting_requests: RwLock::new(HashMap::new()),
|
||||||
pending_terminal_request: RwLock::new(false),
|
pending_terminal_request: RwLock::new(false),
|
||||||
bans: RwLock::new(HashSet::new()),
|
bans: RwLock::new(HashSet::new()),
|
||||||
server: RwLock::new(server),
|
server: RwLock::new(server),
|
||||||
@ -84,26 +83,33 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register_request(&self, chan: Sender<ipc::Approval>) -> u64 {
|
pub async fn register_request(&self, waiter: RequestWaiter) -> u64 {
|
||||||
let count = {
|
let count = {
|
||||||
let mut c = self.request_count.write().await;
|
let mut c = self.request_count.write().await;
|
||||||
*c += 1;
|
*c += 1;
|
||||||
c
|
c
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut waiting_requests = self.waiting_requests.write().await;
|
||||||
open_requests.insert(*count, chan); // `count` is the request id
|
waiting_requests.insert(*count, waiter); // `count` is the request id
|
||||||
*count
|
*count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unregister_request(&self, id: u64) {
|
pub async fn unregister_request(&self, id: u64) {
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut waiting_requests = self.waiting_requests.write().await;
|
||||||
open_requests.remove(&id);
|
waiting_requests.remove(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn req_count(&self) -> usize {
|
pub async fn req_count(&self) -> usize {
|
||||||
let open_requests = self.open_requests.read().await;
|
let waiting_requests = self.waiting_requests.read().await;
|
||||||
open_requests.len()
|
waiting_requests.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn current_rehide_status(&self) -> Option<bool> {
|
||||||
|
// since all requests that are pending at a given time should have the same
|
||||||
|
// value for rehide_after, it doesn't matter which one we use
|
||||||
|
let waiting_requests = self.waiting_requests.read().await;
|
||||||
|
waiting_requests.iter().next().map(|(_id, w)| w.rehide_after)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
|
pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
|
||||||
@ -112,14 +118,11 @@ impl AppState {
|
|||||||
session.renew_if_expired().await?;
|
session.renew_if_expired().await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut open_requests = self.open_requests.write().await;
|
let mut waiting_requests = self.waiting_requests.write().await;
|
||||||
let chan = open_requests
|
waiting_requests
|
||||||
.remove(&response.id)
|
.get_mut(&response.id)
|
||||||
.ok_or(SendResponseError::NotFound)
|
.ok_or(SendResponseError::NotFound)?
|
||||||
?;
|
.notify(response.approval)
|
||||||
|
|
||||||
chan.send(response.approval)
|
|
||||||
.map_err(|_e| SendResponseError::Abandoned)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_ban(&self, client: Option<Client>) {
|
pub async fn add_ban(&self, client: Option<Client>) {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "creddy",
|
"productName": "creddy",
|
||||||
"version": "0.2.3"
|
"version": "0.3.1"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
@ -1,113 +1,42 @@
|
|||||||
<script>
|
<script>
|
||||||
export let color = 'base-content';
|
export let thickness = 8;
|
||||||
export let thickness = '2px';
|
|
||||||
let classes = '';
|
let classes = '';
|
||||||
export { classes as class };
|
export { classes as class };
|
||||||
|
|
||||||
const colorVars = {
|
const radius = (100 - thickness) / 2;
|
||||||
'primary': 'p',
|
// the px are fake, but we need them to satisfy css calc()
|
||||||
'primary-focus': 'pf',
|
const circumference = `${2 * Math.PI * radius}px`;
|
||||||
'primary-content': 'pc',
|
|
||||||
'secondary': 's',
|
|
||||||
'secondary-focus': 'sf',
|
|
||||||
'secondary-content': 'sc',
|
|
||||||
'accent': 'a',
|
|
||||||
'accent-focus': 'af',
|
|
||||||
'accent-content': 'ac',
|
|
||||||
'neutral': 'n',
|
|
||||||
'neutral-focus': 'nf',
|
|
||||||
'neutral-content': 'nc',
|
|
||||||
'base-100': 'b1',
|
|
||||||
'base-200': 'b2',
|
|
||||||
'base-300': 'b3',
|
|
||||||
'base-content': 'bc',
|
|
||||||
'info': 'in',
|
|
||||||
'info-content': 'inc',
|
|
||||||
'success': 'su',
|
|
||||||
'success-content': 'suc',
|
|
||||||
'warning': 'wa',
|
|
||||||
'warning-content': 'wac',
|
|
||||||
'error': 'er',
|
|
||||||
'error-content': 'erc',
|
|
||||||
}
|
|
||||||
|
|
||||||
let arcStyle = `border-width: ${thickness};`;
|
|
||||||
arcStyle += `border-color: hsl(var(--${colorVars[color]})) transparent transparent transparent;`;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
#spinner {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
animation: spin;
|
<svg
|
||||||
animation-duration: 1.5s;
|
style:--circumference={circumference}
|
||||||
animation-iteration-count: infinite;
|
class={classes}
|
||||||
animation-timing-function: linear;
|
viewBox="0 0 100 100"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<circle cx="50" cy="50" r={radius} stroke-width={thickness} />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
circle {
|
||||||
|
fill: transparent;
|
||||||
|
stroke-dasharray: var(--circumference);
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transform-origin: center;
|
||||||
|
animation: chase 3s infinite,
|
||||||
|
spin 1.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chase {
|
||||||
|
0% { stroke-dashoffset: calc(-1 * var(--circumference)); }
|
||||||
|
50% { stroke-dashoffset: calc(-2 * var(--circumference)); }
|
||||||
|
100% { stroke-dashoffset: calc(-3 * var(--circumference)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
50% { transform: rotate(225deg); }
|
50% { transform: rotate(135deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(270deg); }
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.arc {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-top {
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-right {
|
|
||||||
animation: spin-right;
|
|
||||||
animation-duration: 3s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-bottom {
|
|
||||||
animation: spin-bottom;
|
|
||||||
animation-duration: 3s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arc-left {
|
|
||||||
animation: spin-left;
|
|
||||||
animation-duration: 3s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-top {
|
|
||||||
0% { transform: rotate(-45deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(-45deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-right {
|
|
||||||
0% { transform: rotate(45deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(405deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-bottom {
|
|
||||||
0% { transform: rotate(135deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(495deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin-left {
|
|
||||||
0% { transform: rotate(225deg); }
|
|
||||||
50% { transform: rotate(315deg); }
|
|
||||||
100% { transform: rotate(585deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="spinner" class="w-6 h-6 {classes}">
|
|
||||||
<div class="arc arc-top w-full h-full" style={arcStyle}></div>
|
|
||||||
<div class="arc arc-right w-full h-full" style={arcStyle}></div>
|
|
||||||
<div class="arc arc-bottom w-full h-full" style={arcStyle}></div>
|
|
||||||
<div class="arc arc-left w-full h-full" style={arcStyle}></div>
|
|
||||||
</div>
|
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
const id = Math.random().toString().slice(2);
|
const id = Math.random().toString().slice(2);
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
const modifierKeys = new Set(['Alt', 'AltGraph', 'Control', 'Fn', 'FnLock', 'Meta', 'Shift', 'Super', ]);
|
||||||
let listening = false;
|
let listening = false;
|
||||||
|
|
||||||
function listen() {
|
function listen() {
|
||||||
@ -20,12 +21,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setKeybind(event) {
|
function setKeybind(event) {
|
||||||
console.log(event);
|
// separate events fire for modifier keys, even when they are combined with a regular key
|
||||||
|
if (modifierKeys.has(event.key)) return;
|
||||||
|
|
||||||
let keys = [];
|
let keys = [];
|
||||||
if (event.ctrlKey) keys.push('ctrl');
|
if (event.ctrlKey) keys.push('Ctrl');
|
||||||
if (event.altKey) keys.push('alt');
|
if (event.altKey) keys.push('Alt');
|
||||||
if (event.metaKey) keys.push('meta');
|
if (event.metaKey) keys.push('Meta');
|
||||||
if (event.shiftKey) keys.push('shift');
|
if (event.shiftKey) keys.push('Shift');
|
||||||
|
// capitalize
|
||||||
keys.push(event.key);
|
keys.push(event.key);
|
||||||
|
|
||||||
value.keys = keys.join('+');
|
value.keys = keys.join('+');
|
||||||
|
@ -79,8 +79,8 @@
|
|||||||
<input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
|
<input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{#if saving}
|
{#if saving }
|
||||||
<Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
|
<Spinner class="w-5 h-5" thickness="12"/>
|
||||||
{:else}
|
{:else}
|
||||||
Submit
|
Submit
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
<script context="module">
|
|
||||||
import { type } from '@tauri-apps/api/os';
|
|
||||||
const osType = await type();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
import { type } from '@tauri-apps/api/os';
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
import Nav from '../ui/Nav.svelte';
|
import Nav from '../ui/Nav.svelte';
|
||||||
@ -30,6 +25,9 @@
|
|||||||
$appState.config = await invoke('get_config');
|
$appState.config = await invoke('get_config');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let osType = null;
|
||||||
|
type().then(t => osType = t);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
{#if saving}
|
{#if saving}
|
||||||
<Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
|
<Spinner class="w-5 h-5" thickness="12"/>
|
||||||
{:else}
|
{:else}
|
||||||
Submit
|
Submit
|
||||||
{/if}
|
{/if}
|
||||||
|
18
todo.md
Normal file
18
todo.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
## Definitely
|
||||||
|
|
||||||
|
* Switch to "process" provider for AWS credentials (much less hacky)
|
||||||
|
* Session timeout (plain duration, or activity-based?)
|
||||||
|
* Fix rehide behavior when new request comes in while old one is still being resolved
|
||||||
|
* Additional hotkey configuration (approve/deny at the very least)
|
||||||
|
* Logging
|
||||||
|
* Icon
|
||||||
|
* Auto-updates
|
||||||
|
* SSH key handling
|
||||||
|
|
||||||
|
## Maybe
|
||||||
|
|
||||||
|
* Flatten error type hierarchy
|
||||||
|
* Rehide after terminal launch from locked
|
||||||
|
* Generalize Request across both credentials and terminal launch?
|
||||||
|
* Make hotkey configuration a little more tolerant of slight mistiming
|
||||||
|
* Distinguish between request that was denied and request that was canceled (e.g. due to error)
|
Reference in New Issue
Block a user