Skip to content

Commit 83e8011

Browse files
fix(gui): windows
add spinner
1 parent b0f99e9 commit 83e8011

4 files changed

Lines changed: 71 additions & 17 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ brew install osgeonepal/tap/osmsg # macOS / Linux (Homebrew tap)
4646
```
4747

4848
On Windows, download `osmsg.exe` from the [latest release](https://github.com/osgeonepal/osmsg/releases)
49-
and double-click it to open the desktop app. Fill in the dates and options, click Run, and open the
50-
output folder. The CLI below is for macOS, Linux, and pip/uv users.
49+
and double-click it to open the desktop app. Pick a Quick range (last hour, day, week, month, year, or
50+
all time) or type your own dates, set the options, click Compute, and open the output folder. The CLI
51+
below is for macOS, Linux, and pip/uv users.
5152

5253
## Quick start
5354

@@ -175,7 +176,8 @@ Same pipeline as the CLI.
175176
osmsg --config nepal.yaml
176177
```
177178

178-
Any flag works as a YAML key. See [docs/Manual.md](./docs/Manual.md) for the full list.
179+
Each option is a YAML key written with its underscore name: `output_dir`, `history_url`, `all_stats`,
180+
`formats`, `psql_dsn`, and so on (not the dashed flag). See [docs/Manual.md](./docs/Manual.md).
179181

180182
## Output formats
181183

docs/Manual.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ re-exports straight from that store, so adding a format is instant. Changing any
8383

8484
## Config file
8585

86-
Long invocations are easier to maintain in YAML. Keys mirror the CLI flag names.
86+
Long invocations are easier to maintain in YAML. Each key is the option's underscore name (the flag
87+
with `-` turned to `_`, so `output_dir`, `history_url`, `psql_dsn`); `--all` is `all_stats` and
88+
`--keys` is `keys_only`. A key that matches no option is ignored.
8789

8890
```bash
8991
osmsg --config nepal.yaml # all flags from yaml

osmsg/gui.py

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@
1515

1616
UTC = dt.UTC
1717
FORMATS = ["parquet", "csv", "json", "markdown"]
18+
PRESETS = ["Last hour", "Last day", "Last week", "Last month", "Last year", "All time"]
19+
_PRESET_DELTAS = {
20+
"Last hour": dt.timedelta(hours=1),
21+
"Last day": dt.timedelta(days=1),
22+
"Last week": dt.timedelta(days=7),
23+
"Last month": dt.timedelta(days=30),
24+
"Last year": dt.timedelta(days=365),
25+
}
26+
27+
28+
def preset_range(name: str, now: dt.datetime | None = None) -> tuple[dt.datetime, dt.datetime]:
29+
"""Resolve a quick-range label to a (start, end) window."""
30+
now = now or dt.datetime.now(UTC)
31+
if name == "All time":
32+
return dt.datetime(2005, 1, 1, tzinfo=UTC), now
33+
return now - _PRESET_DELTAS[name], now
34+
35+
36+
def _fmt(when: dt.datetime) -> str:
37+
return when.strftime("%Y-%m-%d %H:%M:%S")
1838

1939

2040
def _parse_date(value: str) -> dt.datetime | None:
@@ -112,31 +132,45 @@ def __init__(self) -> None:
112132
ttk.Entry(frame, textvariable=var, width=40).grid(row=i, column=1, columnspan=3, sticky="we", pady=2)
113133
self.vars[key] = var
114134

135+
preset_frame = ttk.LabelFrame(frame, text="Quick range", padding=6)
136+
preset_frame.grid(row=5, column=0, columnspan=4, sticky="we", pady=6)
137+
for i, name in enumerate(PRESETS):
138+
ttk.Button(preset_frame, text=name, width=11, command=lambda n=name: self._apply_preset(n)).grid(
139+
row=0, column=i, padx=2
140+
)
141+
115142
self.vars["all_tags"] = tk.BooleanVar()
116143
self.vars["summary"] = tk.BooleanVar()
117-
ttk.Checkbutton(frame, text="All tags", variable=self.vars["all_tags"]).grid(row=5, column=0, sticky="w")
118-
ttk.Checkbutton(frame, text="Daily summary", variable=self.vars["summary"]).grid(row=5, column=1, sticky="w")
144+
ttk.Checkbutton(frame, text="All tags", variable=self.vars["all_tags"]).grid(row=6, column=0, sticky="w")
145+
ttk.Checkbutton(frame, text="Daily summary", variable=self.vars["summary"]).grid(row=6, column=1, sticky="w")
119146

120147
fmt_frame = ttk.LabelFrame(frame, text="Formats", padding=6)
121-
fmt_frame.grid(row=6, column=0, columnspan=4, sticky="we", pady=6)
148+
fmt_frame.grid(row=7, column=0, columnspan=4, sticky="we", pady=6)
122149
for i, name in enumerate(FORMATS):
123150
var = tk.BooleanVar(value=name in ("parquet", "csv"))
124151
ttk.Checkbutton(fmt_frame, text=name, variable=var).grid(row=0, column=i, padx=4)
125152
self.vars[name] = var
126153

127154
self.out_label = ttk.Label(frame, text=f"Output: {self.out_dir}")
128-
self.out_label.grid(row=7, column=0, columnspan=3, sticky="w")
129-
ttk.Button(frame, text="Choose folder", command=self._choose_folder).grid(row=7, column=3, sticky="e")
155+
self.out_label.grid(row=8, column=0, columnspan=3, sticky="w")
156+
ttk.Button(frame, text="Choose folder", command=self._choose_folder).grid(row=8, column=3, sticky="e")
130157

131-
self.run_btn = ttk.Button(frame, text="Run", command=self._on_run)
132-
self.run_btn.grid(row=8, column=0, pady=8, sticky="w")
158+
self.run_btn = ttk.Button(frame, text="Compute", command=self._on_run)
159+
self.run_btn.grid(row=9, column=0, pady=8, sticky="w")
133160
self.open_btn = ttk.Button(frame, text="Open output folder", command=lambda: _open_folder(Path(self.out_dir)))
134-
self.open_btn.grid(row=8, column=1, pady=8, sticky="w")
161+
self.open_btn.grid(row=9, column=1, pady=8, sticky="w")
162+
self.spinner = ttk.Progressbar(frame, mode="indeterminate", length=160)
163+
self.spinner.grid(row=9, column=2, columnspan=2, pady=8, sticky="we")
135164

136165
self.log = scrolledtext.ScrolledText(frame, width=70, height=14, state="disabled")
137-
self.log.grid(row=9, column=0, columnspan=4, sticky="nsew")
166+
self.log.grid(row=10, column=0, columnspan=4, sticky="nsew")
138167
self.root.after(120, self._drain)
139168

169+
def _apply_preset(self, name: str) -> None:
170+
start, end = preset_range(name)
171+
self.vars["start"].set(_fmt(start))
172+
self.vars["end"].set(_fmt(end))
173+
140174
def _choose_folder(self) -> None:
141175
chosen = self._filedialog.askdirectory(initialdir=self.out_dir)
142176
if chosen:
@@ -155,8 +189,9 @@ def _on_run(self) -> None:
155189
except OsmsgError as exc:
156190
self._append(f"\n{exc}\n")
157191
return
158-
self.run_btn.config(state="disabled")
159-
self._append(f"\nRunning into {self.out_dir} ...\n")
192+
self.run_btn.config(state="disabled", text="Running...")
193+
self.spinner.start(12)
194+
self._append(f"\nComputing into {self.out_dir} ...\n")
160195
threading.Thread(target=self._worker, args=(cfg,), daemon=True).start()
161196

162197
def _worker(self, cfg: RunConfig) -> None:
@@ -182,7 +217,8 @@ def _drain(self) -> None:
182217
self._append(payload)
183218
else:
184219
self._append(f"\n{payload}\n")
185-
self.run_btn.config(state="normal")
220+
self.spinner.stop()
221+
self.run_btn.config(state="normal", text="Compute")
186222
except queue.Empty:
187223
pass
188224
self.root.after(120, self._drain)

tests/test_gui.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import pytest
66

77
from osmsg.exceptions import OsmsgError
8-
from osmsg.gui import build_config
8+
from osmsg.gui import PRESETS, build_config, preset_range
99

1010
UTC = dt.UTC
11+
NOW = dt.datetime(2026, 6, 24, 12, 0, tzinfo=UTC)
1112

1213

1314
def test_build_config_maps_fields(tmp_path):
@@ -54,3 +55,16 @@ def test_build_config_requires_format(tmp_path):
5455
def test_build_config_rejects_bad_date(tmp_path):
5556
with pytest.raises(OsmsgError, match="date"):
5657
build_config({"start": "01/01/2024", "parquet": True}, str(tmp_path))
58+
59+
60+
def test_preset_range_rolling_windows():
61+
assert preset_range("Last hour", NOW) == (NOW - dt.timedelta(hours=1), NOW)
62+
assert preset_range("Last day", NOW) == (NOW - dt.timedelta(days=1), NOW)
63+
assert preset_range("Last month", NOW) == (NOW - dt.timedelta(days=30), NOW)
64+
assert preset_range("All time", NOW) == (dt.datetime(2005, 1, 1, tzinfo=UTC), NOW)
65+
66+
67+
def test_every_preset_resolves():
68+
for name in PRESETS:
69+
start, end = preset_range(name, NOW)
70+
assert start < end

0 commit comments

Comments
 (0)