From 8ea01476f31691fb628163e8dd2320abd76a901a Mon Sep 17 00:00:00 2001 From: vg Date: Thu, 4 Apr 2024 15:20:01 +0200 Subject: Update fancy_sleep.py script - fancy-sleep now accepts an optional command. - time parsing is done on a single argument to differentiate between time and command. - better display by disable tty echo to avoid breaking the fancy display while countdown is active. - simpler code for keyboardexception handling. --- scripts/fancy_sleep.py | 173 +++++++++++++++++++++++++++---------------------- 1 file changed, 96 insertions(+), 77 deletions(-) (limited to 'scripts') diff --git a/scripts/fancy_sleep.py b/scripts/fancy_sleep.py index b56c7cb..5839a32 100755 --- a/scripts/fancy_sleep.py +++ b/scripts/fancy_sleep.py @@ -1,25 +1,36 @@ #!/usr/bin/env python3 -# Copyright 2017 vgm+dev@devys.org +# Copyright 2024 vgm+dev@devys.org # SPDX-License-Identifier: MIT ''' Simple and fancy sleep for console. -Time can be precised as XX[h|m|s] [YY[m|s]] [ZZ[s]] or XX:YY[:ZZ]. +Time can be precised as [DDd][HHh][MMm][SS[s]]. Number of digits for days, +hours, minutes or seconds are not restricted. Every type (days, hours, +minutes, seconds) are optional, but at least one must be given. If no suffix +is given, it means seconds. Spaces between group of types can be given but +TIME as a whole must be a single argument. -Usage: fancy-sleep [options] TIME... +Time can also be given as HH:MM or HH:MM:SS. + +An optional command to run at the end of the time can be given. + +Usage: fancy-sleep [options] TIME [--] [COMMAND...] fancy-sleep -h|--help Options: -h, --help Display this help message + ''' import contextlib import datetime +import os +import re import sys +import termios import time -import re import docopt @@ -124,104 +135,112 @@ def print_translated(string): print('\n'.join(display_lines), end='') -def determine_delta(elements, - time_pattern=re.compile(r'^(\d+)([dhms]?)$')): - 'return a cumulative datetime.timedelta from elements list of strings' +def parse_time(times, time_pattern=re.compile(r'(\d+)([dhms]?)')): + 'return a cumulative datetime.timedelta from time string (times)' default = datetime.datetime.strptime('0', '%S') timedelta = datetime.timedelta() - for element in elements: - groups = time_pattern.match(element) - if groups: - if groups.group(2) == 'd': - timedelta += datetime.timedelta(days=float(groups.group(1))) - elif groups.group(2) == 'h': - timedelta += datetime.timedelta(hours=float(groups.group(1))) - elif groups.group(2) == 'm': - timedelta += datetime.timedelta(minutes=float(groups.group(1))) - elif groups.group(2) == 's' or groups.group(2) == '': - timedelta += datetime.timedelta(seconds=float(groups.group(1))) - - - #with contextlib.suppress(ValueError): - # timedelta += datetime.datetime.strptime(element, '%dd') - default - #with contextlib.suppress(ValueError): - # timedelta += datetime.datetime.strptime(element, '%Hh') - default - #with contextlib.suppress(ValueError): - # timedelta += datetime.datetime.strptime(element, '%Mm') - default - #with contextlib.suppress(ValueError): - # timedelta += datetime.datetime.strptime(element, '%Ss') - default - #with contextlib.suppress(ValueError): - # timedelta += datetime.datetime.strptime(element, '%S') - default - else: - with contextlib.suppress(ValueError): - timedelta += datetime.datetime.strptime(element, '%H:%M') - default - with contextlib.suppress(ValueError): - timedelta += datetime.datetime.strptime(element, '%H:%M:%S') - default + try: + timedelta += datetime.datetime.strptime(times, '%H:%M:%S') - default + return timedelta + except ValueError: + try: + timedelta += datetime.datetime.strptime(times, '%H:%M') - default + return timedelta + except ValueError: + pass + for match in time_pattern.finditer(times): + if match.group(2) == 'd': + timedelta += datetime.timedelta(days=float(match.group(1))) + elif match.group(2) == 'h': + timedelta += datetime.timedelta(hours=float(match.group(1))) + elif match.group(2) == 'm': + timedelta += datetime.timedelta(minutes=float(match.group(1))) + elif match.group(2) == 's' or match.group(2) == '': + timedelta += datetime.timedelta(seconds=float(match.group(1))) return timedelta -def test_determine_delta(): - 'test determine_delta function (callable with pytest-3)' - assert determine_delta('11'.split()) \ +def test_parse_time(): + 'test parse_time function (callable with pytest-3)' + assert parse_time('11') \ == datetime.timedelta(seconds=11) - assert determine_delta('11s'.split()) \ + assert parse_time('11s') \ == datetime.timedelta(seconds=11) - assert determine_delta('11h 47m'.split()) \ + assert parse_time('11s') == parse_time('11') + assert parse_time('11h 47m') \ == datetime.timedelta(hours=11, minutes=47) - assert determine_delta('11h 47'.split()) \ + assert parse_time('11h 47m') == parse_time('0d11h47m0s') + assert parse_time('11h 47') \ == datetime.timedelta(hours=11, seconds=47) - assert determine_delta('11h 47m 09'.split()) \ + assert parse_time('11h 47') == parse_time('11h47s') + assert parse_time('11h 47m 09') \ == datetime.timedelta(hours=11, minutes=47, seconds=9) - assert determine_delta('09:53'.split()) \ + assert parse_time('11h 47m 09') == parse_time('11h47m9s') + assert parse_time('09:53') \ == datetime.timedelta(hours=9, minutes=53) - assert determine_delta('8:2 9'.split()) \ - == datetime.timedelta(hours=8, minutes=2, seconds=9) - assert determine_delta('8d 5:32 6s'.split()) \ - == datetime.timedelta(days=8, hours=5, minutes=32, seconds=6) + assert parse_time('09:53') == parse_time('09h 53m') + assert parse_time('8:2') \ + == datetime.timedelta(hours=8, minutes=2) + assert parse_time('8:2') == parse_time('08:02') + assert parse_time('8d 6s') \ + == datetime.timedelta(days=8, seconds=6) + assert parse_time('8d 6s') == parse_time('8d6s') + + +def fancy_sleep_display(target_time): + print() + print('\n'*5, '\x1b[5A', sep='', end='') + print('\x1b[s', end='') # save cursor + original_termios_attr = termios.tcgetattr(sys.stdin) + noecho_termios_attr = original_termios_attr[:] + noecho_termios_attr[3] &= ~termios.ECHO + termios.tcsetattr(sys.stdin, termios.TCSANOW, noecho_termios_attr) + try: + while True: + delta = target_time.timestamp() - time.time() + if delta <= 0: + break + hours = int(delta / 3600) + minutes = int(delta / 60) % 60 + seconds = int(delta) % 60 + + print('\x1b[u', end='') # restore cursor + time_string = '%02d:%02d:%02d' % (hours, minutes, seconds) + #print('\rremainining time: %s' % time_string, end='') + print_translated(time_string) + + time.sleep(1) + finally: + print() + termios.tcsetattr(sys.stdin, termios.TCSANOW, original_termios_attr) def main(): 'function called only when script invoked directly on command line' - args = docopt.docopt(__doc__) curtime = datetime.datetime.now() - timedelta = determine_delta(args['TIME']) + timedelta = parse_time(args['TIME']) target_time = curtime + timedelta + if timedelta == datetime.timedelta(0): + print('ERROR: TIME is unparsable or equals to 0', file=sys.stderr) + sys.exit(1) + print('Launched at:', curtime.strftime('%F %T')) print('Run until:', target_time.strftime('%F %T')) sys.stdout.flush() - if sys.stdout.isatty(): - print() - - print('\n'*5, '\x1b[5A', sep='', end='') - print('\x1b[s', end='') # save cursor - try: - while True: - delta = target_time.timestamp() - time.time() - if delta <= 0: - break - hours = int(delta / 3600) - minutes = int(delta / 60) % 60 - seconds = int(delta) % 60 - - print('\x1b[u', end='') # restore cursor - time_string = '%02d:%02d:%02d' % (hours, minutes, seconds) - #print('\rremainining time: %s' % time_string, end='') - print_translated(time_string) - - time.sleep(1) - except KeyboardInterrupt: - sys.exit(130) - finally: - print() - else: - try: + try: + if not sys.stdout.isatty(): time.sleep(target_time.timestamp() - time.time()) - except KeyboardInterrupt: - sys.exit(130) + else: + fancy_sleep_display(target_time) + except KeyboardInterrupt: + sys.exit(130) + + if args['COMMAND']: + os.execvp(args['COMMAND'][0], args['COMMAND']) if __name__ == '__main__': -- cgit v1.2.3