diff --git a/README.md b/README.md
index 94afe95..d1ea62e 100644
--- a/README.md
+++ b/README.md
@@ -1,298 +1,345 @@
# socktop-swipe
-Swipe-to-zoom touchscreen navigation for a wall-mounted
-[socktop](https://crates.io/crates/socktop) dashboard.
+A rack-mounted touchscreen you swipe through to see what your machines are doing.
-Hosts are arranged in **groups**. Each group is one tmux window showing its
-hosts tiled together. Swiping right-to-left on the panel zooms into each host
-full-screen, one at a time, then moves on to the next group's overview; swiping
-left-to-right walks back the same way.
+Terminal dashboards are arranged in a grid. Swipe left and right to move along a
+row; swipe up and down to move between rows. A group of hosts is one place in
+that grid: it shows them tiled together first, then each one full-screen as you
+keep swiping.
-```
- group A -> A1 -> A2 -> A3 -> A4 -> group B -> B1 -> B2
- (quarters) zoomed zoomed zoomed zoomed (stacked) zoomed zoomed
- <------------------------- swipe left-to-right -------------------------
-```
-
-With a single group this is simply overview -> each host -> back.
-
-Optionally, swiping **down** from any screen shows a different TUI (an
-[Uptime Kuma](https://github.com/louislam/uptime-kuma) status page on the rack
-display), and swiping **up** returns to exactly the screen you left.
-
-Built for a 10" touch panel on a server rack driving four Raspberry Pis, on a
-low-power x86 box running i3 on X11.
-
-## What it looks like
-
-The panel in place, showing the four-Pi group overview:
-
-
-
-Swiping through the carousel ([media/swipe-demo.mp4](media/swipe-demo.mp4)):
+
-## How it works, and why
+Built for a 9" panel bolted into a 19" server rack, driven by a low-power x86 box
+running i3 on X11.
-**The carousel is tmux pane zoom, not extra socktop instances.** There is exactly
-one socktop process per host. Zooming calls `tmux resize-pane -Z`, which makes one
-pane fill the window and sends `SIGWINCH` so socktop repaints at the new size.
+## What you're looking at
-That matters for three reasons:
+The photo is a 9" touchscreen in a 19" rack, showing four Raspberry Pis at once
+via [socktop](https://github.com/jasonwitty/socktop). It is a wall display: no
+keyboard, no mouse, nothing to log into. It comes back by itself after a power
+cut and it never blanks.
-- **Half the processes.** Separate full-screen instances would mean N extra
- socktop processes and double the polling load on the monitored hosts. On a
- 1.9 GB display host, that is the difference between comfortable and not.
-- **No reconnect delay.** Hidden panes keep running and stay connected, so
- swiping back to a host shows current data, not a stale snapshot.
-- **Constant load.** The monitored hosts see the same connections regardless of
- what is on screen.
+Everything on it is a terminal program. socktop-swipe does not draw anything
+itself — it decides what is on screen and reads your finger.
-**Gestures come from lisgd, below the terminal.** No Linux terminal maps
-horizontal swipes to commands, so this cannot be done in terminal config.
-Critically, **libinput deliberately emits gesture events only for touchpads,
-never for touchscreens** — so `libinput-gestures` and similar tools cannot work
-here at all. [lisgd](https://git.sr.ht/~mil/lisgd) reads raw touch events and
-synthesises the swipes itself, reading `/dev/input` directly and bypassing X.
+```
+ unifly -1x0
+ (UniFi network)
+ ↕
+ ┌──────────────────────────────┐ ┌──────────────────┐
+ │ 4 Raspberry Pis │→ │ 2 more machines │ row 0
+ │ tiled, then each full-screen│ │ │
+ └──────────────────────────────┘ └──────────────────┘
+ 0x0 0x1
+ ↕
+ uptime kuma 1x0
+ (service status)
+```
-## Requirements
+Swipe forward through row 0 and you get: all four Pis tiled, then `rpi-master`
+full-screen, then each of the others, then on to the next group. Swipe up at any
+point for the UniFi network view; swipe back down and you are on the exact host
+you left.
-- `tmux`, `socktop`, and a working `~/.config/socktop/profiles.json`
-- `lisgd` (the installer builds it)
-- A C toolchain plus `libinput` and `libX11` headers, to build lisgd
-- Membership of the `input` group (the installer adds you; needs a relogin)
+## Hardware
+
+| Part | What was used | Notes |
+| --- | --- | --- |
+| Display host | LattePanda (Atom x5-Z8350, 1.9 GB) | Debian 11, i3 on X11 |
+| Display host | Dell Wyse 3040 (Atom x5-Z8350, 2 GB) | ~$35 used; 8/16 GB eMMC, DP out |
+| Panel | GeeekPi 9" 1280x720 3U touchscreen | ILITEK USB touch |
+| Mounting | [`cad/`](cad) — 19" rack adapter | 3D-print or laser-cut aluminium |
+
+Any Linux box with a USB or DSI touchscreen will do. It is deliberately modest
+hardware: the whole point is a display that idles at a few watts.
+
+The panel is sold as a **10-inch mini-rack** part. [`cad/`](cad) has a pair of
+side adapters — OpenSCAD source, STLs with tested print settings, and
+SendCutSend-ready DXFs — that mount it in a standard **19-inch** rack, with three
+keystone jack positions on each side. The geometry has been physically tested.
## Install
+```sh
+curl -fsSL https://gt.wittyoneoff.com/jason/socktop-swipe/raw/branch/main/install.sh | sh
+```
+
+It checks everything first and prints the whole cost — packages, toolchain, disk
+— before changing anything. Then it asks, one thing at a time:
+
+- which touch device to use (it looks for one and offers it)
+- how to grant access to it: a udev rule for touchscreens only, or the `input`
+ group
+- whether to install socktop, uptime-kuma-status or unifly
+- whether to stop the screen blanking, sleeping and locking
+- whether to autostart on login
+- whether to log straight in at boot, so it comes back after a power cut
+
+Every answer has a sensible default and nothing is required. Piping into `sh`
+leaves no keyboard on standard input, so the script reads your terminal directly;
+if there is no terminal at all it takes the defaults and says so. `--yes` skips
+the questions.
+
+There are no prebuilt binaries yet — it builds from source, so a Rust toolchain
+is installed if you do not have one. Budget about 1.8 GB and, on an Atom, twenty
+minutes or so.
+
+
+From a checkout instead
+
```sh
git clone https://gt.wittyoneoff.com/jason/socktop-swipe
cd socktop-swipe
-./install.sh --xignore --i3
+./install.sh
```
+
-Then **log out and back in** — the `input` group and the X rule both need a
-fresh session.
+## Your first config
-| Flag | Effect |
-| --- | --- |
-| *(none)* | Build/install lisgd, install the three scripts and the default config |
-| `--xignore` | Also tell X to ignore the touch panel (see below — usually wanted) |
-| `--i3` | Also add i3 autostart lines (gesture daemon, fullscreen terminal, idle-pointer hiding via `unclutter-xfixes`), with a backup and `i3 -C` validation |
-| `--noblank` | Also stop the screen blanking / DPMS power-down (wall displays) |
-| `--autologin` | Also make lightdm log this user straight into i3 at boot (see [security note](#related-does-the-display-come-back-by-itself)) |
+The config lives at `~/.config/socktop-swipe/config.yaml`. The installer writes a
+starting point; this section builds one up from nothing.
-Re-running is safe: an existing config file is never overwritten, and the i3
-edit is skipped if already present.
-
-### Verify
+**Check every edit** with:
```sh
-tools/find-device.sh # confirm TOUCH_DEV is right
-tools/diag.sh # stage 1: device live? stage 2: what does lisgd see?
-tools/test-foreground.sh # run the real daemon, watch the display, swipe
+socktop-swipe validate
```
-## Configuration
+which resolves the grid, prints the map, and tells you if a program it would run
+is not actually there.
-Everything lives in **`/usr/local/etc/socktop-swipe.env`**. Edit it there, not in
-the repo copy. After changing it, restart the daemon and rebuild the session:
+### One group of hosts
-```sh
-socktop-gestures --replace & # restarts the daemon, running or not
-socktop-rack
+```yaml
+touch:
+ device: /dev/input/by-id/usb-ILITEK_ILITEK-TOUCH-event-if00
+ width: 1280
+ height: 720
+
+screens:
+ - at: "0x0"
+ type: socktop
+ socktop_group: [rpi-master, rpi-worker-1, rpi-worker-2, rpi-worker-3]
```
-Use `--replace` rather than `pkill -x lisgd && socktop-gestures`: `pkill` exits
-non-zero when it matches nothing, so that one-liner silently starts **nothing**
-if no daemon was running — you would swipe, get no response, and reasonably
-blame the config change.
+That is five screens: the four hosts tiled, then each one full-screen.
-| Option | Default | What it does |
+```
+0x0 tiled → rpi-master → rpi-worker-1 → rpi-worker-2 → rpi-worker-3
+```
+
+> **Quote the coordinate.** Unquoted, YAML reads `0x0` as the hexadecimal number
+> zero. `socktop-swipe validate` will tell you so if you forget.
+
+### Something above it
+
+```yaml
+ - at: "-1x0"
+ type: unifly
+```
+
+Row `-1` is above row `0`. Now swiping up from any of those five screens shows
+the UniFi TUI, and swiping down returns you to the exact host you were on.
+
+### Something below, and more hosts alongside
+
+```yaml
+ - at: "0x1"
+ type: socktop
+ socktop_group: "orangepi, trixie"
+ layout: even-vertical
+
+ - at: "1x0"
+ type: uptime-kuma-status
+ url: https://status.example.com/status/mine
+```
+
+`0x1` sits to the right of `0x0`, so you reach it by swiping forward past the
+last Pi. `1x0` is below.
+
+Full worked example with every option commented:
+[`config.example.yaml`](config.example.yaml).
+
+## How the grid works
+
+**Row increases downward.** `-1x0` is above `0x0`; `1x0` is below it. Column
+increases rightward. You start at `0x0`.
+
+**Coordinates are ordering, not slots.** Only their sort order matters, so `0x1`
+and `0x5` are interchangeable. You never have to count how many screens a group
+produces in order to place something next to it — add a host to a group and
+nothing else needs renumbering.
+
+**A group of hosts is one place in the grid.** Swiping forward walks its screens
+— tiled overview, then each host — and only moves on to the next place after the
+last one. Coming back from the right lands you on that group's *last* host, not
+its overview, so the row reads as one continuous strip.
+
+**Swiping up or down returns you to where you were.** Each row remembers its
+position. If you glance at the network view and come back, you are on the same
+host, still zoomed.
+
+**Nothing wraps around.** Swiping past either end of a row does nothing. On a
+wall display, wrapping makes it impossible to tell where you are.
+
+If a row has no screen in your current column, you land on the nearest one —
+`0x3` swiping up with only `-1x0` above goes there. That only happens the first
+time; after that the row remembers.
+
+## Monitor types
+
+Every type also accepts `command:` to replace the generated command outright, and
+`title:` to change the label on the pane border.
+
+| `type:` | Parameters | What it runs |
| --- | --- | --- |
-| `SOCKTOP_GROUPS` | Pis quartered `;` Orange Pi + Debian stacked | Groups separated by `;`, each a list of socktop profile names **in swipe order**, plus an optional `@layout` (`tiled` default, `even-vertical` stacks, `even-horizontal` side-by-side, `main-*` also work). Any count of groups and hosts works. |
-| `SOCKTOP_HOSTS` | empty | Legacy single-group form; used only when `SOCKTOP_GROUPS` is empty |
-| `SOCKTOP_AUX_CMD` | `uptime-kuma-status …` | Command for the swipe-down "aux" screen. Full path. Empty disables the feature and its gestures. |
-| `GESTURE_AUX_IN` / `GESTURE_AUX_OUT` | `UD` / `DU` | Swipe down shows aux, swipe up returns |
-| `SOCKTOP_SESSION` | `socktop4` | tmux session name |
-| `SOCKTOP_BIN` | `$HOME/.cargo/bin/socktop` | Full path — i3 does not have `~/.cargo/bin` on `PATH` |
-| `TOUCH_DEV` | ILITEK by-id path | Touch device. **Always use a `/dev/input/by-id/` path**; `eventN` numbers change on reboot. |
-| `SCREEN_W` / `SCREEN_H` | `1024` / `600` | The **panel's** resolution, not the X screen. See gotcha 2. |
-| `SWIPE_THRESHOLD` | `80` | Pixels of travel before a drag counts as a swipe |
-| `SWIPE_LENIENCY` | `30` | Degrees off-axis tolerated (max 45) |
-| `FINGER_COUNTS` | `1 2 3` | Contact counts accepted. **The setting most likely to need changing** — see gotcha 1. |
-| `GESTURE_IN` / `GESTURE_OUT` | `RL` / `LR` | Swap these to reverse swipe direction |
+| `socktop` | `socktop_group` (list, or comma-separated string), `layout` | one [socktop](https://github.com/jasonwitty/socktop) per host |
+| `uptime-kuma-status` | `url` | [uptime-kuma-status](https://github.com/jasonwitty/uptime-kuma-status) against a public [Uptime Kuma](https://github.com/louislam/uptime-kuma) status page |
+| `unifly` | *(none yet)* | [unifly](https://github.com/jasonwitty/unifly) `tui` |
+| `generic` | `command`, `title` | anything you like |
-### Adding or removing hosts
+`layout` takes any tmux layout name: `tiled` (the default), `even-horizontal`,
+`even-vertical`, `main-horizontal`, `main-vertical`.
-Edit `SOCKTOP_GROUPS` and run `socktop-rack`. Nothing else needs touching — the
-carousel derives its length from the windows and panes at runtime. Every host
-named must exist in `~/.config/socktop/profiles.json` on the display host.
-
-```sh
-# four Pis as quarters, then two more boxes stacked one above the other
-SOCKTOP_GROUPS="rpi-master rpi-worker-1 rpi-worker-2 rpi-worker-3 ; orangepi trixie @even-vertical"
+```yaml
+ - at: "1x1"
+ type: generic
+ title: k3s
+ command: journalctl -f -u k3s-agent
```
-## Autostart
+## Configuration reference
-`./install.sh --i3` appends to `~/.config/i3/config`. For systemd user sessions
+| Key | Default | What it does |
+| --- | --- | --- |
+| `session` | `socktop-swipe` | tmux session name |
+| `terminal` | *(none)* | Terminal that `run` opens the dashboard in. Unset attaches in the current one |
+| `indicator` | `false` | Show position — what is above, where you are, what is below — in the status line |
+| `binaries.socktop` etc. | looked up on `PATH` | Full path to each program. `~/` is expanded |
+| `touch.device` | — | **Always a `/dev/input/by-id/` path**; `eventN` numbers change on reboot |
+| `touch.width` / `.height` | — | The **panel's** resolution, not the X screen |
+| `touch.grab` | `true` | Take the panel exclusively so X never sees the touches |
+| `touch.threshold` | `80` | Pixels of travel before a drag counts as a swipe |
+| `touch.leniency` | `30` | Degrees off-axis tolerated, max 45 |
+| `touch.fingers` | `[1, 2, 3]` | Contact counts accepted. **The setting most likely to need changing** |
+| `gestures.forward` / `.back` | `RL` / `LR` | Named by finger motion. Swap to reverse |
+| `gestures.up` / `.down` | `DU` / `UD` | |
+
+### Commands
+
+| Command | |
+| --- | --- |
+| `socktop-swipe run` | Build the session, open it in a terminal, read the panel. The one thing to autostart |
+| `socktop-swipe validate` | Check the config and print the grid map |
+| `socktop-swipe doctor` | Swipe, and be told what the panel reported |
+| `socktop-swipe doctor --list` | List touch devices — works before any config exists |
+| `socktop-swipe forward` / `back` / `up` / `down` | Move a running instance. For keybindings, or a box with no panel |
+| `socktop-swipe build` / `attach` / `daemon` | The pieces of `run`, separately, for a systemd split |
+
+## Unattended operation
+
+A wall display has to survive a power cut with nobody in the room.
+
+**Never blank.** The installer writes an Xorg `ServerFlags` snippet with all four
+timeouts at zero, which covers every session including the display manager's
+greeter and survives reboots, *and* adds an `xset` line to the session autostart,
+because a session can re-enable blanking after X starts. Both are needed. Check
+with `xset q | grep -A1 -E 'Screen Saver|DPMS'` — you want `timeout: 0`,
+`prefer blanking: no`, `DPMS is Disabled`.
+
+**Come back by itself.** Without autologin, a reboot leaves the panel at a login
+prompt and nothing starts. The installer offers a lightdm drop-in. It is opt-in
+and defaults to no, because **anyone with physical access to the panel gets that
+user's session** — only do it where the account is nothing but the dashboard.
+Delete `/etc/lightdm/lightdm.conf.d/50-autologin-socktop-swipe.conf` to undo it.
+
+**Autostart.** `socktop-swipe run` is a single process that builds the session,
+opens the terminal and reads the panel, so one line in your window manager's
+autostart is enough. For a systemd user session,
+[`packaging/socktop-swipe.service`](packaging/socktop-swipe.service) splits it up.
+
+## Troubleshooting
+
+Start here:
+
+```sh
+socktop-swipe doctor
+```
+
+It watches the panel and says what it saw in English — direction, contact count,
+and why a gesture was ignored.
+
+### Swipes are detected but nothing happens
+
+`doctor` will say `saw 2 contact(s), config accepts [1]`. Many multipoint panels
+report two or even three contacts for a physically one-finger swipe. Add them to
+`touch.fingers`. This is the single most common problem on new hardware.
+
+### Nothing is detected at all
+
+In order: is the device path right (`socktop-swipe doctor --list`)? Can you read
+it — is the udev rule installed, or are you in the `input` group and did you log
+out and back in? Is the panel plugged in?
+
+### Swipes work, but the terminal also reacts — text zooming, panes resizing
+
+X is delivering touch to whatever is on screen as well. `touch.grab: true` (the
+default) prevents this by taking the device exclusively. If you have set it to
+`false` because you want touch in other applications, tell X to ignore the panel
instead:
```sh
-cp socktop-swipe.service ~/.config/systemd/user/
-systemctl --user enable --now socktop-swipe
-```
-
-Either way, **run exactly one gesture daemon**. lisgd does not grab the input
-device exclusively, so two instances make every swipe fire twice.
-`socktop-gestures` enforces this: it exits non-zero if one is already running,
-unless given `--replace`.
-
-## Stopping the screen blanking
-
-A wall display must never blank. `./install.sh --noblank` applies two layers:
-
-1. An Xorg `ServerFlags` snippet with all four timeouts at `0`, so it covers
- every X session including the display-manager greeter, and survives reboots.
-2. An `xset s off s noblank -dpms` line in the i3 autostart, because a session
- or DM can re-enable the screensaver after X has started.
-
-Both are needed. `xset` alone does not survive a reboot, and — the trap worth
-knowing — **`xset` must run as the session user**. Running `sudo xset ...`, or a
-`sudo bash noblank.sh` wrapper, targets root's X connection and silently does
-nothing while appearing to succeed. Check the real state with:
-
-```sh
-xset q | grep -A1 -E 'Screen Saver|DPMS'
-```
-
-You want `timeout: 0`, `prefer blanking: no`, and `DPMS is Disabled`.
-
-### Related: phantom outputs
-
-Some boards report a display output that has no panel behind it. The LattePanda's
-onboard DSI header shows up as `DSI-1 connected 1024x600` with **zero EDID bytes**
-(`xrandr` prints `0mm x 0mm`), and X happily puts workspace 1 on it. Everything
-then looks healthy from ssh -- i3 has the window fullscreen on its primary
-output -- while the real panel shows an empty workspace. Check with:
-
-```sh
-xrandr | grep ' connected' # a real panel has physical mm, phantoms say 0mm x 0mm
-for c in /sys/class/drm/card0-*; do echo "$c $(cat $c/status) edid=$(wc -c <$c/edid)"; done
-```
-
-Tell X to ignore the phantom and mark the real panel primary (adjust the names):
-
-```sh
-sudo tee /etc/X11/xorg.conf.d/20-outputs-socktop-swipe.conf >/dev/null <<'EOF'
-Section "Monitor"
- Identifier "DSI-1"
- Option "Ignore" "true"
-EndSection
-Section "Monitor"
- Identifier "HDMI-2"
- Option "Primary" "true"
+sudo tee /etc/X11/xorg.conf.d/99-ignore-touch-socktop-swipe.conf >/dev/null <<'EOF'
+Section "InputClass"
+ Identifier "ignore touchscreen (socktop-swipe)"
+ MatchProduct "ILITEK"
+ MatchIsTouchscreen "on"
+ Option "Ignore" "on"
EndSection
EOF
```
-Then set `SCREEN_W` / `SCREEN_H` in the config to the real panel's mode.
+Adjust `MatchProduct` to your panel, and restart X.
-### Related: does the display come back by itself?
+### A pane shows `[... exited: status 1]`
-Blanking is only half of unattended operation. If the display manager has no
-autologin configured, a reboot leaves the panel at a login prompt and nothing
-autostarts. Check with:
+That monitor program stopped. The pane is deliberately kept so the layout does
+not reshuffle and you can see what happened. Check the path under `binaries:`,
+and that the program works when you run it by hand.
-```sh
-grep -E '^autologin-(user|session)' /etc/lightdm/lightdm.conf
-```
+### The dashboard is on a screen that isn't there
-No output means no autologin. `./install.sh --autologin` writes
-`/etc/lightdm/lightdm.conf.d/50-autologin-socktop-swipe.conf` for the current
-user into the `i3` session. It is opt-in, not a default: **anyone with physical
-access to the panel gets that user's session**, so only do it where the display
-user is nothing but the dashboard. Delete the file to get the login prompt back.
+Some boards report an output with no panel behind it, and X will happily put the
+dashboard on it — everything looks fine over ssh while the real panel shows an
+empty desktop. See [`notes/HARDWARE-NOTES.md`](notes/HARDWARE-NOTES.md).
## Uninstall
```sh
-./uninstall.sh # scripts, config, X rule
+./uninstall.sh # binary, config, udev rule, X snippets, autologin
./uninstall.sh --keep-config # keep your settings
```
-lisgd, the `input` group and your socktop profiles are deliberately left alone;
-the script prints how to remove each.
+The Rust toolchain, tmux, the monitor programs and your `input` group membership
+are left alone; the script prints how to remove each.
-## Troubleshooting
+## Notes
-Four gotchas account for nearly every failure. All were found the hard way.
+Design decisions and the reasoning behind them are in [`notes/`](notes):
-### 1. Gestures are recognised but never fire
+- [`DESIGN.md`](notes/DESIGN.md) — why tmux, why evdev, why the grid works this way
+- [`HARDWARE-NOTES.md`](notes/HARDWARE-NOTES.md) — panel quirks, phantom outputs, the `sudo xset` trap
+- [`V1-BASH.md`](notes/V1-BASH.md) — the shell implementation this replaced, and how to migrate
+- [`TODO.md`](notes/TODO.md)
-Run `tools/diag.sh` and read stage 2:
+## Related
-```
-[swipe]: Cfg(f=1/s=3/e=0/d=0) <=> Evt(f=2/s=3/e=1/d=2)
- ^ configured ^ what happened
-```
+- [socktop](https://github.com/jasonwitty/socktop) — the host monitor this was built around
+- [uptime-kuma-status](https://github.com/jasonwitty/uptime-kuma-status) — Uptime Kuma status pages in a terminal
+- [unifly](https://github.com/jasonwitty/unifly) — UniFi network TUI
-Matching `s=` with differing `f=` means **your panel reports more contacts than
-you configured**. Many multipoint panels report 2 or even 3 contacts for a
-physically one-finger swipe. Add them to `FINGER_COUNTS`.
+## License
-Direction enum: `0=DU`, `1=UD`, `2=LR`, `3=RL`.
-
-### 2. Swipes register, but erratically — resizing panes, zooming text
-
-X is also delivering touch to whatever is on screen, so a single swipe hits
-several consumers at once: tmux mouse mode drags pane borders, the terminal
-reads 2-contact swipes as pinch-zoom, and socktop enables its own all-motion
-mouse reporting (`?1003h`). Meanwhile lisgd fires too.
-
-Fix with `./install.sh --xignore` and relog. lisgd is unaffected because it
-reads the device directly. Trade-off: no tap or pinch-zoom on that panel —
-terminal keyboard zoom (`Ctrl +` / `Ctrl -`) still works.
-
-Also ensure tmux mouse mode is **off**; `socktop-rack` sets this.
-
-### 3. Every swipe jumps two panes
-
-Two lisgd instances are running. `pgrep -x lisgd` should show exactly one.
-
-`socktop-gestures` refuses to start if a daemon is already running, so this
-should not happen via the normal path; `--replace` restarts cleanly. It can
-still occur if lisgd was launched by hand.
-
-### 4. Swipes do nothing at all
-
-In order: is the session running (`tmux ls`)? Is the daemon running
-(`pgrep -x lisgd`)? Does `tools/diag.sh` stage 1 read bytes? Are you in the
-`input` group (`id -nG | grep input`, needs a relogin)?
-
-Note lisgd's verbose flag is **`-v`**, not `-D`.
-
-### Multi-monitor note
-
-If the display host has a second monitor, X reports the **combined** root window
-(e.g. 2304x720). lisgd would scale its maths to that, so `SCREEN_W`/`SCREEN_H`
-must describe the touch panel alone. `socktop-gestures` always passes them
-explicitly.
-
-## Known limitations
-
-- **~300 ms repaint on zoom-in.** Between tmux making the pane full-screen and
- socktop repainting, you briefly see the old small rendering anchored top-left.
- This is socktop's redraw latency, not the gesture path; nothing outside
- socktop can fix it. Zooming out has no visible artifact, since panes return to
- sizes they were already drawn at.
-- Deliberately **no wrap-around** at the ends of the carousel — on a wall display
- wrapping makes it impossible to tell where you are.
-- X11 only. The lisgd build disables its Wayland backend (`WITHOUT_WAYLAND=1`).
-
-## Tested on
-
-LattePanda (Atom x5-Z8350, 1.9 GB RAM, Cherry Trail graphics), 1024x600 ILITEK
-DSI touch panel, Debian 11, i3 on X11, Alacritty 0.16.1, lisgd from git,
-monitoring four Raspberry Pis.
+Apache-2.0.
diff --git a/cad/README.md b/cad/README.md
index 84e1b89..8a4f0f6 100644
--- a/cad/README.md
+++ b/cad/README.md
@@ -2,9 +2,13 @@
A pair of side adapters for mounting the **GeeekPi 9-inch 1280x720 3U rack-mount touchscreen monitor** in a standard **19-inch equipment rack**.
+Three sizes get mentioned here and they are three different things: the **screen**
+is 9 inches, the mount it ships for is the **10-inch mini-rack** standard, and
+these adapters convert that to a **19-inch** equipment rack.
+
The design also provides **three keystone-jack positions on each side of the display**.
-
+
## Release v1
@@ -18,22 +22,26 @@ The mounting geometry has been physically tested in a standard 19-inch rack.
## Files
```text
-.
-├── README.md
-├── 3d-print/
-│ ├── geeekpi_19in_adapter_LEFT_release-v1.stl
-│ ├── geeekpi_19in_adapter_RIGHT_release-v1.stl
-│ ├── geeekpi_19in_adapter_LEFT_release-v1_AnycubicMegaPro.gcode
-│ └── geeekpi_19in_adapter_RIGHT_release-v1_AnycubicMegaPro.gcode
-├── sendcutsend/
-│ ├── geeekpi_19in_adapter_LEFT_release-v1.dxf
-│ └── geeekpi_19in_adapter_RIGHT_release-v1.dxf
-├── source/
-│ └── geeekpi_19in_adapter_release-v1.scad
-└── docs/
- └── installed.jpg
+cad/
+├── README.md this file
+├── geeekpi_rack_adapter_release_v1/ <- use this one
+│ ├── 3d-print/
+│ │ ├── geeekpi_19in_adapter_LEFT_release-v1.stl
+│ │ ├── geeekpi_19in_adapter_RIGHT_release-v1.stl
+│ │ ├── geeekpi_19in_adapter_LEFT_release-v1_AnycubicMegaPro.gcode
+│ │ └── geeekpi_19in_adapter_RIGHT_release-v1_AnycubicMegaPro.gcode
+│ ├── sendcutsend/
+│ │ ├── geeekpi_19in_adapter_LEFT_release-v1.dxf
+│ │ └── geeekpi_19in_adapter_RIGHT_release-v1.dxf
+│ ├── source/
+│ │ └── geeekpi_19in_adapter_release-v1.scad
+│ └── docs/
+│ └── installed.jpg
+└── geeekpi_v25_FINAL_candidate_package/ earlier candidate, superseded by v1
```
+Paths below are relative to `geeekpi_rack_adapter_release_v1/`.
+
## Key dimensions
| Feature | Dimension |
@@ -70,7 +78,8 @@ Good bed leveling matters a lot for these parts because they are broad and very
## SendCutSend / metal version
-The `sendcutsend/` directory contains separate left and right DXF files.
+The `geeekpi_rack_adapter_release_v1/sendcutsend/` directory contains separate
+left and right DXF files.
A good configuration for the metal adapters is:
@@ -98,7 +107,8 @@ Depending on the rack, display, and door clearance, small spacers or standoffs b
## Editing the design
-The editable OpenSCAD source is in `source/`.
+The editable OpenSCAD source is in
+`geeekpi_rack_adapter_release_v1/source/`.
To generate a side, change:
@@ -108,7 +118,8 @@ side = "left";
to either `"left"` or `"right"`, then render/export the STL.
-The DXF files in `sendcutsend/` are the intended files for sheet-metal cutting.
+The DXF files in `geeekpi_rack_adapter_release_v1/sendcutsend/` are the intended
+files for sheet-metal cutting.
## Notes
diff --git a/config.env b/config.env
deleted file mode 100644
index 58afafb..0000000
--- a/config.env
+++ /dev/null
@@ -1,91 +0,0 @@
-# socktop-swipe configuration.
-#
-# Installed to /usr/local/etc/socktop-swipe.env. Every script reads that file if
-# it exists, so edit it there on the display host -- not this copy in the repo.
-# After editing, restart the gesture daemon and rebuild the session (see README).
-#
-# Values use ${VAR:-default} so an environment variable of the same name always
-# wins over this file. That keeps the file editable in the obvious way while
-# still allowing one-off overrides, e.g. SOCKTOP_SESSION=test socktop-rack
-
-# ---------------------------------------------------------------------------
-# What to monitor
-# ---------------------------------------------------------------------------
-
-# What to show, as GROUPS separated by ";". Each group is one screen of the
-# carousel: a tiled overview of its hosts, then each host zoomed full-screen in
-# turn, then on to the next group. Host names are socktop profile names from
-# ~/.config/socktop/profiles.json.
-#
-# overview(A) -> A1 -> A2 -> ... -> overview(B) -> B1 -> B2 -> ...
-#
-# An optional "@layout" token in a group sets its tmux overview layout
-# (tiled, even-horizontal, even-vertical, main-horizontal, main-vertical).
-# Default is tiled: 4 hosts become quarters, 2 become side-by-side.
-# "@even-vertical" stacks the panes one above the other instead.
-#
-# The example below is the rack display: the four Pis quartered and zoomable,
-# then the Orange Pi and the Debian box stacked and zoomable.
-SOCKTOP_GROUPS="${SOCKTOP_GROUPS:-rpi-master rpi-worker-1 rpi-worker-2 rpi-worker-3 ; orangepi trixie @even-vertical}"
-
-# Legacy single-group form. Used only when SOCKTOP_GROUPS is empty.
-SOCKTOP_HOSTS="${SOCKTOP_HOSTS:-}"
-
-# Optional "aux" screen: a different TUI reached by swiping DOWN from any
-# carousel screen; swiping UP returns to exactly the screen you left. It runs in
-# its own tmux window, kept alive like the socktop panes, so switching is
-# instant. Leave empty to disable. Use full paths -- i3 has no ~/.cargo/bin.
-SOCKTOP_AUX_CMD="${SOCKTOP_AUX_CMD:-$HOME/.cargo/bin/uptime-kuma-status https://status.wittyoneoff.com/status/wittyoneoff}"
-
-# tmux session name. Change only if it collides with something else.
-SOCKTOP_SESSION="${SOCKTOP_SESSION:-socktop4}"
-
-# Absolute path to the socktop binary. i3 and non-login shells do not have
-# ~/.cargo/bin on PATH, so a full path is safer than relying on lookup.
-SOCKTOP_BIN="${SOCKTOP_BIN:-$HOME/.cargo/bin/socktop}"
-
-# ---------------------------------------------------------------------------
-# Touch panel
-# ---------------------------------------------------------------------------
-
-# The touchscreen event device. ALWAYS prefer a /dev/input/by-id/ symlink --
-# /dev/input/eventN numbers get reshuffled on reboot or USB re-enumeration.
-# Find yours with: tools/find-device.sh
-TOUCH_DEV="${TOUCH_DEV:-/dev/input/by-id/usb-ILITEK_ILITEK-TOUCH-event-if00}"
-
-# The touch panel's own resolution -- NOT the X screen size. lisgd otherwise
-# asks X, which reports the full root window across all monitors (e.g. 2304x720
-# with a second display attached) and skews its edge and distance maths.
-SCREEN_W="${SCREEN_W:-1024}"
-SCREEN_H="${SCREEN_H:-600}"
-
-# ---------------------------------------------------------------------------
-# Gesture tuning
-# ---------------------------------------------------------------------------
-
-# Pixels of travel before a drag counts as a swipe. Too low and stray contact
-# triggers it; too high and normal swipes are ignored. Measured on the ILITEK
-# panel: real swipes land at 150-260px, accidental ones under 70px.
-SWIPE_THRESHOLD="${SWIPE_THRESHOLD:-80}"
-
-# Degrees of leniency off the axis, max 45. Real finger swipes came in 1-25
-# degrees off true horizontal, so 30 leaves comfortable margin.
-SWIPE_LENIENCY="${SWIPE_LENIENCY:-30}"
-
-# Contact counts to accept for one logical swipe.
-#
-# THIS IS THE SETTING THAT MOST OFTEN NEEDS CHANGING ON NEW HARDWARE. Many
-# multipoint panels report 2 or even 3 contacts for what is physically a
-# one-finger swipe (palm, or ghost contacts). If gestures are recognised but
-# never fire, run tools/diag.sh and compare Cfg(f=N) against Evt(f=N).
-FINGER_COUNTS="${FINGER_COUNTS:-1 2 3}"
-
-# Swipe directions. RL = right-to-left = zoom in; LR = left-to-right = zoom out.
-# Swap these two values to reverse the direction of travel.
-GESTURE_IN="${GESTURE_IN:-RL}"
-GESTURE_OUT="${GESTURE_OUT:-LR}"
-
-# Aux screen swipes. UD = up-to-down (swipe down) shows it; DU = down-to-up
-# (swipe up) returns. Only bound when SOCKTOP_AUX_CMD is set.
-GESTURE_AUX_IN="${GESTURE_AUX_IN:-UD}"
-GESTURE_AUX_OUT="${GESTURE_AUX_OUT:-DU}"
diff --git a/install.sh b/install.sh
index 6a95197..f90f37f 100755
--- a/install.sh
+++ b/install.sh
@@ -1,238 +1,439 @@
#!/bin/sh
# socktop-swipe installer.
#
-# ./install.sh scripts + config + lisgd (build if missing)
-# ./install.sh --xignore ...and tell X to ignore the touch panel
-# ./install.sh --i3 ...and add i3 autostart lines
-# ./install.sh --noblank ...and stop the screen blanking
-# ./install.sh --autologin ...and make lightdm log this user straight into i3
-# ./install.sh --xignore --i3 --noblank --autologin full wall-display setup
+# curl -fsSL https://gt.wittyoneoff.com/jason/socktop-swipe/raw/branch/main/install.sh | sh
#
-# Safe to re-run: an existing config file is never overwritten, and the i3 edit
-# is skipped if already present.
+# or, from a checkout:
+#
+# ./install.sh
+#
+# Everything is checked BEFORE anything is installed, and the full cost is
+# printed once. Nothing touches the system before you answer the summary.
+#
+# --yes accept every default without asking
+# --uninstall hand over to uninstall.sh
+# --prefix DIR install to DIR/bin instead of /usr/local/bin
set -eu
-PREFIX=${PREFIX:-/usr/local}
-BIN="$PREFIX/bin"
-ETC="$PREFIX/etc"
-CONF="$ETC/socktop-swipe.env"
-XCONF=/etc/X11/xorg.conf.d/99-ignore-touch-socktop-swipe.conf
-BCONF=/etc/X11/xorg.conf.d/10-no-blanking-socktop-swipe.conf
-LCONF=/etc/lightdm/lightdm.conf.d/50-autologin-socktop-swipe.conf
-SRC="$HOME/src/lisgd"
+REPO=https://gt.wittyoneoff.com/jason/socktop-swipe
+PREFIX=/usr/local
+ASSUME_YES=no
+SRC=""
-here=$(cd "$(dirname "$0")" && pwd)
-do_xignore=no
-do_i3=no
-do_noblank=no
-do_autologin=no
for a in "$@"; do
case "$a" in
- --xignore) do_xignore=yes ;;
- --i3) do_i3=yes ;;
- --noblank) do_noblank=yes ;;
- --autologin) do_autologin=yes ;;
+ --yes | -y) ASSUME_YES=yes ;;
+ --prefix=*) PREFIX=${a#--prefix=} ;;
+ --uninstall) exec sh -c "$(dirname "$0")/uninstall.sh" ;;
+ -h | --help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $a" >&2; exit 2 ;;
esac
done
-# --- lisgd ------------------------------------------------------------------
-# libinput deliberately emits gesture events only for touchpads, never for
-# touchscreens, so libinput-gestures and friends cannot work here. lisgd reads
-# raw touch events and synthesises the swipes itself.
-if command -v lisgd >/dev/null 2>&1; then
- echo "==> lisgd already installed: $(command -v lisgd)"
-else
- echo "==> building lisgd"
- if command -v apt-get >/dev/null 2>&1; then
- sudo apt-get update
- sudo apt-get install -y build-essential pkg-config git libinput-dev libx11-dev
- else
- echo "!! Non-Debian system: ensure a C toolchain, libinput and libX11 headers"
- echo "!! are present, then re-run."
+BIN="$PREFIX/bin"
+CONF_DIR="$HOME/.config/socktop-swipe"
+CONF="$CONF_DIR/config.yaml"
+UDEV=/etc/udev/rules.d/70-socktop-swipe.rules
+XBLANK=/etc/X11/xorg.conf.d/10-no-blanking-socktop-swipe.conf
+LIGHTDM=/etc/lightdm/lightdm.conf.d/50-autologin-socktop-swipe.conf
+
+# --------------------------------------------------------------------------
+# Asking
+# --------------------------------------------------------------------------
+# Piping into sh leaves stdin holding the SCRIPT, not a keyboard, so prompts
+# must read the terminal directly. With no terminal at all we take defaults and
+# say so rather than silently guessing.
+if [ -r /dev/tty ] && [ -t 1 ]; then HAVE_TTY=yes; else HAVE_TTY=no; fi
+
+say() { printf '%s\n' "$*"; }
+step() { printf '\n==> %s\n' "$*"; }
+warn() { printf ' !! %s\n' "$*" >&2; }
+
+# ask "question" default(y|n) -> 0 for yes
+ask() {
+ _q=$1
+ _d=$2
+ if [ "$ASSUME_YES" = yes ] || [ "$HAVE_TTY" = no ]; then
+ [ "$_d" = y ] && return 0 || return 1
fi
+ [ "$_d" = y ] && _hint="[Y/n]" || _hint="[y/N]"
+ while :; do
+ printf ' %s %s ' "$_q" "$_hint" >/dev/tty
+ read -r _a echoes the answer
+askval() {
+ if [ "$ASSUME_YES" = yes ] || [ "$HAVE_TTY" = no ]; then
+ printf '%s' "$2"
+ return
+ fi
+ printf ' %s [%s] ' "$1" "$2" >/dev/tty
+ read -r _a /dev/null 2>&1; }
+
+# --------------------------------------------------------------------------
+# Preflight
+# --------------------------------------------------------------------------
+say "socktop-swipe installer"
+say "======================="
+
+OS_NAME=$(. /etc/os-release 2>/dev/null && printf '%s' "${PRETTY_NAME:-unknown}" || true)
+[ -n "$OS_NAME" ] || OS_NAME=unknown
+ARCH=$(uname -m)
+
+PKG=""
+PKG_INSTALL=""
+for p in apt-get dnf pacman zypper apk; do
+ if have $p; then
+ PKG=$p
+ break
+ fi
+done
+case "$PKG" in
+apt-get) PKG_INSTALL="sudo apt-get install -y" ;;
+dnf) PKG_INSTALL="sudo dnf install -y" ;;
+pacman) PKG_INSTALL="sudo pacman -S --needed --noconfirm" ;;
+zypper) PKG_INSTALL="sudo zypper install -y" ;;
+apk) PKG_INSTALL="sudo apk add" ;;
+esac
+
+# Display manager and window manager, for the autostart and autologin steps.
+# NB: written as `if`, not `[ x ] && y`. Under `set -e` an AND-OR list that
+# ends up false aborts the script, so a box with no lightdm would exit here.
+DM=none
+if [ -d /etc/lightdm ]; then DM=lightdm
+elif [ -d /etc/gdm3 ] || [ -d /etc/gdm ]; then DM=gdm
+elif [ -f /etc/sddm.conf ] || [ -d /etc/sddm.conf.d ]; then DM=sddm
+fi
+
+WM=none
+if have i3; then WM=i3; fi
+
+TERMINAL=""
+for t in alacritty foot kitty xterm; do
+ if have $t; then
+ TERMINAL=$t
+ break
+ fi
+done
+
+LOCKER=""
+for l in xss-lock light-locker xscreensaver gnome-screensaver; do
+ if have $l; then
+ LOCKER="$LOCKER $l"
+ fi
+done
+
+# Space for the toolchain and the build. Both land under $HOME unless
+# CARGO_HOME says otherwise, which is exactly the 8GB-of-eMMC case.
+avail_mb() { df -Pm "$1" 2>/dev/null | awk 'NR==2 {print $4}'; }
+HOME_FREE=$(avail_mb "$HOME")
+PREFIX_FREE=$(avail_mb "$(dirname "$PREFIX")")
+
+step "Found"
+say " system $OS_NAME ($ARCH)"
+say " packages ${PKG:-none detected}"
+say " display mgr $DM"
+say " window mgr $WM"
+say " terminal ${TERMINAL:-none found}"
+say " tmux $(tmux -V 2>/dev/null || echo 'NOT INSTALLED')"
+# Not `cargo --version | cut ... || echo`: the pipeline's status is cut's, and
+# cut succeeds on empty input, so the fallback would never fire.
+if have cargo; then
+ say " cargo $(cargo --version 2>/dev/null | cut -d' ' -f1-2)"
+else
+ say " cargo NOT INSTALLED"
+fi
+say " git $(git --version 2>/dev/null || echo 'NOT INSTALLED')"
+say " free space ${HOME_FREE:-?} MB in \$HOME, ${PREFIX_FREE:-?} MB on $PREFIX"
+if [ -n "$LOCKER" ]; then say " screen locker $LOCKER"; fi
+
+# What we are about to need.
+NEED_PKGS=""
+NEED_RUSTUP=no
+COST_MB=10
+
+if ! have tmux; then NEED_PKGS="$NEED_PKGS tmux"; fi
+if ! have git; then NEED_PKGS="$NEED_PKGS git"; fi
+if ! have cargo; then
+ NEED_RUSTUP=yes
+ COST_MB=$((COST_MB + 1200))
+fi
+# The build itself: a debug-free release build of this crate and its deps.
+COST_MB=$((COST_MB + 600))
+
+if [ -z "$TERMINAL" ]; then
+ warn "no terminal emulator found. socktop-swipe can attach in an existing"
+ warn "terminal, but unattended autostart needs one. alacritty is the tested choice."
+fi
+if [ -z "$PKG" ]; then
+ warn "no supported package manager. Nothing will be installed for you;"
+ warn "make sure tmux, git and a Rust toolchain are present."
+fi
+if [ -n "$HOME_FREE" ] && [ "$HOME_FREE" -lt "$COST_MB" ]; then
+ warn "only ${HOME_FREE} MB free in \$HOME but about ${COST_MB} MB is needed."
+ warn "A source build will not fit. Free space first, or set CARGO_HOME and"
+ warn "CARGO_TARGET_DIR to somewhere with room."
+fi
+
+step "This will"
+if [ -n "$NEED_PKGS" ]; then say " install packages:$NEED_PKGS"; fi
+if [ "$NEED_RUSTUP" = yes ]; then say " install the Rust toolchain via rustup (~1.2 GB)"; fi
+say " build socktop-swipe from source (~600 MB of build artifacts)"
+say " install the binary to $BIN"
+say " write a starter config to $CONF"
+say " ...then ask about the touch device, autostart, autologin and blanking."
+say ""
+say " About ${COST_MB} MB of disk. Nothing has been changed yet."
+say ""
+
+if [ "$HAVE_TTY" = no ]; then
+ say "No terminal available, so every question takes its default."
+ say "Run the script directly for the interactive version:"
+ say " curl -fsSLO $REPO/raw/branch/main/install.sh && sh install.sh"
+ say ""
+fi
+
+ask "Continue?" y || { say "Nothing done."; exit 0; }
+
+# --------------------------------------------------------------------------
+# Dependencies
+# --------------------------------------------------------------------------
+if [ -n "$NEED_PKGS" ]; then
+ step "Installing:$NEED_PKGS"
+ if [ -z "$PKG_INSTALL" ]; then
+ warn "no package manager -- install$NEED_PKGS yourself and re-run."
+ exit 1
+ fi
+ if [ "$PKG" = apt-get ]; then sudo apt-get update; fi
+ # shellcheck disable=SC2086
+ $PKG_INSTALL $NEED_PKGS
+fi
+
+if [ "$NEED_RUSTUP" = yes ]; then
+ step "Installing the Rust toolchain"
+ if ask "Install rustup now?" y; then
+ curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --no-modify-path
+ # shellcheck disable=SC1091
+ . "$HOME/.cargo/env"
+ else
+ say "Install Rust yourself, then re-run this script."
+ exit 1
+ fi
+fi
+if ! have cargo; then warn "cargo still not on PATH -- open a new shell and re-run."; exit 1; fi
+
+# --------------------------------------------------------------------------
+# Source
+# --------------------------------------------------------------------------
+here=$(CDPATH='' cd -- "$(dirname -- "$0")" 2>/dev/null && pwd || echo "")
+if [ -n "$here" ] && [ -f "$here/Cargo.toml" ]; then
+ SRC=$here
+ step "Building from this checkout: $SRC"
+else
+ SRC="$HOME/.local/src/socktop-swipe"
+ step "Fetching the source to $SRC"
mkdir -p "$(dirname "$SRC")"
- if [ -d "$SRC/.git" ]; then git -C "$SRC" pull --ff-only; else
- git clone https://git.sr.ht/~mil/lisgd "$SRC"
+ if [ -d "$SRC/.git" ]; then
+ git -C "$SRC" pull --ff-only
+ else
+ git clone "$REPO" "$SRC"
fi
- # WITHOUT_WAYLAND: lisgd builds both backends by default and would otherwise
- # need libwayland-dev for code that never runs on an X11-only host.
- make -C "$SRC" WITHOUT_WAYLAND=1
- sudo make -C "$SRC" install WITHOUT_WAYLAND=1 PREFIX="$PREFIX"
fi
-# --- scripts and config -----------------------------------------------------
-echo "==> installing scripts to $BIN"
-sudo install -d "$BIN" "$ETC"
-sudo install -m 755 "$here/socktop-rack" "$here/socktop-swipe" "$here/socktop-gestures" "$BIN/"
+step "Building (this is the slow part on a low-power box)"
+( cd "$SRC" && cargo build --release )
+sudo install -d "$BIN"
+sudo install -m 755 "$SRC/target/release/socktop-swipe" "$BIN/socktop-swipe"
+say " installed $BIN/socktop-swipe"
-if [ -e "$CONF" ]; then
- echo "==> keeping existing $CONF (not overwritten)"
+# --------------------------------------------------------------------------
+# Touch device
+# --------------------------------------------------------------------------
+step "Touch panel"
+DEVICE=""
+# Prefer the by-id path: eventN numbers get reshuffled on reboot.
+for d in /dev/input/by-id/*event*; do
+ [ -e "$d" ] || continue
+ if udevadm info --query=property --name="$d" 2>/dev/null | grep -q '^ID_INPUT_TOUCHSCREEN=1'; then
+ say " found touchscreen: $d"
+ if [ -z "$DEVICE" ]; then DEVICE=$d; fi
+ fi
+done
+if [ -z "$DEVICE" ]; then
+ warn "no touchscreen found under /dev/input/by-id/."
+ warn "If the panel is not plugged in yet, that is fine -- set touch.device"
+ warn "in the config later. 'socktop-swipe doctor --list' will show it."
+ DEVICE=/dev/input/by-id/CHANGE-ME
else
- sudo install -m 644 "$here/config.env" "$CONF"
- echo "==> installed default config to $CONF"
- echo " EDIT IT before first run if your hardware differs."
+ DEVICE=$(askval "Use which device?" "$DEVICE")
fi
-# --- input group ------------------------------------------------------------
-if id -nG | tr ' ' '\n' | grep -qx input; then
- echo "==> already in the 'input' group"
- relogin=no
-else
- echo "==> adding $(id -un) to the 'input' group (lisgd reads /dev/input directly)"
+step "Device access"
+say " Reading touch events needs permission on the device."
+say " A udev rule grants it for touchscreens only and takes effect at once."
+say " The 'input' group grants access to every input device, keyboard included,"
+say " and needs a full logout."
+RELOGIN=no
+if ask "Install the udev rule?" y; then
+ sudo install -m 644 "$SRC/packaging/70-socktop-swipe.rules" "$UDEV"
+ sudo udevadm control --reload
+ sudo udevadm trigger --subsystem-match=input
+ say " wrote $UDEV"
+ if ! id -nG | tr ' ' '\n' | grep -qx input; then
+ warn "the rule uses GROUP=input and you are not in it."
+ if ask "Add $(id -un) to the 'input' group as well?" y; then
+ sudo usermod -aG input "$(id -un)"
+ RELOGIN=yes
+ fi
+ fi
+elif ask "Add $(id -un) to the 'input' group instead?" n; then
sudo usermod -aG input "$(id -un)"
- relogin=yes
+ RELOGIN=yes
fi
-# --- optional: make X ignore the panel --------------------------------------
-if [ "$do_xignore" = yes ]; then
- # shellcheck disable=SC1090
- . "$CONF"
- product=${XIGNORE_MATCH:-ILITEK}
- echo "==> telling X to ignore touch devices matching '$product'"
- sudo mkdir -p /etc/X11/xorg.conf.d
- sudo tee "$XCONF" >/dev/null <"$CONF"
+ say " wrote $CONF"
+ say " EDIT IT: the example screens are the author's rack, not yours."
fi
-# --- optional: stop the screen blanking -------------------------------------
-if [ "$do_noblank" = yes ]; then
- echo "==> disabling screen blanking and DPMS"
- # Two layers on purpose:
- # 1. An Xorg ServerFlags snippet, so it applies to every X session including
- # the display manager greeter, and survives reboots.
- # 2. An xset call at i3 startup, because a session or DM can re-enable the
- # screensaver after X starts.
- # Note `xset` alone is NOT enough, and must run as the session user -- running
- # it under sudo targets root's X connection and silently does nothing.
- sudo mkdir -p /etc/X11/xorg.conf.d
- sudo tee "$BCONF" >/dev/null <<'EOF'
-# Installed by socktop-swipe. This is a wall display: it must never blank.
-Section "ServerFlags"
- Option "BlankTime" "0"
- Option "StandbyTime" "0"
- Option "SuspendTime" "0"
- Option "OffTime" "0"
-EndSection
-EOF
- echo " wrote $BCONF"
+# --------------------------------------------------------------------------
+# Optional monitors
+# --------------------------------------------------------------------------
+step "Monitor programs (all optional -- socktop-swipe runs whatever you configure)"
+if ! have socktop && ask "Install socktop (the point of the project)?" y; then
+ cargo install socktop
+fi
+if ! have uptime-kuma-status && ask "Install uptime-kuma-status?" n; then
+ cargo install uptime-kuma-status
+fi
+if ! have unifly && ask "Build unifly (UniFi TUI) from source?" n; then
+ U="$HOME/.local/src/unifly"
+ mkdir -p "$(dirname "$U")"
+ if [ -d "$U/.git" ]; then git -C "$U" pull --ff-only; else
+ git clone https://github.com/jasonwitty/unifly "$U"
+ fi
+ ( cd "$U" && cargo build --profile release-small -p unifly )
+ say " built $U/target/release-small/unifly"
+ say " put that path under 'binaries: unifly:' in $CONF"
+fi
- # Apply immediately too, if a session is available.
- if [ -n "${DISPLAY:-}" ]; then
+# --------------------------------------------------------------------------
+# Unattended operation
+# --------------------------------------------------------------------------
+step "Wall-display behaviour"
+
+if ask "Stop the screen blanking, sleeping and locking?" y; then
+ sudo mkdir -p /etc/X11/xorg.conf.d
+ sudo tee "$XBLANK" >/dev/null <<-'EOF'
+ # Installed by socktop-swipe. This is a wall display: it must never blank.
+ Section "ServerFlags"
+ Option "BlankTime" "0"
+ Option "StandbyTime" "0"
+ Option "SuspendTime" "0"
+ Option "OffTime" "0"
+ EndSection
+ EOF
+ say " wrote $XBLANK"
+ # The Xorg snippet covers every session including the greeter and survives
+ # reboots; xset covers a session that re-enables blanking after X starts.
+ # Both are needed, and xset MUST run as the session user -- under sudo it
+ # targets root's X connection and silently does nothing.
+ if [ -n "${DISPLAY:-}" ] && have xset; then
xset s off s noblank -dpms 2>/dev/null || true
- echo " applied to the running session"
+ say " applied to the running session"
fi
-
- i3conf="$HOME/.config/i3/config"
- if [ -e "$i3conf" ] && ! grep -q "xset s off" "$i3conf"; then
- bak="$i3conf.bak-$(date +%Y%m%d-%H%M%S)"
- cp "$i3conf" "$bak"
- printf '\n# Wall display: never blank (socktop-swipe --noblank)\nexec --no-startup-id xset s off s noblank -dpms\n' >>"$i3conf"
- if i3 -C -c "$i3conf" >/dev/null 2>&1; then
- echo " added xset to i3 autostart (backup: $bak)"
- else
- cp "$bak" "$i3conf"
- echo "!! i3 rejected the config; restored $bak" >&2
- fi
- fi
- relogin=yes
+ for l in $LOCKER; do
+ warn "$l is installed and may still lock the screen. Disable it in your"
+ warn "session's autostart."
+ done
fi
-# --- optional: lightdm autologin --------------------------------------------
-if [ "$do_autologin" = yes ]; then
- # A wall display must come back by itself after a power cut. Without this a
- # reboot leaves the panel at the greeter with nothing autostarted.
- # SECURITY: anyone with physical access to the screen gets this user's
- # session. Only use it on a display whose user account is nothing but the
- # dashboard. Debian's lightdm-autologin PAM stack needs no extra group.
- if [ ! -d /etc/lightdm ]; then
- echo "!! /etc/lightdm not found; skipping autologin (not lightdm?)" >&2
- else
- session=i3
- [ -e /usr/share/xsessions/$session.desktop ] ||
- echo "!! /usr/share/xsessions/$session.desktop missing; check autologin-session in $LCONF" >&2
- echo "==> enabling lightdm autologin for $(id -un) into '$session'"
+if [ "$WM" = i3 ] && [ -e "$HOME/.config/i3/config" ]; then
+ if ask "Add the i3 autostart lines?" y; then
+ i3conf="$HOME/.config/i3/config"
+ if grep -q socktop-swipe "$i3conf"; then
+ say " already present; leaving it alone"
+ else
+ bak="$i3conf.bak-$(date +%Y%m%d-%H%M%S)"
+ cp "$i3conf" "$bak"
+ {
+ printf '\n# --- socktop-swipe ---\n'
+ printf '# One process: builds the session, opens it in a terminal and reads the panel.\n'
+ printf 'exec --no-startup-id %s/socktop-swipe run\n' "$BIN"
+ printf '# Never blank (the Xorg snippet covers reboots; this covers this session).\n'
+ printf 'exec --no-startup-id xset s off s noblank -dpms\n'
+ if have unclutter; then
+ printf '# With the panel grabbed, nothing ever moves the pointer, so hide it.\n'
+ printf 'exec --no-startup-id unclutter --timeout 1 --ignore-scrolling\n'
+ fi
+ } >>"$i3conf"
+ if i3 -C -c "$i3conf" >/dev/null 2>&1; then
+ say " added the autostart (backup: $bak)"
+ else
+ cp "$bak" "$i3conf"
+ warn "i3 rejected the config; restored $bak"
+ fi
+ fi
+ fi
+elif [ "$DM" != none ] || [ "$WM" != none ]; then
+ say " no i3 config found. For a systemd user session instead:"
+ say " cp $SRC/packaging/socktop-swipe.service ~/.config/systemd/user/"
+ say " systemctl --user enable --now socktop-swipe"
+fi
+
+if [ "$DM" = lightdm ]; then
+ say ""
+ say " Autologin makes the display come back by itself after a power cut."
+ say " ANYONE WITH PHYSICAL ACCESS TO THE PANEL GETS THIS USER'S SESSION."
+ say " Only do this where the account is nothing but the dashboard."
+ if ask "Log $(id -un) straight into $WM at boot?" n; then
sudo mkdir -p /etc/lightdm/lightdm.conf.d
- sudo tee "$LCONF" >/dev/null </dev/null <<-EOF
+ # Installed by socktop-swipe. Delete this file to restore the login prompt.
+ [Seat:*]
+ autologin-user=$(id -un)
+ autologin-user-timeout=0
+ autologin-session=$WM
+ EOF
+ say " wrote $LIGHTDM (takes effect at next boot)"
fi
+elif [ "$DM" != none ]; then
+ say " Autologin is only automated for lightdm; yours is $DM. Configure it there"
+ say " if the display must come back unattended after a power cut."
fi
-# --- optional: i3 autostart -------------------------------------------------
-if [ "$do_i3" = yes ]; then
- i3conf="$HOME/.config/i3/config"
- if ! command -v unclutter >/dev/null 2>&1 && command -v apt-get >/dev/null 2>&1; then
- echo "==> installing unclutter-xfixes (hides the idle pointer on the wall display)"
- sudo apt-get install -y unclutter-xfixes || echo "!! unclutter-xfixes not installed; the pointer will stay visible" >&2
- fi
- if [ ! -e "$i3conf" ]; then
- echo "!! $i3conf not found; skipping i3 autostart" >&2
- elif grep -q "socktop-gestures" "$i3conf"; then
- echo "==> i3 autostart already present; leaving it alone"
- else
- bak="$i3conf.bak-$(date +%Y%m%d-%H%M%S)"
- cp "$i3conf" "$bak"
- cat >>"$i3conf" </dev/null 2>&1; then
- echo "==> added i3 autostart (backup: $bak)"
- echo " NOTE: set \$TERMINAL in your i3 config, or edit those lines"
- echo " to your terminal's full path."
- else
- cp "$bak" "$i3conf"
- echo "!! i3 rejected the config; restored $bak" >&2
- i3 -C -c "$i3conf" || true
- fi
- fi
-fi
-
-echo
-echo "=============================================================="
-echo "Installed."
-if [ "$relogin" = yes ]; then
- echo "LOG OUT and back in before use (group and/or X changes)."
-fi
+# --------------------------------------------------------------------------
+say ""
+say "=============================================================="
+say "Installed."
+if [ "$RELOGIN" = yes ]; then say "LOG OUT AND BACK IN before use (group membership changed)."; fi
cat <; s=$?; printf '\n[%s exited: status %s]\n' "$s"; while :; do sleep 86400; done
+```
+
+The pane outlives the command, and the failure is visible *on the wall display*
+with its exit status — which is what a wall display is for. tmux already runs
+each command under `sh`, so this costs one shell that stays resident per pane
+rather than one that execs away.
+
+## Why `at: "0x0"` must be quoted
+
+YAML reads an unquoted `0x0` as the hexadecimal number 0. The nasty part is that
+`1x0` is *not* valid hex and arrives as a string, so only the row-0 entries break
+and the failure looks arbitrary. Deserialization catches the integer case and
+prints the fix rather than a type error.
+
+## Multiplexer: tmux, with zellij shelved
+
+`src/session/` is a `Multiplexer` trait with a tmux implementation behind it, so
+the question is cheap to reopen. It was shelved rather than rejected, for reasons
+worth recording so it is not re-litigated:
+
+1. **"Available as a crate" is not an embedding API.** `zellij-server`,
+ `zellij-client` and `zellij-utils` are published, but they are workspace
+ crates for the binary, not a supported library surface. Realistic integration
+ is the CLI or a WASM plugin — so it would still be a subprocess driven over a
+ CLI, exactly like tmux.
+2. **Addressability is what we depend on.** The grid needs *"focus cell 0x1,
+ sub-screen 3, zoomed"* as one deterministic call. tmux gives that directly
+ (`select-pane -t %12`, `resize-pane -Z`). zellij's CLI is direction-oriented
+ (`move-focus left`), which would mean counting relative moves and tracking
+ state we cannot verify.
+3. **Footprint runs the wrong way.** zellij is a client/server async multiplexer
+ with a wasmtime plugin runtime, heavier at idle than tmux's C implementation.
+ On 2 GB boxes that is the binding constraint, and `apt install tmux` versus a
+ zellij source build on an Atom is a much worse story for the install guide.
+
+The one idea worth keeping on the shelf: running the navigation state machine as
+a **WASM plugin inside zellij** via `zellij-tile`, which would give real event
+subscriptions instead of driving a CLI. Revisit only if tmux becomes the
+bottleneck.
diff --git a/notes/HARDWARE-NOTES.md b/notes/HARDWARE-NOTES.md
new file mode 100644
index 0000000..99db9b4
--- /dev/null
+++ b/notes/HARDWARE-NOTES.md
@@ -0,0 +1,105 @@
+# Hardware notes
+
+Things about specific hardware that cost time to work out. Kept out of the README
+because they are not install steps.
+
+## Screen size and rack size are different measurements
+
+Three numbers get confused because they all describe "how big":
+
+- **9 inch** — the GeeekPi touchscreen itself, 1280x720.
+- **10 inch** — the *mini-rack* standard its bracket is made for.
+- **19 inch** — the standard equipment rack, which is what `cad/` adapts it to.
+
+So "a 9-inch screen on a 10-inch mount, adapted to a 19-inch rack" is three
+correct numbers, not a contradiction. The README uses the screen size when
+talking about the panel and the rack size when talking about the adapter.
+
+## The ILITEK panel reports 2-3 contacts for one finger
+
+A physically one-finger swipe arrives as two, sometimes three, simultaneous
+contacts. In v1 this was the single most expensive failure: lisgd detected the
+direction correctly every time and then rejected it on
+`Cfg(f=1) <=> Evt(f=2)`, which reads as noise unless you know what it means.
+
+`touch.fingers: [1, 2, 3]` accepts all three. `socktop-swipe doctor` now reports
+it in English, and the travel is *averaged* across contacts rather than summed —
+otherwise a ghost-contact panel looks like it swiped three times as far.
+
+## Phantom display outputs (LattePanda DSI-1)
+
+The LattePanda's onboard DSI header shows up as `DSI-1 connected 1024x600` with
+**zero EDID bytes** (`xrandr` reports `0mm x 0mm`), and X happily puts workspace 1
+on it. Everything then looks healthy over ssh — the window is fullscreen on the
+primary output — while the real panel shows an empty workspace.
+
+```sh
+xrandr | grep ' connected' # a real panel has physical mm; phantoms say 0mm x 0mm
+for c in /sys/class/drm/card0-*; do echo "$c $(cat "$c"/status) edid=$(wc -c <"$c"/edid)"; done
+```
+
+Fix, adjusting the names:
+
+```sh
+sudo tee /etc/X11/xorg.conf.d/20-outputs-socktop-swipe.conf >/dev/null <<'EOF'
+Section "Monitor"
+ Identifier "DSI-1"
+ Option "Ignore" "true"
+EndSection
+Section "Monitor"
+ Identifier "HDMI-2"
+ Option "Primary" "true"
+EndSection
+EOF
+```
+
+Then set `touch.width`/`touch.height` to the real panel's mode. On the LattePanda
+that is **1280x720**, not the 1024x600 the phantom claims.
+
+## `sudo xset` silently does nothing
+
+`xset` talks to the X connection of the user running it, so `sudo xset s off`
+targets *root's* X connection and succeeds while changing nothing for your
+session. A `sudo bash noblank.sh` wrapper looks like it worked and blanking stays
+armed. It must run as the session user.
+
+Also: `xset` is per-session and dies on reboot. The durable fix is the Xorg
+`ServerFlags` snippet with all four timeouts at 0 — which also covers the display
+manager's greeter — *plus* an `xset` line in the session autostart, because a
+session can re-enable the screensaver after X starts. Both, not either.
+
+Check the real state:
+
+```sh
+xset q | grep -A1 -E 'Screen Saver|DPMS' # want timeout 0, prefer blanking no, DPMS Disabled
+```
+
+## Panel resolution is not the X screen size
+
+With a second display attached, X reports the combined root window (e.g.
+2304x720). `touch.width`/`touch.height` must describe the touch panel alone or
+the edge and distance maths is scaled to the wrong thing. v1 hit this because
+lisgd asks X when not told; v2 always requires the values in the config.
+
+## LattePanda specifics
+
+Atom x5-Z8350 @ 1.44 GHz, 1.9 GB RAM, Cherry Trail Gen8 graphics, Debian 11,
+i3 on X11. Binaries in `~/.cargo/bin`, which is **not** on the PATH that i3 or a
+non-login ssh gives you — hence the `binaries:` block in the config.
+
+Memory is the binding constraint. Prefer the lightweight option and count
+processes; reach for tmux pane zoom rather than duplicate program instances. A
+Wayland compositor with native touch gestures is not an answer here: Hyprland is
+not packaged in Debian, so it means a source build on a 1.44 GHz Atom, and it
+would not help anyway — libinput emits no gesture events for touchscreens on any
+version, so a raw-touch reader is required regardless.
+
+## Wyse 3040
+
+Same silicon as the LattePanda (Atom x5-Z8350, Cherry Trail), so the stack is
+already validated. Differences: 8/16 GB soldered eMMC and no M.2, so a minimal
+Debian netinst and a careful eye on disk; DisplayPort out, so a DP-to-HDMI
+adapter for the panel; roughly 3-4 W idle, 101x101x28 mm, VESA holes.
+
+The eMMC is why the installer checks free space before starting a build: a Rust
+toolchain plus a target directory is around 1.8 GB.
diff --git a/notes/PLAN-v2.md b/notes/PLAN-v2.md
index 3d7d008..148f384 100644
--- a/notes/PLAN-v2.md
+++ b/notes/PLAN-v2.md
@@ -1,6 +1,8 @@
# socktop-swipe v2 — release plan
-Status: **draft for review**, 2026-09-09. Nothing implemented yet.
+Status: **implemented**, 2026-09-09. Milestones 1-7 are done on branch `v2-rust`;
+milestone 8 (the Wyse 3040 validation) is the remaining acceptance gate. Where the
+built thing differs from this plan, `notes/DESIGN.md` is authoritative.
v1 is three POSIX shell scripts driving lisgd and tmux. It works and is running on
the rack display today, but it hard-codes a single linear carousel of socktop hosts,
diff --git a/notes/TODO.md b/notes/TODO.md
new file mode 100644
index 0000000..3e10890
--- /dev/null
+++ b/notes/TODO.md
@@ -0,0 +1,31 @@
+# TODO
+
+## Before calling v2.0 done
+
+- [ ] **Wyse 3040 validation.** Debian minimal to working rack display,
+ following only the README. Every stumble is a README fix, then re-run.
+ This is the acceptance gate, not a nice-to-have.
+- [ ] Switch the LattePanda over from v1. Its v2 config path and session name
+ differ, so both can be installed side by side while testing.
+- [ ] Add the tested-hardware table to the README once the Wyse is done.
+- [ ] Decide on the position indicator (`indicator: true`). Implemented; keep or
+ cut based on whether it actually helps on the wall.
+
+## Open questions from the plan
+
+- **Snap tie-break.** From `0x2`, with cells at `-1x1` and `-1x3` both at
+ distance 1, the lower column wins. Only fires on the first entry into a row.
+- **Indicator.** Off by default. See above.
+
+## Later
+
+- Prebuilt binaries. Deliberately not in v2.0: source-only until the Wyse install
+ shows how bad a build on an Atom really is. `[package.metadata.deb]` is already
+ in `Cargo.toml`, so `cargo deb` is the cheap next step if it turns out to hurt.
+- `unifly` parameters. `site` and `controller` are accepted by the config and
+ passed as `--site` / `--controller`, but the fork does not implement them yet.
+ Verify against the flags it actually grows.
+- A second monitor type that needs a multi-pane sub-sequence would prove the
+ `Vec` model generalises. Nothing needs it yet.
+- SIGTERM handling to remove the control socket on exit. Not urgent: a stale
+ socket is detected and replaced on the next start, which is tested behaviour.
diff --git a/notes/V1-BASH.md b/notes/V1-BASH.md
new file mode 100644
index 0000000..de2c6aa
--- /dev/null
+++ b/notes/V1-BASH.md
@@ -0,0 +1,48 @@
+# What v1 was
+
+v1 was three POSIX shell scripts plus a `.env` file, installed to
+`/usr/local/bin`. Tagged **`v1.2`** — `git show v1.2` has the whole thing, and it
+still works.
+
+- `socktop-rack` built a tmux session: one window per group, one pane per host.
+- `socktop-swipe next|prev|down|up` walked a linear carousel, re-deriving its
+ position from tmux on every single invocation.
+- `socktop-gestures` wrapped [lisgd](https://git.sr.ht/~mil/lisgd) with the right
+ arguments and enforced a single instance.
+- `config.env` at `/usr/local/etc/socktop-swipe.env` held `SOCKTOP_GROUPS`,
+ `SOCKTOP_AUX_CMD`, `TOUCH_DEV` and the gesture tuning.
+
+It worked well for a year on the rack display. What made it worth replacing:
+
+- **The layout was hard-coded as one line.** Groups ran left to right and that
+ was the only shape available. The swipe-down "aux screen" existed because there
+ was nowhere else to put a second kind of monitor — a special case bolted on,
+ not a position in a layout.
+- **Every new monitor type was another special case.** `SOCKTOP_AUX_CMD` is a
+ single command with no type, no parameters and no room for a second one.
+- **The navigation logic could not be tested.** It lived in a shell script that
+ queried a live tmux server, so the only way to check a change was to install it
+ on the display and swipe.
+
+## Migration
+
+There is no converter — deliberately, for a project with one known deployment.
+The mapping is direct:
+
+| v1 | v2 |
+| --- | --- |
+| `SOCKTOP_GROUPS="a b ; c d @even-vertical"` | one `type: socktop` screen per group, at `"0x0"`, `"0x1"`, …, with `socktop_group` and `layout` |
+| `SOCKTOP_AUX_CMD="… uptime-kuma-status URL"` | a `type: uptime-kuma-status` screen at `"1x0"` |
+| `SOCKTOP_BIN` | `binaries: socktop:` |
+| `TOUCH_DEV` | `touch.device` |
+| `SCREEN_W` / `SCREEN_H` | `touch.width` / `touch.height` |
+| `SWIPE_THRESHOLD` / `SWIPE_LENIENCY` | `touch.threshold` / `touch.leniency` |
+| `FINGER_COUNTS="1 2 3"` | `touch.fingers: [1, 2, 3]` |
+| `GESTURE_IN` / `GESTURE_OUT` | `gestures.forward` / `gestures.back` |
+| `GESTURE_AUX_IN` / `GESTURE_AUX_OUT` | `gestures.down` / `gestures.up` |
+| the X `Option "Ignore"` rule | `touch.grab: true` |
+| `remain-on-exit` | the pane keep-alive wrapper (see DESIGN.md) |
+
+Run `./uninstall.sh` from a v1 checkout first, or remove
+`/usr/local/bin/socktop-{rack,swipe,gestures}` and
+`/usr/local/etc/socktop-swipe.env` by hand.
diff --git a/packaging/70-socktop-swipe.rules b/packaging/70-socktop-swipe.rules
new file mode 100644
index 0000000..bd79d5d
--- /dev/null
+++ b/packaging/70-socktop-swipe.rules
@@ -0,0 +1,13 @@
+# Installed by socktop-swipe.
+#
+# Reading touch events means reading /dev/input directly. The usual advice is
+# to join the 'input' group, but that grants access to EVERY input device --
+# including the keyboard, which is a keylogger's worth of access -- and it needs
+# a full logout to take effect.
+#
+# This grants access to touchscreens only, and applies the moment the device
+# appears. Change GROUP to a group the dashboard user is in if 'input' is not
+# suitable; the point is the narrower device match, not the group name.
+#
+# Reload with: sudo udevadm control --reload && sudo udevadm trigger
+SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_INPUT_TOUCHSCREEN}=="1", GROUP="input", MODE="0640"
diff --git a/packaging/socktop-swipe.service b/packaging/socktop-swipe.service
new file mode 100644
index 0000000..2ce722e
--- /dev/null
+++ b/packaging/socktop-swipe.service
@@ -0,0 +1,29 @@
+# systemd --user unit for the gesture daemon, as an alternative to starting
+# socktop-swipe from the window manager's autostart.
+#
+# cp packaging/socktop-swipe.service ~/.config/systemd/user/
+# systemctl --user enable --now socktop-swipe
+#
+# This runs the DAEMON only: it drives a session that something else built and
+# attached (see socktop-swipe build / attach). If you would rather have one unit
+# do everything, change ExecStart to `socktop-swipe run` and drop ExecStartPre --
+# but then the terminal is a child of the unit, and restarting the unit closes
+# the dashboard.
+[Unit]
+Description=socktop-swipe touchscreen gesture daemon
+Documentation=https://gt.wittyoneoff.com/jason/socktop-swipe
+After=graphical-session.target
+PartOf=graphical-session.target
+
+[Service]
+Type=simple
+# Only needed if `terminal:` is set and the terminal is an X client. The gesture
+# path itself reads evdev directly and needs no display connection at all.
+Environment=DISPLAY=:0
+ExecStartPre=/usr/local/bin/socktop-swipe build
+ExecStart=/usr/local/bin/socktop-swipe daemon
+Restart=always
+RestartSec=2
+
+[Install]
+WantedBy=graphical-session.target
diff --git a/socktop-gestures b/socktop-gestures
deleted file mode 100755
index bf4ed4b..0000000
--- a/socktop-gestures
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/bin/sh
-# Touch gesture daemon for the socktop display. Pass -v to log each detection.
-#
-# Single source of truth for the lisgd arguments -- referenced by the i3 autostart,
-# the systemd unit and tools/test-foreground.sh, so they cannot drift apart.
-#
-# IMPORTANT: run exactly ONE instance. lisgd does not grab the input device
-# exclusively, so two running copies make every swipe fire twice.
-set -eu
-
-for c in /usr/local/etc/socktop-swipe.env "$(dirname "$0")/config.env"; do
- [ -r "$c" ] && . "$c" && break
-done
-
-: "${TOUCH_DEV:?no TOUCH_DEV -- is the config installed?}"
-: "${SCREEN_W:=1024}"
-: "${SCREEN_H:=600}"
-: "${SWIPE_THRESHOLD:=80}"
-: "${SWIPE_LENIENCY:=30}"
-: "${FINGER_COUNTS:=1 2 3}"
-: "${GESTURE_IN:=RL}"
-: "${GESTURE_OUT:=LR}"
-: "${SOCKTOP_AUX_CMD:=}"
-: "${GESTURE_AUX_IN:=UD}"
-: "${GESTURE_AUX_OUT:=DU}"
-
-SWIPE_CMD=${SWIPE_CMD:-$(dirname "$0")/socktop-swipe}
-[ -x /usr/local/bin/socktop-swipe ] && SWIPE_CMD=/usr/local/bin/socktop-swipe
-
-verbose=
-replace=no
-for a in "$@"; do
- case "$a" in
- -v) verbose=-v ;;
- --replace) replace=yes ;;
- -h | --help)
- echo "usage: socktop-gestures [-v] [--replace]"
- echo " -v log each detected gesture"
- echo " --replace stop an already-running daemon first"
- exit 0
- ;;
- *)
- echo "socktop-gestures: unknown option '$a'" >&2
- exit 2
- ;;
- esac
-done
-
-# Single-instance guard. lisgd does not grab the input device exclusively, so a
-# second copy makes every swipe fire twice -- the carousel appears to skip panes.
-# Enforced here rather than left to the caller: the obvious restart one-liner
-# `pkill -x lisgd && socktop-gestures` silently starts NOTHING when no daemon was
-# running, because pkill exits non-zero when it matches nothing.
-running=$(pgrep -x -u "$(id -u)" lisgd 2>/dev/null || true)
-if [ -n "$running" ]; then
- if [ "$replace" = yes ]; then
- pkill -x -u "$(id -u)" lisgd || true
- n=0
- while pgrep -x -u "$(id -u)" lisgd >/dev/null 2>&1 && [ "$n" -lt 30 ]; do
- n=$((n + 1))
- sleep 0.1
- done
- if pgrep -x -u "$(id -u)" lisgd >/dev/null 2>&1; then
- echo "socktop-gestures: existing lisgd would not exit" >&2
- exit 1
- fi
- else
- echo "socktop-gestures: lisgd is already running (pid: $(echo "$running" | tr '\n' ' '))" >&2
- echo " Two instances make every swipe fire twice. Use --replace to restart it." >&2
- exit 1
- fi
-fi
-
-if [ ! -e "$TOUCH_DEV" ]; then
- echo "socktop-gestures: $TOUCH_DEV not present" >&2
- exit 1
-fi
-if [ ! -r "$TOUCH_DEV" ]; then
- echo "socktop-gestures: cannot read $TOUCH_DEV -- are you in the 'input' group?" >&2
- echo " sudo usermod -aG input $(id -un) then log out and back in" >&2
- exit 1
-fi
-
-# Bind every configured contact count to the same action; see config.env for why.
-set --
-for f in $FINGER_COUNTS; do
- set -- "$@" -g "$f,$GESTURE_IN,*,*,R,$SWIPE_CMD next"
- set -- "$@" -g "$f,$GESTURE_OUT,*,*,R,$SWIPE_CMD prev"
- if [ -n "$SOCKTOP_AUX_CMD" ]; then
- set -- "$@" -g "$f,$GESTURE_AUX_IN,*,*,R,$SWIPE_CMD down"
- set -- "$@" -g "$f,$GESTURE_AUX_OUT,*,*,R,$SWIPE_CMD up"
- fi
-done
-
-exec lisgd $verbose \
- -d "$TOUCH_DEV" \
- -w "$SCREEN_W" -h "$SCREEN_H" \
- -t "$SWIPE_THRESHOLD" -r "$SWIPE_LENIENCY" \
- "$@"
diff --git a/socktop-rack b/socktop-rack
deleted file mode 100755
index aa07dbf..0000000
--- a/socktop-rack
+++ /dev/null
@@ -1,91 +0,0 @@
-#!/bin/sh
-# Build the socktop display session: one tmux window per GROUP, one pane per
-# host inside it. Window order and pane index order together are the swipe
-# order (see socktop-swipe).
-set -eu
-
-# shellcheck source=config.env
-for c in /usr/local/etc/socktop-swipe.env "$(dirname "$0")/config.env"; do
- [ -r "$c" ] && . "$c" && break
-done
-
-: "${SOCKTOP_GROUPS:=}"
-: "${SOCKTOP_HOSTS:=}"
-: "${SOCKTOP_SESSION:=socktop4}"
-: "${SOCKTOP_BIN:=socktop}"
-: "${SOCKTOP_AUX_CMD:=}"
-
-# Legacy form: SOCKTOP_HOSTS alone is a single tiled group.
-if [ -z "$SOCKTOP_GROUPS" ]; then
- : "${SOCKTOP_HOSTS:?no SOCKTOP_GROUPS or SOCKTOP_HOSTS -- is the config installed?}"
- SOCKTOP_GROUPS=$SOCKTOP_HOSTS
-fi
-
-tmux kill-session -t "$SOCKTOP_SESSION" 2>/dev/null || true
-
-# Pane titles are set on the session, so do it up front; windows inherit.
-build_group() {
- # $1 = window name, remaining args = host names and an optional @layout
- name=$1
- shift
- layout=tiled
- hosts=
- for tok in "$@"; do
- case "$tok" in
- @*) layout=${tok#@} ;;
- *) hosts="$hosts $tok" ;;
- esac
- done
- # shellcheck disable=SC2086
- set -- $hosts
- [ $# -gt 0 ] || return 0
-
- if ! tmux has-session -t "$SOCKTOP_SESSION" 2>/dev/null; then
- tmux new-session -d -s "$SOCKTOP_SESSION" -n "$name" "$SOCKTOP_BIN -P $1"
- else
- tmux new-window -d -t "$SOCKTOP_SESSION" -n "$name" "$SOCKTOP_BIN -P $1"
- fi
- win="$SOCKTOP_SESSION:$name"
- tmux select-pane -t "$win.0" -T "$1"
- shift
- for h in "$@"; do
- # Split the most recently created pane so creation order == index order.
- tmux split-window -t "$win" "$SOCKTOP_BIN -P $h"
- tmux select-pane -t "$win.$(tmux display-message -t "$win" -p '#{pane_index}')" -T "$h"
- done
- tmux select-layout -t "$win" "$layout"
- tmux select-pane -t "$win.0"
-}
-
-n=0
-old_ifs=$IFS
-IFS=';'
-for group in $SOCKTOP_GROUPS; do
- IFS=$old_ifs
- # shellcheck disable=SC2086
- build_group "g$n" $group
- n=$((n + 1))
- IFS=';'
-done
-IFS=$old_ifs
-
-# The aux window sits outside the carousel; socktop-swipe recognises it by name.
-if [ -n "$SOCKTOP_AUX_CMD" ]; then
- tmux new-window -d -t "$SOCKTOP_SESSION" -n aux "$SOCKTOP_AUX_CMD"
-fi
-
-tmux select-window -t "$SOCKTOP_SESSION:g0"
-
-tmux set-option -t "$SOCKTOP_SESSION" pane-border-status top
-tmux set-option -t "$SOCKTOP_SESSION" pane-border-format ' #{pane_title} '
-tmux set-option -t "$SOCKTOP_SESSION" status off
-
-# Keep dead panes so pane indices stay stable if a socktop exits.
-tmux set-option -t "$SOCKTOP_SESSION" remain-on-exit on
-
-# Mouse mode MUST stay off. With it on, every touch swipe is ALSO delivered to
-# tmux as a click-drag: dragging across a pane border resizes it and taps
-# reselect panes, which fights the gesture daemon. See README, "Why no tapping".
-tmux set-option -t "$SOCKTOP_SESSION" mouse off
-
-exec tmux attach -t "$SOCKTOP_SESSION"
diff --git a/socktop-swipe b/socktop-swipe
deleted file mode 100755
index db4ef50..0000000
--- a/socktop-swipe
+++ /dev/null
@@ -1,121 +0,0 @@
-#!/bin/sh
-# socktop-swipe next|prev|down|up
-#
-# Walks a linear "zoom carousel" over the socktop session. Each tmux window is
-# one group of hosts; within a group the line is the tiled overview, then each
-# pane zoomed full-screen in index order; then the next window's overview:
-#
-# overview(g0) <-> g0.0 <-> ... <-> g0.N <-> overview(g1) <-> g1.0 <-> ...
-#
-# "next" moves right and stops at the last pane of the last window. "prev"
-# moves back and stops at the first overview. Deliberately no wrap-around: on
-# a wall display, wrapping makes it impossible to tell where you are.
-#
-# "down" jumps to the optional aux window (see SOCKTOP_AUX_CMD) and remembers
-# where you were; "up" returns there. The aux window is not part of the
-# horizontal carousel, and horizontal swipes while on it do nothing.
-#
-# This is tmux pane zoom, NOT extra socktop instances. All panes keep running
-# and stay connected while hidden, so swiping back shows current data with no
-# reconnect, and the polling load on the monitored hosts is constant.
-set -eu
-
-for c in /usr/local/etc/socktop-swipe.env "$(dirname "$0")/config.env"; do
- [ -r "$c" ] && . "$c" && break
-done
-: "${SOCKTOP_SESSION:=socktop4}"
-
-dir=${1:?usage: socktop-swipe next|prev|down|up}
-
-# Ordered window ids (@N) of the carousel windows, the aux window if any, and
-# the currently active window.
-all=$(tmux list-windows -t "$SOCKTOP_SESSION" -F '#{window_id} #{window_name}' 2>/dev/null) || exit 0
-[ -n "$all" ] || exit 0
-wins= aux=
-while read -r id name; do
- if [ "$name" = aux ]; then aux=$id; else wins="$wins $id"; fi
-done </dev/null || back=${wins# }
- back=${back%% *}
- tmux select-window -t "$back"
- exit 0
- ;;
-esac
-
-# On the aux screen, horizontal swipes are deliberately inert.
-[ "$WIN" = "$aux" ] && exit 0
-
-prev_win= next_win= seen=no
-for w in $wins; do
- if [ "$seen" = yes ]; then next_win=$w; break; fi
- if [ "$w" = "$WIN" ]; then seen=yes; else prev_win=$w; fi
-done
-
-state=$(tmux display-message -t "$WIN" -p '#{window_zoomed_flag} #{pane_index} #{window_panes}') || exit 0
-zoomed=${state%% *}
-rest=${state#* }
-idx=${rest%% *}
-count=${rest##* }
-last=$((count - 1))
-
-zoom_to() {
- # $1 = window id, $2 = pane index.
- # Selecting a different pane auto-unzooms, so zoom explicitly afterwards.
- tmux select-window -t "$1"
- tmux select-pane -t "$1.$2"
- tmux resize-pane -Z -t "$1.$2"
-}
-
-overview() {
- # Show window $1 unzoomed.
- tmux select-window -t "$1"
- if [ "$(tmux display-message -t "$1" -p '#{window_zoomed_flag}')" = 1 ]; then
- tmux resize-pane -Z -t "$1"
- fi
-}
-
-if [ "$zoomed" = 0 ]; then
- case "$dir" in
- next) zoom_to "$WIN" 0 ;;
- prev)
- # Back out of this group's overview onto the previous group's last pane.
- if [ -n "$prev_win" ]; then
- pl=$(tmux display-message -t "$prev_win" -p '#{window_panes}')
- zoom_to "$prev_win" $((pl - 1))
- fi
- ;;
- esac
-else
- case "$dir" in
- next)
- if [ "$idx" -lt "$last" ]; then
- zoom_to "$WIN" $((idx + 1))
- elif [ -n "$next_win" ]; then
- tmux resize-pane -Z -t "$WIN.$idx" # unzoom before leaving
- overview "$next_win"
- fi
- ;;
- prev)
- if [ "$idx" -gt 0 ]; then
- zoom_to "$WIN" $((idx - 1))
- else
- tmux resize-pane -Z -t "$WIN.$idx" # unzoom -> this group's overview
- fi
- ;;
- esac
-fi
diff --git a/socktop-swipe.service b/socktop-swipe.service
deleted file mode 100644
index 949b843..0000000
--- a/socktop-swipe.service
+++ /dev/null
@@ -1,17 +0,0 @@
-[Unit]
-Description=socktop-swipe touchscreen gesture daemon
-Documentation=https://gt.wittyoneoff.com/jason/socktop-swipe
-After=graphical-session.target
-PartOf=graphical-session.target
-
-[Service]
-Type=simple
-# lisgd queries X for screen geometry unless -w/-h are given (socktop-gestures
-# always passes them), but keep DISPLAY set so it starts cleanly either way.
-Environment=DISPLAY=:0
-ExecStart=/usr/local/bin/socktop-gestures
-Restart=always
-RestartSec=2
-
-[Install]
-WantedBy=graphical-session.target
diff --git a/src/config/coord.rs b/src/config/coord.rs
index b02d09c..84d5dfd 100644
--- a/src/config/coord.rs
+++ b/src/config/coord.rs
@@ -48,9 +48,11 @@ impl std::str::FromStr for Coord {
fn from_str(s: &str) -> Result {
let t = s.trim();
// Split on the separator 'x', which cannot be part of either number.
- let (row, col) = t
- .split_once('x')
- .ok_or_else(|| format!("{t:?} is not a coordinate -- expected \"x\", e.g. \"0x0\" or \"-1x0\""))?;
+ let (row, col) = t.split_once('x').ok_or_else(|| {
+ format!(
+ "{t:?} is not a coordinate -- expected \"x\", e.g. \"0x0\" or \"-1x0\""
+ )
+ })?;
let parse = |part: &str, which: &str| -> Result {
part.trim().parse::().map_err(|_| {
format!("{t:?} is not a coordinate -- the {which} {part:?} is not a whole number")
@@ -122,22 +124,43 @@ mod tests {
assert_eq!(Coord::new(1, -2).window_name(), "r1cm2");
for c in [Coord::new(0, 0), Coord::new(-1, -1), Coord::new(9, 9)] {
let n = c.window_name();
- assert!(!n.contains(':') && !n.contains('.') && !n.contains('-'), "{n}");
+ assert!(
+ !n.contains(':') && !n.contains('.') && !n.contains('-'),
+ "{n}"
+ );
}
}
#[test]
fn unquoted_hex_coordinate_gets_a_useful_error() {
// This is what YAML actually hands us for `at: 0x0`.
- let err = serde_yaml::from_str::("0x0").unwrap_err().to_string();
+ let err = serde_yaml::from_str::("0x0")
+ .unwrap_err()
+ .to_string();
assert!(err.contains("hexadecimal"), "unhelpful error: {err}");
- assert!(err.contains("at: \"0x0\""), "error should show the fix: {err}");
+ assert!(
+ err.contains("at: \"0x0\""),
+ "error should show the fix: {err}"
+ );
}
#[test]
fn sorts_by_row_then_column() {
- let mut v = vec![Coord::new(1, 0), Coord::new(-1, 5), Coord::new(0, 2), Coord::new(0, -1)];
+ let mut v = vec![
+ Coord::new(1, 0),
+ Coord::new(-1, 5),
+ Coord::new(0, 2),
+ Coord::new(0, -1),
+ ];
v.sort();
- assert_eq!(v, vec![Coord::new(-1, 5), Coord::new(0, -1), Coord::new(0, 2), Coord::new(1, 0)]);
+ assert_eq!(
+ v,
+ vec![
+ Coord::new(-1, 5),
+ Coord::new(0, -1),
+ Coord::new(0, 2),
+ Coord::new(1, 0)
+ ]
+ );
}
}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 8586376..80889a9 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -360,9 +360,7 @@ impl Config {
or a comma-separated string"
.into(),
),
- Some(g) if g.0.is_empty() => {
- wrong("socktop_group is empty".into())
- }
+ Some(g) if g.0.is_empty() => wrong("socktop_group is empty".into()),
_ => {}
}
if s.url.is_some() {
diff --git a/src/control.rs b/src/control.rs
index b2d1e3c..d7b0772 100644
--- a/src/control.rs
+++ b/src/control.rs
@@ -62,13 +62,17 @@ pub fn listen(session: &str, tx: Sender) -> Result {
let listener = UnixListener::bind(&path)
.with_context(|| format!("cannot create control socket at {}", path.display()))?;
- let accepting = listener.try_clone().context("cannot clone control socket")?;
+ let accepting = listener
+ .try_clone()
+ .context("cannot clone control socket")?;
std::thread::spawn(move || {
for stream in accepting.incoming() {
let Ok(stream) = stream else { continue };
let reply = stream.try_clone().ok();
let mut lines = BufReader::new(stream).lines();
- let Some(Ok(line)) = lines.next() else { continue };
+ let Some(Ok(line)) = lines.next() else {
+ continue;
+ };
match parse_move(&line) {
Some(m) => {
if tx.send(Ctl::Go(m, reply)).is_err() {
diff --git a/src/doctor.rs b/src/doctor.rs
index c289e9f..6a4c742 100644
--- a/src/doctor.rs
+++ b/src/doctor.rs
@@ -32,7 +32,11 @@ pub fn run(cfg: &Config) -> Result<()> {
println!("Watching {}", cfg.touch.device);
println!(
" panel {}x{}, threshold {}px, leniency {}\u{b0}, contacts accepted: {:?}",
- cfg.touch.width, cfg.touch.height, cfg.touch.threshold, cfg.touch.leniency, cfg.touch.fingers
+ cfg.touch.width,
+ cfg.touch.height,
+ cfg.touch.threshold,
+ cfg.touch.leniency,
+ cfg.touch.fingers
);
println!(
" grab: {}\n",
diff --git a/src/grid.rs b/src/grid.rs
index 14d3817..87f0edd 100644
--- a/src/grid.rs
+++ b/src/grid.rs
@@ -71,18 +71,23 @@ impl Grid {
let mut cells: Vec| =
cells.iter().filter(|c| c.coord.row == r).cloned().collect();
cells.sort_by_key(|c| c.coord.col);
- Row { row: r, cells, cursor: 0, visited: false }
+ Row {
+ row: r,
+ cells,
+ cursor: 0,
+ visited: false,
+ }
})
.collect();
// Start at 0x0 when it exists; otherwise the leftmost cell of row 0, and
// failing that the top-left of the whole grid.
- let cursor = rows
- .iter()
- .position(|r| r.row == 0)
- .unwrap_or(0);
+ let cursor = rows.iter().position(|r| r.row == 0).unwrap_or(0);
let mut grid = Grid { rows, cursor };
- let start_col = grid.rows[cursor].cells.iter().position(|c| c.coord.col == 0);
+ let start_col = grid.rows[cursor]
+ .cells
+ .iter()
+ .position(|c| c.coord.col == 0);
grid.rows[cursor].cursor = start_col.unwrap_or(0);
grid.rows[cursor].visited = true;
Ok(grid)
@@ -104,7 +109,11 @@ impl Grid {
pub fn position(&self, changed: bool) -> Position {
let c = self.cell();
- Position { coord: c.coord, screen: c.cursor, changed }
+ Position {
+ coord: c.coord,
+ screen: c.cursor,
+ changed,
+ }
}
pub fn apply(&mut self, m: Move) -> Position {
@@ -218,7 +227,10 @@ mod tests {
kind: MonitorType::Socktop,
label: format!("{coord}"),
panes: (0..n)
- .map(|i| Pane { title: format!("{coord}#{i}"), command: vec!["true".into()] })
+ .map(|i| Pane {
+ title: format!("{coord}#{i}"),
+ command: vec!["true".into()],
+ })
.collect(),
layout: Layout::Tiled,
cursor: 0,
@@ -319,8 +331,14 @@ mod tests {
let mut sparse = Grid::new(vec![cell("0x0", 1), cell("0x5", 1), cell("0x99", 1)]).unwrap();
let mut dense = Grid::new(vec![cell("0x0", 1), cell("0x1", 1), cell("0x2", 1)]).unwrap();
let moves = [Move::Forward, Move::Forward, Move::Back, Move::Forward];
- let s: Vec<_> = walk(&mut sparse, &moves).iter().map(|p| p.split('@').nth(1).unwrap().to_string()).collect();
- let d: Vec<_> = walk(&mut dense, &moves).iter().map(|p| p.split('@').nth(1).unwrap().to_string()).collect();
+ let s: Vec<_> = walk(&mut sparse, &moves)
+ .iter()
+ .map(|p| p.split('@').nth(1).unwrap().to_string())
+ .collect();
+ let d: Vec<_> = walk(&mut dense, &moves)
+ .iter()
+ .map(|p| p.split('@').nth(1).unwrap().to_string())
+ .collect();
assert_eq!(s, d);
assert_eq!(sparse.cell().coord, Coord::new(0, 99));
assert_eq!(dense.cell().coord, Coord::new(0, 2));
@@ -330,7 +348,10 @@ mod tests {
fn columns_out_of_order_in_the_file_still_sort() {
let mut g = Grid::new(vec![cell("0x2", 1), cell("0x0", 1), cell("0x1", 1)]).unwrap();
assert_eq!(at(&g), "0x0@0");
- assert_eq!(walk(&mut g, &[Move::Forward, Move::Forward]), ["0x1@0", "0x2@0"]);
+ assert_eq!(
+ walk(&mut g, &[Move::Forward, Move::Forward]),
+ ["0x1@0", "0x2@0"]
+ );
}
// -- vertical ------------------------------------------------------------
@@ -381,7 +402,11 @@ mod tests {
["0x0@1", "0x0@2", "0x1@0", "0x1@1", "0x1@2"]
);
- assert_eq!(walk(&mut g, &[Move::Up]), ["-1x0@0"], "snapped on first entry");
+ assert_eq!(
+ walk(&mut g, &[Move::Up]),
+ ["-1x0@0"],
+ "snapped on first entry"
+ );
assert_eq!(
walk(&mut g, &[Move::Down]),
["0x1@2"],
@@ -400,7 +425,11 @@ mod tests {
.unwrap();
g.apply(Move::Forward); // to 0x8
assert_eq!(at(&g), "0x8@0");
- assert_eq!(walk(&mut g, &[Move::Up]), ["-1x9@0"], "9 is nearer to 8 than 1");
+ assert_eq!(
+ walk(&mut g, &[Move::Up]),
+ ["-1x9@0"],
+ "9 is nearer to 8 than 1"
+ );
}
#[test]
@@ -434,7 +463,11 @@ mod tests {
assert_eq!(at(&g), "0x0@0");
g.apply(Move::Forward); // now at 0x1
g.apply(Move::Up);
- assert_eq!(walk(&mut g, &[Move::Down]), ["0x1@0"], "memory follows the move");
+ assert_eq!(
+ walk(&mut g, &[Move::Down]),
+ ["0x1@0"],
+ "memory follows the move"
+ );
}
// -- start position ------------------------------------------------------
diff --git a/src/input.rs b/src/input.rs
index ce8997e..0f07f9e 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -120,7 +120,10 @@ impl Touchpanel {
})?;
}
- Ok(Self { device, cfg: cfg.clone() })
+ Ok(Self {
+ device,
+ cfg: cfg.clone(),
+ })
}
/// Blocking gesture loop. Calls `on_event` for every completed gesture,
@@ -144,7 +147,11 @@ impl Touchpanel {
} else {
slots.insert(
current,
- Slot { start: (0, 0), last: (0, 0), active: true },
+ Slot {
+ start: (0, 0),
+ last: (0, 0),
+ active: true,
+ },
);
peak = peak.max(slots.values().filter(|s| s.active).count());
}
@@ -161,8 +168,7 @@ impl Touchpanel {
// The gesture ends when the last contact lifts.
if !slots.is_empty() && slots.values().all(|s| !s.active) {
- let tracks: Vec |