Skip to content
Draft
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
124 changes: 124 additions & 0 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: Linux

on:
push:
branches: [linux-port]
pull_request:
paths:
- 'linux/**'
- 'assets/**'
- '.github/workflows/linux.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
check-and-package:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: '1.94.1'
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install packaging and verification dependencies
run: sudo apt-get update && sudo apt-get install -y python3-pil libglib2.0-bin gjs gir1.2-gtk-4.0 gir1.2-adw-1 xvfb dbus-x11
- name: Check Rust
run: cargo check --manifest-path linux/Cargo.toml --locked
- name: Test Rust
run: cargo test --manifest-path linux/Cargo.toml --locked
- name: Test companion logic
run: node linux/tests/companion.test.cjs
- name: Validate JavaScript and shell syntax
shell: bash
run: |
for file in linux/companion/tinyhand@abdulrehmankhan56/*.js; do
node --input-type=module --check < "$file"
done
bash -n linux/scripts/*.sh
glib-compile-schemas --strict --dry-run linux/companion/tinyhand@abdulrehmankhan56/schemas
- name: Package Linux releases
run: ./linux/scripts/package.sh
- name: Verify repeatable extension packaging
shell: bash
run: |
zip_path=linux/dist/tinyhand@abdulrehmankhan56.shell-extension.zip
before=$(sha256sum "$zip_path")
./linux/scripts/package.sh --extension-only
test "$(sha256sum "$zip_path")" = "$before"
- name: Verify rendering and preferences
shell: bash
run: |
cargo run --manifest-path linux/Cargo.toml --locked -- --selftest
extension_dir=$(mktemp -d)
unzip -q linux/dist/tinyhand@abdulrehmankhan56.shell-extension.zip -d "$extension_dir"
gjs -m linux/tests/companion-render.mjs "$PWD" "$extension_dir"
test_config=$(mktemp -d -t tinyhand-layout.XXXXXX)
XDG_CONFIG_HOME="$test_config" xvfb-run -a dbus-run-session -- gjs -m linux/tests/preferences-layout.mjs
- name: Verify bundle install and uninstall
shell: bash
run: |
test_dir=$(mktemp -d)
tar -xzf linux/dist/tinyhand-linux-x86_64.tar.gz -C "$test_dir"
bundle="$test_dir/tinyhand-linux-x86_64"
test_home="$test_dir/home with spaces"
mkdir -p "$test_home/.config/tinyhand" "$test_home/.local/share/gnome-shell/extensions"
echo preserved > "$test_home/.config/tinyhand/config.json"
echo preserved > "$test_home/.local/share/gnome-shell/extensions/sentinel"
HOME="$test_home" "$bundle/install.sh"
"$test_home/.local/bin/tinyhand" --version
test -x "$test_home/.local/bin/tinyhand-settings"
test -f "$test_home/.local/share/tinyhand/assets/hand_lavender.png"
HOME="$test_home" "$bundle/install.sh" --uninstall
test ! -e "$test_home/.local/bin/tinyhand"
test ! -e "$test_home/.local/share/tinyhand"
test "$(cat "$test_home/.config/tinyhand/config.json")" = preserved
test "$(cat "$test_home/.local/share/gnome-shell/extensions/sentinel")" = preserved
- name: Generate checksums
working-directory: linux/dist
run: sha256sum *.zip *.tar.gz > SHA256SUMS
- uses: actions/upload-artifact@v4
with:
name: tinyhand-linux
path: |
linux/dist/*.zip
linux/dist/*.tar.gz
linux/dist/SHA256SUMS
if-no-files-found: error

publish-prerelease:
if: github.repository == 'AbdulRehman56/tinyhand' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/linux-port'
needs: check-and-package
runs-on: ubuntu-24.04
permissions:
contents: write
concurrency:
group: tinyhand-linux-release
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: tinyhand-linux
path: linux/dist
- name: Publish versioned Linux prerelease once
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
shell: bash
run: |
release_tag="v$(cat linux/VERSION)"
[[ "$release_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-linux\.[0-9]+$ ]]
if gh release view "$release_tag" >/dev/null 2>&1; then
echo "Release already exists; leaving its tag and artifacts unchanged."
exit 0
fi
(cd linux/dist && sha256sum --check SHA256SUMS)
gh release create "$release_tag" \
--target "$GITHUB_SHA" --prerelease --latest=false \
--title "TinyHand for Linux $release_tag (experimental)" \
--notes-file linux/RELEASE_NOTES.md \
linux/dist/*.zip linux/dist/*.tar.gz linux/dist/SHA256SUMS
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ Open `TinyHand.dmg`, drag TinyHand to Applications and allow **Accessibility** w
While the pointer is on, a small toolbar at the bottom of the screen lets you pick one of
36 hands or a laser dot, mute the tap sound, or exit.

## Experimental Linux port

An optional Linux implementation is available in [linux/](./linux/README.md):
a GNOME Shell 45–50 companion for Wayland and a Rust X11 app, with visual settings
and per-user installation. GNOME 45–49 are experimental (API/mocks only); 50.1 is
the only desktop-tested version. Settings need GTK 4.8+ and libadwaita 1.4+.
See the compatibility assessment for limits; full macOS parity remains incomplete.

TinyHand's original app, concept, artwork and sounds are by
[Kamran Ahmed (@nilbuild)](https://github.com/nilbuild). The independent Linux
port is maintained by [AbdulRehman56](https://github.com/AbdulRehman56).
See the [Linux guide](./linux/README.md) for installation, packaging and known gaps.

## License

MIT. See [LICENSE](LICENSE).
3 changes: 3 additions & 0 deletions linux/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target/
**/gschemas.compiled
/dist/
101 changes: 101 additions & 0 deletions linux/COMPATIBILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# GNOME compatibility and maintenance

The extension targets GNOME Shell **45–50**. **45–49 are experimental**: their
versioned APIs were inspected and our fallbacks are covered by automated mocks,
but those desktops have not been run or tested here. GNOME **50.1** is the only
desktop used during development; the latest compatibility changes have been
checked with mocks and installed Mutter 18 API introspection, without reloading
the active extension. No cross-distribution or full macOS parity claim is made.

## Versioned API assessment

| GNOME | Cursor tracker lookup | Cursor visibility | Evidence |
| --- | --- | --- | --- |
| 45 | `Meta.CursorTracker.get_for_display(global.display)` | Save visibility; `set_pointer_visible`; restore saved value | Versioned headers + mock; no live test |
| 46 | Display lookup | Legacy setter | Versioned headers + mock; no live test |
| 47 | Display lookup | Legacy setter | Versioned headers + mock; no live test |
| 48 | `global.backend.get_cursor_tracker()` | Legacy setter | Versioned headers + mock; no live test |
| 49 | Backend lookup | Balanced `inhibit_cursor_visibility` / `uninhibit_cursor_visibility` | Versioned headers + mock; no live test |
| 50 | Backend lookup | Balanced inhibitor | Versioned headers + mock; installed Mutter 18 API check; earlier 50.1 desktop development |

The adapter detects capabilities, rather than parsing a distribution's version
string. It prefers the backend lookup, falls back to the display API, and keeps
the system cursor unchanged when neither API is available. Modern inhibition is
acquired only once and released when disabled or when Alt enables pass-through.
The legacy setter restores the visibility observed before TinyHand hid it;
it cannot coordinate concurrent cursor changes as reliably as the new inhibitor.
Disable cursor hiding in settings if another extension controls cursor visibility.

All six Mutter versions expose the same sound call:
`display.get_sound_player().play_from_file(file, description, cancellable)`.
No guessed argument-count fallback or external audio subprocess is needed in
Shell. Playback is optional; missing sound support does not stop drawing.
The cancellable is cancelled during disable.

Shell code uses Cairo and GdkPixbuf, never imports GTK/Gdk/libadwaita, and draws
through `St.DrawingArea`'s repaint/context APIs. Those APIs were checked in the
45.0 and 50.0 headers. ESM extension and preference entry points are available
from GNOME 45. Preferences open through the extension's `openPreferences()`.

## Preferences baseline

Settings require **GTK 4.8+ and libadwaita 1.4+**, independently of the Shell
version. `Gtk.Picture.content-fit` appeared in 4.8; `Adw.SwitchRow` appeared in
1.4. The preferences builder reports these minimums before building its UI if
the dependencies are too old. A Shell metadata version does not guarantee the
distribution has the necessary settings libraries.

`Adw.PreferencesWindow` and the page/group APIs predate this baseline. Although
PreferencesWindow is deprecated since libadwaita 1.6, it remains available and
matches GNOME's extension-preferences window contract. Replacing it with a
dialog would require a separate standalone-settings migration and cannot be
assumed compatible with older Shell preferences hosts.

## Checks and release policy

- `node linux/tests/companion.test.cjs` runs each 45–50 API shape through lookup,
cursor hiding/restoration, sound arguments and repeated disable. It also tests
preservation of an already hidden cursor, absent APIs, and dependency errors.
- Rust tests and rendering self-tests exercise the independent X11 implementation.
- CI pins Rust 1.94.1, uses `Cargo.lock`, and uses Ubuntu 24.04 to build the x86_64
binary against a lower glibc baseline
than the maintainer's local glibc 2.43. The included `BUILD-INFO.txt` records the
actual build host. This is not a statically linked, universal Linux binary.
- Archive timestamps are normalized (`SOURCE_DATE_EPOCH` may override the default).
CI compares two extension builds in the same environment. Reproducing bytes
across different Pillow/zlib/compiler versions is not promised.
- CI renders all 36 hands from the actual unpacked extension ZIP and runs GTK
settings/layout under Xvfb. These do not emulate a GNOME compositor or establish
live input, shortcut, cursor, scaling or other-extension compatibility.
- The ZIP bundles resized alpha artwork, sounds, schemas, license and credits;
it needs neither the Rust binary nor a separately cloned source/assets tree.
- Release tags and assets are immutable in the publication workflow. For a
compatibility change after publication, increment the extension integer/version
name and `linux/VERSION` release tag, update notes, and publish a new prerelease. Never silently
replace an existing tag's assets.
- Before promoting an older GNOME version from experimental, test enable/disable,
Point/Draw, Alt pass-through, cursor restoration, shortcuts, audio and settings
on that actual desktop, recording distribution and library versions.

## Primary sources inspected

Mutter cursor headers:
[45.0](https://github.com/GNOME/mutter/blob/45.0/src/meta/meta-cursor-tracker.h),
[46.0](https://github.com/GNOME/mutter/blob/46.0/src/meta/meta-cursor-tracker.h),
[47.0](https://github.com/GNOME/mutter/blob/47.0/src/meta/meta-cursor-tracker.h),
[48.0](https://github.com/GNOME/mutter/blob/48.0/src/meta/meta-cursor-tracker.h),
[49.0](https://github.com/GNOME/mutter/blob/49.0/src/meta/meta-cursor-tracker.h),
[50.0](https://github.com/GNOME/mutter/blob/50.0/src/meta/meta-cursor-tracker.h).
Corresponding `meta-backend.h` and `meta-sound-player.h` headers were inspected
at each of those tags; display sound-player getters were checked at 45.0/50.0.

Shell drawing:
[45.0](https://github.com/GNOME/gnome-shell/blob/45.0/src/st/st-drawing-area.h),
[50.0](https://github.com/GNOME/gnome-shell/blob/50.0/src/st/st-drawing-area.h).
Entry points and preferences:
[GNOME 45 porting guide](https://gjs.guide/extensions/upgrading/gnome-shell-45.html),
[45.0 Extension class](https://github.com/GNOME/gnome-shell/blob/45.0/js/extensions/extension.js).
Widget requirements:
[SwitchRow](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.SwitchRow.html),
[PreferencesWindow](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/class.PreferencesWindow.html),
[Picture.content-fit](https://docs.gtk.org/gtk4/property.Picture.content-fit.html).
96 changes: 96 additions & 0 deletions linux/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Contributing to the Linux port

TinyHand is the original work of **[Kamran Ahmed (@nilbuild)](https://github.com/nilbuild)**:
[upstream source](https://github.com/nilbuild/tinyhand) · [tinyhand.app](https://tinyhand.app).
[AbdulRehman56](https://github.com/AbdulRehman56) maintains this independent Linux fork.
Retain creator attribution and the unchanged MIT notice, **Copyright (c) 2026 Kamran Ahmed**,
in source distributions, packages, and release materials.

## Scope and setup

Work from the fork's `linux-port` branch. Keep Linux changes in `linux/` where practical;
leave the original macOS source untouched unless a separately explained change is necessary.
Do not claim upstream acceptance or full macOS parity.

```sh
git clone --branch linux-port https://github.com/AbdulRehman56/tinyhand.git
cd tinyhand
cargo build --manifest-path linux/Cargo.toml
```

The X11 app uses pure-Rust dependencies and needs no system development headers.
Use a normal user account, not sudo. Cargo needs access to dependencies on the first
build; `--offline` works only after they are cached. Node.js runs companion logic tests.
GJS rendering/settings checks require the appropriate GNOME libraries; settings needs
GTK 4.8+ and libadwaita 1.4+, and its layout test needs a graphical desktop.

## Development checks

Run from the repository root:

```sh
cargo test --manifest-path linux/Cargo.toml
cargo run --manifest-path linux/Cargo.toml -- --selftest
node --test linux/tests/companion.test.cjs
gjs -m linux/tests/companion-render.mjs
glib-compile-schemas --strict --dry-run linux/companion/tinyhand@abdulrehmankhan56/schemas

# Graphical desktop required; isolate writes from your real settings:
test_config=$(mktemp -d -t tinyhand-layout.XXXXXX)
XDG_CONFIG_HOME="$test_config" gjs -m linux/tests/preferences-layout.mjs
```

The self-test writes sample poses to `/tmp/tinyhand-selftest/`. Inspect rendering and
fingertip alignment as well as exit status. For a cached/offline Rust check, add
`--offline` to the Cargo commands. Report skipped checks and their reasons honestly.

For desktop testing, inspect and run `./linux/scripts/install.sh --extension` as your
normal user, then enable `tinyhand@abdulrehmankhan56`. A new extension directory may
require logout/login before discovery; copied files are not a guarantee of live reload.
Never run the X11 app and companion simultaneously.

## Pull requests and bug reports

- Describe the problem, the change, and how to reproduce or verify it.
- State your distribution, session (GNOME Wayland or X11), GNOME Shell version, display
scaling/monitor setup, and relevant extension conflicts.
- List exact checks run, results, and untested scope. Metadata targets GNOME 45–50;
45–49 remain experimental with versioned API/mocked coverage, never claimed as
desktop-tested. GNOME 50.1 is the only desktop used in development; the latest
changes have not been hot-loaded. Follow [COMPATIBILITY.md](./COMPATIBILITY.md)
before promoting a version or raising the GTK/libadwaita minimums.
- For interaction changes, test Point/Draw, Alt pass-through, cursor restoration,
shortcuts, settings persistence, enable/disable, and sound where applicable.
- For settings changes, test all affected controls and the isolated GTK layout test.
- Keep patches focused. Avoid unrelated formatting, generated build output, or personal config.
- Include screenshots only when captured from the actual implementation; do not imply
a mockup or offline render proves live Shell behavior.

An upstream PR should clearly describe this as an optional, experimental Linux port,
credit the original creator, explain packaging and dependencies, and separate verified
behavior from remaining gaps. Submit only the tested scope; upstream review and acceptance
remain the original project's decision.

## Release checklist

Keep [release notes](./RELEASE_NOTES.md) accurate and preserve attribution in every artifact.
Verify install/uninstall in a clean per-user environment before publishing packages.
The initial prerelease tag was `v1.0.2-linux.1`; the extension ZIP and Linux tarball
installer must be built and checked before being advertised as downloadable. The ZIP
should be self-contained, with resized alpha artwork for Shell rendering; leave source
artwork unchanged. The portable tarball installer should not require Cargo.
Use native Shell preferences launching and sound playback; do not introduce a Gdk import
into Shell code. Preferences previews and X11 audio may still require `paplay`.
The prepared publication can be run with `./linux/scripts/publish.sh` from a
`linux-port` checkout with an authenticated GitHub CLI and a writable `.git` directory.
It commits only the listed publication files, refuses unrelated or staged changes,
prepares an upstream branch with a minimal root README addition, pushes both branches,
sets the fork's default branch, opens the upstream draft PR, and waits for the release
workflow. It leaves an existing release unchanged. The tag comes from `linux/VERSION`
with a `v` prefix. GNOME's human-readable `version-name` permits only letters,
digits, spaces and dots, not semver hyphens. Update `linux/VERSION`, that display
field, the integer extension version, and release notes together for each follow-up. The workflow builds
on Ubuntu 24.04 and only publishes after its verification job succeeds.
The [GNOME Extensions listing](https://extensions.gnome.org/extension/11682/tinyhand-for-gnome/)
is submitted and awaiting review. Do not add an approved badge or claim store
installation availability before approval and publication.
Loading