mirror of
https://github.com/ayangweb/BongoCat.git
synced 2026-03-12 17:51:48 +08:00
feat: 支持 Linux Wayland 环境 (#554)
This commit is contained in:
62
Cargo.lock
generated
62
Cargo.lock
generated
@@ -453,6 +453,8 @@ name = "bongo-cat"
|
||||
version = "0.6.2"
|
||||
dependencies = [
|
||||
"fs_extra",
|
||||
"input",
|
||||
"nix",
|
||||
"rdev",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1979,6 +1981,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.1"
|
||||
@@ -2322,6 +2330,36 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbdc09524a91f9cacd26f16734ff63d7dc650daffadd2b6f84d17a285bd875a9"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"input-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"udev",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input-sys"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0"
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -2542,6 +2580,16 @@ dependencies = [
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -3433,7 +3481,7 @@ checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.5.1",
|
||||
"pin-project-lite",
|
||||
"rustix 1.0.7",
|
||||
"tracing",
|
||||
@@ -5606,6 +5654,18 @@ version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "udev"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f"
|
||||
dependencies = [
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
本项目的灵感来源于 [MMmmmoko](https://github.com/MMmmmoko) 大佬开发的 [Bongo-Cat-Mver](https://github.com/MMmmmoko/Bongo-Cat-Mver)。它以独特的猫咪互动功能深受用户喜爱,但仅支持 Windows 平台。作为一名深度 macOS 用户,我特别希望在自己的设备上也能使用这款可爱的猫咪,于是我决定开发一个适配 macOS 的版本。
|
||||
|
||||
同时,得益于 [Tauri](https://github.com/tauri-apps/tauri) 强大的跨平台能力,本项目不仅支持 macOS,还兼容 Windows 和 Linux(x11),让更多的用户都能与这只可爱的猫咪互动!
|
||||
同时,得益于 [Tauri](https://github.com/tauri-apps/tauri) 强大的跨平台能力,本项目不仅支持 macOS,还兼容 Windows 和 Linux,让更多的用户都能与这只可爱的猫咪互动!
|
||||
|
||||
## 下载
|
||||
|
||||
@@ -70,12 +70,16 @@
|
||||
|
||||
## 功能介绍
|
||||
|
||||
- 适配 macOS、Windows 和 Linux(x11)。
|
||||
- 适配 macOS、Windows 和 Linux。
|
||||
- 根据据键盘或鼠标操作,同步移动鼠标或敲击键盘。
|
||||
- 支持导入自定义模型,自由打造专属猫咪形象。
|
||||
- 完全开源,代码公开透明,绝不收集任何用户数据。
|
||||
- 支持离线运行,无需联网,保护用户隐私。
|
||||
|
||||
## 使用提示
|
||||
|
||||
- Linux 下需要用户系统安装 libinput 并加入 `input` 用户组,方可在 X11 和 Wayland 下正常使用。
|
||||
|
||||
## 更多模型
|
||||
|
||||
你可以在这个仓库中探索、下载更多猫咪模型,或提交你的创作,与大家一起分享:
|
||||
|
||||
@@ -22,7 +22,6 @@ tauri = { workspace = true, features = ["tray-icon", "protocol-asset", "macos-pr
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
tauri-plugin-custom-window.workspace = true
|
||||
rdev = { git = "https://github.com/ayangweb/rdev" }
|
||||
tauri-plugin-os = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
@@ -42,5 +41,12 @@ tauri-plugin-global-shortcut = "2"
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
tauri-nspanel.workspace = true
|
||||
|
||||
[target."cfg(not(target_os = \"linux\"))".dependencies]
|
||||
rdev = { git = "https://github.com/ayangweb/rdev" }
|
||||
|
||||
[target."cfg(target_os = \"linux\")".dependencies]
|
||||
nix = { version = "0.30", features = ["poll"] }
|
||||
input = "0.9"
|
||||
|
||||
[features]
|
||||
cargo-clippy = []
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
use rdev::{Event, EventType, listen};
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::json;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{AppHandle, Emitter, Runtime, command};
|
||||
|
||||
use crate::core::{device::{DeviceEvent, DeviceKind}};
|
||||
|
||||
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum DeviceKind {
|
||||
MousePress,
|
||||
MouseRelease,
|
||||
MouseMove,
|
||||
KeyboardPress,
|
||||
KeyboardRelease,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DeviceEvent {
|
||||
kind: DeviceKind,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
128
src-tauri/src/core/device/linux.rs
Normal file
128
src-tauri/src/core/device/linux.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use input::{
|
||||
event::{
|
||||
keyboard::{KeyState, KeyboardEventTrait},
|
||||
pointer::ButtonState,
|
||||
PointerEvent,
|
||||
},
|
||||
Event, Libinput, LibinputInterface,
|
||||
};
|
||||
use nix::{
|
||||
libc::{O_RDONLY, O_RDWR, O_WRONLY},
|
||||
poll::{poll, PollFd, PollFlags, PollTimeout},
|
||||
};
|
||||
use std::{
|
||||
fs::{File, OpenOptions}, os::{fd::{AsFd, OwnedFd}, unix::prelude::OpenOptionsExt}, path::Path
|
||||
};
|
||||
|
||||
use serde_json::json;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{AppHandle, Emitter, Runtime, command};
|
||||
|
||||
use crate::core::{device::{DeviceEvent, DeviceKind}, setup::key_from_code};
|
||||
|
||||
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub struct Interface;
|
||||
|
||||
impl LibinputInterface for Interface {
|
||||
fn open_restricted(&mut self, path: &Path, flags: i32) -> Result<OwnedFd, i32> {
|
||||
OpenOptions::new()
|
||||
.custom_flags(flags)
|
||||
.read((flags & O_RDONLY != 0) | (flags & O_RDWR != 0))
|
||||
.write((flags & O_WRONLY != 0) | (flags & O_RDWR != 0))
|
||||
.open(path)
|
||||
.map(|file| file.into())
|
||||
.map_err(|err| err.raw_os_error().unwrap())
|
||||
}
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
fn close_restricted(&mut self, fd: OwnedFd) {
|
||||
File::from(fd);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_device_event(event: &Event) -> Option<DeviceEvent> {
|
||||
match event {
|
||||
Event::Keyboard(ev) => {
|
||||
let key_code = ev.key();
|
||||
let key_name = match key_from_code(key_code) {
|
||||
Some(name) => name.to_string(),
|
||||
None => format!("Unknown({})", key_code),
|
||||
};
|
||||
match ev.key_state() {
|
||||
KeyState::Pressed => Some(DeviceEvent {
|
||||
kind: DeviceKind::KeyboardPress,
|
||||
value: json!(key_name),
|
||||
}),
|
||||
KeyState::Released => Some(DeviceEvent{
|
||||
kind: DeviceKind::KeyboardRelease,
|
||||
value: json!(key_name),
|
||||
})
|
||||
}
|
||||
},
|
||||
Event::Pointer(ev) => {
|
||||
match ev {
|
||||
PointerEvent::Button(e) => {
|
||||
let btn_code = e.button();
|
||||
let btn_name = match btn_code {
|
||||
0x110 => String::from("Left"),
|
||||
0x111 => String::from("Right"),
|
||||
0x112 => String::from("Middle"),
|
||||
_ => format!("Unknown({})", btn_code as u8),
|
||||
};
|
||||
match e.button_state() {
|
||||
ButtonState::Pressed => Some(DeviceEvent {
|
||||
kind: DeviceKind::MousePress,
|
||||
value: json!(btn_name),
|
||||
}),
|
||||
ButtonState::Released => Some(DeviceEvent {
|
||||
kind: DeviceKind::MouseRelease,
|
||||
value: json!(btn_name),
|
||||
})
|
||||
}
|
||||
},
|
||||
PointerEvent::Motion(e) => {
|
||||
Some(DeviceEvent {
|
||||
kind: DeviceKind::MouseMove,
|
||||
value: json!({
|
||||
"x": e.dx_unaccelerated(),
|
||||
"y": e.dy_unaccelerated()
|
||||
}),
|
||||
})
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[command]
|
||||
pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return Err("Device is already listening".to_string());
|
||||
}
|
||||
|
||||
IS_RUNNING.store(true, Ordering::SeqCst);
|
||||
|
||||
let mut input = Libinput::new_with_udev(Interface);
|
||||
match input.udev_assign_seat("seat0") {
|
||||
Ok(_) => {
|
||||
let input_clone = &input.clone();
|
||||
let mut pollfds = [PollFd::new(input_clone.as_fd(), PollFlags::POLLIN)];
|
||||
while poll(&mut pollfds, PollTimeout::NONE).is_ok() {
|
||||
input.dispatch().unwrap();
|
||||
for event in &mut input {
|
||||
let device_event = build_device_event(&event);
|
||||
if let Some(e) = device_event {
|
||||
app_handle.emit("device-changed", e).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => return Err("Failed to assign seat".to_string()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
29
src-tauri/src/core/device/mod.rs
Normal file
29
src-tauri/src/core/device/mod.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub mod common;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub use common::*;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum DeviceKind {
|
||||
MousePress,
|
||||
MouseRelease,
|
||||
MouseMove,
|
||||
KeyboardPress,
|
||||
KeyboardRelease,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DeviceEvent {
|
||||
kind: DeviceKind,
|
||||
value: Value,
|
||||
}
|
||||
@@ -6,3 +6,149 @@ pub fn platform(
|
||||
_preference_window: WebviewWindow,
|
||||
) {
|
||||
}
|
||||
|
||||
pub fn key_from_code(code: u32) -> Option<&'static str> {
|
||||
match code {
|
||||
// Function key
|
||||
1 => Some("Escape"),
|
||||
28 => Some("Return"),
|
||||
14 => Some("Backspace"),
|
||||
15 => Some("Tab"),
|
||||
57 => Some("Space"),
|
||||
58 => Some("CapsLock"),
|
||||
99 => Some("PrintScreen"),
|
||||
70 => Some("ScrollLock"),
|
||||
119 => Some("Pause"),
|
||||
69 => Some("NumLock"),
|
||||
110 => Some("Insert"),
|
||||
102 => Some("Home"),
|
||||
107 => Some("End"),
|
||||
104 => Some("PageUp"),
|
||||
109 => Some("PageDown"),
|
||||
111 => Some("Delete"),
|
||||
|
||||
// Arrow key
|
||||
103 => Some("UpArrow"),
|
||||
108 => Some("DownArrow"),
|
||||
105 => Some("LeftArrow"),
|
||||
106 => Some("RightArrow"),
|
||||
|
||||
// F key
|
||||
59 => Some("F1"),
|
||||
60 => Some("F2"),
|
||||
61 => Some("F3"),
|
||||
62 => Some("F4"),
|
||||
63 => Some("F5"),
|
||||
64 => Some("F6"),
|
||||
65 => Some("F7"),
|
||||
66 => Some("F8"),
|
||||
67 => Some("F9"),
|
||||
68 => Some("F10"),
|
||||
87 => Some("F11"),
|
||||
88 => Some("F12"),
|
||||
|
||||
// Numeric
|
||||
2 => Some("Num1"),
|
||||
3 => Some("Num2"),
|
||||
4 => Some("Num3"),
|
||||
5 => Some("Num4"),
|
||||
6 => Some("Num5"),
|
||||
7 => Some("Num6"),
|
||||
8 => Some("Num7"),
|
||||
9 => Some("Num8"),
|
||||
10 => Some("Num9"),
|
||||
11 => Some("Num0"),
|
||||
|
||||
// Alphabetic
|
||||
16 => Some("KeyQ"),
|
||||
17 => Some("KeyW"),
|
||||
18 => Some("KeyE"),
|
||||
19 => Some("KeyR"),
|
||||
20 => Some("KeyT"),
|
||||
21 => Some("KeyY"),
|
||||
22 => Some("KeyU"),
|
||||
23 => Some("KeyI"),
|
||||
24 => Some("KeyO"),
|
||||
25 => Some("KeyP"),
|
||||
30 => Some("KeyA"),
|
||||
31 => Some("KeyS"),
|
||||
32 => Some("KeyD"),
|
||||
33 => Some("KeyF"),
|
||||
34 => Some("KeyG"),
|
||||
35 => Some("KeyH"),
|
||||
36 => Some("KeyJ"),
|
||||
37 => Some("KeyK"),
|
||||
38 => Some("KeyL"),
|
||||
44 => Some("KeyZ"),
|
||||
45 => Some("KeyX"),
|
||||
46 => Some("KeyC"),
|
||||
47 => Some("KeyV"),
|
||||
48 => Some("KeyB"),
|
||||
49 => Some("KeyN"),
|
||||
50 => Some("KeyM"),
|
||||
|
||||
// Symbolic
|
||||
41 => Some("BackQuote"),
|
||||
12 => Some("Minus"),
|
||||
13 => Some("Equal"),
|
||||
26 => Some("LeftBracket"),
|
||||
27 => Some("RightBracket"),
|
||||
39 => Some("SemiColon"),
|
||||
40 => Some("Quote"),
|
||||
43 => Some("BackSlash"),
|
||||
86 => Some("IntlBackslash"),
|
||||
89 => Some("IntlRo"),
|
||||
124 => Some("IntlYen"),
|
||||
101 => Some("KanaMode"),
|
||||
51 => Some("Comma"),
|
||||
52 => Some("Dot"),
|
||||
53 => Some("Slash"),
|
||||
|
||||
// Control key
|
||||
29 => Some("ControlLeft"),
|
||||
97 => Some("ControlRight"),
|
||||
42 => Some("ShiftLeft"),
|
||||
54 => Some("ShiftRight"),
|
||||
56 => Some("Alt"),
|
||||
100 => Some("AltGr"),
|
||||
125 => Some("MetaLeft"),
|
||||
126 => Some("MetaRight"),
|
||||
127 => Some("Apps"),
|
||||
|
||||
// NumPad
|
||||
55 => Some("KpMultiply"),
|
||||
78 => Some("KpMinus"),
|
||||
74 => Some("KpPlus"),
|
||||
98 => Some("KpDivide"),
|
||||
117 => Some("KpEqual"),
|
||||
121 => Some("KpComma"),
|
||||
96 => Some("KpReturn"),
|
||||
83 => Some("KpDecimal"),
|
||||
79 => Some("Kp1"),
|
||||
80 => Some("Kp2"),
|
||||
81 => Some("Kp3"),
|
||||
75 => Some("Kp4"),
|
||||
76 => Some("Kp5"),
|
||||
77 => Some("Kp6"),
|
||||
71 => Some("Kp7"),
|
||||
72 => Some("Kp8"),
|
||||
73 => Some("Kp9"),
|
||||
82 => Some("Kp0"),
|
||||
|
||||
// Media key
|
||||
115 => Some("VolumeUp"),
|
||||
114 => Some("VolumeDown"),
|
||||
113 => Some("VolumeMute"),
|
||||
|
||||
// Language key
|
||||
90 => Some("Lang1"),
|
||||
91 => Some("Lang2"),
|
||||
92 => Some("Lang3"),
|
||||
93 => Some("Lang4"),
|
||||
94 => Some("Lang5"),
|
||||
|
||||
// Other
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user