diff --git a/src/IRCPanel.py b/src/IRCPanel.py index 6a4c5f3..197eb43 100644 --- a/src/IRCPanel.py +++ b/src/IRCPanel.py @@ -38,12 +38,19 @@ class IRCPanel(wx.Panel): self.input_ctrl.SetHint("Type message here …") self.input_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_send) self.input_ctrl.Bind(wx.EVT_KEY_DOWN, self.on_key_down) + + # Kaomoji picker button - inserts plain ASCII kaomojis into the input box + self.kaomoji_btn = wx.Button(self, label="Emotes") + self.kaomoji_btn.SetToolTip("Emotes :3") + self.kaomoji_btn.Bind(wx.EVT_BUTTON, self.on_pick_kaomoji) send_btn = wx.Button(self, label="Send") send_btn.SetToolTip("Send message (Enter)") send_btn.Bind(wx.EVT_BUTTON, self.on_send) - + + # Order: input field, kaomoji, send input_sizer.Add(self.input_ctrl, 1, wx.EXPAND | wx.ALL, 2) + input_sizer.Add(self.kaomoji_btn, 0, wx.ALL, 2) input_sizer.Add(send_btn, 0, wx.ALL, 2) sizer.Add(input_sizer, 0, wx.EXPAND | wx.ALL, 0) @@ -365,6 +372,7 @@ class IRCPanel(wx.Panel): logger.error(f"Error in tab completion: {e}") def on_send(self, event): + """Send the current input to the active IRC target.""" try: message = self.input_ctrl.GetValue().strip() if message and self.target: @@ -374,3 +382,134 @@ class IRCPanel(wx.Panel): self.input_ctrl.Clear() except Exception as e: logger.error(f"Error sending message: {e}") + + + def insert_text_at_caret(self, text): + """Insert given text at the current caret position in the input box.""" + try: + current = self.input_ctrl.GetValue() + pos = self.input_ctrl.GetInsertionPoint() + new_value = current[:pos] + text + current[pos:] + self.input_ctrl.SetValue(new_value) + self.input_ctrl.SetInsertionPoint(pos + len(text)) + except Exception as e: + logger.error(f"Error inserting text at caret: {e}") + + def on_pick_kaomoji(self, event): + """Show a kaomoji popup next to the button and insert the chosen one. + + All entries are ASCII-only so they are safe for any IRC server. + """ + try: + choices = [ + ":-)", ":)", ":-D", ":D", "^_^", "^o^", "(*^_^*)", "( ^_^)/", "(:3)", "=)", "=]", "^.^", + ":-(", ":'(", "T_T", ";_;", ">_<", "(-_-)", "(_ _)", + ">:(", ">:-(", ">:-O", ">.<", "(-_-)#", + "<3", "(*^3^)", "(^^)v", "(X_X)", "(^_^)", "*^_^*", + ":-O", ":O", ":-0", "O_O", "o_O", "O_o", + "-_-", "(-.-) zzz", "(~_~)", "zzz", + r"¯\_(._.)_/¯", "(¬_¬)", "(*_*)", "(>_>)", "(<_<)", + "OwO", "UwU", ">w<", "^w^", "^u^", "rawr x3", ":3", ":3c", "x3", "nya~", "n_n", "(>ω<)", ":33", "^3^", + "^///^", "(//▽//)", "(*^///^*)", ">///<", "^_^;", "^///^;", + "(*^▽^*)", "(*´▽`*)", "UwU~", "OwO~", + ":33", "x3", ":3~", ":3c", "owo", "uwu", "rawr", ":33p", + "xD", ";-)", ";)", ":-P", ":P", ":-|", ":|", "(o_O)", "(O_o)", "('_')", + ] + + ascii_choices = [c for c in choices if all(ord(ch) < 128 for ch in c)] + if not ascii_choices: + ascii_choices = [":)", ":D", ";)", ":P"] + + popup = wx.PopupTransientWindow(self, wx.BORDER_SIMPLE) + panel = wx.Panel(popup) + sizer = wx.BoxSizer(wx.VERTICAL) + listbox = wx.ListBox(panel, choices=ascii_choices, style=wx.LB_SINGLE) + sizer.Add(listbox, 1, wx.EXPAND | wx.ALL, 4) + panel.SetSizerAndFit(sizer) + popup.SetClientSize(panel.GetBestSize()) + + # Keep a reference so the popup isn't GC'd + self._kaomoji_popup = popup + + def on_select(evt): + """Handle selection from the kaomoji list (keyboard or programmatic).""" + # Ignore synthetic selection changes triggered only for hover visualization + if getattr(self, "_suppress_kaomoji_select", False): + return + + try: + idx = evt.GetSelection() + except AttributeError: + # Fallback for synthetic events where we used SetInt() + idx = evt.GetInt() if hasattr(evt, "GetInt") else -1 + + try: + if 0 <= idx < len(ascii_choices): + choice = ascii_choices[idx] + if choice: + # Insert at current caret position, but DO NOT auto-send. + current = self.input_ctrl.GetValue() + pos = self.input_ctrl.GetInsertionPoint() + needs_space = pos > 0 and not current[pos - 1].isspace() + insert_text = (" " + choice) if needs_space else choice + new_value = current[:pos] + insert_text + current[pos:] + self.input_ctrl.SetValue(new_value) + self.input_ctrl.SetInsertionPoint(pos + len(insert_text)) + finally: + popup.Dismiss() + + def on_left_click(evt): + """Single left-click handler for the kaomoji menu.""" + try: + pos = evt.GetPosition() + idx = listbox.HitTest(pos) + if idx != wx.NOT_FOUND: + # Ensure the item is selected, then reuse on_select logic + listbox.SetSelection(idx) + cmd_evt = wx.CommandEvent(wx.wxEVT_LISTBOX, listbox.GetId()) + cmd_evt.SetEventObject(listbox) + cmd_evt.SetInt(idx) + on_select(cmd_evt) + else: + evt.Skip() + except Exception as e: + logger.error(f"Error in kaomoji left-click handler: {e}") + + def on_motion(evt): + """Visual hover selector so the current row is highlighted.""" + try: + pos = evt.GetPosition() + idx = listbox.HitTest(pos) + current_sel = listbox.GetSelection() + + if idx != wx.NOT_FOUND and idx != current_sel: + # Temporarily suppress on_select so hover highlight doesn't send + self._suppress_kaomoji_select = True + try: + listbox.SetSelection(idx) + finally: + self._suppress_kaomoji_select = False + elif idx == wx.NOT_FOUND: + # Optionally clear selection when hovering outside items + self._suppress_kaomoji_select = True + try: + listbox.DeselectAll() + finally: + self._suppress_kaomoji_select = False + except Exception as e: + logger.error(f"Error in kaomoji hover handler: {e}") + finally: + evt.Skip() + + # Single left-click selects and sends; keyboard selection still works + listbox.Bind(wx.EVT_LISTBOX, on_select) + listbox.Bind(wx.EVT_LEFT_DOWN, on_left_click) + listbox.Bind(wx.EVT_MOTION, on_motion) + + # Position popup under the kaomoji button + btn = self.kaomoji_btn + btn_pos = btn.ClientToScreen((0, btn.GetSize().height)) + popup.Position(btn_pos, (0, 0)) + popup.Popup() + except Exception as e: + logger.error(f"Error in kaomoji picker: {e}") \ No newline at end of file diff --git a/src/ScanWizard.py b/src/ScanWizard.py index 9c96606..ff83008 100644 --- a/src/ScanWizard.py +++ b/src/ScanWizard.py @@ -229,24 +229,41 @@ class ScanWizardResultsPage(adv.WizardPageSimple): start_callback(params) def on_scan_progress(self, scanned, total): - total = max(total, 1) - self.gauge.SetRange(total) - self.gauge.SetValue(min(scanned, total)) - self.summary.SetLabel(f"Scanning… {scanned}/{total} hosts checked") + try: + total = max(total, 1) + self.gauge.SetRange(total) + self.gauge.SetValue(min(scanned, total)) + self.summary.SetLabel(f"Scanning… {scanned}/{total} hosts checked") + except RuntimeError: + # C++ SHIT + logger.debug("Scan progress update after controls destroyed; ignoring") def on_scan_result(self, server_info): - idx = self.results_list.InsertItem(self.results_list.GetItemCount(), server_info["address"]) - self.results_list.SetItem(idx, 1, str(server_info["port"])) - self.results_list.SetItem(idx, 2, server_info.get("banner", "IRC server detected")) - self.discovered.append(server_info) - self.summary.SetLabel(f"Found {len(self.discovered)} {"server" if self.discovered == 1 else "servers"}") + """Handle a single discovered server row.""" + try: + idx = self.results_list.InsertItem(self.results_list.GetItemCount(), server_info["address"]) + self.results_list.SetItem(idx, 1, str(server_info["port"])) + self.results_list.SetItem(idx, 2, server_info.get("banner", "IRC server detected")) + self.discovered.append(server_info) + self.summary.SetLabel( + f"Found {len(self.discovered)} {'server' if len(self.discovered) == 1 else 'servers'}" + ) + except RuntimeError: + logger.debug("Scan result update after controls destroyed; ignoring") def on_scan_complete(self, results): - if results: - self.summary.SetLabel(f"Scan complete : {len(results)} {"server" if len(results) == 1 else "servers"} ready.") - else: - self.summary.SetLabel("Scan complete : no IRC servers discovered.") - self._toggle_buttons() + """Final scan completion callback.""" + try: + if results: + self.summary.SetLabel( + f"Scan complete : {len(results)} " + f"{'server' if len(results) == 1 else 'servers'} ready." + ) + else: + self.summary.SetLabel("Scan complete : no IRC servers discovered.") + self._toggle_buttons() + except RuntimeError: + logger.debug("Scan completion update after controls destroyed; ignoring") def on_quick_connect(self, event): row = self.results_list.GetFirstSelected()