feat: 支持 Linux Wayland 环境 (#554)

This commit is contained in:
Panda527
2025-07-15 14:38:41 +08:00
committed by GitHub
parent 7d541a486e
commit b6ad482851
7 changed files with 380 additions and 21 deletions

62
Cargo.lock generated
View File

@@ -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"

View File

@@ -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 下正常使用。
## 更多模型
你可以在这个仓库中探索、下载更多猫咪模型,或提交你的创作,与大家一起分享:

View File

@@ -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 = []

View File

@@ -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) {

View 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(())
}

View 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,
}

View File

@@ -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,
}
}