v2: Rust rewrite with a YAML grid layout, evdev gestures and a preflighting installer #1
@@ -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)):
|
||||

|
||||
|
||||
<video controls src="media/swipe-demo.mp4"></video>
|
||||
|
||||
## 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.
|
||||
|
||||
<details>
|
||||
<summary>From a checkout instead</summary>
|
||||
|
||||
```sh
|
||||
git clone https://gt.wittyoneoff.com/jason/socktop-swipe
|
||||
cd socktop-swipe
|
||||
./install.sh --xignore --i3
|
||||
./install.sh
|
||||
```
|
||||
</details>
|
||||
|
||||
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.
|
||||
|
||||
+29
-18
@@ -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
|
||||
|
||||
|
||||
-91
@@ -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}"
|
||||
+399
-198
@@ -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 </dev/tty || _a=""
|
||||
[ -z "$_a" ] && _a=$_d
|
||||
case "$_a" in
|
||||
[Yy] | [Yy][Ee][Ss]) return 0 ;;
|
||||
[Nn] | [Nn][Oo]) return 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# askval "question" "default" -> 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/tty || _a=""
|
||||
[ -z "$_a" ] && _a=$2
|
||||
printf '%s' "$_a"
|
||||
}
|
||||
|
||||
have() { command -v "$1" >/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 <<EOF
|
||||
# Installed by socktop-swipe.
|
||||
# The panel is driven by lisgd via /dev/input, NOT through X. Letting X also
|
||||
# deliver touch makes the terminal, tmux and socktop react to the same swipes
|
||||
# the gesture daemon is interpreting. MatchProduct is narrow on purpose so it
|
||||
# cannot match keyboards or other HID devices.
|
||||
Section "InputClass"
|
||||
Identifier "ignore touchscreen (socktop-swipe)"
|
||||
MatchProduct "$product"
|
||||
MatchIsTouchscreen "on"
|
||||
Option "Ignore" "on"
|
||||
EndSection
|
||||
EOF
|
||||
echo " wrote $XCONF (takes effect after X restarts)"
|
||||
relogin=yes
|
||||
# --------------------------------------------------------------------------
|
||||
# Config
|
||||
# --------------------------------------------------------------------------
|
||||
step "Configuration"
|
||||
mkdir -p "$CONF_DIR"
|
||||
if [ -e "$CONF" ]; then
|
||||
say " keeping the existing $CONF"
|
||||
else
|
||||
sed "s|^ device: .*| device: $DEVICE|" "$SRC/config.example.yaml" >"$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 <<EOF
|
||||
# Installed by socktop-swipe (--autologin). Wall display: come back unattended
|
||||
# after a reboot. Remove this file to restore the login prompt.
|
||||
[Seat:*]
|
||||
autologin-user=$(id -un)
|
||||
autologin-user-timeout=0
|
||||
autologin-session=$session
|
||||
EOF
|
||||
echo " wrote $LCONF (takes effect at next boot)"
|
||||
sudo tee "$LIGHTDM" >/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" <<EOF
|
||||
|
||||
# --- socktop-swipe --------------------------------------------------------
|
||||
# Exactly ONE gesture daemon: lisgd does not grab the device exclusively, so a
|
||||
# second instance makes every swipe fire twice.
|
||||
exec --no-startup-id $BIN/socktop-gestures
|
||||
# Start the dashboard on the primary output and fullscreen (which also hides
|
||||
# the i3 bar), so no keyboard is needed after a reboot and a second monitor
|
||||
# cannot capture it. --class gives the window a distinct instance name for the
|
||||
# rule; alacritty syntax -- adapt for other terminals.
|
||||
for_window [instance="^socktop-rack\$"] move window to output primary, fullscreen enable
|
||||
exec --no-startup-id \$TERMINAL --class socktop-rack -e $BIN/socktop-rack
|
||||
# Hide the idle pointer: with X ignoring the touch panel nothing ever moves it,
|
||||
# so it would sit at screen centre forever. (apt install unclutter-xfixes)
|
||||
exec --no-startup-id unclutter --timeout 1 --ignore-scrolling
|
||||
EOF
|
||||
if i3 -C -c "$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 <<EOF
|
||||
|
||||
Start it by hand with:
|
||||
$BIN/socktop-gestures & # exactly one instance
|
||||
$BIN/socktop-rack # builds and attaches the session
|
||||
Next:
|
||||
1. Edit $CONF -- the screens are the author's, not yours.
|
||||
2. socktop-swipe validate check it, and see the grid map
|
||||
3. socktop-swipe doctor swipe, and see what the panel reports
|
||||
4. socktop-swipe run build the session and start swiping
|
||||
|
||||
Configuration: $CONF
|
||||
Diagnostics: tools/diag.sh
|
||||
EOF
|
||||
echo "=============================================================="
|
||||
say "=============================================================="
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
# Design notes
|
||||
|
||||
Why the thing is built the way it is. The README says what to do; this says why,
|
||||
so neither has to carry both jobs.
|
||||
|
||||
## The carousel is tmux pane zoom, not extra processes
|
||||
|
||||
There is exactly one monitor process per host. Zooming calls
|
||||
`tmux resize-pane -Z`, which makes one pane fill the window and sends `SIGWINCH`
|
||||
so the program repaints at the new size.
|
||||
|
||||
Three reasons that matters:
|
||||
|
||||
- **Half the processes.** Separate full-screen instances would mean N extra
|
||||
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 shows current data, not a stale snapshot with a spinner.
|
||||
- **Constant load.** The monitored hosts see the same connections regardless of
|
||||
what is on screen.
|
||||
|
||||
## Why the grid is sparse ordinals
|
||||
|
||||
The obvious design is that a coordinate names a physical slot. It falls apart on
|
||||
the first socktop group: a group of four hosts is five screens, so putting
|
||||
anything to its right means writing `0x5`, and adding a fifth host means
|
||||
renumbering the rest of the row.
|
||||
|
||||
Making coordinates pure ordering removes that entirely. Only the sort order
|
||||
matters, so `0x1` and `0x5` are the same thing, and a group grows without
|
||||
disturbing its neighbours. The cost is that the file is not a literal map of the
|
||||
screen — which is what `socktop-swipe validate` is for.
|
||||
|
||||
## Why return memory beats spatial snapping
|
||||
|
||||
Vertical movement had two plausible rules and they disagree. Purely spatial: from
|
||||
`0x1`, up to `-1x0`, down again lands on `0x0`. Return memory: it lands back on
|
||||
`0x1`.
|
||||
|
||||
Return memory won because the thing you actually do with a wall display is glance
|
||||
away and glance back. Losing your place on every glance is the worse failure, and
|
||||
snapping is only ever needed to decide the *first* entry into a row. A layout
|
||||
where every row has a cell in the same column never snaps at all.
|
||||
|
||||
## Why evdev instead of lisgd
|
||||
|
||||
libinput deliberately emits gesture events only for touchpads, never for
|
||||
touchscreens, so `libinput-gestures` and everything built on it cannot work here
|
||||
at all. Something has to read raw touch events. v1 used
|
||||
[lisgd](https://git.sr.ht/~mil/lisgd); v2 does it in-process.
|
||||
|
||||
What that bought:
|
||||
|
||||
- **No C toolchain in the install path.** No `libinput-dev`, no `libX11-dev`, no
|
||||
`git clone` and `make`. On the Wyse's 8 GB of eMMC that is not a small thing.
|
||||
- **`grab: true` replaces the X ignore rule.** `EVIOCGRAB` takes the device
|
||||
exclusively, so X never sees the touches — which is what v1's
|
||||
`Option "Ignore"` InputClass was faking, except this needs no X restart and no
|
||||
logout. The X rule is still documented in the README as a fallback for anyone
|
||||
who wants touch to reach other applications.
|
||||
- **One process, so the double-instance bug cannot happen.** lisgd does not grab
|
||||
the device, so two copies made every swipe fire twice and the carousel appeared
|
||||
to skip. Now the second copy fails to grab and says so.
|
||||
- **The contact-count workaround became honest.** Instead of binding three
|
||||
separate lisgd gestures per direction, the peak contact count is a field on the
|
||||
detected swipe, and `doctor` prints it in English.
|
||||
|
||||
Averaging rather than summing the contacts' travel matters here: a panel
|
||||
reporting one physical finger as three contacts must not look like three times
|
||||
the displacement. There is a test for exactly that.
|
||||
|
||||
## Why panes are addressed by id, and kept alive
|
||||
|
||||
Pane *indices* renumber when a pane dies. Pane *ids* (`%12`) do not, so every
|
||||
lookup uses them.
|
||||
|
||||
That leaves the question of what happens when a monitor exits — a typo in a
|
||||
`generic` command, a socktop that cannot reach its agent. tmux destroys a window
|
||||
when its last pane goes, which during construction breaks the next
|
||||
`split-window` with a baffling "no current target", and afterwards silently
|
||||
reshuffles the display.
|
||||
|
||||
v1 used `remain-on-exit`, which cannot actually do the job: it is a **per-window**
|
||||
option that new windows do not inherit, so there is always a gap between creating
|
||||
a window and setting it. v2 wraps each command instead:
|
||||
|
||||
```sh
|
||||
<command>; s=$?; printf '\n[%s exited: status %s]\n' <name> "$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.
|
||||
@@ -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.
|
||||
+3
-1
@@ -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,
|
||||
|
||||
@@ -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<Pane>` 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.
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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" \
|
||||
"$@"
|
||||
@@ -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"
|
||||
-121
@@ -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 <<EOF
|
||||
$all
|
||||
EOF
|
||||
WIN=$(tmux display-message -t "$SOCKTOP_SESSION" -p '#{window_id}') || exit 0
|
||||
|
||||
case "$dir" in
|
||||
down)
|
||||
[ -n "$aux" ] || exit 0
|
||||
[ "$WIN" = "$aux" ] && exit 0
|
||||
tmux set-option -t "$SOCKTOP_SESSION" @return_to "$WIN"
|
||||
tmux select-window -t "$aux"
|
||||
exit 0
|
||||
;;
|
||||
up)
|
||||
[ "$WIN" = "$aux" ] || exit 0
|
||||
back=$(tmux show-option -qv -t "$SOCKTOP_SESSION" @return_to)
|
||||
[ -n "$back" ] && tmux has-session -t "$back" 2>/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
|
||||
@@ -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
|
||||
+31
-8
@@ -48,9 +48,11 @@ impl std::str::FromStr for Coord {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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 \"<row>x<col>\", e.g. \"0x0\" or \"-1x0\""))?;
|
||||
let (row, col) = t.split_once('x').ok_or_else(|| {
|
||||
format!(
|
||||
"{t:?} is not a coordinate -- expected \"<row>x<col>\", e.g. \"0x0\" or \"-1x0\""
|
||||
)
|
||||
})?;
|
||||
let parse = |part: &str, which: &str| -> Result<i32, String> {
|
||||
part.trim().parse::<i32>().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::<Coord>("0x0").unwrap_err().to_string();
|
||||
let err = serde_yaml::from_str::<Coord>("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)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -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() {
|
||||
|
||||
+6
-2
@@ -62,13 +62,17 @@ pub fn listen(session: &str, tx: Sender<Ctl>) -> Result<UnixListener> {
|
||||
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() {
|
||||
|
||||
+5
-1
@@ -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",
|
||||
|
||||
+47
-14
@@ -71,18 +71,23 @@ impl Grid {
|
||||
let mut cells: Vec<Cell> =
|
||||
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 ------------------------------------------------------
|
||||
|
||||
+48
-14
@@ -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<Track> =
|
||||
slots.values().map(|s| (s.start, s.last)).collect();
|
||||
let tracks: Vec<Track> = slots.values().map(|s| (s.start, s.last)).collect();
|
||||
if let Some(ev) = classify(&self.cfg, &tracks, peak) {
|
||||
if !on_event(ev) {
|
||||
return Ok(());
|
||||
@@ -173,7 +179,6 @@ impl Touchpanel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// One contact's journey: where it landed and where it lifted.
|
||||
@@ -190,7 +195,10 @@ pub fn classify(cfg: &Touch, tracks: &[Track], peak: usize) -> Option<Event> {
|
||||
return None;
|
||||
}
|
||||
let (dx, dy) = tracks.iter().fold((0.0, 0.0), |(ax, ay), (start, last)| {
|
||||
(ax + (last.0 - start.0) as f64 / n, ay + (last.1 - start.1) as f64 / n)
|
||||
(
|
||||
ax + (last.0 - start.0) as f64 / n,
|
||||
ay + (last.1 - start.1) as f64 / n,
|
||||
)
|
||||
});
|
||||
|
||||
let travel = (dx * dx + dy * dy).sqrt();
|
||||
@@ -207,28 +215,44 @@ pub fn classify(cfg: &Touch, tracks: &[Track], peak: usize) -> Option<Event> {
|
||||
if travel < cfg.threshold as f64 {
|
||||
return Some(Event::Discarded(
|
||||
direction,
|
||||
Rejected::TooShort { travel, threshold: cfg.threshold },
|
||||
Rejected::TooShort {
|
||||
travel,
|
||||
threshold: cfg.threshold,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// Angle away from the dominant axis.
|
||||
let (along, across) = if horizontal { (dx.abs(), dy.abs()) } else { (dy.abs(), dx.abs()) };
|
||||
let (along, across) = if horizontal {
|
||||
(dx.abs(), dy.abs())
|
||||
} else {
|
||||
(dy.abs(), dx.abs())
|
||||
};
|
||||
let degrees = across.atan2(along).to_degrees();
|
||||
if degrees > cfg.leniency as f64 {
|
||||
return Some(Event::Discarded(
|
||||
direction,
|
||||
Rejected::OffAxis { degrees, leniency: cfg.leniency },
|
||||
Rejected::OffAxis {
|
||||
degrees,
|
||||
leniency: cfg.leniency,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if !cfg.fingers.contains(&peak) {
|
||||
return Some(Event::Discarded(
|
||||
direction,
|
||||
Rejected::WrongFingerCount { saw: peak, want: cfg.fingers.clone() },
|
||||
Rejected::WrongFingerCount {
|
||||
saw: peak,
|
||||
want: cfg.fingers.clone(),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
Some(Event::Swipe(Swipe { direction, fingers: peak }))
|
||||
Some(Event::Swipe(Swipe {
|
||||
direction,
|
||||
fingers: peak,
|
||||
}))
|
||||
}
|
||||
|
||||
fn update(slots: &mut HashMap<i32, Slot>, current: i32, f: impl Fn(&mut (i32, i32))) {
|
||||
@@ -251,7 +275,9 @@ pub fn list_touchscreens() -> Vec<(String, String)> {
|
||||
let entries = std::fs::read_dir(by_id).into_iter().flatten().flatten();
|
||||
for e in entries {
|
||||
let path = e.path();
|
||||
let Ok(dev) = Device::open(&path) else { continue };
|
||||
let Ok(dev) = Device::open(&path) else {
|
||||
continue;
|
||||
};
|
||||
let multitouch = dev
|
||||
.supported_absolute_axes()
|
||||
.is_some_and(|a| a.contains(AbsoluteAxisType::ABS_MT_POSITION_X));
|
||||
@@ -368,7 +394,11 @@ mod tests {
|
||||
// nothing ever fires, because the panel reports 2 contacts and the
|
||||
// config accepts only 1.
|
||||
let cfg = touch(80, 30, vec![1]);
|
||||
match swipe(&cfg, &[track((900, 300), -200, 0), track((905, 305), -200, 0)], 2) {
|
||||
match swipe(
|
||||
&cfg,
|
||||
&[track((900, 300), -200, 0), track((905, 305), -200, 0)],
|
||||
2,
|
||||
) {
|
||||
Event::Discarded(Direction::RL, r @ Rejected::WrongFingerCount { saw, .. }) => {
|
||||
assert_eq!(saw, 2);
|
||||
assert!(r.to_string().contains("touch.fingers"), "{r}");
|
||||
@@ -382,7 +412,11 @@ mod tests {
|
||||
// Otherwise a stray tap on a panel with ghost contacts reports the
|
||||
// finger-count problem, sending you to fix the wrong setting.
|
||||
let cfg = touch(80, 30, vec![1]);
|
||||
match swipe(&cfg, &[track((640, 360), -5, 0), track((641, 361), -5, 0)], 2) {
|
||||
match swipe(
|
||||
&cfg,
|
||||
&[track((640, 360), -5, 0), track((641, 361), -5, 0)],
|
||||
2,
|
||||
) {
|
||||
Event::Discarded(_, Rejected::TooShort { .. }) => {}
|
||||
other => panic!("expected the short-travel reason first: {other:?}"),
|
||||
}
|
||||
|
||||
+19
-7
@@ -104,7 +104,10 @@ fn real_main() -> Result<()> {
|
||||
Cmd::Build => {
|
||||
let (tmux, grid) = build(&cfg)?;
|
||||
tmux.build(&grid)?;
|
||||
println!("session {:?} is up. Attach with: tmux attach -t {}", cfg.session, cfg.session);
|
||||
println!(
|
||||
"session {:?} is up. Attach with: tmux attach -t {}",
|
||||
cfg.session, cfg.session
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Cmd::Attach => {
|
||||
@@ -141,7 +144,11 @@ fn validate(cfg: &Config, path: &std::path::Path) -> Result<()> {
|
||||
for row in &grid.rows {
|
||||
println!("row {}:", row.row);
|
||||
for cell in &row.cells {
|
||||
let start = if cell.coord == config::Coord::new(0, 0) { " <- start" } else { "" };
|
||||
let start = if cell.coord == config::Coord::new(0, 0) {
|
||||
" <- start"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(
|
||||
" {:<7} {:<20} {} screen{}{start}",
|
||||
cell.coord.to_string(),
|
||||
@@ -173,7 +180,10 @@ fn validate(cfg: &Config, path: &std::path::Path) -> Result<()> {
|
||||
}
|
||||
|
||||
if !std::path::Path::new(&cfg.touch.device).exists() {
|
||||
println!("touch: \u{2717} {} is not present right now", cfg.touch.device);
|
||||
println!(
|
||||
"touch: \u{2717} {} is not present right now",
|
||||
cfg.touch.device
|
||||
);
|
||||
println!(" `socktop-swipe doctor --list` shows what is.\n");
|
||||
} else {
|
||||
println!("touch: \u{2713} {}\n", cfg.touch.device);
|
||||
@@ -198,9 +208,7 @@ fn which(bin: &str) -> bool {
|
||||
return std::path::Path::new(bin).is_file();
|
||||
}
|
||||
std::env::var_os("PATH")
|
||||
.map(|p| {
|
||||
std::env::split_paths(&p).any(|dir| dir.join(bin).is_file())
|
||||
})
|
||||
.map(|p| std::env::split_paths(&p).any(|dir| dir.join(bin).is_file()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -312,7 +320,11 @@ fn drive(tmux: &Tmux, grid: &mut Grid, rx: mpsc::Receiver<Ctl>) -> Result<()> {
|
||||
answer(reply, &format!("error: {e:#}"));
|
||||
return Err(e);
|
||||
}
|
||||
let edge = if pos.changed { "" } else { " (edge, nothing to move to)" };
|
||||
let edge = if pos.changed {
|
||||
""
|
||||
} else {
|
||||
" (edge, nothing to move to)"
|
||||
};
|
||||
answer(
|
||||
reply,
|
||||
&format!("{} {}{edge}", pos.coord, grid.cell().screen_label()),
|
||||
|
||||
+16
-4
@@ -88,7 +88,10 @@ pub fn build_cell(screen: &Screen, bins: &Binaries) -> Result<Cell> {
|
||||
coord: at,
|
||||
kind: screen.kind,
|
||||
label: title.clone(),
|
||||
panes: vec![Pane { title, command: argv }],
|
||||
panes: vec![Pane {
|
||||
title,
|
||||
command: argv,
|
||||
}],
|
||||
layout: screen.layout.unwrap_or_default(),
|
||||
cursor: 0,
|
||||
});
|
||||
@@ -128,7 +131,10 @@ pub fn build_cell(screen: &Screen, bins: &Binaries) -> Result<Cell> {
|
||||
c.extend(extra);
|
||||
(
|
||||
"uptime kuma".to_string(),
|
||||
vec![Pane { title: "uptime kuma".into(), command: c }],
|
||||
vec![Pane {
|
||||
title: "uptime kuma".into(),
|
||||
command: c,
|
||||
}],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -146,7 +152,10 @@ pub fn build_cell(screen: &Screen, bins: &Binaries) -> Result<Cell> {
|
||||
c.extend(extra);
|
||||
(
|
||||
"unifly".to_string(),
|
||||
vec![Pane { title: "unifly".into(), command: c }],
|
||||
vec![Pane {
|
||||
title: "unifly".into(),
|
||||
command: c,
|
||||
}],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -165,5 +174,8 @@ pub fn build_cell(screen: &Screen, bins: &Binaries) -> Result<Cell> {
|
||||
}
|
||||
|
||||
pub fn build_cells(cfg: &Config) -> Result<Vec<Cell>> {
|
||||
cfg.screens.iter().map(|s| build_cell(s, &cfg.binaries)).collect()
|
||||
cfg.screens
|
||||
.iter()
|
||||
.map(|s| build_cell(s, &cfg.binaries))
|
||||
.collect()
|
||||
}
|
||||
|
||||
+65
-14
@@ -81,7 +81,13 @@ impl Tmux {
|
||||
}
|
||||
|
||||
fn zoomed(&self, window: &str) -> Result<bool> {
|
||||
Ok(self.run(&["display-message", "-p", "-t", window, "#{window_zoomed_flag}"])? == "1")
|
||||
Ok(self.run(&[
|
||||
"display-message",
|
||||
"-p",
|
||||
"-t",
|
||||
window,
|
||||
"#{window_zoomed_flag}",
|
||||
])? == "1")
|
||||
}
|
||||
|
||||
fn place(&self, cell: &Cell, first: bool) -> Result<Placed> {
|
||||
@@ -90,19 +96,33 @@ impl Tmux {
|
||||
|
||||
let window = if first {
|
||||
self.run(&[
|
||||
"new-session", "-d", "-s", &self.session, "-n", &name,
|
||||
"-P", "-F", "#{window_id}", &head,
|
||||
"new-session",
|
||||
"-d",
|
||||
"-s",
|
||||
&self.session,
|
||||
"-n",
|
||||
&name,
|
||||
"-P",
|
||||
"-F",
|
||||
"#{window_id}",
|
||||
&head,
|
||||
])?
|
||||
} else {
|
||||
self.run(&[
|
||||
"new-window", "-d", "-t", &self.session, "-n", &name,
|
||||
"-P", "-F", "#{window_id}", &head,
|
||||
"new-window",
|
||||
"-d",
|
||||
"-t",
|
||||
&self.session,
|
||||
"-n",
|
||||
&name,
|
||||
"-P",
|
||||
"-F",
|
||||
"#{window_id}",
|
||||
&head,
|
||||
])?
|
||||
};
|
||||
|
||||
let first_pane = self.run(&[
|
||||
"display-message", "-p", "-t", &window, "#{pane_id}",
|
||||
])?;
|
||||
let first_pane = self.run(&["display-message", "-p", "-t", &window, "#{pane_id}"])?;
|
||||
self.run(&["select-pane", "-t", &first_pane, "-T", &cell.panes[0].title])?;
|
||||
let mut panes = vec![first_pane];
|
||||
|
||||
@@ -110,7 +130,12 @@ impl Tmux {
|
||||
// Split the most recently created pane so creation order matches
|
||||
// the visual order the layout will impose.
|
||||
let id = self.run(&[
|
||||
"split-window", "-t", panes.last().unwrap(), "-P", "-F", "#{pane_id}",
|
||||
"split-window",
|
||||
"-t",
|
||||
panes.last().unwrap(),
|
||||
"-P",
|
||||
"-F",
|
||||
"#{pane_id}",
|
||||
&Self::shell_command(&pane.command),
|
||||
])?;
|
||||
self.run(&["select-pane", "-t", &id, "-T", &pane.title])?;
|
||||
@@ -131,7 +156,11 @@ impl Tmux {
|
||||
/// daemon would cheerfully drive a stale layout.
|
||||
fn adopt(&self, grid: &Grid) -> Result<()> {
|
||||
let listing = self.run(&[
|
||||
"list-windows", "-t", &self.session, "-F", "#{window_id} #{window_name}",
|
||||
"list-windows",
|
||||
"-t",
|
||||
&self.session,
|
||||
"-F",
|
||||
"#{window_id} #{window_name}",
|
||||
])?;
|
||||
let mut by_name: HashMap<&str, &str> = HashMap::new();
|
||||
for line in listing.lines() {
|
||||
@@ -166,7 +195,13 @@ impl Tmux {
|
||||
cell.panes.len()
|
||||
);
|
||||
}
|
||||
placed.insert(cell.coord, Placed { window: (*window).to_owned(), panes });
|
||||
placed.insert(
|
||||
cell.coord,
|
||||
Placed {
|
||||
window: (*window).to_owned(),
|
||||
panes,
|
||||
},
|
||||
);
|
||||
}
|
||||
*self.placed.borrow_mut() = placed;
|
||||
Ok(())
|
||||
@@ -191,7 +226,12 @@ impl Tmux {
|
||||
parts.push(format!("{} \u{25b2}", u.label));
|
||||
}
|
||||
let here = if cell.screens() > 1 {
|
||||
format!(" {} [{}/{}] ", cell.screen_label(), cell.cursor + 1, cell.screens())
|
||||
format!(
|
||||
" {} [{}/{}] ",
|
||||
cell.screen_label(),
|
||||
cell.cursor + 1,
|
||||
cell.screens()
|
||||
)
|
||||
} else {
|
||||
format!(" {} ", cell.screen_label())
|
||||
};
|
||||
@@ -226,7 +266,13 @@ impl Multiplexer for Tmux {
|
||||
|
||||
let s = &self.session;
|
||||
self.run(&["set-option", "-t", s, "pane-border-status", "top"])?;
|
||||
self.run(&["set-option", "-t", s, "pane-border-format", " #{pane_title} "])?;
|
||||
self.run(&[
|
||||
"set-option",
|
||||
"-t",
|
||||
s,
|
||||
"pane-border-format",
|
||||
" #{pane_title} ",
|
||||
])?;
|
||||
|
||||
// Mouse mode MUST stay off. With it on, a touch swipe is also delivered
|
||||
// to tmux as a click-drag: dragging across a pane border resizes it and
|
||||
@@ -298,6 +344,11 @@ impl Multiplexer for Tmux {
|
||||
}
|
||||
|
||||
fn attach_argv(&self) -> Vec<String> {
|
||||
vec!["tmux".into(), "attach".into(), "-t".into(), self.session.clone()]
|
||||
vec![
|
||||
"tmux".into(),
|
||||
"attach".into(),
|
||||
"-t".into(),
|
||||
self.session.clone(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+23
-5
@@ -94,7 +94,13 @@ fn session_layout_and_navigation_match_the_grid() {
|
||||
mux.build(&grid).expect("session should build");
|
||||
|
||||
// One window per cell, in grid order, named by coordinate.
|
||||
let windows = tmux(&["list-windows", "-t", SESSION, "-F", "#{window_name} #{window_panes}"]);
|
||||
let windows = tmux(&[
|
||||
"list-windows",
|
||||
"-t",
|
||||
SESSION,
|
||||
"-F",
|
||||
"#{window_name} #{window_panes}",
|
||||
]);
|
||||
assert_eq!(
|
||||
windows.lines().collect::<Vec<_>>(),
|
||||
vec!["rm1c0 1", "r0c0 4", "r0c5 2", "r1c0 1"],
|
||||
@@ -120,14 +126,20 @@ fn session_layout_and_navigation_match_the_grid() {
|
||||
let pos = grid.apply(Move::Forward);
|
||||
mux.show(&grid, &pos).unwrap();
|
||||
let (win, _, zoomed) = visible();
|
||||
assert_eq!(win, "r0c5", "0x5 follows 0x0 despite the gap in column numbers");
|
||||
assert_eq!(
|
||||
win, "r0c5",
|
||||
"0x5 follows 0x0 despite the gap in column numbers"
|
||||
);
|
||||
assert!(!zoomed);
|
||||
|
||||
// Back must land on 0x0's LAST host, not its overview.
|
||||
let pos = grid.apply(Move::Back);
|
||||
mux.show(&grid, &pos).unwrap();
|
||||
let (win, title, zoomed) = visible();
|
||||
assert_eq!((win.as_str(), title.as_str(), zoomed), ("r0c0", "delta", true));
|
||||
assert_eq!(
|
||||
(win.as_str(), title.as_str(), zoomed),
|
||||
("r0c0", "delta", true)
|
||||
);
|
||||
|
||||
// Up to unifly: a single-pane cell, so nothing to zoom.
|
||||
let pos = grid.apply(Move::Up);
|
||||
@@ -174,9 +186,15 @@ screens:
|
||||
|
||||
let cells = monitor::build_cells(&cfg).unwrap();
|
||||
let argv = &cells[0].panes[0].command;
|
||||
assert_eq!(argv, &["/bin/echo", "-P", "a b; touch /tmp/socktop-swipe-pwned"]);
|
||||
assert_eq!(
|
||||
argv,
|
||||
&["/bin/echo", "-P", "a b; touch /tmp/socktop-swipe-pwned"]
|
||||
);
|
||||
|
||||
let quoted = shell_words::join(argv.iter().map(String::as_str));
|
||||
let reparsed = shell_words::split("ed).unwrap();
|
||||
assert_eq!(&reparsed, argv, "quoting must survive the round trip through sh");
|
||||
assert_eq!(
|
||||
&reparsed, argv,
|
||||
"quoting must survive the round trip through sh"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Gesture diagnostic. Two stages, each answering exactly one question.
|
||||
#
|
||||
# Stage 1: is the panel delivering events to us at all?
|
||||
# Stage 2: which direction and how many contacts does lisgd see?
|
||||
#
|
||||
# Stage 2 is the important one. Each line reads:
|
||||
# [swipe]: Cfg(f=1/s=3/e=0/d=0) <=> Evt(f=2/s=3/e=1/d=2)
|
||||
# ^ what you configured ^ what actually happened
|
||||
# f=fingers, s=direction, e=edge, d=distance. Direction enum:
|
||||
# 0=DU (down-to-up) 1=UD 2=LR (left-to-right) 3=RL (right-to-left)
|
||||
#
|
||||
# If Cfg and Evt agree on s= but differ on f=, your panel reports more contacts
|
||||
# than you configured -- add them to FINGER_COUNTS in the config.
|
||||
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 configured}"
|
||||
: "${SCREEN_W:=1024}"
|
||||
: "${SCREEN_H:=600}"
|
||||
: "${SWIPE_THRESHOLD:=80}"
|
||||
: "${SWIPE_LENIENCY:=30}"
|
||||
|
||||
echo "=============================================================="
|
||||
echo "STAGE 1 -- is the panel delivering events at all?"
|
||||
echo "Touch and drag on the TOUCHSCREEN for the next 6 seconds..."
|
||||
echo "=============================================================="
|
||||
bytes=$(timeout 6 cat "$TOUCH_DEV" 2>/dev/null | wc -c)
|
||||
echo
|
||||
echo " read $bytes bytes from $TOUCH_DEV"
|
||||
if [ "$bytes" -eq 0 ]; then
|
||||
cat <<-EOF
|
||||
-> NOTHING. Stage 2 cannot work. Check, in order:
|
||||
* are you in the 'input' group? (id -nG | grep input; needs a relogin)
|
||||
* is TOUCH_DEV the right device? (tools/find-device.sh)
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
echo " -> device is live."
|
||||
echo
|
||||
|
||||
echo "=============================================================="
|
||||
echo "STAGE 2 -- what does lisgd actually see?"
|
||||
echo "All four directions are bound, for 1, 2 and 3 contacts."
|
||||
echo "Swipe LEFT, RIGHT, UP, DOWN. Ctrl-C when done."
|
||||
echo "=============================================================="
|
||||
set --
|
||||
for f in 1 2 3; do
|
||||
for g in RL LR DU UD; do
|
||||
set -- "$@" -g "$f,$g,*,*,R,echo \" >>> FIRED: ${f}-contact $g\""
|
||||
done
|
||||
done
|
||||
exec lisgd -v -d "$TOUCH_DEV" -w "$SCREEN_W" -h "$SCREEN_H" \
|
||||
-t "$SWIPE_THRESHOLD" -r "$SWIPE_LENIENCY" "$@"
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Identify the touchscreen and print the stable by-id path for config.env.
|
||||
set -eu
|
||||
|
||||
echo "=== input devices reporting a touch-ish name ==="
|
||||
found=
|
||||
for ev in /dev/input/event*; do
|
||||
name=$(cat "/sys/class/input/$(basename "$ev")/device/name" 2>/dev/null || true)
|
||||
case "$name" in
|
||||
*[Tt]ouch* | *TOUCH*)
|
||||
found=yes
|
||||
echo
|
||||
echo " device: $ev"
|
||||
echo " name: $name"
|
||||
for l in /dev/input/by-id/* /dev/input/by-path/*; do
|
||||
[ -e "$l" ] || continue
|
||||
if [ "$(readlink -f "$l")" = "$(readlink -f "$ev")" ]; then
|
||||
echo " stable: $l"
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$found" ]; then
|
||||
echo " (none matched by name)"
|
||||
echo
|
||||
echo "Fall back to listing everything:"
|
||||
for ev in /dev/input/event*; do
|
||||
printf ' %-22s %s\n' "$ev" "$(cat "/sys/class/input/$(basename "$ev")/device/name" 2>/dev/null)"
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Put the 'stable:' path (prefer /dev/input/by-id/) into TOUCH_DEV."
|
||||
echo "Event numbers change across reboots; by-id symlinks do not."
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Run the real gesture daemon in the foreground so you can watch the display
|
||||
# while you swipe. Ctrl-C to stop.
|
||||
#
|
||||
# Expect: swipe right-to-left -> zooms in one host at a time
|
||||
# swipe left-to-right -> walks back out to the overview
|
||||
# "Execute ..." in the output means a gesture matched and fired.
|
||||
#
|
||||
# Pass --replace to take over from an already-running daemon; without it,
|
||||
# socktop-gestures refuses to start a second instance (which would make every
|
||||
# swipe fire twice).
|
||||
set -eu
|
||||
exec "$(dirname "$0")/../socktop-gestures" -v "$@"
|
||||
+61
-41
@@ -1,56 +1,76 @@
|
||||
#!/bin/sh
|
||||
# Remove socktop-swipe. Leaves lisgd, the 'input' group membership and your
|
||||
# socktop profiles alone -- those are useful independently and removing them
|
||||
# could break other things.
|
||||
# Remove socktop-swipe.
|
||||
#
|
||||
# ./uninstall.sh remove scripts, config and the X ignore rule
|
||||
# ./uninstall.sh --keep-config leave the config file in place
|
||||
# ./uninstall.sh binary, config, udev rule, X snippets, autologin
|
||||
# ./uninstall.sh --keep-config leave the config file alone
|
||||
#
|
||||
# The Rust toolchain, tmux, socktop and friends, and your 'input' group
|
||||
# membership are all left in place: they are useful independently, and removing
|
||||
# them could break something else. What was skipped is printed at the end.
|
||||
set -eu
|
||||
|
||||
PREFIX=${PREFIX:-/usr/local}
|
||||
BIN="$PREFIX/bin"
|
||||
CONF="$PREFIX/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
|
||||
BIN="$PREFIX/bin/socktop-swipe"
|
||||
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
|
||||
XIGNORE=/etc/X11/xorg.conf.d/99-ignore-touch-socktop-swipe.conf
|
||||
LIGHTDM=/etc/lightdm/lightdm.conf.d/50-autologin-socktop-swipe.conf
|
||||
UNIT="$HOME/.config/systemd/user/socktop-swipe.service"
|
||||
|
||||
keep_config=no
|
||||
[ "${1:-}" = "--keep-config" ] && keep_config=yes
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--keep-config) keep_config=yes ;;
|
||||
-h | --help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "unknown option: $a" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Stop anything running. -x matches the exact process name; -f would also match
|
||||
# this script's own command line and kill the shell running it.
|
||||
pkill -x lisgd 2>/dev/null || true
|
||||
say() { printf '%s\n' "$*"; }
|
||||
|
||||
for f in socktop-rack socktop-swipe socktop-gestures; do
|
||||
if [ -e "$BIN/$f" ]; then
|
||||
sudo rm -f "$BIN/$f"
|
||||
echo "removed $BIN/$f"
|
||||
# Stop it first, so nothing is holding the touch device or the tmux session.
|
||||
if command -v systemctl >/dev/null 2>&1 && [ -e "$UNIT" ]; then
|
||||
systemctl --user disable --now socktop-swipe 2>/dev/null || true
|
||||
rm -f "$UNIT"
|
||||
systemctl --user daemon-reload 2>/dev/null || true
|
||||
say "removed $UNIT"
|
||||
fi
|
||||
pkill -x socktop-swipe 2>/dev/null || true
|
||||
|
||||
# Session name comes from the config, which may be about to be deleted.
|
||||
if [ -r "$CONF" ] && command -v tmux >/dev/null 2>&1; then
|
||||
session=$(sed -n 's/^session:[[:space:]]*//p' "$CONF" | head -1)
|
||||
[ -n "${session:-}" ] || session=socktop-swipe
|
||||
tmux kill-session -t "$session" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
for f in "$BIN" "$UDEV" "$XBLANK" "$XIGNORE" "$LIGHTDM"; do
|
||||
if [ -e "$f" ]; then
|
||||
sudo rm -f "$f"
|
||||
say "removed $f"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -e "$UDEV" ] || command -v udevadm >/dev/null 2>&1; then
|
||||
sudo udevadm control --reload 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ "$keep_config" = no ] && [ -e "$CONF" ]; then
|
||||
sudo rm -f "$CONF"
|
||||
echo "removed $CONF"
|
||||
rm -f "$CONF"
|
||||
rmdir "$CONF_DIR" 2>/dev/null || true
|
||||
say "removed $CONF"
|
||||
fi
|
||||
|
||||
if [ -e "$XCONF" ]; then
|
||||
sudo rm -f "$XCONF"
|
||||
echo "removed $XCONF (touch returns to X after the next X restart)"
|
||||
fi
|
||||
|
||||
if [ -e "$BCONF" ]; then
|
||||
sudo rm -f "$BCONF"
|
||||
echo "removed $BCONF (screen blanking returns after the next X restart)"
|
||||
fi
|
||||
|
||||
if [ -e "$LCONF" ]; then
|
||||
sudo rm -f "$LCONF"
|
||||
echo "removed $LCONF (login prompt returns at next boot)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Left in place on purpose:"
|
||||
echo " * lisgd -- remove with: sudo make -C ~/src/lisgd uninstall"
|
||||
echo " * 'input' group -- remove with: sudo gpasswd -d $(id -un) input"
|
||||
echo " * i3 autostart -- delete the 'socktop-swipe' block in ~/.config/i3/config"
|
||||
echo " * socktop profiles -- ~/.config/socktop/profiles.json"
|
||||
say ""
|
||||
say "Left in place on purpose:"
|
||||
say " * the Rust toolchain -- remove with: rustup self uninstall"
|
||||
say " * tmux -- your package manager installed it"
|
||||
say " * socktop and friends -- cargo uninstall socktop uptime-kuma-status"
|
||||
say " * the source checkout -- rm -rf ~/.local/src/socktop-swipe"
|
||||
say " * 'input' group -- sudo gpasswd -d $(id -un) input"
|
||||
say " * i3 autostart -- delete the 'socktop-swipe' block in ~/.config/i3/config"
|
||||
say ""
|
||||
say "Screen blanking and the login prompt come back after the next X restart"
|
||||
say "or reboot."
|
||||
|
||||
Reference in New Issue
Block a user