{"id":50687821,"url":"https://github.com/selamet/pretty-lush","last_synced_at":"2026-06-09T00:04:33.906Z","repository":{"id":357457321,"uuid":"1237024963","full_name":"selamet/pretty-lush","owner":"selamet","description":"In-browser code formatter for 12 languages — Prettier, Ruff, sql-formatter. Encrypted sharing, JSONPath, JWT decoder, offline PWA. Your code never leaves the tab","archived":false,"fork":false,"pushed_at":"2026-05-12T21:52:08.000Z","size":413,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-12T22:27:22.312Z","etag":null,"topics":["code-formatter","codemirror","developer-tools","formatter","prettier","react","snippet-sharing","yaml-formatter"],"latest_commit_sha":null,"homepage":"https://pretty-lush.selamet.dev","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/selamet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-12T20:00:06.000Z","updated_at":"2026-05-12T21:52:12.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/selamet/pretty-lush","commit_stats":null,"previous_names":["selamet/pretty-lush"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/selamet/pretty-lush","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/selamet%2Fpretty-lush","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/selamet%2Fpretty-lush/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/selamet%2Fpretty-lush/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/selamet%2Fpretty-lush/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/selamet","download_url":"https://codeload.github.com/selamet/pretty-lush/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/selamet%2Fpretty-lush/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34085343,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-formatter","codemirror","developer-tools","formatter","prettier","react","snippet-sharing","yaml-formatter"],"created_at":"2026-06-09T00:04:33.782Z","updated_at":"2026-06-09T00:04:33.900Z","avatar_url":"https://github.com/selamet.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pretty-lush\n\nA fast, private code formatter that runs **entirely in your browser**. Paste code, hit Format, copy the result. No uploads, no accounts, no telemetry.\n\nLive: **\u003chttps://pretty-lush.selamet.dev\u003e**\n\nPowered by real formatters compiled to WebAssembly or pure JavaScript:\n\n- **Prettier** — JSON · YAML · Markdown · CSS · HTML · JavaScript · TypeScript · JSX · TSX · Vue SFC\n- **Ruff** — Python (black-compatible)\n- **sh-syntax** — Shell / Bash\n- **sql-formatter** — SQL (Postgres / MySQL / SQLite / BigQuery / …)\n- Built-in heuristics — Dockerfile, Dotenv\n\n---\n\n## Highlights\n\n**Formatter**\n\n- 15 languages out of the box: Python · JSON · YAML · Shell · Dockerfile · JavaScript · TypeScript · JSX · TSX · Vue SFC · HTML · CSS · Markdown · Dotenv · SQL\n- Auto-detect language from pasted content or dropped files\n- Drag-and-drop a file to load it (extension picks the language)\n- Side-by-side editor with syntax highlighting (CodeMirror 6)\n- Inline diff view between input and output\n- Configurable indent (2 / 4 / tab), line width, quote style\n- Auto-format on type (debounced) and Format-on-paste options\n- Local format history (last 20, click to restore)\n\n**JSON power tools**\n\n- **JSONPath filter** — type an expression (`$.items[*].id`, `$..name`) in the JSON pane and the formatted output is the matched values\n- **Table view** — when output is an array of objects, toggle between Code and a sticky-header table\n\n**Compare mode**\n\n- A standalone two-snippet diff (A vs B) with its own pair of editors, live diff below, Swap and Clear actions\n- Both bodies persist across reloads\n\n**Text utilities (⌘K palette)**\n\n- Lines: Sort A→Z / Z→A, Dedupe, Reverse, Trim trailing whitespace, Collapse blank lines\n- Case: UPPERCASE · lowercase · Title Case\n- Encoding: Base64 / URL percent / Hex — encode \u0026 decode in both directions\n- **JWT decoder** with a small modal showing header, payload, signature, issued/expires meta and an `expired` badge\n- Unix timestamp ↔ ISO date\n- **Python ↔ JSON** — paste a `repr(dict)` or `print(dict)` output and convert to JSON (handles `True`/`False`/`None`, single-quoted strings, tuples, sets, hex / underscored ints, triple-quoted strings); JSON → Python emits a properly indented Python literal with single quotes\n\n**Sharing**\n\n- **URL-only** mode — full snippet encoded into `#hash`, no backend\n- **Encrypted link** mode — content is AES-GCM 256 encrypted in the browser, ciphertext stored on the server, the decryption key lives only in the URL fragment (browsers never send it). Optional password derives a second key via PBKDF2 (250k iterations) so a leaked URL alone cannot decrypt.\n- **Secret mode toggle** — shorter TTL default (1h), password recommended\n- **Self-destruct on read** — paste is deleted the moment the first viewer decrypts it\n- **Copy as PNG** with mac-style window chrome + watermark for nice Twitter / blog snippets\n- **Copy as Markdown code block** — wraps output in ``` ```lang ``` `` fences\n\n**Installable PWA**\n\n- Works fully offline after first load — all formatters precached, including the ~10 MB Ruff WASM\n- iOS / Android \"Add to Home Screen\" treats it as a native app\n- Service worker is `NetworkOnly` for `/api/*` so encrypted snippets are never cached\n\n**Quality of life**\n\n- ⌘K command palette — every action is a command, fuzzy-searchable\n- Find \u0026 replace in any editor (`⌘F`), and `⌘G` selects every occurrence of the current selection / word into a multi-cursor\n- Output pane fullscreen mode\n- Resizable sidebar and input/output split (drag the dividers, layout persists)\n- Light / dark theme with 8 popular editor themes (GitHub · Dracula · One Dark · Tokyo Night · Solarized · …)\n- Works on mobile (sidebar collapses, panes stack)\n\n---\n\n## Stack\n\n| Layer | Choice |\n|---|---|\n| Build | Vite 5 + React 18 + `vite-plugin-pwa` |\n| Editor | CodeMirror 6 (`@uiw/react-codemirror`) |\n| Formatters | Prettier 3 standalone · `@astral-sh/ruff-wasm-web` · `sh-syntax` · `sql-formatter` |\n| Diff | `diff` (jsdiff) |\n| JSON queries | `jsonpath-plus` |\n| PNG export | `html-to-image` |\n| Themes | `@uiw/codemirror-theme-*` family |\n| Typography | Geist + Geist Mono (Google Fonts) |\n| Backend (optional) | Vercel Functions + Vercel KV / Upstash Redis |\n\nInitial JS bundle: ~310 KB gzipped. Heavy formatters (Ruff WASM ≈10 MB, sh-syntax) and the JSONPath / PNG-export libraries are lazy-loaded only when first used.\n\n---\n\n## Getting started\n\n```bash\ngit clone https://github.com/selamet/pretty-lush.git\ncd pretty-lush\nnpm install\nnpm run dev      # http://localhost:5173\n```\n\nProduction build:\n\n```bash\nnpm run build\nnpm run preview  # http://localhost:4173\n```\n\nThe output of `npm run build` is a fully static site in `dist/` plus the `api/` functions — drop the whole project on Vercel for a one-click deploy, or take only `dist/` to any static host (Netlify, Cloudflare Pages, S3, GitHub Pages — without the encrypted-share endpoint).\n\n### Optional: encrypted share backend\n\nThe \"encrypted link\" share mode talks to two tiny serverless functions in `api/`. They use any Vercel-KV-compatible Redis (Upstash REST). Two env vars enable it:\n\n```bash\nKV_REST_API_URL=...\nKV_REST_API_TOKEN=...\n```\n\n- **Vercel KV** — add the integration from the project dashboard, env vars are injected automatically.\n- **Upstash Redis** directly — free tier (10k commands/day, 256 MB) is plenty for personal use. Copy the REST URL and token from the Upstash console into the two env vars above.\n\nWithout these env vars:\n\n- **Local `npm run dev`** still works — the API falls back to an in-memory map for the dev session.\n- **Production** returns a clean `503` from `/api/paste`, and the share dialog automatically offers to switch to URL-only mode.\n\nThe URL-only share (`#s=...`) needs no backend and is always available.\n\n---\n\n## How it works\n\nEverything runs client-side. Formatters are dynamically imported the first time their language is requested, so:\n\n- **JSON / YAML / Markdown / CSS / HTML / JS / TS / JSX / TSX** fetch the relevant Prettier parser chunk.\n- **Vue SFC** fetches Prettier's `html` + `babel` + `typescript` + `postcss` plugins together so each `\u003ctemplate\u003e` / `\u003cscript\u003e` / `\u003cstyle\u003e` block formats with its native parser.\n- **JSON** is then re-serialized via `JSON.stringify` for canonical multi-line output (Prettier's `json` parser keeps short objects on one line).\n- **Python** boots a Ruff WebAssembly workspace (cached for subsequent calls).\n- **Shell** loads a small Go-WASM wrapper around `mvdan/sh`.\n- **SQL** loads `sql-formatter`, dialect-detected from the content.\n- **Dockerfile** uses an in-house heuristic (uppercase instructions, expand long `RUN … \u0026\u0026 …` chains, expand long `CMD` / `ENTRYPOINT` JSON arrays).\n- **Dotenv** normalizes `KEY = value` spacing, preserves quoted values and inline `#` comments outside quotes.\n\nWASM URLs are resolved via Vite's `?url` import so they resolve correctly in both dev and production.\n\n---\n\n## Keyboard shortcuts\n\n| Key | Action |\n|---|---|\n| `⌘⏎` / `Ctrl+Enter` | Format |\n| `⌘K` / `Ctrl+K` | Open command palette |\n| `⌘F` / `Ctrl+F` | Find in editor |\n| `⌘G` / `Ctrl+G` | Select all matches of current selection (multi-cursor) |\n| `Esc` | Close palette / search / settings popover / dialogs / fullscreen |\n\nThe ⌘K palette is the canonical surface — every formatter, theme, text utility, view-mode switch and share action is reachable from there. Use it as a discovery tool: type `jwt`, `base64`, `sort`, `compare`, `dracula`, `secret`…\n\n---\n\n## Project layout\n\n```\npretty-lush/\n├── public/                          # static assets (og.svg, robots, sitemap)\n├── api/                             # Vercel serverless functions\n│   ├── _store.js                    # KV / in-memory storage adapter\n│   ├── paste.js                     # POST — store encrypted paste\n│   └── paste/[id].js                # GET  — retrieve (and DEL if burnAfterRead)\n├── src/\n│   ├── main.jsx                     # React entry\n│   ├── App.jsx                      # main shell — state, topbar, sidebar, panes, palette wiring\n│   ├── styles.css                   # all styling, light + dark + theme overrides\n│   ├── editor-themes.js             # editor theme registry + flipTheme helper\n│   │\n│   ├── components/                  # React UI building blocks\n│   │   ├── CodeEditor.jsx            #   CodeMirror wrapper + multi-cursor + match-count\n│   │   ├── CommandPalette.jsx        #   ⌘K palette\n│   │   ├── CopyMarkdownButton.jsx    #   copy output as fenced code block\n│   │   ├── DiffView.jsx              #   input ↔ output line diff\n│   │   ├── JwtModal.jsx              #   modal that renders a decoded JWT\n│   │   ├── Resizer.jsx               #   drag handle for layout splits\n│   │   └── ShareDialog.jsx           #   share modal + password prompt + toggles\n│   │\n│   ├── formatters/                  # one module per backend; index.js dispatches\n│   │   ├── index.js                  #   formatCode(lang, src, opts) + FormatError re-export\n│   │   ├── errors.js                 #   FormatError + Prettier / Ruff / sh error mappers\n│   │   ├── helpers.js                #   shared structural helpers\n│   │   ├── prettier.js               #   Prettier standalone + lazy-loaded plugins\n│   │   ├── json.js                   #   JSON.parse + JSONPath + JSON.stringify\n│   │   ├── python.js                 #   Ruff WASM workspace\n│   │   ├── shell.js                  #   sh-syntax / mvdan-sh WASM\n│   │   ├── sql.js                    #   sql-formatter\n│   │   ├── dockerfile.js             #   in-house heuristic\n│   │   └── dotenv.js                 #   in-house heuristic\n│   │\n│   ├── languages/                   # single source of truth for \"what languages exist\"\n│   │   ├── registry.js               #   LANGUAGES, SAMPLES, EXT_TO_LANG, MARKDOWN_LANG_TAGS\n│   │   ├── detect.js                 #   detectLangFromContent / detectLangFromFilename\n│   │   └── codemirror.js             #   LANG_MAP for CodeMirror 6\n│   │\n│   ├── share/                       # everything that talks to the share backend\n│   │   ├── crypto.js                 #   AES-GCM 256 + optional PBKDF2-derived password key\n│   │   └── url-share.js              #   URL-fragment encode / decode helpers\n│   │\n│   └── text-utils/                  # pure string transforms used by the ⌘K palette\n│       ├── index.js                  #   barrel for ergonomic imports\n│       ├── lines.js                  #   sort, dedupe, reverse, trim, collapse\n│       ├── case.js                   #   upper / lower / title\n│       ├── encoding.js               #   base64 / url-percent / hex (both directions)\n│       ├── jwt.js                    #   decodeJwt\n│       ├── timestamp.js              #   unix ↔ ISO\n│       └── python-json.js            #   Python literal ↔ JSON converter\n├── index.html                       # title / meta / OG / PWA tags\n├── vite.config.js                   # vite + react + PWA config\n└── package.json\n```\n\nAdding a new language is a focused, three-file change: write `src/formatters/\u003clang\u003e.js`, add a case to `src/formatters/index.js`, and register the entry in `src/languages/registry.js` + `src/languages/codemirror.js`.\n\nState is stored in `localStorage`:\n\n- `pretty-lush:state:v1` — current language + per-language inputs\n- `pretty-lush:settings:v1` — indent, line width, quotes, editor theme, auto-format, format-on-paste\n- `pretty-lush:history:v1` — recent format entries\n- `pretty-lush:layout:v1` — sidebar width + input/output split ratio\n- `pretty-lush:compare:v1` — Compare mode A / B bodies\n- `pretty-lush:theme` — last applied light/dark mode (for chrome)\n\n---\n\n## Privacy\n\nThis is a static SPA. Source code you paste lives in your tab — it is never sent to any server controlled by this project, including for analytics. The privacy pill in the top bar (`runs in your browser`) is literal: every formatter is local.\n\nThe optional **encrypted share link** is the only feature that talks to a backend. Even then the snippet is AES-GCM encrypted in the browser before upload and the decryption key never leaves the URL fragment (which browsers never send to servers). With a password, a second key is derived via PBKDF2 (250k iterations) and combined with the random key, so a leaked URL alone cannot decrypt. With **self-destruct on read**, the ciphertext is deleted from the server the moment it is first decrypted — even the encrypted blob doesn't outlive the first viewer.\n\n---\n\n## Contributing\n\nIssues, language requests, formatter tweaks, theme PRs all welcome. The codebase is small (one App component, a handful of helpers) and intentionally has no test framework — keeping the loop tight matters more than coverage here. Run `npm run dev`, edit, refresh. Ship.\n\n---\n\n## License\n\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fselamet%2Fpretty-lush","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fselamet%2Fpretty-lush","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fselamet%2Fpretty-lush/lists"}