forked from dathere/qsv
-
Notifications
You must be signed in to change notification settings - Fork 0
/
prompt.rs
204 lines (174 loc) · 7.66 KB
/
prompt.rs
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
static USAGE: &str = r#"
Open a file dialog to pick a file as input or save to an output file.
Examples:
Pick a single file as input to qsv stats using an INPUT file dialog,
pipe into qsv stats using qsv prompt:
qsv prompt | qsv stats | qsv table
If you want to save the output of a command to a file using a save file OUTPUT dialog,
pipe into qsv prompt using the --fd-output flag:
qsv prompt -m 'Pick a CSV file to summarize' | qsv stats -E | qsv prompt --fd-output
Prompt for a spreadsheet, and export to CSV using a save file dialog:
qsv prompt -m 'Select a spreadsheet to export to CSV' -F xlsx,xls,ods | \
qsv excel - | qsv prompt -m 'Save exported CSV to...' --fd-output
Usage:
qsv prompt [options]
qsv prompt --help
prompt options:
-m, --msg <arg> The prompt message to display in the file dialog title.
When not using --fd-output, the default is "Select a File".
When using --fd-output, the default is "Save File As".
-F, --filters <arg> The filter to use for the INPUT file dialog. Set to "None" to
disable filters. Filters are comma-delimited file extensions.
[default: csv,tsv,tab,ssv,xls,xlsx,ods]
-d, --workdir <dir> The directory to start the file dialog in.
[default: .]
-f, --fd-output Write output to a file by using a save file dialog.
Used when piping into qsv prompt. Mutually exclusive with --output.
--save-fname <file> The filename to save the output as when using --fd-output.
[default: output.csv]
--base-delay-ms <ms> The base delay in milliseconds to use when opening INPUT dialog.
This is to ensure that the INPUT dialog is shown before/over the
OUTPUT dialog when using the prompt command is used in both INPUT
and OUTPUT modes in a single pipeline.
[default: 200]
Common options:
-h, --help Display this message
-o, --output <file> Write output to <file> without showing a save dialog.
Mutually exclusive with --fd-output.
-Q, --quiet Do not print --fd-output message to stderr.
"#;
use std::{
fs,
io::{self, Read, Write},
path::PathBuf,
};
use rfd::FileDialog;
use crate::{util, CliResult, Deserialize};
#[derive(Deserialize)]
#[allow(clippy::struct_field_names)]
struct Args {
flag_msg: Option<String>,
flag_workdir: PathBuf,
flag_filters: String,
flag_fd_output: bool,
flag_output: Option<PathBuf>,
flag_save_fname: String,
flag_base_delay_ms: u64,
flag_quiet: bool,
}
const DEFAULT_INPUT_TITLE: &str = "Select a File";
const DEFAULT_OUTPUT_TITLE: &str = "Save File As";
fn copy_file_to_output(input_path: Option<PathBuf>, output_path: Option<PathBuf>) -> CliResult<()> {
let mut buffer: Vec<u8> = Vec::new();
let input_filename = if let Some(input_path) = input_path {
if output_path.is_none() {
// we are copying from file to stdout
// so read the file into buffer
let mut file = std::fs::File::open(&input_path)?;
file.read_to_end(&mut buffer)?;
}
input_path
} else {
// its stdin, copy to buffer
io::stdin().read_to_end(&mut buffer)?;
PathBuf::new()
};
if let Some(output_path) = output_path {
if input_filename.exists() {
// both input and output are files
// use fs::copy to copy from input file to output file
fs::copy(&input_filename, &output_path)?;
} else {
// we copied stdin into a buffer, write to output file
let mut file = std::fs::File::create(&output_path)?;
file.write_all(&buffer)?;
file.flush()?;
}
} else if input_filename.exists() {
// we copied from file, copy to stdout
let mut input_file = std::fs::File::open(input_filename)?;
let mut stdout = io::stdout();
std::io::copy(&mut input_file, &mut stdout)?;
} else {
// we copied stdin into a buffer, write buffer to stdout
io::stdout().write_all(&buffer)?;
io::stdout().flush()?;
}
Ok(())
}
pub fn run(argv: &[&str]) -> CliResult<()> {
let args: Args = util::get_args(USAGE, argv)?;
if args.flag_fd_output && args.flag_output.is_some() {
return fail_incorrectusage_clierror!(
"Cannot use --fd-output (-f) and --output (-o) together, choose one."
);
}
let mut input_path = None;
if !(args.flag_fd_output || args.flag_output.is_some()) {
// prompt for input
let title = args
.flag_msg
.clone()
.unwrap_or_else(|| DEFAULT_INPUT_TITLE.to_owned());
// piped commands are actually launched in parallel and not executed sequentially
// from left to right as commonly thought. So we need to introduce a delay to ensure
// that the input dialog is not opened before the save dialog.
// e.g. cat file.csv | qsv prompt | qsv stats | qsv prompt --skip-input --fd-output
// The delay ensures that the prompt for input is shown before the prompt for output.
std::thread::sleep(std::time::Duration::from_millis(args.flag_base_delay_ms));
let mut fd = FileDialog::new()
.set_directory(args.flag_workdir.clone())
.set_title(title.clone());
if !args.flag_filters.eq_ignore_ascii_case("none") {
let ext_comma_delimited: Vec<&str> = args.flag_filters.split(',').collect();
let ext_slice: &[&str] = &ext_comma_delimited;
if !ext_slice.is_empty() {
fd = fd.add_filter("Filter".to_string(), ext_slice);
}
}
input_path = fd.pick_file();
if input_path.is_none() {
let err_msg = if title == DEFAULT_INPUT_TITLE {
"Prompt error. Perhaps you did not select a file for input?".to_string()
} else {
format!(r#"Prompt error. Perhaps you did not select a file for input? "{title}""#)
};
return fail_clierror!("{err_msg}");
}
}
if args.flag_fd_output {
let title = args
.flag_msg
.unwrap_or_else(|| DEFAULT_OUTPUT_TITLE.to_owned());
#[cfg(not(target_os = "macos"))]
let fd = FileDialog::new()
.set_directory(args.flag_workdir)
.set_title(title)
.set_file_name(args.flag_save_fname);
#[cfg(target_os = "macos")]
let fd = FileDialog::new()
.set_directory(args.flag_workdir)
.set_title(title)
.set_file_name(args.flag_save_fname)
.set_can_create_directories(true);
// no delay here, we want the save dialog to appear immediately
// the delay is only for input dialogs, so they pop over in reverse order
if let Some(output_path) = fd.save_file() {
copy_file_to_output(None, Some(output_path.clone()))?;
if !args.flag_quiet {
winfo!("Output saved to: {:?}", output_path);
}
} else {
return fail_clierror!(
"Error while running qsv prompt. Perhaps you did not select a file for output?"
);
}
} else if args.flag_output.is_some() {
// If --output then write to output
copy_file_to_output(None, args.flag_output)?;
} else {
// If not --output or --fd-output then write to stdout
copy_file_to_output(input_path, None)?;
}
Ok(())
}