diff --git a/data.py b/data.py index 4fad075..7f92374 100644 --- a/data.py +++ b/data.py @@ -1,6 +1,8 @@ import os import json +import base64 import atexit +from datetime import datetime, date, time from threading import Thread, Event from collections.abc import MutableMapping @@ -12,7 +14,8 @@ data_dir_path = os.path.join(QtCore.QStandardPaths.writableLocation(QtCore.QStan tasks_path = os.path.join(data_dir_path, "tasks.json") data_path = os.path.join(data_dir_path, "data_{}.json") -save_delay = 3 * 60 +#save_delay = 3 * 60 +save_delay = 3 class Tasks: @@ -21,7 +24,8 @@ class Tasks: os.mkdir(data_dir_path) if os.path.exists(tasks_path): with open(tasks_path, "r") as f: - self._tasks = json.loads(f.read()) + encoded_tasks = json.loads(f.read()) + self._tasks = list(map(lambda x: base64.b64decode(x.encode("utf-8")).decode("utf-8"), encoded_tasks)) else: self._tasks = [] @@ -36,8 +40,9 @@ class Tasks: def _save(self): print("...saving tasks...") + encoded_tasks = list(map(lambda x: base64.b64encode(x.encode("utf-8")).decode("utf-8"), self._tasks)) with open(tasks_path, "w+") as f: - f.write(json.dumps(self._tasks)) + f.write(json.dumps(encoded_tasks)) class Data(MutableMapping): @@ -46,22 +51,23 @@ class Data(MutableMapping): os.mkdir(data_dir_path) self._cache = {} self._hot_keys = [] - self._running = False + self._trunning = False self._tevent = Event() self._thread = None def cleanup(): - self._running = False + self._trunning = False self._tevent.set() if self._thread: self._thread.join() + atexit.register(cleanup) def __getitem__(self, key): dpath = data_path.format(key) if key not in self._cache and os.path.exists(dpath): with open(dpath, "r") as f: - self._cache[key] = f.read() + self._cache[key] = json.loads(f.read()) return self._cache[key] def __setitem__(self, key, value): @@ -70,14 +76,14 @@ class Data(MutableMapping): self._schedule_save() def _schedule_save(self): - if self._running: + if self._trunning: return - self._running = True + self._trunning = True self._thread = Thread(target=self._executor, daemon=True) self._thread.start() def _executor(self): - while self._running: + while self._trunning: self._tevent.wait(save_delay) self._save() @@ -91,13 +97,50 @@ class Data(MutableMapping): self._saving = False def __delitem__(self, key): - print("WARNING: deletion of items not supported") + return NotImplemented def __iter__(self): - return iter(self._cache) + return NotImplemented def __len__(self): - return len(self._cache) + # TODO use glob? + return NotImplemented def __repr__(self): return f"{type(self).__name__}({self._cache})" + + +class Log: + def __init__(self): + self._data = Data() + + def log(self, task, ptime=datetime.now()): + month = self._data.setdefault(ptime.strftime("%Y-%m"), {}) + month.setdefault(ptime.strftime("%d"), [])\ + .append(f"{ptime.strftime('%H:%M:%S')} {base64.b64encode(task.encode('utf-8')).decode('utf-8')}") + self._data[ptime.strftime("%Y-%m")] = month # necessary to trigger Data.__setitem__ + + def last_log(self, pdate=date.today()): + if pdate.strftime("%Y-%m") not in self._data or pdate.strftime("%d") not in self._data[pdate.strftime("%Y-%m")]: + return None + return base64.b64decode( + self._data[pdate.strftime("%Y-%m")][pdate.strftime("%d")][-1].split()[1].encode("utf-8")).decode("utf-8") + + def report(self, pdate=date.today()): + tmp = [] + for e in self._data[pdate.strftime("%Y-%m")][pdate.strftime("%d")]: + tstr, b64str = e.split() + task = base64.b64decode(b64str.encode("utf-8")).decode("utf-8") + start_time = datetime.combine(pdate, datetime.strptime(tstr, "%H:%M:%S").time()) + tmp.append((task, start_time)) + + ret = [] + for i, t in enumerate(tmp): + task, start_time = t + if i < len(tmp) - 1: + end_time = tmp[i+1][1] + else: + end_time = datetime.now() + duration = end_time - start_time + ret.append((task, start_time, duration)) + return ret diff --git a/main.py b/main.py index 3147c14..3ca3d38 100644 --- a/main.py +++ b/main.py @@ -6,8 +6,9 @@ from functools import partial from PySide2 import QtCore, QtGui, QtWidgets -from data import Tasks, Data +from data import Tasks, Log from task_edit import TaskEdit +from report import Report class App: @@ -15,8 +16,8 @@ class App: self.app = QtWidgets.QApplication(sys.argv) self.tasks = Tasks() - self.active_task = "Nothing" - self.data = Data() + self.log = Log() + self.active_task = self.log.last_log() or "Nothing" icon = QtGui.QIcon.fromTheme("appointment-new") @@ -32,13 +33,16 @@ class App: self.taskEdit = TaskEdit(None) self.taskEdit.accepted.connect(self.tasks_edited) + self.report = Report(None) + @QtCore.Slot() def tasks_edited(self): self.tasks.tasks = self.taskEdit.tasks self.update_tray_menu() def change_task(self, task): - self.tasks.active = task + self.active_task = task + self.log.log(task) self.update_tray_menu() def update_tray_menu(self): @@ -76,10 +80,12 @@ class App: signal.signal(signal.SIGTERM, self.sigterm_handler) signal.signal(signal.SIGINT, self.sigterm_handler) self.app.exec_() + sys.exit() @QtCore.Slot() def report(self): - pass + self.report.set_data(self.log.report()) + self.report.show() @QtCore.Slot() def edit_tasks(self): diff --git a/report.py b/report.py new file mode 100644 index 0000000..9fc242b --- /dev/null +++ b/report.py @@ -0,0 +1,80 @@ +from PySide2 import QtCore, QtGui, QtWidgets + + +class Report(QtWidgets.QDialog): + def __init__(self, parent, *args, **kwargs): + super().__init__(parent, *args, **kwargs) + self.setWindowTitle("Report") + + self.tableWidget = QtWidgets.QTableWidget() + self.tableWidget.verticalHeader().hide() + self.tableWidget.setColumnCount(3) + self.tableWidget.setHorizontalHeaderLabels(["Task", "Start time", "Duration"]) + self.header = QtWidgets.QHeaderView(QtCore.Qt.Orientation.Horizontal) + self.tableWidget.setHorizontalHeader(self.header) + + new_button = QtWidgets.QPushButton() + new_button.setText("New item") + new_button.setIcon(QtGui.QIcon.fromTheme("list-add")) + new_button.pressed.connect(self.new_task) + + del_button = QtWidgets.QPushButton() + del_button.setText("Delete item") + del_button.setIcon(QtGui.QIcon.fromTheme("list-remove")) + del_button.pressed.connect(self.del_task) + + ok_button = QtWidgets.QPushButton() + ok_button.setText("OK") + ok_button.setIcon(QtGui.QIcon.fromTheme("dialog-ok-apply")) + ok_button.pressed.connect(self.accept) + + blayout = QtWidgets.QHBoxLayout() + blayout.addWidget(new_button) + blayout.addWidget(del_button) + blayout.addWidget(ok_button) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(self.tableWidget) + layout.addLayout(blayout) + self.setLayout(layout) + + def set_data(self, data): + self.tableWidget.setRowCount(len(data)) + + for row, _ in enumerate(data): + self.tableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem(data[row][0])) + self.tableWidget.setItem(row, 1, QtWidgets.QTableWidgetItem(str(data[row][1]))) + self.tableWidget.setItem(row, 2, QtWidgets.QTableWidgetItem(str(data[row][2]))) + + self.tableWidget.resizeColumnsToContents() + + min_width = 0 + for i in range(3): + min_width += self.header.sectionSize(i) + self.tableWidget.setMinimumWidth(min_width * 1.33) + self.header.setSectionResizeMode(QtWidgets.QHeaderView.Stretch) + + @QtCore.Slot() + def new_task(self): + l = self.list.stringList() + l.append("") + self.list.setStringList(l) + i = self.list.index(len(l)-1) + self.tableView.setCurrentIndex(i) + self.tableView.edit(i) + + @QtCore.Slot() + def del_task(self): + l = self.list.stringList() + del l[self.tableView.currentIndex().row()] + self.list.setStringList(l) + + @property + def tasks(self): + ret = self.list.stringList() + return list(filter(None, ret)) # filter empty strings + + @tasks.setter + def tasks(self, tasks): + self.list.setStringList(tasks) +