fatui 0.1.0

a silly little framework for terminal user interfaces
Documentation
//! input normalization and compression for `ansi` backends

use std::collections::VecDeque;

use crossterm::event::{self, KeyCode, KeyModifiers, ModifierKeyCode};

use crate::{
	Event,
	pos::{Pos, Size},
};

use super::CtEvent;

/// auto-collapsing queue of pending inputs
///
/// specifically:
/// - if there's been no meaningful input, it's `None`
/// - adjacent mouse movements are collapsed
///
/// todo:
/// - adjacent **modifier** press/releases or vice versa are discarded
#[derive(Debug, Default)]
pub struct InputQueue {
	evs: VecDeque<Event>,
	mods: ModChanges,
	pub new_sz: Option<Size>,
}

// a "queue" of pending changes to modifier states
//
// each field is one of:
// - `None`: this modifier has not been changed
// - `Some(true)`: this modifier has been pressed
// - `Some(false)`: this modifier has been released
// pushes just set the appropriate state,
// and then it can be read back to produce a minimized sequence of events!
// TODO: DRY this with crate::input::state::Keys?
macro_rules! mod_changes {
	($(
		$field:ident: $mkc:ident $(, $km:ident)?
	);* $(;)?) => {
		#[derive(Debug, Default)]
		struct ModChanges { $( $field: Option<bool> ),* }
		impl ModChanges {} // to unfuck sublime's syntax highlighting
		impl ModChanges {
			fn push_mkc(&mut self, down: bool, mkc: ModifierKeyCode) {
				match mkc {
					$(
						ModifierKeyCode::$mkc => self.$field = Some(down),
					)*
					other => todo!("support modifier {other}"),
				}
			}
			fn push_mods(&mut self, down: bool, mods: KeyModifiers) { $($(
				if mods.contains(KeyModifiers::$km) {
					self.$field = Some(down);
				}
			)?)* }

			fn pop(&mut self, evs: &mut VecDeque<Event>) { $(
				if let Some(down) = self.$field.take() {
					if down {
						evs.push_back(Event::KeyPress(KeyCode::Modifier(ModifierKeyCode::$mkc)))
					} else {
						evs.push_back(Event::KeyRelease(KeyCode::Modifier(ModifierKeyCode::$mkc)))
					}
				}
			)* }
		}
	}
}
mod_changes! {
	lctrl:  LeftControl, CONTROL;  rctrl:  RightControl;
	lalt:   LeftAlt,     ALT;      ralt:   RightAlt;
	lwin:   LeftSuper,   SUPER;    rwin:   RightSuper;
	lshift: LeftShift,   SHIFT;    rshift: RightShift;
	lhyper: LeftHyper,   HYPER;    rhyper: RightHyper;
	lmeta:  LeftMeta,    META;     rmeta:  RightMeta;
}

impl InputQueue {
	pub fn push_ct(&mut self, ct: CtEvent) {
		match ct {
			CtEvent::FocusGained | CtEvent::FocusLost => self.push(Event::Redraw),
			CtEvent::Paste(p) => self.push(Event::Paste(p.into())),
			CtEvent::Resize(w, h) => self.new_sz = Some(Size::rnew(w as _, h as _)),
			CtEvent::Key(ke) => {
				// just in case the keyboard enhancements aren't supported:
				self.mods.push_mods(true, ke.modifiers);
				match ke.kind {
					event::KeyEventKind::Press => self.push(Event::KeyPress(ke.code)),
					event::KeyEventKind::Repeat => self.push_all([
						// there's no documentation about what "repeat" means,
						// but i'm pretty sure it's a release then a press.
						// if not, file a bug with crossterm to document better.
						Event::KeyRelease(ke.code),
						Event::KeyPress(ke.code),
					]),
					event::KeyEventKind::Release => self.push(Event::KeyRelease(ke.code)),
				}
				self.mods.push_mods(false, ke.modifiers);
			}
			CtEvent::Mouse(me) => self.push(Event::MouseMove(Pos::rnew(me.column as _, me.row as _))),
		}
	}
	fn push(&mut self, ev: Event) {
		// collapse adjacent keyboard modifier presses
		match ev {
			Event::KeyPress(KeyCode::Modifier(mkc)) => {
				self.mods.push_mkc(true, mkc);
				return;
			}
			Event::KeyRelease(KeyCode::Modifier(mkc)) => {
				self.mods.push_mkc(false, mkc);
				return;
			}
			_ => self.mods.pop(&mut self.evs),
		}
		// collapse adjacent mouse moves
		if let Event::MouseMove(np) = ev
			&& let Some(Event::MouseMove(op)) = self.evs.back_mut()
		{
			*op = np;
			return;
		}
		// standard event handling
		self.evs.push_back(ev);
	}
	#[inline]
	fn push_all(&mut self, evs: impl IntoIterator<Item = Event>) {
		for ev in evs {
			self.push(ev);
		}
	}

	#[inline]
	pub fn pop(&mut self) -> Option<Event> {
		if self.evs.is_empty() {
			self.mods.pop(&mut self.evs);
		}
		self.evs.pop_front()
	}
}