From f32bad6d3d687e2131aaa076fa371c2a778c38bf Mon Sep 17 00:00:00 2001 From: Robin Moser Date: Thu, 2 Jul 2026 14:50:12 +0200 Subject: [PATCH] Add decimal-hours toggle (shortcut + header button) --- tests/test_client_dt.py | 17 +++++++++++++++-- timetagger/app/dialogs.py | 1 + timetagger/app/dt.py | 18 ++++++++++++++++++ timetagger/app/front.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/tests/test_client_dt.py b/tests/test_client_dt.py index 922c8aee..9598410d 100644 --- a/tests/test_client_dt.py +++ b/tests/test_client_dt.py @@ -97,8 +97,9 @@ def test_time2str(): def test_duration_string(): - js = py2js(open(dt.__file__, "rb").read().decode(), docstrings=False) - js += "\n\nwindow = {};" + js = "window = {};\n" + py2js( + open(dt.__file__, "rb").read().decode(), docstrings=False + ) js1 = evaljs(js, f"duration_string(5, false)") js2 = evaljs(js, f"duration_string(5, true)") @@ -132,6 +133,18 @@ def test_duration_string(): assert js5 == "2:01" assert js6 == "2:01:05" + # Toggle decimal mode: durations render as decimal hours + js += "toggle_duration_decimal_mode();" + js1 = evaljs(js, f"duration_string(7265, false)") + js2 = evaljs(js, f"duration_string(7265, true)") + js3 = evaljs(js, f"duration_string(-7265, false)") + assert js1 == "2.02" + assert js2 == "2.02" + assert js3 == "-2.02" + js += "toggle_duration_decimal_mode();" + js4 = evaljs(js, f"duration_string(7265, false)") + assert js4 == "2h01m" + if __name__ == "__main__": run_tests(globals()) diff --git a/timetagger/app/dialogs.py b/timetagger/app/dialogs.py index 2528a37f..f5f3061f 100644 --- a/timetagger/app/dialogs.py +++ b/timetagger/app/dialogs.py @@ -3944,6 +3944,7 @@ def open(self, callback=None): "F": "Open search dialog", "T": "Select time range", "R": "Open report dialog", + ".": "Toggle decimal hours", "I": "Open the guide", "Backspace": "Unselect all tags", } diff --git a/timetagger/app/dt.py b/timetagger/app/dt.py index eb561365..60d48e56 100644 --- a/timetagger/app/dt.py +++ b/timetagger/app/dt.py @@ -304,8 +304,26 @@ def duration_string_colon(t, show_secs=False): return f"{sign}{m // 60:.0f}:{m % 60:02.0f}" +def toggle_duration_decimal_mode(): + PSCRIPT_OVERLOAD = False # noqa + window.duration_decimal_mode = not window.duration_decimal_mode + return window.duration_decimal_mode + + +def duration_string_decimal(t, show_secs=False): + PSCRIPT_OVERLOAD = False # noqa + sign = "-" if t < 0 else "" + t = abs(t) + text = f"{sign}{t / 3600:0.2f}" + if show_secs == 2: + return (text, "") + return text + + def duration_string(t, show_secs=False, repr=None): PSCRIPT_OVERLOAD = False # noqa + if window.duration_decimal_mode: + return duration_string_decimal(t, show_secs) if not repr: repr = "hms" if window.simplesettings: diff --git a/timetagger/app/front.py b/timetagger/app/front.py index c7b2e61c..d564c237 100644 --- a/timetagger/app/front.py +++ b/timetagger/app/front.py @@ -1128,6 +1128,30 @@ def on_draw(self, ctx, menu_only=False): ) x += margin + x += self._draw_button( + ctx, + x, + y3, + None, + h, + ["fas-\uf1ec"], + "duration_decimal", + ( + "Show decimal hours [.]" + if not window.duration_decimal_mode + else "Show normal duration [.]" + ), + { + "ref": "topleft", + "color": ( + COLORS.button_text + if not window.duration_decimal_mode + else COLORS.acc_clr + ), + }, + ) + x += margin + x += self._draw_button( ctx, x, @@ -1547,6 +1571,8 @@ def _on_key(self, e): self._handle_button_press("record_stopall") elif e.key.lower() == "r": self._handle_button_press("report") + elif e.key == ".": + self._handle_button_press("duration_decimal") elif e.key.lower() == "i": self._handle_button_press("guide") else: @@ -1589,6 +1615,10 @@ def _handle_button_press(self, action): elif action == "guide": self._canvas.guide_dialog.open() + elif action == "duration_decimal": + dt.toggle_duration_decimal_mode() + self.update() + elif action == "pomo": self._canvas.pomodoro_dialog.open()