diff --git a/ctru-rs/src/applets/error.rs b/ctru-rs/src/applets/error.rs new file mode 100644 index 0000000..84dc3e7 --- /dev/null +++ b/ctru-rs/src/applets/error.rs @@ -0,0 +1,153 @@ +//! Error applet. +//! +//! This applet displays error text as a pop-up message on the lower screen. + +use crate::services::{apt::Apt, gfx::Gfx}; + +use ctru_sys::errorConf; + +/// Configuration struct to set up the Error applet. +#[doc(alias = "errorConf")] +pub struct PopUp { + state: Box, +} + +/// Determines whether the Error applet will use word wrapping when displaying a message. +#[doc(alias = "errorType")] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum WordWrap { + /// Error text is centered in the error applet window and does not use word wrapping. + Disabled = ctru_sys::ERROR_TEXT, + /// Error text starts at the top of the error applet window and uses word wrapping. + Enabled = ctru_sys::ERROR_TEXT_WORD_WRAP, +} + +/// Error returned by an unsuccessful [`PopUp::launch()`]. +#[doc(alias = "errorReturnCode")] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(i8)] +pub enum Error { + /// Unknown error occurred. + Unknown = ctru_sys::ERROR_UNKNOWN, + /// Operation not supported. + NotSupported = ctru_sys::ERROR_NOT_SUPPORTED, + /// Home button pressed while [`PopUp`] was running. + HomePressed = ctru_sys::ERROR_HOME_BUTTON, + /// Power button pressed while [`PopUp`] was running. + PowerPressed = ctru_sys::ERROR_POWER_BUTTON, + /// Reset button pressed while [`PopUp`] was running. + ResetPressed = ctru_sys::ERROR_SOFTWARE_RESET, +} + +impl PopUp { + /// Initializes the error applet with the provided word wrap setting. + #[doc(alias = "errorInit")] + pub fn new(word_wrap: WordWrap) -> Self { + let mut state = Box::::default(); + + unsafe { ctru_sys::errorInit(state.as_mut(), word_wrap as _, 0) }; + + Self { state } + } + + /// Sets the error text to display. + /// + /// # Notes + /// + /// The text will be converted to UTF-16 for display with the applet, and the message will be truncated if it exceeds + /// 1900 UTF-16 code units in length after conversion. + #[doc(alias = "errorText")] + pub fn set_text(&mut self, text: &str) { + for (idx, code_unit) in text + .encode_utf16() + .take(self.state.Text.len() - 1) + .chain(std::iter::once(0)) + .enumerate() + { + self.state.Text[idx] = code_unit; + } + } + + /// Launches the error applet. + #[doc(alias = "errorDisp")] + pub fn launch(&mut self, _apt: &Apt, _gfx: &Gfx) -> Result<(), Error> { + unsafe { self.launch_unchecked() } + } + + /// Launches the error applet without requiring an [`Apt`] or [`Gfx`] handle. + /// + /// # Safety + /// + /// Potentially leads to undefined behavior if the aforementioned services are not actually active when the applet launches. + unsafe fn launch_unchecked(&mut self) -> Result<(), Error> { + unsafe { ctru_sys::errorDisp(self.state.as_mut()) }; + + match self.state.returnCode { + ctru_sys::ERROR_NONE | ctru_sys::ERROR_SUCCESS => Ok(()), + ctru_sys::ERROR_NOT_SUPPORTED => Err(Error::NotSupported), + ctru_sys::ERROR_HOME_BUTTON => Err(Error::HomePressed), + ctru_sys::ERROR_POWER_BUTTON => Err(Error::PowerPressed), + ctru_sys::ERROR_SOFTWARE_RESET => Err(Error::ResetPressed), + _ => Err(Error::Unknown), + } + } +} + +/// Sets a custom [panic hook](https://doc.rust-lang.org/std/panic/fn.set_hook.html) that uses the error applet to display panic messages. +/// +/// You can also choose to have the previously registered panic hook called along with the error applet popup, which can be useful +/// if you want to use output redirection to display panic messages over `3dslink` or `GDB`. +/// +/// You can use [`std::panic::take_hook`](https://doc.rust-lang.org/std/panic/fn.take_hook.html) to unregister the panic hook +/// set by this function. +/// +/// # Notes +/// +/// * If the [`Gfx`] service is not initialized during a panic, the error applet will not be displayed and the old panic hook will be called. +pub fn set_panic_hook(call_old_hook: bool) { + use crate::services::gfx::GFX_ACTIVE; + use std::sync::TryLockError; + + let old_hook = std::panic::take_hook(); + + std::panic::set_hook(Box::new(move |panic_info| { + // If we get a `WouldBlock` error, we know that the `Gfx` service has been initialized. + // Otherwise fallback to using the old panic hook. + if let (Err(TryLockError::WouldBlock), Ok(_apt)) = (GFX_ACTIVE.try_lock(), Apt::new()) { + if call_old_hook { + old_hook(panic_info); + } + + let thread = std::thread::current(); + + let name = thread.name().unwrap_or(""); + + let message = format!("thread '{name}' {panic_info}"); + + let mut popup = PopUp::new(WordWrap::Enabled); + + popup.set_text(&message); + + unsafe { + let _ = popup.launch_unchecked(); + } + } else { + old_hook(panic_info); + } + })); +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotSupported => write!(f, "operation not supported"), + Self::HomePressed => write!(f, "home button pressed while error applet was running"), + Self::PowerPressed => write!(f, "power button pressed while error applet was running"), + Self::ResetPressed => write!(f, "reset button pressed while error applet was running"), + Self::Unknown => write!(f, "an unknown error occurred"), + } + } +} + +impl std::error::Error for Error {} diff --git a/ctru-rs/src/applets/mod.rs b/ctru-rs/src/applets/mod.rs index 244fb74..838ef6b 100644 --- a/ctru-rs/src/applets/mod.rs +++ b/ctru-rs/src/applets/mod.rs @@ -8,5 +8,6 @@ //! //! Applets block execution of the thread that launches them as long as the user doesn't close the applet. +pub mod error; pub mod mii_selector; pub mod swkbd; diff --git a/ctru-rs/src/services/gfx.rs b/ctru-rs/src/services/gfx.rs index 7a6ec6e..9799665 100644 --- a/ctru-rs/src/services/gfx.rs +++ b/ctru-rs/src/services/gfx.rs @@ -239,7 +239,7 @@ pub struct Gfx { _service_handler: ServiceReference, } -static GFX_ACTIVE: Mutex<()> = Mutex::new(()); +pub(crate) static GFX_ACTIVE: Mutex<()> = Mutex::new(()); impl Gfx { /// Initialize a new default service handle. diff --git a/ctru-sys/build.rs b/ctru-sys/build.rs index 5e71ae0..ff140bb 100644 --- a/ctru-sys/build.rs +++ b/ctru-sys/build.rs @@ -79,6 +79,8 @@ fn main() { .blocklist_type("u(8|16|32|64)") .blocklist_type("__builtin_va_list") .blocklist_type("__va_list") + .blocklist_type("errorReturnCode") + .blocklist_type("errorScreenFlag") .opaque_type("MiiData") .derive_default(true) .wrap_static_fns(true) diff --git a/ctru-sys/src/lib.rs b/ctru-sys/src/lib.rs index b9f71de..5a60de2 100644 --- a/ctru-sys/src/lib.rs +++ b/ctru-sys/src/lib.rs @@ -16,6 +16,24 @@ pub mod result; pub use result::*; +// Fun fact: C compilers are allowed to represent enums as the smallest integer type that can hold all of its variants, +// meaning that enums are allowed to be the size of a `c_short` or a `c_char` rather than the size of a `c_int`. +// Libctru's `errorConf` struct contains two enums that depend on this narrowing property for size and alignment purposes, +// and since `bindgen` generates all enums with `c_int` sizing, we have to blocklist those types and manually define them +// here with the proper size. +pub type errorReturnCode = libc::c_schar; +pub const ERROR_UNKNOWN: errorReturnCode = -1; +pub const ERROR_NONE: errorReturnCode = 0; +pub const ERROR_SUCCESS: errorReturnCode = 1; +pub const ERROR_NOT_SUPPORTED: errorReturnCode = 2; +pub const ERROR_HOME_BUTTON: errorReturnCode = 10; +pub const ERROR_SOFTWARE_RESET: errorReturnCode = 11; +pub const ERROR_POWER_BUTTON: errorReturnCode = 12; + +pub type errorScreenFlag = libc::c_char; +pub const ERROR_NORMAL: errorScreenFlag = 0; +pub const ERROR_STEREO: errorScreenFlag = 1; + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); /// In lieu of a proper errno function exposed by libc