summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/components/settings-general.tsx44
-rw-r--r--packages/app/src/context/platform.tsx8
-rw-r--r--packages/app/src/i18n/en.ts6
-rw-r--r--packages/app/src/index.ts2
-rw-r--r--packages/desktop/src-tauri/src/lib.rs41
-rw-r--r--packages/desktop/src-tauri/src/linux_display.rs47
-rw-r--r--packages/desktop/src-tauri/src/main.rs17
-rw-r--r--packages/desktop/src/bindings.ts4
-rw-r--r--packages/desktop/src/index.tsx19
9 files changed, 179 insertions, 9 deletions
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index b31cfb6cc..db057a4c4 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -1,8 +1,10 @@
-import { Component, createMemo, type JSX } from "solid-js"
+import { Component, Show, createEffect, createMemo, createResource, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
+import { Icon } from "@opencode-ai/ui/icon"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
+import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
@@ -40,6 +42,8 @@ export const SettingsGeneral: Component = () => {
checking: false,
})
+ const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
+
const check = () => {
if (!platform.checkUpdate) return
setStore("checking", true)
@@ -410,13 +414,49 @@ export const SettingsGeneral: Component = () => {
</SettingsRow>
</div>
</div>
+
+ <Show when={linux()}>
+ {(_) => {
+ const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.())
+ const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest)
+
+ const onChange = (checked: boolean) =>
+ platform.setDisplayBackend?.(checked ? "wayland" : "auto").finally(() => actions.refetch())
+
+ return (
+ <div class="flex flex-col gap-1">
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
+
+ <div class="bg-surface-raised-base px-4 rounded-lg">
+ <SettingsRow
+ title={
+ <div class="flex items-center gap-2">
+ <span>{language.t("settings.general.row.wayland.title")}</span>
+ <Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
+ <span class="text-text-weak">
+ <Icon name="help" size="small" />
+ </span>
+ </Tooltip>
+ </div>
+ }
+ description={language.t("settings.general.row.wayland.description")}
+ >
+ <div data-action="settings-wayland">
+ <Switch checked={value() === "wayland"} onChange={onChange} />
+ </div>
+ </SettingsRow>
+ </div>
+ </div>
+ )
+ }}
+ </Show>
</div>
</div>
)
}
interface SettingsRowProps {
- title: string
+ title: string | JSX.Element
description: string | JSX.Element
children: JSX.Element
}
diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx
index 3fca502ba..7aa6c6554 100644
--- a/packages/app/src/context/platform.tsx
+++ b/packages/app/src/context/platform.tsx
@@ -57,6 +57,12 @@ export type Platform = {
/** Set the default server URL to use on app startup (platform-specific) */
setDefaultServerUrl?(url: string | null): Promise<void> | void
+ /** Get the preferred display backend (desktop only) */
+ getDisplayBackend?(): Promise<DisplayBackend | null> | DisplayBackend | null
+
+ /** Set the preferred display backend (desktop only) */
+ setDisplayBackend?(backend: DisplayBackend): Promise<void>
+
/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
parseMarkdown?(markdown: string): Promise<string>
@@ -70,6 +76,8 @@ export type Platform = {
readClipboardImage?(): Promise<File | null>
}
+export type DisplayBackend = "auto" | "wayland"
+
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
name: "Platform",
init: (props: { value: Platform }) => {
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 8fba6861b..f4f49f055 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -588,6 +588,7 @@ export const dict = {
"settings.general.section.notifications": "System notifications",
"settings.general.section.updates": "Updates",
"settings.general.section.sounds": "Sound effects",
+ "settings.general.section.display": "Display",
"settings.general.row.language.title": "Language",
"settings.general.row.language.description": "Change the display language for OpenCode",
@@ -598,6 +599,11 @@ export const dict = {
"settings.general.row.font.title": "Font",
"settings.general.row.font.description": "Customise the mono font used in code blocks",
+ "settings.general.row.wayland.title": "Use native Wayland",
+ "settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
+ "settings.general.row.wayland.tooltip":
+ "On Linux with mixed refresh-rate monitors, native Wayland can be more stable.",
+
"settings.general.row.releaseNotes.title": "Release notes",
"settings.general.row.releaseNotes.description": "Show What's New popups after updates",
diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts
index fb6682009..59e1431fa 100644
--- a/packages/app/src/index.ts
+++ b/packages/app/src/index.ts
@@ -1,3 +1,3 @@
-export { PlatformProvider, type Platform } from "./context/platform"
+export { PlatformProvider, type Platform, type DisplayBackend } from "./context/platform"
export { AppBaseProviders, AppInterface } from "./app"
export { useCommand } from "./context/command"
diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs
index 14105e5dd..92eead786 100644
--- a/packages/desktop/src-tauri/src/lib.rs
+++ b/packages/desktop/src-tauri/src/lib.rs
@@ -2,6 +2,8 @@ mod cli;
mod constants;
#[cfg(windows)]
mod job_object;
+#[cfg(target_os = "linux")]
+mod linux_display;
mod markdown;
mod server;
mod window_customizer;
@@ -194,6 +196,43 @@ fn check_macos_app(app_name: &str) -> bool {
.unwrap_or(false)
}
+#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
+#[serde(rename_all = "camelCase")]
+pub enum LinuxDisplayBackend {
+ Wayland,
+ Auto,
+}
+
+#[tauri::command]
+#[specta::specta]
+fn get_display_backend() -> Option<LinuxDisplayBackend> {
+ #[cfg(target_os = "linux")]
+ {
+ let prefer = linux_display::read_wayland().unwrap_or(false);
+ return Some(if prefer {
+ LinuxDisplayBackend::Wayland
+ } else {
+ LinuxDisplayBackend::Auto
+ });
+ }
+
+ #[cfg(not(target_os = "linux"))]
+ None
+}
+
+#[tauri::command]
+#[specta::specta]
+fn set_display_backend(_app: AppHandle, _backend: LinuxDisplayBackend) -> Result<(), String> {
+ #[cfg(target_os = "linux")]
+ {
+ let prefer = matches!(_backend, LinuxDisplayBackend::Wayland);
+ return linux_display::write_wayland(&_app, prefer);
+ }
+
+ #[cfg(not(target_os = "linux"))]
+ Ok(())
+}
+
#[cfg(target_os = "linux")]
fn check_linux_app(app_name: &str) -> bool {
return true;
@@ -209,6 +248,8 @@ pub fn run() {
await_initialization,
server::get_default_server_url,
server::set_default_server_url,
+ get_display_backend,
+ set_display_backend,
markdown::parse_markdown_command,
check_app_exists
])
diff --git a/packages/desktop/src-tauri/src/linux_display.rs b/packages/desktop/src-tauri/src/linux_display.rs
new file mode 100644
index 000000000..9e1cf9091
--- /dev/null
+++ b/packages/desktop/src-tauri/src/linux_display.rs
@@ -0,0 +1,47 @@
+use serde::{Deserialize, Serialize};
+use serde_json::json;
+use std::path::PathBuf;
+use tauri::AppHandle;
+use tauri_plugin_store::StoreExt;
+
+use crate::constants::SETTINGS_STORE;
+
+pub const LINUX_DISPLAY_CONFIG_KEY: &str = "linuxDisplayConfig";
+
+#[derive(Default, Serialize, Deserialize)]
+struct DisplayConfig {
+ wayland: Option<bool>,
+}
+
+fn dir() -> Option<PathBuf> {
+ Some(dirs::data_dir()?.join("ai.opencode.desktop"))
+}
+
+fn path() -> Option<PathBuf> {
+ dir().map(|dir| dir.join(SETTINGS_STORE))
+}
+
+pub fn read_wayland() -> Option<bool> {
+ let path = path()?;
+ let raw = std::fs::read_to_string(path).ok()?;
+ let config = serde_json::from_str::<DisplayConfig>(&raw).ok()?;
+ config.wayland
+}
+
+pub fn write_wayland(app: &AppHandle, value: bool) -> Result<(), String> {
+ let store = app
+ .store(SETTINGS_STORE)
+ .map_err(|e| format!("Failed to open settings store: {}", e))?;
+
+ store.set(
+ LINUX_DISPLAY_CONFIG_KEY,
+ json!(DisplayConfig {
+ wayland: Some(value),
+ }),
+ );
+ store
+ .save()
+ .map_err(|e| format!("Failed to save settings store: {}", e))?;
+
+ Ok(())
+}
diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs
index 9ffee8aa5..a95c62578 100644
--- a/packages/desktop/src-tauri/src/main.rs
+++ b/packages/desktop/src-tauri/src/main.rs
@@ -3,6 +3,9 @@
// borrowed from https://github.com/skyline69/balatro-mod-manager
#[cfg(target_os = "linux")]
+mod display;
+
+#[cfg(target_os = "linux")]
fn configure_display_backend() -> Option<String> {
use std::env;
@@ -23,12 +26,16 @@ fn configure_display_backend() -> Option<String> {
return None;
}
- // Allow users to explicitly keep Wayland if they know their setup is stable.
- let allow_wayland = matches!(
- env::var("OC_ALLOW_WAYLAND"),
- Ok(v) if matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes")
- );
+ let prefer_wayland = display::read_wayland().unwrap_or(false);
+ let allow_wayland = prefer_wayland
+ || matches!(
+ env::var("OC_ALLOW_WAYLAND"),
+ Ok(v) if matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes")
+ );
if allow_wayland {
+ if prefer_wayland {
+ return Some("Wayland session detected; using native Wayland from settings".into());
+ }
return Some("Wayland session detected; respecting OC_ALLOW_WAYLAND=1".into());
}
diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts
index 562a98aca..2db1a624c 100644
--- a/packages/desktop/src/bindings.ts
+++ b/packages/desktop/src/bindings.ts
@@ -10,6 +10,8 @@ export const commands = {
awaitInitialization: (events: Channel) => __TAURI_INVOKE<ServerReadyData>("await_initialization", { events }),
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
+ getDisplayBackend: () => __TAURI_INVOKE<"wayland" | "auto" | null>("get_display_backend"),
+ setDisplayBackend: (backend: LinuxDisplayBackend) => __TAURI_INVOKE<null>("set_display_backend", { backend }),
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
checkAppExists: (appName: string) => __TAURI_INVOKE<boolean>("check_app_exists", { appName }),
};
@@ -22,6 +24,8 @@ export const events = {
/* Types */
export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" };
+export type LinuxDisplayBackend = "wayland" | "auto";
+
export type LoadingWindowComplete = null;
export type ServerReadyData = {
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index cf007bdd3..25e9f825c 100644
--- a/packages/desktop/src/index.tsx
+++ b/packages/desktop/src/index.tsx
@@ -1,7 +1,14 @@
// @refresh reload
import { webviewZoom } from "./webview-zoom"
import { render } from "solid-js/web"
-import { AppBaseProviders, AppInterface, PlatformProvider, Platform, useCommand } from "@opencode-ai/app"
+import {
+ AppBaseProviders,
+ AppInterface,
+ PlatformProvider,
+ Platform,
+ DisplayBackend,
+ useCommand,
+} from "@opencode-ai/app"
import { open, save } from "@tauri-apps/plugin-dialog"
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener"
@@ -9,6 +16,7 @@ import { open as shellOpen } from "@tauri-apps/plugin-shell"
import { type as ostype } from "@tauri-apps/plugin-os"
import { check, Update } from "@tauri-apps/plugin-updater"
import { getCurrentWindow } from "@tauri-apps/api/window"
+import { invoke } from "@tauri-apps/api/core"
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
import { relaunch } from "@tauri-apps/plugin-process"
import { AsyncStorage } from "@solid-primitives/storage"
@@ -338,6 +346,15 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
await commands.setDefaultServerUrl(url)
},
+ getDisplayBackend: async () => {
+ const result = await invoke<DisplayBackend | null>("get_display_backend").catch(() => null)
+ return result
+ },
+
+ setDisplayBackend: async (backend) => {
+ await invoke("set_display_backend", { backend }).catch(() => undefined)
+ },
+
parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown),
webviewZoom,