make the thing

This commit is contained in:
starlight 2025-03-18 16:00:57 +13:00
parent b7a9cdbd71
commit 995dedbe82
13 changed files with 122 additions and 341 deletions

4
.vscode/launch.json vendored
View File

@ -8,9 +8,9 @@
"type": "lldb", "type": "lldb",
"request": "launch", "request": "launch",
"name": "Debug", "name": "Debug",
"program": "${workspaceFolder}/src-tauri/target/debug/keydisplay", "program": "${workspaceFolder}/src-tauri/target/debug/copybot",
"args": [], "args": [],
"cwd": "${workspaceFolder}" "cwd": "${workspaceFolder}"
} }
] ]
} }

View File

@ -1,3 +1,3 @@
# keydisplay # copybot
ligma balls you may need to install libxdo-dev, check https://github.com/Enigo-rs/Enigo for instructions

View File

@ -1,5 +1,5 @@
{ {
"name": "keydisplay", "name": "copybot",
"version": "0.1.0", "version": "0.1.0",
"description": "", "description": "",
"type": "module", "type": "module",

View File

@ -1,9 +1,10 @@
[package] [package]
name = "keydisplay" name = "copybot"
version = "0.1.0" version = "0.1.0"
description = "A Tauri App"
authors = ["starlight"]
edition = "2021" edition = "2021"
repository = "https://git.stardust.wtf/starlight/copybot"
license = "GPL-2.0-only"
authors = ["starlight"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -11,7 +12,7 @@ edition = "2021"
# The `_lib` suffix may seem redundant but it is necessary # The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name. # to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "keydisplay_lib" name = "copybot_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
@ -27,3 +28,5 @@ dirs = "5.0"
thiserror = "2.0.11" thiserror = "2.0.11"
tauri-plugin-fs = { version = "2.0.0", features = ["watch"] } tauri-plugin-fs = { version = "2.0.0", features = ["watch"] }
rdev = { version = "0.5.3", features = ["serde", "serialize"] } rdev = { version = "0.5.3", features = ["serde", "serialize"] }
fastrand = "2.3.0"
enigo = "0.3.0"

View File

@ -11,7 +11,7 @@
"fs:default", "fs:default",
{ {
"identifier": "fs:allow-app-read-recursive", "identifier": "fs:allow-app-read-recursive",
"allow": [{"path": "$CONFIG/keydisplay"}] "allow": [{"path": "$CONFIG/copybot"}]
} }
] ]
} }

View File

@ -1,7 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::fs; use std::fs;
use std::path::PathBuf;
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -22,8 +21,9 @@ pub struct Config {
// theme = grey / night / day / catppuccin_mocha // theme = grey / night / day / catppuccin_mocha
pub default: bool, pub default: bool,
pub theme: String, pub theme: String,
pub listen: ListenConfig, // bind = "Delete"
pub display: DisplayConfig, pub bind: String,
pub shift_enter_newline: bool,
} }
// this is hack as fvck // this is hack as fvck
@ -38,44 +38,14 @@ impl Default for Config {
} }
} }
// [listen]
#[derive(Debug, Serialize, Deserialize)]
pub struct ListenConfig {
// keys = ["KeyZ", "KeyX"]
pub keys: Vec<String>,
// mouse = ["LeftButton", "RightButton"]
pub mouse: Vec<String>,
}
// [display]
#[derive(Debug, Serialize, Deserialize)]
pub struct DisplayConfig {
// "instant" or "ease"
pub press: String,
// display mouse buttons as a key or as an svg of a mouse
pub mouse: String,
// multiply default key length for the key by the float value
pub size: Vec<HashMap<String, f64>>,
// which keys to linebreak after
pub r#break: Vec<String>,
}
impl Config { impl Config {
// Provide default values // Provide default values
pub fn default() -> Self { pub fn default() -> Self {
Self { Self {
default: true, default: true,
theme: "grey".to_string(), theme: "grey".to_string(),
listen: ListenConfig { bind: "Delete".to_string(),
keys: vec!["KeyZ".to_string(), "KeyX".to_string(), "MetaLeft".to_string()], shift_enter_newline: true,
mouse: vec!["Left".to_string(), "Right".to_string()],
},
display: DisplayConfig {
press: "ease".to_string(),
mouse: "key".to_string(),
size: vec![HashMap::new()],
r#break: vec![],
},
} }
} }

View File

@ -1,9 +1,13 @@
use config::Config; use config::Config;
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use std::{collections::HashMap, sync::Mutex}; use enigo::{
use tauri::{ipc::Channel, Manager, State}; Direction::{Press, Release},
use rdev::{Event, EventType, listen}; Enigo, Key, Keyboard,
};
use rdev::{listen, Event, EventType};
use std::sync::Mutex;
use std::thread; use std::thread;
use tauri::{ipc::Channel, Manager, State};
mod config; mod config;
@ -21,14 +25,9 @@ pub fn run() {
update_config, update_config,
get_default, get_default,
get_theme, get_theme,
get_keys, get_bind,
get_mouse_buttons,
get_mouse_display,
get_press_display,
get_size_display,
get_breaks,
start_listener, start_listener,
label_from_keycode paste_text
]) ])
.setup(|app| { .setup(|app| {
let config = config::Config::load_or_create(get_config_path()).unwrap(); let config = config::Config::load_or_create(get_config_path()).unwrap();
@ -39,9 +38,7 @@ pub fn run() {
let themes = vec!["grey", "night", "day", "catppuccin_mocha"]; let themes = vec!["grey", "night", "day", "catppuccin_mocha"];
if themes.contains(&theme.as_str()) { if themes.contains(&theme.as_str()) {
println!("Setting theme to: {}", theme); println!("Setting theme to: {}", theme);
app.manage(Mutex::new(AppState { app.manage(Mutex::new(AppState { config: config }));
config: config,
}));
} else { } else {
println!( println!(
"{}", "{}",
@ -49,7 +46,6 @@ pub fn run() {
); );
} }
Ok(()) Ok(())
}) })
.run(tauri::generate_context!()) .run(tauri::generate_context!())
@ -62,7 +58,7 @@ pub fn run() {
#[tauri::command] #[tauri::command]
fn get_config_path() -> std::path::PathBuf { fn get_config_path() -> std::path::PathBuf {
let config_dir = dirs::config_dir().expect("Config directory not found"); let config_dir = dirs::config_dir().expect("Config directory not found");
config_dir.join("keydisplay") config_dir.join("copybot")
} }
// update the app state with new config // update the app state with new config
#[tauri::command] #[tauri::command]
@ -72,7 +68,7 @@ fn update_config(state: State<'_, Mutex<AppState>>) {
} }
#[tauri::command] #[tauri::command]
fn get_default(state: State<'_, Mutex<AppState>>) -> Result<bool, bool> { fn get_default(state: State<'_, Mutex<AppState>>) -> Result<bool, bool> {
let default = state.lock().unwrap().config.default.clone(); let default = state.lock().unwrap().config.default;
Ok(default) Ok(default)
} }
#[tauri::command] #[tauri::command]
@ -81,126 +77,48 @@ fn get_theme(state: State<'_, Mutex<AppState>>) -> Result<String, String> {
Ok(theme) Ok(theme)
} }
#[tauri::command] #[tauri::command]
fn get_keys(state: State<'_, Mutex<AppState>>) -> Result<Vec<String>, Vec<String>> { fn get_bind(state: State<'_, Mutex<AppState>>) -> Result<String, String> {
let keys = state.lock().unwrap().config.listen.keys.clone(); let bind = state.lock().unwrap().config.bind.clone();
Ok(keys) Ok(bind)
}
#[tauri::command]
fn get_mouse_buttons(state: State<'_, Mutex<AppState>>) -> Result<Vec<String>, Vec<String>> {
let mouse_buttons = state.lock().unwrap().config.listen.mouse.clone();
Ok(mouse_buttons)
}
#[tauri::command]
fn get_press_display(state: State<'_, Mutex<AppState>>) -> Result<String, String> {
let press_display = state.lock().unwrap().config.display.press.clone();
Ok(press_display)
}
#[tauri::command]
fn get_size_display(state: State<'_, Mutex<AppState>>) -> Result<Vec<HashMap<String, f64>>, Vec<HashMap<String, f64>>> {
let size_display = state.lock().unwrap().config.display.size.clone();
Ok(size_display)
}
#[tauri::command]
fn get_mouse_display(state: State<'_, Mutex<AppState>>) -> Result<String, String> {
let mouse_display = state.lock().unwrap().config.display.mouse.clone();
Ok(mouse_display)
}
#[tauri::command]
fn get_breaks(state: State<'_, Mutex<AppState>>) -> Result<Vec<String>, Vec<String>> {
let breaks = state.lock().unwrap().config.display.r#break.clone();
Ok(breaks)
} }
// Input events // Input events
#[tauri::command] #[tauri::command]
fn start_listener(keys: Vec<String>, m_buttons: Vec<String>, channel: Channel<Event>) { fn start_listener(bind: String, channel: Channel<Event>) {
thread::spawn(move || { thread::spawn(move || {
println!("Started listening for keys: {:?} and buttons: {:?}", keys, m_buttons); println!("Started listening for bind: {:?}", bind);
listen(move |event| { listen(move |event| match event.event_type {
match event.event_type { EventType::KeyPress(key) | EventType::KeyRelease(key) => {
EventType::KeyPress(key) | EventType::KeyRelease(key) => { if bind == serde_json::to_string(&key).unwrap().replace("\"", "") {
if keys.contains(&serde_json::to_string(&key).unwrap().replace("\"", "")) { channel.send(event).unwrap() }; channel.send(event).unwrap()
}, };
EventType::ButtonPress(button) | EventType::ButtonRelease(button) => {
if m_buttons.contains(&serde_json::to_string(&button).unwrap().replace("\"", "")) { channel.send(event).unwrap() };
},
EventType::MouseMove { x, y } => (),
EventType::Wheel { delta_x, delta_y } => (),
} }
_ => (),
}) })
}); });
} }
// Other rust
#[tauri::command] #[tauri::command]
fn label_from_keycode(code: &str) -> &str { fn paste_text(state: State<'_, Mutex<AppState>>, text: String) {
match code { println!("{}", text);
// Keyboard let mut enigo = Enigo::new(&enigo::Settings::default()).unwrap();
// Fine as-is
"Alt"|"AltGr"|"End"|"Home"|"Pause" => code,
// All alphabetical keys
c if c.starts_with("Key")
&& c.len() == 4
&& c.chars().nth(3).map_or(false, |c| c.is_ascii_alphabetic()) => &c[3..],
// Number row
c if c.starts_with("Num")
&& c.len() == 4
&& c.chars().nth(3).map_or(false, |c| c.is_numeric()) => &c[3..],
// All F keys
c if c.starts_with("F")
&& c.len() == 2
&& c.chars().nth(1).map_or(false, |c| c.is_numeric()) => c,
// Numpad numbers
c if c.starts_with("Kp")
&& c.len() == 3
&& c.chars().nth(2).map_or(false, |c| c.is_numeric()) => &c[2..],
// Individual mappings
"Backspace" => "🡐",
"CapsLock" => "Caps",
"ControlLeft"|"ControlRight" => "Ctrl",
"Delete"|"KpDelete" => "Del",
"DownArrow" => "",
"Escape" => "Esc",
"LeftArrow" => "",
"MetaLeft"|"MetaRight" => "Super",
"PageDown" => "PgDn",
"PageUp" => "PgUp",
"Return" => "",
"RightArrow" => "",
"ShiftLeft"|"ShiftRight" => "Shift",
// needs an obscure blank character (U+E002D) - css acts weird if its a space or anything the program interprets as one
"Space" => "󠀯",
"Tab" => "",
"UpArrow" => "",
"PrintScreen" => "PrtSc",
"ScrollLock" => "ScrLk",
"NumLock" => "Num",
"BackQuote" => "`",
"Minus" => "-",
"Equal" => "=",
"LeftBracket" => "(",
"RightBracket" => ")",
"SemiColon" => ";",
"Quote" => "\"",
"BackSlash"|"IntlBackslash" => "\\",
"Comma" => ",",
"Dot" => ".",
"Slash"|"KpDivide" => "/",
"Insert" => "Ins",
"KpReturn" => "",
"KpPlus" => "+",
"KpMultiply" => "*",
"Function" => "Fn",
// Mouse if state.lock().unwrap().config.shift_enter_newline {
"Left" => "M1", for c in text.chars() {
"Right" => "M2", if c == '\n' {
"Middle" => "M3", // Press Shift+Enter combination
enigo.key(Key::Shift, Press).unwrap();
&_ => { enigo.key(Key::Return, Press).unwrap();
println!("Error creating frontend label for keycode: {}, displaying as Unknown", code); enigo.key(Key::Return, Release).unwrap();
"Unknown" enigo.key(Key::Shift, Release).unwrap();
}, } else {
// Type regular characters normally
enigo.text(&c.to_string()).unwrap();
}
}
} else {
enigo.text(text.as_str()).unwrap();
} }
} }

View File

@ -4,5 +4,5 @@
mod config; mod config;
fn main() { fn main() {
keydisplay_lib::run() copybot_lib::run()
} }

View File

@ -1,8 +1,8 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "keydisplay", "productName": "COPYBOT",
"version": "0.1.0", "version": "0.1.0",
"identifier": "whatisthisanandroidapp.keydisplay.starlight", "identifier": "whatisthisanandroidapp.copybot.starlight",
"build": { "build": {
"beforeDevCommand": "yarn dev", "beforeDevCommand": "yarn dev",
"devUrl": "http://localhost:1420", "devUrl": "http://localhost:1420",
@ -12,7 +12,7 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "keydisplay", "title": "copybot",
"width": 800, "width": 800,
"height": 600 "height": 600
} }

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>keydisplay</title> <title>copybot</title>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { invoke, Channel } from "@tauri-apps/api/core"; import { invoke, Channel } from "@tauri-apps/api/core";
import Key from "./Key.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { watchImmediate } from "@tauri-apps/plugin-fs"; import { watchImmediate } from "@tauri-apps/plugin-fs";
@ -20,22 +19,18 @@
| { Wheel: { delta_x: number, delta_y: number } } | { Wheel: { delta_x: number, delta_y: number } }
} }
// type with props for the Key component // app state
type KeyElement = { let content_tmp: string = $state("");
label: string, let content: string = $state("");
code: string,
pressed: boolean
}
// scoping // scoping
let keys: Array<string>, mouse_buttons: Array<string>; let bind: string = $state("");
let key_elements: Array<KeyElement>, mouse_button_elements: Array<KeyElement>; let bind_pressed: boolean;
let default_config: boolean; let default_config: boolean;
let config_path: string; let config_path: string;
let breaks: Set<string>;
onMount(async () => { onMount(async () => {
// on launch // on launch
await handle_config(); await handle_config();
@ -60,7 +55,6 @@
// create listener for events // create listener for events
const KeyListener = new Channel<KeyEvent>(); const KeyListener = new Channel<KeyEvent>();
// handle key event by modifying the key_elements and mouse_button_elements arrays
KeyListener.onmessage = (message) => { KeyListener.onmessage = (message) => {
switch (Object.keys(message.event_type)[0]) { switch (Object.keys(message.event_type)[0]) {
case 'KeyPress': case 'KeyPress':
@ -71,14 +65,6 @@
handleKeyEvent(Object.values(message.event_type)[0], false); handleKeyEvent(Object.values(message.event_type)[0], false);
break; break;
case 'ButtonPress':
handleMouseEvent(Object.values(message.event_type)[0], true);
break;
case 'ButtonRelease':
handleMouseEvent(Object.values(message.event_type)[0], false);
break;
default: default:
console.warn("Unhandled event: ", message); console.warn("Unhandled event: ", message);
} }
@ -86,28 +72,18 @@
}; };
async function handleKeyEvent(code: string, isPressed: boolean) { async function handleKeyEvent(code: string, isPressed: boolean) {
key_elements = key_elements.map(key => { if(isPressed && !bind_pressed) {
if (key.code === code) { bind_pressed = true;
return { ...key, pressed: isPressed} console.log(`${code} was pressed, pasting..`)
} await invoke('paste_text', { text: content });
return key; } else if(!isPressed) {
}); bind_pressed = false;
} }
async function handleMouseEvent(code: string, isPressed: boolean) {
mouse_button_elements = mouse_button_elements.map(button => {
if (button.code === code) {
return { ...button, pressed: isPressed}
}
return button;
});
} }
// invoke backend key listener that will send the events // invoke backend key listener that will send the events
await invoke('start_listener', { await invoke('start_listener', {
keys: keys, bind: bind,
// named mButtons and not m_buttons because weird naming """conventions""" (WHO CARES)
mButtons: mouse_buttons,
channel: KeyListener channel: KeyListener
}); });
}); });
@ -134,83 +110,26 @@
console.error(`Failed to run get_default from the rust backend: ${error}`); console.error(`Failed to run get_default from the rust backend: ${error}`);
} }
// set display for keypresses // load the bind
try { try {
let press_display = await invoke('get_press_display'); bind = await invoke<string>('get_bind');
switch (press_display) {
case "instant":
case "ease":
root?.setAttribute("data-press-display", press_display);
break;
default:
console.warn('"press" field in [display] of the config is an invalid value, falling back to "ease"');
root?.setAttribute("data-press-display", "ease");
}
} catch (error) { } catch (error) {
console.error(`Failed to run get_press_display from the rust backend: ${error}`); console.error("Failed to process bind: ", error)
} }
}
// get linebreaks async function set_text() {
try { content = content_tmp
breaks = new Set(await invoke<Array<string>>("get_breaks"));
} catch (error) {
console.error(`Failed to get "break" config field from rust backend: ${error}`);
}
// load the keys/mousebuttons
try {
keys = await invoke<Array<string>>('get_keys');
mouse_buttons = await invoke<Array<string>>('get_mouse_buttons');
key_elements = await Promise.all(keys.map(async key => {
return {
label: await invoke<string>("label_from_keycode", { code: key }),
code: key,
pressed: false
}
}));
mouse_button_elements = await Promise.all(mouse_buttons.map(async button => {
return {
label: await invoke<string>("label_from_keycode", { code: button }),
code: button,
pressed: false
};
}));
} catch (error) {
console.error("Failed to process keys: ", error)
}
} }
</script> </script>
<link rel="stylesheet" href="/global.css"> <link rel="stylesheet" href="/global.css">
<main class="container"> <main class="container">
{#if default_config} {#if default_config}
<div id="default_config">Docs for configuring this program are at <a href="https://git.stardust.wtf/starlight/keydisplay/src/branch/main/README.md">https://git.stardust.wtf/starlight/keydisplay/src/branch/main/README.md</a><br>You can disable this annoying notice by removing the <code>default = true</code> line from <code>{config_path}</code> :)</div> <div id="default_config">Docs for configuring this program are at <a href="https://git.stardust.wtf/starlight/copybot/src/branch/main/README.md">https://git.stardust.wtf/starlight/copybot/src/branch/main/README.md</a><br>You can disable this annoying notice by removing the <code>default = true</code> line from <code>{config_path}</code> :)</div>
{/if} {/if}
<div id="keys"> <h1>copybot</h1>
{#each key_elements as key (key.code)} <textarea autocorrect="off" rows="6" bind:value={content_tmp}></textarea>
<Key <button id="set_text" onclick={() => content = content_tmp}>Set</button>
label={key.label} <p>Current bind: <b>{bind}</b></p>
code={key.code} <p>Set to current textarea content: <b>{content == content_tmp}</b></p>
pressed={key.pressed} </main>
>
</Key>
{#if breaks.has(key.code)}
<br>
{/if}
{/each}
</div>
<div id="mouse_buttons">
{#each mouse_button_elements as button (button.code)}
<Key
label={button.label}
code={button.code}
pressed={button.pressed}
>
</Key>
{#if breaks.has(button.code)}
<br>
{/if}
{/each}
</div>
</main>

View File

@ -1,14 +0,0 @@
<!-- generic "key" -->
<script>
export let label = "";
export let code = "";
export let pressed = false;
// Optional: Add press animation
import { quintOut } from "svelte/easing";
import { crossfade } from "svelte/transition";
</script>
<link rel="stylesheet" href="/global.css">
<div class="key" class:pressed={pressed} data-keycode={code}>
{label}
</div>

View File

@ -32,30 +32,30 @@ img {
:root, :root,
[data-selected-theme="grey"] { [data-selected-theme="grey"] {
--color-app-background: #2F2F2F; --color-app-background: #2F2F2F;
--color-key-background: #363636; --color-app-background-2: #4D4D4D;
--color-key-background-pressed: #212121;
--color-text: #fff; --color-text: #fff;
--color-button-pressed: #212121;
--color-accent: #737373; --color-accent: #737373;
} }
[data-selected-theme="night"] { [data-selected-theme="night"] {
--color-app-background: #000; --color-app-background: #000;
--color-key-background: #0f0f0f; --color-app-background-2: #1B1B1B;
--color-key-background-pressed: #212121;
--color-text: #eee; --color-text: #eee;
--color-button-pressed: #212121;
--color-accent: #525151; --color-accent: #525151;
} }
[data-selected-theme="day"] { [data-selected-theme="day"] {
--color-app-background: #e0e0e0; --color-app-background: #e0e0e0;
--color-key-background: #fff; --color-app-background-2: #f0f0f0;
--color-key-background-pressed: #eee;
--color-text: #000; --color-text: #000;
--color-button-pressed: #000;
--color-accent: #fff; --color-accent: #fff;
} }
[data-selected-theme="catppuccin_mocha"] { [data-selected-theme="catppuccin_mocha"] {
--color-app-background: #181825; --color-app-background: #181825;
--color-key-background: #1e1e2e; --color-app-background-2: #1e1e2e;
--color-key-background-pressed: #313244;
--color-text: #cdd6f4; --color-text: #cdd6f4;
--color-button-pressed: #313244;
--color-accent: #f5e0dc; --color-accent: #f5e0dc;
} }
@ -87,22 +87,28 @@ button {
border: 1px solid transparent; border: 1px solid transparent;
padding: 0.6em 1.2em; padding: 0.6em 1.2em;
font-size: 1em; font-size: 1em;
font-weight: 500; font-weight: 400;
font-family: inherit; font-family: inherit;
color: #0f0f0f; color: var(--color-text);
background-color: #ffffff; background-color: var(--color-app-background-2);
transition: border-color 0.25s; transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
cursor: pointer; cursor: pointer;
outline: none; outline: none;
} }
button#set_text {
margin-top: 5px;
width: 25vw;
margin-inline: auto;
}
button:hover { button:hover {
border-color: #396cd8; border-color: #396cd8;
} }
button:active { button:active {
border-color: #396cd8; border-color: #396cd8;
background-color: #e8e8e8; background-color: var(--color-button-pressed);
} }
a { a {
@ -125,34 +131,13 @@ div#default_config {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
div#keys, textarea {
div#mouse_buttons { background-color: var(--color-app-background-2);
/* horizontal spacing between keys is dependent on font-size of the parent div? */ border: 3px solid var(--color-accent);
font-size: 0; border-radius: 5px;
font-size: 16px;
width: 80vw;
margin-top: 5px;
margin-inline: auto;
color: var(--color-text);
} }
.key {
width: 100px;
height: 100px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin: 5px;
border: 5px solid var(--color-accent);
border-radius: 5px;
transition: var(--pressed-transition);
background-color: var(--color-key-background);
cursor: pointer;
user-select: none;
}
[data-press-display="ease"] {
--pressed-transition: background-color 0.1s ease, transform 0.1s ease;
}
.pressed {
background-color: var(--color-key-background-pressed) !important;
transform: scale(0.95);
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}