nimue-term 0.1.6

Terminal emulator with multiplexer capabilities designed for maximum productivity, git worktrees and agentic engineering.
#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

use freya::{
    prelude::*,
    radio::{
        use_init_radio_station,
        use_radio,
    },
    router::*,
};

mod cli;
mod components;
mod config;
mod state;

use components::{
    InstanceSidebar,
    TerminalGrid,
};
use config::Config;
use keyboard_types::Modifiers;
use portable_pty::CommandBuilder;
use state::{
    AppChannel,
    AppState,
};

use crate::state::TerminalState;

fn main() {
    let cli = cli::parse();
    let _ = cli::CLI_ARGS.set(cli);

    let config = Config::load();
    let _ = state::CONFIG.set(config);

    launch(
        LaunchConfig::new().with_window(
            WindowConfig::new(app)
                .with_title("Nimue")
                .with_size(1600., 900.)
                .with_transparency(true)
                .with_background(Color::TRANSPARENT),
        ),
    )
}

fn app() -> impl IntoElement {
    use_init_root_theme(|| {
        let mut theme = DARK_THEME;
        theme.colors.surface_tertiary = Color::from_argb(180, 25, 25, 25);
        theme
    });
    Router::<Route>::new(RouterConfig::default)
}

#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
    #[layout(AppLayout)]
        #[route("/")]
        Home,
        #[route("/instance/:id")]
        Instance { id: usize },
}

#[derive(PartialEq)]
struct AppLayout;

impl Component for AppLayout {
    fn render(&self) -> impl IntoElement {
        let radio = use_init_radio_station::<AppState, AppChannel>(AppState::new);

        let on_global_key_down = move |e: Event<KeyboardEventData>| {
            if e.key == Key::Named(NamedKey::Tab) && e.modifiers.contains(Modifiers::CONTROL) {
                let instances = &radio.read().instances;
                if instances.len() < 2 {
                    return;
                }

                let router = RouterContext::get();
                let current_pos = match router.current::<Route>() {
                    Route::Instance { id } => instances.iter().position(|i| i.id == id),
                    _ => None,
                };

                let len = instances.len();
                let next_index = if e.modifiers.contains(Modifiers::SHIFT) {
                    current_pos.map(|pos| (pos + len - 1) % len).unwrap_or(0)
                } else {
                    current_pos.map(|pos| (pos + 1) % len).unwrap_or(0)
                };

                router.replace(Route::Instance {
                    id: instances[next_index].id,
                });
            }
        };

        NativeRouter::new().child(
            rect()
                .horizontal()
                .width(Size::fill())
                .height(Size::fill())
                .on_global_key_down(on_global_key_down)
                .child(
                    rect()
                        .overflow(Overflow::Clip)
                        .width(Size::px(180.))
                        .height(Size::fill())
                        .background((25, 25, 25, 0.6))
                        .child(InstanceSidebar),
                )
                .child(
                    rect()
                        .overflow(Overflow::Clip)
                        .expanded()
                        .content(Content::Flex)
                        .background((25, 25, 25))
                        .child(Outlet::<Route>::new()),
                ),
        )
    }
}

#[derive(PartialEq)]
struct Home;

impl Component for Home {
    fn render(&self) -> impl IntoElement {
        let radio = use_radio::<AppState, AppChannel>(AppChannel::App);

        if radio.read().instances.is_empty() {
            rect().expanded().center().child(
                label()
                    .text("Open a project and create instances from the sidebar.")
                    .theme_color(),
            )
        } else {
            let first_instance_id = radio.read().instances[0].id;
            let route = Route::Instance {
                id: first_instance_id,
            };
            RouterContext::get().replace(route);
            rect().expanded().center().child("Redirecting...")
        }
    }
}

#[derive(PartialEq)]
struct Instance {
    id: usize,
}

impl Component for Instance {
    fn render(&self) -> impl IntoElement {
        let instance_id = self.id;
        let mut radio = use_radio::<AppState, AppChannel>(AppChannel::Instance(instance_id));

        let selected_instance = radio
            .read()
            .instances
            .iter()
            .find(|i| i.id == instance_id)
            .cloned();

        if let Some(instance) = selected_instance {
            rect().expanded().content(Content::Flex).child::<Element>(
                if instance.terminals.is_empty() {
                    rect()
                        .expanded()
                        .center()
                        .child("No terminals. Click 'New Terminal' to create one.")
                        .into()
                } else {
                    let instance_path = instance.path.clone();
                    let config = state::CONFIG.get().cloned().unwrap_or_default();
                    let on_terminal_reopen = Callback::new(move |terminal_index: usize| {
                        if let Some(inst) = radio
                            .write()
                            .instances
                            .iter_mut()
                            .find(|i| i.id == instance_id)
                        {
                            let command = inst
                                .terminals
                                .get(terminal_index)
                                .map(|t| t.command.clone())
                                .unwrap_or_else(|| config.shell.clone());
                            let mut cmd = CommandBuilder::new(&command);
                            cmd.env("TERM", "xterm-256color");
                            cmd.env("COLORTERM", "truecolor");
                            cmd.env("LANG", "en_GB.UTF-8");
                            cmd.cwd(&instance_path);
                            let terminal = TerminalState::new(terminal_index, cmd, command);
                            inst.terminals[terminal_index] = terminal;
                        }
                    });

                    TerminalGrid {
                        instance_id,
                        terminals: instance.terminals.clone(),
                        on_terminal_reopen,
                    }
                    .into_element()
                },
            )
        } else {
            rect()
                .expanded()
                .center()
                .child(format!("Instance {} not found", instance_id))
        }
    }
}