A simple notes system added for the panel, I38 notes.

This commit is contained in:
Storm Dragon 2025-04-07 15:08:11 -04:00
parent dc8f832840
commit f41741866a
2 changed files with 826 additions and 0 deletions

3
i38.sh
View File

@ -591,6 +591,9 @@ mode "panel" {
# System information bound to s # System information bound to s
bindsym s exec --no-startup-id ${i3Path}/scripts/sysinfo.sh, mode "default" 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 # Exit panel mode without any action
bindsym Escape mode "default" bindsym Escape mode "default"
bindsym Control+g mode "default" bindsym Control+g mode "default"

823
scripts/notes.py Executable file
View File

@ -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", ["<Control>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", ["<Alt>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", ["<Alt>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()