Merge pull request #3 from overcuriousity/claude/fix-umlaut-support-01NVHwQkPasDs5ZPySfRqZgE

Fix UTF-8/umlaut support in note input
This commit is contained in:
overcuriousity
2025-12-13 16:02:15 +01:00
committed by GitHub
3 changed files with 48 additions and 12 deletions

View File

@@ -69,7 +69,7 @@ def export_markdown(output_file: str = "export.md"):
try: try:
storage = Storage() 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("# Forensic Notes Export\n\n")
f.write(f"Generated on: {time.ctime()}\n\n") f.write(f"Generated on: {time.ctime()}\n\n")

View File

@@ -166,7 +166,7 @@ Attachment: invoice.pdf.exe (double extension trick) #email-forensics #phishing-
if not self.data_file.exists(): if not self.data_file.exists():
return [] return []
try: try:
with open(self.data_file, 'r') as f: with open(self.data_file, 'r', encoding='utf-8') as f:
data = json.load(f) data = json.load(f)
return [Case.from_dict(c) for c in data] return [Case.from_dict(c) for c in data]
except (json.JSONDecodeError, IOError): 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] data = [c.to_dict() for c in self.cases]
# Write to temp file then rename for atomic-ish write # Write to temp file then rename for atomic-ish write
temp_file = self.data_file.with_suffix(".tmp") temp_file = self.data_file.with_suffix(".tmp")
with open(temp_file, 'w') as f: with open(temp_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2, ensure_ascii=False)
temp_file.replace(self.data_file) temp_file.replace(self.data_file)
def add_case(self, case: Case): def add_case(self, case: Case):
@@ -225,15 +225,15 @@ class StateManager:
state["evidence_id"] = evidence_id state["evidence_id"] = evidence_id
# Atomic write: write to temp file then rename # Atomic write: write to temp file then rename
temp_file = self.state_file.with_suffix(".tmp") temp_file = self.state_file.with_suffix(".tmp")
with open(temp_file, 'w') as f: with open(temp_file, 'w', encoding='utf-8') as f:
json.dump(state, f) json.dump(state, f, ensure_ascii=False)
temp_file.replace(self.state_file) temp_file.replace(self.state_file)
def get_active(self) -> dict: def get_active(self) -> dict:
if not self.state_file.exists(): if not self.state_file.exists():
return {"case_id": None, "evidence_id": None} return {"case_id": None, "evidence_id": None}
try: try:
with open(self.state_file, 'r') as f: with open(self.state_file, 'r', encoding='utf-8') as f:
return json.load(f) return json.load(f)
except (json.JSONDecodeError, IOError): except (json.JSONDecodeError, IOError):
return {"case_id": None, "evidence_id": None} return {"case_id": None, "evidence_id": None}
@@ -242,7 +242,7 @@ class StateManager:
if not self.settings_file.exists(): if not self.settings_file.exists():
return {"pgp_enabled": True} return {"pgp_enabled": True}
try: try:
with open(self.settings_file, 'r') as f: with open(self.settings_file, 'r', encoding='utf-8') as f:
return json.load(f) return json.load(f)
except (json.JSONDecodeError, IOError): except (json.JSONDecodeError, IOError):
return {"pgp_enabled": True} return {"pgp_enabled": True}
@@ -252,6 +252,6 @@ class StateManager:
settings[key] = value settings[key] = value
# Atomic write: write to temp file then rename # Atomic write: write to temp file then rename
temp_file = self.settings_file.with_suffix(".tmp") temp_file = self.settings_file.with_suffix(".tmp")
with open(temp_file, 'w') as f: with open(temp_file, 'w', encoding='utf-8') as f:
json.dump(settings, f) json.dump(settings, f, ensure_ascii=False)
temp_file.replace(self.settings_file) temp_file.replace(self.settings_file)

View File

@@ -1898,7 +1898,7 @@ class TUI:
line = lines[cursor_line] line = lines[cursor_line]
lines[cursor_line] = line[:cursor_col] + chr(ch) + line[cursor_col:] lines[cursor_line] = line[:cursor_col] + chr(ch) + line[cursor_col:]
cursor_col += 1 cursor_col += 1
# Auto-wrap to next line if cursor exceeds visible width # Auto-wrap to next line if cursor exceeds visible width
if cursor_col >= input_width: if cursor_col >= input_width:
# Always ensure there's a next line to move to # Always ensure there's a next line to move to
@@ -1911,6 +1911,42 @@ class TUI:
if cursor_line >= scroll_offset + input_height: if cursor_line >= scroll_offset + input_height:
scroll_offset = cursor_line - input_height + 1 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): def dialog_confirm(self, message):
curses.curs_set(0) curses.curs_set(0)
h = 5 h = 5
@@ -2643,7 +2679,7 @@ class TUI:
# Write to file # Write to file
try: try:
with open(filepath, 'w') as f: with open(filepath, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines)) f.write('\n'.join(lines))
self.show_message(f"IOCs exported to: {filepath}") self.show_message(f"IOCs exported to: {filepath}")
except Exception as e: except Exception as e: