privacy message. and some more fixes. especially the seraching.

added a requirements file and a build script for linux.
This commit is contained in:
2025-11-23 20:10:03 +01:00
parent a96c164fee
commit 4fdceb46ff
3 changed files with 574 additions and 41 deletions

20
build.sh Normal file
View File

@@ -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

573
main.py
View File

@@ -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)
@@ -300,6 +747,7 @@ class IRCPanel(wx.Panel):
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

20
requirements.txt Normal file
View File

@@ -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