Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions Backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use log::{error, warn};
use std::sync::Arc;
use tauri::path::BaseDirectory;
use tauri::WindowEvent;
use tauri::{Emitter, Manager};
use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder};
use tauri_plugin_updater::UpdaterExt;

use crate::core::BackendId;
Expand Down Expand Up @@ -147,11 +147,24 @@ fn try_reopen_last_repo<R: tauri::Runtime>(app_handle: &tauri::AppHandle<R>) {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
load_local_dotenv();
let initial_config = settings::AppConfig::load_or_default();
workarounds::apply_linux_nvidia_workaround();
workarounds::apply_gpu_acceleration_preference(&initial_config.performance);
#[cfg(target_os = "windows")]
let main_window_browser_args =
workarounds::main_window_browser_args(&initial_config.performance);

// Initialize logging after startup-only process environment adjustments.
logging::init();
let app_state = state::AppState::new_with_config();
log::info!(
"performance: GPU acceleration {} at startup",
if initial_config.performance.gpu_accel {
"enabled"
} else {
"disabled"
}
);
let app_state = state::AppState::new_with_config(initial_config);
monitoring::sync_backend_monitoring(&app_state.config());

println!("Running OpenVCS...");
Expand Down Expand Up @@ -197,6 +210,39 @@ pub fn run() {
crate::plugin_paths::set_resource_dir(parent.to_path_buf());
}
}

if app.get_webview_window("main").is_none() {
#[cfg(target_os = "windows")]
let builder = {
let mut builder = WebviewWindowBuilder::new(
app,
"main",
WebviewUrl::App("index.html".into()),
)
.title("OpenVCS")
.inner_size(1100.0, 600.0)
.min_inner_size(1100.0, 600.0)
.resizable(true);
if let Some(args) = main_window_browser_args.clone() {
builder = builder.additional_browser_args(args);
}
builder
};
#[cfg(not(target_os = "windows"))]
let builder = WebviewWindowBuilder::new(
app,
"main",
WebviewUrl::App("index.html".into()),
)
.title("OpenVCS")
.inner_size(1100.0, 600.0)
.min_inner_size(1100.0, 600.0)
.resizable(true);
if let Err(err) = builder.build() {
log::error!("failed to create main window: {}", err);
}
}

// Keep resource lookup state populated before resolving bundled Node
// candidates. `bundled_node_candidate_paths()` uses both the generic
// RESOURCE_DIR base and the exact Tauri-resolved `node-runtime`
Expand Down
2 changes: 1 addition & 1 deletion Backend/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ impl Default for Lfs {
pub struct Performance {
#[serde(default)]
pub progressive_render: bool,
#[serde(default)]
#[serde(default = "default_true")]
pub gpu_accel: bool,
#[serde(default = "default_true")]
pub animations: bool,
Expand Down
5 changes: 2 additions & 3 deletions Backend/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@ pub struct AppState {
}

impl AppState {
/// Creates app state by loading persisted settings and recent repositories.
/// Creates app state from a preloaded settings snapshot and recent repositories.
///
/// # Returns
/// - A fully initialized [`AppState`] with config and recent repositories loaded.
pub fn new_with_config() -> Self {
let cfg = AppConfig::load_or_default(); // reads ~/.config/openvcs/openvcs.conf
pub fn new_with_config(cfg: AppConfig) -> Self {
let s = Self {
config: RwLock::new(cfg),
repo_config: RwLock::new(RepoConfig::default()),
Expand Down
64 changes: 64 additions & 0 deletions Backend/src/workarounds.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright © 2025-2026 OpenVCS Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::settings::Performance;

#[cfg(target_os = "linux")]
/// Applies a runtime workaround for NVIDIA + Wayland rendering issues.
///
Expand Down Expand Up @@ -37,6 +39,38 @@ pub fn apply_linux_nvidia_workaround() {
}
}

#[cfg(target_os = "linux")]
/// Applies the stored GPU acceleration preference before the webview starts.
///
/// When disabled, this forces WebKitGTK into a software/compositing-off path.
/// When enabled, any previously injected disable flags are removed so the host
/// can use the default accelerated path.
///
/// # Parameters
/// - `performance`: Persisted performance settings.
///
/// # Returns
/// - `()`.
pub fn apply_gpu_acceleration_preference(performance: &Performance) {
const COMPOSITING_KEY: &str = "WEBKIT_DISABLE_COMPOSITING_MODE";
const WEBGL_KEY: &str = "WEBKIT_DISABLE_WEBGL";

if performance.gpu_accel {
eprintln!("Clearing GPU-disable env vars where present");
unsafe {
std::env::remove_var(COMPOSITING_KEY);
std::env::remove_var(WEBGL_KEY);
}
return;
}

eprintln!("Applying GPU-disable env vars: {COMPOSITING_KEY}=1, {WEBGL_KEY}=1");
unsafe {
std::env::set_var(COMPOSITING_KEY, "1");
std::env::set_var(WEBGL_KEY, "1");
}
}

#[cfg(not(target_os = "linux"))]
#[inline]
/// No-op on non-Linux platforms.
Expand All @@ -46,3 +80,33 @@ pub fn apply_linux_nvidia_workaround() {
pub fn apply_linux_nvidia_workaround() {
// no-op on non-Linux
}

#[cfg(not(target_os = "linux"))]
#[inline]
/// No-op on non-Linux platforms.
///
/// # Parameters
/// - `performance`: Persisted performance settings.
///
/// # Returns
/// - `()`.
pub fn apply_gpu_acceleration_preference(_performance: &Performance) {
// no-op on non-Linux
}

#[cfg(target_os = "windows")]
/// Returns additional browser arguments for the main webview when GPU acceleration is disabled.
///
/// # Parameters
/// - `performance`: Persisted performance settings.
///
/// # Returns
/// - Browser argument string when GPU acceleration is disabled.
/// - `None` when no override is needed.
pub fn main_window_browser_args(performance: &Performance) -> Option<String> {
if performance.gpu_accel {
return None;
}

Some("--disable-gpu --disable-gpu-compositing".to_string())
}
11 changes: 1 addition & 10 deletions Backend/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,7 @@
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "OpenVCS",
"width": 1100,
"height": 600,
"resizable": true,
"minWidth": 1100,
"minHeight": 600
}
],
"windows": [],
"security": {
"csp": null
}
Expand Down
18 changes: 9 additions & 9 deletions Frontend/src/modals/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@
<div class="dialog sheet" role="dialog" aria-modal="true" aria-labelledby="settings-title">
<div class="sheet-head">
<h3 id="settings-title" style="margin:0">Settings</h3>
<button class="icon close" data-close aria-label="Close">✕</button>
<button class="icon close" data-close type="button" aria-label="Close">✕</button>
</div>

<section class="sheet-body">
<!-- Sidebar -->
<nav class="list-scroll" aria-label="Settings sections">
<ul id="settings-nav" class="list">
<li><button class="seg-btn active" data-section="general">General</button></li>
<li><button class="seg-btn" data-section="diff">Diff &amp; Merge</button></li>
<li><button class="seg-btn" data-section="performance">Performance</button></li>
<li><button class="seg-btn" data-section="ux">UX</button></li>
<li><button class="seg-btn" data-section="logging">Logging</button></li>
<li><button class="seg-btn" data-section="plugins">Plugins</button></li>
<li><button class="seg-btn active" type="button" data-section="general">General</button></li>
<li><button class="seg-btn" type="button" data-section="diff">Diff &amp; Merge</button></li>
<li><button class="seg-btn" type="button" data-section="performance">Performance</button></li>
<li><button class="seg-btn" type="button" data-section="ux">UX</button></li>
<li><button class="seg-btn" type="button" data-section="logging">Logging</button></li>
<li><button class="seg-btn" type="button" data-section="plugins">Plugins</button></li>
<!-- You can add: integrations, advanced, experimental, logging, network later -->
</ul>
</nav>
Expand Down Expand Up @@ -184,8 +184,8 @@ <h3 id="settings-title" style="margin:0">Settings</h3>
</label>
</div>
<div class="group">
<label class="checkbox"><input type="checkbox" id="set-gpu-accel" disabled /> GPU acceleration
<span class="help-tip" title="Use GPU acceleration when available for smoother rendering.">?</span>
<label class="checkbox"><input type="checkbox" id="set-gpu-accel" /> GPU acceleration
<span class="help-tip" title="Takes effect on next app restart; enables accelerated webview compositing where supported.">?</span>
</label>
</div>

Expand Down
34 changes: 26 additions & 8 deletions Frontend/src/scripts/features/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { openModal, closeModal } from '../ui/modals';
import { toKebab } from '../lib/dom';
import { confirmBool } from '../lib/confirm';
import { notify } from '../lib/notify';
import { setTheme, applyCommitSummaryRestriction } from '../ui/layout';
import { setTheme, applyCommitSummaryRestriction, applyGpuAccelerationPreference } from '../ui/layout';
import { collectGeneralSettings, loadGeneralSettingsIntoForm } from './settingsGeneral';
import { DEFAULT_DARK_THEME_ID, DEFAULT_LIGHT_THEME_ID, DEFAULT_THEME_ID, getActiveThemeId, getAvailableThemes, refreshAvailableThemes, selectThemePack } from '../themes';
import { invokePluginAction, reloadPlugins } from '../plugins';
Expand Down Expand Up @@ -241,11 +241,17 @@ async function renderPluginMenus(modal: HTMLElement): Promise<void> {
const panelsScroll = modal.querySelector('#settings-panels-scroll');
if (!nav || !panelsScroll) return;

nav.querySelectorAll<HTMLElement>('[data-plugin-menu="true"]').forEach((node) => node.remove());
nav.querySelectorAll<HTMLElement>('[data-plugin-menus-wrap="true"]').forEach((node) => node.remove());
nav.querySelectorAll<HTMLElement>('[data-plugin-menu="true"]').forEach((node) => {
node.remove();
});
nav.querySelectorAll<HTMLElement>('[data-plugin-menus-wrap="true"]').forEach((node) => {
node.remove();
});
panelsScroll
.querySelectorAll<HTMLElement>('.panel-form[data-plugin-menu="true"]')
.forEach((node) => node.remove());
.forEach((node) => {
node.remove();
});

let menus: PluginMenuPayload[] = [];
let pluginSummaries: PluginSummary[] = [];
Expand Down Expand Up @@ -462,10 +468,10 @@ function activateSection(modal: HTMLElement, section: string) {
})();

const btn = nav.querySelector<HTMLElement>(`[data-section="${safeSection}"]`);
nav.querySelectorAll<HTMLElement>('.seg-btn').forEach(b => {
nav.querySelectorAll<HTMLElement>('.seg-btn').forEach((b) => {
b.classList.toggle('active', b === btn);
});
panels.querySelectorAll<HTMLElement>('.panel-form').forEach(p => {
panels.querySelectorAll<HTMLElement>('.panel-form').forEach((p) => {
p.classList.toggle('hidden', p.getAttribute('data-panel') !== safeSection);
});

Expand Down Expand Up @@ -575,7 +581,9 @@ export function wireSettings() {
.filter((el): el is HTMLInputElement => !!el);
const updateLfsDependentState = () => {
const enabled = !!lfsToggle?.checked;
lfsDependents.forEach(input => input.disabled = !enabled);
lfsDependents.forEach((input) => {
input.disabled = !enabled;
});
};
updateLfsDependentState();
lfsToggle?.addEventListener('change', updateLfsDependentState);
Expand Down Expand Up @@ -690,6 +698,14 @@ export function wireSettings() {
}

const next = collectSettingsFromForm(modal);
const previousCfg = (() => {
try {
return JSON.parse(String(modal.dataset.currentCfg || '{}')) as GlobalSettings;
} catch {
return {} as GlobalSettings;
}
})();
const gpuChanged = previousCfg.performance?.gpu_accel !== next.performance?.gpu_accel;

await TAURI.invoke('set_global_settings', { cfg: next });
await syncFrontendMonitoring(next);
Expand All @@ -710,10 +726,11 @@ export function wireSettings() {
if (mono) root.style.setProperty('--mono', mono);
else root.style.removeProperty('--mono');
applyAnimationPreference(next?.performance?.animations);
applyGpuAccelerationPreference(next?.performance?.gpu_accel);
applyCommitSummaryRestriction(next?.general?.restrict_commit_summary !== false);
} catch {}

notify('Settings saved');
notify(gpuChanged ? 'Settings saved. GPU changes apply after restart.' : 'Settings saved');
flashSavedState(settingsSave);
} catch (e) {
console.error('Failed to save settings:', e);
Expand Down Expand Up @@ -766,6 +783,7 @@ export function wireSettings() {
await TAURI.invoke('set_global_settings', { cfg: cur });
await syncFrontendMonitoring(cur);
applyAnimationPreference(cur.performance?.animations);
applyGpuAccelerationPreference(cur.performance?.gpu_accel);
applyCommitSummaryRestriction(cur.general?.restrict_commit_summary !== false);
await loadSettingsIntoForm(modal);
setTheme('system');
Expand Down
5 changes: 4 additions & 1 deletion Frontend/src/scripts/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { destroyOverlayScrollbarsFor, initOverlayScrollbarsFor, refreshOverlaySc
import { prefs, state, hasRepo, resolveVcsActionLabel } from './state/state';
import {
bindTabs, initResizer, refreshRepoActions, setRepoHeader, resetRepoHeader, setTab, setTheme,
bindLayoutActionState, applyCommitSummaryRestriction
bindLayoutActionState, applyCommitSummaryRestriction, applyGpuAccelerationPreference
} from './ui/layout';
import { clearPluginMenubarMenus, initMenubar, refreshPluginMenubarMenus } from './ui/menubar';
import { closeAllModals } from './ui/modals';
Expand Down Expand Up @@ -132,18 +132,21 @@ async function boot() {
const mono = String(cfg?.ux?.font_mono || '').trim();
if (mono) root.style.setProperty('--mono', mono);
applyAnimationPreference(cfg?.performance?.animations);
applyGpuAccelerationPreference(cfg?.performance?.gpu_accel);
applyCommitSummaryRestriction(cfg?.general?.restrict_commit_summary !== false);
} catch { /* best-effort */ }
} catch {
try { await selectThemePack(DEFAULT_LIGHT_THEME_ID, { silent: true, mode: 'system' }); } catch {}
setTheme(prefs.theme);
applyAnimationPreference(true);
applyGpuAccelerationPreference(true);
applyCommitSummaryRestriction(true);
}
})();
} else {
setTheme(prefs.theme);
applyAnimationPreference(true);
applyGpuAccelerationPreference(true);
applyCommitSummaryRestriction(true);
}
wireRenderListCallbacks();
Expand Down
28 changes: 28 additions & 0 deletions Frontend/src/scripts/ui/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,34 @@ describe('applyCommitSummaryRestriction', () => {
});
});

describe('applyGpuAccelerationPreference', () => {
beforeEach(() => {
vi.resetModules();
Object.defineProperty(globalThis, 'matchMedia', {
value: createMatchMediaMock,
configurable: true,
writable: true,
});
mountLayoutDom();
});

it('stores the GPU acceleration preference on the document root', async () => {
const { applyGpuAccelerationPreference } = await import('./layout');

applyGpuAccelerationPreference(false);
expect(document.documentElement.dataset.gpuAcceleration).toBe('off');

applyGpuAccelerationPreference(true);
expect(document.documentElement.dataset.gpuAcceleration).toBe('on');

applyGpuAccelerationPreference(undefined);
expect(document.documentElement.dataset.gpuAcceleration).toBe('on');

applyGpuAccelerationPreference(null);
expect(document.documentElement.dataset.gpuAcceleration).toBe('on');
});
});

describe('refreshRepoActions', () => {
beforeEach(() => {
vi.resetModules();
Expand Down
Loading
Loading