use crate::{
BrlApiError, Connection, Result, TtyMode,
parameters::{Parameter, ParameterFlags},
};
use std::collections::VecDeque;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppType {
ScreenReader,
ScreenReaderReview,
SystemAlert,
UserApp,
BackgroundTask,
Debug,
LowPriority,
}
impl AppType {
pub fn base_priority(self) -> u32 {
match self {
AppType::ScreenReader => 50, AppType::ScreenReaderReview => 70, AppType::SystemAlert => 65, AppType::UserApp => 45, AppType::BackgroundTask => 45, AppType::Debug => 45, AppType::LowPriority => 15, }
}
pub fn default_duration(self) -> Duration {
match self {
AppType::ScreenReader => Duration::from_secs(5), AppType::ScreenReaderReview => Duration::from_secs(5), AppType::SystemAlert => Duration::from_secs(4), AppType::UserApp => Duration::from_secs(3), AppType::BackgroundTask => Duration::from_secs(4), AppType::Debug => Duration::from_secs(3), AppType::LowPriority => Duration::from_secs(2), }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentQuality {
Good,
Fair,
Poor,
None,
}
impl ContentQuality {
pub fn priority_offset(self) -> i32 {
match self {
ContentQuality::Good => 20, ContentQuality::Fair => 10, ContentQuality::Poor => 0, ContentQuality::None => -10, }
}
pub fn default_duration(self) -> Duration {
match self {
ContentQuality::Good => Duration::from_secs(5), ContentQuality::Fair => Duration::from_secs(3), ContentQuality::Poor => Duration::from_secs(2), ContentQuality::None => Duration::from_secs(1), }
}
}
#[derive(Debug, Clone)]
pub struct CooperationConfig {
pub respect_screen_reader_focus: bool,
pub auto_adjust_priority: bool,
pub brief_notification_time: Duration,
pub retry_when_busy: bool,
pub retry_delay: Duration,
pub max_retries: u32,
}
impl Default for CooperationConfig {
fn default() -> Self {
Self {
respect_screen_reader_focus: true,
auto_adjust_priority: true,
brief_notification_time: Duration::from_secs(2), retry_when_busy: true,
retry_delay: Duration::from_secs(1),
max_retries: 3,
}
}
}
#[derive(Debug, Clone)]
struct QueuedMessage {
text: String,
quality: ContentQuality,
duration: Duration,
_queued_at: Instant,
retries: u32,
}
#[derive(Debug)]
pub struct CooperativeDisplay {
connection: Connection,
app_type: AppType,
config: CooperationConfig,
message_queue: VecDeque<QueuedMessage>,
current_priority: Option<u32>,
focus_tracker: FocusTracker,
}
#[derive(Debug, Clone)]
struct FocusTracker {
screen_reader_detected: bool,
last_check: Option<std::time::Instant>,
}
impl FocusTracker {
fn new() -> Self {
Self {
screen_reader_detected: false,
last_check: None,
}
}
fn update_screen_reader_detection(&mut self) -> bool {
self.screen_reader_detected = false;
self.screen_reader_detected
}
fn screen_reader_likely_active(&mut self) -> bool {
let now = std::time::Instant::now();
if self
.last_check
.is_none_or(|last| now.duration_since(last) > std::time::Duration::from_secs(5))
{
self.update_screen_reader_detection();
self.last_check = Some(now);
}
self.screen_reader_detected
}
}
impl CooperativeDisplay {
pub fn open(app_type: AppType) -> Result<Self> {
Self::open_with_config(app_type, CooperationConfig::default())
}
pub fn open_with_config(app_type: AppType, config: CooperationConfig) -> Result<Self> {
let connection = Connection::open()?;
let mut display = Self {
connection,
app_type,
config,
message_queue: VecDeque::new(),
current_priority: None,
focus_tracker: FocusTracker::new(),
};
display.set_client_priority(app_type.base_priority())?;
Ok(display)
}
pub fn background_app() -> Result<Self> {
let config = CooperationConfig {
brief_notification_time: Duration::from_secs(3), ..Default::default()
};
Self::open_with_config(AppType::BackgroundTask, config)
}
pub fn interactive_app() -> Result<Self> {
Self::open(AppType::UserApp)
}
pub fn show_message(&self, text: &str) -> Result<()> {
self.show_content(text, ContentQuality::Fair)
}
pub fn show_brief_message(&self, text: &str, duration: Duration) -> Result<()> {
self.show_content_with_duration(text, ContentQuality::Good, duration)
}
pub fn show_content(&self, text: &str, quality: ContentQuality) -> Result<()> {
let duration = quality.default_duration();
self.show_content_with_duration(text, quality, duration)
}
pub fn show_content_with_duration(
&self,
text: &str,
quality: ContentQuality,
duration: Duration,
) -> Result<()> {
let base_priority = self.app_type.base_priority();
let adjusted_priority = if self.config.auto_adjust_priority {
((base_priority as i32) + quality.priority_offset()).max(0) as u32
} else {
base_priority
};
let _priority_guard = self.temporary_priority_adjustment(adjusted_priority)?;
self.display_with_timeout(text, duration)
}
pub fn show_status(&mut self, text: &str) -> Result<()> {
if self.config.respect_screen_reader_focus && self.screen_reader_has_focus() {
let duration = Duration::from_secs(2); self.show_content_with_duration(text, ContentQuality::Poor, duration)
} else {
let duration = self.config.brief_notification_time;
self.show_content_with_duration(text, ContentQuality::Poor, duration)
}
}
pub fn screen_reader_has_focus(&mut self) -> bool {
self.focus_tracker.screen_reader_likely_active()
}
pub fn queue_message(&mut self, text: &str, quality: ContentQuality) -> Result<()> {
let message = QueuedMessage {
text: text.to_string(),
quality,
duration: quality.default_duration(),
_queued_at: Instant::now(),
retries: 0,
};
self.message_queue.push_back(message);
Ok(())
}
pub fn process_queue(&mut self) -> Result<usize> {
let mut processed = 0;
while let Some(mut message) = self.message_queue.pop_front() {
match self.show_content_with_duration(&message.text, message.quality, message.duration)
{
Ok(()) => {
processed += 1;
}
Err(_e)
if self.config.retry_when_busy && message.retries < self.config.max_retries =>
{
message.retries += 1;
self.message_queue.push_back(message);
break; }
Err(_) => {
continue;
}
}
}
Ok(processed)
}
pub fn pending_messages(&self) -> usize {
self.message_queue.len()
}
fn set_client_priority(&mut self, priority: u32) -> Result<()> {
if self.current_priority == Some(priority) {
return Ok(()); }
self.connection.set_parameter(
Parameter::ClientPriority,
0,
ParameterFlags::LOCAL,
&priority,
)?;
self.current_priority = Some(priority);
Ok(())
}
fn temporary_priority_adjustment(&self, new_priority: u32) -> Result<PriorityGuard<'_>> {
let original_priority = self
.connection
.get_parameter::<u32>(Parameter::ClientPriority, 0, ParameterFlags::LOCAL)
.or_else(|_| {
self.connection.get_parameter::<u32>(
Parameter::ClientPriority,
0,
ParameterFlags::GLOBAL,
)
})
.ok();
let priority_set = self
.connection
.set_parameter(
Parameter::ClientPriority,
0,
ParameterFlags::LOCAL,
&new_priority,
)
.or_else(|_| {
self.connection.set_parameter(
Parameter::ClientPriority,
0,
ParameterFlags::GLOBAL,
&new_priority,
)
})
.is_ok();
Ok(PriorityGuard {
connection: &self.connection,
original_priority: if priority_set {
original_priority
} else {
None
},
_new_priority: new_priority,
})
}
fn display_with_timeout(&self, text: &str, duration: Duration) -> Result<()> {
let driver_name = self.connection.display_driver().unwrap_or_else(|_| "Unknown".to_string());
if driver_name == "NoBraille" {
std::thread::sleep(duration);
return Ok(());
}
let approaches = [("root path", None), ("TTY 2", Some(2)), ("TTY 3", Some(3))];
for (_desc, tty_num) in approaches.iter() {
let tty_result = if let Some(num) = tty_num {
TtyMode::with_tty(&self.connection, Some(*num), None)
} else {
TtyMode::with_path(&self.connection, &[], None)
};
match tty_result {
Ok(tty_mode) => {
let writer = tty_mode.writer();
match writer.write_contracted_user_preference(text, crate::text::CursorPosition::Off) {
Ok(()) => {
}
Err(_) => {
match writer.write_text(text) {
Ok(()) => {
}
Err(BrlApiError::InvalidParameter) if driver_name == "NoBraille" => {
}
Err(e) => return Err(e),
}
}
}
std::thread::sleep(duration);
return Ok(());
}
Err(BrlApiError::UnknownTTY) => continue,
Err(BrlApiError::LibCError) => continue, Err(e) => return Err(e),
}
}
Err(BrlApiError::custom(
"All display approaches failed - display may be busy",
))
}
}
#[derive(Debug)]
struct PriorityGuard<'a> {
connection: &'a Connection,
original_priority: Option<u32>,
_new_priority: u32,
}
impl<'a> Drop for PriorityGuard<'a> {
fn drop(&mut self) {
if let Some(original) = self.original_priority {
let _ = self
.connection
.set_parameter(
Parameter::ClientPriority,
0,
ParameterFlags::LOCAL,
&original,
)
.or_else(|_| {
self.connection.set_parameter(
Parameter::ClientPriority,
0,
ParameterFlags::GLOBAL,
&original,
)
});
}
}
}
pub fn notify(text: &str) -> Result<()> {
let display = CooperativeDisplay::background_app()?;
display.show_message(text)
}
pub fn alert(text: &str) -> Result<()> {
let display = CooperativeDisplay::open(AppType::SystemAlert)?;
display.show_content(text, ContentQuality::Fair)
}
pub fn debug(text: &str) -> Result<()> {
let display = CooperativeDisplay::open(AppType::Debug)?;
display.show_content(text, ContentQuality::Fair) }
pub mod simple {
use super::*;
use std::time::{Duration, Instant};
pub struct SimpleCooperativeDisplay {
inner: CooperativeDisplay,
brief_duration: Duration,
yield_interval: Duration,
last_display: Instant,
}
impl SimpleCooperativeDisplay {
pub fn open(app_type: AppType) -> Result<Self> {
let inner = CooperativeDisplay::open(app_type)?;
let (brief_duration, yield_interval) = match app_type {
AppType::ScreenReader => (Duration::from_secs(5), Duration::from_millis(100)),
AppType::SystemAlert => (Duration::from_secs(3), Duration::from_millis(500)),
_ => (Duration::from_secs(2), Duration::from_millis(300)),
};
Ok(Self {
inner,
brief_duration,
yield_interval,
last_display: Instant::now(),
})
}
pub fn show_message(&mut self, text: &str) -> Result<()> {
self.inner.show_brief_message(text, self.brief_duration)?;
self.last_display = Instant::now();
std::thread::sleep(self.yield_interval);
Ok(())
}
pub fn show_status(&mut self, text: &str) -> Result<()> {
let brief_status_duration = self.brief_duration / 2;
self.inner.show_brief_message(text, brief_status_duration)?;
self.last_display = Instant::now();
std::thread::sleep(self.yield_interval / 2);
Ok(())
}
pub fn force_display(&mut self, text: &str, quality: ContentQuality) -> Result<()> {
self.inner.show_content(text, quality)
}
pub fn stats(&self) -> SimpleStats {
SimpleStats {
brief_duration: self.brief_duration,
yield_interval: self.yield_interval,
time_since_last_display: self.last_display.elapsed(),
}
}
}
#[derive(Debug, Clone)]
pub struct SimpleStats {
pub brief_duration: Duration,
pub yield_interval: Duration,
pub time_since_last_display: Duration,
}
}