diff --git a/README.md b/README.md index d848278..d6670b1 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,47 @@ -
- TexSet -

TexSet

-

A fast, local-first LaTeX editor that runs in your browser.

-
- -TexSet is an Overleaf-style editor you run yourself. Write LaTeX on the left, -watch the PDF build on the right. Everything happens on your machine: no -accounts, no external APIs, no cloud. Your documents are just files in a folder -you control. - -It's built to be modular. Today it compiles LaTeX with xelatex; the editor and -compiler are wired through an engine abstraction so Typst support can drop in -later without reworking the app. The accent color even follows the document type -(green for LaTeX) so you always know what you're editing. - -> **Status:** active development. The foundation is in place and features are -> landing branch by branch. +# TexSet + +A fast, local-first LaTeX editor. Write LaTeX on the left, watch the PDF build on +the right. Everything runs on your machine: no accounts, no external APIs, no +cloud. Your documents are just files in a folder you control. + +> **In active development.** TexSet is usable today but still growing, and not +> everything is polished yet. Bug reports, ideas, and feedback are very welcome — +> open an issue and let me know what works and what doesn't. + +It's built to be modular. Today it compiles LaTeX with xelatex (driven by +latexmk); the editor and compiler sit behind an engine abstraction so Typst +support can drop in later. The accent color follows the document type (green for +LaTeX) so you always know what you're editing. + +## What you can do + +- **Editor** — CodeMirror 6 with LaTeX highlighting, a formatting toolbar (bold, + italic, headings, lists, math), and autocomplete for commands, environments, + packages, and your own `\ref`/`\cite`/`\label`. +- **Live preview** — the PDF rebuilds as you type (2s debounce) with a manual + compile button, pinch-to-zoom, and zoom controls. Errors are listed and click + to jump to the offending line. +- **Real LaTeX** — latexmk runs bibtex/biber and makeindex and reruns as needed, + so citations, bibliographies, indexes, and cross-references resolve. The Docker + image ships the full TeX Live plus common fonts. +- **Multiple files** — add and edit several `.tex` files, with `\input`/`\include` + from the main one. +- **Images & assets** — upload images (drag and drop), preview them, insert + `\includegraphics`, and rename or delete files. +- **Projects** — a gallery with Word-like previews, pin to top, rename, and + delete with confirmation. +- **Templates** — article, letter, résumé, and a Beamer presentation, each with a + preview. +- **Import / export** — open a `.tex` as a new project; download your source or + any asset. +- **Dark mode** — a light/dark toggle that's remembered between visits. +- **Keyboard shortcuts** — save (`Ctrl/Cmd+S`) and compile (`Ctrl/Cmd+Enter`). ## Running it -The quickest way is Docker, which bundles Node and a TeX Live install so you -don't have to set up a LaTeX toolchain yourself. +### Docker (recommended) + +Bundles Node and a full TeX Live, so you don't set up a LaTeX toolchain yourself. ```bash git clone https://github.com/texset/texset.git @@ -28,27 +49,29 @@ cd texset docker compose up --build ``` -Then open http://localhost:7474. Your projects appear in `./projects` on your -machine. +Open http://localhost:7474. Your projects appear in `./projects` on your machine. +The first build is large because it installs TeX Live; later builds are fast. +See [docs/SELF_HOSTING.md](docs/SELF_HOSTING.md) for configuration. -See [docs/SELF_HOSTING.md](docs/SELF_HOSTING.md) for configuration and other -deployment notes. +### Desktop app -## Local development +A desktop build (Electron) is in progress so the app can be installed from the +Microsoft Store and run without Docker. It bundles a small TeX distribution +(TinyTeX) and falls back to a system install if you already have one. See +[desktop/README.md](desktop/README.md). -You'll need Node.js 20+, pnpm, and a working `xelatex` on your `PATH`. +### Local development + +You'll need Node.js 20+, pnpm, and — to actually compile — a LaTeX engine on your +`PATH` (TeX Live or MiKTeX). Without one, the app runs and shows a banner +explaining how to install it. ```bash pnpm install pnpm dev ``` -The dev server runs on http://localhost:7474. There's also a Docker dev setup -with hot reload that includes TeX Live, if you'd rather not install it locally: - -```bash -docker compose -f docker-compose.dev.yml up -``` +The dev server runs on http://localhost:7474. ## How it's built @@ -60,15 +83,16 @@ docker compose -f docker-compose.dev.yml up | Editor | CodeMirror 6 | | PDF viewer | pdf.js | | Index | SQLite (better-sqlite3) | -| TeX engine | xelatex (TeX Live) | +| TeX engine | latexmk + xelatex (TeX Live) | | Container | Docker, node:20-bookworm-slim | More detail in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). -## Contributing +## Contributing & feedback -Contributions are welcome. Start with [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) -for the branching and pull request workflow. +Contributions and feedback are welcome — see +[docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for the branching and pull request +workflow, and the issue templates for bug reports and feature requests. ## License diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 0000000..266875b --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist + +# assembled at build time +resources/standalone +resources/tinytex diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 0000000..142d224 --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,76 @@ +# TexSet desktop + +An Electron shell that runs TexSet as a native app — no Docker, no separate +LaTeX install for most people. It bundles a small TeX distribution (TinyTeX) and +prefers a system LaTeX install if you already have one. + +Electron is used (rather than Tauri) because TexSet has a Node backend — the +Next.js API, `better-sqlite3`, and spawning `latexmk` — so the standalone server +runs inside the app via Electron's Node. + +> Status: scaffold. The build has to be assembled and tested on the target OS +> (Windows for the Microsoft Store). The notes below are the steps; expect to +> iterate, especially around the native module and TinyTeX. + +## How it works + +`main.js` starts `resources/standalone/server.js` (the Next standalone server) on +`127.0.0.1:7475`, then opens a window pointing at it. It sets: + +- `TEXSET_DATA_DIR` to the OS user-data folder, so projects persist per user. +- `TEXSET_TEX_BIN_DIR` to the bundled TinyTeX `bin` folder. The app prefers a + system LaTeX engine and only falls back to this (see `src/lib/tex.ts`). + +## Building (outline) + +Run these on the OS you're targeting. For the Microsoft Store, that's Windows. + +1. **Build the web app** (repo root): + ```bash + pnpm install + pnpm build + ``` + +2. **Assemble the server bundle** into `desktop/resources/standalone`: + - copy `.next/standalone` → `desktop/resources/standalone` + - copy `.next/static` → `desktop/resources/standalone/.next/static` + - copy `public` → `desktop/resources/standalone/public` + +3. **Rebuild the native module for Electron.** `better-sqlite3` ships a binary + built for system Node; it must match Electron's Node ABI. From `desktop/`, + after `pnpm install`, run `electron-rebuild` against + `resources/standalone/node_modules` (or reinstall `better-sqlite3` there with + Electron headers). This is the step most likely to need attention. + +4. **Bundle TinyTeX** into `desktop/resources/tinytex`. Install TinyTeX + (https://yihui.org/tinytex/) for the target platform and copy its folder here + so `resources/tinytex/bin//latexmk` exists. TinyTeX installs missing + packages on demand (needs internet the first time for uncommon ones). + +5. **Package**: + ```bash + cd desktop + pnpm install + pnpm dist:win # or dist:mac / dist:linux + ``` + +## Microsoft Store + +`electron-builder`'s `appx` target produces an `.appx`/`.msix`. To publish: + +- Register on Partner Center and create the app listing to get your identity + values, then set `build.appx.identityName`, `publisher`, and + `publisherDisplayName` in `package.json` to match. +- Build the package, then upload it through Partner Center. + +The MSIX runs the bundled `server.js` and TinyTeX binaries from inside the package +(allowed), so no external install is required at runtime. + +## Try it in dev + +With the web app built and `resources/standalone` assembled, from `desktop/`: + +```bash +pnpm install +pnpm start +``` diff --git a/desktop/main.js b/desktop/main.js new file mode 100644 index 0000000..e23f73f --- /dev/null +++ b/desktop/main.js @@ -0,0 +1,94 @@ +// Electron shell for TexSet. It runs the Next.js standalone server in the +// background (with Electron's own Node via ELECTRON_RUN_AS_NODE) and shows it in +// a window. A small bundled TeX distribution (TinyTeX) is pointed at through +// TEXSET_TEX_BIN_DIR; if the user already has a system LaTeX install, the app +// prefers that one (see src/lib/tex.ts). +const { app, BrowserWindow, shell } = require("electron"); +const { spawn } = require("node:child_process"); +const path = require("node:path"); +const http = require("node:http"); + +const PORT = 7475; +const ORIGIN = `http://127.0.0.1:${PORT}`; + +let serverProcess = null; + +// In a packaged build, resources live under process.resourcesPath. In dev we run +// against the repo so you can iterate without packaging. +function resource(...parts) { + return app.isPackaged + ? path.join(process.resourcesPath, ...parts) + : path.join(__dirname, "..", ...parts); +} + +// TinyTeX lays its binaries out per platform. +function tinytexBinDir() { + const platformDir = + process.platform === "win32" + ? "windows" + : process.platform === "darwin" + ? "universal-darwin" + : "x86_64-linux"; + return resource("tinytex", "bin", platformDir); +} + +function startServer() { + const serverDir = resource("standalone"); + serverProcess = spawn(process.execPath, [path.join(serverDir, "server.js")], { + cwd: serverDir, + env: { + ...process.env, + ELECTRON_RUN_AS_NODE: "1", + PORT: String(PORT), + HOSTNAME: "127.0.0.1", + TEXSET_DATA_DIR: path.join(app.getPath("userData"), "projects"), + TEXSET_TEX_BIN_DIR: tinytexBinDir(), + }, + stdio: "inherit", + }); + serverProcess.on("exit", () => { + serverProcess = null; + }); +} + +// Wait until the server answers before showing the window. +function whenServerReady(onReady, attempt = 0) { + http + .get(ORIGIN, () => onReady()) + .on("error", () => { + if (attempt > 100) return; // ~20s, give up quietly + setTimeout(() => whenServerReady(onReady, attempt + 1), 200); + }); +} + +function createWindow() { + const win = new BrowserWindow({ + width: 1280, + height: 800, + title: "TexSet", + backgroundColor: "#0f1115", + webPreferences: { contextIsolation: true }, + }); + win.loadURL(ORIGIN); + // open external links (install guides, etc.) in the system browser + win.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: "deny" }; + }); +} + +app.whenReady().then(() => { + startServer(); + whenServerReady(createWindow); + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); + }); +}); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") app.quit(); +}); + +app.on("quit", () => { + serverProcess?.kill(); +}); diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..0283e3c --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,44 @@ +{ + "name": "texset-desktop", + "version": "0.1.0", + "private": true, + "description": "Desktop shell for TexSet.", + "main": "main.js", + "scripts": { + "start": "electron .", + "dist:win": "electron-builder --win", + "dist:mac": "electron-builder --mac", + "dist:linux": "electron-builder --linux" + }, + "devDependencies": { + "electron": "^33.0.0", + "electron-builder": "^25.1.8" + }, + "build": { + "appId": "com.texset.app", + "productName": "TexSet", + "directories": { + "output": "dist" + }, + "files": ["main.js"], + "extraResources": [ + { "from": "resources/standalone", "to": "standalone" }, + { "from": "resources/tinytex", "to": "tinytex" } + ], + "win": { + "target": ["appx", "nsis"] + }, + "appx": { + "identityName": "TexSet", + "publisher": "CN=YOUR_PUBLISHER_ID", + "publisherDisplayName": "Your Name" + }, + "mac": { + "target": ["dmg"], + "category": "public.app-category.productivity" + }, + "linux": { + "target": ["AppImage"] + } + } +} diff --git a/src/app/api/tex-status/route.ts b/src/app/api/tex-status/route.ts new file mode 100644 index 0000000..9d7e05b --- /dev/null +++ b/src/app/api/tex-status/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from "next/server"; +import { isTexAvailable } from "@/lib/tex"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +// Tells the UI whether a LaTeX engine is reachable, so it can warn up front +// instead of letting a compile mysteriously fail. +export function GET() { + return NextResponse.json({ available: isTexAvailable() }); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f129a9d..ee6660e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata, Viewport } from "next"; import { Inter, JetBrains_Mono } from "next/font/google"; +import { TexStatusBanner } from "@/components/TexStatusBanner"; import "./globals.css"; // fonts are self-hosted at build time, so this stays fully offline @@ -45,7 +46,10 @@ export default function RootLayout({