diff --git a/src/fime/config.py b/src/fime/config.py
index eff46f2..f8025a8 100644
--- a/src/fime/config.py
+++ b/src/fime/config.py
@@ -1,4 +1,5 @@
from configparser import ConfigParser
+from io import StringIO
from pathlib import Path
from loguru import logger
@@ -22,32 +23,63 @@ class Config:
def __init__(self):
self._configparser = ConfigParser()
config_dir_path = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppConfigLocation))
- config_path = config_dir_path / "fime.conf"
- if config_path.exists():
- logger.info(f'Reading config file "{config_path}"')
- with open(config_path) as f:
+ self.config_path = config_dir_path / "fime.conf"
+ if self.config_path.exists():
+ logger.info(f'reading config file "{self.config_path}"')
+ with self.config_path.open(encoding="utf-8") as f:
config_text = f.read()
config_text = "[DEFAULT]\n" + config_text
self._configparser.read_string(config_text)
+ # TODO change menu items
if (not self._configparser.has_option("DEFAULT", "jira_url") or
not self._configparser.has_option("DEFAULT", "jira_token")):
- raise FimeException(f'Please add config file {config_path} '
+ raise FimeException(f'Please add config file {self.config_path} '
f'with config keys "jira_url" and "jira_token" in INI style')
+ def save(self):
+ logger.info(f'writing config file "{self.config_path}"')
+ config_str = StringIO()
+ self._configparser.write(config_str)
+ # do not conform to configparser's stupid section requirement
+ config_str = "\n".join(config_str.getvalue().splitlines()[1:])
+ with self.config_path.open("w", encoding="utf-8") as f:
+ f.write(config_str)
+
@property
def jira_url(self):
return dequotify(self._configparser["DEFAULT"]["jira_url"])
+ @jira_url.setter
+ def jira_url(self, value):
+ self._configparser["DEFAULT"]["jira_url"] = f'"{value}"'
+
@property
def jira_token(self):
return dequotify(self._configparser["DEFAULT"]["jira_token"])
+ @jira_token.setter
+ def jira_token(self, value):
+ self._configparser["DEFAULT"]["jira_token"] = f'"{value}"'
+
@property
def tray_theme(self):
val = dequotify(self._configparser.get("DEFAULT", "tray_theme", fallback="dark")).lower()
return val if val in ["light", "dark"] else "dark"
+ @tray_theme.setter
+ def tray_theme(self, value):
+ value = value.lower()
+ if value not in ["light", "dark"]:
+ raise RuntimeError('config key "tray_theme" can only be set to "light" or "dark"')
+ self._configparser["DEFAULT"]["tray_theme"] = f'"{value}"'
+
@property
def flip_menu(self):
val = dequotify(self._configparser.get("DEFAULT", "flip_menu", fallback="no")).lower()
return val in ["yes", "true", "1"]
+
+ @flip_menu.setter
+ def flip_menu(self, value):
+ if type(value) is not bool:
+ raise RuntimeError('config key "flip_menu" must be a bool')
+ self._configparser["DEFAULT"]["flip_menu"] = f'"{value}"'
diff --git a/src/fime/import_task.py b/src/fime/import_task.py
index 3dc2aff..737e3d5 100644
--- a/src/fime/import_task.py
+++ b/src/fime/import_task.py
@@ -14,17 +14,19 @@ class ImportTask(QtWidgets.QDialog):
super().__init__(parent, *args, **kwargs)
self.setWindowTitle("New Tasks")
+ self.config = config
+
self.line_edit = QtWidgets.QLineEdit(self)
- completer = TaskCompleter(config)
- self.line_edit.setCompleter(completer)
- self.line_edit.textChanged.connect(completer.update_picker)
+ self.completer = TaskCompleter(config)
+ self.line_edit.setCompleter(self.completer)
+ self.line_edit.textChanged.connect(self.completer.update_picker)
self.line_edit.setFocus()
self.indicator = ProgressIndicator(self)
self.indicator.setAnimationDelay(70)
self.indicator.setDisplayedWhenStopped(False)
- completer.running.connect(self.spin)
- completer.stopped.connect(self.no_spin)
+ self.completer.running.connect(self.spin)
+ self.completer.stopped.connect(self.no_spin)
ok_button = QtWidgets.QPushButton()
ok_button.setText("OK")
@@ -77,6 +79,8 @@ class ImportTask(QtWidgets.QDialog):
return self.line_edit.text()
def showEvent(self, _):
+ # pick up config changes
+ self.completer.update_urls()
self.line_edit.setText("")
self.raise_()
self.activateWindow()
diff --git a/src/fime/main.py b/src/fime/main.py
index 7492163..f492fb1 100755
--- a/src/fime/main.py
+++ b/src/fime/main.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
-import os
import signal
import sys
from functools import partial
@@ -22,6 +21,7 @@ from fime.data import Tasks, Log, Data, LogCommentsData, Worklog, Report
from fime.exceptions import FimeException
from fime.import_task import ImportTask
from fime.report import ReportDialog
+from fime.settings import Settings
from fime.task_edit import TaskEdit
from fime.util import get_screen_height, get_icon
@@ -37,17 +37,11 @@ class App:
self.log = Log(lcd)
self._active_task = self.log.last_log() or "Nothing"
- config = Config()
- if config.tray_theme == "light":
- icon = get_icon("appointment-new-light")
- else:
- icon = get_icon("appointment-new")
-
- self.menu_flipped = config.flip_menu
+ self.config = Config()
self.menu = QtWidgets.QMenu(None)
- self.import_task = ImportTask(config, None)
+ self.import_task = ImportTask(self.config, None)
self.import_task.accepted.connect(self.new_task_imported)
self.taskEdit = TaskEdit(self.tasks, None)
@@ -56,20 +50,31 @@ class App:
self.reportDialog = ReportDialog(self.tasks, Report(lcd), None)
self.reportDialog.accepted.connect(self.log_edited)
- self.worklogDialog = WorklogDialog(config, Worklog(lcd), None)
+ self.worklogDialog = WorklogDialog(self.config, Worklog(lcd), None)
self.worklogDialog.accepted.connect(self.log_edited)
+ self.settings = Settings(self.config, None)
+ self.settings.accepted.connect(self.update_icon)
+ self.settings.accepted.connect(self.update_tray_menu)
+
self.about = About(None)
self.tray = QtWidgets.QSystemTrayIcon()
- self.tray.setIcon(icon)
self.tray.setContextMenu(self.menu)
+ self.update_icon()
self.tray.show()
self.tray.setToolTip("fime")
self.update_tray_menu()
self.last_dialog: Optional[QtWidgets.QDialog] = None
+ def update_icon(self):
+ if self.config.tray_theme == "light":
+ icon = get_icon("appointment-new-light")
+ else:
+ icon = get_icon("appointment-new")
+ self.tray.setIcon(icon)
+
@QtCore.Slot()
def new_task_imported(self):
if self.import_task.task_text:
@@ -123,7 +128,7 @@ class App:
add_tasks(self.tasks.tasks)
menu_items.append((1, None))
- already_taken = (len(self.tasks.tasks) + 5) * action_height
+ already_taken = (len(self.tasks.tasks) + 6) * action_height
available_space = get_screen_height(self.menu) * 0.8 - already_taken
jira_entry_count = int(available_space // action_height)
add_tasks(self.tasks.jira_tasks[-jira_entry_count:])
@@ -141,10 +146,11 @@ class App:
menu_items.append((1, None))
+ menu_items.append(("Settings", partial(self.open_new_dialog, self.settings)))
menu_items.append(("About", partial(self.open_new_dialog, self.about)))
menu_items.append(("Close", self.app.quit))
- if self.menu_flipped:
+ if self.config.flip_menu:
menu_items.reverse()
seps = 0
@@ -177,7 +183,7 @@ class App:
def init_logging():
log_dir_path = Path(QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppDataLocation)) / "logs"
- logger.add(log_dir_path / "fime_{time:YYYY-MM-DD}.log", rotation="1d", retention="30d", compression="zip", level="INFO")
+ logger.add(log_dir_path / "fime_{time:YYYY-MM-DD}.log", rotation="1d", retention="30d", compression="zip", level="DEBUG")
def excepthook(e_type, e_value, tb_obj):
diff --git a/src/fime/settings.py b/src/fime/settings.py
new file mode 100644
index 0000000..35ecca7
--- /dev/null
+++ b/src/fime/settings.py
@@ -0,0 +1,93 @@
+from fime.util import get_icon
+
+try:
+ from PySide6 import QtCore, QtGui, QtWidgets
+except ImportError:
+ from PySide2 import QtCore, QtGui, QtWidgets
+
+from fime.config import Config
+
+
+class Settings(QtWidgets.QDialog):
+ def __init__(self, config: Config, parent, *args, **kwargs):
+ super().__init__(parent, *args, **kwargs)
+ self.setWindowTitle("Settings")
+
+ self.config = config
+
+ caption_label = QtWidgets.QLabel()
+ caption_label.setText("Settings")
+ caption_label.setAlignment(QtCore.Qt.AlignHCenter)
+
+ settings_layout = QtWidgets.QGridLayout()
+
+ jira_url_label = QtWidgets.QLabel()
+ jira_url_label.setText("Jira URL")
+ settings_layout.addWidget(jira_url_label, 0, 0)
+ self.jira_url_edit = QtWidgets.QLineEdit()
+ settings_layout.addWidget(self.jira_url_edit, 0, 1)
+
+ jira_token_label = QtWidgets.QLabel()
+ jira_token_label.setText("Jira Personal Access Token
see here for how to get one")
+ jira_token_label.setTextFormat(QtCore.Qt.RichText)
+ jira_token_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
+ jira_token_label.setOpenExternalLinks(True)
+ settings_layout.addWidget(jira_token_label, 1, 0)
+ self.jira_token_edit = QtWidgets.QLineEdit()
+ settings_layout.addWidget(self.jira_token_edit, 1, 1)
+
+ tray_theme_label = QtWidgets.QLabel()
+ tray_theme_label.setText("Tray theme")
+ settings_layout.addWidget(tray_theme_label, 2, 0)
+ self.tray_theme_combo_box = QtWidgets.QComboBox()
+ self.tray_theme_combo_box.addItem("Light")
+ self.tray_theme_combo_box.addItem("Dark")
+ settings_layout.addWidget(self.tray_theme_combo_box, 2, 1, QtCore.Qt.AlignRight)
+
+ flip_menu_label = QtWidgets.QLabel()
+ flip_menu_label.setText("Flip menu")
+ settings_layout.addWidget(flip_menu_label, 3, 0)
+ self.flip_menu_check_box = QtWidgets.QCheckBox()
+ settings_layout.addWidget(self.flip_menu_check_box, 3, 1, QtCore.Qt.AlignRight)
+
+ self.ok_button = QtWidgets.QPushButton()
+ self.ok_button.setText("OK")
+ self.ok_button.setIcon(get_icon("dialog-ok"))
+ self.ok_button.pressed.connect(self.accept)
+ self.ok_button.setAutoDefault(True)
+
+ button_layout = QtWidgets.QHBoxLayout()
+ button_layout.addStretch(66)
+ button_layout.addWidget(self.ok_button, 33)
+
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(caption_label)
+ layout.addLayout(settings_layout)
+ layout.addLayout(button_layout)
+
+ self.setLayout(layout)
+ self.resize(500, 0)
+ self.accepted.connect(self._accepted)
+
+ def showEvent(self, _):
+ self.jira_url_edit.setText(self.config.jira_url)
+ self.jira_token_edit.setText(self.config.jira_token)
+ self.tray_theme_combo_box.setCurrentText(self.config.tray_theme.capitalize())
+ self.flip_menu_check_box.setChecked(self.config.flip_menu)
+
+ def _accepted(self):
+ self.config.jira_url = self.jira_url_edit.text()
+ self.config.jira_token = self.jira_token_edit.text()
+ self.config.tray_theme = self.tray_theme_combo_box.currentText()
+ self.config.flip_menu = self.flip_menu_check_box.isChecked()
+ self.config.save()
+
+
+# only for dev/debug
+if __name__ == "__main__":
+ QtCore.QCoreApplication.setApplicationName("fime")
+ app = QtWidgets.QApplication()
+ cfg = Config()
+ settings = Settings(cfg, None)
+ settings.show()
+ app.exec()
diff --git a/src/fime/task_completer.py b/src/fime/task_completer.py
index e0d2b8b..d539ddf 100644
--- a/src/fime/task_completer.py
+++ b/src/fime/task_completer.py
@@ -29,8 +29,9 @@ class TaskCompleter(QtWidgets.QCompleter):
self.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.session = FuturesSession()
self.config = config
- self.picker_url = os.path.join(self.config.jira_url, "rest/api/2/issue/picker")
- self.search_url = os.path.join(self.config.jira_url, "rest/api/2/search")
+ self.picker_url = None
+ self.search_url = None
+ self.update_urls()
self.text = ""
self.response_text = ""
self.model_data = set()
@@ -44,6 +45,10 @@ class TaskCompleter(QtWidgets.QCompleter):
self.rif_counter_lock = threading.Lock()
self.last_rif_state = TaskCompleter.RifState.STOPPED
+ def update_urls(self):
+ self.picker_url = os.path.join(self.config.jira_url, "rest/api/2/issue/picker")
+ self.search_url = os.path.join(self.config.jira_url, "rest/api/2/search")
+
@QtCore.Slot()
def process_response(self):
with self.rif_counter_lock:
diff --git a/src/fime/worklog.py b/src/fime/worklog.py
index 603bcb7..a08ba40 100644
--- a/src/fime/worklog.py
+++ b/src/fime/worklog.py
@@ -175,7 +175,7 @@ class WorklogDialog(QtWidgets.QDialog):
self.update_timer.timeout.connect(self.update_statuses)
def showEvent(self, _):
- # reinitialize to purge caches
+ # reinitialize to purge caches and pick up config changes
self.rest = WorklogRest(self.config)
self._worklog.date = date.today()
self.update_all()