Compare commits
3 Commits
c98a065587
...
61d9acc7c6
Author | SHA1 | Date | |
---|---|---|---|
61d9acc7c6 | |||
8d7b01629d | |||
5685948608 |
@ -25,7 +25,7 @@ tauri-build = { version = "1.0.4", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.2", features = ["dialog", "dialog-open", "os-all", "system-tray"] }
|
tauri = { version = "1.2", features = ["dialog", "dialog-open", "global-shortcut", "os-all", "system-tray"] }
|
||||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
|
||||||
sodiumoxide = "0.2.7"
|
sodiumoxide = "0.2.7"
|
||||||
tokio = { version = ">=1.19", features = ["full"] }
|
tokio = { version = ">=1.19", features = ["full"] }
|
||||||
|
@ -83,6 +83,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
|||||||
let srv = Server::new(conf.listen_addr, conf.listen_port, app.handle()).await?;
|
let srv = Server::new(conf.listen_addr, conf.listen_port, app.handle()).await?;
|
||||||
|
|
||||||
config::set_auto_launch(conf.start_on_login)?;
|
config::set_auto_launch(conf.start_on_login)?;
|
||||||
|
config::register_hotkeys(&conf.hotkeys)?;
|
||||||
// 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
|
||||||
if !conf.start_minimized || is_first_launch {
|
if !conf.start_minimized || is_first_launch {
|
||||||
app.get_window("main")
|
app.get_window("main")
|
||||||
|
@ -5,6 +5,11 @@ 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::*;
|
||||||
|
|
||||||
@ -20,6 +25,21 @@ pub struct TermConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct Hotkey {
|
||||||
|
pub keys: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
|
pub struct HotkeysConfig {
|
||||||
|
// tauri uses strings to represent keybinds, so we will as well
|
||||||
|
pub show_window: Hotkey,
|
||||||
|
pub launch_terminal: Hotkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
#[serde(default = "default_listen_addr")]
|
#[serde(default = "default_listen_addr")]
|
||||||
@ -34,6 +54,8 @@ pub struct AppConfig {
|
|||||||
pub start_on_login: bool,
|
pub start_on_login: bool,
|
||||||
#[serde(default = "default_term_config")]
|
#[serde(default = "default_term_config")]
|
||||||
pub terminal: TermConfig,
|
pub terminal: TermConfig,
|
||||||
|
#[serde(default = "default_hotkey_config")]
|
||||||
|
pub hotkeys: HotkeysConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -46,6 +68,7 @@ impl Default for AppConfig {
|
|||||||
start_minimized: default_start_minimized(),
|
start_minimized: default_start_minimized(),
|
||||||
start_on_login: default_start_on_login(),
|
start_on_login: default_start_on_login(),
|
||||||
terminal: default_term_config(),
|
terminal: default_term_config(),
|
||||||
|
hotkeys: default_hotkey_config(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,6 +193,51 @@ fn default_term_config() -> TermConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn default_hotkey_config() -> HotkeysConfig {
|
||||||
|
HotkeysConfig {
|
||||||
|
show_window: Hotkey {keys: "alt+shift+C".into(), enabled: true},
|
||||||
|
launch_terminal: Hotkey {keys: "alt+shift+T".into(), enabled: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST }
|
fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST }
|
||||||
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
|
||||||
|
@ -95,6 +95,8 @@ pub enum SetupError {
|
|||||||
ServerSetupError(#[from] std::io::Error),
|
ServerSetupError(#[from] std::io::Error),
|
||||||
#[error("Failed to resolve data directory: {0}")]
|
#[error("Failed to resolve data directory: {0}")]
|
||||||
DataDir(#[from] DataDirError),
|
DataDir(#[from] DataDirError),
|
||||||
|
#[error("Failed to register hotkeys: {0}")]
|
||||||
|
RegisterHotkeys(#[from] tauri::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -242,6 +244,19 @@ pub enum ExecError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
|
pub enum LaunchTerminalError {
|
||||||
|
#[error("Could not discover main window")]
|
||||||
|
NoMainWindow,
|
||||||
|
#[error("Failed to communicate with main Creddy window")]
|
||||||
|
IpcFailed(#[from] tauri::Error),
|
||||||
|
#[error("Failed to launch terminal: {0}")]
|
||||||
|
Exec(#[from] ExecError),
|
||||||
|
#[error(transparent)]
|
||||||
|
GetCredentials(#[from] GetCredentialsError),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Serialize implementations
|
// Serialize implementations
|
||||||
// =========================
|
// =========================
|
||||||
@ -347,3 +362,18 @@ impl Serialize for ExecError {
|
|||||||
map.end()
|
map.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Serialize for LaunchTerminalError {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
map.serialize_entry("code", self.as_ref())?;
|
||||||
|
map.serialize_entry("msg", &format!("{self}"))?;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
LaunchTerminalError::Exec(src) => map.serialize_entry("source", &src)?,
|
||||||
|
_ => serialize_upstream_err(self, &mut map)?,
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -82,6 +82,6 @@ pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> R
|
|||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn launch_terminal(base: bool) -> Result<(), ExecError> {
|
pub async fn launch_terminal(base: bool) -> Result<(), LaunchTerminalError> {
|
||||||
terminal::launch(base).await
|
terminal::launch(base).await
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ pub struct AppState {
|
|||||||
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 open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
|
||||||
|
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>,
|
||||||
pool: sqlx::SqlitePool,
|
pool: sqlx::SqlitePool,
|
||||||
@ -41,6 +42,7 @@ impl AppState {
|
|||||||
session: RwLock::new(session),
|
session: RwLock::new(session),
|
||||||
request_count: RwLock::new(0),
|
request_count: RwLock::new(0),
|
||||||
open_requests: RwLock::new(HashMap::new()),
|
open_requests: RwLock::new(HashMap::new()),
|
||||||
|
pending_terminal_request: RwLock::new(false),
|
||||||
bans: RwLock::new(HashSet::new()),
|
bans: RwLock::new(HashSet::new()),
|
||||||
server: RwLock::new(server),
|
server: RwLock::new(server),
|
||||||
pool,
|
pool,
|
||||||
@ -59,15 +61,23 @@ impl AppState {
|
|||||||
pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
|
pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
|
||||||
let mut live_config = self.config.write().await;
|
let mut live_config = self.config.write().await;
|
||||||
|
|
||||||
|
// update autostart if necessary
|
||||||
if new_config.start_on_login != live_config.start_on_login {
|
if new_config.start_on_login != live_config.start_on_login {
|
||||||
config::set_auto_launch(new_config.start_on_login)?;
|
config::set_auto_launch(new_config.start_on_login)?;
|
||||||
}
|
}
|
||||||
|
// rebind socket if necessary
|
||||||
if new_config.listen_addr != live_config.listen_addr
|
if new_config.listen_addr != live_config.listen_addr
|
||||||
|| new_config.listen_port != live_config.listen_port
|
|| new_config.listen_port != live_config.listen_port
|
||||||
{
|
{
|
||||||
let mut sv = self.server.write().await;
|
let mut sv = self.server.write().await;
|
||||||
sv.rebind(new_config.listen_addr, new_config.listen_port).await?;
|
sv.rebind(new_config.listen_addr, new_config.listen_port).await?;
|
||||||
}
|
}
|
||||||
|
// re-register hotkeys if necessary
|
||||||
|
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)?;
|
||||||
|
}
|
||||||
|
|
||||||
new_config.save(&self.pool).await?;
|
new_config.save(&self.pool).await?;
|
||||||
*live_config = new_config;
|
*live_config = new_config;
|
||||||
@ -141,6 +151,11 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn is_unlocked(&self) -> bool {
|
||||||
|
let session = self.session.read().await;
|
||||||
|
matches!(*session, Session::Unlocked{..})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> {
|
pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> {
|
||||||
let app_session = self.session.read().await;
|
let app_session = self.session.read().await;
|
||||||
let (base, _session) = app_session.try_get()?;
|
let (base, _session) = app_session.try_get()?;
|
||||||
@ -159,4 +174,21 @@ impl AppState {
|
|||||||
*app_session = Session::Unlocked {base, session};
|
*app_session = Session::Unlocked {base, session};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn register_terminal_request(&self) -> Result<(), ()> {
|
||||||
|
let mut req = self.pending_terminal_request.write().await;
|
||||||
|
if *req {
|
||||||
|
// if a request is already pending, we can't register a new one
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
*req = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unregister_terminal_request(&self) {
|
||||||
|
let mut req = self.pending_terminal_request.write().await;
|
||||||
|
*req = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,15 @@ use crate::errors::*;
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
|
||||||
pub async fn launch(use_base: bool) -> Result<(), ExecError> {
|
pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> {
|
||||||
let state = APP.get().unwrap().state::<AppState>();
|
let app = APP.get().unwrap();
|
||||||
// do all this in a block so we don't hold the lock any longer than necessary
|
let state = app.state::<AppState>();
|
||||||
|
|
||||||
|
// register_terminal_request() returns Err if there is another request pending
|
||||||
|
if state.register_terminal_request().await.is_err() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let mut cmd = {
|
let mut cmd = {
|
||||||
let config = state.config.read().await;
|
let config = state.config.read().await;
|
||||||
let mut cmd = Command::new(&config.terminal.exec);
|
let mut cmd = Command::new(&config.terminal.exec);
|
||||||
@ -17,10 +23,38 @@ pub async fn launch(use_base: bool) -> Result<(), ExecError> {
|
|||||||
cmd
|
cmd
|
||||||
};
|
};
|
||||||
|
|
||||||
// similarly
|
// if session is unlocked or empty, wait for credentials from frontend
|
||||||
|
if !state.is_unlocked().await {
|
||||||
|
app.emit_all("launch-terminal-request", ())?;
|
||||||
|
let window = app.get_window("main")
|
||||||
|
.ok_or(LaunchTerminalError::NoMainWindow)?;
|
||||||
|
if !window.is_visible()? {
|
||||||
|
window.unminimize()?;
|
||||||
|
window.show()?;
|
||||||
|
}
|
||||||
|
window.set_focus()?;
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
app.once_global("credentials-event", move |e| {
|
||||||
|
let success = match e.payload() {
|
||||||
|
Some("\"unlocked\"") | Some("\"entered\"") => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
let _ = tx.send(success);
|
||||||
|
});
|
||||||
|
|
||||||
|
if !rx.await.unwrap_or(false) {
|
||||||
|
state.unregister_terminal_request().await;
|
||||||
|
return Ok(()); // request was canceled by user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// more lock-management
|
||||||
{
|
{
|
||||||
let state = APP.get().unwrap().state::<AppState>();
|
|
||||||
let app_session = state.session.read().await;
|
let app_session = state.session.read().await;
|
||||||
|
// session should really be unlocked at this point, but if the frontend misbehaves
|
||||||
|
// (i.e. lies about unlocking) we could end up here with a locked session
|
||||||
|
// this will result in an error popup to the user (see main hotkey handler)
|
||||||
let (base_creds, session_creds) = app_session.try_get()?;
|
let (base_creds, session_creds) = app_session.try_get()?;
|
||||||
if use_base {
|
if use_base {
|
||||||
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
|
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
|
||||||
@ -33,11 +67,16 @@ pub async fn launch(use_base: bool) -> Result<(), ExecError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match cmd.spawn() {
|
let res = match cmd.spawn() {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) if std::io::ErrorKind::NotFound == e.kind() => {
|
Err(e) if std::io::ErrorKind::NotFound == e.kind() => {
|
||||||
Err(ExecError::NotFound(cmd.get_program().to_owned()))
|
Err(ExecError::NotFound(cmd.get_program().to_owned()))
|
||||||
},
|
},
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(ExecError::ExecutionFailed(e)),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
state.unregister_terminal_request().await;
|
||||||
|
|
||||||
|
res?; // ? auto-conversion is more liberal than .into()
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,20 @@ listen('credentials-request', (tauriEvent) => {
|
|||||||
$appState.pendingRequests.put(tauriEvent.payload);
|
$appState.pendingRequests.put(tauriEvent.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
listen('launch-terminal-request', async (tauriEvent) => {
|
||||||
|
if ($appState.currentRequest === null) {
|
||||||
|
let status = await invoke('get_session_status');
|
||||||
|
if (status === 'locked') {
|
||||||
|
navigate('Unlock');
|
||||||
|
}
|
||||||
|
else if (status === 'empty') {
|
||||||
|
navigate('EnterCredentials');
|
||||||
|
}
|
||||||
|
// else, session is unlocked, so do nothing
|
||||||
|
// (although we shouldn't even get the event in that case)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
acceptRequest();
|
acceptRequest();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -9,6 +9,10 @@ export default function() {
|
|||||||
|
|
||||||
resolvers: [],
|
resolvers: [],
|
||||||
|
|
||||||
|
size() {
|
||||||
|
return this.items.length;
|
||||||
|
},
|
||||||
|
|
||||||
put(item) {
|
put(item) {
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
let resolver = this.resolvers.shift();
|
let resolver = this.resolvers.shift();
|
||||||
|
13
src/ui/KeyCombo.svelte
Normal file
13
src/ui/KeyCombo.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
export let keys;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex gap-x-[0.2em] items-center">
|
||||||
|
{#each keys as key, i}
|
||||||
|
{#if i > 0}
|
||||||
|
<span class="mt-[-0.1em]">+</span>
|
||||||
|
{/if}
|
||||||
|
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">{key}</kbd>
|
||||||
|
{/each}
|
||||||
|
</div>
|
@ -4,14 +4,13 @@
|
|||||||
import Setting from './Setting.svelte';
|
import Setting from './Setting.svelte';
|
||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
export let divider = true;
|
|
||||||
export let value;
|
export let value;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<Setting {title} {divider}>
|
<Setting {title}>
|
||||||
<div slot="input">
|
<div slot="input">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
61
src/ui/settings/Keybind.svelte
Normal file
61
src/ui/settings/Keybind.svelte
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import KeyCombo from '../KeyCombo.svelte';
|
||||||
|
|
||||||
|
export let description;
|
||||||
|
export let value;
|
||||||
|
|
||||||
|
const id = Math.random().toString().slice(2);
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
let listening = false;
|
||||||
|
|
||||||
|
function listen() {
|
||||||
|
// don't re-listen if we already are
|
||||||
|
if (listening) return;
|
||||||
|
|
||||||
|
listening = true;
|
||||||
|
window.addEventListener('keyup', setKeybind, {once: true});
|
||||||
|
// setTimeout avoids reacting to the click event that we are currently processing
|
||||||
|
setTimeout(() => window.addEventListener('click', cancel, {once: true}), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setKeybind(event) {
|
||||||
|
console.log(event);
|
||||||
|
let keys = [];
|
||||||
|
if (event.ctrlKey) keys.push('ctrl');
|
||||||
|
if (event.altKey) keys.push('alt');
|
||||||
|
if (event.metaKey) keys.push('meta');
|
||||||
|
if (event.shiftKey) keys.push('shift');
|
||||||
|
keys.push(event.key);
|
||||||
|
|
||||||
|
value.keys = keys.join('+');
|
||||||
|
dispatch('update', {value});
|
||||||
|
listening = false;
|
||||||
|
window.removeEventListener('click', cancel, {once: true});
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
listening = false;
|
||||||
|
window.removeEventListener('keyup', setKeybind, {once: true});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<input
|
||||||
|
{id}
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-primary"
|
||||||
|
bind:checked={value.enabled}
|
||||||
|
on:change={() => dispatch('update', {value})}
|
||||||
|
>
|
||||||
|
<label for={id} class="cursor-pointer ml-4 text-lg">{description}</label>
|
||||||
|
|
||||||
|
<button class="h-12 p-2 rounded border border-neutral cursor-pointer text-center" on:click={listen}>
|
||||||
|
{#if listening}
|
||||||
|
Click to cancel
|
||||||
|
{:else}
|
||||||
|
<KeyCombo keys={value.keys.split('+')} />
|
||||||
|
{/if}
|
||||||
|
</button>
|
@ -4,8 +4,8 @@
|
|||||||
import Setting from './Setting.svelte';
|
import Setting from './Setting.svelte';
|
||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
export let divider = true;
|
|
||||||
export let value;
|
export let value;
|
||||||
|
|
||||||
export let unit = '';
|
export let unit = '';
|
||||||
export let min = null;
|
export let min = null;
|
||||||
export let max = null;
|
export let max = null;
|
||||||
@ -60,7 +60,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<Setting {title} {divider}>
|
<Setting {title}>
|
||||||
<div slot="input">
|
<div slot="input">
|
||||||
{#if unit}
|
{#if unit}
|
||||||
<span class="mr-2">{unit}:</span>
|
<span class="mr-2">{unit}:</span>
|
||||||
|
@ -3,20 +3,20 @@
|
|||||||
import ErrorAlert from '../ErrorAlert.svelte';
|
import ErrorAlert from '../ErrorAlert.svelte';
|
||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
export let divider = true;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
{#if divider}
|
<div>
|
||||||
<div class="divider"></div>
|
<div class="flex flex-wrap justify-between gap-y-4">
|
||||||
{/if}
|
<h3 class="text-lg font-bold shrink-0">{title}</h3>
|
||||||
<div class="flex flex-wrap justify-between gap-y-4">
|
{#if $$slots.input}
|
||||||
<h3 class="text-lg font-bold shrink-0">{title}</h3>
|
<slot name="input"></slot>
|
||||||
<slot name="input"></slot>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $$slots.description}
|
{#if $$slots.description}
|
||||||
<p class="mt-3">
|
<p class="mt-3">
|
||||||
<slot name="description"></slot>
|
<slot name="description"></slot>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
14
src/ui/settings/SettingsGroup.svelte
Normal file
14
src/ui/settings/SettingsGroup.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
export let name;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="divider mt-0 mb-8">
|
||||||
|
<h2 class="text-xl font-bold">{name}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-12">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -3,14 +3,13 @@
|
|||||||
import Setting from './Setting.svelte';
|
import Setting from './Setting.svelte';
|
||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
export let divider = true;
|
|
||||||
export let value;
|
export let value;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<Setting {title} {divider}>
|
<Setting {title}>
|
||||||
<div slot="input">
|
<div slot="input">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -4,14 +4,13 @@
|
|||||||
import Setting from './Setting.svelte';
|
import Setting from './Setting.svelte';
|
||||||
|
|
||||||
export let title;
|
export let title;
|
||||||
export let divider = true; // passed through to Setting
|
|
||||||
export let value;
|
export let value;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<Setting {title} {divider}>
|
<Setting {title}>
|
||||||
<input
|
<input
|
||||||
slot="input"
|
slot="input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
import { appState, completeRequest } from '../lib/state.js';
|
import { appState, completeRequest } from '../lib/state.js';
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
|
import KeyCombo from '../ui/KeyCombo.svelte';
|
||||||
|
|
||||||
|
|
||||||
// Send response to backend, display error if applicable
|
// Send response to backend, display error if applicable
|
||||||
@ -108,17 +109,15 @@
|
|||||||
<div class="w-full flex justify-between">
|
<div class="w-full flex justify-between">
|
||||||
<Link target={deny} hotkey="Escape">
|
<Link target={deny} hotkey="Escape">
|
||||||
<button class="btn btn-error justify-self-start">
|
<button class="btn btn-error justify-self-start">
|
||||||
Deny
|
<span class="mr-2">Deny</span>
|
||||||
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Esc</kbd>
|
<KeyCombo keys={['Esc']} />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link target={approve} hotkey="Enter" shift="{true}">
|
<Link target={approve} hotkey="Enter" shift="{true}">
|
||||||
<button class="btn btn-success justify-self-end">
|
<button class="btn btn-success justify-self-end">
|
||||||
Approve
|
<span class="mr-2">Approve</span>
|
||||||
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Shift</kbd>
|
<KeyCombo keys={['Shift', 'Enter']} />
|
||||||
<span class="mx-0.5">+</span>
|
|
||||||
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">Enter</kbd>
|
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
try {
|
try {
|
||||||
saving = true;
|
saving = true;
|
||||||
await invoke('save_credentials', {credentials, passphrase});
|
await invoke('save_credentials', {credentials, passphrase});
|
||||||
|
emit('credentials-event', 'entered');
|
||||||
if ($appState.currentRequest) {
|
if ($appState.currentRequest) {
|
||||||
navigate('Approve');
|
navigate('Approve');
|
||||||
}
|
}
|
||||||
@ -56,6 +57,11 @@
|
|||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('credentials-event', 'enter-canceled');
|
||||||
|
navigate('Home');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -79,7 +85,7 @@
|
|||||||
Submit
|
Submit
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
<Link target="Home" hotkey="Escape">
|
<Link target={cancel} hotkey="Escape">
|
||||||
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
||||||
</Link>
|
</Link>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
|
<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';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
|
import SettingsGroup from '../ui/settings/SettingsGroup.svelte';
|
||||||
|
import Keybind from '../ui/settings/Keybind.svelte';
|
||||||
import { Setting, ToggleSetting, NumericSetting, FileSetting, TextSetting } from '../ui/settings';
|
import { Setting, ToggleSetting, NumericSetting, FileSetting, TextSetting } from '../ui/settings';
|
||||||
|
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
@ -14,6 +21,7 @@
|
|||||||
|
|
||||||
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});
|
||||||
}
|
}
|
||||||
@ -22,12 +30,6 @@
|
|||||||
$appState.config = await invoke('get_config');
|
$appState.config = await invoke('get_config');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let osType = '';
|
|
||||||
type().then(t => osType = t);
|
|
||||||
|
|
||||||
console.log($appState.config.terminal);
|
|
||||||
window.term = $appState.config.terminal;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@ -36,68 +38,71 @@
|
|||||||
</Nav>
|
</Nav>
|
||||||
|
|
||||||
{#await invoke('get_config') then config}
|
{#await invoke('get_config') then config}
|
||||||
<div class="max-w-lg mx-auto mt-1.5 p-4">
|
<div class="max-w-lg mx-auto mt-1.5 p-4 space-y-16">
|
||||||
<!-- <h2 class="text-2xl font-bold text-center">Settings</h2> -->
|
<SettingsGroup name="General">
|
||||||
|
<ToggleSetting title="Start on login" bind:value={$appState.config.start_on_login} on:update={save}>
|
||||||
|
<svelte:fragment slot="description">
|
||||||
|
Start Creddy when you log in to your computer.
|
||||||
|
</svelte:fragment>
|
||||||
|
</ToggleSetting>
|
||||||
|
|
||||||
<div class="divider mt-0 mb-8">
|
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
|
||||||
<h2 class="text-xl font-bold">General</h2>
|
<svelte:fragment slot="description">
|
||||||
</div>
|
Minimize to the system tray at startup.
|
||||||
|
</svelte:fragment>
|
||||||
|
</ToggleSetting>
|
||||||
|
|
||||||
<ToggleSetting title="Start on login" divider={false} bind:value={$appState.config.start_on_login} on:update={save}>
|
<NumericSetting title="Re-hide delay" bind:value={$appState.config.rehide_ms} min={0} unit="Milliseconds" on:update={save}>
|
||||||
<svelte:fragment slot="description">
|
<svelte:fragment slot="description">
|
||||||
Start Creddy when you log in to your computer.
|
How long to wait after a request is approved/denied before minimizing
|
||||||
</svelte:fragment>
|
the window to tray. Only applicable if the window was minimized
|
||||||
</ToggleSetting>
|
to tray before the request was received.
|
||||||
|
</svelte:fragment>
|
||||||
|
</NumericSetting>
|
||||||
|
|
||||||
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
|
<NumericSetting
|
||||||
<svelte:fragment slot="description">
|
title="Listen port"
|
||||||
Minimize to the system tray at startup.
|
bind:value={$appState.config.listen_port}
|
||||||
</svelte:fragment>
|
min={osType === 'Windows_NT' ? 1 : 0}
|
||||||
</ToggleSetting>
|
on:update={save}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="description">
|
||||||
|
Listen for credentials requests on this port.
|
||||||
|
(Should be used with <code>$AWS_CONTAINER_CREDENTIALS_FULL_URI</code>)
|
||||||
|
</svelte:fragment>
|
||||||
|
</NumericSetting>
|
||||||
|
|
||||||
<NumericSetting title="Re-hide delay" bind:value={$appState.config.rehide_ms} min={0} unit="Milliseconds" on:update={save}>
|
<Setting title="Update credentials">
|
||||||
<svelte:fragment slot="description">
|
<Link slot="input" target="EnterCredentials">
|
||||||
How long to wait after a request is approved/denied before minimizing
|
<button class="btn btn-sm btn-primary">Update</button>
|
||||||
the window to tray. Only applicable if the window was minimized
|
</Link>
|
||||||
to tray before the request was received.
|
<svelte:fragment slot="description">
|
||||||
</svelte:fragment>
|
Update or re-enter your encrypted credentials.
|
||||||
</NumericSetting>
|
</svelte:fragment>
|
||||||
|
</Setting>
|
||||||
|
|
||||||
<NumericSetting
|
<FileSetting
|
||||||
title="Listen port"
|
title="Terminal emulator"
|
||||||
bind:value={$appState.config.listen_port}
|
bind:value={$appState.config.terminal.exec}
|
||||||
min={osType === 'Windows_NT' ? 1 : 0}
|
on:update={save}
|
||||||
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>.
|
||||||
Listen for credentials requests on this port.
|
</svelte:fragment>
|
||||||
(Should be used with <code>$AWS_CONTAINER_CREDENTIALS_FULL_URI</code>)
|
</FileSetting>
|
||||||
</svelte:fragment>
|
</SettingsGroup>
|
||||||
</NumericSetting>
|
|
||||||
|
|
||||||
<Setting title="Update credentials">
|
<SettingsGroup name="Hotkeys">
|
||||||
<Link slot="input" target="EnterCredentials">
|
<div class="space-y-4">
|
||||||
<button class="btn btn-sm btn-primary">Update</button>
|
<p>Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.</p>
|
||||||
</Link>
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Update or re-enter your encrypted credentials.
|
|
||||||
</svelte:fragment>
|
|
||||||
</Setting>
|
|
||||||
|
|
||||||
<div class="divider mt-10 mb-8">
|
<div class="grid grid-cols-[auto_1fr_auto] gap-y-3 items-center">
|
||||||
<h2 class="text-xl font-bold">Terminal</h2>
|
<Keybind description="Show Creddy" value={$appState.config.hotkeys.show_window} on:update={save} />
|
||||||
</div>
|
<Keybind description="Launch terminal" value={$appState.config.hotkeys.launch_terminal} on:update={save} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsGroup>
|
||||||
|
|
||||||
<FileSetting
|
|
||||||
title="Emulator"
|
|
||||||
divider={false}
|
|
||||||
bind:value={$appState.config.terminal.exec}
|
|
||||||
on:update={save}
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="description">
|
|
||||||
Choose your preferred terminal emulator (e.g. <code>gnome-terminal</code>, <code>wt.exe</code>.) May be an absolute path or an executable discoverable on <code>$PATH</code>.
|
|
||||||
</svelte:fragment>
|
|
||||||
</FileSetting>
|
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { appState } from '../lib/state.js';
|
import { appState } from '../lib/state.js';
|
||||||
@ -26,6 +27,7 @@
|
|||||||
saving = true;
|
saving = true;
|
||||||
let r = await invoke('unlock', {passphrase});
|
let r = await invoke('unlock', {passphrase});
|
||||||
$appState.credentialStatus = 'unlocked';
|
$appState.credentialStatus = 'unlocked';
|
||||||
|
emit('credentials-event', 'unlocked');
|
||||||
if ($appState.currentRequest) {
|
if ($appState.currentRequest) {
|
||||||
navigate('Approve');
|
navigate('Approve');
|
||||||
}
|
}
|
||||||
@ -51,6 +53,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('credentials-event', 'unlock-canceled');
|
||||||
|
navigate('Home');
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadTime = Date.now();
|
loadTime = Date.now();
|
||||||
})
|
})
|
||||||
@ -75,7 +82,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Link target="Home" hotkey="Escape">
|
<Link target={cancel} hotkey="Escape">
|
||||||
<button class="btn btn-outline btn-sm w-full">Cancel</button>
|
<button class="btn btn-sm btn-outline w-full">Cancel</button>
|
||||||
</Link>
|
</Link>
|
||||||
</form>
|
</form>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user