]> git.walde.dev - punch/commitdiff
Finish initial working tray icon
authorDustin Walde <redacted>
Mon, 1 May 2023 06:16:43 +0000 (23:16 -0700)
committerDustin Walde <redacted>
Mon, 1 May 2023 06:16:43 +0000 (23:16 -0700)
- Graphic with current punched in

src/image_generator.py [new file with mode: 0644]
src/punch.py [new file with mode: 0644]
src/service.py [new file with mode: 0644]
src/time_sheet.py

diff --git a/src/image_generator.py b/src/image_generator.py
new file mode 100644 (file)
index 0000000..bdc4914
--- /dev/null
@@ -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 (file)
index 0000000..750dff5
--- /dev/null
@@ -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] <id> <name> [<parent> [<color>]]')
+            exit(ERR_MISSING_ARG)
+        if args[2] == 'group':
+            if len(args) < 5:
+                print('Missing args for new group: new group <id> <name> [parent] [color]')
+                exit(ERR_MISSING_ARG)
+            sheet.new_group(*args[3:])
+        else:
+            if len(args) < 4:
+                print('Missing args for new category: new <id> <name> [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: <category>')
+            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]}: <category>')
+            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 (file)
index 0000000..e9bec7c
--- /dev/null
@@ -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
+
index f74f0812889bc9a1f8759740f5c8aa8bb7e938fc..d3d638a44c92827f0638b33d249ccd40317ccf97 100644 (file)
@@ -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['']