Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ What does this PR do?

## Screenshots

## Manual verification
## Added test coverage

What did you test on hardware? (Steam Controller chord open/close, touchpad typing, split layout, touchscreen taps, tray toggles.)
Which behaviors got new/updated headless tests? (pytest suite, `python -m pytest tests -q`.)
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ jobs:

- name: Build Windows binaries
working-directory: windows
env:
# Tag builds name their artifacts after the pushed tag; empty on
# workflow_dispatch (build.py then falls back to the latest tag).
DUALTOUCH_VERSION: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: python build.py --installer

- name: Package Windows release
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ It is a Windows-only fork of [SteamlessKeyboard](https://github.com/PietPetGit/S

- **Global overlay** — draws above every app, not tied to Big Picture or a launcher.
- **Native SDL3 rendering** — low CPU/memory use; skins, transparency, and size selectable from the tray.
- **Steam Controller input** — two-finger trackpad typing with hit-target expansion and debounce, DPAD/stick navigation, auto-repeat on held keys, configurable insert control (bumpers or triggers).
- **Steam Controller input** — two-finger trackpad typing with hit-target expansion and debounce, DPAD/stick navigation, auto-repeat on held keys, configurable insert control (bumpers or triggers), Lift-Off Typing (insert on finger release; the pad press-click then shows a key's accented variants — slide to pick, release commits), click-hold opens accent rows without Lift-Off Typing, six selectable key layouts (language boards inject their symbols as characters, so they type true on any Windows input language).
- **Steam Input coexistence** — reads the shared HID alongside Steam Input, switches to a keyboard layer via `steam://forceinputappid`, and restores your config on close.
- **Opens with a chord** — Steam + X/Y/A/B (configurable) opens the keyboard; Steam's own menu is suppressed via a Guide Button Chord dead-binding.
- **Per-app memory** — remembers the OSK size, skin, and position per foreground app.
Expand Down Expand Up @@ -85,6 +85,8 @@ menus), or by hand-editing `settings.json` for finer control.
| `sc_click_button` | `"L1/R1"` | Click insert per side: `"L1/R1"` (bumpers) or `"L2/R2"` (triggers) |
| `sc_pad_click_enter` | `false` | Trackpad press-click inserts the key under the pointer |
| `sc_pad_click_engage` | `2500` | Pad force that fires the pad-click insert |
| `sc_liftoff_enter` | `false` | Lift-Off Typing: insert the key under the pointer when the finger lifts off the pad (no click needed); the pad press-click then opens a key's accented variants |
| `osk_layout` | `"QWERTY"` | Key layout: `"QWERTY"`/`"AZERTY"`/`"QWERTZ"`/`"Dvorak"`/`"Colemak"`/`"ABC"` (tray "Keyboard Layout"; applied at the next open) |
| `sc_left_stick_nav` | `true` | Sticks control the keyboard while it is open |
| `sc_osk_trigger_actuation` | `"default"` | L2/R2 actuation point: `"default"` or `"low"` |
| `skin` | `"Gruvbox"` | Keyboard skin: Steam OSK themes load from the Steam install; Gruvbox is the bundled original |
Expand Down
15 changes: 15 additions & 0 deletions windows/appsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
# "L1/R1" = bumpers (the original behavior, default) / "L2/R2" = triggers.
"sc_pad_click_enter": False,
"sc_click_button": "L1/R1",
# Lift-off typing (tray "Steam Controller -> Lift-Off Typing"): insert the
# key under the pointer when the finger LIFTS off the trackpad — no pad
# press or click button needed. The click paths keep working alongside it;
# a touch that already clicked never double-fires on lift.
"sc_liftoff_enter": False,
# Click-button focus (the lock-on-key that follows the click button):
# when the click button is L2/R2, the trigger's analog pull (0..32767) at
# which the pointer freezes on the key center (default = half pull); the
Expand All @@ -107,6 +112,11 @@
# reach). Applied on the next OSK open (or live while open, like the
# other SC toggles).
"osk_split_layout": False,
# On-screen-keyboard key layout (tray "Keyboard Layout" radio): which
# bundled layout the OSK builds at the next open. "QWERTY" keeps the
# original keyboard-layout.yaml; the full name->file map lives in
# triton/layouts.py (AZERTY/QWERTZ/Dvorak/Colemak/ABC bundled).
"osk_layout": "QWERTY",
# Per-foreground-app remembered OSK "Move" position: {exe name (lowercase):
# position index 0-5}. Remembered per app so the keyboard reopens where the
# user left it in each app; apps without an entry keep the current spot
Expand Down Expand Up @@ -336,6 +346,11 @@ def _load_settings():
merged[k] = _coerce_number(val, default)
elif k == "skin":
merged[k] = _valid_skin_name(val) or default
elif k == "osk_layout":
# Same plain-identifier rule as the skin name: a layout name is
# matched against triton/layouts.py's registry and must never
# carry a path. (The tray re-validates against the registry.)
merged[k] = _valid_skin_name(val) or default
elif k == "window_position_per_app":
merged[k] = _valid_position_per_app(val)
elif k == "osk_size_per_app":
Expand Down
32 changes: 24 additions & 8 deletions windows/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
from ctypes import wintypes

PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
ENTRY = os.path.join("tray", "__main__.py")
# Compile the package itself (never tray/__main__.py) in -m package mode,
# so the frozen program runs exactly like the documented `python -m tray`.
ENTRY = "tray"
OUTPUT_NAME = "DualTouch-windows"
HELPER_EXE_NAME = "DualTouch-cursor-helper.exe"
DIST_DIR = os.path.join(PROJECT_DIR, "dist")
Expand All @@ -45,20 +47,29 @@
ISS_PATH = os.path.join(INSTALLER_DIR, "DualTouch.iss")


def _app_version():
"""Latest git tag (v-prefixed tags welcome) as the exe version, or 0.0.0
when building outside a tagged checkout (CI uses fetch-depth: 0)."""
def _latest_git_tag():
"""Latest tag reachable from HEAD ('' when unavailable), v-prefix kept."""
try:
tag = subprocess.check_output(
return subprocess.check_output(
["git", "describe", "--tags", "--abbrev=0"],
cwd=PROJECT_DIR,
text=True,
stderr=subprocess.DEVNULL,
).strip()
except Exception:
return "0.0.0"
m = re.match(r"v?(\d[\w.\-]*)", tag)
return m.group(1) if m else "0.0.0"
return ""


def _app_version():
"""The exe/installer version. Precedence: the DUALTOUCH_VERSION env var
(CI forwards the pushed tag's ref name there), then the latest reachable
git tag; "latest" when neither yields a version-looking string (untagged
local builds)."""
for candidate in (os.environ.get("DUALTOUCH_VERSION"), _latest_git_tag()):
m = re.match(r"v?(\d[\w.\-]*)", (candidate or "").strip())
if m:
return m.group(1)
return "latest"


def _preflight():
Expand Down Expand Up @@ -379,6 +390,11 @@ def _run_nuitka():
"--include-module=pynput.mouse._win32",
"--include-module=PIL._tkinter_finder",
"--include-package=sdl3w",
# Our local `triton` package collides with the ML 'triton' PyPI
# package in anti-bloat's eyes ("undesirable import"); allow it so
# every import of our own code compiles without warnings.
"--noinclude-custom-mode=triton:allow",
"--python-flag=-m",
ENTRY,
]
print("running:", " ".join(cmd))
Expand Down
93 changes: 93 additions & 0 deletions windows/data/cfg/keyboard-layout-abc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
# ABC 75% layout — same board shape as keyboard-layout.yaml with the 26
# letters in strict alphabetical order, reading left-to-right, top-to-bottom
# (a-l on the top row, m-v on home, then w z and punctuation below). Built
# for hunt-and-peck on a touchpad: every letter sits where a first-time user
# will guess. Positional like every layout: scancodes sit at their US-QWERTY
# positions, so on a stock US Windows layout the labels always match what
# lands in the text field.
keys:
- - {label: "Esc", keycode: KEY_ESC, width_weight: 1.1, modifier: true, align: left, valign: bottom, outline_opacity: 0.33}
- {label: "F1", keycode: KEY_F1, modifier: true, outline_opacity: 0.33}
- {label: "F2", keycode: KEY_F2, modifier: true, outline_opacity: 0.33}
- {label: "F3", keycode: KEY_F3, modifier: true, outline_opacity: 0.33}
- {label: "F4", keycode: KEY_F4, modifier: true, outline_opacity: 0.33}
- {label: "F5", keycode: KEY_F5, modifier: true, outline_opacity: 0.33}
- {label: "F6", keycode: KEY_F6, modifier: true, outline_opacity: 0.33}
- {label: "F7", keycode: KEY_F7, modifier: true, outline_opacity: 0.33}
- {label: "F8", keycode: KEY_F8, modifier: true, outline_opacity: 0.33}
- {label: "F9", keycode: KEY_F9, modifier: true, outline_opacity: 0.33}
- {label: "F10", keycode: KEY_F10, modifier: true, outline_opacity: 0.33}
- {label: "F11", keycode: KEY_F11, modifier: true, outline_opacity: 0.33}
- {label: "F12", keycode: KEY_F12, modifier: true, outline_opacity: 0.33}
- {label: "Home", keycode: KEY_HOME, modifier: true, outline_opacity: 0.33}
- {label: "End", keycode: KEY_END, modifier: true, outline_opacity: 0.33}
- {label: "Ins", keycode: KEY_INSERT, modifier: true, outline_opacity: 0.33}

- - {label: "`", shifted: "~", keycode: KEY_GRAVE}
- {label: "1", shifted: "!", keycode: KEY_1}
- {label: "2", shifted: "@", keycode: KEY_2}
- {label: "3", shifted: "#", keycode: KEY_3}
- {label: "4", shifted: "$", keycode: KEY_4}
- {label: "5", shifted: "%", keycode: KEY_5}
- {label: "6", shifted: "^", keycode: KEY_6}
- {label: "7", shifted: "&", keycode: KEY_7}
- {label: "8", shifted: "*", keycode: KEY_8}
- {label: "9", shifted: "(", keycode: KEY_9}
- {label: "0", shifted: ")", keycode: KEY_0}
- {label: "-", shifted: "_", keycode: KEY_MINUS}
- {label: "=", shifted: "+", keycode: KEY_EQUAL}
- {label: "Backspace", behavior: backspace, keycode: KEY_BACKSPACE, width_weight: 1.4, modifier: true, glyph: glyph_x.png}

- - {label: "Tab", keycode: KEY_TAB, width_weight: 1.4, modifier: true, align: left, valign: bottom, outline_opacity: 0.33}
- {label: "a", shifted: "A", keycode: KEY_A}
- {label: "b", shifted: "B", keycode: KEY_B}
- {label: "c", shifted: "C", keycode: KEY_C}
- {label: "d", shifted: "D", keycode: KEY_D}
- {label: "e", shifted: "E", keycode: KEY_E}
- {label: "f", shifted: "F", keycode: KEY_F}
- {label: "g", shifted: "G", keycode: KEY_G}
- {label: "h", shifted: "H", keycode: KEY_H}
- {label: "i", shifted: "I", keycode: KEY_I}
- {label: "j", shifted: "J", keycode: KEY_J}
- {label: "k", shifted: "K", keycode: KEY_K}
- {label: "l", shifted: "L", keycode: KEY_L}
- {label: "\\", shifted: "|", keycode: KEY_BACKSLASH, outline_opacity: 0.33}

- - {label: "Caps", keycode: KEY_CAPSLOCK, width_weight: 1.6, modifier: true, glyph: glyph_l3.png, outline_opacity: 0.33}
- {label: "m", shifted: "M", keycode: KEY_M}
- {label: "n", shifted: "N", keycode: KEY_N}
- {label: "o", shifted: "O", keycode: KEY_O}
- {label: "p", shifted: "P", keycode: KEY_P}
- {label: "q", shifted: "Q", keycode: KEY_Q}
- {label: "r", shifted: "R", keycode: KEY_R}
- {label: "s", shifted: "S", keycode: KEY_S}
- {label: "t", shifted: "T", keycode: KEY_T}
- {label: "u", shifted: "U", keycode: KEY_U}
- {label: "v", shifted: "V", keycode: KEY_V}
- {label: "'", shifted: "\"", keycode: KEY_APOSTROPHE, legacy_label_pos: true}
- {label: "Enter", keycode: KEY_ENTER, width_weight: 1.7, modifier: true, align: right, valign: bottom, glyph: sc_r2_md.png, outline_opacity: 0.33}

- - {label: "Shift", behavior: shift, keycode: KEY_LEFTSHIFT, width_weight: 2.0, modifier: true, align: left, valign: bottom, glyph: glyph_l2.png, outline_opacity: 0.33}
- {label: "w", shifted: "W", keycode: KEY_W}
- {label: "x", shifted: "X", keycode: KEY_X}
- {label: "y", shifted: "Y", keycode: KEY_Y}
- {label: "z", shifted: "Z", keycode: KEY_Z}
- {label: ",", shifted: "<", keycode: KEY_COMMA, dpad_down: 3, legacy_label_pos: true}
- {label: ".", shifted: ">", keycode: KEY_DOT, legacy_label_pos: true}
- {label: "/", shifted: "?", keycode: KEY_SLASH, legacy_label_pos: true}
- {label: ";", shifted: ":", keycode: KEY_SEMICOLON, legacy_label_pos: true}
- {label: "[", shifted: "{", keycode: KEY_LEFTBRACE, outline_opacity: 0.33}
- {label: "]", shifted: "}", keycode: KEY_RIGHTBRACE, outline_opacity: 0.33}
- {label: "Select", behavior: select, keycode: KEY_SELECT, width_weight: 2.0, modifier: true, align: right, valign: bottom, outline_opacity: 0.33}

- - {label: "Ctrl", behavior: ctrl, keycode: KEY_LEFTCTRL, width_weight: 1.2, modifier: true, align: left, valign: bottom}
- {label: "Alt", behavior: alt, keycode: KEY_LEFTALT, width_weight: 1.1, modifier: true, align: left, valign: bottom}
- {label: "Win", keycode: KEY_LEFTWIN, width_weight: 1.1, modifier: true, align: left, valign: bottom}
- {label: "Space", keycode: KEY_SPACE, width_weight: 4.5, glyph: glyph_y.png}
- {label: "◀", keycode: KEY_LEFT, width_weight: 0.9, modifier: true, font: symbol, legacy_label_pos: true}
- {label: "▲", keycode: KEY_UP, width_weight: 0.9, modifier: true, font: symbol, legacy_label_pos: true}
- {label: "▼", keycode: KEY_DOWN, width_weight: 0.9, modifier: true, font: symbol, legacy_label_pos: true}
- {label: "▶", keycode: KEY_RIGHT, width_weight: 0.9, modifier: true, font: symbol, legacy_label_pos: true}
- {label: "Paste", shifted: "Copy", behavior: paste, width_weight: 1.2, modifier: true, dual_top_dy: 4, dual_bottom_dy: 4, outline_opacity: 0.33}
- {label: "Move", behavior: move, width_weight: 1.4, modifier: true, valign: top, glyph: glyph_smiley.png}
Loading