pomera-ai-commander 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,9 +47,14 @@ class NotesWidget:
47
47
  self.send_to_input_callback = send_to_input_callback
48
48
  self.dialog_manager = dialog_manager
49
49
 
50
- # Database path - use same directory as Pomera
51
- db_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
52
- self.db_path = os.path.join(db_dir, 'notes.db')
50
+ # Database path - use platform-appropriate data directory
51
+ try:
52
+ from core.data_directory import get_database_path
53
+ self.db_path = get_database_path('notes.db')
54
+ except ImportError:
55
+ # Fallback to legacy behavior - same directory as Pomera
56
+ db_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
57
+ self.db_path = os.path.join(db_dir, 'notes.db')
53
58
 
54
59
  # State management
55
60
  self.search_debounce_timer: Optional[str] = None
@@ -94,6 +99,33 @@ class NotesWidget:
94
99
  if conn:
95
100
  conn.close()
96
101
 
102
+ def _sanitize_text(self, text: str) -> str:
103
+ """
104
+ Sanitize text by removing invalid UTF-8 surrogate characters.
105
+
106
+ Lone surrogates (U+D800 to U+DFFF) are invalid in UTF-8 and cause
107
+ encoding errors when saving to the database. This can happen when
108
+ pasting content from the clipboard that contains malformed data.
109
+
110
+ Args:
111
+ text: Input text that may contain invalid surrogates
112
+
113
+ Returns:
114
+ Sanitized text safe for UTF-8 encoding and database storage
115
+ """
116
+ if not text:
117
+ return text
118
+
119
+ try:
120
+ # This two-step process handles lone surrogates:
121
+ # 1. surrogatepass allows encoding surrogates (normally forbidden in UTF-8)
122
+ # 2. errors='replace' replaces invalid sequences with replacement char
123
+ sanitized = text.encode('utf-8', errors='surrogatepass').decode('utf-8', errors='replace')
124
+ return sanitized
125
+ except Exception:
126
+ # Fallback: manually filter out surrogate characters
127
+ return ''.join(c for c in text if not (0xD800 <= ord(c) <= 0xDFFF))
128
+
97
129
  def init_database(self) -> None:
98
130
  """Initialize the SQLite database and Full-Text Search (FTS5) table."""
99
131
  try:
@@ -702,10 +734,12 @@ class NotesWidget:
702
734
  row = conn.execute('SELECT * FROM notes WHERE id = ?', (self.current_item,)).fetchone()
703
735
  if row:
704
736
  now = datetime.now().isoformat()
737
+ # Sanitize text to prevent UTF-8 surrogate errors
705
738
  conn.execute('''
706
739
  INSERT INTO notes (Created, Modified, Title, Input, Output)
707
740
  VALUES (?, ?, ?, ?, ?)
708
- ''', (now, now, row['Title'], row['Input'], row['Output']))
741
+ ''', (now, now, self._sanitize_text(row['Title']),
742
+ self._sanitize_text(row['Input']), self._sanitize_text(row['Output'])))
709
743
  conn.commit()
710
744
  self.perform_search(select_first=True)
711
745
  self.logger.info(f"Duplicated note {self.current_item}")
@@ -783,9 +817,10 @@ class NotesWidget:
783
817
  return
784
818
 
785
819
  try:
786
- title = self.title_entry.get() if hasattr(self, 'title_entry') else ""
787
- input_content = self.input_display.get(1.0, tk.END).strip()
788
- output_content = self.output_display.get(1.0, tk.END).strip()
820
+ # Sanitize text to prevent UTF-8 surrogate errors from clipboard paste
821
+ title = self._sanitize_text(self.title_entry.get() if hasattr(self, 'title_entry') else "")
822
+ input_content = self._sanitize_text(self.input_display.get(1.0, tk.END).strip())
823
+ output_content = self._sanitize_text(self.output_display.get(1.0, tk.END).strip())
789
824
 
790
825
  now = datetime.now().isoformat()
791
826
 
@@ -957,12 +992,17 @@ class NotesWidget:
957
992
  The ID of the created note, or None on error
958
993
  """
959
994
  try:
995
+ # Sanitize text to prevent UTF-8 surrogate errors
996
+ sanitized_title = self._sanitize_text(title)
997
+ sanitized_input = self._sanitize_text(input_content)
998
+ sanitized_output = self._sanitize_text(output_content)
999
+
960
1000
  now = datetime.now().isoformat()
961
1001
  with self.get_db_connection() as conn:
962
1002
  cursor = conn.execute('''
963
1003
  INSERT INTO notes (Created, Modified, Title, Input, Output)
964
1004
  VALUES (?, ?, ?, ?, ?)
965
- ''', (now, now, title, input_content, output_content))
1005
+ ''', (now, now, sanitized_title, sanitized_input, sanitized_output))
966
1006
  note_id = cursor.lastrowid
967
1007
  conn.commit()
968
1008
 
@@ -350,7 +350,7 @@ class RegexExtractorUI:
350
350
  new_item_id = len(pattern_library) - 1
351
351
  tree.selection_set(str(new_item_id))
352
352
  tree.focus(str(new_item_id))
353
- self.settings_manager.save_settings()
353
+ self.settings_manager.set_pattern_library(pattern_library) if hasattr(self.settings_manager, 'set_pattern_library') else self.settings_manager.save_settings()
354
354
 
355
355
  def delete_pattern():
356
356
  selection = tree.selection()
@@ -358,7 +358,7 @@ class RegexExtractorUI:
358
358
  item_id = int(selection[0])
359
359
  del pattern_library[item_id]
360
360
  refresh_tree()
361
- self.settings_manager.save_settings()
361
+ self.settings_manager.set_pattern_library(pattern_library) if hasattr(self.settings_manager, 'set_pattern_library') else self.settings_manager.save_settings()
362
362
 
363
363
  def move_up():
364
364
  selection = tree.selection()
@@ -371,7 +371,7 @@ class RegexExtractorUI:
371
371
  refresh_tree()
372
372
  tree.selection_set(str(item_id-1))
373
373
  tree.focus(str(item_id-1))
374
- self.settings_manager.save_settings()
374
+ self.settings_manager.set_pattern_library(pattern_library) if hasattr(self.settings_manager, 'set_pattern_library') else self.settings_manager.save_settings()
375
375
 
376
376
  def move_down():
377
377
  selection = tree.selection()
@@ -384,7 +384,7 @@ class RegexExtractorUI:
384
384
  refresh_tree()
385
385
  tree.selection_set(str(item_id+1))
386
386
  tree.focus(str(item_id+1))
387
- self.settings_manager.save_settings()
387
+ self.settings_manager.set_pattern_library(pattern_library) if hasattr(self.settings_manager, 'set_pattern_library') else self.settings_manager.save_settings()
388
388
 
389
389
  ttk.Button(left_buttons, text="Add", command=add_pattern).pack(side=tk.LEFT, padx=(0,5))
390
390
  ttk.Button(left_buttons, text="Delete", command=delete_pattern).pack(side=tk.LEFT, padx=5)
@@ -470,7 +470,7 @@ class RegexExtractorUI:
470
470
  pattern["purpose"] = new_value
471
471
  tree.set(item, column, new_value)
472
472
  entry.destroy()
473
- self.settings_manager.save_settings()
473
+ self.settings_manager.set_pattern_library(pattern_library) if hasattr(self.settings_manager, 'set_pattern_library') else self.settings_manager.save_settings()
474
474
 
475
475
  def cancel_edit():
476
476
  entry.destroy()