]> git.walde.dev - punch/commitdiff
Improve tray service
authorDustin Walde <redacted>
Mon, 1 May 2023 20:12:36 +0000 (13:12 -0700)
committerDustin Walde <redacted>
Mon, 1 May 2023 20:12:36 +0000 (13:12 -0700)
- Update image code
  - More general
  - Better visual indicator
- Cleanly handle concurrent timer/file modify events
- Add punch in to tray menu
- Add pop placeholder

src/image_generator.py
src/service.py

index bdc491449bbe3f03b4c81c9d33c66de4746f6489..533ae6e5686bee8f2962221f2fa9a04c2e6cffc2 100644 (file)
@@ -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)
 
index e9bec7cf81a3cda13a7f0d4b70b36e03ae3728b7..fa5fcd663d0e787fdc5343f0127cee9443877fb9 100644 (file)
@@ -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