fixed a bunch of stuff, added a notes programm, which lets you take notes, did some giant refactoring work and did some general designing

This commit is contained in:
2025-11-24 14:01:15 +01:00
parent 3cd75d97f6
commit 5fd77f4b39
12 changed files with 1768 additions and 866 deletions

18
build.ps1 Normal file
View File

@@ -0,0 +1,18 @@
Write-Host "Building application with PyInstaller..." -ForegroundColor Cyan
# Activate venv
& "$PSScriptRoot\venv\Scripts\Activate.ps1"
pyinstaller `
--onefile `
--noconfirm `
--windowed `
--hidden-import wx._xml `
--add-data "FiraCode-Regular.ttf;." `
--add-data "FiraCode-SemiBold.ttf;." `
--add-data "venv\Lib\site-packages\irc\codes.txt;irc" `
--add-data "icon.ico;." `
--icon "icon.ico" `
"src/main.py"
Write-Host "Build complete!" -ForegroundColor Green

View File

@@ -1,20 +0,0 @@
#!/bin/bash
pyinstaller \
--onefile \
--noconfirm \
--add-data "FiraCode-Regular.ttf:." \
--add-data "FiraCode-SemiBold.ttf:." \
--add-data "$(python -c 'import irc, os; print(os.path.dirname(irc.__file__))'):irc" \
--hidden-import=irc.client \
--hidden-import=irc.connection \
--hidden-import=irc.events \
--hidden-import=irc.strings \
--hidden-import=wx \
--hidden-import=wx.lib.mixins.listctrl \
--hidden-import=wx.lib.mixins.treemixin \
--hidden-import=wx.lib.mixins.inspection \
--hidden-import=psutil \
--hidden-import=queue \
--hidden-import=logging.handlers \
main.py

BIN
icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 168 KiB

57
src/AboutDialog.py Normal file
View File

@@ -0,0 +1,57 @@
import wx
import sys
import os
def get_resource_path(relative_path):
"""Get absolute path to resource, works for dev and for PyInstaller"""
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
class AboutDialog(wx.Dialog):
def __init__(self, parent):
super().__init__(parent, title="About wxIRC Client", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
sizer = wx.BoxSizer(wx.VERTICAL)
icon_path = get_resource_path("icon.ico")
icon_bitmap = wx.Bitmap(icon_path, wx.BITMAP_TYPE_ICO)
icon_ctrl = wx.StaticBitmap(self, bitmap=icon_bitmap)
# Add the icon to the sizer
sizer.Add(icon_ctrl, 0, wx.ALL | wx.ALIGN_CENTER, 10)
# Application info
info_text = wx.StaticText(self, label="wxIRC Client")
info_font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
info_text.SetFont(info_font)
version_text = wx.StaticText(self, label="V 1.1.1.0")
version_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
version_text.SetFont(version_font)
contrubutors_text = wx.StaticText(self, label="MiT License")
contrubutors_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
contrubutors_text.SetFont(contrubutors_font)
# Add info to sizer
sizer.Add(info_text, 0, wx.ALL | wx.ALIGN_CENTER, 5)
sizer.Add(version_text, 0, wx.ALL | wx.ALIGN_CENTER, 5)
sizer.Add(contrubutors_text, 0, wx.ALL | wx.ALIGN_CENTER, 5)
# OK button
ok_btn = wx.Button(self, wx.ID_OK, "OK")
ok_btn.SetDefault()
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
btn_sizer.Add(ok_btn, 0, wx.ALIGN_CENTER | wx.ALL, 10)
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER)
self.SetSizer(sizer)
self.Fit()
self.Centre()

BIN
src/FiraCode-Regular.ttf Normal file

Binary file not shown.

BIN
src/FiraCode-SemiBold.ttf Normal file

Binary file not shown.

373
src/IRCPanel.py Normal file
View File

@@ -0,0 +1,373 @@
import wx
import threading
import logging
from SearchDialog import SearchDialog
import traceback
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class IRCPanel(wx.Panel):
def __init__(self, parent, main_frame):
super().__init__(parent)
self.parent = parent
self.main_frame = main_frame
self.messages = []
sizer = wx.BoxSizer(wx.VERTICAL)
# Use a better font for chat with white theme
self.text_ctrl = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.TE_AUTO_URL)
# White theme colors
self.text_ctrl.SetBackgroundColour(wx.Colour(255, 255, 255)) # White background
self.text_ctrl.SetForegroundColour(wx.Colour(0, 0, 0)) # Black text
# Load appropriate font
self.font = self.load_system_font()
self.text_ctrl.SetFont(self.font)
sizer.Add(self.text_ctrl, 1, wx.EXPAND | wx.ALL, 0)
input_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.input_ctrl = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER)
self.input_ctrl.SetHint("Type message here …")
self.input_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_send)
self.input_ctrl.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
send_btn = wx.Button(self, label="Send")
send_btn.SetToolTip("Send message (Enter)")
send_btn.Bind(wx.EVT_BUTTON, self.on_send)
input_sizer.Add(self.input_ctrl, 1, wx.EXPAND | wx.ALL, 2)
input_sizer.Add(send_btn, 0, wx.ALL, 2)
sizer.Add(input_sizer, 0, wx.EXPAND | wx.ALL, 0)
self.SetSizer(sizer)
self.target = None
self.history = []
self.history_pos = -1
# Search state
self.search_text = ""
self.search_positions = []
self.current_search_index = -1
# Bind Ctrl+F for search
self.text_ctrl.Bind(wx.EVT_KEY_DOWN, self.on_text_key_down)
accel_tbl = wx.AcceleratorTable([(wx.ACCEL_CTRL, ord('F'), wx.ID_FIND)])
self.SetAcceleratorTable(accel_tbl)
self.Bind(wx.EVT_MENU, self.on_search, id=wx.ID_FIND)
def load_system_font(self):
"""Load appropriate system font with high DPI support"""
try:
# Get system DPI scale factor
dc = wx.ClientDC(self)
dpi_scale = dc.GetPPI().GetWidth() / 96.0 # 96 is standard DPI
# Calculate base font size based on DPI
base_size = 10
if dpi_scale > 1.5:
font_size = int(base_size * 1.5) # 150% scaling
elif dpi_scale > 1.25:
font_size = int(base_size * 1.25) # 125% scaling
else:
font_size = base_size
# Try system fonts in order of preference
font_families = [
(wx.FONTFAMILY_TELETYPE, "Consolas"),
(wx.FONTFAMILY_TELETYPE, "Courier New"),
(wx.FONTFAMILY_TELETYPE, "Monaco"),
(wx.FONTFAMILY_TELETYPE, "DejaVu Sans Mono"),
(wx.FONTFAMILY_TELETYPE, "Liberation Mono"),
]
for family, face_name in font_families:
font = wx.Font(font_size, family, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, face_name)
if font.IsOk():
logger.info(f"Using font: {face_name} at {font_size}pt")
return font
# Fallback to default monospace
font = wx.Font(font_size, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
logger.info("Using system monospace font as fallback")
return font
except Exception as e:
logger.error(f"Error loading system font: {e}")
# Ultimate fallback
return wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
def set_target(self, target):
self.target = target
def add_message(self, message, username=None, username_color=None, message_color=None, bold=False, italic=False, underline=False):
"""Thread-safe message addition with username coloring"""
try:
# Use CallAfter for thread safety
if wx.IsMainThread():
self._add_message_safe(message, username, username_color, message_color, bold, italic, underline)
else:
wx.CallAfter(self._add_message_safe, message, username, username_color, message_color, bold, italic, underline)
except Exception as e:
logger.error(f"Error in add_message: {e}")
def _add_message_safe(self, message, username=None, username_color=None, message_color=None, bold=False, italic=False, underline=False):
"""Actually add message - must be called from main thread"""
try:
self.messages.append(message)
# Save current position for formatting
start_pos = self.text_ctrl.GetLastPosition()
if username and username_color:
# Add username with its color
attr = wx.TextAttr()
attr.SetTextColour(username_color)
if bold:
attr.SetFontWeight(wx.FONTWEIGHT_BOLD)
if italic:
attr.SetFontStyle(wx.FONTSTYLE_ITALIC)
if underline:
attr.SetFontUnderlined(True)
attr.SetFont(self.font)
self.text_ctrl.SetDefaultStyle(attr)
self.text_ctrl.AppendText(username)
# Add the rest of the message with message color
attr = wx.TextAttr()
if message_color:
attr.SetTextColour(message_color)
else:
attr.SetTextColour(wx.Colour(0, 0, 0)) # Black text for white theme
attr.SetFont(self.font)
self.text_ctrl.SetDefaultStyle(attr)
# Append the message (without username if we already added it)
if username and username_color:
# Find the message part after username
message_text = message[message.find(username) + len(username):]
self.text_ctrl.AppendText(message_text + "\n")
else:
self.text_ctrl.AppendText(message + "\n")
# Auto-scroll to bottom
self.text_ctrl.ShowPosition(self.text_ctrl.GetLastPosition())
except Exception as e:
logger.error(f"Error adding message safely: {e}")
def add_formatted_message(self, timestamp, username, content, username_color=None, is_action=False):
"""Add a formatted message with colored username"""
try:
if is_action:
message = f"{timestamp}* {username} {content}"
self.add_message(message, f"* {username}", username_color, wx.Colour(128, 0, 128), italic=True) # Dark purple for actions
else:
message = f"{timestamp}<{username}> {content}"
self.add_message(message, f"<{username}>", username_color, wx.Colour(0, 0, 0)) # Black text
except Exception as e:
logger.error(f"Error adding formatted message: {e}")
def add_system_message(self, message, color=None, bold=False):
"""Add system message without username coloring"""
try:
if color is None:
color = wx.Colour(0, 0, 128) # Dark blue for system messages
self.add_message(message, None, None, color, bold, False, False)
except Exception as e:
logger.error(f"Error adding system message: {e}")
def on_text_key_down(self, event):
"""Handle key events in the text control"""
keycode = event.GetKeyCode()
if event.ControlDown() and keycode == ord('F'):
self.on_search(event)
else:
event.Skip()
def on_search(self, event):
"""Open search dialog"""
try:
dlg = SearchDialog(self)
dlg.ShowModal()
dlg.Destroy()
except Exception as e:
logger.error(f"Error in search: {e}")
def perform_search(self, search_text, case_sensitive=False, whole_word=False):
"""Perform text search in the chat history"""
try:
self.search_text = search_text
self.search_positions = []
self.current_search_index = -1
# Get all text
full_text = self.text_ctrl.GetValue()
if not full_text or not search_text:
return
# Prepare search parameters
search_flags = 0
if not case_sensitive:
# For manual search, we'll handle case sensitivity ourselves
search_text_lower = search_text.lower()
full_text_lower = full_text.lower()
# Manual search implementation since wx.TextCtrl doesn't have FindText
pos = 0
while pos < len(full_text):
if case_sensitive:
# Case sensitive search
found_pos = full_text.find(search_text, pos)
else:
# Case insensitive search
found_pos = full_text_lower.find(search_text_lower, pos)
if found_pos == -1:
break
# For whole word search, verify boundaries
if whole_word:
# Check if it's a whole word
is_word_start = (found_pos == 0 or not full_text[found_pos-1].isalnum())
is_word_end = (found_pos + len(search_text) >= len(full_text) or
not full_text[found_pos + len(search_text)].isalnum())
if is_word_start and is_word_end:
self.search_positions.append(found_pos)
pos = found_pos + 1 # Move forward to avoid infinite loop
else:
self.search_positions.append(found_pos)
pos = found_pos + len(search_text)
if self.search_positions:
self.current_search_index = 0
self.highlight_search_result()
self.main_frame.SetStatusText(f"Found {len(self.search_positions)} occurrences of '{search_text}'")
else:
self.main_frame.SetStatusText(f"Text '{search_text}' not found")
wx.Bell()
except Exception as e:
logger.error(f"Error performing search: {e}")
traceback.print_exc()
def highlight_search_result(self):
"""Highlight the current search result"""
try:
if not self.search_positions or self.current_search_index < 0:
return
pos = self.search_positions[self.current_search_index]
# Select the found text
self.text_ctrl.SetSelection(pos, pos + len(self.search_text))
self.text_ctrl.ShowPosition(pos)
# Update status
self.main_frame.SetStatusText(
f"Found {self.current_search_index + 1} of {len(self.search_positions)}: '{self.search_text}'"
)
except Exception as e:
logger.error(f"Error highlighting search result: {e}")
def find_next(self):
"""Find next occurrence"""
if self.search_positions:
self.current_search_index = (self.current_search_index + 1) % len(self.search_positions)
self.highlight_search_result()
def find_previous(self):
"""Find previous occurrence"""
if self.search_positions:
self.current_search_index = (self.current_search_index - 1) % len(self.search_positions)
self.highlight_search_result()
def on_key_down(self, event):
try:
keycode = event.GetKeyCode()
if keycode == wx.WXK_UP:
if self.history and self.history_pos < len(self.history) - 1:
self.history_pos += 1
self.input_ctrl.SetValue(self.history[-(self.history_pos + 1)])
elif keycode == wx.WXK_DOWN:
if self.history_pos > 0:
self.history_pos -= 1
self.input_ctrl.SetValue(self.history[-(self.history_pos + 1)])
elif self.history_pos == 0:
self.history_pos = -1
self.input_ctrl.Clear()
elif keycode == wx.WXK_TAB:
# Tab completion for nicknames
self.handle_tab_completion()
return # Don't skip to prevent default tab behavior
elif keycode == wx.WXK_F3:
# F3 for find next
if self.search_positions:
self.find_next()
return
elif event.ShiftDown() and keycode == wx.WXK_F3:
# Shift+F3 for find previous
if self.search_positions:
self.find_previous()
return
else:
event.Skip()
except Exception as e:
logger.error(f"Error in key handler: {e}")
event.Skip()
def handle_tab_completion(self):
"""Handle tab completion for nicknames"""
try:
current_text = self.input_ctrl.GetValue()
if not current_text or not self.target or not self.target.startswith('#'):
return
users = self.main_frame.channel_users.get(self.target, [])
if not users:
return
# Find word at cursor position
pos = self.input_ctrl.GetInsertionPoint()
text_before = current_text[:pos]
words = text_before.split()
if not words:
return
current_word = words[-1]
# Find matching nicks
matches = [user for user in users if user.lower().startswith(current_word.lower())]
if matches:
if len(matches) == 1:
# Single match - complete it
new_word = matches[0]
if ':' in text_before or text_before.strip().endswith(current_word):
# Replace the current word
new_text = text_before[:-len(current_word)] + new_word + current_text[pos:]
self.input_ctrl.SetValue(new_text)
self.input_ctrl.SetInsertionPoint(pos - len(current_word) + len(new_word))
else:
# Multiple matches - show in status
self.main_frame.SetStatusText(f"Tab completion: {', '.join(matches[:5])}{'...' if len(matches) > 5 else ''}")
except Exception as e:
logger.error(f"Error in tab completion: {e}")
def on_send(self, event):
try:
message = self.input_ctrl.GetValue().strip()
if message and self.target:
self.history.append(message)
self.history_pos = -1
self.main_frame.send_message(self.target, message)
self.input_ctrl.Clear()
except Exception as e:
logger.error(f"Error sending message: {e}")

811
src/NotesDialog.py Normal file
View File

@@ -0,0 +1,811 @@
import wx
import wx.richtext as rt
import json
from collections import defaultdict
import tempfile
import os
import time
import threading
from typing import Dict, Any, Optional
class NotesDialog(wx.Frame):
def __init__(self, parent, notes_data=None, pos=None):
# If no position specified, offset from parent
if pos is None and parent:
parent_pos = parent.GetPosition()
pos = (parent_pos.x + 50, parent_pos.y + 50)
super().__init__(parent, title="IRC Notes", size=(900, 650), pos=pos,
style=wx.DEFAULT_FRAME_STYLE)
self.parent = parent
self.notes_data = notes_data or defaultdict(dict)
self.current_note_key = None
self.updating_title = False
self.is_closing = False
self.content_changed = False
self.last_save_time = time.time()
self.auto_save_interval = 2 # seconds - reduced for immediate saving
self.SetBackgroundColour(wx.Colour(245, 245, 245))
# Set icon if parent has one
if parent:
self.SetIcon(parent.GetIcon())
self.create_controls()
self.load_notes_list()
# Bind close event to save before closing
self.Bind(wx.EVT_CLOSE, self.on_close)
# Auto-save timer (save every 2 seconds)
self.save_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.on_auto_save, self.save_timer)
self.save_timer.Start(self.auto_save_interval * 1000) # Convert to milliseconds
# Status update timer (update status more frequently)
self.status_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.on_status_update, self.status_timer)
self.status_timer.Start(3000) # 3 seconds
# Initialize status
self.update_status("Ready")
def create_controls(self):
# Create menu bar
self.create_menu_bar()
main_sizer = wx.BoxSizer(wx.VERTICAL)
# Main content area with splitter
splitter = wx.SplitterWindow(self, style=wx.SP_3D | wx.SP_LIVE_UPDATE)
# Left panel - notes list
left_panel = wx.Panel(splitter)
left_sizer = wx.BoxSizer(wx.VERTICAL)
notes_label = wx.StaticText(left_panel, label="Your Notes:")
font = notes_label.GetFont()
font.PointSize += 1
font = font.Bold()
notes_label.SetFont(font)
left_sizer.Add(notes_label, 0, wx.ALL, 8)
self.notes_list = wx.ListBox(left_panel, style=wx.LB_SINGLE)
self.notes_list.Bind(wx.EVT_LISTBOX, self.on_note_select)
self.notes_list.Bind(wx.EVT_LISTBOX_DCLICK, self.on_note_double_click)
left_sizer.Add(self.notes_list, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
# Note management buttons
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.new_btn = wx.Button(left_panel, label="New Note")
self.new_btn.Bind(wx.EVT_BUTTON, self.on_new_note)
btn_sizer.Add(self.new_btn, 1, wx.ALL, 3)
self.delete_btn = wx.Button(left_panel, label="Delete")
self.delete_btn.Bind(wx.EVT_BUTTON, self.on_delete_note)
btn_sizer.Add(self.delete_btn, 1, wx.ALL, 3)
left_sizer.Add(btn_sizer, 0, wx.EXPAND | wx.ALL, 8)
left_panel.SetSizer(left_sizer)
# Right panel - editor
right_panel = wx.Panel(splitter)
right_sizer = wx.BoxSizer(wx.VERTICAL)
title_label = wx.StaticText(right_panel, label="Note Title:")
title_label.SetFont(font)
right_sizer.Add(title_label, 0, wx.ALL, 8)
self.title_ctrl = wx.TextCtrl(right_panel, style=wx.TE_PROCESS_ENTER)
self.title_ctrl.Bind(wx.EVT_TEXT, self.on_title_change)
self.title_ctrl.Bind(wx.EVT_KILL_FOCUS, self.on_title_lose_focus)
title_font = self.title_ctrl.GetFont()
title_font.PointSize += 2
self.title_ctrl.SetFont(title_font)
right_sizer.Add(self.title_ctrl, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
content_label = wx.StaticText(right_panel, label="Content:")
content_label.SetFont(font)
right_sizer.Add(content_label, 0, wx.LEFT | wx.RIGHT, 8)
# Formatting toolbar
self.setup_editor_toolbar(right_panel)
right_sizer.Add(self.toolbar, 0, wx.EXPAND | wx.ALL, 8)
# Rich text editor - FIXED: Make sure editor attribute is created
self.editor = rt.RichTextCtrl(right_panel,
style=wx.VSCROLL | wx.HSCROLL | wx.BORDER_SIMPLE)
self.editor.Bind(wx.EVT_TEXT, self.on_content_change)
self.editor.Bind(wx.EVT_KILL_FOCUS, self.on_editor_lose_focus)
# Set default font
attr = rt.RichTextAttr()
attr.SetFontSize(11)
attr.SetFontFaceName("Segoe UI")
self.editor.SetBasicStyle(attr)
right_sizer.Add(self.editor, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
right_panel.SetSizer(right_sizer)
# Split the window
splitter.SplitVertically(left_panel, right_panel, 250)
splitter.SetMinimumPaneSize(200)
main_sizer.Add(splitter, 1, wx.EXPAND | wx.ALL, 5)
# Status bar
self.status_bar = self.CreateStatusBar(2)
self.status_bar.SetStatusWidths([-1, 150])
self.update_status("Ready")
self.SetSizer(main_sizer)
self.enable_editor(False)
def create_menu_bar(self):
menubar = wx.MenuBar()
# File menu
file_menu = wx.Menu()
export_item = file_menu.Append(wx.ID_ANY, "&Export Notes...\tCtrl+E", "Export all notes to file")
import_item = file_menu.Append(wx.ID_ANY, "&Import Notes...\tCtrl+I", "Import notes from file")
file_menu.AppendSeparator()
export_text_item = file_menu.Append(wx.ID_ANY, "Export Current Note as &Text...\tCtrl+T", "Export current note as plain text")
file_menu.AppendSeparator()
exit_item = file_menu.Append(wx.ID_EXIT, "&Close", "Close notes window")
menubar.Append(file_menu, "&File")
# Notes menu
notes_menu = wx.Menu()
new_note_item = notes_menu.Append(wx.ID_NEW, "&New Note\tCtrl+N", "Create a new note")
delete_note_item = notes_menu.Append(wx.ID_DELETE, "&Delete Note\tDel", "Delete current note")
notes_menu.AppendSeparator()
rename_note_item = notes_menu.Append(wx.ID_ANY, "&Rename Note\tF2", "Rename current note")
menubar.Append(notes_menu, "&Notes")
# Edit menu
edit_menu = wx.Menu()
undo_item = edit_menu.Append(wx.ID_UNDO, "&Undo\tCtrl+Z", "Undo last action")
redo_item = edit_menu.Append(wx.ID_REDO, "&Redo\tCtrl+Y", "Redo last action")
edit_menu.AppendSeparator()
cut_item = edit_menu.Append(wx.ID_CUT, "Cu&t\tCtrl+X", "Cut selection")
copy_item = edit_menu.Append(wx.ID_COPY, "&Copy\tCtrl+C", "Copy selection")
paste_item = edit_menu.Append(wx.ID_PASTE, "&Paste\tCtrl+V", "Paste from clipboard")
edit_menu.AppendSeparator()
select_all_item = edit_menu.Append(wx.ID_SELECTALL, "Select &All\tCtrl+A", "Select all text")
menubar.Append(edit_menu, "&Edit")
# Format menu
format_menu = wx.Menu()
bold_item = format_menu.Append(wx.ID_ANY, "&Bold\tCtrl+B", "Bold text")
italic_item = format_menu.Append(wx.ID_ANY, "&Italic\tCtrl+I", "Italic text")
underline_item = format_menu.Append(wx.ID_ANY, "&Underline\tCtrl+U", "Underline text")
format_menu.AppendSeparator()
font_color_item = format_menu.Append(wx.ID_ANY, "&Text Color...", "Change text color")
bg_color_item = format_menu.Append(wx.ID_ANY, "&Highlight Color...", "Change highlight color")
format_menu.AppendSeparator()
clear_format_item = format_menu.Append(wx.ID_ANY, "&Clear Formatting\tCtrl+Space", "Clear text formatting")
menubar.Append(format_menu, "F&ormat")
self.SetMenuBar(menubar)
# Bind menu events
self.Bind(wx.EVT_MENU, self.on_save_to_file, export_item)
self.Bind(wx.EVT_MENU, self.on_load_from_file, import_item)
self.Bind(wx.EVT_MENU, self.on_export_text, export_text_item)
self.Bind(wx.EVT_MENU, self.on_close, exit_item)
self.Bind(wx.EVT_MENU, self.on_new_note, new_note_item)
self.Bind(wx.EVT_MENU, self.on_delete_note, delete_note_item)
self.Bind(wx.EVT_MENU, self.on_rename_note, rename_note_item)
self.Bind(wx.EVT_MENU, self.on_bold, bold_item)
self.Bind(wx.EVT_MENU, self.on_italic, italic_item)
self.Bind(wx.EVT_MENU, self.on_underline, underline_item)
self.Bind(wx.EVT_MENU, self.on_text_color, font_color_item)
self.Bind(wx.EVT_MENU, self.on_background_color, bg_color_item)
self.Bind(wx.EVT_MENU, self.on_clear_formatting, clear_format_item)
self.Bind(wx.EVT_MENU, self.on_undo, undo_item)
self.Bind(wx.EVT_MENU, self.on_redo, redo_item)
self.Bind(wx.EVT_MENU, self.on_cut, cut_item)
self.Bind(wx.EVT_MENU, self.on_copy, copy_item)
self.Bind(wx.EVT_MENU, self.on_paste, paste_item)
self.Bind(wx.EVT_MENU, self.on_select_all, select_all_item)
def setup_editor_toolbar(self, parent):
self.toolbar = wx.Panel(parent)
toolbar_sizer = wx.BoxSizer(wx.HORIZONTAL)
# Text formatting buttons
self.bold_btn = wx.Button(self.toolbar, label="B", size=(35, 30))
self.bold_btn.SetFont(self.bold_btn.GetFont().Bold())
self.bold_btn.Bind(wx.EVT_BUTTON, self.on_bold)
self.bold_btn.SetToolTip("Bold (Ctrl+B)")
toolbar_sizer.Add(self.bold_btn, 0, wx.ALL, 2)
self.italic_btn = wx.Button(self.toolbar, label="I", size=(35, 30))
font = self.italic_btn.GetFont()
font.MakeItalic()
self.italic_btn.SetFont(font)
self.italic_btn.Bind(wx.EVT_BUTTON, self.on_italic)
self.italic_btn.SetToolTip("Italic (Ctrl+I)")
toolbar_sizer.Add(self.italic_btn, 0, wx.ALL, 2)
self.underline_btn = wx.Button(self.toolbar, label="U", size=(35, 30))
self.underline_btn.Bind(wx.EVT_BUTTON, self.on_underline)
self.underline_btn.SetToolTip("Underline (Ctrl+U)")
toolbar_sizer.Add(self.underline_btn, 0, wx.ALL, 2)
toolbar_sizer.Add(wx.StaticLine(self.toolbar, style=wx.LI_VERTICAL), 0,
wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
# Font size
toolbar_sizer.Add(wx.StaticText(self.toolbar, label="Size:"), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2)
self.font_size_choice = wx.Choice(self.toolbar, choices=['8', '9', '10', '11', '12', '14', '16', '18', '20', '24'])
self.font_size_choice.SetSelection(3) # Default to 11
self.font_size_choice.Bind(wx.EVT_CHOICE, self.on_font_size)
self.font_size_choice.SetToolTip("Font Size")
toolbar_sizer.Add(self.font_size_choice, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2)
toolbar_sizer.Add(wx.StaticLine(self.toolbar, style=wx.LI_VERTICAL), 0,
wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
# Alignment buttons
self.align_left_btn = wx.Button(self.toolbar, label="Left", size=(45, 30))
self.align_left_btn.Bind(wx.EVT_BUTTON, self.on_align_left)
self.align_left_btn.SetToolTip("Align Left")
toolbar_sizer.Add(self.align_left_btn, 0, wx.ALL, 2)
self.align_center_btn = wx.Button(self.toolbar, label="Center", size=(55, 30))
self.align_center_btn.Bind(wx.EVT_BUTTON, self.on_align_center)
self.align_center_btn.SetToolTip("Align Center")
toolbar_sizer.Add(self.align_center_btn, 0, wx.ALL, 2)
self.align_right_btn = wx.Button(self.toolbar, label="Right", size=(45, 30))
self.align_right_btn.Bind(wx.EVT_BUTTON, self.on_align_right)
self.align_right_btn.SetToolTip("Align Right")
toolbar_sizer.Add(self.align_right_btn, 0, wx.ALL, 2)
toolbar_sizer.Add(wx.StaticLine(self.toolbar, style=wx.LI_VERTICAL), 0,
wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
# Color buttons
self.text_color_btn = wx.Button(self.toolbar, label="Text Color", size=(80, 30))
self.text_color_btn.Bind(wx.EVT_BUTTON, self.on_text_color)
self.text_color_btn.SetToolTip("Text Color")
toolbar_sizer.Add(self.text_color_btn, 0, wx.ALL, 2)
self.bg_color_btn = wx.Button(self.toolbar, label="Highlight", size=(80, 30))
self.bg_color_btn.Bind(wx.EVT_BUTTON, self.on_background_color)
self.bg_color_btn.SetToolTip("Highlight Color")
toolbar_sizer.Add(self.bg_color_btn, 0, wx.ALL, 2)
toolbar_sizer.AddStretchSpacer()
# Clear formatting
self.clear_fmt_btn = wx.Button(self.toolbar, label="Clear Format", size=(100, 30))
self.clear_fmt_btn.Bind(wx.EVT_BUTTON, self.on_clear_formatting)
self.clear_fmt_btn.SetToolTip("Remove all formatting (Ctrl+Space)")
toolbar_sizer.Add(self.clear_fmt_btn, 0, wx.ALL, 2)
self.toolbar.SetSizer(toolbar_sizer)
self.toolbar.SetBackgroundColour(wx.Colour(230, 230, 230))
def enable_editor(self, enable):
self.title_ctrl.Enable(enable)
self.editor.Enable(enable)
self.delete_btn.Enable(enable)
# Enable/disable toolbar buttons
for child in self.toolbar.GetChildren():
if isinstance(child, wx.Button) or isinstance(child, wx.Choice):
child.Enable(enable)
def update_status(self, message: str):
"""Update status text with timestamp"""
timestamp = wx.DateTime.Now().FormatTime()
self.status_bar.SetStatusText(f"{timestamp} - {message}", 0)
# Update save status in second field
if self.current_note_key and self.content_changed:
time_since_change = int(time.time() - self.notes_data[self.current_note_key]['last_modified'])
self.status_bar.SetStatusText(f"Unsaved changes ({time_since_change}s)", 1)
elif self.current_note_key:
time_since_save = int(time.time() - self.last_save_time)
self.status_bar.SetStatusText(f"Saved {time_since_save}s ago", 1)
else:
self.status_bar.SetStatusText("", 1)
def load_notes_list(self):
self.notes_list.Clear()
sorted_keys = sorted(self.notes_data.keys(),
key=lambda k: self.notes_data[k].get('last_modified', 0),
reverse=True)
for key in sorted_keys:
display_title = self.notes_data[key].get('title', key)
self.notes_list.Append(display_title, key)
def save_current_note_formatting(self) -> bool:
"""Save the current note's formatting to XML"""
if not self.current_note_key or self.is_closing:
return False
try:
# Save plain text
self.notes_data[self.current_note_key]['content'] = self.editor.GetValue()
# Save XML with formatting using a temporary file
with tempfile.NamedTemporaryFile(mode='w+', suffix='.xml', delete=False, encoding='utf-8') as tmp:
tmp_path = tmp.name
# Save to temp file
handler = rt.RichTextXMLHandler()
buffer = self.editor.GetBuffer()
if buffer and handler.SaveFile(buffer, tmp_path):
# Read back the XML content
with open(tmp_path, 'r', encoding='utf-8') as f:
xml_content = f.read()
self.notes_data[self.current_note_key]['xml_content'] = xml_content
self.notes_data[self.current_note_key]['last_modified'] = time.time()
# Clean up temp file
try:
os.unlink(tmp_path)
except Exception:
pass
self.content_changed = False
self.last_save_time = time.time()
return True
except Exception as e:
print(f"Error saving note formatting: {e}")
return False
def auto_save_current_note(self):
"""Auto-save current note if changes were made"""
if self.current_note_key and self.content_changed:
if self.save_current_note_formatting():
return True
return False
def safe_load_xml_content(self, xml_content: str) -> bool:
"""Safely load XML content with error handling"""
try:
# Create a temporary file for XML loading
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False, encoding='utf-8') as tmp:
tmp.write(xml_content)
tmp_path = tmp.name
# Clear current buffer first
self.editor.Clear()
# Load from temp file
handler = rt.RichTextXMLHandler()
success = handler.LoadFile(self.editor.GetBuffer(), tmp_path)
# Clean up temp file
try:
os.unlink(tmp_path)
except Exception:
pass
if success:
self.editor.Refresh()
return True
else:
# If XML load fails, fall back to plain text
return False
except Exception as e:
print(f"Error loading XML content: {e}")
return False
def on_note_select(self, event):
# Auto-save previous note before switching
if self.current_note_key:
self.auto_save_current_note()
selection = self.notes_list.GetSelection()
if selection != wx.NOT_FOUND:
key = self.notes_list.GetClientData(selection)
self.current_note_key = key
note_data = self.notes_data[key]
self.updating_title = True
self.title_ctrl.SetValue(note_data.get('title', ''))
self.updating_title = False
# Load content - check if it's XML format or plain text
content = note_data.get('content', '')
xml_content = note_data.get('xml_content', '')
# Clear any existing content first
self.editor.Clear()
if xml_content:
# Try to load rich text from XML
if not self.safe_load_xml_content(xml_content):
# Fallback to plain text if XML load fails
self.editor.SetValue(content)
elif content:
# Fallback to plain text
self.editor.SetValue(content)
else:
self.editor.Clear()
self.enable_editor(True)
self.editor.SetFocus()
self.content_changed = False
self.update_status(f"Loaded note: {note_data.get('title', 'Untitled')}")
def on_note_double_click(self, event):
"""Rename note on double click"""
self.on_rename_note(event)
def on_new_note(self, event):
note_id = f"note_{int(time.time() * 1000)}" # More precise timestamp
title = f"New Note {len(self.notes_data) + 1}"
self.notes_data[note_id] = {
'title': title,
'content': '',
'xml_content': '',
'created': time.time(),
'last_modified': time.time()
}
self.load_notes_list()
# Select and edit the new note
for i in range(self.notes_list.GetCount()):
if self.notes_list.GetClientData(i) == note_id:
self.notes_list.SetSelection(i)
self.on_note_select(event)
self.title_ctrl.SetFocus()
self.title_ctrl.SelectAll()
break
def on_delete_note(self, event):
if self.current_note_key:
note_title = self.notes_data[self.current_note_key].get('title', 'this note')
result = wx.MessageBox(
f"Are you sure you want to delete '{note_title}'?",
"Confirm Delete",
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION
)
if result == wx.YES:
del self.notes_data[self.current_note_key]
self.current_note_key = None
self.load_notes_list()
self.title_ctrl.SetValue('')
self.editor.Clear()
self.enable_editor(False)
self.update_status("Note deleted")
def on_rename_note(self, event):
"""Rename current note by focusing on title field"""
if self.current_note_key:
self.title_ctrl.SetFocus()
self.title_ctrl.SelectAll()
def on_title_change(self, event):
if self.current_note_key and not self.updating_title:
new_title = self.title_ctrl.GetValue().strip()
if new_title:
self.notes_data[self.current_note_key]['title'] = new_title
self.notes_data[self.current_note_key]['last_modified'] = time.time()
# Update the display in the list
for i in range(self.notes_list.GetCount()):
if self.notes_list.GetClientData(i) == self.current_note_key:
self.notes_list.SetString(i, new_title)
break
self.content_changed = True
# Auto-save immediately on title change
self.auto_save_current_note()
self.update_status("Title updated")
def on_title_lose_focus(self, event):
"""Auto-save when title loses focus"""
if self.content_changed:
self.auto_save_current_note()
event.Skip()
def on_content_change(self, event):
"""Mark content as changed for auto-save"""
if self.current_note_key:
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
# Trigger immediate auto-save on content change
wx.CallLater(1000, self.auto_save_current_note) # Save after 1 second delay
event.Skip()
def on_editor_lose_focus(self, event):
"""Auto-save when editor loses focus"""
if self.content_changed:
self.auto_save_current_note()
event.Skip()
def on_auto_save(self, event):
"""Auto-save current note on timer"""
if self.auto_save_current_note():
self.update_status(f"Auto-saved at {wx.DateTime.Now().FormatTime()}")
def on_status_update(self, event):
"""Update status bar"""
self.update_status("Ready")
# Text formatting methods
def on_bold(self, event):
self.editor.ApplyBoldToSelection()
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_italic(self, event):
self.editor.ApplyItalicToSelection()
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_underline(self, event):
self.editor.ApplyUnderlineToSelection()
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_font_size(self, event):
size = int(self.font_size_choice.GetStringSelection())
attr = rt.RichTextAttr()
attr.SetFontSize(size)
range_obj = self.editor.GetSelectionRange()
if range_obj.GetLength() > 0:
self.editor.SetStyle(range_obj, attr)
else:
# Apply to next typed text
self.editor.SetDefaultStyle(attr)
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_align_left(self, event):
attr = rt.RichTextAttr()
attr.SetAlignment(wx.TEXT_ALIGNMENT_LEFT)
# Get current paragraph range
pos = self.editor.GetInsertionPoint()
para = self.editor.GetBuffer().GetParagraphAtPosition(pos)
if para:
range_obj = para.GetRange()
self.editor.SetStyle(range_obj, attr)
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_align_center(self, event):
attr = rt.RichTextAttr()
attr.SetAlignment(wx.TEXT_ALIGNMENT_CENTRE)
pos = self.editor.GetInsertionPoint()
para = self.editor.GetBuffer().GetParagraphAtPosition(pos)
if para:
range_obj = para.GetRange()
self.editor.SetStyle(range_obj, attr)
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_align_right(self, event):
attr = rt.RichTextAttr()
attr.SetAlignment(wx.TEXT_ALIGNMENT_RIGHT)
pos = self.editor.GetInsertionPoint()
para = self.editor.GetBuffer().GetParagraphAtPosition(pos)
if para:
range_obj = para.GetRange()
self.editor.SetStyle(range_obj, attr)
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_text_color(self, event):
color_data = wx.ColourData()
color_data.SetChooseFull(True)
dlg = wx.ColourDialog(self, color_data)
if dlg.ShowModal() == wx.ID_OK:
color = dlg.GetColourData().GetColour()
attr = rt.RichTextAttr()
attr.SetTextColour(color)
range_obj = self.editor.GetSelectionRange()
if range_obj.GetLength() > 0:
self.editor.SetStyle(range_obj, attr)
else:
self.editor.SetDefaultStyle(attr)
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
dlg.Destroy()
def on_background_color(self, event):
color_data = wx.ColourData()
color_data.SetChooseFull(True)
dlg = wx.ColourDialog(self, color_data)
if dlg.ShowModal() == wx.ID_OK:
color = dlg.GetColourData().GetColour()
attr = rt.RichTextAttr()
attr.SetBackgroundColour(color)
range_obj = self.editor.GetSelectionRange()
if range_obj.GetLength() > 0:
self.editor.SetStyle(range_obj, attr)
else:
self.editor.SetDefaultStyle(attr)
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
dlg.Destroy()
def on_clear_formatting(self, event):
range_obj = self.editor.GetSelectionRange()
if range_obj.GetLength() > 0:
# Get the text
text = self.editor.GetRange(range_obj.GetStart(), range_obj.GetEnd())
# Delete the range and reinsert as plain text
self.editor.Delete(range_obj)
# Set basic style
attr = rt.RichTextAttr()
attr.SetFontSize(11)
attr.SetFontFaceName("Segoe UI")
attr.SetTextColour(wx.BLACK)
attr.SetBackgroundColour(wx.NullColour)
self.editor.BeginStyle(attr)
self.editor.WriteText(text)
self.editor.EndStyle()
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
# Menu command handlers
def on_undo(self, event):
if self.editor.CanUndo():
self.editor.Undo()
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_redo(self, event):
if self.editor.CanRedo():
self.editor.Redo()
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_cut(self, event):
self.editor.Cut()
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_copy(self, event):
self.editor.Copy()
def on_paste(self, event):
self.editor.Paste()
self.content_changed = True
self.notes_data[self.current_note_key]['last_modified'] = time.time()
def on_select_all(self, event):
self.editor.SelectAll()
def on_export_text(self, event):
if not self.current_note_key:
return
note_data = self.notes_data[self.current_note_key]
title = note_data.get('title', 'note')
with wx.FileDialog(self, "Export note as text",
defaultFile=f"{title}.txt",
wildcard="Text files (*.txt)|*.txt",
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg:
if dlg.ShowModal() == wx.ID_OK:
filename = dlg.GetPath()
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(f"{title}\n")
f.write("=" * len(title) + "\n\n")
f.write(self.editor.GetValue())
wx.MessageBox("Note exported successfully!", "Success",
wx.OK | wx.ICON_INFORMATION)
self.update_status("Note exported to text file")
except Exception as e:
wx.MessageBox(f"Error exporting note: {e}", "Error",
wx.OK | wx.ICON_ERROR)
def on_save_to_file(self, event):
# Save current note first
if self.current_note_key:
self.auto_save_current_note()
with wx.FileDialog(self, "Export notes to file",
wildcard="JSON files (*.json)|*.json",
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg:
if dlg.ShowModal() == wx.ID_OK:
filename = dlg.GetPath()
try:
# Convert defaultdict to regular dict for JSON serialization
notes_dict = {k: dict(v) for k, v in self.notes_data.items()}
with open(filename, 'w', encoding='utf-8') as f:
json.dump(notes_dict, f, indent=2, ensure_ascii=False)
wx.MessageBox("Notes exported successfully!", "Success",
wx.OK | wx.ICON_INFORMATION)
self.update_status("All notes exported to file")
except Exception as e:
wx.MessageBox(f"Error saving file: {e}", "Error",
wx.OK | wx.ICON_ERROR)
def on_load_from_file(self, event):
with wx.FileDialog(self, "Import notes from file",
wildcard="JSON files (*.json)|*.json",
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as dlg:
if dlg.ShowModal() == wx.ID_OK:
filename = dlg.GetPath()
try:
with open(filename, 'r', encoding='utf-8') as f:
loaded_data = json.load(f)
# Auto-save current note before importing
if self.current_note_key:
self.auto_save_current_note()
# Merge with existing notes
result = wx.MessageBox(
"Replace existing notes or merge with them?\n\nYes = Replace, No = Merge",
"Import Options",
wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION
)
if result == wx.YES: # Replace
self.notes_data.clear()
self.notes_data.update(loaded_data)
self.update_status("All notes replaced with imported file")
elif result == wx.NO: # Merge
self.notes_data.update(loaded_data)
self.update_status("Imported notes merged with existing notes")
else: # Cancel
return
self.load_notes_list()
self.current_note_key = None
self.title_ctrl.SetValue('')
self.editor.Clear()
self.enable_editor(False)
wx.MessageBox("Notes imported successfully!", "Success",
wx.OK | wx.ICON_INFORMATION)
except Exception as e:
wx.MessageBox(f"Error loading file: {e}", "Error",
wx.OK | wx.ICON_ERROR)
def on_close(self, event):
"""Save everything before closing"""
self.is_closing = True
# Stop timers
self.save_timer.Stop()
self.status_timer.Stop()
# Save current note
if self.current_note_key:
self.auto_save_current_note()
self.update_status("Final auto-save completed")
# Notify parent if it has a callback
if self.parent and hasattr(self.parent, 'on_notes_closed'):
self.parent.on_notes_closed()
self.Destroy()
def get_notes_data(self):
"""Get all notes data"""
# Save current note before returning
if self.current_note_key:
self.auto_save_current_note()
return self.notes_data

396
src/PrivacyNoticeDialog.py Normal file
View File

@@ -0,0 +1,396 @@
import wx
import platform
import psutil
import socket
import getpass
import subprocess
import re
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class PrivacyNoticeDialog(wx.Dialog):
def __init__(self, parent):
super().__init__(parent, title="Privacy Notice", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
self.parent = parent
# Calculate optimal dialog size based on screen size
screen_width, screen_height = wx.DisplaySize()
self.max_width = min(700, screen_width * 0.75)
self.max_height = min(700, screen_height * 0.8)
self.SetMinSize((450, 400))
self.SetSize((self.max_width, self.max_height))
# Create main sizer
main_sizer = wx.BoxSizer(wx.VERTICAL)
# Create header with icon and title
header_sizer = wx.BoxSizer(wx.HORIZONTAL)
# Add info icon
info_icon = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_MESSAGE_BOX, (32, 32))
icon_ctrl = wx.StaticBitmap(self, -1, info_icon)
header_sizer.Add(icon_ctrl, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
# Add title
title_text = wx.StaticText(self, label="Privacy and System Information")
title_font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
title_text.SetFont(title_font)
header_sizer.Add(title_text, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
main_sizer.Add(header_sizer, 0, wx.EXPAND)
# Create scrolled window for the content
self.scrolled_win = wx.ScrolledWindow(self)
self.scrolled_win.SetScrollRate(10, 10)
scrolled_sizer = wx.BoxSizer(wx.VERTICAL)
# Get system information
system_info = self.get_system_info()
# Security and privacy notice section (moved to top)
security_text = wx.StaticText(self.scrolled_win, label="Security and Privacy Notice:")
security_font = wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
security_text.SetFont(security_font)
scrolled_sizer.Add(security_text, 0, wx.ALL, 5)
# Detect potential security features
security_warnings = self.get_security_warnings(system_info)
privacy_notice = (
"wxIRC is an Open Source project and must not be distributed for commercial purposes.\n\n"
"Security Considerations:\n"
f"{security_warnings}\n\n"
"Important Reminders:\n"
"Your system contains hardware level security processors that operate independently\n"
" Network traffic may be monitored by various entities\n"
" Never discuss or plan illegal activities through any communication platform\n"
" Keep your system and software updated for security patches\n"
" Use strong, unique passwords and enable 2FA where available\n\n"
"This application collects no personal data and makes no network connections\n"
"beyond those required for IRC functionality that you explicitly initiate."
)
self.body_text = wx.StaticText(self.scrolled_win, label=privacy_notice)
scrolled_sizer.Add(self.body_text, 0, wx.ALL | wx.EXPAND, 10)
# Add separator
scrolled_sizer.Add(wx.StaticLine(self.scrolled_win), 0, wx.EXPAND | wx.ALL, 10)
# System information section (moved to bottom)
sysinfo_text = wx.StaticText(self.scrolled_win, label="System Information:")
sysinfo_text.SetFont(security_font)
scrolled_sizer.Add(sysinfo_text, 0, wx.ALL, 5)
# System info details
info_details = (
f"Operating System: {system_info['os']}\n"
f"Architecture: {system_info['architecture']}\n"
f"Processor: {system_info['processor']}\n"
f"Physical Cores: {system_info['physical_cores']}\n"
f"Total Cores: {system_info['total_cores']}\n"
f"Max Frequency: {system_info['max_frequency']} MHz\n"
f"Total RAM: {system_info['total_ram']} GB\n"
f"Available RAM: {system_info['available_ram']} GB\n"
f"Hostname: {system_info['hostname']}\n"
f"Username: {system_info['username']}\n"
f"Python Version: {system_info['python_version']}\n"
f"wxPython Version: {system_info['wx_version']}"
)
info_text = wx.StaticText(self.scrolled_win, label=info_details)
scrolled_sizer.Add(info_text, 0, wx.ALL | wx.EXPAND, 10)
self.scrolled_win.SetSizer(scrolled_sizer)
main_sizer.Add(self.scrolled_win, 1, wx.EXPAND | wx.ALL, 5)
# Add OK button
ok_btn = wx.Button(self, wx.ID_OK, "I Understand and Continue")
ok_btn.SetDefault()
ok_btn.SetMinSize((160, 35))
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
btn_sizer.AddStretchSpacer()
btn_sizer.Add(ok_btn, 0, wx.ALL, 10)
btn_sizer.AddStretchSpacer()
main_sizer.Add(btn_sizer, 0, wx.EXPAND)
self.SetSizer(main_sizer)
# Center on parent
self.CentreOnParent()
# Bind events
self.Bind(wx.EVT_BUTTON, self.on_ok, ok_btn)
self.Bind(wx.EVT_SIZE, self.on_resize)
# Fit the dialog to content
self.Fit()
self.adjust_to_screen()
def get_system_info(self):
"""Gather comprehensive system information with improved processor detection"""
try:
# CPU information
cpu_freq = psutil.cpu_freq()
max_freq = int(cpu_freq.max) if cpu_freq else "N/A"
# Memory information
memory = psutil.virtual_memory()
total_ram_gb = round(memory.total / (1024**3), 1)
available_ram_gb = round(memory.available / (1024**3), 1)
# Platform information
system = platform.system()
if system == "Windows":
os_info = f"Windows {platform.release()}"
elif system == "Linux":
# Try to get distro info for Linux
try:
with open('/etc/os-release', 'r') as f:
lines = f.readlines()
for line in lines:
if line.startswith('PRETTY_NAME='):
os_info = line.split('=', 1)[1].strip().strip('"')
break
else:
os_info = f"Linux ({platform.release()})"
except:
os_info = f"Linux ({platform.release()})"
elif system == "Darwin":
os_info = f"macOS {platform.mac_ver()[0]}"
else:
os_info = f"{system} {platform.release()}"
# Improved processor detection
processor = self.detect_processor()
return {
'os': os_info,
'architecture': platform.architecture()[0],
'processor': processor,
'physical_cores': psutil.cpu_count(logical=False),
'total_cores': psutil.cpu_count(logical=True),
'max_frequency': max_freq,
'total_ram': total_ram_gb,
'available_ram': available_ram_gb,
'hostname': socket.gethostname(),
'username': getpass.getuser(),
'python_version': platform.python_version(),
'wx_version': wx.VERSION_STRING,
'platform': platform.platform()
}
except Exception as e:
logger.error(f"Error gathering system info: {e}")
return {
'os': "Unknown",
'architecture': "Unknown",
'processor': "Unknown",
'physical_cores': "N/A",
'total_cores': "N/A",
'max_frequency': "N/A",
'total_ram': "N/A",
'available_ram': "N/A",
'hostname': "Unknown",
'username': "Unknown",
'python_version': platform.python_version(),
'wx_version': wx.VERSION_STRING,
'platform': "Unknown"
}
def detect_processor(self):
"""Improved processor detection for Ryzen and other CPUs"""
try:
# Method 1: Try platform.processor() first
processor = platform.processor()
if processor and processor.strip() and processor != "unknown":
return processor.strip()
# Method 2: Try CPU info file on Linux
if platform.system() == "Linux":
try:
with open('/proc/cpuinfo', 'r') as f:
cpuinfo = f.read()
# Look for model name
for line in cpuinfo.split('\n'):
if line.startswith('model name') or line.startswith('Model Name') or line.startswith('Processor'):
parts = line.split(':', 1)
if len(parts) > 1:
cpu_name = parts[1].strip()
if cpu_name:
return cpu_name
# Look for hardware field
for line in cpuinfo.split('\n'):
if line.startswith('Hardware'):
parts = line.split(':', 1)
if len(parts) > 1:
cpu_name = parts[1].strip()
if cpu_name:
return cpu_name
except Exception as e:
logger.debug(f"Error reading /proc/cpuinfo: {e}")
# Method 3: Try lscpu command
try:
result = subprocess.run(['lscpu'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
for line in result.stdout.split('\n'):
if 'Model name:' in line:
parts = line.split(':', 1)
if len(parts) > 1:
cpu_name = parts[1].strip()
if cpu_name:
return cpu_name
except Exception as e:
logger.debug(f"Error running lscpu: {e}")
# Method 4: Try sysctl on macOS
if platform.system() == "Darwin":
try:
result = subprocess.run(['sysctl', '-n', 'machdep.cpu.brand_string'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
cpu_name = result.stdout.strip()
if cpu_name:
return cpu_name
except Exception as e:
logger.debug(f"Error running sysctl: {e}")
# Method 5: Try WMI on Windows
if platform.system() == "Windows":
try:
import ctypes
from ctypes import wintypes
# Try kernel32 GetNativeSystemInfo
kernel32 = ctypes.windll.kernel32
class SYSTEM_INFO(ctypes.Structure):
_fields_ = [
("wProcessorArchitecture", wintypes.WORD),
("wReserved", wintypes.WORD),
("dwPageSize", wintypes.DWORD),
("lpMinimumApplicationAddress", ctypes.c_void_p),
("lpMaximumApplicationAddress", ctypes.c_void_p),
("dwActiveProcessorMask", ctypes.c_void_p),
("dwNumberOfProcessors", wintypes.DWORD),
("dwProcessorType", wintypes.DWORD),
("dwAllocationGranularity", wintypes.DWORD),
("wProcessorLevel", wintypes.WORD),
("wProcessorRevision", wintypes.WORD)
]
system_info = SYSTEM_INFO()
kernel32.GetNativeSystemInfo(ctypes.byref(system_info))
# Map processor type to name
processor_types = {
586: "Pentium",
8664: "x64",
}
if system_info.dwProcessorType in processor_types:
return processor_types[system_info.dwProcessorType]
except Exception as e:
logger.debug(f"Error with Windows processor detection: {e}")
# Final fallback
return f"Unknown ({platform.machine()})"
except Exception as e:
logger.error(f"Error in processor detection: {e}")
return "Unknown"
def get_security_warnings(self, system_info):
"""Generate security warnings based on system capabilities"""
warnings = []
try:
# Check for Intel ME / AMD PSP indicators
processor_lower = system_info['processor'].lower()
if 'intel' in processor_lower:
warnings.append(" Intel Processor detected: Management Engine (ME) present")
if 'core' in processor_lower and any(gen in processor_lower for gen in ['i3', 'i5', 'i7', 'i9']):
warnings.append(" Modern Intel Core processor: ME capabilities active")
elif 'amd' in processor_lower:
warnings.append(" AMD Processor detected: Platform Security Processor (PSP) present")
if 'ryzen' in processor_lower:
warnings.append(" Modern AMD Ryzen processor: PSP capabilities active")
if '5600x' in processor_lower:
warnings.append(" AMD Ryzen 5 5600X: Zen 3 architecture with PSP")
# Check for specific AMD models
if '5600x' in processor_lower:
warnings.append(" AMD Ryzen 5 5600X detected: Hardware level security processor active")
# Check RAM size
if system_info['total_ram'] != "N/A" and system_info['total_ram'] > 8:
warnings.append(" System has substantial RAM capacity for background processes")
# Check core count
if system_info['total_cores'] != "N/A" and system_info['total_cores'] >= 4:
warnings.append(" Multi core system capable of parallel background processing")
# Network capabilities
warnings.append(" System has network connectivity capabilities")
warnings.append(" WiFi/Ethernet hardware present for data transmission")
# Operating system specific
os_lower = system_info['os'].lower()
if 'windows' in os_lower:
warnings.append(" Windows OS: Telemetry and background services active")
elif 'linux' in os_lower or 'arch' in os_lower:
warnings.append(" Linux OS: Generally more transparent, but hardware level components still active")
if 'arch' in os_lower:
warnings.append(" Arch Linux: Rolling release, ensure regular security updates")
elif 'mac' in os_lower:
warnings.append(" macOS: Apple T2/T1 security chip or Apple Silicon present")
except Exception as e:
logger.error(f"Error generating security warnings: {e}")
warnings = [" Unable to fully assess system security features"]
return "\n".join(warnings)
def adjust_to_screen(self):
"""Adjust dialog size and position to fit within screen bounds"""
screen_width, screen_height = wx.DisplaySize()
current_size = self.GetSize()
final_width = min(current_size.width, screen_width - 40)
final_height = min(current_size.height, screen_height - 40)
self.SetSize(final_width, final_height)
self.body_text.Wrap(final_width - 60)
self.Layout()
self.scrolled_win.FitInside()
self.CentreOnParent()
def on_resize(self, event):
"""Handle dialog resize to re-wrap text appropriately"""
try:
if hasattr(self, 'body_text'):
new_width = self.GetSize().width - 60
if new_width > 200:
self.body_text.Wrap(new_width)
self.scrolled_win.Layout()
self.scrolled_win.FitInside()
except Exception as e:
logger.error(f"Error in resize handler: {e}")
event.Skip()
def on_ok(self, event):
self.EndModal(wx.ID_OK)

53
src/SearchDialog.py Normal file
View File

@@ -0,0 +1,53 @@
import wx
class SearchDialog(wx.Dialog):
def __init__(self, parent):
super().__init__(parent, title="Search", style=wx.DEFAULT_DIALOG_STYLE)
self.parent = parent
sizer = wx.BoxSizer(wx.VERTICAL)
# Search input
search_sizer = wx.BoxSizer(wx.HORIZONTAL)
search_sizer.Add(wx.StaticText(self, label="Search:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
self.search_ctrl = wx.TextCtrl(self, size=(200, -1), style=wx.TE_PROCESS_ENTER)
self.search_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_search)
search_sizer.Add(self.search_ctrl, 1, wx.EXPAND | wx.ALL, 5)
sizer.Add(search_sizer, 0, wx.EXPAND)
# Options
options_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.case_sensitive = wx.CheckBox(self, label="Case sensitive")
self.whole_word = wx.CheckBox(self, label="Whole word")
options_sizer.Add(self.case_sensitive, 0, wx.ALL, 5)
options_sizer.Add(self.whole_word, 0, wx.ALL, 5)
sizer.Add(options_sizer, 0, wx.EXPAND)
# Buttons
btn_sizer = wx.StdDialogButtonSizer()
self.search_btn = wx.Button(self, wx.ID_OK, "Search")
self.search_btn.SetDefault()
self.search_btn.Bind(wx.EVT_BUTTON, self.on_search)
btn_sizer.AddButton(self.search_btn)
close_btn = wx.Button(self, wx.ID_CANCEL, "Close")
btn_sizer.AddButton(close_btn)
btn_sizer.Realize()
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER | wx.ALL, 10)
self.SetSizer(sizer)
self.Fit()
self.search_ctrl.SetFocus()
def on_search(self, event):
search_text = self.search_ctrl.GetValue().strip()
if search_text:
self.parent.perform_search(
search_text,
self.case_sensitive.IsChecked(),
self.whole_word.IsChecked()
)

BIN
src/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@@ -12,6 +12,11 @@ import queue
import os
import sys
from PrivacyNoticeDialog import PrivacyNoticeDialog
from IRCPanel import IRCPanel
from AboutDialog import AboutDialog
from NotesDialog import NotesDialog # Add this import
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
@@ -26,398 +31,6 @@ def get_resource_path(relative_path):
return os.path.join(base_path, relative_path)
import platform
import psutil
import socket
import getpass
import subprocess
import re
class PrivacyNoticeDialog(wx.Dialog):
def __init__(self, parent):
super().__init__(parent, title="Privacy Notice", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
self.parent = parent
# Calculate optimal dialog size based on screen size
screen_width, screen_height = wx.DisplaySize()
self.max_width = min(700, screen_width * 0.75)
self.max_height = min(700, screen_height * 0.8)
self.SetMinSize((450, 400))
self.SetSize((self.max_width, self.max_height))
# Create main sizer
main_sizer = wx.BoxSizer(wx.VERTICAL)
# Create header with icon and title
header_sizer = wx.BoxSizer(wx.HORIZONTAL)
# Add info icon
info_icon = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_MESSAGE_BOX, (32, 32))
icon_ctrl = wx.StaticBitmap(self, -1, info_icon)
header_sizer.Add(icon_ctrl, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
# Add title
title_text = wx.StaticText(self, label="Privacy and System Information")
title_font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
title_text.SetFont(title_font)
header_sizer.Add(title_text, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
main_sizer.Add(header_sizer, 0, wx.EXPAND)
# Create scrolled window for the content
self.scrolled_win = wx.ScrolledWindow(self)
self.scrolled_win.SetScrollRate(10, 10)
scrolled_sizer = wx.BoxSizer(wx.VERTICAL)
# Get system information
system_info = self.get_system_info()
# Create system info section
sysinfo_text = wx.StaticText(self.scrolled_win, label="System Information:")
sysinfo_font = wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
sysinfo_text.SetFont(sysinfo_font)
scrolled_sizer.Add(sysinfo_text, 0, wx.ALL, 5)
# System info details
info_details = (
f"Operating System: {system_info['os']}\n"
f"Architecture: {system_info['architecture']}\n"
f"Processor: {system_info['processor']}\n"
f"Physical Cores: {system_info['physical_cores']}\n"
f"Total Cores: {system_info['total_cores']}\n"
f"Max Frequency: {system_info['max_frequency']} MHz\n"
f"Total RAM: {system_info['total_ram']} GB\n"
f"Available RAM: {system_info['available_ram']} GB\n"
f"Hostname: {system_info['hostname']}\n"
f"Username: {system_info['username']}\n"
f"Python Version: {system_info['python_version']}\n"
f"wxPython Version: {system_info['wx_version']}"
)
info_text = wx.StaticText(self.scrolled_win, label=info_details)
scrolled_sizer.Add(info_text, 0, wx.ALL | wx.EXPAND, 10)
# Add separator
scrolled_sizer.Add(wx.StaticLine(self.scrolled_win), 0, wx.EXPAND | wx.ALL, 10)
# Security and privacy notice
security_text = wx.StaticText(self.scrolled_win, label="Security and Privacy Notice:")
security_text.SetFont(sysinfo_font)
scrolled_sizer.Add(security_text, 0, wx.ALL, 5)
# Detect potential security features
security_warnings = self.get_security_warnings(system_info)
privacy_notice = (
"wxIRC is an Open Source project and must not be distributed for commercial purposes.\n\n"
"Security Considerations:\n"
f"{security_warnings}\n\n"
"Important Reminders:\n"
"Your system contains hardware level security processors that operate independently\n"
"Network traffic may be monitored by various entities\n"
"Never discuss or plan illegal activities through any communication platform\n"
"Keep your system and software updated for security patches\n"
"Use strong, unique passwords and enable 2FA where available\n\n"
"This application collects no personal data and makes no network connections\n"
"beyond those required for IRC functionality that you explicitly initiate."
)
self.body_text = wx.StaticText(self.scrolled_win, label=privacy_notice)
scrolled_sizer.Add(self.body_text, 1, wx.ALL | wx.EXPAND, 10)
self.scrolled_win.SetSizer(scrolled_sizer)
main_sizer.Add(self.scrolled_win, 1, wx.EXPAND | wx.ALL, 5)
# Add OK button
ok_btn = wx.Button(self, wx.ID_OK, "I Understand and Continue")
ok_btn.SetDefault()
ok_btn.SetMinSize((160, 35))
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
btn_sizer.AddStretchSpacer()
btn_sizer.Add(ok_btn, 0, wx.ALL, 10)
btn_sizer.AddStretchSpacer()
main_sizer.Add(btn_sizer, 0, wx.EXPAND)
self.SetSizer(main_sizer)
# Center on parent
self.CentreOnParent()
# Bind events
self.Bind(wx.EVT_BUTTON, self.on_ok, ok_btn)
self.Bind(wx.EVT_SIZE, self.on_resize)
# Fit the dialog to content
self.Fit()
self.adjust_to_screen()
def get_system_info(self):
"""Gather comprehensive system information with improved processor detection"""
try:
# CPU information
cpu_freq = psutil.cpu_freq()
max_freq = int(cpu_freq.max) if cpu_freq else "N/A"
# Memory information
memory = psutil.virtual_memory()
total_ram_gb = round(memory.total / (1024**3), 1)
available_ram_gb = round(memory.available / (1024**3), 1)
# Platform information
system = platform.system()
if system == "Windows":
os_info = f"Windows {platform.release()}"
elif system == "Linux":
# Try to get distro info for Linux
try:
with open('/etc/os-release', 'r') as f:
lines = f.readlines()
for line in lines:
if line.startswith('PRETTY_NAME='):
os_info = line.split('=', 1)[1].strip().strip('"')
break
else:
os_info = f"Linux ({platform.release()})"
except:
os_info = f"Linux ({platform.release()})"
elif system == "Darwin":
os_info = f"macOS {platform.mac_ver()[0]}"
else:
os_info = f"{system} {platform.release()}"
# Improved processor detection
processor = self.detect_processor()
return {
'os': os_info,
'architecture': platform.architecture()[0],
'processor': processor,
'physical_cores': psutil.cpu_count(logical=False),
'total_cores': psutil.cpu_count(logical=True),
'max_frequency': max_freq,
'total_ram': total_ram_gb,
'available_ram': available_ram_gb,
'hostname': socket.gethostname(),
'username': getpass.getuser(),
'python_version': platform.python_version(),
'wx_version': wx.VERSION_STRING,
'platform': platform.platform()
}
except Exception as e:
logger.error(f"Error gathering system info: {e}")
return {
'os': "Unknown",
'architecture': "Unknown",
'processor': "Unknown",
'physical_cores': "N/A",
'total_cores': "N/A",
'max_frequency': "N/A",
'total_ram': "N/A",
'available_ram': "N/A",
'hostname': "Unknown",
'username': "Unknown",
'python_version': platform.python_version(),
'wx_version': wx.VERSION_STRING,
'platform': "Unknown"
}
def detect_processor(self):
"""Improved processor detection for Ryzen and other CPUs"""
try:
# Method 1: Try platform.processor() first
processor = platform.processor()
if processor and processor.strip() and processor != "unknown":
return processor.strip()
# Method 2: Try CPU info file on Linux
if platform.system() == "Linux":
try:
with open('/proc/cpuinfo', 'r') as f:
cpuinfo = f.read()
# Look for model name
for line in cpuinfo.split('\n'):
if line.startswith('model name') or line.startswith('Model Name') or line.startswith('Processor'):
parts = line.split(':', 1)
if len(parts) > 1:
cpu_name = parts[1].strip()
if cpu_name:
return cpu_name
# Look for hardware field
for line in cpuinfo.split('\n'):
if line.startswith('Hardware'):
parts = line.split(':', 1)
if len(parts) > 1:
cpu_name = parts[1].strip()
if cpu_name:
return cpu_name
except Exception as e:
logger.debug(f"Error reading /proc/cpuinfo: {e}")
# Method 3: Try lscpu command
try:
result = subprocess.run(['lscpu'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
for line in result.stdout.split('\n'):
if 'Model name:' in line:
parts = line.split(':', 1)
if len(parts) > 1:
cpu_name = parts[1].strip()
if cpu_name:
return cpu_name
except Exception as e:
logger.debug(f"Error running lscpu: {e}")
# Method 4: Try sysctl on macOS
if platform.system() == "Darwin":
try:
result = subprocess.run(['sysctl', '-n', 'machdep.cpu.brand_string'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
cpu_name = result.stdout.strip()
if cpu_name:
return cpu_name
except Exception as e:
logger.debug(f"Error running sysctl: {e}")
# Method 5: Try WMI on Windows
if platform.system() == "Windows":
try:
import ctypes
from ctypes import wintypes
# Try kernel32 GetNativeSystemInfo
kernel32 = ctypes.windll.kernel32
class SYSTEM_INFO(ctypes.Structure):
_fields_ = [
("wProcessorArchitecture", wintypes.WORD),
("wReserved", wintypes.WORD),
("dwPageSize", wintypes.DWORD),
("lpMinimumApplicationAddress", ctypes.c_void_p),
("lpMaximumApplicationAddress", ctypes.c_void_p),
("dwActiveProcessorMask", ctypes.c_void_p),
("dwNumberOfProcessors", wintypes.DWORD),
("dwProcessorType", wintypes.DWORD),
("dwAllocationGranularity", wintypes.DWORD),
("wProcessorLevel", wintypes.WORD),
("wProcessorRevision", wintypes.WORD)
]
system_info = SYSTEM_INFO()
kernel32.GetNativeSystemInfo(ctypes.byref(system_info))
# Map processor type to name
processor_types = {
586: "Pentium",
8664: "x64",
}
if system_info.dwProcessorType in processor_types:
return processor_types[system_info.dwProcessorType]
except Exception as e:
logger.debug(f"Error with Windows processor detection: {e}")
# Final fallback
return f"Unknown ({platform.machine()})"
except Exception as e:
logger.error(f"Error in processor detection: {e}")
return "Unknown"
def get_security_warnings(self, system_info):
"""Generate security warnings based on system capabilities"""
warnings = []
try:
# Check for Intel ME / AMD PSP indicators
processor_lower = system_info['processor'].lower()
if 'intel' in processor_lower:
warnings.append("Intel Processor detected: Management Engine (ME) present")
if 'core' in processor_lower and any(gen in processor_lower for gen in ['i3', 'i5', 'i7', 'i9']):
warnings.append("Modern Intel Core processor: ME capabilities active")
elif 'amd' in processor_lower:
warnings.append("AMD Processor detected: Platform Security Processor (PSP) present")
if 'ryzen' in processor_lower:
warnings.append("Modern AMD Ryzen processor: PSP capabilities active")
if '5600x' in processor_lower:
warnings.append("AMD Ryzen 5 5600X: Zen 3 architecture with PSP")
# Check for specific AMD models
if '5600x' in processor_lower:
warnings.append("AMD Ryzen 5 5600X detected: Hardware level security processor active")
# Check RAM size
if system_info['total_ram'] != "N/A" and system_info['total_ram'] > 8:
warnings.append("System has substantial RAM capacity for background processes")
# Check core count
if system_info['total_cores'] != "N/A" and system_info['total_cores'] >= 4:
warnings.append("Multi core system capable of parallel background processing")
# Network capabilities
warnings.append("System has network connectivity capabilities")
warnings.append("WiFi/Ethernet hardware present for data transmission")
# Operating system specific
os_lower = system_info['os'].lower()
if 'windows' in os_lower:
warnings.append("Windows OS: Telemetry and background services active")
elif 'linux' in os_lower or 'arch' in os_lower:
warnings.append("Linux OS: Generally more transparent, but hardware level components still active")
if 'arch' in os_lower:
warnings.append("Arch Linux: Rolling release, ensure regular security updates")
elif 'mac' in os_lower:
warnings.append("macOS: Apple T2/T1 security chip or Apple Silicon present")
except Exception as e:
logger.error(f"Error generating security warnings: {e}")
warnings = ["Unable to fully assess system security features"]
return "\n".join(warnings)
def adjust_to_screen(self):
"""Adjust dialog size and position to fit within screen bounds"""
screen_width, screen_height = wx.DisplaySize()
current_size = self.GetSize()
final_width = min(current_size.width, screen_width - 40)
final_height = min(current_size.height, screen_height - 40)
self.SetSize(final_width, final_height)
self.body_text.Wrap(final_width - 60)
self.Layout()
self.scrolled_win.FitInside()
self.CentreOnParent()
def on_resize(self, event):
"""Handle dialog resize to re-wrap text appropriately"""
try:
if hasattr(self, 'body_text'):
new_width = self.GetSize().width - 60
if new_width > 200:
self.body_text.Wrap(new_width)
self.scrolled_win.Layout()
self.scrolled_win.FitInside()
except Exception as e:
logger.error(f"Error in resize handler: {e}")
event.Skip()
def on_ok(self, event):
self.EndModal(wx.ID_OK)
class UIUpdate:
"""Thread-safe UI update container"""
def __init__(self, callback, *args, **kwargs):
@@ -425,454 +38,6 @@ class UIUpdate:
self.args = args
self.kwargs = kwargs
class SearchDialog(wx.Dialog):
def __init__(self, parent):
super().__init__(parent, title="Search", style=wx.DEFAULT_DIALOG_STYLE)
self.parent = parent
sizer = wx.BoxSizer(wx.VERTICAL)
# Search input
search_sizer = wx.BoxSizer(wx.HORIZONTAL)
search_sizer.Add(wx.StaticText(self, label="Search:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
self.search_ctrl = wx.TextCtrl(self, size=(200, -1), style=wx.TE_PROCESS_ENTER)
self.search_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_search)
search_sizer.Add(self.search_ctrl, 1, wx.EXPAND | wx.ALL, 5)
sizer.Add(search_sizer, 0, wx.EXPAND)
# Options
options_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.case_sensitive = wx.CheckBox(self, label="Case sensitive")
self.whole_word = wx.CheckBox(self, label="Whole word")
options_sizer.Add(self.case_sensitive, 0, wx.ALL, 5)
options_sizer.Add(self.whole_word, 0, wx.ALL, 5)
sizer.Add(options_sizer, 0, wx.EXPAND)
# Buttons
btn_sizer = wx.StdDialogButtonSizer()
self.search_btn = wx.Button(self, wx.ID_OK, "Search")
self.search_btn.SetDefault()
self.search_btn.Bind(wx.EVT_BUTTON, self.on_search)
btn_sizer.AddButton(self.search_btn)
close_btn = wx.Button(self, wx.ID_CANCEL, "Close")
btn_sizer.AddButton(close_btn)
btn_sizer.Realize()
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER | wx.ALL, 10)
self.SetSizer(sizer)
self.Fit()
self.search_ctrl.SetFocus()
def on_search(self, event):
search_text = self.search_ctrl.GetValue().strip()
if search_text:
self.parent.perform_search(
search_text,
self.case_sensitive.IsChecked(),
self.whole_word.IsChecked()
)
class AboutDialog(wx.Dialog):
def __init__(self, parent):
super().__init__(parent, title="About wxIRC Client", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
sizer = wx.BoxSizer(wx.VERTICAL)
# Application info
info_text = wx.StaticText(self, label="wxIRC Client")
info_font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
info_text.SetFont(info_font)
version_text = wx.StaticText(self, label=f"V 1.1.1.0")
version_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
version_text.SetFont(version_font)
features_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
# Add to sizer
sizer.Add(info_text, 0, wx.ALL | wx.ALIGN_CENTER, 10)
sizer.Add(version_text, 0, wx.ALL | wx.ALIGN_CENTER, 5)
# OK button
ok_btn = wx.Button(self, wx.ID_OK, "OK")
ok_btn.SetDefault()
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
btn_sizer.Add(ok_btn, 0, wx.ALIGN_CENTER | wx.ALL, 10)
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER)
self.SetSizer(sizer)
self.Fit()
self.Centre()
class IRCPanel(wx.Panel):
def __init__(self, parent, main_frame):
super().__init__(parent)
self.parent = parent
self.main_frame = main_frame
self.messages = []
sizer = wx.BoxSizer(wx.VERTICAL)
# Use a better font for chat with white theme
self.text_ctrl = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.TE_AUTO_URL)
# White theme colors
self.text_ctrl.SetBackgroundColour(wx.Colour(255, 255, 255)) # White background
self.text_ctrl.SetForegroundColour(wx.Colour(0, 0, 0)) # Black text
# Load appropriate font
self.font = self.load_system_font()
self.text_ctrl.SetFont(self.font)
sizer.Add(self.text_ctrl, 1, wx.EXPAND | wx.ALL, 0)
input_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.input_ctrl = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER)
self.input_ctrl.SetHint("Type message here …")
self.input_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_send)
self.input_ctrl.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
send_btn = wx.Button(self, label="Send")
send_btn.SetToolTip("Send message (Enter)")
send_btn.Bind(wx.EVT_BUTTON, self.on_send)
input_sizer.Add(self.input_ctrl, 1, wx.EXPAND | wx.ALL, 2)
input_sizer.Add(send_btn, 0, wx.ALL, 2)
sizer.Add(input_sizer, 0, wx.EXPAND | wx.ALL, 0)
self.SetSizer(sizer)
self.target = None
self.history = []
self.history_pos = -1
# Search state
self.search_text = ""
self.search_positions = []
self.current_search_index = -1
# Bind Ctrl+F for search
self.text_ctrl.Bind(wx.EVT_KEY_DOWN, self.on_text_key_down)
accel_tbl = wx.AcceleratorTable([(wx.ACCEL_CTRL, ord('F'), wx.ID_FIND)])
self.SetAcceleratorTable(accel_tbl)
self.Bind(wx.EVT_MENU, self.on_search, id=wx.ID_FIND)
def load_system_font(self):
"""Load appropriate system font with high DPI support"""
try:
# Get system DPI scale factor
dc = wx.ClientDC(self)
dpi_scale = dc.GetPPI().GetWidth() / 96.0 # 96 is standard DPI
# Calculate base font size based on DPI
base_size = 10
if dpi_scale > 1.5:
font_size = int(base_size * 1.5) # 150% scaling
elif dpi_scale > 1.25:
font_size = int(base_size * 1.25) # 125% scaling
else:
font_size = base_size
# Try system fonts in order of preference
font_families = [
(wx.FONTFAMILY_TELETYPE, "Consolas"),
(wx.FONTFAMILY_TELETYPE, "Courier New"),
(wx.FONTFAMILY_TELETYPE, "Monaco"),
(wx.FONTFAMILY_TELETYPE, "DejaVu Sans Mono"),
(wx.FONTFAMILY_TELETYPE, "Liberation Mono"),
]
for family, face_name in font_families:
font = wx.Font(font_size, family, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, face_name)
if font.IsOk():
logger.info(f"Using font: {face_name} at {font_size}pt")
return font
# Fallback to default monospace
font = wx.Font(font_size, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
logger.info("Using system monospace font as fallback")
return font
except Exception as e:
logger.error(f"Error loading system font: {e}")
# Ultimate fallback
return wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
def set_target(self, target):
self.target = target
def add_message(self, message, username=None, username_color=None, message_color=None, bold=False, italic=False, underline=False):
"""Thread-safe message addition with username coloring"""
try:
# Use CallAfter for thread safety
if wx.IsMainThread():
self._add_message_safe(message, username, username_color, message_color, bold, italic, underline)
else:
wx.CallAfter(self._add_message_safe, message, username, username_color, message_color, bold, italic, underline)
except Exception as e:
logger.error(f"Error in add_message: {e}")
def _add_message_safe(self, message, username=None, username_color=None, message_color=None, bold=False, italic=False, underline=False):
"""Actually add message - must be called from main thread"""
try:
self.messages.append(message)
# Save current position for formatting
start_pos = self.text_ctrl.GetLastPosition()
if username and username_color:
# Add username with its color
attr = wx.TextAttr()
attr.SetTextColour(username_color)
if bold:
attr.SetFontWeight(wx.FONTWEIGHT_BOLD)
if italic:
attr.SetFontStyle(wx.FONTSTYLE_ITALIC)
if underline:
attr.SetFontUnderlined(True)
attr.SetFont(self.font)
self.text_ctrl.SetDefaultStyle(attr)
self.text_ctrl.AppendText(username)
# Add the rest of the message with message color
attr = wx.TextAttr()
if message_color:
attr.SetTextColour(message_color)
else:
attr.SetTextColour(wx.Colour(0, 0, 0)) # Black text for white theme
attr.SetFont(self.font)
self.text_ctrl.SetDefaultStyle(attr)
# Append the message (without username if we already added it)
if username and username_color:
# Find the message part after username
message_text = message[message.find(username) + len(username):]
self.text_ctrl.AppendText(message_text + "\n")
else:
self.text_ctrl.AppendText(message + "\n")
# Auto-scroll to bottom
self.text_ctrl.ShowPosition(self.text_ctrl.GetLastPosition())
except Exception as e:
logger.error(f"Error adding message safely: {e}")
def add_formatted_message(self, timestamp, username, content, username_color=None, is_action=False):
"""Add a formatted message with colored username"""
try:
if is_action:
message = f"{timestamp}* {username} {content}"
self.add_message(message, f"* {username}", username_color, wx.Colour(128, 0, 128), italic=True) # Dark purple for actions
else:
message = f"{timestamp}<{username}> {content}"
self.add_message(message, f"<{username}>", username_color, wx.Colour(0, 0, 0)) # Black text
except Exception as e:
logger.error(f"Error adding formatted message: {e}")
def add_system_message(self, message, color=None, bold=False):
"""Add system message without username coloring"""
try:
if color is None:
color = wx.Colour(0, 0, 128) # Dark blue for system messages
self.add_message(message, None, None, color, bold, False, False)
except Exception as e:
logger.error(f"Error adding system message: {e}")
def on_text_key_down(self, event):
"""Handle key events in the text control"""
keycode = event.GetKeyCode()
if event.ControlDown() and keycode == ord('F'):
self.on_search(event)
else:
event.Skip()
def on_search(self, event):
"""Open search dialog"""
try:
dlg = SearchDialog(self)
dlg.ShowModal()
dlg.Destroy()
except Exception as e:
logger.error(f"Error in search: {e}")
def perform_search(self, search_text, case_sensitive=False, whole_word=False):
"""Perform text search in the chat history"""
try:
self.search_text = search_text
self.search_positions = []
self.current_search_index = -1
# Get all text
full_text = self.text_ctrl.GetValue()
if not full_text or not search_text:
return
# Prepare search parameters
search_flags = 0
if not case_sensitive:
# For manual search, we'll handle case sensitivity ourselves
search_text_lower = search_text.lower()
full_text_lower = full_text.lower()
# Manual search implementation since wx.TextCtrl doesn't have FindText
pos = 0
while pos < len(full_text):
if case_sensitive:
# Case sensitive search
found_pos = full_text.find(search_text, pos)
else:
# Case insensitive search
found_pos = full_text_lower.find(search_text_lower, pos)
if found_pos == -1:
break
# For whole word search, verify boundaries
if whole_word:
# Check if it's a whole word
is_word_start = (found_pos == 0 or not full_text[found_pos-1].isalnum())
is_word_end = (found_pos + len(search_text) >= len(full_text) or
not full_text[found_pos + len(search_text)].isalnum())
if is_word_start and is_word_end:
self.search_positions.append(found_pos)
pos = found_pos + 1 # Move forward to avoid infinite loop
else:
self.search_positions.append(found_pos)
pos = found_pos + len(search_text)
if self.search_positions:
self.current_search_index = 0
self.highlight_search_result()
self.main_frame.SetStatusText(f"Found {len(self.search_positions)} occurrences of '{search_text}'")
else:
self.main_frame.SetStatusText(f"Text '{search_text}' not found")
wx.Bell()
except Exception as e:
logger.error(f"Error performing search: {e}")
traceback.print_exc()
def highlight_search_result(self):
"""Highlight the current search result"""
try:
if not self.search_positions or self.current_search_index < 0:
return
pos = self.search_positions[self.current_search_index]
# Select the found text
self.text_ctrl.SetSelection(pos, pos + len(self.search_text))
self.text_ctrl.ShowPosition(pos)
# Update status
self.main_frame.SetStatusText(
f"Found {self.current_search_index + 1} of {len(self.search_positions)}: '{self.search_text}'"
)
except Exception as e:
logger.error(f"Error highlighting search result: {e}")
def find_next(self):
"""Find next occurrence"""
if self.search_positions:
self.current_search_index = (self.current_search_index + 1) % len(self.search_positions)
self.highlight_search_result()
def find_previous(self):
"""Find previous occurrence"""
if self.search_positions:
self.current_search_index = (self.current_search_index - 1) % len(self.search_positions)
self.highlight_search_result()
def on_key_down(self, event):
try:
keycode = event.GetKeyCode()
if keycode == wx.WXK_UP:
if self.history and self.history_pos < len(self.history) - 1:
self.history_pos += 1
self.input_ctrl.SetValue(self.history[-(self.history_pos + 1)])
elif keycode == wx.WXK_DOWN:
if self.history_pos > 0:
self.history_pos -= 1
self.input_ctrl.SetValue(self.history[-(self.history_pos + 1)])
elif self.history_pos == 0:
self.history_pos = -1
self.input_ctrl.Clear()
elif keycode == wx.WXK_TAB:
# Tab completion for nicknames
self.handle_tab_completion()
return # Don't skip to prevent default tab behavior
elif keycode == wx.WXK_F3:
# F3 for find next
if self.search_positions:
self.find_next()
return
elif event.ShiftDown() and keycode == wx.WXK_F3:
# Shift+F3 for find previous
if self.search_positions:
self.find_previous()
return
else:
event.Skip()
except Exception as e:
logger.error(f"Error in key handler: {e}")
event.Skip()
def handle_tab_completion(self):
"""Handle tab completion for nicknames"""
try:
current_text = self.input_ctrl.GetValue()
if not current_text or not self.target or not self.target.startswith('#'):
return
users = self.main_frame.channel_users.get(self.target, [])
if not users:
return
# Find word at cursor position
pos = self.input_ctrl.GetInsertionPoint()
text_before = current_text[:pos]
words = text_before.split()
if not words:
return
current_word = words[-1]
# Find matching nicks
matches = [user for user in users if user.lower().startswith(current_word.lower())]
if matches:
if len(matches) == 1:
# Single match - complete it
new_word = matches[0]
if ':' in text_before or text_before.strip().endswith(current_word):
# Replace the current word
new_text = text_before[:-len(current_word)] + new_word + current_text[pos:]
self.input_ctrl.SetValue(new_text)
self.input_ctrl.SetInsertionPoint(pos - len(current_word) + len(new_word))
else:
# Multiple matches - show in status
self.main_frame.SetStatusText(f"Tab completion: {', '.join(matches[:5])}{'...' if len(matches) > 5 else ''}")
except Exception as e:
logger.error(f"Error in tab completion: {e}")
def on_send(self, event):
try:
message = self.input_ctrl.GetValue().strip()
if message and self.target:
self.history.append(message)
self.history_pos = -1
self.main_frame.send_message(self.target, message)
self.input_ctrl.Clear()
except Exception as e:
logger.error(f"Error sending message: {e}")
class IRCFrame(wx.Frame):
def __init__(self):
super().__init__(None, title="wxIRC", size=(1200, 700))
@@ -903,6 +68,9 @@ class IRCFrame(wx.Frame):
self.away = False
self.timestamps = True
# Notes data - Add this
self.notes_data = defaultdict(dict)
# User color mapping - darker colors for white theme
self.user_colors = {}
self.available_colors = [
@@ -943,7 +111,7 @@ class IRCFrame(wx.Frame):
(wx.ACCEL_CTRL, ord('F'), wx.ID_FIND),
(wx.ACCEL_NORMAL, wx.WXK_F3, 1001),
(wx.ACCEL_SHIFT, wx.WXK_F3, 1002),
(wx.ACCEL_CTRL, wx.WXK_ESCAPE, 1003), # Quick Escape
(wx.ACCEL_SHIFT, wx.WXK_ESCAPE, 1003), # Quick Escape
])
self.SetAcceleratorTable(accel_tbl)
self.Bind(wx.EVT_MENU, self.on_global_search, id=wx.ID_FIND)
@@ -996,7 +164,7 @@ class IRCFrame(wx.Frame):
current_panel.find_previous()
def on_quick_escape(self, event):
"""Handle Ctrl+Esc for quick escape - exit immediately"""
"""Handle Shift+Esc for quick escape - exit immediately"""
try:
# Stop UI timer first
if self.ui_timer and self.ui_timer.IsRunning():
@@ -1061,6 +229,12 @@ class IRCFrame(wx.Frame):
self.connect_btn.Bind(wx.EVT_BUTTON, self.on_connect)
conn_box_sizer.Add(self.connect_btn, 0, wx.EXPAND | wx.ALL, 5)
# Add Notes button to connection box
# self.notes_btn = wx.Button(conn_box, label="Notes")
# self.notes_btn.SetToolTip("Open notes editor")
# self.notes_btn.Bind(wx.EVT_BUTTON, self.on_notes)
# conn_box_sizer.Add(self.notes_btn, 0, wx.EXPAND | wx.ALL, 5)
left_sizer.Add(conn_box_sizer, 0, wx.EXPAND | wx.ALL, 5)
# Channel management
@@ -1184,6 +358,7 @@ class IRCFrame(wx.Frame):
# Tools menu
tools_menu = wx.Menu()
tools_menu.Append(208, "&Notes\tCtrl+T") # Add Notes menu item
tools_menu.Append(201, "&WHOIS User\tCtrl+I")
tools_menu.Append(202, "Change &Nick\tCtrl+N")
tools_menu.AppendSeparator()
@@ -1194,6 +369,7 @@ class IRCFrame(wx.Frame):
tools_menu.Append(205, "Set &Highlights")
tools_menu.Append(206, "Auto-join Channels")
tools_menu.Append(207, "Command Help")
self.Bind(wx.EVT_MENU, self.on_notes, id=208) # Bind Notes menu item
self.Bind(wx.EVT_MENU, self.on_menu_whois, id=201)
self.Bind(wx.EVT_MENU, self.on_menu_change_nick, id=202)
self.Bind(wx.EVT_MENU, self.on_menu_away, id=203)
@@ -1220,6 +396,40 @@ class IRCFrame(wx.Frame):
except Exception as e:
logger.error(f"Error showing about dialog: {e}")
def on_notes(self, event):
"""Open notes editor dialog"""
try:
# Check if notes window already exists
if hasattr(self, 'notes_frame') and self.notes_frame:
try:
self.notes_frame.Raise() # Bring to front if already open
return
except:
# Frame was destroyed, create new one
pass
self.notes_frame = NotesDialog(self, self.notes_data)
self.notes_frame.Bind(wx.EVT_CLOSE, self.on_notes_closed)
self.notes_frame.Show()
except Exception as e:
logger.error(f"Error opening notes dialog: {e}")
self.log_server(f"Error opening notes: {e}", wx.Colour(255, 0, 0))
def on_notes_closed(self, event=None):
"""Handle notes frame closing"""
if hasattr(self, 'notes_frame') and self.notes_frame:
try:
# Update notes data from the frame before it closes
self.notes_data = self.notes_frame.notes_data
except Exception as e:
logger.error(f"Error getting notes data on close: {e}")
finally:
self.notes_frame = None
if event:
event.Skip() # Allow the event to propagate
def setup_irc_handlers(self):
try:
self.reactor = irc.client.Reactor()
@@ -1355,7 +565,7 @@ class IRCFrame(wx.Frame):
self.is_connecting = False
self.connect_btn.SetLabel("Disconnect")
self.connect_btn.Enable(True)
self.SetStatusText(f"Connected to {self.server} - Use /help for commands, Ctrl+F to search, Ctrl+Esc to quick exit")
self.SetStatusText(f"Connected to {self.server} - Use /help for commands, Ctrl+F to search, Shift+Esc to quick exit")
logger.info(f"Successfully connected to {self.server}")
def on_connect_failed(self, error_msg):
@@ -1779,7 +989,7 @@ BASIC USAGE:
- Use Up/Down arrows for message history
- Tab for nickname completion in channels
- Ctrl+F to search in chat history
- Ctrl+Esc to quickly exit the application
- Shift+Esc to quickly exit the application
TEXT FORMATTING:
- Usernames are colored for easy identification
@@ -2032,6 +1242,9 @@ COMMANDS (type /help in chat for full list):
if self.ui_timer and self.ui_timer.IsRunning():
self.ui_timer.Stop()
# Notes data will be lost when app closes (RAM only)
# User can save to file if they want persistence
if self.is_connected():
self.disconnect()
# Give it a moment to disconnect gracefully
@@ -2046,6 +1259,7 @@ if __name__ == "__main__":
try:
app = wx.App()
frame = IRCFrame()
frame.SetIcon(wx.Icon(get_resource_path("icon.ico"), wx.BITMAP_TYPE_ICO))
app.MainLoop()
except Exception as e:
logger.critical(f"Fatal error: {e}")