You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
219 lines
6.4 KiB
219 lines
6.4 KiB
//! File Explorer example. |
|
//! |
|
//! This (rather complex) example creates a working text-based file explorer which shows off using standard library file system APIs to |
|
//! read the SD card and RomFS (if properly read via the `romfs:/` prefix). |
|
|
|
use ctru::applets::swkbd::{Button, SoftwareKeyboard}; |
|
use ctru::prelude::*; |
|
|
|
use std::fs::DirEntry; |
|
use std::os::horizon::fs::MetadataExt; |
|
use std::path::{Path, PathBuf}; |
|
|
|
fn main() { |
|
let apt = Apt::new().unwrap(); |
|
let mut hid = Hid::new().unwrap(); |
|
let gfx = Gfx::new().unwrap(); |
|
|
|
// Mount the RomFS if available. |
|
#[cfg(all(feature = "romfs", romfs_exists))] |
|
let _romfs = ctru::services::romfs::RomFS::new().unwrap(); |
|
|
|
FileExplorer::new(&apt, &mut hid, &gfx).run(); |
|
} |
|
|
|
struct FileExplorer<'a> { |
|
apt: &'a Apt, |
|
hid: &'a mut Hid, |
|
gfx: &'a Gfx, |
|
console: Console<'a>, |
|
path: PathBuf, |
|
entries: Vec<DirEntry>, |
|
running: bool, |
|
} |
|
|
|
impl<'a> FileExplorer<'a> { |
|
fn new(apt: &'a Apt, hid: &'a mut Hid, gfx: &'a Gfx) -> Self { |
|
let mut top_screen = gfx.top_screen.borrow_mut(); |
|
top_screen.set_wide_mode(true); |
|
let console = Console::new(top_screen); |
|
|
|
FileExplorer { |
|
apt, |
|
hid, |
|
gfx, |
|
console, |
|
path: PathBuf::from("/"), |
|
entries: Vec::new(), |
|
running: false, |
|
} |
|
} |
|
|
|
fn run(&mut self) { |
|
self.running = true; |
|
// Print the file explorer commands. |
|
self.print_menu(); |
|
|
|
while self.running && self.apt.main_loop() { |
|
self.hid.scan_input(); |
|
let input = self.hid.keys_down(); |
|
|
|
if input.contains(KeyPad::START) { |
|
break; |
|
} else if input.contains(KeyPad::B) && self.path.components().count() > 1 { |
|
self.path.pop(); |
|
self.console.clear(); |
|
self.print_menu(); |
|
// Open a directory/file to read. |
|
} else if input.contains(KeyPad::A) { |
|
self.get_input_and_run(Self::set_next_path); |
|
// Open a specific path using the `SoftwareKeyboard` applet. |
|
} else if input.contains(KeyPad::X) { |
|
self.get_input_and_run(Self::set_exact_path); |
|
} |
|
|
|
self.gfx.wait_for_vblank(); |
|
} |
|
} |
|
|
|
fn print_menu(&mut self) { |
|
match std::fs::metadata(&self.path) { |
|
Ok(metadata) => { |
|
println!( |
|
"Viewing {} (size {} bytes, mode {:#o})", |
|
self.path.display(), |
|
metadata.len(), |
|
metadata.st_mode(), |
|
); |
|
|
|
if metadata.is_file() { |
|
self.print_file_contents(); |
|
// let the user continue navigating from the parent dir |
|
// after dumping the file |
|
self.path.pop(); |
|
self.print_menu(); |
|
return; |
|
} else if metadata.is_dir() { |
|
self.print_dir_entries(); |
|
} else { |
|
println!("unsupported file type: {:?}", metadata.file_type()); |
|
} |
|
} |
|
Err(e) => { |
|
println!("Failed to read {}: {e}", self.path.display()) |
|
} |
|
}; |
|
|
|
println!("Press Start to exit, A to select an entry by number, B to go up a directory, X to set the path."); |
|
} |
|
|
|
fn print_dir_entries(&mut self) { |
|
let dir_listing = std::fs::read_dir(&self.path).expect("Failed to open path"); |
|
self.entries = Vec::new(); |
|
|
|
for (i, entry) in dir_listing.enumerate() { |
|
match entry { |
|
Ok(entry) => { |
|
println!("{i:2} - {}", entry.file_name().to_string_lossy()); |
|
self.entries.push(entry); |
|
|
|
if (i + 1) % 20 == 0 { |
|
self.wait_for_page_down(); |
|
} |
|
} |
|
Err(e) => { |
|
println!("{i} - Error: {e}"); |
|
} |
|
} |
|
} |
|
} |
|
|
|
fn print_file_contents(&mut self) { |
|
match std::fs::read_to_string(&self.path) { |
|
Ok(contents) => { |
|
println!("File contents:\n{0:->80}", ""); |
|
println!("{contents}"); |
|
println!("{0:->80}", ""); |
|
} |
|
Err(err) => { |
|
println!("Error reading file: {err}"); |
|
} |
|
} |
|
} |
|
|
|
// Paginate output. |
|
fn wait_for_page_down(&mut self) { |
|
println!("Press A to go to next page, or Start to exit"); |
|
|
|
while self.apt.main_loop() { |
|
self.hid.scan_input(); |
|
let input = self.hid.keys_down(); |
|
|
|
if input.contains(KeyPad::A) { |
|
break; |
|
} |
|
|
|
if input.contains(KeyPad::START) { |
|
self.running = false; |
|
return; |
|
} |
|
|
|
self.gfx.wait_for_vblank(); |
|
} |
|
} |
|
|
|
fn get_input_and_run(&mut self, action: impl FnOnce(&mut Self, String)) { |
|
let mut keyboard = SoftwareKeyboard::default(); |
|
|
|
match keyboard.get_string(2048, self.apt, self.gfx) { |
|
Ok((path, Button::Right)) => { |
|
// Clicked "OK". |
|
action(self, path); |
|
} |
|
Ok((_, Button::Left)) => { |
|
// Clicked "Cancel". |
|
} |
|
Ok((_, Button::Middle)) => { |
|
// This button wasn't shown. |
|
unreachable!() |
|
} |
|
Err(e) => { |
|
panic!("Error: {e:?}") |
|
} |
|
} |
|
} |
|
|
|
fn set_next_path(&mut self, next_path_index: String) { |
|
let next_path_index: usize = match next_path_index.parse() { |
|
Ok(index) => index, |
|
Err(e) => { |
|
println!("Number parsing error: {e}"); |
|
return; |
|
} |
|
}; |
|
|
|
let next_entry = match self.entries.get(next_path_index) { |
|
Some(entry) => entry, |
|
None => { |
|
println!("Input number of bounds"); |
|
return; |
|
} |
|
}; |
|
|
|
self.console.clear(); |
|
self.path = next_entry.path(); |
|
self.print_menu(); |
|
} |
|
|
|
fn set_exact_path(&mut self, new_path_str: String) { |
|
let new_path = Path::new(&new_path_str); |
|
if !new_path.is_dir() { |
|
println!("Not a directory: {new_path_str}"); |
|
return; |
|
} |
|
|
|
self.console.clear(); |
|
self.path = new_path.to_path_buf(); |
|
self.print_menu(); |
|
} |
|
}
|
|
|