Skip to main content

firefly_rust/
misc.rs

1use core::time::Duration;
2
3use crate::*;
4
5/// System settings. Can be requested using [`get_settings`].
6#[derive(Clone, Debug)]
7pub struct Settings {
8    /// The preferred color scheme of the player.
9    pub theme: Theme,
10
11    /// The configured interface language.
12    pub language: Language,
13
14    /// If true, the screen is rotated 180 degrees.
15    ///
16    /// In other words, the player holds the device upside-down.
17    /// The touchpad is now on the right and the buttons are on the left.
18    pub rotate_screen: bool,
19
20    /// The player has photosensitivity. The app should avoid any rapid flashes.
21    pub reduce_flashing: bool,
22
23    /// The player wants increased contrast for colors.
24    ///
25    /// If set, the black and white colors in the default
26    /// palette are adjusted automatically. All other colors
27    /// in the default palette or all colors in a custom palette
28    /// should be adjusted by the app.
29    pub contrast: bool,
30
31    /// If true, the player wants to see easter eggs, holiday effects, and weird jokes.
32    pub easter_eggs: bool,
33}
34
35#[derive(PartialEq, Eq, Copy, Clone, Debug, Default)]
36pub enum Language {
37    /// en ๐Ÿ‡ฌ๐Ÿ‡ง ๐Ÿ’‚
38    #[default]
39    English,
40    /// nl ๐Ÿ‡ณ๐Ÿ‡ฑ ๐Ÿง€
41    Dutch,
42    /// fr ๐Ÿ‡ซ๐Ÿ‡ท ๐Ÿฅ
43    French,
44    /// de ๐Ÿ‡ฉ๐Ÿ‡ช ๐Ÿฅจ
45    German,
46    /// it ๐Ÿ‡ฎ๐Ÿ‡น ๐Ÿ•
47    Italian,
48    /// pl ๐Ÿ‡ต๐Ÿ‡ฑ ๐ŸฅŸ
49    Polish,
50    /// ro ๐Ÿ‡ท๐Ÿ‡ด ๐Ÿง›
51    Romanian,
52    /// ru ๐Ÿ‡ท๐Ÿ‡บ ๐Ÿช†
53    Russian,
54    /// es ๐Ÿ‡ช๐Ÿ‡ธ ๐Ÿ‚
55    Spanish,
56    /// sv ๐Ÿ‡ธ๐Ÿ‡ช โ„๏ธ
57    Swedish,
58    /// tr ๐Ÿ‡น๐Ÿ‡ท ๐Ÿ•Œ
59    Turkish,
60    /// uk ๐Ÿ‡บ๐Ÿ‡ฆ โœŠ
61    Ukrainian,
62    /// tp ๐Ÿ‡จ๐Ÿ‡ฆ ๐Ÿ™‚
63    TokiPona,
64}
65
66impl Language {
67    #[must_use]
68    pub fn from_code(b: [u8; 2]) -> Option<Self> {
69        let code = match b {
70            [b'd', b'e'] => Self::German,
71            [b'e', b'n'] => Self::English,
72            [b'e', b's'] => Self::Spanish,
73            [b'f', b'r'] => Self::French,
74            [b'i', b't'] => Self::Italian,
75            [b'n', b'l'] => Self::Dutch,
76            [b'p', b'o'] => Self::Polish,
77            [b'r', b'o'] => Self::Romanian,
78            [b'r', b'u'] => Self::Russian,
79            [b's', b'v'] => Self::Swedish,
80            [b't', b'p'] => Self::TokiPona,
81            [b't', b'r'] => Self::Turkish,
82            [b'u', b'k'] => Self::Ukrainian,
83            _ => return None,
84        };
85        Some(code)
86    }
87
88    #[must_use]
89    pub fn code_array(self) -> [u8; 2] {
90        match self {
91            Self::English => [b'e', b'n'],
92            Self::Dutch => [b'n', b'l'],
93            Self::French => [b'f', b'r'],
94            Self::German => [b'd', b'e'],
95            Self::Italian => [b'i', b't'],
96            Self::Polish => [b'p', b'o'],
97            Self::Romanian => [b'r', b'o'],
98            Self::Russian => [b'r', b'u'],
99            Self::Spanish => [b'e', b's'],
100            Self::Swedish => [b's', b'v'],
101            Self::Turkish => [b't', b'r'],
102            Self::Ukrainian => [b'u', b'k'],
103            Self::TokiPona => [b't', b'p'],
104        }
105    }
106
107    #[must_use]
108    pub fn code_str(self) -> &'static str {
109        match self {
110            Self::English => "en",
111            Self::Dutch => "nl",
112            Self::French => "fr",
113            Self::German => "de",
114            Self::Italian => "it",
115            Self::Polish => "po",
116            Self::Romanian => "ro",
117            Self::Russian => "ru",
118            Self::Spanish => "es",
119            Self::Swedish => "sv",
120            Self::Turkish => "tr",
121            Self::Ukrainian => "uk",
122            Self::TokiPona => "tp",
123        }
124    }
125
126    /// The language name in English.
127    #[must_use]
128    pub fn name_english(self) -> &'static str {
129        match self {
130            Self::English => "English",
131            Self::Dutch => "Dutch",
132            Self::French => "French",
133            Self::German => "German",
134            Self::Italian => "Italian",
135            Self::Polish => "Polish",
136            Self::Romanian => "Romanian",
137            Self::Russian => "Russian",
138            Self::Spanish => "Spanish",
139            Self::Swedish => "Swedish",
140            Self::TokiPona => "TokiPona",
141            Self::Turkish => "Turkish",
142            Self::Ukrainian => "Ukrainian",
143        }
144    }
145
146    /// The language name in the language itself (endonym).
147    #[must_use]
148    pub fn name_native(self) -> &'static str {
149        match self {
150            Self::English => "English",
151            Self::Dutch => "Nederlands",
152            Self::French => "Franรงais",
153            Self::German => "Deutsch",
154            Self::Italian => "Italiano",
155            Self::Polish => "Polski",
156            Self::Romanian => "Romรขnฤƒ",
157            Self::Russian => "ะ ัƒััะบะธะน",
158            Self::Spanish => "Espaรฑol",
159            Self::Swedish => "Svenska",
160            Self::TokiPona => "toki pona",
161            Self::Turkish => "Tรผrkรงe",
162            Self::Ukrainian => "ะฃะบั€ะฐั—ะฝััŒะบะฐ",
163        }
164    }
165
166    /// ISO 8859 encoding slug for the language.
167    ///
168    /// Useful for dynamically loading the correct font for the given language.
169    #[must_use]
170    pub fn encoding(self) -> &'static str {
171        match self {
172            // Just like English, Dutch has very little non-ASCII characters
173            // which can be avoided in translations to make it possible to stick
174            // to the smaller fonts.
175            Self::English | Self::Dutch | Self::TokiPona => "ascii",
176            Self::Italian | Self::Spanish | Self::Swedish => "iso_8859_1",
177            Self::German | Self::French => "iso_8859_2",
178            Self::Russian | Self::Ukrainian => "iso_8859_5",
179            Self::Turkish => "iso_8859_9",
180            Self::Polish => "iso_8859_13",
181            Self::Romanian => "iso_8859_16",
182        }
183    }
184}
185
186/// The preferred color scheme of the peer.
187///
188/// Can be useful for:
189///
190/// * Making UI that matches the system UI.
191/// * Preventing image flashes by making the UI background
192///   the same as in the system UI.
193/// * Providing and auto-switching the dark and light mode.
194#[derive(Clone, Copy, Debug)]
195pub struct Theme {
196    pub id: u8,
197    /// The main color of text and boxes.
198    pub primary: Color,
199    /// The color of disable options, muted text, etc.
200    pub secondary: Color,
201    /// The color of important elements, active options, etc.
202    pub accent: Color,
203    /// The background color, the most contrast color to primary.
204    pub bg: Color,
205}
206
207impl Default for Theme {
208    fn default() -> Self {
209        Self {
210            id: 0,
211            primary: Color::Black,
212            secondary: Color::LightGray,
213            accent: Color::Green,
214            bg: Color::White,
215        }
216    }
217}
218
219/// The same as the stdlib `dbg!` but prints using [`log_debug`].
220#[macro_export]
221macro_rules! dbg {
222    () => {
223        $crate::log_debug!(
224            "[{}:{}:{}]",
225            $crate::file!(),
226            $crate::line!(),
227            $crate::column!()
228        )
229    };
230    ($val:expr $(,)?) => {
231        match $val {
232            tmp => {
233                $crate::log_debug!(
234                    "[{}:{}:{}] {} = {:#?}",
235                    $crate::file!(),
236                    $crate::line!(),
237                    $crate::column!(),
238                    $crate::stringify!($val),
239                    &&tmp as &dyn $crate::fmt::Debug,
240                );
241                tmp
242            }
243        }
244    };
245}
246
247/// A convenience macro for debug logging using [`log_debug`] function.
248///
249/// * When called without arguments, will print the current file name and line number.
250/// * When called with a string, just prints that string.
251/// * When called with multiple arguments, prints a string produced by [`format!`].
252#[macro_export]
253macro_rules! log_debug {
254    () => {
255        $crate::log_debug(concat!(file!(), ":", line!()));
256    };
257    ($f: literal) => {
258        $crate::log_debug($f);
259    };
260    ($f: expr) => {
261        $crate::log_debug(&$f);
262    };
263    ($f: literal, $( $a:expr ),* ) => {
264        $crate::log_debug(&alloc::format!($f, $( $a ),* ));
265    };
266}
267
268/// Log a debug message.
269pub fn log_debug(t: &str) {
270    let ptr = t.as_ptr() as u32;
271    let len = t.len() as u32;
272    unsafe {
273        bindings::log_debug(ptr, len);
274    }
275}
276
277/// A convenience macro for error logging using [`log_error`] function.
278///
279/// Equivalent to [`log_debug!`] but for errors.
280#[macro_export]
281macro_rules! log_error {
282    () => {
283        $crate::log_error(concat!(file!(), ":", line!()));
284    };
285    ($f: literal) => {
286        $crate::log_error($f);
287    };
288    ($f: expr) => {
289        $crate::log_error(&$f);
290    };
291    ($f: literal, $( $a:expr ),* ) => {
292        $crate::log_error(&alloc::format!($f, $( $a ),* ));
293    };
294}
295
296/// Log an error message.
297pub fn log_error(t: &str) {
298    let ptr = t.as_ptr() as u32;
299    let len = t.len() as u32;
300    unsafe {
301        bindings::log_error(ptr, len);
302    }
303}
304
305/// Set the random seed.
306pub fn set_seed(seed: u32) {
307    unsafe {
308        bindings::set_seed(seed);
309    }
310}
311
312/// Get a random value.
313#[must_use]
314pub fn get_random() -> u32 {
315    unsafe { bindings::get_random() }
316}
317
318// Get the time passed since the app was started.
319#[must_use]
320pub fn get_time() -> Duration {
321    let micros = unsafe { bindings::get_time() };
322    Duration::from_micros(micros)
323}
324
325/// Get the Peer's name.
326///
327/// The name is guaranteed to be a valid ASCII string
328/// and have between 1 and 16 characters.
329#[cfg(feature = "alloc")]
330#[must_use]
331pub fn get_name_buf(p: Peer) -> alloc::string::String {
332    let mut buf = [0u8; 16];
333    let name = get_name(p, &mut buf);
334    alloc::string::String::from(name)
335}
336
337/// Get the Peer's name.
338///
339/// The name is guaranteed to be a valid ASCII string
340/// and have between 1 and 16 characters.
341#[must_use]
342pub fn get_name(p: Peer, buf: &mut [u8; 16]) -> &str {
343    let ptr = buf.as_ptr() as u32;
344    let len = unsafe { bindings::get_name(u32::from(p.0), ptr) };
345    let buf = &buf[..len as usize];
346    unsafe { core::str::from_utf8_unchecked(buf) }
347}
348
349/// Get the peer's system settings.
350///
351/// **IMPORTANT:** This is the only function that accepts as input not only [`Peer`]
352/// but also [`Me`], which might lead to a state drift if used incorrectly.
353/// See [the docs](https://docs.fireflyzero.com/dev/net/) for more info.
354#[must_use]
355pub fn get_settings<P: AnyPeer>(p: P) -> Settings {
356    let raw = unsafe { bindings::get_settings(u32::from(p.into_u8())) };
357    let code = [(raw >> 8) as u8, raw as u8];
358    let language = Language::from_code(code).unwrap_or_default();
359    let flags = raw >> 16;
360    let theme = raw >> 32;
361    let theme = Theme {
362        id: theme as u8,
363        primary: parse_color(theme >> 20),
364        secondary: parse_color(theme >> 16),
365        accent: parse_color(theme >> 12),
366        bg: parse_color(theme >> 8),
367    };
368    Settings {
369        theme,
370        language,
371        rotate_screen: (flags & 0b0001) != 0,
372        reduce_flashing: (flags & 0b0010) != 0,
373        contrast: (flags & 0b0100) != 0,
374        easter_eggs: (flags & 0b1000) != 0,
375    }
376}
377
378fn parse_color(c: u64) -> Color {
379    Color::from((c as u8 & 0xf) + 1)
380}
381
382/// Exit the app after the current update is finished.
383pub fn quit() {
384    unsafe { bindings::quit() }
385}
386
387/// Restart the app after the current update is finished.
388pub fn restart() {
389    unsafe { bindings::restart() }
390}
391
392mod bindings {
393    #[link(wasm_import_module = "misc")]
394    unsafe extern "C" {
395        pub(crate) unsafe fn log_debug(ptr: u32, len: u32);
396        pub(crate) unsafe fn log_error(ptr: u32, len: u32);
397        pub(crate) unsafe fn set_seed(seed: u32);
398        pub(crate) unsafe fn get_random() -> u32;
399        pub(crate) unsafe fn get_time() -> u64;
400        pub(crate) unsafe fn get_name(idx: u32, ptr: u32) -> u32;
401        pub(crate) unsafe fn get_settings(idx: u32) -> u64;
402        pub(crate) unsafe fn restart();
403        pub(crate) unsafe fn quit();
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn test_language_code_roundtrip() {
413        let mut valid_codes = 0;
414        let letters = "abcdefghijklmnopqrstuvwxyz";
415        for fst in letters.as_bytes() {
416            for snd in letters.as_bytes() {
417                let given = [*fst, *snd];
418                let Some(lang) = Language::from_code(given) else {
419                    continue;
420                };
421                valid_codes += 1;
422                let actual = lang.code_array();
423                assert_eq!(actual, given);
424            }
425        }
426        assert!(valid_codes > 8);
427    }
428}