1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
// 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);
}
}