This commit is contained in:
rain
2025-01-20 19:45:11 +08:00
7 changed files with 287 additions and 173 deletions

19
src-tauri/Cargo.lock generated
View File

@@ -595,6 +595,7 @@ dependencies = [
"tauri-plugin-oauth",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-store",
"tauri-plugin-theme",
"tauri-plugin-websocket",
]
@@ -2417,7 +2418,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro-crate 3.2.0",
"proc-macro2",
"quote",
"syn 2.0.90",
@@ -4295,6 +4296,22 @@ dependencies = [
"zbus 4.4.0",
]
[[package]]
name = "tauri-plugin-store"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c0c08fae6995909f5e9a0da6038273b750221319f2c0f3b526d6de1cde21505"
dependencies = [
"dunce",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.6",
"tokio",
"tracing",
]
[[package]]
name = "tauri-plugin-theme"
version = "2.1.2"

View File

@@ -30,6 +30,7 @@ tauri-plugin-theme = "2.1.2"
tauri-plugin-oauth = { git = "https://github.com/FabianLars/tauri-plugin-oauth", branch = "v2" }
tauri-plugin-deep-link = "2.0.0"
tauri-plugin-single-instance = "2.0.0"
tauri-plugin-store = "2.2.0"
# tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }

View File

@@ -1,29 +1,12 @@
use std::{fs::create_dir, io::Read};
use tauri::{AppHandle, Emitter, Listener, Manager, Runtime, WebviewWindow};
// use tauri_nspanel::{panel_delegate, ManagerExt, WebviewWindowExt};
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
mod autostart;
mod shortcut;
use autostart::{change_autostart, enable_autostart};
use tauri_plugin_deep_link::DeepLinkExt;
#[cfg(target_os = "macos")]
use tauri::ActivationPolicy;
#[cfg(target_os = "macos")]
const DEFAULT_SHORTCUT: &str = "command+shift+space";
#[cfg(any(target_os = "windows", target_os = "linux"))]
const DEFAULT_SHORTCUT: &str = "ctrl+shift+space";
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
use tauri::{AppHandle, Emitter, Listener, Manager, WebviewWindow};
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
#[tauri::command]
fn change_window_height(handle: AppHandle, height: u32) {
@@ -88,22 +71,24 @@ pub fn run() {
app.emit("single-instance", Payload { args: argv, cwd })
.unwrap();
}))
.plugin(tauri_plugin_store::Builder::default().build())
.invoke_handler(tauri::generate_handler![
greet,
change_window_height,
change_shortcut,
get_current_shortcut,
shortcut::change_shortcut,
shortcut::unregister_shortcut,
shortcut::get_current_shortcut,
change_autostart,
hide_coco,
switch_tray_icon,
// show_panel,
// hide_panel,
// close_panel
shortcut::check_shortcut_available,
])
.setup(|app| {
init(app.app_handle());
enable_shortcut(app);
shortcut::enable_shortcut(app);
enable_tray(app);
enable_autostart(app);
@@ -111,7 +96,7 @@ pub fn run() {
app.set_activation_policy(ActivationPolicy::Accessory);
app.listen("theme-changed", move |event| {
if let Ok(payload) = serde_json::from_str::<ThemeChangedPayload>(&event.payload()) {
if let Ok(payload) = serde_json::from_str::<ThemeChangedPayload>(event.payload()) {
// switch_tray_icon(app.app_handle(), payload.is_dark_mode);
println!("Theme changed: is_dark_mode = {}", payload.is_dark_mode);
}
@@ -159,101 +144,6 @@ fn init(_app_handle: &AppHandle) {
// panel.set_delegate(delegate);
}
fn enable_shortcut(app: &mut tauri::App) {
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
let window = app.get_webview_window("main").unwrap();
let command_shortcut: Shortcut = current_shortcut(app.app_handle()).unwrap();
app.handle()
.plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_handler(move |_app, shortcut, event| {
//println!("{:?}", shortcut);
if shortcut == &command_shortcut {
if let ShortcutState::Pressed = event.state() {
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
window.show().unwrap();
window.set_focus().unwrap();
}
}
}
})
.build(),
)
.unwrap();
app.global_shortcut().register(command_shortcut).unwrap();
}
#[tauri::command]
fn change_shortcut<R: Runtime>(
app: tauri::AppHandle<R>,
_window: tauri::Window<R>,
key: String,
) -> Result<(), String> {
use std::fs::File;
use std::io::Write;
use tauri_plugin_global_shortcut::ShortcutState;
if let Err(e) = remove_shortcut(&app) {
eprintln!("Failed to remove old shortcut: {}", e);
}
let main_window = app.get_webview_window("main").unwrap();
if key.trim().is_empty() {
let path = app.path().app_config_dir().unwrap();
if !path.exists() {
create_dir(&path).unwrap();
}
let file_path = path.join("shortcut.txt");
let mut file = File::create(file_path).unwrap();
file.write_all(b"").unwrap();
return Ok(());
}
let shortcut: Shortcut = key
.parse()
.map_err(|_| "The format of the shortcut key is incorrect".to_owned())?;
app.global_shortcut()
.on_shortcut(shortcut, move |_app, scut, event| {
if scut == &shortcut {
if let ShortcutState::Pressed = event.state() {
if main_window.is_visible().unwrap() {
main_window.hide().unwrap();
} else {
main_window.show().unwrap();
main_window.set_focus().unwrap();
}
}
}
})
.map_err(|_| "Failed to register new shortcut key".to_owned())?;
let path = app.path().app_config_dir().unwrap();
if path.exists() == false {
create_dir(&path).unwrap();
}
let file_path = path.join("shortcut.txt");
let mut file = File::create(file_path).unwrap();
file.write_all(key.as_bytes()).unwrap();
Ok(())
}
#[tauri::command]
fn get_current_shortcut<R: Runtime>(app: tauri::AppHandle<R>) -> Result<String, String> {
let res = current_shortcut(&app)?;
Ok(res.into_string())
}
#[tauri::command]
fn hide_coco(app: tauri::AppHandle) {
if let Some(window) = app.get_window("main") {
@@ -277,38 +167,6 @@ fn hide_coco(app: tauri::AppHandle) {
}
}
fn current_shortcut<R: Runtime>(app: &tauri::AppHandle<R>) -> Result<Shortcut, String> {
use std::fs::File;
let path = app.path().app_config_dir().unwrap();
let mut old_value = DEFAULT_SHORTCUT.to_owned();
if path.exists() {
let file_path = path.join("shortcut.txt");
if file_path.exists() {
let mut file = File::open(file_path).unwrap();
let mut data = String::new();
if let Ok(_) = file.read_to_string(&mut data) {
if data.is_empty() == false {
old_value = data
}
}
}
};
let short: Shortcut = old_value.parse().unwrap();
Ok(short)
}
#[allow(dead_code)]
fn remove_shortcut<R: Runtime>(app: &tauri::AppHandle<R>) -> Result<(), String> {
let short = current_shortcut(app)?;
app.global_shortcut().unregister(short).unwrap();
Ok(())
}
fn handle_open_coco(app: &AppHandle) {
println!("Open Coco menu clicked!");

190
src-tauri/src/shortcut.rs Normal file
View File

@@ -0,0 +1,190 @@
use tauri::App;
use tauri::AppHandle;
use tauri::Manager;
use tauri::Runtime;
use tauri_plugin_global_shortcut::GlobalShortcutExt;
use tauri_plugin_global_shortcut::Shortcut;
use tauri_plugin_global_shortcut::ShortcutState;
use tauri_plugin_store::JsonValue;
use tauri_plugin_store::StoreExt;
/// Tauri store name
const COCO_TAURI_STORE: &str = "coco_tauri_store";
/// Tauri's store is a key-value database, we use it to store our registered
/// global shortcut.
///
/// This is the key we use to store it.
const COCO_GLOBAL_SHORTCUT: &str = "coco_global_shortcut";
#[cfg(target_os = "macos")]
const DEFAULT_SHORTCUT: &str = "command+shift+space";
#[cfg(any(target_os = "windows", target_os = "linux"))]
const DEFAULT_SHORTCUT: &str = "ctrl+shift+space";
/// Set up the shortcut upon app start.
pub fn enable_shortcut(app: &App) {
let store = app
.store(COCO_TAURI_STORE)
.expect("creating a store should not fail");
if let Some(stored_shortcut) = store.get(COCO_GLOBAL_SHORTCUT) {
let stored_shortcut_str = match stored_shortcut {
JsonValue::String(str) => str,
unexpected_type => panic!(
"COCO shortcut should be stored as a string, found: {} ",
unexpected_type
),
};
let stored_shortcut = stored_shortcut_str
.parse::<Shortcut>()
.expect("stored shortcut string should be valid");
_register_shortcut_upon_start(app, stored_shortcut);
} else {
store.set(
COCO_GLOBAL_SHORTCUT,
JsonValue::String(DEFAULT_SHORTCUT.to_string()),
);
let default_shortcut = DEFAULT_SHORTCUT
.parse::<Shortcut>()
.expect("default shortcut should never be invalid");
_register_shortcut_upon_start(app, default_shortcut);
}
}
/// Get the stored shortcut as a string, same as [`_get_shortcut()`], except that
/// this is a `tauri::command` interface.
#[tauri::command]
pub fn get_current_shortcut<R: Runtime>(app: AppHandle<R>) -> Result<String, String> {
let shortcut = _get_shortcut(&app);
Ok(shortcut)
}
/// Get the current shortcut and unregister it on the tauri side.
#[tauri::command]
pub fn unregister_shortcut<R: Runtime>(app: AppHandle<R>) {
let shortcut_str = _get_shortcut(&app);
let shortcut = shortcut_str
.parse::<Shortcut>()
.expect("stored shortcut string should be valid");
app.global_shortcut()
.unregister(shortcut)
.expect("failed to unregister shortcut")
}
/// Change the global shortcut to `key`.
#[tauri::command]
pub fn change_shortcut<R: Runtime>(
app: AppHandle<R>,
_window: tauri::Window<R>,
key: String,
) -> Result<(), String> {
println!("key {}:", key);
let shortcut = match key.parse::<Shortcut>() {
Ok(shortcut) => shortcut,
Err(_) => return Err(format!("invalid shortcut {}", key)),
};
// Store it
let store = app
.get_store(COCO_TAURI_STORE)
.expect("store should be loaded or created");
store.set(COCO_GLOBAL_SHORTCUT, JsonValue::String(key));
// Register it
_register_shortcut(&app, shortcut);
Ok(())
}
/// Helper function to register a shortcut, used for shortcut updates.
fn _register_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: Shortcut) {
let main_window = app.get_webview_window("main").unwrap();
app.global_shortcut()
.on_shortcut(shortcut, move |_app, scut, event| {
if scut == &shortcut {
if let ShortcutState::Pressed = event.state() {
if main_window.is_visible().unwrap() {
main_window.hide().unwrap();
} else {
main_window.show().unwrap();
main_window.set_focus().unwrap();
}
}
}
})
.map_err(|err| format!("Failed to register new shortcut key '{}'", err))
.unwrap();
}
/// Helper function to register a shortcut, used to set up the shortcut up App's first start.
fn _register_shortcut_upon_start(app: &App, shortcut: Shortcut) {
let window = app.get_webview_window("main").unwrap();
app.handle()
.plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_handler(move |_app, scut, event| {
if scut == &shortcut {
if let ShortcutState::Pressed = event.state() {
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
window.show().unwrap();
window.set_focus().unwrap();
}
}
}
})
.build(),
)
.unwrap();
app.global_shortcut().register(shortcut).unwrap();
}
/// Helper function to get the stored global shortcut, as a string.
pub fn _get_shortcut<R: Runtime>(app: &AppHandle<R>) -> String {
let store = app
.get_store(COCO_TAURI_STORE)
.expect("store should be loaded or created");
match store
.get(COCO_GLOBAL_SHORTCUT)
.expect("shortcut should be stored")
{
JsonValue::String(str) => str,
unexpected_type => panic!(
"COCO shortcut should be stored as a string, found: {} ",
unexpected_type
),
}
}
#[tauri::command]
pub async fn check_shortcut_available(key: String) -> bool {
// 这里可以实现系统级的快捷键检查
// 可以检查是否与其他应用的全局快捷键冲突
// 返回 true 表示可用false 表示已被占用
// 简单实现示例:
!is_system_shortcut(&key)
}
fn is_system_shortcut(key: &str) -> bool {
let system_shortcuts = vec![
"Command+C",
"Command+V",
"Command+X",
"Command+A",
"Command+Z",
"Control+C",
"Control+V",
"Control+X",
"Control+A",
"Control+Z",
// 添加更多系统快捷键
];
system_shortcuts.contains(&key)
}

View File

@@ -67,34 +67,39 @@ export default function GeneralSettings() {
setLaunchAtLogin(false);
};
const [shortcut, setShortcut] = useState<Shortcut>([]);
async function getCurrentShortcut() {
const res: any = await invoke("get_current_shortcut");
setShortcut(res?.split("+"));
try {
const res: any = await invoke("get_current_shortcut");
console.log("DBG: ", res);
setShortcut(res?.split("+"));
} catch (err) {
console.error("Failed to fetch shortcut:", err);
}
}
useEffect(() => {
getCurrentShortcut();
}, []);
const [shortcut, setShortcut] = useState<Shortcut>([]);
const changeShortcut =(key: Shortcut) => {
setShortcut(key)
//
if (key.length === 0) return;
invoke("change_shortcut", { key: key?.join("+") }).catch((err) => {
console.error("Failed to save hotkey:", err);
});
}
const { isEditing, currentKeys, startEditing, saveShortcut, cancelEditing } =
useShortcutEditor(shortcut, setShortcut);
useEffect(() => {
if (shortcut.length === 0) return;
invoke("change_shortcut", { key: shortcut?.join("+") }).catch((err) => {
console.error("Failed to save hotkey:", err);
startEditing();
});
}, [shortcut]);
useShortcutEditor(shortcut, changeShortcut);
const onEditShortcut = async () => {
startEditing();
//
invoke("change_shortcut", { key: "" }).catch((err) => {
invoke("unregister_shortcut").catch((err) => {
console.error("Failed to save hotkey:", err);
startEditing();
});
};
@@ -103,7 +108,6 @@ export default function GeneralSettings() {
//
invoke("change_shortcut", { key: shortcut?.join("+") }).catch((err) => {
console.error("Failed to save hotkey:", err);
startEditing();
});
};

View File

@@ -4,6 +4,31 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { Shortcut } from '@/components/Settings/shortcut';
import { normalizeKey, isModifierKey, sortKeys } from '@/utils/keyboardUtils';
const RESERVED_SHORTCUTS = [
["Command", "C"],
["Command", "V"],
["Command", "X"],
["Command", "A"],
["Command", "Z"],
["Command", "Q"],
// Windows/Linux
["Control", "C"],
["Control", "V"],
["Control", "X"],
["Control", "A"],
["Control", "Z"],
// Coco
["Command", "I"],
["Command", "T"],
["Command", "N"],
["Command", "G"],
["Command", "O"],
["Command", "U"],
["Command", "M"],
["Command", "Enter"],
["Command", "ArrowLeft"],
];
export function useShortcutEditor(shortcut: Shortcut, onChange: (shortcut: Shortcut) => void) {
console.log("shortcut", shortcut)
@@ -16,7 +41,7 @@ export function useShortcutEditor(shortcut: Shortcut, onChange: (shortcut: Short
setCurrentKeys([]);
}, []);
const saveShortcut = useCallback(() => {
const saveShortcut = async () => {
if (!isEditing || currentKeys.length < 2) return;
const hasModifier = currentKeys.some(isModifierKey);
@@ -24,13 +49,29 @@ export function useShortcutEditor(shortcut: Shortcut, onChange: (shortcut: Short
if (!hasModifier || !hasNonModifier) return;
console.log(111111, currentKeys)
const isReserved = RESERVED_SHORTCUTS.some(reserved =>
reserved.length === currentKeys.length &&
reserved.every((key, index) => key.toLowerCase() === currentKeys[index].toLowerCase())
);
console.log(22222, isReserved)
if (isReserved) {
console.error("This is a system reserved shortcut");
return;
}
// Sort keys to ensure consistent order (modifiers first)
const sortedKeys = sortKeys(currentKeys);
onChange(sortedKeys);
setIsEditing(false);
setCurrentKeys([]);
}, [isEditing, currentKeys, onChange]);
};
const cancelEditing = useCallback(() => {
setIsEditing(false);

View File

@@ -11,10 +11,13 @@ export const KEY_SYMBOLS: Record<string, string> = {
Alt: isMac ? '⌥' : 'Alt',
alt: isMac ? '⌥' : 'Alt',
Meta: isMac ? '⌘' : 'Win',
meta: isMac ? '⌘' : 'Win',
Command: isMac ? '⌘' : 'Win',
command: isMac ? '⌘' : 'Win',
super: isMac ? '⌘' : 'Win',
// Special keys
Space: 'Space',
space: 'Space',
Enter: '↵',
Backspace: '⌫',
Delete: 'Del',