Keyboard shortcuts for speech settings added. Control+s for save. Cleanup of some .pyc files.

This commit is contained in:
Storm Dragon
2025-11-14 14:29:36 -05:00
parent 9815448cf8
commit 46f7139be8
20 changed files with 568 additions and 47 deletions

159
.gitignore vendored Normal file
View File

@@ -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

282
README.md
View File

@@ -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 <nick> [message]` - Open PM tab and optionally send message
- `/query <nick>` - Open PM tab (alias for /msg)
- `/me <action>` - Send CTCP ACTION
- `/whois <nick>` - 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 <newnick>` - 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.
### 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.

View File

@@ -1,3 +1,11 @@
PyGObject>=3.44.0
pycairo>=1.20.0
python-speechd>=0.11.0
# 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

View File

@@ -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()

View File

@@ -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'}")