ctru/console.rs
1//! Virtual text console.
2//!
3//! The [`Console`] works as a virtual shell that renders on screen all output of `stdout`. As such, it is useful as a basic interface to show info to the user,
4//! such as in simple "Hello World" applications or more complex software that does not need much user interaction.
5//!
6//! Have a look at [`redirect_stderr`] or [`Soc::redirect_to_3dslink`](crate::services::soc::Soc::redirect_to_3dslink) for better alternatives when debugging applications.
7use std::cell::{RefMut, UnsafeCell};
8
9use ctru_sys::{
10 PrintConsole, consoleClear, consoleDebugInit, consoleInit, consoleSelect, consoleSetWindow,
11};
12
13use crate::services::gfx::{Flush, Screen, Swap};
14
15static mut EMPTY_CONSOLE: PrintConsole = unsafe { std::mem::zeroed::<PrintConsole>() };
16
17/// Error enum for generic errors within [`Console`].
18#[derive(Copy, Clone, Debug, PartialEq, Eq)]
19pub enum Error {
20 /// The coordinate specified on the given axis exceeds the limits imposed by the [`Console`] window.
21 CoordinateOutOfBounds(Axis),
22 /// The size specified for the given dimension exceeds the limits imposed by the [`Console`] window.
23 DimensionOutOfBounds(Dimension),
24}
25
26/// 2D coordinate axes.
27#[allow(missing_docs)]
28#[derive(Copy, Clone, Debug, PartialEq, Eq)]
29pub enum Axis {
30 X,
31 Y,
32}
33
34/// 2D dimensions.
35#[allow(missing_docs)]
36#[derive(Copy, Clone, Debug, PartialEq, Eq)]
37pub enum Dimension {
38 Width,
39 Height,
40}
41
42/// Destination for stderr redirection with [`redirect_stderr`].
43#[doc(alias = "debugDevice")]
44#[repr(u8)]
45pub enum Destination {
46 /// Print stderr to the active [`Console`] window. This is the default behavior.
47 Console = ctru_sys::debugDevice_CONSOLE,
48 /// Print stderr via [`ctru_sys::svcOutputDebugString`]. This allows you to capture error and panic messages with `GDB` or other debuggers.
49 Debugger = ctru_sys::debugDevice_SVC,
50 /// Swallow outputs from stderr.
51 Null = ctru_sys::debugDevice_NULL,
52}
53
54/// A [`Screen`] that can be used as a target for [`Console`].
55pub trait ConsoleScreen: Screen + Swap + Flush {}
56impl<S: Screen + Swap + Flush> ConsoleScreen for S {}
57
58/// Virtual text console.
59///
60/// [`Console`] lets the application redirect `stdout` and `stderr` to a simple text displayer on the 3DS screen.
61/// This means that any text written to `stdout` and `stderr` (e.g. using `println!`, `eprintln!` or `dbg!`) will become visible in the area taken by the console.
62///
63/// # Notes
64///
65/// The [`Console`] will take full possession of the screen handed to it as long as it stays alive. It also supports some ANSI codes, such as text color and cursor positioning.
66/// The [`Console`]'s window size will be:
67/// - 40x30 on the [`BottomScreen`](crate::services::gfx::BottomScreen).
68/// - 50x30 on the normal [`TopScreen`](crate::services::gfx::TopScreen).
69/// - 100x30 on the [`TopScreen`](crate::services::gfx::TopScreen) when wide mode is enabled.
70///
71/// # Alternatives
72///
73/// If you'd like to see live standard output while running the application but cannot or do not want to show the text on the 3DS itself,
74/// you can try using [`Soc::redirect_to_3dslink`](crate::services::soc::Soc::redirect_to_3dslink) while activating the `--server` flag for `3dslink` (also supported by `cargo-3ds`).
75/// More info in the [`cargo-3ds` docs](https://github.com/rust3ds/cargo-3ds#running-executables).
76#[doc(alias = "PrintConsole")]
77pub struct Console<'screen> {
78 context: Box<UnsafeCell<PrintConsole>>,
79 screen: RefMut<'screen, dyn ConsoleScreen>,
80}
81
82/// Send output from stderr to the specified [`Destination`]. This function can be used to capture error and panic messages with `GDB` or other debuggers.
83///
84/// # Notes:
85///
86/// This function is similar in purpose to [`Soc::redirect_to_3dslink`](crate::services::soc::Soc::redirect_to_3dslink), but they each offer slightly different functionality.
87/// `redirect_stderr` enables messages printed via `stderr` to show up in `GDB`, while `redirect_to_3dslink` completely replaces the file descriptor for `stderr` with a network socket instead.
88/// As such, `redirect_to_3dslink` will take priority over this function if both of them are called.
89#[doc(alias = "consoleDebugInit")]
90pub fn redirect_stderr(dest: Destination) {
91 unsafe { consoleDebugInit(dest as _) }
92}
93
94impl<'screen> Console<'screen> {
95 /// Initialize a console on the chosen screen.
96 ///
97 /// # Notes
98 ///
99 /// This operation overwrites whatever was on the screen before the initialization (including other [`Console`]s)
100 /// and changes the [`FramebufferFormat`](crate::services::gspgpu::FramebufferFormat) of the selected screen to better suit the [`Console`].
101 ///
102 /// The new console is automatically selected for printing.
103 ///
104 /// [`Console`] automatically takes care of flushing and swapping buffers for its screen when printing.
105 ///
106 /// # Panics
107 ///
108 /// If the [`Gfx`](crate::services::gfx::Gfx) service was initialised via [`Gfx::with_formats_vram()`](crate::services::gfx::Gfx::with_formats_vram)
109 /// this function will crash the program with an ARM exception.
110 ///
111 /// # Example
112 ///
113 /// ```
114 /// # let _runner = test_runner::GdbRunner::default();
115 /// # use std::error::Error;
116 /// # fn main() -> Result<(), Box<dyn Error>> {
117 /// #
118 /// use ctru::console::Console;
119 /// use ctru::services::gfx::Gfx;
120 ///
121 /// // Initialize graphics (using framebuffers allocated on the HEAP).
122 /// let gfx = Gfx::new()?;
123 ///
124 /// // Create a `Console` that takes control of the upper LCD screen.
125 /// let top_console = Console::new(gfx.top_screen.borrow_mut());
126 ///
127 /// println!("I'm on the top screen!");
128 /// #
129 /// # Ok(())
130 /// # }
131 /// ```
132 #[doc(alias = "consoleInit")]
133 pub fn new<S: ConsoleScreen>(screen: RefMut<'screen, S>) -> Self {
134 let context = Box::<UnsafeCell<PrintConsole>>::default();
135
136 unsafe { consoleInit(screen.as_raw(), context.get()) };
137
138 Console { context, screen }
139 }
140
141 /// Returns `true` if a valid [`Console`] to print on is currently selected.
142 ///
143 /// # Notes
144 ///
145 /// This function is used to check whether one of the two screens has an existing (and selected) [`Console`],
146 /// so that the program can be sure its output will be shown *somewhere*.
147 ///
148 /// # Example
149 ///
150 /// ```
151 /// # let _runner = test_runner::GdbRunner::default();
152 /// # use std::error::Error;
153 /// # fn main() -> Result<(), Box<dyn Error>> {
154 /// #
155 /// # use ctru::services::gfx::Gfx;
156 /// # // Initialize graphics.
157 /// # let gfx = Gfx::new()?;
158 /// #
159 /// use ctru::console::Console;
160 /// let top_console = Console::new(gfx.top_screen.borrow_mut());
161 ///
162 /// // There is at least one selected `Console`.
163 /// assert!(Console::exists());
164 /// #
165 /// # Ok(())
166 /// # }
167 /// ```
168 pub fn exists() -> bool {
169 unsafe {
170 let current_console = ctru_sys::consoleSelect(&raw mut EMPTY_CONSOLE);
171
172 let res = (*current_console).consoleInitialised;
173
174 ctru_sys::consoleSelect(current_console);
175
176 res
177 }
178 }
179
180 /// Select this console as the current target for standard output.
181 ///
182 /// # Notes
183 ///
184 /// Any previously selected console will be unhooked and will not show the `stdout` and `stderr` output.
185 ///
186 /// # Example
187 ///
188 /// ```
189 /// # let _runner = test_runner::GdbRunner::default();
190 /// # use std::error::Error;
191 /// # fn main() -> Result<(), Box<dyn Error>> {
192 /// #
193 /// # use ctru::services::gfx::Gfx;
194 /// # let gfx = Gfx::new()?;
195 /// #
196 /// use ctru::console::Console;
197 ///
198 /// // Create a `Console` that takes control of the upper LCD screen.
199 /// let top_console = Console::new(gfx.top_screen.borrow_mut());
200 ///
201 /// // Create a `Console` that takes control of the lower LCD screen.
202 /// let bottom_console = Console::new(gfx.bottom_screen.borrow_mut());
203 ///
204 /// // Remember that `Console::new` automatically selects the new `Console` for output.
205 /// println!("I'm on the bottom screen!");
206 ///
207 /// top_console.select();
208 ///
209 /// println!("Being on the upper screen is much better!");
210 /// #
211 /// # Ok(())
212 /// # }
213 /// ```
214 #[doc(alias = "consoleSelect")]
215 pub fn select(&self) {
216 unsafe {
217 consoleSelect(self.context.get());
218 }
219 }
220
221 /// Clear all text from the console.
222 #[doc(alias = "consoleClear")]
223 pub fn clear(&self) {
224 unsafe { consoleClear() }
225 }
226
227 /// Resize the console to fit in a smaller portion of the screen.
228 ///
229 /// # Notes
230 ///
231 /// The first two arguments are the desired coordinates of the top-left corner
232 /// of the new window based on the row/column coordinates of a full-screen console.
233 /// The second pair is the new width and height.
234 ///
235 /// # Example
236 ///
237 /// ```
238 /// # use std::error::Error;
239 /// # fn main() -> Result<(), Box<dyn Error>> {
240 /// #
241 /// # use ctru::services::gfx::Gfx;
242 /// # let gfx = Gfx::new()?;
243 /// #
244 /// # use ctru::console::Console;
245 /// #
246 /// let mut top_console = Console::new(gfx.top_screen.borrow_mut());
247 /// top_console.set_window(10, 10, 16, 6);
248 ///
249 /// println!("I'm becoming claustrophobic in here!");
250 /// #
251 /// # Ok(())
252 /// # }
253 /// ```
254 #[doc(alias = "consoleSetWindow")]
255 pub fn set_window(&mut self, x: u8, y: u8, width: u8, height: u8) -> Result<(), Error> {
256 let height_limit = 30;
257 let length_limit = self.max_width();
258
259 if x >= length_limit {
260 return Err(Error::CoordinateOutOfBounds(Axis::X));
261 }
262 if y >= height_limit {
263 return Err(Error::CoordinateOutOfBounds(Axis::Y));
264 }
265
266 if (x + width) > length_limit {
267 return Err(Error::DimensionOutOfBounds(Dimension::Width));
268 }
269 if (y + height) > height_limit {
270 return Err(Error::DimensionOutOfBounds(Dimension::Height));
271 }
272
273 unsafe {
274 consoleSetWindow(
275 self.context.get(),
276 x.into(),
277 y.into(),
278 width.into(),
279 height.into(),
280 )
281 };
282
283 Ok(())
284 }
285
286 /// Reset the window's size to default parameters.
287 ///
288 /// This can be used to undo the changes made by [`set_window()`](Console::set_window()).
289 ///
290 /// # Example
291 ///
292 /// ```
293 /// # use std::error::Error;
294 /// # fn main() -> Result<(), Box<dyn Error>> {
295 /// #
296 /// # use ctru::services::gfx::Gfx;
297 /// # let gfx = Gfx::new()?;
298 /// #
299 /// # use ctru::console::Console;
300 /// #
301 /// let mut top_console = Console::new(gfx.top_screen.borrow_mut());
302 /// top_console.set_window(15, 15, 8, 10);
303 ///
304 /// println!("It's really jammed in here!");
305 ///
306 /// top_console.reset_window();
307 ///
308 /// println!("Phew, finally a breath of fresh air.");
309 /// #
310 /// # Ok(())
311 /// # }
312 /// ```
313 pub fn reset_window(&mut self) {
314 let width = self.max_width();
315
316 self.set_window(0, 0, width, 30).unwrap();
317 }
318
319 /// Returns this [`Console`]'s maximum character width depending on the screen used.
320 ///
321 /// # Example
322 ///
323 /// ```
324 /// # use std::error::Error;
325 /// # fn main() -> Result<(), Box<dyn Error>> {
326 /// #
327 /// # use ctru::services::gfx::Gfx;
328 /// # use ctru::console::Console;
329 /// #
330 /// let gfx = Gfx::new()?;
331 ///
332 /// let top_console = Console::new(gfx.top_screen.borrow_mut());
333 ///
334 /// // The maximum width for the top screen (without any alterations) is 50 characters.
335 /// assert_eq!(top_console.max_width(), 50);
336 /// #
337 /// # Ok(())
338 /// # }
339 /// ```
340 pub fn max_width(&self) -> u8 {
341 match self.screen.as_raw() {
342 ctru_sys::GFX_TOP => {
343 if unsafe { ctru_sys::gfxIsWide() } {
344 100
345 } else {
346 50
347 }
348 }
349 ctru_sys::GFX_BOTTOM => 40,
350 _ => unreachable!(),
351 }
352 }
353}
354
355impl Swap for Console<'_> {
356 /// Swaps the video buffers. Note: The console's cursor position is not reset, only the framebuffer is changed.
357 ///
358 /// Even if double buffering is disabled, "swapping" the buffers has the side effect
359 /// of committing any configuration changes to the buffers (e.g. [`TopScreen::set_wide_mode()`],
360 /// [`Screen::set_framebuffer_format()`], [`Swap::set_double_buffering()`]), so it should still be used.
361 ///
362 /// This should be called once per frame at most.
363 fn swap_buffers(&mut self) {
364 self.screen.swap_buffers();
365
366 unsafe {
367 (*self.context.get()).frameBuffer = self.screen.raw_framebuffer().ptr as *mut u16
368 };
369 }
370
371 fn set_double_buffering(&mut self, enabled: bool) {
372 self.screen.set_double_buffering(enabled);
373 }
374}
375
376impl Flush for Console<'_> {
377 fn flush_buffers(&mut self) {
378 self.screen.flush_buffers();
379 }
380}
381
382impl Drop for Console<'_> {
383 fn drop(&mut self) {
384 unsafe {
385 // Safety: We are about to deallocate the PrintConsole data pointed
386 // to by libctru. Without this drop code libctru would have a
387 // dangling pointer that it writes to on every print. To prevent
388 // this we replace the console with an empty one if it was selected.
389 // This is the same state that libctru starts up in, before
390 // initializing a console. Writes to the console will not show up on
391 // the screen, but it won't crash either.
392
393 // Get the current console by replacing it with an empty one.
394 let current_console = ctru_sys::consoleSelect(&raw mut EMPTY_CONSOLE);
395
396 if std::ptr::eq(current_console, self.context.get()) {
397 // Console dropped while selected. We just replaced it with the
398 // empty console so nothing more to do.
399 } else {
400 // Console dropped while a different console was selected. Put back
401 // the console that was selected.
402 ctru_sys::consoleSelect(current_console);
403 }
404 }
405 }
406}
407
408impl std::fmt::Display for Axis {
409 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410 match self {
411 Self::X => write!(f, "x"),
412 Self::Y => write!(f, "y"),
413 }
414 }
415}
416
417impl std::fmt::Display for Dimension {
418 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419 match self {
420 Self::Width => write!(f, "width"),
421 Self::Height => write!(f, "height"),
422 }
423 }
424}
425
426impl std::fmt::Display for Error {
427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428 match self {
429 Self::CoordinateOutOfBounds(a) => {
430 write!(f, "coordinate specified for the {a} axis is out of bounds")
431 }
432 Self::DimensionOutOfBounds(d) => {
433 write!(f, "size specified for the {d} is out of bounds")
434 }
435 }
436 }
437}
438
439impl std::error::Error for Error {}