diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..2c9e968 --- /dev/null +++ b/build.sh @@ -0,0 +1,20 @@ +#!/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 \ No newline at end of file diff --git a/main.py b/main.py index fce8fe7..90de87a 100644 --- a/main.py +++ b/main.py @@ -10,11 +10,414 @@ import socket import logging import queue import os +import sys # Set up logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) +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) + +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): @@ -32,7 +435,7 @@ class SearchDialog(wx.Dialog): # 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)) + 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) @@ -74,6 +477,38 @@ class SearchDialog(wx.Dialog): 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) @@ -126,24 +561,31 @@ class IRCPanel(wx.Panel): self.Bind(wx.EVT_MENU, self.on_search, id=wx.ID_FIND) def load_fira_code_font(self): - """Load Fira Code font from current directory""" + """Load Fira Code font from resources""" try: - # First try to add the font to the system and use it - font_path = "FiraCode-Regular.ttf" + # Try to use Fira Code if available + font_path = get_resource_path("FiraCode-Regular.ttf") + if os.path.exists(font_path): - # Try to load the font file directly - font = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) - # On some systems, we might need to use the font by name after ensuring it's available - if wx.TheFontList.FindOrCreateFont(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Fira Code"): - font = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Fira Code") - logger.info("Successfully loaded Fira Code font") - else: - # Try creating font from the file path (this works on some wxPython versions) - font = wx.Font(wx.FontInfo(10).Family(wx.FONTFAMILY_TELETYPE).FaceName("Fira Code")) + # On wxPython 4.1+, we can try to add the font to the font manager + try: + font_collection = wx.private.FontCollection() + if font_collection.AddFont(font_path): + font = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, + False, "Fira Code") + if font.IsOk(): + logger.info("Successfully loaded Fira Code font") + return font + except Exception: + pass + + # Fallback: try to create font by name + font = wx.Font(wx.FontInfo(10).Family(wx.FONTFAMILY_TELETYPE).FaceName("Fira Code")) + if font.IsOk(): logger.info("Using Fira Code font via FaceName") - return font + return font else: - logger.warning("FiraCode-Regular.ttf not found in current directory") + logger.warning("FiraCode-Regular.ttf not found in resources") except Exception as e: logger.error(f"Error loading Fira Code font: {e}") @@ -258,35 +700,40 @@ class IRCPanel(wx.Panel): # Get all text full_text = self.text_ctrl.GetValue() - if not full_text: + if not full_text or not search_text: return # Prepare search parameters - flags = 0 + search_flags = 0 if not case_sensitive: - flags |= wx.FR_DOWN - if whole_word: - # For whole word, we'll handle manually - pass + # For manual search, we'll handle case sensitivity ourselves + search_text_lower = search_text.lower() + full_text_lower = full_text.lower() - # Find all occurrences + # Manual search implementation since wx.TextCtrl doesn't have FindText pos = 0 - while pos != -1: - if whole_word: - # Manual whole word search + while pos < len(full_text): + if case_sensitive: + # Case sensitive search found_pos = full_text.find(search_text, pos) - if found_pos == -1: - break - # Check if it's a whole word - if (found_pos == 0 or not full_text[found_pos-1].isalnum()) and \ - (found_pos + len(search_text) >= len(full_text) or - not full_text[found_pos + len(search_text)].isalnum()): - self.search_positions.append(found_pos) - pos = found_pos + 1 else: - found_pos = self.text_ctrl.FindText(pos, len(full_text), search_text, flags) - if found_pos == -1: - break + # 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) @@ -297,9 +744,10 @@ class IRCPanel(wx.Panel): 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""" @@ -418,7 +866,10 @@ class IRCPanel(wx.Panel): class IRCFrame(wx.Frame): def __init__(self): - super().__init__(None, title="IRC Client", size=(1200, 700)) + super().__init__(None, title="wxIRC", size=(1200, 700)) + + # Show privacy notice first + self.show_privacy_notice() self.reactor = None self.connection = None @@ -475,16 +926,24 @@ class IRCFrame(wx.Frame): self.Bind(wx.EVT_CLOSE, self.on_close) - # Bind global accelerators for search + # Bind global accelerators for search and quick escape accel_tbl = wx.AcceleratorTable([ (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 ]) self.SetAcceleratorTable(accel_tbl) self.Bind(wx.EVT_MENU, self.on_global_search, id=wx.ID_FIND) self.Bind(wx.EVT_MENU, self.on_find_next, id=1001) self.Bind(wx.EVT_MENU, self.on_find_previous, id=1002) + self.Bind(wx.EVT_MENU, self.on_quick_escape, id=1003) + + def show_privacy_notice(self): + """Show privacy notice dialog at startup""" + dlg = PrivacyNoticeDialog(self) + dlg.ShowModal() + dlg.Destroy() def get_user_color(self, username): """Get a consistent color for a username""" @@ -511,6 +970,27 @@ class IRCFrame(wx.Frame): if current_panel and hasattr(current_panel, 'find_previous'): current_panel.find_previous() + def on_quick_escape(self, event): + """Handle Ctrl+Esc for quick escape - exit immediately""" + try: + # Stop UI timer first + if self.ui_timer and self.ui_timer.IsRunning(): + self.ui_timer.Stop() + + # Force disconnect without sending quit message + with self.connection_lock: + if self.connection and self.connection.is_connected(): + try: + self.connection.close() + except: + pass + + # Exit immediately + wx.CallAfter(self.Destroy) + except Exception as e: + logger.error(f"Error in quick escape: {e}") + wx.CallAfter(self.Destroy) + def get_current_panel(self): """Get the currently active IRC panel""" try: @@ -650,7 +1130,10 @@ class IRCFrame(wx.Frame): # File menu file_menu = wx.Menu() + file_menu.Append(300, "&About", "About wxIRC Client") + file_menu.AppendSeparator() file_menu.Append(wx.ID_EXIT, "E&xit\tCtrl+Q") + self.Bind(wx.EVT_MENU, self.on_about, id=300) self.Bind(wx.EVT_MENU, self.on_close, id=wx.ID_EXIT) # Edit menu with search @@ -701,6 +1184,15 @@ class IRCFrame(wx.Frame): except Exception as e: logger.error(f"Error creating menu: {e}") + def on_about(self, event): + """Show About dialog""" + try: + dlg = AboutDialog(self) + dlg.ShowModal() + dlg.Destroy() + except Exception as e: + logger.error(f"Error showing about dialog: {e}") + def setup_irc_handlers(self): try: self.reactor = irc.client.Reactor() @@ -786,7 +1278,7 @@ class IRCFrame(wx.Frame): logger.info(f"Attempting connection to {self.server}:{self.port}") self.connection = self.reactor.server().connect( self.server, self.port, self.nick, - username=self.nick, ircname="wxPython IRC Client", + username=self.nick, ircname=self.nick, connect_factory=irc.connection.Factory() ) @@ -836,7 +1328,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") + self.SetStatusText(f"Connected to {self.server} - Use /help for commands, Ctrl+F to search, Ctrl+Esc to quick exit") logger.info(f"Successfully connected to {self.server}") def on_connect_failed(self, error_msg): @@ -1260,6 +1752,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 TEXT FORMATTING: - Usernames are colored for easy identification diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7854fa0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +altgraph==0.17.5 +autocommand==2.2.2 +irc==20.5.0 +jaraco.collections==5.2.1 +jaraco.context==6.0.1 +jaraco.functools==4.3.0 +jaraco.logging==3.4.0 +jaraco.stream==3.0.4 +jaraco.text==4.0.0 +more-itertools==10.8.0 +packaging==25.0 +psutil==7.1.3 +pyinstaller==6.16.0 +pyinstaller-hooks-contrib==2025.10 +python-dateutil==2.9.0.post0 +pytz==2025.2 +setuptools==80.9.0 +six==1.17.0 +tempora==5.8.1 +wxPython==4.2.4