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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
*.bat text eol=crlf
*.ps1 text eol=crlf
*.cs text eol=crlf
*.sh text eol=lf
*.rules text eol=lf
*.desktop text eol=lf
*.plist text eol=lf
*.ico binary
*.png binary
68 changes: 62 additions & 6 deletions .github/workflows/build-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ on:
permissions:
contents: write

env:
# The tag without its "v" on a release, otherwise a development version.
VERSION: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '1.1.0-dev' }}
DOTNET_CLI_TELEMETRY_OPTOUT: '1'
DOTNET_NOLOGO: '1'

jobs:
build:
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -30,16 +36,66 @@ jobs:
- name: Upload exe
uses: actions/upload-artifact@v4
with:
name: CompxTray
name: CompxTray-windows
path: |
bin/CompxTray.exe
bin/CompxTray.exe.sha256

linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Build and package
run: sh cross/linux/package.sh linux-x64 "$VERSION"

- name: Upload package
uses: actions/upload-artifact@v4
with:
name: CompxTray-linux-x64
path: |
out/CompxTray-linux-x64.tar.gz
out/CompxTray-linux-x64.tar.gz.sha256

macos:
runs-on: macos-latest
strategy:
matrix:
rid: [ osx-arm64, osx-x64 ]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Build and package
run: sh cross/macos/package.sh ${{ matrix.rid }} "$VERSION"

- name: Upload package
uses: actions/upload-artifact@v4
with:
name: CompxTray-${{ matrix.rid }}
path: |
out/CompxTray-${{ matrix.rid }}.zip
out/CompxTray-${{ matrix.rid }}.zip.sha256

release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [ windows, linux, macos ]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true

- name: Publish release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: |
bin/CompxTray.exe
bin/CompxTray.exe.sha256
files: dist/*
generate_release_notes: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# build output
bin/
obj/
out/
*.exe
*.pdb

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

- macOS and Linux preview (`cross/`, .NET 10 with Avalonia and HidSharp): battery level in the menu bar or tray with low battery notifications, start at login, and a read-only settings window. Not yet tested on a real Mac or Linux PC.
- `compxtray-cli` for macOS and Linux: battery, settings, diagnostics and device listing, read-only.
- Linux package with an installer and a udev rule for access to the receiver without root. macOS packages for Apple silicon and Intel as an ad-hoc signed `CompxTray.app`.
- The protocol code is split from the Windows HID transport (`src/Device/WinHid.cs`), so both builds share it.
- The log now records every change in battery level or charging state and every voltage change of 10 mV or more, not only when the mouse wakes up.

## 1.0.0 - 2026-10-24

First public release.
Expand Down
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ Det här kan du göra:

**Så kommer du igång:** ladda ner `CompxTray.exe` under [Releases](../../releases/latest) och starta den. Dubbelklicka på batteriikonen nere vid klockan för att öppna inställningarna. Appen är på engelska.

**Mac och Linux:** det finns en förhandsversion för macOS och Linux som visar batterinivån i menyraden eller aktivitetsfältet och musens inställningar (bara läsning än så länge). Den är ännu inte testad på en riktig Mac eller Linux-dator. Se [macOS and Linux](#macos-and-linux-preview).

### Vanliga frågor

**Finns det någon programvara till Svive Flow 7?**
Expand Down Expand Up @@ -132,6 +134,31 @@ To run a downloaded release: *More info → Run anyway*. Code signing through [S

Settings, the log and backups are stored in `%APPDATA%\CompxTray`.

## macOS and Linux (preview)

A separate build for macOS and Linux shows the battery level in the menu bar or tray, with low battery notifications, and a window with the mouse's settings. Settings are **read-only** in this version. It shares the protocol code with the Windows app and has been tested against a real Svive Flow 7 on Windows, but **it has not been tested on a Mac or a Linux PC yet**. If you try it, please open an issue with what you see and the output of `compxtray-cli devices`.

| Download from [Releases](../../releases/latest) | For |
|---|---|
| `CompxTray-osx-arm64.zip` | Macs with Apple silicon (M1 and later) |
| `CompxTray-osx-x64.zip` | Intel Macs |
| `CompxTray-linux-x64.tar.gz` | 64-bit Linux (x86-64) |

**macOS:** unzip and move `CompxTray.app` to *Applications*. The app is not notarized by Apple, so macOS blocks it the first time you open it. Go to *System Settings → Privacy & Security* and click *Open Anyway*. If macOS asks for *Input Monitoring*, allow it so CompxTray can reach the receiver. The log is in `~/Library/Application Support/CompxTray`.

**Linux:** unpack the archive and run `./install.sh`. It copies CompxTray to `~/.local/bin`, adds it to your applications menu and installs a udev rule (asks for your password) so you can open the receiver without root. Unplug and replug the receiver afterwards. The tray icon needs a desktop with StatusNotifier support: KDE Plasma, Xfce, Cinnamon and Ubuntu work out of the box, and on plain GNOME you need the *AppIndicator and KStatusNotifierItem Support* extension. The log is in `~/.config/CompxTray`.

Both packages include `compxtray-cli`, a read-only command-line tool:

| Command | What it shows |
|---|---|
| `compxtray-cli battery` | Battery level, charging state and voltage |
| `compxtray-cli settings --wait 30` | DPI stages, polling rate, lift-off, debounce, motion sync and sleep. `--wait` keeps trying while the mouse is asleep |
| `compxtray-cli diagnose` | Raw replies and the settings memory, for bug reports |
| `compxtray-cli devices` | The receiver's HID interfaces, for bug reports |

To build it yourself you need the [.NET 10 SDK](https://dot.net). On Linux run `sh cross/linux/package.sh linux-x64 1.1.0`, on a Mac `sh cross/macos/package.sh osx-arm64 1.1.0`. The result ends up in `out/`.

## Build from source

You don't need Visual Studio or an SDK. The C# compiler that ships with Windows (.NET Framework 4.x) is enough.
Expand Down Expand Up @@ -174,9 +201,10 @@ If the memory matches the known layout, the model can be added to the verified l
## Project layout

```
src/Device/ HID transport, memory map, read/write/verify logic
src/Device/ protocol, memory map, read/write/verify logic (shared with cross/), and WinHid.cs, the Windows HID transport
src/UI/ dark theme and owner-drawn controls, settings window
src/ tray app, app settings, entry point
cross/ the macOS and Linux build (.NET 10): Core (HidSharp transport), App (Avalonia), Cli, packaging
tools/ diagnostics and the guided hardware test
docs/ protocol notes, screenshots and the project website (GitHub Pages, English and Swedish)
```
Expand All @@ -191,6 +219,8 @@ The protocol was pieced together from public reverse-engineering work by others.

CompxTray's code is written from scratch; only protocol facts were taken from these projects. The memory map was confirmed by reading a real Svive Flow 7.

The macOS and Linux build uses [HidSharp](https://software.seekye.com/hidsharp) (Apache-2.0) for USB access and [Avalonia](https://avaloniaui.net) (MIT) for the interface.

## Disclaimer

CompxTray is an independent project. It is not affiliated with or endorsed by Svive, Compx or any mouse manufacturer, and product names are trademarks of their owners. Changing settings writes to your mouse's memory. CompxTray verifies every write and keeps a backup, but you use it at your own risk.
Expand Down
36 changes: 36 additions & 0 deletions cross/App/CompxTray.App.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>CompxTray</AssemblyName>
<RootNamespace>CompxTray.App</RootNamespace>
<Version>1.1.0</Version>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<ApplicationManifest>..\..\assets\app.manifest</ApplicationManifest>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

<!-- HidSharp is not trim-safe and picks its backend per OS at run time, so it is kept whole. -->
<ItemGroup>
<TrimmerRootAssembly Include="HidSharp" />
</ItemGroup>

<ItemGroup>
<AvaloniaResource Include="..\..\assets\icon.png" Link="Assets\icon.png" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="12.1.4" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.4" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Core\CompxTray.Core.csproj" />
</ItemGroup>

</Project>
40 changes: 40 additions & 0 deletions cross/App/Palette.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Palette.cs - the colours of the Windows version, for the Avalonia app.
using Avalonia.Media;

namespace CompxTray.App
{
static class Palette
{
public static readonly Color Bg = Hex(0x0D0F13);
public static readonly Color Surface = Hex(0x15181E);
public static readonly Color Surface2 = Hex(0x1E222A);
public static readonly Color Border = Hex(0x252A33);
public static readonly Color Text = Hex(0xECEFF4);
public static readonly Color Dim = Hex(0x8A93A3);
public static readonly Color Faint = Hex(0x5A6272);
public static readonly Color Accent = Hex(0x1EC8FF);
public static readonly Color OnAccent = Hex(0x041018);
public static readonly Color Good = Hex(0x35D07F);
public static readonly Color Warn = Hex(0xF4B740);
public static readonly Color Bad = Hex(0xFF5D62);

public static Color Hex(int rgb) { return Color.FromRgb((byte)(rgb >> 16), (byte)(rgb >> 8), (byte)rgb); }

public static IBrush Brush(Color c) { return new SolidColorBrush(c); }

public static Color LevelColor(int percent, bool charging)
{
if (charging) return Accent;
if (percent <= 20) return Bad;
if (percent <= 50) return Warn;
return Good;
}

/// HSL lightness, as System.Drawing.Color.GetBrightness on Windows.
public static double Brightness(Color c)
{
int max = System.Math.Max(c.R, System.Math.Max(c.G, c.B)), min = System.Math.Min(c.R, System.Math.Min(c.G, c.B));
return (max + min) / 510.0;
}
}
}
132 changes: 132 additions & 0 deletions cross/App/Platform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Platform.cs - data folder, log, start at login and desktop notifications on macOS and Linux.
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;

namespace CompxTray.App
{
static class Platform
{
public const string AppName = "CompxTray";
public static readonly string Version = (typeof(Platform).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "?").Split('+')[0];
public const string RepoUrl = "https://github.com/HelloAxoxelol/CompxTray";
const string LaunchAgentId = "io.github.helloaxoxelol.compxtray";

public static bool IsMac { get { return RuntimeInformation.IsOSPlatform(OSPlatform.OSX); } }
public static bool IsLinux { get { return RuntimeInformation.IsOSPlatform(OSPlatform.Linux); } }

/// ~/Library/Application Support/CompxTray on macOS, ~/.config/CompxTray on Linux.
/// On Windows, where this build only runs for testing, a subfolder keeps it apart from the Windows app's log.
public static string DataDir
{
get
{
var root = Environment.GetFolderPath(IsMac ? Environment.SpecialFolder.LocalApplicationData : Environment.SpecialFolder.ApplicationData);
string d = Path.Combine(root, AppName);
if (!IsMac && !IsLinux) d = Path.Combine(d, "cross");
try { Directory.CreateDirectory(d); } catch { }
return d;
}
}

public static void Log(string msg)
{
try
{
string path = Path.Combine(DataDir, "log.txt");
var fi = new FileInfo(path);
if (fi.Exists && fi.Length > 256 * 1024) fi.Delete();
File.AppendAllText(path, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " " + msg + Environment.NewLine);
}
catch { }
}

// ---------- Start at login ----------
static string AutostartFile
{
get
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (IsMac) return Path.Combine(home, "Library", "LaunchAgents", LaunchAgentId + ".plist");
string config = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
if (string.IsNullOrEmpty(config)) config = Path.Combine(home, ".config");
return Path.Combine(config, "autostart", "compxtray.desktop");
}
}

public static bool StartAtLogin
{
get { return File.Exists(AutostartFile); }
set
{
try
{
if (!value) { if (File.Exists(AutostartFile)) File.Delete(AutostartFile); return; }
Directory.CreateDirectory(Path.GetDirectoryName(AutostartFile));
string exe = Environment.ProcessPath;
File.WriteAllText(AutostartFile, IsMac ? LaunchAgent(exe) : DesktopEntry(exe));
}
catch (Exception ex) { Log("Start at login: " + ex.Message); }
}
}

static string LaunchAgent(string exe)
{
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
+ "<plist version=\"1.0\"><dict>\n"
+ " <key>Label</key><string>" + LaunchAgentId + "</string>\n"
+ " <key>ProgramArguments</key><array><string>" + Xml(exe) + "</string></array>\n"
+ " <key>RunAtLoad</key><true/>\n"
+ "</dict></plist>\n";
}

static string DesktopEntry(string exe)
{
return "[Desktop Entry]\nType=Application\nName=CompxTray\nComment=Mouse battery level in the tray\n"
+ "Exec=\"" + exe.Replace("\"", "\\\"") + "\"\nTerminal=false\nX-GNOME-Autostart-enabled=true\n";
}

static string Xml(string s) { return s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;"); }

// ---------- Notifications ----------
public static void Notify(string title, string text)
{
try
{
var psi = new ProcessStartInfo { UseShellExecute = false, CreateNoWindow = true };
if (IsMac)
{
psi.FileName = "osascript";
psi.ArgumentList.Add("-e");
psi.ArgumentList.Add("display notification " + AppleString(text) + " with title " + AppleString(title));
}
else if (IsLinux)
{
psi.FileName = "notify-send";
psi.ArgumentList.Add("--app-name=CompxTray");
psi.ArgumentList.Add(title);
psi.ArgumentList.Add(text);
}
else return;
using (Process.Start(psi)) { }
}
catch (Exception ex) { Log("Notification failed: " + ex.Message); }
}

static string AppleString(string s) { return "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; }

public static void OpenUrl(string url)
{
try
{
var psi = new ProcessStartInfo { UseShellExecute = false, CreateNoWindow = true, FileName = IsMac ? "open" : IsLinux ? "xdg-open" : url };
if (IsMac || IsLinux) psi.ArgumentList.Add(url); else psi.UseShellExecute = true;
using (Process.Start(psi)) { }
}
catch (Exception ex) { Log("Could not open " + url + ": " + ex.Message); }
}
}
}
Loading
Loading