use std::io::IsTerminal;
use lilyco_core::registry::Registry;
use lilyco_core::App;
#[cfg(feature = "tui")]
use lilyco_core::executor;
#[cfg(feature = "tui")]
use lilyco_core::progress::LogLevel;
#[cfg(feature = "tui")]
use lilyco_core::registry::Handler;
#[cfg(feature = "tui")]
use lilyco_core::{AppError, Progress};
#[cfg(feature = "tui")]
use std::sync::Arc;
pub mod prelude {
pub use lilyco_core::prelude::*;
pub use lilyco_macros::{App, ValueEnum};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backend {
Cli,
#[cfg(feature = "tui")]
Tui,
#[cfg(feature = "web")]
Web,
Mcp,
}
pub struct Env<'a> {
pub args: &'a [String],
pub env: &'a dyn Fn(&str) -> Option<String>,
pub stdin_is_terminal: bool,
}
pub fn detect_backend(env: &Env) -> Backend {
if env.args.iter().any(|a| a == "--mcp") {
return Backend::Mcp;
}
#[cfg(feature = "web")]
if env.args.iter().any(|a| a == "--gui" || a == "--web") {
return Backend::Web;
}
if let Some(ui) = (env.env)("LILYCO_UI") {
match ui.as_str() {
"cli" => return Backend::Cli,
#[cfg(feature = "tui")]
"tui" => return Backend::Tui,
#[cfg(feature = "web")]
"web" | "gui" => return Backend::Web,
"mcp" => return Backend::Mcp,
_ => {} }
}
#[cfg(feature = "tui")]
if env.stdin_is_terminal && (env.env)("TERM").map(|t| !t.is_empty()).unwrap_or(false) {
return Backend::Tui;
}
Backend::Cli
}
pub fn detect() -> Backend {
let args: Vec<String> = std::env::args().skip(1).collect();
let env = Env {
args: &args,
env: &|k| std::env::var(k).ok(),
stdin_is_terminal: std::io::stdin().is_terminal(),
};
detect_backend(&env)
}
pub fn run<A: App + Send + 'static>() {
run_with::<A>(detect());
}
pub fn run_with<A: App + Send + 'static>(backend: Backend) {
match backend {
Backend::Cli => run_cli::<A>(),
#[cfg(feature = "tui")]
Backend::Tui => {
if run_tui::<A>().is_err() {
run_cli::<A>();
}
}
#[cfg(feature = "web")]
Backend::Web => run_web::<A>(),
Backend::Mcp => serve_mcp(single_registry::<A>()),
}
}
pub fn serve_mcp(registry: Registry) {
lilyco_mcp::McpServer::new(registry)
.serve_stdio()
.unwrap_or_else(|e| {
eprintln!("MCP server error: {e}");
std::process::exit(1);
});
}
fn single_registry<A: App + Send + 'static>() -> Registry {
let mut registry = Registry::new();
registry
.register(lilyco_core::registry::RegisteredCommand::from_app::<A>())
.expect("register app command");
registry
}
fn run_cli<A: App + Send + 'static>() {
lilyco_cli::run::<A>(|app, ctx| app.run(ctx));
}
#[cfg(feature = "web")]
fn run_web<A: App + Send + 'static>() {
let port: u16 = std::env::var("LILYCO_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(8080);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("tokio runtime");
rt.block_on(async {
let gui = lilyco_gui::GuiRenderer::new(port);
gui.serve_app::<A>(A::schema()).await;
});
}
#[cfg(feature = "tui")]
fn run_tui<A: App + Send + 'static>() -> std::io::Result<()> {
use crossterm::event::Event;
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use lilyco_tui::{AppState, TuiApp};
use std::time::Duration;
let schema = A::schema();
let mut app = TuiApp::new(&schema);
enum TaskState {
Idle,
Running(executor::Task),
}
let mut task_state = TaskState::Idle;
enable_raw_mode()?;
let mut terminal =
ratatui::Terminal::new(ratatui::backend::CrosstermBackend::new(std::io::stdout()))?;
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
loop {
if app.state() == &AppState::Running {
if let TaskState::Idle = task_state {
let args = collect_args(&app);
task_state = TaskState::Running(executor::spawn(build_handler::<A>(), args));
}
}
if app.state() == &AppState::Running {
if let TaskState::Running(task) = &task_state {
drain_task(&mut app, task);
}
}
terminal.draw(|f| {
let area = f.area();
app.render(area, f.buffer_mut());
})?;
if app.should_quit {
break;
}
if app.state() != &AppState::Running {
if let TaskState::Running(task) = &task_state {
task.cancel
.store(true, std::sync::atomic::Ordering::Relaxed);
}
if let TaskState::Running(task) = task_state {
let _ = task.handle.join();
}
task_state = TaskState::Idle;
}
let timeout = if app.state() == &AppState::Running {
Duration::from_millis(150)
} else {
Duration::from_millis(500)
};
if crossterm::event::poll(timeout)? {
match crossterm::event::read()? {
Event::Key(key) => {
let cont = app.handle_event(key);
if !cont {
break;
}
}
Event::Resize(_, _) => {}
_ => {}
}
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}
#[cfg(feature = "tui")]
fn collect_args(app: &lilyco_tui::TuiApp) -> serde_json::Value {
use lilyco_tui::FieldValue;
let mut map = serde_json::Map::new();
for field in app.fields() {
let v = match &field.value {
FieldValue::Flag(b) => serde_json::json!(b),
FieldValue::Text(s) | FieldValue::Path(s) => serde_json::json!(s),
FieldValue::Number(n) => serde_json::json!(n),
FieldValue::Enum { values, selected } => {
serde_json::json!(values.get(*selected).cloned().unwrap_or_default())
}
FieldValue::List { values, .. } => serde_json::json!(values),
};
map.insert(field.name.clone(), v);
}
serde_json::Value::Object(map)
}
#[cfg(feature = "tui")]
fn build_handler<A: App + Send + 'static>() -> Handler {
Arc::new(move |ctx, args| {
let obj = args
.as_object()
.ok_or_else(|| AppError::InvalidArg("args must be a JSON object".into()))?;
let map: std::collections::HashMap<String, serde_json::Value> =
obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
let a = A::from_args(&map)?;
a.run(ctx)
})
}
#[cfg(feature = "tui")]
fn drain_task(app: &mut lilyco_tui::TuiApp, task: &executor::Task) {
use std::sync::mpsc::TryRecvError;
loop {
match task.rx.try_recv() {
Ok(Progress::Started { message, .. }) => app.start_progress(None, message),
Ok(Progress::Tick {
current,
total,
message,
..
}) => app.tick_progress(current, total, message),
Ok(Progress::Log { level, message }) => app.log_progress(level_name(&level), message),
Ok(Progress::Done {
result,
duration_ms,
}) => {
app.finish_progress(result, duration_ms);
}
Ok(Progress::Error { code, message, .. }) => {
app.error_progress(code, message);
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
if *app.state() == lilyco_tui::AppState::Running {
app.error_progress(1, "任务意外终止".into());
}
break;
}
}
}
}
#[cfg(feature = "tui")]
fn level_name(level: &LogLevel) -> &'static str {
match level {
LogLevel::Debug => "debug",
LogLevel::Info => "info",
LogLevel::Warn => "warn",
LogLevel::Error => "error",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn no_env() -> impl Fn(&str) -> Option<String> {
|_k: &str| -> Option<String> { None }
}
#[test]
fn mcp_flag_wins_over_everything() {
let args = vec!["--mcp".to_string()];
let getenv = no_env();
let e = Env {
args: &args,
env: &getenv,
stdin_is_terminal: true,
};
assert_eq!(detect_backend(&e), Backend::Mcp);
}
#[cfg(feature = "web")]
#[test]
fn gui_flag_selects_web() {
let args = vec!["--gui".to_string()];
let getenv = no_env();
let e = Env {
args: &args,
env: &getenv,
stdin_is_terminal: true,
};
assert_eq!(detect_backend(&e), Backend::Web);
}
#[cfg(feature = "web")]
#[test]
fn env_var_forces_backend() {
let args: Vec<String> = Vec::new();
let getenv = |k: &str| -> Option<String> {
if k == "LILYCO_UI" {
Some("web".into())
} else {
None
}
};
let e = Env {
args: &args,
env: &getenv,
stdin_is_terminal: false,
};
assert_eq!(detect_backend(&e), Backend::Web);
}
#[test]
fn unknown_env_value_falls_through_to_auto() {
let args: Vec<String> = Vec::new();
let getenv = |k: &str| -> Option<String> {
if k == "LILYCO_UI" {
Some("bogus".into())
} else {
None
}
};
let e = Env {
args: &args,
env: &getenv,
stdin_is_terminal: true,
};
assert_eq!(detect_backend(&e), Backend::Cli);
}
#[cfg(feature = "tui")]
#[test]
fn auto_tui_when_terminal_and_term() {
let args: Vec<String> = Vec::new();
let getenv = |k: &str| -> Option<String> {
if k == "TERM" {
Some("xterm-256color".into())
} else {
None
}
};
let e = Env {
args: &args,
env: &getenv,
stdin_is_terminal: true,
};
assert_eq!(detect_backend(&e), Backend::Tui);
}
#[test]
fn auto_cli_when_piped() {
let args: Vec<String> = Vec::new();
let getenv = no_env();
let e = Env {
args: &args,
env: &getenv,
stdin_is_terminal: false,
};
assert_eq!(detect_backend(&e), Backend::Cli);
}
}