use gtk::prelude::*;
use gtk::{gdk, gio, glib, pango};
use std::cell::{Cell, RefCell};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use vte::prelude::*;

use crate::cli::{Cli, CliAction};
use crate::config::{
    AppSettings, RendererPreference, ThemeProfile, config_path_for_write, expand_user_path,
    run_config_command,
};
use crate::desktop::{
    ensure_user_icon, icon_name, icon_search_path, install_desktop_entry, refresh_user_icon_cache,
    uninstall_desktop_entry,
};
use crate::terminal::{
    CHILD_COLORTERM, CHILD_IMAGE_PROTOCOL, CHILD_TERM, CHILD_TERM_PROGRAM, CHILD_VTE_VERSION,
    DEFAULT_IMAGE_OPACITY, DEFAULT_IMAGE_TERMINAL_OPACITY, DEFAULT_TERMINAL_OPACITY, LaunchCommand,
    LaunchConfig, MAX_SCROLLBACK_LINES, TerminalPane, effective_terminal_opacity,
};
use crate::theme::{THEME_NAMES, TerminalTheme, XFCE_CURSOR_BACKGROUND, XFCE_CURSOR_FOREGROUND};

const APP_ID: &str = "dev.lios.Terminal";
const ACCENT_PRESETS: &[(&str, &str, &str)] = &[
    ("Violet", "#7c3aed", "accent_violet"),
    ("Cyan", "#0ea5e9", "accent_cyan"),
    ("Emerald", "#10b981", "accent_emerald"),
    ("Amber", "#f59e0b", "accent_amber"),
    ("Rose", "#f43f5e", "accent_rose"),
];
struct CursorPreset {
    label: &'static str,
    background: &'static str,
    foreground: &'static str,
    action_name: &'static str,
}

const CURSOR_PRESET_FOREGROUND: &str = "#020303";
const DEFAULT_MATCH_ACCENT: &str = "#10b981";
const CURSOR_PRESETS: &[CursorPreset] = &[
    CursorPreset {
        label: "XFCE Red",
        background: XFCE_CURSOR_BACKGROUND,
        foreground: XFCE_CURSOR_FOREGROUND,
        action_name: "cursor_xfce_red",
    },
    CursorPreset {
        label: "Violet",
        background: "#7c3aed",
        foreground: CURSOR_PRESET_FOREGROUND,
        action_name: "cursor_violet",
    },
    CursorPreset {
        label: "Cyan",
        background: "#0ea5e9",
        foreground: CURSOR_PRESET_FOREGROUND,
        action_name: "cursor_cyan",
    },
    CursorPreset {
        label: "Emerald",
        background: DEFAULT_MATCH_ACCENT,
        foreground: CURSOR_PRESET_FOREGROUND,
        action_name: "cursor_emerald",
    },
    CursorPreset {
        label: "Amber",
        background: "#f59e0b",
        foreground: CURSOR_PRESET_FOREGROUND,
        action_name: "cursor_amber",
    },
    CursorPreset {
        label: "Rose",
        background: "#f43f5e",
        foreground: CURSOR_PRESET_FOREGROUND,
        action_name: "cursor_rose",
    },
];
const CURSOR_PICKER_BACKGROUND_FALLBACK: &str = "#ffffff";
const CURSOR_PICKER_FOREGROUND_FALLBACK: &str = "#000000";

pub fn run() -> glib::ExitCode {
    let cli = match Cli::parse() {
        Ok(cli) => cli,
        Err(message) => {
            eprintln!("{message}");
            return glib::ExitCode::FAILURE;
        }
    };

    match cli.action {
        CliAction::ShowHelp => {
            print!("{}", Cli::help_text());
            glib::ExitCode::SUCCESS
        }
        CliAction::ShowVersion => {
            println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
            glib::ExitCode::SUCCESS
        }
        CliAction::Config(command) => match run_config_command(command) {
            Ok(output) => {
                print!("{output}");
                glib::ExitCode::SUCCESS
            }
            Err(message) => {
                eprintln!("{message}");
                glib::ExitCode::FAILURE
            }
        },
        CliAction::InstallDesktop => match install_desktop_entry() {
            Ok(output) => {
                print!("{output}");
                glib::ExitCode::SUCCESS
            }
            Err(message) => {
                eprintln!("{message}");
                glib::ExitCode::FAILURE
            }
        },
        CliAction::UninstallDesktop => match uninstall_desktop_entry() {
            Ok(output) => {
                print!("{output}");
                glib::ExitCode::SUCCESS
            }
            Err(message) => {
                eprintln!("{message}");
                glib::ExitCode::FAILURE
            }
        },
        CliAction::Run {
            launch,
            config_path,
            overrides,
        } => match AppSettings::load(config_path.clone(), *overrides) {
            Ok(settings) => {
                run_application(launch, settings, config_path_for_write(config_path).ok())
            }
            Err(message) => {
                eprintln!("{message}");
                glib::ExitCode::FAILURE
            }
        },
    }
}

fn run_application(
    launch: LaunchConfig,
    settings: AppSettings,
    config_path: Option<PathBuf>,
) -> glib::ExitCode {
    apply_renderer(settings.window.renderer);

    let application = gtk::Application::builder()
        .application_id(APP_ID)
        .flags(gio::ApplicationFlags::NON_UNIQUE)
        .build();

    application.connect_activate(move |app| {
        build_window(app, launch.clone(), settings.clone(), config_path.clone())
    });
    application.run_with_args(&[env!("CARGO_PKG_NAME")])
}

fn apply_renderer(renderer: RendererPreference) {
    if let Some(value) = renderer.gsk_renderer() {
        // SAFETY: this runs before GTK creates the application/window and before this
        // process spawns any threads. GTK reads GSK_RENDERER during initialization.
        unsafe { std::env::set_var("GSK_RENDERER", value) };
    }
}

fn apply_window_opacity(window: &gtk::ApplicationWindow, opacity: f64) {
    window.set_opacity(opacity.clamp(0.0, 1.0));
}

fn apply_live_app_settings(
    window: &gtk::ApplicationWindow,
    pane: &TerminalPane,
    settings: &AppSettings,
) {
    apply_window_opacity(window, settings.window.opacity);
    window.set_decorated(settings.window.decorated);
    if settings.window.decorated {
        let header = build_headerbar();
        window.set_titlebar(Some(&header));
    } else {
        window.set_titlebar(None::<&gtk::Widget>);
    }
    pane.apply_config(&settings.terminal);
}

fn build_window(
    app: &gtk::Application,
    launch: LaunchConfig,
    settings: AppSettings,
    config_path: Option<PathBuf>,
) {
    install_app_icon();
    install_app_css();

    let window = gtk::ApplicationWindow::builder()
        .application(app)
        .title(&settings.window.title)
        .default_width(settings.window.default_width)
        .default_height(settings.window.default_height)
        .decorated(settings.window.decorated)
        .icon_name(icon_name())
        .build();
    window.add_css_class("lios-window");

    let state = Rc::new(RefCell::new(settings));
    apply_window_opacity(&window, state.borrow().window.opacity);
    let pane = Rc::new(TerminalPane::new(&state.borrow().terminal));
    let terminal = pane.terminal().clone();
    let preferences =
        build_preferences_revealer(&window, pane.clone(), state.clone(), config_path.clone());

    let root = gtk::Overlay::new();
    root.add_css_class("lios-content");
    root.set_child(Some(pane.widget()));
    preferences.set_halign(gtk::Align::Center);
    preferences.set_valign(gtk::Align::Start);
    preferences.set_hexpand(false);
    preferences.set_vexpand(false);
    preferences.set_margin_top(10);
    preferences.set_margin_start(12);
    preferences.set_margin_end(12);
    root.add_overlay(&preferences);
    root.set_measure_overlay(&preferences, false);
    root.set_clip_overlay(&preferences, true);

    if state.borrow().window.decorated {
        let header = build_headerbar();
        window.set_titlebar(Some(&header));
    }

    install_window_actions(
        &window,
        &terminal,
        pane.clone(),
        state.clone(),
        config_path.clone(),
        launch.clone(),
        &preferences,
    );
    install_keyboard_shortcuts(
        &window,
        &terminal,
        &preferences,
        launch.clone(),
        state.clone(),
        config_path.clone(),
    );
    keep_window_title_in_sync(&window, &terminal, state.borrow().window.title.clone());
    close_window_when_shell_exits(&window, &terminal);

    window.set_child(Some(&root));
    window.present();

    pane.spawn(launch);
    pane.focus();
}

fn install_app_icon() {
    gtk::Window::set_default_icon_name(icon_name());

    let Some(display) = gdk::Display::default() else {
        return;
    };
    if let Ok(user_icon) = ensure_user_icon() {
        if let Ok(path) = icon_search_path() {
            gtk::IconTheme::for_display(&display).add_search_path(path);
        }
        if user_icon.changed {
            refresh_user_icon_cache(&user_icon.file);
        }
    }
}

fn install_app_css() {
    let Some(display) = gdk::Display::default() else {
        return;
    };

    let provider = gtk::CssProvider::new();
    provider.load_from_string(
        "
        .lios-topbar {
            background: linear-gradient(90deg, #030405, #0a0d0f 55%, #111417);
            color: #e7e5de;
            border-bottom: 1px solid rgba(188, 185, 170, 0.16);
            min-height: 36px;
        }

        .lios-window,
        .lios-content,
        .lios-terminal-root {
            background: transparent;
        }

        .lios-terminal {
            background: transparent;
        }

        .lios-brand {
            font-weight: 800;
            letter-spacing: 0.08em;
            color: #f2f0e8;
        }

        .lios-brand-row {
            border-radius: 999px;
            padding: 2px 8px;
        }

        .lios-brand-logo {
            margin-right: 7px;
        }

        .lios-prefs-button {
            border-radius: 999px;
            padding: 5px 16px;
            background: linear-gradient(135deg, #15191b, #080a0b);
            color: #e7e5de;
            border: 1px solid rgba(188, 185, 170, 0.24);
            box-shadow: 0 8px 24px rgba(0, 0, 0, 0.36);
        }

        .lios-drawer {
            background: linear-gradient(135deg, #040506, #090b0d 48%, #101316);
            color: #e7e5de;
            border: 1px solid rgba(188, 185, 170, 0.18);
            border-radius: 18px;
            box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.04), 0 30px 110px rgba(0, 0, 0, 0.58);
            padding: 12px;
        }

        .lios-drawer-scroll,
        .lios-drawer-scroll viewport {
            background: #040506;
            border: none;
        }

        .lios-drawer-body {
            background: #040506;
        }

        .lios-drawer-header {
            border-radius: 16px;
            padding: 2px 4px 8px 4px;
            border-bottom: 1px solid rgba(188, 185, 170, 0.12);
        }

        .lios-drawer-footer {
            padding: 8px 4px 0 4px;
            border-top: 1px solid rgba(188, 185, 170, 0.12);
        }

        .lios-eyebrow {
            color: #a8a497;
            font-size: 11px;
            font-weight: 800;
            letter-spacing: 0.18em;
        }

        .lios-drawer-title {
            font-size: 19px;
            font-weight: 800;
            letter-spacing: -0.02em;
            color: #f2f0e8;
        }

        .lios-muted {
            color: #8e918a;
            font-size: 12px;
        }

        .lios-hint {
            color: #6f756e;
            font-size: 11px;
        }

        .lios-card {
            background: #080a0b;
            border: 1px solid rgba(188, 185, 170, 0.16);
            border-radius: 16px;
            padding: 10px;
            box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.035);
        }

        .lios-row-label {
            color: #d7d5ca;
            font-weight: 750;
        }

        .lios-choice {
            border-radius: 999px;
            padding: 4px 10px;
            background: #050607;
            color: #c9c7bd;
            border: 1px solid rgba(188, 185, 170, 0.16);
            box-shadow: none;
        }

        .lios-choice:hover {
            background: #111518;
            color: #f2f0e8;
            border-color: rgba(16, 185, 129, 0.42);
        }

        .lios-choice.lios-selected {
            background: linear-gradient(135deg, #d8d3c3, #10b981);
            color: #050607;
            border-color: rgba(238, 235, 221, 0.40);
            box-shadow: 0 10px 34px rgba(16, 185, 129, 0.16);
        }

        .lios-swatch {
            min-width: 34px;
            min-height: 18px;
            border-radius: 7px;
            border: 1px solid rgba(238, 235, 221, 0.34);
        }

        .lios-swatch-xfce-cursor-bg {
            background: #e6192e;
        }

        .lios-swatch-xfce-cursor-fg {
            background: #a51d2d;
        }

        .lios-color-button {
            min-width: 34px;
            min-height: 30px;
            border-radius: 10px;
            padding: 0;
        }

        .lios-field {
            border-radius: 12px;
            background: #020303;
            color: #e7e5de;
            border: 1px solid rgba(188, 185, 170, 0.16);
            padding: 5px 10px;
        }

        .lios-slider {
            color: #e7e5de;
            padding: 4px 0;
        }

        .lios-slider trough {
            min-height: 8px;
            border-radius: 999px;
            background: #050607;
            border: 1px solid rgba(188, 185, 170, 0.16);
        }

        .lios-slider highlight {
            border-radius: 999px;
            background: linear-gradient(90deg, #d8d3c3, #10b981);
        }

        .lios-slider slider {
            min-width: 16px;
            min-height: 16px;
            border-radius: 999px;
            background: #f2f0e8;
            border: 1px solid rgba(16, 185, 129, 0.62);
        }

        .lios-collapse {
            border-radius: 999px;
            padding: 5px 12px;
            color: #c9c7bd;
            background: #050607;
            border: 1px solid rgba(188, 185, 170, 0.16);
        }

        .lios-apply {
            border-radius: 999px;
            padding: 7px 16px;
            background: linear-gradient(135deg, #d8d3c3, #10b981);
            color: #050607;
            border: 1px solid rgba(238, 235, 221, 0.36);
            font-weight: 700;
            box-shadow: 0 16px 48px rgba(16, 185, 129, 0.16);
        }

        .lios-live-edit {
            border-radius: 999px;
            padding: 6px 12px;
            background: #050607;
            color: #9ca39b;
            border: 1px solid rgba(188, 185, 170, 0.18);
            box-shadow: none;
            font-weight: 700;
        }

        .lios-live-edit.lios-live-active {
            background: radial-gradient(circle at 20% 20%, #ecfdf5, #10b981 42%, #064e3b 100%);
            color: #02130c;
            border-color: rgba(209, 250, 229, 0.82);
            box-shadow: 0 0 22px rgba(16, 185, 129, 0.72), 0 0 48px rgba(16, 185, 129, 0.28);
        }

        .lios-apply:disabled,
        .lios-collapse:disabled {
            opacity: 0.45;
        }
        ",
    );

    gtk::style_context_add_provider_for_display(
        &display,
        &provider,
        gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
    );
}

fn build_headerbar() -> gtk::HeaderBar {
    let header = gtk::HeaderBar::new();
    header.add_css_class("lios-topbar");
    header.set_show_title_buttons(true);

    let title_row = gtk::Box::new(gtk::Orientation::Horizontal, 0);
    title_row.add_css_class("lios-brand-row");

    let logo = gtk::Image::from_icon_name(icon_name());
    logo.set_pixel_size(22);
    logo.add_css_class("lios-brand-logo");
    title_row.append(&logo);

    let title = gtk::Label::new(Some("LIOS"));
    title.add_css_class("lios-brand");
    title_row.append(&title);
    header.set_title_widget(Some(&title_row));

    header
}

fn build_preferences_revealer(
    parent: &gtk::ApplicationWindow,
    pane: Rc<TerminalPane>,
    settings: Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
) -> gtk::Revealer {
    let revealer = gtk::Revealer::new();
    revealer.set_reveal_child(false);
    revealer.set_transition_type(gtk::RevealerTransitionType::SlideDown);
    revealer.set_transition_duration(180);

    let snapshot = settings.borrow().clone();
    let status = gtk::Label::new(None);
    status.add_css_class("lios-muted");
    status.set_ellipsize(pango::EllipsizeMode::Middle);
    status.set_max_width_chars(72);
    status.set_xalign(0.0);
    status.set_hexpand(true);
    let dirty = Rc::new(Cell::new(false));
    let updating_controls = Rc::new(Cell::new(false));
    let dirty_actions = Rc::new(RefCell::new(Vec::<glib::WeakRef<gtk::Button>>::new()));
    let live_edit_enabled = Rc::new(Cell::new(false));
    let live_edit_action = Rc::new(RefCell::new(None::<Rc<dyn Fn()>>));
    let update_dirty_actions: Rc<dyn Fn(bool)> = {
        let dirty = dirty.clone();
        let dirty_actions = dirty_actions.clone();
        Rc::new(move |is_dirty| {
            dirty.set(is_dirty);
            for button in dirty_actions
                .borrow()
                .iter()
                .filter_map(|button| button.upgrade())
            {
                button.set_sensitive(is_dirty);
            }
        })
    };
    let mark_dirty: Rc<dyn Fn()> = {
        let status = status.clone();
        let update_dirty_actions = update_dirty_actions.clone();
        let updating_controls = updating_controls.clone();
        let live_edit_enabled = live_edit_enabled.clone();
        let live_edit_action = live_edit_action.clone();
        Rc::new(move || {
            if updating_controls.get() {
                return;
            }
            update_dirty_actions(true);
            if live_edit_enabled.get() {
                if let Some(action) = live_edit_action.borrow().as_ref().cloned() {
                    action();
                } else {
                    status.set_text("Live Edit pending. Apply to save.");
                }
            } else {
                status.set_text("Unsaved changes. Apply to save.");
            }
        })
    };

    let drawer = gtk::Box::new(gtk::Orientation::Vertical, 8);
    drawer.add_css_class("lios-drawer");
    drawer.set_vexpand(false);

    let header = gtk::Box::new(gtk::Orientation::Horizontal, 8);
    header.add_css_class("lios-drawer-header");
    let title_block = gtk::Box::new(gtk::Orientation::Vertical, 2);
    let eyebrow = gtk::Label::new(Some("KNOTT DYNAMICS"));
    eyebrow.add_css_class("lios-eyebrow");
    eyebrow.set_xalign(0.0);
    let title = gtk::Label::new(Some("Preferences"));
    title.add_css_class("lios-drawer-title");
    title.set_xalign(0.0);
    let subtitle = gtk::Label::new(Some(
        "Apply updates the session and saves preferences. GPU renderer changes apply next launch.",
    ));
    subtitle.add_css_class("lios-muted");
    subtitle.set_xalign(0.0);
    title_block.append(&eyebrow);
    title_block.append(&title);
    title_block.append(&subtitle);
    title_block.set_hexpand(true);

    let collapse = gtk::Button::with_label("Collapse");
    collapse.add_css_class("lios-collapse");
    let revealer_for_collapse = revealer.downgrade();
    let pane_for_collapse = pane.clone();
    collapse.connect_clicked(move |_| {
        if let Some(revealer) = revealer_for_collapse.upgrade() {
            revealer.set_reveal_child(false);
            pane_for_collapse.focus();
        }
    });
    header.append(&title_block);
    header.append(&collapse);
    drawer.append(&header);

    let cards = gtk::FlowBox::new();
    cards.add_css_class("lios-drawer-body");
    cards.set_column_spacing(8);
    cards.set_row_spacing(8);
    cards.set_homogeneous(true);
    cards.set_min_children_per_line(1);
    cards.set_max_children_per_line(2);
    cards.set_selection_mode(gtk::SelectionMode::None);
    cards.set_valign(gtk::Align::Start);

    let display_card = preference_card("Display", "GPU acceleration, font, and terminal colors");
    let gpu_acceleration = bool_choice(snapshot.window.renderer.gpu_enabled());
    gpu_acceleration.connect_changed(mark_dirty.clone());
    display_card.append(&preference_column(
        "GPU acceleration",
        &gpu_acceleration.widget,
    ));

    let gpu_mode_choice = choice_grid(
        &RendererPreference::GPU_MODE_NAMES,
        snapshot.window.renderer.gpu_mode_config(),
        3,
    );
    gpu_mode_choice.connect_changed(mark_dirty.clone());
    display_card.append(&preference_column("GPU mode", &gpu_mode_choice.widget));

    let theme_choice = choice_grid(THEME_NAMES, &snapshot.terminal.theme_name, 2);
    display_card.append(&preference_column("Theme", &theme_choice.widget));

    let cursor_background = CursorColorControl::new(
        "Block",
        &cursor_entry_text(
            snapshot.terminal.cursor_background.as_deref(),
            snapshot.terminal.theme.cursor_background_color(),
        ),
        CURSOR_PICKER_BACKGROUND_FALLBACK,
        mark_dirty.clone(),
    );
    let cursor_foreground = CursorColorControl::new(
        "Text",
        &cursor_entry_text(
            snapshot.terminal.cursor_foreground.as_deref(),
            snapshot.terminal.theme.cursor_foreground_color(),
        ),
        CURSOR_PICKER_FOREGROUND_FALLBACK,
        mark_dirty.clone(),
    );
    let cursor_controls = gtk::Box::new(gtk::Orientation::Vertical, 6);
    cursor_controls.append(&cursor_background.widget);
    cursor_controls.append(&cursor_foreground.widget);
    cursor_controls.append(&xfce_cursor_example());
    let cursor_actions = gtk::Grid::new();
    cursor_actions.set_column_spacing(6);
    cursor_actions.set_row_spacing(6);
    cursor_actions.set_hexpand(true);
    let cursor_match_overlay = gtk::ToggleButton::with_label("Match Accent");
    cursor_match_overlay.add_css_class("lios-choice");
    cursor_match_overlay.set_hexpand(true);
    cursor_match_overlay.set_active(snapshot.terminal.cursor_match_overlay);
    set_choice_toggle_visual(
        &cursor_match_overlay,
        snapshot.terminal.cursor_match_overlay,
    );
    cursor_background
        .widget
        .set_sensitive(!snapshot.terminal.cursor_match_overlay);
    cursor_foreground
        .widget
        .set_sensitive(!snapshot.terminal.cursor_match_overlay);
    cursor_match_overlay.set_tooltip_text(Some(
        "Persistently match the cursor to the effective accent overlay color",
    ));
    for (index, preset) in CURSOR_PRESETS.iter().enumerate() {
        let button = gtk::Button::with_label(preset.label);
        button.add_css_class("lios-choice");
        button.set_hexpand(true);
        let label = preset.label;
        let background = preset.background.to_string();
        let foreground = preset.foreground.to_string();
        let cursor_background_for_preset = cursor_background.clone();
        let cursor_foreground_for_preset = cursor_foreground.clone();
        let cursor_match_for_preset = cursor_match_overlay.clone();
        let status_for_preset = status.clone();
        let mark_dirty_for_preset = mark_dirty.clone();
        let live_edit_for_preset = live_edit_enabled.clone();
        let updating_controls_for_preset = updating_controls.clone();
        button.connect_clicked(move |_| {
            let changed = cursor_background_for_preset.text() != background
                || cursor_foreground_for_preset.text() != foreground
                || cursor_match_for_preset.is_active();
            updating_controls_for_preset.set(true);
            cursor_match_for_preset.set_active(false);
            cursor_background_for_preset.set_text(&background);
            cursor_foreground_for_preset.set_text(&foreground);
            updating_controls_for_preset.set(false);
            if changed {
                mark_dirty_for_preset();
                if !live_edit_for_preset.get() {
                    status_for_preset.set_text(&format!("{label} cursor selected. Apply to save."));
                }
            } else {
                status_for_preset.set_text(&format!("{label} cursor already selected."));
            }
        });
        cursor_actions.attach(&button, (index as i32) % 3, (index as i32) / 3, 1, 1);
    }

    let match_index = CURSOR_PRESETS.len() as i32;
    cursor_actions.attach(
        &cursor_match_overlay,
        match_index % 3,
        match_index / 3,
        1,
        1,
    );

    let inherit_cursor = gtk::Button::with_label("Clear");
    inherit_cursor.add_css_class("lios-choice");
    inherit_cursor.set_hexpand(true);
    let cursor_background_for_inherit = cursor_background.clone();
    let cursor_foreground_for_inherit = cursor_foreground.clone();
    let cursor_match_for_inherit = cursor_match_overlay.clone();
    let status_for_inherit = status.clone();
    let mark_dirty_for_inherit = mark_dirty.clone();
    let live_edit_for_inherit = live_edit_enabled.clone();
    let updating_controls_for_inherit = updating_controls.clone();
    inherit_cursor.connect_clicked(move |_| {
        let changed = !cursor_background_for_inherit.text().is_empty()
            || !cursor_foreground_for_inherit.text().is_empty()
            || cursor_match_for_inherit.is_active();
        updating_controls_for_inherit.set(true);
        cursor_match_for_inherit.set_active(false);
        cursor_background_for_inherit.set_text("");
        cursor_foreground_for_inherit.set_text("");
        updating_controls_for_inherit.set(false);
        if changed {
            mark_dirty_for_inherit();
            if !live_edit_for_inherit.get() {
                status_for_inherit.set_text("Cursor colors cleared. Apply to save.");
            }
        } else {
            status_for_inherit.set_text("Cursor colors already clear.");
        }
    });
    let clear_index = match_index + 1;
    cursor_actions.attach(&inherit_cursor, clear_index % 3, clear_index / 3, 1, 1);
    cursor_controls.append(&cursor_actions);
    display_card.append(&preference_column("Cursor colors", &cursor_controls));
    theme_choice.connect_changed({
        let theme_choice = theme_choice.clone();
        let cursor_background = cursor_background.clone();
        let cursor_foreground = cursor_foreground.clone();
        let updating_controls = updating_controls.clone();
        let mark_dirty = mark_dirty.clone();
        let status = status.clone();
        Rc::new(move || {
            match TerminalTheme::named(&theme_choice.value.borrow()) {
                Ok(theme) => {
                    updating_controls.set(true);
                    set_cursor_controls_from_theme(&cursor_background, &cursor_foreground, &theme);
                    updating_controls.set(false);
                }
                Err(message) => status.set_text(&message),
            }
            mark_dirty();
        })
    });

    let font_entry = gtk::Entry::new();
    font_entry.add_css_class("lios-field");
    font_entry.set_text(&snapshot.terminal.font);
    font_entry.set_placeholder_text(Some("Monospace 12"));
    font_entry.connect_changed({
        let mark_dirty = mark_dirty.clone();
        move |_| mark_dirty()
    });
    display_card.append(&preference_row("Font", &font_entry));

    let decorated = bool_choice(snapshot.window.decorated);
    decorated.connect_changed(mark_dirty.clone());
    display_card.append(&preference_column("Topbar", &decorated.widget));

    let background_card = preference_card("Background", "Image source and file picker");
    let image_entry = gtk::Entry::new();
    image_entry.add_css_class("lios-field");
    image_entry.set_placeholder_text(Some("/path/to/background.jpg"));
    if let Some(image) = &snapshot.terminal.background.image {
        image_entry.set_text(&image.to_string_lossy());
    }
    image_entry.connect_changed({
        let mark_dirty = mark_dirty.clone();
        move |_| mark_dirty()
    });
    let image_opacity = opacity_slider(snapshot.terminal.background.image_opacity);
    image_opacity.connect_value_changed({
        let mark_dirty = mark_dirty.clone();
        move |_| mark_dirty()
    });
    let terminal_opacity = opacity_slider(snapshot.terminal.background.terminal_opacity);
    terminal_opacity.connect_value_changed({
        let mark_dirty = mark_dirty.clone();
        move |_| mark_dirty()
    });

    let image_picker = gtk::Box::new(gtk::Orientation::Vertical, 6);
    image_entry.set_hexpand(true);
    let choose_image = gtk::Button::with_label("Browse");
    choose_image.add_css_class("lios-choice");
    let parent_for_picker = parent.downgrade();
    let pane_for_picker = pane.clone();
    let settings_for_picker = settings.clone();
    let config_path_for_picker = config_path.clone();
    let image_entry_for_picker = image_entry.clone();
    let image_opacity_for_picker = image_opacity.clone();
    let terminal_opacity_for_picker = terminal_opacity.clone();
    let status_for_picker = status.clone();
    let dirty_for_picker = dirty.clone();
    let update_dirty_for_picker = update_dirty_actions.clone();
    let updating_controls_for_picker = updating_controls.clone();
    choose_image.connect_clicked(move |_| {
        let had_unsaved_changes = dirty_for_picker.get();
        let Some(parent_for_picker) = parent_for_picker.upgrade() else {
            return;
        };
        open_background_image_dialog(&parent_for_picker, {
            let pane_for_picker = pane_for_picker.clone();
            let settings_for_picker = settings_for_picker.clone();
            let config_path_for_picker = config_path_for_picker.clone();
            let image_entry_for_picker = image_entry_for_picker.clone();
            let image_opacity_for_picker = image_opacity_for_picker.clone();
            let terminal_opacity_for_picker = terminal_opacity_for_picker.clone();
            let status_for_picker = status_for_picker.clone();
            let update_dirty_for_picker = update_dirty_for_picker.clone();
            let updating_controls_for_picker = updating_controls_for_picker.clone();
            move |path| {
                let path_for_settings = path.clone();
                let result = apply_terminal_settings(
                    &pane_for_picker,
                    &settings_for_picker,
                    config_path_for_picker.as_deref(),
                    move |next| {
                        next.terminal.background.image = Some(path_for_settings);
                        if next.terminal.background.terminal_opacity
                            > DEFAULT_IMAGE_TERMINAL_OPACITY
                        {
                            next.terminal.background.terminal_opacity =
                                DEFAULT_IMAGE_TERMINAL_OPACITY;
                        }
                        if next.terminal.background.image_opacity <= 0.0 {
                            next.terminal.background.image_opacity = DEFAULT_IMAGE_OPACITY;
                        }
                        Ok(())
                    },
                );
                match result {
                    Ok(()) => {
                        updating_controls_for_picker.set(true);
                        image_entry_for_picker.set_text(&path.to_string_lossy());
                        if terminal_opacity_for_picker.value() > DEFAULT_IMAGE_TERMINAL_OPACITY {
                            terminal_opacity_for_picker.set_value(DEFAULT_IMAGE_TERMINAL_OPACITY);
                        }
                        if image_opacity_for_picker.value() <= 0.0 {
                            image_opacity_for_picker.set_value(DEFAULT_IMAGE_OPACITY);
                        }
                        updating_controls_for_picker.set(false);
                        if had_unsaved_changes {
                            if config_path_for_picker.is_some() {
                                status_for_picker.set_text(
                                    "Background image saved. Other changes still need Apply.",
                                );
                            } else {
                                status_for_picker.set_text(
                                    "Background image applied. Other changes still need Apply.",
                                );
                            }
                        } else {
                            update_dirty_for_picker(false);
                            if config_path_for_picker.is_some() {
                                status_for_picker.set_text("Background image applied and saved.");
                            } else {
                                status_for_picker
                                    .set_text("Background image applied for this session.");
                            }
                        }
                    }
                    Err(message) => status_for_picker.set_text(&message),
                }
            }
        });
    });
    let clear_image = gtk::Button::with_label("Clear");
    clear_image.add_css_class("lios-choice");
    let pane_for_clear_image = pane.clone();
    let settings_for_clear_image = settings.clone();
    let config_path_for_clear_image = config_path.clone();
    let image_entry_for_clear = image_entry.clone();
    let status_for_clear_image = status.clone();
    let dirty_for_clear_image = dirty.clone();
    let update_dirty_for_clear_image = update_dirty_actions.clone();
    let updating_controls_for_clear_image = updating_controls.clone();
    clear_image.connect_clicked(move |_| {
        let had_unsaved_changes = dirty_for_clear_image.get();
        let result = apply_terminal_settings(
            &pane_for_clear_image,
            &settings_for_clear_image,
            config_path_for_clear_image.as_deref(),
            |next| {
                next.terminal.background.image = None;
                Ok(())
            },
        );
        match result {
            Ok(()) => {
                updating_controls_for_clear_image.set(true);
                image_entry_for_clear.set_text("");
                updating_controls_for_clear_image.set(false);
                if had_unsaved_changes {
                    if config_path_for_clear_image.is_some() {
                        status_for_clear_image
                            .set_text("Background image cleared. Other changes still need Apply.");
                    } else {
                        status_for_clear_image.set_text(
                            "Background image cleared for this session. Other changes still need Apply.",
                        );
                    }
                } else {
                    update_dirty_for_clear_image(false);
                    if config_path_for_clear_image.is_some() {
                        status_for_clear_image.set_text("Background image cleared and saved.");
                    } else {
                        status_for_clear_image.set_text("Background image cleared for this session.");
                    }
                }
            }
            Err(message) => status_for_clear_image.set_text(&message),
        }
    });
    let image_actions = gtk::Box::new(gtk::Orientation::Horizontal, 6);
    choose_image.set_hexpand(true);
    clear_image.set_hexpand(true);
    image_actions.append(&choose_image);
    image_actions.append(&clear_image);
    image_picker.append(&image_entry);
    image_picker.append(&image_actions);
    background_card.append(&preference_column("Image", &image_picker));

    let opacity_card = preference_card("Opacity", "Master first, then nested layers");
    let master_opacity = opacity_slider(snapshot.window.opacity);
    master_opacity.connect_value_changed({
        let mark_dirty = mark_dirty.clone();
        move |_| mark_dirty()
    });
    opacity_card.append(&preference_row("Master", &master_opacity));
    opacity_card.append(&preference_row("Terminal shade", &terminal_opacity));
    opacity_card.append(&preference_row("Image", &image_opacity));

    let overlay_opacity = opacity_slider(snapshot.terminal.background.overlay_opacity);
    overlay_opacity.connect_value_changed({
        let mark_dirty = mark_dirty.clone();
        move |_| mark_dirty()
    });
    opacity_card.append(&preference_row("Accent tint", &overlay_opacity));

    let overlay_card = preference_card("Accent", "Tint color and random accent");
    let random_overlay = bool_choice(snapshot.terminal.background.random_overlay);
    let overlay_entry = gtk::Entry::new();
    overlay_entry.add_css_class("lios-field");
    overlay_entry.set_placeholder_text(Some("#7c3aed"));
    if let Some(color) = &snapshot.terminal.background.overlay_color {
        overlay_entry.set_text(&color_to_hex(color));
    }
    let accent_touched = Rc::new(Cell::new(false));
    {
        let accent_touched_for_change = accent_touched.clone();
        let overlay_opacity_for_change = overlay_opacity.clone();
        let random_overlay_for_change = random_overlay.clone();
        let mark_dirty = mark_dirty.clone();
        let updating_controls = updating_controls.clone();
        overlay_entry.connect_changed(move |entry| {
            if updating_controls.get() {
                return;
            }
            accent_touched_for_change.set(true);
            if !entry.text().trim().is_empty() {
                random_overlay_for_change.set(false);
                if overlay_opacity_for_change.value() <= 0.0 {
                    overlay_opacity_for_change.set_value(0.18);
                }
            }
            mark_dirty();
        });
    }
    cursor_match_overlay.connect_toggled({
        let cursor_background = cursor_background.clone();
        let cursor_foreground = cursor_foreground.clone();
        let status = status.clone();
        let mark_dirty = mark_dirty.clone();
        let live_edit_enabled = live_edit_enabled.clone();
        let updating_controls = updating_controls.clone();
        move |button: &gtk::ToggleButton| {
            let active = button.is_active();
            set_choice_toggle_visual(button, active);
            cursor_background.widget.set_sensitive(!active);
            cursor_foreground.widget.set_sensitive(!active);
            if updating_controls.get() {
                return;
            }

            mark_dirty();
            if !live_edit_enabled.get() {
                if active {
                    status.set_text("Cursor will follow the accent. Apply to save.");
                } else {
                    status.set_text("Cursor accent matching disabled. Apply to save.");
                }
            }
        }
    });
    overlay_card.append(&preference_row("Overlay color", &overlay_entry));

    let accent_presets = gtk::Grid::new();
    accent_presets.set_column_spacing(6);
    accent_presets.set_row_spacing(6);
    accent_presets.set_hexpand(true);
    for (index, (label, color, _)) in ACCENT_PRESETS.iter().enumerate() {
        let button = gtk::Button::with_label(label);
        button.add_css_class("lios-choice");
        button.set_hexpand(true);
        let color = (*color).to_string();
        let overlay_entry_for_preset = overlay_entry.clone();
        let overlay_opacity_for_preset = overlay_opacity.clone();
        let random_overlay_for_preset = random_overlay.clone();
        let status_for_preset = status.clone();
        let mark_dirty_for_preset = mark_dirty.clone();
        let live_edit_for_preset = live_edit_enabled.clone();
        let updating_controls_for_preset = updating_controls.clone();
        let accent_touched_for_preset = accent_touched.clone();
        button.connect_clicked(move |_| {
            let changed = overlay_entry_for_preset.text().trim() != color.as_str()
                || random_overlay_for_preset.value.get()
                || overlay_opacity_for_preset.value() <= 0.0;
            updating_controls_for_preset.set(true);
            overlay_entry_for_preset.set_text(&color);
            random_overlay_for_preset.set(false);
            if overlay_opacity_for_preset.value() <= 0.0 {
                overlay_opacity_for_preset.set_value(0.18);
            }
            accent_touched_for_preset.set(true);
            updating_controls_for_preset.set(false);
            if changed {
                mark_dirty_for_preset();
                if !live_edit_for_preset.get() {
                    status_for_preset.set_text("Manual accent selected. Apply to save.");
                }
            } else {
                status_for_preset.set_text("Manual accent already selected.");
            }
        });
        accent_presets.attach(&button, (index as i32) % 3, (index as i32) / 3, 1, 1);
    }
    let clear_accent = gtk::Button::with_label("Clear");
    clear_accent.add_css_class("lios-choice");
    clear_accent.set_hexpand(true);
    let overlay_entry_for_clear = overlay_entry.clone();
    let overlay_opacity_for_clear = overlay_opacity.clone();
    let random_overlay_for_clear = random_overlay.clone();
    let status_for_clear_accent = status.clone();
    let mark_dirty_for_clear_accent = mark_dirty.clone();
    let live_edit_for_clear_accent = live_edit_enabled.clone();
    let updating_controls_for_clear_accent = updating_controls.clone();
    let accent_touched_for_clear_accent = accent_touched.clone();
    clear_accent.connect_clicked(move |_| {
        let changed = !overlay_entry_for_clear.text().trim().is_empty()
            || overlay_opacity_for_clear.value() > 0.0
            || random_overlay_for_clear.value.get();
        updating_controls_for_clear_accent.set(true);
        overlay_entry_for_clear.set_text("");
        overlay_opacity_for_clear.set_value(0.0);
        random_overlay_for_clear.set(false);
        accent_touched_for_clear_accent.set(true);
        updating_controls_for_clear_accent.set(false);
        if changed {
            mark_dirty_for_clear_accent();
            if !live_edit_for_clear_accent.get() {
                status_for_clear_accent.set_text("Accent cleared. Apply to save.");
            }
        } else {
            status_for_clear_accent.set_text("Accent already clear.");
        }
    });
    let clear_index = ACCENT_PRESETS.len() as i32;
    accent_presets.attach(&clear_accent, clear_index % 3, clear_index / 3, 1, 1);
    overlay_card.append(&preference_column("Presets", &accent_presets));

    overlay_card.append(&preference_column("Random overlay", &random_overlay.widget));
    random_overlay.connect_changed({
        let accent_touched = accent_touched.clone();
        let mark_dirty = mark_dirty.clone();
        Rc::new(move || {
            accent_touched.set(false);
            mark_dirty();
        })
    });

    let controls = PreferenceFormControls {
        gpu_acceleration: gpu_acceleration.clone(),
        gpu_mode_choice: gpu_mode_choice.clone(),
        theme_choice: theme_choice.clone(),
        cursor_match_overlay: cursor_match_overlay.clone(),
        cursor_background: cursor_background.clone(),
        cursor_foreground: cursor_foreground.clone(),
        font_entry: font_entry.clone(),
        decorated: decorated.clone(),
        image_entry: image_entry.clone(),
        master_opacity: master_opacity.clone(),
        terminal_opacity: terminal_opacity.clone(),
        image_opacity: image_opacity.clone(),
        overlay_opacity: overlay_opacity.clone(),
        overlay_entry: overlay_entry.clone(),
        random_overlay: random_overlay.clone(),
        accent_touched: accent_touched.clone(),
        dirty: dirty.clone(),
        updating_controls: updating_controls.clone(),
        update_dirty_actions: update_dirty_actions.clone(),
        status: status.clone(),
    };

    let profiles_card = build_theme_profiles_card(
        parent,
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        controls.clone(),
    );

    cards.insert(&display_card, -1);
    cards.insert(&background_card, -1);
    cards.insert(&opacity_card, -1);
    cards.insert(&overlay_card, -1);
    cards.insert(&profiles_card, -1);

    let scroll = gtk::ScrolledWindow::new();
    scroll.add_css_class("lios-drawer-scroll");
    scroll.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic);
    scroll.set_propagate_natural_height(true);
    scroll.set_min_content_height(160);
    scroll.set_max_content_height(360);
    scroll.set_child(Some(&cards));
    drawer.append(&scroll);

    let footer = gtk::Box::new(gtk::Orientation::Horizontal, 8);
    footer.add_css_class("lios-drawer-footer");

    let revert = gtk::Button::with_label("Revert");
    revert.add_css_class("lios-collapse");
    revert.set_sensitive(false);
    let settings_for_revert = settings.clone();
    let controls_for_revert = controls.clone();
    revert.connect_clicked(move |_| {
        let current = settings_for_revert.borrow();
        controls_for_revert.sync_from(&current, Some("Reverted unsaved changes."));
    });

    let apply = gtk::Button::with_label("Apply and Save");
    apply.add_css_class("lios-apply");
    apply.set_sensitive(false);
    dirty_actions.borrow_mut().push(revert.downgrade());
    dirty_actions.borrow_mut().push(apply.downgrade());
    let parent_for_apply = parent.downgrade();
    let pane_for_apply = pane.clone();
    let settings_for_apply = settings.clone();
    let status_for_apply = status.clone();
    let controls_for_apply = controls.clone();
    let config_path_for_apply = config_path.clone();
    let update_dirty_for_apply = update_dirty_actions.clone();
    apply.connect_clicked(move |_| {
        let current = settings_for_apply.borrow().clone();
        let (next, renderer_changed) =
            match settings_from_preferences(&current, &controls_for_apply) {
                Ok(next) => next,
                Err(message) => {
                    status_for_apply.set_text(&message);
                    return;
                }
            };

        if let Some(path) = &config_path_for_apply {
            if let Err(message) = next.persist(path) {
                status_for_apply.set_text(&message);
                return;
            }
            if renderer_changed {
                status_for_apply.set_text(&format!(
                    "Saved {}. Renderer applies on next launch.",
                    path.display()
                ));
            } else {
                status_for_apply.set_text(&format!("Saved {}", path.display()));
            }
        } else {
            status_for_apply.set_text(
                "Applied for this session. Set HOME or XDG_CONFIG_HOME to save preferences.",
            );
        }

        if let Some(parent_for_apply) = parent_for_apply.upgrade() {
            apply_live_app_settings(&parent_for_apply, &pane_for_apply, &next);
        } else {
            pane_for_apply.apply_config(&next.terminal);
        }

        *settings_for_apply.borrow_mut() = next;
        controls_for_apply.accent_touched.set(false);
        update_dirty_for_apply(false);
    });

    let live_edit = gtk::ToggleButton::with_label("Live Edit");
    live_edit.add_css_class("lios-live-edit");
    live_edit.set_tooltip_text(Some(
        "Apply and save changes immediately while this drawer is open",
    ));
    let live_edit_syncing = Rc::new(Cell::new(false));
    let parent_for_live_edit = parent.downgrade();
    let pane_for_live_edit = pane.clone();
    let settings_for_live_edit = settings.clone();
    let status_for_live_edit = status.clone();
    let controls_for_live_edit = controls.clone();
    let config_path_for_live_edit = config_path.clone();
    let update_dirty_for_live_edit = update_dirty_actions.clone();
    *live_edit_action.borrow_mut() = Some(Rc::new(move || {
        let current = settings_for_live_edit.borrow().clone();
        let (next, renderer_changed) =
            match settings_from_preferences(&current, &controls_for_live_edit) {
                Ok(next) => next,
                Err(message) => {
                    status_for_live_edit.set_text(&message);
                    return;
                }
            };

        if let Some(path) = &config_path_for_live_edit {
            if let Err(message) = next.persist(path) {
                status_for_live_edit.set_text(&message);
                return;
            }
            if renderer_changed {
                status_for_live_edit.set_text("Live Edit saved. Renderer applies on next launch.");
            } else {
                status_for_live_edit.set_text("Live Edit applied and saved.");
            }
        } else {
            status_for_live_edit.set_text("Live Edit applied for this session.");
        }

        if let Some(parent) = parent_for_live_edit.upgrade() {
            apply_live_app_settings(&parent, &pane_for_live_edit, &next);
        } else {
            pane_for_live_edit.apply_config(&next.terminal);
        }

        *settings_for_live_edit.borrow_mut() = next;
        controls_for_live_edit.accent_touched.set(false);
        update_dirty_for_live_edit(false);
    }));
    live_edit.connect_toggled({
        let live_edit_enabled = live_edit_enabled.clone();
        let live_edit_action = live_edit_action.clone();
        let live_edit_syncing = live_edit_syncing.clone();
        let status = status.clone();
        let dirty = dirty.clone();
        move |button: &gtk::ToggleButton| {
            let active = button.is_active();
            set_live_edit_button_visual(button, active);
            live_edit_enabled.set(active);
            if live_edit_syncing.get() {
                return;
            }
            if active {
                if dirty.get() {
                    if let Some(action) = live_edit_action.borrow().as_ref().cloned() {
                        action();
                    }
                } else {
                    status.set_text("Live Edit on. Changes apply and save immediately.");
                }
            } else if dirty.get() {
                status.set_text("Live Edit off. Unsaved changes still need Apply.");
            } else {
                status.set_text("Live Edit off.");
            }
        }
    });

    let hint = gtk::Label::new(Some("Esc closes"));
    hint.add_css_class("lios-hint");

    footer.append(&live_edit);
    footer.append(&status);
    footer.append(&hint);
    footer.append(&revert);
    footer.append(&apply);
    drawer.append(&footer);

    let settings_for_reveal = settings.clone();
    let controls_for_reveal = controls.clone();
    let live_edit_for_reveal = live_edit.clone();
    let live_edit_enabled_for_reveal = live_edit_enabled.clone();
    let live_edit_syncing_for_reveal = live_edit_syncing.clone();
    revealer.connect_notify_local(Some("reveal-child"), move |revealer, _| {
        live_edit_syncing_for_reveal.set(true);
        live_edit_enabled_for_reveal.set(false);
        live_edit_for_reveal.set_active(false);
        set_live_edit_button_visual(&live_edit_for_reveal, false);
        live_edit_syncing_for_reveal.set(false);

        if revealer.reveals_child() {
            if !controls_for_reveal.is_dirty() {
                let current = settings_for_reveal.borrow();
                controls_for_reveal.sync_from(&current, None);
            }
        }
    });

    revealer.set_child(Some(&drawer));
    revealer
}

fn toggle_revealer(revealer: &gtk::Revealer) -> bool {
    let reveal = !revealer.reveals_child();
    revealer.set_reveal_child(reveal);
    reveal
}

#[derive(Clone)]
struct CursorColorControl {
    widget: gtk::Box,
    entry: gtk::Entry,
    picker: gtk::ColorDialogButton,
    fallback: String,
    syncing: Rc<Cell<bool>>,
}

impl CursorColorControl {
    fn new(label: &str, initial: &str, fallback: &str, mark_dirty: Rc<dyn Fn()>) -> Self {
        let entry = cursor_color_entry(initial);
        let dialog = gtk::ColorDialog::new();
        dialog.set_title(&format!("Choose {label} Cursor Color"));
        dialog.set_with_alpha(false);
        let picker = gtk::ColorDialogButton::new(Some(dialog));
        picker.add_css_class("lios-color-button");
        picker.set_tooltip_text(Some("Pick a cursor color; clear the text field to inherit"));
        picker.set_valign(gtk::Align::Center);
        set_cursor_color_picker(&picker, initial, fallback);

        let syncing = Rc::new(Cell::new(false));
        entry.connect_changed({
            let picker = picker.clone();
            let fallback = fallback.to_string();
            let syncing = syncing.clone();
            let mark_dirty = mark_dirty.clone();
            move |entry| {
                if syncing.get() {
                    return;
                }
                set_cursor_color_picker(&picker, entry.text().trim(), &fallback);
                mark_dirty();
            }
        });
        picker.connect_rgba_notify({
            let entry = entry.clone();
            let syncing = syncing.clone();
            move |picker| {
                if syncing.get() {
                    return;
                }
                syncing.set(true);
                entry.set_text(&color_to_hex(&picker.rgba()));
                syncing.set(false);
                mark_dirty();
            }
        });

        let editor = gtk::Box::new(gtk::Orientation::Horizontal, 6);
        entry.set_hexpand(true);
        editor.append(&entry);
        editor.append(&picker);

        Self {
            widget: preference_row(label, &editor),
            entry,
            picker,
            fallback: fallback.to_string(),
            syncing,
        }
    }

    fn text(&self) -> String {
        self.entry.text().trim().to_string()
    }

    fn set_text(&self, text: &str) {
        self.syncing.set(true);
        self.entry.set_text(text);
        set_cursor_color_picker(&self.picker, text, &self.fallback);
        self.syncing.set(false);
    }
}

#[derive(Clone)]
struct PreferenceFormControls {
    gpu_acceleration: BoolChoice,
    gpu_mode_choice: ChoiceGroup,
    theme_choice: ChoiceGroup,
    cursor_match_overlay: gtk::ToggleButton,
    cursor_background: CursorColorControl,
    cursor_foreground: CursorColorControl,
    font_entry: gtk::Entry,
    decorated: BoolChoice,
    image_entry: gtk::Entry,
    master_opacity: gtk::Scale,
    terminal_opacity: gtk::Scale,
    image_opacity: gtk::Scale,
    overlay_opacity: gtk::Scale,
    overlay_entry: gtk::Entry,
    random_overlay: BoolChoice,
    accent_touched: Rc<Cell<bool>>,
    dirty: Rc<Cell<bool>>,
    updating_controls: Rc<Cell<bool>>,
    update_dirty_actions: Rc<dyn Fn(bool)>,
    status: gtk::Label,
}

impl PreferenceFormControls {
    fn is_dirty(&self) -> bool {
        self.dirty.get()
    }

    fn sync_from(&self, current: &AppSettings, status_text: Option<&str>) {
        self.updating_controls.set(true);
        self.gpu_acceleration
            .set(current.window.renderer.gpu_enabled());
        self.gpu_mode_choice
            .set(current.window.renderer.gpu_mode_config());
        self.theme_choice.set(&current.terminal.theme_name);
        self.cursor_match_overlay
            .set_active(current.terminal.cursor_match_overlay);
        set_choice_toggle_visual(
            &self.cursor_match_overlay,
            current.terminal.cursor_match_overlay,
        );
        self.cursor_background.set_text(&cursor_entry_text(
            current.terminal.cursor_background.as_deref(),
            current.terminal.theme.cursor_background_color(),
        ));
        self.cursor_foreground.set_text(&cursor_entry_text(
            current.terminal.cursor_foreground.as_deref(),
            current.terminal.theme.cursor_foreground_color(),
        ));
        self.cursor_background
            .widget
            .set_sensitive(!current.terminal.cursor_match_overlay);
        self.cursor_foreground
            .widget
            .set_sensitive(!current.terminal.cursor_match_overlay);
        self.font_entry.set_text(&current.terminal.font);
        self.decorated.set(current.window.decorated);
        if let Some(image) = &current.terminal.background.image {
            self.image_entry.set_text(&image.to_string_lossy());
        } else {
            self.image_entry.set_text("");
        }
        self.master_opacity.set_value(current.window.opacity);
        self.terminal_opacity
            .set_value(current.terminal.background.terminal_opacity);
        self.image_opacity
            .set_value(current.terminal.background.image_opacity);
        self.overlay_opacity
            .set_value(current.terminal.background.overlay_opacity);
        if let Some(color) = &current.terminal.background.overlay_color {
            self.overlay_entry.set_text(&color_to_hex(color));
        } else {
            self.overlay_entry.set_text("");
        }
        self.random_overlay
            .set(current.terminal.background.random_overlay);
        self.updating_controls.set(false);
        self.accent_touched.set(false);
        (self.update_dirty_actions)(false);
        self.status.set_text(status_text.unwrap_or(""));
    }
}

#[derive(Clone)]
struct ProfileActionContext {
    parent: glib::WeakRef<gtk::ApplicationWindow>,
    pane: Rc<TerminalPane>,
    settings: Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
    controls: PreferenceFormControls,
}

fn settings_from_preferences(
    current: &AppSettings,
    controls: &PreferenceFormControls,
) -> Result<(AppSettings, bool), String> {
    let mut next = current.clone();
    let theme_name = controls.theme_choice.value.borrow().clone();
    let gpu_mode = controls.gpu_mode_choice.value.borrow().clone();

    let mut theme = TerminalTheme::named(&theme_name)?;
    let cursor_background = controls.cursor_background.text();
    let cursor_foreground = controls.cursor_foreground.text();
    let (cursor_background, cursor_foreground) =
        apply_cursor_colors(&mut theme, &cursor_background, &cursor_foreground)?;
    let renderer =
        RendererPreference::from_gpu_settings(controls.gpu_acceleration.value.get(), &gpu_mode)?;

    let overlay_text = controls.overlay_entry.text().trim().to_string();
    let overlay_color = if overlay_text.is_empty() {
        None
    } else {
        Some(TerminalTheme::parse_color(&overlay_text)?)
    };
    if controls.accent_touched.get()
        && overlay_color.is_some()
        && controls.random_overlay.value.get()
    {
        controls.random_overlay.set(false);
    }

    let image_path = image_path_from_text(controls.image_entry.text().trim())?;
    let renderer_changed = renderer != current.window.renderer;
    next.window.renderer = renderer;
    next.window.opacity = controls.master_opacity.value();
    next.window.decorated = controls.decorated.value.get();
    next.terminal.theme_name = theme_name;
    next.terminal.cursor_background = cursor_background;
    next.terminal.cursor_foreground = cursor_foreground;
    next.terminal.cursor_match_overlay = controls.cursor_match_overlay.is_active();
    next.terminal.theme = theme;
    next.terminal.font = controls.font_entry.text().to_string();
    next.terminal.background.image = image_path;
    next.terminal.background.image_opacity = controls.image_opacity.value();
    next.terminal.background.terminal_opacity = controls.terminal_opacity.value();
    next.terminal.background.overlay_color = overlay_color;
    next.terminal.background.overlay_opacity = controls.overlay_opacity.value();
    next.terminal.background.random_overlay = controls.random_overlay.value.get();

    Ok((next, renderer_changed))
}

fn build_theme_profiles_card(
    parent: &gtk::ApplicationWindow,
    pane: Rc<TerminalPane>,
    settings: Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
    controls: PreferenceFormControls,
) -> gtk::Box {
    let card = preference_card("Profiles", "Optional saved looks");

    let profile_name = gtk::Entry::new();
    profile_name.add_css_class("lios-field");
    profile_name.set_placeholder_text(Some("Profile name"));

    let save_profile = gtk::Button::with_label("Save Current");
    save_profile.add_css_class("lios-choice");

    let save_row = gtk::Box::new(gtk::Orientation::Horizontal, 6);
    profile_name.set_hexpand(true);
    save_profile.set_hexpand(false);
    save_row.append(&profile_name);
    save_row.append(&save_profile);
    card.append(&preference_row("Profile", &save_row));

    let profile_grid = gtk::Grid::new();
    profile_grid.set_column_spacing(6);
    profile_grid.set_row_spacing(6);
    profile_grid.set_hexpand(true);
    let context = ProfileActionContext {
        parent: parent.downgrade(),
        pane,
        settings: settings.clone(),
        config_path,
        controls,
    };
    rebuild_theme_profile_buttons(
        &profile_grid,
        &settings.borrow().theme_profiles.clone(),
        &context,
    );
    card.append(&profile_grid);

    save_profile.connect_clicked({
        let context = context.clone();
        let profile_grid = profile_grid.clone();
        let profile_name = profile_name.clone();
        move |_| {
            let current = context.settings.borrow().clone();
            let (mut next, _) = match settings_from_preferences(&current, &context.controls) {
                Ok(next) => next,
                Err(message) => {
                    context.controls.status.set_text(&message);
                    return;
                }
            };
            next.window.renderer = current.window.renderer;
            let profile = match ThemeProfile::from_settings(&profile_name.text(), &next) {
                Ok(profile) => profile,
                Err(message) => {
                    context.controls.status.set_text(&message);
                    return;
                }
            };
            let profile_label = profile.name.clone();
            next.upsert_theme_profile(profile);
            if let Some(path) = &context.config_path {
                if let Err(message) = next.persist(path) {
                    context.controls.status.set_text(&message);
                    return;
                }
            }
            if let Some(parent) = context.parent.upgrade() {
                apply_live_app_settings(&parent, &context.pane, &next);
            } else {
                context.pane.apply_config(&next.terminal);
            }
            *context.settings.borrow_mut() = next;

            let current = context.settings.borrow();
            let profiles = current.theme_profiles.clone();
            let message = if context.config_path.is_some() {
                format!("Profile '{profile_label}' saved.")
            } else {
                format!("Profile '{profile_label}' saved for this session.")
            };
            context.controls.sync_from(&current, Some(&message));
            drop(current);
            rebuild_theme_profile_buttons(&profile_grid, &profiles, &context);
        }
    });

    card
}

fn rebuild_theme_profile_buttons(
    grid: &gtk::Grid,
    profiles: &[ThemeProfile],
    context: &ProfileActionContext,
) {
    while let Some(child) = grid.first_child() {
        grid.remove(&child);
    }
    if profiles.is_empty() {
        return;
    }

    for (index, profile) in profiles.iter().cloned().enumerate() {
        add_theme_profile_button(grid, index, profile, context.clone());
    }
}

fn add_theme_profile_button(
    grid: &gtk::Grid,
    index: usize,
    profile: ThemeProfile,
    context: ProfileActionContext,
) {
    let button = gtk::Button::with_label(&profile.name);
    button.add_css_class("lios-choice");
    button.set_hexpand(true);
    button.set_tooltip_text(Some(&format!(
        "Apply {} ({})",
        profile.name,
        choice_label(&profile.theme_name)
    )));
    button.connect_clicked(move |_| {
        let Some(parent) = context.parent.upgrade() else {
            return;
        };
        let profile = profile.clone();
        let profile_label = profile.name.clone();
        let result = apply_app_settings(
            &parent,
            &context.pane,
            &context.settings,
            context.config_path.as_deref(),
            move |next| profile.apply_to(next),
        );
        match result {
            Ok(()) => {
                let current = context.settings.borrow();
                let message = if context.config_path.is_some() {
                    format!("Profile '{profile_label}' applied and saved.")
                } else {
                    format!("Profile '{profile_label}' applied for this session.")
                };
                context.controls.sync_from(&current, Some(&message));
            }
            Err(message) => context.controls.status.set_text(&message),
        }
    });
    grid.attach(&button, (index as i32) % 2, (index as i32) / 2, 1, 1);
}

fn set_live_edit_button_visual(button: &gtk::ToggleButton, active: bool) {
    if active {
        button.add_css_class("lios-live-active");
    } else {
        button.remove_css_class("lios-live-active");
    }
}

fn set_choice_toggle_visual(button: &gtk::ToggleButton, active: bool) {
    if active {
        button.add_css_class("lios-selected");
    } else {
        button.remove_css_class("lios-selected");
    }
}

#[derive(Clone)]
struct ChoiceGroup {
    widget: gtk::Grid,
    value: Rc<RefCell<String>>,
    last_reported_value: Rc<RefCell<String>>,
    buttons: Rc<RefCell<Vec<glib::WeakRef<gtk::Button>>>>,
    options: Rc<Vec<String>>,
}

impl ChoiceGroup {
    fn connect_changed(&self, on_change: Rc<dyn Fn()>) {
        for button in self
            .buttons
            .borrow()
            .iter()
            .filter_map(|button| button.upgrade())
        {
            let on_change = on_change.clone();
            let value = self.value.clone();
            let last_reported_value = self.last_reported_value.clone();
            button.connect_clicked(move |_| {
                let current = value.borrow().clone();
                if current != *last_reported_value.borrow() {
                    *last_reported_value.borrow_mut() = current;
                    on_change();
                }
            });
        }
    }

    fn set(&self, selected: &str) {
        *self.value.borrow_mut() = selected.to_string();
        *self.last_reported_value.borrow_mut() = selected.to_string();
        for (index, button) in self
            .buttons
            .borrow()
            .iter()
            .filter_map(|button| button.upgrade())
            .enumerate()
        {
            let is_selected = self
                .options
                .get(index)
                .map(|option| option == selected)
                .unwrap_or(false);
            if is_selected {
                button.add_css_class("lios-selected");
            } else {
                button.remove_css_class("lios-selected");
            }
        }
    }
}

#[derive(Clone)]
struct BoolChoice {
    widget: gtk::Grid,
    value: Rc<Cell<bool>>,
    last_reported_value: Rc<Cell<bool>>,
    buttons: Rc<RefCell<Vec<glib::WeakRef<gtk::Button>>>>,
}

impl BoolChoice {
    fn connect_changed(&self, on_change: Rc<dyn Fn()>) {
        for button in self
            .buttons
            .borrow()
            .iter()
            .filter_map(|button| button.upgrade())
        {
            let on_change = on_change.clone();
            let value = self.value.clone();
            let last_reported_value = self.last_reported_value.clone();
            button.connect_clicked(move |_| {
                let current = value.get();
                if current != last_reported_value.get() {
                    last_reported_value.set(current);
                    on_change();
                }
            });
        }
    }

    fn set(&self, selected: bool) {
        self.value.set(selected);
        self.last_reported_value.set(selected);
        for (index, button) in self
            .buttons
            .borrow()
            .iter()
            .filter_map(|button| button.upgrade())
            .enumerate()
        {
            if selected == (index == 0) {
                button.add_css_class("lios-selected");
            } else {
                button.remove_css_class("lios-selected");
            }
        }
    }
}

fn choice_grid(options: &[&str], selected: &str, columns: i32) -> ChoiceGroup {
    let grid = gtk::Grid::new();
    grid.set_column_spacing(6);
    grid.set_row_spacing(6);
    grid.set_hexpand(true);

    let value = Rc::new(RefCell::new(selected.to_string()));
    let last_reported_value = Rc::new(RefCell::new(selected.to_string()));
    let options = Rc::new(
        options
            .iter()
            .map(|option| (*option).to_string())
            .collect::<Vec<_>>(),
    );
    let buttons = Rc::new(RefCell::new(Vec::<glib::WeakRef<gtk::Button>>::new()));

    for (index, option) in options.iter().enumerate() {
        let option_value = (*option).to_string();
        let button = gtk::Button::with_label(&choice_label(option));
        button.add_css_class("lios-choice");
        button.set_hexpand(true);
        if *option == selected {
            button.add_css_class("lios-selected");
        }

        let buttons_for_click = buttons.clone();
        let value_for_click = value.clone();
        let option_for_click = option_value.clone();
        button.connect_clicked(move |clicked| {
            *value_for_click.borrow_mut() = option_for_click.clone();
            for button in buttons_for_click
                .borrow()
                .iter()
                .filter_map(|button| button.upgrade())
            {
                button.remove_css_class("lios-selected");
            }
            clicked.add_css_class("lios-selected");
        });

        buttons.borrow_mut().push(button.downgrade());
        grid.attach(
            &button,
            (index as i32) % columns,
            (index as i32) / columns,
            1,
            1,
        );
    }

    ChoiceGroup {
        widget: grid,
        value,
        last_reported_value,
        buttons,
        options,
    }
}

fn bool_choice(selected: bool) -> BoolChoice {
    let grid = gtk::Grid::new();
    grid.set_column_spacing(6);
    grid.set_hexpand(true);

    let value = Rc::new(Cell::new(selected));
    let last_reported_value = Rc::new(Cell::new(selected));
    let buttons = Rc::new(RefCell::new(Vec::<glib::WeakRef<gtk::Button>>::new()));

    for (index, (label, state)) in [("On", true), ("Off", false)].iter().enumerate() {
        let button = gtk::Button::with_label(label);
        button.add_css_class("lios-choice");
        button.set_hexpand(true);
        if *state == selected {
            button.add_css_class("lios-selected");
        }

        let buttons_for_click = buttons.clone();
        let value_for_click = value.clone();
        let state_for_click = *state;
        button.connect_clicked(move |clicked| {
            value_for_click.set(state_for_click);
            for button in buttons_for_click
                .borrow()
                .iter()
                .filter_map(|button| button.upgrade())
            {
                button.remove_css_class("lios-selected");
            }
            clicked.add_css_class("lios-selected");
        });

        buttons.borrow_mut().push(button.downgrade());
        grid.attach(&button, index as i32, 0, 1, 1);
    }

    BoolChoice {
        widget: grid,
        value,
        last_reported_value,
        buttons,
    }
}

fn choice_label(value: &str) -> String {
    match value {
        "auto" => "Auto".to_string(),
        "gl" => "OpenGL".to_string(),
        "vulkan" => "Vulkan".to_string(),
        "cairo" => "Cairo".to_string(),
        "xfce" => "Xfce".to_string(),
        "xterm" => "XTerm".to_string(),
        "solarized-dark" => "Solarized Dark".to_string(),
        "solarized-light" => "Solarized Light".to_string(),
        "dark-pastels" => "Dark Pastels".to_string(),
        "dracula" => "Dracula".to_string(),
        "cyberpunk" => "Cyberpunk".to_string(),
        "matrix" => "Matrix".to_string(),
        "ocean" => "Ocean".to_string(),
        "ember" => "Ember".to_string(),
        "green-on-black" => "Green / Black".to_string(),
        "black-on-white" => "Black / White".to_string(),
        "white-on-black" => "White / Black".to_string(),
        other => other.replace(['-', '_'], " "),
    }
}

fn preference_card(title: &str, subtitle: &str) -> gtk::Box {
    let card = gtk::Box::new(gtk::Orientation::Vertical, 6);
    card.add_css_class("lios-card");
    card.set_hexpand(true);
    card.set_vexpand(false);
    card.set_size_request(250, -1);

    let title = gtk::Label::new(Some(title));
    title.add_css_class("lios-row-label");
    title.set_xalign(0.0);
    let subtitle = gtk::Label::new(Some(subtitle));
    subtitle.add_css_class("lios-muted");
    subtitle.set_xalign(0.0);
    subtitle.set_wrap(true);

    card.append(&title);
    card.append(&subtitle);
    card
}

fn preference_column(label: &str, control: &impl IsA<gtk::Widget>) -> gtk::Box {
    let column = gtk::Box::new(gtk::Orientation::Vertical, 4);
    let column_label = gtk::Label::new(Some(label));
    column_label.add_css_class("lios-row-label");
    column_label.set_xalign(0.0);
    control.as_ref().set_hexpand(true);
    column.append(&column_label);
    column.append(control);
    column
}

fn preference_row(label: &str, control: &impl IsA<gtk::Widget>) -> gtk::Box {
    let row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
    let row_label = gtk::Label::new(Some(label));
    row_label.add_css_class("lios-row-label");
    row_label.set_width_chars(11);
    row_label.set_xalign(0.0);
    control.as_ref().set_hexpand(true);
    row.append(&row_label);
    row.append(control);
    row
}

fn opacity_slider(value: f64) -> gtk::Scale {
    let scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 0.0, 1.0, 0.01);
    scale.add_css_class("lios-slider");
    scale.set_draw_value(true);
    scale.set_value_pos(gtk::PositionType::Right);
    scale.set_digits(2);
    scale.set_increments(0.01, 0.05);
    scale.set_value(value.clamp(0.0, 1.0));
    scale
}

fn cursor_color_entry(initial: &str) -> gtk::Entry {
    let entry = gtk::Entry::new();
    entry.add_css_class("lios-field");
    entry.set_placeholder_text(Some("#rrggbb"));
    entry.set_width_chars(9);
    entry.set_text(initial);
    entry
}

fn set_cursor_color_picker(picker: &gtk::ColorDialogButton, text: &str, fallback: &str) {
    let color = if text.trim().is_empty() {
        fallback
    } else {
        text.trim()
    };
    if let Ok(color) = TerminalTheme::parse_color(color) {
        picker.set_rgba(&color);
    }
}

fn cursor_entry_text(config_color: Option<&str>, theme_color: Option<gdk::RGBA>) -> String {
    config_color
        .map(str::to_string)
        .or_else(|| theme_color.map(|color| color_to_hex(&color)))
        .unwrap_or_default()
}

fn set_cursor_controls_from_theme(
    background: &CursorColorControl,
    foreground: &CursorColorControl,
    theme: &TerminalTheme,
) {
    background.set_text(&cursor_entry_text(None, theme.cursor_background_color()));
    foreground.set_text(&cursor_entry_text(None, theme.cursor_foreground_color()));
}

fn xfce_cursor_example() -> gtk::Box {
    let example = gtk::Box::new(gtk::Orientation::Vertical, 5);

    let swatches = gtk::Box::new(gtk::Orientation::Horizontal, 6);
    swatches.append(&color_swatch("lios-swatch-xfce-cursor-bg"));
    swatches.append(&color_swatch("lios-swatch-xfce-cursor-fg"));

    let values = gtk::Label::new(Some(&format!(
        "cursor_background = \"{}\"  cursor_foreground = \"{}\"",
        XFCE_CURSOR_BACKGROUND, XFCE_CURSOR_FOREGROUND
    )));
    values.add_css_class("lios-muted");
    values.set_xalign(0.0);
    values.set_wrap(true);

    example.append(&swatches);
    example.append(&values);
    example
}

fn color_swatch(class_name: &str) -> gtk::Box {
    let swatch = gtk::Box::new(gtk::Orientation::Horizontal, 0);
    swatch.add_css_class("lios-swatch");
    swatch.add_css_class(class_name);
    swatch
}

fn color_to_hex(color: &gdk::RGBA) -> String {
    let red = (color.red().clamp(0.0, 1.0) * 255.0).round() as u8;
    let green = (color.green().clamp(0.0, 1.0) * 255.0).round() as u8;
    let blue = (color.blue().clamp(0.0, 1.0) * 255.0).round() as u8;
    format!("#{red:02x}{green:02x}{blue:02x}")
}

fn image_path_from_text(text: &str) -> Result<Option<PathBuf>, String> {
    if text.trim().is_empty() {
        return Ok(None);
    }

    let path = expand_user_path(PathBuf::from(text.trim()));
    if !path.exists() {
        return Err(format!("background image not found: {}", path.display()));
    }
    if !path.is_file() {
        return Err(format!(
            "background image is not a file: {}",
            path.display()
        ));
    }

    Ok(Some(path))
}

fn open_background_image_dialog(
    parent: &gtk::ApplicationWindow,
    on_path: impl FnOnce(PathBuf) + 'static,
) {
    let dialog = gtk::FileDialog::builder()
        .title("Choose Background Image")
        .modal(true)
        .build();
    let image_filter = gtk::FileFilter::new();
    image_filter.set_name(Some("Images"));
    image_filter.add_pixbuf_formats();
    let filters = gio::ListStore::new::<gtk::FileFilter>();
    filters.append(&image_filter);
    dialog.set_filters(Some(&filters));
    dialog.set_default_filter(Some(&image_filter));
    dialog.open(Some(parent), gio::Cancellable::NONE, move |result| {
        let Ok(file) = result else {
            return;
        };
        let Some(path) = file.path().map(expand_user_path) else {
            eprintln!("background image must be a local file");
            return;
        };
        if !path.is_file() {
            eprintln!("background image is not a file: {}", path.display());
            return;
        }
        on_path(path);
    });
}

fn install_window_actions(
    window: &gtk::ApplicationWindow,
    terminal: &vte::Terminal,
    pane: Rc<TerminalPane>,
    settings: Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
    launch: LaunchConfig,
    preferences: &gtk::Revealer,
) {
    let menu = gio::Menu::new();
    let terminal_menu = gio::Menu::new();
    terminal_menu.append(Some("New Window"), Some("win.new_window"));
    terminal_menu.append(Some("Diagnostics"), Some("win.diagnostics"));
    terminal_menu.append(Some("Close Window"), Some("win.close_window"));
    menu.append_section(None, &terminal_menu);

    let clipboard_menu = gio::Menu::new();
    clipboard_menu.append(Some("Copy"), Some("win.copy"));
    clipboard_menu.append(Some("Copy as HTML"), Some("win.copy-html"));
    clipboard_menu.append(Some("Paste"), Some("win.paste"));
    clipboard_menu.append(Some("Select All"), Some("win.select-all"));
    menu.append_section(None, &clipboard_menu);

    let appearance_menu = gio::Menu::new();
    appearance_menu.append(Some("Open Preferences..."), Some("win.customize"));
    appearance_menu.append(
        Some("Reset to Dark Default"),
        Some("win.reset_dark_default"),
    );

    let background_menu = gio::Menu::new();
    background_menu.append(Some("Choose Image..."), Some("win.choose_background"));
    background_menu.append(Some("Clear Image"), Some("win.clear_background"));
    background_menu.append(Some("Brighten Image"), Some("win.image_brighter"));
    background_menu.append(Some("Dim Image"), Some("win.image_dimmer"));
    background_menu.append(Some("Reset Background"), Some("win.reset_background"));
    appearance_menu.append_submenu(Some("Background"), &background_menu);

    let opacity_menu = gio::Menu::new();
    opacity_menu.append(Some("Full Opacity"), Some("win.full_opacity"));
    opacity_menu.append(
        Some("More Transparent"),
        Some("win.master_more_transparent"),
    );
    opacity_menu.append(
        Some("Less Transparent"),
        Some("win.master_less_transparent"),
    );
    opacity_menu.append(
        Some("Reset Master Opacity"),
        Some("win.reset_master_opacity"),
    );

    let glass_menu = gio::Menu::new();
    glass_menu.append(Some("More Glass"), Some("win.more_transparent"));
    glass_menu.append(Some("Less Glass"), Some("win.less_transparent"));
    glass_menu.append(Some("Reset Terminal Shade"), Some("win.reset_transparency"));
    opacity_menu.append_submenu(Some("Terminal Shade"), &glass_menu);
    appearance_menu.append_submenu(Some("Opacity"), &opacity_menu);

    let cursor_menu = gio::Menu::new();
    for preset in CURSOR_PRESETS {
        cursor_menu.append(
            Some(preset.label),
            Some(&format!("win.{}", preset.action_name)),
        );
    }
    cursor_menu.append(Some("Match Accent"), Some("win.cursor_match_accent"));
    cursor_menu.append(Some("Inherit Default"), Some("win.cursor_inherit"));
    appearance_menu.append_submenu(Some("Cursor"), &cursor_menu);

    let accent_menu = gio::Menu::new();
    accent_menu.append(
        Some("Toggle Random Accent"),
        Some("win.toggle_random_overlay"),
    );
    for (label, _, action_name) in ACCENT_PRESETS {
        accent_menu.append(Some(label), Some(&format!("win.{action_name}")));
        add_accent_action(
            window,
            action_name,
            label,
            pane.clone(),
            settings.clone(),
            config_path.clone(),
        );
    }
    accent_menu.append(Some("Stronger Accent"), Some("win.stronger_accent"));
    accent_menu.append(Some("Softer Accent"), Some("win.softer_accent"));
    accent_menu.append(Some("Clear Accent"), Some("win.clear_accent"));
    appearance_menu.append_submenu(Some("Accent"), &accent_menu);

    let theme_menu = gio::Menu::new();
    for theme in THEME_NAMES {
        let action_name = format!("theme_{}", theme.replace('-', "_"));
        theme_menu.append(
            Some(&choice_label(theme)),
            Some(&format!("win.{action_name}")),
        );
        add_theme_action(
            window,
            &action_name,
            theme,
            pane.clone(),
            settings.clone(),
            config_path.clone(),
        );
    }
    appearance_menu.append_submenu(Some("Theme"), &theme_menu);
    menu.append_section(Some("Appearance"), &appearance_menu);

    let copy = gio::SimpleAction::new("copy", None);
    let terminal_for_copy = terminal.clone();
    copy.connect_activate(move |_, _| {
        terminal_for_copy.copy_clipboard_format(vte::Format::Text);
    });
    window.add_action(&copy);

    let copy_html = gio::SimpleAction::new("copy-html", None);
    let terminal_for_copy_html = terminal.clone();
    copy_html.connect_activate(move |_, _| {
        terminal_for_copy_html.copy_clipboard_format(vte::Format::Html);
    });
    window.add_action(&copy_html);

    let paste = gio::SimpleAction::new("paste", None);
    let terminal_for_paste = terminal.clone();
    paste.connect_activate(move |_, _| {
        terminal_for_paste.paste_clipboard();
    });
    window.add_action(&paste);

    let select_all = gio::SimpleAction::new("select-all", None);
    let terminal_for_select_all = terminal.clone();
    select_all.connect_activate(move |_, _| {
        terminal_for_select_all.select_all();
    });
    window.add_action(&select_all);

    let new_window = gio::SimpleAction::new("new_window", None);
    let window_for_new_window = window.downgrade();
    let terminal_for_new_window = terminal.clone();
    let launch_for_new_window = launch.clone();
    let settings_for_new_window = settings.clone();
    let config_path_for_new_window = config_path.clone();
    new_window.connect_activate(move |_, _| {
        if let Some(window_for_new_window) = window_for_new_window.upgrade() {
            open_new_terminal_window(
                &window_for_new_window,
                &terminal_for_new_window,
                &launch_for_new_window,
                &settings_for_new_window,
                config_path_for_new_window.clone(),
            );
        }
    });
    window.add_action(&new_window);

    let diagnostics = gio::SimpleAction::new("diagnostics", None);
    let window_for_diagnostics = window.downgrade();
    let pane_for_diagnostics = pane.clone();
    let settings_for_diagnostics = settings.clone();
    let config_path_for_diagnostics = config_path.clone();
    diagnostics.connect_activate(move |_, _| {
        let active_windows = window_for_diagnostics
            .upgrade()
            .and_then(|window| window.application())
            .map(|app| app.windows().len())
            .unwrap_or(0);
        let settings = settings_for_diagnostics.borrow();
        pane_for_diagnostics.feed_text(&diagnostics_text(
            &settings,
            config_path_for_diagnostics.as_deref(),
            active_windows,
        ));
    });
    window.add_action(&diagnostics);

    let close_window = gio::SimpleAction::new("close_window", None);
    let window_for_close = window.downgrade();
    close_window.connect_activate(move |_, _| {
        if let Some(window_for_close) = window_for_close.upgrade() {
            window_for_close.close();
        }
    });
    window.add_action(&close_window);

    let customize = gio::SimpleAction::new("customize", None);
    let preferences_for_customize = preferences.downgrade();
    let terminal_for_customize = terminal.clone();
    customize.connect_activate(move |_, _| {
        if let Some(preferences) = preferences_for_customize.upgrade() {
            if !toggle_revealer(&preferences) {
                terminal_for_customize.grab_focus();
            }
        }
    });
    window.add_action(&customize);

    add_app_action(
        window,
        "reset_dark_default",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        reset_dark_default_settings,
    );

    add_app_action(
        window,
        "full_opacity",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            next.window.opacity = 1.0;
            next.terminal.background.terminal_opacity = 1.0;
            Ok(())
        },
    );

    add_app_action(
        window,
        "master_more_transparent",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            next.window.opacity = (next.window.opacity - 0.05).clamp(0.2, 1.0);
            Ok(())
        },
    );

    add_app_action(
        window,
        "master_less_transparent",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            next.window.opacity = (next.window.opacity + 0.05).clamp(0.2, 1.0);
            Ok(())
        },
    );

    add_app_action(
        window,
        "reset_master_opacity",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            next.window.opacity = 1.0;
            Ok(())
        },
    );

    let choose_background = gio::SimpleAction::new("choose_background", None);
    let window_for_background = window.downgrade();
    let pane_for_background = pane.clone();
    let settings_for_background = settings.clone();
    let config_path_for_background = config_path.clone();
    choose_background.connect_activate(move |_, _| {
        let Some(window_for_background) = window_for_background.upgrade() else {
            return;
        };
        open_background_image_dialog(&window_for_background, {
            let pane_for_background = pane_for_background.clone();
            let settings_for_background = settings_for_background.clone();
            let config_path_for_background = config_path_for_background.clone();
            move |path| {
                let result = apply_terminal_settings(
                    &pane_for_background,
                    &settings_for_background,
                    config_path_for_background.as_deref(),
                    move |next| {
                        next.terminal.background.image = Some(path);
                        if next.terminal.background.terminal_opacity
                            > DEFAULT_IMAGE_TERMINAL_OPACITY
                        {
                            next.terminal.background.terminal_opacity =
                                DEFAULT_IMAGE_TERMINAL_OPACITY;
                        }
                        if next.terminal.background.image_opacity <= 0.0 {
                            next.terminal.background.image_opacity = DEFAULT_IMAGE_OPACITY;
                        }
                        Ok(())
                    },
                );
                report_action_error(result);
            }
        });
    });
    window.add_action(&choose_background);

    let clear_background = gio::SimpleAction::new("clear_background", None);
    let pane_for_clear = pane.clone();
    let settings_for_clear = settings.clone();
    let config_path_for_clear = config_path.clone();
    clear_background.connect_activate(move |_, _| {
        let result = apply_terminal_settings(
            &pane_for_clear,
            &settings_for_clear,
            config_path_for_clear.as_deref(),
            |next| {
                next.terminal.background.image = None;
                Ok(())
            },
        );
        report_action_error(result);
    });
    window.add_action(&clear_background);

    add_terminal_action(
        window,
        "image_brighter",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            next.terminal.background.image_opacity =
                (next.terminal.background.image_opacity + 0.08).clamp(0.0, 1.0);
            Ok(())
        },
    );

    add_terminal_action(
        window,
        "image_dimmer",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            next.terminal.background.image_opacity =
                (next.terminal.background.image_opacity - 0.08).clamp(0.0, 1.0);
            Ok(())
        },
    );

    add_terminal_action(
        window,
        "reset_background",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            next.terminal.background.image = None;
            next.terminal.background.image_opacity = DEFAULT_IMAGE_OPACITY;
            next.terminal.background.terminal_opacity = DEFAULT_TERMINAL_OPACITY;
            next.terminal.background.overlay_color = None;
            next.terminal.background.overlay_opacity = 0.18;
            next.terminal.background.random_overlay = true;
            Ok(())
        },
    );

    let more_transparent = gio::SimpleAction::new("more_transparent", None);
    let pane_for_more_transparent = pane.clone();
    let settings_for_more_transparent = settings.clone();
    let config_path_for_more_transparent = config_path.clone();
    more_transparent.connect_activate(move |_, _| {
        let result = apply_terminal_settings(
            &pane_for_more_transparent,
            &settings_for_more_transparent,
            config_path_for_more_transparent.as_deref(),
            |next| {
                next.terminal.background.terminal_opacity =
                    (next.terminal.background.terminal_opacity - 0.08).clamp(0.0, 1.0);
                Ok(())
            },
        );
        report_action_error(result);
    });
    window.add_action(&more_transparent);

    let less_transparent = gio::SimpleAction::new("less_transparent", None);
    let pane_for_less_transparent = pane.clone();
    let settings_for_less_transparent = settings.clone();
    let config_path_for_less_transparent = config_path.clone();
    less_transparent.connect_activate(move |_, _| {
        let result = apply_terminal_settings(
            &pane_for_less_transparent,
            &settings_for_less_transparent,
            config_path_for_less_transparent.as_deref(),
            |next| {
                next.terminal.background.terminal_opacity =
                    (next.terminal.background.terminal_opacity + 0.08).clamp(0.0, 1.0);
                Ok(())
            },
        );
        report_action_error(result);
    });
    window.add_action(&less_transparent);

    let reset_transparency = gio::SimpleAction::new("reset_transparency", None);
    let pane_for_reset_transparency = pane.clone();
    let settings_for_reset_transparency = settings.clone();
    let config_path_for_reset_transparency = config_path.clone();
    reset_transparency.connect_activate(move |_, _| {
        let result = apply_terminal_settings(
            &pane_for_reset_transparency,
            &settings_for_reset_transparency,
            config_path_for_reset_transparency.as_deref(),
            |next| {
                next.terminal.background.terminal_opacity =
                    if next.terminal.background.image.is_some() {
                        DEFAULT_IMAGE_TERMINAL_OPACITY
                    } else {
                        DEFAULT_TERMINAL_OPACITY
                    };
                Ok(())
            },
        );
        report_action_error(result);
    });
    window.add_action(&reset_transparency);

    let toggle_random_overlay = gio::SimpleAction::new("toggle_random_overlay", None);
    let pane_for_random = pane.clone();
    let settings_for_random = settings.clone();
    let config_path_for_random = config_path.clone();
    toggle_random_overlay.connect_activate(move |_, _| {
        let result = apply_terminal_settings(
            &pane_for_random,
            &settings_for_random,
            config_path_for_random.as_deref(),
            |next| {
                next.terminal.background.random_overlay = !next.terminal.background.random_overlay;
                if next.terminal.background.random_overlay
                    && next.terminal.background.overlay_opacity <= 0.0
                {
                    next.terminal.background.overlay_opacity = 0.18;
                }
                Ok(())
            },
        );
        report_action_error(result);
    });
    window.add_action(&toggle_random_overlay);

    add_terminal_action(
        window,
        "stronger_accent",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            if next.terminal.background.overlay_color.is_none()
                && !next.terminal.background.random_overlay
            {
                next.terminal.background.overlay_color =
                    Some(TerminalTheme::parse_color("#7c3aed")?);
            }
            next.terminal.background.overlay_opacity =
                (next.terminal.background.overlay_opacity + 0.05).clamp(0.0, 0.65);
            Ok(())
        },
    );

    add_terminal_action(
        window,
        "softer_accent",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            next.terminal.background.overlay_opacity =
                (next.terminal.background.overlay_opacity - 0.05).clamp(0.0, 0.65);
            Ok(())
        },
    );

    add_terminal_action(
        window,
        "clear_accent",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            next.terminal.background.overlay_color = None;
            next.terminal.background.random_overlay = false;
            Ok(())
        },
    );

    for preset in CURSOR_PRESETS {
        let background = preset.background;
        let foreground = preset.foreground;
        add_terminal_action(
            window,
            preset.action_name,
            pane.clone(),
            settings.clone(),
            config_path.clone(),
            move |next| set_cursor_colors(next, background, foreground),
        );
    }

    add_terminal_action(
        window,
        "cursor_match_accent",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        match_cursor_to_overlay_settings,
    );

    add_terminal_action(
        window,
        "cursor_inherit",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| set_cursor_colors(next, "", ""),
    );

    terminal.set_context_menu_model(Some(&menu));
}

fn add_terminal_action(
    window: &gtk::ApplicationWindow,
    action_name: &str,
    pane: Rc<TerminalPane>,
    settings: Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
    update: impl Fn(&mut AppSettings) -> Result<(), String> + 'static,
) {
    let action = gio::SimpleAction::new(action_name, None);
    action.connect_activate(move |_, _| {
        let result = apply_terminal_settings(&pane, &settings, config_path.as_deref(), |next| {
            update(next)
        });
        report_action_error(result);
    });
    window.add_action(&action);
}

fn add_app_action(
    window: &gtk::ApplicationWindow,
    action_name: &str,
    pane: Rc<TerminalPane>,
    settings: Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
    update: impl Fn(&mut AppSettings) -> Result<(), String> + 'static,
) {
    let action = gio::SimpleAction::new(action_name, None);
    let window_for_action = window.downgrade();
    action.connect_activate(move |_, _| {
        let Some(window) = window_for_action.upgrade() else {
            return;
        };
        let result =
            apply_app_settings(&window, &pane, &settings, config_path.as_deref(), |next| {
                update(next)
            });
        report_action_error(result);
    });
    window.add_action(&action);
}

fn add_accent_action(
    window: &gtk::ApplicationWindow,
    action_name: &str,
    label: &str,
    pane: Rc<TerminalPane>,
    settings: Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
) {
    let accent = ACCENT_PRESETS
        .iter()
        .find(|(preset_label, _, _)| *preset_label == label)
        .map(|(_, color, _)| *color)
        .unwrap_or("#7c3aed");
    let action = gio::SimpleAction::new(action_name, None);
    action.connect_activate(move |_, _| {
        let result = apply_terminal_settings(&pane, &settings, config_path.as_deref(), |next| {
            next.terminal.background.overlay_color = Some(TerminalTheme::parse_color(accent)?);
            next.terminal.background.random_overlay = false;
            if next.terminal.background.overlay_opacity <= 0.0 {
                next.terminal.background.overlay_opacity = 0.18;
            }
            Ok(())
        });
        report_action_error(result);
    });
    window.add_action(&action);
}

fn add_theme_action(
    window: &gtk::ApplicationWindow,
    action_name: &str,
    theme_name: &str,
    pane: Rc<TerminalPane>,
    settings: Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
) {
    let action = gio::SimpleAction::new(action_name, None);
    let theme_name = theme_name.to_string();
    action.connect_activate(move |_, _| {
        let result = apply_terminal_settings(&pane, &settings, config_path.as_deref(), |next| {
            let canonical_name = TerminalTheme::canonical_name(&theme_name)?;
            next.terminal.theme_name = canonical_name.to_string();
            next.terminal.cursor_background = None;
            next.terminal.cursor_foreground = None;
            next.terminal.theme = TerminalTheme::named(canonical_name)?;
            Ok(())
        });
        report_action_error(result);
    });
    window.add_action(&action);
}

fn set_cursor_colors(
    next: &mut AppSettings,
    background: &str,
    foreground: &str,
) -> Result<(), String> {
    let mut theme = next.terminal.theme.clone();
    let (background, foreground) = apply_cursor_colors(&mut theme, background, foreground)?;
    next.terminal.cursor_background = background;
    next.terminal.cursor_foreground = foreground;
    next.terminal.cursor_match_overlay = false;
    next.terminal.theme = theme;
    Ok(())
}

fn match_cursor_to_overlay_settings(next: &mut AppSettings) -> Result<(), String> {
    next.terminal.cursor_match_overlay = true;
    Ok(())
}

fn apply_cursor_colors(
    theme: &mut TerminalTheme,
    background: &str,
    foreground: &str,
) -> Result<(Option<String>, Option<String>), String> {
    theme.apply_overrides(None, None, Some(background), Some(foreground), None)?;
    Ok((
        cursor_config_value(background),
        cursor_config_value(foreground),
    ))
}

fn cursor_config_value(value: &str) -> Option<String> {
    let value = value.trim();
    if value.is_empty() {
        None
    } else {
        Some(value.to_string())
    }
}

fn reset_dark_default_settings(next: &mut AppSettings) -> Result<(), String> {
    next.window.decorated = false;
    next.window.opacity = 1.0;
    next.terminal.theme_name = "xfce".to_string();
    next.terminal.cursor_background = None;
    next.terminal.cursor_foreground = None;
    next.terminal.cursor_match_overlay = false;
    next.terminal.theme = TerminalTheme::named("xfce")?;
    next.terminal.background.image = None;
    next.terminal.background.image_opacity = DEFAULT_IMAGE_OPACITY;
    next.terminal.background.terminal_opacity = DEFAULT_TERMINAL_OPACITY;
    next.terminal.background.overlay_color = None;
    next.terminal.background.overlay_opacity = 0.0;
    next.terminal.background.random_overlay = true;
    Ok(())
}

fn apply_terminal_settings(
    pane: &TerminalPane,
    settings: &Rc<RefCell<AppSettings>>,
    config_path: Option<&Path>,
    update: impl FnOnce(&mut AppSettings) -> Result<(), String>,
) -> Result<(), String> {
    let mut next = settings.borrow().clone();
    update(&mut next)?;

    if let Some(path) = config_path {
        next.persist(path)?;
    }
    pane.apply_config(&next.terminal);
    *settings.borrow_mut() = next;

    Ok(())
}

fn apply_app_settings(
    window: &gtk::ApplicationWindow,
    pane: &TerminalPane,
    settings: &Rc<RefCell<AppSettings>>,
    config_path: Option<&Path>,
    update: impl FnOnce(&mut AppSettings) -> Result<(), String>,
) -> Result<(), String> {
    let mut next = settings.borrow().clone();
    update(&mut next)?;

    if let Some(path) = config_path {
        next.persist(path)?;
    }
    apply_live_app_settings(window, pane, &next);
    *settings.borrow_mut() = next;

    Ok(())
}

fn report_action_error(result: Result<(), String>) {
    if let Err(message) = result {
        eprintln!("{message}");
    }
}

fn diagnostics_text(
    settings: &AppSettings,
    config_path: Option<&Path>,
    active_windows: usize,
) -> String {
    let image = settings
        .terminal
        .background
        .image
        .as_ref()
        .map(|path| path.display().to_string())
        .unwrap_or_else(|| "none".to_string());
    let overlay = settings
        .terminal
        .background
        .overlay_color
        .as_ref()
        .map(color_to_hex)
        .unwrap_or_else(|| "none".to_string());
    let config = config_path
        .map(|path| path.display().to_string())
        .unwrap_or_else(|| "session-only".to_string());
    let rss = process_rss_kib()
        .map(|rss| format!("{rss} KiB"))
        .unwrap_or_else(|| "unknown".to_string());

    format!(
        "\r\nLios diagnostics\r\n  version: {}\r\n  pid: {}\r\n  rss: {}\r\n  active_windows: {}\r\n  config: {}\r\n  renderer: {}\r\n  terminal_identity: TERM={}, COLORTERM={}, TERM_PROGRAM={}, TERM_PROGRAM_VERSION={}, VTE_VERSION={}\r\n  image_protocol: {}\r\n  window_opacity: {:.2}\r\n  theme: {}\r\n  scrollback_lines: {} / {} max\r\n  background_image: {}\r\n  image_opacity: {:.2}\r\n  terminal_opacity: {:.2}\r\n  effective_terminal_opacity: {:.2}\r\n  overlay: {} @ {:.2}\r\n  random_overlay: {}\r\n",
        env!("CARGO_PKG_VERSION"),
        std::process::id(),
        rss,
        active_windows,
        config,
        settings.window.renderer.as_config(),
        CHILD_TERM,
        CHILD_COLORTERM,
        CHILD_TERM_PROGRAM,
        env!("CARGO_PKG_VERSION"),
        CHILD_VTE_VERSION,
        CHILD_IMAGE_PROTOCOL,
        settings.window.opacity,
        settings.terminal.theme_name,
        settings.terminal.scrollback_lines,
        MAX_SCROLLBACK_LINES,
        image,
        settings.terminal.background.image_opacity,
        settings.terminal.background.terminal_opacity,
        effective_terminal_opacity(&settings.terminal.background),
        overlay,
        settings.terminal.background.overlay_opacity,
        settings.terminal.background.random_overlay,
    )
}

fn process_rss_kib() -> Option<u64> {
    let status = std::fs::read_to_string("/proc/self/status").ok()?;
    status.lines().find_map(|line| {
        let value = line.strip_prefix("VmRSS:")?.trim();
        value.split_whitespace().next()?.parse::<u64>().ok()
    })
}

fn install_keyboard_shortcuts(
    window: &gtk::ApplicationWindow,
    terminal: &vte::Terminal,
    preferences: &gtk::Revealer,
    launch: LaunchConfig,
    settings: Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
) {
    let zoom = Rc::new(Cell::new(1.0));
    let controller = gtk::EventControllerKey::new();
    controller.set_propagation_phase(gtk::PropagationPhase::Capture);

    let terminal_for_keys = terminal.clone();
    let zoom_for_keys = zoom.clone();
    let preferences_for_keys = preferences.downgrade();
    let window_for_keys = window.downgrade();
    let settings_for_keys = settings.clone();
    let config_path_for_keys = config_path.clone();
    let launch_for_keys = launch.clone();
    controller.connect_key_pressed(move |_, key, _, state| {
        if key == gdk::Key::Escape {
            if let Some(preferences_for_keys) = preferences_for_keys.upgrade() {
                if preferences_for_keys.reveals_child() {
                    preferences_for_keys.set_reveal_child(false);
                    terminal_for_keys.grab_focus();
                    return glib::Propagation::Stop;
                }
            }
        }

        let Some(character) = key.to_unicode().map(|ch| ch.to_ascii_lowercase()) else {
            return glib::Propagation::Proceed;
        };

        let has_control = state.contains(gdk::ModifierType::CONTROL_MASK);
        let has_shift = state.contains(gdk::ModifierType::SHIFT_MASK);

        if has_control && has_shift {
            match character {
                'c' => {
                    terminal_for_keys.copy_clipboard_format(vte::Format::Text);
                    return glib::Propagation::Stop;
                }
                'v' => {
                    terminal_for_keys.paste_clipboard();
                    return glib::Propagation::Stop;
                }
                'a' => {
                    terminal_for_keys.select_all();
                    return glib::Propagation::Stop;
                }
                'n' => {
                    if let Some(window_for_keys) = window_for_keys.upgrade() {
                        open_new_terminal_window(
                            &window_for_keys,
                            &terminal_for_keys,
                            &launch_for_keys,
                            &settings_for_keys,
                            config_path_for_keys.clone(),
                        );
                    }
                    return glib::Propagation::Stop;
                }
                'q' => {
                    if let Some(window_for_keys) = window_for_keys.upgrade() {
                        window_for_keys.close();
                    }
                    return glib::Propagation::Stop;
                }
                ',' | '<' => {
                    if let Some(preferences_for_keys) = preferences_for_keys.upgrade() {
                        if !toggle_revealer(&preferences_for_keys) {
                            terminal_for_keys.grab_focus();
                        }
                    }
                    return glib::Propagation::Stop;
                }
                _ => {}
            }
        }

        if has_control {
            match character {
                '+' | '=' => {
                    update_zoom(&terminal_for_keys, &zoom_for_keys, 1.1);
                    return glib::Propagation::Stop;
                }
                '-' => {
                    update_zoom(&terminal_for_keys, &zoom_for_keys, 1.0 / 1.1);
                    return glib::Propagation::Stop;
                }
                '0' => {
                    zoom_for_keys.set(1.0);
                    terminal_for_keys.set_font_scale(1.0);
                    return glib::Propagation::Stop;
                }
                _ => {}
            }
        }

        glib::Propagation::Proceed
    });

    window.add_controller(controller);
}

fn open_new_terminal_window(
    parent: &gtk::ApplicationWindow,
    terminal: &vte::Terminal,
    launch: &LaunchConfig,
    settings: &Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
) {
    let Some(app) = parent.application() else {
        return;
    };
    let next_launch = next_window_launch(launch, terminal_current_working_directory(terminal));
    let next_settings = next_window_settings(&settings.borrow());
    build_window(&app, next_launch, next_settings, config_path);
}

fn terminal_current_working_directory(terminal: &vte::Terminal) -> Option<PathBuf> {
    terminal
        .current_directory_uri()
        .and_then(|uri| path_from_file_uri(uri.as_str()))
        .filter(|path| path.is_dir())
}

fn path_from_file_uri(uri: &str) -> Option<PathBuf> {
    gio::File::for_uri(uri).path()
}

fn next_window_launch(launch: &LaunchConfig, current_directory: Option<PathBuf>) -> LaunchConfig {
    LaunchConfig {
        command: LaunchCommand::DefaultShell,
        working_directory: current_directory.or_else(|| launch.working_directory.clone()),
    }
}

fn next_window_settings(settings: &AppSettings) -> AppSettings {
    let mut next = settings.clone();
    next.window.decorated = false;
    next
}

fn update_zoom(terminal: &vte::Terminal, zoom: &Cell<f64>, multiplier: f64) {
    let next = (zoom.get() * multiplier).clamp(0.5, 2.5);
    zoom.set(next);
    terminal.set_font_scale(next);
}

fn keep_window_title_in_sync(
    window: &gtk::ApplicationWindow,
    terminal: &vte::Terminal,
    fallback_title: String,
) {
    let window_for_title = window.downgrade();
    terminal.connect_window_title_changed(move |terminal| {
        let Some(window_for_title) = window_for_title.upgrade() else {
            return;
        };
        let title = terminal_window_title(terminal)
            .filter(|title| !title.trim().is_empty())
            .unwrap_or_else(|| fallback_title.clone());
        window_for_title.set_title(Some(&title));
    });
}

#[allow(deprecated)]
fn terminal_window_title(terminal: &vte::Terminal) -> Option<String> {
    terminal.window_title().map(|title| title.to_string())
}

fn close_window_when_shell_exits(window: &gtk::ApplicationWindow, terminal: &vte::Terminal) {
    let window_for_exit = window.downgrade();
    terminal.connect_child_exited(move |_, _| {
        if let Some(window_for_exit) = window_for_exit.upgrade() {
            window_for_exit.close();
        }
    });
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn next_window_prefers_active_terminal_directory() {
        let launch = LaunchConfig {
            command: LaunchCommand::Shell("pwd".to_string()),
            working_directory: Some(PathBuf::from("/fallback")),
        };

        let next = next_window_launch(&launch, Some(PathBuf::from("/active")));

        assert!(matches!(next.command, LaunchCommand::DefaultShell));
        assert_eq!(next.working_directory, Some(PathBuf::from("/active")));
    }

    #[test]
    fn next_window_falls_back_to_launch_directory() {
        let launch = LaunchConfig {
            command: LaunchCommand::DefaultShell,
            working_directory: Some(PathBuf::from("/fallback")),
        };

        let next = next_window_launch(&launch, None);

        assert_eq!(next.working_directory, Some(PathBuf::from("/fallback")));
    }

    #[test]
    fn next_window_is_borderless() {
        let mut settings = AppSettings::default();
        settings.window.decorated = true;

        let next = next_window_settings(&settings);

        assert!(!next.window.decorated);
    }

    #[test]
    fn diagnostics_include_terminal_image_capability() {
        let diagnostics = diagnostics_text(&AppSettings::default(), None, 1);

        assert!(diagnostics.contains("terminal_identity: TERM=xterm-256color"));
        assert!(diagnostics.contains("COLORTERM=truecolor"));
        assert!(diagnostics.contains("TERM_PROGRAM=lios"));
        assert!(diagnostics.contains(concat!("TERM_PROGRAM_VERSION=", env!("CARGO_PKG_VERSION"))));
        assert!(diagnostics.contains("VTE_VERSION=7800"));
        assert!(diagnostics.contains("image_protocol: sixel"));
    }

    #[test]
    fn reset_dark_default_restores_borderless_plain_theme() {
        let mut settings = AppSettings::default();
        settings.window.decorated = true;
        settings.window.opacity = 0.5;
        settings.terminal.theme_name = "solarized-dark".to_string();
        settings.terminal.theme = TerminalTheme::named("solarized-dark").unwrap();
        settings.terminal.cursor_background = Some("#112233".to_string());
        settings.terminal.cursor_foreground = Some("#445566".to_string());
        settings.terminal.cursor_match_overlay = true;
        settings.terminal.background.image = Some(PathBuf::from("background.png"));
        settings.terminal.background.overlay_opacity = 0.3;
        settings.terminal.background.random_overlay = false;

        reset_dark_default_settings(&mut settings).unwrap();

        assert!(!settings.window.decorated);
        assert_eq!(settings.window.opacity, 1.0);
        assert_eq!(settings.terminal.theme_name, "xfce");
        assert!(settings.terminal.cursor_background.is_none());
        assert!(settings.terminal.cursor_foreground.is_none());
        assert!(!settings.terminal.cursor_match_overlay);
        assert!(settings.terminal.background.image.is_none());
        assert_eq!(settings.terminal.background.overlay_opacity, 0.0);
        assert!(settings.terminal.background.random_overlay);
    }

    #[test]
    fn cursor_preset_updates_theme_and_persisted_values() {
        let mut settings = AppSettings::default();
        settings.terminal.cursor_match_overlay = true;

        set_cursor_colors(
            &mut settings,
            XFCE_CURSOR_BACKGROUND,
            XFCE_CURSOR_FOREGROUND,
        )
        .unwrap();

        assert_eq!(
            settings.terminal.cursor_background.as_deref(),
            Some(XFCE_CURSOR_BACKGROUND)
        );
        assert_eq!(
            settings.terminal.cursor_foreground.as_deref(),
            Some(XFCE_CURSOR_FOREGROUND)
        );
        assert_eq!(
            color_to_hex(&settings.terminal.theme.cursor_background_color().unwrap()),
            XFCE_CURSOR_BACKGROUND
        );
        assert_eq!(
            color_to_hex(&settings.terminal.theme.cursor_foreground_color().unwrap()),
            XFCE_CURSOR_FOREGROUND
        );
        assert!(!settings.terminal.cursor_match_overlay);
    }

    #[test]
    fn cursor_inherit_clears_theme_and_persisted_values() {
        let mut settings = AppSettings::default();
        settings.terminal.theme = TerminalTheme::named("solarized-dark").unwrap();
        settings.terminal.cursor_background = Some("#112233".to_string());
        settings.terminal.cursor_foreground = Some("#445566".to_string());
        settings.terminal.cursor_match_overlay = true;

        set_cursor_colors(&mut settings, "", "").unwrap();

        assert!(settings.terminal.cursor_background.is_none());
        assert!(settings.terminal.cursor_foreground.is_none());
        assert!(settings.terminal.theme.cursor_background_color().is_none());
        assert!(settings.terminal.theme.cursor_foreground_color().is_none());
        assert!(!settings.terminal.cursor_match_overlay);
    }

    #[test]
    fn cursor_match_accent_enables_persistent_overlay_match_only() {
        let mut settings = AppSettings::default();
        settings.terminal.cursor_background = Some("#112233".to_string());
        settings.terminal.cursor_foreground = Some("#445566".to_string());
        settings.terminal.background.overlay_color = None;
        settings.terminal.background.overlay_opacity = 0.0;
        settings.terminal.background.random_overlay = true;

        match_cursor_to_overlay_settings(&mut settings).unwrap();

        assert!(settings.terminal.cursor_match_overlay);
        assert!(settings.terminal.background.overlay_color.is_none());
        assert_eq!(
            settings.terminal.cursor_background.as_deref(),
            Some("#112233")
        );
        assert_eq!(
            settings.terminal.cursor_foreground.as_deref(),
            Some("#445566")
        );
        assert!(settings.terminal.background.random_overlay);
        assert_eq!(settings.terminal.background.overlay_opacity, 0.0);
    }

    #[test]
    fn file_uri_decodes_to_path() {
        assert_eq!(
            path_from_file_uri("file:///tmp"),
            Some(PathBuf::from("/tmp"))
        );
    }

    #[test]
    fn file_uri_decodes_percent_encoding() {
        assert_eq!(
            path_from_file_uri("file:///tmp/My%20Docs"),
            Some(PathBuf::from("/tmp/My Docs"))
        );
    }

    #[test]
    fn non_file_uri_does_not_decode_to_path() {
        assert_eq!(path_from_file_uri("https://example.com/tmp"), None);
    }
}
