wasite/
lib.rs

1//! Abstraction over Wasite - Terminal interface conventions for WASI
2
3#![no_std]
4#![forbid(unsafe_code)]
5#![doc(
6    html_logo_url = "https://ardaku.github.io/mm/logo.svg",
7    html_favicon_url = "https://ardaku.github.io/mm/icon.svg",
8    html_root_url = "https://docs.rs/wasite"
9)]
10#![warn(
11    anonymous_parameters,
12    missing_copy_implementations,
13    missing_debug_implementations,
14    missing_docs,
15    nonstandard_style,
16    rust_2018_idioms,
17    single_use_lifetimes,
18    trivial_casts,
19    trivial_numeric_casts,
20    unreachable_pub,
21    unused_extern_crates,
22    unused_qualifications,
23    variant_size_differences
24)]
25
26extern crate alloc;
27
28use alloc::string::String;
29use core::num::NonZeroU16;
30
31/// Type alias for [`Result`](core::result::Result)
32pub type Result<T = (), E = Error> = core::result::Result<T, E>;
33
34/// A Wasite Error
35#[derive(Debug)]
36pub struct Error(#[allow(dead_code)] wasi::io::streams::StreamError);
37
38/// The language preferences of the [`User`]
39#[derive(Debug)]
40#[non_exhaustive]
41pub struct Languages(String);
42
43impl Languages {
44    fn parse_all(&self) -> impl Iterator<Item = &str> {
45        self.0.split(':')
46    }
47
48    /// Get language list for collation.
49    pub fn collation(&self) -> impl Iterator<Item = &str> {
50        self.parse_all()
51            .filter_map(|l| l.strip_prefix("Collation="))
52    }
53
54    /// Get language list for character classes.
55    pub fn char_class(&self) -> impl Iterator<Item = &str> {
56        self.parse_all()
57            .filter_map(|l| l.strip_prefix("CharClass="))
58    }
59
60    /// Get language list for monetary values.
61    pub fn monetary(&self) -> impl Iterator<Item = &str> {
62        self.parse_all().filter_map(|l| l.strip_prefix("Monetary="))
63    }
64
65    /// Get language list for messages.
66    pub fn message(&self) -> impl Iterator<Item = &str> {
67        self.parse_all().filter_map(|l| l.strip_prefix("Message="))
68    }
69
70    /// Get language list for numeric values.
71    pub fn numeric(&self) -> impl Iterator<Item = &str> {
72        self.parse_all().filter_map(|l| l.strip_prefix("Numeric="))
73    }
74
75    /// Get language list for time.
76    pub fn time(&self) -> impl Iterator<Item = &str> {
77        self.parse_all().filter_map(|l| l.strip_prefix("Time="))
78    }
79
80    /// Get language list for other.
81    pub fn other(&self) -> impl Iterator<Item = &str> {
82        self.parse_all().filter(|l| !l.contains('='))
83    }
84}
85
86/// The `User` part of the [`Environment`]
87#[derive(Debug)]
88#[non_exhaustive]
89pub struct User {
90    /// The username of the current user
91    pub username: String,
92    /// The user's preferred languages
93    pub langs: Languages,
94}
95
96/// The `Host` part of the [`Environment`]
97#[derive(Debug)]
98#[non_exhaustive]
99pub struct Host {
100    /// The pretty name of the host device
101    pub name: String,
102    /// The hostname of the host device
103    pub hostname: String,
104    /// The IANA TZDB identifier for the timezone (ex: `America/New_York`)
105    pub timezone: String,
106}
107
108/// Information from environment variables
109#[derive(Debug)]
110#[non_exhaustive]
111pub struct Environment {
112    /// Information about the user
113    pub user: User,
114    /// Information about the host device
115    pub host: Host,
116}
117
118/// Dimensions of the terminal of [`State`]
119#[derive(Copy, Clone, Debug)]
120#[non_exhaustive]
121pub struct Dimensions {
122    /// Width of the terminal
123    pub width: NonZeroU16,
124    /// Height of the terminal
125    pub height: NonZeroU16,
126}
127
128/// Position of cursor of [`State`]
129#[derive(Copy, Clone, Debug)]
130#[non_exhaustive]
131pub struct Cursor {
132    /// Column of the cursor
133    pub column: NonZeroU16,
134    /// Line of the cursor
135    pub line: NonZeroU16,
136}
137
138/// Terminal state
139#[derive(Copy, Clone, Debug)]
140#[non_exhaustive]
141pub struct State {
142    /// Dimensions of the terminal
143    pub dimensions: Dimensions,
144    /// Position of cursor
145    pub cursor: Cursor,
146}
147
148/// Get environment variable information
149pub fn environment() -> Environment {
150    let mut env = Environment {
151        user: User {
152            username: String::new(),
153            langs: Languages(String::new()),
154        },
155        host: Host {
156            name: String::new(),
157            hostname: String::new(),
158            timezone: String::new(),
159        },
160    };
161
162    for (key, value) in wasi::cli::environment::get_environment() {
163        match key.as_str() {
164            "USER" => env.user.username = value,
165            "HOSTNAME" => env.host.hostname = value,
166            "NAME" => env.host.name = value,
167            "TZ" => env.host.timezone = value,
168            "LANGS" => env.user.langs = Languages(value),
169            _ => {}
170        }
171    }
172
173    env
174}
175
176/// Get terminal state
177pub fn state() -> Result<State> {
178    let err = Err(Error(wasi::io::streams::StreamError::Closed));
179    let stdout = wasi::cli::stdout::get_stdout();
180
181    stdout.blocking_flush().map_err(Error)?;
182    stdout.write(b"\x05").map_err(Error)?;
183
184    // enquiry mode
185    let stdin = wasi::cli::stdin::get_stdin();
186    let bytes = stdin.read(24).map_err(Error)?;
187    let string = String::from_utf8_lossy(&bytes);
188    let mut parts = string.split(';');
189    let Some(part_one) = parts.next() else {
190        return err;
191    };
192    let Some(part_two) = parts.next() else {
193        return err;
194    };
195    let mut cols = part_one.split('/');
196    let Some(column) = cols.next() else {
197        return err;
198    };
199    let Some(width) = cols.next() else {
200        return err;
201    };
202    let mut row = part_two.split('/');
203    let Some(line) = row.next() else {
204        return err;
205    };
206    let Some(height) = row.next() else {
207        return err;
208    };
209
210    // exit enquire mode
211    stdout.write(b"\x06").map_err(Error)?;
212    stdout.blocking_flush().map_err(Error)?;
213
214    // Parse
215    let Ok(column) = column.parse() else {
216        return err;
217    };
218    let Ok(width) = width.parse() else {
219        return err;
220    };
221    let Ok(line) = line.parse() else {
222        return err;
223    };
224    let Ok(height) = height.parse() else {
225        return err;
226    };
227
228    Ok(State {
229        dimensions: Dimensions { width, height },
230        cursor: Cursor { column, line },
231    })
232}
233
234/// Terminal commands
235#[non_exhaustive]
236#[derive(Debug, Copy, Clone, PartialEq, Eq)]
237#[allow(variant_size_differences)]
238pub enum Command<'a> {
239    /// Clear the screen
240    Clear,
241    /// Flash the screen
242    Alert,
243    /// Turn raw mode on or off
244    Raw(bool),
245    /// Set the terminal title
246    Title(&'a str),
247    /// Turn alternate screen on or off (doesn't scroll)
248    Screen(bool),
249}
250
251/// Execute terminal commands
252pub fn execute(commands: &[Command<'_>]) -> Result {
253    let stdout = wasi::cli::stdout::get_stdout();
254
255    stdout.blocking_flush().map_err(Error)?;
256
257    for command in commands {
258        match command {
259            Command::Clear => stdout.write(b"\x00").map_err(Error)?,
260            Command::Alert => stdout.write(b"\x07").map_err(Error)?,
261            Command::Raw(enabled) => {
262                if *enabled {
263                    stdout.write(b"\x03").map_err(Error)?;
264                } else {
265                    stdout.write(b"\x02").map_err(Error)?;
266                }
267            }
268            Command::Title(title) => {
269                stdout.write(b"\x01").map_err(Error)?;
270                stdout.write(title.as_bytes()).map_err(Error)?;
271                stdout.write(b"\x04").map_err(Error)?;
272            }
273            Command::Screen(enabled) => {
274                if *enabled {
275                    stdout.write(b"\x0F").map_err(Error)?;
276                } else {
277                    stdout.write(b"\x0E").map_err(Error)?;
278                }
279            }
280        }
281    }
282
283    stdout.blocking_flush().map_err(Error)
284}