sigmadock-ui 0.1.2

Native GPUI workspace for supervising local coding agents
use crate::{
    Workspace,
    preferences::{Appearance, Cursor, parse_color},
};
use gpui::{Context, SharedString, Window, div, prelude::*, px, rgb};

pub struct SettingsTooltip;
impl gpui::Render for SettingsTooltip {
    fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
        let theme = crate::theme::Theme::for_appearance(window.appearance());
        div()
            .p_2()
            .rounded_md()
            .bg(rgb(theme.button))
            .text_color(rgb(theme.text))
            .child("Terminal appearance settings · Cmd/Ctrl+,")
    }
}
impl Workspace {
    pub(crate) fn toggle_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        self.settings_open = !self.settings_open;
        self.settings_editor = None;
        if self.settings_open {
            self.settings_focus.focus(window);
        } else if let Some(terminal) = &self.terminal {
            terminal.read(cx).focus_handle().focus(window);
        }
        cx.notify();
    }
    pub(crate) fn refresh_terminal_appearance(&mut self, cx: &mut Context<Self>) {
        if let Some(terminal) = &self.terminal {
            terminal.update(cx, |terminal, cx| {
                terminal.update_config(
                    self.theme
                        .terminal(&self.preferences.appearance)
                        .apply(terminal.config().clone()),
                    cx,
                );
            });
        }
    }
    fn apply_appearance(&mut self, cx: &mut Context<Self>) {
        if let Err(error) = self.preferences.appearance.validate() {
            self.settings_error = Some(error.to_string());
            return;
        }
        self.refresh_terminal_appearance(cx);
        self.settings_error = self
            .preferences
            .save(&self.preferences_path)
            .err()
            .map(|error| error.to_string());
        cx.notify();
    }
    fn edit_appearance(
        &mut self,
        event: &gpui::KeyDownEvent,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if event.keystroke.key == "escape" {
            self.toggle_settings(window, cx);
            cx.stop_propagation();
            return;
        }
        let Some((index, text)) = &mut self.settings_editor else {
            return;
        };
        let modifiers = event.keystroke.modifiers;
        if event.keystroke.key == "backspace" {
            text.pop();
        } else if event.keystroke.key == "a" && (modifiers.platform || modifiers.control) {
            text.clear();
        } else if event.keystroke.key == "v" && (modifiers.platform || modifiers.control) {
            if let Some(value) = cx.read_from_clipboard().and_then(|item| item.text()) {
                text.push_str(&value.replace(['\r', '\n'], ""));
            }
        } else if let Some(value) = &event.keystroke.key_char
            && !modifiers.control
            && !modifiers.platform
        {
            text.push_str(value);
        }
        let index = *index;
        let text = text.clone();
        if index == 0 && !text.trim().is_empty() && text.len() <= 128 {
            self.preferences.appearance.font = text;
            self.apply_appearance(cx);
        } else if index != 0 {
            if let Some(color) = parse_color(&text) {
                self.preferences.appearance.follow_system = false;
                self.preferences.appearance.set_color(index - 1, color);
                self.apply_appearance(cx);
            } else {
                self.settings_error = Some(
                    "Use six hex digits, for example #1e1e1e. The last valid color remains active."
                        .into(),
                );
            }
        }
        cx.stop_propagation();
        cx.notify();
    }
    fn appearance_field(
        &self,
        index: usize,
        label: String,
        value: String,
        cx: &mut Context<Self>,
    ) -> gpui::AnyElement {
        let editing = self.settings_editor.as_ref().filter(|(i, _)| *i == index);
        let displayed = editing
            .map(|(_, text)| text.clone())
            .unwrap_or(value.clone());
        div()
            .flex()
            .gap_2()
            .items_center()
            .child(div().w(px(130.)).text_sm().child(label))
            .child(
                div()
                    .id(SharedString::from(format!("appearance-{index}")))
                    .flex_1()
                    .p_2()
                    .rounded_md()
                    .bg(rgb(if editing.is_some() {
                        self.theme.selection
                    } else {
                        self.theme.card
                    }))
                    .cursor_text()
                    .child(displayed)
                    .on_click(cx.listener(move |this, _, window, cx| {
                        this.settings_editor = Some((index, value.clone()));
                        this.settings_focus.focus(window);
                        cx.notify();
                    })),
            )
            .into_any_element()
    }
    pub(crate) fn settings_panel(&self, cx: &mut Context<Self>) -> gpui::AnyElement {
        let effective = self.theme.terminal(&self.preferences.appearance);
        let appearance = &effective;
        let mut panel = div()
            .id("terminal-settings-panel")
            .absolute()
            .right(px(16.))
            .bottom(px(60.))
            .w(px(460.))
            .max_h(px(640.))
            .overflow_y_scroll()
            .track_focus(&self.settings_focus)
            .focus(|style| style.border_color(rgb(self.theme.focus)))
            .on_key_down(cx.listener(Self::edit_appearance))
            .flex()
            .flex_col()
            .gap_2()
            .p_4()
            .rounded_lg()
            .border_1()
            .border_color(rgb(self.theme.border))
            .bg(rgb(self.theme.panel))
            .child(
                div()
                    .flex()
                    .justify_between()
                    .child(div().text_lg().child("Terminal Appearance"))
                    .child(
                        div()
                            .id("close-settings")
                            .cursor_pointer()
                            .child("Close ×")
                            .on_click(
                                cx.listener(|this, _, window, cx| this.toggle_settings(window, cx)),
                            ),
                    ),
            )
            .child(
                div()
                    .text_sm()
                    .text_color(rgb(self.theme.muted))
                    .child("Changes apply live. Click a value; Cmd/Ctrl+A replaces it."),
            )
            .child(self.appearance_field(0, "Font family".into(), appearance.font.clone(), cx));
        let mut fonts = div().flex().gap_2();
        for font in ["monospace", "Menlo", "JetBrains Mono"] {
            fonts = fonts.child(
                div()
                    .id(SharedString::from(format!("font-{font}")))
                    .p_1()
                    .bg(rgb(self.theme.button))
                    .hover(|style| style.bg(rgb(self.theme.selection)))
                    .cursor_pointer()
                    .text_sm()
                    .child(font)
                    .on_click(cx.listener(move |this, _, _, cx| {
                        this.settings_editor = None;
                        this.preferences.appearance.font = font.into();
                        this.apply_appearance(cx);
                    })),
            );
        }
        panel = panel.child(fonts);
        for (index, label, value) in [
            (0, "Font size", format!("{} px", appearance.size)),
            (1, "Line spacing", format!("{:.1}×", appearance.line_height)),
            (2, "Padding", format!("{} px", appearance.padding)),
        ] {
            let mut row = div()
                .flex()
                .gap_2()
                .items_center()
                .child(div().w(px(130.)).child(label))
                .child(div().flex_1().child(value));
            for (direction, label) in [(-1., "−"), (1., "+")] {
                row = row.child(
                    div()
                        .id(SharedString::from(format!("step-{index}-{direction}")))
                        .p_2()
                        .bg(rgb(self.theme.button))
                        .hover(|style| style.bg(rgb(self.theme.selection)))
                        .cursor_pointer()
                        .child(label)
                        .on_click(cx.listener(move |this, _, _, cx| {
                            let a = &mut this.preferences.appearance;
                            match index {
                                0 => a.size = (a.size + direction).clamp(8., 40.),
                                1 => {
                                    a.line_height = ((a.line_height * 10.).round() + direction)
                                        .clamp(10., 20.)
                                        / 10.
                                }
                                _ => a.padding = (a.padding + direction * 2.).clamp(0., 32.),
                            }
                            this.apply_appearance(cx);
                        })),
                );
            }
            panel = panel.child(row);
        }
        let mut themes = div().flex().gap_2().child(div().w(px(130.)).child("Theme"));
        for (light, label) in [(false, "Dark"), (true, "Light")] {
            themes = themes.child(
                div()
                    .id(SharedString::from(format!("theme-{label}")))
                    .p_2()
                    .bg(rgb(self.theme.button))
                    .hover(|style| style.bg(rgb(self.theme.selection)))
                    .cursor_pointer()
                    .child(label)
                    .on_click(cx.listener(move |this, _, _, cx| {
                        let preset = if light {
                            Appearance::light()
                        } else {
                            Appearance::default()
                        };
                        let a = &mut this.preferences.appearance;
                        a.follow_system = false;
                        a.background = preset.background;
                        a.foreground = preset.foreground;
                        a.ansi = preset.ansi;
                        this.settings_editor = None;
                        this.apply_appearance(cx);
                    })),
            );
        }
        panel = panel
            .child(
                div()
                    .id("terminal-system-theme")
                    .p_2()
                    .bg(rgb(self.theme.button))
                    .cursor_pointer()
                    .child(if appearance.follow_system {
                        "Terminal colors: follow system"
                    } else {
                        "Terminal colors: custom"
                    })
                    .on_click(cx.listener(|this, _, _, cx| {
                        let effective = this.theme.terminal(&this.preferences.appearance);
                        let a = &mut this.preferences.appearance;
                        if a.follow_system {
                            a.background = effective.background;
                            a.foreground = effective.foreground;
                            a.ansi = effective.ansi;
                        }
                        a.follow_system = !a.follow_system;
                        this.settings_editor = None;
                        this.apply_appearance(cx);
                    })),
            )
            .child(themes)
            .child(
                div()
                    .flex()
                    .gap_2()
                    .child(
                        div()
                            .id("cursor-shape")
                            .flex_1()
                            .p_2()
                            .bg(rgb(self.theme.button))
                            .hover(|style| style.bg(rgb(self.theme.selection)))
                            .cursor_pointer()
                            .child(format!("Cursor: {:?} ↻", appearance.cursor))
                            .on_click(cx.listener(|this, _, _, cx| {
                                this.preferences.appearance.cursor =
                                    match this.preferences.appearance.cursor {
                                        Cursor::Block => Cursor::Underline,
                                        Cursor::Underline => Cursor::Beam,
                                        Cursor::Beam => Cursor::Block,
                                    };
                                this.apply_appearance(cx);
                            })),
                    )
                    .child(
                        div()
                            .id("cursor-blink")
                            .p_2()
                            .bg(rgb(self.theme.button))
                            .hover(|style| style.bg(rgb(self.theme.selection)))
                            .cursor_pointer()
                            .child(if appearance.blink {
                                "Blink: on"
                            } else {
                                "Blink: off"
                            })
                            .on_click(cx.listener(|this, _, _, cx| {
                                this.preferences.appearance.blink =
                                    !this.preferences.appearance.blink;
                                this.apply_appearance(cx);
                            })),
                    ),
            );
        for index in 0..18 {
            let label = match index {
                0 => "Background".into(),
                1 => "Foreground".into(),
                _ => format!("ANSI {}", index - 2),
            };
            panel = panel.child(self.appearance_field(
                index + 1,
                label,
                format!("#{:06x}", appearance.color(index)),
                cx,
            ));
        }
        if let Some(error) = &self.settings_error {
            panel = panel.child(
                div()
                    .text_sm()
                    .text_color(rgb(self.theme.error))
                    .child(error.clone()),
            );
        }
        panel
            .child(
                div()
                    .id("restore-appearance")
                    .p_2()
                    .bg(rgb(self.theme.accent))
                    .text_color(rgb(self.theme.base))
                    .cursor_pointer()
                    .child("Restore appearance defaults")
                    .on_click(cx.listener(|this, _, _, cx| {
                        this.settings_editor = None;
                        this.preferences.appearance = Appearance::default();
                        this.apply_appearance(cx);
                    })),
            )
            .child(self.update_controls(cx))
            .into_any_element()
    }
}