diff --git a/build.ps1 b/build.ps1 index 9980fea..55e6916 100644 --- a/build.ps1 +++ b/build.ps1 @@ -10,8 +10,11 @@ pyinstaller ` --hidden-import wx._xml ` --add-data "FiraCode-Regular.ttf;." ` --add-data "FiraCode-SemiBold.ttf;." ` + --add-data "src\sounds\*;sounds" ` --add-data "venv\Lib\site-packages\irc\codes.txt;irc" ` --add-data "icon.ico;." ` + --add-data "src\channel.ico;." ` + --add-data "src\server.ico;." ` --icon "icon.ico" ` "src/main.py" diff --git a/src/IRCPanel.py b/src/IRCPanel.py index 1dc0134..166bb27 100644 --- a/src/IRCPanel.py +++ b/src/IRCPanel.py @@ -561,9 +561,13 @@ class IRCPanel(wx.Panel): logger.error(f"Error in add_message: {e}") def _add_message_safe(self, message, username_color=None, message_color=None, - bold=False, italic=False, underline=False): + bold=False, italic=False, underline=False): """Add message to display with formatting (must be called from main thread).""" try: + # Safety check: ensure text_ctrl still exists + if not self.text_ctrl or not self: + return + self.messages.append(message) # Check if user is at bottom diff --git a/src/LocalServer.py b/src/LocalServer.py index ac2542b..2214c62 100644 --- a/src/LocalServer.py +++ b/src/LocalServer.py @@ -211,3 +211,65 @@ class LocalServerManager: except Exception: logger.info(message) +if __name__ == "__main__": + import argparse + import sys + import signal + + def main(): + parser = argparse.ArgumentParser( + description="Run a local-only IRC server." + ) + parser.add_argument( + "--host", type=str, default="0.0.0.0", + help="Bind host (default: 0.0.0.0)" + ) + parser.add_argument( + "--port", type=int, default=6667, + help="Bind port (default: 6667)" + ) + parser.add_argument( + "--channels", type=str, default="#lobby", + help="Comma-separated list of channels (default: #lobby)" + ) + parser.add_argument( + "--verbose", action="store_true", + help="Enable verbose logging" + ) + + args = parser.parse_args() + + # Set up logging + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s" + ) + + # Initialize the server manager + manager = LocalServerManager( + listen_host=args.host, + listen_port=args.port + ) + manager.set_channels([ch.strip() for ch in args.channels.split(",") if ch.strip()]) + + # Handle Ctrl+C gracefully + def signal_handler(sig, frame): + print("\nStopping server...") + manager.stop() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + + try: + manager.start() + print(f"IRC server running on {args.host}:{args.port}") + # Keep the main thread alive while server runs + while manager.is_running(): + import time + time.sleep(1) + except Exception as e: + print(f"Error: {e}") + manager.stop() + sys.exit(1) + + main() diff --git a/src/Win32API.py b/src/Win32API.py new file mode 100644 index 0000000..2c31d33 --- /dev/null +++ b/src/Win32API.py @@ -0,0 +1,99 @@ +import ctypes +from ctypes import wintypes +import time + +SW_HIDE = 0 +SW_SHOW = 5 + +GWL_EXSTYLE = -20 +WS_EX_TOOLWINDOW = 0x00000080 +WS_EX_APPWINDOW = 0x00040000 +WS_EX_LAYERED = 0x80000 +LWA_ALPHA = 0x2 + + +class Win32API: + def __init__(self, hwnd): + self.hwnd = hwnd + self.hidden = False + self._load_api() + + def _load_api(self): + user32 = ctypes.windll.user32 + + self.ShowWindow = user32.ShowWindow + self.GetWindowLong = user32.GetWindowLongW + self.SetWindowLong = user32.SetWindowLongW + self._RegisterHotKey = user32.RegisterHotKey + self._SetLayeredWindowAttributes = user32.SetLayeredWindowAttributes + + self.ShowWindow.argtypes = [wintypes.HWND, ctypes.c_int] + self.GetWindowLong.argtypes = [wintypes.HWND, ctypes.c_int] + self.SetWindowLong.argtypes = [wintypes.HWND, ctypes.c_int, ctypes.c_long] + self._RegisterHotKey.argtypes = [ + wintypes.HWND, + wintypes.INT, + wintypes.UINT, + wintypes.UINT, + ] + self._SetLayeredWindowAttributes.argtypes = [ + wintypes.HWND, + wintypes.COLORREF, + wintypes.BYTE, + wintypes.DWORD, + ] + + def remove_from_taskbar(self): + style = self.GetWindowLong(self.hwnd, GWL_EXSTYLE) + new_style = (style & ~WS_EX_APPWINDOW) | WS_EX_TOOLWINDOW + self.SetWindowLong(self.hwnd, GWL_EXSTYLE, new_style) + + def hide(self): + self.ShowWindow(self.hwnd, SW_HIDE) + self.hidden = True + + def show(self): + self.ShowWindow(self.hwnd, SW_SHOW) + self.hidden = False + + def toggle(self, fade=True, duration=500, steps=20): + """Toggle window visibility with optional fade effect""" + if self.hidden: + if fade: + self.fade_in(duration, steps) + else: + self.show() + else: + if fade: + self.fade_out(duration, steps) + else: + self.hide() + + + def register_hotkey(self, hotkey_id, key, modifiers): + vk = ord(key.upper()) + return self._RegisterHotKey(None, hotkey_id, modifiers, vk) + + def _set_layered(self): + style = self.GetWindowLong(self.hwnd, GWL_EXSTYLE) + self.SetWindowLong(self.hwnd, GWL_EXSTYLE, style | WS_EX_LAYERED) + + def fade_in(self, duration=50, steps=50): + self.show() + self._set_layered() + for idx in range(steps + 1): + alpha = int(255 * (idx / steps)) + self._SetLayeredWindowAttributes(self.hwnd, 0, alpha, LWA_ALPHA) + ctypes.windll.kernel32.Sleep(int(duration / steps)) + self.hidden = False + + def fade_out(self, duration=50, steps=50): + self._set_layered() + for idx in range(steps + 1): + alpha = int(255 * (1 - idx /steps)) + self._SetLayeredWindowAttributes(self.hwnd, 0, alpha, LWA_ALPHA) + ctypes.windll.kernel32.Sleep(int(duration / steps)) + self.hidden = True + self.hide() + + diff --git a/src/Win32SoundHandler.py b/src/Win32SoundHandler.py new file mode 100644 index 0000000..0eab6f6 --- /dev/null +++ b/src/Win32SoundHandler.py @@ -0,0 +1,97 @@ +import ctypes +import logging +from ctypes import wintypes + +# 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): + import os + import sys + try: + base_path = sys._MEIPASS + except Exception: + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__))) + base_path = project_root + full_path = os.path.normpath(os.path.join(base_path, relative_path)) + return full_path + +class Win32SoundHandler: + def __init__(self, hwnd): + self.hwnd = hwnd + self.SND_ALIAS = 0x00010000 + self.SND_FILENAME = 0x00020000 + self.SND_ASYNC = 0x00000001 + self._setup_playsoundapi() + + def _setup_playsoundapi(self): + winmm = ctypes.windll.winmm + self.PlaySoundW = winmm.PlaySoundW + self.PlaySoundW.argtypes = ( + wintypes.LPCWSTR, + wintypes.HMODULE, + wintypes.DWORD + ) + self.PlaySoundW.restype = wintypes.BOOL + + def play_info_sound(self): + self.PlaySoundW("SystemAsterisk", None, self.SND_ALIAS | self.SND_ASYNC) + + def play_error_sound(self): + self.PlaySoundW("SystemHand", None, self.SND_ALIAS | self.SND_ASYNC) + + def play_warn_sound(self): + self.PlaySoundW("SystemExclamation", None, self.SND_ALIAS | self.SND_ASYNC) + + def play_question_sound(self): + self.PlaySoundW("SystemQuestion", None, self.SND_ALIAS | self.SND_ASYNC) + + def play_default_sound(self): + self.PlaySoundW("SystemDefault", None, self.SND_ALIAS | self.SND_ASYNC) + + def play_notification_sound(self): + self.PlaySoundW("SystemNotification", None, self.SND_ALIAS | self.SND_ASYNC) + + def play_mail_sound(self): + self.PlaySoundW("MailBeep", None, self.SND_ALIAS | self.SND_ASYNC) + + def play_exit_sound(self): + self.PlaySoundW("SystemExit", None, self.SND_ALIAS | self.SND_ASYNC) + + def play_connect_server_or_channel(self): + try: + sound_path = get_resource_path("sounds/space-pdj.wav") + import os + if os.path.exists(sound_path): + self.PlaySoundW(sound_path, None, self.SND_FILENAME | self.SND_ASYNC) + else: + logger.warning(f"Sound file not found: {sound_path}") + except Exception as e: + logger.error(f"Error playing popout click sound: {e}") + + + def play_popout_click(self): + try: + sound_path = get_resource_path("sounds/startup.wav") + import os + if os.path.exists(sound_path): + self.PlaySoundW(sound_path, None, self.SND_FILENAME | self.SND_ASYNC) + else: + logger.warning(f"Sound file not found: {sound_path}") + except Exception as e: + logger.error(f"Error playing popout click sound: {e}") + + def play_msg_recv(self): + try: + sound_path = get_resource_path("sounds/balloon.wav") + import os + if os.path.exists(sound_path): + self.PlaySoundW(sound_path, None, self.SND_FILENAME | self.SND_ASYNC) + else: + logger.warning(f"Sound file not found: {sound_path}") + except Exception as e: + logger.error(f"Error playing message received sound: {e}") + + + diff --git a/src/channel.ico b/src/channel.ico new file mode 100644 index 0000000..3b78eb8 Binary files /dev/null and b/src/channel.ico differ diff --git a/src/main.py b/src/main.py index b220612..9ebb56a 100644 --- a/src/main.py +++ b/src/main.py @@ -1,4 +1,6 @@ import wx +import wx.aui +import wx.lib.agw.aui as aui import irc.client import threading import re @@ -16,6 +18,7 @@ from PrivacyNoticeDialog import PrivacyNoticeDialog from IRCPanel import IRCPanel from AboutDialog import AboutDialog from NotesDialog import NotesDialog +if os.name == "nt": from Win32API import Win32API ; from Win32SoundHandler import Win32SoundHandler from ScanWizard import ScanWizardDialog from LocalServer import LocalServerManager @@ -251,7 +254,7 @@ class IRCFrame(wx.Frame): # Show privacy notice first self.show_privacy_notice() - + self.reactor = None self.connection = None self.reactor_thread = None @@ -271,7 +274,7 @@ class IRCFrame(wx.Frame): self.auto_join_channels = [] self.away = False self.timestamps = True - + self.notes_data = defaultdict(dict) self.server_menu_items = {} self.local_bind_host = "127.0.0.1" @@ -300,6 +303,18 @@ class IRCFrame(wx.Frame): self.motd_lines = [] self.collecting_motd = False + self.hwnd = self.GetHandle() + self.ctrl = Win32API(self.hwnd) + + # Initialize sound handler for Windows + if os.name == "nt": + self.sound_handler = Win32SoundHandler(self.hwnd) + else: + self.sound_handler = None + + # Sound toggle state (enabled by default) + self.sounds_enabled = True + self.setup_irc_handlers() self.create_menubar() self.setup_ui() @@ -329,6 +344,17 @@ class IRCFrame(wx.Frame): 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) + + HOTKEY_ID = 1 + MOD_CONTROL = 0x0002 + MOD_ALT = 0x0001 + + # Register directly on the wx.Frame + self.RegisterHotKey(HOTKEY_ID, MOD_CONTROL | MOD_ALT, ord('H')) + self.Bind(wx.EVT_HOTKEY, self.on_hotkey, id=HOTKEY_ID) + + def on_hotkey(self, event): + self.ctrl.toggle() def build_theme(self): """Build a small theme descriptor that respects the host platform.""" @@ -397,7 +423,6 @@ class IRCFrame(wx.Frame): def show_privacy_notice(self): """Show privacy notice dialog at startup""" dlg = PrivacyNoticeDialog(self) - dlg.ShowModal() dlg.Destroy() def get_user_color(self, username): @@ -528,15 +553,31 @@ class IRCFrame(wx.Frame): left_panel.SetSizer(left_sizer) - # Center - Notebook - self.notebook = wx.Notebook(panel) + self.notebook = wx.aui.AuiNotebook(panel, style= + wx.aui.AUI_NB_DEFAULT_STYLE | + wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB | + wx.aui.AUI_NB_MIDDLE_CLICK_CLOSE + ) self.notebook.SetBackgroundColour(self.theme["content_bg"]) + + # Setup tab icons + self.setup_tab_icons() + + # Bind close event + self.notebook.Bind(wx.aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.on_notebook_page_close) - # Server panel server_panel = IRCPanel(self.notebook, self) server_panel.set_target("SERVER") + + idx = self.notebook.GetPageCount() self.notebook.AddPage(server_panel, "Server") + + # THIS is the missing line + if self.icon_server != -1: + self.notebook.SetPageImage(idx, self.icon_server) + self.channels["SERVER"] = server_panel + # Right sidebar - Users - light gray for contrast right_panel = wx.Panel(panel) @@ -595,8 +636,12 @@ class IRCFrame(wx.Frame): file_menu = wx.Menu() file_menu.Append(300, "&About", "About wxIRC Client") file_menu.AppendSeparator() + self.sound_toggle_item = file_menu.AppendCheckItem(301, "Toggle &Sounds") + self.sound_toggle_item.Check(True) # Sounds enabled by default + 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_toggle_sounds, id=301) self.Bind(wx.EVT_MENU, self.on_close, id=wx.ID_EXIT) # Edit menu with search @@ -669,6 +714,11 @@ class IRCFrame(wx.Frame): except Exception as e: logger.error(f"Error creating menu: {e}") + def on_toggle_sounds(self, event): + """Toggle sound effects on/off""" + self.sounds_enabled = self.sound_toggle_item.IsChecked() + logger.info(f"Sounds {'enabled' if self.sounds_enabled else 'disabled'}") + def on_about(self, event): """Show About dialog""" try: @@ -677,6 +727,11 @@ class IRCFrame(wx.Frame): dlg.Destroy() except Exception as e: logger.error(f"Error showing about dialog: {e}") + if self.sound_handler and self.sounds_enabled: + try: + self.sound_handler.play_error_sound() + except: + pass def on_notes(self, event): """Open notes editor dialog""" @@ -697,6 +752,11 @@ class IRCFrame(wx.Frame): except Exception as e: logger.error(f"Error opening notes dialog: {e}") self.log_server(f"Error opening notes: {e}", wx.Colour(255, 0, 0)) + if self.sound_handler and self.sounds_enabled: + try: + self.sound_handler.play_error_sound() + except: + pass def on_notes_closed(self, event=None): """Handle notes frame closing""" @@ -862,6 +922,13 @@ class IRCFrame(wx.Frame): self.connect_btn.Enable(True) self.SetStatusText("Connection failed") logger.error(f"Connection failed: {error_msg}") + + # Play error sound + if self.sound_handler and self.sounds_enabled: + try: + self.sound_handler.play_error_sound() + except Exception as e: + logger.error(f"Error playing connection failure sound: {e}") def disconnect(self): """Safely disconnect from IRC server""" @@ -892,6 +959,13 @@ class IRCFrame(wx.Frame): def on_disconnect_cleanup(self): """Clean up after disconnect""" + # Play disconnect notification sound + if self.sound_handler and self.sounds_enabled: + try: + self.sound_handler.play_warn_sound() + except Exception as e: + logger.error(f"Error playing disconnect sound: {e}") + with self.connection_lock: self.connection = None self.is_connecting = False @@ -1146,11 +1220,18 @@ Available commands: try: if channel in self.channels and channel != "SERVER": def _close_channel(): + # Find and delete the page for i in range(self.notebook.GetPageCount()): if self.notebook.GetPageText(i) == channel: self.notebook.DeletePage(i) break - del self.channels[channel] + + # Clean up channel data + if channel in self.channels: + del self.channels[channel] + + if channel in self.channel_users: + del self.channel_users[channel] idx = self.channel_list.FindString(channel) if idx != wx.NOT_FOUND: @@ -1159,7 +1240,7 @@ Available commands: self.safe_ui_update(_close_channel) except Exception as e: logger.error(f"Error closing channel: {e}") - + def log_server(self, message, color=None, bold=False, italic=False, underline=False): try: if "SERVER" in self.channels: @@ -1180,8 +1261,11 @@ Available commands: def log_channel_message(self, channel, username, message, is_action=False, is_system=False): """Log a message to a channel with username coloring""" try: + # Don't create new channels if they don't exist and we're trying to log to them if channel not in self.channels: - self.safe_ui_update(self.add_channel, channel) + # Only create channel if it's being opened by the user, not just receiving messages + return + if channel in self.channels: timestamp = self.get_timestamp() @@ -1204,12 +1288,140 @@ Available commands: except Exception as e: logger.error(f"Error logging channel message: {e}") + def setup_tab_icons(self): + try: + self.tab_image_list = wx.ImageList(32, 32) + + # Icon file names to search for + icon_files = { + 'server': 'server.ico', + 'channel': 'channel.ico', + 'query': 'channel.ico' # Reuse channel.ico for queries if no query.ico exists + } + + # Search paths for icons + base_paths = [ + os.path.dirname(__file__), + get_resource_path(""), + os.path.join(os.getcwd(), "src"), + ] + + # Try to load each icon + loaded_icons = {} + for icon_type, filename in icon_files.items(): + icon_path = None + for base_path in base_paths: + test_path = os.path.join(base_path, filename) + if os.path.exists(test_path): + icon_path = test_path + break + + if icon_path: + try: + img = wx.Image(icon_path, wx.BITMAP_TYPE_ICO) + img = img.Scale(32, 32, wx.IMAGE_QUALITY_HIGH) + bmp = wx.Bitmap(img) + loaded_icons[icon_type] = self.tab_image_list.Add(bmp) + logger.info(f"Loaded {icon_type} icon from {icon_path}") + except Exception as e: + logger.warning(f"Failed to load {icon_type} icon: {e}") + loaded_icons[icon_type] = -1 + else: + logger.info(f"{filename} not found for {icon_type}") + loaded_icons[icon_type] = -1 + + # Assign icon indices + self.icon_server = loaded_icons.get('server', -1) + self.icon_channel = loaded_icons.get('channel', -1) + self.icon_query = loaded_icons.get('query', -1) + + # Use fallback icons if any failed to load + if self.icon_server == -1 or self.icon_channel == -1 or self.icon_query == -1: + logger.info("Using fallback icons for missing icon files") + self._setup_fallback_icons() + + self.notebook.SetImageList(self.tab_image_list) + + except Exception as e: + logger.error(f"Error setting up tab icons: {e}") + self.icon_server = -1 + self.icon_channel = -1 + self.icon_query = -1 + + def _setup_fallback_icons(self): + """Setup fallback icons using wx.ArtProvider.""" + try: + self.icon_server = self.tab_image_list.Add( + wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_OTHER, (48, 48)) + ) + self.icon_channel = self.tab_image_list.Add( + wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, (48, 48)) + ) + self.icon_query = self.tab_image_list.Add( + wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_OTHER, (48, 48)) + ) + except Exception as e: + logger.error(f"Error setting up fallback icons: {e}") + self.icon_server = -1 + self.icon_channel = -1 + self.icon_query = -1 + + def on_notebook_page_close(self, event): + """Handle tab close button clicks""" + try: + page_idx = event.GetSelection() + channel = self.notebook.GetPageText(page_idx) + + if channel == "Server": + event.Veto() + wx.MessageBox("Can't close Server!", "Error", wx.OK | wx.ICON_ERROR) + return + + if channel in self.channels: + del self.channels[channel] + + if channel in self.channel_users: + del self.channel_users[channel] + + # Remove from channel list + idx = self.channel_list.FindString(channel) + if idx != wx.NOT_FOUND: + self.channel_list.Delete(idx) + + if channel.startswith('#') and self.is_connected(): + try: + self.connection.part(channel) + except: + pass + + # Allow the close to proceed + event.Skip() + + except Exception as e: + logger.error(f"Error closing tab: {e}") + event.Skip() + def add_channel(self, channel): + """Add a new channel/query tab with appropriate icon""" try: if channel not in self.channels: panel = IRCPanel(self.notebook, self) panel.set_target(channel) - self.notebook.AddPage(panel, channel) + + # Determine icon based on channel type + if channel == "SERVER": + icon_idx = getattr(self, 'icon_server', -1) + elif channel.startswith('#'): + icon_idx = getattr(self, 'icon_channel', -1) + else: + icon_idx = getattr(self, 'icon_query', -1) + + # Add page with icon (if icon_idx is -1, no icon will be shown) + if icon_idx >= 0: + self.notebook.AddPage(panel, channel, select=True, imageId=icon_idx) + else: + self.notebook.AddPage(panel, channel, select=True) + self.channels[channel] = panel if channel.startswith('#'): @@ -1226,6 +1438,11 @@ Available commands: except Exception as e: logger.error(f"Error opening scan wizard: {e}") self.log_server(f"Scan wizard failed to open: {e}", wx.Colour(255, 0, 0)) + if self.sound_handler and self.sounds_enabled: + try: + self.sound_handler.play_error_sound() + except: + pass def quick_connect(self, server, port): """Populate connection fields and initiate a connection if idle.""" @@ -1434,6 +1651,13 @@ COMMANDS (type /help in chat for full list): self.log_server("Connected to server!", wx.Colour(0, 128, 0), bold=True) # Dark green self.log_server(f"Welcome message: {' '.join(event.arguments)}", wx.Colour(0, 100, 0)) + # Play welcome/connection sound + if self.sound_handler and self.sounds_enabled: + try: + self.sound_handler.play_connect_server_or_channel() + except Exception as e: + logger.error(f"Error playing welcome sound: {e}") + # Auto-join channels for channel in self.auto_join_channels: if not channel.startswith('#'): @@ -1452,6 +1676,13 @@ COMMANDS (type /help in chat for full list): if nick == self.nick: self.safe_ui_update(self.add_channel, channel) self.log_server(f"Joined channel {channel}", wx.Colour(0, 128, 0)) # Dark green + + # Play sound when we join a channel + if self.sound_handler and self.sounds_enabled: + try: + self.sound_handler.play_connect_server_or_channel() + except Exception as e: + logger.error(f"Error playing channel join sound: {e}") self.log_channel_message(channel, nick, f"→ {nick} joined", is_system=True) @@ -1509,6 +1740,13 @@ COMMANDS (type /help in chat for full list): self.log_channel_message(channel, nick, message, is_action=True) else: self.log_channel_message(channel, nick, message) + + # Play sound notification only for real user messages (not from self) + if self.sound_handler and self.sounds_enabled and nick.lower() != self.nick.lower(): + try: + self.sound_handler.play_msg_recv() + except Exception as e: + logger.error(f"Error playing message sound: {e}") # Highlight own nick in messages if self.nick.lower() in message.lower(): @@ -1527,6 +1765,13 @@ COMMANDS (type /help in chat for full list): self.log_channel_message(nick, nick, message, is_action=True) else: self.log_channel_message(nick, nick, message) + + # Play mail sound for private messages + if self.sound_handler and self.sounds_enabled and nick.lower() != self.nick.lower(): + try: + self.sound_handler.play_mail_sound() + except Exception as e: + logger.error(f"Error playing private message sound: {e}") except Exception as e: logger.error(f"Error in privmsg handler: {e}") @@ -1728,14 +1973,12 @@ if os.name == 'nt': ctypes.windll.user32.SetProcessDPIAware() except: pass -else: - pass - if __name__ == "__main__": try: if os.name == 'nt': enable_high_dpi() + app = wx.App() frame = IRCFrame() frame.SetIcon(wx.Icon(get_resource_path("icon.ico"), wx.BITMAP_TYPE_ICO)) diff --git a/src/server.ico b/src/server.ico new file mode 100644 index 0000000..04115f4 Binary files /dev/null and b/src/server.ico differ diff --git a/src/sounds/balloon.wav b/src/sounds/balloon.wav new file mode 100644 index 0000000..315fa43 Binary files /dev/null and b/src/sounds/balloon.wav differ diff --git a/src/sounds/space-pdj.wav b/src/sounds/space-pdj.wav new file mode 100644 index 0000000..06621fd Binary files /dev/null and b/src/sounds/space-pdj.wav differ diff --git a/src/sounds/startup.wav b/src/sounds/startup.wav new file mode 100644 index 0000000..531b2a1 Binary files /dev/null and b/src/sounds/startup.wav differ