Compare commits
No commits in common. "497cdc6561b986e102d1128ee7ff4686a95b2e14" and "69fef39bf78c3375ee031a8b0cdb24420a12df11" have entirely different histories.
497cdc6561
...
69fef39bf7
204
.gitignore
vendored
204
.gitignore
vendored
@ -1,4 +1,94 @@
|
|||||||
# ---> Python
|
|
||||||
|
# Created by https://www.gitignore.io/api/python,pycharm+all
|
||||||
|
# Edit at https://www.gitignore.io/?templates=python,pycharm+all
|
||||||
|
|
||||||
|
### PyCharm+all ###
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### PyCharm+all Patch ###
|
||||||
|
# Ignores the whole .idea folder and all .iml files
|
||||||
|
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||||
|
|
||||||
|
*.iml
|
||||||
|
modules.xml
|
||||||
|
.idea/misc.xml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# Sonarlint plugin
|
||||||
|
.idea/sonarlint
|
||||||
|
|
||||||
|
### Python ###
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
@ -48,7 +138,6 @@ htmlcov/
|
|||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
*.cover
|
*.cover
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
||||||
@ -56,16 +145,6 @@ coverage.xml
|
|||||||
*.mo
|
*.mo
|
||||||
*.pot
|
*.pot
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
# Scrapy stuff:
|
||||||
.scrapy
|
.scrapy
|
||||||
|
|
||||||
@ -75,13 +154,6 @@ docs/_build/
|
|||||||
# PyBuilder
|
# PyBuilder
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
@ -92,25 +164,12 @@ ipython_config.py
|
|||||||
# install all needed dependencies.
|
# install all needed dependencies.
|
||||||
#Pipfile.lock
|
#Pipfile.lock
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
# celery beat schedule file
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
# SageMath parsed files
|
||||||
*.sage.py
|
*.sage.py
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
.spyproject
|
.spyproject
|
||||||
@ -118,6 +177,11 @@ venv.bak/
|
|||||||
# Rope project settings
|
# Rope project settings
|
||||||
.ropeproject
|
.ropeproject
|
||||||
|
|
||||||
|
# Mr Developer
|
||||||
|
.mr.developer.cfg
|
||||||
|
.project
|
||||||
|
.pydevproject
|
||||||
|
|
||||||
# mkdocs documentation
|
# mkdocs documentation
|
||||||
/site
|
/site
|
||||||
|
|
||||||
@ -129,76 +193,4 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
# ---> JetBrains
|
# End of https://www.gitignore.io/api/python,pycharm+all
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
|
||||||
|
|
||||||
# User-specific stuff
|
|
||||||
.idea/**/workspace.xml
|
|
||||||
.idea/**/tasks.xml
|
|
||||||
.idea/**/usage.statistics.xml
|
|
||||||
.idea/**/dictionaries
|
|
||||||
.idea/**/shelf
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
.idea/**/contentModel.xml
|
|
||||||
|
|
||||||
# Sensitive or high-churn files
|
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
.idea/**/dbnavigator.xml
|
|
||||||
|
|
||||||
# Gradle
|
|
||||||
.idea/**/gradle.xml
|
|
||||||
.idea/**/libraries
|
|
||||||
|
|
||||||
# Gradle and Maven with auto-import
|
|
||||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
|
||||||
# since they will be recreated, and may cause churn. Uncomment if using
|
|
||||||
# auto-import.
|
|
||||||
# .idea/artifacts
|
|
||||||
# .idea/compiler.xml
|
|
||||||
# .idea/jarRepositories.xml
|
|
||||||
# .idea/modules.xml
|
|
||||||
# .idea/*.iml
|
|
||||||
# .idea/modules
|
|
||||||
# *.iml
|
|
||||||
# *.ipr
|
|
||||||
|
|
||||||
# CMake
|
|
||||||
cmake-build-*/
|
|
||||||
|
|
||||||
# Mongo Explorer plugin
|
|
||||||
.idea/**/mongoSettings.xml
|
|
||||||
|
|
||||||
# File-based project format
|
|
||||||
*.iws
|
|
||||||
|
|
||||||
# IntelliJ
|
|
||||||
out/
|
|
||||||
|
|
||||||
# mpeltonen/sbt-idea plugin
|
|
||||||
.idea_modules/
|
|
||||||
|
|
||||||
# JIRA plugin
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
|
|
||||||
# Cursive Clojure plugin
|
|
||||||
.idea/replstate.xml
|
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics.properties
|
|
||||||
crashlytics-build.properties
|
|
||||||
fabric.properties
|
|
||||||
|
|
||||||
# Editor-based Rest Client
|
|
||||||
.idea/httpRequests
|
|
||||||
|
|
||||||
# Android studio 3.1+ serialized cache file
|
|
||||||
.idea/caches/build_file_checksums.ser
|
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
MIT License Copyright (c) <year> <copyright holders>
|
MIT License Copyright (c) 2020 Faerbit
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
@ -1,3 +0,0 @@
|
|||||||
# fimefracking
|
|
||||||
|
|
||||||
Simple time tracking app written with Python and Qt5
|
|
7
Readme.md
Normal file
7
Readme.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# fimefracking
|
||||||
|
|
||||||
|
Simple time tracking app written with Python and Qt5
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under MIT license. See License.md for more details.
|
103
data.py
Normal file
103
data.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import atexit
|
||||||
|
from threading import Thread, Event
|
||||||
|
from collections.abc import MutableMapping
|
||||||
|
|
||||||
|
from PySide2 import QtCore
|
||||||
|
|
||||||
|
data_dir_path = os.path.join(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation),
|
||||||
|
"fimefracking")
|
||||||
|
|
||||||
|
tasks_path = os.path.join(data_dir_path, "tasks.json")
|
||||||
|
|
||||||
|
data_path = os.path.join(data_dir_path, "data_{}.json")
|
||||||
|
save_delay = 3 * 60
|
||||||
|
|
||||||
|
|
||||||
|
class Tasks:
|
||||||
|
def __init__(self):
|
||||||
|
if not os.path.exists(data_dir_path):
|
||||||
|
os.mkdir(data_dir_path)
|
||||||
|
if os.path.exists(tasks_path):
|
||||||
|
with open(tasks_path, "r") as f:
|
||||||
|
self._tasks = json.loads(f.read())
|
||||||
|
else:
|
||||||
|
self._tasks = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tasks(self):
|
||||||
|
return self._tasks
|
||||||
|
|
||||||
|
@tasks.setter
|
||||||
|
def tasks(self, tasks):
|
||||||
|
self._tasks = tasks
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
print("...saving tasks...")
|
||||||
|
with open(tasks_path, "w+") as f:
|
||||||
|
f.write(json.dumps(self._tasks))
|
||||||
|
|
||||||
|
|
||||||
|
class Data(MutableMapping):
|
||||||
|
def __init__(self):
|
||||||
|
if not os.path.exists(data_dir_path):
|
||||||
|
os.mkdir(data_dir_path)
|
||||||
|
self._cache = {}
|
||||||
|
self._hot_keys = []
|
||||||
|
self._running = False
|
||||||
|
self._tevent = Event()
|
||||||
|
self._thread = None
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
self._running = 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()
|
||||||
|
return self._cache[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self._cache[key] = value
|
||||||
|
self._hot_keys.append(key)
|
||||||
|
self._schedule_save()
|
||||||
|
|
||||||
|
def _schedule_save(self):
|
||||||
|
if self._running:
|
||||||
|
return
|
||||||
|
self._running = True
|
||||||
|
self._thread = Thread(target=self._executor, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def _executor(self):
|
||||||
|
while self._running:
|
||||||
|
self._tevent.wait(save_delay)
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
for key in self._hot_keys:
|
||||||
|
print(f"... saving dict {key} ...")
|
||||||
|
to_write = self._cache[key] # apparently thread-safe
|
||||||
|
with open(data_path.format(key), "w+") as f:
|
||||||
|
f.write(json.dumps(to_write))
|
||||||
|
self._hot_keys = []
|
||||||
|
self._saving = False
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
print("WARNING: deletion of items not supported")
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._cache)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._cache)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{type(self).__name__}({self._cache})"
|
92
main.py
Normal file
92
main.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import signal
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PySide2 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
from data import Tasks, Data
|
||||||
|
from task_edit import TaskEdit
|
||||||
|
|
||||||
|
|
||||||
|
class App:
|
||||||
|
def __init__(self):
|
||||||
|
self.app = QtWidgets.QApplication(sys.argv)
|
||||||
|
|
||||||
|
self.tasks = Tasks()
|
||||||
|
self.active_task = "Nothing"
|
||||||
|
self.data = Data()
|
||||||
|
|
||||||
|
icon = QtGui.QIcon.fromTheme("appointment-new")
|
||||||
|
|
||||||
|
self.menu = QtWidgets.QMenu()
|
||||||
|
|
||||||
|
self.tray = QtWidgets.QSystemTrayIcon()
|
||||||
|
self.tray.setIcon(icon)
|
||||||
|
self.tray.setContextMenu(self.menu)
|
||||||
|
self.tray.show()
|
||||||
|
self.tray.setToolTip("fimefracking")
|
||||||
|
self.update_tray_menu()
|
||||||
|
|
||||||
|
self.taskEdit = TaskEdit(None)
|
||||||
|
self.taskEdit.accepted.connect(self.tasks_edited)
|
||||||
|
|
||||||
|
@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.update_tray_menu()
|
||||||
|
|
||||||
|
def update_tray_menu(self):
|
||||||
|
self.menu.clear()
|
||||||
|
tasks = list(self.tasks.tasks)
|
||||||
|
tasks.append("Nothing")
|
||||||
|
|
||||||
|
for t in tasks:
|
||||||
|
a = self.menu.addAction(t)
|
||||||
|
a.triggered.connect(partial(self.change_task, t))
|
||||||
|
if t == self.active_task:
|
||||||
|
a.setIcon(QtGui.QIcon.fromTheme("go-next"))
|
||||||
|
|
||||||
|
self.menu.addSeparator()
|
||||||
|
|
||||||
|
new_action = self.menu.addAction("Edit tasks")
|
||||||
|
new_action.triggered.connect(self.edit_tasks)
|
||||||
|
|
||||||
|
report_action = self.menu.addAction("Report")
|
||||||
|
report_action.triggered.connect(self.report)
|
||||||
|
|
||||||
|
self.menu.addSeparator()
|
||||||
|
|
||||||
|
exit_action = self.menu.addAction("Close")
|
||||||
|
exit_action.triggered.connect(self.app.quit)
|
||||||
|
|
||||||
|
def sigterm_handler(self, _signo, _frame):
|
||||||
|
self.app.quit()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
timer = QtCore.QTimer()
|
||||||
|
# interrupt event loop regularly for signal handling
|
||||||
|
timer.timeout.connect(lambda: None)
|
||||||
|
timer.start(500)
|
||||||
|
signal.signal(signal.SIGTERM, self.sigterm_handler)
|
||||||
|
signal.signal(signal.SIGINT, self.sigterm_handler)
|
||||||
|
self.app.exec_()
|
||||||
|
|
||||||
|
@QtCore.Slot()
|
||||||
|
def report(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@QtCore.Slot()
|
||||||
|
def edit_tasks(self):
|
||||||
|
self.taskEdit.tasks = self.tasks.tasks
|
||||||
|
self.taskEdit.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = App()
|
||||||
|
app.run()
|
63
task_edit.py
Normal file
63
task_edit.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from PySide2 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class TaskEdit(QtWidgets.QDialog):
|
||||||
|
def __init__(self, parent, *args, **kwargs):
|
||||||
|
super().__init__(parent, *args, **kwargs)
|
||||||
|
self.setWindowTitle("Edit Tasks")
|
||||||
|
self.list = QtCore.QStringListModel()
|
||||||
|
|
||||||
|
self.tableView = QtWidgets.QTableView()
|
||||||
|
self.tableView.setModel(self.list)
|
||||||
|
self.tableView.horizontalHeader().hide()
|
||||||
|
self.tableView.verticalHeader().hide()
|
||||||
|
|
||||||
|
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()
|
||||||
|
layout.addWidget(self.tableView)
|
||||||
|
layout.addLayout(blayout)
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user