1use datui_cli::{CacheAction, ViewsAction};
4
5use crate::cache::CacheManager;
6use crate::config::ConfigManager;
7use crate::view::{MatchCriteria, ViewManager};
8
9pub fn cache(cache: Option<&CacheManager>, action: &CacheAction) -> (String, i32) {
11 let CacheAction::Clear { recents } = action;
12 let Some(cache) = cache else {
13 return ("No cache to clear\n".into(), 0);
14 };
15 if *recents {
16 cache.clear_recents();
17 return ("Recently opened datasets forgotten\n".into(), 0);
18 }
19 match cache.clear_all() {
20 Ok(()) => (format!("Cleared {}\n", cache.cache_dir().display()), 0),
21 Err(e) => (format!("{e}\n"), 1),
22 }
23}
24
25pub fn views(config: &ConfigManager, action: &ViewsAction) -> (String, i32) {
27 let mut views = match ViewManager::new(config) {
28 Ok(views) => views,
29 Err(e) => return (format!("{e}\n"), 1),
30 };
31 match action {
32 ViewsAction::List => {
33 let all = views.all_views();
34 if all.is_empty() {
35 return ("No saved views\n".into(), 0);
36 }
37 let width = all
38 .iter()
39 .map(|v| v.name.chars().count())
40 .max()
41 .unwrap_or(0);
42 let mut out = String::new();
43 for view in all {
44 out.push_str(&format!(
45 "{:<width$} {}\n",
46 view.name,
47 matches(&view.match_criteria)
48 ));
49 }
50 (out, 0)
51 }
52 ViewsAction::Rm { name } => {
53 let Some(id) = views.get_view_by_name(name).map(|v| v.id.clone()) else {
54 return (
55 format!("No saved view named \"{name}\"; `datui views list` lists them\n"),
56 1,
57 );
58 };
59 match views.delete_view(&id) {
60 Ok(()) => (format!("Removed {name}\n"), 0),
61 Err(e) => (format!("{e}\n"), 1),
62 }
63 }
64 ViewsAction::Clear => {
65 let count = views.all_views().len();
66 match views.remove_all_views() {
67 Ok(()) => (format!("Removed {count} saved views\n"), 0),
68 Err(e) => (format!("{e}\n"), 1),
69 }
70 }
71 }
72}
73
74fn matches(criteria: &MatchCriteria) -> String {
76 let mut parts: Vec<String> = Vec::new();
77 if let Some(path) = &criteria.exact_path {
78 parts.push(path.display().to_string());
79 } else if let Some(path) = &criteria.relative_path {
80 parts.push(path.clone());
81 }
82 if let Some(pattern) = criteria
83 .path_pattern
84 .as_ref()
85 .or(criteria.filename_pattern.as_ref())
86 {
87 parts.push(pattern.clone());
88 }
89 if let Some(columns) = &criteria.schema_columns {
90 parts.push(format!("{} columns", columns.len()));
91 }
92 if parts.is_empty() {
93 "any file".into()
94 } else {
95 parts.join(", ")
96 }
97}
98
99pub fn man(
105 page: Option<&str>,
106 list: bool,
107 dir: Option<&std::path::Path>,
108 terminal: bool,
109) -> (String, i32) {
110 use datui_cli::man::{PAGES, find};
111 if list {
112 let width = PAGES.iter().map(|p| p.title().len()).max().unwrap_or(0);
113 let text = PAGES
114 .iter()
115 .map(|p| format!("{:width$} {}\n", p.title(), p.summary()))
116 .collect();
117 return (text, 0);
118 }
119 if let Some(dir) = dir {
120 let written: std::io::Result<()> = PAGES.iter().try_for_each(|p| {
121 let section = dir.join(format!("man{}", p.section));
122 std::fs::create_dir_all(§ion)?;
123 std::fs::write(section.join(p.file_name()), p.roff())
124 });
125 return match written {
126 Ok(()) => (
127 format!("Wrote {} pages under {}\n", PAGES.len(), dir.display()),
128 0,
129 ),
130 Err(e) => (format!("{}: {e}\n", dir.display()), 1),
131 };
132 }
133 let name = page.unwrap_or("datui");
134 let Some(page) = find(name) else {
135 return (
136 format!("No manual page {name}; `datui man --list` lists them\n"),
137 1,
138 );
139 };
140 if terminal {
141 if let Some(code) = show_with_man(page) {
142 return (String::new(), code);
143 }
144 let width = crossterm::terminal::size().map_or(80, |(cols, _)| usize::from(cols));
145 let text = page.plain(width.clamp(40, 100));
146 if show_with_pager(&text) {
147 return (String::new(), 0);
148 }
149 return (text, 0);
150 }
151 (page.roff(), 0)
152}
153
154fn show_with_pager(text: &str) -> bool {
156 pagers()
157 .into_iter()
158 .any(|mut pager| page_through(&mut pager, text))
159}
160
161fn pagers() -> Vec<std::process::Command> {
164 use std::process::Command;
165 let mut out = Vec::new();
166 if let Some(pager) = std::env::var("PAGER").ok().filter(|p| !p.trim().is_empty()) {
167 if cfg!(unix) {
168 let mut sh = Command::new("sh");
169 sh.arg("-c").arg(&pager);
170 out.push(sh);
171 } else {
172 let mut words = pager.split_whitespace();
173 if let Some(program) = words.next() {
174 let mut command = Command::new(program);
175 command.args(words);
176 out.push(command);
177 }
178 }
179 }
180 let mut less = Command::new("less");
181 if std::env::var_os("LESS").is_none() {
183 less.env("LESS", "FRX");
184 }
185 out.push(less);
186 out.push(Command::new("more"));
187 out
188}
189
190fn page_through(pager: &mut std::process::Command, text: &str) -> bool {
192 use std::io::Write;
193 let Ok(mut child) = pager.stdin(std::process::Stdio::piped()).spawn() else {
194 return false;
195 };
196 if let Some(mut stdin) = child.stdin.take() {
197 let _ = stdin.write_all(text.as_bytes());
199 }
200 let _ = child.wait();
201 true
202}
203
204fn show_with_man(page: &datui_cli::man::Page) -> Option<i32> {
207 let dir = tempfile::Builder::new()
208 .prefix("datui-man-")
209 .tempdir()
210 .ok()?;
211 let path = dir.path().join(page.file_name());
212 std::fs::write(&path, page.roff()).ok()?;
213 let status = std::process::Command::new("man").arg(&path).status().ok()?;
215 Some(status.code().unwrap_or(1))
216}
217
218#[cfg(test)]
219mod tests;