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
//! Typed input: bytes, text, keys, paste, and focus reports.
use super::Session;
use crate::{Error, InputSegment, Result};
use tastty::{IoError, IoErrorReceiver, KeyEvent};
impl Session {
/// Returns a fatal terminal I/O-thread error if one has been observed.
///
/// Each reader or writer thread emits at most one [`IoError`] before
/// exiting. Reader EOF remains silent.
#[must_use]
pub fn try_recv_io_error(&self) -> Option<IoError> {
self.terminal.try_recv_io_error()
}
/// Takes ownership of the terminal I/O-error receiver.
///
/// Returns `None` if the receiver was already taken by a previous call.
/// After this call, [`Session::try_recv_io_error`] will always return
/// `None`.
pub fn take_io_error_receiver(&self) -> Option<IoErrorReceiver> {
self.terminal.take_io_error_receiver()
}
/// Send raw bytes to the child.
///
/// # Errors
///
/// Returns [`Error::Send`] when the writer side is gone (child
/// exited, session terminated) or the writer queue is full.
pub fn send_bytes(&self, bytes: &[u8]) -> Result<()> {
self.terminal.send(bytes).map_err(Error::Send)
}
/// Send UTF-8 text to the child.
///
/// # Errors
///
/// Same as [`Session::send_bytes`].
pub fn send_text(&self, text: &str) -> Result<()> {
self.send_bytes(text.as_bytes())
}
/// Encode and send a key event to the child.
///
/// # Errors
///
/// Returns [`Error::SendKey`] when the encoded bytes cannot be
/// written (writer side gone or queue full).
pub fn send_key(&self, key: KeyEvent) -> Result<()> {
self.terminal.send_key(key).map_err(Error::SendKey)
}
/// Send a mixed input sequence.
///
/// # Errors
///
/// Returns the first error encountered while delivering segments:
/// [`Error::Send`] for [`InputSegment::Text`] / [`InputSegment::Bytes`]
/// or [`Error::SendKey`] for [`InputSegment::Key`]. Earlier segments
/// in the slice are delivered before the error surfaces.
pub fn send_input(&self, segments: &[InputSegment]) -> Result<()> {
for segment in segments {
match segment {
InputSegment::Text(text) => self.send_text(text)?,
InputSegment::Key(key) => self.send_key(*key)?,
InputSegment::Bytes(bytes) => self.send_bytes(bytes)?,
}
}
Ok(())
}
/// Parse `input` with [`parse_input`](crate::parse_input) and forward
/// the resulting segments through [`Session::send_input`].
///
/// Sugar for the common script-runner shape `parse_input(s)?` ->
/// `send_input(&segments)`. The grammar is the angle-bracket
/// syntax: literal text passes through verbatim, and `<...>` tags
/// resolve to key events. Recognised tags include `<Enter>`,
/// `<Tab>`, `<Esc>`, `<Backspace>`, `<Space>`, arrow and navigation
/// keys (`<Up>`, `<PageDown>`, ...), function keys `<F1>`..`<F24>`,
/// and any single character with optional modifiers
/// (`<Ctrl-c>`, `<Alt-Shift-x>`, `<Shift-Tab>`).
///
/// # Errors
///
/// Returns [`Error::ParseInput`] if `input` contains an unclosed
/// `<...>` tag or an unrecognised key name. Otherwise propagates
/// the same errors as [`Session::send_input`].
pub fn send_keys(&self, input: &str) -> Result<()> {
let segments = crate::parse_input(input)?;
self.send_input(&segments)
}
/// Send `text` as a paste, framing it with bracketed-paste markers
/// when the child has enabled them and rewriting bare LF to CR
/// otherwise so receiving shells commit each line.
///
/// This is the paste-aware sibling of [`Session::send_text`]: that
/// method writes the bytes verbatim (intended for short typed strings
/// and pre-encoded escape sequences); `send_paste` is the right call
/// for any payload the user thinks of as "pasted content", whether
/// from a clipboard, a multi-line script, or a captured file.
///
/// The byte assembly is delegated to `tastty_core::frame::bracketed_paste`,
/// which strips embedded `ESC[200~` / `ESC[201~` markers and most C0
/// control bytes from the payload before assembly.
///
/// Blocks if the writer queue is full and rejects calls made from
/// inside a Tokio runtime to avoid panicking the executor; callers
/// driven by an async runtime should use
/// [`Session::send_paste_async`] when the `async` feature is
/// enabled.
///
/// # Errors
///
/// Returns [`Error::SendPaste`] wrapping
/// [`tastty::Error::SendClosed`] when the writer side is gone, or
/// [`tastty::Error::BlockingInsideAsync`] when called from inside a
/// Tokio runtime.
pub fn send_paste(&self, text: &str) -> Result<()> {
self.terminal.send_paste(text).map_err(Error::SendPaste)
}
/// Asynchronous variant of [`Session::send_paste`].
///
/// The bracketed-paste frame is built and the writer handle is
/// cloned synchronously up-front, then the writer-queue insert is
/// the only work awaited. This means concurrent [`Session`] callers
/// are not blocked behind this paste's `.await`, and the future is
/// a clean `tokio::select!` arm: backed by
/// `tokio::sync::mpsc::Sender::send`, dropping the future neither
/// queues nor drops the bytes.
///
/// # Errors
///
/// Returns [`Error::SendPaste`] wrapping [`tastty::Error::SendClosed`]
/// when the writer side is gone before or during the await.
#[cfg(feature = "async")]
pub async fn send_paste_async(&self, text: &str) -> Result<()> {
let writer = self
.terminal
.writer()
.ok_or_else(|| Error::SendPaste(tastty::Error::SendClosed))?;
let frame = self.terminal.paste_frame(text);
writer.send(&frame).await.map_err(Error::SendPaste)
}
/// Deliver a focus-in report when the child has enabled focus
/// reporting (DEC private mode 1004).
///
/// Sends the `ESC [ I` byte sequence; a no-op when the child has
/// not opted into focus reports. Inherits the blocking semantics
/// of [`Session::send_bytes`]; for async callers see
/// [`Session::send_focus_in_async`].
///
/// # Errors
///
/// Returns [`Error::Send`] wrapping
/// [`tastty::Error::BlockingInsideAsync`] when invoked from inside
/// a Tokio runtime, or [`tastty::Error::SendClosed`] when the
/// writer thread is gone.
pub fn send_focus_in(&self) -> Result<()> {
self.terminal.send_focus(true).map_err(Error::Send)
}
/// Deliver a focus-out report when the child has enabled focus
/// reporting (DEC private mode 1004).
///
/// Sends the `ESC [ O` byte sequence; a no-op when the child has
/// not opted into focus reports. Inherits the blocking semantics
/// of [`Session::send_bytes`]; for async callers see
/// [`Session::send_focus_out_async`].
///
/// # Errors
///
/// Returns [`Error::Send`] wrapping
/// [`tastty::Error::BlockingInsideAsync`] when invoked from inside
/// a Tokio runtime, or [`tastty::Error::SendClosed`] when the
/// writer thread is gone.
pub fn send_focus_out(&self) -> Result<()> {
self.terminal.send_focus(false).map_err(Error::Send)
}
/// Asynchronous variant of [`Session::send_focus_in`].
///
/// The focus-report frame is resolved and the writer handle is
/// cloned synchronously up-front, then the writer-queue insert is
/// the only work awaited. A no-op when the child has not enabled
/// focus reporting.
///
/// # Errors
///
/// Returns [`Error::Send`] wrapping [`tastty::Error::SendClosed`]
/// when the writer side is gone.
#[cfg(feature = "async")]
pub async fn send_focus_in_async(&self) -> Result<()> {
self.send_focus_async_inner(true).await
}
/// Asynchronous variant of [`Session::send_focus_out`].
///
/// Cancellation-safety mirrors [`Session::send_focus_in_async`].
///
/// # Errors
///
/// Same as [`Session::send_focus_in_async`].
#[cfg(feature = "async")]
pub async fn send_focus_out_async(&self) -> Result<()> {
self.send_focus_async_inner(false).await
}
#[cfg(feature = "async")]
async fn send_focus_async_inner(&self, gained: bool) -> Result<()> {
let Some(bytes) = self.terminal.focus_frame(gained) else {
return Ok(());
};
let writer = self
.terminal
.writer()
.ok_or_else(|| Error::Send(tastty::Error::SendClosed))?;
writer.send(bytes).await.map_err(Error::Send)
}
}