Compare commits

..

7 Commits

Author SHA1 Message Date
Ferdinand Schober
0d2190e787 chore: Release 2026-06-12 15:07:11 +02:00
Ty Smith
4b93be3228 docs(macos): clarify repeat-task cleanup releases the key
Address @feschber review feedback on PR #441. The repeat-task cleanup
already releases the key with the correct CGKeyCode via the existing
key_event call at the end of the closure — this commit just expands
the surrounding comment to make that explicit and to document why
update_modifiers is intentionally NOT called from this path (Mac
CGKeyCode vs Linux evdev scancode collision).

No behavioral change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 12:07:03 +02:00
Ty Smith
c32d695cd9 macos: stop corrupting modifier state in repeat-task cleanup
spawn_repeat_task() takes a Mac CGKeyCode, but the cleanup block was
passing that value to update_modifiers(), which expects a Linux evdev
scancode (it calls scancode::Linux::try_from(key)). The two codespaces
collide on several values, so cancelling the repeat task could
silently clear a still-held modifier:

  Mac LeftShift   = 56  == Linux KeyLeftAlt   = 56  -> clears Mod1Mask
  Mac Down arrow  = 125 == Linux KeyLeftMeta  = 125 -> clears Mod4Mask
  Mac Up arrow    = 126 == Linux KeyRightMeta = 126 -> clears Mod4Mask
  Mac Backslash   = 42  == Linux KeyLeftShift = 42  -> clears ShiftMask
  Mac "9"         = 29  == Linux KeyLeftCtrl  = 29  -> clears ControlMask

In practice this broke chords such as Shift+Option+X and Cmd+Down:
pressing Shift while holding Option cancels Option's repeat task and
runs the buggy cleanup, which then interprets Mac LeftShift's code
(56) as Linux KeyLeftAlt and removes Option from the modifier state.
The next key arrives with Shift only, so window-manager bindings on
the original Option chord never fire.

Remove the buggy update_modifiers() call. Modifier state is owned by
the main consume() loop, which already calls update_modifiers() with
the correct Linux scancode on the real release event.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 12:07:03 +02:00
Ty Smith
82d677f9c8 macos: post NumericPad and SecondaryFn flags for synthesized arrow keys
Hardware-generated arrow key events on macOS carry the NumericPad and
SecondaryFn flags in addition to any user-pressed modifiers. CGEventTap-
based hotkey matchers (tiling window managers, accessibility tools, etc.)
commonly check those flags to disambiguate navigation arrows from generic
chords, and reject events that lack them.

Before this change, synthesized Option+Arrow chords were silently
swallowed by the focused application instead of being captured by the
window manager, because the events arrived with only the Alternate flag
set. Hardware Option+Arrow chords on the local keyboard worked because
the OS itself set the missing flags.

Add NumericPad + SecondaryFn to the flags posted with arrow key events
(Mac key codes 0x7B-0x7E) so synthesized arrow chords match hardware
chords on the wire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 12:07:03 +02:00
Ferdinand Schober
7ef43418c9 fix output name 2026-06-11 17:27:04 +02:00
Ferdinand Schober
8f32b7fe96 fix short-sha 2026-06-11 16:55:33 +02:00
Ferdinand Schober
02ac0bf220 include commit hash in pre-release (#456)
this solves the "create new release" issue as well as tag conflicts with
branch name
2026-06-11 16:50:04 +02:00
21 changed files with 68 additions and 202 deletions

View File

@@ -174,13 +174,16 @@ jobs:
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
- name: Get short SHA
id: vars
run: echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
- name: Create Pre-Release
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ github.event.inputs.name || github.ref_name }}
name: ${{ github.event.inputs.name || github.ref_name }}
tag_name: ${{ format('{0}-{1}', github.event.inputs.name || github.ref_name, steps.vars.outputs.short_sha) }}
name: ${{ format('{0}-{1}', github.event.inputs.name || github.ref_name, steps.vars.outputs.short_sha) }}
prerelease: true
generate_release_notes: true
files: |

16
Cargo.lock generated
View File

@@ -1647,7 +1647,7 @@ dependencies = [
[[package]]
name = "input-capture"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"ashpd",
"async-trait",
@@ -1677,7 +1677,7 @@ dependencies = [
[[package]]
name = "input-emulation"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"ashpd",
"async-trait",
@@ -1703,7 +1703,7 @@ dependencies = [
[[package]]
name = "input-event"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"futures-core",
"log",
@@ -1840,7 +1840,7 @@ dependencies = [
[[package]]
name = "lan-mouse"
version = "0.10.0"
version = "0.11.0"
dependencies = [
"clap",
"env_logger",
@@ -1875,7 +1875,7 @@ dependencies = [
[[package]]
name = "lan-mouse-cli"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"clap",
"futures",
@@ -1886,7 +1886,7 @@ dependencies = [
[[package]]
name = "lan-mouse-gtk"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"async-channel",
"glib-build-tools",
@@ -1900,7 +1900,7 @@ dependencies = [
[[package]]
name = "lan-mouse-ipc"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"futures",
"log",
@@ -1913,7 +1913,7 @@ dependencies = [
[[package]]
name = "lan-mouse-proto"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"input-event",
"num_enum",

View File

@@ -12,7 +12,7 @@ members = [
[package]
name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.10.0"
version = "0.11.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
@@ -27,13 +27,13 @@ panic = "abort"
shadow-rs = "1.2.0"
[dependencies]
input-event = { path = "input-event", version = "0.3.0" }
input-emulation = { path = "input-emulation", version = "0.3.0", default-features = false }
input-capture = { path = "input-capture", version = "0.3.0", default-features = false }
lan-mouse-cli = { path = "lan-mouse-cli", version = "0.2.0" }
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.2.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.2.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.2.0" }
input-event = { path = "input-event", version = "0.4.0" }
input-emulation = { path = "input-emulation", version = "0.4.0", default-features = false }
input-capture = { path = "input-capture", version = "0.4.0", default-features = false }
lan-mouse-cli = { path = "lan-mouse-cli", version = "0.3.0" }
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.3.0", optional = true }
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.3.0" }
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.3.0" }
shadow-rs = { version = "1.2.0", features = ["metadata"] }
hickory-resolver = "0.25.2"

View File

@@ -1,7 +1,7 @@
[package]
name = "input-capture"
description = "cross-platform input-capture library used by lan-mouse"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
@@ -10,7 +10,7 @@ repository = "https://github.com/feschber/lan-mouse"
futures = "0.3.28"
futures-core = "0.3.30"
log = "0.4.22"
input-event = { path = "../input-event", version = "0.3.0" }
input-event = { path = "../input-event", version = "0.4.0" }
memmap = "0.7"
tempfile = "3.25.0"
thiserror = "2.0.0"

View File

@@ -1,7 +1,7 @@
[package]
name = "input-emulation"
description = "cross-platform input emulation library used by lan-mouse"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
@@ -10,7 +10,7 @@ repository = "https://github.com/feschber/lan-mouse"
async-trait = "0.1.80"
futures = "0.3.28"
log = "0.4.22"
input-event = { path = "../input-event", version = "0.3.0" }
input-event = { path = "../input-event", version = "0.4.0" }
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [
"io-util",

View File

@@ -106,8 +106,19 @@ impl MacOSEmulation {
}
}
}
// release key when cancelled
update_modifiers(&modifiers, key as u32, 0);
// Always release the key with the correct CGKeyCode, regardless of
// whether the repeat loop ran. This matches @feschber's review
// request: "still release the key repeat task but with the correct
// code."
//
// Do NOT call update_modifiers here: `key` is a Mac CGKeyCode but
// update_modifiers expects a Linux evdev scancode, and the two
// codespaces collide (e.g. Mac LeftShift=56 == Linux KeyLeftAlt=56,
// Mac Down=125 == Linux KeyLeftMeta=125), corrupting modifier
// state for chords like Shift+Option+X or Cmd+Down. Modifier state
// is owned by the main consume() loop, which already calls
// update_modifiers with the correct Linux scancode on the real key
// release event from the client.
key_event(event_source.clone(), key, 0, modifiers.get());
});
self.repeat_task = Some(repeat_task);
@@ -157,6 +168,19 @@ extern "C" {
fn AXIsProcessTrusted() -> bool;
}
/// Mac virtual key codes for the four arrow keys.
const MAC_KEY_LEFT: u16 = 0x7B;
const MAC_KEY_RIGHT: u16 = 0x7C;
const MAC_KEY_DOWN: u16 = 0x7D;
const MAC_KEY_UP: u16 = 0x7E;
fn is_arrow_key(key: u16) -> bool {
matches!(
key,
MAC_KEY_LEFT | MAC_KEY_RIGHT | MAC_KEY_DOWN | MAC_KEY_UP
)
}
fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods) {
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
Ok(e) => e,
@@ -165,7 +189,15 @@ fn key_event(event_source: CGEventSource, key: u16, state: u8, modifiers: XMods)
return;
}
};
event.set_flags(to_cgevent_flags(modifiers));
let mut flags = to_cgevent_flags(modifiers);
// Hardware-generated arrow keys on macOS carry NumericPad + SecondaryFn.
// CGEventTap-based hotkey matchers (e.g. tiling window managers) check
// these flags to recognize navigation keys; without them synthesized
// arrow chords fall through to the focused app.
if is_arrow_key(key) {
flags |= CGEventFlags::CGEventFlagNumericPad | CGEventFlags::CGEventFlagSecondaryFn;
}
event.set_flags(flags);
event.post(CGEventTapLocation::HID);
log::trace!("key event: {key} {state}");
}

View File

@@ -1,7 +1,7 @@
[package]
name = "input-event"
description = "cross-platform input-event types for input-capture / input-emulation"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"

View File

@@ -1,14 +1,14 @@
[package]
name = "lan-mouse-cli"
description = "CLI Frontend for lan-mouse"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
[dependencies]
futures = "0.3.30"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.3.0" }
clap = { version = "4.4.11", features = ["derive"] }
thiserror = "2.0.0"
tokio = { version = "1.32.0", features = [

View File

@@ -1,7 +1,7 @@
[package]
name = "lan-mouse-gtk"
description = "GTK4 / Libadwaita Frontend for lan-mouse"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
@@ -12,7 +12,7 @@ adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] }
async-channel = { version = "2.1.1" }
hostname = "0.4.0"
log = "0.4.20"
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" }
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.3.0" }
thiserror = "2.0.0"
[build-dependencies]

View File

@@ -1,7 +1,7 @@
[package]
name = "lan-mouse-ipc"
description = "library for communication between lan-mouse service and frontends"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"

View File

@@ -1,7 +1,7 @@
[package]
name = "lan-mouse-proto"
description = "network protocol for lan-mouse"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/feschber/lan-mouse"
@@ -9,5 +9,5 @@ repository = "https://github.com/feschber/lan-mouse"
[dependencies]
num_enum = "0.7.2"
thiserror = "2.0.0"
input-event = { path = "../input-event", version = "0.3.0" }
input-event = { path = "../input-event", version = "0.4.0" }
paste = "1.0"

View File

@@ -1,3 +0,0 @@
/bin
/obj
icon.ico

View File

@@ -1,16 +0,0 @@
<Project Sdk="WixToolset.Sdk/5.0.0">
<PropertyGroup>
<OutputType>Bundle</OutputType>
<TargetExt>.exe</TargetExt>
<Platforms>x64</Platforms>
<InstallerPlatform>x64</InstallerPlatform>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Heat">
<Version>5.0.2</Version>
</PackageReference>
<PackageReference Include="WixToolset.Bal.wixext">
<Version>5.0.2</Version>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,42 +0,0 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal">
<Bundle
Name="Lan Mouse"
Version="0.10.0"
UpgradeCode="{39A9744D-9D6E-4CD3-A84F-9E034786A7B1}"
Compressed="no"
SplashScreenSourceFile="icon.ico">
<BootstrapperApplication>
<bal:WixStandardBootstrapperApplication
LicenseUrl=""
Theme="hyperlinkLicense" />
</BootstrapperApplication>
<Chain>
<!-- Visual C++ 2015-2022 Redistributable (x64) - 14.40.33810 -->
<ExePackage
Id="VC_REDIST_X64"
DisplayName="Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.40.33810"
PerMachine="yes"
Permanent="yes"
Protocol="burn"
InstallCondition="VersionNT64 AND (ARCH_NAME = &quot;AMD64&quot;)"
DetectCondition="(VCRUNTIME_X64_VER &gt;= VCRUNTIME_VER) AND VersionNT64 AND (ARCH_NAME = &quot;AMD64&quot;)"
InstallArguments="/install /quiet /norestart"
RepairArguments="/repair /quiet /norestart"
UninstallArguments="/uninstall /quiet /norestart">
<ExePackagePayload
Name="VC_redist.x64.exe"
ProductName="Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.40.33810"
Description="Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.40.33810"
Hash="5935B69F5138AC3FBC33813C74DA853269BA079F910936AEFA95E230C6092B92F6225BFFB594E5DD35FF29BF260E4B35F91ADEDE90FDF5F062030D8666FD0104"
Size="25397512"
Version="14.40.33810.0"
DownloadUrl="https://download.visualstudio.microsoft.com/download/pr/1754ea58-11a6-44ab-a262-696e194ce543/3642E3F95D50CC193E4B5A0B0FFBF7FE2C08801517758B4C8AEB7105A091208A/VC_redist.x64.exe" />
</ExePackage>
<MsiPackage SourceFile="..\lan-mouse\bin\Debug\en-US\LanMouse.msi" Compressed="yes"/>
</Chain>
</Bundle>
</Wix>

View File

@@ -1,3 +0,0 @@
/bin
/obj
icon.ico

View File

@@ -1,13 +0,0 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<StandardDirectory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="!(bind.Property.Manufacturer) !(bind.Property.ProductName)">
<Directory Id="SHARE" Name="share"/>
<Directory Id="LIB" Name="lib"/>
</Directory>
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="!(bind.Property.ProductName)"/>
</StandardDirectory>
</Fragment>
</Wix>

View File

@@ -1,34 +0,0 @@
<Project Sdk="WixToolset.Sdk/5.0.2">
<PropertyGroup>
<InstallerPlatform>x64</InstallerPlatform>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Heat">
<Version>5.0.2</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<HarvestDirectory Include="C:\gtk-build\gtk\x64\release\bin">
<ComponentGroupName>GTKBIN</ComponentGroupName>
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
<SuppressRegistry>true</SuppressRegistry>
</HarvestDirectory>
<BindPath Include="C:\gtk-build\gtk\x64\release\bin" />
</ItemGroup>
<ItemGroup>
<HarvestDirectory Include="C:\gtk-build\gtk\x64\release\share\icons">
<ComponentGroupName>GTKICONS</ComponentGroupName>
<DirectoryRefId>SHARE</DirectoryRefId>
<SuppressRegistry>true</SuppressRegistry>
</HarvestDirectory>
<BindPath Include="C:\gtk-build\gtk\x64\release\share\icons" />
</ItemGroup>
<ItemGroup>
<HarvestDirectory Include="C:\gtk-build\gtk\x64\release\lib\gdk-pixbuf-2.0">
<ComponentGroupName>GTKLIBS</ComponentGroupName>
<DirectoryRefId>LIB</DirectoryRefId>
<SuppressRegistry>true</SuppressRegistry>
</HarvestDirectory>
<BindPath Include="C:\gtk-build\gtk\x64\release\lib\gdk-pixbuf-2.0" />
</ItemGroup>
</Project>

View File

@@ -1,32 +0,0 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<ComponentGroup Id="LanMouseComponents" Directory="INSTALLFOLDER" Subdirectory="bin">
<Component Guid="{ECB52D3E-28AD-4BEC-B9DF-E01CCAB356BE}">
<!-- the main binary -->
<File Source="..\..\target\release\lan-mouse.exe"/>
<!-- visual c runtime dll -->
<!--<File Source="C:\windows\system32\VCRUNTIME140.dll"/>-->
<!--<File Source="C:\windows\system32\VCRUNTIME140_1.dll"/>-->
</Component>
<!-- start menu entry-->
<Component Id="ApplicationShortcut" Directory="ApplicationProgramsFolder">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="!(bind.Property.ProductName)"
Description ="Mouse and Keyboard sharing Software"
Target="[INSTALLFOLDER]bin\lan-mouse.exe"
WorkingDirectory="INSTALLFOLDER">
<Icon Id="LanMouse" SourceFile=".\icon.ico"/>
</Shortcut>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue
Root="HKCU"
Key="Software\Feschber\LanMouse"
Name="installed"
Type="integer"
Value="1"
KeyPath="yes"/>
</Component>
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -1,8 +0,0 @@
<!--
This file contains the declaration of all the localizable strings.
-->
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
<String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />
</WixLocalization>

View File

@@ -1,16 +0,0 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package Name="Lan Mouse"
Manufacturer="Ferdinand Schober"
Version="0.10.0.0"
UpgradeCode="{a330cd60-4c35-4a54-8bb6-75b3049b46c6}">
<MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" />
<MediaTemplate EmbedCab="yes"/>
<Feature Id="Main">
<ComponentGroupRef Id="GTKBIN"/>
<ComponentGroupRef Id="GTKICONS"/>
<ComponentGroupRef Id="GTKLIBS"/>
<ComponentGroupRef Id="LanMouseComponents"/>
</Feature>
</Package>
</Wix>

View File

@@ -1,2 +0,0 @@
magick -background none -density 384 ..\lan-mouse-gtk\resources\de.feschber.LanMouse.svg -trim -define icon:auto-resize icon.ico
dotnet build