Skip to content
Open
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
94 changes: 89 additions & 5 deletions Mouser.spec
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,96 @@ block_cipher = None
ROOT = os.path.abspath(".")
PYSIDE6_DIR = os.path.dirname(PySide6.__file__)


def _dir_if_exists(src, dest):
if os.path.isdir(src):
return [(src, dest)]
return []


def _file_if_exists(src, dest):
if os.path.isfile(src):
return [(src, dest)]
return []


_manual_qt_datas = []
for rel in (
os.path.join("plugins", "platforms"),
os.path.join("plugins", "imageformats"),
os.path.join("plugins", "iconengines"),
os.path.join("plugins", "styles"),
os.path.join("plugins", "platforminputcontexts"),
os.path.join("qml", "Qt"),
os.path.join("qml", "QtCore"),
os.path.join("qml", "QtNetwork"),
os.path.join("qml", "QtQml"),
os.path.join("qml", "QtQuick"),
):
_manual_qt_datas += _dir_if_exists(
os.path.join(PYSIDE6_DIR, rel),
os.path.join("PySide6", rel),
)

_manual_qt_binaries = []
for name in (
"MSVCP140.dll",
"MSVCP140_1.dll",
"MSVCP140_2.dll",
"VCRUNTIME140.dll",
"VCRUNTIME140_1.dll",
"opengl32sw.dll",
"pyside6.abi3.dll",
"pyside6qml.abi3.dll",
"shiboken6.abi3.dll",
"Qt6Core.dll",
"Qt6Gui.dll",
"Qt6Network.dll",
"Qt6OpenGL.dll",
"Qt6Qml.dll",
"Qt6QmlCore.dll",
"Qt6QmlMeta.dll",
"Qt6QmlModels.dll",
"Qt6QmlNetwork.dll",
"Qt6QmlWorkerScript.dll",
"Qt6Quick.dll",
"Qt6QuickControls2.dll",
"Qt6QuickControls2Basic.dll",
"Qt6QuickControls2BasicStyleImpl.dll",
"Qt6QuickControls2Impl.dll",
"Qt6QuickControls2Material.dll",
"Qt6QuickControls2MaterialStyleImpl.dll",
"Qt6QuickControls2WindowsStyleImpl.dll",
"Qt6QuickEffects.dll",
"Qt6QuickLayouts.dll",
"Qt6QuickShapes.dll",
"Qt6QuickTemplates2.dll",
"Qt6ShaderTools.dll",
"Qt6Svg.dll",
"Qt6Widgets.dll",
"Qt6LabsAnimation.dll",
"Qt6LabsFolderListModel.dll",
"Qt6LabsPlatform.dll",
"Qt6LabsQmlModels.dll",
"Qt6LabsSettings.dll",
"Qt6LabsSharedImage.dll",
"Qt6LabsWavefrontMesh.dll",
):
_manual_qt_binaries += _file_if_exists(
os.path.join(PYSIDE6_DIR, name),
"PySide6",
)

a = Analysis(
["main_qml.py"],
pathex=[ROOT],
binaries=[],
binaries=_manual_qt_binaries,
datas=[
# QML UI files
(os.path.join(ROOT, "ui", "qml"), os.path.join("ui", "qml")),
# Image assets
(os.path.join(ROOT, "images"), "images"),
],
] + _manual_qt_datas,
hiddenimports=[
# conditional / lazy imports PyInstaller may miss
"hid",
Expand Down Expand Up @@ -119,10 +199,14 @@ _qt_keep = {
"Qt6Quick", "Qt6QuickControls2", "Qt6QuickControls2Impl",
"Qt6QuickControls2Basic", "Qt6QuickControls2BasicStyleImpl",
"Qt6QuickControls2Material", "Qt6QuickControls2MaterialStyleImpl",
"Qt6QuickControls2WindowsStyleImpl",
"Qt6QuickTemplates2", "Qt6QuickLayouts", "Qt6QuickEffects",
"Qt6QuickShapes",
"Qt6LabsAnimation", "Qt6LabsFolderListModel", "Qt6LabsPlatform",
"Qt6LabsQmlModels", "Qt6LabsSettings", "Qt6LabsSharedImage",
"Qt6LabsWavefrontMesh",
# Rendering
"Qt6ShaderTools", "Qt6Svg",
"Qt6ShaderTools", "Qt6Svg", "opengl32sw",
# PySide6 runtime
"pyside6.abi3", "pyside6qml.abi3", "shiboken6.abi3",
# VC runtime
Expand Down Expand Up @@ -150,7 +234,7 @@ def _should_keep(name):
if keep in name:
return True
# Keep QML dirs we need
for keep_qml in ("QtCore", "QtQml", "QtQuick", "QtNetwork"):
for keep_qml in ("Qt", "QtCore", "QtQml", "QtQuick", "QtNetwork"):
pat = os.path.join("qml", keep_qml)
if pat in name.replace("/", os.sep):
return True
Expand Down Expand Up @@ -194,7 +278,7 @@ _dist = os.path.join("dist", "Mouser", "_internal", "PySide6")

# QML dirs to KEEP (everything else under qml/ is deleted)
_keep_qml = {
"QtCore", "QtQml", "QtQuick", "QtNetwork",
"Qt", "QtCore", "QtQml", "QtQuick", "QtNetwork",
}

# Under QtQuick, keep only what the app uses
Expand Down
93 changes: 87 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ No telemetry. No cloud. No Logitech account required.

### 🖥️ Cross-Platform
- **Windows, macOS, and Linux** — native hooks on each platform (WH_MOUSE_LL, CGEventTap, evdev/uinput)
- **Start at login** — Windows registry and macOS LaunchAgent, with an independent "Start minimized" tray-only option
- **Start at login** — Windows registry, macOS LaunchAgent, and Linux XDG autostart, with an independent "Start minimized" tray-only option
- **Single instance guard** — launching a second copy brings the existing window to the front

### 🔌 Smart Connectivity
Expand Down Expand Up @@ -80,7 +80,7 @@ Action labels adapt by platform. For example, Windows exposes `Win+D` and `Task

| Category | Actions |
|---|---|
| **Navigation** | Alt+Tab, Alt+Shift+Tab, Show Desktop, Previous Desktop, Next Desktop, Task View (Windows), Mission Control (macOS), App Expose (macOS), Launchpad (macOS) |
| **Navigation** | Alt+Tab, Alt+Shift+Tab, Show Desktop, Previous Desktop, Next Desktop, Task View (Windows), Snipping Tool (Windows), Mission Control (macOS), App Expose (macOS), Launchpad (macOS) |
| **Browser** | Back, Forward, Close Tab (Ctrl+W), New Tab (Ctrl+T), Next Tab (Ctrl+Tab), Previous Tab (Ctrl+Shift+Tab) |
| **Editing** | Copy, Paste, Cut, Undo, Select All, Save, Find |
| **Media** | Volume Up, Volume Down, Volume Mute, Play/Pause, Next Track, Previous Track |
Expand Down Expand Up @@ -175,6 +175,60 @@ pip install -r requirements.txt
| `pyobjc-framework-Cocoa` | macOS app detection and media-key support |
| `evdev` | Linux mouse grab and virtual device forwarding (uinput) |

### Fedora / GNOME Setup

Fedora needs two extra pieces beyond `pip install -r requirements.txt`:

1. Device permissions for `hidraw` and `uinput`
2. A fresh login session after adding your user to the `input` group

Install the Fedora system package used to build `evdev`:

```bash
sudo dnf install -y python3-devel xdotool
```

Create the udev rule that grants Mouser access to Logitech `hidraw` devices
and `/dev/uinput`, then add your user to the `input` group:

```bash
sudo tee /etc/udev/rules.d/99-mouser-fedora-input.rules >/dev/null <<'EOF'
KERNEL=="uinput", MODE="0660", GROUP="input", OPTIONS+="static_node=uinput"
SUBSYSTEM=="hidraw", KERNELS=="*046D:*", MODE="0660", GROUP="input"
EOF

sudo usermod -aG input "$USER"
sudo modprobe uinput
sudo udevadm control --reload-rules
sudo udevadm trigger
```

> **Important:** launching Mouser from a terminal after `newgrp input` is not
> enough for the desktop launcher or autostart entry. You must fully log out
> and back in (or reboot) so your GNOME session, app launcher, and autostarted
> processes all inherit the new `input` group membership.

After logging back in, verify the device access before troubleshooting Mouser:

```bash
id

python - <<'PY'
import os
for path in ('/dev/hidraw8', '/dev/uinput'):
try:
fd = os.open(path, os.O_RDWR | os.O_NONBLOCK)
except Exception as exc:
print(path, 'FAIL', repr(exc))
else:
print(path, 'OK')
os.close(fd)
PY
```

If both devices report `OK`, Mouser should be able to connect to supported
Logitech mice from both the terminal and the app menu.

### Running

```bash
Expand Down Expand Up @@ -206,6 +260,33 @@ Use this only for troubleshooting. On macOS, Mouser now defaults to `iokit`;
`hidapi` and `auto` remain available as manual overrides for debugging. Other
platforms continue to default to `auto`.

### Linux App Launcher

Linux autostart writes an entry under `~/.config/autostart`, which does **not**
make Mouser show up in the desktop app grid. To add a regular launcher entry on
GNOME/KDE, create a `.desktop` file in `~/.local/share/applications`:

```bash
mkdir -p ~/.local/share/applications

cat > ~/.local/share/applications/io.github.tombadash.mouser.desktop <<'EOF'
[Desktop Entry]
Type=Application
Version=1.0
Name=Mouser
Comment=Logitech mouse remapper
Exec=/path/to/mouser/.venv/bin/python /path/to/mouser/main_qml.py
Path=/path/to/mouser
Icon=/path/to/mouser/images/logo_icon.png
Terminal=false
StartupNotify=false
Categories=Utility;Settings;
EOF
```

Replace `/path/to/mouser` with your checkout path. After that, Mouser appears
in the app menu and can be pinned like a normal desktop app.

### Creating a Desktop Shortcut

A `Mouser.lnk` shortcut is included. To create one manually:
Expand Down Expand Up @@ -373,7 +454,7 @@ mouser/
│ ├── logi_devices.py # Known Logitech device catalog + connected-device metadata
│ ├── device_layouts.py # Device-family layout registry for QML overlays
│ ├── key_simulator.py # Platform-specific action simulator
│ ├── startup.py # Cross-platform login startup (Windows registry + macOS LaunchAgent)
│ ├── startup.py # Cross-platform login startup (Windows registry + macOS LaunchAgent + Linux autostart)
│ ├── config.py # Config manager (JSON load/save/migrate)
│ └── app_detector.py # Foreground app polling
Expand Down Expand Up @@ -415,7 +496,7 @@ The app has two pages accessible from a slim sidebar:
- **DPI slider:** 200–8000 with quick presets (400, 800, 1000, 1600, 2400, 4000, 6000, 8000). Reads the current DPI from the device on startup.
- **Scroll inversion:** Independent toggles for vertical and horizontal scroll direction.
- **Smart Shift:** Toggle Logitech Smart Shift (ratchet-to-free-spin scroll mode switching) on or off.
- **Startup controls:** **Start at login** (Windows and macOS) and **Start minimized** (all platforms) to launch directly into the system tray.
- **Startup controls:** **Start at login** (Windows, macOS, and Linux) and **Start minimized** (all platforms) to launch directly into the system tray.

---

Expand All @@ -436,7 +517,7 @@ The app has two pages accessible from a slim sidebar:
- [ ] **True per-device config** — separate mappings and layout state cleanly when multiple Logitech mice are used on the same machine
- [ ] **Dynamic button inventory** — build button lists from discovered `REPROG_CONTROLS_V4` controls instead of relying on the current fixed mapping set
- [x] **Custom key combos** — user-defined arbitrary key sequences (e.g., Ctrl+Shift+P)
- [x] **Windows login item support** — cross-platform login startup via Windows registry and macOS LaunchAgent
- [x] **Desktop login item support** — cross-platform login startup via Windows registry, macOS LaunchAgent, and Linux autostart
- [ ] **Improved scroll inversion** — explore driver-level or interception-driver approaches
- [ ] **Gesture swipe tuning** — improve swipe reliability and defaults across more Logitech devices
- [ ] **Per-app profile auto-creation** — detect new apps and prompt to create a profile
Expand Down Expand Up @@ -484,7 +565,7 @@ This project is licensed under the [MIT License](LICENSE).

- **[@andrew-sz](https://github.com/andrew-sz)** — macOS port: CGEventTap mouse hooking, Quartz key simulation, NSWorkspace app detection, and NSEvent media key support
- **[@thisislvca](https://github.com/thisislvca)** — significant expansion of the project including macOS compatibility improvements, multi-device support, new UI features, and active involvement in triaging and resolving open issues
- **[@awkure](https://github.com/awkure)** — cross-platform login startup (Windows registry + macOS LaunchAgent), single-instance guard, start minimized option, and MX Master 4 detection
- **[@awkure](https://github.com/awkure)** — cross-platform login startup (Windows registry + macOS LaunchAgent, later extended to Linux autostart), single-instance guard, start minimized option, and MX Master 4 detection
- **[@hieshima](https://github.com/hieshima)** — Linux support (evdev + HID++ + uinput), mode shift button mapping, Smart Shift toggle, and custom keyboard shortcut support
- **[@pavelzaichyk](https://github.com/pavelzaichyk)** — Next Tab and Previous Tab browser actions

Expand Down
Loading