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