Add ability to set alarm reminders
Some checks failed
CI / test (push) Failing after 3m25s
Lint / test (push) Failing after 25s
Trivy / test (push) Successful in 22s

This commit is contained in:
Miguel Jacq 2025-11-18 20:38:39 +11:00
parent 63cf561bfe
commit 83f25405db
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
8 changed files with 241 additions and 7 deletions

View file

@ -5,6 +5,7 @@
* Add Tag relationship visualisation tool * Add Tag relationship visualisation tool
* Improve size of checkboxes * Improve size of checkboxes
* Convert bullet - to actual unicode bullets * Convert bullet - to actual unicode bullets
* Add alarm option to set reminders
# 0.3.2 # 0.3.2

View file

@ -16,10 +16,16 @@ report from within the app.
## Screenshots ## Screenshots
### General view
![Screenshot of Bouquin](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/screenshot.png) ![Screenshot of Bouquin](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/screenshot.png)
### History panes
![Screenshot of Bouquin History Preview pane](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/history_preview.png) ![Screenshot of Bouquin History Preview pane](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/history_preview.png)
![Screenshot of Bouquin History Diff pane](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/history_diff.png) ![Screenshot of Bouquin History Diff pane](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/history_diff.png)
### Tag relationship visualiser
![Screenshot of Tag Relationship Visualiser](https://git.mig5.net/mig5/bouquin/raw/branch/main/screenshots/bouquin_tag_relationship_graph.png)
## Features ## Features
* Data is encrypted at rest * Data is encrypted at rest
@ -41,6 +47,7 @@ report from within the app.
* Automatically generate checkboxes when typing 'TODO' * Automatically generate checkboxes when typing 'TODO'
* It is possible to automatically move unchecked checkboxes from yesterday to today, on startup * It is possible to automatically move unchecked checkboxes from yesterday to today, on startup
* English, French and Italian locales provided * English, French and Italian locales provided
* Ability to set reminder alarms in the app against the current line of text on today's date
## How to install ## How to install

View file

@ -155,5 +155,12 @@
"bug_report_empty": "Please enter some details about the bug before sending.", "bug_report_empty": "Please enter some details about the bug before sending.",
"bug_report_send_failed": "Could not send bug report.", "bug_report_send_failed": "Could not send bug report.",
"bug_report_sent_ok": "Bug report sent. Thank you!", "bug_report_sent_ok": "Bug report sent. Thank you!",
"send": "Send" "send": "Send",
"set_reminder": "Set reminder prompt",
"set_reminder_prompt": "Enter a time",
"reminder_no_text_fallback": "You scheduled a reminder to alert you now!",
"invalid_time_title": "Invalid time",
"invalid_time_message": "Please enter a time in the format HH:MM",
"dismiss": "Dismiss",
"toolbar_alarm": "Set reminder alarm"
} }

View file

@ -16,6 +16,8 @@ from PySide6.QtCore import (
QUrl, QUrl,
QEvent, QEvent,
QSignalBlocker, QSignalBlocker,
QDateTime,
QTime,
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QAction, QAction,
@ -43,6 +45,10 @@ from PySide6.QtWidgets import (
QTabWidget, QTabWidget,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
QInputDialog,
QLabel,
QPushButton,
QApplication,
) )
from .db import DBManager from .db import DBManager
@ -164,8 +170,6 @@ class MainWindow(QMainWindow):
self._locked = False self._locked = False
# reset idle timer on any key press anywhere in the app # reset idle timer on any key press anywhere in the app
from PySide6.QtWidgets import QApplication
QApplication.instance().installEventFilter(self) QApplication.instance().installEventFilter(self)
# Focus on the editor # Focus on the editor
@ -292,7 +296,9 @@ class MainWindow(QMainWindow):
self._save_timer = QTimer(self) self._save_timer = QTimer(self)
self._save_timer.setSingleShot(True) self._save_timer.setSingleShot(True)
self._save_timer.timeout.connect(self._save_current) self._save_timer.timeout.connect(self._save_current)
# Note: textChanged will be connected per-editor in _create_new_tab
# Reminders / alarms
self._reminder_timers: list[QTimer] = []
# First load + mark dates in calendar with content # First load + mark dates in calendar with content
if not self._load_yesterday_todos(): if not self._load_yesterday_todos():
@ -310,6 +316,9 @@ class MainWindow(QMainWindow):
# apply once on startup so links / calendar colors are set immediately # apply once on startup so links / calendar colors are set immediately
self._retheme_overrides() self._retheme_overrides()
# Build any alarms for *today* from stored markdown
self._rebuild_reminders_for_today()
@property @property
def editor(self) -> MarkdownEditor | None: def editor(self) -> MarkdownEditor | None:
"""Get the currently active editor.""" """Get the currently active editor."""
@ -966,6 +975,7 @@ class MainWindow(QMainWindow):
self._tb_bullets = lambda: self._call_editor("toggle_bullets") self._tb_bullets = lambda: self._call_editor("toggle_bullets")
self._tb_numbers = lambda: self._call_editor("toggle_numbers") self._tb_numbers = lambda: self._call_editor("toggle_numbers")
self._tb_checkboxes = lambda: self._call_editor("toggle_checkboxes") self._tb_checkboxes = lambda: self._call_editor("toggle_checkboxes")
self._tb_alarm = self._on_alarm_requested
tb.boldRequested.connect(self._tb_bold) tb.boldRequested.connect(self._tb_bold)
tb.italicRequested.connect(self._tb_italic) tb.italicRequested.connect(self._tb_italic)
@ -975,9 +985,9 @@ class MainWindow(QMainWindow):
tb.bulletsRequested.connect(self._tb_bullets) tb.bulletsRequested.connect(self._tb_bullets)
tb.numbersRequested.connect(self._tb_numbers) tb.numbersRequested.connect(self._tb_numbers)
tb.checkboxesRequested.connect(self._tb_checkboxes) tb.checkboxesRequested.connect(self._tb_checkboxes)
tb.alarmRequested.connect(self._tb_alarm)
tb.historyRequested.connect(self._open_history)
tb.insertImageRequested.connect(self._on_insert_image) tb.insertImageRequested.connect(self._on_insert_image)
tb.historyRequested.connect(self._open_history)
self._toolbar_bound = True self._toolbar_bound = True
@ -1023,6 +1033,177 @@ class MainWindow(QMainWindow):
self.toolBar.actBullets.setChecked(bool(bullets_on)) self.toolBar.actBullets.setChecked(bool(bullets_on))
self.toolBar.actNumbers.setChecked(bool(numbers_on)) self.toolBar.actNumbers.setChecked(bool(numbers_on))
# ----------- Alarms handler ------------#
def _on_alarm_requested(self):
"""Create a one-shot reminder based on the current line in the editor."""
editor = getattr(self, "editor", None)
if editor is None:
return
# Use the current line in the markdown editor as the reminder text
try:
line_text = editor.get_current_line_text().strip()
except AttributeError:
c = editor.textCursor()
line_text = c.block().text().strip()
# Ask user for a time today in HH:MM format
time_str, ok = QInputDialog.getText(
self,
strings._("set_reminder"),
strings._("set_reminder_prompt") + " (HH:MM)",
)
if not ok or not time_str.strip():
return
try:
hour, minute = map(int, time_str.strip().split(":", 1))
except ValueError:
QMessageBox.warning(
self,
strings._("invalid_time_title"),
strings._("invalid_time_message"),
)
return
now = QDateTime.currentDateTime()
target = QDateTime(now.date(), QTime(hour, minute))
t = QTime(hour, minute)
if not t.isValid():
QMessageBox.warning(
self,
strings._("invalid_time_title"),
strings._("invalid_time_message"),
)
return
# Normalise to HH:MM
time_str = f"{t.hour():02d}:{t.minute():02d}"
# Insert / update ⏰ in the editor text
if hasattr(editor, "insert_alarm_marker"):
editor.insert_alarm_marker(time_str)
# Rebuild timers, but only if this page is for "today"
self._rebuild_reminders_for_today()
def _show_flashing_reminder(self, text: str):
"""
Show a small flashing dialog and request attention from the OS.
Called by reminder timers.
"""
# Ask OS to flash / bounce our app in the dock/taskbar
QApplication.alert(self, 0)
# Try to bring the window to the front
self.showNormal()
self.raise_()
self.activateWindow()
# Simple dialog with a flashing background to reinforce the alert
dlg = QDialog(self)
dlg.setWindowTitle(strings._("reminder"))
dlg.setModal(True)
layout = QVBoxLayout(dlg)
label = QLabel(text)
label.setWordWrap(True)
layout.addWidget(label)
btn = QPushButton(strings._("dismiss"))
btn.clicked.connect(dlg.accept)
layout.addWidget(btn)
flash_timer = QTimer(dlg)
flash_state = {"on": False}
def toggle():
flash_state["on"] = not flash_state["on"]
if flash_state["on"]:
dlg.setStyleSheet("background-color: #3B3B3B;")
else:
dlg.setStyleSheet("")
flash_timer.timeout.connect(toggle)
flash_timer.start(500) # ms
dlg.exec()
flash_timer.stop()
def _clear_reminder_timers(self):
"""Stop and delete any existing reminder timers."""
for t in self._reminder_timers:
try:
t.stop()
t.deleteLater()
except Exception:
pass
self._reminder_timers = []
def _rebuild_reminders_for_today(self):
"""
Scan the markdown for today's date and create QTimers
only for alarms on the *current day* (system date).
"""
# We only ever set timers for the real current date
today = QDate.currentDate()
today_iso = today.toString("yyyy-MM-dd")
# Clear any previously scheduled "today" reminders
self._clear_reminder_timers()
# Prefer live editor content if it is showing today's page
text = ""
if (
hasattr(self, "editor")
and hasattr(self.editor, "current_date")
and self.editor.current_date == today
):
text = self.editor.to_markdown()
else:
# Fallback to DB: still only today's date
text = self.db.get_entry(today_iso) if hasattr(self, "db") else ""
if not text:
return
now = QDateTime.currentDateTime()
for line in text.splitlines():
# Look for "⏰ HH:MM" anywhere in the line
m = re.search(r"\s*(\d{1,2}):(\d{2})", line)
if not m:
continue
hour = int(m.group(1))
minute = int(m.group(2))
t = QTime(hour, minute)
if not t.isValid():
continue
target = QDateTime(today, t)
# Skip alarms that are already in the past
if target <= now:
continue
# The reminder text is the part before the symbol
reminder_text = line.split("", 1)[0].strip()
if not reminder_text:
reminder_text = strings._("reminder_no_text_fallback")
msecs = now.msecsTo(target)
timer = QTimer(self)
timer.setSingleShot(True)
timer.timeout.connect(
lambda txt=reminder_text: self._show_flashing_reminder(txt)
)
timer.start(msecs)
self._reminder_timers.append(timer)
# ----------- History handler ------------# # ----------- History handler ------------#
def _open_history(self): def _open_history(self):
if hasattr(self.editor, "current_date"): if hasattr(self.editor, "current_date"):

View file

@ -391,12 +391,41 @@ class MarkdownEditor(QTextEdit):
cursor.setPosition(match.end(), QTextCursor.MoveMode.KeepAnchor) cursor.setPosition(match.end(), QTextCursor.MoveMode.KeepAnchor)
cursor.insertImage(img_format) cursor.insertImage(img_format)
def insert_alarm_marker(self, time_str: str) -> None:
"""
Append or replace an HH:MM marker on the current line.
time_str is expected to be 'HH:MM'.
"""
cursor = self.textCursor()
block = cursor.block()
line = block.text()
# Strip any existing ⏰ HH:MM at the end of the line
new_line = re.sub(r"\s*⏰\s*\d{1,2}:\d{2}\s*$", "", line).rstrip()
# Append the new marker
new_line = f"{new_line}{time_str}"
bc = QTextCursor(block)
bc.beginEditBlock()
bc.select(QTextCursor.SelectionType.BlockUnderCursor)
bc.insertText(new_line)
bc.endEditBlock()
# Move cursor to end of the edited line
cursor.setPosition(block.position() + len(new_line))
self.setTextCursor(cursor)
def _get_current_line(self) -> str: def _get_current_line(self) -> str:
"""Get the text of the current line.""" """Get the text of the current line."""
cursor = self.textCursor() cursor = self.textCursor()
cursor.select(QTextCursor.SelectionType.LineUnderCursor) cursor.select(QTextCursor.SelectionType.LineUnderCursor)
return cursor.selectedText() return cursor.selectedText()
def get_current_line_text(self) -> str:
"""Public wrapper used by MainWindow for reminders."""
return self._get_current_line()
def _detect_list_type(self, line: str) -> tuple[str | None, str]: def _detect_list_type(self, line: str) -> tuple[str | None, str]:
""" """
Detect if line is a list item. Returns (list_type, prefix). Detect if line is a list item. Returns (list_type, prefix).

View file

@ -281,7 +281,7 @@ class TagGraphDialog(QDialog):
# Update labels # Update labels
for i, label in enumerate(self._label_items): for i, label in enumerate(self._label_items):
label.setPos(float(pos[i, 0]), float(pos[i, 1]) + 0.30) label.setPos(float(pos[i, 0]), float(pos[i, 1]) + 0.15)
# Update halo positions to match nodes # Update halo positions to match nodes
if self._halo_sizes and self._halo_brushes: if self._halo_sizes and self._halo_brushes:

View file

@ -18,6 +18,7 @@ class ToolBar(QToolBar):
checkboxesRequested = Signal() checkboxesRequested = Signal()
historyRequested = Signal() historyRequested = Signal()
insertImageRequested = Signal() insertImageRequested = Signal()
alarmRequested = Signal()
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(strings._("toolbar_format"), parent) super().__init__(strings._("toolbar_format"), parent)
@ -85,6 +86,11 @@ class ToolBar(QToolBar):
self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes")) self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes"))
self.actCheckboxes.triggered.connect(self.checkboxesRequested) self.actCheckboxes.triggered.connect(self.checkboxesRequested)
# Alarm / reminder
self.actAlarm = QAction("", self)
self.actAlarm.setToolTip(strings._("toolbar_alarm"))
self.actAlarm.triggered.connect(self.alarmRequested)
# Images # Images
self.actInsertImg = QAction(strings._("images"), self) self.actInsertImg = QAction(strings._("images"), self)
self.actInsertImg.setToolTip(strings._("insert_images")) self.actInsertImg.setToolTip(strings._("insert_images"))
@ -129,6 +135,7 @@ class ToolBar(QToolBar):
self.actBullets, self.actBullets,
self.actNumbers, self.actNumbers,
self.actCheckboxes, self.actCheckboxes,
self.actAlarm,
self.actInsertImg, self.actInsertImg,
self.actHistory, self.actHistory,
] ]
@ -151,6 +158,8 @@ class ToolBar(QToolBar):
# Lists # Lists
self._style_letter_button(self.actBullets, "") self._style_letter_button(self.actBullets, "")
self._style_letter_button(self.actNumbers, "1.") self._style_letter_button(self.actNumbers, "1.")
self._style_letter_button(self.actCheckboxes, "")
self._style_letter_button(self.actAlarm, "")
# History # History
self._style_letter_button(self.actHistory, strings._("view_history")) self._style_letter_button(self.actHistory, strings._("view_history"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB