1515
1616UTC = dt .UTC
1717FORMATS = ["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
2040def _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"\n Running into { self .out_dir } ...\n " )
192+ self .run_btn .config (state = "disabled" , text = "Running..." )
193+ self .spinner .start (12 )
194+ self ._append (f"\n Computing 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 )
0 commit comments