use rich_intuituive::prelude::*;
use crate::actions::keys_label;
use crate::app::{current_media, Browser};
use crate::clean::{markup, truncate};
use crate::config::Images;
use crate::engine::Playback;
const BAND: &str = "white on rgb(53,31,83)";
const DARK: &str = "rgb(200,203,215) on rgb(24,26,35)";
pub fn pixels_name(images: Images, pixels: rich_intuituive::Pixels) -> String {
match images {
Images::None => "NO PICTURES".into(),
_ => pixels.name().to_uppercase(),
}
}
pub fn media_state(playback: &Playback) -> String {
let time = format!(
"{} / {}",
clock(playback.time),
playback.duration.map_or("live".into(), clock)
);
let muted = if playback.muted { " muted" } else { "" };
format!("media {} {time}{muted}", playback.label())
}
pub fn clock(seconds: f64) -> String {
let s = seconds.max(0.0).floor() as u64;
format!("{}:{:02}", s / 60, s % 60)
}
pub fn header(b: Browser) -> Node {
let band = text(move || {
let state = b.state.get();
let back = if state.can_go_back {
"◀"
} else {
"[dim]◀[/]"
};
let forward = if state.can_go_forward {
"▶"
} else {
"[dim]▶[/]"
};
let loading = if state.loading { " [yellow]…[/]" } else { "" };
let title = b
.page
.with(|p| p.as_ref().map(|p| p.title.clone()))
.unwrap_or_default();
let view = b.view.get().name().to_uppercase();
let pixels = pixels_name(b.images.get(), b.pixels.get());
format!(
" [bold]NOCTILUCA[/] {view} + {pixels} {back}{forward}{loading} [bold]{}[/]",
markup(&truncate(&title, 80))
)
})
.no_wrap()
.style(BAND)
.fixed(1)
.name("header");
let address = text(move || {
let url = b
.page
.with(|p| p.as_ref().map(|p| p.url.clone()))
.unwrap_or_default();
format!(" {}", markup(&truncate(&url, 200)))
})
.no_wrap()
.style(DARK)
.fixed(1)
.name("address");
column([band, address]).fixed(2)
}
pub fn status(b: Browser) -> Node {
let keys_for = move |id: &str| b.keymap.with(|k| keys_label(k, id));
let state = text(move || {
if let Some(hints) = b.hints.get() {
return format!(
" [bold black on yellow] hints [/] type a label: {} [dim]Esc cancels[/]",
markup(&hints.typed)
);
}
let message = b.message.get();
if !message.is_empty() {
return format!(" {message}");
}
if let Some(find) = b.find.get() {
return format!(
" find: {} [dim]{} next · {} previous[/]",
markup(&find),
markup(&keys_for("find.next")),
markup(&keys_for("find.previous"))
);
}
let mut parts = vec![b.view.get().name().to_string()];
if b.page.with(|p| p.as_ref().is_some_and(|p| p.truncated)) {
parts.push("[yellow]page cut: too long to read whole[/]".into());
}
if let Some((_, playback)) = current_media(b) {
parts.push(markup(&media_state(&playback)));
if !b.config.with(|c| c.audio) {
parts.push("sound off (--audio)".into());
}
}
let rows = b.layout.with(|l| l.lines.len());
let row = b.scroll.get() + 1;
parts.push(format!("row {row}/{}", rows.max(1)));
format!(" {}", parts.join(" | "))
})
.no_wrap()
.style(DARK)
.fixed(1)
.name("status");
let keys = text(move || {
let has_media = current_media(b).is_some();
let mut parts = vec![
format!("{} actions", keys_for("actions")),
format!("{} hints", keys_for("hints")),
format!("{} open", keys_for("open")),
format!("{} reader", keys_for("reader")),
];
if has_media {
parts.push(format!("{} play/pause", keys_for("media.play_pause")));
parts.push(format!(
"{}/{} seek",
keys_for("media.seek_back"),
keys_for("media.seek_forward")
));
parts.push(format!("{} mute", keys_for("media.mute")));
}
parts.push(format!("{} quit", keys_for("quit")));
format!(" {}", markup(&parts.join(" | ")))
})
.no_wrap()
.style("dim")
.fixed(1)
.name("keys");
column([state, keys]).fixed(2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_media_state_says_what_the_element_does() {
let mut p = Playback {
paused: false,
volume: 1.0,
time: 3.4,
duration: Some(5.0),
ready: 4,
..Playback::default()
};
assert_eq!(media_state(&p), "media PLAYING 0:03 / 0:05");
p.paused = true;
p.muted = true;
assert_eq!(media_state(&p), "media PAUSED 0:03 / 0:05 muted");
p.paused = false;
p.muted = false;
p.ready = 2;
p.duration = None;
assert_eq!(media_state(&p), "media BUFFERING 0:03 / live");
p.ended = true;
assert!(media_state(&p).starts_with("media ENDED"));
}
}