diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 24d7cc6..2993336 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"], } diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..6f00bdc --- /dev/null +++ b/biome.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "a11y": { + "useAltText": "off", + "noSvgWithoutTitle": "off", + "useKeyWithClickEvents": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/bun.lockb b/bun.lockb index 137b3d7..b4af33b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index b6bdd54..605c5d3 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,22 @@ "@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-fs": "~2", "@tauri-apps/plugin-shell": "^2", + "antd": "^5.22.0", + "clsx": "^2.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.27.0" }, "devDependencies": { + "@biomejs/biome": "1.9.4", + "@tauri-apps/cli": "^2", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.14", "typescript": "^5.2.2", - "vite": "^5.3.1", - "@tauri-apps/cli": "^2" + "vite": "^5.3.1" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 1ee3cf8..6799274 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,15 +2,20 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": [ - "main" - ], + "windows": ["main"], "permissions": [ "core:default", "shell:allow-open", "dialog:default", "fs:allow-read-file", - "fs:allow-read-text-file", + { + "identifier": "fs:allow-read-dir", + "allow": [{ "path": "**" }] + }, + { + "identifier": "fs:allow-read-text-file", + "allow": [{ "path": "**" }] + }, "fs:allow-open" ] -} \ No newline at end of file +} diff --git a/src/api/fs.ts b/src/api/fs.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/assets/icons.tsx b/src/assets/icons.tsx new file mode 100644 index 0000000..1cdc092 --- /dev/null +++ b/src/assets/icons.tsx @@ -0,0 +1,29 @@ +import type { SVGProps } from "react"; + +function createIcon(svgPath: string) { + return function Icon(props: SVGProps) { + return ( + + ); + }; +} +export const DirectoryIcon = createIcon( + "M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z", +); +export const DirectoryOpenIcon = createIcon( + "M.513 1.513A1.75 1.75 0 0 1 1.75 1h3.5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1H13a1 1 0 0 1 1 1v.5H2.75a.75.75 0 0 0 0 1.5h11.978a1 1 0 0 1 .994 1.117L15 13.25A1.75 1.75 0 0 1 13.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75c0-.464.184-.91.513-1.237Z", +); +export const FileIcon = createIcon( + "M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z", +); diff --git a/src/assets/icons/directory.svg b/src/assets/icons/directory.svg new file mode 100644 index 0000000..38c304e --- /dev/null +++ b/src/assets/icons/directory.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/src/assets/icons/file.svg b/src/assets/icons/file.svg new file mode 100644 index 0000000..319568f --- /dev/null +++ b/src/assets/icons/file.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/src/components/FilePicker.tsx b/src/components/FilePicker.tsx new file mode 100644 index 0000000..8a12476 --- /dev/null +++ b/src/components/FilePicker.tsx @@ -0,0 +1,40 @@ +import type { DirEntry } from "@tauri-apps/plugin-fs"; +import clsx from "clsx"; +import { DirectoryIcon, FileIcon } from "../assets/icons"; + +export function FilePicker({ + entries, + onSelectFile, + className, +}: { + entries: DirEntry[]; + onSelectFile: (entry: DirEntry) => void; + className?: string; +}) { + return ( + + ); +} + +function FileRow({ + entry, + onClick, +}: { entry: DirEntry; onClick?: (entry: DirEntry) => void }) { + return ( +
  • onClick?.(entry)} + > + {!entry.isDirectory ? ( + + ) : ( + + )} + {entry.name} +
  • + ); +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..d7ee959 --- /dev/null +++ b/src/index.css @@ -0,0 +1,10 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + overscroll-behavior-y: none; /* Stop rubber banding */ + overscroll-behavior-x: none; /* Stop rubber banding */ + user-select: none; /* Disable text selection */ +} diff --git a/src/api/greet.ts b/src/ipc.ts similarity index 100% rename from src/api/greet.ts rename to src/ipc.ts diff --git a/src/main.tsx b/src/main.tsx index e247bab..91a43ca 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,8 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import "./index.css"; -import { - createMemoryRouter, - RouterProvider, -} from "react-router-dom"; +import { RouterProvider, createMemoryRouter } from "react-router-dom"; import FileSystemView from "./views/FileSystemView"; const router = createMemoryRouter([ diff --git a/src/views/FileSystemView.tsx b/src/views/FileSystemView.tsx index e4b2563..d0b1b4d 100644 --- a/src/views/FileSystemView.tsx +++ b/src/views/FileSystemView.tsx @@ -1,45 +1,98 @@ -import { open } from '@tauri-apps/plugin-dialog'; -import { readTextFile } from '@tauri-apps/plugin-fs'; -import { useState } from 'react'; +import { open } from "@tauri-apps/plugin-dialog"; +import { type DirEntry, readDir, readTextFile } from "@tauri-apps/plugin-fs"; +import { Button } from "antd"; +import { useEffect, useState } from "react"; +import { FilePicker } from "../components/FilePicker"; export default function FileSystemView() { - + const [currentPath, setCurrentPath] = useState( + "/Users/tomhill/Developer/projects/DNAvigate/examples", + ); const [fileContents, setFileContents] = useState(null); + const [entries, setEntries] = useState([]); + + const handlePick = (entry: DirEntry) => { + if (entry.name === "..") { + setCurrentPath(currentPath.replace(/\/[^/]+$/, "")); + return; + } + + if (entry.isDirectory) { + setCurrentPath(`${currentPath}/${entry.name}`); + } else { + handleSelectFile(`${currentPath}/${entry.name}`); + } + }; const handleFileOpen = async () => { const path = await open({ multiple: false, directory: false, - title: 'Open File', - filters: [{ - name: 'Text Files', - extensions: ['txt'] - }, { - name: "FASTA Files", - extensions: ["fasta", "fa", "fasta.gz", "fa.gz"] - }, { - name: "FASTQ Files", - extensions: ["fastq", "fq", "fastq.gz", "fq.gz"] - }] + title: "Open File", + filters: [ + { + name: "Text Files", + extensions: ["txt"], + }, + { + name: "FASTA Files", + extensions: ["fasta", "fa", "fasta.gz", "fa.gz"], + }, + { + name: "FASTQ Files", + extensions: ["fastq", "fq", "fastq.gz", "fq.gz"], + }, + ], }); + handleSelectFile(path); + }; + + const handleSelectFile = async (path: string | null) => { if (!path) { - console.log('No file selected'); return; } - - console.log('Selected file:', path); // TODO: consider how to handle very large files const contents = await readTextFile(path); setFileContents(contents); + }; + + async function getFiles(path: string) { + const contents = await readDir(path); + setEntries(contents); } + // biome-ignore lint/correctness/useExhaustiveDependencies: not getFiles + useEffect(() => { + getFiles(currentPath); + }, [currentPath]); + + const sortedEntries = [ + { name: "..", isDirectory: true, isSymlink: false, isFile: false }, + ...entries + .sort((a, b) => { + // Sort directories before files + if (a.isDirectory !== b.isDirectory) { + return a.isDirectory ? -1 : 1; + } + // If both are directories or both are files, sort by name + return a.name.localeCompare(b.name); + }) + .filter((entry) => !entry.name.startsWith(".")), // Hide hidden files + ]; + return ( -
    -

    File System

    -

    File System view

    - - {fileContents &&
    {fileContents}
    } +
    +
    +

    Files

    + + +
    +
    + {fileContents &&
    {fileContents}
    } +
    - ) -} \ No newline at end of file + ); +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..614c86b --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +};