#!/usr/bin/python3 # The MIT License (MIT) # # Copyright © 2024 pacman64 # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from datetime import datetime from sys import argv, exit, stderr, stdin, stdout from threading import Lock, Thread from time import sleep info = ''' timer Show a live timer on stderr, which updates `in-place` by not emitting line-feeds, and which shows both the time elapsed since its start, as as well as the current date/time. If anything comes from stdin, this will show its lines verbatim on stdout; piping data from stdin is optional. ''' if len(argv) > 1 and argv[1] in ('-h', '--h', '-help', '--help'): print(info.strip(), file=stderr) exit(0) if len(argv) > 1: msg = f'\x1b[31m{argv[0]}: no args are supported/allowed\x1b[0m' print(msg, file=stderr) exit(1) # clear hides previous contents on a line of text, while staying on the # same line; this is used to visually-clear the timer line on stderr clear = f'\r{" " * 60}\r' # mutex serializes output from the 2 concurrent tasks, avoiding any # collisions mutex = Lock() # done tells the timer task when it's time to quit; always use while # mutex is `engaged`, both when checking/changing it done = False def timer_done() -> bool: 'Check safely if the timer task should end right away.' with mutex: return done def emit_time(started: datetime, now: datetime) -> None: 'This func does what it says, without using mutexes.' dur = now - started h, rest = divmod(dur.seconds, 3600) m, s = divmod(rest, 60) fmt = '%Y-%m-%d %H:%M:%S %b %a' print(f'{h:02}:{m:02}:{s:02} {now.strftime(fmt)}', end='', file=stderr) stderr.flush() def track_time(started: datetime) -> None: 'Handle the separate task which live-updates the timer line.' while not timer_done(): # show an updated timer line on stderr with mutex: now = datetime.now() print(clear, end='', file=stderr) emit_time(started, now) # wait a second, while checking for the quit-condition more # frequently than that, to make script end right away for _ in range(20): if timer_done(): break sleep(1/20) # when script is done, end with a final timer line on stderr track_last_time(started) def track_last_time(started: datetime) -> None: 'Show/update the timer line for the last time.' with mutex: now = datetime.now() # visually-clear stderr contents on its incomplete line print(clear, end='', file=stderr) # after visual clearing, update timer message on stderr emit_time(started, now) dur = now - started decs = int(dur.microseconds / 1000) # complete the final stderr line with the total time elapsed print(f' {dur.seconds}.{decs:03} seconds', file=stderr) def handle_lines(w, src) -> None: 'Emit all lines, as they come from the input-source given.' for line in src: with mutex: now = datetime.now() # visually-clear stderr contents on its incomplete line print(clear, end='', file=stderr) # emit latest line from stdin, ignoring carriage-returns print(line.rstrip('\r\n').rstrip('\n'), file=w) # after visual clearing, update timer message on stderr emit_time(started, now) try: code = 0 started = datetime.now() # start timer task separately from the main one, which handles # lines from stdin t = Thread(target=track_time, args=[started], daemon=False) t.start() # quit when/if the stdin is over; the timer task is otherwise # supposed to continue until force-quit by the user # handle stdin, ignoring trailing carriage-returns in input lines handle_lines(stdout, stdin) except (BrokenPipeError, KeyboardInterrupt): code = 1 # quit quietly, instead of showing a confusing error message track_last_time(started) stderr.close() except Exception as e: code = 1 with mutex: now = datetime.now() print(clear, end='', file=stderr) print(f'\x1b[31m{e}\x1b[0m', file=stderr) # tell the timer task to quit, so the whole script can quit with mutex: done = True t.join() exit(code)