use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
type Repainter = Arc<dyn Fn() + Send + Sync>;
#[derive(Debug, Clone, Copy)]
pub enum PromptSegment {
Left,
Right,
Indicator,
ViInsert,
ViNormal,
Multiline,
}
#[derive(Debug, Default, Clone)]
pub struct PromptContents {
pub left: Option<Arc<str>>,
pub right: Option<Arc<str>>,
pub indicator: Option<Arc<str>>,
pub vi_insert: Option<Arc<str>>,
pub vi_normal: Option<Arc<str>>,
pub multiline: Option<Arc<str>>,
pub render_right_on_last_line: bool,
}
impl PromptContents {
pub fn apply_segment_override(&mut self, segment: PromptSegment, content: impl Into<Arc<str>>) {
let content = content.into();
match segment {
PromptSegment::Left => self.left = Some(content),
PromptSegment::Right => self.right = Some(content),
PromptSegment::Indicator => self.indicator = Some(content),
PromptSegment::ViInsert => self.vi_insert = Some(content),
PromptSegment::ViNormal => self.vi_normal = Some(content),
PromptSegment::Multiline => self.multiline = Some(content),
}
}
pub fn overridden_by(&self, overrides: &PromptContents) -> PromptContents {
PromptContents {
left: overrides.left.clone().or_else(|| self.left.clone()),
right: overrides.right.clone().or_else(|| self.right.clone()),
indicator: overrides
.indicator
.clone()
.or_else(|| self.indicator.clone()),
vi_insert: overrides
.vi_insert
.clone()
.or_else(|| self.vi_insert.clone()),
vi_normal: overrides
.vi_normal
.clone()
.or_else(|| self.vi_normal.clone()),
multiline: overrides
.multiline
.clone()
.or_else(|| self.multiline.clone()),
render_right_on_last_line: self.render_right_on_last_line,
}
}
}
#[derive(derive_more::Debug, Default)]
pub struct PromptState {
contents: RwLock<PromptContents>,
#[debug(skip)]
repainter: Mutex<Option<Repainter>>,
}
impl PromptState {
pub fn new() -> Self {
Self::default()
}
fn acquire_read_lock(&self) -> RwLockReadGuard<'_, PromptContents> {
self.contents
.read()
.unwrap_or_else(|poisoned_error| poisoned_error.into_inner())
}
fn acquire_write_lock(&self) -> RwLockWriteGuard<'_, PromptContents> {
self.contents
.write()
.unwrap_or_else(|poisoned_error| poisoned_error.into_inner())
}
fn acquire_repainter_lock(&self) -> MutexGuard<'_, Option<Repainter>> {
self.repainter
.lock()
.unwrap_or_else(|poisoned_error| poisoned_error.into_inner())
}
pub fn with_contents<ReturnType>(
&self,
action: impl FnOnce(&PromptContents) -> ReturnType,
) -> ReturnType {
action(&self.acquire_read_lock())
}
fn modify_contents<ReturnType>(
&self,
action: impl FnOnce(&mut PromptContents) -> ReturnType,
) -> ReturnType {
action(&mut self.acquire_write_lock())
}
pub fn contents(&self) -> PromptContents {
self.with_contents(PromptContents::clone)
}
pub fn set_contents(&self, new_contents: PromptContents) {
self.modify_contents(|contents| *contents = new_contents);
}
pub fn apply(&self, overrides: impl FnOnce(&mut PromptContents)) {
self.modify_contents(overrides);
self.request_repaint();
}
pub fn set(&self, segment: PromptSegment, content: impl Into<Arc<str>>) {
let content = content.into();
self.apply(|contents| contents.apply_segment_override(segment, content));
}
pub fn set_repainter(&self, new_repainter: Option<Repainter>) {
*self.acquire_repainter_lock() = new_repainter;
}
fn request_repaint(&self) {
let local_repainter = self.acquire_repainter_lock().clone();
if let Some(repainter) = local_repainter {
repainter();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
fn setup_state_with_counter() -> (Arc<PromptState>, Arc<AtomicUsize>) {
let state = Arc::new(PromptState::new());
let repainter_count = Arc::new(AtomicUsize::new(0));
let counter_reference = Arc::clone(&repainter_count);
state.set_repainter(Some(Arc::new(move || {
counter_reference.fetch_add(1, Ordering::Relaxed);
})));
(state, repainter_count)
}
#[test]
fn set_writes_only_the_targeted_segment() {
let state = PromptState::new();
state.set(PromptSegment::Left, "LeftSegment");
let contents = state.contents();
assert_eq!(contents.left.as_deref(), Some("LeftSegment"));
assert_eq!(contents.right, None);
}
#[test]
fn indicator_vi_insert_vi_normal_and_multiline_are_independent() {
let state = PromptState::new();
state.apply(|contents| {
contents.apply_segment_override(PromptSegment::Indicator, "Indicator");
contents.apply_segment_override(PromptSegment::ViInsert, "ViInsert");
contents.apply_segment_override(PromptSegment::ViNormal, "ViNormal");
contents.apply_segment_override(PromptSegment::Multiline, "Multiline");
});
let contents = state.contents();
assert_eq!(contents.indicator.as_deref(), Some("Indicator"));
assert_eq!(contents.vi_insert.as_deref(), Some("ViInsert"));
assert_eq!(contents.vi_normal.as_deref(), Some("ViNormal"));
assert_eq!(contents.multiline.as_deref(), Some("Multiline"));
}
#[test]
fn each_set_triggers_exactly_one_repaint() {
let (state, repainter_count) = setup_state_with_counter();
state.set(PromptSegment::Left, "Alpha");
state.set(PromptSegment::Right, "Beta");
assert_eq!(repainter_count.load(Ordering::Relaxed), 2);
}
#[test]
fn apply_batches_multiple_segments_into_one_repaint() {
let (state, repainter_count) = setup_state_with_counter();
state.apply(|contents| {
contents.apply_segment_override(PromptSegment::Left, "Alpha");
contents.apply_segment_override(PromptSegment::Right, "Beta");
contents.apply_segment_override(PromptSegment::Indicator, "Gamma");
});
let contents = state.contents();
assert_eq!(contents.left.as_deref(), Some("Alpha"));
assert_eq!(contents.right.as_deref(), Some("Beta"));
assert_eq!(contents.indicator.as_deref(), Some("Gamma"));
assert_eq!(repainter_count.load(Ordering::Relaxed), 1);
}
#[test]
fn set_contents_overwrites_a_pushed_override() {
let state = PromptState::new();
state.set(PromptSegment::Left, "Pushed");
state.set_contents(PromptContents {
left: Some("Baseline".into()),
..Default::default()
});
assert_eq!(state.contents().left.as_deref(), Some("Baseline"));
}
#[test]
fn detaching_repainter_stops_repaints() {
let (state, repainter_count) = setup_state_with_counter();
state.set_repainter(None);
state.set(PromptSegment::Left, "Delta");
assert_eq!(repainter_count.load(Ordering::Relaxed), 0);
}
}