Skip to content

Commit 5aea719

Browse files
committed
Add file picker module for CLI file selection
Introduces src/cli/file_picker.rs, providing FilePickerOptions, FilePickerResult, and functions for selecting files via GUI or TTY. Also registers the new file_picker module in src/lib.rs. This enables flexible file selection for CLI workflows, supporting both single and multiple file picking.
1 parent b2a3471 commit 5aea719

2 files changed

Lines changed: 244 additions & 0 deletions

File tree

src/cli/file_picker.rs

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
use std::path::PathBuf;
2+
3+
use crate::file::UploadFile;
4+
5+
/// Configuration options for file selection.
6+
///
7+
/// This struct controls how files are selected, including whether multiple files can be picked
8+
/// and whether to use a GUI dialog or command-line prompt.
9+
#[derive(Debug, Clone)]
10+
pub struct FilePickerOptions {
11+
/// If `true`, allow selecting multiple files.
12+
pub multi: bool,
13+
/// If `true`, force TTY prompt instead of GUI dialog.
14+
pub no_gui: bool,
15+
}
16+
17+
impl Default for FilePickerOptions {
18+
/// Returns the default configuration for file picking (single file, GUI allowed).
19+
fn default() -> Self {
20+
Self {
21+
multi: false,
22+
no_gui: false,
23+
}
24+
}
25+
}
26+
27+
impl FilePickerOptions {
28+
/// Creates a new `FilePickerOptions` with default settings (single file, GUI allowed).
29+
pub fn new() -> Self {
30+
Self::default()
31+
}
32+
33+
/// Creates options for multiple file selection.
34+
pub fn as_multi() -> Self {
35+
Self {
36+
multi: true,
37+
no_gui: false,
38+
}
39+
}
40+
41+
/// Creates options for single file selection.
42+
pub fn as_single() -> Self {
43+
Self::default()
44+
}
45+
46+
/// Sets whether to allow multiple file selection.
47+
pub fn multi(mut self, multi: bool) -> Self {
48+
self.multi = multi;
49+
self
50+
}
51+
52+
/// Sets whether to disable the GUI file picker.
53+
pub fn no_gui(mut self, no_gui: bool) -> Self {
54+
self.no_gui = no_gui;
55+
self
56+
}
57+
}
58+
59+
/// The result of a file picking operation.
60+
///
61+
/// This enum represents either a single file or multiple files selected by the user.
62+
pub enum FilePickerResult {
63+
/// A single file was selected.
64+
Single(PathBuf),
65+
/// Multiple files were selected.
66+
Multiple(Vec<PathBuf>),
67+
}
68+
69+
impl From<Vec<PathBuf>> for FilePickerResult {
70+
/// Converts a vector of `PathBuf` into a `FilePickerResult::Multiple`.
71+
fn from(files: Vec<PathBuf>) -> Self {
72+
FilePickerResult::Multiple(files)
73+
}
74+
}
75+
76+
impl From<PathBuf> for FilePickerResult {
77+
/// Converts a single `PathBuf` into a `FilePickerResult::Single`.
78+
fn from(file: PathBuf) -> Self {
79+
FilePickerResult::Single(file)
80+
}
81+
}
82+
83+
impl Into<Vec<UploadFile>> for FilePickerResult {
84+
/// Converts a `FilePickerResult` into a vector of `UploadFile`.
85+
fn into(self) -> Vec<UploadFile> {
86+
match self {
87+
FilePickerResult::Single(path) => vec![UploadFile::from(path)],
88+
FilePickerResult::Multiple(paths) => paths.into_iter().map(UploadFile::from).collect(),
89+
}
90+
}
91+
}
92+
93+
impl Into<UploadFile> for FilePickerResult {
94+
/// Converts a `FilePickerResult::Single` into an `UploadFile`.
95+
///
96+
/// # Panics
97+
///
98+
/// Panics if called on `FilePickerResult::Multiple`.
99+
fn into(self) -> UploadFile {
100+
match self {
101+
FilePickerResult::Single(path) => UploadFile::from(path),
102+
FilePickerResult::Multiple(_) => {
103+
panic!("Multiple files are not supported for this operation.");
104+
}
105+
}
106+
}
107+
}
108+
109+
/// Picks a file for upload, converting the result to the specified type.
110+
///
111+
/// This is a convenience wrapper around `pick_files` that handles conversion to the desired type.
112+
///
113+
/// # Arguments
114+
///
115+
/// * `options` - The file picker configuration options.
116+
/// * `files` - A vector of pre-selected file paths.
117+
///
118+
/// # Returns
119+
///
120+
/// An `Option<T>` containing the converted result, or `None` if no files were selected.
121+
pub fn pick_upload_file<T>(options: &FilePickerOptions, files: Vec<PathBuf>) -> Option<T>
122+
where
123+
FilePickerResult: Into<T>,
124+
{
125+
pick_files(options, files).map(|result| result.into())
126+
}
127+
128+
/// Picks files based on the provided options and optional pre-selected files.
129+
///
130+
/// The selection priority is as follows:
131+
/// 1. If `files` contains paths, use them directly.
132+
/// 2. If GUI is allowed and available, show GUI file picker dialog.
133+
/// 3. Otherwise, use TTY prompt.
134+
///
135+
/// # Arguments
136+
///
137+
/// * `options` - The file picker configuration options.
138+
/// * `files` - A vector of pre-selected file paths.
139+
///
140+
/// # Returns
141+
///
142+
/// An `Option<FilePickerResult>` containing the selected files, or `None` if no files were selected.
143+
pub fn pick_files(options: &FilePickerOptions, files: Vec<PathBuf>) -> Option<FilePickerResult> {
144+
if !files.is_empty() {
145+
return Some(files.into());
146+
}
147+
148+
if !options.no_gui && gui_probably_available() {
149+
if let Some(result) = pick_files_gui(options.multi) {
150+
return Some(result);
151+
}
152+
eprintln!("No file selected via GUI; falling back to TTY prompt…");
153+
}
154+
155+
Some(pick_file_tty(options.multi).into())
156+
}
157+
158+
/// Checks if a GUI is probably available on the current platform.
159+
///
160+
/// On Linux, this checks for the presence of the `DISPLAY` or `WAYLAND_DISPLAY` environment variables.
161+
/// On macOS and Windows, this always returns `true`.
162+
/// On other platforms, this returns `false`.
163+
fn gui_probably_available() -> bool {
164+
#[cfg(target_os = "linux")]
165+
{
166+
std::env::var_os("DISPLAY").is_some() || std::env::var_os("WAYLAND_DISPLAY").is_some()
167+
}
168+
#[cfg(any(target_os = "macos", target_os = "windows"))]
169+
{
170+
true
171+
}
172+
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
173+
{
174+
false
175+
}
176+
}
177+
178+
/// Shows a GUI file picker dialog to the user.
179+
///
180+
/// If `multi` is `true`, allows selecting multiple files. Otherwise, only a single file can be selected.
181+
///
182+
/// # Arguments
183+
///
184+
/// * `multi` - Whether to allow multiple file selection.
185+
///
186+
/// # Returns
187+
///
188+
/// An `Option<FilePickerResult>` containing the selected files, or `None` if no files were selected.
189+
fn pick_files_gui(multi: bool) -> Option<FilePickerResult> {
190+
let dlg = rfd::FileDialog::new();
191+
192+
if multi {
193+
dlg.pick_files().map(|files| files.into())
194+
} else {
195+
dlg.pick_file().map(|file| file.into())
196+
}
197+
}
198+
199+
/// Prompts the user for file path(s) via TTY (command-line).
200+
///
201+
/// If `multi` is `true`, prompts for comma-separated file paths. Otherwise, prompts for a single file path
202+
/// and validates that the path exists.
203+
///
204+
/// # Arguments
205+
///
206+
/// * `multi` - Whether to allow multiple file selection.
207+
///
208+
/// # Returns
209+
///
210+
/// A vector of `PathBuf` containing the selected file paths.
211+
fn pick_file_tty(multi: bool) -> Vec<PathBuf> {
212+
use inquire::{validator::Validation, Text};
213+
214+
if multi {
215+
let input = Text::new("Enter file paths (comma-separated):")
216+
.prompt()
217+
.unwrap_or_default();
218+
input
219+
.split(',')
220+
.map(|s| s.trim())
221+
.filter(|s| !s.is_empty())
222+
.map(PathBuf::from)
223+
.collect()
224+
} else {
225+
let path_str = Text::new("Enter a file path:")
226+
.with_validator(|input: &str| {
227+
if PathBuf::from(input).exists() {
228+
Ok(Validation::Valid)
229+
} else {
230+
Ok(Validation::Invalid("Path does not exist".into()))
231+
}
232+
})
233+
.prompt()
234+
.unwrap_or_default();
235+
236+
if path_str.is_empty() {
237+
vec![]
238+
} else {
239+
vec![PathBuf::from(path_str)]
240+
}
241+
}
242+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ pub mod cli {
226226
pub mod dataset;
227227
/// File commands
228228
pub mod file;
229+
/// File picker functionality
230+
pub mod file_picker;
229231
/// Information commands
230232
pub mod info;
231233
}

0 commit comments

Comments
 (0)