make keybinds configurable

This commit is contained in:
Joseph Montanaro 2023-09-10 14:04:09 -07:00
parent 5685948608
commit 8d7b01629d
12 changed files with 220 additions and 121 deletions

View File

@ -6,7 +6,6 @@ use is_terminal::IsTerminal;
use serde::{Serialize, Deserialize};
use sqlx::SqlitePool;
use tauri::{
AppHandle,
Manager,
GlobalShortcutManager,
async_runtime as rt,
@ -27,10 +26,17 @@ pub struct TermConfig {
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct HotkeyConfig {
pub struct HotkeysConfig {
// tauri uses strings to represent keybinds, so we will as well
pub show_window: String,
pub launch_terminal: String,
pub show_window: Hotkey,
pub launch_terminal: Hotkey,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct Hotkey {
pub keys: String,
pub enabled: bool,
}
@ -49,7 +55,7 @@ pub struct AppConfig {
#[serde(default = "default_term_config")]
pub terminal: TermConfig,
#[serde(default = "default_hotkey_config")]
pub hotkeys: HotkeyConfig,
pub hotkeys: HotkeysConfig,
}
@ -187,42 +193,46 @@ fn default_term_config() -> TermConfig {
}
fn default_hotkey_config() -> HotkeyConfig {
HotkeyConfig {
show_window: "alt+shift+C".into(),
launch_terminal: "alt+shift+T".into(),
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: &HotkeyConfig) -> tauri::Result<()> {
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()?;
let h = app.app_handle();
manager.register(
&hotkeys.show_window,
move || {
h.get_window("main")
.map(|w| w.show().error_popup("Failed to show"))
.ok_or(HandlerError::NoMainWindow)
.error_popup("No main window");
},
)?;
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");
},
)?;
}
// register() doesn't take an async fn, so we have to use spawn
manager.register(
&hotkeys.launch_terminal,
|| {
rt::spawn(async {
crate::terminal::launch(false)
.await
.error_popup("Failed to launch");
});
}
)?;
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(())
}

View File

@ -71,7 +71,9 @@ impl AppState {
sv.rebind(new_config.listen_addr, new_config.listen_port).await?;
}
// re-register hotkeys if necessary
if new_config.hotkeys != live_config.hotkeys {
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)?;
}

11
src/ui/KeyCombo.svelte Normal file
View File

@ -0,0 +1,11 @@
<script>
export let keys;
</script>
<div class="flex gap-x-[0.2em] items-center">
{#each keys as key, i}
{#if i > 0} + {/if}
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">{key}</kbd>
{/each}
</div>

View File

@ -4,14 +4,13 @@
import Setting from './Setting.svelte';
export let title;
export let divider = true;
export let value;
const dispatch = createEventDispatcher();
</script>
<Setting {title} {divider}>
<Setting {title}>
<div slot="input">
<input
type="text"

View 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>

View File

@ -4,8 +4,8 @@
import Setting from './Setting.svelte';
export let title;
export let divider = true;
export let value;
export let unit = '';
export let min = null;
export let max = null;
@ -60,7 +60,7 @@
</script>
<Setting {title} {divider}>
<Setting {title}>
<div slot="input">
{#if unit}
<span class="mr-2">{unit}:</span>

View File

@ -3,20 +3,20 @@
import ErrorAlert from '../ErrorAlert.svelte';
export let title;
export let divider = true;
</script>
{#if divider}
<div class="divider"></div>
{/if}
<div class="flex flex-wrap justify-between gap-y-4">
<h3 class="text-lg font-bold shrink-0">{title}</h3>
<slot name="input"></slot>
</div>
<div>
<div class="flex flex-wrap justify-between gap-y-4">
<h3 class="text-lg font-bold shrink-0">{title}</h3>
{#if $$slots.input}
<slot name="input"></slot>
{/if}
</div>
{#if $$slots.description}
<p class="mt-3">
<slot name="description"></slot>
</p>
{/if}
{#if $$slots.description}
<p class="mt-3">
<slot name="description"></slot>
</p>
{/if}
</div>

View 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>

View File

@ -3,14 +3,13 @@
import Setting from './Setting.svelte';
export let title;
export let divider = true;
export let value;
const dispatch = createEventDispatcher();
</script>
<Setting {title} {divider}>
<Setting {title}>
<div slot="input">
<input
type="text"

View File

@ -4,14 +4,13 @@
import Setting from './Setting.svelte';
export let title;
export let divider = true; // passed through to Setting
export let value;
const dispatch = createEventDispatcher();
</script>
<Setting {title} {divider}>
<Setting {title}>
<input
slot="input"
type="checkbox"

View File

@ -6,6 +6,7 @@
import { appState, completeRequest } from '../lib/state.js';
import ErrorAlert from '../ui/ErrorAlert.svelte';
import Link from '../ui/Link.svelte';
import KeyCombo from '../ui/KeyCombo.svelte';
// Send response to backend, display error if applicable
@ -108,17 +109,15 @@
<div class="w-full flex justify-between">
<Link target={deny} hotkey="Escape">
<button class="btn btn-error justify-self-start">
Deny
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Esc</kbd>
<span class="mr-2">Deny</span>
<KeyCombo keys={['Esc']} />
</button>
</Link>
<Link target={approve} hotkey="Enter" shift="{true}">
<button class="btn btn-success justify-self-end">
Approve
<kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Shift</kbd>
<span class="mx-0.5">+</span>
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">Enter</kbd>
<span class="mr-2">Approve</span>
<KeyCombo keys={['Shift', 'Enter']} />
</button>
</Link>
</div>

View File

@ -1,11 +1,18 @@
<script context="module">
import { type } from '@tauri-apps/api/os';
const osType = await type();
</script>
<script>
import { invoke } from '@tauri-apps/api/tauri';
import { type } from '@tauri-apps/api/os';
import { appState } from '../lib/state.js';
import Nav from '../ui/Nav.svelte';
import Link from '../ui/Link.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 { fly } from 'svelte/transition';
@ -14,6 +21,7 @@
let error = null;
async function save() {
console.log('updating config');
try {
await invoke('save_config', {config: $appState.config});
}
@ -22,12 +30,6 @@
$appState.config = await invoke('get_config');
}
}
let osType = '';
type().then(t => osType = t);
console.log($appState.config.terminal);
window.term = $appState.config.terminal;
</script>
@ -36,68 +38,71 @@
</Nav>
{#await invoke('get_config') then config}
<div class="max-w-lg mx-auto mt-1.5 p-4">
<!-- <h2 class="text-2xl font-bold text-center">Settings</h2> -->
<div class="max-w-lg mx-auto mt-1.5 p-4 space-y-16">
<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">
<h2 class="text-xl font-bold">General</h2>
</div>
<ToggleSetting title="Start on login" divider={false} 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>
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
<svelte:fragment slot="description">
Minimize to the system tray at startup.
</svelte:fragment>
</ToggleSetting>
<ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
<svelte:fragment slot="description">
Minimize to the system tray at startup.
</svelte:fragment>
</ToggleSetting>
<NumericSetting title="Re-hide delay" bind:value={$appState.config.rehide_ms} min={0} unit="Milliseconds" on:update={save}>
<svelte:fragment slot="description">
How long to wait after a request is approved/denied before minimizing
the window to tray. Only applicable if the window was minimized
to tray before the request was received.
</svelte:fragment>
</NumericSetting>
<NumericSetting title="Re-hide delay" bind:value={$appState.config.rehide_ms} min={0} unit="Milliseconds" on:update={save}>
<svelte:fragment slot="description">
How long to wait after a request is approved/denied before minimizing
the window to tray. Only applicable if the window was minimized
to tray before the request was received.
</svelte:fragment>
</NumericSetting>
<NumericSetting
title="Listen port"
bind:value={$appState.config.listen_port}
min={osType === 'Windows_NT' ? 1 : 0}
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="Listen port"
bind:value={$appState.config.listen_port}
min={osType === 'Windows_NT' ? 1 : 0}
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>
<Setting title="Update credentials">
<Link slot="input" target="EnterCredentials">
<button class="btn btn-sm btn-primary">Update</button>
</Link>
<svelte:fragment slot="description">
Update or re-enter your encrypted credentials.
</svelte:fragment>
</Setting>
<Setting title="Update credentials">
<Link slot="input" target="EnterCredentials">
<button class="btn btn-sm btn-primary">Update</button>
</Link>
<svelte:fragment slot="description">
Update or re-enter your encrypted credentials.
</svelte:fragment>
</Setting>
<FileSetting
title="Terminal emulator"
bind:value={$appState.config.terminal.exec}
on:update={save}
>
<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>.
</svelte:fragment>
</FileSetting>
</SettingsGroup>
<div class="divider mt-10 mb-8">
<h2 class="text-xl font-bold">Terminal</h2>
</div>
<SettingsGroup name="Hotkeys">
<div class="space-y-4">
<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">
<Keybind description="Show Creddy" value={$appState.config.hotkeys.show_window} on:update={save} />
<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>
{/await}