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
96 changes: 60 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,77 @@
<div align="center">
<img src="public/TexSet.svg" alt="TexSet" width="72" height="72" />
<h1>TexSet</h1>
<p>A fast, local-first LaTeX editor that runs in your browser.</p>
</div>

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
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

Expand All @@ -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

Expand Down
6 changes: 6 additions & 0 deletions desktop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist

# assembled at build time
resources/standalone
resources/tinytex
76 changes: 76 additions & 0 deletions desktop/README.md
Original file line number Diff line number Diff line change
@@ -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/<platform>/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
```
94 changes: 94 additions & 0 deletions desktop/main.js
Original file line number Diff line number Diff line change
@@ -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();
});
44 changes: 44 additions & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
}
11 changes: 11 additions & 0 deletions src/app/api/tex-status/route.ts
Original file line number Diff line number Diff line change
@@ -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() });
}
6 changes: 5 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -45,7 +46,10 @@ export default function RootLayout({
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body>{children}</body>
<body>
<TexStatusBanner />
{children}
</body>
</html>
);
}
Loading
Loading