Tauri v1 -> v2

Refactor appSettings to accommodate new Tauri v2 API

- creates AppSettings class and injects it where needed
- avoids `window` undeclared variable during vite build process
This commit is contained in:
Mattias Granlund 2024-10-07 16:23:05 +02:00 committed by Nico Domino
parent 7be3e7a6e7
commit 2ef866baa6
52 changed files with 2669 additions and 2343 deletions

View File

@ -157,6 +157,7 @@ jobs:
run: | run: |
sudo apt update; sudo apt update;
sudo apt install -y \ sudo apt install -y \
libwebkit2gtk-4.1-dev \
build-essential \ build-essential \
curl \ curl \
wget \ wget \
@ -186,11 +187,10 @@ jobs:
--dist "./release" \ --dist "./release" \
--version "${{ env.version }}" --version "${{ env.version }}"
env: env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_PROVIDER_SHORT_NAME }} APPLE_TEAM_ID: ${{ secrets.APPLE_PROVIDER_SHORT_NAME }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}

2909
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
"prepare": "svelte-kit sync" "prepare": "svelte-kit sync"
}, },
"devDependencies": { "devDependencies": {
"@anthropic-ai/sdk": "^0.27.3",
"@codemirror/lang-cpp": "^6.0.2", "@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-css": "^6.2.1", "@codemirror/lang-css": "^6.2.1",
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",
@ -42,7 +43,16 @@
"@sveltejs/adapter-static": "catalog:svelte", "@sveltejs/adapter-static": "catalog:svelte",
"@sveltejs/kit": "catalog:svelte", "@sveltejs/kit": "catalog:svelte",
"@sveltejs/vite-plugin-svelte": "catalog:svelte", "@sveltejs/vite-plugin-svelte": "catalog:svelte",
"@tauri-apps/api": "^1.6.0", "@tauri-apps/api": "^2.0.3",
"@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-http": "^2.0.1",
"@tauri-apps/plugin-fs": "^2.0.1",
"@tauri-apps/plugin-log": "^2.0.0",
"@tauri-apps/plugin-os": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"@tauri-apps/plugin-store": "^2.1.0",
"@tauri-apps/plugin-updater": "^2.0.0",
"@testing-library/jest-dom": "^6.4.8", "@testing-library/jest-dom": "^6.4.8",
"@testing-library/svelte": "^5.2.1", "@testing-library/svelte": "^5.2.1",
"@types/diff-match-patch": "^1.0.36", "@types/diff-match-patch": "^1.0.36",
@ -77,15 +87,12 @@
"svelte": "catalog:svelte", "svelte": "catalog:svelte",
"svelte-check": "catalog:svelte", "svelte-check": "catalog:svelte",
"svelte-french-toast": "^1.2.0", "svelte-french-toast": "^1.2.0",
"tauri-plugin-log-api": "https://github.com/tauri-apps/tauri-plugin-log#v1",
"tauri-plugin-store-api": "https://github.com/tauri-apps/tauri-plugin-store#v1",
"tinykeys": "^2.1.0", "tinykeys": "^2.1.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"vite": "catalog:", "vite": "catalog:",
"vitest": "^2.0.5" "vitest": "^2.0.5"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.27.3",
"openai": "^4.47.3" "openai": "^4.47.3"
} }
} }

View File

@ -1,6 +1,6 @@
import { showError } from '$lib/notifications/toasts'; import { showError } from '$lib/notifications/toasts';
import { captureException } from '@sentry/sveltekit'; import { captureException } from '@sentry/sveltekit';
import { error as logErrorToFile } from 'tauri-plugin-log-api'; import { error as logErrorToFile } from '@tauri-apps/plugin-log';
import type { HandleClientError } from '@sveltejs/kit'; import type { HandleClientError } from '@sveltejs/kit';
// SvelteKit error handler. // SvelteKit error handler.
@ -25,20 +25,23 @@ window.onunhandledrejection = (e: PromiseRejectionEvent) => {
}; };
function logError(error: unknown) { function logError(error: unknown) {
let message = error instanceof Error ? error.message : String(error); try {
const stack = error instanceof Error ? error.stack : undefined; let message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
const id = captureException(message, { const id = captureException(message, {
mechanism: { mechanism: {
type: 'sveltekit', type: 'sveltekit',
handled: false handled: false
} }
}); });
message = `${id}: ${message}\n`; message = `${id}: ${message}\n`;
if (stack) message = `${message}\n${stack}\n`; if (stack) message = `${message}\n${stack}\n`;
logErrorToFile(message); logErrorToFile(message);
console.error(message); showError('Something went wrong', message);
showError('Something went wrong', message); return id;
return id; } catch (err: unknown) {
console.error('Error while trying to log error.', err);
}
} }

View File

@ -4,13 +4,10 @@ import {
SHORT_DEFAULT_BRANCH_TEMPLATE, SHORT_DEFAULT_BRANCH_TEMPLATE,
SHORT_DEFAULT_PR_TEMPLATE SHORT_DEFAULT_PR_TEMPLATE
} from '$lib/ai/prompts'; } from '$lib/ai/prompts';
import { import { type AIEvalOptions } from '$lib/ai/types';
type AIClient, import { type AIClient, type AnthropicModelName, type Prompt } from '$lib/ai/types';
type AIEvalOptions, import { andThenAsync, wrapAsync } from '$lib/result';
type AnthropicModelName, import { ok, type Result } from '$lib/result';
type Prompt
} from '$lib/ai/types';
import { andThenAsync, ok, wrapAsync, type Result } from '$lib/result';
import Anthropic from '@anthropic-ai/sdk'; import Anthropic from '@anthropic-ai/sdk';
import type { RawMessageStreamEvent } from '@anthropic-ai/sdk/resources/messages.mjs'; import type { RawMessageStreamEvent } from '@anthropic-ai/sdk/resources/messages.mjs';
import type { Stream } from '@anthropic-ai/sdk/streaming.mjs'; import type { Stream } from '@anthropic-ai/sdk/streaming.mjs';

View File

@ -6,7 +6,7 @@ import {
import { MessageRole, type PromptMessage, type AIClient, type Prompt } from '$lib/ai/types'; import { MessageRole, type PromptMessage, type AIClient, type Prompt } from '$lib/ai/types';
import { andThen, buildFailureFromAny, ok, wrap, wrapAsync, type Result } from '$lib/result'; import { andThen, buildFailureFromAny, ok, wrap, wrapAsync, type Result } from '$lib/result';
import { isNonEmptyObject } from '@gitbutler/ui/utils/typeguards'; import { isNonEmptyObject } from '@gitbutler/ui/utils/typeguards';
import { fetch, Body, Response } from '@tauri-apps/api/http'; import { fetch } from '@tauri-apps/plugin-http';
export const DEFAULT_OLLAMA_ENDPOINT = 'http://127.0.0.1:11434'; export const DEFAULT_OLLAMA_ENDPOINT = 'http://127.0.0.1:11434';
export const DEFAULT_OLLAMA_MODEL_NAME = 'llama3'; export const DEFAULT_OLLAMA_MODEL_NAME = 'llama3';
@ -137,9 +137,9 @@ ${JSON.stringify(OLLAMA_CHAT_MESSAGE_FORMAT_SCHEMA, null, 2)}`
* @param request - The OllamaChatRequest object containing the request details. * @param request - The OllamaChatRequest object containing the request details.
* @returns A Promise that resolves to the Response object. * @returns A Promise that resolves to the Response object.
*/ */
private async fetchChat(request: OllamaChatRequest): Promise<Result<Response<any>, Error>> { private async fetchChat(request: OllamaChatRequest): Promise<Result<any, Error>> {
const url = new URL(OllamaAPEndpoint.Chat, this.endpoint); const url = new URL(OllamaAPEndpoint.Chat, this.endpoint);
const body = Body.json(request); const body = JSON.stringify(request);
return await wrapAsync( return await wrapAsync(
async () => async () =>
await fetch(url.toString(), { await fetch(url.toString(), {

View File

@ -1,4 +1,4 @@
import { type Prompt, MessageRole } from '$lib/ai/types'; import { type Prompt, MessageRole } from './types';
export const SHORT_DEFAULT_COMMIT_TEMPLATE: Prompt = [ export const SHORT_DEFAULT_COMMIT_TEMPLATE: Prompt = [
{ {

View File

@ -1,4 +1,12 @@
import { DEFAULT_PR_SUMMARY_MAIN_DIRECTIVE, getPrTemplateDirective } from './prompts'; import { DEFAULT_PR_SUMMARY_MAIN_DIRECTIVE, getPrTemplateDirective } from './prompts';
import {
OpenAIModelName,
type AIClient,
AnthropicModelName,
ModelKind,
MessageRole,
type Prompt
} from './types';
import { AnthropicAIClient } from '$lib/ai/anthropicClient'; import { AnthropicAIClient } from '$lib/ai/anthropicClient';
import { ButlerAIClient } from '$lib/ai/butlerClient'; import { ButlerAIClient } from '$lib/ai/butlerClient';
import { import {
@ -7,14 +15,6 @@ import {
OllamaClient OllamaClient
} from '$lib/ai/ollamaClient'; } from '$lib/ai/ollamaClient';
import { OpenAIClient } from '$lib/ai/openAIClient'; import { OpenAIClient } from '$lib/ai/openAIClient';
import {
OpenAIModelName,
type AIClient,
AnthropicModelName,
ModelKind,
MessageRole,
type Prompt
} from '$lib/ai/types';
import { buildFailureFromAny, isFailure, ok, type Result } from '$lib/result'; import { buildFailureFromAny, isFailure, ok, type Result } from '$lib/result';
import { splitMessage } from '$lib/utils/commitMessage'; import { splitMessage } from '$lib/utils/commitMessage';
import { get } from 'svelte/store'; import { get } from 'svelte/store';

View File

@ -1,36 +1,24 @@
import { initPostHog } from '$lib/analytics/posthog'; import { initPostHog } from '$lib/analytics/posthog';
import { initSentry } from '$lib/analytics/sentry'; import { initSentry } from '$lib/analytics/sentry';
import { appAnalyticsConfirmed } from '$lib/config/appSettings'; import { AppSettings } from '$lib/config/appSettings';
import {
appMetricsEnabled,
appErrorReportingEnabled,
appNonAnonMetricsEnabled
} from '$lib/config/appSettings';
import posthog from 'posthog-js'; import posthog from 'posthog-js';
export function initAnalyticsIfEnabled() { export function initAnalyticsIfEnabled(appSettings: AppSettings) {
const analyticsConfirmed = appAnalyticsConfirmed(); appSettings.appAnalyticsConfirmed.onDisk().then((confirmed) => {
analyticsConfirmed.onDisk().then((confirmed) => {
if (confirmed) { if (confirmed) {
appErrorReportingEnabled() appSettings.appErrorReportingEnabled.onDisk().then((enabled) => {
.onDisk() if (enabled) initSentry();
.then((enabled) => { });
if (enabled) initSentry(); appSettings.appMetricsEnabled.onDisk().then((enabled) => {
}); if (enabled) initPostHog();
appMetricsEnabled() });
.onDisk() appSettings.appNonAnonMetricsEnabled.onDisk().then((enabled) => {
.then((enabled) => { if (enabled) {
if (enabled) initPostHog(); posthog.capture('nonAnonMetricsEnabled');
}); } else {
appNonAnonMetricsEnabled() posthog.capture('nonAnonMetricsDisabled');
.onDisk() }
.then((enabled) => { });
if (enabled) {
posthog.capture('nonAnonMetricsEnabled');
} else {
posthog.capture('nonAnonMetricsDisabled');
}
});
} }
}); });
} }

View File

@ -1,5 +1,5 @@
import { invoke as invokeTauri } from '@tauri-apps/api/core';
import { listen as listenTauri } from '@tauri-apps/api/event'; import { listen as listenTauri } from '@tauri-apps/api/event';
import { invoke as invokeTauri } from '@tauri-apps/api/tauri';
import type { EventCallback, EventName } from '@tauri-apps/api/event'; import type { EventCallback, EventName } from '@tauri-apps/api/event';
export enum Code { export enum Code {

View File

@ -2,7 +2,7 @@ import { invoke } from '$lib/backend/ipc';
import { showError } from '$lib/notifications/toasts'; import { showError } from '$lib/notifications/toasts';
import * as toasts from '$lib/utils/toasts'; import * as toasts from '$lib/utils/toasts';
import { persisted } from '@gitbutler/shared/persisted'; import { persisted } from '@gitbutler/shared/persisted';
import { open } from '@tauri-apps/api/dialog'; import { open } from '@tauri-apps/plugin-dialog';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { derived, get, writable, type Readable } from 'svelte/store'; import { derived, get, writable, type Readable } from 'svelte/store';
import type { ForgeType } from './forge'; import type { ForgeType } from './forge';

View File

@ -1,11 +1,10 @@
import { invoke as invokeIpc, listen as listenIpc } from './ipc'; import { invoke as invokeIpc, listen as listenIpc } from './ipc';
import { getVersion } from '@tauri-apps/api/app'; import { getVersion } from '@tauri-apps/api/app';
import { checkUpdate, onUpdaterEvent } from '@tauri-apps/api/updater'; import { check } from '@tauri-apps/plugin-updater';
export class Tauri { export class Tauri {
invoke = invokeIpc; invoke = invokeIpc;
listen = listenIpc; listen = listenIpc;
checkUpdate = checkUpdate; checkUpdate = check;
onUpdaterEvent = onUpdaterEvent;
currentVersion = getVersion; currentVersion = getVersion;
} }

View File

@ -2,6 +2,7 @@ import { Tauri } from './tauri';
import { UPDATE_INTERVAL_MS, UpdaterService } from './updater'; import { UPDATE_INTERVAL_MS, UpdaterService } from './updater';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest'; import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest';
import type { Update } from '@tauri-apps/plugin-updater';
/** /**
* It is important to understand the sync `get` method performs a store subscription * It is important to understand the sync `get` method performs a store subscription
@ -16,7 +17,6 @@ describe('Updater', () => {
tauri = new Tauri(); tauri = new Tauri();
updater = new UpdaterService(tauri); updater = new UpdaterService(tauri);
vi.spyOn(tauri, 'listen').mockReturnValue(async () => {}); vi.spyOn(tauri, 'listen').mockReturnValue(async () => {});
vi.spyOn(tauri, 'onUpdaterEvent').mockReturnValue(Promise.resolve(() => {}));
vi.spyOn(tauri, 'currentVersion').mockReturnValue(Promise.resolve('0.1')); vi.spyOn(tauri, 'currentVersion').mockReturnValue(Promise.resolve('0.1'));
}); });
@ -28,38 +28,33 @@ describe('Updater', () => {
test('should not show up-to-date on interval check', async () => { test('should not show up-to-date on interval check', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue( vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({ Promise.resolve({
shouldUpdate: false available: false
}) } as Update)
); );
vi.spyOn(tauri, 'onUpdaterEvent').mockImplementation(async (handler) => {
handler({ status: 'UPTODATE' });
return await Promise.resolve(() => {});
});
await updater.checkForUpdate(); await updater.checkForUpdate();
expect(get(updater.update)).toHaveProperty('status', undefined); expect(get(updater.update)).toMatchObject({});
}); });
test('should show up-to-date on manual check', async () => { test('should show up-to-date on manual check', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue( vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({ Promise.resolve({
shouldUpdate: false available: false,
}) version: '1'
} as Update)
); );
await updater.checkForUpdate(true); // manual = true; await updater.checkForUpdate(true); // manual = true;
expect(get(updater.update)).toHaveProperty('status', 'UPTODATE'); expect(get(updater.update)).toHaveProperty('status', 'Up-to-date');
}); });
test('should prompt again on new version', async () => { test('should prompt again on new version', async () => {
const body = 'release notes'; const body = 'release notes';
const date = '2024-01-01';
vi.spyOn(tauri, 'checkUpdate').mockReturnValue( vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({ Promise.resolve({
shouldUpdate: true, available: true,
manifest: { version: '1', body, date } version: '1',
}) body
} as Update)
); );
await updater.checkForUpdate(); await updater.checkForUpdate();
@ -70,9 +65,10 @@ describe('Updater', () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue( vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({ Promise.resolve({
shouldUpdate: true, available: true,
manifest: { version: '2', body, date } version: '2',
}) body
} as Update)
); );
await updater.checkForUpdate(); await updater.checkForUpdate();
const update2 = get(updater.update); const update2 = get(updater.update);
@ -83,13 +79,13 @@ describe('Updater', () => {
test('should not prompt download for seen version', async () => { test('should not prompt download for seen version', async () => {
const version = '1'; const version = '1';
const body = 'release notes'; const body = 'release notes';
const date = '2024-01-01';
vi.spyOn(tauri, 'checkUpdate').mockReturnValue( vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({ Promise.resolve({
shouldUpdate: true, available: true,
manifest: { version, body, date } version,
}) body
} as Update)
); );
const updater = new UpdaterService(tauri); const updater = new UpdaterService(tauri);
await updater.checkForUpdate(); await updater.checkForUpdate();
@ -101,22 +97,20 @@ describe('Updater', () => {
updater.dismiss(); updater.dismiss();
await updater.checkForUpdate(); await updater.checkForUpdate();
const update2 = get(updater.update); const update2 = get(updater.update);
expect(update2).toHaveProperty('version', undefined); expect(update2).toMatchObject({});
expect(update2).toHaveProperty('releaseNotes', undefined);
}); });
test('should check for updates continously', async () => { test('should check for updates continously', async () => {
const mock = vi.spyOn(tauri, 'checkUpdate').mockReturnValue( const mock = vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({ Promise.resolve({
shouldUpdate: false available: false
}) } as Update)
); );
const unsubscribe = updater.update.subscribe(() => {}); const unsubscribe = updater.update.subscribe(() => {});
await vi.advanceTimersToNextTimerAsync();
expect(mock).toHaveBeenCalledOnce(); expect(mock).toHaveBeenCalledOnce();
for (let i = 2; i < 24; i++) { for (let i = 2; i < 12; i++) {
await vi.advanceTimersByTimeAsync(UPDATE_INTERVAL_MS); await vi.advanceTimersByTimeAsync(UPDATE_INTERVAL_MS);
expect(mock).toHaveBeenCalledTimes(i); expect(mock).toHaveBeenCalledTimes(i);
} }

View File

@ -1,17 +1,30 @@
import { Tauri } from './tauri'; import { Tauri } from './tauri';
import { showToast } from '$lib/notifications/toasts'; import { showToast } from '$lib/notifications/toasts';
import { relaunch } from '@tauri-apps/api/process'; import { relaunch } from '@tauri-apps/plugin-process';
import { import { type DownloadEvent, Update } from '@tauri-apps/plugin-updater';
installUpdate,
type UpdateResult,
type UpdateManifest,
type UpdateStatus
} from '@tauri-apps/api/updater';
import posthog from 'posthog-js'; import posthog from 'posthog-js';
import { derived, readable, writable } from 'svelte/store'; import { writable } from 'svelte/store';
type Status = UpdateStatus | 'DOWNLOADED'; type UpdateStatus = {
const TIMEOUT_SECONDS = 30; version?: string;
releaseNotes?: string;
status?: InstallStatus | undefined;
};
export type InstallStatus =
| 'Checking'
| 'Downloading'
| 'Downloaded'
| 'Installing'
| 'Done'
| 'Up-to-date'
| 'Error';
const downloadStatusMap: { [K in DownloadEvent['event']]: InstallStatus } = {
Started: 'Downloading',
Progress: 'Downloading',
Finished: 'Downloaded'
};
export const UPDATE_INTERVAL_MS = 3600000; // Hourly export const UPDATE_INTERVAL_MS = 3600000; // Hourly
@ -19,39 +32,23 @@ export const UPDATE_INTERVAL_MS = 3600000; // Hourly
* Note that the Tauri API `checkUpdate` hangs indefinitely in dev mode, build * Note that the Tauri API `checkUpdate` hangs indefinitely in dev mode, build
* a nightly if you want to test the updater manually. * a nightly if you want to test the updater manually.
* *
* export TAURI_PRIVATE_KEY=doesnot * export TAURI_SIGNING_PRIVATE_KEY=doesnot
* export TAURI_KEY_PASSWORD=matter * export TAURI_SIGNING_PRIVATE_KEY_PASSWORD=matter
* ./scripts/release.sh --channel nightly --version "0.5.678" * ./scripts/release.sh --channel nightly --version "0.5.678"
*/ */
export class UpdaterService { export class UpdaterService {
readonly loading = writable(false); readonly loading = writable(false);
readonly status = writable<Status | undefined>(); readonly update = writable<UpdateStatus>({}, () => {
private manifest = writable<UpdateManifest | undefined>(undefined, () => {
this.start(); this.start();
return () => { return () => {
this.stop(); this.stop();
}; };
}); });
private currentVersion = readable<string | undefined>(undefined, (set) => {
this.tauri.currentVersion().then((version) => set(version));
});
readonly update = derived(
[this.manifest, this.status, this.currentVersion],
([manifest, status, currentVersion]) => {
return {
version: manifest?.version,
releaseNotes: manifest?.body,
status,
currentVersion
};
}
);
private intervalId: any; private intervalId: any;
private seenVersion: string | undefined; private seenVersion: string | undefined;
private lastCheckWasManual = false; private tauriDownload: Update['download'] | undefined;
private tauriInstall: Update['install'] | undefined;
unlistenStatus?: () => void; unlistenStatus?: () => void;
unlistenMenu?: () => void; unlistenMenu?: () => void;
@ -62,18 +59,6 @@ export class UpdaterService {
this.unlistenMenu = this.tauri.listen<string>('menu://global/update/clicked', () => { this.unlistenMenu = this.tauri.listen<string>('menu://global/update/clicked', () => {
this.checkForUpdate(true); this.checkForUpdate(true);
}); });
this.unlistenStatus = await this.tauri.onUpdaterEvent((event) => {
const { error, status } = event;
if (status !== 'UPTODATE' || this.lastCheckWasManual) {
this.status.set(status);
}
if (error) {
handleError(error, false);
posthog.capture('App Update Status Error', { error });
}
});
setInterval(async () => await this.checkForUpdate(), UPDATE_INTERVAL_MS); setInterval(async () => await this.checkForUpdate(), UPDATE_INTERVAL_MS);
this.checkForUpdate(); this.checkForUpdate();
} }
@ -81,7 +66,6 @@ export class UpdaterService {
private async stop() { private async stop() {
this.unlistenStatus?.(); this.unlistenStatus?.();
this.unlistenMenu?.(); this.unlistenMenu?.();
if (this.intervalId) { if (this.intervalId) {
clearInterval(this.intervalId); clearInterval(this.intervalId);
this.intervalId = undefined; this.intervalId = undefined;
@ -90,55 +74,81 @@ export class UpdaterService {
async checkForUpdate(manual = false) { async checkForUpdate(manual = false) {
this.loading.set(true); this.loading.set(true);
this.lastCheckWasManual = manual;
try { try {
const update = await Promise.race([ this.handleUpdate(await this.tauri.checkUpdate(), manual); // In DEV mode this never returns.
this.tauri.checkUpdate(), // In DEV mode this never returns.
new Promise<UpdateResult>((_resolve, reject) =>
// For manual testing use resolve instead of reject here.
setTimeout(
() => reject(`Timed out after ${TIMEOUT_SECONDS} seconds.`),
TIMEOUT_SECONDS * 1000
)
)
]);
await this.processUpdate(update, manual);
} catch (err: unknown) { } catch (err: unknown) {
// No toast unless manually invoked. handleError(err, manual);
if (manual) {
handleError(err, true);
} else {
console.error(err);
}
} finally { } finally {
this.loading.set(false); this.loading.set(false);
} }
} }
private async processUpdate(update: UpdateResult, manual: boolean) { private handleUpdate(update: Update | null, manual: boolean) {
const { shouldUpdate, manifest } = update; if (update === null) {
if (shouldUpdate === false && manual) { this.update.set({});
this.status.set('UPTODATE'); return;
} }
if (manifest && manifest.version !== this.seenVersion) { if (!update.available && manual) {
this.manifest.set(manifest); this.setStatus('Up-to-date');
this.seenVersion = manifest.version; } else if (
update.available &&
update.version !== this.seenVersion &&
update.currentVersion !== '0.0.0' // DEV mode.
) {
const { version, body, download, install } = update;
this.tauriDownload = download.bind(update);
this.tauriInstall = install.bind(update);
this.seenVersion = version;
this.update.set({
version,
releaseNotes: body,
status: undefined
});
} }
} }
async installUpdate() { async downloadAndInstall() {
this.loading.set(true); this.loading.set(true);
try { try {
await installUpdate(); await this.download();
await this.install();
posthog.capture('App Update Successful'); posthog.capture('App Update Successful');
} catch (err: any) { } catch (error: any) {
// We expect toast to be shown by error handling in `onUpdaterEvent` // We expect toast to be shown by error handling in `onUpdaterEvent`
posthog.capture('App Update Install Error', { error: err }); handleError(error, true);
this.update.set({ status: 'Error' });
posthog.capture('App Update Install Error', { error });
} finally { } finally {
this.loading.set(false); this.loading.set(false);
} }
} }
private async download() {
if (!this.tauriDownload) {
throw new Error('Download function not available.');
}
this.setStatus('Downloading');
await this.tauriDownload((progress: DownloadEvent) => {
this.setStatus(downloadStatusMap[progress.event]);
});
this.setStatus('Downloaded');
}
private async install() {
if (!this.tauriInstall) {
throw new Error('Install function not available.');
}
this.setStatus('Installing');
await this.tauriInstall();
this.setStatus('Done');
}
private setStatus(status: InstallStatus) {
this.update.update((update) => {
return { ...update, status };
});
}
async relaunchApp() { async relaunchApp() {
try { try {
await relaunch(); await relaunch();
@ -148,8 +158,7 @@ export class UpdaterService {
} }
dismiss() { dismiss() {
this.manifest.set(undefined); this.update.set({});
this.status.set(undefined);
} }
} }

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { UpdaterService } from '$lib/backend/updater'; import { UpdaterService, type InstallStatus } from '$lib/backend/updater';
import { showToast } from '$lib/notifications/toasts'; import { showToast } from '$lib/notifications/toasts';
import { getContext } from '@gitbutler/shared/context'; import { getContext } from '@gitbutler/shared/context';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
@ -11,10 +11,10 @@
let version = $state<string | undefined>(); let version = $state<string | undefined>();
let releaseNotes = $state<string | undefined>(); let releaseNotes = $state<string | undefined>();
let status = $state<string | undefined>(); let status = $state<InstallStatus | undefined>();
$effect(() => { $effect(() => {
({ version, releaseNotes, status } = $update || {}); ({ version, releaseNotes, status } = $update);
}); });
function handleDismiss() { function handleDismiss() {
@ -22,14 +22,14 @@
} }
</script> </script>
{#if version || status === 'UPTODATE'} {#if version || status === 'Up-to-date'}
<div class="update-banner" data-testid="update-banner" class:busy={$loading}> <div class="update-banner" data-testid="update-banner" class:busy={$loading}>
<div class="floating-button"> <div class="floating-button">
<Button icon="cross-small" style="ghost" onclick={handleDismiss} /> <Button icon="cross-small" style="ghost" onclick={handleDismiss} />
</div> </div>
<div class="img"> <div class="img">
<div class="circle-img"> <div class="circle-img">
{#if status !== 'DONE' && status !== 'UPTODATE'} {#if status !== 'Done' && status !== 'Up-to-date'}
<svg <svg
class="arrow-img" class="arrow-img"
width="12" width="12"
@ -96,17 +96,19 @@
</div> </div>
<h4 class="text-13 label"> <h4 class="text-13 label">
{#if status === 'UPTODATE'} {#if status === 'Up-to-date'}
You are up-to-date! You are up-to-date!
{:else if status === 'PENDING'} {:else if status === 'Downloading'}
Downloading update... Downloading update…
{:else if status === 'DOWNLOADED'} {:else if status === 'Downloaded'}
Installing update... Update downloaded
{:else if status === 'DONE'} {:else if status === 'Installing'}
Installing update…
{:else if status === 'Done'}
Install complete Install complete
{:else if status === 'CHECKING'} {:else if status === 'Checking'}
Checking for update… Checking for update…
{:else if status === 'ERROR'} {:else if status === 'Error'}
Error occurred Error occurred
{:else if version} {:else if version}
New version available New version available
@ -130,7 +132,7 @@
</Button> </Button>
{/if} {/if}
<div class="status-section"> <div class="status-section">
{#if status !== 'ERROR' && status !== 'UPTODATE'} {#if status !== 'Error' && status !== 'Up-to-date'}
<div class="sliding-gradient"></div> <div class="sliding-gradient"></div>
{/if} {/if}
<div class="cta-btn" transition:fade={{ duration: 100 }}> <div class="cta-btn" transition:fade={{ duration: 100 }}>
@ -141,12 +143,12 @@
kind="solid" kind="solid"
testId="download-update" testId="download-update"
onmousedown={async () => { onmousedown={async () => {
await updaterService.installUpdate(); await updaterService.downloadAndInstall();
}} }}
> >
Update to {version} Update to {version}
</Button> </Button>
{:else if status === 'UPTODATE'} {:else if status === 'Up-to-date'}
<Button <Button
wide wide
style="pop" style="pop"
@ -158,7 +160,7 @@
> >
Got it! Got it!
</Button> </Button>
{:else if status === 'DONE'} {:else if status === 'Done'}
<Button <Button
style="pop" style="pop"
kind="solid" kind="solid"

View File

@ -2,7 +2,9 @@ import AppUpdater from './AppUpdater.svelte';
import { Tauri } from '$lib/backend/tauri'; import { Tauri } from '$lib/backend/tauri';
import { UpdaterService } from '$lib/backend/updater'; import { UpdaterService } from '$lib/backend/updater';
import { render, screen } from '@testing-library/svelte'; import { render, screen } from '@testing-library/svelte';
import { get } from 'svelte/store';
import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest'; import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest';
import type { Update } from '@tauri-apps/plugin-updater';
describe('AppUpdater', () => { describe('AppUpdater', () => {
let tauri: Tauri; let tauri: Tauri;
@ -15,7 +17,6 @@ describe('AppUpdater', () => {
updater = new UpdaterService(tauri); updater = new UpdaterService(tauri);
context = new Map([[UpdaterService, updater]]); context = new Map([[UpdaterService, updater]]);
vi.spyOn(tauri, 'listen').mockReturnValue(async () => {}); vi.spyOn(tauri, 'listen').mockReturnValue(async () => {});
vi.spyOn(tauri, 'onUpdaterEvent').mockReturnValue(Promise.resolve(() => {}));
vi.spyOn(tauri, 'currentVersion').mockReturnValue(Promise.resolve('0.1')); vi.spyOn(tauri, 'currentVersion').mockReturnValue(Promise.resolve('0.1'));
}); });
@ -27,8 +28,8 @@ describe('AppUpdater', () => {
test('should be hidden if no update', async () => { test('should be hidden if no update', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue( vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({ Promise.resolve({
shouldUpdate: false version: '1'
}) } as Update)
); );
render(AppUpdater, { context }); render(AppUpdater, { context });
@ -41,13 +42,10 @@ describe('AppUpdater', () => {
test('should display download button', async () => { test('should display download button', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue( vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({ Promise.resolve({
shouldUpdate: true, available: true,
manifest: { version: '1',
version: '1', body: 'release notes'
body: 'release notes', } as Update)
date: '2024-01-01'
}
})
); );
render(AppUpdater, { context }); render(AppUpdater, { context });
@ -60,8 +58,8 @@ describe('AppUpdater', () => {
test('should display up-to-date on manaul check', async () => { test('should display up-to-date on manaul check', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue( vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({ Promise.resolve({
shouldUpdate: false available: false
}) } as Update)
); );
render(AppUpdater, { context }); render(AppUpdater, { context });
updater.checkForUpdate(true); updater.checkForUpdate(true);
@ -74,18 +72,31 @@ describe('AppUpdater', () => {
test('should display restart button on install complete', async () => { test('should display restart button on install complete', async () => {
vi.spyOn(tauri, 'checkUpdate').mockReturnValue( vi.spyOn(tauri, 'checkUpdate').mockReturnValue(
Promise.resolve({ Promise.resolve({
shouldUpdate: true, available: true,
manifest: { version: '1', body: 'release notes', date: '2024-01-01' } currentVersion: '1',
}) version: '2',
body: 'release notes',
download: () => {
console.log('HELLO');
},
install: () => {
console.log('WORLD');
}
} as Update)
); );
vi.spyOn(tauri, 'onUpdaterEvent').mockImplementation(async (handler) => {
handler({ status: 'DONE' });
return () => {};
});
render(AppUpdater, { context }); render(AppUpdater, { context });
updater.checkForUpdate(true); await updater.checkForUpdate(true);
await vi.runOnlyPendingTimersAsync();
console.log('download and install');
await updater.downloadAndInstall();
await vi.runOnlyPendingTimersAsync();
await vi.advanceTimersToNextTimerAsync(); await vi.advanceTimersToNextTimerAsync();
await vi.advanceTimersToNextTimerAsync();
await vi.advanceTimersToNextTimerAsync();
await vi.advanceTimersToNextTimerAsync();
await vi.advanceTimersToNextTimerAsync();
console.log(get(updater.update));
const button = screen.getByTestId('restart-app'); const button = screen.getByTestId('restart-app');
expect(button).toBeVisible(); expect(button).toBeVisible();

View File

@ -27,7 +27,7 @@
loading = true; loading = true;
try { try {
// TODO: Refactor temporary solution to forcing Windows to use system executable // TODO: Refactor temporary solution to forcing Windows to use system executable
if ($platformName === 'win32') { if (platformName === 'windows') {
project.preferred_key = 'systemExecutable'; project.preferred_key = 'systemExecutable';
await projectsService.updateProject(project); await projectsService.updateProject(project);
await baseBranchService.refresh(); await baseBranchService.refresh();
@ -41,7 +41,7 @@
</script> </script>
<DecorativeSplitView img={newProjectSvg}> <DecorativeSplitView img={newProjectSvg}>
{#if selectedBranch[0] && selectedBranch[0] !== '' && $platformName !== 'win32'} {#if selectedBranch[0] && selectedBranch[0] !== '' && platformName !== 'windows'}
{@const [remoteName, branchName] = selectedBranch[0].split(/\/(.*)/s)} {@const [remoteName, branchName] = selectedBranch[0].split(/\/(.*)/s)}
<KeysForm {remoteName} {branchName} disabled={loading} /> <KeysForm {remoteName} {branchName} disabled={loading} />
<div class="actions"> <div class="actions">
@ -59,7 +59,7 @@
on:branchSelected={async (e) => { on:branchSelected={async (e) => {
selectedBranch = e.detail; selectedBranch = e.detail;
// TODO: Temporary solution to forcing Windows to use system executable // TODO: Temporary solution to forcing Windows to use system executable
if ($platformName === 'win32') { if (platformName === 'windows') {
setTarget(); setTarget();
} }
}} }}

View File

@ -254,7 +254,7 @@
testId="set-base-branch" testId="set-base-branch"
id="set-base-branch" id="set-base-branch"
> >
{#if $platformName === 'win32'} {#if platformName === 'windows'}
Let's go Let's go
{:else} {:else}
Continue Continue

View File

@ -54,8 +54,8 @@
} }
async function readZipFile(path: string, filename?: string): Promise<File | Blob> { async function readZipFile(path: string, filename?: string): Promise<File | Blob> {
const { readBinaryFile } = await import('@tauri-apps/api/fs'); const { readFile } = await import('@tauri-apps/plugin-fs');
const file = await readBinaryFile(path); const file = await readFile(path);
const fileName = filename ?? path.split('/').pop(); const fileName = filename ?? path.split('/').pop();
return fileName return fileName
? new File([file], fileName, { type: 'application/zip' }) ? new File([file], fileName, { type: 'application/zip' })

View File

@ -2,99 +2,83 @@
* This file contains functions for managing application settings. * This file contains functions for managing application settings.
* Settings are persisted in <Application Data>/settings.json and are used by both the UI and the backend. * Settings are persisted in <Application Data>/settings.json and are used by both the UI and the backend.
* *
* @module appSettings * TODO: Rewrite this to be an injectable object so we don't need `storeInstance`.
*/ */
import { Store } from '@tauri-apps/plugin-store';
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
import { Store } from 'tauri-plugin-store-api';
const store = new Store('settings.json'); export async function loadAppSettings() {
const diskStore = await Store.load('settings.json', { autoSave: true });
/** return new AppSettings(diskStore);
* Persisted confirmation that user has confirmed their analytics settings.
*/
export function appAnalyticsConfirmed() {
return persisted(false, 'appAnalyticsConfirmed');
} }
/** export class AppSettings {
* Provides a writable store for obtaining or setting the current state of application metrics. constructor(private diskStore: Store) {}
* The application metrics can be enabled or disabled by setting the value of the store to true or false.
* @returns A writable store with the appMetricsEnabled config.
*/
export function appMetricsEnabled() {
return persisted(true, 'appMetricsEnabled');
}
/** /**
* Provides a writable store for obtaining or setting the current state of application error reporting. * Persisted confirmation that user has confirmed their analytics settings.
* The application error reporting can be enabled or disabled by setting the value of the store to true or false. */
* @returns A writable store with the appErrorReportingEnabled config. readonly appAnalyticsConfirmed = this.persisted(false, 'appAnalyticsConfirmed');
*/
export function appErrorReportingEnabled() {
return persisted(true, 'appErrorReportingEnabled');
}
/** /**
* Provides a writable store for obtaining or setting the current state of non-anonemous application metrics. * Provides a writable store for obtaining or setting the current state of application metrics.
* The setting can be enabled or disabled by setting the value of the store to true or false. * The application metrics can be enabled or disabled by setting the value of the store to true or false.
* @returns A writable store with the appNonAnonMetricsEnabled config. * @returns A writable store with the appMetricsEnabled config.
*/ */
export function appNonAnonMetricsEnabled() { readonly appMetricsEnabled = this.persisted(true, 'appMetricsEnabled');
return persisted(false, 'appNonAnonMetricsEnabled');
}
function persisted<T>(initial: T, key: string): Writable<T> & { onDisk: () => Promise<T> } { /**
async function setAndPersist(value: T, set: (value: T) => void) { * Provides a writable store for obtaining or setting the current state of application error reporting.
await store.set(key, value); * The application error reporting can be enabled or disabled by setting the value of the store to true or false.
await store.save(); * @returns A writable store with the appErrorReportingEnabled config.
*/
readonly appErrorReportingEnabled = this.persisted(true, 'appErrorReportingEnabled');
set(value); /**
* Provides a writable store for obtaining or setting the current state of non-anonemous application metrics.
* The setting can be enabled or disabled by setting the value of the store to true or false.
* @returns A writable store with the appNonAnonMetricsEnabled config.
*/
readonly appNonAnonMetricsEnabled = this.persisted(false, 'appNonAnonMetricsEnabled');
private persisted<T>(initial: T, key: string): Writable<T> & { onDisk: () => Promise<T> } {
const diskStore = this.diskStore;
const storeValueWithDefault = this.storeValueWithDefault.bind(this);
const keySpecificStore = writable<T>(initial, (set) => {
synchronize(set);
});
const subscribe = keySpecificStore.subscribe;
async function setAndPersist(value: T, set: (value: T) => void) {
diskStore?.set(key, value);
set(value);
}
async function synchronize(set: (value: T) => void): Promise<void> {
const value = await storeValueWithDefault(initial, key);
set(value);
}
async function set(value: T) {
setAndPersist(value, keySpecificStore.set);
}
async function onDisk() {
return await storeValueWithDefault(initial, key);
}
function update() {
throw 'Not implemented';
}
return { subscribe, set, update, onDisk };
} }
async function synchronize(set: (value: T) => void): Promise<void> { async storeValueWithDefault<T>(initial: T, key: string): Promise<T> {
const value = await storeValueWithDefault(initial, key); const stored = this.diskStore?.get(key) as T;
set(value); return stored === null ? initial : stored;
}
function update() {
throw 'Not implemented';
}
const thisStore = writable<T>(initial, (set) => {
synchronize(set);
});
async function set(value: T) {
setAndPersist(value, thisStore.set);
}
async function onDisk() {
return await storeValueWithDefault(initial, key);
}
const subscribe = thisStore.subscribe;
return {
subscribe,
set,
update,
onDisk
};
}
async function storeValueWithDefault<T>(initial: T, key: string): Promise<T> {
try {
await store.load();
} catch {
// If file does not exist, reset it
store.reset();
}
const stored = (await store.get(key)) as T;
if (stored === null) {
return initial;
} else {
return stored;
} }
} }

View File

@ -1,4 +1,4 @@
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/core';
import { readable } from 'svelte/store'; import { readable } from 'svelte/store';
export const editor = readable<string>('vscode', (set) => { export const editor = readable<string>('vscode', (set) => {

View File

@ -113,9 +113,9 @@
role="menu" role="menu"
> >
<!-- condition prevents split second UI shift --> <!-- condition prevents split second UI shift -->
{#if $platformName || env.PUBLIC_TESTING} {#if platformName || env.PUBLIC_TESTING}
<div class="navigation-top"> <div class="navigation-top">
{#if $platformName === 'darwin'} {#if platformName === 'macos'}
<div class="drag-region" data-tauri-drag-region></div> <div class="drag-region" data-tauri-drag-region></div>
{/if} {/if}
<ProjectSelector isNavCollapsed={$isNavCollapsed} /> <ProjectSelector isNavCollapsed={$isNavCollapsed} />

View File

@ -10,9 +10,9 @@
import Spacer from '@gitbutler/ui/Spacer.svelte'; import Spacer from '@gitbutler/ui/Spacer.svelte';
import Textbox from '@gitbutler/ui/Textbox.svelte'; import Textbox from '@gitbutler/ui/Textbox.svelte';
import * as Sentry from '@sentry/sveltekit'; import * as Sentry from '@sentry/sveltekit';
import { open } from '@tauri-apps/api/dialog';
import { documentDir } from '@tauri-apps/api/path'; import { documentDir } from '@tauri-apps/api/path';
import { join } from '@tauri-apps/api/path'; import { join } from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
import { posthog } from 'posthog-js'; import { posthog } from 'posthog-js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';

View File

@ -1,6 +1,3 @@
import { platform } from '@tauri-apps/api/os'; import { platform } from '@tauri-apps/plugin-os';
import { readable } from 'svelte/store';
export const platformName = readable<string | undefined>(undefined, (set) => { export const platformName = platform();
platform().then((platform) => set(platform));
});

View File

@ -1,13 +1,7 @@
import { AISecretHandle } from '$lib/ai/service';
import { invoke } from '$lib/backend/ipc'; import { invoke } from '$lib/backend/ipc';
import { buildContext } from '@gitbutler/shared/context'; import { buildContext } from '@gitbutler/shared/context';
import type { GitConfigService } from '$lib/backend/gitConfigService'; import type { GitConfigService } from '$lib/backend/gitConfigService';
const MIGRATION_HANDLES = [
AISecretHandle.AnthropicKey.toString(),
AISecretHandle.OpenAIKey.toString()
];
export type SecretsService = { export type SecretsService = {
get(handle: string): Promise<string | undefined>; get(handle: string): Promise<string | undefined>;
set(handle: string, secret: string): Promise<void>; set(handle: string, secret: string): Promise<void>;
@ -22,12 +16,6 @@ export class RustSecretService implements SecretsService {
async get(handle: string) { async get(handle: string) {
const secret = await invoke<string>('secret_get_global', { handle }); const secret = await invoke<string>('secret_get_global', { handle });
if (secret) return secret; if (secret) return secret;
if (MIGRATION_HANDLES.includes(handle)) {
const key = 'gitbutler.' + handle;
const migratedSecret = await this.migrate(key, handle);
if (migratedSecret !== undefined) return migratedSecret;
}
} }
async set(handle: string, secret: string) { async set(handle: string, secret: string) {

View File

@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import { initAnalyticsIfEnabled } from '$lib/analytics/analytics'; import { initAnalyticsIfEnabled } from '$lib/analytics/analytics';
import { AppSettings } from '$lib/config/appSettings';
import AnalyticsSettings from '$lib/settings/AnalyticsSettings.svelte'; import AnalyticsSettings from '$lib/settings/AnalyticsSettings.svelte';
import { getContext } from '@gitbutler/shared/context';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import type { Writable } from 'svelte/store';
export let analyticsConfirmed: Writable<boolean>; const appSettings = getContext(AppSettings);
const analyticsConfirmed = appSettings.appAnalyticsConfirmed;
</script> </script>
<div class="analytics-confirmation"> <div class="analytics-confirmation">
@ -19,7 +21,7 @@
icon="chevron-right-small" icon="chevron-right-small"
onclick={() => { onclick={() => {
$analyticsConfirmed = true; $analyticsConfirmed = true;
initAnalyticsIfEnabled(); initAnalyticsIfEnabled(appSettings);
}} }}
> >
Continue Continue

View File

@ -1,16 +1,14 @@
<script lang="ts"> <script lang="ts">
import SectionCard from '$lib/components/SectionCard.svelte'; import SectionCard from '$lib/components/SectionCard.svelte';
import { import { AppSettings } from '$lib/config/appSettings';
appErrorReportingEnabled,
appMetricsEnabled,
appNonAnonMetricsEnabled
} from '$lib/config/appSettings';
import Link from '$lib/shared/Link.svelte'; import Link from '$lib/shared/Link.svelte';
import { getContext } from '@gitbutler/shared/context';
import Toggle from '@gitbutler/ui/Toggle.svelte'; import Toggle from '@gitbutler/ui/Toggle.svelte';
const errorReportingEnabled = appErrorReportingEnabled(); const appSettings = getContext(AppSettings);
const metricsEnabled = appMetricsEnabled(); const errorReportingEnabled = appSettings.appErrorReportingEnabled;
const nonAnonMetricsEnabled = appNonAnonMetricsEnabled(); const metricsEnabled = appSettings.appMetricsEnabled;
const nonAnonMetricsEnabled = appSettings.appNonAnonMetricsEnabled;
</script> </script>
<div class="analytics-settings__content"> <div class="analytics-settings__content">

View File

@ -12,7 +12,7 @@
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import Textbox from '@gitbutler/ui/Textbox.svelte'; import Textbox from '@gitbutler/ui/Textbox.svelte';
import Toggle from '@gitbutler/ui/Toggle.svelte'; import Toggle from '@gitbutler/ui/Toggle.svelte';
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/core';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
const projectsService = getContext(ProjectsService); const projectsService = getContext(ProjectsService);

View File

@ -26,7 +26,7 @@
<Section> <Section>
<CommitSigningForm /> <CommitSigningForm />
{#if $platformName !== 'win32'} {#if platformName !== 'windows'}
<Spacer /> <Spacer />
<KeysForm showProjectName={false} /> <KeysForm showProjectName={false} />
{/if} {/if}

View File

@ -1,5 +1,5 @@
import { persisted } from '@gitbutler/shared/persisted'; import { persisted } from '@gitbutler/shared/persisted';
import { get, type Readable } from 'svelte/store'; import { get, type Readable, type Writable } from 'svelte/store';
import type { Project } from '$lib/backend/projects'; import type { Project } from '$lib/backend/projects';
import type { GitHostIssueService } from '$lib/gitHost/interface/gitHostIssueService'; import type { GitHostIssueService } from '$lib/gitHost/interface/gitHostIssueService';
@ -12,15 +12,13 @@ export type Topic = {
}; };
export class TopicService { export class TopicService {
topics = persisted<Topic[]>([], this.localStorageKey); topics: Writable<Topic[]>;
constructor( constructor(
private project: Project, private project: Project,
private issueService: Readable<GitHostIssueService | undefined> private issueService: Readable<GitHostIssueService | undefined>
) {} ) {
this.topics = persisted<Topic[]>([], `TopicService--${this.project.id}`);
private get localStorageKey(): string {
return `TopicService--${this.project.id}`;
} }
create(title: string, body: string, hasIssue: boolean = false): Topic { create(title: string, body: string, hasIssue: boolean = false): Topic {

View File

@ -1,7 +1,7 @@
import { listen } from '$lib/backend/ipc'; import { listen } from '$lib/backend/ipc';
import { parseRemoteFiles } from '$lib/vbranches/remoteCommits'; import { parseRemoteFiles } from '$lib/vbranches/remoteCommits';
import { RemoteFile } from '$lib/vbranches/types'; import { RemoteFile } from '$lib/vbranches/types';
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/core';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { readable, type Readable } from 'svelte/store'; import { readable, type Readable } from 'svelte/store';
import type { Project } from '$lib/backend/projects'; import type { Project } from '$lib/backend/projects';

View File

@ -1,6 +1,7 @@
import { appWindow, type Theme } from '@tauri-apps/api/window'; import { getCurrentWindow, type Theme } from '@tauri-apps/api/window';
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
import type { Settings } from '$lib/settings/userSettings'; import type { Settings } from '$lib/settings/userSettings';
const appWindow = getCurrentWindow();
export const theme = writable('dark'); export const theme = writable('dark');

View File

@ -22,6 +22,7 @@
import AppUpdater from '$lib/components/AppUpdater.svelte'; import AppUpdater from '$lib/components/AppUpdater.svelte';
import PromptModal from '$lib/components/PromptModal.svelte'; import PromptModal from '$lib/components/PromptModal.svelte';
import ShareIssueModal from '$lib/components/ShareIssueModal.svelte'; import ShareIssueModal from '$lib/components/ShareIssueModal.svelte';
import { AppSettings } from '$lib/config/appSettings';
import { import {
createGitHubUserServiceStore as createGitHubUserServiceStore, createGitHubUserServiceStore as createGitHubUserServiceStore,
GitHubUserService GitHubUserService
@ -70,6 +71,7 @@
setContext(AIPromptService, data.aiPromptService); setContext(AIPromptService, data.aiPromptService);
setContext(LineManagerFactory, data.lineManagerFactory); setContext(LineManagerFactory, data.lineManagerFactory);
setContext(StackingLineManagerFactory, data.stackingLineManagerFactory); setContext(StackingLineManagerFactory, data.stackingLineManagerFactory);
setContext(AppSettings, data.appSettings);
const webRoutesService = new WebRoutesService(true, env.PUBLIC_CLOUD_BASE_URL); const webRoutesService = new WebRoutesService(true, env.PUBLIC_CLOUD_BASE_URL);
const desktopRoutesService = new DesktopRoutesService(webRoutesService); const desktopRoutesService = new DesktopRoutesService(webRoutesService);

View File

@ -8,6 +8,7 @@ import { ProjectsService } from '$lib/backend/projects';
import { PromptService } from '$lib/backend/prompt'; import { PromptService } from '$lib/backend/prompt';
import { Tauri } from '$lib/backend/tauri'; import { Tauri } from '$lib/backend/tauri';
import { UpdaterService } from '$lib/backend/updater'; import { UpdaterService } from '$lib/backend/updater';
import { loadAppSettings } from '$lib/config/appSettings';
import { RemotesService } from '$lib/remotes/service'; import { RemotesService } from '$lib/remotes/service';
import { RustSecretService } from '$lib/secrets/secretsService'; import { RustSecretService } from '$lib/secrets/secretsService';
import { TokenMemoryService } from '$lib/stores/tokenMemoryService'; import { TokenMemoryService } from '$lib/stores/tokenMemoryService';
@ -28,7 +29,10 @@ export const csr = true;
// eslint-disable-next-line // eslint-disable-next-line
export const load: LayoutLoad = async () => { export const load: LayoutLoad = async () => {
initAnalyticsIfEnabled(); // Awaited and will block initial render, but it is necessary in order to respect the user
// settings on telemetry.
const appSettings = await loadAppSettings();
initAnalyticsIfEnabled(appSettings);
// TODO: Find a workaround to avoid this dynamic import // TODO: Find a workaround to avoid this dynamic import
// https://github.com/sveltejs/kit/issues/905 // https://github.com/sveltejs/kit/issues/905
@ -57,6 +61,7 @@ export const load: LayoutLoad = async () => {
return { return {
commandService, commandService,
tokenMemoryService, tokenMemoryService,
appSettings,
authService, authService,
cloud: httpClient, cloud: httpClient,
projectsService, projectsService,

View File

@ -3,16 +3,18 @@
import newProjectSvg from '$lib/assets/illustrations/new-project.svg?raw'; import newProjectSvg from '$lib/assets/illustrations/new-project.svg?raw';
import DecorativeSplitView from '$lib/components/DecorativeSplitView.svelte'; import DecorativeSplitView from '$lib/components/DecorativeSplitView.svelte';
import Welcome from '$lib/components/Welcome.svelte'; import Welcome from '$lib/components/Welcome.svelte';
import { appAnalyticsConfirmed } from '$lib/config/appSettings'; import { AppSettings } from '$lib/config/appSettings';
import AnalyticsConfirmation from '$lib/settings/AnalyticsConfirmation.svelte'; import AnalyticsConfirmation from '$lib/settings/AnalyticsConfirmation.svelte';
import { getContext } from '@gitbutler/shared/context';
const analyticsConfirmed = appAnalyticsConfirmed(); const appSettings = getContext(AppSettings);
const analyticsConfirmed = appSettings.appAnalyticsConfirmed;
</script> </script>
<DecorativeSplitView img={$analyticsConfirmed ? newProjectSvg : analyticsSvg}> <DecorativeSplitView img={$analyticsConfirmed ? newProjectSvg : analyticsSvg}>
{#if $analyticsConfirmed} {#if $analyticsConfirmed}
<Welcome /> <Welcome />
{:else} {:else}
<AnalyticsConfirmation {analyticsConfirmed} /> <AnalyticsConfirmation />
{/if} {/if}
</DecorativeSplitView> </DecorativeSplitView>

View File

@ -45,7 +45,7 @@ export default defineConfig({
strict: false strict: false
} }
}, },
// to make use of `TAURI_DEBUG` and other env variables // to make use of `TAURI_ENV_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ['VITE_', 'TAURI_'], envPrefix: ['VITE_', 'TAURI_'],
resolve: { resolve: {
@ -53,9 +53,9 @@ export default defineConfig({
}, },
build: { build: {
// Tauri supports es2021 // Tauri supports es2021
target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13', target: process.env.TAURI_ENV_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
// minify production builds // minify production builds
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false,
// ship sourcemaps for better sentry error reports // ship sourcemaps for better sentry error reports
sourcemap: true sourcemap: true
}, },

View File

@ -1 +1,5 @@
/gitbutler-git-* # Tauri generated output.
gen/
# Extra binaries, figure out why they end up in this directory on build.
gitbutler-git-*

View File

@ -7,6 +7,7 @@ publish = false
[lib] [lib]
doctest = false doctest = false
crate-type = ["lib", "staticlib", "cdylib"]
[[bin]] [[bin]]
name = "gitbutler-tauri" name = "gitbutler-tauri"
@ -14,7 +15,7 @@ path = "src/main.rs"
test = false test = false
[build-dependencies] [build-dependencies]
tauri-build = { version = "1.5.5", features = [] } tauri-build = { version = "2.0.2", features = [] }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4" pretty_assertions = "1.4"
@ -37,11 +38,18 @@ once_cell = "1.20"
reqwest = { version = "0.12.8", features = ["json"] } reqwest = { version = "0.12.8", features = ["json"] }
serde.workspace = true serde.workspace = true
serde_json = { version = "1.0", features = ["std", "arbitrary_precision"] } serde_json = { version = "1.0", features = ["std", "arbitrary_precision"] }
tauri-plugin-context-menu = { git = "https://github.com/c2r0b/tauri-plugin-context-menu", branch = "main" } tauri = { version = "^2.0.6", features = ["unstable"] }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-dialog = "2.0.3"
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-fs = "2.0.3"
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-http = "2.0.3"
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-log = "2.0.1"
tauri-plugin-os = "2.0.1"
tauri-plugin-process = "2.0.1"
tauri-plugin-shell = "2.0.2"
tauri-plugin-single-instance = "2.0.1"
tauri-plugin-store = "2.1.0"
tauri-plugin-updater = "2.0.2"
tauri-plugin-window-state = "2.0.1"
parking_lot.workspace = true parking_lot.workspace = true
log = "^0.4" log = "^0.4"
thiserror.workspace = true thiserror.workspace = true
@ -78,22 +86,6 @@ gitbutler-forge.workspace = true
open = "5" open = "5"
url = "2.5.2" url = "2.5.2"
[dependencies.tauri]
version = "1.8.0"
features = [
"http-all",
"os-all",
"dialog-open",
"fs-read-file",
"path-all",
"process-relaunch",
"protocol-asset",
"window-maximize",
"window-start-dragging",
"window-unmaximize",
"shell-open",
]
[lints.clippy] [lints.clippy]
all = "deny" all = "deny"
perf = "deny" perf = "deny"

View File

@ -0,0 +1,18 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main",
"description": "permissions for gitbutler tauri",
"windows": ["main"],
"local": true,
"permissions": [
"core:default",
"core:window:allow-start-dragging",
"core:window:default",
"dialog:allow-open",
"log:default",
"process:default",
"shell:allow-open",
"store:default",
"updater:default"
]
}

View File

@ -0,0 +1,7 @@
{
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": ["main"],
"permissions": ["core:default"]
}

View File

@ -7,7 +7,7 @@ use tracing_subscriber::{fmt::format::FmtSpan, layer::SubscriberExt, Layer};
pub fn init(app_handle: &AppHandle, performance_logging: bool) { pub fn init(app_handle: &AppHandle, performance_logging: bool) {
let logs_dir = app_handle let logs_dir = app_handle
.path_resolver() .path()
.app_log_dir() .app_log_dir()
.expect("failed to get logs dir"); .expect("failed to get logs dir");
fs::create_dir_all(&logs_dir).expect("failed to create logs dir"); fs::create_dir_all(&logs_dir).expect("failed to create logs dir");
@ -88,11 +88,7 @@ pub fn init(app_handle: &AppHandle, performance_logging: bool) {
fn get_server_addr(app_handle: &AppHandle) -> (Ipv4Addr, u16) { fn get_server_addr(app_handle: &AppHandle) -> (Ipv4Addr, u16) {
let config = app_handle.config(); let config = app_handle.config();
let product_name = config let product_name = config.product_name.as_ref().expect("product name not set");
.package
.product_name
.as_ref()
.expect("product name not set");
let port = if product_name.eq("GitButler") { let port = if product_name.eq("GitButler") {
6667 6667
} else if product_name.eq("GitButler Nightly") { } else if product_name.eq("GitButler Nightly") {

View File

@ -15,16 +15,15 @@ use gitbutler_tauri::{
askpass, commands, config, forge, github, logs, menu, modes, open, projects, remotes, repo, askpass, commands, config, forge, github, logs, menu, modes, open, projects, remotes, repo,
secret, stack, undo, users, virtual_branches, zip, App, WindowState, secret, stack, undo, users, virtual_branches, zip, App, WindowState,
}; };
use tauri::Emitter;
use tauri::{generate_context, Manager}; use tauri::{generate_context, Manager};
use tauri_plugin_log::LogTarget; use tauri_plugin_log::{Target, TargetKind};
fn main() { fn main() {
let performance_logging = std::env::var_os("GITBUTLER_PERFORMANCE_LOG").is_some(); let performance_logging = std::env::var_os("GITBUTLER_PERFORMANCE_LOG").is_some();
gitbutler_project::configure_git2(); gitbutler_project::configure_git2();
let tauri_context = generate_context!(); let tauri_context = generate_context!();
gitbutler_secret::secret::set_application_namespace( gitbutler_secret::secret::set_application_namespace(&tauri_context.config().identifier);
&tauri_context.config().tauri.bundle.identifier,
);
tokio::runtime::Builder::new_multi_thread() tokio::runtime::Builder::new_multi_thread()
.enable_all() .enable_all()
@ -34,33 +33,30 @@ fn main() {
tauri::async_runtime::set(tokio::runtime::Handle::current()); tauri::async_runtime::set(tokio::runtime::Handle::current());
let log = tauri_plugin_log::Builder::default() let log = tauri_plugin_log::Builder::default()
.log_name("ui-logs") .target(Target::new(TargetKind::LogDir {
.target(LogTarget::LogDir) file_name: Some("ui-logs".to_string()),
}))
.level(log::LevelFilter::Error); .level(log::LevelFilter::Error);
let builder = tauri::Builder::default() let builder = tauri::Builder::default()
.setup(move |tauri_app| { .setup(move |tauri_app| {
let window = gitbutler_tauri::window::create( let window = gitbutler_tauri::window::create(
&tauri_app.handle(), tauri_app.handle(),
"main", "main",
"index.html".into(), "index.html".into(),
) )
.expect("Failed to create window"); .expect("Failed to create window");
#[cfg(debug_assertions)]
window.open_devtools();
tokio::task::spawn(async move { // TODO(mtsgrd): Is there a better way to disable devtools in E2E tests?
let mut six_hours = #[cfg(debug_assertions)]
tokio::time::interval(tokio::time::Duration::new(6 * 60 * 60, 0)); if tauri_app.config().product_name != Some("GitButler Test".to_string()) {
loop { window.open_devtools();
six_hours.tick().await; }
_ = window.emit_and_trigger("tauri://update", ());
}
});
let app_handle = tauri_app.handle(); let app_handle = tauri_app.handle();
logs::init(&app_handle, performance_logging); logs::init(app_handle, performance_logging);
tracing::info!( tracing::info!(
"system git executable for fetch/push: {git:?}", "system git executable for fetch/push: {git:?}",
git = gix::path::env::exe_invocation(), git = gix::path::env::exe_invocation(),
@ -81,14 +77,14 @@ fn main() {
let handle = app_handle.clone(); let handle = app_handle.clone();
move |event| { move |event| {
handle handle
.emit_all("git_prompt", event) .emit("git_prompt", event)
.expect("tauri event emission doesn't fail in practice") .expect("tauri event emission doesn't fail in practice")
} }
}); });
} }
let (app_data_dir, app_cache_dir, app_log_dir) = { let (app_data_dir, app_cache_dir, app_log_dir) = {
let paths = app_handle.path_resolver(); let paths = app_handle.path();
( (
paths.app_data_dir().expect("missing app data dir"), paths.app_data_dir().expect("missing app data dir"),
paths.app_cache_dir().expect("missing app cache dir"), paths.app_cache_dir().expect("missing app cache dir"),
@ -116,10 +112,20 @@ fn main() {
}); });
app_handle.manage(app); app_handle.manage(app);
tauri_app.on_menu_event(move |_handle, event| {
menu::handle_event(&window.clone(), &event)
});
Ok(()) Ok(())
}) })
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_single_instance::init(|_, _, _| {})) .plugin(tauri_plugin_single_instance::init(|_, _, _| {}))
.plugin(tauri_plugin_context_menu::init()) .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
// .plugin(tauri_plugin_context_menu::init())
.plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_store::Builder::default().build())
.plugin(log.build()) .plugin(log.build())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
@ -221,36 +227,30 @@ fn main() {
forge::commands::get_available_review_templates, forge::commands::get_available_review_templates,
forge::commands::get_review_template_contents, forge::commands::get_review_template_contents,
]) ])
.menu(menu::build(tauri_context.package_info())) .menu(menu::build)
.on_menu_event(|event| menu::handle_event(&event)) .on_window_event(|window, event| match event {
.on_window_event(|event| { #[cfg(target_os = "macos")]
let window = event.window(); tauri::WindowEvent::CloseRequested { api, .. } => {
match event.event() { if window.app_handle().windows().len() == 1 {
#[cfg(target_os = "macos")] tracing::debug!("Hiding all application windows and preventing exit");
tauri::WindowEvent::CloseRequested { api, .. } => { window.app_handle().hide().ok();
if window.app_handle().windows().len() == 1 { api.prevent_close();
tracing::debug!(
"Hiding all application windows and preventing exit"
);
window.app_handle().hide().ok();
api.prevent_close();
}
} }
tauri::WindowEvent::Destroyed => {
window
.app_handle()
.state::<WindowState>()
.remove(window.label());
}
tauri::WindowEvent::Focused(focused) if *focused => {
window
.app_handle()
.state::<WindowState>()
.flush(window.label())
.ok();
}
_ => {}
} }
tauri::WindowEvent::Destroyed => {
window
.app_handle()
.state::<WindowState>()
.remove(window.label());
}
tauri::WindowEvent::Focused(focused) if *focused => {
window
.app_handle()
.state::<WindowState>()
.flush(window.label())
.ok();
}
_ => {}
}); });
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]

View File

@ -1,14 +1,14 @@
use std::{env, fs}; use std::{env, fs};
use crate::open::open_that as open_url;
use anyhow::Context; use anyhow::Context;
use gitbutler_error::{error, error::Code}; use gitbutler_error::error::{self, Code};
use serde_json::json; use serde_json::json;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::AboutMetadata; use tauri::menu::AboutMetadata;
use tauri::Emitter;
use tauri::{ use tauri::{
AppHandle, CustomMenuItem, Manager, Menu, MenuItem, PackageInfo, Runtime, Submenu, menu::{Menu, MenuEvent, MenuItemBuilder, PredefinedMenuItem, Submenu, SubmenuBuilder},
WindowMenuEvent, AppHandle, Manager, Runtime, WebviewWindow,
}; };
use tracing::instrument; use tracing::instrument;
@ -16,21 +16,22 @@ use crate::error::Error;
#[tauri::command(async)] #[tauri::command(async)]
#[instrument(skip(handle), err(Debug))] #[instrument(skip(handle), err(Debug))]
pub fn menu_item_set_enabled( pub fn menu_item_set_enabled(handle: AppHandle, id: &str, enabled: bool) -> Result<(), Error> {
handle: AppHandle,
menu_item_id: &str,
enabled: bool,
) -> Result<(), Error> {
let window = handle let window = handle
.get_window("main") .get_window("main")
.expect("main window always present"); .expect("main window always present");
let menu_item = window let menu_item = window
.menu_handle() .menu()
.try_get_item(menu_item_id) .context("menu not found")?
.with_context(|| error::Context::new(format!("menu item not found: {}", menu_item_id)))?; .get(id)
.with_context(|| error::Context::new(format!("menu item not found: {}", id)))?;
menu_item.set_enabled(enabled).context(Code::Unknown)?; menu_item
.as_menuitem()
.context(Code::Unknown)?
.set_enabled(enabled)
.context(Code::Unknown)?;
Ok(()) Ok(())
} }
@ -56,275 +57,264 @@ fn check_if_installed(executable_name: &str) -> bool {
} }
} }
pub fn build(_package_info: &PackageInfo) -> Menu { pub fn build<R: Runtime>(handle: &AppHandle<R>) -> tauri::Result<tauri::menu::Menu<R>> {
let mut menu = Menu::new(); let check_for_updates =
MenuItemBuilder::with_id("global/update", "Check for updates…").build(handle)?;
// Used in different menus depending on target os.
let check_for_updates = CustomMenuItem::new("global/update", "Check for updates…");
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ let app_name = handle
let app_name = &_package_info.name; .config()
.product_name
menu = menu.add_submenu(Submenu::new( .clone()
app_name, .context("App name not defined.")?;
Menu::new()
.add_native_item(MenuItem::About(
app_name.to_string(),
AboutMetadata::default(),
))
.add_native_item(MenuItem::Separator)
.add_item(CustomMenuItem::new("global/settings", "Settings").accelerator("Cmd+,"))
.add_item(check_for_updates)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Services)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
.add_native_item(MenuItem::ShowAll)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Quit),
));
}
let mut file_menu = Menu::new();
file_menu = file_menu.add_item(
CustomMenuItem::new("file/add-local-repo", "Add Local Repository…")
.accelerator("CmdOrCtrl+O"),
);
file_menu = file_menu.add_item(
CustomMenuItem::new("file/clone-repo", "Clone Repository…")
.accelerator("CmdOrCtrl+Shift+O"),
);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ let mac_menu = &SubmenuBuilder::new(handle, app_name)
// NB: macOS has the concept of having an app running but its .about(Some(AboutMetadata::default()))
// window closed, but other platforms do not .separator()
file_menu = file_menu.add_native_item(MenuItem::Separator); .text("global/settings", "Settings")
file_menu = file_menu.add_native_item(MenuItem::CloseWindow); .item(&check_for_updates)
} .separator()
.services()
.separator()
.hide()
.hide_others()
.show_all()
.separator()
.quit()
.build()?;
let file_menu = &SubmenuBuilder::new(handle, "File")
.items(&[
&MenuItemBuilder::with_id("file/add-local-repo", "Add Local Repository…")
.accelerator("CmdOrCtrl+O")
.build(handle)?,
&MenuItemBuilder::with_id("file/clone-repo", "Clone Repository…")
.accelerator("CmdOrCtrl+Shift+O")
.build(handle)?,
&PredefinedMenuItem::separator(handle)?,
])
.build()?;
#[cfg(target_os = "macos")]
file_menu.append(&PredefinedMenuItem::close_window(handle, None)?)?;
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
{ file_menu.append_items(&[&PredefinedMenuItem::quit(handle, None)?, &check_for_updates])?;
file_menu = file_menu.add_native_item(MenuItem::Separator);
file_menu = file_menu.add_native_item(MenuItem::Quit);
file_menu = file_menu.add_item(check_for_updates)
}
menu = menu.add_submenu(Submenu::new("File", file_menu));
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
let mut edit_menu = Menu::new(); let edit_menu = &Submenu::new(handle, "Edit", true)?;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
edit_menu = edit_menu.add_native_item(MenuItem::Undo); edit_menu.append_items(&[
edit_menu = edit_menu.add_native_item(MenuItem::Redo); &PredefinedMenuItem::undo(handle, None)?,
edit_menu = edit_menu.add_native_item(MenuItem::Separator); &PredefinedMenuItem::redo(handle, None)?,
&PredefinedMenuItem::separator(handle)?,
])?;
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{ {
edit_menu = edit_menu.add_native_item(MenuItem::Cut); edit_menu.append_items(&[
edit_menu = edit_menu.add_native_item(MenuItem::Copy); &PredefinedMenuItem::cut(handle, None)?,
edit_menu = edit_menu.add_native_item(MenuItem::Paste); &PredefinedMenuItem::copy(handle, None)?,
&PredefinedMenuItem::paste(handle, None)?,
])?;
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ edit_menu.append(&PredefinedMenuItem::select_all(handle, None)?)?;
edit_menu = edit_menu.add_native_item(MenuItem::SelectAll);
}
#[cfg(not(target_os = "linux"))] let view_menu = &Submenu::new(handle, "View", true)?;
{
menu = menu.add_submenu(Submenu::new("Edit", edit_menu));
}
let mut view_menu = Menu::new();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ view_menu.append(&PredefinedMenuItem::fullscreen(handle, None)?)?;
view_menu = view_menu.add_native_item(MenuItem::EnterFullScreen); view_menu.append_items(&[
} &MenuItemBuilder::with_id("view/switch-theme", "Switch Theme")
.accelerator("CmdOrCtrl+T")
view_menu = view_menu.add_item( .build(handle)?,
CustomMenuItem::new("view/switch-theme", "Switch Theme").accelerator("CmdOrCtrl+T"), &PredefinedMenuItem::separator(handle)?,
); &MenuItemBuilder::with_id("view/zoom-in", "Zoom In")
.accelerator("CmdOrCtrl+=")
view_menu = view_menu.add_native_item(MenuItem::Separator); .build(handle)?,
view_menu = view_menu &MenuItemBuilder::with_id("view/zoom-out", "Zoom Out")
.add_item(CustomMenuItem::new("view/zoom-in", "Zoom In").accelerator("CmdOrCtrl+=")); .accelerator("CmdOrCtrl+-")
view_menu = view_menu .build(handle)?,
.add_item(CustomMenuItem::new("view/zoom-out", "Zoom Out").accelerator("CmdOrCtrl+-")); &MenuItemBuilder::with_id("view/zoom-reset", "Reset Zoom")
view_menu = view_menu .accelerator("CmdOrCtrl+0")
.add_item(CustomMenuItem::new("view/zoom-reset", "Reset Zoom").accelerator("CmdOrCtrl+0")); .build(handle)?,
&PredefinedMenuItem::separator(handle)?,
view_menu = view_menu.add_native_item(MenuItem::Separator); ])?;
#[cfg(any(debug_assertions, feature = "devtools"))] #[cfg(any(debug_assertions, feature = "devtools"))]
{ view_menu.append_items(&[
view_menu = view_menu.add_item(CustomMenuItem::new("view/devtools", "Developer Tools")); &MenuItemBuilder::with_id("view/devtools", "Developer Tools").build(handle)?,
} &MenuItemBuilder::with_id("view/reload", "Reload View")
.accelerator("CmdOrCtrl+R")
.build(handle)?,
])?;
view_menu = view_menu let project_menu = &SubmenuBuilder::new(handle, "Project")
.add_item(CustomMenuItem::new("view/reload", "Reload View").accelerator("CmdOrCtrl+R")); .item(
&MenuItemBuilder::with_id("project/history", "Project History")
menu = menu.add_submenu(Submenu::new("View", view_menu)); .accelerator("CmdOrCtrl+Shift+H")
.build(handle)?,
let mut project_menu = Menu::new(); )
project_menu = project_menu.add_item( .text("project/open-in-vscode", "Open in VS Code")
CustomMenuItem::new("project/history", "Project History").accelerator("CmdOrCtrl+Shift+H"), .separator()
); .text("project/settings", "Project Settings")
project_menu = project_menu.add_item(CustomMenuItem::new( .build()?;
"project/open-in-vscode",
"Open in VS Code",
));
project_menu = project_menu.add_native_item(MenuItem::Separator);
project_menu =
project_menu.add_item(CustomMenuItem::new("project/settings", "Project Settings"));
menu = menu.add_submenu(Submenu::new("Project", project_menu));
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ let window_menu = &SubmenuBuilder::new(handle, "Window")
let mut window_menu = Menu::new(); .items(&[
window_menu = window_menu.add_native_item(MenuItem::Minimize); &PredefinedMenuItem::minimize(handle, None)?,
&PredefinedMenuItem::maximize(handle, None)?,
&PredefinedMenuItem::separator(handle)?,
&PredefinedMenuItem::close_window(handle, None)?,
])
.build()?;
window_menu = window_menu.add_native_item(MenuItem::Zoom); let help_menu = &SubmenuBuilder::new(handle, "Help")
window_menu = window_menu.add_native_item(MenuItem::Separator); .text("help/documentation", "Documentation")
.text("help/github", "Source Code")
.text("help/release-notes", "Release Notes")
.separator()
.text("help/share-debug-info", "Share Debug Info…")
.text("help/report-issue", "Report an Issue…")
.separator()
.text("help/discord", "Discord")
.text("help/youtube", "YouTube")
.text("help/x", "X")
.separator()
.item(
&MenuItemBuilder::with_id(
"help/version",
format!("Version {}", handle.package_info().version),
)
.enabled(false)
.build(handle)?,
)
.build()?;
window_menu = window_menu.add_native_item(MenuItem::CloseWindow); Menu::with_items(
menu = menu.add_submenu(Submenu::new("Window", window_menu)); handle,
} &[
#[cfg(target_os = "macos")]
let mut help_menu = Menu::new(); mac_menu,
help_menu = help_menu.add_item(CustomMenuItem::new("help/documentation", "Documentation")); file_menu,
help_menu = help_menu.add_item(CustomMenuItem::new("help/github", "Source Code")); #[cfg(not(target_os = "linux"))]
help_menu = help_menu.add_item(CustomMenuItem::new("help/release-notes", "Release Notes")); edit_menu,
help_menu = help_menu.add_native_item(MenuItem::Separator); view_menu,
help_menu = help_menu.add_item(CustomMenuItem::new( project_menu,
"help/share-debug-info", #[cfg(target_os = "macos")]
"Share Debug Info…", window_menu,
)); help_menu,
help_menu = help_menu.add_item(CustomMenuItem::new("help/report-issue", "Report an Issue…")); ],
help_menu = help_menu.add_native_item(MenuItem::Separator); )
help_menu = help_menu.add_item(CustomMenuItem::new("help/discord", "Discord"));
help_menu = help_menu.add_item(CustomMenuItem::new("help/youtube", "YouTube"));
help_menu = help_menu.add_item(CustomMenuItem::new("help/x", "X"));
help_menu = help_menu.add_native_item(MenuItem::Separator);
help_menu = help_menu.add_item(disabled_menu_item(
"help/version",
&format!("Version {}", _package_info.version),
));
menu = menu.add_submenu(Submenu::new("Help", help_menu));
menu
} }
fn disabled_menu_item(id: &str, title: &str) -> CustomMenuItem { pub fn handle_event(webview: &WebviewWindow, event: &MenuEvent) {
let mut item = CustomMenuItem::new(id, title); if event.id() == "file/add-local-repo" {
item.enabled = false; emit(webview, "menu://file/add-local-repo/clicked");
item
}
pub fn handle_event<R: Runtime>(event: &WindowMenuEvent<R>) {
if event.menu_item_id() == "file/add-local-repo" {
emit(event.window(), "menu://file/add-local-repo/clicked");
return; return;
} }
if event.menu_item_id() == "file/clone-repo" { if event.id() == "file/clone-repo" {
emit(event.window(), "menu://file/clone-repo/clicked"); emit(webview, "menu://file/clone-repo/clicked");
return; return;
} }
#[cfg(any(debug_assertions, feature = "devtools"))] #[cfg(any(debug_assertions, feature = "devtools"))]
{ {
if event.menu_item_id() == "view/devtools" { if event.id() == "view/devtools" {
event.window().open_devtools(); webview.open_devtools();
return; return;
} }
} }
if event.menu_item_id() == "view/switch-theme" { if event.id() == "view/switch-theme" {
emit(event.window(), "menu://view/switch-theme/clicked"); emit(webview, "menu://view/switch-theme/clicked");
return; return;
} }
if event.menu_item_id() == "view/reload" { if event.id() == "view/reload" {
emit(event.window(), "menu://view/reload/clicked"); emit(webview, "menu://view/reload/clicked");
return; return;
} }
if event.menu_item_id() == "view/zoom-in" { if event.id() == "view/zoom-in" {
emit(event.window(), "menu://view/zoom-in/clicked"); emit(webview, "menu://view/zoom-in/clicked");
return; return;
} }
if event.menu_item_id() == "view/zoom-out" { if event.id() == "view/zoom-out" {
emit(event.window(), "menu://view/zoom-out/clicked"); emit(webview, "menu://view/zoom-out/clicked");
return; return;
} }
if event.menu_item_id() == "view/zoom-reset" { if event.id() == "view/zoom-reset" {
emit(event.window(), "menu://view/zoom-reset/clicked"); emit(webview, "menu://view/zoom-reset/clicked");
return; return;
} }
if event.menu_item_id() == "help/share-debug-info" { if event.id() == "help/share-debug-info" {
emit(event.window(), "menu://help/share-debug-info/clicked"); emit(webview, "menu://help/share-debug-info/clicked");
return; return;
} }
if event.menu_item_id() == "project/history" { if event.id() == "project/history" {
emit(event.window(), "menu://project/history/clicked"); emit(webview, "menu://project/history/clicked");
return; return;
} }
if event.menu_item_id() == "project/open-in-vscode" { if event.id() == "project/open-in-vscode" {
emit(event.window(), "menu://project/open-in-vscode/clicked"); emit(webview, "menu://project/open-in-vscode/clicked");
return; return;
} }
if event.menu_item_id() == "project/settings" { if event.id() == "project/settings" {
emit(event.window(), "menu://project/settings/clicked"); emit(webview, "menu://project/settings/clicked");
return; return;
} }
if event.menu_item_id() == "global/settings" { if event.id() == "global/settings" {
emit(event.window(), "menu://global/settings/clicked"); emit(webview, "menu://global/settings/clicked");
return; return;
} }
if event.menu_item_id() == "global/update" { if event.id() == "global/update" {
emit(event.window(), "menu://global/update/clicked"); emit(webview, "menu://global/update/clicked");
return; return;
} }
'open_link: { 'open_link: {
let result = match event.menu_item_id() { let result = match event.id().0.as_str() {
"help/documentation" => open_url("https://docs.gitbutler.com"), "help/documentation" => open::that("https://docs.gitbutler.com"),
"help/github" => open_url("https://github.com/gitbutlerapp/gitbutler"), "help/github" => open::that("https://github.com/gitbutlerapp/gitbutler"),
"help/release-notes" => open_url("https://github.com/gitbutlerapp/gitbutler/releases"), "help/release-notes" => {
"help/report-issue" => open_url("https://github.com/gitbutlerapp/gitbutler/issues/new"), open::that("https://discord.com/channels/1060193121130000425/1183737922785116161")
"help/discord" => open_url("https://discord.com/invite/MmFkmaJ42D"), }
"help/youtube" => open_url("https://www.youtube.com/@gitbutlerapp"), "help/report-issue" => {
"help/x" => open_url("https://x.com/gitbutler"), open::that("https://github.com/gitbutlerapp/gitbutler/issues/new")
}
"help/discord" => open::that("https://discord.com/invite/MmFkmaJ42D"),
"help/youtube" => open::that("https://www.youtube.com/@gitbutlerapp"),
"help/x" => open::that("https://x.com/gitbutler"),
_ => break 'open_link, _ => break 'open_link,
}; };
if let Err(err) = result { if let Err(err) = result {
tracing::error!(error = ?err, "failed to open url for {}", event.menu_item_id()); tracing::error!(error = ?err, "failed to open url for {}", event.id().0);
} }
return; return;
} }
tracing::error!("unhandled 'help' menu event: {}", event.menu_item_id()); tracing::error!("unhandled 'help' menu event: {}", event.id().0);
} }
fn emit<R: Runtime>(window: &tauri::Window<R>, event: &str) { fn emit<R: Runtime>(window: &tauri::WebviewWindow<R>, event: &str) {
if let Err(err) = window.emit(event, json!({})) { if let Err(err) = window.emit(event, json!({})) {
tracing::error!(error = ?err, "failed to emit event"); tracing::error!(error = ?err, "failed to emit event");
} }

View File

@ -12,7 +12,7 @@ pub(super) mod state {
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use gitbutler_project::ProjectId; use gitbutler_project::ProjectId;
use gitbutler_watcher::Change; use gitbutler_watcher::Change;
use tauri::Manager; use tauri::Emitter;
/// A change we want to inform the frontend about. /// A change we want to inform the frontend about.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -64,7 +64,7 @@ pub(super) mod state {
impl ChangeForFrontend { impl ChangeForFrontend {
pub(super) fn send(&self, app_handle: &tauri::AppHandle) -> Result<()> { pub(super) fn send(&self, app_handle: &tauri::AppHandle) -> Result<()> {
app_handle app_handle
.emit_all(&self.name, Some(&self.payload)) .emit(&self.name, Some(&self.payload))
.context("emit event")?; .context("emit event")?;
tracing::trace!(event_name = self.name); tracing::trace!(event_name = self.name);
Ok(()) Ok(())
@ -204,16 +204,16 @@ pub fn create(
handle: &tauri::AppHandle, handle: &tauri::AppHandle,
label: &state::WindowLabelRef, label: &state::WindowLabelRef,
window_relative_url: String, window_relative_url: String,
) -> tauri::Result<tauri::Window> { ) -> tauri::Result<tauri::WebviewWindow> {
tracing::info!("creating window '{label}' created at '{window_relative_url}'"); tracing::info!("creating window '{label}' created at '{window_relative_url}'");
let window = tauri::WindowBuilder::new( let window = tauri::WebviewWindowBuilder::new(
handle, handle,
label, label,
tauri::WindowUrl::App(window_relative_url.into()), tauri::WebviewUrl::App(window_relative_url.into()),
) )
.resizable(true) .resizable(true)
.title(handle.package_info().name.clone()) .title(handle.package_info().name.clone())
.disable_file_drop_handler() .disable_drag_drop_handler()
.min_inner_size(800.0, 600.0) .min_inner_size(800.0, 600.0)
.inner_size(1160.0, 720.0) .inner_size(1160.0, 720.0)
.build()?; .build()?;
@ -225,19 +225,19 @@ pub fn create(
handle: &tauri::AppHandle, handle: &tauri::AppHandle,
label: &state::WindowLabelRef, label: &state::WindowLabelRef,
window_relative_url: String, window_relative_url: String,
) -> tauri::Result<tauri::Window> { ) -> tauri::Result<tauri::WebviewWindow> {
tracing::info!("creating window '{label}' created at '{window_relative_url}'"); tracing::info!("creating window '{label}' created at '{window_relative_url}'");
let window = tauri::WindowBuilder::new( let window = tauri::WebviewWindowBuilder::new(
handle, handle,
label, label,
tauri::WindowUrl::App(window_relative_url.into()), tauri::WebviewUrl::App(window_relative_url.into()),
) )
.resizable(true) .resizable(true)
.title(handle.package_info().name.clone()) .title(handle.package_info().name.clone())
.min_inner_size(800.0, 600.0) .min_inner_size(800.0, 600.0)
.inner_size(1160.0, 720.0) .inner_size(1160.0, 720.0)
.hidden_title(true) .hidden_title(true)
.disable_file_drop_handler() .disable_drag_drop_handler()
.title_bar_style(tauri::TitleBarStyle::Overlay) .title_bar_style(tauri::TitleBarStyle::Overlay)
.build()?; .build()?;
Ok(window) Ok(window)

View File

@ -1,74 +1,53 @@
{ {
"productName": "GitButler Dev",
"identifier": "com.gitbutler.app.dev",
"build": { "build": {
"beforeDevCommand": "pnpm dev:internal-tauri", "beforeDevCommand": "pnpm dev:internal-tauri",
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode development", "beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode development",
"devPath": "http://localhost:1420", "frontendDist": "../../apps/desktop/build",
"distDir": "../../apps/desktop/build", "devUrl": "http://localhost:1420"
"withGlobalTauri": false
}, },
"package": { "bundle": {
"productName": "GitButler Dev" "active": false,
}, "category": "DeveloperTool",
"tauri": { "copyright": "Copyright © 2023-2024 GitButler. All rights reserved.",
"allowlist": { "createUpdaterArtifacts": "v1Compatible",
"fs": { "targets": ["app", "dmg", "appimage", "deb", "rpm", "msi"],
"readFile": true, "icon": [
"scope": ["$APPCACHE/archives/*", "$RESOURCE/_up_/scripts/*"] "icons/dev/32x32.png",
"icons/dev/128x128.png",
"icons/dev/128x128@2x.png",
"icons/dev/icon.icns",
"icons/dev/icon.ico"
],
"windows": {
"certificateThumbprint": null
},
"linux": {
"rpm": {
"depends": ["webkit2gtk4.1-devel"]
}, },
"dialog": { "deb": {
"open": true "depends": ["libwebkit2gtk-4.1-dev", "libgtk-3-dev"]
},
"os": {
"all": true
},
"protocol": {
"asset": true,
"assetScope": ["$APPCACHE/images/*"]
},
"process": {
"relaunch": true
},
"window": {
"startDragging": true,
"maximize": true,
"unmaximize": true
},
"path": {
"all": true
},
"http": {
"all": true,
"request": true,
"scope": [
"https://api.anthropic.com/v1/messages",
"http://127.0.0.1:11434/api/chat",
"http://127.0.0.1:11434/api/generate",
"http://127.0.0.1:11434/api/embeddings"
]
},
"shell": {
"open": true
} }
}, }
"bundle": { },
"active": true, "plugins": {
"identifier": "com.gitbutler.app.dev", "updater": {
"category": "DeveloperTool", "endpoints": [
"copyright": "Copyright © 2023-2024 GitButler. All rights reserved.", "https://app.gitbutler.com/releases/nightly/{{target}}-{{arch}}/{{current_version}}"
"icon": [
"icons/dev/32x32.png",
"icons/dev/128x128.png",
"icons/dev/128x128@2x.png",
"icons/dev/icon.icns",
"icons/dev/icon.ico"
], ],
"targets": ["app", "dmg", "appimage", "deb", "rpm", "updater", "msi"] "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDYwNTc2RDhBM0U0MjM4RUIKUldUck9FSStpbTFYWUE5UkJ3eXhuekZOL2V2RnpKaFUxbGJRNzBMVmF5V0gzV1JvN3hRblJMRDIK"
}, }
},
"app": {
"withGlobalTauri": false,
"enableGTKAppId": true,
"security": { "security": {
"csp": { "csp": {
"default-src": "'self'", "default-src": "'self'",
"img-src": "'self' asset: https://asset.localhost data: tauri://localhost https://avatars.githubusercontent.com https://*.gitbutler.com https://gitbutler-public.s3.amazonaws.com https://*.gravatar.com https://lh3.googleusercontent.com", "img-src": "'self' asset: https://asset.localhost data: tauri://localhost https://avatars.githubusercontent.com https://*.gitbutler.com https://gitbutler-public.s3.amazonaws.com https://*.gravatar.com https://io.wp.com https://i0.wp.com https://i1.wp.com https://i2.wp.com https://i3.wp.com https://github.com https://*.googleusercontent.com",
"connect-src": "'self' https://eu.posthog.com https://eu.i.posthog.com https://app.gitbutler.com https://o4504644069687296.ingest.sentry.io ws://localhost:7703 https://github.com https://api.github.com", "connect-src": "'self' ipc: https://eu.posthog.com https://eu.i.posthog.com https://app.gitbutler.com https://o4504644069687296.ingest.sentry.io ws://localhost:7703 https://github.com https://api.github.com https://api.openai.com",
"script-src": "'self' https://eu.posthog.com https://eu.i.posthog.com", "script-src": "'self' https://eu.posthog.com https://eu.i.posthog.com",
"style-src": "'self' 'unsafe-inline'" "style-src": "'self' 'unsafe-inline'"
} }

View File

@ -1,43 +1,21 @@
{ {
"productName": "GitButler Nightly",
"identifier": "com.gitbutler.app.nightly",
"build": { "build": {
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode nightly && cargo build --release -p gitbutler-git && bash ./gitbutler-tauri/inject-git-binaries.sh" "beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode nightly && cargo build --release -p gitbutler-git && bash ./crates/gitbutler-tauri/inject-git-binaries.sh"
}, },
"package": { "bundle": {
"productName": "GitButler Nightly" "active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": ["gitbutler-git-setsid", "gitbutler-git-askpass"]
}, },
"tauri": { "plugins": {
"bundle": {
"identifier": "com.gitbutler.app.nightly",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": ["gitbutler-git-setsid", "gitbutler-git-askpass"],
"windows": {
"certificateThumbprint": null,
"wix": {
"template": "templates/installer.wxs"
}
},
"rpm": {
"depends": ["webkit2gtk4.0-devel"]
},
"deb": {
"depends": ["libwebkit2gtk-4.0-dev", "libgtk-3-dev"]
}
},
"security": {
"csp": {
"default-src": "'self'",
"img-src": "'self' asset: https://asset.localhost data: tauri://localhost https://avatars.githubusercontent.com https://*.gitbutler.com https://gitbutler-public.s3.amazonaws.com https://*.gravatar.com https://io.wp.com https://i0.wp.com https://i1.wp.com https://i2.wp.com https://i3.wp.com https://github.com https://*.googleusercontent.com",
"connect-src": "'self' https://eu.posthog.com https://eu.i.posthog.com https://app.gitbutler.com https://o4504644069687296.ingest.sentry.io ws://localhost:7703 https://github.com https://api.github.com https://api.openai.com https://api.anthropic.com/v1/messages",
"script-src": "'self' https://eu.posthog.com https://eu.i.posthog.com",
"style-src": "'self' 'unsafe-inline'"
}
},
"updater": { "updater": {
"active": true, "active": true,
"dialog": false, "dialog": false,

View File

@ -1,43 +1,21 @@
{ {
"productName": "GitButler",
"identifier": "com.gitbutler.app",
"build": { "build": {
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode production && cargo build --release -p gitbutler-git && bash ./gitbutler-tauri/inject-git-binaries.sh" "beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode production && cargo build --release -p gitbutler-git && bash ./crates/gitbutler-tauri/inject-git-binaries.sh"
}, },
"package": { "bundle": {
"productName": "GitButler" "active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": ["gitbutler-git-setsid", "gitbutler-git-askpass"]
}, },
"tauri": { "plugins": {
"bundle": {
"identifier": "com.gitbutler.app",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": ["gitbutler-git-setsid", "gitbutler-git-askpass"],
"windows": {
"certificateThumbprint": null,
"wix": {
"template": "templates/installer.wxs"
}
},
"rpm": {
"depends": ["webkit2gtk4.0-devel"]
},
"deb": {
"depends": ["libwebkit2gtk-4.0-dev", "libgtk-3-dev"]
}
},
"security": {
"csp": {
"default-src": "'self'",
"img-src": "'self' asset: https://asset.localhost data: tauri://localhost https://avatars.githubusercontent.com https://*.gitbutler.com https://gitbutler-public.s3.amazonaws.com https://*.gravatar.com https://io.wp.com https://i0.wp.com https://i1.wp.com https://i2.wp.com https://i3.wp.com https://github.com https://*.googleusercontent.com",
"connect-src": "'self' https://eu.posthog.com https://eu.i.posthog.com https://app.gitbutler.com https://o4504644069687296.ingest.sentry.io ws://localhost:7703 https://github.com https://api.github.com https://api.openai.com https://api.anthropic.com/v1/messages",
"script-src": "'self' https://eu.posthog.com https://eu.i.posthog.com",
"style-src": "'self' 'unsafe-inline'"
}
},
"updater": { "updater": {
"active": true, "active": true,
"dialog": false, "dialog": false,

View File

@ -1,51 +1,7 @@
{ {
"productName": "GitButler Test",
"identifier": "com.gitbutler.app.test",
"build": { "build": {
"beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode development", "beforeBuildCommand": "[ $CI = true ] || pnpm build:desktop -- --mode development && cargo build -p gitbutler-git"
"distDir": "../../apps/desktop/build"
},
"package": {
"productName": "GitButler Dev"
},
"tauri": {
"allowlist": {
"fs": {
"readFile": true,
"scope": ["$APPCACHE/archives/*", "$RESOURCE/_up_/scripts/*"]
},
"dialog": {
"open": true
},
"os": {
"all": true
},
"protocol": {
"asset": true,
"assetScope": ["$APPCACHE/images/*"]
},
"process": {
"relaunch": true
},
"window": {
"startDragging": true,
"maximize": true,
"unmaximize": true
},
"path": {
"all": true
},
"http": {
"all": true,
"request": true,
"scope": [
"https://api.anthropic.com/v1/messages",
"http://127.0.0.1:11434/api/chat",
"http://127.0.0.1:11434/api/generate",
"http://127.0.0.1:11434/api/embeddings"
]
}
},
"bundle": {
"active": false
}
} }
} }

View File

@ -1,350 +0,0 @@
<?if $(sys.BUILDARCH)="x86"?>
<?define Win64 = "no" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?elseif $(sys.BUILDARCH)="x64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?elseif $(sys.BUILDARCH)="arm64"?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else?>
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
<?endif?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product
Id="*"
Name="{{product_name}}"
UpgradeCode="{{upgrade_code}}"
Language="!(loc.TauriLanguage)"
Manufacturer="{{manufacturer}}"
Version="{{version}}">
<Package Id="*"
Keywords="Installer"
InstallerVersion="450"
Languages="0"
Compressed="yes"
InstallScope="perMachine"
SummaryCodepage="!(loc.TauriCodepage)"/>
<!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->
<!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
<Property Id="REINSTALLMODE" Value="amus" />
<!-- Auto launch app after installation, useful for passive mode which usually used in updates -->
<Property Id="AUTOLAUNCHAPP" Secure="yes" />
<!-- Property to forward cli args to the launched app to not lose those of the pre-update instance -->
<Property Id="LAUNCHAPPARGS" Secure="yes" />
{{#if allow_downgrades}}
<MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
{{else}}
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="!(loc.DowngradeErrorMessage)" AllowSameVersionUpgrades="yes" />
{{/if}}
<InstallExecuteSequence>
<RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>
</InstallExecuteSequence>
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
{{#if banner_path}}
<WixVariable Id="WixUIBannerBmp" Value="{{banner_path}}" />
{{/if}}
{{#if dialog_image_path}}
<WixVariable Id="WixUIDialogBmp" Value="{{dialog_image_path}}" />
{{/if}}
{{#if license}}
<WixVariable Id="WixUILicenseRtf" Value="{{license}}" />
{{/if}}
<Icon Id="ProductIcon" SourceFile="{{icon_path}}"/>
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" /> <!-- Remove repair -->
<SetProperty Id="ARPNOMODIFY" Value="1" After="InstallValidate" Sequence="execute"/>
{{#if homepage}}
<Property Id="ARPURLINFOABOUT" Value="{{homepage}}"/>
<Property Id="ARPHELPLINK" Value="{{homepage}}"/>
<Property Id="ARPURLUPDATEINFO" Value="{{homepage}}"/>
{{/if}}
<!-- initialize with previous InstallDir -->
<Property Id="INSTALLDIR">
<RegistrySearch Id="PrevInstallDirReg" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw"/>
</Property>
<!-- launch app checkbox -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
<CustomAction Id="LaunchApplication" Impersonate="yes" FileKey="Path" ExeCommand="[LAUNCHAPPARGS]" Return="asyncNoWait" />
<UI>
<!-- launch app checkbox -->
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
{{#unless license}}
<!-- Skip license dialog -->
<Publish Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="InstallDirDlg"
Order="2">1</Publish>
<Publish Dialog="InstallDirDlg"
Control="Back"
Event="NewDialog"
Value="WelcomeDlg"
Order="2">1</Publish>
{{/unless}}
</UI>
<UIRef Id="WixUI_InstallDir" />
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="DesktopFolder" Name="Desktop">
<Component Id="ApplicationShortcutDesktop" Guid="*">
<Shortcut Id="ApplicationDesktopShortcut" Name="{{product_name}}" Description="Runs {{product_name}}" Target="[!Path]" WorkingDirectory="INSTALLDIR" />
<RemoveFolder Id="DesktopFolder" On="uninstall" />
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Desktop Shortcut" Type="integer" Value="1" KeyPath="yes" />
</Component>
</Directory>
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
<Directory Id="INSTALLDIR" Name="{{product_name}}"/>
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="{{product_name}}"/>
</Directory>
</Directory>
<DirectoryRef Id="INSTALLDIR">
<Component Id="RegistryEntries" Guid="*">
<RegistryKey Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}">
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLDIR]" KeyPath="yes" />
</RegistryKey>
<!-- Change the Root to HKCU for perUser installations -->
{{#each deep_link_protocols as |protocol| ~}}
<RegistryKey Root="HKLM" Key="Software\Classes\\{{protocol}}">
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
<RegistryValue Type="string" Value="URL:{{bundle_id}} protocol"/>
<RegistryKey Key="DefaultIcon">
<RegistryValue Type="string" Value="&quot;[!Path]&quot;,0" />
</RegistryKey>
<RegistryKey Key="shell\open\command">
<RegistryValue Type="string" Value="&quot;[!Path]&quot; &quot;%1&quot;" />
</RegistryKey>
</RegistryKey>
{{/each~}}
</Component>
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
{{#each file_associations as |association| ~}}
{{#each association.ext as |ext| ~}}
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">
<Extension Id="{{ext}}" Advertise="yes">
<Verb Id="open" Command="Open with {{../../product_name}}" Argument="&quot;%1&quot;" />
</Extension>
</ProgId>
{{/each~}}
{{/each~}}
</Component>
{{#each binaries as |bin| ~}}
<Component Id="{{ bin.id }}" Guid="{{bin.guid}}" Win64="$(var.Win64)">
<File Id="Bin_{{ bin.id }}" Source="{{bin.path}}" KeyPath="yes"/>
</Component>
{{/each~}}
{{#if enable_elevated_update_task}}
<Component Id="UpdateTask" Guid="C492327D-9720-4CD5-8DB8-F09082AF44BE" Win64="$(var.Win64)">
<File Id="UpdateTask" Source="update.xml" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskInstaller" Guid="011F25ED-9BE3-50A7-9E9B-3519ED2B9932" Win64="$(var.Win64)">
<File Id="UpdateTaskInstaller" Source="install-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="UpdateTaskUninstaller" Guid="D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1" Win64="$(var.Win64)">
<File Id="UpdateTaskUninstaller" Source="uninstall-task.ps1" KeyPath="yes" Checksum="yes"/>
</Component>
{{/if}}
{{resources}}
<Component Id="CMP_UninstallShortcut" Guid="*">
<Shortcut Id="UninstallShortcut"
Name="Uninstall {{product_name}}"
Description="Uninstalls {{product_name}}"
Target="[System64Folder]msiexec.exe"
Arguments="/x [ProductCode]" />
<RemoveFolder Id="INSTALLDIR"
On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\\{{manufacturer}}\\{{product_name}}"
Name="Uninstaller Shortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ApplicationShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="{{product_name}}"
Description="Runs {{product_name}}"
Target="[!Path]"
Icon="ProductIcon"
WorkingDirectory="INSTALLDIR">
<ShortcutProperty Key="System.AppUserModel.ID" Value="{{bundle_id}}"/>
</Shortcut>
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Start Menu Shortcut" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</DirectoryRef>
{{#each merge_modules as |msm| ~}}
<DirectoryRef Id="TARGETDIR">
<Merge Id="{{ msm.name }}" SourceFile="{{ msm.path }}" DiskId="1" Language="!(loc.TauriLanguage)" />
</DirectoryRef>
<Feature Id="{{ msm.name }}" Title="{{ msm.name }}" AllowAdvertise="no" Display="hidden" Level="1">
<MergeRef Id="{{ msm.name }}"/>
</Feature>
{{/each~}}
<Feature
Id="MainProgram"
Title="Application"
Description="!(loc.InstallAppFeature)"
Level="1"
ConfigurableDirectory="INSTALLDIR"
AllowAdvertise="no"
Display="expand"
Absent="disallow">
<ComponentRef Id="RegistryEntries"/>
{{#each resource_file_ids as |resource_file_id| ~}}
<ComponentRef Id="{{ resource_file_id }}"/>
{{/each~}}
{{#if enable_elevated_update_task}}
<ComponentRef Id="UpdateTask" />
<ComponentRef Id="UpdateTaskInstaller" />
<ComponentRef Id="UpdateTaskUninstaller" />
{{/if}}
<Feature Id="ShortcutsFeature"
Title="Shortcuts"
Level="1">
<ComponentRef Id="Path"/>
<ComponentRef Id="CMP_UninstallShortcut" />
<ComponentRef Id="ApplicationShortcut" />
<ComponentRef Id="ApplicationShortcutDesktop" />
</Feature>
<Feature
Id="Environment"
Title="PATH Environment Variable"
Description="!(loc.PathEnvVarFeature)"
Level="1"
Absent="allow">
<ComponentRef Id="Path"/>
{{#each binaries as |bin| ~}}
<ComponentRef Id="{{ bin.id }}"/>
{{/each~}}
</Feature>
</Feature>
<Feature Id="External" AllowAdvertise="no" Absent="disallow">
{{#each component_group_refs as |id| ~}}
<ComponentGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each component_refs as |id| ~}}
<ComponentRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_group_refs as |id| ~}}
<FeatureGroupRef Id="{{ id }}"/>
{{/each~}}
{{#each feature_refs as |id| ~}}
<FeatureRef Id="{{ id }}"/>
{{/each~}}
{{#each merge_refs as |id| ~}}
<MergeRef Id="{{ id }}"/>
{{/each~}}
</Feature>
{{#if install_webview}}
<!-- WebView2 -->
<Property Id="WVRTINSTALLED">
<RegistrySearch Id="WVRTInstalledSystem" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" Win64="no" />
<RegistrySearch Id="WVRTInstalledUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
</Property>
{{#if download_bootstrapper}}
<CustomAction Id='DownloadAndInvokeBootstrapper' Directory="INSTALLDIR" Execute="deferred" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\{] [\[]Net.ServicePointManager[\]]::SecurityProtocol = [\[]Net.SecurityProtocolType[\]]::Tls12 [\}] catch [\{][\}]; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ({{webview_installer_args}} &apos;/install&apos;) -Wait' Return='check'/>
<InstallExecuteSequence>
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded webview bootstrapper mode -->
{{#if webview2_bootstrapper_path}}
<Binary Id="MicrosoftEdgeWebview2Setup.exe" SourceFile="{{webview2_bootstrapper_path}}"/>
<CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded offline installer -->
{{#if webview2_installer_path}}
<Binary Id="MicrosoftEdgeWebView2RuntimeInstaller.exe" SourceFile="{{webview2_installer_path}}"/>
<CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeStandalone' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
{{/if}}
{{#if enable_elevated_update_task}}
<!-- Install an elevated update task within Windows Task Scheduler -->
<CustomAction
Id="CreateUpdateTask"
Return="check"
Directory="INSTALLDIR"
Execute="commit"
Impersonate="yes"
ExeCommand="powershell.exe -WindowStyle hidden .\install-task.ps1" />
<InstallExecuteSequence>
<Custom Action='CreateUpdateTask' Before='InstallFinalize'>
NOT(REMOVE)
</Custom>
</InstallExecuteSequence>
<!-- Remove elevated update task during uninstall -->
<CustomAction
Id="DeleteUpdateTask"
Return="check"
Directory="INSTALLDIR"
ExeCommand="powershell.exe -WindowStyle hidden .\uninstall-task.ps1" />
<InstallExecuteSequence>
<Custom Action="DeleteUpdateTask" Before='InstallFinalize'>
(REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE
</Custom>
</InstallExecuteSequence>
{{/if}}
<InstallExecuteSequence>
<Custom Action="LaunchApplication" After="InstallFinalize">AUTOLAUNCHAPP AND NOT Installed</Custom>
</InstallExecuteSequence>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
</Product>
</Wix>

View File

@ -32,8 +32,8 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.5.0", "@eslint/js": "^9.5.0",
"@tauri-apps/cli": "^1.6.2",
"@types/eslint": "9.6.0", "@types/eslint": "9.6.0",
"@tauri-apps/cli": "^2.0.1",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"@types/node": "^22.3.0", "@types/node": "^22.3.0",
"@typescript-eslint/parser": "^7.13.1", "@typescript-eslint/parser": "^7.13.1",

View File

@ -34,8 +34,8 @@ importers:
specifier: ^9.5.0 specifier: ^9.5.0
version: 9.5.0 version: 9.5.0
'@tauri-apps/cli': '@tauri-apps/cli':
specifier: ^1.6.2 specifier: ^2.0.1
version: 1.6.2 version: 2.0.1
'@types/eslint': '@types/eslint':
specifier: 9.6.0 specifier: 9.6.0
version: 9.6.0 version: 9.6.0
@ -90,13 +90,13 @@ importers:
apps/desktop: apps/desktop:
dependencies: dependencies:
'@anthropic-ai/sdk':
specifier: ^0.27.3
version: 0.27.3
openai: openai:
specifier: ^4.47.3 specifier: ^4.47.3
version: 4.47.3 version: 4.47.3
devDependencies: devDependencies:
'@anthropic-ai/sdk':
specifier: ^0.27.3
version: 0.27.3
'@codemirror/lang-cpp': '@codemirror/lang-cpp':
specifier: ^6.0.2 specifier: ^6.0.2
version: 6.0.2 version: 6.0.2
@ -173,8 +173,35 @@ importers:
specifier: catalog:svelte specifier: catalog:svelte
version: 4.0.0-next.6(svelte@5.0.0-next.243)(vite@5.2.13(@types/node@22.3.0)) version: 4.0.0-next.6(svelte@5.0.0-next.243)(vite@5.2.13(@types/node@22.3.0))
'@tauri-apps/api': '@tauri-apps/api':
specifier: ^1.6.0 specifier: ^2.0.3
version: 1.6.0 version: 2.0.3
'@tauri-apps/plugin-dialog':
specifier: ^2.0.1
version: 2.0.1
'@tauri-apps/plugin-fs':
specifier: ^2.0.1
version: 2.0.1
'@tauri-apps/plugin-http':
specifier: ^2.0.1
version: 2.0.1
'@tauri-apps/plugin-log':
specifier: ^2.0.0
version: 2.0.0
'@tauri-apps/plugin-os':
specifier: ^2.0.0
version: 2.0.0
'@tauri-apps/plugin-process':
specifier: ^2.0.0
version: 2.0.0
'@tauri-apps/plugin-shell':
specifier: ^2.0.1
version: 2.0.1
'@tauri-apps/plugin-store':
specifier: ^2.1.0
version: 2.1.0
'@tauri-apps/plugin-updater':
specifier: ^2.0.0
version: 2.0.0
'@testing-library/jest-dom': '@testing-library/jest-dom':
specifier: ^6.4.8 specifier: ^6.4.8
version: 6.4.8 version: 6.4.8
@ -277,12 +304,6 @@ importers:
svelte-french-toast: svelte-french-toast:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0(svelte@5.0.0-next.243) version: 1.2.0(svelte@5.0.0-next.243)
tauri-plugin-log-api:
specifier: https://github.com/tauri-apps/tauri-plugin-log#v1
version: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/cc86b2d9878d6ec02c9f25bd48292621a4bc2a6f
tauri-plugin-store-api:
specifier: https://github.com/tauri-apps/tauri-plugin-store#v1
version: https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/0b7079b8c55bf25f6d9d9e8c57812d03b48a9788
tinykeys: tinykeys:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0 version: 2.1.0
@ -1788,75 +1809,101 @@ packages:
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
'@tauri-apps/api@1.6.0': '@tauri-apps/api@2.0.3':
resolution: {integrity: sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==} resolution: {integrity: sha512-840qk6n8rbXBXMA5/aAgTYsg5JAubKO0nXw5wf7IzGnUuYKGbB4oFBIZtXOIWy+E0kNTDI3qhq5iqsoMJfwp8g==}
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
'@tauri-apps/cli-darwin-arm64@1.6.2': '@tauri-apps/cli-darwin-arm64@2.0.1':
resolution: {integrity: sha512-6mdRyf9DaLqlZvj8kZB09U3rwY+dOHSGzTZ7+GDg665GJb17f4cb30e8dExj6/aghcsOie9EGpgiURcDUvLNSQ==} resolution: {integrity: sha512-oWjCZoFbm57V0eLEkIbc6aUmB4iW65QF7J8JVh5sNzH4xHGP9rzlQarbkg7LOn89z7mFSZpaLJAWlaaZwoV2Ug==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@tauri-apps/cli-darwin-x64@1.6.2': '@tauri-apps/cli-darwin-x64@2.0.1':
resolution: {integrity: sha512-PLxZY5dn38H3R9VRmBN/l0ZDB5JFanCwlK4rmpzDQPPg3tQmbu5vjSCP6TVj5U6aLKsj79kFyULblPr5Dn9+vw==} resolution: {integrity: sha512-bARd5yAnDGpG/FPhSh87+tzQ6D0TPyP2mZ5bg6cioeoXDmry68nT/FBzp87ySR1/KHvuhEQYWM/4RPrDjvI1Yg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@1.6.2': '@tauri-apps/cli-linux-arm-gnueabihf@2.0.1':
resolution: {integrity: sha512-xnpj4BLeeGOh5I/ewCQlYJZwHH0CBNBN+4q8BNWNQ9MKkjN9ST366RmHRzl2ANNgWwijOPxyce7GiUmvuH8Atw==} resolution: {integrity: sha512-OK3/RpxujoZAUbV7GHe4IPAUsIO6IuWEHT++jHXP+YW5Y7QezGGjQRc43IlWaQYej/yE8wfcrwrbqisc5wtiCw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@1.6.2': '@tauri-apps/cli-linux-arm64-gnu@2.0.1':
resolution: {integrity: sha512-uaiRE0vE2P+tdsCngfKt+7yKr3VZXIq/t3w01DzSdnBgHSp0zmRsRR4AhZt7ibvoEgA8GzBP+eSHJdFNZsTU9w==} resolution: {integrity: sha512-MGSQJduiMEApspMK97mFt4kr6ig0OtxO5SUFpPDfYPw/XmY9utaRa9CEG6LcH8e0GN9xxYMhCv+FeU48spYPhA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@tauri-apps/cli-linux-arm64-musl@1.6.2': '@tauri-apps/cli-linux-arm64-musl@2.0.1':
resolution: {integrity: sha512-o9JunVrMrhqTBLrdvEbS64W0bo1dPm0lxX51Mx+6x9SmbDjlEWGgaAHC3iKLK9khd5Yu1uO1e+8TJltAcScvmw==} resolution: {integrity: sha512-R6+vgxaPpxgGi4suMkQgGuhjMbZzMJfVyWfv2DOE/xxOzSK1BAOc54/HOjfOLxlnkA6uD6V69MwCwXgxW00A2g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@tauri-apps/cli-linux-x64-gnu@1.6.2': '@tauri-apps/cli-linux-x64-gnu@2.0.1':
resolution: {integrity: sha512-jL9f+o61DdQmNYKIt2Q3BA8YJ+hyC5+GdNxqDf7j5SoQ85j//YfUWbmp9ZgsPHVBxgSGZVvgGMNvf64Ykp0buQ==} resolution: {integrity: sha512-xrasYQnUZVhKJhBxHAeu4KxZbofaQlsG9KfZ9p1Bx+hmjs5BuujzwMnXsVD2a4l6GPW6gwblf2a6d600rySmWQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@tauri-apps/cli-linux-x64-musl@1.6.2': '@tauri-apps/cli-linux-x64-musl@2.0.1':
resolution: {integrity: sha512-xsa4Pu9YMHKAX0J8pIoXfN/uhvAAAoECZDixDhWw8zi57VZ4QX28ycqolS+NscdD9NAGSgHk45MpBZWdvRtvjQ==} resolution: {integrity: sha512-SPk+EzRTlbvk46p5aURc7O4GihzxbqG80m74vstm0rolnmQ0FX3qqIh3as3cQpDiZWLod4j6EEmX0mTU3QpvXA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@tauri-apps/cli-win32-arm64-msvc@1.6.2': '@tauri-apps/cli-win32-arm64-msvc@2.0.1':
resolution: {integrity: sha512-eJtUOx2UFhJpCCkm5M5+4Co9JbjvgIHTdyS/hTSZfOEdT58CNEGVJXMA39FsSZXYoxYPE+9K7Km6haMozSmlxw==} resolution: {integrity: sha512-LAELK01eOMyEt+JZLmx4EUOdRuPYr1a+mHjlxAxCnCaS3dpeg/c5/NMZfbRAJbAH4id+STRHIfPXTdCT2zUNAw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@1.6.2': '@tauri-apps/cli-win32-ia32-msvc@2.0.1':
resolution: {integrity: sha512-9Jwx3PrhNw3VKOgPISRRXPkvoEAZP+7rFRHXIo49dvlHy2E/o9qpWi1IntE33HWeazP6KhvsCjvXB2Ai4eGooA==} resolution: {integrity: sha512-eMUgOS4mAusk5njU2TBxBjCUO1P4cV4uzY5CHihysoXSL2TVQdWrXT42VGeoahJh+yeQWkYFka2s4Bu0iWDMXg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@tauri-apps/cli-win32-x64-msvc@1.6.2': '@tauri-apps/cli-win32-x64-msvc@2.0.1':
resolution: {integrity: sha512-5Z+ZjRFJE8MXghJe1UXvGephY5ZcgVhiTI9yuMi9xgX3CEaAXASatyXllzsvGJ9EDaWMEpa0PHjAzi7LBAWROw==} resolution: {integrity: sha512-U9esAOcFIv80/slzlpwjkG31Wx1OqbfDgC5KjGT1Dd9iUOSuJZCwbiY7m3rYG2I6RWLfd9zhNu86CVohsKjBfA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@tauri-apps/cli@1.6.2': '@tauri-apps/cli@2.0.1':
resolution: {integrity: sha512-zpfZdxhm20s7d/Uejpg/T3a9sqLVe3Ih2ztINfy8v6iLw9Ohowkb9g+agZffYKlEWfOSpmCy69NFyBLj7OZL0A==} resolution: {integrity: sha512-fCheW0iWYWUtFV3ui3HlMhk3ZJpAQ5KJr7B7UmfhDzBSy1h5JBdrCtvDwy+3AcPN+Fg5Ey3JciF8zEP8eBx+vQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
hasBin: true hasBin: true
'@tauri-apps/plugin-dialog@2.0.1':
resolution: {integrity: sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==}
'@tauri-apps/plugin-fs@2.0.1':
resolution: {integrity: sha512-PkeZG2WAob9Xpmr66aPvj+McDVgFjV2a7YBzYVZjiCvbGeMs6Yk09tlXhCe3EyZdT/pwWMSi8lXUace+hlsjsw==}
'@tauri-apps/plugin-http@2.0.1':
resolution: {integrity: sha512-j6IA3pVBybSCwPpsihpX4z8bs6PluuGtr06ahL/xy4D8HunNBTmRmadJrFOQi0gOAbaig4MkQ15nzNLBLy8R1A==}
'@tauri-apps/plugin-log@2.0.0':
resolution: {integrity: sha512-C+NII9vzswqnOQE8k7oRtnaw0z5TZsMmnirRhXkCKDEhQQH9841Us/PC1WHtGiAaJ8za1A1JB2xXndEq/47X/w==}
'@tauri-apps/plugin-os@2.0.0':
resolution: {integrity: sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==}
'@tauri-apps/plugin-process@2.0.0':
resolution: {integrity: sha512-OYzi0GnkrF4NAnsHZU7U3tjSoP0PbeAlO7T1Z+vJoBUH9sFQ1NSLqWYWQyf8hcb3gVWe7P1JggjiskO+LST1ug==}
'@tauri-apps/plugin-shell@2.0.1':
resolution: {integrity: sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==}
'@tauri-apps/plugin-store@2.1.0':
resolution: {integrity: sha512-GADqrc17opUKYIAKnGHIUgEeTZ2wJGu1ZITKQ1WMuOFdv8fvXRFBAqsqPjE3opgWohbczX6e1NpwmZK1AnuWVw==}
'@tauri-apps/plugin-updater@2.0.0':
resolution: {integrity: sha512-N0cl71g7RPr7zK2Fe5aoIwzw14NcdLcz7XMGFWZVjprsqgDRWoxbnUkknyCQMZthjhGkppCd/wN2MIsUz+eAhQ==}
'@terrazzo/cli@0.0.11': '@terrazzo/cli@0.0.11':
resolution: {integrity: sha512-VJTzZ+uw5bzFcvX73g9kvsEtMebRAP/J1oUB7uB8KZYkHtbjKt153jL7YZl5N6B5pHRFLFRE3RU6XPslSvG29g==} resolution: {integrity: sha512-VJTzZ+uw5bzFcvX73g9kvsEtMebRAP/J1oUB7uB8KZYkHtbjKt153jL7YZl5N6B5pHRFLFRE3RU6XPslSvG29g==}
hasBin: true hasBin: true
@ -5535,14 +5582,6 @@ packages:
tar-stream@3.1.7: tar-stream@3.1.7:
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
tauri-plugin-log-api@https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/cc86b2d9878d6ec02c9f25bd48292621a4bc2a6f:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/cc86b2d9878d6ec02c9f25bd48292621a4bc2a6f}
version: 0.0.0
tauri-plugin-store-api@https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/0b7079b8c55bf25f6d9d9e8c57812d03b48a9788:
resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/0b7079b8c55bf25f6d9d9e8c57812d03b48a9788}
version: 0.0.0
telejson@7.2.0: telejson@7.2.0:
resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==} resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==}
@ -7725,50 +7764,86 @@ snapshots:
dependencies: dependencies:
defer-to-connect: 2.0.1 defer-to-connect: 2.0.1
'@tauri-apps/api@1.6.0': {} '@tauri-apps/api@2.0.3': {}
'@tauri-apps/cli-darwin-arm64@1.6.2': '@tauri-apps/cli-darwin-arm64@2.0.1':
optional: true optional: true
'@tauri-apps/cli-darwin-x64@1.6.2': '@tauri-apps/cli-darwin-x64@2.0.1':
optional: true optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@1.6.2': '@tauri-apps/cli-linux-arm-gnueabihf@2.0.1':
optional: true optional: true
'@tauri-apps/cli-linux-arm64-gnu@1.6.2': '@tauri-apps/cli-linux-arm64-gnu@2.0.1':
optional: true optional: true
'@tauri-apps/cli-linux-arm64-musl@1.6.2': '@tauri-apps/cli-linux-arm64-musl@2.0.1':
optional: true optional: true
'@tauri-apps/cli-linux-x64-gnu@1.6.2': '@tauri-apps/cli-linux-x64-gnu@2.0.1':
optional: true optional: true
'@tauri-apps/cli-linux-x64-musl@1.6.2': '@tauri-apps/cli-linux-x64-musl@2.0.1':
optional: true optional: true
'@tauri-apps/cli-win32-arm64-msvc@1.6.2': '@tauri-apps/cli-win32-arm64-msvc@2.0.1':
optional: true optional: true
'@tauri-apps/cli-win32-ia32-msvc@1.6.2': '@tauri-apps/cli-win32-ia32-msvc@2.0.1':
optional: true optional: true
'@tauri-apps/cli-win32-x64-msvc@1.6.2': '@tauri-apps/cli-win32-x64-msvc@2.0.1':
optional: true optional: true
'@tauri-apps/cli@1.6.2': '@tauri-apps/cli@2.0.1':
optionalDependencies: optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 1.6.2 '@tauri-apps/cli-darwin-arm64': 2.0.1
'@tauri-apps/cli-darwin-x64': 1.6.2 '@tauri-apps/cli-darwin-x64': 2.0.1
'@tauri-apps/cli-linux-arm-gnueabihf': 1.6.2 '@tauri-apps/cli-linux-arm-gnueabihf': 2.0.1
'@tauri-apps/cli-linux-arm64-gnu': 1.6.2 '@tauri-apps/cli-linux-arm64-gnu': 2.0.1
'@tauri-apps/cli-linux-arm64-musl': 1.6.2 '@tauri-apps/cli-linux-arm64-musl': 2.0.1
'@tauri-apps/cli-linux-x64-gnu': 1.6.2 '@tauri-apps/cli-linux-x64-gnu': 2.0.1
'@tauri-apps/cli-linux-x64-musl': 1.6.2 '@tauri-apps/cli-linux-x64-musl': 2.0.1
'@tauri-apps/cli-win32-arm64-msvc': 1.6.2 '@tauri-apps/cli-win32-arm64-msvc': 2.0.1
'@tauri-apps/cli-win32-ia32-msvc': 1.6.2 '@tauri-apps/cli-win32-ia32-msvc': 2.0.1
'@tauri-apps/cli-win32-x64-msvc': 1.6.2 '@tauri-apps/cli-win32-x64-msvc': 2.0.1
'@tauri-apps/plugin-dialog@2.0.1':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-fs@2.0.1':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-http@2.0.1':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-log@2.0.0':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-os@2.0.0':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-process@2.0.0':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-shell@2.0.1':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-store@2.1.0':
dependencies:
'@tauri-apps/api': 2.0.3
'@tauri-apps/plugin-updater@2.0.0':
dependencies:
'@tauri-apps/api': 2.0.3
'@terrazzo/cli@0.0.11': '@terrazzo/cli@0.0.11':
dependencies: dependencies:
@ -12044,14 +12119,6 @@ snapshots:
fast-fifo: 1.3.2 fast-fifo: 1.3.2
streamx: 2.18.0 streamx: 2.18.0
tauri-plugin-log-api@https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/cc86b2d9878d6ec02c9f25bd48292621a4bc2a6f:
dependencies:
'@tauri-apps/api': 1.6.0
tauri-plugin-store-api@https://codeload.github.com/tauri-apps/tauri-plugin-store/tar.gz/0b7079b8c55bf25f6d9d9e8c57812d03b48a9788:
dependencies:
'@tauri-apps/api': 1.6.0
telejson@7.2.0: telejson@7.2.0:
dependencies: dependencies:
memoizerific: 1.11.3 memoizerific: 1.11.3

View File

@ -111,27 +111,22 @@ done
[ -z "${VERSION-}" ] && error "--version is not set" [ -z "${VERSION-}" ] && error "--version is not set"
[ -z "${TAURI_PRIVATE_KEY-}" ] && error "$TAURI_PRIVATE_KEY is not set" [ -z "${TAURI_SIGNING_PRIVATE_KEY-}" ] && error "$TAURI_SIGNING_PRIVATE_KEY is not set"
[ -z "${TAURI_KEY_PASSWORD-}" ] && error "$TAURI_KEY_PASSWORD is not set" [ -z "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD-}" ] && error "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD is not set"
if [ "$CHANNEL" != "release" ] && [ "$CHANNEL" != "nightly" ]; then if [ "$CHANNEL" != "release" ] && [ "$CHANNEL" != "nightly" ]; then
error "--channel must be either 'release' or 'nightly'" error "--channel must be either 'release' or 'nightly'"
fi fi
export TAURI_PRIVATE_KEY="$TAURI_PRIVATE_KEY"
export TAURI_KEY_PASSWORD="$TAURI_KEY_PASSWORD"
if [ "$DO_SIGN" = "true" ]; then if [ "$DO_SIGN" = "true" ]; then
if [ "$OS" = "macos" ]; then if [ "$OS" = "macos" ]; then
[ -z "${APPLE_CERTIFICATE-}" ] && error "$APPLE_CERTIFICATE is not set" [ -z "${APPLE_CERTIFICATE-}" ] && error "$APPLE_CERTIFICATE is not set"
[ -z "${APPLE_CERTIFICATE_PASSWORD-}" ] && error "$APPLE_CERTIFICATE_PASSWORD is not set" [ -z "${APPLE_CERTIFICATE_PASSWORD-}" ] && error "$APPLE_CERTIFICATE_PASSWORD is not set"
[ -z "${APPLE_SIGNING_IDENTITY-}" ] && error "$APPLE_SIGNING_IDENTITY is not set"
[ -z "${APPLE_ID-}" ] && error "$APPLE_ID is not set" [ -z "${APPLE_ID-}" ] && error "$APPLE_ID is not set"
[ -z "${APPLE_TEAM_ID-}" ] && error "$APPLE_TEAM_ID is not set" [ -z "${APPLE_TEAM_ID-}" ] && error "$APPLE_TEAM_ID is not set"
[ -z "${APPLE_PASSWORD-}" ] && error "$APPLE_PASSWORD is not set" [ -z "${APPLE_PASSWORD-}" ] && error "$APPLE_PASSWORD is not set"
export APPLE_CERTIFICATE="$APPLE_CERTIFICATE" export APPLE_CERTIFICATE="$APPLE_CERTIFICATE"
export APPLE_CERTIFICATE_PASSWORD="$APPLE_CERTIFICATE_PASSWORD" export APPLE_CERTIFICATE_PASSWORD="$APPLE_CERTIFICATE_PASSWORD"
export APPLE_SIGNING_IDENTITY="$APPLE_SIGNING_IDENTITY"
export APPLE_ID="$APPLE_ID" export APPLE_ID="$APPLE_ID"
export APPLE_TEAM_ID="$APPLE_TEAM_ID" export APPLE_TEAM_ID="$APPLE_TEAM_ID"
export APPLE_PASSWORD="$APPLE_PASSWORD" export APPLE_PASSWORD="$APPLE_PASSWORD"
@ -163,7 +158,7 @@ trap 'rm -rf "$TMP_DIR"' exit
CONFIG_PATH=$(readlink -f "$PWD/../crates/gitbutler-tauri/tauri.conf.$CHANNEL.json") CONFIG_PATH=$(readlink -f "$PWD/../crates/gitbutler-tauri/tauri.conf.$CHANNEL.json")
# update the version in the tauri release config # update the version in the tauri release config
jq '.package.version="'"$VERSION"'"' "$CONFIG_PATH" >"$TMP_DIR/tauri.conf.json" jq '.version="'"$VERSION"'"' "$CONFIG_PATH" >"$TMP_DIR/tauri.conf.json"
if [ "$OS" = "windows" ]; then if [ "$OS" = "windows" ]; then
FEATURES="windows" FEATURES="windows"