Skip to content

Commit 55235a1

Browse files
committed
Add battery indicator, keyboard key remapping; fix overlapping pickers
- Sidebar now shows a battery badge next to the mouse name when connected - TopBar background/theme pickers share one open-state so they no longer overlap when switched directly - Button remapping can now assign any single keyboard key per button (HID keyboard macro + slot write, verified against capture for 'a') - Bump version to 1.3.5
1 parent cd461ec commit 55235a1

17 files changed

Lines changed: 342 additions & 32 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ipi-fly-driver",
33
"private": true,
4-
"version": "1.3.4",
4+
"version": "1.3.5",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ipi-fly-driver"
3-
version = "1.3.4"
3+
version = "1.3.5"
44
edition = "2021"
55

66
[build-dependencies]

src-tauri/src/commands.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,23 @@ pub fn cmd_set_button(slot: u8, code: u8) -> Result<(), String> {
345345
with_device(|dev| dev.write(&frame))
346346
}
347347

348+
#[tauri::command]
349+
pub fn cmd_set_button_key(slot: u8, keycode: u8) -> Result<(), String> {
350+
if !protocol::is_valid_button_slot(slot) {
351+
return Err(format!("unknown button slot 0x{slot:02x}"));
352+
}
353+
if keycode == 0 {
354+
return Err("keycode must not be zero".to_string());
355+
}
356+
let frames = protocol::cmd_button_keyboard(slot, keycode);
357+
with_device(|dev| {
358+
for frame in &frames {
359+
dev.write(frame)?;
360+
}
361+
Ok(())
362+
})
363+
}
364+
348365
#[tauri::command]
349366
pub fn cmd_raw(hex_frame: String) -> Result<String, String> {
350367
let bytes: Vec<u8> = hex_frame

src-tauri/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ fn main() {
3232
commands::cmd_factory_reset,
3333
commands::cmd_reset_buttons,
3434
commands::cmd_set_button,
35+
commands::cmd_set_button_key,
3536
commands::cmd_raw,
3637
])
3738
.run(tauri::generate_context!())

src-tauri/src/protocol.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,55 @@ pub fn cmd_button(slot: u8, category: u8, code: u8, modifier: u8) -> [u8; FRAME_
9494
/// Mouse-button action category.
9595
pub const ACTION_MOUSE: u8 = 0x01;
9696

97+
/// Keyboard-key action category (category `0x05` in the button-slot write).
98+
pub const ACTION_KEYBOARD: u8 = 0x05;
99+
100+
/// Macro-buffer block-write address used to stage a single key-press/release
101+
/// sequence before the button slot is pointed at category `ACTION_KEYBOARD`.
102+
const MACRO_BUFFER_ADDR: u8 = 0x20;
103+
104+
/// Build the macro-buffer write that stages a single keyboard key (press then
105+
/// release of `keycode`, no modifiers).
106+
///
107+
/// Captured remap of the right button to keyboard `a` (0x04):
108+
/// `07 00 01 20 08 02 81 04 00 41 04 00 89 00 00 c8`
109+
///
110+
/// Layout: byte[2]=0x01 (macro marker), byte[3]=0x20 (buffer addr),
111+
/// byte[4]=0x08 (data length), byte[5]=0x02 (event count: down+up),
112+
/// byte[6]=0x81/byte[9]=0x41 (key-down / key-up event tags),
113+
/// byte[7]/byte[10]=keycode, byte[8]/byte[11]=0x00 (inter-event delay),
114+
/// byte[12]=inner checksum `0x55 - sum(byte[5..=11])`, byte[15]=tail checksum
115+
/// `0xF1 - byte[2] - byte[3] - byte[4]`, byte[0]=global checksum.
116+
pub fn cmd_keyboard_macro(keycode: u8) -> [u8; FRAME_LEN] {
117+
let mut f = [0u8; FRAME_LEN];
118+
f[2] = 0x01;
119+
f[3] = MACRO_BUFFER_ADDR;
120+
f[4] = 0x08;
121+
f[5] = 0x02; // two events: key down, key up
122+
f[6] = 0x81; // key-down event tag
123+
f[7] = keycode;
124+
f[8] = 0x00; // delay
125+
f[9] = 0x41; // key-up event tag
126+
f[10] = keycode;
127+
f[11] = 0x00; // delay
128+
let inner: u8 = f[5..=11].iter().fold(0u8, |a, &b| a.wrapping_add(b));
129+
f[12] = 0x55u8.wrapping_sub(inner);
130+
// Tail rule generalised to include byte[2] (0 for ordinary config frames).
131+
f[15] = 0xF1u8.wrapping_sub(f[2]).wrapping_sub(f[3]).wrapping_sub(f[4]);
132+
checksum(&mut f);
133+
f
134+
}
135+
136+
/// The two frames that remap a physical button slot to a single keyboard key:
137+
/// first the macro-buffer stage, then the button-slot write pointing the slot
138+
/// at category `ACTION_KEYBOARD` with the keycode in the modifier byte.
139+
pub fn cmd_button_keyboard(slot: u8, keycode: u8) -> [[u8; FRAME_LEN]; 2] {
140+
[
141+
cmd_keyboard_macro(keycode),
142+
cmd_button(slot, ACTION_KEYBOARD, 0x00, keycode),
143+
]
144+
}
145+
97146
// Mouse-button action codes (category `ACTION_MOUSE`). Confirmed from captures:
98147
// these are a standard HID button bitmask.
99148
pub const MOUSE_LEFT: u8 = 0x01;
@@ -522,6 +571,21 @@ mod tests {
522571
);
523572
}
524573

574+
#[test]
575+
fn test_button_right_slot_to_keyboard_a() {
576+
// Captured: right button slot 0x64 remapped to keyboard 'a' (0x04).
577+
// The official software sends the macro-buffer stage then the slot write.
578+
let frames = cmd_button_keyboard(SLOT_RIGHT, 0x04);
579+
assert_eq!(
580+
frames[0],
581+
hex("07 00 01 20 08 02 81 04 00 41 04 00 89 00 00 c8")
582+
);
583+
assert_eq!(
584+
frames[1],
585+
hex("07 00 00 64 04 05 00 04 4c 00 00 00 00 00 00 89")
586+
);
587+
}
588+
525589
#[test]
526590
fn test_button_left_slot_to_left_click() {
527591
// Captured: left button slot 0x60 set to Left Click.

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"package": {
99
"productName": "IPI STAY FLY Driver",
10-
"version": "1.3.4"
10+
"version": "1.3.5"
1111
},
1212
"tauri": {
1313
"allowlist": {

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export default function App() {
116116
<div className={`atk-shell flex h-screen w-screen overflow-hidden text-white ${demoMode ? 'demo-shell' : ''}`}>
117117
<div className="atk-ribbon" />
118118
<BackgroundLayer mode={background} wallpaper={wallpaper} />
119-
{tab !== 'home' && <Sidebar activeTab={tab} onTabChange={setTab} />}
119+
{tab !== 'home' && <Sidebar activeTab={tab} onTabChange={setTab} status={status} />}
120120
<div className="flex flex-col flex-1 overflow-hidden">
121121
<TopBar
122122
status={status}

src/components/BackgroundPicker.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,23 @@ interface Props {
1111
onBackgroundChange: (id: BackgroundId) => void
1212
hasWallpaper: boolean
1313
onWallpaperChange: (dataUrl: string | null) => void
14+
open: boolean
15+
onOpenChange: (open: boolean) => void
1416
}
1517

1618
export default function BackgroundPicker({
1719
background,
1820
onBackgroundChange,
1921
hasWallpaper,
2022
onWallpaperChange,
23+
open,
24+
onOpenChange,
2125
}: Props) {
22-
const [open, setOpen] = useState(false)
2326
const [busy, setBusy] = useState(false)
2427
const [error, setError] = useState(false)
2528
const rootRef = useRef<HTMLDivElement>(null)
2629
const fileRef = useRef<HTMLInputElement>(null)
30+
const setOpen = onOpenChange
2731

2832
useEffect(() => {
2933
if (!open) return
@@ -52,7 +56,7 @@ export default function BackgroundPicker({
5256
<div ref={rootRef} className="relative z-50" onPointerDown={event => event.stopPropagation()}>
5357
<button
5458
type="button"
55-
onClick={() => setOpen(value => !value)}
59+
onClick={() => setOpen(!open)}
5660
className="grid h-8 w-8 place-items-center rounded-lg text-white/55 transition hover:bg-white/[.08] hover:text-accent"
5761
title="Background"
5862
aria-label="Change background"

src/components/Sidebar.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
import { useTranslation } from 'react-i18next'
2-
import { Boxes, ChevronDown, Gauge, Home, Languages, Mouse, SlidersHorizontal, Wand2, Zap } from 'lucide-react'
2+
import {
3+
Battery,
4+
BatteryFull,
5+
BatteryLow,
6+
BatteryMedium,
7+
Boxes,
8+
ChevronDown,
9+
Gauge,
10+
Home,
11+
Languages,
12+
Mouse,
13+
SlidersHorizontal,
14+
Wand2,
15+
Zap,
16+
} from 'lucide-react'
317
import mouseImage from '../assets/fly-pro-top.png'
18+
import { StatusInfo } from '../lib/ipc'
419

520
type Tab = 'home' | 'dpi' | 'performance' | 'buttons' | 'advanced' | 'other' | 'lightning'
621

@@ -9,6 +24,14 @@ export type { Tab }
924
interface Props {
1025
activeTab: Tab
1126
onTabChange: (tab: Tab) => void
27+
status: StatusInfo
28+
}
29+
30+
function batteryVisual(percent: number) {
31+
if (percent >= 80) return { Icon: BatteryFull, color: 'text-emerald-300' }
32+
if (percent >= 45) return { Icon: BatteryMedium, color: 'text-emerald-300' }
33+
if (percent >= 20) return { Icon: BatteryLow, color: 'text-amber-300' }
34+
return { Icon: Battery, color: 'text-red-300' }
1235
}
1336

1437
const navItems: { id: Tab; labelKey: string; icon: typeof Mouse }[] = [
@@ -32,8 +55,10 @@ const languageOptions = [
3255
{ code: 'zh', label: '中文(简体)' },
3356
]
3457

35-
export default function Sidebar({ activeTab, onTabChange }: Props) {
58+
export default function Sidebar({ activeTab, onTabChange, status }: Props) {
3659
const { i18n, t } = useTranslation()
60+
const battery = batteryVisual(status.battery_percent)
61+
const BatteryIcon = battery.Icon
3762
const activeLanguage = i18n.resolvedLanguage ?? i18n.language
3863
const baseLanguage = activeLanguage.split('-')[0]
3964
const selectedLanguage = languageOptions.some(option => option.code === activeLanguage)
@@ -51,6 +76,15 @@ export default function Sidebar({ activeTab, onTabChange }: Props) {
5176
<div className="flex items-center gap-2">
5277
<img src={mouseImage} alt="" className="h-8 w-8 object-contain opacity-80" draggable={false} />
5378
<p className="text-base font-semibold">IPI STAY FLY</p>
79+
{status.connected && (
80+
<span
81+
className="flex items-center gap-1 rounded-full border border-white/10 bg-white/[.06] px-1.5 py-0.5 text-xs font-semibold text-white/80"
82+
title={`${t('topbar.battery', 'Battery')}: ${status.battery_percent}%`}
83+
>
84+
<BatteryIcon size={14} className={battery.color} />
85+
{status.battery_percent}%
86+
</span>
87+
)}
5488
</div>
5589
<Home size={14} className="text-white/40" />
5690
</div>

0 commit comments

Comments
 (0)