Skip to content
Open
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
64 changes: 62 additions & 2 deletions packages/app/src/components/settings-general.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Component, createMemo, type JSX } from "solid-js"
import { Component, Show, createEffect, createMemo, 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"
Expand Down Expand Up @@ -40,6 +42,38 @@ export const SettingsGeneral: Component = () => {
checking: false,
})

const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
const synced = { value: false }

createEffect(() => {
if (!linux()) return
if (synced.value) return
if (!platform.getDisplayBackend) return
synced.value = true

const result = platform.getDisplayBackend?.()
if (result instanceof Promise) {
void result
.then((backend) => {
if (!backend) return
settings.general.setWayland(backend === "wayland")
})
.catch(() => undefined)
return
}
if (!result) return
settings.general.setWayland(result === "wayland")
})

const setBackend = (checked: boolean) => {
settings.general.setWayland(checked)
if (!platform.setDisplayBackend) return
const result = platform.setDisplayBackend(checked ? "wayland" : "auto")
if (result instanceof Promise) {
void result.catch(() => undefined)
}
}

const check = () => {
if (!platform.checkUpdate) return
setStore("checking", true)
Expand Down Expand Up @@ -410,13 +444,39 @@ export const SettingsGeneral: Component = () => {
</SettingsRow>
</div>
</div>

<Show when={linux()}>
<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={settings.general.wayland()} onChange={setBackend} />
</div>
</SettingsRow>
</div>
</div>
</Show>
</div>
</div>
)
}

interface SettingsRowProps {
title: string
title: string | JSX.Element
description: string | JSX.Element
children: JSX.Element
}
Expand Down
8 changes: 8 additions & 0 deletions packages/app/src/context/platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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> | void

/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
parseMarkdown?(markdown: string): Promise<string>

Expand All @@ -67,6 +73,8 @@ export type Platform = {
checkAppExists?(appName: string): Promise<boolean>
}

export type DisplayBackend = "auto" | "wayland"

export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
name: "Platform",
init: (props: { value: Platform }) => {
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/context/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface Settings {
general: {
autoSave: boolean
releaseNotes: boolean
wayland: boolean
}
updates: {
startup: boolean
Expand All @@ -39,6 +40,7 @@ const defaultSettings: Settings = {
general: {
autoSave: true,
releaseNotes: true,
wayland: false,
},
updates: {
startup: true,
Expand Down Expand Up @@ -109,6 +111,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setReleaseNotes(value: boolean) {
setStore("general", "releaseNotes", value)
},
wayland: createMemo(() => store.general?.wayland ?? defaultSettings.general.wayland),
setWayland(value: boolean) {
setStore("general", "wayland", value)
},
},
updates: {
startup: createMemo(() => store.updates?.startup ?? defaultSettings.updates.startup),
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,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",
Expand All @@ -595,6 +596,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",

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/index.ts
Original file line number Diff line number Diff line change
@@ -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"
38 changes: 38 additions & 0 deletions packages/desktop/src-tauri/src/display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Default, Serialize, Deserialize)]
struct DisplayConfig {
wayland: Option<bool>,
}

fn dir() -> Option<PathBuf> {
if let Ok(value) = std::env::var("XDG_CONFIG_HOME") {
return Some(PathBuf::from(value).join("opencode"));
}

let home = std::env::var("HOME").ok()?;
Some(PathBuf::from(home).join(".config").join("opencode"))
}

fn path() -> Option<PathBuf> {
dir().map(|dir| dir.join("desktop.json"))
}

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(value: bool) -> Result<(), String> {
let dir = dir().ok_or_else(|| "Could not resolve config directory".to_string())?;
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let data = serde_json::to_string_pretty(&DisplayConfig {
wayland: Some(value),
})
.map_err(|e| e.to_string())?;
std::fs::write(dir.join("desktop.json"), data).map_err(|e| e.to_string())?;
Ok(())
}
37 changes: 37 additions & 0 deletions packages/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ mod cli;
mod constants;
#[cfg(windows)]
mod job_object;
#[cfg(target_os = "linux")]
mod display;
mod markdown;
mod server;
mod window_customizer;
Expand Down Expand Up @@ -194,6 +196,39 @@ fn check_macos_app(app_name: &str) -> bool {
.unwrap_or(false)
}

#[tauri::command]
#[specta::specta]
fn get_display_backend() -> Option<String> {
#[cfg(target_os = "linux")]
{
let prefer = display::read_wayland().unwrap_or(false);
if prefer {
return Some("wayland".to_string());
}
return Some("auto".to_string());
}

#[cfg(not(target_os = "linux"))]
None
}

#[tauri::command]
#[specta::specta]
fn set_display_backend(backend: String) -> Result<(), String> {
#[cfg(target_os = "linux")]
{
let prefer = match backend.as_str() {
"wayland" => true,
"auto" => false,
_ => return Err("Unknown display backend".to_string()),
};
return display::write_wayland(prefer);
}

#[cfg(not(target_os = "linux"))]
Ok(())
}

#[cfg(target_os = "linux")]
fn check_linux_app(app_name: &str) -> bool {
return true;
Expand All @@ -209,6 +244,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
])
Expand Down
17 changes: 12 additions & 5 deletions packages/desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

// 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;
Expand All @@ -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());
}

Expand Down
3 changes: 2 additions & 1 deletion packages/desktop/src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>("get_display_backend"),
setDisplayBackend: (backend: string) => __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 }),
};
Expand Down Expand Up @@ -45,4 +47,3 @@ function makeEvent<T>(name: string) {

return Object.assign(fn, base);
}

19 changes: 18 additions & 1 deletion packages/desktop/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
// @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"
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"
Expand Down Expand Up @@ -337,6 +345,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,
Expand Down
Loading