-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
124 lines (98 loc) · 3.56 KB
/
Copy pathapp.py
File metadata and controls
124 lines (98 loc) · 3.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Multi-fetch spinner — Cmd, Batch, TickCmd, ViewState, message filter.
Demonstrates the Bubbletea-inspired patterns: lightweight Cmd thunks
instead of sagas, Batch for concurrent effects, TickCmd for self-sustaining
animation, ViewState for declarative terminal control, and a message
filter to block quit during loading.
uv run python examples/spinner/app.py
"""
from __future__ import annotations
import urllib.request
from dataclasses import dataclass, replace
from milo import (
Action,
App,
Batch,
Cmd,
Key,
Quit,
ReducerResult,
SpecialKey,
TickCmd,
ViewState,
)
from milo.live import Spinner
SPINNER = Spinner.BRAILLE
URLS = (
"https://example.com",
"https://httpbin.org/status/200",
"https://httpbin.org/status/404",
)
@dataclass(frozen=True, slots=True)
class FetchResult:
url: str
status: int = 0
error: str = ""
@dataclass(frozen=True, slots=True)
class State:
status: str = "idle" # idle | loading | done
results: tuple[FetchResult, ...] = ()
tick: int = 0
def make_fetch_cmd(url: str):
"""Create a Cmd that fetches a URL and returns the result."""
def fetch():
try:
req = urllib.request.Request(url, method="HEAD")
with urllib.request.urlopen(req, timeout=5) as resp:
return Action("FETCH_DONE", payload=FetchResult(url=url, status=resp.status))
except Exception as e:
return Action("FETCH_DONE", payload=FetchResult(url=url, error=str(e)))
return Cmd(fetch)
def block_quit_while_loading(state, action):
"""Message filter: ignore Ctrl+C while fetches are in-flight."""
if action.type == "@@QUIT" and isinstance(state, State) and state.status == "loading":
return None
return action
def reducer(state: State | None, action: Action) -> State | ReducerResult | Quit:
if state is None:
return State()
match action.type:
case "@@KEY":
key: Key = action.payload
if key.name == SpecialKey.ESCAPE:
return Quit(state, view=ViewState(cursor_visible=True))
if key.name == SpecialKey.ENTER and state.status != "loading":
# Batch-fetch all URLs concurrently + start ticking
cmds = Batch(tuple(make_fetch_cmd(url) for url in URLS))
return ReducerResult(
replace(state, status="loading", results=(), tick=0),
cmds=(cmds, TickCmd(0.08)),
view=ViewState(cursor_visible=False, window_title="Fetching..."),
)
case "@@TICK":
if state.status == "loading":
return ReducerResult(
replace(state, tick=state.tick + 1),
cmds=(TickCmd(0.08),), # Keep spinning
)
case "FETCH_DONE":
results = (*state.results, action.payload)
if len(results) == len(URLS):
# All done — stop ticking (no TickCmd returned)
return ReducerResult(
replace(state, status="done", results=results),
view=ViewState(cursor_visible=True, window_title="Done"),
)
return replace(state, results=results)
case "@@CMD_ERROR":
return replace(state, status="done")
return state
if __name__ == "__main__":
app = App.from_dir(
__file__,
template="spinner.kida",
reducer=reducer,
initial_state=State(),
exit_template="exit.kida",
filter=block_quit_while_loading,
)
app.run()