From 988a5f1490c6793eb9d75a74a3a481aa32c15746 Mon Sep 17 00:00:00 2001 From: LeikeBaus Date: Tue, 19 May 2026 22:38:42 +0200 Subject: [PATCH 01/16] Created basic test case structure --- test_delete.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test_delete.py diff --git a/test_delete.py b/test_delete.py new file mode 100644 index 0000000..4be4b83 --- /dev/null +++ b/test_delete.py @@ -0,0 +1,25 @@ +import os +import tempfile +import unittest +from unittest.mock import patch + +class TestDeleteDB(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.mkdtemp() + self._db = os.path.join(self._tmp, 'test.db') + self._patch = patch('database.DB_PATH', self._db) + self._patch.start() + import database + database.init() + self.db = database + print(f"Using temporary database at {self._db}") + + def tearDown(self): + self._patch.stop() + + def test_find_note_by_keyword_found(self): + pass + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 7e19663375a71fce5ce6545f276eb51298503308 Mon Sep 17 00:00:00 2001 From: LeikeBaus Date: Wed, 20 May 2026 10:46:34 +0200 Subject: [PATCH 02/16] Added tests for finding note, appointment and reminder --- test_delete.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/test_delete.py b/test_delete.py index 4be4b83..7657203 100644 --- a/test_delete.py +++ b/test_delete.py @@ -12,13 +12,31 @@ def setUp(self): import database database.init() self.db = database - print(f"Using temporary database at {self._db}") + # print(f"Using temporary database at {self._db}") def tearDown(self): self._patch.stop() - def test_find_note_by_keyword_found(self): - pass + def test_find_note_by_keyword(self): + self.db.save_note("Learn for software engineering", title="Study") + result = self.db.find_note_by_keyword("Study") + self.assertIsNotNone(result) + self.assertEqual(result['title'], "Study") + self.assertEqual(result['content'], "Learn for software engineering") + + def test_find_appointment_by_keyword(self): + self.db.create_appointment("Test am Rechner", "2026-06-01T10:15") + result = self.db.find_appointment_by_keyword("Test") + self.assertIsNotNone(result) + self.assertEqual(result['title'], "Test am Rechner") + self.assertEqual(result['dt'], "2026-06-01T10:15") + + def test_find_reminder_by_keyword(self): + self.db.set_reminder("Learn for test", "2026-05-26T18:00") + result = self.db.find_reminder_by_keyword("Learn") + self.assertIsNotNone(result) + self.assertEqual(result['message'], "Learn for test") + self.assertEqual(result['remind_at'], "2026-05-26T18:00") if __name__ == '__main__': From f0c3b64e9ac349f3941e7400318c23892776c2e0 Mon Sep 17 00:00:00 2001 From: LeikeBaus Date: Thu, 21 May 2026 18:50:18 +0200 Subject: [PATCH 03/16] Added tests for dispatch and created constants --- test_delete.py | 70 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/test_delete.py b/test_delete.py index 7657203..b6f0338 100644 --- a/test_delete.py +++ b/test_delete.py @@ -3,6 +3,17 @@ import unittest from unittest.mock import patch +NOTE_CONTENT = "Learn for software engineering" +NOTE_TITLE = "Study" +NOTE_KEYWORD = "software" +APPOINTMENT_TITLE = "Short test" +APPOINTMENT_DT = "2026-06-01T10:15" +APPOINTMENT_KEYWORD = "short" +REMINDER_MESSAGE = "Learn for test" +REMINDER_DT = "2026-05-26T18:00" +REMINDER_KEYWORD = "learn" + + class TestDeleteDB(unittest.TestCase): def setUp(self): self._tmp = tempfile.mkdtemp() @@ -12,31 +23,64 @@ def setUp(self): import database database.init() self.db = database - # print(f"Using temporary database at {self._db}") def tearDown(self): self._patch.stop() def test_find_note_by_keyword(self): - self.db.save_note("Learn for software engineering", title="Study") - result = self.db.find_note_by_keyword("Study") + self.db.save_note(NOTE_CONTENT, title=NOTE_TITLE) + result = self.db.find_note_by_keyword(NOTE_KEYWORD) self.assertIsNotNone(result) - self.assertEqual(result['title'], "Study") - self.assertEqual(result['content'], "Learn for software engineering") + self.assertEqual(result['title'], NOTE_TITLE) + self.assertEqual(result['content'], NOTE_CONTENT) def test_find_appointment_by_keyword(self): - self.db.create_appointment("Test am Rechner", "2026-06-01T10:15") - result = self.db.find_appointment_by_keyword("Test") + self.db.create_appointment(APPOINTMENT_TITLE, APPOINTMENT_DT) + result = self.db.find_appointment_by_keyword(APPOINTMENT_KEYWORD) self.assertIsNotNone(result) - self.assertEqual(result['title'], "Test am Rechner") - self.assertEqual(result['dt'], "2026-06-01T10:15") + self.assertEqual(result['title'], APPOINTMENT_TITLE) + self.assertEqual(result['dt'], APPOINTMENT_DT) def test_find_reminder_by_keyword(self): - self.db.set_reminder("Learn for test", "2026-05-26T18:00") - result = self.db.find_reminder_by_keyword("Learn") + self.db.set_reminder(REMINDER_MESSAGE, REMINDER_DT) + result = self.db.find_reminder_by_keyword(REMINDER_KEYWORD) self.assertIsNotNone(result) - self.assertEqual(result['message'], "Learn for test") - self.assertEqual(result['remind_at'], "2026-05-26T18:00") + self.assertEqual(result['message'], REMINDER_MESSAGE) + self.assertEqual(result['remind_at'], REMINDER_DT) + + +class TestDeleteDispatch(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.mkdtemp() + self._db = os.path.join(self._tmp, "test.db") + self._patch = patch("database.DB_PATH", self._db) + self._patch.start() + import database + database.init() + self.db = database + from assistant import _dispatch + self._dispatch = _dispatch + + def tearDown(self): + self._patch.stop() + + def test_delete_note_returns_confirmation(self): + self.db.save_note(NOTE_CONTENT, title=NOTE_TITLE) + result = self._dispatch({"function": "delete_note", + "arguments": {"keyword": NOTE_KEYWORD}}) + self.assertTrue(result.startswith("__confirm_delete__:note:")) + + def test_delete_appointment_returns_confirmation(self): + self.db.create_appointment(APPOINTMENT_TITLE, APPOINTMENT_DT) + result = self._dispatch({"function": "delete_appointment", + "arguments": {"keyword": APPOINTMENT_KEYWORD}}) + self.assertTrue(result.startswith("__confirm_delete__:appointment:")) + + def test_delete_reminder_returns_confirmation(self): + self.db.set_reminder(REMINDER_MESSAGE, REMINDER_DT) + result = self._dispatch({"function": "delete_reminder", + "arguments": {"keyword": REMINDER_KEYWORD}}) + self.assertTrue(result.startswith("__confirm_delete__:reminder:")) if __name__ == '__main__': From e39a9478e228cf5c55fca7fc8b95cbf25032bb54 Mon Sep 17 00:00:00 2001 From: LikeARealGinger Date: Thu, 21 May 2026 21:55:53 +0200 Subject: [PATCH 04/16] Add confirmed note deletion by keyword --- assistant.py | 19 +++++++++++++++++++ database.py | 30 +++++++++++++++++++++++++++++- locales.py | 4 ++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/assistant.py b/assistant.py index 38bc58b..78724c6 100644 --- a/assistant.py +++ b/assistant.py @@ -68,6 +68,22 @@ }, }, }, + { + "type": "function", + "function": { + "name": "delete_note", + "description": "Delete a saved note, found by keyword.", + "parameters": { + "type": "object", + "properties": { + "keyword": {"type": "string", "description": "Keyword from the note title or content"}, + "confirmed": {"type": "boolean", "description": "True only after the user confirmed deletion", + "default": False}, + }, + "required": ["keyword"], + }, + }, + }, { "type": "function", "function": { @@ -239,6 +255,9 @@ def _dispatch(fc: dict) -> str: else: return locales.get("list_not_found", title=args.get("list_title", "")) + elif name == "delete_note": + return db.delete_note(args.get("keyword", ""), args.get("confirmed", False)) + elif name == "create_appointment": aid = db.create_appointment( title=args.get("title", ""), diff --git a/database.py b/database.py index e9f1fc5..1d2c4ff 100644 --- a/database.py +++ b/database.py @@ -5,6 +5,7 @@ import sqlite3 import threading from datetime import datetime, timedelta +import locales from paths import DB_PATH _lock = threading.Lock() @@ -162,6 +163,24 @@ def find_list_by_title(title: str) -> dict | None: return None +def find_note_by_keyword(keyword: str) -> dict | None: + """Find a note by fuzzy title/content match. Returns dict or None.""" + target = keyword.strip().lower() + if not target: + return None + keyword_pattern = f"%{target}%" + with _lock: + c = _conn() + row = c.execute( + "SELECT * FROM notes" + " WHERE lower(title) LIKE ? OR lower(content) LIKE ?" + " ORDER BY updated_at DESC", + (keyword_pattern, keyword_pattern), + ).fetchone() + c.close() + return dict(row) if row else None + + def get_all_notes() -> list[dict]: with _lock: c = _conn() @@ -170,12 +189,21 @@ def get_all_notes() -> list[dict]: return [dict(r) for r in rows] -def delete_note(note_id: int): +def delete_note(note_id: int | str, confirmed: bool = False): + if isinstance(note_id, str): + note = find_note_by_keyword(note_id) + if not note: + return locales.get("note_not_found", keyword=note_id) + if confirmed is not True: + return f"__confirm_delete__:note:{note['id']}" + note_id = note["id"] + with _lock: c = _conn() c.execute("DELETE FROM notes WHERE id=?", (note_id,)) c.commit() c.close() + return locales.get("note_deleted") # ── Appointments ────────────────────────────────────────────────────────── diff --git a/locales.py b/locales.py index 88b86a5..fe80b55 100644 --- a/locales.py +++ b/locales.py @@ -18,6 +18,8 @@ "list_saved": "List '{title}' saved ({count} items)", "added_to_list": "Added to '{title}'", "list_not_found": "List '{title}' not found", + "note_not_found": "Note matching '{keyword}' not found", + "note_deleted": "Note deleted", "appointment_created": "Appointment created: {title} ({dt})", "reminder_set": "Reminder set: {dt}", "unknown_command": "Unknown command: {name}", @@ -97,6 +99,8 @@ "list_saved": "Lista '{title}' salvata ({count} elementi)", "added_to_list": "Aggiunto a '{title}'", "list_not_found": "Lista '{title}' non trovata", + "note_not_found": "Nota con '{keyword}' non trovata", + "note_deleted": "Nota eliminata", "appointment_created": "Appuntamento creato: {title} ({dt})", "reminder_set": "Reminder impostato: {dt}", "unknown_command": "Comando sconosciuto: {name}", From 75010dccae9dc40270a35dda675abb3b318e328d Mon Sep 17 00:00:00 2001 From: LikeARealGinger Date: Thu, 21 May 2026 22:56:15 +0200 Subject: [PATCH 05/16] Refactor note deletion and include deleted note title and id --- assistant.py | 11 ++++++++++- database.py | 12 +----------- locales.py | 4 ++-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/assistant.py b/assistant.py index 78724c6..c710c15 100644 --- a/assistant.py +++ b/assistant.py @@ -256,7 +256,16 @@ def _dispatch(fc: dict) -> str: return locales.get("list_not_found", title=args.get("list_title", "")) elif name == "delete_note": - return db.delete_note(args.get("keyword", ""), args.get("confirmed", False)) + keyword = args.get("keyword", "") + note = db.find_note_by_keyword(keyword) + if not note: + return locales.get("note_not_found", keyword=keyword) + if args.get("confirmed", False) is not True: + return f"__confirm_delete__:note:{note['id']}" + title = note["title"] or locales.get("default_note_title") + nid = note["id"] + db.delete_note(nid) + return locales.get("note_deleted", title=title, nid=nid) elif name == "create_appointment": aid = db.create_appointment( diff --git a/database.py b/database.py index 1d2c4ff..88bcdcb 100644 --- a/database.py +++ b/database.py @@ -5,7 +5,6 @@ import sqlite3 import threading from datetime import datetime, timedelta -import locales from paths import DB_PATH _lock = threading.Lock() @@ -189,21 +188,12 @@ def get_all_notes() -> list[dict]: return [dict(r) for r in rows] -def delete_note(note_id: int | str, confirmed: bool = False): - if isinstance(note_id, str): - note = find_note_by_keyword(note_id) - if not note: - return locales.get("note_not_found", keyword=note_id) - if confirmed is not True: - return f"__confirm_delete__:note:{note['id']}" - note_id = note["id"] - +def delete_note(note_id: int): with _lock: c = _conn() c.execute("DELETE FROM notes WHERE id=?", (note_id,)) c.commit() c.close() - return locales.get("note_deleted") # ── Appointments ────────────────────────────────────────────────────────── diff --git a/locales.py b/locales.py index fe80b55..c208550 100644 --- a/locales.py +++ b/locales.py @@ -19,7 +19,7 @@ "added_to_list": "Added to '{title}'", "list_not_found": "List '{title}' not found", "note_not_found": "Note matching '{keyword}' not found", - "note_deleted": "Note deleted", + "note_deleted": "Note '{title}' deleted (#{nid})", "appointment_created": "Appointment created: {title} ({dt})", "reminder_set": "Reminder set: {dt}", "unknown_command": "Unknown command: {name}", @@ -100,7 +100,7 @@ "added_to_list": "Aggiunto a '{title}'", "list_not_found": "Lista '{title}' non trovata", "note_not_found": "Nota con '{keyword}' non trovata", - "note_deleted": "Nota eliminata", + "note_deleted": "Nota '{title}' eliminata (#{nid})", "appointment_created": "Appuntamento creato: {title} ({dt})", "reminder_set": "Reminder impostato: {dt}", "unknown_command": "Comando sconosciuto: {name}", From 5404b634a6ff2076171662de87467f5351806c8a Mon Sep 17 00:00:00 2001 From: LeikeBaus Date: Fri, 22 May 2026 11:13:16 +0200 Subject: [PATCH 06/16] Added test cases when keyword doesn't match --- test_delete.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/test_delete.py b/test_delete.py index b6f0338..24853fd 100644 --- a/test_delete.py +++ b/test_delete.py @@ -12,6 +12,7 @@ REMINDER_MESSAGE = "Learn for test" REMINDER_DT = "2026-05-26T18:00" REMINDER_KEYWORD = "learn" +NON_EXISTENT = "nonexistent" class TestDeleteDB(unittest.TestCase): @@ -27,27 +28,39 @@ def setUp(self): def tearDown(self): self._patch.stop() - def test_find_note_by_keyword(self): + def test_find_note_by_keyword_found(self): self.db.save_note(NOTE_CONTENT, title=NOTE_TITLE) result = self.db.find_note_by_keyword(NOTE_KEYWORD) self.assertIsNotNone(result) self.assertEqual(result['title'], NOTE_TITLE) self.assertEqual(result['content'], NOTE_CONTENT) + + def test_find_note_by_keyword_not_found(self): + result = self.db.find_note_by_keyword(NON_EXISTENT) + self.assertIsNone(result) - def test_find_appointment_by_keyword(self): + def test_find_appointment_by_keyword_found(self): self.db.create_appointment(APPOINTMENT_TITLE, APPOINTMENT_DT) result = self.db.find_appointment_by_keyword(APPOINTMENT_KEYWORD) self.assertIsNotNone(result) self.assertEqual(result['title'], APPOINTMENT_TITLE) self.assertEqual(result['dt'], APPOINTMENT_DT) + + def test_find_appointment_by_keyword_not_found(self): + result = self.db.find_appointment_by_keyword(NON_EXISTENT) + self.assertIsNone(result) - def test_find_reminder_by_keyword(self): + def test_find_reminder_by_keyword_found(self): self.db.set_reminder(REMINDER_MESSAGE, REMINDER_DT) result = self.db.find_reminder_by_keyword(REMINDER_KEYWORD) self.assertIsNotNone(result) self.assertEqual(result['message'], REMINDER_MESSAGE) self.assertEqual(result['remind_at'], REMINDER_DT) + def test_find_reminder_by_keyword_not_found(self): + result = self.db.find_reminder_by_keyword(NON_EXISTENT) + self.assertIsNone(result) + class TestDeleteDispatch(unittest.TestCase): def setUp(self): From 90f8bdad5d0bb36e8e25bb7ec1eef3dcd49c6337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steven=20Oh=C3=A1d?= Date: Sat, 23 May 2026 17:49:44 +0200 Subject: [PATCH 07/16] Add deletion of Appointment by keyword --- assistant.py | 28 ++++++++++++++++++++++++++++ database.py | 18 ++++++++++++++++++ locales.py | 4 ++++ 3 files changed, 50 insertions(+) diff --git a/assistant.py b/assistant.py index c710c15..aa8fb79 100644 --- a/assistant.py +++ b/assistant.py @@ -84,6 +84,22 @@ }, }, }, + { + "type": "function", + "function": { + "name": "delete_appointment", + "description": "Delete a saved appointment, found by keyword.", + "parameters": { + "type": "object", + "properties": { + "keyword": {"type": "string", "description": "Keyword from the appointment title or description"}, + "confirmed": {"type": "boolean", "description": "True only after the user confirmed deletion", + "default": False}, + }, + "required": ["keyword"], + }, + }, + }, { "type": "function", "function": { @@ -267,6 +283,18 @@ def _dispatch(fc: dict) -> str: db.delete_note(nid) return locales.get("note_deleted", title=title, nid=nid) + elif name == "delete_appointment": + keyword = args.get("keyword", "") + appointment = db.find_appointment_by_keyword(keyword) + if not appointment: + return locales.get("appointment_not_found", keyword=keyword) + if args.get("confirmed", False) is not True: + return f"__confirm_delete__:appointment:{appointment['id']}" + title = appointment["title"] + aid = appointment["id"] + db.delete_appointment(aid) + return locales.get("appointment_deleted", title=title, aid=aid) + elif name == "create_appointment": aid = db.create_appointment( title=args.get("title", ""), diff --git a/database.py b/database.py index 88bcdcb..24e3ce1 100644 --- a/database.py +++ b/database.py @@ -234,6 +234,24 @@ def get_appointments(from_dt: str | None = None, to_dt: str | None = None) -> li return [dict(r) for r in rows] +def find_appointment_by_keyword(keyword: str) -> dict | None: + """Find an appointment by fuzzy title/description match. Returns dict or None.""" + target = keyword.strip().lower() + if not target: + return None + keyword_pattern = f"%{target}%" + with _lock: + c = _conn() + row = c.execute( + "SELECT * FROM appointments" + " WHERE lower(title) LIKE ? OR lower(description) LIKE ?" + " ORDER BY dt ASC", + (keyword_pattern, keyword_pattern), + ).fetchone() + c.close() + return dict(row) if row else None + + def delete_appointment(aid: int): with _lock: c = _conn() diff --git a/locales.py b/locales.py index c208550..9f4663e 100644 --- a/locales.py +++ b/locales.py @@ -21,6 +21,8 @@ "note_not_found": "Note matching '{keyword}' not found", "note_deleted": "Note '{title}' deleted (#{nid})", "appointment_created": "Appointment created: {title} ({dt})", + "appointment_not_found": "Appointment matching '{keyword}' not found", + "appointment_deleted": "Appointment '{title}' deleted (#{aid})", "reminder_set": "Reminder set: {dt}", "unknown_command": "Unknown command: {name}", "error": "Error: {detail}", @@ -102,6 +104,8 @@ "note_not_found": "Nota con '{keyword}' non trovata", "note_deleted": "Nota '{title}' eliminata (#{nid})", "appointment_created": "Appuntamento creato: {title} ({dt})", + "appointment_not_found": "Appuntamento con '{keyword}' non trovato", + "appointment_deleted": "Appuntamento '{title}' eliminato (#{aid})", "reminder_set": "Reminder impostato: {dt}", "unknown_command": "Comando sconosciuto: {name}", "error": "Errore: {detail}", From 27cf91c0b8a732354271c5028b6ddb34882f48b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steven=20Oh=C3=A1d?= Date: Sat, 23 May 2026 17:56:02 +0200 Subject: [PATCH 08/16] Add deletion of Reminder by keyword --- assistant.py | 28 ++++++++++++++++++++++++++++ database.py | 18 ++++++++++++++++++ locales.py | 4 ++++ 3 files changed, 50 insertions(+) diff --git a/assistant.py b/assistant.py index aa8fb79..2ebc102 100644 --- a/assistant.py +++ b/assistant.py @@ -100,6 +100,22 @@ }, }, }, + { + "type": "function", + "function": { + "name": "delete_reminder", + "description": "Delete a saved reminder, found by keyword.", + "parameters": { + "type": "object", + "properties": { + "keyword": {"type": "string", "description": "Keyword from the reminder message"}, + "confirmed": {"type": "boolean", "description": "True only after the user confirmed deletion", + "default": False}, + }, + "required": ["keyword"], + }, + }, + }, { "type": "function", "function": { @@ -294,6 +310,18 @@ def _dispatch(fc: dict) -> str: aid = appointment["id"] db.delete_appointment(aid) return locales.get("appointment_deleted", title=title, aid=aid) + + elif name == "delete_reminder": + keyword = args.get("keyword", "") + reminder = db.find_reminder_by_keyword(keyword) + if not reminder: + return locales.get("reminder_not_found", keyword=keyword) + if args.get("confirmed", False) is not True: + return f"__confirm_delete__:reminder:{reminder['id']}" + message = reminder["message"] + rid = reminder["id"] + db.delete_reminder(rid) + return locales.get("reminder_deleted", message=message, rid=rid) elif name == "create_appointment": aid = db.create_appointment( diff --git a/database.py b/database.py index 24e3ce1..04f08e7 100644 --- a/database.py +++ b/database.py @@ -328,6 +328,24 @@ def get_pending_reminders() -> list[dict]: return [dict(r) for r in rows] +def find_reminder_by_keyword(keyword: str) -> dict | None: + """Find a reminder by fuzzy message match. Returns dict or None.""" + target = keyword.strip().lower() + if not target: + return None + keyword_pattern = f"%{target}%" + with _lock: + c = _conn() + row = c.execute( + "SELECT * FROM reminders" + " WHERE lower(message) LIKE ?" + " ORDER BY remind_at ASC", + (keyword_pattern,) + ).fetchone() + c.close() + return dict(row) if row else None + + def mark_reminder_notified(rid: int): with _lock: c = _conn() diff --git a/locales.py b/locales.py index 9f4663e..ab8b431 100644 --- a/locales.py +++ b/locales.py @@ -23,6 +23,8 @@ "appointment_created": "Appointment created: {title} ({dt})", "appointment_not_found": "Appointment matching '{keyword}' not found", "appointment_deleted": "Appointment '{title}' deleted (#{aid})", + "reminder_not_found": "Reminder matching '{keyword}' not found", + "reminder_deleted": "Reminder '{message}' deleted (#{rid})", "reminder_set": "Reminder set: {dt}", "unknown_command": "Unknown command: {name}", "error": "Error: {detail}", @@ -106,6 +108,8 @@ "appointment_created": "Appuntamento creato: {title} ({dt})", "appointment_not_found": "Appuntamento con '{keyword}' non trovato", "appointment_deleted": "Appuntamento '{title}' eliminato (#{aid})", + "reminder_not_found": "Reminder con '{keyword}' non trovato", + "reminder_deleted": "Reminder '{message}' eliminato (#{rid})", "reminder_set": "Reminder impostato: {dt}", "unknown_command": "Comando sconosciuto: {name}", "error": "Errore: {detail}", From 946e762786f11295de92b6b58e9a574aacba2073 Mon Sep 17 00:00:00 2001 From: LikeARealGinger Date: Mon, 25 May 2026 13:59:00 +0200 Subject: [PATCH 09/16] Add localized delete confirmation choices --- locales.py | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/locales.py b/locales.py index ab8b431..14ab74b 100644 --- a/locales.py +++ b/locales.py @@ -9,9 +9,11 @@ import config +LocaleValue = str | tuple[str, ...] + # ── String tables ───────────────────────────────────────────────────────── -_STRINGS: dict[str, dict[str, str]] = { +_STRINGS: dict[str, dict[str, LocaleValue]] = { "en": { # assistant.py — dispatch confirmations "note_saved": "Note saved (#{nid})", @@ -29,6 +31,16 @@ "unknown_command": "Unknown command: {name}", "error": "Error: {detail}", "not_understood": "I didn't understand the command", + "delete_confirmations": ( + "yes", "yeah", "yep", "yup", "sure", "ok", "okay", + "confirm", "confirmed", "please do", "do it", "go ahead", + "delete", "delete it", + ), + "delete_rejections": ( + "no", "nope", "nah", "cancel", "stop", "abort", "do not", + "don't", "dont", "keep it", "never mind", "nevermind", + "leave it", + ), # assistant.py — system prompt fragments "system_prompt": ( @@ -114,6 +126,16 @@ "unknown_command": "Comando sconosciuto: {name}", "error": "Errore: {detail}", "not_understood": "Non ho capito il comando", + "delete_confirmations": ( + "si", "sì", "certo", "certamente", "ok", "okay", + "va bene", "confermo", "conferma", "procedi", + "elimina", "eliminalo", "cancella", "cancellalo", + ), + "delete_rejections": ( + "no", "nope", "annulla", "stop", "ferma", "fermati", + "aspetta", "lascia", "lascia stare", "lascia perdere", + "non eliminare", "non cancellare", "mantieni", + ), "system_prompt": ( "You are Writher, a voice assistant for productivity. " @@ -184,17 +206,35 @@ # ── Public API ──────────────────────────────────────────────────────────── +def _lookup(key: str) -> LocaleValue: + lang = getattr(config, "LANGUAGE", _FALLBACK) + table = _STRINGS.get(lang, _STRINGS[_FALLBACK]) + return table.get(key, _STRINGS[_FALLBACK].get(key, key)) + + def get(key: str, **kwargs) -> str: """Return the localised string for *key*, formatted with *kwargs*. Falls back to English if the key is missing in the active language. """ - lang = getattr(config, "LANGUAGE", _FALLBACK) - table = _STRINGS.get(lang, _STRINGS[_FALLBACK]) - template = table.get(key, _STRINGS[_FALLBACK].get(key, key)) + template = _lookup(key) + if not isinstance(template, str): + return key if kwargs: try: return template.format(**kwargs) except (KeyError, IndexError): return template return template + + +def get_choices(key: str) -> tuple[str, ...]: + """Return the localised choice list for *key*. + + Use this for non-display locale entries, such as spoken confirmation + variants for destructive actions. + """ + choices = _lookup(key) + if isinstance(choices, tuple): + return choices + return (choices,) From ab8ef534948400c634dae36324a1be98e51fdff9 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 27 May 2026 03:13:43 +0200 Subject: [PATCH 10/16] refactor assisntant.py dispatcher for confirmation handling --- assistant.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/assistant.py b/assistant.py index 2ebc102..b5bd5de 100644 --- a/assistant.py +++ b/assistant.py @@ -292,36 +292,21 @@ def _dispatch(fc: dict) -> str: note = db.find_note_by_keyword(keyword) if not note: return locales.get("note_not_found", keyword=keyword) - if args.get("confirmed", False) is not True: - return f"__confirm_delete__:note:{note['id']}" - title = note["title"] or locales.get("default_note_title") - nid = note["id"] - db.delete_note(nid) - return locales.get("note_deleted", title=title, nid=nid) + return f"__confirm_delete__:note:{note['id']}" elif name == "delete_appointment": keyword = args.get("keyword", "") appointment = db.find_appointment_by_keyword(keyword) if not appointment: return locales.get("appointment_not_found", keyword=keyword) - if args.get("confirmed", False) is not True: - return f"__confirm_delete__:appointment:{appointment['id']}" - title = appointment["title"] - aid = appointment["id"] - db.delete_appointment(aid) - return locales.get("appointment_deleted", title=title, aid=aid) + return f"__confirm_delete__:appointment:{appointment['id']}" elif name == "delete_reminder": keyword = args.get("keyword", "") reminder = db.find_reminder_by_keyword(keyword) if not reminder: return locales.get("reminder_not_found", keyword=keyword) - if args.get("confirmed", False) is not True: - return f"__confirm_delete__:reminder:{reminder['id']}" - message = reminder["message"] - rid = reminder["id"] - db.delete_reminder(rid) - return locales.get("reminder_deleted", message=message, rid=rid) + return f"__confirm_delete__:reminder:{reminder['id']}" elif name == "create_appointment": aid = db.create_appointment( From 03f24027c19d53d64340b01761618330e9faf327 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 27 May 2026 03:17:31 +0200 Subject: [PATCH 11/16] add further dispatch confirmations (eng, ita) --- locales.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/locales.py b/locales.py index ab8b431..d3d70a3 100644 --- a/locales.py +++ b/locales.py @@ -26,6 +26,14 @@ "reminder_not_found": "Reminder matching '{keyword}' not found", "reminder_deleted": "Reminder '{message}' deleted (#{rid})", "reminder_set": "Reminder set: {dt}", + "delete_confirm_prompt": "Say yes within {seconds}s to delete this {item}", + "delete_confirm_repeat": "Please say yes or no ({seconds}s left)", + "delete_confirm_timeout": "Delete confirmation timed out", + "delete_cancelled": "Delete cancelled", + "delete_item_missing": "{item} was not found", + "delete_item_note": "note", + "delete_item_appointment": "appointment", + "delete_item_reminder": "reminder", "unknown_command": "Unknown command: {name}", "error": "Error: {detail}", "not_understood": "I didn't understand the command", @@ -111,6 +119,14 @@ "reminder_not_found": "Reminder con '{keyword}' non trovato", "reminder_deleted": "Reminder '{message}' eliminato (#{rid})", "reminder_set": "Reminder impostato: {dt}", + "delete_confirm_prompt": "Di' si entro {seconds}s per eliminare questo {item}", + "delete_confirm_repeat": "Di' si o no ({seconds}s rimasti)", + "delete_confirm_timeout": "Conferma eliminazione scaduta", + "delete_cancelled": "Eliminazione annullata", + "delete_item_missing": "{item} non trovato", + "delete_item_note": "nota", + "delete_item_appointment": "appuntamento", + "delete_item_reminder": "reminder", "unknown_command": "Comando sconosciuto: {name}", "error": "Errore: {detail}", "not_understood": "Non ho capito il comando", From 785ddbdbdb1a1b77eb812c2e68de93a0d9afd167 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 27 May 2026 03:22:54 +0200 Subject: [PATCH 12/16] implement logic for pending delete handling; add handling of delete to _assistant_worker() --- main.py | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index fe4d941..894b7e0 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ import ctypes import locale import queue +import re import threading import time import tkinter as tk @@ -53,6 +54,9 @@ _rec_start = 0.0 _MIN_DURATION = 0.5 +_DELETE_CONFIRM_SECONDS = 5.0 +_DELETE_CONFIRM_TOKEN = re.compile(r"^__confirm_delete__:(note|appointment|reminder):(\d+)$") +_pending_delete = None # Toggle-mode timeout timers _dict_timeout_timer = None @@ -236,6 +240,111 @@ def _dictation_worker(): widget.hide() +def _parse_delete_token(result: str): + m = _DELETE_CONFIRM_TOKEN.match(result or "") + if not m: + return None + return m.group(1), int(m.group(2)) + + +def _set_pending_delete(kind: str, item_id: int): + global _pending_delete + _pending_delete = { + "kind": kind, + "id": item_id, + "expires_at": time.monotonic() + _DELETE_CONFIRM_SECONDS, + } + log.info("Pending delete set: %s #%d (expires in %.1fs)", kind, item_id, _DELETE_CONFIRM_SECONDS) + + +def _clear_pending_delete(): + global _pending_delete + _pending_delete = None + + +def _is_affirmative(text: str) -> bool: + t = (text or "").strip().lower() + if not t: + return False + return bool(re.search(r"\b(yes|yeah|yep|ok|okay|confirm|si|sì|certo|confermo)\b", t)) + + +def _is_negative(text: str) -> bool: + t = (text or "").strip().lower() + if not t: + return False + return bool(re.search(r"\b(no|nope|cancel|stop|annulla|annullare)\b", t)) + + +def _refresh_notes_window_if_open(): + if notes_win and notes_win._win and notes_win._win.winfo_exists(): + root.after(0, notes_win._refresh) + + +def _delete_by_pending(kind: str, item_id: int) -> str: + if kind == "note": + title = locales.get("default_note_title") + for note in db.get_all_notes(): + if note["id"] == item_id: + title = note["title"] or title + break + else: + return locales.get("delete_item_missing", item=locales.get("delete_item_note")) + db.delete_note(item_id) + _refresh_notes_window_if_open() + return locales.get("note_deleted", title=title, nid=item_id) + + if kind == "appointment": + title = None + for appointment in db.get_appointments(): + if appointment["id"] == item_id: + title = appointment["title"] + break + if title is None: + return locales.get("delete_item_missing", item=locales.get("delete_item_appointment")) + db.delete_appointment(item_id) + _refresh_notes_window_if_open() + return locales.get("appointment_deleted", title=title, aid=item_id) + + if kind == "reminder": + message = None + for reminder in db.get_all_reminders(include_notified=True): + if reminder["id"] == item_id: + message = reminder["message"] + break + if message is None: + return locales.get("delete_item_missing", item=locales.get("delete_item_reminder")) + db.delete_reminder(item_id) + _refresh_notes_window_if_open() + return locales.get("reminder_deleted", message=message, rid=item_id) + + return locales.get("error", detail=f"Unsupported delete kind: {kind}") + + +def _handle_pending_delete_confirmation(text: str): + if _pending_delete is None: + return None + + now = time.monotonic() + if now > _pending_delete["expires_at"]: + log.info("Pending delete expired.") + _clear_pending_delete() + return "__delete_confirm_timeout__" + + if _is_affirmative(text): + kind = _pending_delete["kind"] + item_id = _pending_delete["id"] + _clear_pending_delete() + return _delete_by_pending(kind, item_id) + + if _is_negative(text): + _clear_pending_delete() + return "__delete_cancelled__" + + remaining = int(max(1, _pending_delete["expires_at"] - now)) + return f"__delete_confirm_repeat__:{remaining}" + + def _assistant_worker(): """Transcribe audio, send to Ollama, and execute the returned action.""" while True: @@ -252,11 +361,42 @@ def _assistant_worker(): continue log.info("Assistant heard: %r", text) - result = assistant.process(text) + result = _handle_pending_delete_confirmation(text) + if result is None: + result = assistant.process(text) log.info("Assistant result: %s", result) + token = _parse_delete_token(result) + if token: + kind, item_id = token + _set_pending_delete(kind, item_id) + if widget: + widget.set_expression("listening") + widget.show_message( + locales.get( + "delete_confirm_prompt", + item=locales.get(f"delete_item_{kind}"), + seconds=int(_DELETE_CONFIRM_SECONDS), + ), + 2200, + ) + continue + # Handle special show commands - if result == "__show_notes__": + if result == "__delete_confirm_timeout__": + if widget: + widget.set_expression("sad") + widget.show_message(locales.get("delete_confirm_timeout"), 2200) + elif result == "__delete_cancelled__": + if widget: + widget.set_expression("sad") + widget.show_message(locales.get("delete_cancelled"), 2200) + elif result.startswith("__delete_confirm_repeat__:"): + remaining = int(result.rsplit(":", 1)[1]) + if widget: + widget.set_expression("listening") + widget.show_message(locales.get("delete_confirm_repeat", seconds=remaining), 2200) + elif result == "__show_notes__": if notes_win: root.after(0, lambda: notes_win.show("notes")) if widget: From 0681f9752ab27a8b6173dc66c95c9189f7711f98 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 27 May 2026 03:24:09 +0200 Subject: [PATCH 13/16] add further unit tests for coverage --- test_delete.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test_delete.py b/test_delete.py index 24853fd..8d1e1b1 100644 --- a/test_delete.py +++ b/test_delete.py @@ -82,6 +82,12 @@ def test_delete_note_returns_confirmation(self): result = self._dispatch({"function": "delete_note", "arguments": {"keyword": NOTE_KEYWORD}}) self.assertTrue(result.startswith("__confirm_delete__:note:")) + + def test_delete_note_ignores_confirmed_flag_and_still_requires_confirmation(self): + self.db.save_note(NOTE_CONTENT, title=NOTE_TITLE) + result = self._dispatch({"function": "delete_note", + "arguments": {"keyword": NOTE_KEYWORD, "confirmed": True}}) + self.assertTrue(result.startswith("__confirm_delete__:note:")) def test_delete_appointment_returns_confirmation(self): self.db.create_appointment(APPOINTMENT_TITLE, APPOINTMENT_DT) @@ -89,12 +95,24 @@ def test_delete_appointment_returns_confirmation(self): "arguments": {"keyword": APPOINTMENT_KEYWORD}}) self.assertTrue(result.startswith("__confirm_delete__:appointment:")) + def test_delete_appointment_ignores_confirmed_flag_and_still_requires_confirmation(self): + self.db.create_appointment(APPOINTMENT_TITLE, APPOINTMENT_DT) + result = self._dispatch({"function": "delete_appointment", + "arguments": {"keyword": APPOINTMENT_KEYWORD, "confirmed": True}}) + self.assertTrue(result.startswith("__confirm_delete__:appointment:")) + def test_delete_reminder_returns_confirmation(self): self.db.set_reminder(REMINDER_MESSAGE, REMINDER_DT) result = self._dispatch({"function": "delete_reminder", "arguments": {"keyword": REMINDER_KEYWORD}}) self.assertTrue(result.startswith("__confirm_delete__:reminder:")) + def test_delete_reminder_ignores_confirmed_flag_and_still_requires_confirmation(self): + self.db.set_reminder(REMINDER_MESSAGE, REMINDER_DT) + result = self._dispatch({"function": "delete_reminder", + "arguments": {"keyword": REMINDER_KEYWORD, "confirmed": True}}) + self.assertTrue(result.startswith("__confirm_delete__:reminder:")) + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From 4524e9a34f8fb52bab7a118d65ed392dfc606e71 Mon Sep 17 00:00:00 2001 From: LikeARealGinger Date: Wed, 27 May 2026 16:07:41 +0200 Subject: [PATCH 14/16] Use localized delete confirmation choices --- main.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index 894b7e0..f77975f 100644 --- a/main.py +++ b/main.py @@ -262,18 +262,30 @@ def _clear_pending_delete(): _pending_delete = None -def _is_affirmative(text: str) -> bool: - t = (text or "").strip().lower() +def _matches_locale_choice(text: str, key: str) -> bool: + t = " ".join((text or "").strip().lower().split()) if not t: return False - return bool(re.search(r"\b(yes|yeah|yep|ok|okay|confirm|si|sì|certo|confermo)\b", t)) + + choices = [ + " ".join(choice.strip().lower().split()) + for choice in locales.get_choices(key) + if choice.strip() + ] + if not choices: + return False + + alternatives = "|".join(re.escape(choice) for choice in choices) + pattern = rf"(? bool: + return _matches_locale_choice(text, "delete_confirmations") def _is_negative(text: str) -> bool: - t = (text or "").strip().lower() - if not t: - return False - return bool(re.search(r"\b(no|nope|cancel|stop|annulla|annullare)\b", t)) + return _matches_locale_choice(text, "delete_rejections") def _refresh_notes_window_if_open(): From b3dc810e92cc3a284a9eaca8d22ce2658181a78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steven=20Oh=C3=A1d?= Date: Wed, 27 May 2026 21:25:04 +0200 Subject: [PATCH 15/16] Creat_Pop-Up_delete_confirm --- locales.py | 18 +++ main.py | 48 +++++++- notes_window.py | 295 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 353 insertions(+), 8 deletions(-) diff --git a/locales.py b/locales.py index 7773cd4..22f6f02 100644 --- a/locales.py +++ b/locales.py @@ -36,6 +36,15 @@ "delete_item_note": "note", "delete_item_appointment": "appointment", "delete_item_reminder": "reminder", + "confirm_delete_title": "Confirm delete {item}", + "field_name": "Name", + "field_created": "Created", + "field_event": "Event", + "field_remind": "Remind At", + "confirm_delete_warning": "This action cannot be undone.", + "btn_cancel": "Cancel", + "btn_delete": "Delete", + "listening_for_confirm": "Listening for voice confirmation...", "unknown_command": "Unknown command: {name}", "error": "Error: {detail}", "not_understood": "I didn't understand the command", @@ -139,6 +148,15 @@ "delete_item_note": "nota", "delete_item_appointment": "appuntamento", "delete_item_reminder": "reminder", + "confirm_delete_title": "Conferma eliminazione {item}", + "field_name": "Nome", + "field_created": "Creato", + "field_event": "Evento", + "field_remind": "Ricorda il", + "confirm_delete_warning": "Questa azione non può essere annullata.", + "btn_cancel": "Annulla", + "btn_delete": "Elimina", + "listening_for_confirm": "In ascolto della conferma vocale...", "unknown_command": "Comando sconosciuto: {name}", "error": "Errore: {detail}", "not_understood": "Non ho capito il comando", diff --git a/main.py b/main.py index f77975f..b1175f2 100644 --- a/main.py +++ b/main.py @@ -54,7 +54,7 @@ _rec_start = 0.0 _MIN_DURATION = 0.5 -_DELETE_CONFIRM_SECONDS = 5.0 +_DELETE_CONFIRM_SECONDS = 10.0 _DELETE_CONFIRM_TOKEN = re.compile(r"^__confirm_delete__:(note|appointment|reminder):(\d+)$") _pending_delete = None @@ -246,6 +246,10 @@ def _parse_delete_token(result: str): return None return m.group(1), int(m.group(2)) +def _run_on_ui_thread(func): + """Schedule a Tk/CustomTkinter operation on the Tk main thread.""" + if root: + root.after(0, func) def _set_pending_delete(kind: str, item_id: int): global _pending_delete @@ -254,13 +258,50 @@ def _set_pending_delete(kind: str, item_id: int): "id": item_id, "expires_at": time.monotonic() + _DELETE_CONFIRM_SECONDS, } - log.info("Pending delete set: %s #%d (expires in %.1fs)", kind, item_id, _DELETE_CONFIRM_SECONDS) + def _open_dialog(): + try: + if not notes_win: + return + + if not (notes_win._win and notes_win._win.winfo_exists()): + notes_win.show("notes") + + notes_win.show_voice_delete_confirmation(kind, item_id) + log.info("Pending delete dialog opened: %s #%d", kind, item_id) + + except Exception as exc: + log.error("Could not open voice delete dialog: %s", exc) + + _run_on_ui_thread(_open_dialog) + + log.info( + "Pending delete set: %s #%d (expires in %.1fs)", + kind, + item_id, + _DELETE_CONFIRM_SECONDS, + ) def _clear_pending_delete(): global _pending_delete _pending_delete = None +def _close_voice_delete_dialog(): + """Close/hide the voice delete dialog on the Tk main thread.""" + if not root: + return + + def _close(): + try: + if notes_win and notes_win._voice_delete_dialog: + dialog = notes_win._voice_delete_dialog + notes_win._voice_delete_dialog = None + notes_win._safe_destroy_dialog(dialog) + except Exception as exc: + log.error("Could not close voice delete dialog: %s", exc) + + root.after(0, _close) + def _matches_locale_choice(text: str, key: str) -> bool: t = " ".join((text or "").strip().lower().split()) @@ -340,16 +381,19 @@ def _handle_pending_delete_confirmation(text: str): now = time.monotonic() if now > _pending_delete["expires_at"]: log.info("Pending delete expired.") + _close_voice_delete_dialog() _clear_pending_delete() return "__delete_confirm_timeout__" if _is_affirmative(text): kind = _pending_delete["kind"] item_id = _pending_delete["id"] + _close_voice_delete_dialog() _clear_pending_delete() return _delete_by_pending(kind, item_id) if _is_negative(text): + _close_voice_delete_dialog() _clear_pending_delete() return "__delete_cancelled__" diff --git a/notes_window.py b/notes_window.py index 39654d9..512c262 100644 --- a/notes_window.py +++ b/notes_window.py @@ -54,6 +54,7 @@ def __init__(self, root: tk.Tk): self._title_eye_tk = None self._tab_buttons = {} self._scroll_frame = None + self._voice_delete_dialog = None def show(self, tab: str = "notes"): if self._win is not None: @@ -248,6 +249,11 @@ class RECT(ctypes.Structure): def _close(self): if self._win: + try: + if self._voice_delete_dialog and self._voice_delete_dialog.winfo_exists(): + self._safe_destroy_dialog(self._voice_delete_dialog) + except Exception: + pass try: self._win.destroy() except Exception: @@ -256,6 +262,31 @@ def _close(self): self._maximized = False self._tab_buttons = {} + def _safe_destroy_dialog(self, dialog): + """Hide a CustomTkinter dialog safely. + + Important: + We do NOT destroy this dialog here, because CustomTkinter can still have + pending draw callbacks for CTkButton canvases. + """ + if not dialog: + return + + try: + if not dialog.winfo_exists(): + return + except Exception: + return + + try: + dialog.grab_release() + except Exception: + pass + + try: + dialog.withdraw() + except Exception: + pass # ── Tabs ────────────────────────────────────────────────────────────── def _switch_tab(self, tab: str): @@ -386,10 +417,252 @@ def _toggle_item(self, note_id: int, item_text: str): db.check_item(note_id, item_text) self._refresh() - def _delete_note(self, note_id: int): - db.delete_note(note_id) + # ── Delete Confirmation Dialog ──────────────────────────────────────── + + def _show_delete_confirmation_dialog(self, item_type: str, item_id: int, item_data: dict, voice_mode: bool = False, on_voice_confirm=None): + """Show a modal confirmation dialog before deleting an item. + + Args: + item_type: "note", "appointment", or "reminder" + item_id: ID of the item to delete + item_data: Dictionary with item information + voice_mode: If True, dialog is waiting for voice confirmation + on_voice_confirm: Callback function for voice confirmation + """ + # Create modal dialog window, falling back to root if main window is not yet available + parent = self._win if self._win and self._win.winfo_exists() else self._root + dialog = ctk.CTkToplevel(parent) + dialog.overrideredirect(True) + dialog.attributes("-topmost", True) + dialog.resizable(False, False) + + # Configure style + dialog.configure(fg_color=T.BG_DEEP) + + # Dialog size and position (centered over main window) + dialog_w, dialog_h = 400, 300 if not voice_mode else 320 + try: + mx = self._win.winfo_x() + self._win.winfo_width() // 2 + my = self._win.winfo_y() + self._win.winfo_height() // 2 + except: + mx = self._win.winfo_screenwidth() // 2 + my = self._win.winfo_screenheight() // 2 + + dialog.geometry(f"{dialog_w}x{dialog_h}+{mx - dialog_w//2}+{my - dialog_h//2}") + + # Outer border frame + outer = ctk.CTkFrame(dialog, fg_color=T.BG_DEEP, border_color=T.BORDER, + border_width=1, corner_radius=8) + outer.pack(fill="both", expand=True, padx=0, pady=0) + + # Title + title_map = { + "note": locales.get("delete_item_note"), + "appointment": locales.get("delete_item_appointment"), + "reminder": locales.get("delete_item_reminder"), + } + title_text = locales.get("confirm_delete_title", item=title_map.get(item_type, item_type)) + + title_lbl = ctk.CTkLabel( + outer, text=title_text, font=(T.FONT_FAMILY, 14, "bold"), + text_color=T.FG + ) + title_lbl.pack(pady=(16, 12), padx=16) + + # Content frame + content = ctk.CTkFrame(outer, fg_color="transparent") + content.pack(fill="both", expand=True, padx=16, pady=12) + + # Build info lines + info_lines = [] + + # Name + if item_type == "note": + name = item_data.get("title") or locales.get("default_note_title") + info_lines.append((locales.get("field_name", default="Name"), name)) + elif item_type == "appointment": + name = item_data.get("title", "") + info_lines.append((locales.get("field_name", default="Name"), name)) + elif item_type == "reminder": + name = item_data.get("message", "") + info_lines.append((locales.get("field_name", default="Name"), name)) + + # Created date + created_at = item_data.get("created_at", "") + if created_at: + try: + created_dt = datetime.fromisoformat(created_at) + created_str = _format_dt_os(created_dt) + except: + created_str = created_at + info_lines.append((locales.get("field_created", default="Created"), created_str)) + + # Event date (for appointment and reminder) + if item_type == "appointment": + event_at = item_data.get("dt", "") + if event_at: + try: + event_dt = datetime.fromisoformat(event_at) + event_str = _format_dt_os(event_dt) + except: + event_str = event_at + info_lines.append((locales.get("field_event", default="Event"), event_str)) + elif item_type == "reminder": + remind_at = item_data.get("remind_at", "") + if remind_at: + try: + remind_dt = datetime.fromisoformat(remind_at) + remind_str = _format_dt_os(remind_dt) + except: + remind_str = remind_at + info_lines.append((locales.get("field_remind", default="Remind At"), remind_str)) + + # Display info lines + for label, value in info_lines: + row = ctk.CTkFrame(content, fg_color="transparent") + row.pack(fill="x", pady=4) + + lbl = ctk.CTkLabel(row, text=f"{label}:", font=T.FONT_SMALL, + text_color=T.FG_DIM, width=80, anchor="w") + lbl.pack(side="left", padx=(0, 8)) + + val = ctk.CTkLabel(row, text=str(value), font=T.FONT_BODY, + text_color=T.FG, anchor="w", wraplength=250) + val.pack(side="left", fill="x", expand=True) + + # Voice mode indicator + if voice_mode: + voice_lbl = ctk.CTkLabel( + content, text="🎤 " + locales.get("listening_for_confirm", default="Listening for voice confirmation..."), + font=T.FONT_SMALL, text_color=T.ACCENT, wraplength=350 + ) + voice_lbl.pack(pady=(8, 0)) + + # Warning + warning = ctk.CTkLabel( + content, text=locales.get("confirm_delete_warning", default="This action cannot be undone."), + font=T.FONT_SMALL, text_color=T.RED, wraplength=350 + ) + warning.pack(pady=(8, 0)) + + # Button frame + btn_frame = ctk.CTkFrame(outer, fg_color="transparent") + btn_frame.pack(fill="x", padx=16, pady=12) + + # Cancel button + def close_dialog(): + if on_voice_confirm: + on_voice_confirm(False) # Signal cancellation + else: + self._safe_destroy_dialog(dialog) + cancel_btn = tk.Button( + btn_frame, + text=locales.get("btn_cancel", default="Cancel"), + bg=T.BG, + fg=T.FG, + activebackground=T.BG_HOVER, + activeforeground=T.FG, + relief="flat", + bd=0, + font=T.FONT_BODY, + command=close_dialog, + ) + cancel_btn.pack(side="left", fill="x", expand=True, padx=(0, 8)) + + # Delete button + def confirm_delete(): + if on_voice_confirm: + on_voice_confirm(True) # Signal confirmation + else: + self._safe_destroy_dialog(dialog) + self._confirm_delete(item_type, item_id) + + delete_btn = tk.Button( + btn_frame, + text=locales.get("btn_delete", default="Delete"), + bg=T.RED, + fg=T.FG, + activebackground=T.RED_HOVER, + activeforeground=T.FG, + relief="flat", + bd=0, + font=T.FONT_BODY, + command=confirm_delete, + ) + delete_btn.pack(side="left", fill="x", expand=True, padx=(8, 0)) + + # Make dialog modal + dialog.grab_set() + dialog.focus() + + # Store reference for voice mode + if voice_mode: + self._voice_delete_dialog = dialog + + def show_voice_delete_confirmation(self, item_type: str, item_id: int): + """Show delete confirmation dialog in voice mode (called from main.py). + + Returns callback function for voice confirmation. + """ + # Get item data + item_data = None + + if item_type == "note": + for note in db.get_all_notes(): + if note["id"] == item_id: + item_data = note + break + elif item_type == "appointment": + for appt in db.get_appointments(): + if appt["id"] == item_id: + item_data = appt + break + elif item_type == "reminder": + for rem in db.get_all_reminders(include_notified=True): + if rem["id"] == item_id: + item_data = rem + break + + if not item_data: + return None + + def on_voice_confirm(confirmed: bool): + dialog = self._voice_delete_dialog + self._voice_delete_dialog = None + + if dialog: + self._safe_destroy_dialog(dialog) + + if confirmed: + self._confirm_delete(item_type, item_id) + + self._show_delete_confirmation_dialog( + item_type, item_id, item_data, + voice_mode=True, + on_voice_confirm=on_voice_confirm + ) + + return on_voice_confirm + + def _confirm_delete(self, item_type: str, item_id: int): + """Execute the deletion after confirmation.""" + if item_type == "note": + db.delete_note(item_id) + elif item_type == "appointment": + db.delete_appointment(item_id) + elif item_type == "reminder": + db.delete_reminder(item_id) self._refresh() + def _delete_note(self, note_id: int): + note = None + for n in db.get_all_notes(): + if n["id"] == note_id: + note = n + break + if note: + self._show_delete_confirmation_dialog("note", note_id, note) + # ── Appointments ────────────────────────────────────────────────────── def _populate_appointments(self): @@ -427,8 +700,13 @@ def _populate_appointments(self): justify="left").pack(fill="x", pady=(T.PAD_S, 0)) def _delete_appt(self, aid: int): - db.delete_appointment(aid) - self._refresh() + appointment = None + for a in db.get_appointments(): + if a["id"] == aid: + appointment = a + break + if appointment: + self._show_delete_confirmation_dialog("appointment", aid, appointment) # ── Reminders ───────────────────────────────────────────────────────── @@ -465,5 +743,10 @@ def _populate_reminders(self): pady=(T.PAD_M, 0)) def _delete_rem(self, rid: int): - db.delete_reminder(rid) - self._refresh() + reminder = None + for r in db.get_all_reminders(include_notified=True): + if r["id"] == rid: + reminder = r + break + if reminder: + self._show_delete_confirmation_dialog("reminder", rid, reminder) From 96c85f9ad0e9eb642ac6bb452cbd9d31c4a9a93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steven=20Oh=C3=A1d?= Date: Wed, 27 May 2026 21:35:05 +0200 Subject: [PATCH 16/16] move delet_note for overview --- main.py | 2 +- notes_window.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index b1175f2..881f423 100644 --- a/main.py +++ b/main.py @@ -54,7 +54,7 @@ _rec_start = 0.0 _MIN_DURATION = 0.5 -_DELETE_CONFIRM_SECONDS = 10.0 +_DELETE_CONFIRM_SECONDS = 15.0 _DELETE_CONFIRM_TOKEN = re.compile(r"^__confirm_delete__:(note|appointment|reminder):(\d+)$") _pending_delete = None diff --git a/notes_window.py b/notes_window.py index 512c262..fcb36ea 100644 --- a/notes_window.py +++ b/notes_window.py @@ -417,6 +417,15 @@ def _toggle_item(self, note_id: int, item_text: str): db.check_item(note_id, item_text) self._refresh() + def _delete_note(self, note_id: int): + note = None + for n in db.get_all_notes(): + if n["id"] == note_id: + note = n + break + if note: + self._show_delete_confirmation_dialog("note", note_id, note) + # ── Delete Confirmation Dialog ──────────────────────────────────────── def _show_delete_confirmation_dialog(self, item_type: str, item_id: int, item_data: dict, voice_mode: bool = False, on_voice_confirm=None): @@ -654,15 +663,6 @@ def _confirm_delete(self, item_type: str, item_id: int): db.delete_reminder(item_id) self._refresh() - def _delete_note(self, note_id: int): - note = None - for n in db.get_all_notes(): - if n["id"] == note_id: - note = n - break - if note: - self._show_delete_confirmation_dialog("note", note_id, note) - # ── Appointments ────────────────────────────────────────────────────── def _populate_appointments(self):