Skip to content

Commit 6da7c9d

Browse files
authored
feat: add --disable-csd and --disable-transparency for Wayland (#1)
On Wayland, WebKitGTK cannot composite an alpha surface, and GTK CSD renders as a black bar. These flags rewrite the window config before the webview is created. --disable-csd also sets GTK_CSD=0 before gtk_init.
1 parent 2d492e5 commit 6da7c9d

3 files changed

Lines changed: 207 additions & 1 deletion

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ Download a prebuilt installer from **[GitHub Releases](https://github.com/3xian/
8181
- macOS: Apple Silicon and Intel builds
8282
- Linux: build from source; CI installers are not available yet
8383

84+
On Wayland (niri, Sway, Hyprland, …) WebKitGTK cannot composite a transparent
85+
window, and GTK client-side decorations render as a black bar. Launch with:
86+
87+
```bash
88+
PinkCode --disable-csd --disable-transparency
89+
```
90+
8491
## Development
8592

8693
### Prerequisites
@@ -118,6 +125,14 @@ npm run check # frontend + Rust (fmt/clippy/test) — same as CI
118125
| `GROK_BIN` | Path to `grok` / `grok.exe` |
119126
| `GROK_HOME` | Grok data root (default `~/.grok`) |
120127

128+
**CLI (optional)**
129+
130+
| Flag | Meaning |
131+
|------|---------|
132+
| `--disable-csd` | Hide client-side decorations (black title bar on Wayland) |
133+
| `--disable-transparency` | Opaque window (WebKitGTK has no Wayland alpha protocol) |
134+
| `-h`, `--help` | Show flags |
135+
121136
## Architecture
122137

123138
```

src-tauri/src/cli.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//! Startup flags for window chrome that must be applied before the webview exists.
2+
//!
3+
//! Acrylic + Overlay CSD is shaped for macOS/Windows. On Wayland, WebKitGTK
4+
//! cannot composite an alpha surface (no protocol support), and GTK CSD shows
5+
//! as a black bar. These flags rewrite `tauri.conf.json` window fields in
6+
//! memory so the window is created opaque and undecorated.
7+
8+
use tauri::utils::config::{Color, WindowConfig};
9+
use tauri::TitleBarStyle;
10+
11+
/// Cream fill used by the acrylic tint, with a fully opaque alpha.
12+
const OPAQUE_BACKGROUND: Color = Color(250, 244, 237, 255);
13+
14+
pub const USAGE: &str = "\
15+
PinkCode - desktop GUI for Grok Build
16+
17+
Usage:
18+
PinkCode [options]
19+
20+
Options:
21+
--disable-csd Hide client-side decorations (fixes the black
22+
title bar on Wayland compositors such as niri)
23+
--disable-transparency Opaque window (WebKitGTK has no Wayland
24+
protocol for alpha compositing)
25+
-h, --help Show this help
26+
";
27+
28+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29+
pub struct Flags {
30+
pub disable_csd: bool,
31+
pub disable_transparency: bool,
32+
}
33+
34+
pub enum Action {
35+
Run(Flags),
36+
Help,
37+
}
38+
39+
impl Flags {
40+
/// GTK reads `GTK_CSD` at init; `set_decorations(false)` after the window
41+
/// exists is not enough to stop the black CSD bar.
42+
pub fn prepare_env(self) {
43+
if self.disable_csd {
44+
#[cfg(target_os = "linux")]
45+
{
46+
std::env::set_var("GTK_CSD", "0");
47+
}
48+
}
49+
}
50+
51+
pub fn apply(self, config: &mut tauri::Config) {
52+
if !self.disable_csd && !self.disable_transparency {
53+
return;
54+
}
55+
for win in &mut config.app.windows {
56+
self.apply_window(win);
57+
}
58+
}
59+
60+
fn apply_window(self, win: &mut WindowConfig) {
61+
if self.disable_csd {
62+
win.decorations = false;
63+
win.hidden_title = false;
64+
win.title_bar_style = TitleBarStyle::Visible;
65+
}
66+
if self.disable_transparency {
67+
win.transparent = false;
68+
win.window_effects = None;
69+
win.background_color = Some(OPAQUE_BACKGROUND);
70+
}
71+
}
72+
}
73+
74+
/// Unknown args are ignored so WebKit / `tauri dev` extra flags still work.
75+
pub fn parse<I, S>(args: I) -> Action
76+
where
77+
I: IntoIterator<Item = S>,
78+
S: AsRef<str>,
79+
{
80+
let mut flags = Flags::default();
81+
for arg in args.into_iter().skip(1) {
82+
match arg.as_ref() {
83+
"-h" | "--help" => return Action::Help,
84+
"--disable-csd" => flags.disable_csd = true,
85+
"--disable-transparency" => flags.disable_transparency = true,
86+
_ => {}
87+
}
88+
}
89+
Action::Run(flags)
90+
}
91+
92+
#[cfg(test)]
93+
mod tests {
94+
use super::*;
95+
96+
fn flags_of(args: &[&str]) -> Flags {
97+
match parse(args.iter().copied()) {
98+
Action::Run(flags) => flags,
99+
Action::Help => panic!("expected Run"),
100+
}
101+
}
102+
103+
#[test]
104+
fn no_args_leaves_defaults() {
105+
assert_eq!(flags_of(&["PinkCode"]), Flags::default());
106+
}
107+
108+
#[test]
109+
fn each_flag_and_both() {
110+
assert_eq!(
111+
flags_of(&["PinkCode", "--disable-csd"]),
112+
Flags {
113+
disable_csd: true,
114+
disable_transparency: false,
115+
}
116+
);
117+
assert_eq!(
118+
flags_of(&["PinkCode", "--disable-transparency"]),
119+
Flags {
120+
disable_csd: false,
121+
disable_transparency: true,
122+
}
123+
);
124+
assert_eq!(
125+
flags_of(&["PinkCode", "--disable-csd", "--disable-transparency"]),
126+
Flags {
127+
disable_csd: true,
128+
disable_transparency: true,
129+
}
130+
);
131+
}
132+
133+
#[test]
134+
fn help_and_unknown_args() {
135+
assert!(matches!(parse(["PinkCode", "--help"]), Action::Help));
136+
assert_eq!(
137+
flags_of(&["PinkCode", "--something-webkit-passes"]),
138+
Flags::default()
139+
);
140+
}
141+
142+
#[test]
143+
fn apply_rewrites_overlay_acrylic_window() {
144+
let mut win = WindowConfig {
145+
transparent: true,
146+
decorations: true,
147+
hidden_title: true,
148+
title_bar_style: TitleBarStyle::Overlay,
149+
window_effects: Some(Default::default()),
150+
background_color: Some(Color(0, 0, 0, 0)),
151+
..Default::default()
152+
};
153+
Flags {
154+
disable_csd: true,
155+
disable_transparency: true,
156+
}
157+
.apply_window(&mut win);
158+
assert!(!win.decorations);
159+
assert!(!win.hidden_title);
160+
assert_eq!(win.title_bar_style, TitleBarStyle::Visible);
161+
assert!(!win.transparent);
162+
assert!(win.window_effects.is_none());
163+
assert_eq!(win.background_color, Some(OPAQUE_BACKGROUND));
164+
}
165+
166+
#[test]
167+
fn apply_is_a_no_op_without_flags() {
168+
let mut win = WindowConfig {
169+
transparent: true,
170+
decorations: true,
171+
..Default::default()
172+
};
173+
Flags::default().apply_window(&mut win);
174+
assert!(win.transparent);
175+
assert!(win.decorations);
176+
}
177+
}

src-tauri/src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mod agent_types;
66
mod ask_user_question;
77
mod auth;
88
mod billing;
9+
mod cli;
910
mod config;
1011
mod fs_atomic;
1112
mod json_util;
@@ -613,6 +614,19 @@ pub fn run() {
613614
// Layered config + structured logging before any agent/watcher work.
614615
config::init_tracing();
615616

617+
let flags = match cli::parse(std::env::args()) {
618+
cli::Action::Help => {
619+
print!("{}", cli::USAGE);
620+
return;
621+
}
622+
cli::Action::Run(flags) => flags,
623+
};
624+
// GTK_CSD must be set before gtk_init inside Builder::run.
625+
flags.prepare_env();
626+
627+
let mut context = tauri::generate_context!();
628+
flags.apply(context.config_mut());
629+
616630
tauri::Builder::default()
617631
.plugin(tauri_plugin_opener::init())
618632
.plugin(tauri_plugin_dialog::init())
@@ -708,6 +722,6 @@ pub fn run() {
708722
git_commit,
709723
git_apply_patch,
710724
])
711-
.run(tauri::generate_context!())
725+
.run(context)
712726
.expect("error while running tauri application");
713727
}

0 commit comments

Comments
 (0)