diff --git a/ctru-rs/Cargo.toml b/ctru-rs/Cargo.toml index cd27eb5..c10e13e 100644 --- a/ctru-rs/Cargo.toml +++ b/ctru-rs/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["rlib"] name = "ctru" [dependencies] +cfg-if = "1.0" ctru-sys = { path = "../ctru-sys", version = "0.4" } const-zero = "0.1.0" linker-fix-3ds = { git = "https://github.com/Meziu/rust-linker-fix-3ds.git" } @@ -19,6 +20,13 @@ libc = "0.2" bitflags = "1.0.0" widestring = "0.2.2" +[build-dependencies] +toml = "0.5" + [dev-dependencies] ferris-says = "0.2.1" time = "0.3.7" + +[features] +default = ["romfs"] +romfs = [] diff --git a/ctru-rs/build.rs b/ctru-rs/build.rs new file mode 100644 index 0000000..ecdef33 --- /dev/null +++ b/ctru-rs/build.rs @@ -0,0 +1,32 @@ +use std::path::PathBuf; + +fn main() { + // Open Cargo.toml + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let manifest_path = format!("{manifest_dir}/Cargo.toml"); + let manifest_str = std::fs::read_to_string(&manifest_path) + .unwrap_or_else(|e| panic!("Could not open {manifest_path}: {e}")); + let manifest_data: toml::Value = + toml::de::from_str(&manifest_str).expect("Could not parse Cargo manifest as TOML"); + + // Find the romfs setting and compute the path + let romfs_dir_setting = manifest_data + .as_table() + .and_then(|table| table.get("package")) + .and_then(toml::Value::as_table) + .and_then(|table| table.get("metadata")) + .and_then(toml::Value::as_table) + .and_then(|table| table.get("cargo-3ds")) + .and_then(toml::Value::as_table) + .and_then(|table| table.get("romfs_dir")) + .and_then(toml::Value::as_str) + .unwrap_or("romfs"); + let romfs_path = PathBuf::from(format!("{manifest_dir}/{romfs_dir_setting}")); + + // Check if the romfs path exists so we can compile the module + if romfs_path.exists() { + println!("cargo:rustc-cfg=romfs_exists"); + } + + println!("cargo:rerun-if-changed={}", manifest_dir); +} diff --git a/ctru-rs/examples/file-explorer.rs b/ctru-rs/examples/file-explorer.rs new file mode 100644 index 0000000..8a249f9 --- /dev/null +++ b/ctru-rs/examples/file-explorer.rs @@ -0,0 +1,178 @@ +//! A file explorer which shows off using standard library file system APIs to +//! read the SD card. + +use ctru::applets::swkbd::{Button, Swkbd}; +use ctru::console::Console; +use ctru::services::hid::KeyPad; +use ctru::services::{Apt, Hid}; +use ctru::Gfx; +use std::fs::DirEntry; +use std::path::{Path, PathBuf}; + +fn main() { + ctru::init(); + let apt = Apt::init().unwrap(); + let hid = Hid::init().unwrap(); + let gfx = Gfx::default(); + + #[cfg(all(feature = "romfs", romfs_exists))] + let _romfs = ctru::romfs::RomFS::new().unwrap(); + + FileExplorer::init(&apt, &hid, &gfx).run(); +} + +struct FileExplorer<'a> { + apt: &'a Apt, + hid: &'a Hid, + gfx: &'a Gfx, + console: Console<'a>, + path: PathBuf, + entries: Vec, + running: bool, +} + +impl<'a> FileExplorer<'a> { + fn init(apt: &'a Apt, hid: &'a Hid, gfx: &'a Gfx) -> Self { + gfx.top_screen.borrow_mut().set_wide_mode(true); + let console = Console::init(gfx.top_screen.borrow_mut()); + + FileExplorer { + apt, + hid, + gfx, + console, + path: PathBuf::from("/"), + entries: Vec::new(), + running: false, + } + } + + fn run(&mut self) { + self.running = true; + self.print_menu(); + + while self.running && self.apt.main_loop() { + self.hid.scan_input(); + let input = self.hid.keys_down(); + + if input.contains(KeyPad::KEY_START) { + break; + } else if input.contains(KeyPad::KEY_B) { + self.path.pop(); + self.console.clear(); + self.print_menu(); + } else if input.contains(KeyPad::KEY_A) { + self.get_input_and_run(Self::set_next_path); + } else if input.contains(KeyPad::KEY_X) { + self.get_input_and_run(Self::set_exact_path); + } + + self.gfx.flush_buffers(); + self.gfx.swap_buffers(); + self.gfx.wait_for_vblank(); + } + } + + fn print_menu(&mut self) { + println!("Viewing {}", self.path.display()); + + 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!("{:2} - {}", i, entry.file_name().to_string_lossy()); + self.entries.push(entry); + + // Paginate the output + if (i + 1) % 20 == 0 { + 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::KEY_A) { + break; + } + + if input.contains(KeyPad::KEY_START) { + self.running = false; + return; + } + + self.gfx.wait_for_vblank(); + } + } + } + Err(e) => { + println!("{} - Error: {}", i, e); + } + } + } + + println!("Start to exit, A to select an entry by number, B to go up a directory, X to set the path."); + } + + fn get_input_and_run(&mut self, action: impl FnOnce(&mut Self, String)) { + let mut keyboard = Swkbd::default(); + let mut new_path_str = String::new(); + + match keyboard.get_utf8(&mut new_path_str) { + Ok(Button::Right) => { + // Clicked "OK" + action(self, new_path_str); + } + 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; + } + }; + + if !next_entry.file_type().unwrap().is_dir() { + println!("Not a directory: {}", next_path_index); + 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(); + } +} diff --git a/ctru-rs/examples/thread-basic.rs b/ctru-rs/examples/thread-basic.rs new file mode 100644 index 0000000..a96d15f --- /dev/null +++ b/ctru-rs/examples/thread-basic.rs @@ -0,0 +1,47 @@ +use ctru::console::Console; +use ctru::gfx::Gfx; +use ctru::services::apt::Apt; +use ctru::services::hid::{Hid, KeyPad}; +use ctru::thread; + +use std::time::Duration; + +fn main() { + // Initialize services + ctru::init(); + let apt = Apt::init().unwrap(); + let hid = Hid::init().unwrap(); + let gfx = Gfx::default(); + let _console = Console::init(gfx.top_screen.borrow_mut()); + + let prio = thread::current().priority(); + println!("Main thread prio: {}\n", prio); + + for ix in 0..3 { + thread::Builder::new() + .priority(prio - 1) + .spawn(move || { + let sleep_duration: u64 = 1000 + ix * 250; + let mut i = 0; + loop { + println!("Thread{ix} says {i}"); + i += 1; + thread::sleep(Duration::from_millis(sleep_duration)); + } + }) + .unwrap(); + + println!("Created thread {ix}"); + } + + while apt.main_loop() { + gfx.flush_buffers(); + gfx.swap_buffers(); + gfx.wait_for_vblank(); + + hid.scan_input(); + if hid.keys_down().contains(KeyPad::KEY_START) { + break; + } + } +} diff --git a/ctru-rs/examples/time_rtc.rs b/ctru-rs/examples/time-rtc.rs similarity index 100% rename from ctru-rs/examples/time_rtc.rs rename to ctru-rs/examples/time-rtc.rs diff --git a/ctru-rs/romfs/test-file.txt b/ctru-rs/romfs/test-file.txt new file mode 100644 index 0000000..30d74d2 --- /dev/null +++ b/ctru-rs/romfs/test-file.txt @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/ctru-rs/src/lib.rs b/ctru-rs/src/lib.rs index c2c8243..23be6b8 100644 --- a/ctru-rs/src/lib.rs +++ b/ctru-rs/src/lib.rs @@ -33,13 +33,30 @@ pub mod applets; pub mod console; pub mod error; pub mod gfx; -pub mod sdmc; pub mod services; pub mod srv; pub mod thread; +cfg_if::cfg_if! { + if #[cfg(all(feature = "romfs", romfs_exists))] { + pub mod romfs; + } else { + pub mod romfs { + //! The RomFS folder has not been detected and/or the `romfs` feature has not been enabled. + //! + //! Configure the path in Cargo.toml (the default path is "romfs"). Paths are relative to the + //! `CARGO_MANIFEST_DIR` environment variable, which is the directory containing the manifest of + //! your package. + //! + //! ```toml + //! [package.metadata.cargo-3ds] + //! romfs_dir = "romfs" + //! ``` + } + } +} + pub use crate::error::{Error, Result}; pub use crate::gfx::Gfx; -pub use crate::sdmc::Sdmc; pub use crate::srv::Srv; diff --git a/ctru-rs/src/romfs.rs b/ctru-rs/src/romfs.rs new file mode 100644 index 0000000..12190b9 --- /dev/null +++ b/ctru-rs/src/romfs.rs @@ -0,0 +1,36 @@ +//! This module only gets compiled if the configured RomFS directory is found and the `romfs` +//! feature is enabled. +//! +//! Configure the path in Cargo.toml (the default path is "romfs"). Paths are relative to the +//! `CARGO_MANIFEST_DIR` environment variable, which is the directory containing the manifest of +//! your package. +//! +//! ```toml +//! [package.metadata.cargo-3ds] +//! romfs_dir = "romfs" +//! ``` + +use std::ffi::CStr; + +#[non_exhaustive] +pub struct RomFS; + +impl RomFS { + pub fn new() -> crate::Result { + let mount_name = CStr::from_bytes_with_nul(b"romfs\0").unwrap(); + let result = unsafe { ctru_sys::romfsMountSelf(mount_name.as_ptr()) }; + + if result < 0 { + Err(result.into()) + } else { + Ok(Self) + } + } +} + +impl Drop for RomFS { + fn drop(&mut self) { + let mount_name = CStr::from_bytes_with_nul(b"romfs\0").unwrap(); + unsafe { ctru_sys::romfsUnmount(mount_name.as_ptr()) }; + } +} diff --git a/ctru-rs/src/sdmc.rs b/ctru-rs/src/sdmc.rs deleted file mode 100644 index eaef331..0000000 --- a/ctru-rs/src/sdmc.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub struct Sdmc(()); - -impl Sdmc { - pub fn init() -> crate::Result { - unsafe { - let r = ctru_sys::archiveMountSdmc(); - if r < 0 { - Err(r.into()) - } else { - Ok(Sdmc(())) - } - } - } -} - -impl Drop for Sdmc { - fn drop(&mut self) { - unsafe { ctru_sys::archiveUnmountAll() }; - } -} diff --git a/ctru-rs/src/thread.rs b/ctru-rs/src/thread.rs index e12a554..225b1f8 100644 --- a/ctru-rs/src/thread.rs +++ b/ctru-rs/src/thread.rs @@ -10,8 +10,7 @@ //! 3DS-specific threading API //! -//! While it is possible to create threads on the 3DS using functions found in -//! `std::thread`, the standard API does not expose the ability to set a thread's +//! The standard API does not expose the ability to set a thread's //! priority level and to pin a thread to a specific CPU core. This module exists //! to address those and other shortcomings. //!