import { h, Fragment } from 'preact'; import { useMemo } from 'preact/hooks'; import cs from 'classnames'; import { Apple2 as Apple2Impl } from 'js/apple2'; import { BlockDisk, DiskFormat, DriveNumber, MassStorage, NibbleDisk } from 'js/formats/types'; import { slot } from 'js/apple2io'; import DiskII from 'js/cards/disk2'; import SmartPort from 'js/cards/smartport'; import createDiskFrom2MG from 'js/formats/2mg'; import createBlockDisk from 'js/formats/block'; import { ProDOSVolume } from 'js/formats/prodos'; import { FILE_TYPES, STORAGE_TYPES } from 'js/formats/prodos/constants'; import { Directory } from 'js/formats/prodos/directory'; import { FileEntry } from 'js/formats/prodos/file_entry'; import { VDH } from 'js/formats/prodos/vdh'; import { toHex } from 'js/util'; import styles from './css/Disks.module.css'; import debuggerStyles from './css/Debugger.module.css'; import { useCallback, useState } from 'preact/hooks'; import { DOS33, FileEntry as DOSEntry, isMaybeDOS33 } from 'js/formats/dos/dos33'; import createDiskFromDOS from 'js/formats/do'; import { FileData, FileViewer } from './FileViewer'; /** * Formats a short date string * * @param date Data object * @returns Short string date */ const formatDate = (date: Date) => { return date.toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' }); }; /** * Guard for determining whether a disk is a nibble or block based disk * * @param disk NibbleDisk or BlockDisk * @returns true if is BlockDisk */ function isBlockDisk(disk: NibbleDisk | BlockDisk): disk is BlockDisk { return !!((disk as BlockDisk).blocks); } /** * Props for FileListing component */ interface FileListingProps { volume: ProDOSVolume; fileEntry: FileEntry; depth: number; setFileData: (fileData: FileData) => void; } /** * Renders a ProDOS file entry. * * @param depth Depth of listing from root * @param fileEntry ProDOS file entry to display * @returns FileListing component */ const FileListing = ({ depth, fileEntry, setFileData }: FileListingProps) => { const deleted = fileEntry.storageType === STORAGE_TYPES.DELETED; const doSetFileData = useCallback(() => { const binary = fileEntry.getFileData(); const text = fileEntry.getFileText(); if (binary && text) { setFileData({ binary, text, fileName: fileEntry.name, }); } }, [fileEntry, setFileData]); return ( {'| '.repeat(depth)} {deleted ? : } {' '} {fileEntry.name} {FILE_TYPES[fileEntry.fileType] ?? `$${toHex(fileEntry.fileType)}`} {`$${toHex(fileEntry.auxType, 4)}`} {fileEntry.blocksUsed} {formatDate(fileEntry.creation)} {formatDate(fileEntry.lastMod)} ); }; /** * Props for DirectoryListing Component. */ interface DirectoryListingProps { volume: ProDOSVolume; dirEntry: VDH | Directory; depth: number; setFileData: (fileData: FileData) => void; } /** * Displays information about a ProDOS directory, recursing through child * directories. * * @param volume ProDOS volume * @param depth Current directory depth * @param dirEntry Current directory entry to display * @returns DirectoryListing component */ const DirectoryListing = ({ volume, depth, dirEntry, setFileData }: DirectoryListingProps) => { const [open, setOpen] = useState(depth === 0); return ( <> setOpen((open) => !open)} title={dirEntry.name} > {'| '.repeat(depth)} {' '} {dirEntry.name} {formatDate(dirEntry.creation)} {open && dirEntry.entries.map((fileEntry, idx) => { if (fileEntry.storageType === STORAGE_TYPES.DIRECTORY) { const dirEntry = new Directory(volume, fileEntry); return ; } else { return ; } })} ); }; /** * Props for CatalogEntry component */ interface CatalogEntryProps { dos: DOS33; fileEntry: DOSEntry; setFileData: (fileData: FileData) => void; } /** * Component for a single DOS 3.x catalog entry * * @param entry Catalog entry to display * @returns CatalogEntry component */ const CatalogEntry = ({ dos, fileEntry, setFileData }: CatalogEntryProps) => { const doSetFileData = useCallback(() => { const { data } = dos.readFile(fileEntry); setFileData({ binary: data, text: dos.dumpFile(fileEntry), fileName: fileEntry.name, }); }, [dos, fileEntry, setFileData]); return ( {fileEntry.locked && } {' '} {fileEntry.name} {fileEntry.type} {fileEntry.size} ); }; /** * Catalog component props */ interface CatalogProps { dos: DOS33; setFileData: (fileData: FileData) => void; } /** * DOS 3.3 disk catalog component * * @param dos DOS 3.3 disk object * @returns Catalog component */ const Catalog = ({ dos, setFileData }: CatalogProps) => { const catalog = useMemo(() => dos.readCatalog(), [dos]); return ( <> {catalog.map((fileEntry, idx) => ( ))} ); }; /** * Props for DiskInfo component */ interface DiskInfoProps { massStorage: MassStorage; drive: DriveNumber; setFileData: (fileData: FileData) => void; } /** * Top level disk info component, handles determining what sort of disk * is present and using the appropriate sub-component depending on whether * it's a ProDOS block disk or a DOS 3.3 disk. * * TODO(whscullin): Does not handle woz or 13 sector. * * @param massStorage The storage device * @param drive The drive number * @returns DiskInfo component */ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { const disk = useMemo(() => { const massStorageData = massStorage.getBinary(drive, 'po'); if (massStorageData) { const { name, data, readOnly, ext } = massStorageData; let disk: BlockDisk | NibbleDisk | null = null; if (ext === '2mg') { disk = createDiskFrom2MG({ name, rawData: data, readOnly, volume: 254, }); } else if (data.byteLength < 800 * 1024) { const doData = massStorage.getBinary(drive, 'do'); if (doData) { if (isMaybeDOS33(doData)) { disk = createDiskFromDOS({ name, rawData: doData.data, readOnly, volume: 254, }); } } } if (!disk) { disk = createBlockDisk({ name, rawData: data, readOnly, volume: 254, }); } return disk; } return null; }, [massStorage, drive]); if (disk) { try { if (isBlockDisk(disk)) { if (disk.blocks.length) { const prodos = new ProDOSVolume(disk); const { totalBlocks } = prodos.vdh(); const freeCount = prodos.bitMap().freeBlocks().length; const usedCount = totalBlocks - freeCount; return (
Filename Type Aux Blocks Created Modified
Blocks Free: {freeCount} Used: {usedCount} Total: {totalBlocks}
); } } else { const dos = new DOS33(disk); return (
Filename Type Sectors
Volume Number: {dos.getVolumeNumber()}
Used Sectors: {dos.usedSectorCount()}
Free Sectors: {dos.freeSectorCount()}
); } } catch (error) { console.error(error); return
Unknown volume
; } } return
No disk
; }; /** * Disks component props */ export interface DisksProps { apple2: Apple2Impl; } /** * A debugger panel that displays information about currently mounted * disks. * * @param apple2 The apple2 object * @returns Disks component */ export const Disks = ({ apple2 }: DisksProps) => { const [fileData, setFileData] = useState(null); const io = apple2.getIO(); const cards: MassStorage[] = []; const onClose = useCallback(() => { setFileData(null); }, []); for (let idx = 0; idx <= 7; idx++) { const card = io.getSlot(idx as slot); if (card instanceof DiskII || card instanceof SmartPort) { cards.push(card); } } return (
{cards.map((card, idx) => (
{card.constructor.name} - 1
{card.constructor.name} - 2
))}
); };