Add command autocomplete feature to IRCPanel
- Introduced CommandAutocomplete class for command suggestions. - Enhanced IRCPanel to show command suggestions while typing. - Improved error handling and user feedback for command execution. - Updated message handling to support formatted text display.
This commit is contained in:
398
src/IRCPanel.py
398
src/IRCPanel.py
@@ -200,7 +200,179 @@ class KaomojiPicker(wx.PopupTransientWindow):
|
|||||||
self.Popup()
|
self.Popup()
|
||||||
|
|
||||||
|
|
||||||
|
class CommandAutocomplete(wx.PopupTransientWindow):
|
||||||
|
"""Popup window for IRC command autocomplete, similar to Minecraft."""
|
||||||
|
def __init__(self, parent, commands, on_select_callback):
|
||||||
|
super().__init__(parent, wx.BORDER_SIMPLE)
|
||||||
|
self.on_select_callback = on_select_callback
|
||||||
|
self.commands = commands # List of (command, description) tuples
|
||||||
|
self.filtered_commands = commands.copy()
|
||||||
|
self.selected_index = 0
|
||||||
|
|
||||||
|
self._init_ui()
|
||||||
|
self.Bind(wx.EVT_WINDOW_DESTROY, self.on_destroy)
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
panel = wx.Panel(self)
|
||||||
|
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
|
# Command list
|
||||||
|
self.list_ctrl = wx.ListCtrl(panel, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_NO_HEADER)
|
||||||
|
self.list_ctrl.InsertColumn(0, "Command", width=120)
|
||||||
|
self.list_ctrl.InsertColumn(1, "Description", width=280)
|
||||||
|
|
||||||
|
self._update_list()
|
||||||
|
|
||||||
|
# Bind events
|
||||||
|
self.list_ctrl.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.on_item_activated)
|
||||||
|
self.list_ctrl.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_item_selected)
|
||||||
|
self.Bind(wx.EVT_CHAR_HOOK, self.on_char_hook)
|
||||||
|
self.Bind(wx.EVT_KILL_FOCUS, self.on_kill_focus)
|
||||||
|
|
||||||
|
main_sizer.Add(self.list_ctrl, 1, wx.EXPAND | wx.ALL, 2)
|
||||||
|
panel.SetSizer(main_sizer)
|
||||||
|
main_sizer.Fit(panel)
|
||||||
|
self.SetClientSize(panel.GetBestSize())
|
||||||
|
|
||||||
|
# Select first item
|
||||||
|
if self.list_ctrl.GetItemCount() > 0:
|
||||||
|
self.list_ctrl.Select(0)
|
||||||
|
self.selected_index = 0
|
||||||
|
|
||||||
|
def _update_list(self):
|
||||||
|
"""Update the list with filtered commands."""
|
||||||
|
self.list_ctrl.DeleteAllItems()
|
||||||
|
for cmd, desc in self.filtered_commands:
|
||||||
|
idx = self.list_ctrl.InsertItem(self.list_ctrl.GetItemCount(), cmd)
|
||||||
|
self.list_ctrl.SetItem(idx, 1, desc)
|
||||||
|
|
||||||
|
# Resize to fit content (max 8 items visible)
|
||||||
|
item_height = 20
|
||||||
|
max_items = min(8, len(self.filtered_commands))
|
||||||
|
self.list_ctrl.SetSize((410, item_height * max_items + 4))
|
||||||
|
self.SetClientSize((410, item_height * max_items + 4))
|
||||||
|
|
||||||
|
def filter_commands(self, search_text):
|
||||||
|
"""Filter commands based on search text."""
|
||||||
|
search_lower = search_text.lower().strip()
|
||||||
|
if not search_lower:
|
||||||
|
self.filtered_commands = self.commands.copy()
|
||||||
|
else:
|
||||||
|
self.filtered_commands = [
|
||||||
|
(cmd, desc) for cmd, desc in self.commands
|
||||||
|
if cmd.lower().startswith(search_lower)
|
||||||
|
]
|
||||||
|
|
||||||
|
self.selected_index = 0
|
||||||
|
self._update_list()
|
||||||
|
if self.list_ctrl.GetItemCount() > 0:
|
||||||
|
self.list_ctrl.Select(0)
|
||||||
|
|
||||||
|
def on_item_activated(self, event):
|
||||||
|
"""Handle double-click or Enter on item."""
|
||||||
|
idx = event.GetIndex()
|
||||||
|
if 0 <= idx < len(self.filtered_commands):
|
||||||
|
cmd, _ = self.filtered_commands[idx]
|
||||||
|
if self.on_select_callback:
|
||||||
|
self.on_select_callback(cmd)
|
||||||
|
self.safe_dismiss()
|
||||||
|
|
||||||
|
def on_item_selected(self, event):
|
||||||
|
"""Handle item selection."""
|
||||||
|
self.selected_index = event.GetIndex()
|
||||||
|
|
||||||
|
def select_next(self):
|
||||||
|
"""Select next item."""
|
||||||
|
if self.list_ctrl.GetItemCount() > 0:
|
||||||
|
self.selected_index = (self.selected_index + 1) % self.list_ctrl.GetItemCount()
|
||||||
|
self.list_ctrl.Select(self.selected_index)
|
||||||
|
self.list_ctrl.EnsureVisible(self.selected_index)
|
||||||
|
|
||||||
|
def select_previous(self):
|
||||||
|
"""Select previous item."""
|
||||||
|
if self.list_ctrl.GetItemCount() > 0:
|
||||||
|
self.selected_index = (self.selected_index - 1) % self.list_ctrl.GetItemCount()
|
||||||
|
self.list_ctrl.Select(self.selected_index)
|
||||||
|
self.list_ctrl.EnsureVisible(self.selected_index)
|
||||||
|
|
||||||
|
def get_selected_command(self):
|
||||||
|
"""Get the currently selected command."""
|
||||||
|
if 0 <= self.selected_index < len(self.filtered_commands):
|
||||||
|
return self.filtered_commands[self.selected_index][0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def on_char_hook(self, event):
|
||||||
|
"""Handle keyboard events."""
|
||||||
|
keycode = event.GetKeyCode()
|
||||||
|
if keycode == wx.WXK_ESCAPE:
|
||||||
|
self.safe_dismiss()
|
||||||
|
elif keycode == wx.WXK_UP:
|
||||||
|
self.select_previous()
|
||||||
|
elif keycode == wx.WXK_DOWN:
|
||||||
|
self.select_next()
|
||||||
|
elif keycode == wx.WXK_RETURN or keycode == wx.WXK_NUMPAD_ENTER:
|
||||||
|
cmd = self.get_selected_command()
|
||||||
|
if cmd and self.on_select_callback:
|
||||||
|
self.on_select_callback(cmd)
|
||||||
|
self.safe_dismiss()
|
||||||
|
else:
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
|
def on_kill_focus(self, event):
|
||||||
|
"""Handle focus loss."""
|
||||||
|
focused = wx.Window.FindFocus()
|
||||||
|
if focused and (focused == self.list_ctrl or self.IsDescendant(focused)):
|
||||||
|
event.Skip()
|
||||||
|
return
|
||||||
|
wx.CallLater(100, self.safe_dismiss)
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
|
def on_destroy(self, event):
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
|
def safe_dismiss(self):
|
||||||
|
"""Safely dismiss the popup."""
|
||||||
|
if not self.IsBeingDeleted() and self.IsShown():
|
||||||
|
try:
|
||||||
|
self.Dismiss()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error dismissing command autocomplete: {e}")
|
||||||
|
|
||||||
|
def show_at_input(self, input_ctrl):
|
||||||
|
"""Show popup near the input control."""
|
||||||
|
input_screen_pos = input_ctrl.ClientToScreen((0, 0))
|
||||||
|
popup_size = self.GetSize()
|
||||||
|
display_size = wx.GetDisplaySize()
|
||||||
|
|
||||||
|
# Show above the input, otherwise below
|
||||||
|
if input_screen_pos.y - popup_size.height > 0:
|
||||||
|
popup_y = input_screen_pos.y - popup_size.height - 2
|
||||||
|
else:
|
||||||
|
popup_y = input_screen_pos.y + input_ctrl.GetSize().height + 2
|
||||||
|
|
||||||
|
# Keep popup on screen horizontally
|
||||||
|
popup_x = max(10, min(input_screen_pos.x, display_size.x - popup_size.width - 10))
|
||||||
|
|
||||||
|
self.Position((popup_x, popup_y), (0, 0))
|
||||||
|
self.Popup()
|
||||||
|
|
||||||
|
|
||||||
class IRCPanel(wx.Panel):
|
class IRCPanel(wx.Panel):
|
||||||
|
# IRC commands with descriptions
|
||||||
|
IRC_COMMANDS = [
|
||||||
|
("help", "Show available commands"),
|
||||||
|
("join", "Join a channel - /join <channel>"),
|
||||||
|
("part", "Leave current or specified channel - /part [channel]"),
|
||||||
|
("msg", "Send private message - /msg <nick> <message>"),
|
||||||
|
("me", "Send action message - /me <action>"),
|
||||||
|
("nick", "Change nickname - /nick <newnick>"),
|
||||||
|
("whois", "Get user information - /whois <nick>"),
|
||||||
|
("topic", "Get or set channel topic - /topic [newtopic]"),
|
||||||
|
("kick", "Kick user from channel - /kick <user> [reason]"),
|
||||||
|
("away", "Set away status - /away [message]"),
|
||||||
|
("quit", "Disconnect from server - /quit [message]"),
|
||||||
|
]
|
||||||
|
|
||||||
# Default kaomoji groups
|
# Default kaomoji groups
|
||||||
KAOMOJI_GROUPS = {
|
KAOMOJI_GROUPS = {
|
||||||
"Happy": [
|
"Happy": [
|
||||||
@@ -245,6 +417,7 @@ class IRCPanel(wx.Panel):
|
|||||||
self.history = []
|
self.history = []
|
||||||
self.history_pos = -1
|
self.history_pos = -1
|
||||||
self.current_popup = None
|
self.current_popup = None
|
||||||
|
self.command_popup = None
|
||||||
|
|
||||||
# Search state
|
# Search state
|
||||||
self.search_text = ""
|
self.search_text = ""
|
||||||
@@ -262,10 +435,10 @@ class IRCPanel(wx.Panel):
|
|||||||
"""Initialize the UI components."""
|
"""Initialize the UI components."""
|
||||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
# Text display
|
# Text display - use TE_RICH2 for styled text support (especially on Windows)
|
||||||
self.text_ctrl = wx.TextCtrl(
|
self.text_ctrl = wx.TextCtrl(
|
||||||
self,
|
self,
|
||||||
style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_AUTO_URL
|
style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_AUTO_URL | wx.TE_RICH2
|
||||||
)
|
)
|
||||||
self.text_ctrl.SetFont(self._load_system_font())
|
self.text_ctrl.SetFont(self._load_system_font())
|
||||||
self._apply_theme()
|
self._apply_theme()
|
||||||
@@ -288,6 +461,7 @@ class IRCPanel(wx.Panel):
|
|||||||
self.input_ctrl.SetHint("Type message here…")
|
self.input_ctrl.SetHint("Type message here…")
|
||||||
self.input_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_send)
|
self.input_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_send)
|
||||||
self.input_ctrl.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
|
self.input_ctrl.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
|
||||||
|
self.input_ctrl.Bind(wx.EVT_TEXT, self.on_input_text_change)
|
||||||
|
|
||||||
# Kaomoji button
|
# Kaomoji button
|
||||||
random_kaomoji = self._get_random_kaomoji()
|
random_kaomoji = self._get_random_kaomoji()
|
||||||
@@ -380,14 +554,15 @@ class IRCPanel(wx.Panel):
|
|||||||
"""Thread-safe message addition."""
|
"""Thread-safe message addition."""
|
||||||
try:
|
try:
|
||||||
if wx.IsMainThread():
|
if wx.IsMainThread():
|
||||||
self._add_message_safe(message)
|
self._add_message_safe(message, username_color, message_color, bold, italic, underline)
|
||||||
else:
|
else:
|
||||||
wx.CallAfter(self._add_message_safe, message)
|
wx.CallAfter(self._add_message_safe, message, username_color, message_color, bold, italic, underline)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in add_message: {e}")
|
logger.error(f"Error in add_message: {e}")
|
||||||
|
|
||||||
def _add_message_safe(self, message):
|
def _add_message_safe(self, message, username_color=None, message_color=None,
|
||||||
"""Add message to display (must be called from main thread)."""
|
bold=False, italic=False, underline=False):
|
||||||
|
"""Add message to display with formatting (must be called from main thread)."""
|
||||||
try:
|
try:
|
||||||
self.messages.append(message)
|
self.messages.append(message)
|
||||||
|
|
||||||
@@ -396,9 +571,29 @@ class IRCPanel(wx.Panel):
|
|||||||
current_pos = self.text_ctrl.GetInsertionPoint()
|
current_pos = self.text_ctrl.GetInsertionPoint()
|
||||||
at_bottom = (last_pos - current_pos) <= 10
|
at_bottom = (last_pos - current_pos) <= 10
|
||||||
|
|
||||||
# Append message
|
# Create text attribute for styling
|
||||||
|
text_attr = wx.TextAttr()
|
||||||
|
if message_color:
|
||||||
|
text_attr.SetTextColour(message_color)
|
||||||
|
else:
|
||||||
|
text_attr.SetTextColour(self.default_text_colour)
|
||||||
|
|
||||||
|
if bold:
|
||||||
|
pass # i hate bold
|
||||||
|
if italic:
|
||||||
|
text_attr.SetFontStyle(wx.FONTSTYLE_ITALIC)
|
||||||
|
if underline:
|
||||||
|
text_attr.SetUnderlined(True)
|
||||||
|
|
||||||
|
# Set style and append text
|
||||||
|
self.text_ctrl.SetDefaultStyle(text_attr)
|
||||||
self.text_ctrl.AppendText(message + "\n")
|
self.text_ctrl.AppendText(message + "\n")
|
||||||
|
|
||||||
|
# Reset to default style
|
||||||
|
default_attr = wx.TextAttr()
|
||||||
|
default_attr.SetTextColour(self.default_text_colour)
|
||||||
|
self.text_ctrl.SetDefaultStyle(default_attr)
|
||||||
|
|
||||||
# Auto-scroll if at bottom
|
# Auto-scroll if at bottom
|
||||||
if at_bottom:
|
if at_bottom:
|
||||||
self.text_ctrl.SetInsertionPoint(self.text_ctrl.GetLastPosition())
|
self.text_ctrl.SetInsertionPoint(self.text_ctrl.GetLastPosition())
|
||||||
@@ -409,20 +604,60 @@ class IRCPanel(wx.Panel):
|
|||||||
|
|
||||||
def add_formatted_message(self, timestamp, username, content,
|
def add_formatted_message(self, timestamp, username, content,
|
||||||
username_color=None, is_action=False):
|
username_color=None, is_action=False):
|
||||||
"""Add a formatted IRC message."""
|
"""Add a formatted IRC message with colored username."""
|
||||||
try:
|
try:
|
||||||
|
# Check if user is at bottom
|
||||||
|
last_pos = self.text_ctrl.GetLastPosition()
|
||||||
|
current_pos = self.text_ctrl.GetInsertionPoint()
|
||||||
|
at_bottom = (last_pos - current_pos) <= 10
|
||||||
|
|
||||||
|
# Append timestamp with default color
|
||||||
|
default_attr = wx.TextAttr()
|
||||||
|
default_attr.SetTextColour(self.default_text_colour)
|
||||||
|
self.text_ctrl.SetDefaultStyle(default_attr)
|
||||||
|
self.text_ctrl.AppendText(timestamp)
|
||||||
|
|
||||||
|
# Append action marker or opening bracket
|
||||||
if is_action:
|
if is_action:
|
||||||
message = f"{timestamp}* {username} {content}"
|
self.text_ctrl.AppendText("* ")
|
||||||
else:
|
else:
|
||||||
message = f"{timestamp}<{username}> {content}"
|
self.text_ctrl.AppendText("<")
|
||||||
self.add_message(message)
|
|
||||||
|
# Append username with color
|
||||||
|
if username_color:
|
||||||
|
username_attr = wx.TextAttr()
|
||||||
|
username_attr.SetTextColour(username_color)
|
||||||
|
username_attr.SetFontWeight(wx.FONTWEIGHT_BOLD)
|
||||||
|
self.text_ctrl.SetDefaultStyle(username_attr)
|
||||||
|
else:
|
||||||
|
default_attr.SetFontWeight(wx.FONTWEIGHT_BOLD)
|
||||||
|
self.text_ctrl.SetDefaultStyle(default_attr)
|
||||||
|
|
||||||
|
self.text_ctrl.AppendText(username)
|
||||||
|
|
||||||
|
# Append closing bracket and content with default color
|
||||||
|
default_attr.SetFontWeight(wx.FONTWEIGHT_NORMAL)
|
||||||
|
self.text_ctrl.SetDefaultStyle(default_attr)
|
||||||
|
if not is_action:
|
||||||
|
self.text_ctrl.AppendText("> ")
|
||||||
|
else:
|
||||||
|
self.text_ctrl.AppendText(" ")
|
||||||
|
|
||||||
|
self.text_ctrl.AppendText(content)
|
||||||
|
self.text_ctrl.AppendText("\n")
|
||||||
|
|
||||||
|
# Auto-scroll if at bottom
|
||||||
|
if at_bottom:
|
||||||
|
self.text_ctrl.SetInsertionPoint(self.text_ctrl.GetLastPosition())
|
||||||
|
self.text_ctrl.ShowPosition(self.text_ctrl.GetLastPosition())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding formatted message: {e}")
|
logger.error(f"Error adding formatted message: {e}")
|
||||||
|
|
||||||
def add_system_message(self, message, color=None, bold=False):
|
def add_system_message(self, message, color=None, bold=False):
|
||||||
"""Add a system message."""
|
"""Add a system message with optional color and bold formatting."""
|
||||||
try:
|
try:
|
||||||
self.add_message(message)
|
self.add_message(message, message_color=color, bold=bold)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding system message: {e}")
|
logger.error(f"Error adding system message: {e}")
|
||||||
|
|
||||||
@@ -467,6 +702,7 @@ class IRCPanel(wx.Panel):
|
|||||||
def on_send(self, event):
|
def on_send(self, event):
|
||||||
"""Send the current message."""
|
"""Send the current message."""
|
||||||
try:
|
try:
|
||||||
|
self._hide_command_popup() # Hide command popup when sending
|
||||||
message = self.input_ctrl.GetValue().strip()
|
message = self.input_ctrl.GetValue().strip()
|
||||||
if message and self.target:
|
if message and self.target:
|
||||||
self.history.append(message)
|
self.history.append(message)
|
||||||
@@ -476,11 +712,110 @@ class IRCPanel(wx.Panel):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending message: {e}")
|
logger.error(f"Error sending message: {e}")
|
||||||
|
|
||||||
|
def on_input_text_change(self, event):
|
||||||
|
"""Handle text changes in input field to show command autocomplete."""
|
||||||
|
try:
|
||||||
|
current_text = self.input_ctrl.GetValue()
|
||||||
|
pos = self.input_ctrl.GetInsertionPoint()
|
||||||
|
|
||||||
|
# Check if we're typing a command (starts with /)
|
||||||
|
if current_text.startswith('/') and pos > 0:
|
||||||
|
# Extract the command part (everything after / up to cursor or space)
|
||||||
|
text_before_cursor = current_text[:pos]
|
||||||
|
if ' ' in text_before_cursor:
|
||||||
|
# Command already has arguments, don't show autocomplete
|
||||||
|
self._hide_command_popup()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the command being typed
|
||||||
|
command_part = text_before_cursor[1:] # Remove the '/'
|
||||||
|
self._show_command_popup(command_part)
|
||||||
|
else:
|
||||||
|
self._hide_command_popup()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in text change handler: {e}")
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
|
def _show_command_popup(self, command_part=""):
|
||||||
|
"""Show or update command autocomplete popup."""
|
||||||
|
try:
|
||||||
|
if not self.command_popup or self.command_popup.IsBeingDeleted():
|
||||||
|
# Create new popup
|
||||||
|
self.command_popup = CommandAutocomplete(
|
||||||
|
self,
|
||||||
|
self.IRC_COMMANDS,
|
||||||
|
self.on_command_select
|
||||||
|
)
|
||||||
|
self.command_popup.show_at_input(self.input_ctrl)
|
||||||
|
|
||||||
|
# Filter commands
|
||||||
|
self.command_popup.filter_commands(command_part)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error showing command popup: {e}")
|
||||||
|
|
||||||
|
def _hide_command_popup(self):
|
||||||
|
"""Hide command autocomplete popup."""
|
||||||
|
try:
|
||||||
|
if self.command_popup and not self.command_popup.IsBeingDeleted():
|
||||||
|
self.command_popup.safe_dismiss()
|
||||||
|
self.command_popup = None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error hiding command popup: {e}")
|
||||||
|
|
||||||
|
def on_command_select(self, command):
|
||||||
|
"""Handle command selection from autocomplete."""
|
||||||
|
try:
|
||||||
|
current_text = self.input_ctrl.GetValue()
|
||||||
|
pos = self.input_ctrl.GetInsertionPoint()
|
||||||
|
|
||||||
|
# Find the command part (from / to cursor or space)
|
||||||
|
text_before_cursor = current_text[:pos]
|
||||||
|
if text_before_cursor.startswith('/'):
|
||||||
|
# Replace the command part with the selected command
|
||||||
|
if ' ' in text_before_cursor:
|
||||||
|
# Has arguments, keep them
|
||||||
|
space_pos = text_before_cursor.find(' ')
|
||||||
|
new_text = f"/{command}{current_text[space_pos:]}"
|
||||||
|
else:
|
||||||
|
# No arguments, just replace command
|
||||||
|
new_text = f"/{command} {current_text[pos:]}"
|
||||||
|
|
||||||
|
self.input_ctrl.SetValue(new_text)
|
||||||
|
# Position cursor after command and space
|
||||||
|
new_pos = len(f"/{command} ")
|
||||||
|
self.input_ctrl.SetInsertionPoint(new_pos)
|
||||||
|
self.input_ctrl.SetFocus()
|
||||||
|
|
||||||
|
self._hide_command_popup()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error selecting command: {e}")
|
||||||
|
|
||||||
def on_key_down(self, event):
|
def on_key_down(self, event):
|
||||||
"""Handle input field keyboard events."""
|
"""Handle input field keyboard events."""
|
||||||
try:
|
try:
|
||||||
keycode = event.GetKeyCode()
|
keycode = event.GetKeyCode()
|
||||||
|
|
||||||
|
# Handle command popup navigation
|
||||||
|
if self.command_popup and not self.command_popup.IsBeingDeleted() and self.command_popup.IsShown():
|
||||||
|
if keycode == wx.WXK_UP:
|
||||||
|
self.command_popup.select_previous()
|
||||||
|
return # Don't skip, handled
|
||||||
|
elif keycode == wx.WXK_DOWN:
|
||||||
|
self.command_popup.select_next()
|
||||||
|
return # Don't skip, handled
|
||||||
|
elif keycode == wx.WXK_RETURN or keycode == wx.WXK_NUMPAD_ENTER:
|
||||||
|
# If popup is shown, select command instead of sending
|
||||||
|
cmd = self.command_popup.get_selected_command()
|
||||||
|
if cmd:
|
||||||
|
self.on_command_select(cmd)
|
||||||
|
return # Don't skip, handled
|
||||||
|
elif keycode == wx.WXK_ESCAPE:
|
||||||
|
self._hide_command_popup()
|
||||||
|
return # Don't skip, handled
|
||||||
|
|
||||||
if keycode == wx.WXK_UP:
|
if keycode == wx.WXK_UP:
|
||||||
self._navigate_history_up()
|
self._navigate_history_up()
|
||||||
elif keycode == wx.WXK_DOWN:
|
elif keycode == wx.WXK_DOWN:
|
||||||
@@ -517,20 +852,47 @@ class IRCPanel(wx.Panel):
|
|||||||
self.input_ctrl.Clear()
|
self.input_ctrl.Clear()
|
||||||
|
|
||||||
def handle_tab_completion(self):
|
def handle_tab_completion(self):
|
||||||
"""Handle nickname tab completion."""
|
"""Handle tab completion for commands and nicknames."""
|
||||||
try:
|
try:
|
||||||
current_text = self.input_ctrl.GetValue()
|
current_text = self.input_ctrl.GetValue()
|
||||||
if not current_text or not self.target or not self.target.startswith('#'):
|
if not current_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
pos = self.input_ctrl.GetInsertionPoint()
|
||||||
|
text_before = current_text[:pos]
|
||||||
|
|
||||||
|
# Check if we're completing a command (starts with / and no space yet)
|
||||||
|
if text_before.startswith('/') and ' ' not in text_before:
|
||||||
|
command_part = text_before[1:] # Remove the '/'
|
||||||
|
matches = [cmd for cmd, _ in self.IRC_COMMANDS if cmd.lower().startswith(command_part.lower())]
|
||||||
|
|
||||||
|
if len(matches) == 1:
|
||||||
|
# Single match - complete it
|
||||||
|
new_text = f"/{matches[0]} {current_text[pos:]}"
|
||||||
|
self.input_ctrl.SetValue(new_text)
|
||||||
|
self.input_ctrl.SetInsertionPoint(len(f"/{matches[0]} "))
|
||||||
|
elif len(matches) > 1:
|
||||||
|
# Multiple matches - show autocomplete popup or status
|
||||||
|
if not self.command_popup or self.command_popup.IsBeingDeleted():
|
||||||
|
self._show_command_popup(command_part)
|
||||||
|
else:
|
||||||
|
# Update existing popup
|
||||||
|
self.command_popup.filter_commands(command_part)
|
||||||
|
display = ', '.join(matches[:5])
|
||||||
|
if len(matches) > 5:
|
||||||
|
display += '...'
|
||||||
|
self.main_frame.SetStatusText(f"Tab completion: {display}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Otherwise, try nickname completion (only in channels)
|
||||||
|
if not self.target or not self.target.startswith('#'):
|
||||||
return
|
return
|
||||||
|
|
||||||
users = self.main_frame.channel_users.get(self.target, [])
|
users = self.main_frame.channel_users.get(self.target, [])
|
||||||
if not users:
|
if not users:
|
||||||
return
|
return
|
||||||
|
|
||||||
pos = self.input_ctrl.GetInsertionPoint()
|
|
||||||
text_before = current_text[:pos]
|
|
||||||
words = text_before.split()
|
words = text_before.split()
|
||||||
|
|
||||||
if not words:
|
if not words:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
100
src/main.py
100
src/main.py
@@ -1059,38 +1059,76 @@ Available commands:
|
|||||||
user_color = self.get_user_color(self.nick)
|
user_color = self.get_user_color(self.nick)
|
||||||
timestamp = self.get_timestamp()
|
timestamp = self.get_timestamp()
|
||||||
self.channels[target].add_formatted_message(timestamp, self.nick, args, user_color, is_action=True)
|
self.channels[target].add_formatted_message(timestamp, self.nick, args, user_color, is_action=True)
|
||||||
elif cmd == "nick" and self.is_connected():
|
elif cmd == "nick":
|
||||||
self.connection.nick(args)
|
if self.is_connected():
|
||||||
elif cmd == "join" and self.is_connected():
|
self.connection.nick(args)
|
||||||
if args and not args.startswith('#'):
|
|
||||||
args = '#' + args
|
|
||||||
self.connection.join(args)
|
|
||||||
elif cmd == "part" and self.is_connected():
|
|
||||||
channel = args if args else target
|
|
||||||
self.connection.part(channel)
|
|
||||||
elif cmd == "quit" and self.is_connected():
|
|
||||||
reason = args if args else "Goodbye"
|
|
||||||
self.connection.quit(reason)
|
|
||||||
elif cmd == "msg" and self.is_connected():
|
|
||||||
nick_msg = args.split(' ', 1)
|
|
||||||
if len(nick_msg) == 2:
|
|
||||||
self.connection.privmsg(nick_msg[0], nick_msg[1])
|
|
||||||
elif cmd == "whois" and self.is_connected():
|
|
||||||
self.connection.whois([args])
|
|
||||||
elif cmd == "kick" and self.is_connected():
|
|
||||||
kick_args = args.split(' ', 1)
|
|
||||||
user = kick_args[0]
|
|
||||||
reason = kick_args[1] if len(kick_args) > 1 else "Kicked"
|
|
||||||
self.connection.kick(target, user, reason)
|
|
||||||
elif cmd == "topic" and self.is_connected():
|
|
||||||
if args:
|
|
||||||
self.connection.topic(target, args)
|
|
||||||
else:
|
else:
|
||||||
self.connection.topic(target)
|
self.safe_ui_update(self.log_server, f"Not connected. Cannot change nickname.", wx.Colour(255, 0, 0))
|
||||||
elif cmd == "away" and self.is_connected():
|
elif cmd == "join":
|
||||||
self.connection.send_raw(f"AWAY :{args}" if args else "AWAY")
|
if self.is_connected():
|
||||||
self.away = bool(args)
|
if not args:
|
||||||
self.safe_ui_update(self.away_item.Check, self.away)
|
self.safe_ui_update(self.log_server, f"Usage: /join <channel>", wx.Colour(255, 0, 0))
|
||||||
|
else:
|
||||||
|
if not args.startswith('#'):
|
||||||
|
args = '#' + args
|
||||||
|
self.connection.join(args)
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Not connected. Cannot join channel.", wx.Colour(255, 0, 0))
|
||||||
|
elif cmd == "part":
|
||||||
|
if self.is_connected():
|
||||||
|
channel = args if args else target
|
||||||
|
self.connection.part(channel)
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Not connected. Cannot part channel.", wx.Colour(255, 0, 0))
|
||||||
|
elif cmd == "quit":
|
||||||
|
if self.is_connected():
|
||||||
|
reason = args if args else "Goodbye"
|
||||||
|
self.connection.quit(reason)
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Not connected.", wx.Colour(255, 0, 0))
|
||||||
|
elif cmd == "msg":
|
||||||
|
if self.is_connected():
|
||||||
|
nick_msg = args.split(' ', 1)
|
||||||
|
if len(nick_msg) == 2:
|
||||||
|
self.connection.privmsg(nick_msg[0], nick_msg[1])
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Usage: /msg <nick> <message>", wx.Colour(255, 0, 0))
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Not connected. Cannot send private message.", wx.Colour(255, 0, 0))
|
||||||
|
elif cmd == "whois":
|
||||||
|
if self.is_connected():
|
||||||
|
if args:
|
||||||
|
self.connection.whois([args])
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Usage: /whois <nick>", wx.Colour(255, 0, 0))
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Not connected. Cannot perform WHOIS.", wx.Colour(255, 0, 0))
|
||||||
|
elif cmd == "kick":
|
||||||
|
if self.is_connected():
|
||||||
|
if args:
|
||||||
|
kick_args = args.split(' ', 1)
|
||||||
|
user = kick_args[0]
|
||||||
|
reason = kick_args[1] if len(kick_args) > 1 else "Kicked"
|
||||||
|
self.connection.kick(target, user, reason)
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Usage: /kick <user> [reason]", wx.Colour(255, 0, 0))
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Not connected. Cannot kick user.", wx.Colour(255, 0, 0))
|
||||||
|
elif cmd == "topic":
|
||||||
|
if self.is_connected():
|
||||||
|
if args:
|
||||||
|
self.connection.topic(target, args)
|
||||||
|
else:
|
||||||
|
self.connection.topic(target)
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Not connected. Cannot get/set topic.", wx.Colour(255, 0, 0))
|
||||||
|
elif cmd == "away":
|
||||||
|
if self.is_connected():
|
||||||
|
self.connection.send_raw(f"AWAY :{args}" if args else "AWAY")
|
||||||
|
self.away = bool(args)
|
||||||
|
self.safe_ui_update(self.away_item.Check, self.away)
|
||||||
|
else:
|
||||||
|
self.safe_ui_update(self.log_server, f"Not connected. Cannot set away status.", wx.Colour(255, 0, 0))
|
||||||
else:
|
else:
|
||||||
self.safe_ui_update(self.log_server, f"Unknown command: {cmd}. Use /help for available commands.", wx.Colour(255, 0, 0))
|
self.safe_ui_update(self.log_server, f"Unknown command: {cmd}. Use /help for available commands.", wx.Colour(255, 0, 0))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user