#!/usr/bin/env python3 import curses import datetime import math import os import signal import sys import time from optparse import OptionParser ascii_digits = { "0": "0000 00 00 0000", "1": " 1" * 5, "2": "222 22222 222", "3": "333 3 33 3333", "4": "4 4 4444 4 4", "5": "5555 555 5555", "6": "66 6 6666 6666", "7": "777 7 7 7 7", "8": "8888 88888 8888", "9": "9999 9999 9 99", ":": " . . ", } # fmt: off fancy_digits = { "0": ("▗▄▖" "█ █" "█ █" "█ █" "▝▀▘"), "1": ("▗▄ " " █ " " █ " " █ " "▝▀▘"), "2": ("▗▄▖" "▘ █" " ▟▘" "▟▘ " "▀▀▀"), "3": ("▗▄▖" "▘ █" " ▜▌" "▖ █" "▝▀▘"), "4": (" ▗▖" "▗▜▌" "▌▐▌" "▀▜▛" " ▀▀"), "5": ("▄▄▄" "█ " "▀▀▙" "▖ █" "▝▀▘"), "6": ("▗▄▖" "█ ▝" "█▀▙" "█ █" "▝▀▘"), "7": ("▄▄▄" " █" " ▐▌" " █ " "▝▘ "), #▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ █ ▀ ▄ ▌ ▐ "8": ("▗▄▖" "█ █" "▟▀▙" "█ █" "▝▀▘"), "9": ("▗▄▖" "█ █" "▜▄█" "▖ █" "▝▀▘"), ":": " ▗ ▝ ", } # fmt: on # ~ ░ ▒ ▓ def termsize(): rows, columns = os.popen("stty size", "r").read().split() return (int(rows), int(columns)) def centerpoint(ry, rx): r = int(round(min(ry / 1.5, rx / 1.5)) - 1) return (int(round(ry / 2)), int(round(rx / 2)), r) def saddstr(win, y, x, s, a=None): if not a: a = curses.color_pair(0) try: win.addstr(y, x, s, a) except: pass def drawcircle(win, cy, cx, r, s="·", attr=None): # TODO: get seconds?, color by angle, darkening to past. current seconds bright precision = 360 for a in range(precision): alpha = 2.0 * math.pi * a / precision dy = int(round(cy - float(r) * math.cos(alpha))) dx = int(round(cx + 2.0 * float(r) * math.sin(alpha))) saddstr(win, dy, dx, s, attr) return def drawline(win, cy, cx, a, s, r, char, attr=None): prec = 2 points = {} for l in range(int(r * prec)): if l > s: ly = cy - float(l) * math.cos(a) / prec lx = cx + 2.0 * float(l) * math.sin(a) / prec iy, ix = int(round(ly)), int(round(lx)) saddstr(win, iy, ix, char, attr) return def drawdigit(win, y, x, d, ascii=False): s = " " if ascii: s = ascii_digits.get(d, s) else: s = fancy_digits.get(d, s) drawsplitstr(win, y, x, s) return def drawsplitstr(win, y, x, st): ls = list(st) for r in range(5): rs = 3 * r saddstr(win, y + r, x, ls[rs] + ls[rs + 1] + ls[rs + 2], curses.A_BOLD) return def drawdigital(win, y, x, t, ascii=False, tick_tock=False): c = ":" if tick_tock else " " sec = "{:}{:02d}".format(c, t.tm_sec) if options.seconds else "" hrs = list("{:02d}{:}{:02d}{:}".format(t.tm_hour, c, t.tm_min, sec)) for c in range(len(hrs)): drawdigit(win, y, x + 4 * c, hrs[c], ascii=ascii) def drawalarms(stdscr, y, x, t, alarms, tick_tock): ya = y now = 100 * t.tm_hour + t.tm_min is_alarm = False for alarm in alarms: if alarm["hstart"] <= now and alarm["hend"] >= now: color = ( curses.color_pair(4) + curses.A_BOLD + curses.A_REVERSE if tick_tock else curses.color_pair(2) + curses.A_BOLD ) is_alarm = True else: color = curses.color_pair(7) st = "{:02d}:{:02d}".format(*alarm["start"]) ed = "{:02d}:{:02d}".format(*alarm["end"]) s = f"{st}-{ed} {alarm['name']}" stdscr.addstr(ya, x, s, color) ya += 1 return is_alarm def parse_alarms(alarms): parsed = [] for alarm in alarms: t, n = alarm.split("/", 1) t = t.split("-") tstart = [int(x) for x in t[0].split(":")] if len(t) > 1: tend = [int(x) for x in t[1].split(":")] else: tend = tstart parsed.append( { "start": tstart, "end": tend, "name": n, "hstart": 100 * tstart[0] + tstart[1], "hend": 100 * tend[0] + tend[1], } ) parsed.sort(key=lambda x: x["start"][1]) parsed.sort(key=lambda x: x["start"][0]) return parsed def readinput(win, timeout): started = time.time() try: while time.time() - timeout < started: input = win.getch() if input in [ord(x) for x in ["x", "X", "q", "Q"]]: return "x" time.sleep(0.01) except: return "" return "" class timer_struct: """Class for storing timer.""" def __init__(self, h, m, s): self.tm_hour = int(h) self.tm_min = int(m) self.tm_sec = int(s) def main(): stdscr = curses.initscr() alarms = parse_alarms(options.alarm) curses.curs_set(0) curses.start_color() curses.use_default_colors() stdscr.nodelay(2) for i in range(0, curses.COLORS): curses.init_pair(i + 1, i, -1) start_t = time.time() is_alarm = False t_old = time.localtime(time.time()) tick_tock = False try: # rows,columns = termsize() curses.cbreak() while 1: rows, columns = stdscr.getmaxyx() cy, cx, r = centerpoint(rows, columns) now = time.time() t = time.localtime(now) if options.timer: t_new = now - start_t t_m, t_s = divmod(t_new, 60) t_h, t_m = divmod(t_m, 60) f = 0 else: f = now % 1 t_s = float(t.tm_sec) t_m = float(t.tm_min) t_h = float(t.tm_hour) alphas = math.pi * (f + t_s) / 30.0 alpham = math.pi * t_m / 30.0 + alphas / 60.0 alphah = math.pi * t_h / 6.0 + alpham / 12.0 if t_old.tm_min != t.tm_min: # clear once a minute stdscr.clear() tick_tock = not tick_tock if options.refresh > 1 else t.tm_sec % 2 drawcircle( stdscr, cy, cx, r / 2, attr=curses.color_pair(2) + curses.A_BOLD + curses.A_BLINK if is_alarm else curses.color_pair(1), ) if options.seconds: drawline(stdscr, cy, cx, alphas, 1, r / 2, "■", curses.color_pair(2)) drawline(stdscr, cy, cx, alpham, 1, int(round(r * 0.9) / 2), "█", curses.color_pair(3)) drawline(stdscr, cy, cx, alphah, 1, int(round(r * 0.6) / 2), "██", curses.color_pair(7)) stdscr.addstr(cy, cx, "⊙") for h in range(12): drawline(stdscr, cy, cx, math.pi * h / 6.0, r, 1 + r / 2, "◆", curses.color_pair(4)) if options.timer: drawdigital(stdscr, 1, 1, timer_struct(t_h, t_m, t_s), options.ascii, tick_tock) drawdigital(stdscr, 7, 1, t, options.ascii, tick_tock) else: drawdigital(stdscr, 1, 1, t, options.ascii, tick_tock) is_alarm = drawalarms(stdscr, 1, 35, t, alarms, tick_tock) stdscr.refresh() userinput = readinput(stdscr, options.refresh) if userinput == "x": curses.nocbreak() stdscr.keypad(0) curses.endwin() sys.exit(0) # wipe if options.seconds: drawline(stdscr, cy, cx, alphas, 1, r / 2, " ", curses.color_pair(2)) drawline(stdscr, cy, cx, alpham, 1, int(round(r * 0.9) / 2), " ", curses.color_pair(3)) drawline(stdscr, cy, cx, alphah, 1, int(round(r * 0.6) / 2), " ", curses.color_pair(7)) t_old = t except KeyboardInterrupt: curses.nocbreak() stdscr.keypad(0) # curses.echo() curses.endwin() usage = """Usage: %prog [options] Display a clockface """ parser = OptionParser(usage=usage) parser.add_option("-s", action="store_true", dest="seconds", default=False, help="Show seconds [%default]") parser.add_option("-r", type="float", dest="refresh", default=3, help="Refresh rate in seconds [%default]") parser.add_option( "-t", action="store_true", dest="timer", default=False, help="Timer instead of current time [%default]" ) parser.add_option("-a", action="store_true", dest="ascii", default=False, help="Plain ascii characters only [%default]") parser.add_option("--alarm", action="append", dest="alarm", default=[], help="alarms") global options (options, args) = parser.parse_args() main()