Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/oversized.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
370 crates/cp-panel/src/app.rs::wire the panel callbacks, one after another: splitting it would only move the list
1039 crates/cp-win/examples/probe/battery.rs::main a diagnostic main that walks every invariant of its platform in one pass
357 crates/cp-panel/src/app.rs::wire the panel callbacks, one after another: splitting it would only move the list
986 crates/cp-win/examples/probe/battery.rs::main a diagnostic main that walks every invariant of its platform in one pass
1037 crates/cp-mac/examples/probe/battery.rs::main the same battery, for the other platform
55 changes: 55 additions & 0 deletions app/src-tauri/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,61 @@ fn remember<R: Runtime>(app: &AppHandle<R>, said: Option<&str>) {
}
}

#[cfg(target_os = "macos")]
pub const SPARE: &[&str] = &[
"Alt+Cmd+V",
"Shift+Cmd+V",
"Alt+Cmd+C",
"Shift+Cmd+Space",
"Ctrl+Alt+V",
"Alt+Cmd+Space",
];
#[cfg(not(target_os = "macos"))]
pub const SPARE: &[&str] = &[
"Ctrl+Alt+V",
"Ctrl+Shift+V",
"Ctrl+Alt+C",
"Ctrl+Shift+Space",
"Alt+Shift+V",
"Ctrl+Alt+Space",
];

fn grantable<R: Runtime>(app: &AppHandle<R>, one: Shortcut) -> bool {
let keys = app.global_shortcut();
if keys.is_registered(one) {
return false;
}
let handle = app.clone();
if keys
.on_shortcut(one, move |_app, _shortcut, event| {
if event.state() == ShortcutState::Pressed {
crate::panel::show(&handle);
}
})
.is_err()
{
return false;
}
if let Err(why) = keys.unregister(one) {
note(&format!(
"«{one:?}» stayed registered after the probe: {why}"
));
}
true
}

#[tauri::command]
pub fn spare(app: tauri::AppHandle, taken: String) -> Vec<String> {
let held = taken.parse::<Shortcut>().ok();
SPARE
.iter()
.filter_map(|said| said.parse::<Shortcut>().ok().map(|one| (said, one)))
.filter(|(_, one)| held != Some(*one))
.filter(|(_, one)| grantable(&app, *one))
.map(|(said, _)| (*said).to_owned())
.collect()
}

#[tauri::command]
pub fn keys(app: tauri::AppHandle) -> Keys {
let wanted = crate::settings::settings()
Expand Down
41 changes: 41 additions & 0 deletions app/src-tauri/src/keys_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,44 @@ fn nothing_is_bound_until_the_system_says_yes() {
let bound = Bound::default();
assert!(bound.0.lock().expect("unpoisoned").is_none());
}

#[test]
fn every_spare_combination_is_one_the_system_can_be_asked_for() {
assert!(!SPARE.is_empty());
for said in SPARE {
assert!(
said.parse::<Shortcut>().is_ok(),
"«{said}» would be dropped in silence"
);
}
}

#[test]
fn the_spare_list_spells_its_modifiers_the_way_the_picker_does() {
for said in SPARE {
let parts: Vec<&str> = said.split('+').collect();
let order = ["Ctrl", "Alt", "Shift", "Cmd"];
let mut seen = 0;
for part in &parts[..parts.len() - 1] {
let at = order
.iter()
.position(|one| one == part)
.unwrap_or_else(|| panic!("«{part}» is not a modifier the picker emits"));
assert!(at >= seen, "«{said}» is spelled out of the picker's order");
seen = at;
}
}
}

#[test]
fn no_two_spare_entries_are_the_same_combination_spelled_differently() {
let parsed: Vec<Shortcut> = SPARE
.iter()
.filter_map(|said| said.parse::<Shortcut>().ok())
.collect();
for (at, one) in parsed.iter().enumerate() {
for other in &parsed[at + 1..] {
assert_ne!(one, other, "one combination is offered twice");
}
}
}
1 change: 1 addition & 0 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub fn run() {
waking::waking,
waking::wake,
keys::keys,
keys::spare,
trust::trust,
trust::ask_trust,
backup::save_backup,
Expand Down
7 changes: 6 additions & 1 deletion app/src-tauri/src/panel.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::sync::Mutex;
use tauri::{AppHandle, Manager, Runtime};
use tauri::{AppHandle, Emitter, Manager, Runtime};
use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::{CommandChild, CommandEvent};

Expand All @@ -15,6 +15,11 @@ pub fn trouble<R: Runtime>(app: &AppHandle<R>) -> Option<String> {
}

fn heard_from_panel<R: Runtime>(app: &AppHandle<R>, said: &str) {
if said == "settings" {
let _ = app.emit("rail", "keys");
crate::tray::surface(app);
return;
}
let Some(what) = said.strip_prefix("trouble ") else {
return;
};
Expand Down
18 changes: 15 additions & 3 deletions app/src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub fn raise<R: Runtime>(app: &AppHandle<R>, spanish: bool) -> Option<()> {
..
} = event
{
surface(tray.app_handle());
crate::panel::show(tray.app_handle());
}
})
.build(app)
Expand Down Expand Up @@ -96,17 +96,29 @@ pub fn surface<R: Runtime>(app: &AppHandle<R>) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
ahead(&window);
return;
}

let _ = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
let built = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.title("CopyPaste")
.inner_size(780.0, 580.0)
.min_inner_size(620.0, 460.0)
.decorations(false)
.center()
.build();
if let Ok(window) = built {
ahead(&window);
}
}

fn ahead<R: Runtime>(window: &tauri::WebviewWindow<R>) {
let was = window.is_always_on_top().unwrap_or(false);
let _ = window.set_always_on_top(true);
let _ = window.set_focus();
if !was {
let _ = window.set_always_on_top(false);
}
}

#[cfg(test)]
Expand Down
19 changes: 17 additions & 2 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { useState } from "react";
import { listen } from "@tauri-apps/api/event";
import { useEffect, useState } from "react";
import { useKept, useTrouble } from "./core";
import { fill, t } from "./locales";
import About from "./ui/About";
import Backup from "./ui/Backup";
import Chrome from "./ui/Chrome";
import General from "./ui/General";
import History from "./ui/History";
import { Clock, Gear, Info, Vault } from "./ui/Icons";
import { Clock, Gear, Info, Key, Vault } from "./ui/Icons";
import Keys from "./ui/Keys";

const WHERE = [
{ key: "general", says: "railGeneral", icon: Gear },
{ key: "keys", says: "railKeys", icon: Key },
{ key: "history", says: "railHistory", icon: Clock },
{ key: "backup", says: "railBackup", icon: Vault },
] as const;
Expand All @@ -21,6 +24,17 @@ export default function App() {
const { kept, trouble, change, look } = useKept();
const panelTrouble = useTrouble();

useEffect(() => {
const asked = listen<string>("rail", (event) => {
if (WHERE.some((one) => one.key === event.payload)) {
setWhere(event.payload as Where);
}
});
return () => {
void asked.then((drop) => drop());
};
}, []);

return (
<>
<Chrome />
Expand Down Expand Up @@ -58,6 +72,7 @@ export default function App() {
</p>
)}
{kept && where === "general" && <General kept={kept} change={change} />}
{kept && where === "keys" && <Keys kept={kept} change={change} />}
{kept && where === "history" && <History kept={kept} change={change} />}
{where === "backup" && <Backup />}
{where === "about" && <About />}
Expand Down
42 changes: 34 additions & 8 deletions app/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function useKept() {
(what: Partial<Kept>) => {
const was = held.current;
if (!was) {
return;
return Promise.resolve();
}
const next = { ...was, ...what };
held.current = next;
Expand All @@ -71,9 +71,13 @@ export function useKept() {
setTrouble(null);
}
})
.catch((why) => setTrouble(String(why)));
.catch((why) => {
look();
setTrouble(String(why));
});
return queue.current.then(() => undefined);
},
[land],
[land, look],
);

return { kept, trouble, change, look };
Expand Down Expand Up @@ -140,16 +144,38 @@ export function empty(): Promise<void> {

export type Keys = { wanted: string; bound: boolean };

export function useKeys() {
export function useKeys(shortcut: string) {
const [keys, setKeys] = useState<Keys | null>(null);
const [spare, setSpare] = useState<string[]>([]);
const live = useRef(true);

useEffect(() => {
const ask = useCallback((taken: string) => {
invoke<Keys>("keys")
.then(setKeys)
.catch(() => setKeys(null));
.then((said) => {
if (!live.current) {
return;
}
setKeys(said);
if (said.bound) {
setSpare([]);
return;
}
invoke<string[]>("spare", { taken })
.then((free) => live.current && setSpare(free))
.catch(() => live.current && setSpare([]));
})
.catch(() => live.current && setKeys(null));
}, []);

return keys;
useEffect(() => {
live.current = true;
ask(shortcut);
return () => {
live.current = false;
};
}, [shortcut, ask]);

return { keys, spare, recheck: ask };
}

export type Waking = { offered: boolean; wakes: boolean; theirs: boolean };
Expand Down
Loading
Loading