From: Dustin Walde Date: Mon, 1 May 2023 06:16:43 +0000 (-0700) Subject: Finish initial working tray icon X-Git-Url: https://git.walde.dev/?a=commitdiff_plain;h=0e3c71ae95e332bd20caaa9481b8d6ab87f0b76b;p=punch Finish initial working tray icon - Graphic with current punched in --- diff --git a/src/image_generator.py b/src/image_generator.py new file mode 100644 index 0000000..bdc4914 --- /dev/null +++ b/src/image_generator.py @@ -0,0 +1,63 @@ +# Punch(card) — Personal time tracking +# ©2023 Dustin Walde +# SPDX-License-Identifier: GPL-3.0-or-later + +import datetime +from math import floor +from PIL import Image + +from time_sheet import TimeSheet + +def colstr(hex: str): + hex = hex[1:] + rgb = [0,0,0] + if len(hex) == 3: + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + if len(hex) == 6: + rgb[0] = int(hex[0:2], 16) + rgb[1] = int(hex[2:4], 16) + rgb[2] = int(hex[4:], 16) + + return (rgb[0], rgb[1], rgb[2], 255) + +IMAGE_SIZE = 8 +HOURS = IMAGE_SIZE +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) + de = dt + datetime.timedelta(days=1) + tt = datetime.timedelta(0) + ct = 0 + + 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)) + + for entry in sheet.entries: + if entry is None: + continue + color = colstr(sheet.categories[entry.category].color) + 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) + 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) + ct += 1 + + img = img.resize((20,20), Image.NEAREST) + return img + diff --git a/src/punch.py b/src/punch.py new file mode 100644 index 0000000..750dff5 --- /dev/null +++ b/src/punch.py @@ -0,0 +1,130 @@ +# Punch(card) — Personal time tracking +# ©2023 Dustin Walde +# SPDX-License-Identifier: GPL-3.0-or-later + +from datetime import date, datetime, time +import sys +from typing import List, Optional + + +from puncher import Puncher +from time_sheet import TimeSheet + +import logging + +logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) + + +ERR_MISSING_ARG = 2 +ERR_UNKNOWN_COMMAND = 3 + + +def load_time_sheet() -> TimeSheet: + ts = TimeSheet() + ts.load_csv('/home/users/.local/share/dwalde/time-sheet.csv') + return ts + + +def punch(sheet: TimeSheet, dir: str, category: Optional[str] = None, args: List[str] = []): + ''' + Attempt to add and entry to punch a category in or out. + _dir_ is the string 'in' or 'out'. + ''' + now = datetime.now().astimezone() + if len(args) < 2 or args[0] != "at": + punch_time = now + else: + punch_time = datetime.combine( + date.today(), + time(hour=int(args[1].split(':')[0]), + minute=int(args[1].split(':')[1]), + tzinfo=now.tzinfo)) + + if dir == "in" and category is not None: + sheet.punch_in(category, punch_time) + elif dir == "out": + sheet.punch_out(category, punch_time) + + +def main(args): + if len(args) < 2: + print('Missing command (daemon, new, in, out, list, print, query)') + exit(ERR_MISSING_ARG) + + if args[1] == 'daemon' or args[1] == 'service': + import service + service.Service(load_time_sheet()).run() + + sheet: Puncher = load_time_sheet() + + if args[1] == 'new': + if len(args) < 3: + print("Missing arg group|category") + print('punch new [group] [ []]') + exit(ERR_MISSING_ARG) + if args[2] == 'group': + if len(args) < 5: + print('Missing args for new group: new group [parent] [color]') + exit(ERR_MISSING_ARG) + sheet.new_group(*args[3:]) + else: + if len(args) < 4: + print('Missing args for new category: new [parent] [color]') + exit(ERR_MISSING_ARG) + sheet.new_category(*args[2:]) + + elif args[1] == 'out': + if len(args) < 3: + punch(sheet, 'out') + else: + punch(sheet, 'out', args[2], args[3:]) + + elif args[1] == 'in': + if len(args) < 3: + print(f'Missing arg for punch in: ') + exit(ERR_MISSING_ARG) + punch(sheet, 'in', args[2], args[3:]) + + elif args[1] == 'pop' or args[1] == 'rm' or args[1] == 'undo': + if len(args) < 3: + print(f'Missing arg for pop {args[2]}: ') + exit(ERR_MISSING_ARG) + sheet.list_category(args[2]) + resp = input("Remove entry [y/N]? ") + if resp.lower() == 'y': + sheet.pop(args[2]) + sheet.list_category(args[2]) + + elif args[1] == 'list': + if len(args) > 2: + sheet.list_category(args[2]) + else: + sheet.list_all() + + elif args[1] == 'to': + if len(args) < 3: + print(f'Missing category to punch in to.') + exit(ERR_MISSING_ARG) + punch(sheet, 'out') + punch(sheet, 'in', args[2]) + + elif args[1] == 'img': + import image_generator + if len(args) > 2: + dt = date.fromisoformat(args[2]) + else: + dt = date.today() + image = image_generator.day(sheet, dt) + image.save("test.png") + + elif args[1] == 'query': + raise NotImplementedError + + else: + print(f"Unknown subcommand {args[1]}") + exit(ERR_UNKNOWN_COMMAND) + + +if __name__ == "__main__": + main(sys.argv) + diff --git a/src/service.py b/src/service.py new file mode 100644 index 0000000..e9bec7c --- /dev/null +++ b/src/service.py @@ -0,0 +1,60 @@ +# Punch(card) — Personal time tracking +# ©2023 Dustin Walde +# SPDX-License-Identifier: GPL-3.0-or-later + +import datetime +import pystray +from time import sleep +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +import image_generator +from time_sheet import TimeSheet + + +PATH = '/home/users/.local/share/dwalde/time-sheet.csv' + + +class Service: + + def __init__(self, sheet: TimeSheet): + self.sheet = sheet + self.reload = False + self.handler = OnDataModify(self) + self.observer = Observer() + self.observer.schedule(self.handler, PATH) + self.observer.start() + + def keep_update(self, icon: pystray.Icon): + icon.visible = True + while True: + sleep(12) + if self.reload: + self.sheet.load_csv(PATH) + self.reload = False + icon.update_menu() + icon.icon = image_generator.day(self.sheet, datetime.date.today()) + + def _generate_menu_items(self): + return [ + pystray.MenuItem("Punch out", + lambda e, v: self.sheet.punch_out(), + enabled=len(self.sheet.get_open_entries())==1, + default=True), + ] + + 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)) + + +class OnDataModify(FileSystemEventHandler): + + def __init__(self, service: Service): + self.service = service + super().__init__() + + def on_any_event(self, event): + self.service.reload = True + diff --git a/src/time_sheet.py b/src/time_sheet.py index f74f081..d3d638a 100644 --- a/src/time_sheet.py +++ b/src/time_sheet.py @@ -40,8 +40,8 @@ class TimeSheet(Puncher): def get_open_entries(self) -> List[Entry]: entries = [] for entrys in self.cat_entries.values(): - if len(entrys) > 0 and not entrys[-1].is_complete: - entries.append(entrys[-1]) + if len(entrys) > 0 and not self.entries[entrys[-1]].is_complete: + entries.append(self.entries[entrys[-1]]) return entries @@ -190,14 +190,15 @@ class TimeSheet(Puncher): return None cat_entries = self.cat_entries[category] - if len(cat_entries) == 0 or cat_entries[-1].is_complete: + if len(cat_entries) == 0 or self.entries[cat_entries[-1]] is None or \ + self.entries[cat_entries[-1]].is_complete: logger.error("Category %s has nothing punched in.", category) return None if time is None: time = datetime.now().astimezone() - entry = cat_entries[-1] + entry = self.entries[cat_entries[-1]] last_time = entry.t_in if last_time >= time: logger.error(f"Punch in time is not before {time}.") @@ -236,12 +237,16 @@ class TimeSheet(Puncher): abbr = row[0], name = row[2], ) + if len(row) > 4: + self.categories[row[0]].color = row[4] elif row[1] == 'group': self.categories[row[2]] = Category( abbr = row[2], name = row[3], children = [], ) + if len(row) > 5: + self.categories[row[2]].color = row[5] for row in category_rows: parent = "" if row[1] == 'category' and len(row) > 3: @@ -257,7 +262,7 @@ class TimeSheet(Puncher): current = self.categories[abbr] if parent.children is None: logger.warning(f"Parent {parent.abbr} is not a group.") - else: + elif parent.abbr != '' or abbr != '': parent.children.append(current) current.parent = parent self.root = self.categories['']