From 9309509858888d05cd29cac71f8d19b63155d39a Mon Sep 17 00:00:00 2001 From: Talon Date: Tue, 22 Apr 2025 17:21:44 +0200 Subject: [PATCH] Fix MDI --- src/lib/components/MudConnection.svelte | 408 +++++++++++++++++------- src/lib/components/MudMdi.svelte | 291 ++++++++++++----- src/lib/components/MudTerminal.svelte | 65 +++- src/lib/connection/ConnectionManager.ts | 191 +++++++++++ src/lib/stores/mudStore.ts | 26 +- src/lib/utils/EventEmitter.ts | 9 +- src/routes/+page.svelte | 22 +- 7 files changed, 814 insertions(+), 198 deletions(-) create mode 100644 src/lib/connection/ConnectionManager.ts diff --git a/src/lib/components/MudConnection.svelte b/src/lib/components/MudConnection.svelte index 3bac49e..8c3eca7 100644 --- a/src/lib/components/MudConnection.svelte +++ b/src/lib/components/MudConnection.svelte @@ -1,6 +1,7 @@
-
- {#each tabs as tab} +
+ {#each safeTabs as tab}
{:else} - {#each tabs as tab (tab.id)} + + {#each safeTabs.filter(tab => tab.id === activeTab) as tab (tab.id)}
@@ -226,7 +381,6 @@ import { onMount, onDestroy, createEventDispatcher } from 'svelte'; - import { activeOutputHistory, addToOutputHistory, addToInputHistory, navigateInputHistory, activeInputHistoryIndex, activeConnection, uiSettings, accessibilitySettings, activeInputHistory } from '$lib/stores/mudStore'; + import { activeOutputHistory, addToOutputHistory, addToInputHistory, navigateInputHistory, activeInputHistoryIndex, activeConnection, uiSettings, accessibilitySettings, activeInputHistory, activeProfileId, connectionStatus } from '$lib/stores/mudStore'; import { tick } from 'svelte'; import AnsiToHtml from 'ansi-to-html'; import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager'; + // Create safe defaults for reactivity + $: safeOutputHistory = $activeOutputHistory || []; + $: safeActiveProfileId = $activeProfileId || null; + // Create event dispatcher const dispatch = createEventDispatcher(); @@ -92,7 +96,7 @@ } // Handle input submission - function handleSubmit(event: Event) { + async function handleSubmit(event: Event) { event.preventDefault(); if (!currentInput.trim()) return; @@ -112,17 +116,35 @@ } else { addToOutputHistory(`> ********`, true); } + + // Get the currently active profile id + const profileId = $activeProfileId; // Send the command if connected - if ($activeConnection) { - try { - $activeConnection.send(currentInput); - } catch (error) { - console.error('Error sending command:', error); - addToOutputHistory(`Error sending command: ${error}`, false, [{ pattern: 'Error', color: '#ff5555', isRegex: false }]); + if (profileId) { + // Get connection status for this profile + const status = $connectionStatus[profileId]; + + if (status === 'connected') { + try { + // Try using the activeConnection first + if ($activeConnection) { + $activeConnection.send(currentInput); + } else { + // If not available, use the ConnectionManager directly + const { ConnectionManager } = await import('$lib/connection/ConnectionManager'); + const connectionManager = ConnectionManager.getInstance(); + connectionManager.send(profileId, currentInput); + } + } catch (error) { + console.error('Error sending command:', error); + addToOutputHistory(`Error sending command: ${error}`, false, [{ pattern: 'Error', color: '#ff5555', isRegex: false }]); + } + } else { + addToOutputHistory(`Not connected to any MUD server. Status: ${status || 'disconnected'}`, false, [{ pattern: 'Not connected', color: '#ff5555', isRegex: false }]); } } else { - addToOutputHistory('Not connected to any MUD server.', false, [{ pattern: 'Not connected', color: '#ff5555', isRegex: false }]); + addToOutputHistory('No profile selected.', false, [{ pattern: 'No profile', color: '#ff5555', isRegex: false }]); } // Clear the input @@ -266,16 +288,35 @@ } } - // Watch output history changes to scroll to bottom + // Watch output history and profile changes to update display $: { - if ($activeOutputHistory) { + if (safeOutputHistory.length > 0) { + console.log('Active output history changed, updating terminal display'); scrollToBottom(); // Update message elements when output history changes setTimeout(updateMessageElements, 0); } } + $: { + // Every time the active profile changes, update the terminal content + if ($activeProfileId) { + console.log(`Active profile is now: ${$activeProfileId}, updating output display`); + updateOutputDisplay(); + } + } + + // Function to update the displayed output based on the active profile + async function updateOutputDisplay() { + console.log('Updating output display...'); + await tick(); // Wait for Svelte to update + scrollToBottom(); + updateMessageElements(); + } + onMount(() => { + console.log('MudTerminal component mounted for profile:', $activeProfileId); + // Initialize accessibility manager accessibilityManager = new AccessibilityManager(); @@ -318,7 +359,7 @@ tabindex="0" on:keydown={handleOutputKeyDown} style="font-family: {$uiSettings.font}; font-size: {$accessibilitySettings.fontSize}px; line-height: {$accessibilitySettings.lineSpacing};"> - {#each $activeOutputHistory as item (item.id)} + {#each safeOutputHistory as item (item.id)} {#if item.isInput}
diff --git a/src/lib/connection/ConnectionManager.ts b/src/lib/connection/ConnectionManager.ts new file mode 100644 index 0000000..191e335 --- /dev/null +++ b/src/lib/connection/ConnectionManager.ts @@ -0,0 +1,191 @@ +import { writable, get } from 'svelte/store'; +import { MudConnection } from './MudConnection'; +import type { GmcpHandler } from '$lib/gmcp/GmcpHandler'; +import { connectionStatus } from '$lib/stores/mudStore'; + +// Store for active MUD connections (persistent across tab switches) +export const activeConnections = writable<{ [key: string]: MudConnection }>({}); + +/** + * ConnectionManager - Singleton service to manage MUD connections + * This ensures connections stay alive even when components are unmounted during tab switches + */ +export class ConnectionManager { + private static instance: ConnectionManager; + + // Private constructor for singleton pattern + private constructor() { + console.log('ConnectionManager initialized'); + } + + /** + * Get the singleton instance of ConnectionManager + */ + public static getInstance(): ConnectionManager { + if (!ConnectionManager.instance) { + ConnectionManager.instance = new ConnectionManager(); + } + return ConnectionManager.instance; + } + + /** + * Get an existing connection without creating a new one + */ + public getExistingConnection(profileId: string): MudConnection | null { + const connections = get(activeConnections); + return connections[profileId] || null; + } + + /** + * Create a new connection or return an existing one + */ + public getConnection(profileId: string, options: { + host: string; + port: number; + useSSL?: boolean; + gmcpHandler?: GmcpHandler; + }): MudConnection { + // Get current connections + const connections = get(activeConnections); + + // Check if a connection already exists for this profile + if (connections[profileId]) { + console.log(`Returning existing connection for profile ${profileId}`); + return connections[profileId]; + } + + // Create a new connection + console.log(`Creating new connection for profile ${profileId}`); + const connection = new MudConnection({ + host: options.host, + port: options.port, + useSSL: options.useSSL, + gmcpHandler: options.gmcpHandler + }); + + // Set up event handlers + this.setupConnectionEvents(connection, profileId); + + // Store the connection + activeConnections.update(connections => ({ + ...connections, + [profileId]: connection + })); + + // Return the connection + return connection; + } + + /** + * Connect to a MUD server + */ + public connect(profileId: string, options: { + host: string; + port: number; + useSSL?: boolean; + gmcpHandler?: GmcpHandler; + }): void { + const connection = this.getConnection(profileId, options); + + // Update connection status + connectionStatus.update(statuses => ({ + ...statuses, + [profileId]: 'connecting' + })); + + // Connect + connection.connect(); + } + + /** + * Disconnect from a MUD server + */ + public disconnect(profileId: string): void { + const connections = get(activeConnections); + + if (connections[profileId]) { + connections[profileId].disconnect(); + + // Update connection status + connectionStatus.update(statuses => ({ + ...statuses, + [profileId]: 'disconnected' + })); + } + } + + /** + * Send text to a MUD server + */ + public send(profileId: string, text: string): void { + const connections = get(activeConnections); + + if (connections[profileId]) { + connections[profileId].send(text); + } + } + + /** + * Close a connection and remove it + */ + public closeConnection(profileId: string): void { + const connections = get(activeConnections); + + if (connections[profileId]) { + // Disconnect first + connections[profileId].disconnect(); + + // Then remove from store + activeConnections.update(connections => { + const newConnections = { ...connections }; + delete newConnections[profileId]; + return newConnections; + }); + + // Update connection status + connectionStatus.update(statuses => { + const newStatuses = { ...statuses }; + delete newStatuses[profileId]; + return newStatuses; + }); + } + } + + /** + * Set up event handlers for a connection + */ + private setupConnectionEvents(connection: MudConnection, profileId: string): void { + // Handle connection established + connection.on('connected', () => { + console.log(`ConnectionManager: Connection established for profile ${profileId}`); + + // Update connection status + connectionStatus.update(statuses => ({ + ...statuses, + [profileId]: 'connected' + })); + }); + + // Handle connection closed + connection.on('disconnected', () => { + console.log(`ConnectionManager: Connection closed for profile ${profileId}`); + + // Update connection status + connectionStatus.update(statuses => ({ + ...statuses, + [profileId]: 'disconnected' + })); + }); + + // Handle connection error + connection.on('error', (error) => { + console.error(`ConnectionManager: Connection error for profile ${profileId}:`, error); + + // Update connection status + connectionStatus.update(statuses => ({ + ...statuses, + [profileId]: 'error' + })); + }); + } +} \ No newline at end of file diff --git a/src/lib/stores/mudStore.ts b/src/lib/stores/mudStore.ts index ecd6b90..8303159 100644 --- a/src/lib/stores/mudStore.ts +++ b/src/lib/stores/mudStore.ts @@ -49,10 +49,30 @@ export const activeProfile = derived( // Derived store for active connection export const activeConnection = derived( - [connections, activeProfileId], - ([$connections, $activeProfileId]) => { + [connections, activeProfileId, connectionStatus], + ([$connections, $activeProfileId, $connectionStatus]) => { + // First check if there's an active profile ID if (!$activeProfileId) return null; - return $connections[$activeProfileId] || null; + + // Check for the connection in the local connections store + const localConnection = $connections[$activeProfileId]; + if (localConnection) return localConnection; + + // If not in local store but the connection status shows connected/connecting, + // use the ConnectionManager's connection instead + if ($connectionStatus[$activeProfileId] === 'connected' || $connectionStatus[$activeProfileId] === 'connecting') { + try { + // We can't do a dynamic import in a derived store, so we'll return null + // and let the components handle getting the connection from the manager directly + console.log('Connection not in local store but status indicates it exists in manager'); + return null; + } catch(e) { + console.error('Error getting connection from manager:', e); + return null; + } + } + + return null; } ); diff --git a/src/lib/utils/EventEmitter.ts b/src/lib/utils/EventEmitter.ts index 709f430..a4de7f6 100644 --- a/src/lib/utils/EventEmitter.ts +++ b/src/lib/utils/EventEmitter.ts @@ -18,9 +18,14 @@ export class EventEmitter { return this.on(event, onceWrapper); } - off(event: string, listener: Function): this { + off(event: string, listener?: Function): this { if (this.events[event]) { - this.events[event] = this.events[event].filter(l => l !== listener); + if (listener) { + this.events[event] = this.events[event].filter(l => l !== listener); + } else { + // Remove all listeners for this event if no specific listener is provided + this.events[event] = []; + } } return this; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f43cff7..15a1e3b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -18,7 +18,10 @@ triggers, uiSettings, accessibilitySettings, - addToOutputHistory + addToOutputHistory, + connectionStatus, + connections, + outputHistory } from '$lib/stores/mudStore'; import { ProfileManager } from '$lib/profiles/ProfileManager'; import type { MudProfile } from '$lib/profiles/ProfileManager'; @@ -78,6 +81,14 @@ triggerSystem = new TriggerSystem(); console.log('Trigger system initialized'); + // Initialize the connection status store + connectionStatus.set({}); + console.log('Connection status store initialized'); + + // Clear connections store to start fresh + connections.set({}); + console.log('Connections store cleared'); + // Load profiles first to ensure we have them console.log('Loading profiles...'); loadProfiles(); @@ -114,6 +125,15 @@ console.log('No active profile selected, setting first profile as active'); activeProfileId.set($profiles[0].id); } + + // Initialize output history for all profiles + const outputHistoryObject = {}; + $profiles.forEach(profile => { + outputHistoryObject[profile.id] = []; + }); + outputHistory.set(outputHistoryObject); + console.log('Output history initialized for all profiles'); + } catch (error) { console.error('Error during page initialization:', error); console.error('Error details:', error.message, error.stack);