Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 56 additions & 35 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Copilot Instructions for mo (Markdown Opener)
# Copilot Instructions for markview

## What is mo
## What is markview

`mo` is a CLI tool that opens Markdown files in a browser with live-reload. It runs a Go HTTP server that embeds a React SPA as a single binary. The Go module is `github.com/k1LoW/mo`.
`markview` is a CLI tool that opens Markdown files in a browser with live-reload. It runs a Go HTTP server that embeds a React SPA as a single binary. The Go module is `github.com/kooksee/markview`.

## Build & Run

Requires Go and [pnpm](https://pnpm.io/). Node.js version is managed via `pnpm.executionEnv.nodeVersion` in `internal/frontend/package.json`.
Requires Go 1.26+ and [pnpm](https://pnpm.io/). Node.js version is managed via `pnpm.executionEnv.nodeVersion` in `frontend/package.json`.

```bash
# Full build (frontend + Go binary, with ldflags)
Expand All @@ -25,7 +25,7 @@ make generate
make test

# Run a single frontend test (vitest)
cd internal/frontend && pnpm test src/utils/buildTree.test.ts
cd frontend && pnpm test src/utils/buildTree.test.ts

# Run Go tests only
go test ./...
Expand All @@ -36,10 +36,27 @@ go test ./internal/server/ -run TestHandleFiles
# Run linters (golangci-lint + gostyle)
make lint

# Format code (frontend)
make fmt

# Check formatting without modifying
make fmt-check

# Take screenshots for README (requires Chrome)
make screenshot

# CI target (install dev deps + generate + test)
make ci

# Install to $GOPATH/bin (run after modifications to verify build and install)
make install

# Frontend dev server with backend proxy (proxies /_/ to localhost:6275)
cd frontend && pnpm run dev
```

**Workflow**: After making code changes, run `make install` to build and install the binary.

### CLI Flags

- `--port` / `-p` — Server port (default: 6275)
Expand All @@ -48,10 +65,11 @@ make ci
- `--no-open` — Never open browser
- `--watch` / `-w` — Glob pattern to watch for matching files (repeatable)
- `--unwatch` — Remove a watched glob pattern (repeatable)
- `--status` — Show status of all running mo servers
- `--shutdown` — Shut down the running mo server
- `--restart` — Restart the running mo server
- `--foreground` — Run mo server in foreground (do not background)
- `--status` — Show status of all running markview servers
- `--shutdown` — Shut down the running markview server
- `--restart` — Restart the running markview server
- `--foreground` — Run markview server in foreground (do not background)
- `--dangerously-allow-remote-access` — Allow remote access without authentication (trusted networks only)

## Architecture

Expand All @@ -60,12 +78,39 @@ make ci
- `cmd/root.go` — CLI entry point (Cobra). Handles single-instance detection: if a server is already running on the port, adds files via HTTP API instead of starting a new one.
- `internal/server/server.go` — HTTP server, state management (mutex-guarded), SSE for live-reload, file watcher (fsnotify). All API routes use `/_/` prefix to avoid collision with SPA route paths (group names).
- `internal/static/static.go` — `go:generate` runs the frontend build, then `go:embed` embeds the output from `internal/static/dist/`.
- `internal/frontend/` — Vite + React 19 + TypeScript + Tailwind CSS v4 SPA. Build output goes to `internal/static/dist/` (configured in `vite.config.ts`).
- `frontend/` — Vite + React 19 + TypeScript + Tailwind CSS v4 SPA. Build output goes to `internal/static/dist/` (configured in `vite.config.ts`).
- `internal/backup/` — State persistence for open files/groups using atomic JSON writes to `$XDG_STATE_HOME/markview/backup/`. Enables session restoration across server restarts.
- `internal/logfile/` — Rotating JSON logging to `$XDG_STATE_HOME/markview/log/` (max 10MB, 3 backups, 7-day retention).
- `internal/xdg/` — XDG Base Directory helper. `StateHome()` returns `$XDG_STATE_HOME` or default `~/.local/state`.
- `version/version.go` — Version info, updated by tagpr on release. Build embeds revision via ldflags.

## Frontend

- Package manager: **pnpm** (version specified in `frontend/package.json` `packageManager` field)
- Markdown rendering: `react-markdown` + `remark-gfm` + `rehype-raw` + `rehype-slug` (heading IDs) + `@shikijs/rehype` (syntax highlighting) + `mermaid` (diagram rendering)
- SPA routing via `window.location.pathname` (no router library)
- Key components: `App.tsx` (routing/state), `Sidebar.tsx` (file list with flat/tree view, resizable, drag-and-drop reorder), `TreeView.tsx` (tree view with collapsible directories), `MarkdownViewer.tsx` (rendering + raw view toggle), `TocPanel.tsx` (table of contents, resizable), `GroupDropdown.tsx` (group switcher), `FileContextMenu.tsx` (shared kebab menu for file operations), `WidthToggle.tsx` (wide/narrow content width toggle)
- Custom hooks: `useSSE.ts` (SSE subscription with auto-reconnect), `useApi.ts` (typed API fetch wrappers), `useActiveHeading.ts` (scroll-based active heading tracking via IntersectionObserver)
- Utilities: `buildTree.ts` (converts flat file list to hierarchical tree with common prefix removal and single-child directory collapsing)
- Theme: GitHub-style light/dark via CSS custom properties (`--color-gh-*`) in `styles/app.css`, toggled by `data-theme` attribute on `<html>`. UI components use Tailwind classes like `bg-gh-bg-sidebar`, `text-gh-text-secondary`, etc.
- Toggle button pattern: `RawToggle.tsx` and `TocToggle.tsx` follow the same style (`bg-transparent border border-gh-border rounded-md p-1.5 text-gh-text-secondary`). Header buttons (`ViewModeToggle`, `ThemeToggle`, `WidthToggle`, sidebar toggle) use `text-gh-header-text` instead. New buttons should match the appropriate variant.

## Key Design Patterns

- **Single instance**: CLI probes `/_/api/status` on the target port via `probeServer()`. If already running, pushes files via `POST /_/api/files` and exits.
- **File IDs**: Files get deterministic string IDs derived from the SHA-256 hash of the absolute path (first 8 hex characters). IDs are stable across server restarts, enabling deep linking. The frontend primarily references files by ID. Absolute paths are available via `FileEntry.path` for display (e.g., tooltip, tree view).
- **Tab groups**: Files are organized into named groups. Group name maps to the URL path (e.g., `/design`). Default group name is `"default"`.
- **Live-reload via SSE**: fsnotify watches files; `file-changed` events trigger frontend to re-fetch content by file ID.
- **Sidebar view modes**: Flat (default, with drag-and-drop reorder via dnd-kit) and tree (hierarchical directory view). View mode is persisted per-group in localStorage. Collapsed directory state is managed inside `TreeView` and also persisted per-group.
- **Resizable panels**: Both `Sidebar.tsx` (left) and `TocPanel.tsx` (right) use the same drag-to-resize pattern with localStorage persistence. Left sidebar uses `e.clientX`, right panel uses `window.innerWidth - e.clientX`.
- **Toolbar buttons in content area**: The toolbar column (ToC + Raw toggles) lives inside `MarkdownViewer.tsx`, positioned with `shrink-0 flex flex-col gap-2 -mr-4 -mt-4` to align with the header.
- **State persistence**: Server state (files, groups, patterns) is backed up to `$XDG_STATE_HOME/markview/backup/markview-<port>.json` via `internal/backup`. On `--restart`, the server reloads this state to preserve the session. When starting a new server, backup is always restored and merged with CLI-specified files/patterns (restored entries first, CLI entries appended, duplicates skipped). The backup file is preserved across clean `--shutdown` and is only removed via the `--clear` path in the CLI.
- **Glob pattern watching**: `--watch` registers glob patterns that are expanded to matching files and monitored for new files via fsnotify directory watches. Patterns are stored with reference-counted directory watches (`watchedDirs map[string]int`). `--unwatch` removes patterns and decrements watch ref counts. Groups persist as long as they have files or patterns.
- **localStorage conventions**: All keys use `markview-` prefix (e.g., `markview-sidebar-width`, `markview-sidebar-viewmode`, `markview-sidebar-tree-collapsed`, `markview-theme`). Read patterns use `try/catch` around `JSON.parse` with fallback defaults.

## API Conventions

All internal API endpoints are under `/_/api/` and SSE under `/_/events`. The `/_/` prefix is intentional to avoid collisions with user-facing group name routes (e.g., `/mygroup`).
All internal endpoints use `/_/api/` prefix and SSE uses `/_/events`. The `/_/` prefix avoids collisions with user-facing group name routes.

Key endpoints:
- `GET /_/api/groups` — List all groups with files
Expand All @@ -80,30 +125,6 @@ Key endpoints:
- `GET /_/api/status` — Server status (version, pid, groups with patterns)
- `GET /_/events` — SSE (event types: `update`, `file-changed`, `restart`)

## Frontend

- Located in `internal/frontend/`, uses **pnpm** as the package manager.
- React 19, TypeScript, Tailwind CSS v4.
- Markdown rendering: `react-markdown` + `remark-gfm` + `rehype-raw` + `rehype-slug` (heading IDs) + `@shikijs/rehype` (syntax highlighting) + `mermaid` (diagram rendering).
- SPA routing via `window.location.pathname` (no router library).
- Key components: `App.tsx` (routing/state), `Sidebar.tsx` (file list with flat/tree view, resizable, drag-and-drop reorder), `TreeView.tsx` (tree view with collapsible directories), `MarkdownViewer.tsx` (rendering + raw view toggle), `TocPanel.tsx` (table of contents, resizable), `GroupDropdown.tsx` (group switcher), `FileContextMenu.tsx` (shared kebab menu for file operations), `WidthToggle.tsx` (wide/narrow content width toggle).
- Custom hooks: `useSSE.ts` (SSE subscription with auto-reconnect), `useApi.ts` (typed API fetch wrappers), `useActiveHeading.ts` (scroll-based active heading tracking via IntersectionObserver).
- Theme: GitHub-style light/dark via CSS custom properties (`--color-gh-*`) in `styles/app.css`, toggled by `data-theme` attribute on `<html>`. UI components use Tailwind classes like `bg-gh-bg-sidebar`, `text-gh-text-secondary`, etc.
- Toggle button pattern: `RawToggle.tsx` and `TocToggle.tsx` follow the same style (`bg-transparent border border-gh-border rounded-md p-1.5 text-gh-text-secondary`). Header buttons (`ViewModeToggle`, `ThemeToggle`, `WidthToggle`, sidebar toggle) use `text-gh-header-text` instead. New buttons should match the appropriate variant.

## Key Patterns

- **Single instance design**: CLI probes `/_/api/status` on the target port via `probeServer()`. If already running, pushes files via `POST /_/api/files` and exits.
- **File IDs**: Files get deterministic string IDs derived from the SHA-256 hash of the absolute path (first 8 hex characters). IDs are stable across server restarts, enabling deep linking. The frontend primarily references files by ID. Absolute paths are available via `FileEntry.path` for display.
- **Tab groups**: Files are organized into named groups (default: "default"). Group name maps to the URL path.
- **Live-reload via SSE**: fsnotify watches files; `file-changed` events trigger frontend to re-fetch content by file ID.
- **State persistence**: Server state (files, groups, patterns) is backed up to `$XDG_STATE_HOME/mo/backup/mo-<port>.json` via `internal/backup`. When starting a new server, backup is always restored and merged with CLI-specified files/patterns (restored entries first, CLI entries appended, duplicates skipped). The backup file is only deleted when the CLI is invoked with `--clear`.
- **Glob pattern watching**: `--watch` registers glob patterns that are expanded to matching files and monitored for new files via fsnotify directory watches. Patterns are stored with reference-counted directory watches (`watchedDirs map[string]int`). `--unwatch` removes patterns and decrements watch ref counts. Groups persist as long as they have files or patterns.
- **Resizable panels**: Both `Sidebar.tsx` (left) and `TocPanel.tsx` (right) use the same drag-to-resize pattern with localStorage persistence. Left sidebar uses `e.clientX`, right panel uses `window.innerWidth - e.clientX`.
- **Toolbar buttons in content area**: The toolbar column (ToC + Raw toggles) lives inside `MarkdownViewer.tsx`, positioned with `shrink-0 flex flex-col gap-2 -mr-4 -mt-4` to align with the header.
- **Sidebar view modes**: Flat (default, with drag-and-drop reorder via dnd-kit) and tree (hierarchical directory view). View mode is persisted per-group in localStorage. Collapsed directory state is managed inside `TreeView` and also persisted per-group.
- **localStorage conventions**: All keys use `mo-` prefix (e.g., `mo-sidebar-width`, `mo-sidebar-viewmode`, `mo-sidebar-tree-collapsed`, `mo-theme`). Read patterns use `try/catch` around `JSON.parse` with fallback defaults.

## CI/CD

- **CI**: golangci-lint (via reviewdog), gostyle, `make ci` (test + coverage), octocov
Expand Down
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ updates:
- "k1LoW"

- package-ecosystem: "npm"
directory: "/internal/frontend"
directory: "/frontend"
groups:
dependencies:
patterns:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
os: [ ubuntu-latest, macos-latest ]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
Expand All @@ -31,18 +31,18 @@ jobs:
- name: Set up pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
package_json_file: internal/frontend/package.json
package_json_file: frontend/package.json

- name: Generate frontend assets
run: go generate ./internal/static/

- name: Run oxlint
if: runner.os == 'Linux'
run: cd internal/frontend && pnpm install && pnpm run lint
run: cd frontend && pnpm install && pnpm run lint

- name: Run oxfmt check
if: runner.os == 'Linux'
run: cd internal/frontend && pnpm run fmt:check
run: cd frontend && pnpm run fmt:check

- name: Run lint
if: runner.os == 'Linux'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tagpr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Set up pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
package_json_file: internal/frontend/package.json
package_json_file: frontend/package.json

- id: run-tagpr
name: Run tagpr
Expand Down Expand Up @@ -60,7 +60,7 @@ jobs:
- name: Set up pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
package_json_file: internal/frontend/package.json
package_json_file: frontend/package.json

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
internal/static/dist/**/*
internal/frontend/CREDITS_FRONTEND
frontend/CREDITS_FRONTEND
coverage.out
mo
markview
.pnpm-store/
40 changes: 18 additions & 22 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,34 @@ before:
- go mod tidy
- go generate ./internal/static/
builds:
-
id: mo-darwin
- id: markview-darwin
flags:
- -trimpath
ldflags:
- -s -w -X github.com/k1LoW/mo/version.Revision={{.ShortCommit}}
- -s -w -X github.com/kooksee/markview/version.Revision={{.ShortCommit}}
env:
- CGO_ENABLED=0
goos:
- darwin
goarch:
- amd64
- arm64
-
id: mo-windows
- id: markview-windows
flags:
- -trimpath
ldflags:
- -s -w -X github.com/k1LoW/mo/version.Revision={{.ShortCommit}}
- -s -w -X github.com/kooksee/markview/version.Revision={{.ShortCommit}}
env:
- CGO_ENABLED=0
goos:
- windows
goarch:
- amd64
-
id: mo-linux
- id: markview-linux
flags:
- -trimpath
ldflags:
- -s -w -X github.com/k1LoW/mo/version.Revision={{.ShortCommit}}
- -s -w -X github.com/kooksee/markview/version.Revision={{.ShortCommit}}
env:
- CGO_ENABLED=0
goos:
Expand All @@ -44,9 +41,9 @@ builds:
- amd64
- arm64
archives:
-
id: mo-archive
name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
- id: markview-archive
name_template: "{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if
.Arm }}v{{ .Arm }}{{ end }}"
format_overrides:
- goos: darwin
formats:
Expand All @@ -57,28 +54,27 @@ archives:
- README.md
- CHANGELOG.md
checksum:
name_template: 'checksums.txt'
name_template: "checksums.txt"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- "^docs:"
- "^test:"
nfpms:
-
id: mo-nfpms
- id: markview-nfpms
file_name_template: "{{ .ProjectName }}_{{ .Version }}-1_{{ .Arch }}"
ids:
- mo-linux
homepage: https://github.com/k1LoW/mo
maintainer: Ken'ichiro Oyama <k1lowxb@gmail.com>
description: "mo is a Markdown viewer that opens .md files in a browser"
- markview-linux
homepage: https://github.com/kooksee/markview
maintainer: barry <kooksee@163.com>
description: "markview is a Markdown viewer that opens .md files in a browser"
license: MIT
formats:
- deb
- rpm
- apk
bindir: /usr/bin
epoch: 1
epoch: "1"
release:
use_existing_draft: true
22 changes: 11 additions & 11 deletions .octocov.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
coverage:
paths:
- coverage.out
- internal/frontend/coverage/lcov.info
- frontend/coverage/lcov.info
codeToTestRatio:
code:
- '**/*.go'
- '!**/*_test.go'
- 'internal/frontend/src/**/*.ts'
- 'internal/frontend/src/**/*.tsx'
- '!internal/frontend/src/**/*.test.ts'
- '!internal/frontend/src/**/*.test.tsx'
- '!internal/frontend/node_modules/**'
- "**/*.go"
- "!**/*_test.go"
- "frontend/src/**/*.ts"
- "frontend/src/**/*.tsx"
- "!frontend/src/**/*.test.ts"
- "!frontend/src/**/*.test.tsx"
- "!frontend/node_modules/**"
test:
- '**/*_test.go'
- 'internal/frontend/src/**/*.test.ts'
- 'internal/frontend/src/**/*.test.tsx'
- "**/*_test.go"
- "frontend/src/**/*.test.ts"
- "frontend/src/**/*.test.tsx"
testExecutionTime:
if: true
diff:
Expand Down
Loading
Loading