use bubbletea_rs::{tick, Cmd, Model as BubbleTeaModel, Msg};
use lipgloss_extras::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
static LAST_ID: AtomicUsize = AtomicUsize::new(0);
fn next_id() -> usize {
LAST_ID.fetch_add(1, Ordering::Relaxed)
}
const DEFAULT_BLINK_SPEED: Duration = Duration::from_millis(530);
#[derive(Debug, Clone)]
pub struct InitialBlinkMsg;
#[derive(Debug, Clone)]
pub struct BlinkMsg {
pub id: usize,
pub tag: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Blink,
Static,
Hide,
}
impl std::fmt::Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Mode::Blink => "blink",
Mode::Static => "static",
Mode::Hide => "hidden",
}
)
}
}
#[derive(Debug, Clone)]
pub struct Model {
pub blink_speed: Duration,
pub style: Style,
pub text_style: Style,
char: String,
id: usize,
focus: bool,
is_off_phase: bool, blink_tag: usize,
mode: Mode,
}
impl Default for Model {
fn default() -> Self {
Self {
blink_speed: DEFAULT_BLINK_SPEED,
style: Style::new(),
text_style: Style::new(),
char: " ".to_string(),
id: next_id(),
focus: false,
is_off_phase: true, blink_tag: 0,
mode: Mode::Blink,
}
}
}
impl Model {
pub fn new() -> Self {
Self::default()
}
pub fn set_visible(&mut self, visible: bool) {
self.is_off_phase = !visible;
}
pub fn update(&mut self, msg: &Msg) -> Option<Cmd> {
if msg.downcast_ref::<InitialBlinkMsg>().is_some() {
if self.mode != Mode::Blink || !self.focus {
return None;
}
return self.blink_cmd();
}
if let Some(blink_msg) = msg.downcast_ref::<BlinkMsg>() {
if self.mode != Mode::Blink || !self.focus {
return None;
}
if blink_msg.id != self.id || blink_msg.tag != self.blink_tag {
return None;
}
self.is_off_phase = !self.is_off_phase;
return self.blink_cmd();
}
None
}
pub fn mode(&self) -> Mode {
self.mode
}
pub fn set_mode(&mut self, mode: Mode) -> Option<Cmd> {
self.mode = mode;
self.is_off_phase = self.mode == Mode::Hide || !self.focus;
if mode == Mode::Blink {
return Some(blink());
}
None
}
fn blink_cmd(&mut self) -> Option<Cmd> {
if self.mode != Mode::Blink {
return None;
}
self.blink_tag += 1;
let tag = self.blink_tag;
let id = self.id;
let speed = self.blink_speed;
Some(tick(speed, move |_| Box::new(BlinkMsg { id, tag }) as Msg))
}
pub fn focus(&mut self) -> Option<Cmd> {
self.focus = true;
self.is_off_phase = self.mode == Mode::Hide; if self.mode == Mode::Blink && self.focus {
return self.blink_cmd();
}
None
}
pub fn blur(&mut self) {
self.focus = false;
self.is_off_phase = true;
}
pub fn focused(&self) -> bool {
self.focus
}
pub fn set_char(&mut self, s: &str) {
self.char = s.to_string();
}
pub fn view(&self) -> String {
if self.mode == Mode::Hide || self.is_off_phase {
return self.text_style.clone().inline(true).render(&self.char);
}
self.style
.clone()
.inline(true)
.reverse(true)
.render(&self.char)
}
}
impl BubbleTeaModel for Model {
fn init() -> (Self, Option<Cmd>) {
let model = Self::new();
(model, Some(blink()))
}
fn update(&mut self, msg: Msg) -> Option<Cmd> {
self.update(&msg)
}
fn view(&self) -> String {
self.view()
}
}
pub fn blink() -> Cmd {
tick(Duration::from_millis(0), |_| {
Box::new(InitialBlinkMsg) as Msg
})
}
pub fn new() -> Model {
Model::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blink_cmd_tag_captured_no_race() {
let mut m = Model::new();
m.blink_speed = Duration::from_millis(10);
m.mode = Mode::Blink;
m.focus = true;
let _cmd1 = m.blink_cmd().expect("cmd1");
let expected_tag = m.blink_tag; let _expected_id = m.id;
let _cmd2 = m.blink_cmd();
let new_tag = m.blink_tag;
assert_ne!(
expected_tag, new_tag,
"Tags should be different after second blink_cmd"
);
}
}