From: Dustin Walde Date: Mon, 1 May 2023 20:12:36 +0000 (-0700) Subject: Improve tray service X-Git-Url: https://git.walde.dev/?a=commitdiff_plain;h=4768e353ad0c19ce7007b3b1295d2bf1ee12bebd;p=punch Improve tray service - Update image code - More general - Better visual indicator - Cleanly handle concurrent timer/file modify events - Add punch in to tray menu - Add pop placeholder --- diff --git a/src/image_generator.py b/src/image_generator.py index bdc4914..533ae6e 100644 --- a/src/image_generator.py +++ b/src/image_generator.py @@ -4,6 +4,8 @@ import datetime from math import floor +from typing import Tuple + from PIL import Image from time_sheet import TimeSheet @@ -20,12 +22,8 @@ def colstr(hex: str): return (rgb[0], rgb[1], rgb[2], 255) -IMAGE_SIZE = 8 -HOURS = IMAGE_SIZE +IMAGE_SIZE = 16 TARGET_HOURS = 8 -SEGS = IMAGE_SIZE - 2 -SEG_MINS = floor(60/SEGS) -SEG_SECS = floor(60*(60/SEGS-SEG_MINS)) def day(sheet: TimeSheet, date: datetime.date) -> Image.Image: dt = datetime.datetime.combine(date, datetime.time(), datetime.datetime.now().astimezone().tzinfo) @@ -35,29 +33,66 @@ def day(sheet: TimeSheet, date: datetime.date) -> Image.Image: img = Image.new('RGBA', (IMAGE_SIZE, IMAGE_SIZE), (0,0,0,0)) - for i in range(TARGET_HOURS): - for j in range(SEGS): - img.putpixel((2+j, HOURS-1-i), (255,255,255,48)) + # draw active + active = sheet.get_open_entries() + if len(active) == 1: + color = colstr(sheet.categories[active[0].category].color) + color = color[:3] + (200,) + for i in range(IMAGE_SIZE): + img.putpixel((0,i), color) + img.putpixel((IMAGE_SIZE-1,i), color) + for i in range(IMAGE_SIZE-2): + img.putpixel((i+1,0), color) + img.putpixel((i+1,IMAGE_SIZE-1), color) + _day(sheet, img, date, (2, IMAGE_SIZE-2), (2, IMAGE_SIZE-2), TARGET_HOURS, 255) + else: + _day(sheet, img, date, (0, IMAGE_SIZE), (0, IMAGE_SIZE), TARGET_HOURS, 255) + img = img.resize((32,32), Image.NEAREST) + return img + + +def _day(sheet: TimeSheet, image: Image.Image, date: datetime.date, + x_range: Tuple[int,int], y_range: Tuple[int,int], + target_hours: int = 0, alpha: int = 255) -> None: + dt = datetime.datetime.combine(date, datetime.time(), datetime.datetime.now().astimezone().tzinfo) + de = dt + datetime.timedelta(days=1) + tt = datetime.timedelta(0) + ct = 0 + + segments = x_range[1] - x_range[0] + hours = y_range[1] - y_range[0] + + seg_time = _time_per_segment(segments) + + # pre-fill taget range + for i in range(min(target_hours, hours)): + for j in range(segments): + image.putpixel((x_range[0]+j, y_range[1]-1-i), (255,255,255,32)) for entry in sheet.entries: if entry is None: continue color = colstr(sheet.categories[entry.category].color) + color = color[:3] + (alpha,) if not entry.is_complete: tt += datetime.datetime.now().astimezone() - max(entry.t_in, dt) - for i in range(HOURS): - img.putpixel((0,i), color) + #for i in range(HOURS): + # image.putpixel((0,i), color) elif entry.t_in >= dt and entry.t_in <= de: time = min(entry.t_out, de) - entry.t_in # type: ignore tt += time elif entry.t_out >= dt and entry.t_out <= de: # type: ignore time = entry.t_out - max(dt, entry.t_in) # type: ignore tt += time - while ct < HOURS * SEGS and tt > datetime.timedelta(0): - img.putpixel((2+ct%SEGS, HOURS-1-floor(ct/SEGS)), color) - tt += datetime.timedelta(minutes=-SEG_MINS, seconds=-SEG_SECS) + while ct < hours * segments and tt > datetime.timedelta(0): + image.putpixel((x_range[0]+ct%segments, y_range[1]-1-floor(ct/segments)), color) + tt -= seg_time ct += 1 - img = img.resize((20,20), Image.NEAREST) - return img + +def _time_per_segment(segments: int) -> datetime.timedelta: + seg_minutes = floor(60/segments) + seg_seconds = floor(60*(60/segments-seg_minutes)) + + return datetime.timedelta(minutes=seg_minutes, seconds=seg_seconds) diff --git a/src/service.py b/src/service.py index e9bec7c..fa5fcd6 100644 --- a/src/service.py +++ b/src/service.py @@ -4,7 +4,9 @@ import datetime import pystray +from threading import Condition, Thread from time import sleep + from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler @@ -20,41 +22,101 @@ class Service: def __init__(self, sheet: TimeSheet): self.sheet = sheet self.reload = False + self.cv = Condition() self.handler = OnDataModify(self) self.observer = Observer() + self.thread = Thread(target=lambda: self._timer_thread(), name="TimerThread", daemon=True) self.observer.schedule(self.handler, PATH) self.observer.start() + self.thread.start() + def keep_update(self, icon: pystray.Icon): icon.visible = True while True: - sleep(12) - if self.reload: + reload = False + with self.cv: + self.cv.wait() + if self.reload: + reload = True + self.reload = False + if reload: self.sheet.load_csv(PATH) - self.reload = False icon.update_menu() + reload = False icon.icon = image_generator.day(self.sheet, datetime.date.today()) + + def run(self): + img = image_generator.day(self.sheet, datetime.date.today()) + icon = pystray.Icon('Punch(card)', icon=img, title="Punch", menu=[lambda: self._generate_menu_items()]) + icon.run(lambda icn: self.keep_update(icn)) + + def _generate_menu_items(self): + active = self.sheet.get_open_entries() + punch_in_menu = pystray.Menu(*self._generate_punch_in_items()) return [ + pystray.MenuItem("Punch in", punch_in_menu), pystray.MenuItem("Punch out", - lambda e, v: self.sheet.punch_out(), - enabled=len(self.sheet.get_open_entries())==1, + lambda: self.sheet.punch_out(), + enabled=len(active)==1, default=True), + pystray.Menu.SEPARATOR, + pystray.MenuItem("Pop", lambda: self._pop_last()), ] - def run(self): - img = image_generator.day(self.sheet, datetime.date.today()) - icon = pystray.Icon('Punch(card)', icon=img, title="Punch", menu=[lambda: self._generate_menu_items()]) - icon.run(lambda icn: self.keep_update(icn)) + def _generate_punch_in_items(self): + latest = [] + for idxs in self.sheet.cat_entries.values(): + if len(idxs) > 0: + latest.append(self.sheet.entries[idxs[-1]]) + latest.sort(key=lambda e: e.t_in, reverse=True) + punch_in_items = [] + for i, entry in enumerate(latest): + if i >= 5: + break + punch_in_items.append( + pystray.MenuItem(entry.category, + lambda _icon, item: self._punch_in(item))) + return tuple(punch_in_items) + + + def _punch_in(self, item: pystray.MenuItem): + active = self.sheet.get_open_entries() + if len(active) > 1: + return + if len(active) == 1: + self.sheet.punch_out() + self.sheet.punch_in(item.text) + + + def _pop_last(self): + pass + + + def _timer_thread(self): + while True: + sleep(60) + with self.cv: + self.cv.notify_all() + class OnDataModify(FileSystemEventHandler): def __init__(self, service: Service): self.service = service + self.mod = False super().__init__() - def on_any_event(self, event): - self.service.reload = True + def on_modified(self, event): + self.mod = True + + def on_closed(self, event): + if self.mod: + with self.service.cv: + self.service.reload = True + self.service.cv.notify_all() + self.mod = False