use gtk::prelude::*;
use gtk::{gdk, gio, glib};
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, config_path_for_write, expand_user_path, run_config_command,
};
use crate::desktop::{install_desktop_entry, uninstall_desktop_entry};
use crate::terminal::{
    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};

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"),
];

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 build_window(
    app: &gtk::Application,
    launch: LaunchConfig,
    settings: AppSettings,
    config_path: Option<PathBuf>,
) {
    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)
        .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::Box::new(gtk::Orientation::Vertical, 0);
    root.add_css_class("lios-content");
    root.append(&preferences);
    root.append(pane.widget());

    if state.borrow().window.decorated {
        let header = build_headerbar(&preferences);
        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_css() {
    let Some(display) = gdk::Display::default() else {
        return;
    };

    let provider = gtk::CssProvider::new();
    provider.load_from_string(
        "
        .lios-topbar {
            background: linear-gradient(90deg, #050816, #101626 55%, #151127);
            color: #e5e7eb;
            border-bottom: 1px solid rgba(148, 163, 184, 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: #f8fafc;
        }

        .lios-prefs-button {
            border-radius: 999px;
            padding: 5px 16px;
            background: linear-gradient(135deg, #1d2540, #31204a);
            color: #e5e7eb;
            border: 1px solid rgba(167, 139, 250, 0.32);
            box-shadow: 0 6px 18px rgba(0, 0, 0, 0.24);
        }

        .lios-drawer {
            background: linear-gradient(135deg, #080b17, #101827 58%, #171025);
            color: #e5e7eb;
            border-bottom: 1px solid rgba(148, 163, 184, 0.18);
            padding: 18px;
        }

        .lios-drawer-title {
            font-size: 18px;
            font-weight: 800;
            letter-spacing: 0.04em;
        }

        .lios-muted {
            color: #9ca3af;
            font-size: 12px;
        }

        .lios-card {
            background: rgba(15, 23, 42, 0.78);
            border: 1px solid rgba(148, 163, 184, 0.14);
            border-radius: 18px;
            padding: 14px;
            box-shadow: 0 18px 44px rgba(0, 0, 0, 0.22);
        }

        .lios-row-label {
            color: #cbd5e1;
            font-weight: 600;
        }

        .lios-choice {
            border-radius: 999px;
            padding: 6px 12px;
            background: rgba(15, 23, 42, 0.82);
            color: #cbd5e1;
            border: 1px solid rgba(148, 163, 184, 0.16);
            box-shadow: none;
        }

        .lios-choice:hover {
            background: rgba(30, 41, 59, 0.92);
            color: #f8fafc;
            border-color: rgba(167, 139, 250, 0.35);
        }

        .lios-choice.lios-selected {
            background: linear-gradient(135deg, #7c3aed, #0ea5e9);
            color: #ffffff;
            border-color: rgba(255, 255, 255, 0.36);
            box-shadow: 0 8px 24px rgba(14, 165, 233, 0.20);
        }

        .lios-field {
            border-radius: 12px;
            background: rgba(2, 6, 23, 0.72);
            color: #e5e7eb;
            border: 1px solid rgba(148, 163, 184, 0.16);
            padding: 7px 10px;
        }

        .lios-collapse {
            border-radius: 999px;
            padding: 5px 12px;
            color: #cbd5e1;
            background: rgba(15, 23, 42, 0.75);
            border: 1px solid rgba(148, 163, 184, 0.14);
        }

        .lios-apply {
            border-radius: 999px;
            padding: 7px 16px;
            background: linear-gradient(135deg, #0ea5e9, #7c3aed);
            color: #ffffff;
            border: 1px solid rgba(255, 255, 255, 0.24);
            font-weight: 700;
        }
        ",
    );

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

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

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

    let preferences_button = gtk::Button::with_label("Customize");
    preferences_button.add_css_class("lios-prefs-button");
    let preferences_for_button = preferences.downgrade();
    preferences_button.connect_clicked(move |_| {
        if let Some(preferences) = preferences_for_button.upgrade() {
            toggle_revealer(&preferences);
        }
    });
    header.pack_end(&preferences_button);

    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_wrap(true);
    status.set_xalign(0.0);
    status.set_hexpand(true);

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

    let header = gtk::Box::new(gtk::Orientation::Horizontal, 12);
    let title_block = gtk::Box::new(gtk::Orientation::Vertical, 2);
    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(
        "Live theme changes. Renderer changes apply on next launch.",
    ));
    subtitle.add_css_class("lios-muted");
    subtitle.set_xalign(0.0);
    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();
    collapse.connect_clicked(move |_| {
        if let Some(revealer) = revealer_for_collapse.upgrade() {
            revealer.set_reveal_child(false);
        }
    });
    header.append(&title_block);
    header.append(&collapse);
    drawer.append(&header);

    let grid = gtk::Grid::new();
    grid.set_column_spacing(12);
    grid.set_row_spacing(12);
    grid.set_column_homogeneous(true);

    let display_card = preference_card("Display", "Renderer, font, and terminal colors");
    let renderer_choice = choice_grid(
        &RendererPreference::NAMES,
        snapshot.window.renderer.as_config(),
        4,
    );
    display_card.append(&preference_column("Renderer", &renderer_choice.widget));

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

    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"));
    display_card.append(&preference_row("Font", &font_entry));

    let decorated = bool_choice(snapshot.window.decorated);
    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());
    }
    let image_opacity = opacity_spin(snapshot.terminal.background.image_opacity);
    let terminal_opacity = opacity_spin(snapshot.terminal.background.terminal_opacity);

    let image_picker = gtk::Box::new(gtk::Orientation::Horizontal, 8);
    image_entry.set_hexpand(true);
    let choose_image = gtk::Button::with_label("Choose Image");
    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();
    choose_image.connect_clicked(move |_| {
        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();
            move |path| {
                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);
                }
                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);
                        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(()) => status_for_picker.set_text("Background image applied and saved."),
                    Err(message) => status_for_picker.set_text(&message),
                }
            }
        });
    });
    image_picker.append(&image_entry);
    image_picker.append(&choose_image);
    background_card.append(&preference_row("Image", &image_picker));

    let opacity_card = preference_card("Opacity", "Master first, then nested layers");
    let master_opacity = opacity_spin(snapshot.window.opacity);
    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_spin(snapshot.terminal.background.overlay_opacity);
    opacity_card.append(&preference_row("Accent tint", &overlay_opacity));

    let overlay_card = preference_card("Accent", "Tint color and random accent");
    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));
    }
    overlay_card.append(&preference_row("Overlay color", &overlay_entry));

    let random_overlay = bool_choice(snapshot.terminal.background.random_overlay);
    overlay_card.append(&preference_column("Random overlay", &random_overlay.widget));

    grid.attach(&display_card, 0, 0, 1, 1);
    grid.attach(&background_card, 1, 0, 1, 1);
    grid.attach(&opacity_card, 0, 1, 1, 1);
    grid.attach(&overlay_card, 1, 1, 1, 1);
    drawer.append(&grid);

    let footer = gtk::Box::new(gtk::Orientation::Horizontal, 12);

    let apply = gtk::Button::with_label("Apply and Save");
    apply.add_css_class("lios-apply");
    let parent_for_apply = parent.downgrade();
    let revealer_for_apply = revealer.downgrade();
    let status_for_apply = status.clone();
    apply.connect_clicked(move |_| {
        let mut next = settings.borrow().clone();
        let theme_name = theme_choice.value.borrow().clone();
        let renderer_name = renderer_choice.value.borrow().clone();

        let theme = match TerminalTheme::named(&theme_name) {
            Ok(theme) => theme,
            Err(message) => {
                status_for_apply.set_text(&message);
                return;
            }
        };
        let renderer = match RendererPreference::parse(&renderer_name) {
            Ok(renderer) => renderer,
            Err(message) => {
                status_for_apply.set_text(&message);
                return;
            }
        };

        let overlay_text = overlay_entry.text().trim().to_string();
        let overlay_color = if overlay_text.is_empty() {
            None
        } else {
            match TerminalTheme::parse_color(&overlay_text) {
                Ok(color) => Some(color),
                Err(message) => {
                    status_for_apply.set_text(&message);
                    return;
                }
            }
        };

        let image_path = match image_path_from_text(image_entry.text().trim()) {
            Ok(path) => path,
            Err(message) => {
                status_for_apply.set_text(&message);
                return;
            }
        };
        next.window.renderer = renderer;
        next.window.opacity = master_opacity.value();
        next.terminal.theme_name = theme_name;
        next.terminal.theme = theme;
        next.terminal.font = font_entry.text().to_string();
        next.terminal.background.image = image_path;
        next.terminal.background.image_opacity = image_opacity.value();
        next.terminal.background.terminal_opacity = terminal_opacity.value();
        next.terminal.background.overlay_color = overlay_color;
        next.terminal.background.overlay_opacity = overlay_opacity.value();
        next.terminal.background.random_overlay = random_overlay.value.get();
        next.window.decorated = decorated.value.get();

        pane.apply_config(&next.terminal);
        if let Some(parent_for_apply) = parent_for_apply.upgrade() {
            parent_for_apply.set_decorated(next.window.decorated);
            apply_window_opacity(&parent_for_apply, next.window.opacity);
            if next.window.decorated {
                if let Some(revealer_for_apply) = revealer_for_apply.upgrade() {
                    let header = build_headerbar(&revealer_for_apply);
                    parent_for_apply.set_titlebar(Some(&header));
                }
            } else {
                parent_for_apply.set_titlebar(None::<&gtk::Widget>);
            }
        }

        if let Some(path) = &config_path {
            if let Err(message) = next.persist(path) {
                status_for_apply.set_text(&message);
                return;
            }
            if renderer != settings.borrow().window.renderer {
                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.",
            );
        }

        *settings.borrow_mut() = next;
    });

    footer.append(&status);
    footer.append(&apply);
    drawer.append(&footer);

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

fn toggle_revealer(revealer: &gtk::Revealer) {
    revealer.set_reveal_child(!revealer.reveals_child());
}

struct ChoiceGroup {
    widget: gtk::Grid,
    value: Rc<RefCell<String>>,
}

struct BoolChoice {
    widget: gtk::Grid,
    value: Rc<Cell<bool>>,
}

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 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,
    }
}

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 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,
    }
}

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(),
        "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, 10);
    card.add_css_class("lios-card");
    card.set_hexpand(true);
    card.set_vexpand(false);

    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, 6);
    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, 12);
    let row_label = gtk::Label::new(Some(label));
    row_label.add_css_class("lios-row-label");
    row_label.set_width_chars(15);
    row_label.set_xalign(0.0);
    control.as_ref().set_hexpand(true);
    row.append(&row_label);
    row.append(control);
    row
}

fn opacity_spin(value: f64) -> gtk::SpinButton {
    let spin = gtk::SpinButton::with_range(0.0, 1.0, 0.05);
    spin.add_css_class("lios-field");
    spin.set_digits(2);
    spin.set_value(value.clamp(0.0, 1.0));
    spin
}

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 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 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,
                &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();
    customize.connect_activate(move |_, _| {
        if let Some(preferences) = preferences_for_customize.upgrade() {
            toggle_revealer(&preferences);
        }
    });
    window.add_action(&customize);

    add_app_action(
        window,
        "reset_dark_default",
        pane.clone(),
        settings.clone(),
        config_path.clone(),
        |next| {
            next.window.opacity = 1.0;
            next.terminal.theme_name = "xfce".to_string();
            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 = false;
            Ok(())
        },
    );

    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 = false;
            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(())
        },
    );

    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.theme = TerminalTheme::named(canonical_name)?;
            Ok(())
        });
        report_action_error(result);
    });
    window.add_action(&action);
}

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)?;

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

    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)?;

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

    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  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(),
        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| {
        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,
                            &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() {
                        toggle_revealer(&preferences_for_keys);
                    }
                    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,
    launch: &LaunchConfig,
    settings: &Rc<RefCell<AppSettings>>,
    config_path: Option<PathBuf>,
) {
    let Some(app) = parent.application() else {
        return;
    };
    let next_launch = LaunchConfig {
        command: LaunchCommand::DefaultShell,
        working_directory: launch.working_directory.clone(),
    };
    let next_settings = settings.borrow().clone();
    build_window(&app, next_launch, next_settings, config_path);
}

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();
        }
    });
}
