1use crate::app::terminal::{
5 QuietTerminal, TakenTerminal, follow_focus, push_keyboard_flags, restore_terminal,
6};
7use crate::config::AppConfig;
8use crate::view::Views;
9use crate::{
10 App, AppEvent, RunInput, app::event_pump, app::pointer, app::startup, app::terminal_color,
11 app::terminal_input, glyphs, inspector::external_open, logging,
12};
13use color_eyre::Result;
14use crossterm::event::{KeyCode, KeyModifiers};
15use polars::prelude::LazyFrame;
16
17#[derive(Default)]
20pub struct External {
21 pub(crate) open: Option<crate::inspector::external_open::ExternalOpen>,
24 pub(crate) open_dir: Option<tempfile::TempDir>,
26 pub(crate) clipboard: Option<Box<dyn crate::clipboard::Destination>>,
29}
30
31#[derive(Default)]
33pub struct Pipes {
34 pub(crate) stdin_reader: Option<Box<dyn std::io::Read + Send>>,
36 pub(crate) stdout_pass: Option<Box<dyn std::io::Write + Send>>,
38 pub(crate) follow_drawn: Option<crate::render::footer::FollowMark>,
40 pub(crate) recording_on: Option<std::sync::Arc<crate::loading::follow::SpoolHandle>>,
42 pub(crate) recording_end_said: bool,
44}
45
46fn conclude(
50 end: event_pump::Ended,
51 app: &App,
52 capture: bool,
53 reader: &mut terminal_input::TerminalInput,
54 screen: &mut TakenTerminal,
55) -> Result<Option<LazyFrame>> {
56 reader.stop();
57 screen.restore();
58 match end {
59 event_pump::Ended::Quit if capture => app.capture_view(),
60 event_pump::Ended::Quit => Ok(None),
61 event_pump::Ended::Crash(msg) => Err(color_eyre::eyre::eyre!(msg)),
62 event_pump::Ended::NotFound(path) => Err(std::io::Error::new(
63 std::io::ErrorKind::NotFound,
64 format!("File not found: {}", path.display()),
65 )
66 .into()),
67 }
68}
69
70static ENDED_BY_SIGNAL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
72
73pub fn ended_by_signal() -> Option<i32> {
77 let status = ENDED_BY_SIGNAL.load(std::sync::atomic::Ordering::SeqCst);
78 (status != 0).then_some(status)
79}
80
81fn end_session(status: i32, tx: &std::sync::mpsc::Sender<AppEvent>) {
86 use std::sync::atomic::Ordering;
87 const STRAGGLE: std::time::Duration = std::time::Duration::from_secs(3);
90 fn end_now(status: i32) -> ! {
91 restore_terminal();
92 std::process::exit(status)
93 }
94 if ENDED_BY_SIGNAL
95 .compare_exchange(0, status, Ordering::SeqCst, Ordering::SeqCst)
96 .is_err()
97 {
98 end_now(ENDED_BY_SIGNAL.load(Ordering::SeqCst));
99 }
100 let _ = tx.send(AppEvent::Exit);
101 tokio::spawn(async move {
102 tokio::time::sleep(STRAGGLE).await;
103 end_now(status);
104 });
105}
106
107#[cfg(unix)]
110fn quit_on_signals(runtime: &tokio::runtime::Handle, tx: &std::sync::mpsc::Sender<AppEvent>) {
111 use tokio::signal::unix::{SignalKind, signal};
112 let _runtime = runtime.enter();
114 for kind in [
115 SignalKind::terminate(),
116 SignalKind::hangup(),
117 SignalKind::interrupt(),
118 ] {
119 let Ok(mut arrivals) = signal(kind) else {
120 continue;
121 };
122 let tx = tx.clone();
123 runtime.spawn(async move {
124 while arrivals.recv().await.is_some() {
125 end_session(128 + kind.as_raw_value(), &tx);
126 }
127 });
128 }
129}
130
131#[cfg(windows)]
134fn quit_on_signals(runtime: &tokio::runtime::Handle, tx: &std::sync::mpsc::Sender<AppEvent>) {
135 use tokio::signal::windows::{ctrl_close, ctrl_logoff, ctrl_shutdown};
136 const CLOSED: i32 = 0xC000_013A_u32 as i32;
138 let _runtime = runtime.enter();
140 macro_rules! quit_on {
142 ($listen:expr) => {
143 if let Ok(mut arrivals) = $listen {
144 let tx = tx.clone();
145 runtime.spawn(async move {
146 while arrivals.recv().await.is_some() {
147 end_session(CLOSED, &tx);
148 }
149 });
150 }
151 };
152 }
153 quit_on!(ctrl_close());
154 quit_on!(ctrl_logoff());
155 quit_on!(ctrl_shutdown());
156}
157
158pub fn run(input: RunInput, config: Option<AppConfig>) -> Result<()> {
161 run_impl(input, config, false).map(|_| ())
162}
163
164pub fn run_captured(input: RunInput, config: Option<AppConfig>) -> Result<Option<LazyFrame>> {
167 run_impl(input, config, true)
168}
169
170fn run_impl(
171 input: RunInput,
172 config: Option<AppConfig>,
173 capture: bool,
174) -> Result<Option<LazyFrame>> {
175 use event_pump::EventPump;
176 use std::io::Write;
177
178 let input = startup::expand_home(input);
180 use std::sync::{Mutex, Once, mpsc};
181
182 let views = Views::read_in_background();
184
185 static COLOR_EYRE_INIT: Once = Once::new();
187 static INSTALL_RESULT: Mutex<Option<Result<(), color_eyre::Report>>> = Mutex::new(None);
188 COLOR_EYRE_INIT.call_once(|| {
189 *INSTALL_RESULT.lock().unwrap_or_else(|e| e.into_inner()) = Some(color_eyre::install());
190 });
191 if let Some(Err(e)) = INSTALL_RESULT
192 .lock()
193 .unwrap_or_else(|e| e.into_inner())
194 .as_ref()
195 {
196 return Err(color_eyre::eyre::eyre!(e.to_string()));
197 }
198 let rt = tokio::runtime::Builder::new_multi_thread()
199 .worker_threads(2)
200 .enable_all()
201 .build()
202 .map_err(|e| color_eyre::eyre::eyre!("Failed to create tokio runtime: {}", e))?;
203
204 struct RtGuard(Option<tokio::runtime::Runtime>);
208 impl Drop for RtGuard {
209 fn drop(&mut self) {
210 if let Some(rt) = self.0.take() {
211 rt.shutdown_background();
212 }
213 }
214 }
215 let rt_guard = RtGuard(Some(rt));
216 let rt_handle = rt_guard
217 .0
218 .as_ref()
219 .expect("runtime present")
220 .handle()
221 .clone();
222
223 let passed = match &input {
226 RunInput::Cli(args)
227 if args
228 .tee
229 .as_deref()
230 .is_some_and(crate::loading::stdin::is_stdin) =>
231 {
232 Some(crate::loading::tee::pass_stdout_on().map_err(|e| color_eyre::eyre::eyre!(e))?)
233 }
234 _ => None,
235 };
236 let mut terminal = match ratatui::try_init() {
237 Ok(terminal) => QuietTerminal(Some(terminal)),
238 Err(e) => {
239 if config.is_none() {
241 startup::load_config(&input)?;
242 }
243 if let Some(missing) =
245 App::missing_named_path(startup::named_paths(&input), &Default::default())
246 {
247 return Err(std::io::Error::new(
248 std::io::ErrorKind::NotFound,
249 format!("File not found: {}", missing.display()),
250 )
251 .into());
252 }
253 return Err(color_eyre::eyre::eyre!(
254 "datui requires an interactive terminal (TTY). No terminal detected: {}. \
255 There is no TTY inside a Jupyter notebook or when output is piped or \
256 redirected; run from a terminal with stdout connected to it.",
257 e
258 ));
259 }
260 };
261 let mut screen = TakenTerminal { restored: false };
263 let session = logging::TuiSession::begin(restore_terminal);
265 push_keyboard_flags();
266 let asked = terminal_color::supported()
269 && config.as_ref().is_none_or(|c| c.theme.follow)
270 && terminal_color::ask(&mut std::io::stdout());
271 let mut background = None;
272 let (tx, rx) = mpsc::channel::<AppEvent>();
273 {
274 let tx = tx.clone();
275 session.wake_with(move || {
276 let _ = tx.send(AppEvent::Wake);
277 });
278 }
279 let mut reader = terminal_input::TerminalInput::start(tx.clone())?;
280 #[cfg(any(unix, windows))]
283 if matches!(input, RunInput::Cli(_)) {
284 quit_on_signals(&rt_handle, &tx);
285 }
286
287 let waiting_on = startup::named(&input);
290 {
291 let tx = tx.clone();
292 std::thread::Builder::new()
293 .name("datui-settings".into())
294 .spawn(move || {
295 let read = logging::catch_panic(|| startup::read(input, config))
296 .unwrap_or_else(|panic| Err(color_eyre::eyre::eyre!(panic)));
297 let _ = tx.send(AppEvent::SettingsRead(Box::new(read)));
298 })?;
299 }
300 let mut backlog = Vec::new();
301 let grace_ends = std::time::Instant::now() + startup::GRACE;
302 let mut waiting_shown = false;
303 let settings = loop {
304 let timeout = if waiting_shown {
305 std::time::Duration::MAX
306 } else {
307 grace_ends.saturating_duration_since(std::time::Instant::now())
308 };
309 match rx.recv_timeout(timeout) {
310 Ok(AppEvent::SettingsRead(read)) => break *read,
311 Ok(AppEvent::TerminalBackground(mode)) => background = Some(mode),
312 Ok(AppEvent::Terminal(crossterm::event::Event::Key(key)))
313 if key.modifiers.contains(KeyModifiers::CONTROL)
314 && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('q')) =>
315 {
316 reader.stop();
317 screen.restore();
318 return Ok(None);
319 }
320 Ok(AppEvent::Crash(msg)) => {
321 reader.stop();
322 screen.restore();
323 return Err(color_eyre::eyre::eyre!(msg));
324 }
325 Ok(AppEvent::Exit) => {
327 reader.stop();
328 screen.restore();
329 return Ok(None);
330 }
331 Ok(event) => {
332 if waiting_shown
333 && matches!(
334 event,
335 AppEvent::Terminal(crossterm::event::Event::Resize(..))
336 )
337 {
338 terminal
339 .get()
340 .draw(|frame| startup::draw_waiting(frame, waiting_on.as_deref()))?;
341 }
342 backlog.push(event);
344 }
345 Err(_) => {
346 terminal
347 .get()
348 .draw(|frame| startup::draw_waiting(frame, waiting_on.as_deref()))?;
349 let _ = std::io::stdout().flush();
350 waiting_shown = true;
351 }
352 }
353 };
354 let startup::Settings {
355 config,
356 theme,
357 input,
358 opts,
359 notes,
360 } = match settings {
361 Ok(settings) => settings,
362 Err(e) => {
363 reader.stop();
364 screen.restore();
365 return Err(e);
366 }
367 };
368
369 let asked_threads = std::env::var_os("POLARS_MAX_THREADS");
373 if matches!(input, RunInput::Cli(_))
374 && let Some(threads) =
375 startup::polars_threads(config.performance.threads, asked_threads.as_deref())
376 {
377 unsafe { std::env::set_var("POLARS_MAX_THREADS", threads) };
381 }
382
383 let background = (asked && config.theme.follow)
386 .then(|| startup::take_answer(&rx, background, &mut backlog))
387 .flatten();
388 if config.theme.follow && terminal_color::supported() {
389 follow_focus(&mut std::io::stdout());
390 }
391
392 glyphs::init_with_overrides(config.display.unicode, &config.glyphs.overrides);
395 crate::limits::set(config.limits);
396
397 pointer::capture(config.display.mouse, &mut std::io::stdout());
399
400 let mut app = App::new_with_views(tx.clone(), rt_handle, theme, config, views);
401 app.settle_first_palette(background);
402 if let Some(out) = passed {
403 app.pass_stdout_to(out);
404 }
405 app.source.startup_view = opts.view.clone();
406 let debug_env = std::env::var_os("DATUI_DEBUG").is_some_and(|v| !v.is_empty() && v != "0");
408 if opts.debug || debug_env {
409 app.enable_debug();
410 }
411
412 let open = match input {
414 RunInput::Paths(paths, _) if paths.is_empty() => {
416 app.enter_home();
417 None
418 }
419 RunInput::Paths(paths, opts) => {
420 app.set_loading_phase("Scanning input", 10);
423 if let [path] = paths.as_slice() {
424 app.name_what_is_loading(path.clone());
425 }
426 Some(AppEvent::OpenNamed(paths, opts))
427 }
428 RunInput::LazyFrame(lf, opts) => {
429 app.set_loading_phase("Scanning input", 10);
430 Some(AppEvent::OpenLazyFrame(lf, opts))
431 }
432 RunInput::Cli(_) | RunInput::Host(..) => {
433 unreachable!("read_settings resolves the command line")
434 }
435 };
436 let _sweep = app.exit_sweep();
439 let input_tx = tx.clone();
440 let mut pump = EventPump::new(app, tx, rx);
441 pump.handle_first(backlog.into_iter().chain(open));
444 let end = pump.run(|app| {
445 if let Some(open) = app.take_external_open() {
446 let mouse = app.mouse_enabled();
447 let focus = app.follows_terminal() && terminal_color::supported();
448 let note = open_externally(&open, &mut reader, &input_tx, mouse, focus, terminal.get());
449 app.external_opened(&open, note);
450 }
451 if app.take_background_query() && terminal_color::supported() {
453 terminal_color::ask(&mut std::io::stdout());
454 }
455 terminal
456 .get()
457 .draw(|frame| frame.render_widget(app, frame.area()))?;
458 let _ = std::io::stdout().flush();
459 Ok(())
460 })?;
461 let result = conclude(end, &pump.app, capture, &mut reader, &mut screen);
462 drop(session);
464 for note in notes {
466 let _ = writeln!(std::io::stderr(), "datui: {note}");
467 }
468 if let Some((tee, handle)) = pump.app.recording_after_exit() {
471 let to = if tee.to_stdout() {
472 format!("passing standard input on to {}", tee.name())
473 } else {
474 format!("recording standard input to {}", tee.path.display())
475 };
476 let _ = writeln!(
477 std::io::stderr(),
478 "datui: {to} until it ends (Ctrl+C stops it)"
479 );
480 handle.spool().wait();
481 let done = if tee.to_stdout() {
482 "datui: standard input ended".to_string()
483 } else {
484 format!("datui: saved {}", tee.path.display())
485 };
486 let _ = writeln!(std::io::stderr(), "{done}");
487 }
488 result
489}
490
491fn open_externally(
495 open: &external_open::ExternalOpen,
496 reader: &mut terminal_input::TerminalInput,
497 tx: &std::sync::mpsc::Sender<AppEvent>,
498 mouse: bool,
499 focus: bool,
500 terminal: &mut ratatui::DefaultTerminal,
501) -> Option<String> {
502 let program = external_open::program_for(open.document, |name| std::env::var(name).ok());
503 let result = match &program {
504 external_open::Program::Opener(_) => external_open::run(&program, &open.path),
505 external_open::Program::Wait(_) => {
506 reader.stop();
507 restore_terminal();
508 let result = external_open::run(&program, &open.path);
509 let _ = crossterm::terminal::enable_raw_mode();
510 let _ = crossterm::execute!(
511 std::io::stdout(),
512 crossterm::terminal::EnterAlternateScreen,
513 crossterm::cursor::Hide
514 );
515 push_keyboard_flags();
516 pointer::capture(mouse, &mut std::io::stdout());
517 if focus {
518 follow_focus(&mut std::io::stdout());
519 }
520 let _ = terminal.clear();
521 match terminal_input::TerminalInput::start(tx.clone()) {
522 Ok(started) => *reader = started,
523 Err(e) => {
524 let _ = tx.send(AppEvent::Crash(format!("Could not read keys again: {e}")));
525 }
526 }
527 result
528 }
529 };
530 result.err().map(|e| e.to_string())
531}