Inital commit.

This commit is contained in:
Faerbit 2020-02-19 23:12:56 +01:00
commit 7299491825
4 changed files with 454 additions and 0 deletions

196
.gitignore vendored Normal file
View File

@ -0,0 +1,196 @@
# 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
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# End of https://www.gitignore.io/api/python,pycharm+all

103
data.py Normal file
View 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
View 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
View 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)