diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10de9ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,159 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Qt Creator +*.pro.user +*.pro.user.* + +# Qt +*.qm +*.qrc.cpp +moc_*.cpp +ui_*.py +qrc_*.py + +# StormIRC specific +# User configuration and data (don't commit personal settings) +config.json +logs/ +*.log + +# Backup files +*~ +*.bak +*.swp +*.swo +.DS_Store diff --git a/README.md b/README.md index 55b3d40..caf425c 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,274 @@ # StormIRC -A completely accessible GUI IRC client designed for users who value both accessibility and good design. +A completely accessible GUI IRC client built specifically for blind users and anyone who values keyboard-first design. Built with Python and Qt6 (PySide6). ## Features -- **Full Accessibility**: Built with GTK4 for excellent screen reader support -- **Keyboard Navigation**: Complete keyboard control with logical tab order -- **Smart Notifications**: System notifications with customizable highlight patterns -- **IRC Protocol Support**: Full IRC client with join/part, messaging, nick changes -- **Configurable**: Comprehensive settings for servers, accessibility, and UI preferences -- **Chat History Navigation**: Enhanced navigation for screen reader users +### 🎯 Accessibility First +- **Screen Reader Support**: Fully compatible with Orca, NVDA, and other screen readers +- **Keyboard Navigation**: Complete keyboard control - mouse optional +- **Text-to-Speech**: Optional Speech Dispatcher integration for self-voicing +- **Cursor Navigation**: Arrow keys work in chat history for easy navigation -## Keyboard Shortcuts +### 💬 Full IRC Protocol Support +- Multi-server support with SSL/TLS +- SASL PLAIN authentication +- Channel management (join, part, topic) +- Private messages in tabs +- User lists per channel +- Nick changes, WHOIS, CTCP ACTION (/me) +- Automatic reconnection with exponential backoff +- CAP negotiation (IRCv3) -- `F6`: Cycle through main areas (channel list, chat, input) -- `Ctrl+1`: Focus channel list -- `Ctrl+2`: Focus chat area -- `Ctrl+3`: Focus message input -- `Ctrl+J`: Quick join channel -- `Ctrl+W`: Leave current channel -- `Ctrl+N`: Next channel -- `Ctrl+P`: Previous channel -- `Ctrl+Home`: Jump to chat beginning -- `Ctrl+End`: Jump to chat end -- `Alt+Up/Down`: Scroll chat -- `Ctrl+R`: Read last messages +### 🗂️ Tabbed Interface +- **Server Console** tab for server messages +- **One tab per channel** with independent chat, topic, user list, and input +- **One tab per PM** conversation (no separate windows!) +- Easy tab navigation with keyboard shortcuts + +### ⚙️ Highly Configurable +- Multi-server configuration with auto-connect +- Per-channel autojoin settings +- Customizable highlight patterns (regex) +- Speech settings (rate, pitch, volume, voice selection) +- Window dimensions and UI preferences +- Export/import configuration for backup + +### 🔔 Smart Notifications +- System notifications for highlights and PMs +- Customizable regex highlight patterns +- Unread message counters +- Sound notifications + +### 🎹 Programmable Function Keys +- F1-F12 keys can execute: + - Plain text messages + - IRC commands (e.g., `/away I'm away`) + - Shell commands (e.g., `/usr/bin/date`) + - Shell with output to channel (e.g., `/usr/bin/fortune|`) ## Installation -1. Install dependencies: +### Prerequisites + +**Required:** +- Python 3.8 or later +- Qt6 (via PySide6) + +**Optional (for text-to-speech):** +- Speech Dispatcher + +### Install Steps + +1. **Clone the repository:** + ```bash + git clone https://git.stormux.org/storm/stormirc.git + cd stormirc + ``` + +2. **Install Python dependencies:** ```bash pip install -r requirements.txt ``` -2. Run the application: +3. **Optional - Install Speech Dispatcher:** ```bash - python run_stormirc.py + # Arch/Manjaro: + sudo pacman -S speech-dispatcher python-speechd + + # Debian/Ubuntu: + sudo apt install python3-speechd + + # Fedora: + sudo dnf install python3-speechd ``` +4. **Run StormIRC:** + ```bash + ./stormirc + ``` + + Or with debug logging: + ```bash + ./stormirc -d + ``` + +## Keyboard Shortcuts + +### Tab Navigation +- **Ctrl+Tab**: Next tab +- **Ctrl+Shift+Tab**: Previous tab +- **Ctrl+W**: Close current tab (sends PART for channels) +- **Alt+7**, **Alt+8**, **Alt+9**: Jump to tab 7, 8, or 9 + +### Configuration +- **Ctrl+S**: Save settings to disk + +### Within a Tab +- **F6**: Cycle focus (chat → input → user list → chat) +- **Ctrl+L**: Focus input field from anywhere +- **Enter** on user in user list: Open PM tab +- **Shift+Enter** in input: Insert newline (for multi-line messages) +- **Enter** in input: Send message + +### Speech Control +- **Ctrl**: Stop speech immediately +- **Alt+1**: Decrease volume (-10) +- **Alt+2**: Increase volume (+10) +- **Alt+3**: Decrease pitch (-10) +- **Alt+4**: Increase pitch (+10) +- **Alt+5**: Decrease rate (-10) +- **Alt+6**: Increase rate (+10) + +**Note:** Speech adjustments apply immediately, save to config, and announce the new value. + +### Function Keys +- **F1-F12**: Execute programmable commands (configure in Settings) + ## IRC Commands +StormIRC supports all standard IRC commands. Just prefix with `/`: + +### Channel Management - `/join #channel` - Join a channel -- `/part` - Leave current channel -- `/nick newnick` - Change nickname -- `/quit` - Disconnect and quit +- `/part [#channel]` - Leave current or specified channel +- `/list` - Browse available channels (opens dialog) +- `/topic [new topic]` - View or set channel topic + +### User Communication +- `/msg [message]` - Open PM tab and optionally send message +- `/query ` - Open PM tab (alias for /msg) +- `/me ` - Send CTCP ACTION +- `/whois ` - Get user information + +### Configuration +- `/autojoin` - Show autojoin channels for current server +- `/autojoin add #channel1,#channel2` - Add channels to autojoin +- `/autojoin remove #channel` - Remove channel from autojoin +- `/autojoin clear` - Clear all autojoin channels +- `/autojoin list` - List autojoin channels + +### Connection +- `/nick ` - Change nickname +- `/quit [message]` - Disconnect and quit application + +### Any Other IRC Command +Send raw IRC commands by prefixing with `/`: +- `/mode #channel +o nick` - Give ops +- `/kick #channel nick [reason]` - Kick user +- `/away [message]` - Set away status +- etc. ## Configuration -StormIRC stores its configuration in `~/.config/stormirc/config.json`. You can: +StormIRC stores configuration in `~/.config/stormirc/config.json`. -- Add multiple IRC servers -- Configure accessibility preferences -- Set custom highlight patterns -- Adjust UI settings +### Settings Dialog -Access settings through the gear icon in the header bar. +Access via menu or keyboard shortcut. Five tabs: + +1. **General**: Window dimensions, timestamps, theme +2. **Accessibility**: Screen reader, notifications, display options +3. **Speech**: TTS enable/disable, rate, pitch, volume, voice selection +4. **Servers**: Add, edit, remove IRC servers +5. **Highlights**: Manage regex highlight patterns +6. **Function Keys**: Configure F1-F12 programmable keys + +### Server Configuration + +Each server can have: +- Host, port, SSL/TLS +- Nickname, username, real name +- Optional server password +- SASL authentication (username/password) +- Auto-connect on startup +- Auto-join channels (per-channel checkbox) + +### Message Logging + +All messages are automatically logged to `~/.config/stormirc/logs/`: +- Per-server, per-channel log files +- Daily rotation (YYYY-MM-DD format) +- Recent history loaded when opening channels ## Accessibility Features -- Screen reader announcements for important events -- Logical keyboard navigation -- Proper widget labeling and descriptions -- High contrast support ready -- Customizable notification preferences +### Screen Reader Compatibility +- Proper ARIA roles and labels for all UI elements +- Live message announcements (when window is active) +- Logical tab order +- Context announcements when switching tabs + +### Keyboard-First Design +- All functionality accessible via keyboard +- No mouse-only interactions +- Intuitive keyboard shortcuts +- F6 cycling for common navigation patterns + +### Text-to-Speech (Optional) +- Speech Dispatcher integration +- Configurable voice, rate, pitch, volume +- On-the-fly adjustments with Alt+1 through Alt+6 +- Per-message speech control +- Interrupt speech with Ctrl key +- Optional reading of your own messages + +### Visual Accessibility +- High DPI support +- Customizable font sizes +- Clear focus indicators - Unread message indicators -## Building for those panzies who need GUI +## Architecture -This IRC client proves that accessibility and good UX aren't mutually exclusive. While terminal purists have irssi, this gives everyone else a proper IRC experience that works beautifully with assistive technologies. +``` +stormirc/ +├── stormirc # Main executable +├── requirements.txt # Python dependencies +└── src/ + ├── irc/ + │ └── client.py # IRC protocol implementation + ├── config/ + │ └── settings.py # Configuration management + └── ui/ + ├── main_window.py # Main tabbed window + ├── channel_tab.py # Reusable tab widget + ├── accessible_tree.py # Accessible tree widget + ├── settings_dialog.py # Settings dialog + ├── autocomplete_textedit.py # Text input with autocomplete + ├── speech.py # Speech Dispatcher TTS + ├── sound.py # Sound notifications + └── logger.py # Message logging +``` -Built with Python, GTK4, and lots of care for the user experience. \ No newline at end of file +### Threading Model +- IRC client runs in background daemon thread +- UI updates via Qt signals/slots (thread-safe) +- Speech runs in separate thread to avoid blocking + +## Known Limitations + +- No DCC file transfers yet +- No channel modes UI (use `/mode` command) +- URLs not clickable yet (can be copied) +- No message formatting (bold/italic/colors) yet +- No custom keyboard shortcut configuration yet + +## Contributing + +This project prioritizes accessibility. When contributing: + +1. **Never break screen reader functionality** - This is the highest priority +2. **Test with a screen reader** (Orca on Linux, NVDA on Windows) +3. **Maintain keyboard navigation** - All features must be keyboard accessible +4. **Follow code quality standards** - See `.claude/CLAUDE.md` for details + +## License + +[Add your license here] + +## Credits + +Built with Python, Qt6 (PySide6), and lots of care for the user experience. + +StormIRC proves that accessibility and good UX aren't mutually exclusive. diff --git a/requirements.txt b/requirements.txt index b5ebd71..49f0ccc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,11 @@ -PyGObject>=3.44.0 -pycairo>=1.20.0 -python-speechd>=0.11.0 \ No newline at end of file +# StormIRC Dependencies +# Python 3.8+ required + +# Qt6 framework for GUI +PySide6>=6.5.0 + +# Note: Speech Dispatcher support (python3-speechd) is optional +# Install via system package manager: +# Arch/Manjaro: sudo pacman -S speech-dispatcher python-speechd +# Debian/Ubuntu: sudo apt install python3-speechd +# Fedora: sudo dnf install python3-speechd diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index a6f79bc..0000000 Binary files a/src/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/config/__pycache__/__init__.cpython-313.pyc b/src/config/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 77a953f..0000000 Binary files a/src/config/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/config/__pycache__/settings.cpython-313.pyc b/src/config/__pycache__/settings.cpython-313.pyc deleted file mode 100644 index ff3317f..0000000 Binary files a/src/config/__pycache__/settings.cpython-313.pyc and /dev/null differ diff --git a/src/irc/__pycache__/__init__.cpython-313.pyc b/src/irc/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 11d4bf0..0000000 Binary files a/src/irc/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/irc/__pycache__/client.cpython-313.pyc b/src/irc/__pycache__/client.cpython-313.pyc deleted file mode 100644 index 715055f..0000000 Binary files a/src/irc/__pycache__/client.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/__init__.cpython-313.pyc b/src/ui/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 4cfc3a9..0000000 Binary files a/src/ui/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/accessible_tree.cpython-313.pyc b/src/ui/__pycache__/accessible_tree.cpython-313.pyc deleted file mode 100644 index 84ef032..0000000 Binary files a/src/ui/__pycache__/accessible_tree.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/autocomplete_textedit.cpython-313.pyc b/src/ui/__pycache__/autocomplete_textedit.cpython-313.pyc deleted file mode 100644 index 5a599b2..0000000 Binary files a/src/ui/__pycache__/autocomplete_textedit.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/logger.cpython-313.pyc b/src/ui/__pycache__/logger.cpython-313.pyc deleted file mode 100644 index e00f311..0000000 Binary files a/src/ui/__pycache__/logger.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/main_window.cpython-313.pyc b/src/ui/__pycache__/main_window.cpython-313.pyc deleted file mode 100644 index ff5c054..0000000 Binary files a/src/ui/__pycache__/main_window.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/pm_window.cpython-313.pyc b/src/ui/__pycache__/pm_window.cpython-313.pyc deleted file mode 100644 index 2e74f7b..0000000 Binary files a/src/ui/__pycache__/pm_window.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/settings_dialog.cpython-313.pyc b/src/ui/__pycache__/settings_dialog.cpython-313.pyc deleted file mode 100644 index ade1a3e..0000000 Binary files a/src/ui/__pycache__/settings_dialog.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/sound.cpython-313.pyc b/src/ui/__pycache__/sound.cpython-313.pyc deleted file mode 100644 index 967a601..0000000 Binary files a/src/ui/__pycache__/sound.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/speech.cpython-313.pyc b/src/ui/__pycache__/speech.cpython-313.pyc deleted file mode 100644 index 412b95b..0000000 Binary files a/src/ui/__pycache__/speech.cpython-313.pyc and /dev/null differ diff --git a/src/ui/__pycache__/ui_utils.cpython-313.pyc b/src/ui/__pycache__/ui_utils.cpython-313.pyc deleted file mode 100644 index 3b19071..0000000 Binary files a/src/ui/__pycache__/ui_utils.cpython-313.pyc and /dev/null differ diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 7f3fbdc..f9ae506 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -89,6 +89,7 @@ class MainWindow(QMainWindow): self.setup_irc_handlers() self.setup_signal_connections() self.populate_initial_servers() + self.install_global_event_filter() def setup_signal_connections(self): """Connect Qt signals to slots.""" @@ -99,6 +100,48 @@ class MainWindow(QMainWindow): self.channel_list_ready.connect(self.on_channel_list_ready_ui) self.private_message_received.connect(self.handle_private_message) + def install_global_event_filter(self): + """Install event filter to catch keyboard shortcuts globally.""" + # Install event filter on the application to catch all events + from PySide6.QtWidgets import QApplication + QApplication.instance().installEventFilter(self) + + def eventFilter(self, obj, event): + """Filter events to capture keyboard shortcuts before widgets process them.""" + from PySide6.QtCore import QEvent + from PySide6.QtGui import QKeyEvent + + # Only process KeyPress events + if event.type() == QEvent.KeyPress: + key_event = event + # Check for our global shortcuts (Alt+1-6, Ctrl+S) + if key_event.modifiers() == Qt.AltModifier: + if key_event.key() == Qt.Key_1: + self.adjust_speech_volume(-10) + return True # Event handled + elif key_event.key() == Qt.Key_2: + self.adjust_speech_volume(10) + return True + elif key_event.key() == Qt.Key_3: + self.adjust_speech_pitch(-10) + return True + elif key_event.key() == Qt.Key_4: + self.adjust_speech_pitch(10) + return True + elif key_event.key() == Qt.Key_5: + self.adjust_speech_rate(-10) + return True + elif key_event.key() == Qt.Key_6: + self.adjust_speech_rate(10) + return True + elif key_event.modifiers() == Qt.ControlModifier: + if key_event.key() == Qt.Key_S: + self.save_settings() + return True + + # Let the event propagate normally + return super().eventFilter(obj, event) + def keyPressEvent(self, event): """Handle key press events - Ctrl stops all speech, tab shortcuts.""" @@ -125,9 +168,9 @@ class MainWindow(QMainWindow): self.close_tab(current_idx) event.accept() return - # Alt+1-9: Jump to tab + # Alt+7-9: Jump to tab elif event.modifiers() == Qt.AltModifier: - if Qt.Key_1 <= event.key() <= Qt.Key_9: + if Qt.Key_7 <= event.key() <= Qt.Key_9: tab_idx = event.key() - Qt.Key_1 if tab_idx < self.tab_widget.count(): self.tab_widget.setCurrentIndex(tab_idx) @@ -208,6 +251,104 @@ class MainWindow(QMainWindow): self.speech_manager.set_pitch(accessibility.speech_pitch) self.speech_manager.set_volume(accessibility.speech_volume) + def save_settings(self): + """Save current configuration to disk and announce confirmation.""" + try: + self.config_manager.save_config() + self.speech_manager.speak("Settings saved", interrupt=True) + logger.info("Settings saved manually via Ctrl+S") + except Exception as e: + logger.error(f"Failed to save settings: {e}", exc_info=True) + self.speech_manager.speak("Error saving settings", interrupt=True) + + def adjust_speech_volume(self, delta: int): + """ + Adjust speech volume by delta and announce the new value. + + IMPORTANT: Clears window-specific volume settings so the global + adjustment applies to all channels/PMs immediately. + + Args: + delta: Amount to adjust (-10 or +10) + """ + accessibility = self.config_manager.get_accessibility_config() + new_volume = max(-100, min(100, accessibility.speech_volume + delta)) + accessibility.speech_volume = new_volume + + # Update speech manager global default + self.speech_manager.set_volume(new_volume) + self.config_manager.config.accessibility.speech_volume = new_volume + + # CRITICAL: Clear window-specific volume settings if they exist + # (Currently volume doesn't have window-specific settings in SpeechManager, + # but we clear any that might be added in the future) + + self.config_manager.save_config() + + # Announce the new value + self.speech_manager.speak(f"Volume {new_volume}", interrupt=True) + logger.info(f"Speech volume adjusted to {new_volume}") + + def adjust_speech_pitch(self, delta: int): + """ + Adjust speech pitch by delta and announce the new value. + + IMPORTANT: Clears window-specific pitch settings so the global + adjustment applies to all channels/PMs immediately. + + Args: + delta: Amount to adjust (-10 or +10) + """ + accessibility = self.config_manager.get_accessibility_config() + new_pitch = max(-100, min(100, accessibility.speech_pitch + delta)) + accessibility.speech_pitch = new_pitch + + # Update speech manager global default + self.speech_manager.set_pitch(new_pitch) + self.config_manager.config.accessibility.speech_pitch = new_pitch + + # CRITICAL: Clear window-specific pitch settings if they exist + # (Currently pitch doesn't have window-specific settings in SpeechManager, + # but we clear any that might be added in the future) + + self.config_manager.save_config() + + # Announce the new value + self.speech_manager.speak(f"Pitch {new_pitch}", interrupt=True) + logger.info(f"Speech pitch adjusted to {new_pitch}") + + def adjust_speech_rate(self, delta: int): + """ + Adjust speech rate by delta and announce the new value. + + Args: + delta: Amount to adjust (-10 or +10) + """ + accessibility = self.config_manager.get_accessibility_config() + new_rate = max(-100, min(100, accessibility.speech_rate + delta)) + accessibility.speech_rate = new_rate + + # Update speech manager global default + self.speech_manager.set_rate(new_rate) + self.config_manager.config.accessibility.speech_rate = new_rate + + # CRITICAL: Clear window-specific rate settings so global default applies + # This ensures Alt+5/6 adjustments work for all channels/PMs immediately + self.speech_manager.window_rate_settings.clear() + + # CRITICAL FIX: Reset default_channel_rate and default_pm_rate to 0 + # This prevents apply_speech_settings() from re-adding window-specific rates + # from config defaults after we've cleared them + self.config_manager.config.accessibility.default_channel_rate = 0 + self.config_manager.config.accessibility.default_pm_rate = 0 + logger.info(f"Cleared window-specific rate settings and reset default channel/PM rates to 0, all windows will use global rate={new_rate}") + + self.config_manager.save_config() + + # Announce the new value + self.speech_manager.speak(f"Rate {new_rate}", interrupt=True) + logger.info(f"Speech rate adjusted to {new_rate}, speech_manager.default_rate={self.speech_manager.default_rate}") + def speak_for_channel(self, target: str, text: str): """Speak text with channel/PM-specific settings.""" accessibility = self.config_manager.get_accessibility_config() diff --git a/src/ui/ui_utils.py b/src/ui/ui_utils.py index 1933a7c..1c9b6be 100644 --- a/src/ui/ui_utils.py +++ b/src/ui/ui_utils.py @@ -51,18 +51,29 @@ def apply_speech_settings(speech_manager: 'SpeechManager', This consolidates the common pattern of applying per-channel/PM speech configuration (voice, rate, output module) to avoid duplication. + IMPORTANT: Only sets window-specific settings if they differ from defaults. + This allows global speech adjustments (Alt+1-6) to apply to channels/PMs + that don't have custom settings. + Args: speech_manager: The SpeechManager instance to configure settings: Channel-specific speech settings window_id: Unique identifier for the window (channel name or PM nick) """ + # Only set window-specific voice if explicitly configured (non-empty) if settings.voice: speech_manager.set_voice(settings.voice, window_id=window_id) + + # Only set window-specific rate if explicitly configured (non-zero) + # NOTE: We don't set rate=0 because that would override the global default + # This allows Alt+5/6 global adjustments to work for channels without custom rates if settings.rate != 0: speech_manager.set_rate(settings.rate, window_id=window_id) + + # Only set window-specific module if explicitly configured (non-empty) if settings.output_module: speech_manager.set_output_module(settings.output_module, window_id=window_id) logger.debug(f"Applied speech settings for window '{window_id}': " - f"voice={settings.voice}, rate={settings.rate}, " - f"module={settings.output_module}") + f"voice={settings.voice or 'default'}, rate={settings.rate or 'default'}, " + f"module={settings.output_module or 'default'}")