chore: disables transparency for Nvidia and App Image temporarily (#177)

* docs: update README to clarify window transparency handling

* feat: implement rendering environment detection

* chore: optimize rendering environment hook

* fix: improve NVIDIA detection
This commit is contained in:
Gustavo Carvalho
2026-02-14 01:56:21 -03:00
committed by GitHub
parent baa04e6194
commit 17cf8d1081
10 changed files with 429 additions and 9 deletions

View File

@@ -201,6 +201,8 @@ sudo setfacl -m u:$USER:rw /dev/uinput
> **Note:** You may need to log out and back in for the permanent udev rules to take full effect.
> **Note:** Window transparency is automatically disabled when running as an AppImage to prevent rendering artefacts. The app will use a solid opaque background instead. See [Troubleshooting → Transparency](#transparency--opacity-rendering-issues-nvidia-or-appimage) for details.
</details>
<details>
@@ -411,6 +413,27 @@ Config auto-reloads.
4. **X11:** Ensure `xclip` is installed
5. The app simulates `Ctrl+V` — ensure the target app accepts this shortcut
### Transparency / opacity rendering issues (NVIDIA or AppImage)
Users running **NVIDIA proprietary drivers** or launching the app via **AppImage** may experience visual glitches with window transparency (black background, flickering, or garbled content). The app detects these environments automatically and:
1. Sets `WEBKIT_DISABLE_DMABUF_RENDERER=1` to work around WebKit DMA-BUF bugs.
2. Forces window opacity to **100 %** (fully opaque) and removes rounded window corners.
3. Disables the transparency sliders in **Settings → Window Transparency** with an explanatory message.
If detection fails or you want to force this behaviour manually, set the environment variable before launching:
```bash
# Force NVIDIA workaround
IS_NVIDIA=1 win11-clipboard-history
# Force AppImage workaround
IS_APPIMAGE=1 win11-clipboard-history
# Or disable the DMA-BUF renderer directly
WEBKIT_DISABLE_DMABUF_RENDERER=1 win11-clipboard-history
```
### Window appears on the wrong monitor
The app uses smart cursor tracking. If it appears incorrectly, try moving your mouse to the center of the desired screen and pressing the hotkey again.

View File

@@ -89,6 +89,53 @@ sanitize_xdg_data_dirs() {
}
sanitize_xdg_data_dirs
# ---------------------------------------------------------------------------
# NVIDIA GPU detection
# ---------------------------------------------------------------------------
# NVIDIA GPUs with proprietary drivers have known issues with WebKit's
# DMA-BUF renderer causing opacity/transparency rendering artifacts.
# Detect via lspci or the nvidia kernel module presence.
detect_nvidia() {
# Check if user has already forced the flag
if [[ -n "${IS_NVIDIA:-}" ]]; then
return 0
fi
# Method 1: Check for loaded nvidia kernel module
if lsmod 2>/dev/null | grep -qi '^nvidia'; then
export IS_NVIDIA=1
return 0
fi
# Method 2: Check lspci for NVIDIA VGA controller
if command -v lspci &>/dev/null && lspci 2>/dev/null | grep -qi 'vga.*nvidia'; then
export IS_NVIDIA=1
return 0
fi
return 1
}
detect_nvidia
# ---------------------------------------------------------------------------
# AppImage detection
# ---------------------------------------------------------------------------
# AppImage bundles may ship incompatible Mesa/GL libraries that conflict
# with the host GPU driver, causing similar opacity rendering issues.
if [[ -n "${APPIMAGE:-}" ]]; then
export IS_APPIMAGE=1
fi
# ---------------------------------------------------------------------------
# WebKit DMA-BUF workaround for NVIDIA / AppImage
# ---------------------------------------------------------------------------
# When running on NVIDIA or inside an AppImage, disable the DMA-BUF
# renderer in WebKitGTK to prevent opacity/transparency glitches.
if [[ "${IS_NVIDIA:-}" == "1" || "${IS_APPIMAGE:-}" == "1" ]]; then
echo "Info: Disabling WebKit DMA-BUF renderer due to NVIDIA GPU or AppImage environment."
export WEBKIT_DISABLE_DMABUF_RENDERER=1
fi
# ---------------------------------------------------------------------------
# Display & rendering defaults
# ---------------------------------------------------------------------------

View File

@@ -9,6 +9,7 @@ pub mod focus_manager;
pub mod gif_manager;
pub mod input_simulator;
pub mod permission_checker;
pub mod rendering_env;
pub mod session;
pub mod shortcut_conflict_detector;
pub mod shortcut_setup;
@@ -28,6 +29,7 @@ pub use permission_checker::{
check_permissions, fix_permissions_now, is_first_run, mark_first_run_complete, reset_first_run,
PermissionStatus,
};
pub use rendering_env::{get_rendering_env, get_rendering_environment, RenderingEnv};
pub use session::{get_session_type, is_wayland, is_x11, SessionType};
pub use shortcut_conflict_detector::{
auto_resolve_conflicts, detect_shortcut_conflicts, ConflictDetectionResult, ShortcutConflict,

View File

@@ -19,6 +19,7 @@ use win11_clipboard_history_lib::focus_manager::x11_robust_activate;
use win11_clipboard_history_lib::focus_manager::{restore_focused_window, save_focused_window};
use win11_clipboard_history_lib::input_simulator::simulate_paste_keystroke;
use win11_clipboard_history_lib::permission_checker;
use win11_clipboard_history_lib::rendering_env;
use win11_clipboard_history_lib::session::is_wayland;
use win11_clipboard_history_lib::shortcut_setup;
use win11_clipboard_history_lib::theme_manager::{self, ThemeInfo};
@@ -697,6 +698,10 @@ fn main() {
return;
}
// MUST run before Tauri / WebKit init detects NVIDIA & AppImage and
// sets WEBKIT_DISABLE_DMABUF_RENDERER=1 when needed.
rendering_env::init();
// Check if --background flag is present (start minimized to tray)
let start_in_background = args.iter().any(|arg| arg == "--background");
if start_in_background {
@@ -1002,6 +1007,7 @@ fn main() {
autostart_manager::autostart_disable,
autostart_manager::autostart_is_enabled,
autostart_manager::autostart_migrate,
rendering_env::get_rendering_environment,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -0,0 +1,158 @@
//! Rendering Environment Detection Module
//!
//! Centralised detection of environments where transparency and rounded corners
//! must be disabled to avoid opacity rendering glitches (NVIDIA GPUs, AppImage builds).
//!
//! Detection is done **programmatically** so it works even when the wrapper
//! script is not in the execution path (e.g. AppImage launches the binary
//! directly). The wrapper may *also* set `IS_NVIDIA` / `IS_APPIMAGE` — those
//! are respected as overrides.
//!
//! **IMPORTANT**: [`init()`] must be called very early in `main()`, *before*
//! any Tauri / WebKit initialisation, because it sets
//! `WEBKIT_DISABLE_DMABUF_RENDERER=1` when needed.
use serde::Serialize;
use std::process::Command;
use std::sync::OnceLock;
/// Immutable snapshot of the rendering environment, computed once at startup.
#[derive(Debug, Clone, Serialize)]
pub struct RenderingEnv {
/// `true` when an NVIDIA GPU is detected.
pub is_nvidia: bool,
/// `true` when the app is running from an AppImage.
pub is_appimage: bool,
/// `true` when **either** flag is set the frontend uses this as a single
/// gate to disable transparency & rounded corners.
pub transparency_disabled: bool,
/// Human-readable reason string shown in the Settings UI.
/// Empty when transparency is supported.
pub reason: String,
}
/// Singleton computed once by [`init()`] and read thereafter.
static RENDERING_ENV: OnceLock<RenderingEnv> = OnceLock::new();
// ---------------------------------------------------------------------------
// Detection helpers
// ---------------------------------------------------------------------------
/// Detect NVIDIA GPU presence.
///
/// Order of checks:
/// 1. `IS_NVIDIA=1` env var (set by wrapper or user).
/// 2. `/proc/modules` contains a loaded `nvidia` kernel module.
/// 3. `lspci` output mentions an NVIDIA VGA controller.
fn detect_nvidia() -> bool {
// 1. Explicit env override
if std::env::var("IS_NVIDIA")
.map(|v| v == "1")
.unwrap_or(false)
{
return true;
}
// 2. Check loaded kernel modules (fast, no subprocess)
if let Ok(modules) = std::fs::read_to_string("/proc/modules") {
// Each line starts with the module name followed by a space.
// The NVIDIA driver suite loads multiple modules: nvidia, nvidia_drm,
// nvidia_modeset, nvidia_uvm — match any of them.
for line in modules.lines() {
if let Some(name) = line.split_whitespace().next() {
if name.to_ascii_lowercase().starts_with("nvidia") {
return true;
}
}
}
}
// 3. Fall back to lspci
if let Ok(output) = Command::new("lspci").output() {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.lines().any(|l| {
l.to_ascii_lowercase().contains("vga") && l.to_ascii_lowercase().contains("nvidia")
}) {
return true;
}
}
}
false
}
/// Detect whether we are running inside an AppImage.
///
/// The AppImage runtime always sets the `APPIMAGE` env var pointing to the
/// `.AppImage` file path. We also accept the explicit `IS_APPIMAGE=1`
/// override that the wrapper may set.
fn detect_appimage() -> bool {
if std::env::var("IS_APPIMAGE")
.map(|v| v == "1")
.unwrap_or(false)
{
return true;
}
// The standard AppImage runtime sets $APPIMAGE
std::env::var("APPIMAGE")
.map(|v| !v.is_empty())
.unwrap_or(false)
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// **Must be called at the very start of `main()`** before Tauri / WebKit init.
///
/// Performs detection, caches the result, sets `WEBKIT_DISABLE_DMABUF_RENDERER`
/// if needed, and logs the outcome.
pub fn init() {
let env = RENDERING_ENV.get_or_init(|| {
let is_nvidia = detect_nvidia();
let is_appimage = detect_appimage();
let transparency_disabled = is_nvidia || is_appimage;
let reason = if is_nvidia && is_appimage {
"Transparency is not supported on NVIDIA GPUs running via AppImage.".to_string()
} else if is_nvidia {
"Transparency is not supported on NVIDIA GPUs due to rendering issues.".to_string()
} else if is_appimage {
"Transparency is not supported when running as an AppImage.".to_string()
} else {
String::new()
};
RenderingEnv {
is_nvidia,
is_appimage,
transparency_disabled,
reason,
}
});
// Set the WebKit env var *before* any WebView is created.
if env.transparency_disabled {
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
println!(
"[RenderingEnv] WEBKIT_DISABLE_DMABUF_RENDERER=1 (NVIDIA={}, AppImage={})",
env.is_nvidia, env.is_appimage
);
} else {
println!("[RenderingEnv] Transparency enabled (no NVIDIA/AppImage detected)");
}
}
/// Return the cached rendering environment (panics if [`init()`] was not called).
pub fn get_rendering_env() -> &'static RenderingEnv {
RENDERING_ENV
.get()
.expect("rendering_env::init() must be called before get_rendering_env()")
}
/// Tauri command returns the rendering environment to the frontend.
#[tauri::command]
pub fn get_rendering_environment() -> RenderingEnv {
get_rendering_env().clone()
}

View File

@@ -13,6 +13,7 @@ import { KaomojiPicker } from './components/KaomojiPicker'
import { SymbolPicker } from './components/SymbolPicker'
import { calculateSecondaryOpacity, calculateTertiaryOpacity } from './utils/themeUtils'
import { useSystemThemePreference } from './utils/systemTheme'
import { useRenderingEnv } from './hooks/useRenderingEnv'
import type { ActiveTab, UserSettings } from './types/clipboard'
import { ClipboardTab } from './components/ClipboardTab'
@@ -93,8 +94,18 @@ function ClipboardApp() {
const [settings, setSettings] = useState<UserSettings>(DEFAULT_SETTINGS)
const [settingsLoaded, setSettingsLoaded] = useState(false)
const renderingEnv = useRenderingEnv()
const isDark = useThemeMode(settings.theme_mode)
const opacity = isDark ? settings.dark_background_opacity : settings.light_background_opacity
// When transparency is disabled (NVIDIA / AppImage) force opacity to fully opaque
const effectiveDarkOpacity = renderingEnv.transparency_disabled
? 1
: settings.dark_background_opacity
const effectiveLightOpacity = renderingEnv.transparency_disabled
? 1
: settings.light_background_opacity
const opacity = isDark ? effectiveDarkOpacity : effectiveLightOpacity
const secondaryOpacity = calculateSecondaryOpacity(opacity)
const tertiaryOpacity = calculateTertiaryOpacity(opacity)
@@ -144,6 +155,21 @@ function ClipboardApp() {
}
}, [])
// Re-apply CSS opacity variables whenever renderingEnv or settings change
useEffect(() => {
if (renderingEnv.transparency_disabled) {
// Force fully opaque CSS variables
const opaque: UserSettings = {
...settings,
dark_background_opacity: 1,
light_background_opacity: 1,
}
applyBackgroundOpacity(opaque)
} else {
applyBackgroundOpacity(settings)
}
}, [renderingEnv.transparency_disabled, settings])
// Apply theme class when isDark changes
useEffect(() => {
applyThemeClass(isDark)
@@ -261,7 +287,8 @@ function ClipboardApp() {
return (
<div
className={clsx(
'h-screen w-screen overflow-hidden flex flex-col rounded-win11-lg select-none',
'h-screen w-screen overflow-hidden flex flex-col select-none',
renderingEnv.transparency_disabled ? 'rounded-none' : 'rounded-win11-lg',
isDark ? 'glass-effect' : 'glass-effect-light',
isDark ? 'bg-win11-acrylic-bg' : 'bg-win11Light-acrylic-bg',
isDark ? 'text-win11-text-primary' : 'text-win11Light-text-primary'

View File

@@ -9,6 +9,7 @@ import type { UserSettings, CustomKaomoji, BooleanSettingKey } from './types/cli
import { FeaturesSection } from './components/FeaturesSection'
import { Switch } from './components/Switch'
import { useSystemThemePreference } from './utils/systemTheme'
import { useRenderingEnv } from './hooks/useRenderingEnv'
const MIN_HISTORY_SIZE = 1
const MAX_HISTORY_SIZE = 100_000
@@ -125,6 +126,9 @@ function SettingsApp() {
// Custom Kaomoji State
const [newKaomoji, setNewKaomoji] = useState('')
// Rendering environment (NVIDIA / AppImage detection)
const renderingEnv = useRenderingEnv()
// Apply theme to settings window itself
const isDark = useThemeMode(settings.theme_mode)
@@ -543,7 +547,8 @@ function SettingsApp() {
<section
className={clsx(
'rounded-xl border shadow-sm overflow-hidden',
isDark ? 'bg-win11-bg-secondary border-white/5' : 'bg-white border-gray-200/60'
isDark ? 'bg-win11-bg-secondary border-white/5' : 'bg-white border-gray-200/60',
renderingEnv.transparency_disabled && 'opacity-60'
)}
>
<div className="p-6 border-b border-inherit">
@@ -553,6 +558,43 @@ function SettingsApp() {
</p>
</div>
{renderingEnv.transparency_disabled && (
<div
className={clsx(
'mx-6 mt-6 p-3 rounded-lg flex items-start gap-3 text-sm',
isDark ? 'bg-yellow-500/10 text-yellow-300' : 'bg-yellow-50 text-yellow-800'
)}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="flex-shrink-0 mt-0.5"
>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
<div>
<p className="font-medium text-xs">{renderingEnv.reason}</p>
<p
className={clsx(
'text-[11px] mt-1',
isDark ? 'text-yellow-400/70' : 'text-yellow-700'
)}
>
Transparency and rounded window corners have been automatically disabled to
prevent rendering artefacts.
</p>
</div>
</div>
)}
<div className="p-6 space-y-8">
{/* Dark Mode Slider */}
<div className="space-y-4">
@@ -566,7 +608,9 @@ function SettingsApp() {
isDark ? 'bg-black/20' : 'bg-gray-100'
)}
>
{Math.round(settings.dark_background_opacity * 100)}%
{renderingEnv.transparency_disabled
? '100%'
: `${Math.round(settings.dark_background_opacity * 100)}%`}
</div>
</div>
<input
@@ -575,11 +619,15 @@ function SettingsApp() {
min="0"
max="1"
step="0.01"
value={settings.dark_background_opacity}
value={renderingEnv.transparency_disabled ? 1 : settings.dark_background_opacity}
onChange={(e) => handleDarkOpacityChange(Number.parseFloat(e.target.value))}
onMouseUp={commitOpacityChange}
onTouchEnd={commitOpacityChange}
className="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-win11-bg-accent"
disabled={renderingEnv.transparency_disabled}
className={clsx(
'w-full h-1.5 bg-gray-200 rounded-lg appearance-none dark:bg-gray-700 accent-win11-bg-accent',
renderingEnv.transparency_disabled ? 'cursor-not-allowed' : 'cursor-pointer'
)}
/>
</div>
@@ -595,7 +643,9 @@ function SettingsApp() {
isDark ? 'bg-black/20' : 'bg-gray-100'
)}
>
{Math.round(settings.light_background_opacity * 100)}%
{renderingEnv.transparency_disabled
? '100%'
: `${Math.round(settings.light_background_opacity * 100)}%`}
</div>
</div>
<input
@@ -604,11 +654,15 @@ function SettingsApp() {
min="0"
max="1"
step="0.01"
value={settings.light_background_opacity}
value={renderingEnv.transparency_disabled ? 1 : settings.light_background_opacity}
onChange={(e) => handleLightOpacityChange(Number.parseFloat(e.target.value))}
onMouseUp={commitOpacityChange}
onTouchEnd={commitOpacityChange}
className="w-full h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-win11-bg-accent"
disabled={renderingEnv.transparency_disabled}
className={clsx(
'w-full h-1.5 bg-gray-200 rounded-lg appearance-none dark:bg-gray-700 accent-win11-bg-accent',
renderingEnv.transparency_disabled ? 'cursor-not-allowed' : 'cursor-pointer'
)}
/>
</div>
</div>

View File

@@ -0,0 +1,74 @@
import { useState, useEffect } from 'react'
import { invoke } from '@tauri-apps/api/core'
import type { RenderingEnv } from '../types/clipboard'
const DEFAULT_RENDERING_ENV: RenderingEnv = {
is_nvidia: false,
is_appimage: false,
transparency_disabled: false,
reason: '',
}
/**
* Cached rendering environment shared across all hook consumers.
* This avoids repeated IPC calls to `get_rendering_environment`.
*/
let cachedEnv: RenderingEnv | null = null
/**
* Tracks an in-flight request so concurrent hook calls can share the same
* promise instead of issuing multiple IPC calls.
*/
let pendingEnvPromise: Promise<RenderingEnv> | null = null
/**
* Queries the backend once for the rendering environment (NVIDIA / AppImage
* detection) and caches the result process-wide.
*
* Multiple components can safely call this hook — only one IPC call will be made.
*
* When `transparency_disabled` is `true` the caller should:
* - Force opacity to 1 (fully opaque)
* - Remove rounded outer corners (use `rounded-none`)
* - Disable the transparency sliders in Settings
*/
export function useRenderingEnv() {
const [env, setEnv] = useState<RenderingEnv>(cachedEnv ?? DEFAULT_RENDERING_ENV)
useEffect(() => {
// If we already have a cached env, state is already initialized - skip IPC
if (cachedEnv) {
return
}
// Reuse an in-flight request if one exists
if (!pendingEnvPromise) {
pendingEnvPromise = invoke<RenderingEnv>('get_rendering_environment')
.then((result) => {
cachedEnv = result
return result
})
.catch((err) => {
console.error('Failed to query rendering environment:', err)
// On error, keep cachedEnv as-is (likely null) so we can retry
// on next mount if desired
throw err
})
.finally(() => {
pendingEnvPromise = null
})
}
pendingEnvPromise
.then((result) => {
// Guard against cases where the component unmounts; React ignores
// state updates on unmounted components so this is safe
setEnv(result)
})
.catch(() => {
// Error already logged above; keep default env in state
})
}, [])
return env
}

View File

@@ -133,6 +133,23 @@ body {
0 8px 32px rgba(0, 0, 0, 0.15),
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}
/*
* Solid-background fallback for NVIDIA / AppImage environments where
* transparency causes rendering artefacts. Applied automatically when
* the rendering-env hook forces opacity CSS vars to 1.0.
*/
.rounded-none .glass-effect,
.glass-effect.no-transparency {
background: rgb(40, 40, 40);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}
.rounded-none .glass-effect-light,
.glass-effect-light.no-transparency {
background: rgb(255, 255, 255);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
}
}
/* Animations */

View File

@@ -81,3 +81,15 @@ export interface UserSettings {
export type BooleanSettingKey = {
[K in keyof UserSettings]: UserSettings[K] extends boolean ? K : never
}[keyof UserSettings]
/** Rendering environment flags from the backend (NVIDIA / AppImage detection) */
export interface RenderingEnv {
/** true when an NVIDIA GPU is detected */
is_nvidia: boolean
/** true when running from an AppImage */
is_appimage: boolean
/** true when transparency & rounded corners must be disabled */
transparency_disabled: boolean
/** Human-readable reason shown in Settings UI */
reason: string
}