#![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))
}
}
}