muxplex-deck 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/PKG-INFO +18 -19
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/README.md +17 -18
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/pyproject.toml +1 -1
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/cli.py +230 -25
- muxplex_deck-0.5.0/src/muxplex_deck/hidhelp.py +390 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/init_wizard.py +27 -3
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/main.py +108 -8
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/service.py +219 -32
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/statusfile.py +11 -0
- muxplex_deck-0.5.0/src/muxplex_deck/usbnode.py +109 -0
- muxplex_deck-0.5.0/src/muxplex_deck/wsl.py +221 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/deck_probe/__init__.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/deck_probe/__main__.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/deck_probe/capabilities.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/deck_probe/events.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/deck_probe/main.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/deck_probe/rendering.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/__init__.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/__main__.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/attention.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/config.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/device.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/device_real.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/emulator.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/focus.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/interaction.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/layout.py +0 -0
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/rendering.py +1 -1
- {muxplex_deck-0.4.0 → muxplex_deck-0.5.0}/src/muxplex_deck/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: muxplex-deck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Hardware probe/diagnostic app for the Elgato Stream Deck+ (seed of the muxplex sidecar)
|
|
5
5
|
Author: Amplifier
|
|
6
6
|
Author-email: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
|
|
@@ -108,10 +108,14 @@ without a udev rule granting your user access to the device (vendor id
|
|
|
108
108
|
prints a copy-pasteable remediation block (the same rule shown in
|
|
109
109
|
"Permissions" under the hardware probe section above) instead of silently
|
|
110
110
|
installing a service that won't work. It never writes to `/etc` itself --
|
|
111
|
-
only detects and reports.
|
|
111
|
+
only detects and reports. This remediation is only ever printed when udev
|
|
112
|
+
is actually running (`/run/udev/control` present) -- on WSL without
|
|
113
|
+
systemd, or in a container, it never fires anyway, so `muxplex-deck`
|
|
114
|
+
detects that and prints a different, WSL/container-appropriate fix instead
|
|
115
|
+
(see `docs/WSL.md`). Run the printed `sudo tee ... && sudo udevadm
|
|
112
116
|
control --reload-rules && sudo udevadm trigger` commands, then unplug and
|
|
113
|
-
replug the deck (or
|
|
114
|
-
restart`.
|
|
117
|
+
replug the deck (or `muxplex-deck wsl attach` under WSL) and `muxplex-deck
|
|
118
|
+
service restart`.
|
|
115
119
|
|
|
116
120
|
On Linux, `service install` also attempts `loginctl enable-linger
|
|
117
121
|
<you>` (best-effort, non-fatal) so the service keeps running after you log
|
|
@@ -159,25 +163,20 @@ uv run deck-probe
|
|
|
159
163
|
|
|
160
164
|
#### Windows + WSL
|
|
161
165
|
|
|
162
|
-
USB devices are
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
```powershell
|
|
166
|
-
usbipd list # find the Stream Deck's BUSID (VID 0fd9)
|
|
167
|
-
usbipd bind --busid <BUSID> # first time only
|
|
168
|
-
usbipd attach --wsl --busid <BUSID>
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Then inside WSL, grant HID access via a udev rule (first time only):
|
|
166
|
+
USB devices are invisible to WSL until Windows hands them over. Rather
|
|
167
|
+
than driving `usbipd`/udev rules by hand for this PoC probe, install and
|
|
168
|
+
use the actual product instead -- it handles this for you:
|
|
172
169
|
|
|
173
170
|
```sh
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# then detach/re-attach the device (usbipd detach + attach)
|
|
178
|
-
sudo apt install libhidapi-libusb0 # native HIDAPI, if not present
|
|
171
|
+
uv tool install muxplex-deck
|
|
172
|
+
muxplex-deck wsl attach # finds the deck, attaches it, tells you what's left
|
|
173
|
+
muxplex-deck doctor # diagnoses every remaining step, with real values
|
|
179
174
|
```
|
|
180
175
|
|
|
176
|
+
Neither needs the details -- but see `docs/WSL.md` if you want them (BUSID/
|
|
177
|
+
device-node churn, the `usbipd` vs `usbipd.exe` trap, durable vs per-attach
|
|
178
|
+
permission options).
|
|
179
|
+
|
|
181
180
|
**Close the Elgato Stream Deck app on Windows first** -- it holds
|
|
182
181
|
exclusive HID access. The probe prints this same guidance when it finds
|
|
183
182
|
no device.
|
|
@@ -95,10 +95,14 @@ without a udev rule granting your user access to the device (vendor id
|
|
|
95
95
|
prints a copy-pasteable remediation block (the same rule shown in
|
|
96
96
|
"Permissions" under the hardware probe section above) instead of silently
|
|
97
97
|
installing a service that won't work. It never writes to `/etc` itself --
|
|
98
|
-
only detects and reports.
|
|
98
|
+
only detects and reports. This remediation is only ever printed when udev
|
|
99
|
+
is actually running (`/run/udev/control` present) -- on WSL without
|
|
100
|
+
systemd, or in a container, it never fires anyway, so `muxplex-deck`
|
|
101
|
+
detects that and prints a different, WSL/container-appropriate fix instead
|
|
102
|
+
(see `docs/WSL.md`). Run the printed `sudo tee ... && sudo udevadm
|
|
99
103
|
control --reload-rules && sudo udevadm trigger` commands, then unplug and
|
|
100
|
-
replug the deck (or
|
|
101
|
-
restart`.
|
|
104
|
+
replug the deck (or `muxplex-deck wsl attach` under WSL) and `muxplex-deck
|
|
105
|
+
service restart`.
|
|
102
106
|
|
|
103
107
|
On Linux, `service install` also attempts `loginctl enable-linger
|
|
104
108
|
<you>` (best-effort, non-fatal) so the service keeps running after you log
|
|
@@ -146,25 +150,20 @@ uv run deck-probe
|
|
|
146
150
|
|
|
147
151
|
#### Windows + WSL
|
|
148
152
|
|
|
149
|
-
USB devices are
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
```powershell
|
|
153
|
-
usbipd list # find the Stream Deck's BUSID (VID 0fd9)
|
|
154
|
-
usbipd bind --busid <BUSID> # first time only
|
|
155
|
-
usbipd attach --wsl --busid <BUSID>
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Then inside WSL, grant HID access via a udev rule (first time only):
|
|
153
|
+
USB devices are invisible to WSL until Windows hands them over. Rather
|
|
154
|
+
than driving `usbipd`/udev rules by hand for this PoC probe, install and
|
|
155
|
+
use the actual product instead -- it handles this for you:
|
|
159
156
|
|
|
160
157
|
```sh
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# then detach/re-attach the device (usbipd detach + attach)
|
|
165
|
-
sudo apt install libhidapi-libusb0 # native HIDAPI, if not present
|
|
158
|
+
uv tool install muxplex-deck
|
|
159
|
+
muxplex-deck wsl attach # finds the deck, attaches it, tells you what's left
|
|
160
|
+
muxplex-deck doctor # diagnoses every remaining step, with real values
|
|
166
161
|
```
|
|
167
162
|
|
|
163
|
+
Neither needs the details -- but see `docs/WSL.md` if you want them (BUSID/
|
|
164
|
+
device-node churn, the `usbipd` vs `usbipd.exe` trap, durable vs per-attach
|
|
165
|
+
permission options).
|
|
166
|
+
|
|
168
167
|
**Close the Elgato Stream Deck app on Windows first** -- it holds
|
|
169
168
|
exclusive HID access. The probe prints this same guidance when it finds
|
|
170
169
|
no device.
|
|
@@ -251,11 +251,14 @@ def _get_install_info() -> dict:
|
|
|
251
251
|
return info
|
|
252
252
|
|
|
253
253
|
|
|
254
|
+
_PYPI_PROJECT_URL = "https://pypi.org/pypi/muxplex-deck/json"
|
|
255
|
+
|
|
256
|
+
|
|
254
257
|
def _check_for_update(info: dict) -> tuple[bool, str]:
|
|
255
258
|
"""Check if an update is available.
|
|
256
259
|
|
|
257
|
-
muxplex-deck is
|
|
258
|
-
|
|
260
|
+
muxplex-deck 0.4.0+ is published to PyPI, so both the git and pypi
|
|
261
|
+
comparison paths apply now. Editable installs are never flagged.
|
|
259
262
|
"""
|
|
260
263
|
if info["source"] == "editable":
|
|
261
264
|
return False, "editable install -- manage updates manually"
|
|
@@ -283,6 +286,24 @@ def _check_for_update(info: dict) -> tuple[bool, str]:
|
|
|
283
286
|
except Exception: # noqa: BLE001 -- any git/network failure means "upgrade to be safe"
|
|
284
287
|
return True, "check failed -- upgrading to be safe"
|
|
285
288
|
|
|
289
|
+
if info["source"] == "pypi":
|
|
290
|
+
try:
|
|
291
|
+
import httpx
|
|
292
|
+
|
|
293
|
+
response = httpx.get(
|
|
294
|
+
_PYPI_PROJECT_URL,
|
|
295
|
+
headers={"Accept": "application/json"},
|
|
296
|
+
timeout=10,
|
|
297
|
+
)
|
|
298
|
+
response.raise_for_status()
|
|
299
|
+
latest = response.json()["info"]["version"]
|
|
300
|
+
current = info["version"]
|
|
301
|
+
if latest == current:
|
|
302
|
+
return False, f"up to date (v{current})"
|
|
303
|
+
return True, f"update available (v{current} -> v{latest})"
|
|
304
|
+
except Exception: # noqa: BLE001 -- any network/parse failure means "upgrade to be safe"
|
|
305
|
+
return True, "could not check PyPI -- upgrading to be safe"
|
|
306
|
+
|
|
286
307
|
return True, "unknown install source -- could not check"
|
|
287
308
|
|
|
288
309
|
|
|
@@ -400,16 +421,9 @@ def probe_deck_status(manager: Any) -> dict:
|
|
|
400
421
|
|
|
401
422
|
_NO_DEVICE_GUIDANCE = (
|
|
402
423
|
"No Stream Deck found. Things to check:\n"
|
|
403
|
-
" -
|
|
404
|
-
"
|
|
405
|
-
" -
|
|
406
|
-
" (vendor id 0x0fd9). Run 'muxplex-deck service install' for the exact\n"
|
|
407
|
-
" remediation block, or see AGENTS.md.\n"
|
|
408
|
-
" - WSL specifically: USB devices are not visible until attached from\n"
|
|
409
|
-
" Windows via usbipd -- in an admin PowerShell:\n"
|
|
410
|
-
" usbipd list (find the Stream Deck's BUSID)\n"
|
|
411
|
-
" usbipd bind --busid <BUSID> (first time only)\n"
|
|
412
|
-
" usbipd attach --wsl --busid <BUSID>\n"
|
|
424
|
+
" - Close the official Elgato Stream Deck app -- it holds exclusive HID access,\n"
|
|
425
|
+
" so muxplex-deck cannot open the device while it is running.\n"
|
|
426
|
+
" - Check the USB cable and try a different port.\n"
|
|
413
427
|
)
|
|
414
428
|
|
|
415
429
|
|
|
@@ -481,11 +495,13 @@ def check_hid_openable() -> tuple[str, str]:
|
|
|
481
495
|
if service_is_active():
|
|
482
496
|
return "ok", "HID: device in use by the muxplex-deck service (expected)"
|
|
483
497
|
|
|
498
|
+
from . import hidhelp
|
|
499
|
+
|
|
484
500
|
hint = ""
|
|
485
501
|
if sys.platform not in ("darwin", "win32") and not udev_rule_exists():
|
|
486
|
-
hint =
|
|
502
|
+
hint = hidhelp.HID_HINT_RUN_SERVICE_INSTALL
|
|
487
503
|
elif sys.platform not in ("darwin", "win32"):
|
|
488
|
-
hint =
|
|
504
|
+
hint = hidhelp.HID_HINT_RULE_EXISTS_BUT_STILL_FAILED
|
|
489
505
|
return "warn", f"HID: could not open device ({status['error']}).{hint}"
|
|
490
506
|
|
|
491
507
|
|
|
@@ -647,6 +663,15 @@ def doctor(config_path: str | None = None) -> int:
|
|
|
647
663
|
ca_file = config_mod._expand(raw["ca_file"])
|
|
648
664
|
checks.append(check_ca_file(ca_file))
|
|
649
665
|
|
|
666
|
+
# Environment guidance (WSL/usbipd/udev-liveness) BEFORE the device
|
|
667
|
+
# checks -- it explains why the next line is about to warn. Returns []
|
|
668
|
+
# on a healthy platform (macOS, or native Linux with udev running), so
|
|
669
|
+
# this adds no output there -- see hidhelp.explain_environment().
|
|
670
|
+
from . import hidhelp
|
|
671
|
+
|
|
672
|
+
for guidance in hidhelp.explain_environment():
|
|
673
|
+
checks.append((guidance.status, guidance.message))
|
|
674
|
+
|
|
650
675
|
checks.append(check_deck_detected(config_path))
|
|
651
676
|
checks.append(check_hid_openable())
|
|
652
677
|
|
|
@@ -681,6 +706,9 @@ def _format_device_line(device: dict[str, Any]) -> tuple[str, str]:
|
|
|
681
706
|
if not device.get("connected"):
|
|
682
707
|
return "warn", "Device: not connected"
|
|
683
708
|
caps = device.get("capabilities") or {}
|
|
709
|
+
hint = device.get("hint")
|
|
710
|
+
if not caps and hint:
|
|
711
|
+
return "warn", "Device: connected (capabilities unavailable)"
|
|
684
712
|
if not caps:
|
|
685
713
|
return "ok", "Device: connected (capabilities unavailable)"
|
|
686
714
|
touchscreen = "yes" if caps.get("has_touchscreen") else "no"
|
|
@@ -780,7 +808,15 @@ def status(config_path: str | None = None, *, as_json: bool = False) -> int:
|
|
|
780
808
|
else:
|
|
781
809
|
print_check("ok", f"Status updated {age:.0f}s ago (pid {data.get('pid', '?')})")
|
|
782
810
|
|
|
783
|
-
|
|
811
|
+
device_data = data.get("device", {})
|
|
812
|
+
print_check(*_format_device_line(device_data))
|
|
813
|
+
hint = device_data.get("hint")
|
|
814
|
+
if hint and not device_data.get("connected"):
|
|
815
|
+
# Populated by the sidecar's open-failure branch (see main.py /
|
|
816
|
+
# hidhelp.explain_open_failure) -- this is what turns a stale
|
|
817
|
+
# status file into the primary teaching surface instead of a
|
|
818
|
+
# bare "not connected" with no explanation.
|
|
819
|
+
print_check("warn", hint)
|
|
784
820
|
print_check(*_format_server_line(data.get("server", {})))
|
|
785
821
|
print_check("ok", _format_state_line(data.get("state", {})))
|
|
786
822
|
|
|
@@ -855,19 +891,54 @@ def _service_is_active() -> bool:
|
|
|
855
891
|
return service_is_active()
|
|
856
892
|
|
|
857
893
|
|
|
858
|
-
def update() -> None:
|
|
894
|
+
def update(*, force: bool = False) -> None:
|
|
859
895
|
"""Update muxplex-deck to the latest version and restart the service (if installed).
|
|
860
896
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
897
|
+
Respects how the tool was installed (see `_get_install_info`), mirroring
|
|
898
|
+
muxplex's own `upgrade()`: a `pypi` install upgrades from PyPI (package
|
|
899
|
+
name, so `uv tool install --force muxplex-deck` / `pip install --upgrade
|
|
900
|
+
muxplex-deck`); a `git` (or `unknown`) install keeps reinstalling from
|
|
901
|
+
git HEAD via `_REPO_URL`, exactly as before -- this honors an explicit
|
|
902
|
+
git install rather than migrating it. An `editable` install is left
|
|
903
|
+
alone entirely (dev checkout -- manage it via git yourself).
|
|
904
|
+
|
|
905
|
+
Now that a real PyPI release exists, the "already up to date" version
|
|
906
|
+
gate this module previously lacked (see AGENTS.md history) is real:
|
|
907
|
+
unless `force=True`, an install already at the latest version/commit
|
|
908
|
+
is reported and left untouched rather than reinstalled and restarted
|
|
909
|
+
for no code change. Reporting style (plain ` ERROR: ...` on stderr
|
|
910
|
+
equivalent) matches muxplex's.
|
|
866
911
|
"""
|
|
867
912
|
from .service import service_install
|
|
868
913
|
|
|
869
914
|
print("\nmuxplex-deck update\n")
|
|
870
915
|
|
|
916
|
+
info = _get_install_info()
|
|
917
|
+
commit_suffix = f" @ {info['commit'][:8]}" if info["commit"] else ""
|
|
918
|
+
print(
|
|
919
|
+
f" Installed: muxplex-deck {info['version']} (via {info['source']}{commit_suffix})"
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
if info["source"] == "editable":
|
|
923
|
+
print(
|
|
924
|
+
"\n Editable install detected -- manage updates via git yourself (no action taken).\n"
|
|
925
|
+
)
|
|
926
|
+
return
|
|
927
|
+
|
|
928
|
+
if force:
|
|
929
|
+
print(" Status: --force specified -- skipping version check")
|
|
930
|
+
else:
|
|
931
|
+
update_available, message = _check_for_update(info)
|
|
932
|
+
print(f" Status: {message}")
|
|
933
|
+
if not update_available:
|
|
934
|
+
print(
|
|
935
|
+
"\n Already up to date."
|
|
936
|
+
" Use 'muxplex-deck update --force' to reinstall anyway.\n"
|
|
937
|
+
)
|
|
938
|
+
return
|
|
939
|
+
|
|
940
|
+
install_target = "muxplex-deck" if info["source"] == "pypi" else f"git+{_REPO_URL}"
|
|
941
|
+
|
|
871
942
|
was_active = _service_is_active()
|
|
872
943
|
if was_active:
|
|
873
944
|
print(" Stopping service...")
|
|
@@ -896,7 +967,7 @@ def update() -> None:
|
|
|
896
967
|
uv_path = _find_uv()
|
|
897
968
|
if uv_path:
|
|
898
969
|
result = subprocess.run(
|
|
899
|
-
[uv_path, "tool", "install", "--force",
|
|
970
|
+
[uv_path, "tool", "install", "--force", install_target],
|
|
900
971
|
capture_output=True,
|
|
901
972
|
text=True,
|
|
902
973
|
check=False,
|
|
@@ -910,7 +981,7 @@ def update() -> None:
|
|
|
910
981
|
pip_path = _find_pip()
|
|
911
982
|
if pip_path:
|
|
912
983
|
result = subprocess.run(
|
|
913
|
-
[pip_path, "install", "--upgrade",
|
|
984
|
+
[pip_path, "install", "--upgrade", install_target],
|
|
914
985
|
capture_output=True,
|
|
915
986
|
text=True,
|
|
916
987
|
check=False,
|
|
@@ -960,6 +1031,122 @@ def update() -> None:
|
|
|
960
1031
|
doctor()
|
|
961
1032
|
|
|
962
1033
|
|
|
1034
|
+
# ---------------------------------------------------------------------------
|
|
1035
|
+
# wsl attach -- find, share-check, and attach the Stream Deck under WSL2.
|
|
1036
|
+
#
|
|
1037
|
+
# This is the ONLY command whose entire purpose is to mutate host state
|
|
1038
|
+
# (WSL_COLD_START_SPEC.md section 6.1): it's the sole caller of
|
|
1039
|
+
# `wsl.attach()`. Its deciding rationale isn't convenience -- it removes
|
|
1040
|
+
# the `usbipd` vs `usbipd.exe` trap from the recurring path entirely (a
|
|
1041
|
+
# user who never types either name cannot get the wrong binary; see
|
|
1042
|
+
# section 6.4). Never invokes `sudo` (P3) -- when a permission fix is
|
|
1043
|
+
# still needed after attaching, it prints the command, it doesn't run it.
|
|
1044
|
+
# ---------------------------------------------------------------------------
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def wsl_attach(*, vendor_id: str = "0fd9") -> int:
|
|
1048
|
+
"""`muxplex-deck wsl attach` -- attach the Stream Deck to this WSL2 distro.
|
|
1049
|
+
|
|
1050
|
+
Exit 0 on a successful attach (even if a follow-up permission fix is
|
|
1051
|
+
still needed -- the attach itself succeeded). Exit 1 for every state
|
|
1052
|
+
that stops short of attaching (not WSL, WSL1, usbipd.exe missing, not
|
|
1053
|
+
found, not shared) so scripts can branch on it.
|
|
1054
|
+
"""
|
|
1055
|
+
from . import hidhelp
|
|
1056
|
+
from . import usbnode as usbnode_mod
|
|
1057
|
+
from . import wsl as wsl_mod
|
|
1058
|
+
|
|
1059
|
+
print("\nmuxplex-deck wsl attach\n")
|
|
1060
|
+
|
|
1061
|
+
info = wsl_mod.detect()
|
|
1062
|
+
if not info.is_wsl:
|
|
1063
|
+
print_check(
|
|
1064
|
+
"fail", "Not running under WSL -- this command only makes sense there."
|
|
1065
|
+
)
|
|
1066
|
+
print()
|
|
1067
|
+
return 1
|
|
1068
|
+
print_check("ok", f"WSL{info.version} detected ({info.kernel})")
|
|
1069
|
+
|
|
1070
|
+
if info.version == 1:
|
|
1071
|
+
print_check("fail", hidhelp.W1_MESSAGE)
|
|
1072
|
+
print()
|
|
1073
|
+
return 1
|
|
1074
|
+
|
|
1075
|
+
paths = wsl_mod.find_usbipd()
|
|
1076
|
+
if paths.linux_impostor is not None and paths.windows is not None:
|
|
1077
|
+
print_check("warn", hidhelp.impostor_message(paths))
|
|
1078
|
+
|
|
1079
|
+
if paths.windows is None:
|
|
1080
|
+
print_check("warn", hidhelp.W2_MESSAGE)
|
|
1081
|
+
print()
|
|
1082
|
+
return 1
|
|
1083
|
+
print_check("ok", f"usbipd.exe: {paths.windows}")
|
|
1084
|
+
|
|
1085
|
+
devices = wsl_mod.list_devices(paths.windows, vendor_id=vendor_id)
|
|
1086
|
+
if devices is None:
|
|
1087
|
+
print_check("warn", hidhelp.W2_MESSAGE)
|
|
1088
|
+
print()
|
|
1089
|
+
return 1
|
|
1090
|
+
if not devices:
|
|
1091
|
+
print_check("warn", hidhelp.NO_MATCHING_WINDOWS_DEVICE_MESSAGE)
|
|
1092
|
+
print()
|
|
1093
|
+
return 1
|
|
1094
|
+
|
|
1095
|
+
device = devices[0]
|
|
1096
|
+
print_check(
|
|
1097
|
+
"ok",
|
|
1098
|
+
f"Found on Windows: BUSID {device.busid} {device.vid_pid} {device.description}",
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
if device.state == "not_shared":
|
|
1102
|
+
print_check("warn", hidhelp.w4_message(device))
|
|
1103
|
+
print()
|
|
1104
|
+
return 1
|
|
1105
|
+
|
|
1106
|
+
if device.state == "unknown":
|
|
1107
|
+
print_check("warn", hidhelp.w3_unknown_state_message(device))
|
|
1108
|
+
print()
|
|
1109
|
+
return 1
|
|
1110
|
+
|
|
1111
|
+
if device.state == "shared":
|
|
1112
|
+
print_check("ok", "Shared -- attaching...")
|
|
1113
|
+
success, message = wsl_mod.attach(paths.windows, device.busid)
|
|
1114
|
+
if not success:
|
|
1115
|
+
print_check("fail", f"Attach failed: {message}")
|
|
1116
|
+
print()
|
|
1117
|
+
return 1
|
|
1118
|
+
print_check("ok", "Attached")
|
|
1119
|
+
else:
|
|
1120
|
+
print_check("ok", f"Already attached (BUSID {device.busid})")
|
|
1121
|
+
|
|
1122
|
+
node = usbnode_mod.find_usb_node(vendor_id)
|
|
1123
|
+
if node is None:
|
|
1124
|
+
print_check("warn", hidhelp.w6_message(device))
|
|
1125
|
+
print()
|
|
1126
|
+
return 1
|
|
1127
|
+
print_check("ok", f"Visible to Linux: {node.path}")
|
|
1128
|
+
|
|
1129
|
+
if not node.readable_writable:
|
|
1130
|
+
print_check("warn", hidhelp.w7_message(node))
|
|
1131
|
+
if not usbnode_mod.udev_is_live():
|
|
1132
|
+
print_check(
|
|
1133
|
+
"warn",
|
|
1134
|
+
hidhelp.u_dead_wsl_message(wsl_mod.wsl_conf_systemd_state()),
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
print()
|
|
1138
|
+
print(" Next:")
|
|
1139
|
+
print(" muxplex-deck service restart")
|
|
1140
|
+
print(" muxplex-deck status")
|
|
1141
|
+
print()
|
|
1142
|
+
print(
|
|
1143
|
+
" The device number changes on every attach -- re-run "
|
|
1144
|
+
"`muxplex-deck wsl attach`\n after any unplug or Windows reboot."
|
|
1145
|
+
)
|
|
1146
|
+
print()
|
|
1147
|
+
return 0
|
|
1148
|
+
|
|
1149
|
+
|
|
963
1150
|
# ---------------------------------------------------------------------------
|
|
964
1151
|
# main
|
|
965
1152
|
# ---------------------------------------------------------------------------
|
|
@@ -989,11 +1176,16 @@ def main() -> None:
|
|
|
989
1176
|
"--json", action="store_true", help="Emit raw status as JSON"
|
|
990
1177
|
)
|
|
991
1178
|
|
|
992
|
-
sub.add_parser(
|
|
1179
|
+
update_parser = sub.add_parser(
|
|
993
1180
|
"update",
|
|
994
1181
|
aliases=["upgrade"],
|
|
995
1182
|
help="Update muxplex-deck to the latest version and restart the service",
|
|
996
1183
|
)
|
|
1184
|
+
update_parser.add_argument(
|
|
1185
|
+
"--force",
|
|
1186
|
+
action="store_true",
|
|
1187
|
+
help="Reinstall even if already at the latest version/commit",
|
|
1188
|
+
)
|
|
997
1189
|
|
|
998
1190
|
init_parser = sub.add_parser(
|
|
999
1191
|
"init",
|
|
@@ -1024,6 +1216,13 @@ def main() -> None:
|
|
|
1024
1216
|
"key", nargs="?", help="Config key (omit to reset all)"
|
|
1025
1217
|
)
|
|
1026
1218
|
|
|
1219
|
+
wsl_parser = sub.add_parser("wsl", help="WSL2 USB/IP helpers")
|
|
1220
|
+
wsl_sub = wsl_parser.add_subparsers(dest="wsl_command")
|
|
1221
|
+
wsl_sub.add_parser(
|
|
1222
|
+
"attach",
|
|
1223
|
+
help="Find, share-check, and attach the Stream Deck to this WSL2 distro",
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1027
1226
|
service_parser = sub.add_parser(
|
|
1028
1227
|
"service", help="Manage the muxplex-deck background service"
|
|
1029
1228
|
)
|
|
@@ -1057,7 +1256,7 @@ def main() -> None:
|
|
|
1057
1256
|
status(getattr(args, "config", None), as_json=getattr(args, "json", False))
|
|
1058
1257
|
)
|
|
1059
1258
|
elif args.command in ("update", "upgrade"):
|
|
1060
|
-
update()
|
|
1259
|
+
update(force=getattr(args, "force", False))
|
|
1061
1260
|
elif args.command == "init":
|
|
1062
1261
|
from .init_wizard import run_init
|
|
1063
1262
|
|
|
@@ -1068,6 +1267,12 @@ def main() -> None:
|
|
|
1068
1267
|
non_interactive=getattr(args, "non_interactive", False),
|
|
1069
1268
|
)
|
|
1070
1269
|
)
|
|
1270
|
+
elif args.command == "wsl":
|
|
1271
|
+
cmd = getattr(args, "wsl_command", None)
|
|
1272
|
+
if cmd == "attach":
|
|
1273
|
+
sys.exit(wsl_attach())
|
|
1274
|
+
else:
|
|
1275
|
+
wsl_parser.print_help()
|
|
1071
1276
|
elif args.command == "config":
|
|
1072
1277
|
config_path = getattr(args, "config", None)
|
|
1073
1278
|
cmd = getattr(args, "config_command", None)
|