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

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

1267
src/main.py Normal file

File diff suppressed because it is too large Load Diff