main
rs 167 lines 5.17 KB
Raw
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 }