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
12 changes: 12 additions & 0 deletions rust/limux-ghostty-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,18 @@ pub struct ghostty_runtime_config_s {
pub close_surface_cb: ghostty_runtime_close_surface_cb,
}

// -------------------------------------------------------------------
// Config types
// -------------------------------------------------------------------

#[repr(C)]
#[derive(Clone, Copy)]
pub struct ghostty_config_color_s {
pub r: u8,
pub g: u8,
pub b: u8,
}

// -------------------------------------------------------------------
// Functions
// -------------------------------------------------------------------
Expand Down
10 changes: 10 additions & 0 deletions rust/limux-host-linux/src/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,16 @@ pub fn refresh_terminal_displays_in_root(root: &gtk::Widget) {
}
}

pub fn set_terminals_split_state(root: &gtk::Widget, is_split: bool) {
for internals in pane_internals_for_root(root) {
for entry in &internals.tab_state.borrow().tabs {
if let TabKind::Terminal { state } = &entry.kind {
state.handle.set_split_state(is_split);
}
}
}
}

pub fn activate_tab_in_pane(pane_widget: &gtk::Widget, tab_id: &str) -> bool {
let Some(internals) = find_pane_internals(pane_widget) else {
return false;
Expand Down
11 changes: 9 additions & 2 deletions rust/limux-host-linux/src/split_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,17 @@ impl SplitTreeContainer {
let widget = build_widget_tree(&node, state);
bin.append(&widget);

Rc::new(Self {
let container = Rc::new(Self {
tree: RefCell::new(node),
bin,
rebuild_source: RefCell::new(None),
last_focused: RefCell::new(None),
zoomed_pane: RefCell::new(None),
state: state.clone(),
})
});

pane::set_terminals_split_state(container.bin.upcast_ref(), !container.tree.borrow().is_leaf());
container
}

/// The container widget to add to the gtk::Stack.
Expand Down Expand Up @@ -350,6 +353,10 @@ impl SplitTreeContainer {
self.bin.append(&widget);
}
refresh_terminal_displays_after_rebuild(self.bin.upcast_ref());
pane::set_terminals_split_state(
self.bin.upcast_ref(),
!self.tree.borrow().is_leaf(),
);

// Newly created panes are tracked as pane containers rather than the
// inner terminal/browser widget, so restore through the pane helper
Expand Down
106 changes: 106 additions & 0 deletions rust/limux-host-linux/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ use crate::shortcut_config::NormalizedShortcut;
struct GhosttyState {
app: ghostty_app_t,
background_opacity: f64,
background_color: (u8, u8, u8),
unfocused_split_opacity: f64,
}

// Safety: ghostty_app_t is thread-safe for the operations we perform
Expand Down Expand Up @@ -170,6 +172,9 @@ pub struct TerminalHandle {
search_bar: gtk::SearchBar,
search_entry: gtk::SearchEntry,
callbacks: Rc<RefCell<TerminalCallbacks>>,
had_focus: Rc<Cell<bool>>,
unfocused_revealer: gtk::Revealer,
is_split: Rc<Cell<bool>>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -385,6 +390,13 @@ impl TerminalHandle {
unsafe { ghostty_surface_free_text(surface, &mut text) };
selection
}

/// Set whether the terminal is part of a split layout.
/// When unfocused and split, a semi-transparent overlay dims the surface.
pub fn set_split_state(&self, split: bool) {
self.is_split.set(split);
update_unfocused_split(&self.unfocused_revealer, self.had_focus.get(), split);
}
}

fn empty_ghostty_text() -> ghostty_text_s {
Expand Down Expand Up @@ -517,6 +529,8 @@ pub fn init_ghostty() {

let config = load_ghostty_config();
let background_opacity = load_background_opacity(config);
let background_color = load_background_color(config);
let unfocused_split_opacity = load_unfocused_split_opacity(config);
CURRENT_SCROLLBAR_ENABLED.store(load_scrollbar_enabled(config), Ordering::Relaxed);

let runtime_config = ghostty_runtime_config_s {
Expand Down Expand Up @@ -546,6 +560,8 @@ pub fn init_ghostty() {
GhosttyState {
app,
background_opacity,
background_color,
unfocused_split_opacity,
}
});
}
Expand All @@ -562,6 +578,22 @@ pub fn ghostty_background_opacity() -> f64 {
.unwrap_or(1.0)
}

pub fn ghostty_background_color() -> (u8, u8, u8) {
init_ghostty();
GHOSTTY
.get()
.map(|state| state.background_color)
.unwrap_or((0, 0, 0))
}

pub fn ghostty_unfocused_split_opacity() -> f64 {
init_ghostty();
GHOSTTY
.get()
.map(|state| state.unfocused_split_opacity)
.unwrap_or(0.7)
}

fn load_background_opacity(config: ghostty_config_t) -> f64 {
let mut opacity = 1.0_f64;
let key = b"background-opacity";
Expand All @@ -581,6 +613,42 @@ fn load_background_opacity(config: ghostty_config_t) -> f64 {
}
}

fn load_background_color(config: ghostty_config_t) -> (u8, u8, u8) {
let mut color = ghostty_config_color_s { r: 0, g: 0, b: 0 };
let key = b"background";
let loaded = unsafe {
ghostty_config_get(
config,
(&mut color as *mut ghostty_config_color_s).cast::<c_void>(),
key.as_ptr().cast::<c_char>(),
key.len(),
)
};
if loaded {
(color.r, color.g, color.b)
} else {
(0, 0, 0)
}
}

fn load_unfocused_split_opacity(config: ghostty_config_t) -> f64 {
let mut opacity = 0.7_f64;
let key = b"unfocused-split-opacity";
let loaded = unsafe {
ghostty_config_get(
config,
(&mut opacity as *mut f64).cast::<c_void>(),
key.as_ptr().cast::<c_char>(),
key.len(),
)
};
if loaded && opacity.is_finite() {
opacity.clamp(0.15, 1.0)
} else {
0.7
}
}

fn load_scrollbar_enabled(config: ghostty_config_t) -> bool {
let mut value: *const c_char = ptr::null();
let key = b"scrollbar";
Expand Down Expand Up @@ -1179,6 +1247,23 @@ pub fn create_terminal(
search_bar.set_margin_end(8);
overlay.add_overlay(&search_bar);

// Unfocused split dimming overlay (Revealer + styled Box).
// Mirrors Ghostty's surface.blp Revealer + DrawingArea approach:
// visible when the surface is unfocused AND part of a split.
let is_split = Rc::new(Cell::new(false));
let unfocused_revealer = gtk::Revealer::new();
unfocused_revealer.set_transition_duration(0);
unfocused_revealer.set_can_focus(false);
unfocused_revealer.set_can_target(false);
unfocused_revealer.set_halign(gtk::Align::Fill);
unfocused_revealer.set_valign(gtk::Align::Fill);
let unfocused_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
unfocused_box.set_hexpand(true);
unfocused_box.set_vexpand(true);
unfocused_box.add_css_class("unfocused-split");
unfocused_revealer.set_child(Some(&unfocused_box));
overlay.add_overlay(&unfocused_revealer);

let im_context = gtk::IMMulticontext::new();
im_context.set_client_widget(Some(&gl_area));
im_context.set_use_preedit(true);
Expand All @@ -1190,6 +1275,9 @@ pub fn create_terminal(
search_bar: search_bar.clone(),
search_entry: search_entry.clone(),
callbacks: callbacks.clone(),
had_focus: had_focus.clone(),
unfocused_revealer: unfocused_revealer.clone(),
is_split: is_split.clone(),
};

{
Expand Down Expand Up @@ -1730,21 +1818,35 @@ pub fn create_terminal(
let had_focus_leave = had_focus.clone();
let im_context_enter = im_context.clone();
let im_context_leave = im_context.clone();
let unfocused_revealer_enter = unfocused_revealer.clone();
let unfocused_revealer_leave = unfocused_revealer.clone();
let focus_ctrl = gtk::EventControllerFocus::new();
let sc = surface_cell.clone();
let is_split_enter = is_split.clone();
focus_ctrl.connect_enter(move |_| {
had_focus_enter.set(true);
im_context_enter.focus_in();
if let Some(surface) = *sc.borrow() {
unsafe { ghostty_surface_set_focus(surface, true) };
}
update_unfocused_split(
&unfocused_revealer_enter,
true,
is_split_enter.get(),
);
});
let is_split_leave = is_split.clone();
focus_ctrl.connect_leave(move |_| {
had_focus_leave.set(false);
im_context_leave.focus_out();
if let Some(surface) = *surface_cell.borrow() {
unsafe { ghostty_surface_set_focus(surface, false) };
}
update_unfocused_split(
&unfocused_revealer_leave,
false,
is_split_leave.get(),
);
});
gl_area.add_controller(focus_ctrl);
}
Expand Down Expand Up @@ -2159,6 +2261,10 @@ fn fallback_unshifted_codepoint(keyval: gtk::gdk::Key) -> u32 {
}

/// Show a brief "Copied to clipboard" toast at the bottom of the terminal.
fn update_unfocused_split(revealer: &gtk::Revealer, had_focus: bool, is_split: bool) {
revealer.set_reveal_child(!had_focus && is_split);
}

fn show_clipboard_toast(overlay: &gtk::Overlay) {
let toast = gtk::Box::new(gtk::Orientation::Horizontal, 6);
toast.set_halign(gtk::Align::Center);
Expand Down
13 changes: 12 additions & 1 deletion rust/limux-host-linux/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1706,8 +1706,19 @@ pub fn build_window(app: &adw::Application) {
fn build_window_css(background_opacity: f64) -> String {
let background_opacity = sanitize_background_opacity(background_opacity);
let (r, g, b) = CONTENT_BACKGROUND_RGB;

let (bg_r, bg_g, bg_b) = crate::terminal::ghostty_background_color();
let unfocused_opacity = 1.0 - crate::terminal::ghostty_unfocused_split_opacity();

format!(
"{BASE_CSS}\n.limux-content {{\n background-color: rgba({r}, {g}, {b}, {background_opacity:.3});\n}}\n"
"{BASE_CSS}\n\
.limux-content {{\n\
\x20 background-color: rgba({r}, {g}, {b}, {background_opacity:.3});\n\
}}\n\
.unfocused-split {{\n\
\x20 opacity: {unfocused_opacity:.2};\n\
\x20 background-color: rgb({bg_r}, {bg_g}, {bg_b});\n\
}}\n"
)
}

Expand Down