import datetime
from math import floor
+from typing import Tuple
+
from PIL import Image
from time_sheet import TimeSheet
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)
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)
import datetime
import pystray
+from threading import Condition, Thread
from time import sleep
+
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
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