Inital commit.
This commit is contained in:
commit
7299491825
196
.gitignore
vendored
Normal file
196
.gitignore
vendored
Normal 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
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