Compare commits

..

2 Commits
main ... main

Author SHA1 Message Date
69ba76e82e quick readme 2025-03-18 16:11:37 +13:00
995dedbe82 make the thing 2025-03-18 16:00:57 +13:00
13 changed files with 131 additions and 343 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,6 +1,13 @@
# keydisplay # copybot
ligma balls you may need to install libxdo-dev, check https://github.com/Enigo-rs/Enigo for instructions
if it still doesnt work, you may need to add yourself to the input group on linux
you may need to add yourself to the input group for the key listener to work
`usermod -a -G input $(whoami)` `usermod -a -G input $(whoami)`
## config
- default: remove default config notice
- theme: choose between grey / night / day / catppuccin_mocha
- bind: a key from https://docs.rs/rdev/latest/rdev/enum.Key.html
- shift_enter_newline: if true, hold shift while pressing enter for a new line. this is useful for preparing a whole message for things like chat apps (also, when false, delay/spam timeouts may cut off some messages/characters)
config is stored in a folder in your os config directory (found using https://crates.io/crates/dirs)

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);
}