aboutsummaryrefslogtreecommitdiff
path: root/src/pane/cursor.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/pane/cursor.rs')
-rw-r--r--src/pane/cursor.rs315
1 files changed, 315 insertions, 0 deletions
diff --git a/src/pane/cursor.rs b/src/pane/cursor.rs
new file mode 100644
index 00000000..c72f697a
--- /dev/null
+++ b/src/pane/cursor.rs
@@ -0,0 +1,315 @@
+use std::time::{Duration, Instant};
+
+use super::terminal::TerminalCursorState;
+
+pub(crate) const CURSOR_POSITION_SETTLE: Duration = Duration::from_millis(20);
+const CURSOR_POSITION_MAX_HOLD: Duration = Duration::from_millis(100);
+
+#[derive(Debug, Default)]
+pub(crate) struct DecscusrTracker {
+ state: DecscusrParseState,
+ cursor_shape_overridden: bool,
+}
+
+#[derive(Debug, Default)]
+enum DecscusrParseState {
+ #[default]
+ Ground,
+ Escape,
+ Csi {
+ first_param: Option<u16>,
+ collecting_first_param: bool,
+ has_space_intermediate: bool,
+ },
+}
+
+impl DecscusrTracker {
+ pub(crate) fn observe(&mut self, bytes: &[u8]) {
+ for &byte in bytes {
+ self.observe_byte(byte);
+ }
+ }
+
+ fn observe_byte(&mut self, byte: u8) {
+ match &mut self.state {
+ DecscusrParseState::Ground => {
+ if byte == 0x1b {
+ self.state = DecscusrParseState::Escape;
+ }
+ }
+ DecscusrParseState::Escape => {
+ self.state = if byte == b'[' {
+ DecscusrParseState::Csi {
+ first_param: None,
+ collecting_first_param: true,
+ has_space_intermediate: false,
+ }
+ } else if byte == 0x1b {
+ DecscusrParseState::Escape
+ } else {
+ DecscusrParseState::Ground
+ };
+ }
+ DecscusrParseState::Csi {
+ first_param,
+ collecting_first_param,
+ has_space_intermediate,
+ } => {
+ if byte == 0x1b {
+ self.state = DecscusrParseState::Escape;
+ } else if byte.is_ascii_digit() && *collecting_first_param {
+ let digit = u16::from(byte - b'0');
+ *first_param = Some(first_param.unwrap_or(0).saturating_mul(10) + digit);
+ } else if byte == b';' || byte == b':' {
+ *collecting_first_param = false;
+ } else if byte == b' ' {
+ *has_space_intermediate = true;
+ *collecting_first_param = false;
+ } else if (0x40..=0x7e).contains(&byte) {
+ if byte == b'q' && *has_space_intermediate {
+ let param = first_param.unwrap_or(0);
+ if param <= 6 {
+ self.cursor_shape_overridden = param != 0;
+ }
+ }
+ self.state = DecscusrParseState::Ground;
+ } else if !(0x20..=0x3f).contains(&byte) {
+ self.state = DecscusrParseState::Ground;
+ }
+ }
+ }
+ }
+
+ pub(crate) fn cursor_shape_overridden(&self) -> bool {
+ self.cursor_shape_overridden
+ }
+}
+
+#[derive(Debug, Default)]
+pub(crate) struct CursorPositionSettleState {
+ settled: Option<TerminalCursorState>,
+ candidate: Option<TerminalCursorState>,
+ pending_since: Option<Instant>,
+}
+
+impl CursorPositionSettleState {
+ pub(crate) fn observe(&mut self, current: Option<TerminalCursorState>, now: Instant) {
+ let Some(current) = current else {
+ self.settled = None;
+ self.candidate = None;
+ self.pending_since = None;
+ return;
+ };
+ if !current.visible {
+ self.settled = Some(current);
+ self.candidate = None;
+ self.pending_since = None;
+ return;
+ }
+ let Some(settled) = self.settled else {
+ self.settled = Some(current);
+ self.candidate = None;
+ self.pending_since = None;
+ return;
+ };
+ if same_cursor_position(settled, current) && settled.visible {
+ self.settled = Some(current);
+ self.candidate = None;
+ self.pending_since = None;
+ return;
+ }
+
+ let Some(candidate) = self.candidate else {
+ self.candidate = Some(current);
+ self.pending_since = Some(now);
+ return;
+ };
+
+ let pending_since = self.pending_since.unwrap_or(now);
+ if now.duration_since(pending_since) >= CURSOR_POSITION_MAX_HOLD {
+ self.settled = Some(current);
+ self.candidate = None;
+ self.pending_since = None;
+ } else if same_cursor_position(candidate, current) {
+ if now.duration_since(pending_since) >= CURSOR_POSITION_SETTLE {
+ self.settled = Some(current);
+ self.candidate = None;
+ self.pending_since = None;
+ } else {
+ self.candidate = Some(current);
+ }
+ } else {
+ self.candidate = Some(current);
+ }
+ }
+
+ pub(crate) fn reported_cursor(
+ &self,
+ current: Option<TerminalCursorState>,
+ now: Instant,
+ ) -> Option<TerminalCursorState> {
+ let current = current?;
+ let Some(candidate) = self.candidate else {
+ return Some(current);
+ };
+ let pending_since = self.pending_since.unwrap_or(now);
+ if now.duration_since(pending_since) >= CURSOR_POSITION_SETTLE {
+ return Some(TerminalCursorState {
+ visible: current.visible && candidate.visible,
+ shape: current.shape,
+ ..candidate
+ });
+ }
+ self.settled
+ .map(|settled| TerminalCursorState {
+ visible: current.visible && settled.visible,
+ shape: current.shape,
+ ..settled
+ })
+ .or(Some(TerminalCursorState {
+ visible: false,
+ shape: current.shape,
+ ..candidate
+ }))
+ }
+
+ pub(crate) fn pending(&self) -> bool {
+ self.candidate.is_some()
+ }
+}
+
+fn same_cursor_position(left: TerminalCursorState, right: TerminalCursorState) -> bool {
+ left.x == right.x && left.y == right.y
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn cursor(x: u16, y: u16, visible: bool, shape: u8) -> TerminalCursorState {
+ TerminalCursorState {
+ x,
+ y,
+ visible,
+ shape,
+ }
+ }
+
+ #[test]
+ fn cursor_settle_holds_position_change_until_quiet_window() {
+ let now = Instant::now();
+ let mut settle = CursorPositionSettleState::default();
+ settle.observe(Some(cursor(1, 0, true, 0)), now);
+ settle.observe(Some(cursor(20, 5, true, 0)), now + Duration::from_millis(1));
+
+ let reported = settle
+ .reported_cursor(Some(cursor(20, 5, true, 0)), now + Duration::from_millis(2))
+ .unwrap();
+
+ assert_eq!((reported.x, reported.y), (1, 0));
+ }
+
+ #[test]
+ fn cursor_settle_adopts_position_change_after_quiet_window() {
+ let now = Instant::now();
+ let mut settle = CursorPositionSettleState::default();
+ settle.observe(Some(cursor(1, 0, true, 0)), now);
+ settle.observe(Some(cursor(2, 0, true, 0)), now + Duration::from_millis(1));
+
+ let reported = settle
+ .reported_cursor(
+ Some(cursor(2, 0, true, 0)),
+ now + CURSOR_POSITION_SETTLE + Duration::from_millis(1),
+ )
+ .unwrap();
+
+ assert_eq!((reported.x, reported.y), (2, 0));
+ }
+
+ #[test]
+ fn cursor_settle_caps_continuous_position_changes_from_first_pending_time() {
+ let now = Instant::now();
+ let mut settle = CursorPositionSettleState::default();
+ settle.observe(Some(cursor(1, 0, true, 0)), now);
+ settle.observe(Some(cursor(2, 0, true, 0)), now + Duration::from_millis(1));
+ settle.observe(
+ Some(cursor(3, 0, true, 0)),
+ now + CURSOR_POSITION_MAX_HOLD + Duration::from_millis(1),
+ );
+
+ assert!(!settle.pending());
+ assert_eq!(
+ settle.reported_cursor(
+ Some(cursor(3, 0, true, 0)),
+ now + CURSOR_POSITION_MAX_HOLD + Duration::from_millis(2),
+ ),
+ Some(cursor(3, 0, true, 0))
+ );
+ }
+
+ #[test]
+ fn cursor_settle_keeps_render_read_pure() {
+ let now = Instant::now();
+ let mut settle = CursorPositionSettleState::default();
+ settle.observe(Some(cursor(1, 0, true, 0)), now);
+ settle.observe(Some(cursor(2, 0, true, 0)), now + Duration::from_millis(1));
+
+ assert!(settle.pending());
+ let _ = settle.reported_cursor(
+ Some(cursor(2, 0, true, 0)),
+ now + CURSOR_POSITION_SETTLE + Duration::from_millis(1),
+ );
+
+ assert!(settle.pending());
+ }
+
+ #[test]
+ fn cursor_settle_passes_shape_through_while_position_is_held() {
+ let now = Instant::now();
+ let mut settle = CursorPositionSettleState::default();
+ settle.observe(Some(cursor(1, 0, true, 2)), now);
+ settle.observe(Some(cursor(2, 0, true, 6)), now + Duration::from_millis(1));
+
+ let reported = settle
+ .reported_cursor(Some(cursor(2, 0, true, 6)), now + Duration::from_millis(2))
+ .unwrap();
+
+ assert_eq!((reported.x, reported.y, reported.shape), (1, 0, 6));
+ }
+
+ #[test]
+ fn cursor_settle_passes_shape_through_after_quiet_window() {
+ let now = Instant::now();
+ let mut settle = CursorPositionSettleState::default();
+ settle.observe(Some(cursor(1, 0, true, 2)), now);
+ settle.observe(Some(cursor(2, 0, true, 2)), now + Duration::from_millis(1));
+
+ let reported = settle
+ .reported_cursor(
+ Some(cursor(2, 0, true, 6)),
+ now + CURSOR_POSITION_SETTLE + Duration::from_millis(1),
+ )
+ .unwrap();
+
+ assert_eq!((reported.x, reported.y, reported.shape), (2, 0, 6));
+ }
+
+ #[test]
+ fn cursor_settle_hides_immediately_and_waits_to_reveal() {
+ let now = Instant::now();
+ let mut settle = CursorPositionSettleState::default();
+ settle.observe(Some(cursor(1, 0, true, 0)), now);
+ settle.observe(Some(cursor(1, 0, false, 0)), now + Duration::from_millis(1));
+
+ assert_eq!(
+ settle.reported_cursor(Some(cursor(1, 0, false, 0)), now + Duration::from_millis(2)),
+ Some(cursor(1, 0, false, 0))
+ );
+
+ settle.observe(Some(cursor(1, 0, true, 0)), now + Duration::from_millis(3));
+ assert_eq!(
+ settle.reported_cursor(Some(cursor(1, 0, true, 0)), now + Duration::from_millis(4)),
+ Some(cursor(1, 0, false, 0))
+ );
+ }
+}