From dc03aaeb4f64e6a7b3397c0d327dc90b116d7b17 Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Fri, 9 Feb 2024 23:51:47 +0100 Subject: [PATCH] refactor(music-player): using react-dom-curse --- package.json | 1 + pnpm-lock.yaml | 49 +++++++++++- src/App.tsx | 16 ++-- src/components/Kitty.tsx | 15 ++++ src/components/MusicPlayer.tsx | 108 ++++++++++++------------- src/components/Terminal.tsx | 64 --------------- src/context/TerminalContext.tsx | 16 ---- src/utils/terminal/cell.ts | 9 --- src/utils/terminal/element.ts | 7 -- src/utils/terminal/elements/box.ts | 38 --------- src/utils/terminal/renderer.tsx | 124 ----------------------------- src/utils/{terminal => }/theme.ts | 0 12 files changed, 120 insertions(+), 327 deletions(-) create mode 100644 src/components/Kitty.tsx delete mode 100644 src/components/Terminal.tsx delete mode 100644 src/context/TerminalContext.tsx delete mode 100644 src/utils/terminal/cell.ts delete mode 100644 src/utils/terminal/element.ts delete mode 100644 src/utils/terminal/elements/box.ts delete mode 100644 src/utils/terminal/renderer.tsx rename src/utils/{terminal => }/theme.ts (100%) diff --git a/package.json b/package.json index bd58c08..ddbdd97 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "axios": "^1.6.7", "react": "18.2.0", "react-dom": "18.2.0", + "react-dom-curse": "github:pihkaal/react-dom-curse", "react-router-dom": "^6.21.3", "tailwind-merge": "^2.2.1", "vite-tsconfig-paths": "^4.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37d1bad..d5f3a5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + react-dom-curse: + specifier: github:pihkaal/react-dom-curse + version: git@github.com+pihkaal/react-dom-curse/99f542457ffbe8dbba08261c8b423a7546fea56e(@types/react@18.2.47) react-router-dom: specifier: ^6.21.3 version: 6.21.3(react-dom@18.2.0)(react@18.2.0) @@ -672,7 +675,6 @@ packages: /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} - dev: true /@types/react-dom@18.2.18: resolution: {integrity: sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==} @@ -686,11 +688,9 @@ packages: '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.8 csstype: 3.1.3 - dev: true /@types/scheduler@0.16.8: resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - dev: true /@types/semver@7.5.6: resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} @@ -1177,7 +1177,6 @@ packages: /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - dev: true /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -3260,6 +3259,14 @@ packages: punycode: 2.3.1 dev: true + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true @@ -3412,3 +3419,37 @@ packages: /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false + + /zustand@4.5.0(@types/react@18.2.47)(react@18.2.0): + resolution: {integrity: sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': 18.2.47 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + + git@github.com+pihkaal/react-dom-curse/99f542457ffbe8dbba08261c8b423a7546fea56e(@types/react@18.2.47): + resolution: {commit: 99f542457ffbe8dbba08261c8b423a7546fea56e, repo: git@github.com:pihkaal/react-dom-curse.git, type: git} + id: git@github.com+pihkaal/react-dom-curse/99f542457ffbe8dbba08261c8b423a7546fea56e + name: react-dom-curse + version: 0.0.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + zustand: 4.5.0(@types/react@18.2.47)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + dev: false diff --git a/src/App.tsx b/src/App.tsx index 850ca6b..fecae5c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import { BrowserRouter } from "react-router-dom"; import { MusicPlayer } from "./components/MusicPlayer"; import { MusicVisualizer } from "./components/MusicVisualizer"; import { Nvim } from "./components/Nvim/Nvim"; -import { Terminal } from "./components/Terminal"; +import { Kitty } from "./components/Kitty"; import { AppContextProvider } from "./context/AppContext"; import { Waybar } from "./components/Waybar/Waybar"; @@ -17,23 +17,19 @@ function App() { > - - - + -
- +
+ - + - - - +
diff --git a/src/components/Kitty.tsx b/src/components/Kitty.tsx new file mode 100644 index 0000000..85d7f81 --- /dev/null +++ b/src/components/Kitty.tsx @@ -0,0 +1,15 @@ +import { type ReactNode } from "react"; +import clsx from "clsx"; +import { Terminal } from "react-dom-curse"; + +export const Kitty = (props: { children?: ReactNode; className?: string }) => ( +
+ {props.children} +
+); diff --git a/src/components/MusicPlayer.tsx b/src/components/MusicPlayer.tsx index b14f132..1f085f4 100644 --- a/src/components/MusicPlayer.tsx +++ b/src/components/MusicPlayer.tsx @@ -1,20 +1,6 @@ -import { useTerminal } from "~/context/TerminalContext"; -import { TerminalRenderer } from "~/utils/terminal/renderer"; -import { TerminalBoxElement } from "~/utils/terminal/elements/box"; import { useEffect, useState } from "react"; - -const theme = { - black: "#45475a", - red: "#f38ba8", - green: "#a6e3a1", - yellow: "#f9e2af", - blue: "#89bafa", - magenta: "#f5c2e7", - cyan: "#94e2d5", - white: "#bac2de", - grey: "#585B70", - lightGrey: "#a6adc8", -}; +import { Bar, Frame, Group, Terminal, Text, useTerminal } from "react-dom-curse"; +import { theme } from "~/utils/theme"; const formatDurationMSS = (duration: number) => { const minutes = Math.floor(duration / 60); @@ -29,8 +15,7 @@ export const MusicPlayer = (props: { album: string; duration: number; }) => { - const { cols } = useTerminal(); - const canvas = new TerminalRenderer(cols, 5); + const { terminal } = useTerminal(); const [played, setPlayed] = useState(0); useEffect(() => { @@ -41,45 +26,58 @@ export const MusicPlayer = (props: { return () => clearInterval(interval); }, [setPlayed, props.duration]); - canvas.writeElement( - new TerminalBoxElement(canvas.width, canvas.height), - 0, - 0, - ); - - canvas.write(1, 0, "Playback".substring(0, Math.min(8, canvas.width - 2)), { - foreground: theme.magenta, - }); - - const inner = new TerminalRenderer(canvas.width - 2, canvas.height - 2); - // Title and Artist - inner.write(2, 0, `${props.title} · ${props.artist}`, { - foreground: theme.cyan, - fontWeight: 700, - }); - inner.apply(0, 0, { - char: "\udb81\udc0a", - foreground: theme.cyan, - fontWeight: 800, - }); - - // Album - inner.write(0, 1, props.album, { foreground: theme.yellow }); - - // Bar - inner.write(0, 2, " ".repeat(inner.width), { - foreground: theme.green, - background: "#55576d", - }); - inner.write(0, 2, " ".repeat((inner.width * played) / props.duration), { - foreground: "#55576d", - background: theme.green, - }); const time = `${formatDurationMSS(played)}/${formatDurationMSS( props.duration, )}`; - inner.write(inner.width / 2 - time.length / 2, 2, time, { fontWeight: 800 }); - canvas.writeElement(inner, 1, 1); - return

{canvas.render()}

; + return ( + + + + {props.title} · {props.artist} + + + {props.album} + + + + + + + {time} + + + + + + Playback + + + ); }; diff --git a/src/components/Terminal.tsx b/src/components/Terminal.tsx deleted file mode 100644 index 17d18d8..0000000 --- a/src/components/Terminal.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useRef, useState, useEffect, type ReactNode } from "react"; -import clsx from "clsx"; -import { TerminalContextProvider } from "~/context/TerminalContext"; - -export const Terminal = (props: { - children?: ReactNode; - className?: string; -}) => { - const terminalRef = useRef(null); - - const [size, setSize] = useState<{ cols: number; rows: number }>(); - - useEffect(() => { - const precision = 300; - - const calculateSize = () => { - if (!terminalRef.current) return; - - const node = document.createElement("span"); - node.style.color = "transparent"; - node.style.position = "absolute"; - node.textContent = "A".repeat(precision); - - terminalRef.current.appendChild(node); - - setSize({ - cols: Math.floor( - (terminalRef.current.offsetWidth - 4) / - (node.offsetWidth / precision), - ), - rows: Math.floor( - (terminalRef.current.offsetHeight - 4) / node.offsetHeight, - ), - }); - - node.remove(); - }; - - calculateSize(); - - setTimeout(() => calculateSize(), 1); - - window.addEventListener("resize", calculateSize); - - return () => { - window.removeEventListener("resize", calculateSize); - }; - }, []); - - return ( - -
- {size && props.children} -
-
- ); -}; diff --git a/src/context/TerminalContext.tsx b/src/context/TerminalContext.tsx deleted file mode 100644 index 9dd44b5..0000000 --- a/src/context/TerminalContext.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable react-refresh/only-export-components */ -import { createContext, useContext } from "react"; - -const TerminalContext = createContext< - { cols: number; rows: number } | undefined ->(undefined); - -export const TerminalContextProvider = TerminalContext.Provider; - -export const useTerminal = () => { - const context = useContext(TerminalContext); - if (!context) - throw new Error("useTerminal must be used inside a Terminal component"); - - return context; -}; diff --git a/src/utils/terminal/cell.ts b/src/utils/terminal/cell.ts deleted file mode 100644 index f2984cc..0000000 --- a/src/utils/terminal/cell.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type Cell = { - char: string; -} & CellStyle; - -export type CellStyle = Partial<{ - foreground: string; - background: string; - fontWeight: number; -}>; diff --git a/src/utils/terminal/element.ts b/src/utils/terminal/element.ts deleted file mode 100644 index 2768d78..0000000 --- a/src/utils/terminal/element.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type Cell } from "./cell"; - -export interface TerminalElement { - readonly data: Array>; - readonly width: number; - readonly height: number; -} diff --git a/src/utils/terminal/elements/box.ts b/src/utils/terminal/elements/box.ts deleted file mode 100644 index 4c3cd0d..0000000 --- a/src/utils/terminal/elements/box.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { type Cell, type CellStyle } from "../cell"; -import { TerminalRenderer } from "../renderer"; -import { type TerminalElement } from "../element"; - -export class TerminalBoxElement implements TerminalElement { - public readonly data: Array>; - - constructor( - public readonly width: number, - public readonly height: number, - style: CellStyle = {}, - ) { - const canvas = new TerminalRenderer(width, height, style); - - if (width == 1 && height > 1) { - for (let y = 0; y < height - 1; y++) { - canvas.write(0, y, "│"); - } - } else if (height == 1 && width > 1) { - canvas.write(0, 0, "─".repeat(width - 2)); - } else { - canvas.write(0, 0, "┌"); - canvas.write(width - 1, 0, "┐"); - canvas.write(0, height - 1, "└"); - canvas.write(width - 1, height - 1, "┘"); - - canvas.write(1, 0, "─".repeat(width - 2)); - canvas.write(1, height - 1, "─".repeat(width - 2)); - - for (let y = 1; y < height - 1; y++) { - canvas.write(0, y, "│"); - canvas.write(width - 1, y, "│"); - } - } - - this.data = canvas.data; - } -} diff --git a/src/utils/terminal/renderer.tsx b/src/utils/terminal/renderer.tsx deleted file mode 100644 index 4917ce4..0000000 --- a/src/utils/terminal/renderer.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { type ReactNode } from "react"; -import { floorAll } from "../math"; -import { type CellStyle, type Cell } from "./cell"; -import { type TerminalElement } from "./element"; - -export class TerminalRenderer implements TerminalElement { - public readonly data: Array>; - - constructor( - public readonly width: number, - public readonly height: number, - public readonly defaultStyle: CellStyle = {}, - ) { - [this.width, this.height] = floorAll(this.width, this.height); - - this.data = new Array(this.height).fill(0).map(() => - new Array(this.width).fill({ - char: " ", - ...defaultStyle, - }), - ); - } - - apply(x: number, y: number, cell: Partial): void { - [x, y] = floorAll(x, y); - - if (x < 0 || x >= this.width || y < 0 || y >= this.height) return; - - this.data[y][x] = { - ...this.data[y][x], - ...cell, - }; - } - - write(x: number, y: number, text: string, style: CellStyle = {}): void { - [x, y] = floorAll(x, y); - - for (let i = 0; i < text.length; i++) { - this.apply(x + i, y, { - char: text[i], - ...style, - }); - } - } - - writeFilter( - x: number, - y: number, - text: string, - filter: (cell: Cell) => Cell, - ): void { - [x, y] = floorAll(x, y); - - for (let i = 0; i < text.length; i++) { - this.apply(x + i, y, { - ...filter(this.data[y][x + i]), - char: text[i], - }); - } - } - - writeElement(canvas: TerminalElement, dx: number, dy: number): void { - [dx, dy] = floorAll(dx, dy); - - for (let y = 0; y < canvas.height; y++) { - for (let x = 0; x < canvas.width; x++) { - this.apply(dx + x, dy + y, canvas.data[y][x]); - } - } - } - - subCanvas( - x: number, - y: number, - width: number, - height: number, - ): TerminalRenderer { - [x, y, width, height] = floorAll(x, y, width, height); - - const canvas = new TerminalRenderer(width, height); - for (let cy = 0; cy < height; cy++) { - for (let cx = 0; cx < width; cx++) { - canvas.apply(cx, cy, this.data[y + cy][x + cx]); - } - } - - return canvas; - } - - render(): Array { - const nodes: Array = []; - - for (let y = 0; y < this.height; y++) { - for (let x = 0; x < this.width; x++) { - const cell = this.data[y][x]; - /* - const span = document.createElement("span"); - span.innerHTML = cell.char; - span.style.color = cell.foreground ?? "unset"; - span.style.background = cell.background ?? "unset"; - span.style.fontWeight = String(cell.fontWeight ?? "unset"); - - target.appendChild(span); - */ - nodes.push( - - {cell.char} - , - ); - } - - nodes.push(
); - } - - return nodes; - } -} diff --git a/src/utils/terminal/theme.ts b/src/utils/theme.ts similarity index 100% rename from src/utils/terminal/theme.ts rename to src/utils/theme.ts