| 1 | mod app; |
| 2 | mod chat; |
| 3 | mod finetune; |
| 4 | mod gguf; |
| 5 | mod gresiq; |
| 6 | mod hf; |
| 7 | mod hf_clone; |
| 8 | mod hf_search; |
| 9 | mod hf_upload; |
| 10 | mod mcp; |
| 11 | mod merge; |
| 12 | mod project; |
| 13 | mod token; |
| 14 | mod ui; |
| 15 | |
| 16 | use std::io::{IsTerminal, Write}; |
| 17 | |
| 18 | /// Usage text, printed for `--help`, for bare `onde`, and (to stderr) for |
| 19 | /// unknown arguments. |
| 20 | fn usage() -> String { |
| 21 | format!( |
| 22 | "onde {}\n{}\n\n\ |
| 23 | Usage:\n \ |
| 24 | onde Print this usage and exit\n \ |
| 25 | onde --tui Launch the interactive terminal UI (requires a TTY)\n \ |
| 26 | onde --mcp Run the MCP JSON-RPC server over stdio\n \ |
| 27 | onde --version Print the version and exit\n \ |
| 28 | onde --help Print this help and exit", |
| 29 | env!("CARGO_PKG_VERSION"), |
| 30 | env!("CARGO_PKG_DESCRIPTION"), |
| 31 | ) |
| 32 | } |
| 33 | |
| 34 | /// Redirect **both** stdout and stderr to `~/.cache/onde/debug.log` before |
| 35 | /// ratatui takes over the alternate screen. |
| 36 | /// |
| 37 | /// mistral.rs writes to both file descriptors: |
| 38 | /// - `stderr` — tracing/log output from `mistralrs_core` |
| 39 | /// - `stdout` — `println!` in `print_metadata`, pipeline info, etc. |
| 40 | /// |
| 41 | /// Without redirecting stdout the `println!` calls write raw text into the |
| 42 | /// alternate screen buffer, tearing up the TUI layout. |
| 43 | /// |
| 44 | /// We `dup` the real stdout *before* the redirect so ratatui can still |
| 45 | /// render to the terminal via the returned `File` handle. |
| 46 | #[cfg(unix)] |
| 47 | fn redirect_output() -> Option<std::fs::File> { |
| 48 | use std::fs::{self, OpenOptions}; |
| 49 | use std::os::fd::{AsRawFd, FromRawFd}; |
| 50 | |
| 51 | let log_dir = dirs::cache_dir() |
| 52 | .unwrap_or_else(|| std::path::PathBuf::from("/tmp")) |
| 53 | .join("onde"); |
| 54 | |
| 55 | if fs::create_dir_all(&log_dir).is_err() { |
| 56 | return None; |
| 57 | } |
| 58 | |
| 59 | let log_file = match OpenOptions::new() |
| 60 | .create(true) |
| 61 | .append(true) |
| 62 | .open(log_dir.join("debug.log")) |
| 63 | { |
| 64 | Ok(f) => f, |
| 65 | Err(_) => return None, |
| 66 | }; |
| 67 | |
| 68 | // SAFETY: called before tokio or ratatui start, so no threads yet. |
| 69 | unsafe { |
| 70 | // Preserve the real stdout so ratatui can render to the terminal. |
| 71 | let tty_fd = libc::dup(libc::STDOUT_FILENO); |
| 72 | if tty_fd < 0 { |
| 73 | return None; |
| 74 | } |
| 75 | |
| 76 | let log_fd = log_file.as_raw_fd(); |
| 77 | |
| 78 | // Point both stdout and stderr at the log file. |
| 79 | libc::dup2(log_fd, libc::STDOUT_FILENO); |
| 80 | libc::dup2(log_fd, libc::STDERR_FILENO); |
| 81 | |
| 82 | Some(std::fs::File::from_raw_fd(tty_fd)) |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | #[cfg(not(unix))] |
| 87 | fn redirect_output() -> Option<std::fs::File> { |
| 88 | None |
| 89 | } |
| 90 | |
| 91 | #[tokio::main] |
| 92 | async fn main() -> anyhow::Result<()> { |
| 93 | // MCP mode runs a JSON-RPC server over stdio instead of the TUI. stdout is |
| 94 | // the JSON-RPC channel, so — unlike the TUI path below — we must NOT redirect |
| 95 | // stdout/stderr, and we return before touching the terminal at all. |
| 96 | if std::env::args().skip(1).any(|arg| arg == "--mcp") { |
| 97 | return mcp::serve().await; |
| 98 | } |
| 99 | |
| 100 | // Bare `--version`/`--help` must work without a terminal. |
| 101 | let args: Vec<String> = std::env::args().skip(1).collect(); |
| 102 | if args.iter().any(|arg| arg == "--version" || arg == "-V") { |
| 103 | println!("onde {}", env!("CARGO_PKG_VERSION")); |
| 104 | return Ok(()); |
| 105 | } |
| 106 | if args.iter().any(|arg| arg == "--help" || arg == "-h") { |
| 107 | println!("{}", usage()); |
| 108 | return Ok(()); |
| 109 | } |
| 110 | |
| 111 | // Bare `onde` is headless: winget's validator launches the exe with no |
| 112 | // arguments and expects exit 0, so it must not open the TUI. A real |
| 113 | // one-shot mode can replace this later. |
| 114 | if args.is_empty() { |
| 115 | println!("{}", usage()); |
| 116 | return Ok(()); |
| 117 | } |
| 118 | |
| 119 | // Anything but `--tui` is a typo; don't let it silently succeed. |
| 120 | if args != ["--tui"] { |
| 121 | eprintln!("{}", usage()); |
| 122 | std::process::exit(2); |
| 123 | } |
| 124 | |
| 125 | // The TUI needs a real terminal. When stdout isn't a TTY (`onde --tui | cat`), |
| 126 | // enabling raw mode / the alternate screen below fails, so print usage and |
| 127 | // exit 0 instead of launching ratatui into a pipe. |
| 128 | if !std::io::stdout().is_terminal() { |
| 129 | println!("{}", usage()); |
| 130 | return Ok(()); |
| 131 | } |
| 132 | |
| 133 | let tty = redirect_output(); |
| 134 | |
| 135 | // Build a writer that targets the real terminal. When |
| 136 | // redirect_output() moved fd 1 to the log file, `tty` holds |
| 137 | // the dup'd real terminal fd. Otherwise we use stdout. |
| 138 | let mut writer: Box<dyn Write> = match tty { |
| 139 | Some(f) => Box::new(std::io::BufWriter::new(f)), |
| 140 | None => Box::new(std::io::stdout()), |
| 141 | }; |
| 142 | |
| 143 | // Enter raw mode + alternate screen. The control sequences must go |
| 144 | // to the real terminal writer, not stdout/stderr (which may be |
| 145 | // redirected to the log file). |
| 146 | crossterm::terminal::enable_raw_mode()?; |
| 147 | crossterm::execute!( |
| 148 | writer, |
| 149 | crossterm::terminal::EnterAlternateScreen, |
| 150 | crossterm::event::EnableMouseCapture |
| 151 | )?; |
| 152 | |
| 153 | let backend = ratatui::backend::CrosstermBackend::new(writer); |
| 154 | let mut terminal = ratatui::Terminal::new(backend)?; |
| 155 | let result = app::run(&mut terminal).await; |
| 156 | |
| 157 | // Restore terminal state via the backend writer. |
| 158 | crossterm::execute!( |
| 159 | terminal.backend_mut(), |
| 160 | crossterm::terminal::LeaveAlternateScreen, |
| 161 | crossterm::event::DisableMouseCapture |
| 162 | )?; |
| 163 | crossterm::terminal::disable_raw_mode()?; |
| 164 | terminal.show_cursor()?; |
| 165 | |
| 166 | result |
| 167 | } |