mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-23 12:00:46 +02:00
Rename spotwatch to frunner
The search for apps and remote displays is now frunner everywhere: the frunner/ folder, ft-frunner, the @ft_frunner socket, ft-screens' "frunner on|off", the relay's frunner action, docs/frunner.md, the tour and Input Settings labels. ft-spotpanel and spotsearch.py keep their names. Spotwatch shipped in releases since v0.3.0-exp.8, so old state still works: - rules files that bind "spotwatch" mean frunner (the relay and Input Settings read it as frunner; Input Settings saves the new name) - use counts are read from ~/.config/frametop-spotwatch.json until the first save to frametop-frunner.json - the desktop's session removes the old frametop-spotwatch.desktop autostart Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e709c7dbd9
commit
9f330089a3
29 files changed
+211
-168
No files matched your search
@@ -41,7 +41,7 @@ You need a Steam Frame with an internet connection, a keyboard (Bluetooth, or th
|
||||
|
||||
After the restart, Launch a program → Desktop opens the multi-screen desktop, with its screens arranged around where you're facing. Frametop Display Settings and Frametop Input Settings are in the desktop's application menu, under Settings. SteamOS's own single-screen desktop is still there, as Native Desktop in the same list.
|
||||
|
||||
The first time the desktop starts, Frametop Tour opens: a page at a time on the screens, the keyboard shortcuts, eye and hand tracking, spotwatch, floating windows, pins, profiles, and pausing for VR games, with buttons to try each one. Closing it, skipping it, or finishing it keeps it from opening again. It's in the application menu, in spotwatch, in SteamVR's Launch a program list, and in Frametop Input Settings' menu for whenever you want it again, and `tour/ft-tour --reset` has it open at the desktop's next start.
|
||||
The first time the desktop starts, Frametop Tour opens: a page at a time on the screens, the keyboard shortcuts, eye and hand tracking, frunner, floating windows, pins, profiles, and pausing for VR games, with buttons to try each one. Closing it, skipping it, or finishing it keeps it from opening again. It's in the application menu, in frunner, in SteamVR's Launch a program list, and in Frametop Input Settings' menu for whenever you want it again, and `tour/ft-tour --reset` has it open at the desktop's next start.
|
||||
|
||||
If you work in the desktop for long stretches, or leave the headset on a stand, open Frametop Display Settings → Power. Turn on Stay awake while plugged in: by default Steam puts the Frame to sleep after an hour without input, even while it charges. And choose when the displays turn off while the headset isn't used. SteamVR turns them off a few seconds after you take the headset off, but a stand or mount that covers the proximity sensor inside it makes the headset seem worn, and its displays stay on all night.
|
||||
|
||||
@@ -66,7 +66,7 @@ If you work in the desktop for long stretches, or leave the headset on a stand,
|
||||
| Set a screen to On your head (Frametop Display Settings, Visibility & pins) | Pins it to your head where it is, like a HUD. Grab its bar to move it; it stays on your head |
|
||||
| Meta+Shift+R in the desktop | Puts the screens back in their layout (also in the menu as Reset Screen Layout, and mappable to a mouse button) |
|
||||
| Meta+Alt+Tab, or Meta+Alt+Shift+Tab | Spins every screen and floating window around you together, like a lazy susan, so the next one on your right (or left) glides to straight ahead, with the pointer and typing going to it. Their arrangement stays the same: the room turns instead of you. Tap again to keep going; Meta+Shift+R puts the screens back. Pinned screens stay where they are |
|
||||
| Meta+Space, on any keyboard | Opens spotwatch, a search pill in front of you: type an app's or a remote display's name and press Enter. A running app's window spins to the front, an app that isn't running opens floating, and a remote display connects and spins to the front. Shift+Enter (or a right click) has more: a private window, a new window, docked on this screen, or one of its windows. Esc closes it. See [docs/spotwatch.md](docs/spotwatch.md) |
|
||||
| Meta+Space, on any keyboard | Opens frunner, a search pill in front of you: type an app's or a remote display's name and press Enter. A running app's window spins to the front, an app that isn't running opens floating, and a remote display connects and spins to the front. Shift+Enter (or a right click) has more: a private window, a new window, docked on this screen, or one of its windows. Esc closes it. See [docs/frunner.md](docs/frunner.md) |
|
||||
| Meta+Shift+H in the desktop | Hides or shows all screens (also in the menu as Hide/Show Screens, and mappable). The Visibility & pins tab of Frametop Display Settings can instead show them only with the dashboard open, or while you look at your wrist |
|
||||
| Tap Meta, on any keyboard | Opens the Steam menu in the SteamVR dashboard, or closes the dashboard, wherever you are. The desktop's launcher is still on the taskbar and Alt+F1. Change it in Frametop Input Settings (Keyboard page), where any key combination or modifier tap can do a Frametop or Steam action, open a profile, or run a command of your own |
|
||||
| Switch a screen to Hidden (Frametop Display Settings, Visibility & pins → Screens shown) | Hides just that screen until you switch it back, whatever the other visibility settings say; new windows that would open on it float instead |
|
||||
|
||||
@@ -1029,7 +1029,7 @@ Kirigami.ApplicationWindow {
|
||||
Repeater {
|
||||
model: [
|
||||
{ value: "steamvr", text: "Open as SteamVR windows", help: "Apps picked in the dashboard's Launch a program list open as SteamVR's own windows, as without Frametop." },
|
||||
{ value: "frametop", text: "Open as Frametop floating windows", help: "They open in the Frametop desktop instead, floating in front of you (where that app last floated), so drag and drop, the clipboard, spotwatch and profiles work with them. When the desktop isn't running, Frametop is paused, or all floating windows are in use, they open as SteamVR windows. Steam, SteamVR and Frametop's own layout entries always stay SteamVR's." }
|
||||
{ value: "frametop", text: "Open as Frametop floating windows", help: "They open in the Frametop desktop instead, floating in front of you (where that app last floated), so drag and drop, the clipboard, frunner and profiles work with them. When the desktop isn't running, Frametop is paused, or all floating windows are in use, they open as SteamVR windows. Steam, SteamVR and Frametop's own layout entries always stay SteamVR's." }
|
||||
]
|
||||
delegate: ColumnLayout {
|
||||
required property var modelData
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Spotwatch
|
||||
# Frunner
|
||||
|
||||
Spotwatch is Frametop's search for apps and remote displays, like Spotlight on a Mac. Press Meta+Space and a glass search pill opens straight ahead of where your head points. Type part of an app's name, or a remote display's `{host} - {display}`, and pick from what it finds:
|
||||
Frunner is Frametop's search for apps and remote displays, like Spotlight on a Mac. Press Meta+Space and a glass search pill opens straight ahead of where your head points. Type part of an app's name, or a remote display's `{host} - {display}`, and pick from what it finds:
|
||||
|
||||
- **An app that's already running:** its most recent window comes to you. The lazy susan spins the panel it's on, a screen or its own floating window, to straight ahead, and typing goes to it.
|
||||
- **An app that isn't running:** it starts as a floating window, where that app last floated or in front of you.
|
||||
- **A remote display:** spotwatch connects it if needed, then spins it to the front.
|
||||
- **A remote display:** frunner connects it if needed, then spins it to the front.
|
||||
|
||||
Use Tab, Shift+Tab, or the arrow keys to move through the results, and Enter or a click to open one. Shift+Enter or a right click opens the item's menu:
|
||||
|
||||
@@ -13,13 +13,13 @@ Use Tab, Shift+Tab, or the arrow keys to move through the results, and Enter or
|
||||
|
||||
Esc goes back from a menu and closes the search. Meta+Space again, or a click anywhere else, closes it too.
|
||||
|
||||
Spotwatch works while the Frametop desktop is running and Frametop isn't paused for a VR game. It's the `spotwatch` action in Frametop Input Settings, so a mouse button, a controller button, or another key combination can open it too. When it opens from a mouse or controller, Frametop's keyboard opens with it.
|
||||
Frunner works while the Frametop desktop is running and Frametop isn't paused for a VR game. It's the `frunner` action in Frametop Input Settings, so a mouse button, a controller button, or another key combination can open it too. When it opens from a mouse or controller, Frametop's keyboard opens with it.
|
||||
|
||||
## Not built
|
||||
|
||||
- Searching open windows by title, Frametop's own actions ("Reset layout"), and profiles (decision 4).
|
||||
- Moving the caret within the query. Typing and Backspace work at the end only.
|
||||
- A setting for its distance and size (`--distance` and `--width` on ft-spotpanel; 0.75 m and 0.42 m, `spotwatch/reserve.h`).
|
||||
- A setting for its distance and size (`--distance` and `--width` on ft-spotpanel; 0.75 m and 0.42 m, `frunner/reserve.h`).
|
||||
|
||||
## Decisions
|
||||
|
||||
@@ -33,24 +33,24 @@ These were settled with the user on 2026-10-10. The numbers may be cited in the
|
||||
| 4 | What it searches | Apps and remote displays only. Hidden remote displays are left out, and disconnected ones show with Connect |
|
||||
| 5 | Disconnected remote displays | Enter connects the display (`ft-layout remote connect`), waits for its panel, then spins to it |
|
||||
| 6 | When it works | Only while Frametop is running and active: the session starts it, and the input relay doesn't run the action while Frametop is paused. A pause while it's open closes it |
|
||||
| 7 | Without a keyboard | `spotwatch` is an action like the others, so it can be bound to a mouse button or a controller button. Opened that way, Frametop's keyboard opens too and closes with it |
|
||||
| 7 | Without a keyboard | `frunner` is an action like the others, so it can be bound to a mouse button or a controller button. Opened that way, Frametop's keyboard opens too and closes with it |
|
||||
| 8 | The menu | The app's desktop actions, Open new window (the app's own new-window action when it has one, else a plain launch), Open docked on this screen (the screen nearest straight ahead), and its windows |
|
||||
| 9 | An app that isn't running | It floats, where ft-floatd puts any launched app (where it last floated, or in front of you) |
|
||||
| 10 | A window on a pinned panel | Pinned panels don't spin. Typing goes to it where it is (`front N`), and a short note says where it is ("on your left wrist") |
|
||||
| 11 | Shortcut | Meta+Space by default. Plasma's KRunner has Alt+Space, and nothing in Frametop used Meta+Space |
|
||||
| 12 | Branch | `spotwatch`, off `experimental`, merged into `experimental` |
|
||||
| 12 | Branch | `spotwatch` (its first name; renamed frunner 2026-10-20), off `experimental`, merged into `experimental` |
|
||||
|
||||
## How it works
|
||||
|
||||
Spotwatch has three parts, and needs changes in ft-screens, ft-floatd and the input relay:
|
||||
Frunner has three parts, and needs changes in ft-screens, ft-floatd and the input relay:
|
||||
|
||||
- **`spotwatch/ft-spotwatch`** (`ft_spotwatch.py`) runs inside the desktop's Plasma session, started from its autostart like ft-floatd, so it's up exactly while the desktop is. It keeps the app index (Gio's, so it sees what the desktop's menus see), the remote displays from the layout, the usage counts, and the search state. It decides what each key and click does. Its log is `/tmp/frametop-spotwatch.log`.
|
||||
- **`spotwatch/ft-spotpanel.cpp`** is the pill: a SteamVR overlay drawn on the CPU, like ft-gazepanel, built by `screens/build.sh` and run by ft-spotwatch in the container. It holds no state beyond the picture. ft-spotwatch sends it the whole state each time, and it sends back hovers, clicks, right clicks, scrolls, and clicks beside the card. Its clear area catches those clicks.
|
||||
- **`spotwatch/spotsearch.py`** ranks the results with plain Python, so its test runs anywhere. Name matches come first, from the strongest kind of match to the weakest: the name's start, a word's start, initials, anywhere in the name, then letters in order. Generic names, keywords, and program names count for less and never match fuzzily. Every word of the query must match. Use counts and use in the last day raise an item, and so does an app that's running.
|
||||
- **`frunner/ft-frunner`** (`ft_frunner.py`) runs inside the desktop's Plasma session, started from its autostart like ft-floatd, so it's up exactly while the desktop is. It keeps the app index (Gio's, so it sees what the desktop's menus see), the remote displays from the layout, the usage counts, and the search state. It decides what each key and click does. Its log is `/tmp/frametop-frunner.log`.
|
||||
- **`frunner/ft-spotpanel.cpp`** is the pill: a SteamVR overlay drawn on the CPU, like ft-gazepanel, built by `screens/build.sh` and run by ft-frunner in the container. It holds no state beyond the picture. ft-frunner sends it the whole state each time, and it sends back hovers, clicks, right clicks, scrolls, and clicks beside the card. Its clear area catches those clicks.
|
||||
- **`frunner/spotsearch.py`** ranks the results with plain Python, so its test runs anywhere. Name matches come first, from the strongest kind of match to the weakest: the name's start, a word's start, initials, anywhere in the name, then letters in order. Generic names, keywords, and program names count for less and never match fuzzily. Every word of the query must match. Use counts and use in the last day raise an item, and so does an app that's running.
|
||||
|
||||
The search has to take the typing from wherever it went, Steam included. While it's open, ft-screens sends every key to `@ft_spotwatch` instead of KWin or a remote display's stream, from keyboards and Frametop's keyboard alike, and tells the input relay that typing goes to the desktop, so the relay grabs the keyboards. ft-spotwatch holds this with `spotwatch on`, renewed every second. ft-screens lets it lapse after 3 seconds, so a crashed ft-spotwatch can't keep the keys. A click on any other panel, or a pause, ends it, and ft-screens says `dismiss` so the pill closes.
|
||||
The search has to take the typing from wherever it went, Steam included. While it's open, ft-screens sends every key to `@ft_frunner` instead of KWin or a remote display's stream, from keyboards and Frametop's keyboard alike, and tells the input relay that typing goes to the desktop, so the relay grabs the keyboards. ft-frunner holds this with `frunner on`, renewed every second. ft-screens lets it lapse after 3 seconds, so a crashed ft-frunner can't keep the keys. A click on any other panel, or a pause, ends it, and ft-screens says `dismiss` so the pill closes.
|
||||
|
||||
ft-spotwatch turns key codes into text on the desktop's keyboard layout (Plasma's `kxkbrc`) through libxkbcommon, and falls back to a US layout without it. The input relay sends no key repeats, so ft-spotwatch repeats a held Backspace or arrow key itself.
|
||||
ft-frunner turns key codes into text on the desktop's keyboard layout (Plasma's `kxkbrc`) through libxkbcommon, and falls back to a US layout without it. The input relay sends no key repeats, so ft-frunner repeats a held Backspace or arrow key itself.
|
||||
|
||||
Bringing a window to the front takes three commands:
|
||||
|
||||
@@ -60,13 +60,13 @@ Bringing a window to the front takes three commands:
|
||||
|
||||
ft-floatd's `spotlist` lists every app window with its desktop file name, title, panel, and when it was last active. The KWin script keeps that timestamp.
|
||||
|
||||
Launching uses ft-floatd's `launch APP.desktop`, which now takes `action=ACTION` (one of the desktop file's actions) and `screen=N` (open docked on screen N; the KWin script's `to-output`). Without ft-floatd (`FLOAT_SLOTS=0`), spotwatch launches through Gio itself, and nothing counts as running.
|
||||
Launching uses ft-floatd's `launch APP.desktop`, which now takes `action=ACTION` (one of the desktop file's actions) and `screen=N` (open docked on screen N; the KWin script's `to-output`). Without ft-floatd (`FLOAT_SLOTS=0`), frunner launches through Gio itself, and nothing counts as running.
|
||||
|
||||
Apps' icons come from the desktop's icon theme (`kdeglobals`, then breeze and hicolor). GdkPixbuf rasterizes them to 64x64 RGBA files in `~/.cache/frametop/spotwatch` for the panel, one at a time in idle moments after the desktop starts. Use counts are kept in `~/.config/frametop-spotwatch.json`.
|
||||
Apps' icons come from the desktop's icon theme (`kdeglobals`, then breeze and hicolor). GdkPixbuf rasterizes them to 64x64 RGBA files in `~/.cache/frametop/frunner` for the panel, one at a time in idle moments after the desktop starts. Use counts are kept in `~/.config/frametop-frunner.json`.
|
||||
|
||||
```
|
||||
spotwatch/test/spotwatch-test.py # the ranking, then ft-spotwatch against a fake ft-screens and ft-floatd (offline)
|
||||
spotwatch/tools/render.py OUT.png # what the panel draws, without a headset (ft-spotpanel --render)
|
||||
frunner/test/frunner-test.py # the ranking, then ft-frunner against a fake ft-screens and ft-floatd (offline)
|
||||
frunner/tools/render.py OUT.png # what the panel draws, without a headset (ft-spotpanel --render)
|
||||
```
|
||||
|
||||
## Risks
|
||||
+8
-8
@@ -98,12 +98,12 @@ cutouts matte circle|mesh|capsules cutouts margin deg [ms [deg]] cutouts s
|
||||
hands L R allowed handinput on|off|force|state palmmenu on|off|state grabthat on|off|state
|
||||
float N mpp x y w h title unfloat N pose N matrix sub N k x y w h | sub N k off minimized N 0|1 carry N
|
||||
rates focused in_view hidden rates? watch seconds phase ms
|
||||
spin next|prev|DEGREES|to N front N spotwatch on|off
|
||||
spin next|prev|DEGREES|to N front N frunner on|off
|
||||
```
|
||||
|
||||
Each screen draws at a frame rate for how much of it you see. KWin draws a screen only after ft-screens gives it a frame callback, and its apps wait for theirs, so the rate of callbacks is the screen's frame rate, for KWin and the apps on it alike. A screen is focused while you look at it (within 12 degrees of where your head points), while a laser or the mouse is on it or was in the last 1.5 seconds, while it's carried, and while you type on it; it gets every display frame. The rest of what you can see (within 60 degrees) gets 15 frames a second, and a hidden screen, one behind you, and everything while paused get one a second. A level goes up at once and comes down after a moment (1.5 s from focused, 0.5 s from in view). A video, or anything moving over a large part of a screen (6% or more of it, redrawn on 8 commits in a row, 10 or more a second), keeps every frame while in view. A floating window is a screen of its own here. Nothing that stands still costs anything at any rate: KWin sends a frame only when something on the screen changed. `rates F V H` sets the three rates in Hz (0: every display frame; default `0 15 1`, also `ft-screens --rates 0,15,1`), and `rates?` shows them, the display's rate, and for each screen its level, its milliseconds between frames, and whether it counts as a video. `watch S` gives every screen full rate for S seconds: remote desktop renews it while a VNC client is connected, since a viewer sees what KWin draws. The ticks (SteamVR events and the callbacks) come once per display frame, 1 ms after the vsync (`phase ms` changes that, for tuning), in step with the display rather than on a timer that drifted through the frame.
|
||||
|
||||
`spin` is the lazy susan. By default it brings panels to a front slot at reading distance and leaves the rest where they are: `next` and `prev` bring the next one round on the right or left (the one in the slot goes home), and `to N` brings panel N. With `SCREEN_SPIN_BRING=0` in `~/.config/frametop.conf` it turns every panel in the room about your head instead: `next` and `prev` bring the next one on the right or left to straight ahead, and `to N` turns the short way to panel N. Either way, a number of degrees turns the room that far. Typing goes to the panel that ends up in front. `front N` gives panel N the typing where it is, as a spin to it would. `spotwatch on`, renewed every second, gives spotwatch's search the typing, and `spotwatch off` gives it back ([Spotwatch](#spotwatch)).
|
||||
`spin` is the lazy susan. By default it brings panels to a front slot at reading distance and leaves the rest where they are: `next` and `prev` bring the next one round on the right or left (the one in the slot goes home), and `to N` brings panel N. With `SCREEN_SPIN_BRING=0` in `~/.config/frametop.conf` it turns every panel in the room about your head instead: `next` and `prev` bring the next one on the right or left to straight ahead, and `to N` turns the short way to panel N. Either way, a number of degrees turns the room that far. Typing goes to the panel that ends up in front. `front N` gives panel N the typing where it is, as a spin to it would. `frunner on`, renewed every second, gives frunner's search the typing, and `frunner off` gives it back ([Frunner](#frunner)).
|
||||
|
||||
`conceal` and `reveal` hide and show one screen on its own (`ft-layout hide` and `show` send them), and `concealed` lists those screens. `pause on` (from the input relay, when Frametop pauses for a VR game) hides every screen and floating window whatever else says, and slows the desktop down; `pause off` undoes it. `cutouts` turns the hand cutouts on and off (`ft-handsctl cutouts`), and sets their matte, its margin and the per-distance correction (hands/README.md, "Hand cutouts and near input"); they cut only with Room View on, detected or forced (`cutouts roomview`, hands/README.md "Rendered hands"). `hands` is the pointer helper's word on hand input (each side `hand`, `controller` or `off`, and whether it's allowed now); `handinput` turns near input on (following that word), off, or on without it, and `handinput state` reports what each hand is doing, its counts (presses, one-frame presses, pinch clicks, grabs by card and fist, poke attempts and those refused, crossings off the content, presses at the edge, flicks and throws, holds through tracking loss, second hands joining a held panel, pinches that waited to become fists, buttons pinched) and what held presses back; each press, release, refused poke and grab also goes to ft-screens' log as a `hands:` line. `palmmenu` turns the palm menu (a bare hand palm up, pinch: the Steam menu) on and off, and `palmmenu state` reports it. `grabthat` turns "grab that" (an open hand held out to a panel farther than arm's reach, then a fist pulled back: it comes to arm's reach) on and off, and `grabthat state` reports each hand's state, its counts and the panels that came. The last line is ft-floatd's, for floating windows: N is a floating window's panel, numbered on from the screens, one per spare output. `float` gives the window's rectangle in its output, metres per pixel, and the title bar's height, and shows the panel; `unfloat` hides it. `pose` places it (a 3x4 matrix, standing universe), `sub` shows popup or dialog k over it, `minimized` hides it while its window is minimized, and `carry` moves it with the laser that pressed the window's own title bar.
|
||||
|
||||
@@ -238,20 +238,20 @@ float/ft-float activate ID # make a window the active one, unminimize
|
||||
float/ft-float dashlaunch CMDLINE # what ft-dashroute sends for a dashboard launch: ok, or steamvr REASON
|
||||
```
|
||||
|
||||
## Spotwatch
|
||||
## Frunner
|
||||
|
||||
Meta+Space opens spotwatch, a search pill straight ahead of where your head points, for apps and remote displays (`{host} - {display}`). Tab, Shift+Tab, and the arrow keys move through what it finds, and Enter or a click opens it. A running app's most recent window spins to the front with the lazy susan. An app that isn't running starts floating. A remote display connects if needed and spins to the front. Shift+Enter or a right click opens an item's menu: an app's own actions (a private window), Open new window, Open docked on this screen, and its windows; a remote display's Go to and Disconnect. Esc, Meta+Space again, or a click elsewhere closes it. It's the `spotwatch` action in Frametop Input Settings, bindable to mouse and controller buttons too, and opened that way it brings Frametop's keyboard. [spotwatch.md](spotwatch.md) has the decisions and how it works.
|
||||
Meta+Space opens frunner, a search pill straight ahead of where your head points, for apps and remote displays (`{host} - {display}`). Tab, Shift+Tab, and the arrow keys move through what it finds, and Enter or a click opens it. A running app's most recent window spins to the front with the lazy susan. An app that isn't running starts floating. A remote display connects if needed and spins to the front. Shift+Enter or a right click opens an item's menu: an app's own actions (a private window), Open new window, Open docked on this screen, and its windows; a remote display's Go to and Disconnect. Esc, Meta+Space again, or a click elsewhere closes it. It's the `frunner` action in Frametop Input Settings, bindable to mouse and controller buttons too, and opened that way it brings Frametop's keyboard. [frunner.md](frunner.md) has the decisions and how it works.
|
||||
|
||||
`spotwatch/ft-spotwatch` runs inside the desktop's Plasma session (log: `/tmp/frametop-spotwatch.log`) and runs the pill, `ft-spotpanel` (built by `screens/build.sh`), in the container. While the search is open, ft-screens sends it every key. Use counts are in `~/.config/frametop-spotwatch.json` and icons in `~/.cache/frametop/spotwatch`.
|
||||
`frunner/ft-frunner` runs inside the desktop's Plasma session (log: `/tmp/frametop-frunner.log`) and runs the pill, `ft-spotpanel` (built by `screens/build.sh`), in the container. While the search is open, ft-screens sends it every key. Use counts are in `~/.config/frametop-frunner.json` and icons in `~/.cache/frametop/frunner`.
|
||||
|
||||
```
|
||||
spotwatch/test/spotwatch-test.py # the ranking and the search against a fake ft-screens and ft-floatd (offline)
|
||||
spotwatch/tools/render.py OUT.png # the panel's picture, without a headset
|
||||
frunner/test/frunner-test.py # the ranking and the search against a fake ft-screens and ft-floatd (offline)
|
||||
frunner/tools/render.py OUT.png # the panel's picture, without a headset
|
||||
```
|
||||
|
||||
## Frametop Tour
|
||||
|
||||
`tour/ft-tour` (in the app menu, spotwatch, and SteamVR's Launch a program list as Frametop Tour, and in Frametop Input Settings' menu) walks through Frametop a page at a time: the screens and the 3D mouse, the key combinations the relay has now (`key_bindings`, as Frametop Input Settings shows them) and macros, gaze mode with Meta+J and Meta+K, hand tracking with and without gaze, spotwatch, floating windows (with the Apps from the SteamVR dashboard choice), pinning, profiles, pausing for VR games, and the rest. Each page has buttons that try things (spotwatch, a spin, a calibration or quick check) or open their settings page (`FT_INPUT_PAGE`, `FT_DISPLAY_PAGE`); the gaze and hands pages have a button to practise clicking on. A part that isn't installed says how to install it. A Kirigami app with a Python backend (`tour/ft_tour.py`), in the container like Frametop Input Settings; `FT_TOUR_PAGE=ID` opens it on a page.
|
||||
`tour/ft-tour` (in the app menu, frunner, and SteamVR's Launch a program list as Frametop Tour, and in Frametop Input Settings' menu) walks through Frametop a page at a time: the screens and the 3D mouse, the key combinations the relay has now (`key_bindings`, as Frametop Input Settings shows them) and macros, gaze mode with Meta+J and Meta+K, hand tracking with and without gaze, frunner, floating windows (with the Apps from the SteamVR dashboard choice), pinning, profiles, pausing for VR games, and the rest. Each page has buttons that try things (frunner, a spin, a calibration or quick check) or open their settings page (`FT_INPUT_PAGE`, `FT_DISPLAY_PAGE`); the gaze and hands pages have a button to practise clicking on. A part that isn't installed says how to install it. A Kirigami app with a Python backend (`tour/ft_tour.py`), in the container like Frametop Input Settings; `FT_TOUR_PAGE=ID` opens it on a page.
|
||||
|
||||
The desktop's autostart runs `ft-tour --first-run` (`frametop-tour.desktop`, written by `session/frametop-session.sh`; log: `/tmp/frametop-tour.log`), which opens it only while `~/.local/state/frametop/tour.json` is missing or from an older tour (`TOUR_VERSION`). Closing the tour, Skip the tour, and Finish write that file, unless Show this tour when the desktop starts is ticked on its last page. `ft-tour --reset` removes it.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const settled = {}; // id -> {output, frame, fullScreen, maximized}: where a
|
||||
const held = {}; // id -> {w, h, until, asked}: a size asked for, for a second (see hold)
|
||||
const moved = {}; // id -> true, or "back" once put back: it moved while KWin changed the outputs
|
||||
let layout = "", layoutOutputs = {}, layoutSince = 0; // the outputs at the last screensChanged
|
||||
const activeAt = {}; // id -> when it was last KWin's active window (ms), for spotwatch's most recent
|
||||
const activeAt = {}; // id -> when it was last KWin's active window (ms), for frunner's most recent
|
||||
|
||||
function send(ev) {
|
||||
callDBus(SERVICE, PATH, IFACE, "Event", JSON.stringify(ev));
|
||||
@@ -382,7 +382,7 @@ function run(c) {
|
||||
if (w && w.minimized) w.minimized = false;
|
||||
if (w) workspace.activeWindow = w;
|
||||
break;
|
||||
case "to-output": { // a launched window that opens docked, onto that screen (spotwatch)
|
||||
case "to-output": { // a launched window that opens docked, onto that screen (frunner)
|
||||
const o = outputByName(c.output);
|
||||
if (w && o && (!w.output || w.output.name !== o.name)) workspace.sendClientToScreen(w, o);
|
||||
if (w) workspace.activeWindow = w;
|
||||
|
||||
+4
-4
@@ -19,8 +19,8 @@ command-line side). Replies go to the sender:
|
||||
list, Steam's command line for it: launch it floating -> "ok", or "steamvr REASON"
|
||||
when it should be SteamVR's own window after all; see dash_launch)
|
||||
windows (-> JSON: the open apps' windows and where they are, for a profile; ft-layout save)
|
||||
spotlist (-> JSON: every app window, its title, panel, and when it was last active; spotwatch)
|
||||
activate ID (make that window KWin's active one, unminimized; spotwatch)
|
||||
spotlist (-> JSON: every app window, its title, panel, and when it was last active; frunner)
|
||||
activate ID (make that window KWin's active one, unminimized; frunner)
|
||||
profile NAME (open a profile's apps: move the matching windows, launch the missing ones)
|
||||
("float pointer" is the float key: the window under the pointer, else the active one,
|
||||
floated or docked; the input relay sends it for float_toggle, and "dock all" for dock_all)
|
||||
@@ -243,7 +243,7 @@ def recordable(ev):
|
||||
|
||||
|
||||
def listable(ev):
|
||||
"""An app's top-level window, the kind spotwatch finds: Frametop's own settings apps too,
|
||||
"""An app's top-level window, the kind frunner finds: Frametop's own settings apps too,
|
||||
which profiles leave out (an open Input Settings launched a second copy, 2026-10-13)."""
|
||||
if not ev.get("normal") or ev.get("popup") or ev.get("transient"):
|
||||
return False
|
||||
@@ -1002,7 +1002,7 @@ class Daemon:
|
||||
log(f"{what}: {e}")
|
||||
|
||||
def spot_entries(self):
|
||||
"""Every app window for spotwatch's search: its desktop file name, title, the panel
|
||||
"""Every app window for frunner's search: its desktop file name, title, the panel
|
||||
it's on (a screen or a floating window's; None elsewhere), and when it was last active."""
|
||||
out = []
|
||||
for wid, ev in self.windows.items():
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
# ft-frunner on the Frame host, inside the Frametop desktop's session (see ft_frunner.py).
|
||||
exec python3 "$(dirname "$(readlink -f "$0")")/ft_frunner.py" "$@"
|
||||
@@ -1,7 +1,7 @@
|
||||
// ft-spotpanel: spotwatch's search panel (spotwatch/ft-spotwatch drives it; docs/spotwatch.md).
|
||||
// ft-spotpanel: frunner's search panel (frunner/ft-frunner drives it; docs/frunner.md).
|
||||
// A SteamVR overlay that opens straight ahead of where your head points and stays there in the
|
||||
// room: a glass search pill, and under it a card of results, or of an app's launch options.
|
||||
// It's drawn on the CPU and holds no state of its own beyond the picture: ft-spotwatch sends it
|
||||
// It's drawn on the CPU and holds no state of its own beyond the picture: ft-frunner sends it
|
||||
// everything to show, and it sends back what the lasers and the 3D mouse do on it.
|
||||
//
|
||||
// "Glass" is faux: SteamVR's overlays have alpha but no backdrop blur, so the pill and the card
|
||||
@@ -11,7 +11,7 @@
|
||||
// Control socket: abstract unix datagram "@ft_spotpanel" (--socket NAME); a sender with an
|
||||
// address gets "ok" or "error ...":
|
||||
// show place it in front of you (head direction, at --distance: its reserved spot,
|
||||
// spotwatch/reserve.h) and show it with
|
||||
// frunner/reserve.h) and show it with
|
||||
// its next picture
|
||||
// show toast the same, for a toast: the lasers pass through it
|
||||
// hide
|
||||
@@ -23,19 +23,19 @@
|
||||
// row <kind> <icon> <title> <subtitle> <badge>
|
||||
// kind: item, header (a menu's top row: back on a click),
|
||||
// or section (a label, not selectable); icon: the path of
|
||||
// a 64x64 RGBA file ft-spotwatch made, or "-"
|
||||
// a 64x64 RGBA file ft-frunner made, or "-"
|
||||
// ping -> "ok shown" or "ok hidden"
|
||||
// To ft-spotwatch (@ft_spotwatch, --notify NAME): "hover <row>", "click <row>", "menu <row>"
|
||||
// To ft-frunner (@ft_frunner, --notify NAME): "hover <row>", "click <row>", "menu <row>"
|
||||
// (a right click), "back" (the header), "scroll <+1|-1>", and "outside" (a click on the panel
|
||||
// but on no row or the pill, which the panel's clear part catches: it closes the search).
|
||||
//
|
||||
// Pictures go into the next of three shared buffers (linear DMA-BUFs SteamVR imported once),
|
||||
// as screens/keyboard.cpp and gaze/panel/ft-gazepanel.cpp do; SetOverlayRaw is the fallback.
|
||||
//
|
||||
// Options: --watch-stdin (quit when stdin closes: ft-spotwatch runs it), --socket NAME,
|
||||
// Options: --watch-stdin (quit when stdin closes: ft-frunner runs it), --socket NAME,
|
||||
// --notify NAME, --distance METRES, --width METRES; --render FILE draws the state on stdin
|
||||
// (the lines after "state") into FILE as raw RGBA, 1120x720, and quits: no SteamVR (a look at
|
||||
// the picture: spotwatch/tools/render.py). Runs in the container (screens/build.sh
|
||||
// the picture: frunner/tools/render.py). Runs in the container (screens/build.sh
|
||||
// builds it into screens/build).
|
||||
#include <openvr.h>
|
||||
|
||||
@@ -248,7 +248,7 @@ void Magnifier(Canvas &c, double cx, double cy, Color k) {
|
||||
}
|
||||
}
|
||||
|
||||
// Icons: 64x64 RGBA files ft-spotwatch rasterized, kept once loaded.
|
||||
// Icons: 64x64 RGBA files ft-frunner rasterized, kept once loaded.
|
||||
std::map<std::string, std::vector<uint8_t>> g_icons;
|
||||
|
||||
const std::vector<uint8_t> *Icon(const std::string &path) {
|
||||
@@ -327,7 +327,7 @@ void Draw(Canvas &c, const State &s) {
|
||||
}
|
||||
const int tx = int(px0) + 92, tmax = int(px1 - px0) - 92 - 40, tsize = 44;
|
||||
if (s.query.empty()) {
|
||||
Text(c, "Spotwatch Search", tsize, tx, int(cy), kDim);
|
||||
Text(c, "Frunner Search", tsize, tx, int(cy), kDim);
|
||||
Round(c, tx - 2, cy - 24, tx + 1, cy + 24, 1.5, kAccent, 0.95); // the caret
|
||||
} else {
|
||||
// Long queries show their end, where you type.
|
||||
@@ -510,7 +510,7 @@ void ParseState(const std::string &text, State &s) {
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
bool watchStdin = false;
|
||||
std::string sockName = "ft_spotpanel", notifyName = "ft_spotwatch", renderPath;
|
||||
std::string sockName = "ft_spotpanel", notifyName = "ft_frunner", renderPath;
|
||||
double distance = spotreserve::kDistance, width = spotreserve::kWidth;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (!std::strcmp(argv[i], "--watch-stdin")) watchStdin = true;
|
||||
@@ -578,7 +578,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
vr::IVROverlay *ov = vr::VROverlay();
|
||||
vr::VROverlayHandle_t h = vr::k_ulOverlayHandleInvalid;
|
||||
if (ov->CreateOverlay("frametop.spotwatch", "Frametop spotwatch", &h) != vr::VROverlayError_None) {
|
||||
if (ov->CreateOverlay("frametop.frunner", "Frametop frunner", &h) != vr::VROverlayError_None) {
|
||||
std::fprintf(stderr, "ft-spotpanel: can't create the overlay (another copy running?)\n");
|
||||
return 1;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ft-spotwatch: spotwatch, Frametop's search for apps and remote displays (docs/spotwatch.md).
|
||||
"""ft-frunner: frunner, Frametop's search for apps and remote displays (docs/frunner.md).
|
||||
|
||||
Meta+Space (the input relay's spotwatch action) opens a search pill straight ahead of where your
|
||||
Meta+Space (the input relay's frunner action) opens a search pill straight ahead of where your
|
||||
head points. Type to find an app or a remote display ("{host} - {display}"); Tab, Shift+Tab,
|
||||
and the arrow keys move through what's found, and Enter or a click opens it:
|
||||
- an app that's running: its most recent window comes to the front. The lazy susan spins the
|
||||
@@ -15,13 +15,13 @@ Disconnect. Esc goes back from a menu, and closes the search.
|
||||
|
||||
Runs inside the Frametop desktop's Plasma session (its autostart, like ft-floatd), so it's up
|
||||
exactly while the desktop is, and starts apps with the desktop's environment. The pill is
|
||||
ft-spotpanel (spotwatch/ft-spotpanel.cpp, in the container), which this runs and keeps running.
|
||||
While the search is open, ft-screens sends it the typing ("spotwatch on", renewed every
|
||||
ft-spotpanel (frunner/ft-spotpanel.cpp, in the container), which this runs and keeps running.
|
||||
While the search is open, ft-screens sends it the typing ("frunner on", renewed every
|
||||
second; see compositor.c), from keyboards and Frametop's keyboard alike, so nothing reaches an
|
||||
app or Steam meanwhile.
|
||||
|
||||
Datagrams on @ft_spotwatch (--socket NAME); a sender with an address gets "ok ..." or "error":
|
||||
toggle [keyboard|mouse|left|right] the input relay's spotwatch action, and what pressed it:
|
||||
Datagrams on @ft_frunner (--socket NAME); a sender with an address gets "ok ..." or "error":
|
||||
toggle [keyboard|mouse|left|right] the input relay's frunner action, and what pressed it:
|
||||
from a mouse or controller, Frametop's keyboard opens too
|
||||
open [SOURCE] | close | status (status -> "ok open|closed list|menu <query>")
|
||||
rows -> "ok [titles]" (JSON): what's listed now, for tests
|
||||
@@ -31,11 +31,11 @@ Datagrams on @ft_spotwatch (--socket NAME); a sender with an address gets "ok ..
|
||||
hover N | click N | menu N | back | scroll +1|-1 | outside ft-spotpanel: the lasers and
|
||||
the 3D mouse on the panel (N: a row shown)
|
||||
|
||||
Usage counts (what you open often comes first) are in ~/.config/frametop-spotwatch.json, and
|
||||
the icons, rasterized for the panel, in ~/.cache/frametop/spotwatch. Log:
|
||||
/tmp/frametop-spotwatch.log.
|
||||
Usage counts (what you open often comes first) are in ~/.config/frametop-frunner.json, and
|
||||
the icons, rasterized for the panel, in ~/.cache/frametop/frunner. Log:
|
||||
/tmp/frametop-frunner.log.
|
||||
|
||||
Usage: ft-spotwatch [--socket NAME] [--panel-socket NAME] [--screens NAME] [--float NAME]
|
||||
Usage: ft-frunner [--socket NAME] [--panel-socket NAME] [--screens NAME] [--float NAME]
|
||||
[--no-panel]
|
||||
"""
|
||||
import argparse
|
||||
@@ -66,11 +66,12 @@ PANEL = os.path.join(REPO, "screens", "build", "ft-spotpanel")
|
||||
IN_BOX = os.path.join(REPO, "scripts", "in-box") # runs a program in the container it was built in
|
||||
FT_LAYOUT = os.path.join(REPO, "layout", "ft-layout")
|
||||
STANDALONE_ACTION = "frametop-standalone" # Launch as Standalone (float/ft_apps.py): what Enter does anyway
|
||||
USAGE_PATH = os.path.expanduser("~/.config/frametop-spotwatch.json")
|
||||
ICON_DIR = os.path.expanduser("~/.cache/frametop/spotwatch")
|
||||
USAGE_PATH = os.path.expanduser("~/.config/frametop-frunner.json")
|
||||
OLD_USAGE_PATH = os.path.expanduser("~/.config/frametop-spotwatch.json") # read until the first save (its name before 2026-10-20)
|
||||
ICON_DIR = os.path.expanduser("~/.cache/frametop/frunner")
|
||||
ICON_PX = 64 # ft-spotpanel's icon files: this square, RGBA
|
||||
ROWS = 7 # rows the panel shows (ft-spotpanel's kMaxRows)
|
||||
RENEW_MS = 1000 # "spotwatch on" again this often while open (ft-screens' lease is 3 s)
|
||||
RENEW_MS = 1000 # "frunner on" again this often while open (ft-screens' lease is 3 s)
|
||||
REPEAT_DELAY_MS, REPEAT_MS = 450, 40 # a held Backspace or arrow repeats (the relay sends no repeats)
|
||||
REPEATS = {14, 103, 108, 104, 109}
|
||||
SHIFT, CTRL, ALT, META = {42, 54}, {29, 97}, {56, 100}, {125, 126}
|
||||
@@ -292,7 +293,7 @@ class Entry:
|
||||
self.item = item # the result's spotsearch.Item (list rows)
|
||||
|
||||
|
||||
class Spotwatch:
|
||||
class Frunner:
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||
@@ -623,7 +624,7 @@ class Spotwatch:
|
||||
if not self.panel and not self.args.no_panel:
|
||||
self.start_panel()
|
||||
return "error the panel isn't running yet"
|
||||
if not self.screens("spotwatch on").startswith("ok"):
|
||||
if not self.screens("frunner on").startswith("ok"):
|
||||
return "error ft-screens didn't answer"
|
||||
self.is_open, self.query, self.mode, self.entries, self.sel, self.top = True, "", "list", [], 0, 0
|
||||
self.held.clear()
|
||||
@@ -650,7 +651,7 @@ class Spotwatch:
|
||||
if not self.is_open:
|
||||
return False
|
||||
try:
|
||||
self.fire.sendto(b"spotwatch on", "\0" + self.args.screens)
|
||||
self.fire.sendto(b"frunner on", "\0" + self.args.screens)
|
||||
except OSError:
|
||||
pass
|
||||
return True
|
||||
@@ -665,7 +666,7 @@ class Spotwatch:
|
||||
self.renew = None
|
||||
self.stop_repeat()
|
||||
if tell:
|
||||
self.screens("spotwatch off")
|
||||
self.screens("frunner off")
|
||||
if self.opened_keyboard:
|
||||
self.screens("vrkeyboard close")
|
||||
self.opened_keyboard = False
|
||||
@@ -702,8 +703,9 @@ class Spotwatch:
|
||||
|
||||
@staticmethod
|
||||
def load_usage():
|
||||
path = USAGE_PATH if os.path.exists(USAGE_PATH) else OLD_USAGE_PATH
|
||||
try:
|
||||
with open(USAGE_PATH) as f:
|
||||
with open(path) as f:
|
||||
u = json.load(f).get("usage", {})
|
||||
return u if isinstance(u, dict) else {}
|
||||
except (OSError, ValueError, AttributeError):
|
||||
@@ -953,17 +955,17 @@ class Spotwatch:
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Spotwatch: Frametop's search for apps and remote displays")
|
||||
p.add_argument("--socket", default="ft_spotwatch")
|
||||
p = argparse.ArgumentParser(description="Frunner: Frametop's search for apps and remote displays")
|
||||
p.add_argument("--socket", default="ft_frunner")
|
||||
p.add_argument("--panel-socket", default="ft_spotpanel")
|
||||
p.add_argument("--screens", default="ft_screens")
|
||||
p.add_argument("--float", default="frametop_float")
|
||||
p.add_argument("--no-panel", action="store_true", help="no ft-spotpanel (tests)")
|
||||
args = p.parse_args()
|
||||
try:
|
||||
sw = Spotwatch(args)
|
||||
sw = Frunner(args)
|
||||
except OSError as e:
|
||||
sys.exit(f"ft-spotwatch: @{args.socket}: {e} (another copy running?)")
|
||||
sys.exit(f"ft-frunner: @{args.socket}: {e} (another copy running?)")
|
||||
GLib.io_add_watch(sw.sock.fileno(), GLib.IO_IN, sw.readable)
|
||||
GLib.io_add_watch(sw.float_sock.fileno(), GLib.IO_IN, sw.float_readable)
|
||||
sw.start_panel()
|
||||
@@ -1,5 +1,5 @@
|
||||
// Spotwatch's reserved spot (user, 2026-10-17: "spotwatch should be a reserved spot and should
|
||||
// ideally always display through displays"). Its search opens kDistance straight ahead, nearer than
|
||||
// Frunner's reserved spot (user, 2026-10-17, when it was named spotwatch: "spotwatch should be a
|
||||
// reserved spot and should ideally always display through displays"). Its search opens kDistance straight ahead, nearer than
|
||||
// the lazy susan's front slot, which ft-screens keeps at least kFrontGap behind it: a brought panel
|
||||
// never lands in the search's plane (both were 1 m ahead). Panels nearer than it (grab that, a
|
||||
// carry) still don't cover it: its sort order is over theirs (ft-spotpanel.cpp).
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Spotwatch's search: which apps and remote displays match what's typed, best first
|
||||
(spotwatch/ft-spotwatch, docs/spotwatch.md). Plain Python, no GLib, so its test runs anywhere.
|
||||
"""Frunner's search: which apps and remote displays match what's typed, best first
|
||||
(frunner/ft-frunner, docs/frunner.md). Plain Python, no GLib, so its test runs anywhere.
|
||||
|
||||
A query matches an item's name best at its start, then at the start of one of its words, then
|
||||
by its initials ("vsc": Visual Studio Code), then anywhere in it, then as letters in order
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Spotwatch, offline: the search's ranking (spotsearch.py), then ft-spotwatch itself against a
|
||||
"""Frunner, offline: the search's ranking (spotsearch.py), then ft-frunner itself against a
|
||||
fake ft-screens and ft-floatd (sockets of its own, so it's safe next to the live desktop), with
|
||||
made-up apps and remote displays in a temporary home. No panel, no SteamVR.
|
||||
|
||||
spotwatch/test/spotwatch-test.py
|
||||
frunner/test/frunner-test.py
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
@@ -64,7 +64,7 @@ check("drop_word", (spotsearch.drop_word("hello world"), spotsearch.drop_word("h
|
||||
("hello ", "", ""))
|
||||
check("US text fallback", (spotsearch.US_TEXT[30], spotsearch.US_TEXT[57]), (("a", "A"), (" ", " ")))
|
||||
|
||||
# ---------------------------------------------------------------- ft-spotwatch
|
||||
# ---------------------------------------------------------------- ft-frunner
|
||||
|
||||
try:
|
||||
import gi
|
||||
@@ -72,11 +72,11 @@ try:
|
||||
from gi.repository import Gio # noqa: F401
|
||||
# AttributeError: a "gi" that isn't PyGObject (the release image's test stage has one).
|
||||
except (ImportError, ValueError, AttributeError):
|
||||
print("skip ft-spotwatch itself: no PyGObject here")
|
||||
print("skip ft-frunner itself: no PyGObject here")
|
||||
sys.exit(1 if failed else 0)
|
||||
|
||||
tag = f"ftsw{os.getpid()}"
|
||||
home = tempfile.mkdtemp(prefix="ft-spotwatch-test-")
|
||||
home = tempfile.mkdtemp(prefix="ft-frunner-test-")
|
||||
apps = os.path.join(home, "share", "applications")
|
||||
os.makedirs(apps)
|
||||
os.makedirs(os.path.join(home, ".config"))
|
||||
@@ -161,7 +161,7 @@ screens = Fake(f"{tag}_screens", screens_answer)
|
||||
floatd = Fake(f"{tag}_float", float_answer)
|
||||
env = dict(os.environ, HOME=home, XDG_DATA_HOME=os.path.join(home, "share"), XDG_DATA_DIRS=os.path.join(home, "none"),
|
||||
XDG_CONFIG_HOME=os.path.join(home, ".config"), XDG_CACHE_HOME=os.path.join(home, ".cache"))
|
||||
daemon = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(HERE), "ft_spotwatch.py"), "--no-panel",
|
||||
daemon = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(HERE), "ft_frunner.py"), "--no-panel",
|
||||
"--socket", f"{tag}_sw", "--screens", f"{tag}_screens", "--float", f"{tag}_float"],
|
||||
env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
me = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||
@@ -205,8 +205,8 @@ try:
|
||||
time.sleep(0.1)
|
||||
check("starts closed", ask("status").split()[1], "closed")
|
||||
check("toggle opens it", ask("toggle keyboard"), "ok")
|
||||
check("...and takes the typing from ft-screens", [m for m in screens.take() if m.startswith("spotwatch")],
|
||||
["spotwatch on"])
|
||||
check("...and takes the typing from ft-screens", [m for m in screens.take() if m.startswith("frunner")],
|
||||
["frunner on"])
|
||||
time.sleep(0.2)
|
||||
type_text("fir")
|
||||
check("typing goes into the query", ask("status"), "ok open list fir")
|
||||
@@ -218,8 +218,8 @@ try:
|
||||
press(ENTER)
|
||||
check("Enter on a running app: its most recent window is activated", floatd.take(), ["activate w-new"])
|
||||
got = screens.take()
|
||||
check("...typing back from spotwatch, and the lazy susan spins its panel to the front",
|
||||
[m for m in got if m.startswith(("spotwatch", "spin"))], ["spotwatch off", "spin to 4"])
|
||||
check("...typing back from frunner, and the lazy susan spins its panel to the front",
|
||||
[m for m in got if m.startswith(("frunner", "spin"))], ["frunner off", "spin to 4"])
|
||||
check("...and it closed", ask("status").split()[1], "closed")
|
||||
|
||||
ask("open")
|
||||
@@ -309,7 +309,7 @@ try:
|
||||
ask("open")
|
||||
tell("dismiss click")
|
||||
check("ft-screens' dismiss closes it", ask("status").split()[1], "closed")
|
||||
check("...without telling ft-screens again", [m for m in screens.take() if m == "spotwatch off"], [])
|
||||
check("...without telling ft-screens again", [m for m in screens.take() if m == "frunner off"], [])
|
||||
|
||||
ask("open")
|
||||
type_text("fi")
|
||||
@@ -319,7 +319,7 @@ try:
|
||||
tell("outside")
|
||||
check("a click beside it closes it", ask("status").split()[1], "closed")
|
||||
|
||||
with open(os.path.join(home, ".config", "frametop-spotwatch.json")) as f:
|
||||
with open(os.path.join(home, ".config", "frametop-frunner.json")) as f:
|
||||
counts = json.load(f)["usage"]
|
||||
check("usage is counted", sorted(counts), ["editor", "firefox", "remote:desk-oled"])
|
||||
finally:
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A look at spotwatch's panel without a headset: ft-spotpanel --render draws a made-up search
|
||||
"""A look at frunner's panel without a headset: ft-spotpanel --render draws a made-up search
|
||||
(or the state given as a file) into a PNG, over a dark checkerboard so the glass shows.
|
||||
|
||||
spotwatch/tools/render.py OUT.png [STATE_FILE]
|
||||
frunner/tools/render.py OUT.png [STATE_FILE]
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
@@ -211,7 +211,7 @@ ACTION_LABELS = {
|
||||
"dictate_hold": "Dictation: while held",
|
||||
"spin_next": "Spin the panels: next one on the right to the front",
|
||||
"spin_prev": "Spin the panels: next one on the left to the front",
|
||||
"spotwatch": "Spotwatch: search apps and remote displays",
|
||||
"frunner": "Frunner: search apps and remote displays",
|
||||
"key": "Pass through as key",
|
||||
"none": "Do nothing",
|
||||
}
|
||||
@@ -310,10 +310,10 @@ MODIFIER_NAMES = {29: "Ctrl", 42: "Shift", 56: "Alt", 125: "Meta"}
|
||||
RIGHT_MODIFIER_NAMES = {97: "Right Ctrl", 54: "Right Shift", 100: "Right Alt", 126: "Right Meta"}
|
||||
# What a rules file without "key_bindings" gets (the relay's DEFAULT_KEY_BINDINGS): a Meta tap
|
||||
# opens Steam's menu, Meta+J and Meta+K click at the gaze, Meta+Shift+F floats a window,
|
||||
# Meta+Alt+Tab and Meta+Alt+Shift+Tab spin the panels, Meta+Space opens spotwatch.
|
||||
# Meta+Alt+Tab and Meta+Alt+Shift+Tab spin the panels, Meta+Space opens frunner.
|
||||
DEFAULT_KEY_BINDINGS = {"125": "steam_menu", "125+36": "gaze_left", "125+37": "gaze_right",
|
||||
"42+125+33": "float_toggle", "56+125+15": "spin_next", "42+56+125+15": "spin_prev",
|
||||
"125+57": "spotwatch"}
|
||||
"125+57": "frunner"}
|
||||
|
||||
|
||||
def voice_installed():
|
||||
@@ -377,12 +377,29 @@ def code_names():
|
||||
return names
|
||||
|
||||
|
||||
# Actions renamed since a release (the relay's RENAMED_ACTIONS): a rules file saved before shows
|
||||
# and saves them under their new names.
|
||||
RENAMED_ACTIONS = {"spotwatch": "frunner"}
|
||||
|
||||
|
||||
def renamed_actions(table):
|
||||
if not isinstance(table, dict):
|
||||
return table
|
||||
return {k: renamed_actions(v) if isinstance(v, dict) else RENAMED_ACTIONS.get(v, v) if isinstance(v, str) else v
|
||||
for k, v in table.items()}
|
||||
|
||||
|
||||
def read_json(path):
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
data = json.load(f)
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
if isinstance(data, dict):
|
||||
for table in ("buttons", "controller_buttons", "key_bindings"):
|
||||
if table in data:
|
||||
data[table] = renamed_actions(data[table])
|
||||
return data
|
||||
|
||||
|
||||
def read_conf():
|
||||
|
||||
+24
-11
@@ -40,8 +40,8 @@ use NAME: its screens and apps; docs/profiles.md), steam_menu = open the SteamVR
|
||||
Steam's menu, or close the dashboard (steam/ft-steam menu, through Steam's UI), pause_toggle =
|
||||
pause Frametop for a VR game, or resume it (game_pause.py), dictate = start or stop dictation, and
|
||||
dictate_hold = dictation while held (Frametop Voice, voice/ft-voiced: "toggle|start|stop <source>"
|
||||
to @frametop_voice), spotwatch = open or close spotwatch's search for apps and remote displays
|
||||
("toggle <source>" to @ft_spotwatch; Meta+Space by default), command:CMD = run CMD with sh -c (on the host, as this service: its
|
||||
to @frametop_voice), frunner = open or close frunner's search for apps and remote displays
|
||||
("toggle <source>" to @ft_frunner; Meta+Space by default), command:CMD = run CMD with sh -c (on the host, as this service: its
|
||||
environment, output to its log, and COMMAND_PATH, so ft-layout, ft-float, ft-steam and ft-voice
|
||||
need no path), key = pass through as a key, none).
|
||||
|
||||
@@ -275,17 +275,17 @@ ACTIONS = ("left", "right", "middle", "back", "scroll_up", "scroll_down", "dashb
|
||||
"pointer_toggle", "follow_toggle", "gaze_toggle", "gaze_precision", "gaze_drag", "gaze_left", "gaze_right",
|
||||
"gaze_quickcal", "sens_up", "sens_down", "layout_reset", "screens_toggle", "keyboard_toggle", "float_toggle",
|
||||
"dock_all", "spin_next", "spin_prev", "steam_menu", "pause_toggle", "dictate", "dictate_hold",
|
||||
"spotwatch", "key", "none")
|
||||
"frunner", "key", "none")
|
||||
# Gaze mode is a mouse feature: these never come from a controller button (docs/gaze-controllers.md).
|
||||
GAZE_ACTIONS = ("gaze_toggle", "gaze_precision", "gaze_drag", "gaze_left", "gaze_right", "gaze_quickcal")
|
||||
# Key combinations a rules file without "key_bindings" gets: a Meta tap opens Steam's menu, Meta+J
|
||||
# and Meta+K click at the gaze (free on the Frametop desktop, and apps don't use Meta),
|
||||
# Meta+Shift+F floats a window, and Meta+Alt+Tab and Meta+Alt+Shift+Tab spin the panels around
|
||||
# you (ft-screens' lazy susan); not Meta+Tab, which is Cmd+Tab on a Mac reached through a remote
|
||||
# desktop like RustDesk. Meta+Space opens spotwatch's search (Plasma's KRunner is Alt+Space).
|
||||
# desktop like RustDesk. Meta+Space opens frunner's search (Plasma's KRunner is Alt+Space).
|
||||
DEFAULT_KEY_BINDINGS = {"125": "steam_menu", "125+36": "gaze_left", "125+37": "gaze_right",
|
||||
"42+125+33": "float_toggle", "56+125+15": "spin_next", "42+56+125+15": "spin_prev",
|
||||
"125+57": "spotwatch"}
|
||||
"125+57": "frunner"}
|
||||
# Frametop Voice (voice/): dictation starts and stops on its socket. With it installed (its unit
|
||||
# file), a rules file without "key_bindings" gets these too: a Right Ctrl tap, Right Ctrl alone,
|
||||
# so a Left Ctrl tap (crouch in many games) doesn't start it.
|
||||
@@ -330,14 +330,25 @@ FLOAT_ACTIONS = {"float_toggle": b"float pointer", "dock_all": b"dock all"}
|
||||
# Actions for ft-screens: spin_next and spin_prev turn every panel in the room about your head,
|
||||
# so the next one to the right or left comes to the front. They don't need pointer mode either.
|
||||
SCREENS_ACTIONS = {"spin_next": b"spin next", "spin_prev": b"spin prev"}
|
||||
# spotwatch (spotwatch/ft-spotwatch, in the Frametop desktop) opens or closes its search; it's told
|
||||
# frunner (frunner/ft-frunner, in the Frametop desktop) opens or closes its search; it's told
|
||||
# what pressed it, since one from a mouse or controller opens Frametop's keyboard too. It doesn't
|
||||
# need pointer mode, and does nothing while Frametop is paused.
|
||||
SPOTWATCH = "\0ft_spotwatch"
|
||||
FRUNNER = "\0ft_frunner"
|
||||
# Actions renamed since a release: rules files saved before still mean them (spotwatch was
|
||||
# frunner's name until 2026-10-20).
|
||||
RENAMED_ACTIONS = {"spotwatch": "frunner"}
|
||||
PROFILE = "profile:" # "profile:NAME": switch to that profile (doesn't need pointer mode either)
|
||||
COMMAND = "command:" # "command:CMD": run CMD (nor does this)
|
||||
|
||||
|
||||
def renamed_actions(table):
|
||||
"""A rules table ({id: action}, or {id: {code: action}}) with renamed actions under their new names."""
|
||||
if not isinstance(table, dict):
|
||||
return table
|
||||
return {k: renamed_actions(v) if isinstance(v, dict) else RENAMED_ACTIONS.get(v, v) if isinstance(v, str) else v
|
||||
for k, v in table.items()}
|
||||
|
||||
|
||||
def known_action(a):
|
||||
return a in ACTIONS or (isinstance(a, str) and any(a.startswith(p) and a[len(p):].strip()
|
||||
for p in (PROFILE, COMMAND)))
|
||||
@@ -345,7 +356,7 @@ def known_action(a):
|
||||
|
||||
def needs_pointer(a):
|
||||
return (a not in FLOAT_ACTIONS and a not in SCREENS_ACTIONS and a not in VOICE_ACTIONS
|
||||
and a not in ("steam_menu", "pause_toggle", "spotwatch") and not a.startswith((PROFILE, COMMAND)))
|
||||
and a not in ("steam_menu", "pause_toggle", "frunner") and not a.startswith((PROFILE, COMMAND)))
|
||||
|
||||
|
||||
def works_paused(a):
|
||||
@@ -576,6 +587,8 @@ def read_rules(path=RULES_PATH):
|
||||
rules.setdefault("controller_buttons", {})
|
||||
if not isinstance(rules.get("key_bindings"), dict):
|
||||
rules["key_bindings"] = default_key_bindings()
|
||||
for table in ("buttons", "controller_buttons", "key_bindings"):
|
||||
rules[table] = renamed_actions(rules[table])
|
||||
return rules
|
||||
|
||||
|
||||
@@ -1211,12 +1224,12 @@ def main():
|
||||
except OSError:
|
||||
pass # the Frametop desktop isn't running
|
||||
log(action)
|
||||
elif action == "spotwatch":
|
||||
elif action == "frunner":
|
||||
if value == 1:
|
||||
try:
|
||||
screens_sock.sendto(f"toggle {source}".encode(), SPOTWATCH)
|
||||
screens_sock.sendto(f"toggle {source}".encode(), FRUNNER)
|
||||
except OSError:
|
||||
log("spotwatch: not running (the Frametop desktop starts it)")
|
||||
log("frunner: not running (the Frametop desktop starts it)")
|
||||
log(action)
|
||||
elif action in VOICE_ACTIONS:
|
||||
# dictate starts or stops on a press; dictate_hold starts on the press and stops on
|
||||
|
||||
+21
-11
@@ -14,6 +14,7 @@ relay, so it's safe next to them. Rules come from the test, not ~/.config/framet
|
||||
"""
|
||||
import fcntl
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
@@ -68,7 +69,7 @@ class StubPause:
|
||||
stub = {}
|
||||
relay.game_pause.GamePause = StubPause
|
||||
|
||||
for name in ("SCREENS", "HELPER", "FLOAT", "GAZED", "VOICE", "SPOTWATCH"):
|
||||
for name in ("SCREENS", "HELPER", "FLOAT", "GAZED", "VOICE", "FRUNNER"):
|
||||
setattr(relay, name, f"\0{tag}_{name.lower()}")
|
||||
OUT = tempfile.mkdtemp(prefix="ft-keys-test-")
|
||||
steam_log, cmd_log = f"{OUT}/steam.log", f"{OUT}/cmd.log"
|
||||
@@ -114,6 +115,7 @@ def read_rules(path=None):
|
||||
return rules
|
||||
|
||||
|
||||
real_read_rules = relay.read_rules # the file reader, for the renamed-actions check
|
||||
relay.read_rules = read_rules
|
||||
conf = {"POINTER": "0"} # ~/.config/frametop.conf, as the relay reads it
|
||||
relay.read_config = lambda path=None: dict(conf)
|
||||
@@ -186,9 +188,9 @@ screens.settimeout(0.05)
|
||||
voice = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) # Frametop Voice's socket
|
||||
voice.bind(relay.VOICE)
|
||||
voice.settimeout(0.05)
|
||||
spotwatch = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) # spotwatch's socket (spotwatch/ft-spotwatch)
|
||||
spotwatch.bind(relay.SPOTWATCH)
|
||||
spotwatch.settimeout(0.05)
|
||||
frunner = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) # frunner's socket (frunner/ft-frunner)
|
||||
frunner.bind(relay.FRUNNER)
|
||||
frunner.settimeout(0.05)
|
||||
EVENT = struct.Struct("llHHi")
|
||||
|
||||
|
||||
@@ -213,12 +215,12 @@ def typed():
|
||||
return got
|
||||
|
||||
|
||||
def spotwatched():
|
||||
"""What spotwatch was sent since the last call."""
|
||||
def frunner_got():
|
||||
"""What frunner was sent since the last call."""
|
||||
got = []
|
||||
while True:
|
||||
try:
|
||||
got.append(spotwatch.recv(256).decode())
|
||||
got.append(frunner.recv(256).decode())
|
||||
except socket.timeout:
|
||||
return got
|
||||
|
||||
@@ -462,18 +464,26 @@ def tests():
|
||||
check("spinning works without pointer mode", (relay.needs_pointer("spin_next"), relay.needs_pointer("spin_prev")),
|
||||
(False, False))
|
||||
|
||||
# Meta+Space (a default): spotwatch's search opens or closes, told it came from a keyboard.
|
||||
# Meta+Space (a default): frunner's search opens or closes, told it came from a keyboard.
|
||||
SPACE = 57
|
||||
key(META, 1); key(SPACE, 1); key(SPACE, 0); key(META, 0)
|
||||
check("Meta+Space (default): spotwatch toggles", spotwatched(), ["toggle keyboard"])
|
||||
check("Meta+Space (default): frunner toggles", frunner_got(), ["toggle keyboard"])
|
||||
check("...Space isn't typed, and the desktop gets F24 and Meta's release first", typed(),
|
||||
["key 125 1"] + F24 + ["key 125 0"])
|
||||
pause("on")
|
||||
key(META, 1); key(SPACE, 1); key(SPACE, 0); key(META, 0)
|
||||
check("paused: Meta+Space doesn't open spotwatch", spotwatched(), [])
|
||||
check("paused: Meta+Space doesn't open frunner", frunner_got(), [])
|
||||
pause("off")
|
||||
typed()
|
||||
check("spotwatch works without pointer mode", relay.needs_pointer("spotwatch"), False)
|
||||
check("frunner works without pointer mode", relay.needs_pointer("frunner"), False)
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json") as old:
|
||||
json.dump({"buttons": {"mouse": {"275": "spotwatch"}}, "controller_buttons": {"left/view": "spotwatch"},
|
||||
"key_bindings": {"125+57": "spotwatch", "125": "steam_menu"}}, old)
|
||||
old.flush()
|
||||
r = real_read_rules(old.name)
|
||||
check("a rules file from before the rename: spotwatch means frunner",
|
||||
(r["buttons"], r["controller_buttons"], r["key_bindings"]),
|
||||
({"mouse": {"275": "frunner"}}, {"left/view": "frunner"}, {"125+57": "frunner", "125": "steam_menu"}))
|
||||
|
||||
# Dictation: Right Ctrl alone (the default with Frametop Voice installed) starts and stops it.
|
||||
use({"97": "dictate"})
|
||||
|
||||
@@ -20,7 +20,7 @@ test-python:
|
||||
set -uo pipefail
|
||||
python3 -c 'import PySide6, numpy, cv2, sherpa_onnx' || { echo "python3 can't import PySide6, numpy, cv2, and sherpa_onnx"; exit 1; }
|
||||
status=0
|
||||
for t in models/test/*.py input/test/*.py hands/tests/test_*.py hands/rec/tests/*.py recorder/tests/*.py gaze/test/*.py gaze/rec/tests/*.py pack/test/*.py voice/test/*.py spotwatch/test/*.py tour/test/*.py; do
|
||||
for t in models/test/*.py input/test/*.py hands/tests/test_*.py hands/rec/tests/*.py recorder/tests/*.py gaze/test/*.py gaze/rec/tests/*.py pack/test/*.py voice/test/*.py frunner/test/*.py tour/test/*.py; do
|
||||
[ -f "$t" ] || continue
|
||||
if python3 "$t" >/dev/null; then echo "$t: ok"; else echo "$t: FAILED"; status=1; fi
|
||||
done
|
||||
|
||||
@@ -21,7 +21,7 @@ Frametop's desktop runs in a nested KWin that gamescope never sees, so keys sent
|
||||
|
||||
- Full Keyboard says `kbhello OPEN` every second, and at once when it opens or closes. The relay answers each hello, and any change, with `kb DESKTOP SURFACE`: whether typing goes to the desktop, and whether the screens show with Frametop not paused (then it may show over them with the dashboard closed).
|
||||
- While typing goes to the desktop, its keys go to the relay as `kbkey CODE 0|1` and composed text as `kbtext TEXT`. The relay types them on the desktop's seat through ft-screens, in order with dictation's typing; the text goes through the desktop's keyboard layout and isn't logged. Otherwise they go to gamescope as before. A key comes up where it went down, whichever way typing goes by then, and when Full Keyboard stops, the relay lets go of the keys it held.
|
||||
- Opening it: the relay tells ft-screens it runs (`fullkb OPEN`), and ft-screens then passes on its `vrkeyboard` commands (a desktop text field gets focus, a mapped button, spotwatch, a layout reset) as `kbpanel show|hide`, which the relay sends Full Keyboard as `kb show|hide`. Frametop's older panel (`screens/keyboard.cpp`) is used only while Full Keyboard isn't running.
|
||||
- Opening it: the relay tells ft-screens it runs (`fullkb OPEN`), and ft-screens then passes on its `vrkeyboard` commands (a desktop text field gets focus, a mapped button, frunner, a layout reset) as `kbpanel show|hide`, which the relay sends Full Keyboard as `kb show|hide`. Frametop's older panel (`screens/keyboard.cpp`) is used only while Full Keyboard isn't running.
|
||||
- A click on its panel (`org.framekeyboard.*` overlays) doesn't take typing away from the desktop.
|
||||
- Its Steam keyboard replacement (`bridge/`) is theirs, unchanged.
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
# Build ft-screens in the dev container on the Frame (screens/build/ft-screens), and
|
||||
# ft-handtest, which tries the hand cutouts (handcut.cpp) on a test panel of its own, and
|
||||
# ft-handview, which draws the hands when Room View is off (handview.cpp), and
|
||||
# ft-spotpanel, spotwatch's search panel (spotwatch/ft-spotpanel.cpp; ft-spotwatch runs it).
|
||||
# ft-spotpanel, frunner's search panel (frunner/ft-spotpanel.cpp; ft-frunner runs it).
|
||||
# compositor.c is the wlroots side (C; wlroots headers aren't C++), vr.cpp the OpenVR side,
|
||||
# remote.c the remote screens (their streams are ft-stream's, stream/build.sh).
|
||||
# vr.cpp needs OpenVR's IVRIPCResourceManagerClient (ImportDmabuf), which the header
|
||||
@@ -42,6 +42,6 @@ g++ -o build/ft-screens build/compositor.o build/remote.o build/vr.o build/keybo
|
||||
$(pkg-config --libs wlroots-0.20 wayland-server xkbcommon pixman-1) $vrlibs
|
||||
g++ -o build/ft-handtest build/handtest.o build/handcut.o build/handrender.o $vrlibs
|
||||
g++ -o build/ft-handview build/handview.o build/handmesh.o build/handskin.o build/handrobot.o build/handcut.o build/handrender.o $vrlibs
|
||||
$cxx -o build/ft-spotpanel ../spotwatch/ft-spotpanel.cpp $(pkg-config --libs gbm libdrm) $OPENVR_LIBS -lpthread
|
||||
$cxx -o build/ft-spotpanel ../frunner/ft-spotpanel.cpp $(pkg-config --libs gbm libdrm) $OPENVR_LIBS -lpthread
|
||||
strip --strip-unneeded build/ft-screens build/ft-handtest build/ft-handview build/ft-spotpanel
|
||||
echo "built build/ft-screens build/ft-handtest build/ft-handview build/ft-spotpanel"'
|
||||
+15
-15
@@ -157,7 +157,7 @@ struct server {
|
||||
bool keys_desktop; // ...and the screens are showing: typing goes to the desktop
|
||||
bool keys_shown; // the relay was told the screens show (see keys_update)
|
||||
int key_remote; // the last click was on this remote screen (index): typing goes to it; -1 not
|
||||
// Spotwatch's search (spotwatch/ft-spotwatch) has the typing while it's open: "spotwatch on",
|
||||
// Frunner's search (frunner/ft-frunner) has the typing while it's open: "frunner on",
|
||||
// renewed every second, lapses SPOT_LEASE_MS after the last one (see spot_open).
|
||||
uint32_t spot_until; // ms; 0 closed
|
||||
int relay_fd; // unbound, so the relay can't reply into our control socket
|
||||
@@ -182,7 +182,7 @@ static uint32_t now_ms(void) {
|
||||
return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- spotwatch
|
||||
// ---------------------------------------------------------------- frunner
|
||||
|
||||
#define SPOT_LEASE_MS 3000
|
||||
|
||||
@@ -190,11 +190,11 @@ static bool spot_open(const struct server *s) {
|
||||
return s->spot_until && (int32_t)(s->spot_until - now_ms()) > 0;
|
||||
}
|
||||
|
||||
// To ft-spotwatch's socket (@ft_spotwatch): "key <code> <value>" while it has the typing, and
|
||||
// To ft-frunner's socket (@ft_frunner): "key <code> <value>" while it has the typing, and
|
||||
// "dismiss <why>" when something else takes it (a click elsewhere, a pause).
|
||||
static void spot_send(struct server *s, const char *msg) {
|
||||
struct sockaddr_un addr = {.sun_family = AF_UNIX};
|
||||
const char name[] = "ft_spotwatch";
|
||||
const char name[] = "ft_frunner";
|
||||
memcpy(addr.sun_path + 1, name, sizeof name - 1);
|
||||
sendto(s->relay_fd, msg, strlen(msg), MSG_DONTWAIT, (struct sockaddr *)&addr,
|
||||
offsetof(struct sockaddr_un, sun_path) + 1 + sizeof name - 1);
|
||||
@@ -216,7 +216,7 @@ static bool fullkb(const struct server *s) {
|
||||
static void spot_dismiss(struct server *s, const char *why) {
|
||||
if (!spot_open(s)) return;
|
||||
s->spot_until = 0;
|
||||
wlr_log(WLR_INFO, "spotwatch dismissed (%s)", why);
|
||||
wlr_log(WLR_INFO, "frunner dismissed (%s)", why);
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof msg, "dismiss %s", why);
|
||||
spot_send(s, msg);
|
||||
@@ -624,9 +624,9 @@ static void keys_update(struct server *s) {
|
||||
if (s->spot_until && ft_vr_paused()) spot_dismiss(s, "paused");
|
||||
if (s->spot_until && !spot_open(s)) {
|
||||
s->spot_until = 0;
|
||||
wlr_log(WLR_INFO, "spotwatch's lease lapsed");
|
||||
wlr_log(WLR_INFO, "frunner's lease lapsed");
|
||||
}
|
||||
// Spotwatch's search takes typing from wherever it went, Steam too.
|
||||
// Frunner's search takes typing from wherever it went, Steam too.
|
||||
const bool shown = ft_vr_screens_shown();
|
||||
const bool desktop = spot_open(s) || (s->keys_clicked && shown);
|
||||
const uint32_t t = now_ms();
|
||||
@@ -821,12 +821,12 @@ static void handle_key(struct server *s, uint32_t code, int value, char *reply,
|
||||
return;
|
||||
}
|
||||
if (kind == FT_RELAY_DROP) return (void)snprintf(reply, size, "ok not a key or mouse button");
|
||||
// Spotwatch's search has the typing, except for the release of a key the desktop holds.
|
||||
// Frunner's search has the typing, except for the release of a key the desktop holds.
|
||||
if (spot_open(s) && (value || !key_held(&s->keyboard, code))) {
|
||||
char msg[32];
|
||||
snprintf(msg, sizeof msg, "key %u %d", code, value);
|
||||
spot_send(s, msg);
|
||||
return (void)snprintf(reply, size, "ok spotwatch");
|
||||
return (void)snprintf(reply, size, "ok frunner");
|
||||
}
|
||||
// A remote screen has the typing, except for the release of a key the desktop holds.
|
||||
if (s->key_remote >= 0 && ft_remote_is(s->key_remote) && (value || !key_held(&s->keyboard, code))) {
|
||||
@@ -954,7 +954,7 @@ static void panel_key(struct server *s, uint32_t code, bool pressed) {
|
||||
// as if from that panel, x and y in its pixels; for tests, mostly with --no-vr),
|
||||
// "key <code> <value>", "vrkeyboard show|hide|toggle|close" (our keyboard or Full Keyboard,
|
||||
// from the input relay), "fullkb 0|1" (the relay: Full Keyboard runs, up or not),
|
||||
// "spotwatch on|off" (spotwatch's search has the typing; see spot_open), and "click <overlay
|
||||
// "frunner on|off" (frunner's search has the typing; see spot_open), and "click <overlay
|
||||
// key>" (from the pointer helper: a mouse click landed on that panel, "-" for none); the rest
|
||||
// is in vr.cpp (ft_vr_command).
|
||||
static int control_readable(int fd, uint32_t mask, void *data) {
|
||||
@@ -1089,15 +1089,15 @@ static int control_readable(int fd, uint32_t mask, void *data) {
|
||||
}
|
||||
len = sizeof from;
|
||||
continue;
|
||||
} else if (strcmp(buf, "spotwatch on") == 0 || strcmp(buf, "spotwatch off") == 0) {
|
||||
// Spotwatch's search opened (renewed every second while open) or closed: it has the
|
||||
} else if (strcmp(buf, "frunner on") == 0 || strcmp(buf, "frunner off") == 0) {
|
||||
// Frunner's search opened (renewed every second while open) or closed: it has the
|
||||
// typing meanwhile. A remote screen lets go of the keys it holds first.
|
||||
const bool on = buf[11] == 'n';
|
||||
if (on && !spot_open(s)) {
|
||||
if (s->key_remote >= 0 && ft_remote_is(s->key_remote)) ft_remote_blur(s->key_remote);
|
||||
wlr_log(WLR_INFO, "spotwatch open: typing goes to it");
|
||||
wlr_log(WLR_INFO, "frunner open: typing goes to it");
|
||||
} else if (!on && spot_open(s)) {
|
||||
wlr_log(WLR_INFO, "spotwatch closed");
|
||||
wlr_log(WLR_INFO, "frunner closed");
|
||||
}
|
||||
s->spot_until = on ? (now_ms() + SPOT_LEASE_MS) | 1 : 0;
|
||||
keys_update(s);
|
||||
@@ -1110,7 +1110,7 @@ static int control_readable(int fd, uint32_t mask, void *data) {
|
||||
"%s kb_screen=%d kb_auto=%d fullkb=%s typing=%s focused_screen=%d pointer_screen=%d", vr,
|
||||
s->kb_screen + 1, s->kb_auto ? 1 : 0,
|
||||
!fullkb(s) ? "off" : s->fullkb_open ? (s->fullkb_auto ? "open-auto" : "open") : "closed",
|
||||
spot_open(s) ? "spotwatch" : s->key_remote >= 0 ? "remote" : s->keys_desktop ? "desktop" : s->keys_clicked ? "desktop (hidden)" : "steam",
|
||||
spot_open(s) ? "frunner" : s->key_remote >= 0 ? "remote" : s->keys_desktop ? "desktop" : s->keys_clicked ? "desktop (hidden)" : "steam",
|
||||
focused_screen(s) + 1, s->pointer_focus ? s->pointer_focus->index + 1 : 0);
|
||||
} else if (!ft_remote_command(buf, reply, sizeof reply)) {
|
||||
ft_vr_command(buf, reply, sizeof reply);
|
||||
|
||||
+6
-6
@@ -113,7 +113,7 @@
|
||||
#include "keyboard.h"
|
||||
#include "roomview.h"
|
||||
#include "steampanels.h"
|
||||
#include "../spotwatch/reserve.h"
|
||||
#include "../frunner/reserve.h"
|
||||
|
||||
#include <drm_fourcc.h>
|
||||
#include <openvr.h>
|
||||
@@ -1887,7 +1887,7 @@ Screen *Find(int one_based) {
|
||||
// turns instead of you. A spin that arrives during one adds to it, from where the panels are
|
||||
// headed, so quick taps carry on smoothly. Grabbing a panel, or a command that places it,
|
||||
// takes it out of the spin where it is. The 3D mouse's pointer goes to straight ahead.
|
||||
// "spin to N" turns the ring the short way so panel N comes to straight ahead (spotwatch).
|
||||
// "spin to N" turns the ring the short way so panel N comes to straight ahead (frunner).
|
||||
constexpr double kSpinSeconds = 0.3; // how long a spin takes
|
||||
constexpr double kSpinAhead = 8; // degrees: a panel this near straight ahead is the current one
|
||||
constexpr double kSpinFocus = 30; // degrees: when a spin settles, the panel this near ahead gets typing
|
||||
@@ -2822,7 +2822,7 @@ void StartGrabMove(int index, Screen &s, const Mat &to, double width, double ms
|
||||
// ---------------------------------------------------------------- the front slot
|
||||
// One place in front of you for the lazy susan (user, 2026-10-11: "make the system a bit more logical
|
||||
// with the lazy susan"; the proposals they took): a stroke (or Meta+Alt+Tab) brings the next screen
|
||||
// there, 1 m ahead (g_frontReading; never nearer than behind spotwatch's reserved spot), as big as its resolution needs for its text to read
|
||||
// there, 1 m ahead (g_frontReading; never nearer than behind frunner's reserved spot), as big as its resolution needs for its text to read
|
||||
// (grabthat::ReadableSize), one at a time: the one there goes home first. Turned away from, it goes
|
||||
// home, unless you used it a while (g_frontKeepMs of input on it: kept). Resized there, it comes that
|
||||
// size next time (g_frontDeg). Its home: where it lived before it came.
|
||||
@@ -3377,7 +3377,7 @@ void SusanCommand(bool next, char *reply, int size) {
|
||||
BringReadable(i, grabthat::InFront(head, g_frontReading, g_frontDrop), true, next ? "the lazy susan, next" : "the lazy susan, back");
|
||||
std::snprintf(reply, size, "ok screen %d", i + 1);
|
||||
}
|
||||
// "spin to N" (spotwatch) while the lazy susan brings: panel N to the front slot, the rest stay put.
|
||||
// "spin to N" (frunner) while the lazy susan brings: panel N to the front slot, the rest stay put.
|
||||
void SusanBring(int i, const Mat &head, char *reply, int size) {
|
||||
if (g_front.panel != i) {
|
||||
if (g_front.released) FrontHome("restored by spin to");
|
||||
@@ -4296,7 +4296,7 @@ bool ft_vr_init(void) {
|
||||
}
|
||||
g_susanBring = handcut::Setting("SCREEN_SPIN_BRING", "1") != "0"; // the lazy susan brings panels (0: the room turns)
|
||||
num("SCREEN_SPIN_DISTANCE", g_frontReading, 0.4, 3); // the front slot's reading distance (m)...
|
||||
g_frontReading = std::max(g_frontReading, spotreserve::kDistance + spotreserve::kFrontGap); // ...behind spotwatch's spot
|
||||
g_frontReading = std::max(g_frontReading, spotreserve::kDistance + spotreserve::kFrontGap); // ...behind frunner's spot
|
||||
num("SCREEN_SPIN_PPD", g_frontRead.ppd, 8, 80); // ...as big as its pixels at this many per degree need
|
||||
num("SCREEN_SPIN_MAX_DEG", g_frontRead.maxW, 30, 100); // ...at most this wide (deg; wider ones curve round you)
|
||||
num("SCREEN_SPIN_MAX_TALL_DEG", g_frontRead.maxH, 20, 90);
|
||||
@@ -5345,7 +5345,7 @@ void ft_vr_command(const char *cmd, char *reply, int size) {
|
||||
} else if (!std::strncmp(cmd, "spin ", 5)) {
|
||||
SpinCommand(cmd + 5, reply, size);
|
||||
} else if (std::sscanf(cmd, "front %d", &n) == 1) {
|
||||
// Typing and the active window go to panel N where it is, as after a spin to it (spotwatch,
|
||||
// Typing and the active window go to panel N where it is, as after a spin to it (frunner,
|
||||
// for a pinned panel, which doesn't spin).
|
||||
if (!Find(n)) return (void)std::snprintf(reply, size, "error no panel %d", n);
|
||||
g_spin.front = n - 1;
|
||||
|
||||
@@ -250,19 +250,19 @@ def check_host():
|
||||
"kwinrc may no longer turn it off (session/frametop-session.sh)")
|
||||
check_kwin_host_protocols()
|
||||
|
||||
# Spotwatch (spotwatch/ft_spotwatch.py) runs on the host's Python: GdkPixbuf's SVG loader
|
||||
# Frunner (frunner/ft_frunner.py) runs on the host's Python: GdkPixbuf's SVG loader
|
||||
# draws the apps' icons, and libxkbcommon turns keys into text on the desktop's layout.
|
||||
code, out = run("python3", "-c", "import gi; gi.require_version('GdkPixbuf', '2.0'); "
|
||||
"from gi.repository import GdkPixbuf; "
|
||||
"print(' '.join(f.get_name() for f in GdkPixbuf.Pixbuf.get_formats()))")
|
||||
if code != 0:
|
||||
report("warn", "spotwatch", "the host's Python has no GdkPixbuf: spotwatch can't start")
|
||||
report("warn", "frunner", "the host's Python has no GdkPixbuf: frunner can't start")
|
||||
elif "svg" not in out.split():
|
||||
report("warn", "spotwatch", "GdkPixbuf has no SVG loader: most apps show no icon in spotwatch")
|
||||
report("warn", "frunner", "GdkPixbuf has no SVG loader: most apps show no icon in frunner")
|
||||
elif not any(os.path.exists(p) for p in ("/usr/lib/libxkbcommon.so.0", "/usr/lib64/libxkbcommon.so.0")):
|
||||
report("warn", "spotwatch", "no libxkbcommon: spotwatch types on a US layout")
|
||||
report("warn", "frunner", "no libxkbcommon: frunner types on a US layout")
|
||||
else:
|
||||
report("ok", "spotwatch", "GdkPixbuf with SVG, and libxkbcommon")
|
||||
report("ok", "frunner", "GdkPixbuf with SVG, and libxkbcommon")
|
||||
|
||||
# ft-notifyfwd uses Plasma's notification watcher API, which no spec covers, and PyGObject.
|
||||
plasma = b""
|
||||
|
||||
@@ -306,16 +306,17 @@ EOF
|
||||
*) rm -f "$autostart" ;;
|
||||
esac
|
||||
|
||||
# Spotwatch (spotwatch/ft-spotwatch, Meta+Space): the search for apps and remote displays, up
|
||||
# Frunner (frunner/ft-frunner, Meta+Space): the search for apps and remote displays, up
|
||||
# exactly while this desktop is. ft-screens gives it the typing while it's open (screens backend).
|
||||
autostart=$XDG_CONFIG_HOME/autostart/frametop-spotwatch.desktop
|
||||
rm -f "$XDG_CONFIG_HOME/autostart/frametop-spotwatch.desktop" # its name before 2026-10-20
|
||||
autostart=$XDG_CONFIG_HOME/autostart/frametop-frunner.desktop
|
||||
if [ "$backend" = screens ]; then
|
||||
mkdir -p "$(dirname "$autostart")"
|
||||
cat > "$autostart" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Frametop spotwatch
|
||||
Exec=sh -c 'exec "$here/../spotwatch/ft-spotwatch" > /tmp/frametop-spotwatch.log 2>&1'
|
||||
Name=Frametop frunner
|
||||
Exec=sh -c 'exec "$here/../frunner/ft-frunner" > /tmp/frametop-frunner.log 2>&1'
|
||||
X-KDE-autostart-phase=2
|
||||
NoDisplay=true
|
||||
EOF
|
||||
|
||||
@@ -112,7 +112,7 @@ SCREEN_FRONT_AWAY=0 # ... 1 = turned away from, it goes home (unless used
|
||||
SCREEN_FRONT_KEEP_MS=3000 # ... this much input on the screen in the slot keeps it there when you look away (0 = never kept)
|
||||
SCREEN_UI_SCALE= # ... a remote display's own UI scale for its readable size, by screen number: "102=2" (a Mac's Retina display); KWin's screens use their own scale
|
||||
HANDS_RESET_HOLD_MS=0 # the reset button under a screen resets the layout when held this long (ms; laser or finger) | 0 = a press
|
||||
SCREEN_SPIN_DISTANCE=1.0 # ... a brought screen comes this far ahead (m); at least 0.9, behind spotwatch's search (0.75)
|
||||
SCREEN_SPIN_DISTANCE=1.0 # ... a brought screen comes this far ahead (m); at least 0.9, behind frunner's search (0.75)
|
||||
SCREEN_SPIN_PPD=26 # ... as big as its text needs to read: its logical pixels (KWin's scale undone) at this many per degree of view
|
||||
SCREEN_SPIN_MAX_DEG=100 # ... at most this wide (degrees; wider than 60 it curves round you) and SCREEN_SPIN_MAX_TALL_DEG (55) tall, its shape kept
|
||||
SCREEN_SPIN_MAX_WIDE_DEG=150 # ... an ultrawide (3:1 or wider, like 5120x1440) at most this wide instead, so its text reads too
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
# ft-spotwatch on the Frame host, inside the Frametop desktop's session (see ft_spotwatch.py).
|
||||
exec python3 "$(dirname "$(readlink -f "$0")")/ft_spotwatch.py" "$@"
|
||||
@@ -2,7 +2,7 @@
|
||||
Type=Application
|
||||
Name=Frametop Tour
|
||||
GenericName=A walk through Frametop
|
||||
Comment=What Frametop does: screens, shortcuts, eye and hand tracking, spotwatch, floating windows, pins, profiles
|
||||
Comment=What Frametop does: screens, shortcuts, eye and hand tracking, frunner, floating windows, pins, profiles
|
||||
Exec=@REPO@/tour/ft-tour
|
||||
Icon=help-about
|
||||
Categories=Utility;
|
||||
|
||||
+12
-12
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Frametop Tour: a walk through what Frametop does, the first time the desktop starts, and
|
||||
whenever it's opened again (the app menu, spotwatch, SteamVR's Launch a program list, the Tour
|
||||
whenever it's opened again (the app menu, frunner, SteamVR's Launch a program list, the Tour
|
||||
entry in Frametop Input Settings, or tour/ft-tour).
|
||||
|
||||
A Kirigami (QML) app with a Python backend, like Frametop Input Settings, in Frametop's
|
||||
@@ -29,7 +29,7 @@ DONE_PATH = os.path.expanduser("~/.local/state/frametop/tour.json")
|
||||
TOUR_VERSION = 1 # a tour with new pages can show itself again by raising this
|
||||
UNITS = os.path.expanduser("~/.config/systemd/user")
|
||||
CONF_PATH = os.path.expanduser("~/.config/frametop.conf")
|
||||
SOCKETS = {"gazed": "\0ft_gazed", "spotwatch": "\0ft_spotwatch", "screens": "\0ft_screens",
|
||||
SOCKETS = {"gazed": "\0ft_gazed", "frunner": "\0ft_frunner", "screens": "\0ft_screens",
|
||||
"float": "\0frametop_float"}
|
||||
INPUT_SETTINGS = os.path.join(REPO, "input-settings", "ft_input_settings.py")
|
||||
DISPLAY_SETTINGS = os.path.join(REPO, "display-settings", "ft_display_settings.py")
|
||||
@@ -104,7 +104,7 @@ def combo_name(combo, names, modifiers, right_modifiers):
|
||||
|
||||
|
||||
# Order on the keyboard page: the everyday ones first.
|
||||
BINDING_ORDER = ("steam_menu", "spotwatch", "gaze_left", "gaze_right", "float_toggle", "spin_next", "spin_prev",
|
||||
BINDING_ORDER = ("steam_menu", "frunner", "gaze_left", "gaze_right", "float_toggle", "spin_next", "spin_prev",
|
||||
"dictate", "dictate_hold", "pause_toggle", "screens_toggle", "layout_reset")
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ def pages(f, keys):
|
||||
"intro": "Frametop puts a Plasma desktop around you in VR: as many screens as you like, single windows "
|
||||
"floating on their own, and the monitors of your other computers. This tour takes a few minutes. "
|
||||
"Each page has buttons to try things or open their settings.",
|
||||
"note": "You can take the tour again any time: Frametop Tour is in the app menu, in spotwatch (Meta+Space), "
|
||||
"note": "You can take the tour again any time: Frametop Tour is in the app menu, in frunner (Meta+Space), "
|
||||
"in SteamVR's Launch a program list, and in Frametop Input Settings.",
|
||||
"rows": [], "actions": []}, {
|
||||
"id": "screens", "title": "Screens and the 3D mouse", "icon": "video-display",
|
||||
@@ -203,14 +203,14 @@ def pages(f, keys):
|
||||
out.append(hands)
|
||||
|
||||
out += [{
|
||||
"id": "spotwatch", "title": "Spotwatch", "icon": "search",
|
||||
"intro": "Spotwatch finds apps and remote displays by name, like Spotlight.",
|
||||
"id": "frunner", "title": "Frunner", "icon": "search",
|
||||
"intro": "Frunner finds apps and remote displays by name, like Spotlight.",
|
||||
"rows": [["Meta+Space, on any keyboard", "Opens the search in front of you"],
|
||||
["Type a name and press Enter", "A running app's window spins to the front; an app that isn't "
|
||||
"running opens floating; a remote display connects"],
|
||||
["Shift+Enter, or a right click", "More: a private window, a new window, docked on this screen"],
|
||||
["Esc", "Closes it"]],
|
||||
"actions": [{"id": "spotwatch", "text": "Open spotwatch", "icon": "search"}]}, {
|
||||
"actions": [{"id": "frunner", "text": "Open frunner", "icon": "search"}]}, {
|
||||
"id": "floating", "title": "Floating windows", "icon": "window-new",
|
||||
"intro": "Any desktop window can float in VR as a panel of its own. It's still a window of the same desktop, "
|
||||
"so drag and drop and the clipboard work between floating windows and the screens.",
|
||||
@@ -263,7 +263,7 @@ def pages(f, keys):
|
||||
more["actions"].insert(0, {"id": "input:voice", "text": "Voice", "icon": "audio-input-microphone"})
|
||||
out.append(more)
|
||||
out.append({"id": "done", "title": "That's the tour", "icon": "checkmark",
|
||||
"intro": "Frametop Tour stays in the app menu, spotwatch, and SteamVR's Launch a program list, for "
|
||||
"intro": "Frametop Tour stays in the app menu, frunner, and SteamVR's Launch a program list, for "
|
||||
"when you want it again.",
|
||||
"rows": [], "actions": []})
|
||||
return out
|
||||
@@ -355,10 +355,10 @@ def main():
|
||||
self.message.emit(("Calibration: look at each dot in the headset; right click or Meta+K stops"
|
||||
if what == "gaze:calibrate" else "Quick check: look at the dot in front of you")
|
||||
if ok else "The gaze service isn't running (gaze mode may be off)", not ok)
|
||||
elif what == "spotwatch":
|
||||
ok = send("spotwatch", "open mouse")
|
||||
self.message.emit("Spotwatch is open in front of you: type an app's name" if ok
|
||||
else "Spotwatch isn't running (it runs with the Frametop desktop)", not ok)
|
||||
elif what == "frunner":
|
||||
ok = send("frunner", "open mouse")
|
||||
self.message.emit("Frunner is open in front of you: type an app's name" if ok
|
||||
else "Frunner isn't running (it runs with the Frametop desktop)", not ok)
|
||||
elif what == "spin":
|
||||
ok = send("screens", "spin next")
|
||||
self.message.emit("Spun to the next panel. Meta+Shift+R puts the screens back" if ok
|
||||
|
||||
@@ -103,18 +103,18 @@ check("Meta+Alt+Shift+Tab", name("42+56+125+15"), "Meta+Alt+Shift+Tab")
|
||||
check("Meta+Space", name("125+57"), "Meta+Space")
|
||||
check("unknown code", name("125+999"), "Meta+999")
|
||||
|
||||
labels = {"steam_menu": "Steam menu", "gaze_left": "Left click at the gaze", "spotwatch": "Spotwatch",
|
||||
labels = {"steam_menu": "Steam menu", "gaze_left": "Left click at the gaze", "frunner": "Frunner",
|
||||
"float_toggle": "Float a window", "dictate": "Dictate"}
|
||||
bound = {"125": "steam_menu", "125+36": "gaze_left", "42+125+33": "float_toggle", "125+57": "spotwatch",
|
||||
bound = {"125": "steam_menu", "125+36": "gaze_left", "42+125+33": "float_toggle", "125+57": "frunner",
|
||||
"97": "dictate", "125+30": "command:konsole", "125+37": "none"}
|
||||
rows = ft_tour.bindings(bound, lambda a: labels.get(a, a), name)
|
||||
check("key table: everyday ones first, none left out", [r["action"] for r in rows],
|
||||
["Steam menu", "Spotwatch", "Left click at the gaze", "Float a window", "Dictate", "command:konsole"])
|
||||
["Steam menu", "Frunner", "Left click at the gaze", "Float a window", "Dictate", "command:konsole"])
|
||||
check("key table row", rows[0], {"combo": "Meta tap", "action": "Steam menu"})
|
||||
|
||||
# ---------------------------------------------------------------- the pages
|
||||
|
||||
KNOWN = {"spotwatch", "spin", "remote", "gaze:calibrate", "gaze:quickcal"}
|
||||
KNOWN = {"frunner", "spin", "remote", "gaze:calibrate", "gaze:quickcal"}
|
||||
INPUT = {"buttons", "controllers", "games", "keyboard", "voice", "pointer", "ignore", "gaze", "hands", "bluetooth"}
|
||||
DISPLAY = {"screens", "layout", "visibility", "power"}
|
||||
|
||||
@@ -128,7 +128,7 @@ none = ft_tour.features(os.path.join(tmp, "empty"), conf={})
|
||||
everything = ft_tour.features(units, conf={"POINTER_GAZE": "1", "POINTER_HANDS": "1"})
|
||||
bare, full = ft_tour.pages(none, rows), ft_tour.pages(everything, rows)
|
||||
ids = [p["id"] for p in full]
|
||||
check("pages, in order", ids, ["welcome", "screens", "keys", "gaze", "hands", "spotwatch", "floating", "pinning",
|
||||
check("pages, in order", ids, ["welcome", "screens", "keys", "gaze", "hands", "frunner", "floating", "pinning",
|
||||
"profiles", "pause", "more", "done"])
|
||||
check("the same pages with nothing installed", [p["id"] for p in bare], ids)
|
||||
for pages in (bare, full):
|
||||
|
||||
@@ -79,7 +79,7 @@ VR_BUTTONS = ("left/view", "left/dpad_up", "left/dpad_down", "left/dpad_left", "
|
||||
# (its DEFAULT_KEY_BINDINGS and VOICE_KEY_BINDINGS; voice/test checks they match).
|
||||
DEFAULT_KEY_BINDINGS = {"125": "steam_menu", "125+36": "gaze_left", "125+37": "gaze_right",
|
||||
"42+125+33": "float_toggle", "56+125+15": "spin_next", "42+56+125+15": "spin_prev",
|
||||
"125+57": "spotwatch"}
|
||||
"125+57": "frunner"}
|
||||
VOICE_KEY_BINDINGS = {"97": "dictate"}
|
||||
DICTATION_ACTIONS = ("dictate", "dictate_hold")
|
||||
# frame-voice's keys, by evdev name: the modifiers (a left one taps either side, a right one only
|
||||
|
||||
Reference in new issue
Block a user