Skip to main content

godspeed/
io.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Writing to the screen.
3//!
4//! # The authority every function here needs is `log_write`
5//!
6//! Including [`report`]. All of them make the kernel's `ConsoleWrite` call, which writes serial and
7//! hands the bytes to the `console` service, and the kernel checks ONE capability before it does:
8//! `LOG_WRITE` (`kernel/src/syscall/dispatch.rs`, `handle_console_write`). Every task is minted that
9//! capability at spawn, in slot 0, so a program's output reaches the screen whatever else its
10//! contract says. Declare `log_write = true` anyway: the contract is the reviewable statement of what
11//! a program may do (CLAUDE.md 13.6, 26.9), and printing is something it does.
12//!
13//! **`console_push` is NOT for printing, and must not be asked for to print.** It is the authority to
14//! push bytes into the keyboard's input ring, which the shell reads as typed commands (CLAUDE.md 6.4,
15//! SEC-2): a keyboard driver needs it, and nothing else should hold it.
16//!
17//! **This module said the opposite until 2026-10-09**, in four places and a gate, and the error is
18//! worth recording because the whole design depends on authority being stated truthfully. A Stranger
19//! Test run concluded from the documentation that a program without `console_push` prints nothing,
20//! the documentation was rewritten to say so, and `scripts/contract_check.py` began failing any
21//! printer whose contract did not ask for `console_push`. Nobody had run it. `stdlib-hello` declared
22//! `console_push` and its spawn row grants it nothing (privilege word 0), and
23//! `build/tests/examples_serial.log` holds its `io::report` and `io::println` lines regardless. Moving
24//! every service onto this library made the gate fire on four drivers that print a notice - one of
25//! which, `net-stack`, refuses keystroke authority in a comment for exactly the SEC-2 reason - and
26//! obeying it would have handed them the power to type commands. The rule was false; the gate now
27//! checks the true one.
28//!
29//! The thinnest module here, and deliberately so. `ServiceContext` already has a clean console
30//! surface; this exists to give it the name a programmer reaches for first, and to put the
31//! authority note somewhere they will read it.
32//!
33//! # Authority
34//!
35//! `log_write`, which every task holds. A program whose output is missing has not lost a capability:
36//! look instead at whether a full-screen program owns the display (the kernel then sends background
37//! output to serial only), or whether the `console` service is up.
38//!
39//! There is deliberately no `print!` macro that reaches a global stdout. A global would be ambient
40//! authority wearing a familiar name (invariant 1), and the whole point of passing `ctx` is that
41//! the answer to "what can this task reach" is its contract plus the arguments it was handed.
42
43use godspeed_sdk::service_context::ServiceContext;
44
45/// The most one `ConsoleWrite` carries. The SDK's `console_write` DROPS a longer string outright and
46/// its formatted forms TRUNCATE at this length, both silently (CLAUDE.md 3.12 says a failure is loud).
47/// Every function here writes in pieces of at most this size instead, so a long line arrives whole.
48const CHUNK: usize = 256;
49
50/// Write `s` in pieces the SDK will carry, each cut on a character boundary.
51fn write_chunked(ctx: &ServiceContext, mut s: &str) {
52    while s.len() > CHUNK {
53        let mut cut = CHUNK;
54        while !s.is_char_boundary(cut) { cut -= 1; }
55        ctx.console_write(&s[..cut]);
56        s = &s[cut..];
57    }
58    if !s.is_empty() { ctx.console_write(s); }
59}
60
61/// Renders `format_args!` through one fixed stack buffer, flushing it whenever the next fragment
62/// would not fit, so output of any length is written in order and nothing is cut off. Bounded and
63/// heap-free (CLAUDE.md 26.6.1).
64struct Chunker<'a> {
65    ctx: &'a ServiceContext,
66    buf: [u8; CHUNK],
67    len: usize,
68}
69
70impl Chunker<'_> {
71    fn flush(&mut self) {
72        if self.len > 0 {
73            // Only whole `&str` fragments are ever copied in, so the buffer is always valid UTF-8.
74            if let Ok(t) = core::str::from_utf8(&self.buf[..self.len]) { self.ctx.console_write(t); }
75            self.len = 0;
76        }
77    }
78}
79
80impl core::fmt::Write for Chunker<'_> {
81    fn write_str(&mut self, s: &str) -> core::fmt::Result {
82        if self.len + s.len() > CHUNK { self.flush(); }
83        if s.len() > CHUNK {
84            write_chunked(self.ctx, s);
85        } else {
86            self.buf[self.len..self.len + s.len()].copy_from_slice(s.as_bytes());
87            self.len += s.len();
88        }
89        Ok(())
90    }
91}
92
93fn write_fmt(ctx: &ServiceContext, args: core::fmt::Arguments) {
94    let mut w = Chunker { ctx, buf: [0u8; CHUNK], len: 0 };
95    let _ = core::fmt::write(&mut w, args);
96    w.flush();
97}
98
99/// Write a line to the console.
100///
101/// A line of any length is written whole, in pieces of at most 256 bytes. **It may park the caller
102/// briefly**: serial output is synchronous, and when the `console` service's queue is full the kernel
103/// parks the writer until there is room (`handle_console_write`). A program printing in a tight loop
104/// runs at the speed of the screen. **Authority:** `log_write`.
105pub fn println(ctx: &ServiceContext, s: &str) {
106    write_chunked(ctx, s);
107    ctx.console_write("\n");
108}
109
110/// Write without a trailing newline. Any length; may park briefly, as [`println`].
111/// **Authority:** `log_write`.
112pub fn print(ctx: &ServiceContext, s: &str) {
113    write_chunked(ctx, s);
114}
115
116/// Write a formatted line.
117///
118/// Formatting is bounded and allocates nothing: it renders through a fixed stack buffer
119/// (CLAUDE.md ยง26.6.1 is explicit that `format_args!` is the sanctioned bounded tool, and that
120/// hand-rolling digit formatting to avoid a heap it never touches is the mistake). Output longer
121/// than the buffer is written in pieces, never cut off.
122///
123/// ```ignore
124/// io::println_fmt(ctx, format_args!("read {} bytes from {}", n, path));
125/// ```
126/// **Authority:** `log_write`.
127pub fn println_fmt(ctx: &ServiceContext, args: core::fmt::Arguments) {
128    write_fmt(ctx, args);
129    ctx.console_write("\n");
130}
131
132/// Write a formatted fragment, without the newline.
133/// **Authority:** `log_write`.
134pub fn print_fmt(ctx: &ServiceContext, args: core::fmt::Arguments) {
135    write_fmt(ctx, args);
136}
137
138/// The bytes a keyboard driver puts on the console stream for a chord that is not a keystroke - outside
139/// ASCII, so no typed key produces one - and that the program reading the console acts on: Ctrl+Alt+Del
140/// (`CTRL_ALT_DEL_SIGNAL`, the shell reboots), and the audio shortcuts Ctrl+Alt+Up, Ctrl+Alt+Down and
141/// Ctrl+Alt+M (`docs/audio.md`); and a USB host's word that it just printed a device notice
142/// (`NOTICE_SIGNAL`, the shell redraws its prompt). The drivers write them from the SDK's `hid` module; this
143/// is the same definition, so the two ends cannot drift.
144pub mod keys {
145    pub use godspeed_sdk::hid::{CTRL_ALT_DEL_SIGNAL, MUTE_TOGGLE_SIGNAL, NOTICE_SIGNAL, VOLUME_DOWN_SIGNAL, VOLUME_UP_SIGNAL};
146}
147
148/// Report a failed operation in one line, in the house style: `<what>: <why>`.
149///
150/// Exists because every utility writes this by hand and they do not agree on the wording. Takes the
151/// error rather than a string so the phrasing stays in one place and stays honest - in particular
152/// [`crate::Error::OutcomeUnknown`] prints as a warning that the operation MAY have happened,
153/// which is the fact a user most needs and the one a hand-written message usually drops.
154/// **Authority:** `log_write` - this writes to the SCREEN (and serial), not to the kernel log ring.
155pub fn report(ctx: &ServiceContext, what: &str, e: crate::Error) {
156    println_fmt(ctx, format_args!("{}: {}", what, e.as_str()));
157}