diff --git a/src/components/Terminal.tsx b/src/components/Terminal.tsx index 0132cc8..6175876 100644 --- a/src/components/Terminal.tsx +++ b/src/components/Terminal.tsx @@ -1,14 +1,17 @@ import { useState, useEffect, useRef } from "preact/hooks"; import type { JSX } from "preact"; -import type { Command } from "../lib/terminal/types"; -import { buildFileSystem } from "../lib/terminal/fileSystem"; -import { executeCommand, type CommandContext } from "../lib/terminal/commands"; +import type { Command } from "../utils/terminal/types"; +import { buildFileSystem } from "../utils/terminal/fs"; +import { + executeCommand, + type CommandContext, +} from "../utils/terminal/commands"; import { getCompletions, formatOutput, saveCommandToHistory, loadCommandHistory, -} from "../lib/terminal/utils"; +} from "../utils/terminal/utils"; const Terminal = () => { const [currentPath, setCurrentPath] = useState("/"); @@ -145,7 +148,7 @@ const Terminal = () => {
!isTrainRunning && inputRef.current?.focus()} > @@ -198,7 +201,6 @@ const Terminal = () => { )}
- {/* Train animation overlay - positioned over the content area but outside the opacity div */} diff --git a/src/pages/api/spotify/config.ts b/src/pages/api/spotify/config.ts index d9cb64e..5ae7ee8 100644 --- a/src/pages/api/spotify/config.ts +++ b/src/pages/api/spotify/config.ts @@ -1,40 +1,48 @@ -import type { APIRoute } from 'astro'; +import type { APIRoute } from "astro"; +import { + getSpotifyCredentials, + isSpotifyConfigured, +} from "../../../utils/spotify"; export const GET: APIRoute = async () => { try { - // Only check environment variables at runtime, not build time - const clientId = process.env.SPOTIFY_CLIENT_ID; - const clientSecret = process.env.SPOTIFY_CLIENT_SECRET; - const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN; + const isConfigured = isSpotifyConfigured(); - const isConfigured = !!(clientId && clientSecret && refreshToken); - if (!isConfigured) { - console.log('Spotify integration disabled - missing environment variables:', { - hasClientId: !!clientId, - hasClientSecret: !!clientSecret, - hasRefreshToken: !!refreshToken - }); + const credentials = getSpotifyCredentials(); + console.log( + "Spotify integration disabled - missing environment variables:", + { + hasClientId: !!credentials?.clientId, + hasClientSecret: !!credentials?.clientSecret, + hasRefreshToken: !!credentials?.refreshToken, + }, + ); } - return new Response(JSON.stringify({ - configured: isConfigured - }), { - status: 200, - headers: { - 'Content-Type': 'application/json', + return new Response( + JSON.stringify({ + configured: isConfigured, + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + }, }, - }); - + ); } catch (error) { - console.error('Error checking Spotify configuration:', error); - return new Response(JSON.stringify({ - configured: false - }), { - status: 200, - headers: { - 'Content-Type': 'application/json', + console.error("Error checking Spotify configuration:", error); + return new Response( + JSON.stringify({ + configured: false, + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + }, }, - }); + ); } -}; \ No newline at end of file +}; diff --git a/src/pages/api/spotify/stream.ts b/src/pages/api/spotify/stream.ts index 12d072d..31379d0 100644 --- a/src/pages/api/spotify/stream.ts +++ b/src/pages/api/spotify/stream.ts @@ -1,62 +1,74 @@ -import type { APIRoute } from 'astro'; +import type { APIRoute } from "astro"; +import { getSpotifyCredentials } from "../../../utils/spotify"; -// Helper function to refresh the access token -async function refreshSpotifyToken(refreshToken: string, clientId: string, clientSecret: string) { - const response = await fetch('https://accounts.spotify.com/api/token', { - method: 'POST', +// Refresh the access token +async function refreshSpotifyToken( + refreshToken: string, + clientId: string, + clientSecret: string, +) { + const response = await fetch("https://accounts.spotify.com/api/token", { + method: "POST", headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`, + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`, }, body: new URLSearchParams({ - grant_type: 'refresh_token', + grant_type: "refresh_token", refresh_token: refreshToken, }), }); if (!response.ok) { - throw new Error('Failed to refresh token'); + throw new Error("Failed to refresh token"); } return await response.json(); } // Function to fetch current track from Spotify -async function fetchCurrentTrack() { +async function fetchCurrentTrack( + clientId: string, + clientSecret: string, + refreshToken: string, + accessToken?: string, +) { try { - // Use runtime env vars instead of build-time - const clientId = process.env.SPOTIFY_CLIENT_ID; - const clientSecret = process.env.SPOTIFY_CLIENT_SECRET; - let accessToken = process.env.SPOTIFY_ACCESS_TOKEN; - const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN; - - if (!clientId || !clientSecret || !refreshToken) { - return null; - } + let currentAccessToken = accessToken; // Try to fetch current track with existing token - let spotifyResponse = await fetch('https://api.spotify.com/v1/me/player/currently-playing', { - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json', + let spotifyResponse = await fetch( + "https://api.spotify.com/v1/me/player/currently-playing", + { + headers: { + Authorization: `Bearer ${currentAccessToken}`, + "Content-Type": "application/json", + }, }, - }); + ); // If token is expired (401), refresh it if (spotifyResponse.status === 401) { try { - const tokenData = await refreshSpotifyToken(refreshToken, clientId, clientSecret); - accessToken = tokenData.access_token; - + const tokenData = await refreshSpotifyToken( + refreshToken, + clientId, + clientSecret, + ); + currentAccessToken = tokenData.access_token; + // Retry the request with new token - spotifyResponse = await fetch('https://api.spotify.com/v1/me/player/currently-playing', { - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json', + spotifyResponse = await fetch( + "https://api.spotify.com/v1/me/player/currently-playing", + { + headers: { + Authorization: `Bearer ${currentAccessToken}`, + "Content-Type": "application/json", + }, }, - }); + ); } catch (refreshError) { - console.error('Failed to refresh token:', refreshError); + console.error("Failed to refresh token:", refreshError); return null; } } @@ -65,7 +77,7 @@ async function fetchCurrentTrack() { // Nothing is currently playing return { is_playing: false, - item: null + item: null, }; } @@ -74,118 +86,160 @@ async function fetchCurrentTrack() { } const data = await spotifyResponse.json(); - + return { is_playing: data.is_playing, - item: data.item ? { - name: data.item.name, - artists: data.item.artists, - is_playing: data.is_playing, - external_urls: data.item.external_urls - } : null + item: data.item + ? { + name: data.item.name, + artists: data.item.artists, + is_playing: data.is_playing, + external_urls: data.item.external_urls, + } + : null, }; - } catch (error) { - console.error('Spotify API Error:', error); + console.error("Spotify API Error:", error); return null; } } export const GET: APIRoute = async ({ request }) => { - // Set up Server-Sent Events - const encoder = new TextEncoder(); - let controller: ReadableStreamDefaultController; - let isClosed = false; - let pollInterval: NodeJS.Timeout | null = null; - - const stream = new ReadableStream({ - start(ctrl) { - controller = ctrl; - }, - cancel() { - // Client disconnected - console.log('SSE stream cancelled by client'); - isClosed = true; - if (pollInterval) { - clearInterval(pollInterval); - pollInterval = null; - } - } - }); + try { + // Get Spotify credentials + const credentials = getSpotifyCredentials(); - // Function to send SSE message - const sendMessage = (data: any) => { - if (isClosed) { - return; // Don't try to send if stream is closed + if (!credentials) { + console.log( + "Spotify SSE stream disabled - missing environment variables", + ); + return new Response(JSON.stringify({ error: "Spotify not configured" }), { + status: 503, + headers: { + "Content-Type": "application/json", + }, + }); } - - try { - const message = `data: ${JSON.stringify(data)}\n\n`; - controller.enqueue(encoder.encode(message)); - } catch (error) { - if (error instanceof TypeError && error.message.includes('Controller is already closed')) { - console.log('SSE controller is closed, stopping polling'); + + const { clientId, clientSecret, refreshToken, accessToken } = credentials; + + // Set up Server-Sent Events + const encoder = new TextEncoder(); + let controller: ReadableStreamDefaultController; + let isClosed = false; + let pollInterval: NodeJS.Timeout | null = null; + + const stream = new ReadableStream({ + start(ctrl) { + controller = ctrl; + }, + cancel() { + // Client disconnected + console.log("SSE stream cancelled by client"); isClosed = true; if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } - } else { - console.error('Error sending SSE message:', error); - } - } - }; + }, + }); - // Start polling and sending updates - let lastTrackData: any = null; - - const poll = async () => { - if (isClosed) { + // Function to send SSE message + const sendMessage = (data: any) => { + if (isClosed) { + return; // Don't try to send if stream is closed + } + + try { + const message = `data: ${JSON.stringify(data)}\n\n`; + controller.enqueue(encoder.encode(message)); + } catch (error) { + if ( + error instanceof TypeError && + error.message.includes("Controller is already closed") + ) { + console.log("SSE controller is closed, stopping polling"); + isClosed = true; + if (pollInterval) { + clearInterval(pollInterval); + pollInterval = null; + } + } else { + console.error("Error sending SSE message:", error); + } + } + }; + + // Start polling and sending updates + let lastTrackData: any = null; + + const poll = async () => { + if (isClosed) { + if (pollInterval) { + clearInterval(pollInterval); + pollInterval = null; + } + return; + } + + try { + const currentTrack = await fetchCurrentTrack( + clientId, + clientSecret, + refreshToken, + accessToken, + ); + + // Only send if data has changed and stream is still open + if ( + !isClosed && + JSON.stringify(currentTrack) !== JSON.stringify(lastTrackData) + ) { + lastTrackData = currentTrack; + sendMessage(currentTrack || { is_playing: false, item: null }); + } + } catch (error) { + if (!isClosed) { + console.error("Polling error:", error); + } + } + }; + + // Send initial data + poll(); + + // Poll every 3 seconds + pollInterval = setInterval(poll, 3000); + + // Clean up when client disconnects (abort signal) + request.signal.addEventListener("abort", () => { + console.log("SSE request aborted"); + isClosed = true; if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } - return; - } + }); - try { - const currentTrack = await fetchCurrentTrack(); - - // Only send if data has changed and stream is still open - if (!isClosed && JSON.stringify(currentTrack) !== JSON.stringify(lastTrackData)) { - lastTrackData = currentTrack; - sendMessage(currentTrack || { is_playing: false, item: null }); - } - } catch (error) { - if (!isClosed) { - console.error('Polling error:', error); - } - } - }; - - // Send initial data - poll(); - - // Poll every 3 seconds - pollInterval = setInterval(poll, 3000); - - // Clean up when client disconnects (abort signal) - request.signal.addEventListener('abort', () => { - console.log('SSE request aborted'); - isClosed = true; - if (pollInterval) { - clearInterval(pollInterval); - pollInterval = null; - } - }); - - return new Response(stream, { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': 'Cache-Control', - }, - }); -}; \ No newline at end of file + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Cache-Control", + }, + }); + } catch (error) { + console.error("Error setting up Spotify SSE stream:", error); + return new Response( + JSON.stringify({ error: "Failed to initialize stream" }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + }, + ); + } +}; diff --git a/src/utils/spotify.ts b/src/utils/spotify.ts new file mode 100644 index 0000000..80cde39 --- /dev/null +++ b/src/utils/spotify.ts @@ -0,0 +1,41 @@ +interface SpotifyCredentials { + clientId: string; + clientSecret: string; + refreshToken: string; + accessToken?: string; +} + +/** + * Get Spotify credentials from environment variables + * Checks both process.env and import.meta.env for compatibility + */ +export function getSpotifyCredentials(): SpotifyCredentials | null { + const clientId = + process.env.SPOTIFY_CLIENT_ID || import.meta.env.SPOTIFY_CLIENT_ID; + const clientSecret = + process.env.SPOTIFY_CLIENT_SECRET || + import.meta.env.SPOTIFY_CLIENT_SECRET; + const refreshToken = + process.env.SPOTIFY_REFRESH_TOKEN || + import.meta.env.SPOTIFY_REFRESH_TOKEN; + const accessToken = + process.env.SPOTIFY_ACCESS_TOKEN || import.meta.env.SPOTIFY_ACCESS_TOKEN; + + if (!clientId || !clientSecret || !refreshToken) { + return null; + } + + return { + clientId, + clientSecret, + refreshToken, + accessToken, + }; +} + +/** + * Check if Spotify integration is properly configured + */ +export function isSpotifyConfigured(): boolean { + return getSpotifyCredentials() !== null; +} diff --git a/src/lib/terminal/commands.ts b/src/utils/terminal/commands.ts similarity index 64% rename from src/lib/terminal/commands.ts rename to src/utils/terminal/commands.ts index 6648e2c..f5133a4 100644 --- a/src/lib/terminal/commands.ts +++ b/src/utils/terminal/commands.ts @@ -1,5 +1,5 @@ -import type { FileSystemNode } from './types'; -import { getCurrentDirectory, resolvePath } from './fileSystem'; +import type { FileSystemNode } from "./types"; +import { getCurrentDirectory, resolvePath } from "./fs"; export interface CommandContext { currentPath: string; @@ -11,30 +11,30 @@ export interface CommandContext { export function executeCommand(input: string, context: CommandContext): string { const trimmedInput = input.trim(); - if (!trimmedInput) return ''; + if (!trimmedInput) return ""; - const [command, ...args] = trimmedInput.split(' '); + const [command, ...args] = trimmedInput.split(" "); switch (command.toLowerCase()) { - case 'help': + case "help": return handleHelp(); - case 'ls': + case "ls": return handleLs(args, context); - case 'cd': + case "cd": return handleCd(args, context); - case 'pwd': + case "pwd": return handlePwd(context); - case 'cat': + case "cat": return handleCat(args, context); - case 'tree': + case "tree": return handleTree(context); - case 'clear': - return ''; - case 'whoami': + case "clear": + return ""; + case "whoami": return handleWhoami(); - case 'open': + case "open": return handleOpen(args); - case 'sl': + case "sl": return handleSl(context); default: return `${command}: command not found. Type 'help' for available commands.`; @@ -73,162 +73,178 @@ function handleHelp(): string { function handleLs(args: string[], context: CommandContext): string { const { currentPath, fileSystem } = context; const targetPath = args[0] ? resolvePath(currentPath, args[0]) : currentPath; - const pathParts = targetPath.split('/').filter((part: string) => part !== ''); - let target = fileSystem['/']; - + const pathParts = targetPath.split("/").filter((part: string) => part !== ""); + let target = fileSystem["/"]; + for (const part of pathParts) { - if (target?.children && target.children[part] && target.children[part].type === 'directory') { + if ( + target?.children && + target.children[part] && + target.children[part].type === "directory" + ) { target = target.children[part]; } else if (pathParts.length > 0) { return `ls: cannot access '${targetPath}': No such file or directory`; } } - + if (!target?.children) { return `ls: cannot access '${targetPath}': Not a directory`; } - + const items = Object.values(target.children) - .map(item => { - const color = item.type === 'directory' ? '\x1b[34m' : '\x1b[0m'; - const suffix = item.type === 'directory' ? '/' : ''; + .map((item) => { + const color = item.type === "directory" ? "\x1b[34m" : "\x1b[0m"; + const suffix = item.type === "directory" ? "/" : ""; return `${color}${item.name}${suffix}\x1b[0m`; }) - .join(' '); - - return items || 'Directory is empty'; + .join(" "); + + return items || "Directory is empty"; } function handleCd(args: string[], context: CommandContext): string { const { currentPath, fileSystem, setCurrentPath } = context; - const targetPath = args[0] ? resolvePath(currentPath, args[0]) : '/'; - const pathParts = targetPath.split('/').filter((part: string) => part !== ''); - let current = fileSystem['/']; - + const targetPath = args[0] ? resolvePath(currentPath, args[0]) : "/"; + const pathParts = targetPath.split("/").filter((part: string) => part !== ""); + let current = fileSystem["/"]; + for (const part of pathParts) { - if (current?.children && current.children[part] && current.children[part].type === 'directory') { + if ( + current?.children && + current.children[part] && + current.children[part].type === "directory" + ) { current = current.children[part]; } else { return `cd: no such file or directory: ${targetPath}`; } } - - setCurrentPath(targetPath || '/'); - return ''; + + setCurrentPath(targetPath || "/"); + return ""; } function handlePwd(context: CommandContext): string { - return context.currentPath || '/'; + return context.currentPath || "/"; } function handleCat(args: string[], context: CommandContext): string { const { currentPath, fileSystem } = context; - + if (!args[0]) { - return 'cat: missing file argument'; + return "cat: missing file argument"; } - + const filePath = resolvePath(currentPath, args[0]); - const pathParts = filePath.split('/').filter((part: string) => part !== ''); + const pathParts = filePath.split("/").filter((part: string) => part !== ""); const fileName = pathParts.pop(); - - let current = fileSystem['/']; + + let current = fileSystem["/"]; for (const part of pathParts) { - if (current?.children && current.children[part] && current.children[part].type === 'directory') { + if ( + current?.children && + current.children[part] && + current.children[part].type === "directory" + ) { current = current.children[part]; } else { return `cat: ${filePath}: No such file or directory`; } } - + if (!fileName || !current?.children || !current.children[fileName]) { return `cat: ${filePath}: No such file or directory`; } - + const file = current.children[fileName]; - if (file.type !== 'file') { + if (file.type !== "file") { return `cat: ${filePath}: Is a directory`; } - - return file.content || ''; + + return file.content || ""; } function handleTree(context: CommandContext): string { const { fileSystem } = context; - - const buildTree = (node: FileSystemNode, prefix: string = '', isLast: boolean = true): string => { - let result = ''; + + const buildTree = ( + node: FileSystemNode, + prefix: string = "", + isLast: boolean = true, + ): string => { + let result = ""; if (!node.children) return result; - + const entries = Object.entries(node.children); entries.forEach(([name, child], index) => { const isLastChild = index === entries.length - 1; - const connector = isLastChild ? '└── ' : '├── '; - const color = child.type === 'directory' ? '\x1b[34m' : '\x1b[0m'; - const suffix = child.type === 'directory' ? '/' : ''; - + const connector = isLastChild ? "└── " : "├── "; + const color = child.type === "directory" ? "\x1b[34m" : "\x1b[0m"; + const suffix = child.type === "directory" ? "/" : ""; + result += `${prefix}${connector}${color}${name}${suffix}\x1b[0m\n`; - - if (child.type === 'directory') { - const newPrefix = prefix + (isLastChild ? ' ' : '│ '); + + if (child.type === "directory") { + const newPrefix = prefix + (isLastChild ? " " : "│ "); result += buildTree(child, newPrefix, isLastChild); } }); - + return result; }; - - return '.\n' + buildTree(fileSystem['/']); + + return ".\n" + buildTree(fileSystem["/"]); } function handleWhoami(): string { - return 'guest@atri.dad'; + return "guest@atri.dad"; } function handleOpen(args: string[]): string { const path = args[0]; if (!path) { - return 'open: missing path argument'; + return "open: missing path argument"; } - - let url = ''; - if (path === '/resume' || path.startsWith('/resume')) { - url = '/resume'; - } else if (path === '/projects' || path.startsWith('/projects')) { - url = '/projects'; - } else if (path === '/posts' || path.startsWith('/posts')) { - url = '/posts'; - } else if (path === '/talks' || path.startsWith('/talks')) { - url = '/talks'; - } else if (path === '/' || path === '/about.txt') { - url = '/'; + + let url = ""; + if (path === "/resume" || path.startsWith("/resume")) { + url = "/resume"; + } else if (path === "/projects" || path.startsWith("/projects")) { + url = "/projects"; + } else if (path === "/posts" || path.startsWith("/posts")) { + url = "/posts"; + } else if (path === "/talks" || path.startsWith("/talks")) { + url = "/talks"; + } else if (path === "/" || path === "/about.txt") { + url = "/"; } else { return `open: cannot open '${path}': No associated page`; } - - window.open(url, '_blank'); + + window.open(url, "_blank"); return `Opening ${url} in new tab...`; } function handleSl(context: CommandContext): string { const { setIsTrainRunning, setTrainPosition } = context; - + setIsTrainRunning(true); setTrainPosition(100); - + const animateTrain = () => { let position = 100; const interval = setInterval(() => { position -= 1.5; setTrainPosition(position); - + if (position < -50) { clearInterval(interval); setIsTrainRunning(false); } }, 60); }; - + setTimeout(animateTrain, 100); - return ''; -} \ No newline at end of file + return ""; +} diff --git a/src/lib/terminal/fileSystem.ts b/src/utils/terminal/fs.ts similarity index 100% rename from src/lib/terminal/fileSystem.ts rename to src/utils/terminal/fs.ts diff --git a/src/lib/terminal/types.ts b/src/utils/terminal/types.ts similarity index 100% rename from src/lib/terminal/types.ts rename to src/utils/terminal/types.ts diff --git a/src/lib/terminal/utils.ts b/src/utils/terminal/utils.ts similarity index 54% rename from src/lib/terminal/utils.ts rename to src/utils/terminal/utils.ts index 0922d49..b954f2c 100644 --- a/src/lib/terminal/utils.ts +++ b/src/utils/terminal/utils.ts @@ -1,98 +1,108 @@ -import type { FileSystemNode } from './types'; -import { resolvePath } from './fileSystem'; +import type { FileSystemNode } from "./types"; +import { resolvePath } from "./fs"; export function getCompletions( input: string, currentPath: string, - fileSystem: { [key: string]: FileSystemNode } -): { completion: string | null, replaceFrom: number } { - const parts = input.trim().split(' '); + fileSystem: { [key: string]: FileSystemNode }, +): { completion: string | null; replaceFrom: number } { + const parts = input.trim().split(" "); const command = parts[0]; - const partialPath = parts[parts.length - 1] || ''; - + const partialPath = parts[parts.length - 1] || ""; + // Only complete paths for these commands - if (parts.length > 1 && ['ls', 'cd', 'cat', 'open'].includes(command)) { + if (parts.length > 1 && ["ls", "cd", "cat", "open"].includes(command)) { // Path completion - const isAbsolute = partialPath.startsWith('/'); - const pathToComplete = isAbsolute ? partialPath : resolvePath(currentPath, partialPath); - + const isAbsolute = partialPath.startsWith("/"); + const pathToComplete = isAbsolute + ? partialPath + : resolvePath(currentPath, partialPath); + // Find the directory to search in and the prefix to match let dirPath: string; let searchPrefix: string; - - if (pathToComplete.endsWith('/')) { + + if (pathToComplete.endsWith("/")) { // Path ends with slash - complete inside this directory dirPath = pathToComplete; - searchPrefix = ''; + searchPrefix = ""; } else { // Base case - find directory and prefix - const lastSlash = pathToComplete.lastIndexOf('/'); + const lastSlash = pathToComplete.lastIndexOf("/"); if (lastSlash >= 0) { dirPath = pathToComplete.substring(0, lastSlash + 1); searchPrefix = pathToComplete.substring(lastSlash + 1); } else { - dirPath = currentPath.endsWith('/') ? currentPath : currentPath + '/'; + dirPath = currentPath.endsWith("/") ? currentPath : currentPath + "/"; searchPrefix = pathToComplete; } } - + // Calculate where to start replacement in the original input - const spaceBeforeArg = input.lastIndexOf(' '); + const spaceBeforeArg = input.lastIndexOf(" "); const replaceFrom = spaceBeforeArg >= 0 ? spaceBeforeArg + 1 : 0; - + // Navigate to the directory - const dirParts = dirPath.split('/').filter((part: string) => part !== ''); - let current = fileSystem['/']; - + const dirParts = dirPath.split("/").filter((part: string) => part !== ""); + let current = fileSystem["/"]; + for (const part of dirParts) { - if (current?.children && current.children[part] && current.children[part].type === 'directory') { + if ( + current?.children && + current.children[part] && + current.children[part].type === "directory" + ) { current = current.children[part]; } else { return { completion: null, replaceFrom }; } } - + if (!current?.children) { return { completion: null, replaceFrom }; } - + // Get first matching item - const match = Object.keys(current.children) - .find(name => name.startsWith(searchPrefix)); - + const match = Object.keys(current.children).find((name) => + name.startsWith(searchPrefix), + ); + if (match) { const item = current.children[match]; - const completion = item.type === 'directory' ? `${match}/` : match; + const completion = item.type === "directory" ? `${match}/` : match; return { completion, replaceFrom }; } } - + return { completion: null, replaceFrom: input.length }; } export function formatOutput(text: string): string { return text .replace(/\x1b\[34m/g, '') - .replace(/\x1b\[0m/g, ''); + .replace(/\x1b\[0m/g, ""); } -export function saveCommandToHistory(command: string, persistentHistory: string[]): string[] { +export function saveCommandToHistory( + command: string, + persistentHistory: string[], +): string[] { if (command.trim()) { const updatedHistory = [...persistentHistory, command].slice(-100); // Keep last 100 commands - localStorage.setItem('terminal-history', JSON.stringify(updatedHistory)); + localStorage.setItem("terminal-history", JSON.stringify(updatedHistory)); return updatedHistory; } return persistentHistory; } export function loadCommandHistory(): string[] { - const savedHistory = localStorage.getItem('terminal-history'); + const savedHistory = localStorage.getItem("terminal-history"); if (savedHistory) { try { return JSON.parse(savedHistory); } catch (error) { - console.error('Error loading command history:', error); + console.error("Error loading command history:", error); } } return []; -} \ No newline at end of file +}