diff --git a/i38.sh b/i38.sh index d50b108..1e1768a 100755 --- a/i38.sh +++ b/i38.sh @@ -591,6 +591,9 @@ mode "panel" { # System information bound to s bindsym s exec --no-startup-id ${i3Path}/scripts/sysinfo.sh, mode "default" + # Simple notes system bound to n + bindsym n exec --no-startup-id ${i3Path}/scripts/notes.py, mode "default" + # Exit panel mode without any action bindsym Escape mode "default" bindsym Control+g mode "default" diff --git a/scripts/notes.py b/scripts/notes.py new file mode 100755 index 0000000..ab3ac32 --- /dev/null +++ b/scripts/notes.py @@ -0,0 +1,823 @@ +#!/usr/bin/env python3 +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GLib, Gio, Gdk +import os +import datetime +import sqlite3 +from pathlib import Path + +class JotApp(Gtk.Application): + def __init__(self): + super().__init__(application_id="com.example.jot", + flags=Gio.ApplicationFlags.FLAGS_NONE) + + # Set up data structures + self.dbPath = self.get_db_path() + self.conn = None + self.init_database() + + # Initialize settings + self.expirationDays = self.get_setting("expirationDays", 0) + self.confirmDelete = self.get_setting("confirmDelete", 1) + + def do_activate(self): + # Create main window when app is activated + self.window = Gtk.ApplicationWindow(application=self, title="I38 Notes") + self.window.set_default_size(500, 400) + + # Connect the delete-event signal (for window close button) + self.window.connect("delete-event", self.on_window_close) + + # Set up keyboard shortcuts + self.setup_actions() + + # Build the main interface + self.build_ui() + + # Check for expired notes + self.check_expirations() + + self.window.show_all() + + def on_window_close(self, window, event): + """Handle window close event""" + # Close the database connection before quitting + if self.conn: + self.conn.close() + self.quit() + return True + + def get_db_path(self): + """Get path to the SQLite database""" + configHome = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) + configDir = os.path.join(configHome, 'stormux', 'I38') + os.makedirs(configDir, exist_ok=True) + return os.path.join(configDir, 'notes.sqlite') + + def init_database(self): + """Initialize the SQLite database""" + try: + self.conn = sqlite3.connect(self.dbPath) + cursor = self.conn.cursor() + + # Create notes table if it doesn't exist + cursor.execute(''' + CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text TEXT NOT NULL, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires TIMESTAMP NULL, + locked BOOLEAN DEFAULT 0 + ) + ''') + + # Create settings table if it doesn't exist + cursor.execute(''' + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + ''') + + self.conn.commit() + + # Initialize default settings if they don't exist + self.init_default_settings() + + # Check if we need to migrate from old format + self.migrate_if_needed() + + except sqlite3.Error as e: + print(f"Database error: {e}") + + def init_default_settings(self): + """Initialize default settings if they don't exist""" + defaultSettings = { + "expirationDays": "0", + "confirmDelete": "1" # 1 = enabled, 0 = disabled + } + + cursor = self.conn.cursor() + for key, value in defaultSettings.items(): + cursor.execute( + "INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", + (key, value) + ) + self.conn.commit() + + def migrate_if_needed(self): + """Check if we need to migrate from old format""" + # Check for old config directory + oldConfigHome = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) + oldConfigDir = os.path.join(oldConfigHome, 'jot') + oldNotesFile = os.path.join(oldConfigDir, 'notes') + + if os.path.exists(oldNotesFile): + try: + # Check if we already have notes in the database + cursor = self.conn.cursor() + cursor.execute("SELECT COUNT(*) FROM notes") + count = cursor.fetchone()[0] + + # Only migrate if database is empty + if count == 0: + with open(oldNotesFile, 'r') as f: + for line in f: + parts = line.strip().split(': ', 1) + if len(parts) == 2: + noteNum, noteText = parts + cursor.execute( + "INSERT INTO notes (text, locked) VALUES (?, ?)", + (noteText, 0) + ) + + self.conn.commit() + print(f"Migrated notes from {oldNotesFile}") + except Exception as e: + print(f"Migration error: {e}") + + def get_setting(self, key, default=None): + """Get a setting from the database""" + try: + cursor = self.conn.cursor() + cursor.execute("SELECT value FROM settings WHERE key = ?", (key,)) + result = cursor.fetchone() + + if result: + return result[0] + else: + # Set default if not exists + self.save_setting(key, default) + return default + except sqlite3.Error: + return default + + def save_setting(self, key, value): + """Save a setting to the database""" + try: + cursor = self.conn.cursor() + cursor.execute( + "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", + (key, value) + ) + self.conn.commit() + except sqlite3.Error as e: + print(f"Settings error: {e}") + + def check_expirations(self): + """Check and remove expired notes""" + try: + cursor = self.conn.cursor() + now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Find expired notes that aren't locked + cursor.execute( + "SELECT id, text FROM notes WHERE expires IS NOT NULL AND expires < ? AND locked = 0", + (now,) + ) + expired = cursor.fetchall() + + if expired: + # Delete expired notes + cursor.execute( + "DELETE FROM notes WHERE expires IS NOT NULL AND expires < ? AND locked = 0", + (now,) + ) + self.conn.commit() + + expiredCount = len(expired) + if expiredCount > 0: + self.show_status_message(f"Removed {expiredCount} expired notes") + # Refresh notes list + self.populate_notes() + except sqlite3.Error as e: + print(f"Expiration check error: {e}") + + def build_ui(self): + """Build the main user interface with tabs""" + # Create notebook (tabbed interface) + self.notebook = Gtk.Notebook() + self.notebook.set_tab_pos(Gtk.PositionType.TOP) + + # Make tabs keyboard navigable + self.notebook.set_can_focus(True) + + self.window.add(self.notebook) + + # Build notes tab + notesTab = self.build_notes_tab() + notesTabLabel = Gtk.Label(label="Notes") + self.notebook.append_page(notesTab, notesTabLabel) + self.notebook.set_tab_reorderable(notesTab, False) + + # Build settings tab + settingsTab = self.build_settings_tab() + settingsTabLabel = Gtk.Label(label="Settings") + self.notebook.append_page(settingsTab, settingsTabLabel) + self.notebook.set_tab_reorderable(settingsTab, False) + + # Connect tab change signal + self.notebook.connect("switch-page", self.on_tab_switched) + + def build_notes_tab(self): + """Build the notes tab""" + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + vbox.set_border_width(10) + + # Notes list with scrolling + scrolled = Gtk.ScrolledWindow() + scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + + # Create list store and view + self.notesStore = Gtk.ListStore(int, str, bool, str) # id, text, locked, expiry + self.notesView = Gtk.TreeView(model=self.notesStore) + self.notesView.set_activate_on_single_click(False) + self.notesView.connect("row-activated", self.on_row_activated) + + # Improve keyboard navigation in the tree view + self.notesView.set_can_focus(True) + self.notesView.set_headers_clickable(True) + self.notesView.set_enable_search(True) + self.notesView.set_search_column(1) # Search by note text + + # Add columns with renderers + self.add_columns() + + # Populate the list + self.populate_notes() + + scrolled.add(self.notesView) + vbox.pack_start(scrolled, True, True, 0) + + # Action buttons + actionBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + + # Copy button + copyButton = Gtk.Button.new_with_label("Copy to Clipboard") + copyButton.connect("clicked", self.on_copy_clicked) + copyButton.set_can_focus(True) + actionBox.pack_start(copyButton, False, False, 0) + + # Delete button + deleteButton = Gtk.Button.new_with_label("Delete Note") + deleteButton.connect("clicked", self.on_delete_button_clicked) + deleteButton.set_can_focus(True) + actionBox.pack_start(deleteButton, False, False, 0) + + vbox.pack_start(actionBox, False, False, 0) + + # Entry for adding new notes + entryBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + self.newNoteEntry = Gtk.Entry() + self.newNoteEntry.set_placeholder_text("Type a new note and press Enter") + self.newNoteEntry.connect("activate", self.on_entry_activate) + self.newNoteEntry.set_can_focus(True) + entryBox.pack_start(self.newNoteEntry, True, True, 0) + + # Add button + addButton = Gtk.Button.new_with_label("Add Note") + addButton.connect("clicked", self.on_add_clicked) + addButton.set_can_focus(True) + entryBox.pack_start(addButton, False, False, 0) + + vbox.pack_start(entryBox, False, False, 0) + + # Status bar + self.statusbar = Gtk.Statusbar() + self.statusbarCtx = self.statusbar.get_context_id("jot") + vbox.pack_start(self.statusbar, False, False, 0) + + return vbox + + def build_settings_tab(self): + """Build the settings tab""" + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + vbox.set_border_width(20) + + # Create a frame for expiration settings + expiryFrame = Gtk.Frame(label="Note Expiration") + vbox.pack_start(expiryFrame, False, False, 0) + + # Container for frame content + expiryBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + expiryBox.set_border_width(10) + expiryFrame.add(expiryBox) + + # Note expiration setting + # First radio button for "Never expire" + self.neverExpireRadio = Gtk.RadioButton.new_with_label_from_widget(None, "Never expire notes") + self.neverExpireRadio.set_can_focus(True) + expiryBox.pack_start(self.neverExpireRadio, False, False, 0) + + # Container for expiration days selection + expireDaysBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + + # Radio button for custom expiration + self.customExpireRadio = Gtk.RadioButton.new_with_label_from_widget( + self.neverExpireRadio, + "Expire notes after" + ) + self.customExpireRadio.set_can_focus(True) + expireDaysBox.pack_start(self.customExpireRadio, False, False, 0) + + # Spin button for days + adjustment = Gtk.Adjustment( + value=max(1, int(self.expirationDays)) if int(self.expirationDays) > 0 else 7, + lower=1, + upper=30, + step_increment=1 + ) + self.daysSpinButton = Gtk.SpinButton() + self.daysSpinButton.set_adjustment(adjustment) + self.daysSpinButton.set_can_focus(True) + expireDaysBox.pack_start(self.daysSpinButton, False, False, 0) + + # Label for "days" + daysLabel = Gtk.Label(label="days") + expireDaysBox.pack_start(daysLabel, False, False, 0) + + expiryBox.pack_start(expireDaysBox, False, False, 0) + + # Create a frame for confirmation settings + confirmFrame = Gtk.Frame(label="Confirmations") + vbox.pack_start(confirmFrame, False, False, 10) + + # Container for confirmation settings + confirmBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + confirmBox.set_border_width(10) + confirmFrame.add(confirmBox) + + # Delete confirmation checkbox + self.confirmDeleteCheck = Gtk.CheckButton(label="Confirm before deleting notes") + self.confirmDeleteCheck.set_active(bool(int(self.confirmDelete))) + self.confirmDeleteCheck.set_can_focus(True) + confirmBox.pack_start(self.confirmDeleteCheck, False, False, 0) + + # Set the active radio button based on current setting + if int(self.expirationDays) > 0: + self.customExpireRadio.set_active(True) + else: + self.neverExpireRadio.set_active(True) + + # Connect signals + self.neverExpireRadio.connect("toggled", self.on_expiry_radio_toggled) + self.customExpireRadio.connect("toggled", self.on_expiry_radio_toggled) + + # Enable/disable the spin button based on the selected radio + self.on_expiry_radio_toggled(None) + + # Save button + saveButton = Gtk.Button.new_with_label("Save Settings") + saveButton.connect("clicked", self.on_save_settings) + saveButton.set_can_focus(True) + vbox.pack_start(saveButton, False, False, 10) + + return vbox + + def add_columns(self): + """Add columns to the TreeView""" + # ID Column + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("ID", renderer, text=0) + column.set_sort_column_id(0) + self.notesView.append_column(column) + + # Note Text Column + renderer = Gtk.CellRendererText() + renderer.set_property("ellipsize", True) + column = Gtk.TreeViewColumn("Note", renderer, text=1) + column.set_expand(True) + self.notesView.append_column(column) + + # Locked Column + renderer = Gtk.CellRendererToggle() + renderer.connect("toggled", self.on_locked_toggled) + column = Gtk.TreeViewColumn("Locked", renderer, active=2) + self.notesView.append_column(column) + + # Expiration Column + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("Expires", renderer, text=3) + self.notesView.append_column(column) + + def populate_notes(self): + """Populate the list store with notes from the database""" + self.notesStore.clear() + + try: + cursor = self.conn.cursor() + cursor.execute( + "SELECT id, text, locked, expires FROM notes ORDER BY id" + ) + notes = cursor.fetchall() + + for note in notes: + noteId, text, locked, expires = note + + # Format expiry date if it exists + expiryText = "" + if expires: + try: + expiryDate = datetime.datetime.strptime(expires, "%Y-%m-%d %H:%M:%S") + expiryText = expiryDate.strftime("%Y-%m-%d") + except: + expiryText = "Invalid date" + + self.notesStore.append([ + noteId, # Database ID + text, # Note text + bool(locked), # Locked status + expiryText # Expiry date + ]) + except sqlite3.Error as e: + print(f"Error populating notes: {e}") + + def setup_actions(self): + """Set up keyboard shortcuts""" + # Delete selected note + deleteAction = Gio.SimpleAction.new("delete", None) + deleteAction.connect("activate", self.on_delete_clicked) + self.add_action(deleteAction) + self.set_accels_for_action("app.delete", ["Delete"]) + + # Toggle lock on selected note + lockAction = Gio.SimpleAction.new("lock", None) + lockAction.connect("activate", self.on_lock_toggled) + self.add_action(lockAction) + self.set_accels_for_action("app.lock", ["l"]) + + # Copy note to clipboard + copyAction = Gio.SimpleAction.new("copy", None) + copyAction.connect("activate", self.on_copy_clicked) + self.add_action(copyAction) + self.set_accels_for_action("app.copy", ["c"]) + + # Switch to notes tab + notesTabAction = Gio.SimpleAction.new("notes_tab", None) + notesTabAction.connect("activate", lambda a, p: self.notebook.set_current_page(0)) + self.add_action(notesTabAction) + self.set_accels_for_action("app.notes_tab", ["1"]) + + # Switch to settings tab + settingsTabAction = Gio.SimpleAction.new("settings_tab", None) + settingsTabAction.connect("activate", lambda a, p: self.notebook.set_current_page(1)) + self.add_action(settingsTabAction) + self.set_accels_for_action("app.settings_tab", ["2"]) + + # Quit application + quitAction = Gio.SimpleAction.new("quit", None) + quitAction.connect("activate", lambda a, p: self.quit()) + self.add_action(quitAction) + self.set_accels_for_action("app.quit", ["Escape"]) + + def show_status_message(self, message): + """Show a message in the statusbar""" + self.statusbar.push(self.statusbarCtx, message) + # Auto-remove after 5 seconds + GLib.timeout_add_seconds(5, self.statusbar.pop, self.statusbarCtx) + + def on_tab_switched(self, notebook, page, page_num): + """Handler for tab switching""" + # Reset status bar on tab switch + self.statusbar.pop(self.statusbarCtx) + + # Set focus appropriately + if page_num == 0: # Notes tab + self.newNoteEntry.grab_focus() + elif page_num == 1: # Settings tab + if self.neverExpireRadio.get_active(): + self.neverExpireRadio.grab_focus() + else: + self.customExpireRadio.grab_focus() + + def on_expiry_radio_toggled(self, widget): + """Handler for expiry radio button toggles""" + # Enable/disable spin button based on which radio is active + self.daysSpinButton.set_sensitive(self.customExpireRadio.get_active()) + + def on_save_settings(self, button): + """Handler for Save Settings button""" + if self.neverExpireRadio.get_active(): + self.expirationDays = 0 + else: + self.expirationDays = self.daysSpinButton.get_value_as_int() + + # Get delete confirmation setting + self.confirmDelete = 1 if self.confirmDeleteCheck.get_active() else 0 + + # Save to database + self.save_setting("expirationDays", self.expirationDays) + self.save_setting("confirmDelete", self.confirmDelete) + + # Apply expiration to notes if needed + if self.expirationDays > 0: + try: + cursor = self.conn.cursor() + now = datetime.datetime.now() + expiryDate = now + datetime.timedelta(days=self.expirationDays) + expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S") + + # Set expiration for notes that aren't locked and don't have expiration + cursor.execute( + "UPDATE notes SET expires = ? WHERE locked = 0 AND expires IS NULL", + (expiryStr,) + ) + self.conn.commit() + + # Refresh the notes list + self.populate_notes() + except sqlite3.Error as e: + print(f"Error updating expirations: {e}") + + self.show_status_message("Settings saved") + + # Switch back to notes tab + self.notebook.set_current_page(0) + + def on_entry_activate(self, entry): + """Handle Enter key in the entry field""" + self.add_new_note(entry.get_text()) + entry.set_text("") + + def on_add_clicked(self, button): + """Handle Add Note button click""" + self.add_new_note(self.newNoteEntry.get_text()) + self.newNoteEntry.set_text("") + + def add_new_note(self, text): + """Add a new note to the database""" + if not text.strip(): + self.show_status_message("Note text cannot be empty") + return + + try: + cursor = self.conn.cursor() + + # Set expiration if enabled + expires = None + if int(self.expirationDays) > 0: + expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays)) + expires = expiryDate.strftime("%Y-%m-%d %H:%M:%S") + + # Insert the new note + cursor.execute( + "INSERT INTO notes (text, expires, locked) VALUES (?, ?, ?)", + (text, expires, 0) + ) + self.conn.commit() + + # Refresh the notes list + self.populate_notes() + self.show_status_message("Note added") + except sqlite3.Error as e: + self.show_status_message(f"Error adding note: {e}") + + def on_row_activated(self, view, path, column): + """Handle double-click on a note - edit the note""" + model = view.get_model() + noteId = model[path][0] + + # Get the note from the database + try: + cursor = self.conn.cursor() + cursor.execute("SELECT text, locked, expires FROM notes WHERE id = ?", (noteId,)) + note = cursor.fetchone() + + if note: + self.edit_note_dialog(noteId, note) + except sqlite3.Error as e: + self.show_status_message(f"Error retrieving note: {e}") + + def on_locked_toggled(self, renderer, path): + """Handle toggling the locked state from the view""" + model = self.notesView.get_model() + noteId = model[path][0] + currentLocked = model[path][2] + newLocked = not currentLocked + + try: + cursor = self.conn.cursor() + + # Update locked status + cursor.execute( + "UPDATE notes SET locked = ? WHERE id = ?", + (1 if newLocked else 0, noteId) + ) + + # If unlocking and expiration is enabled, set expiration + if not newLocked and int(self.expirationDays) > 0: + expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays)) + expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S") + + cursor.execute( + "UPDATE notes SET expires = ? WHERE id = ?", + (expiryStr, noteId) + ) + + # Update the expiry text in the model + model[path][3] = expiryDate.strftime("%Y-%m-%d") + + self.show_status_message("Note unlocked - expiration set") + elif newLocked: + self.show_status_message("Note locked - will not expire") + else: + self.show_status_message("Note unlocked") + + # Update the model + model[path][2] = newLocked + + self.conn.commit() + except sqlite3.Error as e: + self.show_status_message(f"Error updating note: {e}") + + def on_lock_toggled(self, action, parameter): + """Handle keyboard shortcut to toggle lock""" + selection = self.notesView.get_selection() + model, treeiter = selection.get_selected() + if treeiter: + noteId = model[treeiter][0] + currentLocked = model[treeiter][2] + + # Simulate clicking the toggle + path = model.get_path(treeiter) + self.on_locked_toggled(None, path) + + def on_copy_clicked(self, action=None, parameter=None): + """Copy the selected note to clipboard""" + selection = self.notesView.get_selection() + model, treeiter = selection.get_selected() + if treeiter: + noteText = model[treeiter][1] + + # Get the clipboard + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + clipboard.set_text(noteText, -1) + + self.show_status_message("Note copied to clipboard") + else: + self.show_status_message("No note selected") + + def on_delete_button_clicked(self, button): + """Handle Delete button click""" + selection = self.notesView.get_selection() + model, treeiter = selection.get_selected() + if treeiter: + noteId = model[treeiter][0] + noteText = model[treeiter][1] + + if int(self.confirmDelete): + self.confirm_delete_note(noteId, noteText) + else: + self.delete_note(noteId) + + def on_delete_clicked(self, action, parameter): + """Handle Delete key to remove a note""" + selection = self.notesView.get_selection() + model, treeiter = selection.get_selected() + if treeiter: + noteId = model[treeiter][0] + noteText = model[treeiter][1] + + if int(self.confirmDelete): + self.confirm_delete_note(noteId, noteText) + else: + self.delete_note(noteId) + + def delete_note(self, noteId): + """Delete a note by ID without confirmation""" + try: + cursor = self.conn.cursor() + cursor.execute("DELETE FROM notes WHERE id = ?", (noteId,)) + self.conn.commit() + + self.populate_notes() + self.show_status_message("Note deleted") + except sqlite3.Error as e: + self.show_status_message(f"Error deleting note: {e}") + + def confirm_delete_note(self, noteId, noteText): + """Show confirmation dialog before deleting a note""" + if len(noteText) > 30: + noteText = noteText[:30] + "..." + + dialog = Gtk.MessageDialog( + transient_for=self.window, + flags=0, + message_type=Gtk.MessageType.QUESTION, + buttons=Gtk.ButtonsType.YES_NO, + text=f"Delete note: {noteText}?" + ) + dialog.set_default_response(Gtk.ResponseType.NO) + + response = dialog.run() + if response == Gtk.ResponseType.YES: + self.delete_note(noteId) + + dialog.destroy() + + def edit_note_dialog(self, noteId, note): + """Show dialog to edit a note""" + text, locked, expires = note + + dialog = Gtk.Dialog( + title="Edit Note", + parent=self.window, + flags=0, + buttons=( + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, Gtk.ResponseType.OK + ) + ) + dialog.set_default_size(400, 200) + + # Make the dialog accessible + dialog.set_role("dialog") + dialog.set_property("has-tooltip", True) + + # Create a text view for the note + entryBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + entryBox.set_border_width(10) + + label = Gtk.Label(label="Edit note text:") + label.set_halign(Gtk.Align.START) + entryBox.pack_start(label, False, False, 0) + + # Scrolled window for text view + scrolled = Gtk.ScrolledWindow() + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled.set_shadow_type(Gtk.ShadowType.IN) + + # Text view for multi-line editing + textBuffer = Gtk.TextBuffer() + textBuffer.set_text(text) + textView = Gtk.TextView.new_with_buffer(textBuffer) + textView.set_wrap_mode(Gtk.WrapMode.WORD) + textView.set_can_focus(True) + + scrolled.add(textView) + entryBox.pack_start(scrolled, True, True, 0) + + # Add lock checkbox + lockCheck = Gtk.CheckButton(label="Lock note (prevent expiration)") + lockCheck.set_active(bool(locked)) + lockCheck.set_can_focus(True) + entryBox.pack_start(lockCheck, False, False, 0) + + # Show expiration date if it exists + if expires and not locked: + try: + expiryDate = datetime.datetime.strptime(expires, "%Y-%m-%d %H:%M:%S") + expiryLabel = Gtk.Label(label=f"Expires on: {expiryDate.strftime('%Y-%m-%d')}") + expiryLabel.set_halign(Gtk.Align.START) + entryBox.pack_start(expiryLabel, False, False, 0) + except: + pass + + dialog.get_content_area().add(entryBox) + dialog.set_default_response(Gtk.ResponseType.OK) + dialog.show_all() + + # Set focus to text view + textView.grab_focus() + + response = dialog.run() + if response == Gtk.ResponseType.OK: + # Get the text from the buffer + start, end = textBuffer.get_bounds() + newText = textBuffer.get_text(start, end, False) + newLocked = lockCheck.get_active() + + try: + cursor = self.conn.cursor() + + # Update the note + cursor.execute( + "UPDATE notes SET text = ?, locked = ? WHERE id = ?", + (newText, 1 if newLocked else 0, noteId) + ) + + # Update expiration if needed + if not newLocked and int(self.expirationDays) > 0: + expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays)) + expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S") + + cursor.execute( + "UPDATE notes SET expires = ? WHERE id = ?", + (expiryStr, noteId) + ) + + self.conn.commit() + self.populate_notes() + self.show_status_message("Note updated") + except sqlite3.Error as e: + self.show_status_message(f"Error updating note: {e}") + + dialog.destroy() + +def main(): + app = JotApp() + return app.run(None) + +if __name__ == "__main__": + main()