summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-07-30 09:00:55 +0800
committerhaoyuren <13851610112@163.com>2026-07-30 09:00:55 +0800
commit7297f7c339186f8af50f4f0ed3734e4ad4b47aed (patch)
tree9c5b02d50bf7ab5ac64b07ba436f9e2319a87208
parentc9cd5119077c69088eb8ba768ee1e1602baec751 (diff)
Cross-platform support: Windows build target and CI release workflow
- Normalize watcher paths to forward slashes (Windows events use backslashes; all sync maps are '/'-keyed), path.delimiter for PATH/TEXINPUTS, dirname() instead of '/'-substring splits, bridge log in os.tmpdir() - Platform-aware TeX search paths (TeX Live / MiKTeX on Windows), terminal shell default (COMSPEC/powershell), native window frame off macOS - electron-builder: NSIS target + win icon + GitHub publish provider - GitHub Actions workflow builds the Windows installer on tag push (node-pty needs MSVC — cannot cross-compile from macOS) and attaches it to the release Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
-rw-r--r--.github/workflows/release-windows.yml34
-rw-r--r--README.md5
-rw-r--r--electron-builder.yml10
-rw-r--r--resources/icon.pngbin0 -> 34710 bytes
-rw-r--r--src/main/compilationManager.ts28
-rw-r--r--src/main/fileSyncBridge.ts17
-rw-r--r--src/main/index.ts29
7 files changed, 96 insertions, 27 deletions
diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml
new file mode 100644
index 0000000..fb557a5
--- /dev/null
+++ b/.github/workflows/release-windows.yml
@@ -0,0 +1,34 @@
+# Builds the Windows installer on a Windows runner (node-pty must be
+# compiled with MSVC, which cannot be cross-compiled from macOS) and
+# attaches it to the GitHub release for the pushed tag.
+name: Release (Windows)
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ windows:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+
+ - name: Package and publish Windows installer
+ run: npx electron-builder --win --publish always
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index 046433b..a648000 100644
--- a/README.md
+++ b/README.md
@@ -30,8 +30,9 @@ Download the latest `.dmg` from [Releases](https://github.com/YurenHao0426/latte
### Requirements
-- macOS (Apple Silicon)
-- [TeX Live](https://www.tug.org/texlive/) or [MacTeX](https://www.tug.org/mactex/) for local compilation
+- macOS (Apple Silicon) — primary platform
+- Windows 10+ (x64) — experimental installer built by CI (`lattex-*-win-x64.exe`); expects [TeX Live](https://www.tug.org/texlive/) or [MiKTeX](https://miktex.org/) for local compilation
+- [TeX Live](https://www.tug.org/texlive/) or [MacTeX](https://www.tug.org/mactex/) for local compilation on macOS
## Recommended: Claude Code
diff --git a/electron-builder.yml b/electron-builder.yml
index 6f74aa6..fba8e54 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -34,3 +34,13 @@ mac:
- dmg
category: public.app-category.productivity
artifactName: ${name}-${version}-${arch}.${ext}
+win:
+ icon: resources/icon.png
+ target:
+ - nsis
+ artifactName: ${name}-${version}-win-${arch}.${ext}
+nsis:
+ oneClick: false
+ allowToChangeInstallationDirectory: true
+publish:
+ provider: github
diff --git a/resources/icon.png b/resources/icon.png
new file mode 100644
index 0000000..a3762f0
--- /dev/null
+++ b/resources/icon.png
Binary files differ
diff --git a/src/main/compilationManager.ts b/src/main/compilationManager.ts
index 72c0c5e..2e27476 100644
--- a/src/main/compilationManager.ts
+++ b/src/main/compilationManager.ts
@@ -2,7 +2,7 @@
// Licensed under AGPL-3.0 - see LICENSE file
// Manages temp directory for Overleaf socket-mode compilation
-import { join, basename } from 'path'
+import { join, basename, dirname, delimiter } from 'path'
import { writeFile, mkdir, rm } from 'fs/promises'
import { existsSync } from 'fs'
import { spawn } from 'child_process'
@@ -42,7 +42,7 @@ export class CompilationManager {
await mkdir(this.tmpDir, { recursive: true })
for (const [relPath, content] of this.docContents) {
const fullPath = join(this.tmpDir, relPath)
- const dir = fullPath.substring(0, fullPath.lastIndexOf('/'))
+ const dir = dirname(fullPath)
await mkdir(dir, { recursive: true })
await writeFile(fullPath, content, 'utf-8')
}
@@ -53,7 +53,7 @@ export class CompilationManager {
if (this.fileRefCache.has(relativePath)) return
const fullPath = join(this.tmpDir, relativePath)
- const dir = fullPath.substring(0, fullPath.lastIndexOf('/'))
+ const dir = dirname(fullPath)
await mkdir(dir, { recursive: true })
return new Promise((resolve, reject) => {
@@ -99,17 +99,23 @@ export class CompilationManager {
): Promise<{ success: boolean; log: string; pdfPath: string }> {
await this.syncDocs()
- const texPaths = [
- '/Library/TeX/texbin',
- '/usr/local/texlive/2024/bin/universal-darwin',
- '/usr/texbin',
- '/opt/homebrew/bin'
- ]
- const envPath = texPaths.join(':') + ':' + (process.env.PATH || '')
+ const texPaths = process.platform === 'win32'
+ ? [
+ 'C:\\texlive\\2025\\bin\\windows',
+ 'C:\\texlive\\2024\\bin\\windows',
+ join(process.env.LOCALAPPDATA || '', 'Programs', 'MiKTeX', 'miktex', 'bin', 'x64')
+ ]
+ : [
+ '/Library/TeX/texbin',
+ '/usr/local/texlive/2024/bin/universal-darwin',
+ '/usr/texbin',
+ '/opt/homebrew/bin'
+ ]
+ const envPath = texPaths.join(delimiter) + delimiter + (process.env.PATH || '')
// Use // suffix for recursive search of ALL subdirectories in the project tree.
// This ensures .sty, .bst, .cls, images, etc. are always found regardless of nesting.
- const texInputs = `${this.tmpDir}//:`
+ const texInputs = `${this.tmpDir}//${delimiter}`
const texBase = basename(mainTexRelPath, '.tex')
const pdfPath = join(this.tmpDir, texBase + '.pdf')
diff --git a/src/main/fileSyncBridge.ts b/src/main/fileSyncBridge.ts
index 3072ae6..ae22eb8 100644
--- a/src/main/fileSyncBridge.ts
+++ b/src/main/fileSyncBridge.ts
@@ -2,7 +2,8 @@
// Licensed under AGPL-3.0 - see LICENSE file
// Bidirectional file sync bridge: temp dir ↔ Overleaf via OT (text) + REST (binary)
-import { join, dirname } from 'path'
+import { join, dirname, relative, sep } from 'path'
+import { tmpdir } from 'os'
import { existsSync } from 'fs'
import { readFile, writeFile, mkdir, unlink, rename as fsRename, appendFile, readdir, rm } from 'fs/promises'
import { createHash } from 'crypto'
@@ -16,7 +17,7 @@ import type { OtOp } from './otTypes'
import { isInsert, isDelete } from './otTypes'
const dmp = new diff_match_patch()
-const LOG_FILE = '/tmp/lattex-bridge.log'
+const LOG_FILE = join(tmpdir(), 'lattex-bridge.log')
function bridgeLog(msg: string, ...rest: unknown[]) {
const line = `[${new Date().toISOString()}] ${msg}${rest.length ? ' ' + rest.map(String).join(' ') : ''}`
console.log(line)
@@ -225,7 +226,7 @@ export class FileSyncBridge {
}, 60_000)
this.watcher.on('change', (absPath: string) => {
- const relPath = absPath.replace(this.tmpDir + '/', '')
+ const relPath = this.relFromAbs(absPath)
bridgeLog(`[FileSyncBridge] chokidar change: ${relPath}`)
// A real fs event means new content — give parked failures a fresh chance
this.permanentFailures.delete(relPath)
@@ -237,7 +238,7 @@ export class FileSyncBridge {
})
this.watcher.on('add', (absPath: string) => {
- const relPath = absPath.replace(this.tmpDir + '/', '')
+ const relPath = this.relFromAbs(absPath)
bridgeLog(`[FileSyncBridge] chokidar add: ${relPath}`)
this.permanentFailures.delete(relPath)
if (this.pathDocMap[relPath] || this.pathFileRefMap[relPath]) {
@@ -250,7 +251,7 @@ export class FileSyncBridge {
})
this.watcher.on('unlink', (absPath: string) => {
- const relPath = absPath.replace(this.tmpDir + '/', '')
+ const relPath = this.relFromAbs(absPath)
bridgeLog(`[FileSyncBridge] chokidar unlink: ${relPath}`)
})
@@ -1478,6 +1479,12 @@ export class FileSyncBridge {
}
}
+ /** Absolute watcher path → project-relative path with forward slashes
+ (Windows watcher events use backslashes; all maps are '/'-keyed) */
+ private relFromAbs(absPath: string): string {
+ return relative(this.tmpDir, absPath).split(sep).join('/')
+ }
+
// ── Compile-artifact detection ───────────────────────────────
/** Register a file our own local compile will write (e.g. root main.pdf) */
diff --git a/src/main/index.ts b/src/main/index.ts
index 4f23762..8dffc26 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -2,7 +2,7 @@
// Licensed under AGPL-3.0 - see LICENSE file
import { app, BrowserWindow, ipcMain, dialog, shell, net } from 'electron'
-import { join, basename, dirname, relative, extname } from 'path'
+import { join, basename, dirname, relative, extname, delimiter } from 'path'
import { copyFile, readFile, writeFile, mkdir as mkdirAsync, unlink, readdir, stat, rename as fsRename, rm, cp } from 'fs/promises'
import { existsSync } from 'fs'
import { spawn } from 'child_process'
@@ -125,8 +125,11 @@ function createWindow(): void {
height: 900,
minWidth: 900,
minHeight: 600,
- titleBarStyle: 'hiddenInset',
- trafficLightPosition: { x: 15, y: 15 },
+ // Frameless inset title bar is a macOS affordance; use the native
+ // frame elsewhere
+ ...(process.platform === 'darwin'
+ ? { titleBarStyle: 'hiddenInset' as const, trafficLightPosition: { x: 15, y: 15 } }
+ : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
@@ -255,19 +258,25 @@ ipcMain.handle('settings:setApiKeys', async (_e, keys: Record<string, string>) =
// ── LaTeX Compilation ────────────────────────────────────────────
-// Ensure TeX binaries are in PATH (Electron launched from Finder may miss them)
-const texPaths = ['/Library/TeX/texbin', '/usr/local/texlive/2024/bin/universal-darwin', '/usr/texbin', '/opt/homebrew/bin']
+// Ensure TeX binaries are in PATH (GUI-launched apps may miss them)
+const texPaths = process.platform === 'win32'
+ ? [
+ 'C:\\texlive\\2025\\bin\\windows',
+ 'C:\\texlive\\2024\\bin\\windows',
+ join(process.env.LOCALAPPDATA || '', 'Programs', 'MiKTeX', 'miktex', 'bin', 'x64')
+ ]
+ : ['/Library/TeX/texbin', '/usr/local/texlive/2024/bin/universal-darwin', '/usr/texbin', '/opt/homebrew/bin']
const currentPath = process.env.PATH || ''
for (const p of texPaths) {
if (!currentPath.includes(p)) {
- process.env.PATH = `${p}:${process.env.PATH}`
+ process.env.PATH = `${p}${delimiter}${process.env.PATH}`
}
}
// SyncTeX: PDF position → source file:line (inverse search)
ipcMain.handle('synctex:editFromPdf', async (_e, pdfPath: string, page: number, x: number, y: number) => {
return new Promise<{ file: string; line: number } | null>((resolve) => {
- const pdfDir = pdfPath.substring(0, pdfPath.lastIndexOf('/'))
+ const pdfDir = dirname(pdfPath)
console.log(`[synctex] edit -o ${page}:${x}:${y}:${pdfPath} (cwd: ${pdfDir})`)
const proc = spawn('synctex', ['edit', '-o', `${page}:${x}:${y}:${pdfPath}`], {
env: process.env,
@@ -418,8 +427,10 @@ ipcMain.handle('pty:spawn', async (_e, id: string, cwd: string, cmd?: string, ar
ptyInstances.delete(id)
}
- const shellPath = cmd || process.env.SHELL || '/bin/zsh'
- const shellArgs = args || ['-l']
+ const shellPath = cmd || (process.platform === 'win32'
+ ? process.env.COMSPEC || 'powershell.exe'
+ : process.env.SHELL || '/bin/zsh')
+ const shellArgs = args || (process.platform === 'win32' ? [] : ['-l'])
const ptyEnv: Record<string, string> = {
...(process.env as Record<string, string>),
TERM: 'xterm-256color',