diff --git a/.github/workflows/linux-deb.yml b/.github/workflows/linux-deb.yml new file mode 100644 index 0000000..514be59 --- /dev/null +++ b/.github/workflows/linux-deb.yml @@ -0,0 +1,51 @@ +# Builds the Linux .deb on pull requests and manual runs. Does not publish a +# GitHub Release. The published-release upload lives in release.yml. +name: Linux deb + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-deb: + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + + - name: Install Debian packaging tools + run: sudo apt-get update && sudo apt-get install -y fakeroot dpkg + + - name: Install dependencies + run: npm ci + + - name: Cache Electron binaries + uses: actions/cache@v4 + with: + path: | + ~/.cache/electron + ~/.cache/electron-builder + key: ${{ runner.os }}-electron-${{ hashFiles('package-lock.json') }} + + - name: Build deb + run: npm run build:deb -- --publish never + env: + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + + - name: Upload deb artifact + uses: actions/upload-artifact@v4 + with: + name: basil-deb + path: release/Basil-*-amd64.deb + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index adf0af8..63129ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,6 @@ -# Builds Windows portable + setup EXEs when a GitHub Release is published -# (not on push, PR, or draft release creation). Local `npm run build` is unchanged. +# Builds Windows portable + setup EXEs, and the Linux .deb, when a GitHub +# Release is published (not on push, PR, or draft release creation). +# Local `npm run build` is unchanged. Pull-request deb builds live in linux-deb.yml. name: Release on: @@ -67,3 +68,63 @@ jobs: } gh release upload $tag $portable.FullName $setup.FullName --clobber Write-Host "Uploaded $($portable.Name) and $($setup.Name) to $tag" + + build-linux: + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + + - name: Install Debian packaging tools + run: sudo apt-get update && sudo apt-get install -y fakeroot dpkg + + - name: Install dependencies + run: npm ci + + - name: Sync package version from release tag + run: | + tag="${{ github.event.release.tag_name }}" + version="${tag#v}" + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "Release tag '$tag' must be a semver version (e.g. 26.7.1 or v26.7.1)." >&2 + exit 1 + fi + npm version "$version" --no-git-tag-version --allow-same-version + echo "Building as version $version" + + - name: Cache Electron binaries + uses: actions/cache@v4 + with: + path: | + ~/.cache/electron + ~/.cache/electron-builder + key: ${{ runner.os }}-electron-${{ hashFiles('package-lock.json') }} + + - name: Build deb + # --publish never: on tagged commits electron-builder otherwise tries to + # publish and fails without GH_TOKEN. The asset is uploaded via gh below. + run: npm run build:deb -- --publish never + env: + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + + - name: Upload deb to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + tag="${{ github.event.release.tag_name }}" + deb="$(find release -maxdepth 1 -type f -name 'Basil-*-amd64.deb' | head -n 1)" + if [[ -z "$deb" ]]; then + ls -la release || true + echo "Expected Basil-*-amd64.deb in release/" >&2 + exit 1 + fi + gh release upload "$tag" "$deb" --clobber + echo "Uploaded $(basename "$deb") to $tag" diff --git a/.gitignore b/.gitignore index 908ddb5..20bd2ff 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ yarn-error.log* pnpm-debug.log* # Local environment and secrets. -# Basil keeps its API key in %USERPROFILE%\.basil\ai.json, outside the repo, but +# Basil keeps its API key in %USERPROFILE%\.basil\ai.json (or ~/.basil/ai.json), outside the repo, but # never let a stray local env file be committed. .env .env.* diff --git a/README.md b/README.md index baefb07..f322227 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Basil -**Basil** is an intuitive text editor for Windows with optional AI assistance. Open source and free! +**Basil** is an intuitive text editor for Windows, with a `.deb` package for Ubuntu 26.04 and newer, and optional AI assistance. Open source and free! [github.com/michaelstaake/Basil](https://github.com/michaelstaake/Basil) @@ -14,6 +14,14 @@ Grab the latest `Basil--setup.exe` (installer) or At this time, the executables are not signed, so you may receive Windows SmartScreen warnings. Choose run anyway, or build from source using the steps below (refer to Build section of this document). +On Ubuntu 26.04 or newer (x64), install the `.deb` the same way: + +```bash +sudo apt install ./Basil--amd64.deb +``` + +`apt` pulls in GTK and the other libraries the package depends on. Basil then shows up in the applications menu, and under **Open With** for common text files. The current default text editor stays as it is. + ## Features - Built on Monaco editor, which powers popular applications like VS Code. - Powerful yet easy handling of multiple files with tabs. @@ -23,14 +31,14 @@ At this time, the executables are not signed, so you may receive Windows SmartSc - Compare your current file state to the original file state with git-style diff feature. - Optional AI features - compatible with any OpenAI API-compatible provider or service, including LM Studio, LmPanel, OpenRouter, xAI, etc. - If enabled, each file gets its own AI context that can assist you. AI features are specifically designed to be read-only so you always remain in complete control. -- Basil comes packaged with a portable version, or install it for convenient features like "Open with Basil" context menu, system tray persistence, etc. Alternately, you may build it from source. +- Basil comes packaged with a portable version, or install it for convenient features like "Open with Basil" context menu, system tray persistence, etc. The Ubuntu `.deb` adds an applications-menu entry and Open With for common text files. Alternately, you may build it from source. Hint: Click the Basil logo in the toolbar to access settings and more features. ## Requirements -- Windows 10/11 -- Node.js 22+ (if building from source - otherwise you can just run the included portable or setup executables) +- Windows 10/11, or Ubuntu 26.04+ (x64) for the `.deb` +- Node.js 22+ (if building from source - otherwise you can just run the included portable or setup executables, or install the `.deb`) ## Development @@ -50,7 +58,8 @@ Other scripts: | `npm run typecheck` | TypeScript, no emit | | `npm run lint` | ESLint | | `npm test` | Unit tests (Vitest) | -| `npm run icons` | Regenerate `build/icon.png` and `build/icon.ico` from `build/icon.svg` | +| `npm run build:deb` | Linux `.deb` for x64 (`release/Basil--amd64.deb`) | +| `npm run icons` | Regenerate `build/icon.png`, `build/icon.ico`, and `build/icons/` from `build/icon.svg` | ## Build @@ -61,6 +70,14 @@ npm run build Writes `release/Basil--portable.exe` and `release/Basil--setup.exe`. Use `npm run build:dir` for an unpacked build without installers. +The Linux package is a separate command. `npm run build` still writes the Windows executables above. + +```bash +npm run build:deb +``` + +Writes `release/Basil--amd64.deb` (Debian package name `basil`, executable `/usr/bin/basil`). Run it on Linux. electron-builder shells out to `dpkg` and `fakeroot` to assemble the package. The `.deb` depends on the Ubuntu 26.04 library names (`libgtk-3-0t64` and the other `t64` packages), with the older names as alternatives where those still exist. It also depends on glibc 2.25 or newer, which Ubuntu 26.04 (glibc 2.43) satisfies, and on `libudev1`, which the Electron binary links directly. The only Linux target is this `.deb`. + Everything the app needs at runtime is bundled into `dist/` by Vite, so `package.json` has **no production dependencies**. Adding one would ship it a second time inside `app.asar` — keep new libraries in `devDependencies` unless @@ -68,9 +85,11 @@ they genuinely need to be loaded from disk at runtime. ## Configuration -On first launch Basil creates a hidden folder in your home directory: +On first launch Basil creates a folder in your home directory: -`%USERPROFILE%\.basil\` +`%USERPROFILE%\.basil\` on Windows, `~/.basil/` on Linux. + +On Windows that folder is marked hidden. On Linux the leading dot is what hides it. | File | Purpose | |------|---------| @@ -79,9 +98,22 @@ On first launch Basil creates a hidden folder in your home directory: | `session.json` | Files to reopen on next launch | | `logs\main.log` | Main-process log, including crashes | -The API key is encrypted with the Windows credential store (DPAPI) and stored as +The API key is encrypted with the OS keystore (Windows DPAPI, or the Secret Service on Linux) and stored as `apiKeyEnc`. A file that cannot be parsed is renamed with a `.corrupt-` -suffix and Basil starts with defaults rather than failing to launch. +suffix and Basil starts with defaults rather than failing to launch. When a Linux Secret Service is available, the key is encrypted with it. Otherwise Basil stores the key in the file so the setting is kept. + +### Linux startup + +"Start with boot" on Linux writes `~/.config/autostart/com.basil.editor.desktop` (or `$XDG_CONFIG_HOME/autostart/` when that variable is set). Electron's login-item API does not run on Linux, so this is a normal XDG autostart file. It launches `/opt/Basil/basil --hidden`, the same hidden start the Windows Run key uses. Desktops that honor that directory (GNOME, KDE, XFCE, and other freedesktop sessions) will start Basil in the tray. + +A few limits: + +- A session that ignores `autostart` will not launch Basil at login. +- The tray icon is a PNG. It needs a StatusNotifier host. Ubuntu's desktop includes one (`libayatana-appindicator3-1` is a recommended package). A minimal session with no tray still starts Basil hidden, with no window to click. Turn the setting off, or delete the autostart file, and open Basil from the menu. +- The package install leaves that file alone, and removing the `.deb` leaves it in place. `TryExec` points at the binary, so a removed install is skipped on later logins. +- `npm run dev` removes this file so a dev Electron binary stays off the login list. The Windows NSIS login item is unchanged. + +System light and dark mode on Linux follows Electron's `nativeTheme` (the GTK / portal setting). ### AI presets diff --git a/build/icons/128x128.png b/build/icons/128x128.png new file mode 100644 index 0000000..9365061 Binary files /dev/null and b/build/icons/128x128.png differ diff --git a/build/icons/16x16.png b/build/icons/16x16.png new file mode 100644 index 0000000..751e21d Binary files /dev/null and b/build/icons/16x16.png differ diff --git a/build/icons/22x22.png b/build/icons/22x22.png new file mode 100644 index 0000000..fcde227 Binary files /dev/null and b/build/icons/22x22.png differ diff --git a/build/icons/24x24.png b/build/icons/24x24.png new file mode 100644 index 0000000..0f9c666 Binary files /dev/null and b/build/icons/24x24.png differ diff --git a/build/icons/256x256.png b/build/icons/256x256.png new file mode 100644 index 0000000..de2e982 Binary files /dev/null and b/build/icons/256x256.png differ diff --git a/build/icons/32x32.png b/build/icons/32x32.png new file mode 100644 index 0000000..f9b6f8e Binary files /dev/null and b/build/icons/32x32.png differ diff --git a/build/icons/48x48.png b/build/icons/48x48.png new file mode 100644 index 0000000..2a0db2c Binary files /dev/null and b/build/icons/48x48.png differ diff --git a/build/icons/512x512.png b/build/icons/512x512.png new file mode 100644 index 0000000..8d30012 Binary files /dev/null and b/build/icons/512x512.png differ diff --git a/build/icons/64x64.png b/build/icons/64x64.png new file mode 100644 index 0000000..4c4d3a3 Binary files /dev/null and b/build/icons/64x64.png differ diff --git a/electron/host-platform.test.ts b/electron/host-platform.test.ts new file mode 100644 index 0000000..a3d7818 --- /dev/null +++ b/electron/host-platform.test.ts @@ -0,0 +1,88 @@ +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { + LINUX_DESKTOP_NAME, + linuxAutostartDesktopPath, + quoteDesktopExecArg, + renderLinuxAutostartDesktop, + shouldHideBasilDir, + trayIconCandidates, +} from './host-platform'; + +const trayLocations = { + devIco: 'dev.ico', + devPng: 'dev.png', + resourceIco: 'res.ico', + resourcePng: 'res.png', +}; + +describe('shouldHideBasilDir', () => { + it('uses attrib only on Windows', () => { + expect(shouldHideBasilDir('win32')).toBe(true); + expect(shouldHideBasilDir('linux')).toBe(false); + expect(shouldHideBasilDir('darwin')).toBe(false); + }); +}); + +describe('trayIconCandidates', () => { + it('searches PNG before ICO on Linux', () => { + expect(trayIconCandidates('linux', trayLocations)).toEqual([ + 'res.png', + 'dev.png', + 'res.ico', + 'dev.ico', + ]); + }); + + it('keeps the Windows ico, png, resource-ico order', () => { + expect(trayIconCandidates('win32', trayLocations)).toEqual([ + 'dev.ico', + 'dev.png', + 'res.ico', + ]); + }); +}); + +describe('linux autostart desktop file', () => { + it('places the file under XDG_CONFIG_HOME when that is set', () => { + expect(linuxAutostartDesktopPath('/home/me', '/tmp/cfg')).toBe( + path.join('/tmp/cfg', 'autostart', `${LINUX_DESKTOP_NAME}.desktop`), + ); + }); + + it('falls back to ~/.config/autostart', () => { + expect(linuxAutostartDesktopPath('/home/me', ' ')).toBe( + path.join('/home/me', '.config', 'autostart', `${LINUX_DESKTOP_NAME}.desktop`), + ); + }); + + it('launches the packaged binary hidden, matching the Windows Run key', () => { + const text = renderLinuxAutostartDesktop({ + execPath: '/opt/Basil/basil', + hidden: true, + }); + expect(text.startsWith('[Desktop Entry]\n')).toBe(true); + expect(text).toContain('Exec=/opt/Basil/basil --hidden\n'); + expect(text).toContain('TryExec=/opt/Basil/basil\n'); + expect(text).toContain('X-GNOME-Autostart-enabled=true\n'); + expect(text.endsWith('\n')).toBe(true); + }); + + it('quotes an executable path that contains spaces', () => { + expect(quoteDesktopExecArg('/opt/My Apps/basil')).toBe('"/opt/My Apps/basil"'); + const text = renderLinuxAutostartDesktop({ + execPath: '/opt/My Apps/basil', + hidden: true, + }); + expect(text).toContain('Exec="/opt/My Apps/basil" --hidden\n'); + }); + + it('escapes percent signs so the launcher does not treat them as field codes', () => { + expect(quoteDesktopExecArg('/opt/100%/basil')).toBe('"/opt/100%%/basil"'); + }); + + it('leaves freedesktop field codes unquoted', () => { + expect(quoteDesktopExecArg('%U')).toBe('%U'); + expect(quoteDesktopExecArg('%f')).toBe('%f'); + }); +}); diff --git a/electron/host-platform.ts b/electron/host-platform.ts new file mode 100644 index 0000000..e5ea096 --- /dev/null +++ b/electron/host-platform.ts @@ -0,0 +1,87 @@ +import path from 'node:path'; + +/** + * Installed desktop id. electron-builder writes + * `/usr/share/applications/com.basil.editor.desktop` when `desktopName` is set + * and `linux.syncDesktopName` is true. Electron uses the same id for WM_CLASS. + */ +export const LINUX_DESKTOP_NAME = 'com.basil.editor'; + +/** Windows marks `~\.basil` hidden with `attrib +h`. Other platforms use the leading dot. */ +export function shouldHideBasilDir(platform: NodeJS.Platform): boolean { + return platform === 'win32'; +} + +export interface TrayIconLocations { + devIco: string; + devPng: string; + resourceIco: string; + resourcePng: string; +} + +/** + * Linux status icons want a PNG. The packaged file is preferred so a dev tree + * is not required after install; Windows keeps the `.ico` search it already had. + */ +export function trayIconCandidates( + platform: NodeJS.Platform, + locations: TrayIconLocations, +): string[] { + if (platform === 'linux') { + return [ + locations.resourcePng, + locations.devPng, + locations.resourceIco, + locations.devIco, + ]; + } + return [locations.devIco, locations.devPng, locations.resourceIco]; +} + +/** Characters that must be quoted in a freedesktop `Exec` value. */ +const DESKTOP_EXEC_RESERVED = /[\s"'`\\<>~|&;$*?#()%]/; + +/** + * Quote one `Exec` argument. Field codes (`%f`, `%U`, …) stay bare so the + * launcher expands them. `%` inside a real argument is escaped as `%%`. + */ +export function quoteDesktopExecArg(arg: string): string { + if (/^%[a-zA-Z]$/.test(arg)) return arg; + const escaped = arg.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%'); + if (DESKTOP_EXEC_RESERVED.test(arg)) return `"${escaped}"`; + return escaped; +} + +export function linuxAutostartDesktopPath(home: string, xdgConfigHome: string | undefined): string { + const configHome = xdgConfigHome?.trim() || path.join(home, '.config'); + return path.join(configHome, 'autostart', `${LINUX_DESKTOP_NAME}.desktop`); +} + +/** + * Per-user XDG autostart entry. Electron's `setLoginItemSettings` is + * macOS/Windows only, so Linux login startup is this file. + * `--hidden` matches the Windows Run key (`basil` → `"exe" --hidden`). + */ +export function renderLinuxAutostartDesktop(options: { + execPath: string; + hidden: boolean; +}): string { + const exec = [ + quoteDesktopExecArg(options.execPath), + ...(options.hidden ? ['--hidden'] : []), + ].join(' '); + return [ + '[Desktop Entry]', + 'Type=Application', + 'Version=1.0', + 'Name=Basil', + 'Comment=Basil Text Editor', + `Exec=${exec}`, + `TryExec=${quoteDesktopExecArg(options.execPath)}`, + 'Terminal=false', + 'Icon=basil', + 'X-GNOME-Autostart-enabled=true', + 'StartupNotify=false', + '', + ].join('\n'); +} diff --git a/electron/main.ts b/electron/main.ts index 28456a9..c8913bf 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -54,10 +54,22 @@ import { decode, detectEncoding, normalizeEncoding } from '../shared/encoding'; import { capChatHistory } from '../shared/ai'; import type { LineEnding } from '../shared/text'; import { getMtimeMs, readTextFile, writeTextFile } from './file-io'; +import { + LINUX_DESKTOP_NAME, + linuxAutostartDesktopPath, + renderLinuxAutostartDesktop, + shouldHideBasilDir, + trayIconCandidates, +} from './host-platform'; import { initLogger, installGlobalErrorHandlers } from './logger'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +if (process.platform === 'linux') { + // Match the installed com.basil.editor.desktop (package.json desktopName). + app.setDesktopName(LINUX_DESKTOP_NAME); +} + const windows = new Map(); /** Windows allowed to close without asking the renderer. */ const allowCloseIds = new Set(); @@ -340,7 +352,35 @@ function clearLoginItem(name: string): void { }); } +function applyLinuxAutostart(): void { + // setLoginItemSettings is macOS and Windows only. Login startup on Linux is + // a per-user XDG autostart file. Limits are documented in the README. + const target = linuxAutostartDesktopPath(app.getPath('home'), process.env.XDG_CONFIG_HOME); + try { + // Unpackaged dev runs must not register node_modules/electron, and must + // drop a stale entry the way the Windows Run key is cleared. + if (!app.isPackaged || !preferences.startWithBoot) { + fsSync.rmSync(target, { force: true }); + return; + } + const contents = renderLinuxAutostartDesktop({ + execPath: process.execPath, + hidden: true, + }); + if (fsSync.existsSync(target) && fsSync.readFileSync(target, 'utf8') === contents) return; + fsSync.mkdirSync(path.dirname(target), { recursive: true }); + fsSync.writeFileSync(target, contents, { encoding: 'utf8', mode: 0o644 }); + } catch (err) { + console.error('[basil] could not update the Linux autostart entry:', err); + } +} + function applyLoginItemSettings(): void { + if (process.platform === 'linux') { + applyLinuxAutostart(); + return; + } + // Drop legacy duplicates so boot never launches Basil twice (second instance // would focus the main window) and so stale electron.exe Run entries die. for (const name of LEGACY_LOGIN_ITEM_NAMES) { @@ -362,24 +402,41 @@ function applyLoginItemSettings(): void { }); } +function loadTrayIconFromFiles(): Electron.NativeImage { + const candidates = trayIconCandidates(process.platform, { + devIco: path.join(__dirname, '../build/icon.ico'), + devPng: path.join(__dirname, '../build/icon.png'), + resourceIco: path.join(process.resourcesPath, 'icon.ico'), + resourcePng: path.join(process.resourcesPath, 'icon.png'), + }); + for (const candidate of candidates) { + if (!fsSync.existsSync(candidate)) continue; + const image = nativeImage.createFromPath(candidate); + if (!image.isEmpty()) return image; + } + return nativeImage.createEmpty(); +} + async function ensureTray(): Promise { if (tray) return; let icon = nativeImage.createEmpty(); - try { - icon = await app.getFileIcon(process.execPath, { size: 'small' }); - } catch { - const candidates = [ - path.join(__dirname, '../build/icon.ico'), - path.join(__dirname, '../build/icon.png'), - path.join(process.resourcesPath, 'icon.ico'), - ]; - for (const candidate of candidates) { - if (fsSync.existsSync(candidate)) { - icon = nativeImage.createFromPath(candidate); - if (!icon.isEmpty()) break; + if (process.platform === 'linux') { + // getFileIcon is Windows/macOS-oriented and may return an empty image. + // The tray wants the packaged PNG. + icon = loadTrayIconFromFiles(); + if (!icon.isEmpty()) { + const { width, height } = icon.getSize(); + if (width > 32 || height > 32) { + icon = icon.resize({ width: 24, height: 24 }); } } + } else { + try { + icon = await app.getFileIcon(process.execPath, { size: 'small' }); + } catch { + icon = loadTrayIconFromFiles(); + } } tray = new Tray(icon); @@ -512,6 +569,7 @@ function getBasilDir(): string { } async function hideBasilDir(dir: string): Promise { + if (!shouldHideBasilDir(process.platform)) return; try { const { execFile } = await import('node:child_process'); const { promisify } = await import('node:util'); @@ -527,6 +585,9 @@ async function hideBasilDir(dir: string): Promise { * blocks the main process, and this is consulted on every theme read, every * window creation and every broadcast — so it is refreshed only when Windows * actually reports a theme change. + * + * Off Windows the probe is skipped (`null`) and the system theme comes from + * Electron's nativeTheme, which tracks the GTK / portal preference on Linux. */ let cachedAppsPreferDark: boolean | null | undefined; @@ -688,7 +749,8 @@ async function savePreferences(next: Preferences): Promise { /** * The API key is a real credential, so it is encrypted at rest with the OS - * keystore (DPAPI on Windows) rather than sitting in plaintext in ai.json. + * keystore (DPAPI on Windows, the Secret Service via libsecret on Linux) + * rather than sitting in plaintext in ai.json. * Files written by older versions still hold `apiKey` and are migrated on load. */ function hasEncryptedKey(raw: unknown): boolean { diff --git a/package.json b/package.json index ba5ad7c..e0a0c00 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,13 @@ "url": "https://github.com/michaelstaake/Basil/issues" }, "main": "dist-electron/main.js", + "desktopName": "com.basil.editor", "type": "module", "scripts": { "dev": "vite", "build": "vite build && node scripts/with-build-version.mjs --win", "build:dir": "vite build && node scripts/with-build-version.mjs --win dir", + "build:deb": "vite build && node scripts/with-build-version.mjs --linux deb --x64", "icons": "node scripts/generate-icons.mjs", "preview": "vite preview", "typecheck": "tsc --noEmit", @@ -98,6 +100,91 @@ "createDesktopShortcut": true, "createStartMenuShortcut": true, "artifactName": "Basil-${version}-setup.${ext}" + }, + "linux": { + "target": [ + { + "target": "deb", + "arch": [ + "x64" + ] + } + ], + "icon": "icons", + "executableName": "basil", + "category": "Utility;TextEditor", + "synopsis": "Intuitive text editor", + "description": "An intuitive text editor with optional AI assistance.", + "maintainer": "Michael Staake ", + "syncDesktopName": true, + "mimeTypes": [ + "text/plain", + "text/markdown", + "text/x-markdown", + "text/html", + "text/css", + "text/javascript", + "application/javascript", + "application/json", + "application/xml", + "text/xml", + "text/x-python", + "text/x-shellscript", + "text/csv", + "text/yaml", + "application/x-yaml" + ], + "desktop": { + "entry": { + "Name": "Basil", + "GenericName": "Text Editor", + "Keywords": "text;editor;notes;markdown;", + "StartupNotify": "true" + } + }, + "extraResources": [ + { + "from": "build/icon.png", + "to": "icon.png" + } + ] + }, + "deb": { + "artifactName": "Basil-${version}-${arch}.${ext}", + "packageName": "basil", + "packageCategory": "editors", + "priority": "optional", + "depends": [ + "libc6 (>= 2.25)", + "libgtk-3-0t64 | libgtk-3-0", + "libglib2.0-0t64 | libglib2.0-0", + "libatk1.0-0t64 | libatk1.0-0", + "libatk-bridge2.0-0t64 | libatk-bridge2.0-0", + "libatspi2.0-0t64 | libatspi2.0-0", + "libasound2t64 | libasound2", + "libcups2t64 | libcups2", + "libdrm2", + "libgbm1", + "libnotify4", + "libnspr4", + "libnss3", + "libx11-xcb1", + "libxcb-dri3-0", + "libxcomposite1", + "libxdamage1", + "libxfixes3", + "libxkbcommon0", + "libxrandr2", + "libxss1", + "libxtst6", + "libudev1", + "libsecret-1-0", + "libuuid1", + "xdg-utils" + ], + "recommends": [ + "libayatana-appindicator3-1" + ] } } } diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs index ee83488..2bbb913 100644 --- a/scripts/generate-icons.mjs +++ b/scripts/generate-icons.mjs @@ -14,12 +14,28 @@ const svg = await fs.readFile(svgPath); await sharp(svg).resize(512, 512).png().toFile(pngPath); -const sizes = [16, 24, 32, 48, 64, 128, 256]; +const icoSizes = [16, 24, 32, 48, 64, 128, 256]; const buffers = await Promise.all( - sizes.map((size) => sharp(svg).resize(size, size).png().toBuffer()), + icoSizes.map((size) => sharp(svg).resize(size, size).png().toBuffer()), ); const ico = await pngToIco(buffers); await fs.writeFile(icoPath, ico); -console.log('Wrote build/icon.png and build/icon.ico'); +// hicolor sizes for the Linux .deb. electron-builder installs each +// NxN.png under /usr/share/icons/hicolor. The Windows .ico above is unchanged. +const iconDir = path.join(root, 'build', 'icons'); +await fs.mkdir(iconDir, { recursive: true }); +const linuxSizes = [16, 22, 24, 32, 48, 64, 128, 256, 512]; +await Promise.all( + linuxSizes.map(async (size) => { + const dest = path.join(iconDir, `${size}x${size}.png`); + if (size === 512) { + await fs.copyFile(pngPath, dest); + return; + } + await sharp(svg).resize(size, size).png().toFile(dest); + }), +); + +console.log('Wrote build/icon.png, build/icon.ico, and build/icons/*.png'); diff --git a/scripts/with-build-version.mjs b/scripts/with-build-version.mjs index 5f27302..cd839d6 100644 --- a/scripts/with-build-version.mjs +++ b/scripts/with-build-version.mjs @@ -38,6 +38,12 @@ function removeStaleDirs() { } } +const builderArgs = process.argv.slice(2); +// `npm run build` / `build:dir` pass --win. Linux `.deb` builds (`--linux`) must +// not stamp a Windows file version onto the package, and must not delete a +// leftover win-unpacked directory. +const buildingWindows = builderArgs.includes('--win'); + const pad = (n) => String(n).padStart(2, '0'); const now = new Date(); const stamp = [ @@ -52,26 +58,31 @@ const stamp = [ // File Version (PE Details) = appVersion.buildDatetime, e.g. 1.0.2.20260722165342 // Product / app version stays package.json "version" (e.g. 1.0.2). const buildVersion = `${pkg.version}.${stamp}`; -console.log(`File version: ${buildVersion}`); +const cliArgs = [...builderArgs]; +if (buildingWindows) { + console.log(`File version: ${buildVersion}`); + cliArgs.push(`-c.buildVersion=${buildVersion}`); +} const MAX_ATTEMPTS = 3; let status = 1; for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { - removeStaleDirs(); + if (buildingWindows) removeStaleDirs(); const result = spawnSync( process.execPath, - [electronBuilderCli, ...process.argv.slice(2), `-c.buildVersion=${buildVersion}`], + [electronBuilderCli, ...cliArgs], { stdio: 'inherit', cwd: root }, ); status = result.status ?? 1; if (status === 0) break; - // Only the file-lock failure is worth retrying, and it always leaves the - // half-extracted win-unpacked.tmp behind. Anything else (a config error, a - // missing binary) would just fail again more slowly. + // Only the Windows file-lock failure is worth retrying, and it always leaves + // the half-extracted win-unpacked.tmp behind. Linux deb builds never create + // that directory, so a real failure there is not retried. Anything else (a + // config error, a missing binary) would just fail again more slowly. const lockFailure = existsSync(path.join(root, 'release', 'win-unpacked.tmp')); if (!lockFailure) break;