brlapi 0.4.1

Safe Rust bindings for the BrlAPI library
// SPDX-License-Identifier: LGPL-2.1

//! TTY mode operations for taking control of the braille display

use crate::{
    Result, brlapi_call,
    connection::Connection,
    error::BrlApiError,
    text::{CursorPosition, TextWriter},
};
use brlapi_sys::*;
use std::ptr;

/// RAII wrapper for TTY mode
///
/// Automatically enters TTY mode on creation and leaves it on drop.
#[derive(Debug)]
pub struct TtyMode<'a> {
    connection: &'a Connection,
}

impl<'a> TryFrom<&'a Connection> for TtyMode<'a> {
    type Error = BrlApiError;

    fn try_from(connection: &'a Connection) -> std::result::Result<Self, Self::Error> {
        Self::enter_tty_mode_impl(connection)?;
        Ok(TtyMode { connection })
    }
}

impl<'a> TtyMode<'a> {
    /// Enter TTY mode automatically finding an available virtual console
    ///
    /// This is the recommended constructor for most applications as it
    /// automatically finds an appropriate TTY across different system configurations.
    ///
    /// The TTY mode will be automatically exited when this object is dropped.
    ///
    /// Returns the TTY mode wrapper and the TTY number that was selected.
    ///
    /// # Arguments
    /// * `connection` - The BrlAPI connection to use
    /// * `driver` - Optional driver name for key handling. None means BRLTTY commands (driver-independent),
    ///   Some(driver_name) means driver-specific keycodes.
    ///
    /// # Errors
    ///
    /// Returns `BrlApiError::ConnectionRefused` if no TTY can be accessed or if the connection is invalid.
    pub fn enter_auto(connection: &'a Connection, driver: Option<&str>) -> Result<(Self, i32)> {
        let tty_num = Self::enter_tty_mode_auto_impl(connection, driver)?;
        Ok((TtyMode { connection }, tty_num))
    }

    /// Enter TTY mode for a specific TTY
    ///
    /// The TTY mode will be automatically exited when this object is dropped.
    ///
    /// # Arguments
    /// * `connection` - The BrlAPI connection to use
    /// * `tty` - TTY number to use. None means auto-detection.
    /// * `driver` - Optional driver name for key handling. None means BRLTTY commands (driver-independent),
    ///   Some(driver_name) means driver-specific keycodes.
    pub fn with_tty(
        connection: &'a Connection,
        tty: Option<i32>,
        driver: Option<&str>,
    ) -> Result<Self> {
        Self::enter_tty_mode_with_tty_impl(connection, tty, driver)?;
        Ok(TtyMode { connection })
    }

    /// Enter TTY mode with a specific TTY path
    ///
    /// This allows entering TTY mode by specifying a path through the TTY tree.
    /// The TTY mode will be automatically exited when this object is dropped.
    ///
    /// # Arguments
    /// * `connection` - The BrlAPI connection to use
    /// * `ttys` - Array of TTY numbers representing the path. Empty slice means root.
    /// * `driver` - Optional driver name for key handling
    ///
    /// # Example
    /// ```no_run
    /// use brlapi::{Connection, TtyMode};
    ///
    /// fn main() -> Result<(), brlapi::BrlApiError> {
    ///     let connection = Connection::open()?;
    ///     
    ///     // Enter root TTY mode (typical for screen readers)
    ///     let tty_mode = TtyMode::with_path(&connection, &[], None)?;
    ///     tty_mode.write_text("Screen reader active")?;
    ///     
    ///     Ok(())
    /// }
    /// ```
    pub fn with_path(
        connection: &'a Connection,
        ttys: &[i32],
        driver: Option<&str>,
    ) -> Result<Self> {
        Self::enter_tty_mode_with_path_impl(connection, ttys, driver)?;
        Ok(TtyMode { connection })
    }

    // Private implementation methods for TTY operations

    /// Enter TTY mode to take control of braille output
    ///
    /// This must be called before writing to the display.
    /// Uses the current TTY by default.
    fn enter_tty_mode_impl(connection: &Connection) -> Result<()> {
        Self::enter_tty_mode_with_tty_impl(connection, None, None)
    }

    /// Enter TTY mode for a specific TTY
    ///
    /// Pass None to use auto-detection, or Some(tty_number) for a specific TTY.
    fn enter_tty_mode_with_tty_impl(
        connection: &Connection,
        tty: Option<i32>,
        driver: Option<&str>,
    ) -> Result<()> {
        let tty_num = tty.unwrap_or(BRLAPI_TTY_DEFAULT);

        let c_driver = if let Some(driver) = driver {
            Some(std::ffi::CString::new(driver)?)
        } else {
            None
        };

        let driver_ptr = c_driver.as_ref().map_or(ptr::null(), |s| s.as_ptr());

        // SAFETY: connection.handle_ptr() returns a valid handle from successful connection.
        // tty_num is a valid TTY number. driver_ptr is either null or points to valid C string.
        brlapi_call!(unsafe {
            brlapi__enterTtyMode(connection.handle_ptr(), tty_num, driver_ptr)
        })?;
        Ok(())
    }

    /// Enter TTY mode using BrlAPI's auto-detection
    ///
    /// This uses BrlAPI's built-in auto-detection to find the appropriate TTY.
    /// The implementation uses different strategies for X sessions vs virtual consoles:
    ///
    /// - X Sessions: Uses `enterTtyModeWithPath(NULL, 0)` to properly handle WINDOWPATH
    /// - Virtual Consoles: Falls back to `enterTtyMode(BRLAPI_TTY_DEFAULT)`
    ///
    /// This is the recommended method for most applications.
    fn enter_tty_mode_auto_impl(connection: &Connection, driver: Option<&str>) -> Result<i32> {
        let c_driver = if let Some(driver) = driver {
            Some(std::ffi::CString::new(driver)?)
        } else {
            None
        };

        let driver_ptr = c_driver.as_ref().map_or(ptr::null(), |s| s.as_ptr());

        // First try: Use enterTtyModeWithPath for X sessions (handles WINDOWPATH properly)
        // This is the preferred method for X sessions as it automatically prepends WINDOWPATH/XDG_VTNR
        match Self::try_enter_tty_mode_with_path_impl(connection, &[], driver_ptr) {
            Ok(tty) => Ok(tty),
            Err(first_error) => {
                // Second try: Fall back to classic auto-detection for virtual consoles
                // This works when running directly from VT (Ctrl+Alt+F2-F6)
                match Self::try_enter_tty_mode_default_impl(connection, driver_ptr) {
                    Ok(tty) => Ok(tty),
                    Err(second_error) => {
                        // Both methods failed - provide helpful error message based on environment
                        Err(Self::create_auto_detection_error(first_error, second_error))
                    }
                }
            }
        }
    }

    /// Try entering TTY mode using enterTtyModeWithPath (preferred for X sessions)
    fn try_enter_tty_mode_with_path_impl(
        connection: &Connection,
        ttys: &[i32],
        driver_ptr: *const i8,
    ) -> Result<i32> {
        // SAFETY: connection.handle_ptr() returns a valid handle from successful connection.
        // ttys.as_ptr() points to valid slice memory, ttys.len() provides correct count.
        // driver_ptr is either null or points to valid C string.
        let result = brlapi_call!(unsafe {
            brlapi__enterTtyModeWithPath(
                connection.handle_ptr(),
                ttys.as_ptr(),
                ttys.len() as i32,
                driver_ptr,
            )
        })?;
        Ok(result)
    }

    /// Try entering TTY mode using classic auto-detection (fallback for virtual consoles)
    fn try_enter_tty_mode_default_impl(
        connection: &Connection,
        driver_ptr: *const i8,
    ) -> Result<i32> {
        // SAFETY: connection.handle_ptr() returns a valid handle from successful connection.
        // BRLAPI_TTY_DEFAULT is a valid constant. driver_ptr is either null or points to valid C string.
        let selected_tty = brlapi_call!(unsafe {
            brlapi__enterTtyMode(connection.handle_ptr(), BRLAPI_TTY_DEFAULT, driver_ptr)
        })?;
        Ok(selected_tty)
    }

    /// Create a helpful error message when both auto-detection methods fail
    fn create_auto_detection_error(
        first_error: BrlApiError,
        _second_error: BrlApiError,
    ) -> BrlApiError {
        use std::env;

        let in_x_session = env::var("DISPLAY").is_ok();
        let has_windowpath = env::var("WINDOWPATH").is_ok();
        let has_windowid = env::var("WINDOWID").is_ok();

        let context_info = if in_x_session {
            format!(
                "Running in X session (DISPLAY={}). WINDOWPATH: {}, WINDOWID: {}",
                env::var("DISPLAY").unwrap_or_else(|_| "unknown".to_string()),
                if has_windowpath { "set" } else { "not set" },
                if has_windowid { "set" } else { "not set" }
            )
        } else {
            "Running outside X session (no DISPLAY variable)".to_string()
        };

        BrlApiError::Custom {
            message: format!(
                "Could not automatically detect TTY for braille display attachment. {} \
                 \nOriginal error: {} \
                 \nSuggestions: \
                 \n- Ensure BRLTTY daemon is running \
                 \n- For X sessions: Check WINDOWPATH is set (xinit/xdm should set it) \
                 \n- For virtual consoles: Try running from VT2-VT6 (Ctrl+Alt+F2-F6) \
                 \n- Check BrlAPI permissions and authentication",
                context_info, first_error
            ),
        }
    }

    /// Leave TTY mode and release control of braille output
    fn leave_tty_mode_impl(connection: &Connection) -> Result<()> {
        // SAFETY: connection.handle_ptr() returns a valid handle that was used to enter TTY mode.
        // brlapi__leaveTtyMode is safe to call once per TTY mode session.
        brlapi_call!(unsafe { brlapi__leaveTtyMode(connection.handle_ptr()) })?;
        Ok(())
    }

    /// Enter TTY mode with a specific TTY path
    ///
    /// This allows entering TTY mode by specifying a path through the TTY tree
    /// rather than just a single TTY number. This is useful for applications
    /// that need to control specific parts of the TTY hierarchy.
    ///
    /// # Arguments
    /// * `ttys` - Array of TTY numbers representing the path through the TTY tree.
    ///   An empty slice means the root of the tree (usually what screen readers want).
    /// * `driver` - Optional driver name for key handling. None means BRLTTY commands,
    ///   Some(driver_name) means driver-specific keycodes.
    fn enter_tty_mode_with_path_impl(
        connection: &Connection,
        ttys: &[i32],
        driver: Option<&str>,
    ) -> Result<()> {
        let c_driver = if let Some(driver) = driver {
            Some(std::ffi::CString::new(driver)?)
        } else {
            None
        };

        let driver_ptr = c_driver.as_ref().map_or(ptr::null(), |s| s.as_ptr());

        // SAFETY: connection.handle_ptr() returns a valid handle from successful connection.
        // ttys.as_ptr() points to valid slice memory, ttys.len() provides correct count.
        // driver_ptr is either null or points to valid C string.
        brlapi_call!(unsafe {
            brlapi__enterTtyModeWithPath(
                connection.handle_ptr(),
                ttys.as_ptr(),
                ttys.len() as i32,
                driver_ptr,
            )
        })?;
        Ok(())
    }

    /// Get a reference to the underlying connection
    pub fn connection(&self) -> &Connection {
        self.connection
    }

    /// Get a text writer for this TTY mode
    ///
    /// Since we're already in TTY mode, this is the most efficient way to write text.
    /// The returned TextWriter is tied to this TtyMode's lifetime, ensuring text operations
    /// are only performed while TTY mode is active.
    pub fn writer(&self) -> TextWriter<'_> {
        TextWriter::new(self)
    }

    /// Get a text writer for this TTY mode (alternative method name)
    ///
    /// This is an alias for `writer()` with a more explicit name.
    pub fn text_writer(&self) -> TextWriter<'_> {
        TextWriter::from_tty_mode(self)
    }

    /// Write text directly to the braille display
    ///
    /// This is a convenience method that creates a TextWriter internally.
    /// Since we're already in TTY mode, this is efficient.
    pub fn write_text(&self, text: &str) -> Result<()> {
        self.writer().write_text(text)
    }

    /// Write text to the braille display with cursor positioning
    ///
    /// This is a convenience method that creates a TextWriter internally.
    /// - text: The text to display
    /// - cursor: Where to position the cursor
    pub fn write_text_with_cursor(&self, text: &str, cursor: CursorPosition) -> Result<()> {
        self.writer().write_with_cursor(text, cursor)
    }

    /// Write a notification message with cursor turned off
    ///
    /// This is a convenience method for temporary messages and notifications
    /// that don't need cursor indication.
    pub fn write_notification(&self, text: &str) -> Result<()> {
        self.writer().write_notification(text)
    }

    /// Set focus to a specific TTY
    ///
    /// This tells BRLTTY which TTY currently has focus. This is primarily
    /// used by focus tellers like window managers, terminal multiplexers, etc.
    /// The connection must be in TTY mode before calling this function.
    ///
    /// # Arguments
    /// * `tty` - The TTY number that should have focus
    ///
    /// # Example
    /// ```no_run
    /// use brlapi::{Connection, TtyMode};
    ///
    /// fn main() -> Result<(), brlapi::BrlApiError> {
    ///     let connection = Connection::open()?;
    ///     let (tty_mode, _) = TtyMode::enter_auto(&connection, None)?;
    ///
    ///     // Tell BRLTTY that TTY 2 has focus
    ///     tty_mode.set_focus(2)?;
    ///
    ///     Ok(())
    /// }
    /// ```
    pub fn set_focus(&self, tty: i32) -> Result<()> {
        // SAFETY: self.connection.handle_ptr() returns a valid handle that's in TTY mode.
        // tty is a valid TTY number parameter.
        brlapi_call!(unsafe { brlapi__setFocus(self.connection.handle_ptr(), tty) })?;
        Ok(())
    }

    /// Get a key reader for this TTY mode
    ///
    /// Since we're already in TTY mode, this is the most efficient way to read keys.
    /// The returned KeyReader is tied to this TtyMode's lifetime, ensuring key operations
    /// are only performed while TTY mode is active.
    pub fn key_reader(&self) -> crate::keys::KeyReader<'_> {
        crate::keys::KeyReader::new(self)
    }
}

impl Drop for TtyMode<'_> {
    fn drop(&mut self) {
        // Ignore errors during cleanup - connection might already be closed
        let _ = Self::leave_tty_mode_impl(self.connection);
    }
}