use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use bubbletea::{Cmd, Message, Model};
use lipgloss::Style;
const DEFAULT_BLINK_SPEED: Duration = Duration::from_millis(530);
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
fn next_id() -> u64 {
NEXT_ID.fetch_add(1, Ordering::Relaxed)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mode {
#[default]
Blink,
Static,
Hide,
}
impl std::fmt::Display for Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Blink => write!(f, "blink"),
Self::Static => write!(f, "static"),
Self::Hide => write!(f, "hidden"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct InitialBlinkMsg;
#[derive(Debug, Clone, Copy)]
pub struct BlinkMsg {
pub id: u64,
pub tag: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct BlinkCanceledMsg;
#[derive(Debug, Clone)]
pub struct Cursor {
pub blink_speed: Duration,
pub style: Style,
pub text_style: Style,
char: String,
id: u64,
focus: bool,
blink: bool,
blink_tag: u64,
mode: Mode,
}
impl Default for Cursor {
fn default() -> Self {
Self::new()
}
}
impl Cursor {
#[must_use]
pub fn new() -> Self {
Self {
blink_speed: DEFAULT_BLINK_SPEED,
style: Style::new(),
text_style: Style::new(),
char: String::new(),
id: next_id(),
focus: false,
blink: true,
blink_tag: 0,
mode: Mode::Blink,
}
}
#[must_use]
pub fn id(&self) -> u64 {
self.id
}
#[must_use]
pub fn mode(&self) -> Mode {
self.mode
}
pub fn set_mode(&mut self, mode: Mode) -> Option<Cmd> {
self.mode = mode;
self.blink = mode == Mode::Hide || !self.focus;
if mode == Mode::Blink {
Some(blink_cmd())
} else {
None
}
}
pub fn set_char(&mut self, c: &str) {
self.char = c.to_string();
}
#[must_use]
pub fn char(&self) -> &str {
&self.char
}
#[must_use]
pub fn focused(&self) -> bool {
self.focus
}
#[must_use]
pub fn is_blinking_off(&self) -> bool {
self.blink
}
pub fn focus(&mut self) -> Option<Cmd> {
self.focus = true;
self.blink = self.mode == Mode::Hide;
if self.mode == Mode::Blink && self.focus {
Some(self.blink_tick_cmd())
} else {
None
}
}
pub fn blur(&mut self) {
self.focus = false;
self.blink = true;
}
fn blink_tick_cmd(&mut self) -> Cmd {
if self.mode != Mode::Blink {
return Cmd::new(|| Message::new(BlinkCanceledMsg));
}
self.blink_tag = self.blink_tag.wrapping_add(1);
let id = self.id;
let tag = self.blink_tag;
let speed = self.blink_speed;
Cmd::new(move || {
std::thread::sleep(speed);
Message::new(BlinkMsg { id, tag })
})
}
pub fn update(&mut self, msg: Message) -> Option<Cmd> {
if msg.is::<InitialBlinkMsg>() {
if self.mode != Mode::Blink || !self.focus {
return None;
}
return Some(self.blink_tick_cmd());
}
if msg.is::<bubbletea::FocusMsg>() {
return self.focus();
}
if msg.is::<bubbletea::BlurMsg>() {
self.blur();
return None;
}
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.blink = !self.blink;
return Some(self.blink_tick_cmd());
}
if msg.is::<BlinkCanceledMsg>() {
return None;
}
None
}
#[must_use]
pub fn view(&self) -> String {
if self.blink {
self.text_style.clone().inline().render(&self.char)
} else {
self.style.clone().inline().reverse().render(&self.char)
}
}
}
#[must_use]
pub fn blink_cmd() -> Cmd {
Cmd::new(|| Message::new(InitialBlinkMsg))
}
impl Model for Cursor {
fn init(&self) -> Option<Cmd> {
if self.mode == Mode::Blink && self.focus {
Some(blink_cmd())
} else {
None
}
}
fn update(&mut self, msg: Message) -> Option<Cmd> {
Cursor::update(self, msg)
}
fn view(&self) -> String {
Cursor::view(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cursor_new() {
let cursor = Cursor::new();
assert_eq!(cursor.mode(), Mode::Blink);
assert!(!cursor.focused());
assert!(cursor.is_blinking_off());
}
#[test]
fn test_cursor_unique_ids() {
let cursor1 = Cursor::new();
let cursor2 = Cursor::new();
assert_ne!(cursor1.id(), cursor2.id());
}
#[test]
fn test_cursor_focus_blur() {
let mut cursor = Cursor::new();
assert!(!cursor.focused());
cursor.focus();
assert!(cursor.focused());
assert!(!cursor.is_blinking_off());
cursor.blur();
assert!(!cursor.focused());
assert!(cursor.is_blinking_off()); }
#[test]
fn test_cursor_mode_static() {
let mut cursor = Cursor::new();
cursor.set_mode(Mode::Static);
assert_eq!(cursor.mode(), Mode::Static);
}
#[test]
fn test_cursor_mode_hide() {
let mut cursor = Cursor::new();
cursor.set_mode(Mode::Hide);
assert_eq!(cursor.mode(), Mode::Hide);
assert!(cursor.is_blinking_off());
}
#[test]
fn test_cursor_set_char() {
let mut cursor = Cursor::new();
cursor.set_char("_");
assert_eq!(cursor.char(), "_");
}
#[test]
fn test_cursor_view() {
let mut cursor = Cursor::new();
cursor.set_char("a");
let view = cursor.view();
assert!(!view.is_empty());
}
fn strip_ansi(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut in_escape = false;
let mut in_csi = false;
for c in s.chars() {
if c == '\x1b' {
in_escape = true;
in_csi = false;
continue;
}
if in_escape {
if c == '[' {
in_csi = true;
continue;
}
if in_csi {
if ('@'..='~').contains(&c) {
in_escape = false;
in_csi = false;
}
continue;
}
in_escape = false;
continue;
}
result.push(c);
}
result
}
#[test]
fn test_cursor_view_inline_removes_padding() {
let mut cursor = Cursor::new();
cursor.set_char("x");
cursor.text_style = Style::new().padding(1);
cursor.blink = true;
assert_eq!(cursor.view(), "x");
cursor.style = Style::new().padding(1);
cursor.blink = false;
assert_eq!(strip_ansi(&cursor.view()), "x");
}
#[test]
fn test_mode_display() {
assert_eq!(Mode::Blink.to_string(), "blink");
assert_eq!(Mode::Static.to_string(), "static");
assert_eq!(Mode::Hide.to_string(), "hidden");
}
#[test]
fn test_blink_msg_routing() {
let mut cursor1 = Cursor::new();
let mut cursor2 = Cursor::new();
cursor1.focus();
cursor2.focus();
let msg = Message::new(BlinkMsg {
id: cursor1.id(),
tag: cursor1.blink_tag,
});
let cmd2 = cursor2.update(msg);
assert!(cmd2.is_none()); }
#[test]
fn test_model_init_unfocused() {
let cursor = Cursor::new();
let cmd = Model::init(&cursor);
assert!(cmd.is_none());
}
#[test]
fn test_model_init_focused_blink() {
let mut cursor = Cursor::new();
cursor.focus();
let cmd = Model::init(&cursor);
assert!(cmd.is_some());
}
#[test]
fn test_model_init_focused_static() {
let mut cursor = Cursor::new();
cursor.set_mode(Mode::Static);
cursor.focus();
let cmd = Model::init(&cursor);
assert!(cmd.is_none());
}
#[test]
fn test_model_view() {
let mut cursor = Cursor::new();
cursor.set_char("x");
let model_view = Model::view(&cursor);
let cursor_view = Cursor::view(&cursor);
assert_eq!(model_view, cursor_view);
}
}