nimue-term 0.1.6

Terminal emulator with multiplexer capabilities designed for maximum productivity, git worktrees and agentic engineering.
use freya::{
    prelude::*,
    radio::use_radio,
};
use freya_terminal::prelude::*;
use keyboard_types::Modifiers;

use crate::state::{
    AppChannel,
    AppState,
};

#[derive(PartialEq)]
pub struct TerminalPane {
    pub handle: TerminalHandle,
    pub on_reopen: Callback<(), ()>,
    pub is_focused: bool,
    pub focus_id: AccessibilityId,
    pub instance_id: usize,
    pub terminal_index: usize,
}

impl Component for TerminalPane {
    fn render(&self) -> impl IntoElement {
        let focus = Focus::new_for_id(self.focus_id);
        let instance_id = self.instance_id;

        let mut radio = use_radio::<AppState, AppChannel>(AppChannel::Instance(instance_id));
        let mut exited = use_state(|| false);

        let on_reopen = self.on_reopen.clone();
        let handle = self.handle.clone();
        let handle_for_future = handle.clone();
        let handle_for_terminal = handle.clone();
        let handle_for_mouse = handle.clone();

        use_future(move || {
            let handle = handle_for_future.clone();
            async move {
                handle.closed().await;
                *exited.write() = true;
            }
        });

        let mut dimensions = use_state(|| (0.0, 0.0));
        let font_size = radio.read().font_size;

        let on_secondary_press = move |_: Event<PressEventData>| {
            let on_reopen = on_reopen.clone();
            ContextMenu::open(
                Menu::new().child(
                    MenuButton::new()
                        .on_press(move |_| {
                            ContextMenu::close();
                            on_reopen.call(());
                        })
                        .child("Reopen"),
                ),
            );
        };

        rect()
            .expanded()
            .padding(6.)
            .background(if self.is_focused {
                (30, 30, 30)
            } else {
                (10, 10, 10)
            })
            .a11y_id(focus.a11y_id())
            .a11y_auto_focus(self.is_focused)
            .on_mouse_down(move |_| {
                focus.request_focus();
                if let Some(instance) = radio
                    .write()
                    .instances
                    .iter_mut()
                    .find(|i| i.id == instance_id)
                {
                    instance.focused_terminal_id = Some(focus.a11y_id());
                }
            })
            .on_secondary_press(on_secondary_press)
            .on_key_down(move |e: Event<KeyboardEventData>| {
                let mods = e.modifiers;
                let ctrl_shift = mods.contains(Modifiers::CONTROL | Modifiers::SHIFT);
                let ctrl = mods.contains(Modifiers::CONTROL);

                match &e.key {
                    Key::Character(ch) if ctrl_shift && ch.eq_ignore_ascii_case("c") => {
                        if let Some(text) = handle.get_selected_text() {
                            let _ = Clipboard::set(text);
                        }
                    }
                    Key::Character(ch) if ctrl_shift && ch.eq_ignore_ascii_case("v") => {
                        if let Ok(text) = Clipboard::get() {
                            let _ = handle.write(text.as_bytes());
                        }
                    }
                    Key::Character(ch) if ctrl && (ch == "+" || ch == "=") => {
                        radio.write_channel(AppChannel::Instances).font_size =
                            (font_size + 1.0).min(48.0);
                    }
                    Key::Character(ch) if ctrl && ch == "-" => {
                        radio.write_channel(AppChannel::Instances).font_size =
                            (font_size - 1.0).max(8.0);
                    }
                    Key::Character(ch) if ctrl && ch.len() == 1 => {
                        let _ = handle.write(&[ch.as_bytes()[0] & 0x1f]);
                    }
                    Key::Named(NamedKey::ArrowUp)
                    | Key::Named(NamedKey::ArrowRight)
                    | Key::Named(NamedKey::ArrowDown)
                    | Key::Named(NamedKey::ArrowUp)
                        if !mods.is_empty() => {}
                    Key::Named(NamedKey::Enter) => {
                        let _ = handle.write(b"\r");
                    }
                    Key::Named(NamedKey::Backspace) => {
                        let _ = handle.write(&[0x7f]);
                    }
                    Key::Named(NamedKey::Delete) => {
                        let _ = handle.write(b"\x1b[3~");
                    }
                    Key::Named(NamedKey::Tab) if !ctrl => {
                        let _ = handle.write(b"\t");
                        e.stop_propagation();
                        e.prevent_default();
                    }
                    Key::Named(NamedKey::Escape) => {
                        let _ = handle.write(&[0x1b]);
                    }
                    Key::Named(NamedKey::ArrowUp) => {
                        let _ = handle.write(b"\x1b[A");
                    }
                    Key::Named(NamedKey::ArrowDown) => {
                        let _ = handle.write(b"\x1b[B");
                    }
                    Key::Named(NamedKey::ArrowLeft) => {
                        let _ = handle.write(b"\x1b[D");
                    }
                    Key::Named(NamedKey::ArrowRight) => {
                        let _ = handle.write(b"\x1b[C");
                    }
                    _ => {
                        if let Some(ch) = e.try_as_str() {
                            let _ = handle.write(ch.as_bytes());
                        }
                    }
                }
            })
            .child(if *exited.read() {
                "Terminal exited".into_element()
            } else {
                Terminal::new(handle_for_terminal)
                    .font_size(font_size)
                    .on_measured(move |(char_width, line_height)| {
                        dimensions.set((char_width, line_height));
                    })
                    .on_mouse_down({
                        let handle = handle_for_mouse.clone();
                        move |e: Event<MouseEventData>| {
                            focus.request_focus();
                            let (char_width, line_height) = dimensions();
                            let col = (e.element_location.x / char_width as f64).floor() as usize;
                            let row = (e.element_location.y / line_height as f64).floor() as usize;
                            handle.start_selection(row, col);
                        }
                    })
                    .on_mouse_move({
                        let handle = handle_for_mouse.clone();
                        move |e: Event<MouseEventData>| {
                            let (char_width, line_height) = dimensions();
                            let col = (e.element_location.x / char_width as f64).floor() as usize;
                            let row = (e.element_location.y / line_height as f64).floor() as usize;
                            handle.update_selection(row, col);
                        }
                    })
                    .on_mouse_up({
                        let handle = handle_for_mouse.clone();
                        move |_| {
                            handle.end_selection();
                        }
                    })
                    .on_wheel({
                        let handle = handle_for_mouse.clone();
                        move |e: Event<WheelEventData>| {
                            let delta = if e.delta_y < 0.0 { -3 } else { 3 };
                            handle.scroll(delta);
                        }
                    })
                    .into_element()
            })
    }
}