1#![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
31pub type Result<T = (), E = Error> = core::result::Result<T, E>;
33
34#[derive(Debug)]
36pub struct Error(#[allow(dead_code)] wasi::io::streams::StreamError);
37
38#[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 pub fn collation(&self) -> impl Iterator<Item = &str> {
50 self.parse_all()
51 .filter_map(|l| l.strip_prefix("Collation="))
52 }
53
54 pub fn char_class(&self) -> impl Iterator<Item = &str> {
56 self.parse_all()
57 .filter_map(|l| l.strip_prefix("CharClass="))
58 }
59
60 pub fn monetary(&self) -> impl Iterator<Item = &str> {
62 self.parse_all().filter_map(|l| l.strip_prefix("Monetary="))
63 }
64
65 pub fn message(&self) -> impl Iterator<Item = &str> {
67 self.parse_all().filter_map(|l| l.strip_prefix("Message="))
68 }
69
70 pub fn numeric(&self) -> impl Iterator<Item = &str> {
72 self.parse_all().filter_map(|l| l.strip_prefix("Numeric="))
73 }
74
75 pub fn time(&self) -> impl Iterator<Item = &str> {
77 self.parse_all().filter_map(|l| l.strip_prefix("Time="))
78 }
79
80 pub fn other(&self) -> impl Iterator<Item = &str> {
82 self.parse_all().filter(|l| !l.contains('='))
83 }
84}
85
86#[derive(Debug)]
88#[non_exhaustive]
89pub struct User {
90 pub username: String,
92 pub langs: Languages,
94}
95
96#[derive(Debug)]
98#[non_exhaustive]
99pub struct Host {
100 pub name: String,
102 pub hostname: String,
104 pub timezone: String,
106}
107
108#[derive(Debug)]
110#[non_exhaustive]
111pub struct Environment {
112 pub user: User,
114 pub host: Host,
116}
117
118#[derive(Copy, Clone, Debug)]
120#[non_exhaustive]
121pub struct Dimensions {
122 pub width: NonZeroU16,
124 pub height: NonZeroU16,
126}
127
128#[derive(Copy, Clone, Debug)]
130#[non_exhaustive]
131pub struct Cursor {
132 pub column: NonZeroU16,
134 pub line: NonZeroU16,
136}
137
138#[derive(Copy, Clone, Debug)]
140#[non_exhaustive]
141pub struct State {
142 pub dimensions: Dimensions,
144 pub cursor: Cursor,
146}
147
148pub 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
176pub 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 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 stdout.write(b"\x06").map_err(Error)?;
212 stdout.blocking_flush().map_err(Error)?;
213
214 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#[non_exhaustive]
236#[derive(Debug, Copy, Clone, PartialEq, Eq)]
237#[allow(variant_size_differences)]
238pub enum Command<'a> {
239 Clear,
241 Alert,
243 Raw(bool),
245 Title(&'a str),
247 Screen(bool),
249}
250
251pub 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}