privacy message. and some more fixes. especially the seraching.
added a requirements file and a build script for linux.
This commit is contained in:
20
build.sh
Normal file
20
build.sh
Normal 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
|
||||||
575
main.py
575
main.py
@@ -10,11 +10,414 @@ import socket
|
|||||||
import logging
|
import logging
|
||||||
import queue
|
import queue
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
class UIUpdate:
|
||||||
"""Thread-safe UI update container"""
|
"""Thread-safe UI update container"""
|
||||||
def __init__(self, callback, *args, **kwargs):
|
def __init__(self, callback, *args, **kwargs):
|
||||||
@@ -32,7 +435,7 @@ class SearchDialog(wx.Dialog):
|
|||||||
# Search input
|
# Search input
|
||||||
search_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
search_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
search_sizer.Add(wx.StaticText(self, label="Search:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
|
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)
|
self.search_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_search)
|
||||||
search_sizer.Add(self.search_ctrl, 1, wx.EXPAND | wx.ALL, 5)
|
search_sizer.Add(self.search_ctrl, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
|
||||||
@@ -74,6 +477,38 @@ class SearchDialog(wx.Dialog):
|
|||||||
self.whole_word.IsChecked()
|
self.whole_word.IsChecked()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class AboutDialog(wx.Dialog):
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent, title="About wxIRC Client", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
|
||||||
|
|
||||||
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
|
# Application info
|
||||||
|
info_text = wx.StaticText(self, label="wxIRC Client")
|
||||||
|
info_font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
|
||||||
|
info_text.SetFont(info_font)
|
||||||
|
|
||||||
|
version_text = wx.StaticText(self, label=f"V 1.1.1.0")
|
||||||
|
version_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
|
||||||
|
version_text.SetFont(version_font)
|
||||||
|
|
||||||
|
features_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
|
||||||
|
|
||||||
|
# Add to sizer
|
||||||
|
sizer.Add(info_text, 0, wx.ALL | wx.ALIGN_CENTER, 10)
|
||||||
|
sizer.Add(version_text, 0, wx.ALL | wx.ALIGN_CENTER, 5)
|
||||||
|
|
||||||
|
# OK button
|
||||||
|
ok_btn = wx.Button(self, wx.ID_OK, "OK")
|
||||||
|
ok_btn.SetDefault()
|
||||||
|
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
btn_sizer.Add(ok_btn, 0, wx.ALIGN_CENTER | wx.ALL, 10)
|
||||||
|
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER)
|
||||||
|
|
||||||
|
self.SetSizer(sizer)
|
||||||
|
self.Fit()
|
||||||
|
self.Centre()
|
||||||
|
|
||||||
class IRCPanel(wx.Panel):
|
class IRCPanel(wx.Panel):
|
||||||
def __init__(self, parent, main_frame):
|
def __init__(self, parent, main_frame):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -126,24 +561,31 @@ class IRCPanel(wx.Panel):
|
|||||||
self.Bind(wx.EVT_MENU, self.on_search, id=wx.ID_FIND)
|
self.Bind(wx.EVT_MENU, self.on_search, id=wx.ID_FIND)
|
||||||
|
|
||||||
def load_fira_code_font(self):
|
def load_fira_code_font(self):
|
||||||
"""Load Fira Code font from current directory"""
|
"""Load Fira Code font from resources"""
|
||||||
try:
|
try:
|
||||||
# First try to add the font to the system and use it
|
# Try to use Fira Code if available
|
||||||
font_path = "FiraCode-Regular.ttf"
|
font_path = get_resource_path("FiraCode-Regular.ttf")
|
||||||
|
|
||||||
if os.path.exists(font_path):
|
if os.path.exists(font_path):
|
||||||
# Try to load the font file directly
|
# On wxPython 4.1+, we can try to add the font to the font manager
|
||||||
font = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
|
try:
|
||||||
# On some systems, we might need to use the font by name after ensuring it's available
|
font_collection = wx.private.FontCollection()
|
||||||
if wx.TheFontList.FindOrCreateFont(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Fira Code"):
|
if font_collection.AddFont(font_path):
|
||||||
font = wx.Font(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,
|
||||||
logger.info("Successfully loaded Fira Code font")
|
False, "Fira Code")
|
||||||
else:
|
if font.IsOk():
|
||||||
# Try creating font from the file path (this works on some wxPython versions)
|
logger.info("Successfully loaded Fira Code font")
|
||||||
font = wx.Font(wx.FontInfo(10).Family(wx.FONTFAMILY_TELETYPE).FaceName("Fira Code"))
|
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")
|
logger.info("Using Fira Code font via FaceName")
|
||||||
return font
|
return font
|
||||||
else:
|
else:
|
||||||
logger.warning("FiraCode-Regular.ttf not found in current directory")
|
logger.warning("FiraCode-Regular.ttf not found in resources")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading Fira Code font: {e}")
|
logger.error(f"Error loading Fira Code font: {e}")
|
||||||
|
|
||||||
@@ -258,35 +700,40 @@ class IRCPanel(wx.Panel):
|
|||||||
|
|
||||||
# Get all text
|
# Get all text
|
||||||
full_text = self.text_ctrl.GetValue()
|
full_text = self.text_ctrl.GetValue()
|
||||||
if not full_text:
|
if not full_text or not search_text:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Prepare search parameters
|
# Prepare search parameters
|
||||||
flags = 0
|
search_flags = 0
|
||||||
if not case_sensitive:
|
if not case_sensitive:
|
||||||
flags |= wx.FR_DOWN
|
# For manual search, we'll handle case sensitivity ourselves
|
||||||
if whole_word:
|
search_text_lower = search_text.lower()
|
||||||
# For whole word, we'll handle manually
|
full_text_lower = full_text.lower()
|
||||||
pass
|
|
||||||
|
|
||||||
# Find all occurrences
|
# Manual search implementation since wx.TextCtrl doesn't have FindText
|
||||||
pos = 0
|
pos = 0
|
||||||
while pos != -1:
|
while pos < len(full_text):
|
||||||
if whole_word:
|
if case_sensitive:
|
||||||
# Manual whole word search
|
# Case sensitive search
|
||||||
found_pos = full_text.find(search_text, pos)
|
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:
|
else:
|
||||||
found_pos = self.text_ctrl.FindText(pos, len(full_text), search_text, flags)
|
# Case insensitive search
|
||||||
if found_pos == -1:
|
found_pos = full_text_lower.find(search_text_lower, pos)
|
||||||
break
|
|
||||||
|
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)
|
self.search_positions.append(found_pos)
|
||||||
pos = found_pos + len(search_text)
|
pos = found_pos + len(search_text)
|
||||||
|
|
||||||
@@ -297,9 +744,10 @@ class IRCPanel(wx.Panel):
|
|||||||
else:
|
else:
|
||||||
self.main_frame.SetStatusText(f"Text '{search_text}' not found")
|
self.main_frame.SetStatusText(f"Text '{search_text}' not found")
|
||||||
wx.Bell()
|
wx.Bell()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error performing search: {e}")
|
logger.error(f"Error performing search: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def highlight_search_result(self):
|
def highlight_search_result(self):
|
||||||
"""Highlight the current search result"""
|
"""Highlight the current search result"""
|
||||||
@@ -418,7 +866,10 @@ class IRCPanel(wx.Panel):
|
|||||||
|
|
||||||
class IRCFrame(wx.Frame):
|
class IRCFrame(wx.Frame):
|
||||||
def __init__(self):
|
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.reactor = None
|
||||||
self.connection = None
|
self.connection = None
|
||||||
@@ -475,16 +926,24 @@ class IRCFrame(wx.Frame):
|
|||||||
|
|
||||||
self.Bind(wx.EVT_CLOSE, self.on_close)
|
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([
|
accel_tbl = wx.AcceleratorTable([
|
||||||
(wx.ACCEL_CTRL, ord('F'), wx.ID_FIND),
|
(wx.ACCEL_CTRL, ord('F'), wx.ID_FIND),
|
||||||
(wx.ACCEL_NORMAL, wx.WXK_F3, 1001),
|
(wx.ACCEL_NORMAL, wx.WXK_F3, 1001),
|
||||||
(wx.ACCEL_SHIFT, wx.WXK_F3, 1002),
|
(wx.ACCEL_SHIFT, wx.WXK_F3, 1002),
|
||||||
|
(wx.ACCEL_CTRL, wx.WXK_ESCAPE, 1003), # Quick Escape
|
||||||
])
|
])
|
||||||
self.SetAcceleratorTable(accel_tbl)
|
self.SetAcceleratorTable(accel_tbl)
|
||||||
self.Bind(wx.EVT_MENU, self.on_global_search, id=wx.ID_FIND)
|
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_next, id=1001)
|
||||||
self.Bind(wx.EVT_MENU, self.on_find_previous, id=1002)
|
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):
|
def get_user_color(self, username):
|
||||||
"""Get a consistent color for a 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'):
|
if current_panel and hasattr(current_panel, 'find_previous'):
|
||||||
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):
|
def get_current_panel(self):
|
||||||
"""Get the currently active IRC panel"""
|
"""Get the currently active IRC panel"""
|
||||||
try:
|
try:
|
||||||
@@ -650,7 +1130,10 @@ class IRCFrame(wx.Frame):
|
|||||||
|
|
||||||
# File menu
|
# File menu
|
||||||
file_menu = wx.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")
|
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)
|
self.Bind(wx.EVT_MENU, self.on_close, id=wx.ID_EXIT)
|
||||||
|
|
||||||
# Edit menu with search
|
# Edit menu with search
|
||||||
@@ -701,6 +1184,15 @@ class IRCFrame(wx.Frame):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error creating menu: {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):
|
def setup_irc_handlers(self):
|
||||||
try:
|
try:
|
||||||
self.reactor = irc.client.Reactor()
|
self.reactor = irc.client.Reactor()
|
||||||
@@ -786,7 +1278,7 @@ class IRCFrame(wx.Frame):
|
|||||||
logger.info(f"Attempting connection to {self.server}:{self.port}")
|
logger.info(f"Attempting connection to {self.server}:{self.port}")
|
||||||
self.connection = self.reactor.server().connect(
|
self.connection = self.reactor.server().connect(
|
||||||
self.server, self.port, self.nick,
|
self.server, self.port, self.nick,
|
||||||
username=self.nick, ircname="wxPython IRC Client",
|
username=self.nick, ircname=self.nick,
|
||||||
connect_factory=irc.connection.Factory()
|
connect_factory=irc.connection.Factory()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -836,7 +1328,7 @@ class IRCFrame(wx.Frame):
|
|||||||
self.is_connecting = False
|
self.is_connecting = False
|
||||||
self.connect_btn.SetLabel("Disconnect")
|
self.connect_btn.SetLabel("Disconnect")
|
||||||
self.connect_btn.Enable(True)
|
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}")
|
logger.info(f"Successfully connected to {self.server}")
|
||||||
|
|
||||||
def on_connect_failed(self, error_msg):
|
def on_connect_failed(self, error_msg):
|
||||||
@@ -1260,6 +1752,7 @@ BASIC USAGE:
|
|||||||
- Use Up/Down arrows for message history
|
- Use Up/Down arrows for message history
|
||||||
- Tab for nickname completion in channels
|
- Tab for nickname completion in channels
|
||||||
- Ctrl+F to search in chat history
|
- Ctrl+F to search in chat history
|
||||||
|
- Ctrl+Esc to quickly exit the application
|
||||||
|
|
||||||
TEXT FORMATTING:
|
TEXT FORMATTING:
|
||||||
- Usernames are colored for easy identification
|
- Usernames are colored for easy identification
|
||||||
|
|||||||
20
requirements.txt
Normal file
20
requirements.txt
Normal 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
|
||||||
Reference in New Issue
Block a user