diff options
| author | haoyuren <13851610112@163.com> | 2026-07-30 09:00:55 +0800 |
|---|---|---|
| committer | haoyuren <13851610112@163.com> | 2026-07-30 09:00:55 +0800 |
| commit | 7297f7c339186f8af50f4f0ed3734e4ad4b47aed (patch) | |
| tree | 9c5b02d50bf7ab5ac64b07ba436f9e2319a87208 | |
| parent | c9cd5119077c69088eb8ba768ee1e1602baec751 (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.yml | 34 | ||||
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | electron-builder.yml | 10 | ||||
| -rw-r--r-- | resources/icon.png | bin | 0 -> 34710 bytes | |||
| -rw-r--r-- | src/main/compilationManager.ts | 28 | ||||
| -rw-r--r-- | src/main/fileSyncBridge.ts | 17 | ||||
| -rw-r--r-- | src/main/index.ts | 29 |
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 }} @@ -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 Binary files differnew file mode 100644 index 0000000..a3762f0 --- /dev/null +++ b/resources/icon.png 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', |
