file_preview/
file_preview.rs

1use std::{
2    fs::read_to_string,
3    io::{self, stdout},
4    path::Path,
5};
6
7use crossterm::{
8    event::{read, Event, KeyCode},
9    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
10    ExecutableCommand,
11};
12use ratatui::{prelude::*, widgets::*};
13
14use copypasta::{ClipboardContext, ClipboardProvider};
15use fpicker::{File, FileExplorer, Theme};
16
17fn main() -> io::Result<()> {
18    enable_raw_mode()?;
19    stdout().execute(EnterAlternateScreen)?;
20
21    let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
22    let layout = Layout::vertical([
23        Constraint::Ratio(4, 5), // Main file explorer and content area
24        Constraint::Ratio(1, 5), // Bottom window for selected file paths
25    ]);
26
27    // Inner layout for the top section
28    let top_layout = Layout::horizontal([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
29
30    // Create a new file explorer with the default theme and title.
31    let theme = get_theme();
32    let mut file_explorer = FileExplorer::with_theme(theme)?;
33
34    loop {
35        // Get the content of the current selected file (if it's indeed a file).
36        let file_content = get_file_content(file_explorer.current().path())?;
37        let selected_files: String = file_explorer
38            .selected_files()
39            .iter()
40            .map(|file| file.path().display().to_string())
41            .collect::<Vec<String>>()
42            .join("\n");
43
44        // Render the file explorer widget, file content, and selected file paths.
45        terminal.draw(|f| {
46            let chunks = layout.split(f.area());
47            let top_chunks = top_layout.split(chunks[0]);
48
49            // Top section
50            f.render_widget(&file_explorer.widget(), top_chunks[0]);
51            f.render_widget(
52                Paragraph::new(file_content).block(
53                    Block::default()
54                        .borders(Borders::ALL)
55                        .border_type(BorderType::Double),
56                ),
57                top_chunks[1],
58            );
59
60            // Bottom section
61            f.render_widget(
62                Paragraph::new(selected_files.clone()).block(
63                    Block::default()
64                        .borders(Borders::ALL)
65                        .title("q:Quit, c: copy selected file contents to clipboard, p: copy paths to clipboard"),
66                ),
67                chunks[1],
68            );
69        })?;
70
71        // Read the next event from the terminal.
72        let event = read()?;
73        if let Event::Key(key) = event {
74            match key.code {
75                KeyCode::Char('q') => break, // Quit the application
76                KeyCode::Char('c') => {
77                    // Copy selected file paths to clipboard
78                    if let Ok(mut clipboard) = ClipboardContext::new() {
79                        if let Err(err) = clipboard.set_contents(get_selected_files_content(
80                            &file_explorer.selected_files(),
81                        )) {
82                            eprintln!("Failed to copy to clipboard: {}", err);
83                        }
84                    } else {
85                        eprintln!("Clipboard not available.");
86                    }
87                }
88                KeyCode::Char('p') => {
89                    // Copy selected file paths to clipboard
90                    if let Ok(mut clipboard) = ClipboardContext::new() {
91                        if let Err(err) = clipboard.set_contents(selected_files.clone()) {
92                            eprintln!("Failed to copy to clipboard: {}", err);
93                        }
94                    } else {
95                        eprintln!("Clipboard not available.");
96                    }
97                }
98                _ => {}
99            }
100        }
101        // Handle the event in the file explorer.
102        file_explorer.handle(&event)?;
103    }
104
105    disable_raw_mode()?;
106    stdout().execute(LeaveAlternateScreen)?;
107    // Print the selected file paths to stdout.
108    for file in file_explorer.selected_files() {
109        println!("{}", file.path().display());
110    }
111    Ok(())
112}
113
114fn get_file_content(path: &Path) -> io::Result<String> {
115    let mut content = String::new();
116
117    // If the path is a file, read its content.
118    if path.is_file() {
119        content = read_to_string(path)?;
120    }
121
122    Ok(content)
123}
124fn get_selected_files_content(selected_files: &Vec<File>) -> String {
125    selected_files
126        .iter()
127        .map(|file| {
128            let path = file.path().display().to_string();
129            let content = get_file_content(file.path())
130                .unwrap_or_else(|_| "Unable to read file.".to_string());
131            format!("File: {}\nContent:\n{}\n", path, content)
132        })
133        .collect::<Vec<String>>()
134        .join("\n")
135}
136
137fn get_theme() -> Theme {
138    Theme::default()
139        .with_block(Block::default().borders(Borders::ALL))
140        .with_dir_style(
141            Style::default()
142                .fg(Color::White)
143                .add_modifier(Modifier::BOLD),
144        )
145        .with_highlight_dir_style(
146            Style::default()
147                .fg(Color::White)
148                .add_modifier(Modifier::BOLD)
149                .bg(Color::DarkGray),
150        )
151}