use crate::cmd::{Cmd, ExecRequest};
use crate::core::Element;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct AppId(u64);
impl AppId {
pub(crate) fn new() -> Self {
Self(APP_ID_COUNTER.fetch_add(1, Ordering::SeqCst))
}
pub(crate) fn from_raw(raw: u64) -> Option<Self> {
if raw == 0 { None } else { Some(Self(raw)) }
}
pub(crate) fn raw(self) -> u64 {
self.0
}
}
static APP_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
#[derive(Clone)]
pub enum Printable {
Text(String),
Element(Box<Element>),
}
pub trait IntoPrintable {
fn into_printable(self) -> Printable;
}
impl IntoPrintable for String {
fn into_printable(self) -> Printable {
Printable::Text(self)
}
}
impl IntoPrintable for &str {
fn into_printable(self) -> Printable {
Printable::Text(self.to_string())
}
}
impl IntoPrintable for Element {
fn into_printable(self) -> Printable {
Printable::Element(Box::new(self))
}
}
pub(crate) trait AppSink: Send + Sync {
fn request_render(&self);
fn println(&self, message: Printable);
fn enter_alt_screen(&self);
fn exit_alt_screen(&self);
fn is_alt_screen(&self) -> bool;
fn queue_exec(&self, request: ExecRequest);
fn request_suspend(&self);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModeSwitch {
EnterAltScreen,
ExitAltScreen,
}
pub(crate) struct AppRuntime {
id: AppId,
render_flag: Arc<AtomicBool>,
println_queue: Mutex<Vec<Printable>>,
mode_switch_request: Mutex<Option<ModeSwitch>>,
alt_screen_state: Arc<AtomicBool>,
exec_queue: Mutex<Vec<ExecRequest>>,
suspend_request: AtomicBool,
}
impl AppRuntime {
pub(crate) fn new(alternate_screen: bool) -> Arc<Self> {
Arc::new(Self {
id: AppId::new(),
render_flag: Arc::new(AtomicBool::new(true)),
println_queue: Mutex::new(Vec::new()),
mode_switch_request: Mutex::new(None),
alt_screen_state: Arc::new(AtomicBool::new(alternate_screen)),
exec_queue: Mutex::new(Vec::new()),
suspend_request: AtomicBool::new(false),
})
}
pub(crate) fn id(&self) -> AppId {
self.id
}
pub(crate) fn set_alt_screen_state(&self, value: bool) {
self.alt_screen_state.store(value, Ordering::SeqCst);
}
pub(crate) fn render_requested(&self) -> bool {
self.render_flag.load(Ordering::SeqCst)
}
pub(crate) fn clear_render_request(&self) {
self.render_flag.store(false, Ordering::SeqCst);
}
pub(crate) fn take_mode_switch_request(&self) -> Option<ModeSwitch> {
match self.mode_switch_request.lock() {
Ok(mut request) => request.take(),
Err(poisoned) => {
poisoned.into_inner().take()
}
}
}
pub(crate) fn take_println_messages(&self) -> Vec<Printable> {
match self.println_queue.lock() {
Ok(mut queue) => std::mem::take(&mut *queue),
Err(poisoned) => {
std::mem::take(&mut *poisoned.into_inner())
}
}
}
pub(crate) fn queue_exec(&self, request: ExecRequest) {
match self.exec_queue.lock() {
Ok(mut queue) => queue.push(request),
Err(poisoned) => poisoned.into_inner().push(request),
}
self.request_render();
}
pub(crate) fn take_exec_requests(&self) -> Vec<ExecRequest> {
match self.exec_queue.lock() {
Ok(mut queue) => std::mem::take(&mut *queue),
Err(poisoned) => std::mem::take(&mut *poisoned.into_inner()),
}
}
pub(crate) fn request_suspend(&self) {
self.suspend_request.store(true, Ordering::SeqCst);
self.request_render();
}
pub(crate) fn suspend_requested(&self) -> bool {
self.suspend_request.load(Ordering::SeqCst)
}
pub(crate) fn take_suspend_request(&self) -> bool {
self.suspend_request.swap(false, Ordering::SeqCst)
}
}
impl AppSink for AppRuntime {
fn request_render(&self) {
self.render_flag.store(true, Ordering::SeqCst);
}
fn println(&self, message: Printable) {
match self.println_queue.lock() {
Ok(mut queue) => queue.push(message),
Err(poisoned) => poisoned.into_inner().push(message),
}
self.request_render();
}
fn enter_alt_screen(&self) {
match self.mode_switch_request.lock() {
Ok(mut request) => *request = Some(ModeSwitch::EnterAltScreen),
Err(poisoned) => *poisoned.into_inner() = Some(ModeSwitch::EnterAltScreen),
}
self.request_render();
}
fn exit_alt_screen(&self) {
match self.mode_switch_request.lock() {
Ok(mut request) => *request = Some(ModeSwitch::ExitAltScreen),
Err(poisoned) => *poisoned.into_inner() = Some(ModeSwitch::ExitAltScreen),
}
self.request_render();
}
fn is_alt_screen(&self) -> bool {
self.alt_screen_state.load(Ordering::SeqCst)
}
fn queue_exec(&self, request: ExecRequest) {
AppRuntime::queue_exec(self, request);
}
fn request_suspend(&self) {
self.request_suspend();
}
}
type AppRegistry = HashMap<AppId, Arc<dyn AppSink>>;
static APP_REGISTRY: OnceLock<Mutex<AppRegistry>> = OnceLock::new();
static CURRENT_APP: AtomicU64 = AtomicU64::new(0);
fn registry() -> &'static Mutex<AppRegistry> {
APP_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}
fn set_current_app(id: Option<AppId>) {
let raw = id.map(|value| value.raw()).unwrap_or(0);
CURRENT_APP.store(raw, Ordering::SeqCst);
}
pub(crate) fn current_app_sink() -> Option<Arc<dyn AppSink>> {
let id = AppId::from_raw(CURRENT_APP.load(Ordering::SeqCst))?;
let registry = registry().lock().ok()?;
registry.get(&id).cloned()
}
pub(crate) struct AppRegistrationGuard {
id: AppId,
}
impl Drop for AppRegistrationGuard {
fn drop(&mut self) {
unregister_app(self.id);
}
}
pub(crate) fn register_app(runtime: Arc<AppRuntime>) -> AppRegistrationGuard {
let id = runtime.id();
if let Ok(mut registry) = registry().lock() {
let sink: Arc<dyn AppSink> = runtime;
registry.insert(id, sink);
set_current_app(Some(id));
}
AppRegistrationGuard { id }
}
fn unregister_app(id: AppId) {
if let Ok(mut registry) = registry().lock() {
registry.remove(&id);
}
if AppId::from_raw(CURRENT_APP.load(Ordering::SeqCst)) == Some(id) {
set_current_app(None);
}
}
pub fn request_render() {
if let Some(sink) = current_app_sink() {
sink.request_render();
}
}
pub fn println(message: impl IntoPrintable) {
if let Some(sink) = current_app_sink() {
sink.println(message.into_printable());
return;
}
use crate::renderer::render_to_string_auto;
let printable = message.into_printable();
let output = match printable {
Printable::Text(text) => text,
Printable::Element(element) => render_to_string_auto(&element),
};
std::println!("{}", output);
}
pub fn println_trimmed(message: impl IntoPrintable) {
let printable = message.into_printable();
match printable {
Printable::Text(text) => println(text.trim_end()),
Printable::Element(element) => println(*element),
}
}
pub fn enter_alt_screen() {
if let Some(sink) = current_app_sink() {
sink.enter_alt_screen();
}
}
pub fn exit_alt_screen() {
if let Some(sink) = current_app_sink() {
sink.exit_alt_screen();
}
}
pub fn is_alt_screen() -> Option<bool> {
current_app_sink().map(|sink| sink.is_alt_screen())
}
pub(crate) fn queue_exec_request(request: ExecRequest) {
if let Some(sink) = current_app_sink() {
sink.queue_exec(request);
}
}
static TERMINAL_CMD_QUEUE: OnceLock<Mutex<Vec<Cmd>>> = OnceLock::new();
fn terminal_cmd_queue() -> &'static Mutex<Vec<Cmd>> {
TERMINAL_CMD_QUEUE.get_or_init(|| Mutex::new(Vec::new()))
}
pub(crate) fn queue_terminal_cmd(cmd: Cmd) {
if let Ok(mut queue) = terminal_cmd_queue().lock() {
queue.push(cmd);
}
request_render();
}
pub(crate) fn take_terminal_cmds() -> Vec<Cmd> {
match terminal_cmd_queue().lock() {
Ok(mut queue) => std::mem::take(&mut *queue),
Err(poisoned) => std::mem::take(&mut *poisoned.into_inner()),
}
}
#[derive(Clone)]
pub struct RenderHandle {
sink: Arc<dyn AppSink>,
}
impl RenderHandle {
pub(crate) fn new(sink: Arc<dyn AppSink>) -> Self {
Self { sink }
}
pub fn request_render(&self) {
self.sink.request_render();
}
pub fn println(&self, message: impl IntoPrintable) {
self.sink.println(message.into_printable());
}
pub fn enter_alt_screen(&self) {
self.sink.enter_alt_screen();
}
pub fn exit_alt_screen(&self) {
self.sink.exit_alt_screen();
}
pub fn is_alt_screen(&self) -> bool {
self.sink.is_alt_screen()
}
pub fn request_suspend(&self) {
self.sink.request_suspend();
}
}
pub fn render_handle() -> Option<RenderHandle> {
current_app_sink().map(RenderHandle::new)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_id_counter() {
let id1 = AppId::new();
let id2 = AppId::new();
assert_ne!(id1, id2);
assert!(id2.raw() > id1.raw());
}
#[test]
fn test_app_id_from_raw() {
assert_eq!(AppId::from_raw(0), None);
let id = AppId::from_raw(42).unwrap();
assert_eq!(id.raw(), 42);
}
#[test]
fn test_printable_text() {
let p = "hello".into_printable();
match p {
Printable::Text(text) => assert_eq!(text, "hello"),
_ => panic!("Expected Text"),
}
}
#[test]
fn test_printable_string() {
let p = String::from("world").into_printable();
match p {
Printable::Text(text) => assert_eq!(text, "world"),
_ => panic!("Expected Text"),
}
}
#[test]
fn test_app_runtime_creation() {
let runtime = AppRuntime::new(false);
assert!(!runtime.is_alt_screen());
assert!(runtime.render_requested()); }
#[test]
fn test_app_runtime_alt_screen() {
let runtime = AppRuntime::new(true);
assert!(runtime.is_alt_screen());
runtime.set_alt_screen_state(false);
assert!(!runtime.is_alt_screen());
}
#[test]
fn test_app_runtime_render_flag() {
let runtime = AppRuntime::new(false);
assert!(runtime.render_requested());
runtime.clear_render_request();
assert!(!runtime.render_requested());
runtime.request_render();
assert!(runtime.render_requested());
}
#[test]
fn test_app_runtime_println() {
let runtime = AppRuntime::new(false);
runtime.println(Printable::Text("test".to_string()));
let messages = runtime.take_println_messages();
assert_eq!(messages.len(), 1);
match &messages[0] {
Printable::Text(text) => assert_eq!(text, "test"),
_ => panic!("Expected Text"),
}
let messages2 = runtime.take_println_messages();
assert_eq!(messages2.len(), 0);
}
#[test]
fn test_app_runtime_mode_switch() {
let runtime = AppRuntime::new(false);
runtime.enter_alt_screen();
let switch = runtime.take_mode_switch_request();
assert_eq!(switch, Some(ModeSwitch::EnterAltScreen));
let switch2 = runtime.take_mode_switch_request();
assert_eq!(switch2, None);
}
#[test]
fn test_registry_operations() {
let runtime = AppRuntime::new(false);
let guard = register_app(runtime.clone());
let sink = current_app_sink();
assert!(sink.is_some());
request_render();
assert!(runtime.render_requested());
drop(guard);
let sink2 = current_app_sink();
assert!(sink2.is_none());
}
#[test]
fn test_println_fallback() {
println("test message");
println(String::from("another test"));
}
#[test]
fn test_cross_thread_apis() {
request_render();
enter_alt_screen();
exit_alt_screen();
assert_eq!(is_alt_screen(), None);
}
#[test]
fn test_render_handle() {
let runtime = AppRuntime::new(false);
let _guard = register_app(runtime.clone());
let handle = render_handle().expect("Should get handle");
handle.request_render();
assert!(runtime.render_requested());
runtime.clear_render_request();
handle.println("test");
assert!(runtime.render_requested());
}
}