From 5644c9c4e91488113d91960760becf4de66cb38c Mon Sep 17 00:00:00 2001 From: wallpants <47203170+wallpants@users.noreply.github.com> Date: Sat, 30 May 2026 13:17:32 -0600 Subject: [PATCH 1/4] fix(dependencies): update dependencies & replace prettier with oxfmt --- .eslintrc.cjs | 54 +- .oxfmtrc.json | 21 + .prettierignore | 2 - .prettierrc.json | 11 - README.md | 24 +- app/github-preview.ts | 503 +++--- app/global.d.ts | 1 + app/index.ts | 74 +- app/nvim/on-before-exit.ts | 26 +- app/nvim/on-config-update.ts | 8 +- app/nvim/on-content-change.ts | 100 +- app/nvim/on-cursor-move.ts | 26 +- app/sample.test.ts | 2 +- app/server/http.ts | 196 ++- app/server/index.ts | 16 +- app/server/websocket.ts | 158 +- app/tsconfig.json | 6 +- app/types.ts | 294 ++-- app/web/.eslintrc.cjs | 34 +- app/web/app.tsx | 21 +- app/web/components/explorer/entry.tsx | 157 +- app/web/components/explorer/footer.tsx | 46 +- app/web/components/explorer/header.tsx | 104 +- app/web/components/explorer/index.tsx | 114 +- .../explorer/settings/collapsed-option.tsx | 188 +-- .../explorer/settings/collapsed.tsx | 100 +- .../components/explorer/settings/index.tsx | 162 +- .../components/explorer/settings/option.tsx | 236 +-- .../explorer/settings/options/cursorline.tsx | 84 +- .../settings/options/details-tags.tsx | 50 +- .../explorer/settings/options/scroll.tsx | 62 +- .../explorer/settings/options/single-file.tsx | 48 +- .../explorer/settings/options/theme.tsx | 95 +- .../components/explorer/settings/select.tsx | 110 +- app/web/components/filling-circle.tsx | 60 +- app/web/components/icon-button.tsx | 60 +- app/web/components/icons/check.tsx | 30 +- app/web/components/icons/chevron-right.tsx | 28 +- app/web/components/icons/close.tsx | 32 +- app/web/components/icons/copy.tsx | 32 +- app/web/components/icons/cursorline.tsx | 32 +- app/web/components/icons/dir.tsx | 26 +- app/web/components/icons/file.tsx | 30 +- app/web/components/icons/fold-vertical.tsx | 44 +- app/web/components/icons/moon.tsx | 26 +- app/web/components/icons/mouse.tsx | 32 +- app/web/components/icons/open-dir.tsx | 26 +- app/web/components/icons/panel-close.tsx | 30 +- app/web/components/icons/panel-open.tsx | 30 +- app/web/components/icons/pin-off.tsx | 36 +- app/web/components/icons/pin.tsx | 32 +- app/web/components/icons/settings.tsx | 32 +- app/web/components/icons/sun.tsx | 42 +- app/web/components/icons/system.tsx | 26 +- app/web/components/icons/unfold-vertical.tsx | 44 +- app/web/components/markdown/breadcrumbs.tsx | 116 +- app/web/components/markdown/cursor-line.tsx | 92 +- app/web/components/markdown/explorer.tsx | 104 +- app/web/components/markdown/index.tsx | 282 ++-- app/web/components/markdown/line-numbers.tsx | 58 +- app/web/components/markdown/mermaid.ts | 145 +- app/web/components/markdown/post-process.ts | 264 +-- app/web/components/markdown/scroll.ts | 344 ++-- app/web/components/theme-provider.tsx | 92 +- app/web/components/toggle.tsx | 32 +- .../components/websocket-provider/context.ts | 42 +- .../websocket-provider/provider.tsx | 208 +-- app/web/index.html | 32 +- app/web/index.tsx | 6 +- app/web/postcss.config.cjs | 10 +- app/web/static/index.css | 76 +- app/web/static/preflight.css | 184 +-- app/web/tailwind.config.mjs | 120 +- app/web/use-on-document-click.ts | 40 +- app/web/utils.ts | 90 +- app/web/vite.config.ts | 34 +- bun.lock | 1452 ++++++++--------- package.json | 106 +- 78 files changed, 3801 insertions(+), 3991 deletions(-) create mode 100644 .oxfmtrc.json delete mode 100644 .prettierignore delete mode 100644 .prettierrc.json create mode 100644 app/global.d.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f93cba77..c788e184 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,30 +1,30 @@ /** @type {import('eslint').ESLint.Options} */ module.exports = { - root: true, - ignorePatterns: [".eslintrc.cjs"], - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: [__dirname + "/app/tsconfig.json"], - }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/strict-type-checked", - "plugin:@typescript-eslint/stylistic-type-checked", - ], - rules: { - "@typescript-eslint/no-unused-expressions": "off", - "@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }], - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/consistent-type-definitions": ["error", "type"], - "@typescript-eslint/no-unused-vars": [ - "error", - { argsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }, - ], - "@typescript-eslint/consistent-type-imports": [ - "warn", - { prefer: "type-imports", fixStyle: "inline-type-imports" }, - ], - }, + root: true, + ignorePatterns: [".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + project: [__dirname + "/app/tsconfig.json"], + }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/strict-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", + ], + rules: { + "@typescript-eslint/no-unused-expressions": "off", + "@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }], + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/consistent-type-definitions": ["error", "type"], + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }, + ], + "@typescript-eslint/consistent-type-imports": [ + "warn", + { prefer: "type-imports", fixStyle: "inline-type-imports" }, + ], + }, }; diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000..0b0784a7 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,21 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [".github", "tailwind.css"], + "printWidth": 100, + "tabWidth": 3, + "sortImports": { + "newlinesBetween": false + }, + "sortTailwindcss": { + "stylesheet": "./app/web/static/tailwind.css", + "functions": ["cn"] + }, + "overrides": [ + { + "files": ["*.json", "*.md"], + "options": { + "tabWidth": 2 + } + } + ] +} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 26b57c6a..00000000 --- a/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -tailwind.css -.github/dependabot.yml diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 081f39bd..00000000 --- a/.prettierrc.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "tabWidth": 4, - "printWidth": 100, - "plugins": ["prettier-plugin-organize-imports"], - "overrides": [ - { - "files": ["*.json", "*.md", "*.yml"], - "options": { "tabWidth": 2 } - } - ] -} diff --git a/README.md b/README.md index 61887b30..9ddea764 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,21 @@ https://github.com/wallpants/github-preview.nvim/assets/47203170/e332a671-0ee4-4 ## ✨ Features -* [💻 Linux / macOS / WSL](/docs/FEATURES.md#-linux--macos--wsl) -* [🔴 LIVE updates](/docs/FEATURES.md#-live-updates) -* [♻️ Synced Scrolling](/docs/FEATURES.md#%EF%B8%8F-synced-scrolling) -* [🌈 Dark & Light modes](/docs/FEATURES.md#-dark--light-modes) -* [🖍️ Cursorline in Preview](/docs/FEATURES.md#%EF%B8%8F-cursorline-in-preview) -* [📹 Video Support](/docs/FEATURES.md#-video-support) -* [🏞️ Local Image Support](/docs/FEATURES.md#%EF%B8%8F-local-image-support) -* [🧜 Mermaid Support](/docs/FEATURES.md#-mermaid-support) -* [📌 Single-file mode](/docs/FEATURES.md#-single-file-mode) -* [📂 Repository mode](/docs/FEATURES.md#-repository-mode) +- [💻 Linux / macOS / WSL](/docs/FEATURES.md#-linux--macos--wsl) +- [🔴 LIVE updates](/docs/FEATURES.md#-live-updates) +- [♻️ Synced Scrolling](/docs/FEATURES.md#%EF%B8%8F-synced-scrolling) +- [🌈 Dark & Light modes](/docs/FEATURES.md#-dark--light-modes) +- [🖍️ Cursorline in Preview](/docs/FEATURES.md#%EF%B8%8F-cursorline-in-preview) +- [📹 Video Support](/docs/FEATURES.md#-video-support) +- [🏞️ Local Image Support](/docs/FEATURES.md#%EF%B8%8F-local-image-support) +- [🧜 Mermaid Support](/docs/FEATURES.md#-mermaid-support) +- [📌 Single-file mode](/docs/FEATURES.md#-single-file-mode) +- [📂 Repository mode](/docs/FEATURES.md#-repository-mode) ## ✅ Requirements -* [x] [Bun](https://bun.sh) -* [x] [Neovim](https://neovim.io) +- [x] [Bun](https://bun.sh) +- [x] [Neovim](https://neovim.io) ## 📦 Installation diff --git a/app/github-preview.ts b/app/github-preview.ts index ddbf451f..35985b28 100644 --- a/app/github-preview.ts +++ b/app/github-preview.ts @@ -1,300 +1,297 @@ +import { existsSync } from "node:fs"; +import { basename, dirname, normalize, resolve } from "node:path"; import { type Server } from "bun"; import { NVIM_LOG_LEVELS, attach, type LogLevel, type Nvim } from "bunvim"; import { globby } from "globby"; import { isBinaryFile } from "isbinaryfile"; -import { existsSync } from "node:fs"; -import { basename, dirname, normalize, resolve } from "node:path"; import { parse } from "valibot"; import { startServer } from "./server"; import { UNALIVE_URL } from "./server/http"; import { EDITOR_EVENTS_TOPIC } from "./server/websocket"; import { - PluginPropsSchema, - type Config, - type ContentChange, - type CustomEvents, - type PluginProps, - type UpdateConfigAction, - type WsServerMessage, + PluginPropsSchema, + type Config, + type ContentChange, + type CustomEvents, + type PluginProps, + type UpdateConfigAction, + type WsServerMessage, } from "./types"; const ENV = { - NVIM: process.env["NVIM"], - LOG_LEVEL: process.env["LOG_LEVEL"] as LogLevel | undefined, - DEV: Boolean(process.env["IS_DEV"]), + NVIM: process.env["NVIM"], + LOG_LEVEL: process.env["LOG_LEVEL"] as LogLevel | undefined, + DEV: Boolean(process.env["IS_DEV"]), }; export class GithubPreview { - nvim: Nvim; - /** - * Neovim autocommand group id, - * under which all autocommands are to be registered - */ - augroupId: number; - /** - * repo root absolute path. - * - * Includes trailing slash - */ - root: string; - /** - * currentPath: relative to root - */ - currentPath: string; - config: { - dotfiles: Config; - overrides: Config; - }; - repoName: string; - server: Server; - cursorLine: null | number = null; - lines: ContentChange["lines"] = []; + nvim: Nvim; + /** + * Neovim autocommand group id, + * under which all autocommands are to be registered + */ + augroupId: number; + /** + * repo root absolute path. + * + * Includes trailing slash + */ + root: string; + /** + * currentPath: relative to root + */ + currentPath: string; + config: { + dotfiles: Config; + overrides: Config; + }; + repoName: string; + server: Server; + cursorLine: null | number = null; + lines: ContentChange["lines"] = []; - private constructor(nvim: Nvim, augroupId: number, repoName: string, props: PluginProps) { - this.nvim = nvim as Nvim; - this.augroupId = augroupId; - this.repoName = repoName; - this.config = { - dotfiles: Object.assign({}, props.config), - overrides: Object.assign({}, props.config), - }; - this.currentPath = props.init.path.slice(props.init.root.length); - this.root = props.init.root; + private constructor(nvim: Nvim, augroupId: number, repoName: string, props: PluginProps) { + this.nvim = nvim as Nvim; + this.augroupId = augroupId; + this.repoName = repoName; + this.config = { + dotfiles: Object.assign({}, props.config), + overrides: Object.assign({}, props.config), + }; + this.currentPath = props.init.path.slice(props.init.root.length); + this.root = props.init.root; - // must be called at end of constructor, - // because it requires the values set above - this.server = startServer(this); - } + // must be called at end of constructor, + // because it requires the values set above + this.server = startServer(this); + } - static async start() { - // we use a static method to initialize GithubPreview instead - // of using the constructor, because async constructors are not a thing - if (!ENV.NVIM) throw Error("socket missing"); + static async start() { + // we use a static method to initialize GithubPreview instead + // of using the constructor, because async constructors are not a thing + if (!ENV.NVIM) throw Error("socket missing"); - const nvim = await attach({ - socket: ENV.NVIM, - client: { name: "github-preview" }, - logging: { level: ENV.LOG_LEVEL }, - }); + const nvim = await attach({ + socket: ENV.NVIM, + client: { name: "github-preview" }, + logging: { level: ENV.LOG_LEVEL }, + }); - const props = (await nvim.call("nvim_get_var", ["github_preview_props"])) as PluginProps; - if (ENV.DEV) parse(PluginPropsSchema, props); + const props = (await nvim.call("nvim_get_var", ["github_preview_props"])) as PluginProps; + if (ENV.DEV) parse(PluginPropsSchema, props); - try { - // try to unalive already running instances of github-preview - await fetch(`http://${props.config.host}:${props.config.port}${UNALIVE_URL}`); - // eslint-disable-next-line - } catch (err) {} + try { + // try to unalive already running instances of github-preview + await fetch(`http://${props.config.host}:${props.config.port}${UNALIVE_URL}`); + // eslint-disable-next-line + } catch (err) {} - const repoName = await GithubPreview.getRepoName({ root: props.init.root }); - const augroupId = await nvim.call("nvim_create_augroup", [ - "github-preview", - { clear: true }, - ]); + const repoName = await GithubPreview.getRepoName({ root: props.init.root }); + const augroupId = await nvim.call("nvim_create_augroup", ["github-preview", { clear: true }]); - return new GithubPreview(nvim, augroupId, repoName, props); - } + return new GithubPreview(nvim, augroupId, repoName, props); + } - static async getRepoName({ root }: { root: string }): Promise { - let repoName = "root"; - const gitConfig = Bun.file(resolve(root, ".git/config")); + static async getRepoName({ root }: { root: string }): Promise { + let repoName = "root"; + const gitConfig = Bun.file(resolve(root, ".git/config")); - if (!(await gitConfig.exists())) { - return repoName; - } + if (!(await gitConfig.exists())) { + return repoName; + } - const lines = (await gitConfig.text()).split("\n"); + const lines = (await gitConfig.text()).split("\n"); - for (let i = 0; i < lines.length; i += 1) { - const line = lines[i]; - if (line === '[remote "origin"]') { - // nextLine = git@github.com:gualcasas/github-preview.nvim.git - const nextLine = lines[i + 1]; - const repo = nextLine?.split(":")[1]?.slice(0, -4).split("/")[1]; - if (repo) repoName = repo; - } - } - return repoName; - } + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + if (line === '[remote "origin"]') { + // nextLine = git@github.com:gualcasas/github-preview.nvim.git + const nextLine = lines[i + 1]; + const repo = nextLine?.split(":")[1]?.slice(0, -4).split("/")[1]; + if (repo) repoName = repo; + } + } + return repoName; + } - async getEntries(path: string): Promise { - const currentDir = path.endsWith("/") ? path : dirname(path) + "/"; - const paths = await globby(currentDir + "*", { - cwd: this.root, - dot: true, - ignore: [".git"], - gitignore: true, - onlyFiles: false, - markDirectories: true, - }); + async getEntries(path: string): Promise { + const currentDir = path.endsWith("/") ? path : dirname(path) + "/"; + const paths = await globby(currentDir + "*", { + cwd: this.root, + dot: true, + ignore: [".git"], + gitignore: true, + onlyFiles: false, + markDirectories: true, + }); - return paths.sort((a, b) => { - // sort dirs first and then alphabetically - if (a.endsWith("/") && !b.endsWith("/")) return -1; - if (b.endsWith("/") && !a.endsWith("/")) return 1; - if (a > b) return 1; - if (a < b) return -1; - return 0; - }); - } + return paths.sort((a, b) => { + // sort dirs first and then alphabetically + if (a.endsWith("/") && !b.endsWith("/")) return -1; + if (b.endsWith("/") && !a.endsWith("/")) return 1; + if (a > b) return 1; + if (a < b) return -1; + return 0; + }); + } - /** - * Updates this.currentPath & this.lines based on "path" provided. - * returns entries if path is a dir - */ - async setCurrPath(path: string): Promise { - // do not return any entries outside of repo root - const normalized = normalize(this.root + path); - if (normalized.length < this.root.length) return; + /** + * Updates this.currentPath & this.lines based on "path" provided. + * returns entries if path is a dir + */ + async setCurrPath(path: string): Promise { + // do not return any entries outside of repo root + const normalized = normalize(this.root + path); + if (normalized.length < this.root.length) return; - this.currentPath = path; + this.currentPath = path; - // check open buffers and get lines from there before falling back to filesystem. - const bufs = await this.nvim.call("nvim_list_bufs", []); - for (const buf of bufs) { - const name = await this.nvim.call("nvim_buf_get_name", [buf]); - if (name === this.root + this.currentPath) { - const isLoaded = await this.nvim.call("nvim_buf_is_loaded", [buf]); - if (isLoaded) { - this.lines = await this.nvim.call("nvim_buf_get_lines", [buf, 0, -1, true]); - return; - } + // check open buffers and get lines from there before falling back to filesystem. + const bufs = await this.nvim.call("nvim_list_bufs", []); + for (const buf of bufs) { + const name = await this.nvim.call("nvim_buf_get_name", [buf]); + if (name === this.root + this.currentPath) { + const isLoaded = await this.nvim.call("nvim_buf_is_loaded", [buf]); + if (isLoaded) { + this.lines = await this.nvim.call("nvim_buf_get_lines", [buf, 0, -1, true]); + return; } - } + } + } - if (!existsSync(this.root + this.currentPath)) { - this.lines = [`Path: ${this.currentPath}`, "", "ERROR: path not found"]; - return; - } + if (!existsSync(this.root + this.currentPath)) { + this.lines = [`Path: ${this.currentPath}`, "", "ERROR: path not found"]; + return; + } - const isDir = (this.root + this.currentPath).endsWith("/"); - if (isDir) { - const entries = await this.getEntries(this.currentPath); - // search for readme.md - const readmePath = entries.find((e) => basename(e).toLowerCase() === "readme.md"); - if (readmePath) this.currentPath = readmePath; - else { - this.lines = []; - return entries; - } - } + const isDir = (this.root + this.currentPath).endsWith("/"); + if (isDir) { + const entries = await this.getEntries(this.currentPath); + // search for readme.md + const readmePath = entries.find((e) => basename(e).toLowerCase() === "readme.md"); + if (readmePath) this.currentPath = readmePath; + else { + this.lines = []; + return entries; + } + } - if (await isBinaryFile(this.root + this.currentPath)) { - this.lines = [`File: ${this.currentPath}`, "", "ERROR: binary files not yet supported"]; - return; - } + if (await isBinaryFile(this.root + this.currentPath)) { + this.lines = [`File: ${this.currentPath}`, "", "ERROR: binary files not yet supported"]; + return; + } - const file = Bun.file(this.root + this.currentPath); - // limit file size or browser freezes when trying to apply syntax highlight - if (file.size > 500_000) { - this.lines = [`File: ${this.currentPath}`, "", "ERROR: file too large (>500kB)"]; - return; - } + const file = Bun.file(this.root + this.currentPath); + // limit file size or browser freezes when trying to apply syntax highlight + if (file.size > 500_000) { + this.lines = [`File: ${this.currentPath}`, "", "ERROR: file too large (>500kB)"]; + return; + } - const fileContent = await file.text(); - this.lines = fileContent.split("\n"); - return; - } + const fileContent = await file.text(); + this.lines = fileContent.split("\n"); + return; + } - wsSend(m: WsServerMessage) { - this.nvim.logger?.verbose({ OUTGOING_WEBSOCKET: m }); - this.server.publish(EDITOR_EVENTS_TOPIC, JSON.stringify(m)); - } + wsSend(m: WsServerMessage) { + this.nvim.logger?.verbose({ OUTGOING_WEBSOCKET: m }); + this.server.publish(EDITOR_EVENTS_TOPIC, JSON.stringify(m)); + } - async goodbye() { - this.wsSend({ type: "goodbye" }); - await this.nvim.call("nvim_del_augroup_by_id", [this.augroupId]); - await this.nvim.call("nvim_notify", ["github-preview: goodbye", NVIM_LOG_LEVELS.INFO, {}]); - } + async goodbye() { + this.wsSend({ type: "goodbye" }); + await this.nvim.call("nvim_del_augroup_by_id", [this.augroupId]); + await this.nvim.call("nvim_notify", ["github-preview: goodbye", NVIM_LOG_LEVELS.INFO, {}]); + } - async updateConfig([action, value]: UpdateConfigAction) { - let update: Partial = {}; + async updateConfig([action, value]: UpdateConfigAction) { + let update: Partial = {}; - const updateSingleFile = async (dotfileValue: boolean, newValue: boolean) => { - // we need a function to validate single-file mode config updates - // because single-file mode cannot be disabled if plugin launched - // in single-file mode - if (dotfileValue && !newValue) { - await this.nvim.call("nvim_notify", [ - "github-preview: if plugin launched in single-file mode, it cannot be changed.", - NVIM_LOG_LEVELS.WARN, - {}, - ]); - } else { - update.single_file = newValue; - } - }; + const updateSingleFile = async (dotfileValue: boolean, newValue: boolean) => { + // we need a function to validate single-file mode config updates + // because single-file mode cannot be disabled if plugin launched + // in single-file mode + if (dotfileValue && !newValue) { + await this.nvim.call("nvim_notify", [ + "github-preview: if plugin launched in single-file mode, it cannot be changed.", + NVIM_LOG_LEVELS.WARN, + {}, + ]); + } else { + update.single_file = newValue; + } + }; - const { overrides, dotfiles } = this.config; + const { overrides, dotfiles } = this.config; - switch (action) { - case "theme_name": - update.theme = { - ...overrides.theme, - name: value, - }; - break; - case "theme_high_contrast": - update.theme = { - ...overrides.theme, - high_contrast: value === "on", - }; - break; - case "clear_overrides": - update = dotfiles; - break; - case "single_file": { - if (value === "toggle") { - await updateSingleFile(dotfiles.single_file, !overrides.single_file); - } else { - await updateSingleFile(dotfiles.single_file, value === "on" ? true : false); - } - break; + switch (action) { + case "theme_name": + update.theme = { + ...overrides.theme, + name: value, + }; + break; + case "theme_high_contrast": + update.theme = { + ...overrides.theme, + high_contrast: value === "on", + }; + break; + case "clear_overrides": + update = dotfiles; + break; + case "single_file": { + if (value === "toggle") { + await updateSingleFile(dotfiles.single_file, !overrides.single_file); + } else { + await updateSingleFile(dotfiles.single_file, value === "on" ? true : false); + } + break; + } + case "details_tags": + if (value === "toggle") { + update.details_tags_open = !overrides.details_tags_open; + } else { + update.details_tags_open = value === "open" ? true : false; + } + break; + case "scroll": + if (value === "toggle") { + update.scroll = { ...overrides.scroll, disable: !overrides.scroll.disable }; + } else { + update.scroll = { ...overrides.scroll, disable: value === "on" ? false : true }; + } + break; + case "scroll.offset": + update.scroll = { ...overrides.scroll, top_offset_pct: value }; + break; + case "cursorline": + if (value === "toggle") { + update.cursor_line = { + ...overrides.cursor_line, + disable: !overrides.cursor_line.disable, + }; + } else { + update.cursor_line = { + ...overrides.cursor_line, + disable: value === "on" ? false : true, + }; } - case "details_tags": - if (value === "toggle") { - update.details_tags_open = !overrides.details_tags_open; - } else { - update.details_tags_open = value === "open" ? true : false; - } - break; - case "scroll": - if (value === "toggle") { - update.scroll = { ...overrides.scroll, disable: !overrides.scroll.disable }; - } else { - update.scroll = { ...overrides.scroll, disable: value === "on" ? false : true }; - } - break; - case "scroll.offset": - update.scroll = { ...overrides.scroll, top_offset_pct: value }; - break; - case "cursorline": - if (value === "toggle") { - update.cursor_line = { - ...overrides.cursor_line, - disable: !overrides.cursor_line.disable, - }; - } else { - update.cursor_line = { - ...overrides.cursor_line, - disable: value === "on" ? false : true, - }; - } - break; - case "cursorline.color": - update.cursor_line = { - ...overrides.cursor_line, - color: value, - }; - break; - case "cursorline.opacity": - update.cursor_line = { - ...overrides.cursor_line, - opacity: value, - }; - break; - } + break; + case "cursorline.color": + update.cursor_line = { + ...overrides.cursor_line, + color: value, + }; + break; + case "cursorline.opacity": + update.cursor_line = { + ...overrides.cursor_line, + opacity: value, + }; + break; + } - Object.assign(overrides, update); - } + Object.assign(overrides, update); + } } diff --git a/app/global.d.ts b/app/global.d.ts new file mode 100644 index 00000000..cbe652db --- /dev/null +++ b/app/global.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/app/index.ts b/app/index.ts index 8d1a487a..c3e08233 100644 --- a/app/index.ts +++ b/app/index.ts @@ -9,57 +9,57 @@ import { type CustomEvents, type WsServerMessage } from "./types.ts"; const app = await GithubPreview.start(); onConfigUpdate(app, async (update) => { - await app.updateConfig(update); - app.wsSend({ type: "update_config", config: app.config }); - return null; + await app.updateConfig(update); + app.wsSend({ type: "update_config", config: app.config }); + return null; }); await onBeforeExit(app, async () => { - await app.goodbye(); - return null; + await app.goodbye(); + return null; }); await onCursorMove( - app, - async ([buffer, path, cursor_line]: CustomEvents["notifications"]["cursor_move"]) => { - const relativePath = relative(app.root, path); - if (!path || (app.config.overrides.single_file && relativePath !== app.currentPath)) return; - app.nvim.logger?.verbose({ - ON_CURSOR_MOVE: { buffer, path: relativePath, cursorLine: cursor_line }, - }); + app, + async ([buffer, path, cursor_line]: CustomEvents["notifications"]["cursor_move"]) => { + const relativePath = relative(app.root, path); + if (!path || (app.config.overrides.single_file && relativePath !== app.currentPath)) return; + app.nvim.logger?.verbose({ + ON_CURSOR_MOVE: { buffer, path: relativePath, cursorLine: cursor_line }, + }); - const message: WsServerMessage = { - type: "cursor_move", - cursorLine: cursor_line, - currentPath: relativePath, - }; + const message: WsServerMessage = { + type: "cursor_move", + cursorLine: cursor_line, + currentPath: relativePath, + }; - if (app.currentPath !== relativePath) { - message.lines = await app.nvim.call("nvim_buf_get_lines", [buffer, 0, -1, true]); - app.lines = message.lines; - } + if (app.currentPath !== relativePath) { + message.lines = await app.nvim.call("nvim_buf_get_lines", [buffer, 0, -1, true]); + app.lines = message.lines; + } - app.cursorLine = message.cursorLine; - app.currentPath = message.currentPath; + app.cursorLine = message.cursorLine; + app.currentPath = message.currentPath; - app.wsSend(message); - }, + app.wsSend(message); + }, ); await onContentChange(app, (lines, path) => { - const relativePath = relative(app.root, path); - if (!path || (app.config.overrides.single_file && relativePath !== app.currentPath)) return; - app.nvim.logger?.verbose({ ON_CONTENT_CHANGE: { lines, path: relativePath } }); + const relativePath = relative(app.root, path); + if (!path || (app.config.overrides.single_file && relativePath !== app.currentPath)) return; + app.nvim.logger?.verbose({ ON_CONTENT_CHANGE: { lines, path: relativePath } }); - const message: WsServerMessage = { - type: "content_change", - currentPath: relativePath, - linesCountChange: app.lines.length !== lines.length, - lines, - }; + const message: WsServerMessage = { + type: "content_change", + currentPath: relativePath, + linesCountChange: app.lines.length !== lines.length, + lines, + }; - app.currentPath = message.currentPath; - app.lines = message.lines; + app.currentPath = message.currentPath; + app.lines = message.lines; - app.wsSend(message); + app.wsSend(message); }); diff --git a/app/nvim/on-before-exit.ts b/app/nvim/on-before-exit.ts index 952885d1..66af3552 100644 --- a/app/nvim/on-before-exit.ts +++ b/app/nvim/on-before-exit.ts @@ -5,20 +5,20 @@ import { type CustomEvents } from "../types.ts"; const REQUEST = "before_exit"; export async function onBeforeExit( - app: GithubPreview, - callback: (args: CustomEvents["requests"][typeof REQUEST]) => Awaitable, + app: GithubPreview, + callback: (args: CustomEvents["requests"][typeof REQUEST]) => Awaitable, ) { - // Request handler - app.nvim.onRequest(REQUEST, callback); + // Request handler + app.nvim.onRequest(REQUEST, callback); - // Create autocmd to make RPCRequest - await app.nvim.call("nvim_create_autocmd", [ - ["VimLeavePre"], - { - group: app.augroupId, - desc: "Notify github-preview", - command: `lua + // Create autocmd to make RPCRequest + await app.nvim.call("nvim_create_autocmd", [ + ["VimLeavePre"], + { + group: app.augroupId, + desc: "Notify github-preview", + command: `lua vim.rpcrequest(${app.nvim.channelId}, "${REQUEST}")`, - }, - ]); + }, + ]); } diff --git a/app/nvim/on-config-update.ts b/app/nvim/on-config-update.ts index 486c7398..fdf141a3 100644 --- a/app/nvim/on-config-update.ts +++ b/app/nvim/on-config-update.ts @@ -5,9 +5,9 @@ import { type CustomEvents } from "../types.ts"; const REQUEST = "config_update"; export function onConfigUpdate( - app: GithubPreview, - callback: (args: CustomEvents["requests"][typeof REQUEST]) => Awaitable, + app: GithubPreview, + callback: (args: CustomEvents["requests"][typeof REQUEST]) => Awaitable, ) { - // Request handler - app.nvim.onRequest(REQUEST, callback); + // Request handler + app.nvim.onRequest(REQUEST, callback); } diff --git a/app/nvim/on-content-change.ts b/app/nvim/on-content-change.ts index 98344d86..b91928ed 100644 --- a/app/nvim/on-content-change.ts +++ b/app/nvim/on-content-change.ts @@ -3,64 +3,64 @@ import { type GithubPreview } from "../github-preview.ts"; const NOTIFICATION = "attach_buffer"; export async function onContentChange( - app: GithubPreview, - callback: (content: string[], path: string) => void, + app: GithubPreview, + callback: (content: string[], path: string) => void, ) { - // We attach buffers to receive notifications on content change - let attachedBuffer: null | number = null; + // We attach buffers to receive notifications on content change + let attachedBuffer: null | number = null; - // Buffer may have been implicitly detached - // https://neovim.io/doc/user/api.html#nvim_buf_detach_event - app.nvim.onNotification("nvim_buf_detach_event", ([buffer]) => { - if (attachedBuffer === buffer) attachedBuffer = null; - }); + // Buffer may have been implicitly detached + // https://neovim.io/doc/user/api.html#nvim_buf_detach_event + app.nvim.onNotification("nvim_buf_detach_event", ([buffer]) => { + if (attachedBuffer === buffer) attachedBuffer = null; + }); - // Notification handler - app.nvim.onNotification(NOTIFICATION, async ([buffer, path]) => { - if (!path) return; + // Notification handler + app.nvim.onNotification(NOTIFICATION, async ([buffer, path]) => { + if (!path) return; - if (attachedBuffer !== buffer) { - if (attachedBuffer !== null) { - await app.nvim.call("nvim_buf_detach", [attachedBuffer]); - attachedBuffer = null; - } - // attach to buffer to receive content change notifications - const attached = await app.nvim.call("nvim_buf_attach", [buffer, true, {}]); - if (attached) attachedBuffer = buffer; - } - }); + if (attachedBuffer !== buffer) { + if (attachedBuffer !== null) { + await app.nvim.call("nvim_buf_detach", [attachedBuffer]); + attachedBuffer = null; + } + // attach to buffer to receive content change notifications + const attached = await app.nvim.call("nvim_buf_attach", [buffer, true, {}]); + if (attached) attachedBuffer = buffer; + } + }); - // Create autocmd to notify us with event "attach_buffer" - await app.nvim.call("nvim_create_autocmd", [ - ["InsertEnter", "TextChanged"], - { - group: app.augroupId, - desc: "Notify github-preview", - command: `lua + // Create autocmd to notify us with event "attach_buffer" + await app.nvim.call("nvim_create_autocmd", [ + ["InsertEnter", "TextChanged"], + { + group: app.augroupId, + desc: "Notify github-preview", + command: `lua local buffer = vim.api.nvim_get_current_buf() local path = vim.api.nvim_buf_get_name(0) vim.rpcnotify(${app.nvim.channelId}, "${NOTIFICATION}", buffer, path)`, - }, - ]); + }, + ]); - // "nvim_buf_lines_event" and "nvim_buf_changedtick_event" events are - // only emitted by neovim if we've attached a buffer. - app.nvim.onNotification( - "nvim_buf_lines_event", - async ([buffer, _changedtick, firstline, lastline, linedata, _more]) => { - const path = await app.nvim.call("nvim_buf_get_name", [buffer]); - const replaceAll = lastline === -1 && firstline === 0; - const deleteCount = lastline - firstline; - const newContent = replaceAll - ? linedata - : app.lines.toSpliced(firstline, deleteCount, ...linedata); - callback(newContent, path); - }, - ); + // "nvim_buf_lines_event" and "nvim_buf_changedtick_event" events are + // only emitted by neovim if we've attached a buffer. + app.nvim.onNotification( + "nvim_buf_lines_event", + async ([buffer, _changedtick, firstline, lastline, linedata, _more]) => { + const path = await app.nvim.call("nvim_buf_get_name", [buffer]); + const replaceAll = lastline === -1 && firstline === 0; + const deleteCount = lastline - firstline; + const newContent = replaceAll + ? linedata + : app.lines.toSpliced(firstline, deleteCount, ...linedata); + callback(newContent, path); + }, + ); - app.nvim.onNotification("nvim_buf_changedtick_event", async ([buffer, _changedtick]) => { - const path = await app.nvim.call("nvim_buf_get_name", [buffer]); - const linedata = await app.nvim.call("nvim_buf_get_lines", [buffer, 0, -1, true]); - callback(linedata, path); - }); + app.nvim.onNotification("nvim_buf_changedtick_event", async ([buffer, _changedtick]) => { + const path = await app.nvim.call("nvim_buf_get_name", [buffer]); + const linedata = await app.nvim.call("nvim_buf_get_lines", [buffer, 0, -1, true]); + callback(linedata, path); + }); } diff --git a/app/nvim/on-cursor-move.ts b/app/nvim/on-cursor-move.ts index 13e7eac4..0068b1af 100644 --- a/app/nvim/on-cursor-move.ts +++ b/app/nvim/on-cursor-move.ts @@ -5,23 +5,23 @@ import { type CustomEvents } from "../types.ts"; const NOTIFICATION = "cursor_move"; export async function onCursorMove( - app: GithubPreview, - callback: (args: CustomEvents["notifications"][typeof NOTIFICATION]) => Awaitable, + app: GithubPreview, + callback: (args: CustomEvents["notifications"][typeof NOTIFICATION]) => Awaitable, ) { - // Notification handler - app.nvim.onNotification(NOTIFICATION, callback); + // Notification handler + app.nvim.onNotification(NOTIFICATION, callback); - // Create autocmd to notify us with event "CursorMove" - await app.nvim.call("nvim_create_autocmd", [ - ["CursorHold", "CursorHoldI"], - { - group: app.augroupId, - desc: "Notify github-preview", - command: `lua + // Create autocmd to notify us with event "CursorMove" + await app.nvim.call("nvim_create_autocmd", [ + ["CursorHold", "CursorHoldI"], + { + group: app.augroupId, + desc: "Notify github-preview", + command: `lua local buffer = vim.api.nvim_get_current_buf() local path = vim.api.nvim_buf_get_name(0) local cursor_line = vim.api.nvim_win_get_cursor(0)[1] vim.rpcnotify(${app.nvim.channelId}, "${NOTIFICATION}", buffer, path, cursor_line)`, - }, - ]); + }, + ]); } diff --git a/app/sample.test.ts b/app/sample.test.ts index 87bf407e..97960fa2 100644 --- a/app/sample.test.ts +++ b/app/sample.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; test("1 + 3", () => { - expect(1 + 3).toBe(4); + expect(1 + 3).toBe(4); }); diff --git a/app/server/http.ts b/app/server/http.ts index c1c15b01..e3cea300 100644 --- a/app/server/http.ts +++ b/app/server/http.ts @@ -11,108 +11,104 @@ const GP_PREFIX = "/__github_preview__"; // bun does not have a loader for css, // that's why we manually run `bun run tailwind:compile` const mockCssLoader: BunPlugin = { - name: "Mock CSS Loader", - setup(builder) { - builder.onLoad({ filter: /\.css$/ }, () => ({ - contents: "", - })); - }, + name: "Mock CSS Loader", + setup(builder) { + builder.onLoad({ filter: /\.css$/ }, () => ({ + contents: "", + })); + }, }; export function httpHandler(app: GithubPreview) { - return async (req: Request, server: Server) => { - const upgradedToWs = server.upgrade(req); - if (upgradedToWs) { - // If client (browser) requested to upgrade connection to websocket - // and we successfully upgraded request - return; - } - - const { pathname } = new URL(req.url); - - app.nvim.logger?.verbose({ HTTP: pathname }); - - if (pathname === UNALIVE_URL) { - // This endpoint is called when starting the service to kill - // github-preview instances started by other nvim instances - await app.goodbye(); - app.nvim.detach(); - process.exit(0); - } - - if (pathname.startsWith(GP_PREFIX)) { - // static files (js, img, css) - const requested = pathname.slice(GP_PREFIX.length); - - if (requested.startsWith("/image/")) { - // images with relative sources - const file = Bun.file(app.root + requested.slice("/image/".length)); - return new Response(file); - } - - if (requested === "/index.tsx") { - const { outputs } = await Bun.build({ - entrypoints: [webRoot + requested], - plugins: [mockCssLoader], - define: { - __HOST__: JSON.stringify(app.config.dotfiles.host), - __PORT__: JSON.stringify(app.config.dotfiles.port), - __IS_DEV__: JSON.stringify(false), - __THEME__: JSON.stringify(app.config.overrides.theme), - }, - }); - - return new Response(outputs[0], { - headers: { "content-type": "text/javascript" }, - }); - } - - if (requested === "/pantsdown.css") { - const pantsdownCss = Bun.resolveSync("pantsdown/styles.css", import.meta.dir); - const file = Bun.file(pantsdownCss); - return new Response(file, { - headers: { "content-type": "text/css" }, - }); - } - - if (requested === "/katex.css") { - const katexCss = Bun.resolveSync("katex/dist/katex.min.css", import.meta.dir); - const file = Bun.file(katexCss); - return new Response(file, { - headers: { "content-type": "text/css" }, - }); - } - - if (requested.startsWith("/fonts/KaTeX_")) { - const fontPath = Bun.resolveSync("katex/dist" + requested, import.meta.dir); - const file = Bun.file(fontPath); - const ext = requested.split(".").pop(); - const contentType = - ext === "woff2" - ? "font/woff2" - : ext === "woff" - ? "font/woff" - : "font/ttf"; - return new Response(file, { - headers: { "content-type": contentType }, - }); - } - - if (requested === "/mermaid.js") { - const mermaid = Bun.resolveSync("mermaid/dist/mermaid.min.js", import.meta.dir); - const file = Bun.file(mermaid); - return new Response(file, { - headers: { "content-type": "text/javascript" }, - }); - } - - const file = Bun.file(webRoot + requested); + return async (req: Request, server: Server) => { + const upgradedToWs = server.upgrade(req); + if (upgradedToWs) { + // If client (browser) requested to upgrade connection to websocket + // and we successfully upgraded request + return; + } + + const { pathname } = new URL(req.url); + + app.nvim.logger?.verbose({ HTTP: pathname }); + + if (pathname === UNALIVE_URL) { + // This endpoint is called when starting the service to kill + // github-preview instances started by other nvim instances + await app.goodbye(); + app.nvim.detach(); + process.exit(0); + } + + if (pathname.startsWith(GP_PREFIX)) { + // static files (js, img, css) + const requested = pathname.slice(GP_PREFIX.length); + + if (requested.startsWith("/image/")) { + // images with relative sources + const file = Bun.file(app.root + requested.slice("/image/".length)); return new Response(file); - } - - // If none of the previous cases match the request, the client (browser) is - // probably making its first request to get index.html - const index = Bun.file(webRoot + "/index.html"); - return new Response(index); - }; + } + + if (requested === "/index.tsx") { + const { outputs } = await Bun.build({ + entrypoints: [webRoot + requested], + plugins: [mockCssLoader], + define: { + __HOST__: JSON.stringify(app.config.dotfiles.host), + __PORT__: JSON.stringify(app.config.dotfiles.port), + __IS_DEV__: JSON.stringify(false), + __THEME__: JSON.stringify(app.config.overrides.theme), + }, + }); + + return new Response(outputs[0], { + headers: { "content-type": "text/javascript" }, + }); + } + + if (requested === "/pantsdown.css") { + const pantsdownCss = Bun.resolveSync("pantsdown/styles.css", import.meta.dir); + const file = Bun.file(pantsdownCss); + return new Response(file, { + headers: { "content-type": "text/css" }, + }); + } + + if (requested === "/katex.css") { + const katexCss = Bun.resolveSync("katex/dist/katex.min.css", import.meta.dir); + const file = Bun.file(katexCss); + return new Response(file, { + headers: { "content-type": "text/css" }, + }); + } + + if (requested.startsWith("/fonts/KaTeX_")) { + const fontPath = Bun.resolveSync("katex/dist" + requested, import.meta.dir); + const file = Bun.file(fontPath); + const ext = requested.split(".").pop(); + const contentType = + ext === "woff2" ? "font/woff2" : ext === "woff" ? "font/woff" : "font/ttf"; + return new Response(file, { + headers: { "content-type": contentType }, + }); + } + + if (requested === "/mermaid.js") { + const mermaid = Bun.resolveSync("mermaid/dist/mermaid.min.js", import.meta.dir); + const file = Bun.file(mermaid); + return new Response(file, { + headers: { "content-type": "text/javascript" }, + }); + } + + const file = Bun.file(webRoot + requested); + return new Response(file); + } + + // If none of the previous cases match the request, the client (browser) is + // probably making its first request to get index.html + const index = Bun.file(webRoot + "/index.html"); + return new Response(index); + }; } diff --git a/app/server/index.ts b/app/server/index.ts index 975d430c..649a6806 100644 --- a/app/server/index.ts +++ b/app/server/index.ts @@ -5,14 +5,14 @@ import { httpHandler } from "./http.ts"; import { websocketHandler } from "./websocket.ts"; export function startServer(app: GithubPreview): Server { - const { port, host } = app.config.overrides; + const { port, host } = app.config.overrides; - const server = Bun.serve({ - port: port, - fetch: httpHandler(app), - websocket: websocketHandler(app), - }); + const server = Bun.serve({ + port: port, + fetch: httpHandler(app), + websocket: websocketHandler(app), + }); - opener(`http://${host}:${port}`); - return server; + opener(`http://${host}:${port}`); + return server; } diff --git a/app/server/websocket.ts b/app/server/websocket.ts index d70e7163..aa72788e 100644 --- a/app/server/websocket.ts +++ b/app/server/websocket.ts @@ -5,96 +5,96 @@ import { type WsBrowserMessage, type WsServerMessage } from "../types.ts"; export const EDITOR_EVENTS_TOPIC = "editor_events"; export function websocketHandler(app: GithubPreview): WebSocketHandler { - return { - open(webSocket) { - // subscribe to a topic so we can publish messages from outside - // webServer.publish(EDITOR_EVENTS_TOPIC, payload); - // - // it doesn't matter who's on the other end of the websocket connection, - // all clients (browsers) share the same app state - webSocket.subscribe(EDITOR_EVENTS_TOPIC); - }, - async message(webSocket, message: string) { - const browserMessage = JSON.parse(message) as WsBrowserMessage; - app.nvim.logger?.verbose({ INCOMING_WEBSOCKET: browserMessage }); + return { + open(webSocket) { + // subscribe to a topic so we can publish messages from outside + // webServer.publish(EDITOR_EVENTS_TOPIC, payload); + // + // it doesn't matter who's on the other end of the websocket connection, + // all clients (browsers) share the same app state + webSocket.subscribe(EDITOR_EVENTS_TOPIC); + }, + async message(webSocket, message: string) { + const browserMessage = JSON.parse(message) as WsBrowserMessage; + app.nvim.logger?.verbose({ INCOMING_WEBSOCKET: browserMessage }); - function wsSend(m: WsServerMessage) { - app.nvim.logger?.verbose({ OUTGOING_WEBSOCKET: m }); - webSocket.send(JSON.stringify(m)); - } + function wsSend(m: WsServerMessage) { + app.nvim.logger?.verbose({ OUTGOING_WEBSOCKET: m }); + webSocket.send(JSON.stringify(m)); + } - let hash: string | undefined; + let hash: string | undefined; - if ("path" in browserMessage) { - // remove hash from browserMessage.path to prevent filesystem - // operations from failing - const [path, messageHash] = browserMessage.path.split("#"); - browserMessage.path = path!; - hash = messageHash; - } + if ("path" in browserMessage) { + // remove hash from browserMessage.path to prevent filesystem + // operations from failing + const [path, messageHash] = browserMessage.path.split("#"); + browserMessage.path = path!; + hash = messageHash; + } - if (browserMessage.type === "init") { - // call "setCurrPath" in case app started in repository mode and no buffer - // was loaded. "setCurrPath" should resolve to readme.md if it exists. - const entries = await app.setCurrPath(app.currentPath); + if (browserMessage.type === "init") { + // call "setCurrPath" in case app started in repository mode and no buffer + // was loaded. "setCurrPath" should resolve to readme.md if it exists. + const entries = await app.setCurrPath(app.currentPath); - const message: WsServerMessage = { - type: "init", - currentPath: app.currentPath, - repoName: app.repoName, - lines: app.lines, - config: app.config, - cursorLine: app.cursorLine, - currentEntries: entries, - }; - wsSend(message); - } + const message: WsServerMessage = { + type: "init", + currentPath: app.currentPath, + repoName: app.repoName, + lines: app.lines, + config: app.config, + cursorLine: app.cursorLine, + currentEntries: entries, + }; + wsSend(message); + } - if (browserMessage.type === "get_entries") { - const message: WsServerMessage = { - type: "entries", - path: browserMessage.path, - entries: await app.getEntries(browserMessage.path), - }; - wsSend(message); - } + if (browserMessage.type === "get_entries") { + const message: WsServerMessage = { + type: "entries", + path: browserMessage.path, + entries: await app.getEntries(browserMessage.path), + }; + wsSend(message); + } - if (browserMessage.type === "get_entry") { - // if single-file mode is enabled, dont respond if browser requests - // an entry other than the currentPath - if (app.config.overrides.single_file && browserMessage.path !== app.currentPath) { - return; - } + if (browserMessage.type === "get_entry") { + // if single-file mode is enabled, dont respond if browser requests + // an entry other than the currentPath + if (app.config.overrides.single_file && browserMessage.path !== app.currentPath) { + return; + } - const isDir = browserMessage.path.endsWith("/"); + const isDir = browserMessage.path.endsWith("/"); - // is same path, keep cursorLine - const cursorLine = - browserMessage.path === app.currentPath && !isDir ? app.cursorLine : null; + // is same path, keep cursorLine + const cursorLine = + browserMessage.path === app.currentPath && !isDir ? app.cursorLine : null; - // list of entries if path is dir, otherwise undefined - const entries = await app.setCurrPath(browserMessage.path); + // list of entries if path is dir, otherwise undefined + const entries = await app.setCurrPath(browserMessage.path); - const message: WsServerMessage = { - type: "entry", - currentPath: app.currentPath, - lines: app.lines, - cursorLine: cursorLine, - hash: hash, - currentEntries: entries, - }; + const message: WsServerMessage = { + type: "entry", + currentPath: app.currentPath, + lines: app.lines, + cursorLine: cursorLine, + hash: hash, + currentEntries: entries, + }; - wsSend(message); - } + wsSend(message); + } - if (browserMessage.type === "update_config") { - await app.updateConfig(browserMessage.action); - const message: WsServerMessage = { - type: "update_config", - config: app.config, - }; - wsSend(message); - } - }, - }; + if (browserMessage.type === "update_config") { + await app.updateConfig(browserMessage.action); + const message: WsServerMessage = { + type: "update_config", + config: app.config, + }; + wsSend(message); + } + }, + }; } diff --git a/app/tsconfig.json b/app/tsconfig.json index 84276a99..71867a0c 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -9,14 +9,11 @@ "noEmit": true, "composite": true, "strict": true, - "downlevelIteration": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "allowJs": true, "jsx": "react-jsx", - - // strict: https://github.com/tsconfig/bases/blob/main/bases/strictest.json "allowUnusedLabels": false, "allowUnreachableCode": false, "exactOptionalPropertyTypes": true, @@ -29,6 +26,7 @@ "noUnusedParameters": true, "isolatedModules": true, "checkJs": true, - "esModuleInterop": true + "esModuleInterop": true, + "verbatimModuleSyntax": true } } diff --git a/app/types.ts b/app/types.ts index d768958d..6705448f 100644 --- a/app/types.ts +++ b/app/types.ts @@ -1,184 +1,184 @@ import { type BaseEvents } from "bunvim"; import { - array, - boolean, - coerce, - literal, - maxValue, - minValue, - number, - object, - string, - union, - type Output, + array, + boolean, + coerce, + literal, + maxValue, + minValue, + number, + object, + string, + union, + type Output, } from "valibot"; import { type GithubPreview } from "./github-preview"; export const ThemeSchema = object({ - name: union([literal("system"), literal("light"), literal("dark")]), - high_contrast: boolean(), + name: union([literal("system"), literal("light"), literal("dark")]), + high_contrast: boolean(), }); export type Theme = Output; export const BuildConstsSchema = object({ - HOST: string(), - PORT: coerce(number(), Number), - IS_DEV: coerce(boolean(), Boolean), - THEME: ThemeSchema, + HOST: string(), + PORT: coerce(number(), Number), + IS_DEV: coerce(boolean(), Boolean), + THEME: ThemeSchema, }); export type BuildConsts = Output; export const PluginPropsSchema = object({ - init: object({ - /** dir path where ".git" dir was found */ - root: string(), - /** - * current path when plugin was loaded - * if no buffer was loaded when plugin started, path is dir and ends with "/" - * otherwise path looks something like "/Users/.../README.md" - * */ - path: string(), - }), + init: object({ + /** dir path where ".git" dir was found */ + root: string(), + /** + * current path when plugin was loaded + * if no buffer was loaded when plugin started, path is dir and ends with "/" + * otherwise path looks something like "/Users/.../README.md" + * */ + path: string(), + }), - config: object({ - /** http/ws host "localhost" */ - host: string(), - /** port to host the http/ws server "localhost:\{port\}" */ - port: number(), - single_file: boolean(), - theme: ThemeSchema, - details_tags_open: boolean(), - cursor_line: object({ - disable: boolean(), - color: string(), - opacity: number([minValue(0), maxValue(1)]), - }), - scroll: object({ - disable: boolean(), - top_offset_pct: number(), - }), - }), + config: object({ + /** http/ws host "localhost" */ + host: string(), + /** port to host the http/ws server "localhost:\{port\}" */ + port: number(), + single_file: boolean(), + theme: ThemeSchema, + details_tags_open: boolean(), + cursor_line: object({ + disable: boolean(), + color: string(), + opacity: number([minValue(0), maxValue(1)]), + }), + scroll: object({ + disable: boolean(), + top_offset_pct: number(), + }), + }), }); export type PluginProps = Output; export type Config = PluginProps["config"]; export const CursorMoveSchema = object({ - /** - * Used to attach & detach buffers (subscribe to buffer changes) - * as user navigates from buffer to buffer in neovim. - * */ - buffer_id: number(), - abs_path: string(), - cursor_line: number(), + /** + * Used to attach & detach buffers (subscribe to buffer changes) + * as user navigates from buffer to buffer in neovim. + * */ + buffer_id: number(), + abs_path: string(), + cursor_line: number(), }); export type CursorMove = Output; export const ContentChangeSchema = object({ - abs_path: string(), - lines: array(string()), + abs_path: string(), + lines: array(string()), }); export type ContentChange = Output; export type WsServerMessage = - | { - type: "init"; - lines: string[]; - repoName: string; - currentPath: string; - config: GithubPreview["config"]; - cursorLine: number | null; - currentEntries: string[] | undefined; - } - | { - type: "entries"; - path: string; - entries: string[]; - } - | { - type: "entry"; - currentPath: string; - lines: string[]; - cursorLine: number | null; - hash: string | undefined; - currentEntries: string[] | undefined; - } - | { - type: "cursor_move"; - cursorLine: number | null; - currentPath: string; - lines?: string[]; - } - | { - type: "content_change"; - currentPath: string; - /** - * Offset recalculation is triggered on markdown container element resize. - * Sometimes adding new lines doesn't change the element size so offsets - * are not recalculated, leading to incorrect cursorline position. - * To fix that, we notify the browser on linesCountChange - * to trigger offset calculation. - * */ - linesCountChange: boolean; - lines: string[]; - } - | { - type: "update_config"; - config: GithubPreview["config"]; - } - | { - type: "goodbye"; - }; + | { + type: "init"; + lines: string[]; + repoName: string; + currentPath: string; + config: GithubPreview["config"]; + cursorLine: number | null; + currentEntries: string[] | undefined; + } + | { + type: "entries"; + path: string; + entries: string[]; + } + | { + type: "entry"; + currentPath: string; + lines: string[]; + cursorLine: number | null; + hash: string | undefined; + currentEntries: string[] | undefined; + } + | { + type: "cursor_move"; + cursorLine: number | null; + currentPath: string; + lines?: string[]; + } + | { + type: "content_change"; + currentPath: string; + /** + * Offset recalculation is triggered on markdown container element resize. + * Sometimes adding new lines doesn't change the element size so offsets + * are not recalculated, leading to incorrect cursorline position. + * To fix that, we notify the browser on linesCountChange + * to trigger offset calculation. + * */ + linesCountChange: boolean; + lines: string[]; + } + | { + type: "update_config"; + config: GithubPreview["config"]; + } + | { + type: "goodbye"; + }; export type WsBrowserMessage = - | { - type: "init"; - } - | { - type: "get_entries"; - path: string; - } - | { - type: "get_entry"; - path: string; - } - | { - type: "update_config"; - action: UpdateConfigAction; - }; + | { + type: "init"; + } + | { + type: "get_entries"; + path: string; + } + | { + type: "get_entry"; + path: string; + } + | { + type: "update_config"; + action: UpdateConfigAction; + }; export type UpdateConfigAction = - | ["clear_overrides"] - | ["theme_name", "system" | "light" | "dark"] - | ["theme_high_contrast", "on" | "off"] - | ["single_file", "toggle" | "on" | "off"] - | ["details_tags", "toggle" | "open" | "closed"] - | ["scroll", "toggle" | "on" | "off"] - | ["scroll.offset", number] - | ["cursorline", "toggle" | "on" | "off"] - | ["cursorline.color", string] - | ["cursorline.opacity", number]; + | ["clear_overrides"] + | ["theme_name", "system" | "light" | "dark"] + | ["theme_high_contrast", "on" | "off"] + | ["single_file", "toggle" | "on" | "off"] + | ["details_tags", "toggle" | "open" | "closed"] + | ["scroll", "toggle" | "on" | "off"] + | ["scroll.offset", number] + | ["cursorline", "toggle" | "on" | "off"] + | ["cursorline.color", string] + | ["cursorline.opacity", number]; // eslint-disable-next-line export interface CustomEvents extends BaseEvents { - requests: { - config_update: UpdateConfigAction; - before_exit: []; - }; - notifications: { - // github-preview - attach_buffer: [buffer: number, path: string]; - cursor_move: [buffer: number, path: string, cursor_line: number]; + requests: { + config_update: UpdateConfigAction; + before_exit: []; + }; + notifications: { + // github-preview + attach_buffer: [buffer: number, path: string]; + cursor_move: [buffer: number, path: string, cursor_line: number]; - // neovim native - nvim_buf_detach_event: [buffer: number]; - nvim_buf_lines_event: [ - buffer: number, - changedtick: number, - firstline: number, - lastline: number, - linedata: string[], - more: boolean, - ]; - nvim_buf_changedtick_event: [buffer: number, changedtick: number]; - }; + // neovim native + nvim_buf_detach_event: [buffer: number]; + nvim_buf_lines_event: [ + buffer: number, + changedtick: number, + firstline: number, + lastline: number, + linedata: string[], + more: boolean, + ]; + nvim_buf_changedtick_event: [buffer: number, changedtick: number]; + }; } diff --git a/app/web/.eslintrc.cjs b/app/web/.eslintrc.cjs index 059ae8e9..531c39de 100644 --- a/app/web/.eslintrc.cjs +++ b/app/web/.eslintrc.cjs @@ -1,20 +1,20 @@ /** @type {import('eslint').ESLint.Options} */ module.exports = { - env: { browser: true }, - ignorePatterns: [".eslintrc.cjs", "*.min.js"], - extends: ["plugin:react-hooks/recommended", "plugin:tailwindcss/recommended"], - settings: { - tailwindcss: { - config: __dirname + "/tailwind.config.cjs", - }, - }, - rules: { - "tailwindcss/no-custom-classname": [ - "warn", - { - whitelist: ["pantsdown", "dark"], - callees: ["cn"], - }, - ], - }, + env: { browser: true }, + ignorePatterns: [".eslintrc.cjs", "*.min.js"], + extends: ["plugin:react-hooks/recommended", "plugin:tailwindcss/recommended"], + settings: { + tailwindcss: { + config: __dirname + "/tailwind.config.cjs", + }, + }, + rules: { + "tailwindcss/no-custom-classname": [ + "warn", + { + whitelist: ["pantsdown", "dark"], + callees: ["cn"], + }, + ], + }, }; diff --git a/app/web/app.tsx b/app/web/app.tsx index f7692082..e0063d17 100644 --- a/app/web/app.tsx +++ b/app/web/app.tsx @@ -1,5 +1,4 @@ import "./static/dev-tailwind.css"; - import { parse } from "valibot"; import { BuildConstsSchema } from "../types.ts"; import { Explorer } from "./components/explorer/index.tsx"; @@ -13,17 +12,17 @@ declare const __IS_DEV__: unknown; declare const __THEME__: unknown; const BUILD_CONSTS = parse(BuildConstsSchema, { - HOST: __HOST__, - PORT: __PORT__, - IS_DEV: __IS_DEV__, - THEME: __THEME__, + HOST: __HOST__, + PORT: __PORT__, + IS_DEV: __IS_DEV__, + THEME: __THEME__, }); export const App = () => ( - -
- - -
-
+ +
+ + +
+
); diff --git a/app/web/components/explorer/entry.tsx b/app/web/components/explorer/entry.tsx index 2a4c5fe2..2587e711 100644 --- a/app/web/components/explorer/entry.tsx +++ b/app/web/components/explorer/entry.tsx @@ -9,98 +9,93 @@ import { websocketContext } from "../websocket-provider/context.ts"; const iconClassName = "mr-3 h-5 w-5"; const IconMap = { - dir: , - openDir: , - file: , + dir: , + openDir: , + file: , }; type Props = { - path: string; - depth: number; - currentPath: string | null; + path: string; + depth: number; + currentPath: string | null; }; export const EntryComponent = ({ path, depth, currentPath }: Props) => { - const { registerHandler, wsRequest } = useContext(websocketContext); - const [entries, setEntries] = useState([]); - const [isSelected, setIsSelected] = useState(false); - const [expanded, setExpanded] = useState( - // expand root by default ("" is root) - path === "", - ); + const { registerHandler, wsRequest } = useContext(websocketContext); + const [entries, setEntries] = useState([]); + const [isSelected, setIsSelected] = useState(false); + const [expanded, setExpanded] = useState( + // expand root by default ("" is root) + path === "", + ); - const isDir = path === "" || path.endsWith("/"); - const entryName = getEntryName(path); + const isDir = path === "" || path.endsWith("/"); + const entryName = getEntryName(path); - useEffect(() => { - registerHandler(`explorer-${path}`, (message) => { - if (message.type === "entries" && message.path === path) { - setEntries(message.entries); - } - }); - }, [path, registerHandler]); + useEffect(() => { + registerHandler(`explorer-${path}`, (message) => { + if (message.type === "entries" && message.path === path) { + setEntries(message.entries); + } + }); + }, [path, registerHandler]); - useEffect(() => { - if (!isDir) return; - wsRequest({ type: "get_entries", path: path }); - }, [wsRequest, path, isDir]); + useEffect(() => { + if (!isDir) return; + wsRequest({ type: "get_entries", path: path }); + }, [wsRequest, path, isDir]); - useEffect(() => { - const segments = getSegments(currentPath); - let entrySlice = segments.slice(0, depth + 1).join("/"); + useEffect(() => { + const segments = getSegments(currentPath); + let entrySlice = segments.slice(0, depth + 1).join("/"); - if (isDir) { - entrySlice += "/"; - if (entrySlice === path) setExpanded(true); - } + if (isDir) { + entrySlice += "/"; + if (entrySlice === path) setExpanded(true); + } - setIsSelected(currentPath === path); - }, [currentPath, depth, path, isDir]); + setIsSelected(currentPath === path); + }, [currentPath, depth, path, isDir]); - return ( -
- {entryName && ( -
{ - setExpanded(true); - wsRequest({ type: "get_entry", path }); - }} - style={{ paddingLeft: depth * 11 + (isDir ? 0 : 20) }} - className={cn( - "relative group flex h-[34px] cursor-pointer items-center mx-3 rounded-md", - "hover:bg-github-canvas-subtle", - isSelected && "bg-github-canvas-subtle", - )} - > - {isSelected && ( -
- )} - {isDir && ( -
{ - e.stopPropagation(); - setExpanded(!expanded); - }} - className="hover:bg-github-border-default mr-1 flex h-full items-center" - > - -
- )} - {IconMap[isDir ? (expanded ? "openDir" : "dir") : "file"]} - - {entryName} - -
- )} - {expanded && - entries.map((path) => ( - - ))} -
- ); + return ( +
+ {entryName && ( +
{ + setExpanded(true); + wsRequest({ type: "get_entry", path }); + }} + style={{ paddingLeft: depth * 11 + (isDir ? 0 : 20) }} + className={cn( + "group mx-3 rounded-md relative flex h-[34px] cursor-pointer items-center", + "hover:bg-github-canvas-subtle", + isSelected && "bg-github-canvas-subtle", + )} + > + {isSelected && ( +
+ )} + {isDir && ( +
{ + e.stopPropagation(); + setExpanded(!expanded); + }} + className="hover:bg-github-border-default mr-1 flex h-full items-center" + > + +
+ )} + {IconMap[isDir ? (expanded ? "openDir" : "dir") : "file"]} + + {entryName} + +
+ )} + {expanded && + entries.map((path) => ( + + ))} +
+ ); }; diff --git a/app/web/components/explorer/footer.tsx b/app/web/components/explorer/footer.tsx index 8d8c1b8b..f245f9ab 100644 --- a/app/web/components/explorer/footer.tsx +++ b/app/web/components/explorer/footer.tsx @@ -1,28 +1,28 @@ import { cn } from "../../utils.ts"; export const Footer = ({ isExpanded }: { isExpanded: boolean }) => ( -
- {isExpanded ? ( -

- with ♥️ by{" "} - - wallpants.io - -

- ) : ( - - + +

+ ) : ( + + + + )} +
); diff --git a/app/web/components/explorer/header.tsx b/app/web/components/explorer/header.tsx index 1248e72d..640220ed 100644 --- a/app/web/components/explorer/header.tsx +++ b/app/web/components/explorer/header.tsx @@ -8,59 +8,59 @@ import { SettingsIcon } from "../icons/settings.tsx"; import { websocketContext } from "../websocket-provider/context.ts"; export const Header = ({ - isExpanded, - setIsExpanded, - setConfigOpen, - className, - isOverriden, - setSettingsOffset, + isExpanded, + setIsExpanded, + setConfigOpen, + className, + isOverriden, + setSettingsOffset, }: { - isExpanded: boolean; - setIsExpanded: (e: boolean) => void; - setConfigOpen: Dispatch>; - className: string; - isOverriden: boolean; - setSettingsOffset: (o: number) => void; + isExpanded: boolean; + setIsExpanded: (e: boolean) => void; + setConfigOpen: Dispatch>; + className: string; + isOverriden: boolean; + setSettingsOffset: (o: number) => void; }) => { - const { refObject } = useContext(websocketContext); + const { refObject } = useContext(websocketContext); - return ( -
- {isExpanded && ( - <> -

Files

-
- { - e.stopPropagation(); - setSettingsOffset(105); - setConfigOpen((c) => (c === "no-key" ? null : "no-key")); - }} - /> - {isOverriden ? ( -
- ) : null} -
- - )} - { - refObject.current.skipScroll = true; - setIsExpanded(!isExpanded); - }} - /> -
- ); + return ( +
+ {isExpanded && ( + <> +

Files

+
+ { + e.stopPropagation(); + setSettingsOffset(105); + setConfigOpen((c) => (c === "no-key" ? null : "no-key")); + }} + /> + {isOverriden ? ( +
+ ) : null} +
+ + )} + { + refObject.current.skipScroll = true; + setIsExpanded(!isExpanded); + }} + /> +
+ ); }; diff --git a/app/web/components/explorer/index.tsx b/app/web/components/explorer/index.tsx index 5453dd8a..d21005c4 100644 --- a/app/web/components/explorer/index.tsx +++ b/app/web/components/explorer/index.tsx @@ -10,66 +10,64 @@ import { CollapsedSettings } from "./settings/collapsed.tsx"; import { Settings } from "./settings/index.tsx"; export const Explorer = () => { - const { currentPath, config } = useContext(websocketContext); - const [configOpen, setConfigOpen] = useState(null); - const [isExpanded, setIsExpanded] = useState(false); - const [startExit, setStartExit] = useState(false); - const [settingsOffset, setSettingsOffset] = useState(0); - const isOverriden = !isEqual(config?.dotfiles, config?.overrides); + const { currentPath, config } = useContext(websocketContext); + const [configOpen, setConfigOpen] = useState(null); + const [isExpanded, setIsExpanded] = useState(false); + const [startExit, setStartExit] = useState(false); + const [settingsOffset, setSettingsOffset] = useState(0); + const isOverriden = !isEqual(config?.dotfiles, config?.overrides); - useOnDocumentClick({ - disabled: !configOpen, - callback: () => { - setConfigOpen(null); - }, - }); + useOnDocumentClick({ + disabled: !configOpen, + callback: () => { + setConfigOpen(null); + }, + }); - return ( -
+
+ {config?.overrides.single_file ? ( +
+

Single-file mode

+

File explorer only available when in repository mode.

+
+ ) : ( + )} - > -
- {config?.overrides.single_file ? ( -
-

Single-file mode

-

File explorer only available when in repository mode.

-
- ) : ( - - )} -
-
+
+ {!isExpanded && ( + - {!isExpanded && ( - - )} - {configOpen ? ( - - ) : null} -
-
- ); + )} + {configOpen ? ( + + ) : null} +
+
+ ); }; diff --git a/app/web/components/explorer/settings/collapsed-option.tsx b/app/web/components/explorer/settings/collapsed-option.tsx index fb8ac6c5..4ef15f7b 100644 --- a/app/web/components/explorer/settings/collapsed-option.tsx +++ b/app/web/components/explorer/settings/collapsed-option.tsx @@ -1,10 +1,10 @@ import { - useContext, - useEffect, - useState, - type Dispatch, - type FC, - type SetStateAction, + useContext, + useEffect, + useState, + type Dispatch, + type FC, + type SetStateAction, } from "react"; import { type Config } from "../../../../types"; import { cn, isEqual } from "../../../utils"; @@ -22,113 +22,113 @@ import { UnfoldVerticalIcon } from "../../icons/unfold-vertical"; import { websocketContext } from "../../websocket-provider/context"; type Props = { - cKey: keyof Config; - active: boolean; - setConfigOpen: Dispatch>; - setSettingsOffset: (o: number) => void; - className?: string; - startExit: boolean; + cKey: keyof Config; + active: boolean; + setConfigOpen: Dispatch>; + setSettingsOffset: (o: number) => void; + className?: string; + startExit: boolean; }; const DURATION_SECS = 2.5; export const CollapsedOption = ({ - cKey, - active, - setConfigOpen, - setSettingsOffset, - className, - startExit, + cKey, + active, + setConfigOpen, + setSettingsOffset, + className, + startExit, }: Props) => { - const { config } = useContext(websocketContext); - const [isHovering, setIsHovering] = useState(false); + const { config } = useContext(websocketContext); + const [isHovering, setIsHovering] = useState(false); - useEffect(() => { - if (!active || !startExit || isHovering) return; - const timeout = setTimeout(() => { - setConfigOpen(null); - }, DURATION_SECS * 1000); + useEffect(() => { + if (!active || !startExit || isHovering) return; + const timeout = setTimeout(() => { + setConfigOpen(null); + }, DURATION_SECS * 1000); - return () => { - clearTimeout(timeout); - }; - }, [startExit, setConfigOpen, active, isHovering]); + return () => { + clearTimeout(timeout); + }; + }, [startExit, setConfigOpen, active, isHovering]); - if (!config) return null; + if (!config) return null; - const dotfiles = config.dotfiles[cKey]; - const override = config.overrides[cKey]; - const isOverriden = !isEqual(dotfiles, override); + const dotfiles = config.dotfiles[cKey]; + const override = config.overrides[cKey]; + const isOverriden = !isEqual(dotfiles, override); - const [Icon, iconClassName] = findIcon(cKey, config.overrides); + const [Icon, iconClassName] = findIcon(cKey, config.overrides); - return ( -
- {Icon && ( -
- - { - setIsHovering(true); - }} - onMouseLeave={() => { - setIsHovering(false); - }} - className={cn("mx-auto", active && !startExit && "bg-github-canvas-subtle")} - iconClassName={iconClassName} - noBorder - onClick={(event) => { - event.stopPropagation(); - const element = event.currentTarget; - setSettingsOffset(element.getBoundingClientRect().y); - setConfigOpen((c) => (c === cKey ? null : cKey)); - }} - /> -
- )} - {isOverriden ? ( -
- ) : null} -
- ); + return ( +
+ {Icon && ( +
+ + { + setIsHovering(true); + }} + onMouseLeave={() => { + setIsHovering(false); + }} + className={cn("mx-auto", active && !startExit && "bg-github-canvas-subtle")} + iconClassName={iconClassName} + noBorder + onClick={(event) => { + event.stopPropagation(); + const element = event.currentTarget; + setSettingsOffset(element.getBoundingClientRect().y); + setConfigOpen((c) => (c === cKey ? null : cKey)); + }} + /> +
+ )} + {isOverriden ? ( +
+ ) : null} +
+ ); }; function findIcon( - cKey: Props["cKey"], - config: Config, + cKey: Props["cKey"], + config: Config, ): [Icon: FC<{ className: string }> | null, className: string] { - if (cKey === "theme") { - if (config[cKey].name === "dark") return [MoonIcon, ""]; - if (config[cKey].name === "light") return [SunIcon, ""]; - return [SystemIcon, ""]; - } + if (cKey === "theme") { + if (config[cKey].name === "dark") return [MoonIcon, ""]; + if (config[cKey].name === "light") return [SunIcon, ""]; + return [SystemIcon, ""]; + } - if (cKey === "scroll") { - if (config[cKey].disable) return [MouseIcon, ""]; - return [MouseIcon, "stroke-github-accent-fg"]; - } + if (cKey === "scroll") { + if (config[cKey].disable) return [MouseIcon, ""]; + return [MouseIcon, "stroke-github-accent-fg"]; + } - if (cKey === "single_file") { - if (config[cKey]) return [PinIcon, "stroke-github-danger-fg fill-github-danger-fg"]; - return [PinOffIcon, ""]; - } + if (cKey === "single_file") { + if (config[cKey]) return [PinIcon, "stroke-github-danger-fg fill-github-danger-fg"]; + return [PinOffIcon, ""]; + } - if (cKey === "cursor_line") { - if (config[cKey].disable) return [CursorlineIcon, ""]; - return [CursorlineIcon, "stroke-[#ff00a2]"]; - } + if (cKey === "cursor_line") { + if (config[cKey].disable) return [CursorlineIcon, ""]; + return [CursorlineIcon, "stroke-[#ff00a2]"]; + } - if (cKey === "details_tags_open") { - if (config[cKey]) return [UnfoldVerticalIcon, "stroke-github-success-fg"]; - return [FoldVerticalIcon, ""]; - } + if (cKey === "details_tags_open") { + if (config[cKey]) return [UnfoldVerticalIcon, "stroke-github-success-fg"]; + return [FoldVerticalIcon, ""]; + } - return [null, ""]; + return [null, ""]; } diff --git a/app/web/components/explorer/settings/collapsed.tsx b/app/web/components/explorer/settings/collapsed.tsx index f7eb6068..cf39fd3c 100644 --- a/app/web/components/explorer/settings/collapsed.tsx +++ b/app/web/components/explorer/settings/collapsed.tsx @@ -4,58 +4,58 @@ import { cn } from "../../../utils"; import { CollapsedOption } from "./collapsed-option"; type Props = { - className?: string; - setSettingsOffset: (o: number) => void; - setConfigOpen: Dispatch>; - configOpen: null | keyof Config | "no-key"; - startExit: boolean; + className?: string; + setSettingsOffset: (o: number) => void; + setConfigOpen: Dispatch>; + configOpen: null | keyof Config | "no-key"; + startExit: boolean; }; export const CollapsedSettings = ({ - className, - setSettingsOffset, - setConfigOpen, - configOpen, - startExit, + className, + setSettingsOffset, + setConfigOpen, + configOpen, + startExit, }: Props) => ( -
-
- - - - - -
-
+
+
+ + + + + +
+
); diff --git a/app/web/components/explorer/settings/index.tsx b/app/web/components/explorer/settings/index.tsx index 16b40004..5b0d1878 100644 --- a/app/web/components/explorer/settings/index.tsx +++ b/app/web/components/explorer/settings/index.tsx @@ -9,95 +9,91 @@ import { SingleFileOption } from "./options/single-file"; import { ThemeOption } from "./options/theme"; type Props = { - isOverriden: boolean; - cKey: keyof Config | "no-key"; - settingsOffset: number; - setStartExit: (s: boolean) => void; + isOverriden: boolean; + cKey: keyof Config | "no-key"; + settingsOffset: number; + setStartExit: (s: boolean) => void; }; export const Settings = ({ isOverriden, cKey, settingsOffset, setStartExit }: Props) => { - const { wsRequest } = useContext(websocketContext); - const [isHovering, setIsHovering] = useState(false); - const [isSelectingColor, setIsSelectingColor] = useState(false); - const [tick, setTick] = useState(0); + const { wsRequest } = useContext(websocketContext); + const [isHovering, setIsHovering] = useState(false); + const [isSelectingColor, setIsSelectingColor] = useState(false); + const [tick, setTick] = useState(0); - const smallSettings = cKey !== "no-key"; + const smallSettings = cKey !== "no-key"; - useEffect(() => { - // we need to keep track of `isPickingColor`, because we stop hovering - // when picking color, but we don't want the settings modal to close - if (smallSettings && !isSelectingColor) { - setStartExit(!isHovering); - } - }, [smallSettings, setStartExit, isHovering, isSelectingColor]); + useEffect(() => { + // we need to keep track of `isPickingColor`, because we stop hovering + // when picking color, but we don't want the settings modal to close + if (smallSettings && !isSelectingColor) { + setStartExit(!isHovering); + } + }, [smallSettings, setStartExit, isHovering, isSelectingColor]); - return ( -
{ - setIsHovering(true); - }} - onMouseLeave={() => { - setIsHovering(false); - }} - onClick={(e) => { - e.stopPropagation(); - // a little bit of a hack to close "select" dropdowns. - // by triggering a rerender - setTick(tick + 1); - }} - style={{ top: settingsOffset - 50 }} - className={cn( - "absolute left-14 top-[55px] z-20 p-2 text-sm", - "rounded border border-github-border-default bg-github-canvas-subtle", - !smallSettings && "w-[430px]", - )} - > - {!smallSettings && ( -

- Temporarily override your settings. -
- To persist changes, update your{" "} - - neovim config files - - . -

- )} -
- {(!smallSettings || cKey === "theme") && } - {(!smallSettings || cKey === "details_tags_open") && } - {(!smallSettings || cKey === "single_file") && } -
+ return ( +
{ + setIsHovering(true); + }} + onMouseLeave={() => { + setIsHovering(false); + }} + onClick={(e) => { + e.stopPropagation(); + // a little bit of a hack to close "select" dropdowns. + // by triggering a rerender + setTick(tick + 1); + }} + style={{ top: settingsOffset - 50 }} + className={cn( + "left-14 p-2 text-sm absolute top-[55px] z-20", + "rounded border-github-border-default bg-github-canvas-subtle border", + !smallSettings && "w-[430px]", + )} + > + {!smallSettings && ( +

+ Temporarily override your settings. +
+ To persist changes, update your{" "} + + neovim config files + + . +

+ )} +
+ {(!smallSettings || cKey === "theme") && } + {(!smallSettings || cKey === "details_tags_open") && } + {(!smallSettings || cKey === "single_file") && } +
-
- {(!smallSettings || cKey === "cursor_line") && ( - - )} - {(!smallSettings || cKey === "scroll") && } -
- {!smallSettings && ( - +
+ {(!smallSettings || cKey === "cursor_line") && ( + )} -
- ); + {(!smallSettings || cKey === "scroll") && } +
+ {!smallSettings && ( + + )} +
+ ); }; diff --git a/app/web/components/explorer/settings/option.tsx b/app/web/components/explorer/settings/option.tsx index 4d255606..0d35fcda 100644 --- a/app/web/components/explorer/settings/option.tsx +++ b/app/web/components/explorer/settings/option.tsx @@ -6,138 +6,138 @@ import { websocketContext } from "../../websocket-provider/context"; import { Select, type SelectOption } from "./select"; type Props = { - className?: string; - name: string; - cKey: keyof GithubPreview["config"]["overrides"]; - select?: SelectOption[]; - toggle?: { - value: boolean; - onChange: (v: boolean) => void; - }; - color?: { - value: string; - onChange: (v: string) => void; - setIsSelectingColor: (b: boolean) => void; - }; - range?: { - value: number; - min: number; - max: number; - step: number; - onChange: (value: number) => void; - }; - disabled?: string | undefined; + className?: string; + name: string; + cKey: keyof GithubPreview["config"]["overrides"]; + select?: SelectOption[]; + toggle?: { + value: boolean; + onChange: (v: boolean) => void; + }; + color?: { + value: string; + onChange: (v: string) => void; + setIsSelectingColor: (b: boolean) => void; + }; + range?: { + value: number; + min: number; + max: number; + step: number; + onChange: (value: number) => void; + }; + disabled?: string | undefined; }; export const Option = ({ - name, - cKey, - select, - color, - toggle, - range, - className, - disabled, + name, + cKey, + select, + color, + toggle, + range, + className, + disabled, }: Props) => { - const { config } = useContext(websocketContext); + const { config } = useContext(websocketContext); - useEffect(() => { - if (!color) return; + useEffect(() => { + if (!color) return; - // we need to do all this to handle color change instead of a simple - // onChange handler in the input, because we need a way to know - // when the user is picking a color (the native color picker is showing) - // and when they're done picking. we need to know this to prevent - // the settings modal from closing whilst the user is picking a color. - // - // According to mozilla: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color#result - // the way to know this is to have a handler for "input" and a handler for "change" - // added to the color input, but the react listeners onInput and onChange - // are both triggered at the same time every time the color input value changes. + // we need to do all this to handle color change instead of a simple + // onChange handler in the input, because we need a way to know + // when the user is picking a color (the native color picker is showing) + // and when they're done picking. we need to know this to prevent + // the settings modal from closing whilst the user is picking a color. + // + // According to mozilla: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color#result + // the way to know this is to have a handler for "input" and a handler for "change" + // added to the color input, but the react listeners onInput and onChange + // are both triggered at the same time every time the color input value changes. - const input = document.getElementById(`${cKey}-color`); - if (!input) return; + const input = document.getElementById(`${cKey}-color`); + if (!input) return; - function onInput(event: ChangeEvent) { - color?.onChange(event.target.value); - color?.setIsSelectingColor(true); - } + function onInput(event: ChangeEvent) { + color?.onChange(event.target.value); + color?.setIsSelectingColor(true); + } - function onChange(event: ChangeEvent) { - color?.onChange(event.target.value); - color?.setIsSelectingColor(false); - } + function onChange(event: ChangeEvent) { + color?.onChange(event.target.value); + color?.setIsSelectingColor(false); + } - // eslint-disable-next-line - input.addEventListener("input", onInput as any); - // eslint-disable-next-line - input.addEventListener("change", onChange as any); + // eslint-disable-next-line + input.addEventListener("input", onInput as any); + // eslint-disable-next-line + input.addEventListener("change", onChange as any); - return () => { - // eslint-disable-next-line - input.removeEventListener("input", onInput as any); - // eslint-disable-next-line - input.removeEventListener("change", onChange as any); - }; - }, [color, cKey]); + return () => { + // eslint-disable-next-line + input.removeEventListener("input", onInput as any); + // eslint-disable-next-line + input.removeEventListener("change", onChange as any); + }; + }, [color, cKey]); - if (!config) return null; + if (!config) return null; - const dotfiles = config.dotfiles[cKey]; - const override = config.overrides[cKey]; - const isOverriden = !isEqual(dotfiles, override); + const dotfiles = config.dotfiles[cKey]; + const override = config.overrides[cKey]; + const isOverriden = !isEqual(dotfiles, override); - return ( -
+ {isOverriden ? ( +
+ ) : null} +

{name}

+
+ {toggle && } + {select && + {color.value} + )} - > - {isOverriden ? ( -
- ) : null} -

{name}

-
- {toggle && } - {select && - {color.value} - - )} - {range && ( -
- { - range.onChange(Number(e.target.value)); - }} - /> - {range.value} -
- )} -
- {disabled && ( -
-
-

{disabled}

-
-
+ {range && ( +
+ { + range.onChange(Number(e.target.value)); + }} + /> + {range.value} +
)} -
- ); +
+ {disabled && ( +
+
+

{disabled}

+
+
+ )} +
+ ); }; diff --git a/app/web/components/explorer/settings/options/cursorline.tsx b/app/web/components/explorer/settings/options/cursorline.tsx index 336654a2..f8db32f4 100644 --- a/app/web/components/explorer/settings/options/cursorline.tsx +++ b/app/web/components/explorer/settings/options/cursorline.tsx @@ -3,51 +3,51 @@ import { websocketContext } from "../../../websocket-provider/context"; import { Option } from "../option"; type Props = { - setIsSelectingColor: (s: boolean) => void; + setIsSelectingColor: (s: boolean) => void; }; export const CursorlineOption = ({ setIsSelectingColor }: Props) => { - const { wsRequest, config } = useContext(websocketContext); + const { wsRequest, config } = useContext(websocketContext); - if (!config) return null; - const { overrides } = config; + if (!config) return null; + const { overrides } = config; - return ( -