busymeter 0.2.0__py3-none-any.whl
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.
- busymeter/__init__.py +39 -0
- busymeter/__main__.py +7 -0
- busymeter/cli.py +120 -0
- busymeter/client.py +553 -0
- busymeter/recipes/__init__.py +32 -0
- busymeter/recipes/_ws.py +186 -0
- busymeter/recipes/autostart.py +311 -0
- busymeter/recipes/base.py +51 -0
- busymeter/recipes/cli.py +206 -0
- busymeter/recipes/control.py +268 -0
- busymeter/recipes/focus_timer.py +118 -0
- busymeter/recipes/game_state.py +331 -0
- busymeter/recipes/manage.py +260 -0
- busymeter/recipes/runner.py +360 -0
- busymeter/recipes/settings.py +357 -0
- busymeter/recipes/stream_status.py +227 -0
- busymeter/recipes/vibe_meter.py +391 -0
- busymeter/tokens.py +202 -0
- busymeter-0.2.0.dist-info/METADATA +169 -0
- busymeter-0.2.0.dist-info/RECORD +28 -0
- busymeter-0.2.0.dist-info/WHEEL +5 -0
- busymeter-0.2.0.dist-info/entry_points.txt +3 -0
- busymeter-0.2.0.dist-info/licenses/LICENSE +21 -0
- busymeter-0.2.0.dist-info/top_level.txt +2 -0
- busymeter_mcp/__init__.py +5 -0
- busymeter_mcp/__main__.py +3 -0
- busymeter_mcp/server.py +149 -0
- busymeter_mcp/tools.py +284 -0
busymeter/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Busy Meter plugin API client (MIT). Standard library only. See README.md."""
|
|
2
|
+
|
|
3
|
+
from .client import (
|
|
4
|
+
API_VERSION,
|
|
5
|
+
NOT_RESPONDING_MESSAGE,
|
|
6
|
+
NOT_RUNNING_MESSAGE,
|
|
7
|
+
AppNotRespondingError,
|
|
8
|
+
DEFAULT_PORT,
|
|
9
|
+
EFFECTS,
|
|
10
|
+
LEVELS,
|
|
11
|
+
MAX_TTL_SECONDS,
|
|
12
|
+
TURN_ON_MESSAGE,
|
|
13
|
+
WANTS,
|
|
14
|
+
ApprovalTimeoutError,
|
|
15
|
+
BusyMeter,
|
|
16
|
+
BusyMeterError,
|
|
17
|
+
CancelledError,
|
|
18
|
+
ConflictError,
|
|
19
|
+
DeniedError,
|
|
20
|
+
Endpoint,
|
|
21
|
+
ForbiddenError,
|
|
22
|
+
NotListeningError,
|
|
23
|
+
RateLimitedError,
|
|
24
|
+
UnauthorizedError,
|
|
25
|
+
discover,
|
|
26
|
+
discovery_path,
|
|
27
|
+
normalize_level,
|
|
28
|
+
parse_duration,
|
|
29
|
+
)
|
|
30
|
+
from .tokens import TokenStore, default_token_dir
|
|
31
|
+
|
|
32
|
+
__version__ = "0.2.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"API_VERSION", "NOT_RESPONDING_MESSAGE", "NOT_RUNNING_MESSAGE", "AppNotRespondingError", "DEFAULT_PORT", "EFFECTS", "LEVELS", "MAX_TTL_SECONDS", "TURN_ON_MESSAGE", "WANTS",
|
|
36
|
+
"ApprovalTimeoutError", "BusyMeter", "BusyMeterError", "CancelledError", "ConflictError", "DeniedError", "Endpoint",
|
|
37
|
+
"ForbiddenError", "NotListeningError", "RateLimitedError", "TokenStore", "UnauthorizedError",
|
|
38
|
+
"default_token_dir", "discover", "discovery_path", "normalize_level", "parse_duration",
|
|
39
|
+
]
|
busymeter/__main__.py
ADDED
busymeter/cli.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""``busymeter`` command: set a level from the command line.
|
|
2
|
+
|
|
3
|
+
busymeter level 5 --for 10m # 5 stars for 10 minutes, then back to whatever the app shows
|
|
4
|
+
busymeter level flash --for 30s
|
|
5
|
+
busymeter level 4 --for 2h # longer than 10 minutes: stays running and refreshes; Ctrl+C clears
|
|
6
|
+
busymeter clear
|
|
7
|
+
busymeter status
|
|
8
|
+
busymeter run vibe_meter # a bundled recipe: stream_status, game_state, focus_timer, vibe_meter
|
|
9
|
+
busymeter focus 25m # a focus block (--pomodoro for cycles)
|
|
10
|
+
busymeter recipes # which recipes are installed or running
|
|
11
|
+
busymeter stop vibe_meter # stop one running in the background
|
|
12
|
+
|
|
13
|
+
The first run asks you to click Allow in the Busy Meter tray (needs Settings > Sources > External connections on).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
from .client import MAX_TTL_SECONDS, BusyMeter, BusyMeterError, normalize_level, parse_duration
|
|
23
|
+
|
|
24
|
+
DEFAULT_ID = "com.thebusymeter.cli"
|
|
25
|
+
REFRESH_TTL = 60
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def safe_console() -> None:
|
|
29
|
+
"""Messages use arrows (Settings → Sources); don't crash on a console code page without them."""
|
|
30
|
+
for stream in (sys.stdout, sys.stderr):
|
|
31
|
+
try:
|
|
32
|
+
stream.reconfigure(errors="replace")
|
|
33
|
+
except (AttributeError, ValueError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def call(meter, fn):
|
|
38
|
+
"""Run fn; if the token was revoked, register again (the user clicks Allow in the tray) and retry once."""
|
|
39
|
+
reconnecting = getattr(meter, "reconnecting", None)
|
|
40
|
+
return reconnecting(fn) if reconnecting is not None else fn()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _connect(meter: BusyMeter) -> None:
|
|
44
|
+
meter.connect(
|
|
45
|
+
name="Busy Meter command line",
|
|
46
|
+
wants=("signals", "status"),
|
|
47
|
+
on_wait=lambda: print("Waiting for you to click Allow in the Busy Meter tray...", file=sys.stderr),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def hold_level(meter: BusyMeter, level: int | str, seconds: int, *, sleep=time.sleep, clock=time.monotonic) -> None:
|
|
52
|
+
"""Show ``level`` for ``seconds``. Up to 10 minutes is one call; longer refreshes every 30 s, then clears."""
|
|
53
|
+
if seconds <= MAX_TTL_SECONDS:
|
|
54
|
+
call(meter, lambda: meter.set_level(level, seconds))
|
|
55
|
+
return
|
|
56
|
+
end = clock() + seconds
|
|
57
|
+
try:
|
|
58
|
+
while True:
|
|
59
|
+
left = end - clock()
|
|
60
|
+
if left <= 0:
|
|
61
|
+
break
|
|
62
|
+
ttl = max(1, min(REFRESH_TTL, int(left + 0.999)))
|
|
63
|
+
call(meter, lambda: meter.set_level(level, ttl))
|
|
64
|
+
sleep(min(REFRESH_TTL / 2, max(left, 0)))
|
|
65
|
+
finally:
|
|
66
|
+
try:
|
|
67
|
+
meter.clear()
|
|
68
|
+
except BusyMeterError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: list[str] | None = None) -> int:
|
|
73
|
+
safe_console()
|
|
74
|
+
parser = argparse.ArgumentParser(prog="busymeter", description="Set the Busy Meter's level from the command line.")
|
|
75
|
+
parser.add_argument("--id", default=DEFAULT_ID, help=f"plugin id to register as (default {DEFAULT_ID})")
|
|
76
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
77
|
+
level = sub.add_parser("level", help="show a level for a while")
|
|
78
|
+
level.add_argument("level", help="0-6 (stars; 0 = lip accent only) or flash")
|
|
79
|
+
level.add_argument("--for", dest="duration", default="10m", help="how long, e.g. 30s, 10m, 2h (default 10m)")
|
|
80
|
+
sub.add_parser("clear", help="clear this command's level now")
|
|
81
|
+
sub.add_parser("status", help="print what the meter shows")
|
|
82
|
+
from .recipes import cli as recipes_cli
|
|
83
|
+
|
|
84
|
+
recipes_cli.add_commands(sub)
|
|
85
|
+
args = parser.parse_args(argv)
|
|
86
|
+
if recipes_cli.handles(args.command):
|
|
87
|
+
return recipes_cli.dispatch(args, parser)
|
|
88
|
+
|
|
89
|
+
meter = BusyMeter(args.id)
|
|
90
|
+
try:
|
|
91
|
+
if args.command == "level":
|
|
92
|
+
try:
|
|
93
|
+
value = normalize_level(args.level)
|
|
94
|
+
seconds = parse_duration(args.duration)
|
|
95
|
+
except ValueError as e:
|
|
96
|
+
parser.error(str(e))
|
|
97
|
+
_connect(meter)
|
|
98
|
+
if seconds > MAX_TTL_SECONDS:
|
|
99
|
+
print(f"Holding {value} for {seconds} s; keep this running (Ctrl+C clears).", file=sys.stderr)
|
|
100
|
+
hold_level(meter, value, seconds)
|
|
101
|
+
if seconds <= MAX_TTL_SECONDS:
|
|
102
|
+
print(f"Level {value} for {seconds} s.")
|
|
103
|
+
elif args.command == "clear":
|
|
104
|
+
_connect(meter)
|
|
105
|
+
call(meter, meter.clear)
|
|
106
|
+
print("Cleared.")
|
|
107
|
+
else:
|
|
108
|
+
_connect(meter)
|
|
109
|
+
s = call(meter, meter.status)
|
|
110
|
+
print(f"level={s.get('level')} paused={str(s.get('paused')).lower()} source={s.get('source')}")
|
|
111
|
+
except KeyboardInterrupt:
|
|
112
|
+
return 130
|
|
113
|
+
except BusyMeterError as e:
|
|
114
|
+
print(e.message, file=sys.stderr)
|
|
115
|
+
return 1
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
sys.exit(main())
|