use crate::cmd::{ExecRequest, TerminalCmd};
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 {
if let Some(id) = take_recycled_app_id() {
id
} else {
Self(next_fresh_app_id())
}
}
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);
static RECYCLED_APP_IDS: OnceLock<Mutex<Vec<u64>>> = OnceLock::new();
static APP_REGISTRATION_ORDER: OnceLock<Mutex<Vec<AppId>>> = OnceLock::new();
fn recycled_app_ids() -> &'static Mutex<Vec<u64>> {
RECYCLED_APP_IDS.get_or_init(|| Mutex::new(Vec::new()))
}
fn app_registration_order() -> &'static Mutex<Vec<AppId>> {
APP_REGISTRATION_ORDER.get_or_init(|| Mutex::new(Vec::new()))
}
fn take_recycled_app_id() -> Option<AppId> {
let mut ids = recycled_app_ids().lock().ok()?;
ids.pop().and_then(AppId::from_raw)
}
fn recycle_app_id(id: AppId) {
if let Ok(mut ids) = recycled_app_ids().lock() {
ids.push(id.raw());
}
}
fn next_fresh_app_id() -> u64 {
APP_ID_COUNTER
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
current.checked_add(1).filter(|next| *next != 0)
})
.unwrap_or_else(|_| panic!("AppId counter exhausted; cannot allocate more app IDs"))
}
fn track_registered_app(id: AppId) {
if let Ok(mut order) = app_registration_order().lock() {
order.push(id);
}
}
fn untrack_registered_app(id: AppId) -> Option<AppId> {
let mut order = app_registration_order().lock().ok()?;
if let Some(pos) = order.iter().position(|app_id| *app_id == id) {
order.remove(pos);
}
order.last().copied()
}
#[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 queue_terminal_cmd(&self, cmd: TerminalCmd);
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>>,
terminal_cmd_queue: Mutex<Vec<TerminalCmd>>,
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()),
terminal_cmd_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 queue_terminal_cmd(&self, cmd: TerminalCmd) {
match self.terminal_cmd_queue.lock() {
Ok(mut queue) => queue.push(cmd),
Err(poisoned) => poisoned.into_inner().push(cmd),
}
self.request_render();
}
pub(crate) fn take_terminal_cmds(&self) -> Vec<TerminalCmd> {
match self.terminal_cmd_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 queue_terminal_cmd(&self, cmd: TerminalCmd) {
AppRuntime::queue_terminal_cmd(self, cmd);
}
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);
track_registered_app(id);
set_current_app(Some(id));
}
AppRegistrationGuard { id }
}
fn unregister_app(id: AppId) {
if let Ok(mut registry) = registry().lock() {
registry.remove(&id);
}
let fallback_current = untrack_registered_app(id);
if AppId::from_raw(CURRENT_APP.load(Ordering::SeqCst)) == Some(id) {
set_current_app(fallback_current);
}
recycle_app_id(id);
}
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);
}
}
pub(crate) fn queue_terminal_cmd(cmd: TerminalCmd) {
if let Some(sink) = current_app_sink() {
sink.queue_terminal_cmd(cmd);
}
}
#[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);
}
#[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_unregister_current_app_falls_back_to_previous() {
let runtime1 = AppRuntime::new(false);
runtime1.clear_render_request();
let guard1 = register_app(runtime1.clone());
let runtime2 = AppRuntime::new(false);
runtime2.clear_render_request();
let guard2 = register_app(runtime2.clone());
request_render();
assert!(runtime2.render_requested());
assert!(!runtime1.render_requested());
runtime1.clear_render_request();
runtime2.clear_render_request();
drop(guard2);
request_render();
assert!(runtime1.render_requested());
drop(guard1);
}
#[test]
fn test_app_id_recycled_after_unregister() {
let runtime1 = AppRuntime::new(false);
let id1 = runtime1.id();
let guard1 = register_app(runtime1);
drop(guard1);
let runtime2 = AppRuntime::new(false);
let id2 = runtime2.id();
let guard2 = register_app(runtime2);
drop(guard2);
assert_eq!(id1, id2);
}
#[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());
}
}