muxplex-deck 0.7.0__tar.gz → 0.8.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.
Files changed (35) hide show
  1. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/PKG-INFO +1 -1
  2. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/pyproject.toml +1 -1
  3. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/cli.py +31 -24
  4. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/main.py +36 -3
  5. muxplex_deck-0.8.0/src/muxplex_deck/service.py +1664 -0
  6. muxplex_deck-0.7.0/src/muxplex_deck/service.py +0 -973
  7. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/README.md +0 -0
  8. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/deck_probe/__init__.py +0 -0
  9. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/deck_probe/__main__.py +0 -0
  10. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/deck_probe/capabilities.py +0 -0
  11. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/deck_probe/events.py +0 -0
  12. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/deck_probe/main.py +0 -0
  13. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/deck_probe/rendering.py +0 -0
  14. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/__init__.py +0 -0
  15. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/__main__.py +0 -0
  16. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/_vendor/hidapi/LICENSE-bsd.txt +0 -0
  17. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/_vendor/hidapi/VERSION +0 -0
  18. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/_vendor/hidapi/x64/hidapi.dll +0 -0
  19. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/attention.py +0 -0
  20. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/config.py +0 -0
  21. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/device.py +0 -0
  22. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/device_real.py +0 -0
  23. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/emulator.py +0 -0
  24. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/focus.py +0 -0
  25. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/hidapi_win.py +0 -0
  26. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/hidhelp.py +0 -0
  27. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/init_wizard.py +0 -0
  28. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/interaction.py +0 -0
  29. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/layout.py +0 -0
  30. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/rendering.py +0 -0
  31. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/report.py +0 -0
  32. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/statusfile.py +0 -0
  33. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/usbnode.py +0 -0
  34. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/views.py +0 -0
  35. {muxplex_deck-0.7.0 → muxplex_deck-0.8.0}/src/muxplex_deck/wsl.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: muxplex-deck
3
- Version: 0.7.0
3
+ Version: 0.8.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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "muxplex-deck"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  description = "Hardware probe/diagnostic app for the Elgato Stream Deck+ (seed of the muxplex sidecar)"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -60,6 +60,15 @@ def _add_run_flags(parser: argparse.ArgumentParser) -> None:
60
60
  default=8484,
61
61
  help="Port for the emulator's web UI (default: 8484). Ignored without --emulator.",
62
62
  )
63
+ parser.add_argument(
64
+ "--log-file",
65
+ default=None,
66
+ help="Write logs to this file instead of stderr (default: stderr, unchanged "
67
+ "behavior). All platforms -- required in practice under Windows Task "
68
+ "Scheduler, since pythonw.exe has no console for stderr to go to "
69
+ "(see WINDOWS_NATIVE_SPEC.md section 1.5); `muxplex-deck service install` "
70
+ "passes this automatically on Windows.",
71
+ )
63
72
 
64
73
 
65
74
  def run(
@@ -67,6 +76,7 @@ def run(
67
76
  *,
68
77
  emulator: bool = False,
69
78
  emulator_port: int = 8484,
79
+ log_file: str | None = None,
70
80
  ) -> int:
71
81
  """Load config, build the device backend, and run the sidecar's main loop.
72
82
 
@@ -90,7 +100,8 @@ def run(
90
100
  print(str(exc), file=sys.stderr)
91
101
  return 1
92
102
 
93
- return main_mod.run(cfg, manager)
103
+ log_path = Path(log_file).expanduser() if log_file else None
104
+ return main_mod.run(cfg, manager, log_file=log_path)
94
105
 
95
106
 
96
107
  # ---------------------------------------------------------------------------
@@ -758,19 +769,15 @@ def check_service_status() -> tuple[str, str]:
758
769
  if sys.platform == "darwin":
759
770
  manager, tool = "launchd", "launchctl"
760
771
  elif sys.platform == "win32":
761
- # Accurate-but-misleading is still misleading: "systemctl not
762
- # found" frames this as a missing Linux tool, when the real story
763
- # is that background-service support (Task Scheduler) is a
764
- # not-yet-built increment on this platform -- not something the
765
- # user is missing or can install their way out of.
766
- return "warn", (
767
- "Service: background-service install isn't supported on "
768
- "Windows yet -- run: muxplex-deck"
769
- )
772
+ # Task Scheduler has no single "is the tool present" probe the way
773
+ # `shutil.which("systemctl")` does -- `schtasks.exe` ships with
774
+ # every Windows install, so there's nothing to gate on here; skip
775
+ # straight to the installed/active checks below.
776
+ manager, tool = "Task Scheduler", None
770
777
  else:
771
778
  manager, tool = "systemd", "systemctl"
772
779
 
773
- if shutil.which(tool) is None:
780
+ if tool is not None and shutil.which(tool) is None:
774
781
  return "warn", f"Service: {tool} not found -- run muxplex-deck directly"
775
782
 
776
783
  if not service_is_installed():
@@ -1378,19 +1385,19 @@ def update(*, force: bool = False) -> None:
1378
1385
  was_active = _service_is_active()
1379
1386
  if was_active:
1380
1387
  print(" Stopping service...")
1381
- if sys.platform == "darwin":
1382
- uid = os.getuid()
1383
- subprocess.run(
1384
- ["launchctl", "bootout", f"gui/{uid}/com.muxplex-deck"],
1385
- capture_output=True,
1386
- check=False,
1387
- )
1388
- else:
1389
- subprocess.run(
1390
- ["systemctl", "--user", "stop", "muxplex-deck"],
1391
- capture_output=True,
1392
- check=False,
1393
- )
1388
+ # Was a hardcoded launchctl/systemctl-only block -- the `else`
1389
+ # branch ran `systemctl` unconditionally, which raises
1390
+ # `FileNotFoundError` on Windows (`check=False` only suppresses a
1391
+ # nonzero *exit*, not a failed *exec*). This was latent only
1392
+ # because `service_is_active()` returned False on Windows before
1393
+ # Task Scheduler support existed -- now that it can return True
1394
+ # there, this must dispatch per platform. `service.service_stop()`
1395
+ # already does exactly that (darwin/windows/systemd), so this is
1396
+ # strictly less code, one dispatch site, not a duplicated stop
1397
+ # implementation.
1398
+ from .service import service_stop
1399
+
1400
+ service_stop()
1394
1401
  else:
1395
1402
  print(" No active service found (skipping stop)")
1396
1403
 
@@ -50,9 +50,11 @@ from __future__ import annotations
50
50
 
51
51
  import logging
52
52
  import signal
53
+ import sys
53
54
  import threading
54
55
  import time
55
56
  from collections.abc import Callable
57
+ from pathlib import Path
56
58
  from urllib.parse import urlparse
57
59
 
58
60
  from muxplex_client import (
@@ -94,7 +96,38 @@ FULL_BRIGHTNESS_PERCENT = 100
94
96
  _MAX_VIEW_LABEL_CHARS = 20
95
97
 
96
98
 
97
- def _configure_logging() -> None:
99
+ def _configure_logging(log_file: Path | None = None) -> None:
100
+ """Configure logging, optionally to a rotating file instead of stderr.
101
+
102
+ `log_file` is required in practice on Windows under Task Scheduler:
103
+ `pythonw.exe` (the GUI-subsystem interpreter `service._resolve_pythonw()`
104
+ resolves to -- see WINDOWS_NATIVE_SPEC.md section 1.5) leaves
105
+ `sys.stdout`/`sys.stderr` as `None`, and `logging.StreamHandler()` around
106
+ a `None` stream would fail. `cli.py`'s `--log-file` flag (all platforms,
107
+ default `None`) is what plumbs a path down to here; on macOS/Linux this
108
+ stays `None` and behavior is byte-for-byte unchanged from before this
109
+ parameter existed.
110
+ """
111
+ if log_file is not None:
112
+ from logging.handlers import RotatingFileHandler
113
+
114
+ log_file.parent.mkdir(parents=True, exist_ok=True)
115
+ handler = RotatingFileHandler(
116
+ log_file, maxBytes=2_000_000, backupCount=3, encoding="utf-8"
117
+ )
118
+ handler.setFormatter(
119
+ logging.Formatter(
120
+ fmt="%(asctime)s.%(msecs)03d %(levelname)s %(message)s",
121
+ datefmt="%H:%M:%S",
122
+ )
123
+ )
124
+ logging.basicConfig(level=logging.INFO, handlers=[handler])
125
+ return
126
+ if sys.stderr is None:
127
+ # Defensive: a console-less launch with no --log-file must not
128
+ # crash trying to build a StreamHandler around a None stream.
129
+ logging.basicConfig(level=logging.INFO, handlers=[logging.NullHandler()])
130
+ return
98
131
  logging.basicConfig(
99
132
  level=logging.INFO,
100
133
  format="%(asctime)s.%(msecs)03d %(levelname)s %(message)s",
@@ -1035,8 +1068,8 @@ def _install_signal_handler() -> threading.Event:
1035
1068
  return shutting_down
1036
1069
 
1037
1070
 
1038
- def run(config: Config, manager: DeviceManager) -> int:
1039
- _configure_logging()
1071
+ def run(config: Config, manager: DeviceManager, *, log_file: Path | None = None) -> int:
1072
+ _configure_logging(log_file)
1040
1073
 
1041
1074
  shutting_down = _install_signal_handler()
1042
1075
  hostname = urlparse(config.server_url).hostname or config.server_url