1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#![doc = include_str!("../README.md")]
// Library root: re-exports for integration tests and external access.
pub mod app;
pub(crate) mod app_state;
pub mod clipboard;
pub mod data;
pub mod event;
pub mod keymap;
pub mod mcp;
pub mod sheet;
#[cfg(test)]
mod test;
pub mod theme;
pub mod types;
pub mod ui;
use clap::Parser;
use color_eyre::Result;
use std::path::PathBuf;
/// TuiTab — Terminal tabular data explorer
#[derive(Parser, Debug)]
#[command(version, about)]
pub struct Cli {
/// One or more files to open. Pass multiple files to browse them as a list.
/// Use '-' to read from stdin (pipe mode).
pub files: Vec<PathBuf>,
/// Column delimiter (auto-detected if not specified)
#[arg(short, long)]
pub delimiter: Option<char>,
/// Data format (e.g. csv, json, jsonl, yaml, toml). Required when reading from
/// stdin; for a file it overrides the extension, so `-t yaml deploy.conf` works.
#[arg(short = 't', long = "type")]
pub data_type: Option<String>,
/// Run as an MCP server on stdio, so a language model can query data files
/// through tuitab's engine instead of computing over them itself.
#[arg(long)]
pub mcp: bool,
}
pub fn run() -> Result<()> {
color_eyre::install()?;
let cli = Cli::parse();
// Before anything else: the MCP transport owns stdin and stdout, so neither
// the terminal detection below nor the /dev/tty juggling after it may run.
if cli.mcp {
// Tools name their own files, so a path here does nothing. Say so on
// stderr — which the transport allows for logging — rather than let it
// vanish.
if !cli.files.is_empty() {
eprintln!("tuitab: --mcp ignores file arguments; the model names the files it wants.");
}
return mcp::serve();
}
use std::io::IsTerminal;
let is_terminal = std::io::stdin().is_terminal();
let use_stdin = (!is_terminal && cli.files.is_empty())
|| cli
.files
.first()
.map(|p| p.to_str() == Some("-"))
.unwrap_or(false);
let mut app = if use_stdin {
if cli.data_type.is_none() {
eprintln!("Error: When reading from stdin, you must specify the data type using the -t or --type argument.");
eprintln!("Examples:");
eprintln!(" cat data.csv | tuitab -t csv");
eprintln!(" echo '[{{\"a\":1}}]' | tuitab -t json");
std::process::exit(1);
}
app::App::from_stdin_typed(cli.data_type.unwrap().as_str(), cli.delimiter)?
} else if cli.files.len() >= 2 {
for p in &cli.files {
if !p.exists() {
eprintln!("Error: '{}': no such file or directory", p.display());
std::process::exit(1);
}
}
app::App::from_file_list(cli.files, cli.delimiter)?
} else {
let path = cli
.files
.into_iter()
.next()
.unwrap_or_else(|| std::path::PathBuf::from("."));
if !path.exists() {
eprintln!("Error: '{}': no such file or directory", path.display());
std::process::exit(1);
}
let forced = cli
.data_type
.as_deref()
.and_then(data::doc::Format::from_name);
// `--type` only forces the structured formats; csv/tsv/txt keep falling through
// to extension-based loading, which is what they did before.
app::App::new_as(&path, cli.delimiter, forced)?
};
#[cfg(unix)]
{
use std::io::IsTerminal;
if !std::io::stdin().is_terminal() {
use std::os::unix::io::AsRawFd;
unsafe {
let mut buf = [0u8; 256];
let mut real_tty_opened = false;
if libc::ttyname_r(
libc::STDERR_FILENO,
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
) == 0
{
let c_str = std::ffi::CStr::from_ptr(buf.as_ptr() as *const libc::c_char);
if let Ok(path) = c_str.to_str() {
if let Ok(real_tty) = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
{
libc::dup2(real_tty.as_raw_fd(), libc::STDIN_FILENO);
real_tty_opened = true;
}
}
}
if !real_tty_opened {
if let Ok(tty) = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")
{
libc::dup2(tty.as_raw_fd(), libc::STDIN_FILENO);
}
}
}
}
}
let mut terminal = ratatui::init();
let result = app.run(&mut terminal);
ratatui::restore();
result
}