UNPKG

interm-mcp

Version:

MCP server for terminal applications and TUI automation with 127 tools

405 lines (404 loc) 14.7 kB
import { createTerminalError } from './utils/error-utils.js'; export class AccessibilityManager { static instance; settings; screenReaderEvents = []; voiceCommands = []; currentFocus = null; lastAnnouncedContent = ''; constructor() { this.settings = { screenReaderEnabled: false, highContrastEnabled: false, magnificationLevel: 1.0, voiceInputEnabled: false, keyboardNavigationOnly: false, announceChanges: true, speechRate: 1.0, speechVolume: 0.8 }; } static getInstance() { if (!AccessibilityManager.instance) { AccessibilityManager.instance = new AccessibilityManager(); } return AccessibilityManager.instance; } /** * Initialize accessibility features based on system capabilities */ async initialize() { try { // Detect system accessibility features const info = await this.detectAccessibilityFeatures(); // Auto-enable features if detected if (info.screenReaderActive) { this.settings.screenReaderEnabled = true; this.settings.announceChanges = true; } if (info.highContrastMode) { this.settings.highContrastEnabled = true; } if (info.magnificationLevel && info.magnificationLevel > 1.0) { this.settings.magnificationLevel = info.magnificationLevel; } return info; } catch (error) { throw createTerminalError('RESOURCE_ERROR', `Failed to initialize accessibility: ${error}`); } } /** * Detect system accessibility features */ async detectAccessibilityFeatures() { const platform = process.platform; // Platform-specific accessibility detection let screenReaderActive = false; let highContrastMode = false; let voiceInputActive = false; let eyeTrackingActive = false; let magnificationLevel = 1.0; if (platform === 'darwin') { // macOS accessibility detection screenReaderActive = await this.detectMacOSScreenReader(); highContrastMode = await this.detectMacOSHighContrast(); voiceInputActive = await this.detectMacOSVoiceControl(); magnificationLevel = await this.detectMacOSZoom(); } else if (platform === 'win32') { // Windows accessibility detection screenReaderActive = await this.detectWindowsNarrator(); highContrastMode = await this.detectWindowsHighContrast(); voiceInputActive = await this.detectWindowsSpeechRecognition(); } else if (platform === 'linux') { // Linux accessibility detection screenReaderActive = await this.detectLinuxScreenReader(); highContrastMode = await this.detectLinuxHighContrast(); } return { screenReaderActive, highContrastMode, voiceInputActive, eyeTrackingActive, magnificationLevel }; } /** * macOS accessibility detection methods */ async detectMacOSScreenReader() { try { // Check for VoiceOver or other screen readers const { execSync } = await import('child_process'); const result = execSync('defaults read com.apple.universalaccess voiceOverOnOffKey 2>/dev/null || echo "false"', { encoding: 'utf8' }); return result.trim() !== 'false'; } catch { return false; } } async detectMacOSHighContrast() { try { const { execSync } = await import('child_process'); const result = execSync('defaults read com.apple.universalaccess increaseContrast 2>/dev/null || echo "0"', { encoding: 'utf8' }); return result.trim() === '1'; } catch { return false; } } async detectMacOSVoiceControl() { try { const { execSync } = await import('child_process'); const result = execSync('defaults read com.apple.speech.voice.prefs VoiceOverUsesAppleVoice 2>/dev/null || echo "false"', { encoding: 'utf8' }); return result.trim() === 'true'; } catch { return false; } } async detectMacOSZoom() { try { const { execSync } = await import('child_process'); const result = execSync('defaults read com.apple.universalaccess closeViewScrollWheelToggle 2>/dev/null || echo "0"', { encoding: 'utf8' }); return result.trim() === '1' ? 2.0 : 1.0; } catch { return 1.0; } } /** * Windows accessibility detection methods */ async detectWindowsNarrator() { try { const { execSync } = await import('child_process'); const result = execSync('tasklist /fi "imagename eq narrator.exe" 2>nul | find /i "narrator.exe"', { encoding: 'utf8' }); return result.includes('narrator.exe'); } catch { return false; } } async detectWindowsHighContrast() { try { const { execSync } = await import('child_process'); const result = execSync('reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize" /v AppsUseLightTheme 2>nul', { encoding: 'utf8' }); return result.includes('0x0'); } catch { return false; } } async detectWindowsSpeechRecognition() { try { const { execSync } = await import('child_process'); const result = execSync('tasklist /fi "imagename eq speechruntime.exe" 2>nul | find /i "speechruntime.exe"', { encoding: 'utf8' }); return result.includes('speechruntime.exe'); } catch { return false; } } /** * Linux accessibility detection methods */ async detectLinuxScreenReader() { try { const { execSync } = await import('child_process'); const result = execSync('pgrep -f "orca|espeak|festival" 2>/dev/null || echo ""', { encoding: 'utf8' }); return result.trim().length > 0; } catch { return false; } } async detectLinuxHighContrast() { try { const { execSync } = await import('child_process'); const result = execSync('gsettings get org.gnome.desktop.a11y.interface high-contrast 2>/dev/null || echo "false"', { encoding: 'utf8' }); return result.trim() === 'true'; } catch { return false; } } /** * Announce content changes to screen readers */ announceToScreenReader(content, priority = 'polite') { if (!this.settings.screenReaderEnabled || !this.settings.announceChanges) { return; } // Avoid duplicate announcements if (content === this.lastAnnouncedContent) { return; } this.lastAnnouncedContent = content; const event = { type: 'content_changed', content, announcement: this.processContentForSpeech(content), timestamp: new Date() }; this.screenReaderEvents.push(event); // Limit event history if (this.screenReaderEvents.length > 1000) { this.screenReaderEvents = this.screenReaderEvents.slice(-500); } // Platform-specific screen reader announcement this.performScreenReaderAnnouncement(event.announcement, priority); } /** * Process content for speech synthesis */ processContentForSpeech(content) { // Remove ANSI escape sequences let processed = content.replace(/\x1b\[[0-9;]*m/g, ''); // Convert common symbols to speech processed = processed .replace(/\$/g, 'dollar ') .replace(/#/g, 'hash ') .replace(/&/g, 'and ') .replace(/\*/g, 'asterisk ') .replace(/\+/g, 'plus ') .replace(/-/g, 'dash ') .replace(/\//g, 'slash ') .replace(/\\/g, 'backslash ') .replace(/\|/g, 'pipe ') .replace(/~/g, 'tilde '); // Limit length for speech if (processed.length > 200) { processed = processed.substring(0, 197) + '...'; } return processed.trim(); } /** * Perform platform-specific screen reader announcement */ performScreenReaderAnnouncement(text, priority) { const platform = process.platform; try { if (platform === 'darwin') { // Use macOS say command const { spawn } = require('child_process'); const rate = Math.round(this.settings.speechRate * 200); spawn('say', ['-r', rate.toString(), text], { stdio: 'ignore' }); } else if (platform === 'win32') { // Use Windows SAPI const { spawn } = require('child_process'); const script = `Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('${text.replace(/'/g, "''")}')`; spawn('powershell', ['-Command', script], { stdio: 'ignore' }); } else if (platform === 'linux') { // Use espeak or festival const { spawn } = require('child_process'); const rate = Math.round(this.settings.speechRate * 200); spawn('espeak', ['-s', rate.toString(), text], { stdio: 'ignore' }); } } catch (error) { console.warn('Screen reader announcement failed:', error); } } /** * Process voice command */ processVoiceCommand(audioData) { if (!this.settings.voiceInputEnabled) { return Promise.resolve(null); } // Placeholder for voice recognition implementation // In a real implementation, this would integrate with speech recognition APIs return Promise.resolve(null); } /** * Generate high contrast terminal output */ applyHighContrastFiltering(terminalState) { if (!this.settings.highContrastEnabled) { return terminalState; } // Apply high contrast color modifications const highContrastState = { ...terminalState }; // Convert colors to high contrast equivalents if (highContrastState.attributes) { highContrastState.attributes = highContrastState.attributes.map(attr => ({ ...attr, foregroundColor: this.convertToHighContrastColor(attr.foregroundColor, true), backgroundColor: this.convertToHighContrastColor(attr.backgroundColor, false) })); } return highContrastState; } /** * Convert colors to high contrast equivalents */ convertToHighContrastColor(color, isForeground) { const highContrastMap = { 'black': { fg: '#FFFFFF', bg: '#000000' }, 'red': { fg: '#FF0000', bg: '#000000' }, 'green': { fg: '#00FF00', bg: '#000000' }, 'yellow': { fg: '#FFFF00', bg: '#000000' }, 'blue': { fg: '#0000FF', bg: '#FFFFFF' }, 'magenta': { fg: '#FF00FF', bg: '#000000' }, 'cyan': { fg: '#00FFFF', bg: '#000000' }, 'white': { fg: '#000000', bg: '#FFFFFF' }, 'default': { fg: '#FFFFFF', bg: '#000000' } }; const mapping = highContrastMap[color.toLowerCase()] || highContrastMap['default']; return isForeground ? mapping.fg : mapping.bg; } /** * Get current accessibility settings */ getSettings() { return { ...this.settings }; } /** * Update accessibility settings */ updateSettings(newSettings) { this.settings = { ...this.settings, ...newSettings }; } /** * Get screen reader event history */ getScreenReaderEvents(limit = 100) { return this.screenReaderEvents.slice(-limit); } /** * Get voice command history */ getVoiceCommands(limit = 50) { return this.voiceCommands.slice(-limit); } /** * Clear accessibility history */ clearHistory() { this.screenReaderEvents = []; this.voiceCommands = []; } /** * Generate accessibility report */ generateAccessibilityReport() { return { systemInfo: { screenReaderActive: this.settings.screenReaderEnabled, highContrastMode: this.settings.highContrastEnabled, voiceInputActive: this.settings.voiceInputEnabled, eyeTrackingActive: false, // Not yet implemented magnificationLevel: this.settings.magnificationLevel }, settings: this.getSettings(), eventStats: { screenReaderEvents: this.screenReaderEvents.length, voiceCommands: this.voiceCommands.length, lastActivity: this.screenReaderEvents.length > 0 ? this.screenReaderEvents[this.screenReaderEvents.length - 1].timestamp : null } }; } /** * Handle focus changes for screen reader navigation */ handleFocusChange(newFocus, content) { if (!this.settings.screenReaderEnabled) { return; } this.currentFocus = newFocus; const event = { type: 'focus_changed', target: newFocus, content, announcement: `Focus moved to ${newFocus}${content ? `: ${content}` : ''}`, timestamp: new Date() }; this.screenReaderEvents.push(event); this.performScreenReaderAnnouncement(event.announcement, 'assertive'); } /** * Provide keyboard navigation hints */ getKeyboardNavigationHints() { return [ 'Tab: Move to next focusable element', 'Shift+Tab: Move to previous focusable element', 'Enter: Activate focused element', 'Escape: Cancel current operation', 'Arrow Keys: Navigate within content', 'Ctrl+Home: Go to beginning', 'Ctrl+End: Go to end', 'Ctrl+A: Select all content' ]; } }