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