feat(nvim): improve tree and file switching, and implements links
This commit is contained in:
@@ -15,7 +15,6 @@
|
|||||||
"music-metadata-browser": "^2.5.10",
|
"music-metadata-browser": "^2.5.10",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-router-dom": "^6.21.3",
|
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"vite-tsconfig-paths": "^4.3.1",
|
"vite-tsconfig-paths": "^4.3.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
|
|||||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -20,9 +20,6 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: 18.2.0
|
specifier: 18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
react-router-dom:
|
|
||||||
specifier: ^6.21.3
|
|
||||||
version: 6.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
@@ -335,10 +332,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
'@remix-run/router@1.14.2':
|
|
||||||
resolution: {integrity: sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
|
|
||||||
'@rollup/plugin-inject@5.0.5':
|
'@rollup/plugin-inject@5.0.5':
|
||||||
resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==}
|
resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -1688,19 +1681,6 @@ packages:
|
|||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
react-router-dom@6.21.3:
|
|
||||||
resolution: {integrity: sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16.8'
|
|
||||||
react-dom: '>=16.8'
|
|
||||||
|
|
||||||
react-router@6.21.3:
|
|
||||||
resolution: {integrity: sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16.8'
|
|
||||||
|
|
||||||
react@18.2.0:
|
react@18.2.0:
|
||||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2248,8 +2228,6 @@ snapshots:
|
|||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@remix-run/router@1.14.2': {}
|
|
||||||
|
|
||||||
'@rollup/plugin-inject@5.0.5(rollup@4.9.6)':
|
'@rollup/plugin-inject@5.0.5(rollup@4.9.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.1.0(rollup@4.9.6)
|
'@rollup/pluginutils': 5.1.0(rollup@4.9.6)
|
||||||
@@ -3688,18 +3666,6 @@ snapshots:
|
|||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
react-router-dom@6.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
|
||||||
dependencies:
|
|
||||||
'@remix-run/router': 1.14.2
|
|
||||||
react: 18.2.0
|
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
|
||||||
react-router: 6.21.3(react@18.2.0)
|
|
||||||
|
|
||||||
react-router@6.21.3(react@18.2.0):
|
|
||||||
dependencies:
|
|
||||||
'@remix-run/router': 1.14.2
|
|
||||||
react: 18.2.0
|
|
||||||
|
|
||||||
react@18.2.0:
|
react@18.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
|
|||||||
41
src/App.tsx
41
src/App.tsx
@@ -3,35 +3,32 @@ import { Kitty } from "./components/Kitty";
|
|||||||
import { AppProvider } from "./providers/AppProvider";
|
import { AppProvider } from "./providers/AppProvider";
|
||||||
import { Music } from "./components/Music";
|
import { Music } from "./components/Music";
|
||||||
import { Nvim } from "./components/Nvim";
|
import { Nvim } from "./components/Nvim";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [loggedIn, setLoggedIn] = useState(false);
|
const [loggedIn, setLoggedIn] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppProvider>
|
<AppProvider>
|
||||||
<BrowserRouter>
|
<main className="h-screen w-screen overflow-hidden bg-[url(/wallpaper.jpg)] bg-cover">
|
||||||
<main className="h-screen w-screen overflow-hidden bg-[url(/wallpaper.jpg)] bg-cover">
|
{loggedIn ? (
|
||||||
{loggedIn ? (
|
<div className="flex h-full w-full flex-col">
|
||||||
<div className="flex h-full w-full flex-col">
|
<Kitty className="w-full flex-1 pb-1 pl-2 pr-2 pt-2">
|
||||||
<Kitty className="w-full flex-1 pb-1 pl-2 pr-2 pt-2">
|
<Nvim />
|
||||||
<Nvim />
|
</Kitty>
|
||||||
</Kitty>
|
|
||||||
|
|
||||||
<Music />
|
<Music />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<button
|
<button
|
||||||
className="rounded-md border border-black px-2 py-1 hover:border-2 hover:font-bold"
|
className="rounded-md border border-black px-2 py-1 hover:border-2 hover:font-bold"
|
||||||
onClick={() => setLoggedIn(true)}
|
onClick={() => setLoggedIn(true)}
|
||||||
>
|
>
|
||||||
Log in
|
Log in
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</BrowserRouter>
|
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export const NvimEditor = (props: { data: string }) => (
|
export const NvimEditor = (props: { source: string | undefined }) => (
|
||||||
<div className="h-full">{props.data}</div>
|
<div className="h-full">{props.source}</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
import { useApp } from "~/hooks/useApp";
|
|
||||||
import { CHAR_HEIGHT, CHAR_WIDTH } from "../Kitty";
|
|
||||||
import { type ReactNode, useEffect, useState, useCallback } from "react";
|
|
||||||
import { type InnerKittyProps } from "~/utils/types";
|
|
||||||
import { type Nvim } from ".";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
type FileIcon = {
|
|
||||||
char: string;
|
|
||||||
color: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type File = {
|
|
||||||
name: string;
|
|
||||||
} & (
|
|
||||||
| { type: "file"; directory?: File & { type: "directory" } }
|
|
||||||
| { type: "directory"; files: Array<string>; folded: boolean }
|
|
||||||
);
|
|
||||||
|
|
||||||
const FILE_ICONS: Record<string, FileIcon> = {
|
|
||||||
md: {
|
|
||||||
char: " ",
|
|
||||||
color: "#89bafa",
|
|
||||||
},
|
|
||||||
asc: {
|
|
||||||
char: " ",
|
|
||||||
color: "#f9e2af",
|
|
||||||
},
|
|
||||||
UNKNOWN: { char: " ", color: "#f599ae" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortFiles = (files: Array<File>) =>
|
|
||||||
files
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
.sort((a, b) =>
|
|
||||||
a.type === "directory" && b.type !== "directory"
|
|
||||||
? -1
|
|
||||||
: a.type !== "directory" && b.type === "directory"
|
|
||||||
? 1
|
|
||||||
: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const NvimTree = (props: InnerKittyProps<typeof Nvim>) => {
|
|
||||||
const { rootManifest, activeKitty } = useApp();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [files, setFiles] = useState<Array<File>>(
|
|
||||||
sortFiles([
|
|
||||||
{
|
|
||||||
type: "directory",
|
|
||||||
name: "projects",
|
|
||||||
files: rootManifest.projects,
|
|
||||||
folded: true,
|
|
||||||
},
|
|
||||||
...rootManifest.files.map((name) => ({ type: "file" as const, name })),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
const [selected, setSelected] = useState(files.length - 1);
|
|
||||||
|
|
||||||
const handleOpen = (file: File) => {
|
|
||||||
if (file.type === "directory") {
|
|
||||||
file.folded = !file.folded;
|
|
||||||
} else {
|
|
||||||
let filePath = "";
|
|
||||||
if (file.directory) {
|
|
||||||
filePath += `${file.directory.name}/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(`?view=${filePath}${file.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFiles([...files]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tree: Array<ReactNode> = [];
|
|
||||||
let y = 0;
|
|
||||||
let selectedFile: File;
|
|
||||||
for (const fileOrDir of files) {
|
|
||||||
if (y === selected) selectedFile = fileOrDir;
|
|
||||||
if (fileOrDir.type === "directory") {
|
|
||||||
const dy = y;
|
|
||||||
tree.push(
|
|
||||||
<li
|
|
||||||
key={y}
|
|
||||||
className="text-[#a0b6ee]"
|
|
||||||
style={{ background: y === selected ? "#504651" : "" }}
|
|
||||||
onMouseDown={() => setSelected(dy)}
|
|
||||||
onDoubleClick={() => handleOpen(fileOrDir)}
|
|
||||||
>
|
|
||||||
{fileOrDir.folded ? (
|
|
||||||
<>
|
|
||||||
<span className="text-[#716471]"> </span>{" "}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<> </>
|
|
||||||
)}
|
|
||||||
{fileOrDir.name}
|
|
||||||
</li>,
|
|
||||||
);
|
|
||||||
if (!fileOrDir.folded) {
|
|
||||||
for (let i = 0; i < fileOrDir.files.length; i++) {
|
|
||||||
y++;
|
|
||||||
if (y === selected)
|
|
||||||
selectedFile = {
|
|
||||||
type: "file",
|
|
||||||
name: fileOrDir.files[i],
|
|
||||||
directory: fileOrDir,
|
|
||||||
};
|
|
||||||
|
|
||||||
const icon = FILE_ICONS.UNKNOWN;
|
|
||||||
const fy = y;
|
|
||||||
tree.push(
|
|
||||||
<li
|
|
||||||
key={y}
|
|
||||||
style={{ background: y === selected ? "#504651" : "" }}
|
|
||||||
onMouseDown={() => setSelected(fy)}
|
|
||||||
onDoubleClick={() =>
|
|
||||||
handleOpen({
|
|
||||||
type: "file",
|
|
||||||
name: fileOrDir.files[i],
|
|
||||||
directory: fileOrDir,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
<span className="text-[#5b515b]">
|
|
||||||
{i === fileOrDir.files.length - 1 ? "└ " : "│ "}
|
|
||||||
</span>
|
|
||||||
<span style={{ color: icon.color }}>{`${icon.char}`}</span>
|
|
||||||
<span>{fileOrDir.files[i]}</span>
|
|
||||||
</li>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const parts = fileOrDir.name.split(".");
|
|
||||||
let extension = parts[parts.length - 1];
|
|
||||||
if (!FILE_ICONS[extension]) extension = "UNKNOWN";
|
|
||||||
|
|
||||||
const icon = FILE_ICONS[extension];
|
|
||||||
|
|
||||||
const fy = y;
|
|
||||||
tree.push(
|
|
||||||
<li
|
|
||||||
key={y}
|
|
||||||
style={{ background: y === selected ? "#504651" : "" }}
|
|
||||||
onMouseDown={() => setSelected(fy)}
|
|
||||||
onDoubleClick={() => handleOpen(fileOrDir)}
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
<span style={{ color: icon.color }}>{`${icon.char}`}</span>
|
|
||||||
{fileOrDir.name === "README.md" ? (
|
|
||||||
<span className="font-bold text-[#d8c5a1]">README.md</span>
|
|
||||||
) : (
|
|
||||||
<span>{fileOrDir.name}</span>
|
|
||||||
)}
|
|
||||||
</li>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
y++;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onScroll = (event: KeyboardEvent) => {
|
|
||||||
if (activeKitty !== props.id) return;
|
|
||||||
switch (event.key) {
|
|
||||||
case "ArrowUp":
|
|
||||||
setSelected((x) => Math.max(0, x - 1));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "ArrowDown":
|
|
||||||
setSelected((x) => Math.min(y - 1, x + 1));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Enter":
|
|
||||||
handleOpen(selectedFile);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("keydown", onScroll);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("keydown", onScroll);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full select-none bg-[#0000001a]">
|
|
||||||
<ul
|
|
||||||
style={{
|
|
||||||
padding: `${CHAR_HEIGHT}px ${CHAR_WIDTH}px 0 ${CHAR_WIDTH * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tree}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
25
src/components/Nvim/NvimTree/NvimTreeDirectory.tsx
Normal file
25
src/components/Nvim/NvimTree/NvimTreeDirectory.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Directory } from "~/utils/types";
|
||||||
|
|
||||||
|
export const NvimTreeDirectory = (props: {
|
||||||
|
directory: Directory;
|
||||||
|
y: number;
|
||||||
|
selected: boolean;
|
||||||
|
onSelect: (y: number) => void;
|
||||||
|
onOpen: (directory: Directory) => void;
|
||||||
|
}) => (
|
||||||
|
<li
|
||||||
|
className="text-[#a0b6ee]"
|
||||||
|
style={{ background: props.selected ? "#504651" : "" }}
|
||||||
|
onMouseDown={() => props.onSelect(props.y)}
|
||||||
|
onDoubleClick={() => props.onOpen(props.directory)}
|
||||||
|
>
|
||||||
|
{props.directory.opened ? (
|
||||||
|
<> </>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-[#716471]"> </span>{" "}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{props.directory.name}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
41
src/components/Nvim/NvimTree/NvimTreeFile.tsx
Normal file
41
src/components/Nvim/NvimTree/NvimTreeFile.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { ICONS } from "~/utils/icons";
|
||||||
|
import { File } from "~/utils/types";
|
||||||
|
|
||||||
|
export const NvimTreeFile = (props: {
|
||||||
|
file: File;
|
||||||
|
y: number;
|
||||||
|
selected: boolean;
|
||||||
|
inDirectory: boolean | "last";
|
||||||
|
onSelect: (y: number) => void;
|
||||||
|
onOpen: (file: File) => void;
|
||||||
|
}) => {
|
||||||
|
let iconName = props.file.icon;
|
||||||
|
if (!iconName) {
|
||||||
|
const parts = props.file.name.split(".");
|
||||||
|
iconName = parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ICONS[iconName]) iconName = "UNKNOWN";
|
||||||
|
const icon = ICONS[iconName];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
style={{ background: props.selected ? "#504651" : "" }}
|
||||||
|
onMouseDown={() => props.onSelect(props.y)}
|
||||||
|
onDoubleClick={() => props.onOpen(props.file)}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
{props.inDirectory && (
|
||||||
|
<span className="text-[#5b515b]">
|
||||||
|
{props.inDirectory === "last" ? "└ " : "│ "}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span style={{ color: icon.color }}>{`${icon.char}`}</span>
|
||||||
|
{props.file.name === "README.md" ? (
|
||||||
|
<span className="font-bold text-[#d8c5a1]">README.md</span>
|
||||||
|
) : (
|
||||||
|
<span>{props.file.name}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
178
src/components/Nvim/NvimTree/index.tsx
Normal file
178
src/components/Nvim/NvimTree/index.tsx
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { useApp } from "~/hooks/useApp";
|
||||||
|
import { CHAR_HEIGHT, CHAR_WIDTH } from "../../Kitty";
|
||||||
|
import { type ReactNode, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
type File,
|
||||||
|
type InnerKittyProps,
|
||||||
|
type RootManifest,
|
||||||
|
type Directory,
|
||||||
|
} from "~/utils/types";
|
||||||
|
import { type Nvim } from "..";
|
||||||
|
import { NvimTreeDirectory } from "./NvimTreeDirectory";
|
||||||
|
import { NvimTreeFile } from "./NvimTreeFile";
|
||||||
|
import { promiseHooks } from "v8";
|
||||||
|
|
||||||
|
const sortFiles = (files: Array<File | Directory>) =>
|
||||||
|
files
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.type === "directory" && b.type !== "directory"
|
||||||
|
? -1
|
||||||
|
: a.type !== "directory" && b.type === "directory"
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const manifestToTree = (manifest: RootManifest) =>
|
||||||
|
sortFiles([
|
||||||
|
{
|
||||||
|
type: "directory",
|
||||||
|
name: "links",
|
||||||
|
opened: false,
|
||||||
|
files: manifest.links.map((link) => ({
|
||||||
|
type: "link" as const,
|
||||||
|
...link,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "directory",
|
||||||
|
name: "projects",
|
||||||
|
opened: false,
|
||||||
|
files: manifest.projects.map((project) => ({
|
||||||
|
type: "file" as const,
|
||||||
|
repo: project.name,
|
||||||
|
fileName: "README.md",
|
||||||
|
...project,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
...manifest.files.map((file) => ({
|
||||||
|
type: "file" as const,
|
||||||
|
name: file,
|
||||||
|
repo: "pihkaal",
|
||||||
|
fileName: file,
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const NvimTree = (
|
||||||
|
props: InnerKittyProps<typeof Nvim> & {
|
||||||
|
onOpen: (file: File) => void;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const { rootManifest, activeKitty } = useApp();
|
||||||
|
|
||||||
|
const [files, setFiles] = useState(manifestToTree(rootManifest));
|
||||||
|
const [selectedY, setSelectedY] = useState(0);
|
||||||
|
|
||||||
|
const tree: Array<ReactNode> = [];
|
||||||
|
let y = 0;
|
||||||
|
let selectedFile = files[0];
|
||||||
|
for (const file of files) {
|
||||||
|
if (selectedY === y) selectedFile = file;
|
||||||
|
if (file.type === "directory") {
|
||||||
|
tree.push(
|
||||||
|
<NvimTreeDirectory
|
||||||
|
key={y}
|
||||||
|
directory={file}
|
||||||
|
y={y}
|
||||||
|
selected={selectedY === y}
|
||||||
|
onSelect={setSelectedY}
|
||||||
|
onOpen={(directory) => {
|
||||||
|
directory.opened = !directory.opened;
|
||||||
|
setFiles([...files]);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (file.opened) {
|
||||||
|
file.files.forEach((childFile, i) => {
|
||||||
|
y++;
|
||||||
|
if (selectedY === y) selectedFile = childFile;
|
||||||
|
tree.push(
|
||||||
|
<NvimTreeFile
|
||||||
|
key={y}
|
||||||
|
file={childFile}
|
||||||
|
y={y}
|
||||||
|
inDirectory={i === file.files.length - 1 ? "last" : true}
|
||||||
|
selected={selectedY === y}
|
||||||
|
onSelect={setSelectedY}
|
||||||
|
onOpen={props.onOpen}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tree.push(
|
||||||
|
<NvimTreeFile
|
||||||
|
key={y}
|
||||||
|
file={file}
|
||||||
|
y={y}
|
||||||
|
inDirectory={false}
|
||||||
|
selected={selectedY === y}
|
||||||
|
onSelect={setSelectedY}
|
||||||
|
onOpen={props.onOpen}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onScroll = (event: KeyboardEvent) => {
|
||||||
|
if (activeKitty !== props.id) return;
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowUp":
|
||||||
|
setSelectedY((x) => Math.max(0, x - 1));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ArrowDown":
|
||||||
|
setSelectedY((x) => Math.min(y - 1, x + 1));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Enter":
|
||||||
|
if (selectedFile.type === "directory") {
|
||||||
|
selectedFile.opened = !selectedFile.opened;
|
||||||
|
setFiles([...files]);
|
||||||
|
} else {
|
||||||
|
props.onOpen(selectedFile);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", onScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onScroll);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full select-none bg-[#0000001a]">
|
||||||
|
<ul
|
||||||
|
style={{
|
||||||
|
padding: `${CHAR_HEIGHT}px ${CHAR_WIDTH}px 0 ${CHAR_WIDTH * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tree}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type FileIcon = {
|
||||||
|
char: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FILE_ICONS: Record<string, FileIcon> = {
|
||||||
|
md: {
|
||||||
|
char: " ",
|
||||||
|
color: "#89bafa",
|
||||||
|
},
|
||||||
|
asc: {
|
||||||
|
char: " ",
|
||||||
|
color: "#f9e2af",
|
||||||
|
},
|
||||||
|
UNKNOWN: { char: " ", color: "#f599ae" },
|
||||||
|
};
|
||||||
@@ -4,59 +4,23 @@ import { NvimEditor } from "./NvimEditor";
|
|||||||
import { NvimInput } from "./NvimInput";
|
import { NvimInput } from "./NvimInput";
|
||||||
import { NvimStatusBar } from "./NvimStatusBar";
|
import { NvimStatusBar } from "./NvimStatusBar";
|
||||||
import { NvimTree } from "./NvimTree";
|
import { NvimTree } from "./NvimTree";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useState } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { File } from "~/utils/types";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const fetchData = async (
|
|
||||||
branch: string,
|
|
||||||
repo: string,
|
|
||||||
file: string,
|
|
||||||
): Promise<string | null> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get<string>(
|
|
||||||
`https://raw.githubusercontent.com/pihkaal/${repo}/${branch}/${file}`,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Nvim = (_props: {}) => {
|
export const Nvim = (_props: {}) => {
|
||||||
const kitty = useKitty();
|
const kitty = useKitty();
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [data, setData] = useState<string>();
|
const [activeFile, setActiveFile] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
const handleOpenFile = (file: File) => {
|
||||||
const params = new URLSearchParams(location.search);
|
if (file.type === "link") {
|
||||||
const view = params.get("view");
|
window.open(file.url, "_blank")?.focus();
|
||||||
if (!view) {
|
} else {
|
||||||
navigate("?view=README.md");
|
setActiveFile(
|
||||||
return;
|
`https://raw.githubusercontent.com/pihkaal/${file.repo}/main/${file.fileName}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
const path = view.split("/");
|
|
||||||
if (path.length === 1) {
|
|
||||||
path.splice(0, 0, "pihkaal");
|
|
||||||
}
|
|
||||||
const repo = path[0]!;
|
|
||||||
const file = path[1]!;
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
const data =
|
|
||||||
(await fetchData("main", repo, file)) ??
|
|
||||||
(await fetchData("dev", repo, file));
|
|
||||||
if (!data) {
|
|
||||||
navigate("?view=README.md");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(data);
|
|
||||||
})();
|
|
||||||
}, [location, navigate]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -69,10 +33,10 @@ export const Nvim = (_props: {}) => {
|
|||||||
{kitty && (
|
{kitty && (
|
||||||
<>
|
<>
|
||||||
<div style={{ gridArea: "1 / 1 / 1 / 2" }}>
|
<div style={{ gridArea: "1 / 1 / 1 / 2" }}>
|
||||||
<NvimTree {...kitty} />
|
<NvimTree {...kitty} onOpen={handleOpenFile} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ gridArea: "1 / 2 / 1 / 3", overflow: "scroll" }}>
|
<div style={{ gridArea: "1 / 2 / 1 / 3", overflow: "scroll" }}>
|
||||||
<NvimEditor data={data ?? ""} />
|
<NvimEditor source={activeFile} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ gridArea: "2 / 1 / 2 / 3" }}>
|
<div style={{ gridArea: "2 / 1 / 2 / 3" }}>
|
||||||
<NvimStatusBar
|
<NvimStatusBar
|
||||||
|
|||||||
@@ -7,7 +7,28 @@ export const AppProvider = (props: { children?: ReactNode }) => {
|
|||||||
const [activeKitty, setActiveKitty] = useState(":r0:");
|
const [activeKitty, setActiveKitty] = useState(":r0:");
|
||||||
const [rootManifest, setRootManifest] = useState<RootManifest>({
|
const [rootManifest, setRootManifest] = useState<RootManifest>({
|
||||||
files: ["README.md", "pubkey.asc"],
|
files: ["README.md", "pubkey.asc"],
|
||||||
projects: ["me", "tlock"],
|
projects: [
|
||||||
|
{
|
||||||
|
name: "me",
|
||||||
|
icon: "ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tlock",
|
||||||
|
icon: "rs",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
name: "github",
|
||||||
|
url: "https://github.com/pihkaal",
|
||||||
|
icon: "github",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "instagram",
|
||||||
|
url: "https://instagram.com/pihkaal",
|
||||||
|
icon: "instagram",
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
29
src/utils/icons.ts
Normal file
29
src/utils/icons.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Icon } from "./types";
|
||||||
|
|
||||||
|
export const ICONS: Record<string, Icon> = {
|
||||||
|
md: {
|
||||||
|
char: " ",
|
||||||
|
color: "#89bafa",
|
||||||
|
},
|
||||||
|
asc: {
|
||||||
|
char: " ",
|
||||||
|
color: "#f9e2af",
|
||||||
|
},
|
||||||
|
ts: {
|
||||||
|
char: " ",
|
||||||
|
color: "#4d86a2",
|
||||||
|
},
|
||||||
|
rs: {
|
||||||
|
char: " ",
|
||||||
|
color: "#be8f78",
|
||||||
|
},
|
||||||
|
instagram: {
|
||||||
|
char: " ",
|
||||||
|
color: "#e1306c",
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
char: " ",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
UNKNOWN: { char: " ", color: "#f599ae" },
|
||||||
|
};
|
||||||
@@ -9,5 +9,36 @@ export type InnerKittyProps<T extends (...args: any[]) => any> = Prettify<
|
|||||||
|
|
||||||
export type RootManifest = {
|
export type RootManifest = {
|
||||||
files: Array<string>;
|
files: Array<string>;
|
||||||
projects: Array<string>;
|
projects: Array<{
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
}>;
|
||||||
|
links: Array<{
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
icon: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Icon = {
|
||||||
|
char: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type File = {
|
||||||
|
name: string;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: "link";
|
||||||
|
url: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
| { type: "file"; repo: string; fileName: string; icon?: string }
|
||||||
|
);
|
||||||
|
|
||||||
|
export type Directory = {
|
||||||
|
name: string;
|
||||||
|
type: "directory";
|
||||||
|
files: Array<File>;
|
||||||
|
opened: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user