mod app;
mod display_mode;
mod error;
mod protocol;
mod tcp_client;
mod before_after;
mod compact_tree;
mod data_flow;
mod deduction_tree;
mod effect_flow;
mod graph_layout;
mod interactive;
mod navigation;
mod selection;
mod theme;
mod widgets;
use std::{
fs,
io::{stdout, Stdout},
path::PathBuf,
process,
time::Duration,
};
use clap::Parser;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, EventStream},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use futures::StreamExt;
use ratatui::prelude::*;
use tokio::{
runtime::Builder as RuntimeBuilder,
time::{interval, sleep, Interval},
};
use crate::{
app::App,
error::Result,
tcp_client::{discover_workers, ConnectionPool},
};
#[derive(Parser)]
#[command(version, about)]
pub struct Args {
#[arg(short, long)]
verbose: bool,
#[arg(short, long, default_value_t = Args::default().port)]
port: u16,
}
impl Default for Args {
fn default() -> Self {
Self {
verbose: false,
port: 9742,
}
}
}
pub fn init_logging(verbose: bool) {
let log_path = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("lean-tui.log");
if let Ok(log_file) = fs::File::create(&log_path) {
let level = if verbose { "debug" } else { "info" };
let filter = tracing_subscriber::EnvFilter::from_default_env()
.add_directive(format!("lean_tui={level}").parse().unwrap_or_default());
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(log_file)
.with_ansi(true)
.with_target(false)
.pretty()
.init();
}
}
pub fn run_from_args() {
let args = Args::parse();
init_logging(args.verbose);
if let Err(e) = run_blocking(args.port) {
eprintln!("Error: {e}");
process::exit(1);
}
}
fn run_blocking(port: u16) -> Result<()> {
RuntimeBuilder::new_multi_thread()
.enable_all()
.build()?
.block_on(run(port))
}
async fn run(port: u16) -> Result<()> {
let mut terminal = setup_terminal()?;
let result = event_loop(&mut terminal, port).await;
disable_raw_mode()?;
stdout()
.execute(DisableMouseCapture)?
.execute(LeaveAlternateScreen)?;
result
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
stdout()
.execute(EnterAlternateScreen)?
.execute(EnableMouseCapture)?;
Ok(Terminal::new(CrosstermBackend::new(stdout()))?)
}
async fn event_loop(
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
initial_port: u16,
) -> Result<()> {
let mut pool = ConnectionPool::new();
let mut app = App::new(initial_port);
let mut event_stream = EventStream::new();
let mut scan_interval = interval(Duration::from_secs(2));
while !app.should_exit {
handle_active_switch(&mut app, &mut pool);
scan_and_connect(&mut app, &mut pool).await;
if let Some(port) = pool.take_reconnected_port() {
tracing::info!("Worker on port {port} reconnected, clearing state");
app.clear_for_worker_switch(port);
}
drain_messages(&mut app, &mut pool).await;
app.update();
if let Some(port) = pool.active_port() {
app.connected = true;
app.connected_file_uri = pool.file_uri(port).cloned();
} else {
app.connected = false;
app.connected_file_uri = None;
}
terminal.draw(|frame| app.render(frame))?;
handle_events(&mut app, &mut pool, &mut event_stream, &mut scan_interval).await;
}
Ok(())
}
fn handle_active_switch(app: &mut App, pool: &mut ConnectionPool) {
if let Some(new_port) = app.take_port_switch() {
if Some(new_port) != pool.active_port() {
tracing::info!("Switching active to port {new_port}");
pool.set_active(new_port);
app.clear_for_worker_switch(new_port);
}
}
}
async fn drain_messages(app: &mut App, pool: &mut ConnectionPool) {
while let Ok(Some(pool_msg)) = pool.recv_any().await {
app.handle_pool_message(pool_msg.port, pool_msg.message, pool);
}
}
async fn scan_and_connect(app: &mut App, pool: &mut ConnectionPool) {
if !app.scan_requested {
return;
}
app.scan_requested = false;
let (port_min, port_max) = app.port_range();
let workers = discover_workers(port_min, port_max).await;
pool.connect_all(&workers).await;
app.set_discovered_workers(workers, pool);
}
async fn handle_events(
app: &mut App,
pool: &mut ConnectionPool,
event_stream: &mut EventStream,
scan_interval: &mut Interval,
) {
tokio::select! {
Some(Ok(event)) = event_stream.next() => {
app.handle_event(&event);
}
_ = scan_interval.tick() => {
app.scan_requested = true;
}
() = sleep(Duration::from_millis(16)) => {
}
}
loop {
match pool.recv_any().await {
Ok(Some(pool_msg)) => {
app.handle_pool_message(pool_msg.port, pool_msg.message, pool);
}
Ok(None) => break,
Err(e) => {
app.set_error(e.to_string());
break;
}
}
}
for cmd in app.take_commands() {
let _ = pool.send_to_active(&cmd).await;
}
}