|
1 | | -#!/bin/bash |
| 1 | +#!/Users/evanthomas/.dotfiles/scripts/venv-stopwatch/bin/python |
2 | 2 |
|
3 | | -# stolen from https://stackoverflow.com/questions/37986523/how-can-i-create-a-stopwatch-in-bash |
4 | | -now=$(date +%s)sec; watch -n0.1 -p TZ=UTC date --date now-$now +%H:%M:%S.%N |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import argparse |
| 6 | +import re |
| 7 | +import time |
| 8 | + |
| 9 | + |
| 10 | +def _no_curses(offset: TimeOffset | None = None): |
| 11 | + now = time.time() |
| 12 | + |
| 13 | + while True: |
| 14 | + elapsed = time.time() - now + (offset.offset_seconds if offset else 0) |
| 15 | + |
| 16 | + hours, remainder = divmod(int(elapsed), 3600) |
| 17 | + minutes, seconds = divmod(remainder, 60) |
| 18 | + elapsed_time = f"{hours:02}:{minutes:02}:{seconds:02}" |
| 19 | + # https://stackoverflow.com/questions/37774983/clearing-the-screen-by-printing-a-character |
| 20 | + print("\033c") |
| 21 | + print(elapsed_time, end="\n\r") |
| 22 | + time.sleep(1) |
| 23 | + |
| 24 | + |
| 25 | +def _curses(offset: TimeOffset | None = None): |
| 26 | + import curses |
| 27 | + |
| 28 | + def _run(stdscr): |
| 29 | + # Initialize curses |
| 30 | + curses.curs_set(0) # Hide the cursor |
| 31 | + stdscr.clear() # Clear the screen |
| 32 | + |
| 33 | + now = time.time() |
| 34 | + while True: |
| 35 | + # Calculate elapsed time |
| 36 | + elapsed = time.time() - now + (offset.offset_seconds if offset else 0) |
| 37 | + hours, remainder = divmod(int(elapsed), 3600) |
| 38 | + minutes, seconds = divmod(remainder, 60) |
| 39 | + elapsed_time = f"{hours:02}:{minutes:02}:{seconds:02}" |
| 40 | + |
| 41 | + # Display elapsed time in the center of the screen |
| 42 | + height, width = stdscr.getmaxyx() |
| 43 | + x = width // 2 - len(elapsed_time) // 2 |
| 44 | + y = height // 2 |
| 45 | + stdscr.addstr(y, x, elapsed_time) |
| 46 | + stdscr.refresh() |
| 47 | + |
| 48 | + time.sleep(1) |
| 49 | + |
| 50 | + curses.wrapper(_run) |
| 51 | + |
| 52 | + |
| 53 | +class TimeOffset: |
| 54 | + def __init__(self, offset_seconds: int): |
| 55 | + self.offset_seconds = offset_seconds |
| 56 | + |
| 57 | + @classmethod |
| 58 | + def parse(cls, time_str: str) -> TimeOffset | None: |
| 59 | + """ |
| 60 | + time_str is like 5m, 1h, '5minutes', '5 minutes', '1 hour' |
| 61 | + """ |
| 62 | + pattern = r"(?P<value>\d+)\s*(?P<unit>\w+)$" |
| 63 | + m = re.match(pattern, time_str) |
| 64 | + if m is None: |
| 65 | + return None |
| 66 | + |
| 67 | + value = int(m.group("value")) |
| 68 | + unit = m.group("unit") |
| 69 | + |
| 70 | + conversion = { |
| 71 | + "m": 60, |
| 72 | + "minute": 60, |
| 73 | + "minutes": 60, |
| 74 | + "h": 3600, |
| 75 | + "hour": 3600, |
| 76 | + "hours": 3600, |
| 77 | + "s": 1, |
| 78 | + "second": 1, |
| 79 | + "seconds": 1, |
| 80 | + }.get(unit) |
| 81 | + if conversion is None: |
| 82 | + raise ValueError(f"Unknown unit {unit}") |
| 83 | + return TimeOffset(conversion * value) |
| 84 | + |
| 85 | + |
| 86 | +def test_time_offset(): |
| 87 | + good_patterns = [ |
| 88 | + ("5m", 300), |
| 89 | + ("5 m", 300), |
| 90 | + ("5minutes", 300), |
| 91 | + ("5 minutes", 300), |
| 92 | + ("5s", 5), |
| 93 | + ("5 second", 5), |
| 94 | + ("5hour", 18_000), |
| 95 | + ("5h", 18_000), |
| 96 | + ] |
| 97 | + for time_str, expected_offset in good_patterns: |
| 98 | + o = TimeOffset.parse(time_str) |
| 99 | + assert o is not None, f"Expected {time_str} to parse, got None" |
| 100 | + assert ( |
| 101 | + o.offset_seconds == expected_offset |
| 102 | + ), f"Expected {expected_offset=} for {time_str=} got {o.offset_seconds=}" |
| 103 | + |
| 104 | + |
| 105 | +def main(): |
| 106 | + parser = argparse.ArgumentParser() |
| 107 | + parser.add_argument( |
| 108 | + "--no-curses", action="store_true", help="Use plain text instead of curses" |
| 109 | + ) |
| 110 | + parser.add_argument("--offset", type=TimeOffset.parse, required=False) |
| 111 | + |
| 112 | + args = parser.parse_args() |
| 113 | + if args.no_curses: |
| 114 | + _no_curses(offset=args.offset) |
| 115 | + else: |
| 116 | + _curses(offset=args.offset) |
| 117 | + |
| 118 | + |
| 119 | +if __name__ == "__main__": |
| 120 | + main() |
0 commit comments