mirror of
https://github.com/overcuriousity/trace.git
synced 2025-12-20 13:02:21 +00:00
Fix UTF-8/umlaut support in note input
Added full Unicode character support to the TUI's multiline input dialog. Previously, only ASCII characters (32-126) were captured when typing notes. Changes: - Added UTF-8 multibyte character handling to _multiline_input_dialog() - Properly collects and decodes 2/3/4-byte UTF-8 sequences - Added explicit UTF-8 encoding to all file I/O operations - Added ensure_ascii=False to JSON serialization for proper Unicode preservation This fix allows users to enter umlauts (ä, ö, ü), accented characters (é, à, ñ), and other Unicode characters in notes, case names, and all other text input fields. Tested with German umlauts, Asian characters, Cyrillic, and special chars.
This commit is contained in:
@@ -69,7 +69,7 @@ def export_markdown(output_file: str = "export.md"):
|
||||
try:
|
||||
storage = Storage()
|
||||
|
||||
with open(output_file, "w") as f:
|
||||
with open(output_file, "w", encoding='utf-8') as f:
|
||||
f.write("# Forensic Notes Export\n\n")
|
||||
f.write(f"Generated on: {time.ctime()}\n\n")
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ Attachment: invoice.pdf.exe (double extension trick) #email-forensics #phishing-
|
||||
if not self.data_file.exists():
|
||||
return []
|
||||
try:
|
||||
with open(self.data_file, 'r') as f:
|
||||
with open(self.data_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return [Case.from_dict(c) for c in data]
|
||||
except (json.JSONDecodeError, IOError):
|
||||
@@ -176,8 +176,8 @@ Attachment: invoice.pdf.exe (double extension trick) #email-forensics #phishing-
|
||||
data = [c.to_dict() for c in self.cases]
|
||||
# Write to temp file then rename for atomic-ish write
|
||||
temp_file = self.data_file.with_suffix(".tmp")
|
||||
with open(temp_file, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
temp_file.replace(self.data_file)
|
||||
|
||||
def add_case(self, case: Case):
|
||||
@@ -225,15 +225,15 @@ class StateManager:
|
||||
state["evidence_id"] = evidence_id
|
||||
# Atomic write: write to temp file then rename
|
||||
temp_file = self.state_file.with_suffix(".tmp")
|
||||
with open(temp_file, 'w') as f:
|
||||
json.dump(state, f)
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(state, f, ensure_ascii=False)
|
||||
temp_file.replace(self.state_file)
|
||||
|
||||
def get_active(self) -> dict:
|
||||
if not self.state_file.exists():
|
||||
return {"case_id": None, "evidence_id": None}
|
||||
try:
|
||||
with open(self.state_file, 'r') as f:
|
||||
with open(self.state_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return {"case_id": None, "evidence_id": None}
|
||||
@@ -242,7 +242,7 @@ class StateManager:
|
||||
if not self.settings_file.exists():
|
||||
return {"pgp_enabled": True}
|
||||
try:
|
||||
with open(self.settings_file, 'r') as f:
|
||||
with open(self.settings_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return {"pgp_enabled": True}
|
||||
@@ -252,6 +252,6 @@ class StateManager:
|
||||
settings[key] = value
|
||||
# Atomic write: write to temp file then rename
|
||||
temp_file = self.settings_file.with_suffix(".tmp")
|
||||
with open(temp_file, 'w') as f:
|
||||
json.dump(settings, f)
|
||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(settings, f, ensure_ascii=False)
|
||||
temp_file.replace(self.settings_file)
|
||||
|
||||
38
trace/tui.py
38
trace/tui.py
@@ -1911,6 +1911,42 @@ class TUI:
|
||||
if cursor_line >= scroll_offset + input_height:
|
||||
scroll_offset = cursor_line - input_height + 1
|
||||
|
||||
elif ch > 127:
|
||||
# UTF-8 multi-byte character (umlauts, etc.)
|
||||
# curses returns the first byte, we need to read the rest
|
||||
try:
|
||||
# Try to decode as UTF-8
|
||||
# For multibyte UTF-8, we need to collect all bytes
|
||||
bytes_collected = [ch]
|
||||
|
||||
# Determine how many bytes we need based on the first byte
|
||||
if ch >= 0xF0: # 4-byte character
|
||||
num_bytes = 4
|
||||
elif ch >= 0xE0: # 3-byte character
|
||||
num_bytes = 3
|
||||
elif ch >= 0xC0: # 2-byte character
|
||||
num_bytes = 2
|
||||
else:
|
||||
num_bytes = 1
|
||||
|
||||
# Read remaining bytes
|
||||
for _ in range(num_bytes - 1):
|
||||
next_ch = win.getch()
|
||||
bytes_collected.append(next_ch)
|
||||
|
||||
# Convert to character
|
||||
char_bytes = bytes([b & 0xFF for b in bytes_collected])
|
||||
char = char_bytes.decode('utf-8')
|
||||
|
||||
# Insert character at cursor
|
||||
line = lines[cursor_line]
|
||||
lines[cursor_line] = line[:cursor_col] + char + line[cursor_col:]
|
||||
cursor_col += 1
|
||||
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
# If decode fails, ignore the character
|
||||
pass
|
||||
|
||||
def dialog_confirm(self, message):
|
||||
curses.curs_set(0)
|
||||
h = 5
|
||||
@@ -2643,7 +2679,7 @@ class TUI:
|
||||
|
||||
# Write to file
|
||||
try:
|
||||
with open(filepath, 'w') as f:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines))
|
||||
self.show_message(f"IOCs exported to: {filepath}")
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user