Skip to main content

tern_sdk/
session.rs

1//! Layer 5: the session. Detection and the `hello` handshake, raw mode and
2//! input modes, surfaces, credit-based flow control, blobs, event routing
3//! to handlers, recording and a clean exit.
4//!
5//! A session is synchronous: [`Session::next`] waits for input with a
6//! timeout and returns keys, unhandled events and the messages of the
7//! handlers that events were routed to. Acks are handled inside: a
8//! `render` while out of credits only stores the view, and the frame that
9//! carries the difference goes out when an ack returns credit.
10//!
11//! # Example
12//! ```no_run
13//! use std::time::Duration;
14//!
15//! use tern_sdk::{Input, Options, Session, SurfaceOptions, ui};
16//!
17//! #[derive(Clone)]
18//! enum Msg {
19//! 	Go,
20//! }
21//!
22//! # fn main() -> Result<(), tern_sdk::Error> {
23//! let Some(mut session) = Session::<Msg>::connect(Options::new())? else {
24//! 	println!("no TSP here");
25//! 	return Ok(());
26//! };
27//! let sf = session.open(SurfaceOptions::flow())?;
28//! session.render(sf, ui::html::button("Go").on_click(Msg::Go))?;
29//! while let Some(input) = session.next(None)? {
30//! 	match input {
31//! 		Input::Msg(Msg::Go, _) => break,
32//! 		Input::Key(key) if key.is("escape") => break,
33//! 		_ => {},
34//! 	}
35//! }
36//! session.close()
37//! # }
38//! ```
39
40use std::{
41	collections::{HashSet, VecDeque},
42	path::PathBuf,
43	time::{Duration, Instant},
44};
45
46use serde_json::Value;
47
48#[cfg(any(unix, windows))]
49use crate::term::Tty;
50use crate::{
51	Error,
52	input::{Item, Parser},
53	keys::{Decoder, Key},
54	reconcile::{Doc, Routes},
55	record::Recorder,
56	term::{self, Terminal},
57	ui::View,
58	wire::{
59		Blob, Cell, Close, Encoder, Event, Frame, Message, Mode, Op, Open, Palette, Query, Reply,
60		RevealAt, ScrollBy, Sheet,
61	},
62};
63
64/// Undecided input is released as keys after this long without input.
65const FLUSH_AFTER: Duration = Duration::from_millis(30);
66/// How long close drains input so late replies never reach the shell.
67const DRAIN: Duration = Duration::from_millis(50);
68/// How long [`Session::blobs`] waits for its reply.
69const QUERY_TIMEOUT: Duration = Duration::from_secs(2);
70
71/// How to connect.
72#[derive(Clone, Debug)]
73pub struct Options {
74	/// The program's name (default: the executable's).
75	pub app:             Option<String>,
76	/// The program's version.
77	pub version:         Option<String>,
78	/// Program features (`edit`, `undo`, `send`).
79	pub features:        Vec<String>,
80	/// How long to wait for the `hello` reply.
81	pub timeout:         Duration,
82	/// Enable bracketed paste while connected.
83	pub bracketed_paste: bool,
84	/// Push the kitty keyboard flag 1 while connected.
85	pub kitty_keyboard:  bool,
86	/// Record every message as JSONL (default: `TERN_TSP_RECORD`).
87	pub record:          Option<PathBuf>,
88}
89
90impl Default for Options {
91	fn default() -> Self {
92		Self {
93			app:             None,
94			version:         None,
95			features:        Vec::new(),
96			timeout:         Duration::from_secs(1),
97			bracketed_paste: true,
98			kitty_keyboard:  true,
99			record:          std::env::var_os("TERN_TSP_RECORD")
100				.filter(|v| !v.is_empty())
101				.map(PathBuf::from),
102		}
103	}
104}
105
106impl Options {
107	/// The defaults.
108	pub fn new() -> Self {
109		Self::default()
110	}
111
112	/// Sets the program's name.
113	pub fn app(mut self, app: impl Into<String>) -> Self {
114		self.app = Some(app.into());
115		self
116	}
117
118	/// Sets the program's version.
119	pub fn version(mut self, version: impl Into<String>) -> Self {
120		self.version = Some(version.into());
121		self
122	}
123
124	/// Adds a program feature ([`crate::wire::feature`]).
125	pub fn feature(mut self, feature: impl Into<String>) -> Self {
126		self.features.push(feature.into());
127		self
128	}
129
130	/// Sets the handshake timeout.
131	pub const fn timeout(mut self, timeout: Duration) -> Self {
132		self.timeout = timeout;
133		self
134	}
135
136	/// Turns bracketed paste on or off.
137	pub const fn bracketed_paste(mut self, on: bool) -> Self {
138		self.bracketed_paste = on;
139		self
140	}
141
142	/// Turns the kitty keyboard flag on or off.
143	pub const fn kitty_keyboard(mut self, on: bool) -> Self {
144		self.kitty_keyboard = on;
145		self
146	}
147
148	/// Records to `path` (or not).
149	pub fn record(mut self, path: Option<PathBuf>) -> Self {
150		self.record = path;
151		self
152	}
153}
154
155/// What the terminal draws and supports, kept current by `resize`,
156/// `theme` and `motion` events.
157#[derive(Clone, Debug, Default, PartialEq)]
158pub struct Capabilities {
159	/// The terminal (`tern`).
160	pub term:          String,
161	/// Its version.
162	pub version:       String,
163	/// Every kind it draws.
164	pub kinds:         Vec<String>,
165	/// Terminal features.
166	pub features:      Vec<String>,
167	/// The largest body per message.
168	pub apc:           usize,
169	/// Frames that may be unacknowledged.
170	pub credits:       u32,
171	/// The pane's width in columns.
172	pub cols:          u32,
173	/// The cell size in pixels.
174	pub cell:          Option<Cell>,
175	/// Whether the terminal shows its dark appearance.
176	pub dark:          bool,
177	/// Whether Reduce Motion is on.
178	pub reduce_motion: bool,
179	/// Whether the user's system reads a 12-hour clock (`3:05 PM`); false
180	/// (24-hour) on terminals that don't say.
181	pub hour12:        bool,
182}
183
184impl Capabilities {
185	/// Whether the terminal draws `kind`.
186	pub fn has_kind(&self, kind: &str) -> bool {
187		self.kinds.iter().any(|k| k == kind)
188	}
189
190	/// Whether the terminal has `feature`.
191	pub fn has_feature(&self, feature: &str) -> bool {
192		self.features.iter().any(|f| f == feature)
193	}
194}
195
196/// How to open a surface.
197#[derive(Clone, Debug)]
198pub struct SurfaceOptions {
199	/// The id (default `s1`, `s2`, … per session).
200	pub id:     Option<String>,
201	/// The mode.
202	pub mode:   Mode,
203	/// Names the pane until the program sets its own title.
204	pub title:  Option<String>,
205	/// `data-surface` for stylesheets.
206	pub role:   Option<String>,
207	/// Whether the program reads its events (default `true`).
208	pub listen: bool,
209	/// Reopen a closed inline surface with this id.
210	pub adopt:  bool,
211	/// Keep it on screen when the session closes it (default `true`).
212	pub keep:   bool,
213}
214
215impl Default for SurfaceOptions {
216	fn default() -> Self {
217		Self {
218			id:     None,
219			mode:   Mode::default(),
220			title:  None,
221			role:   None,
222			listen: true,
223			adopt:  false,
224			keep:   true,
225		}
226	}
227}
228
229impl SurfaceOptions {
230	/// A surface of `mode`, listening, kept on close.
231	pub fn new(mode: Mode) -> Self {
232		Self { mode, ..Self::default() }
233	}
234
235	/// An inline surface (a session program).
236	pub fn inline() -> Self {
237		Self::new(Mode::Inline)
238	}
239
240	/// A screen surface (a full-screen view).
241	pub fn screen() -> Self {
242		Self::new(Mode::Screen)
243	}
244
245	/// A flow surface (command output that stays in the scrollback).
246	pub fn flow() -> Self {
247		Self::new(Mode::Flow)
248	}
249
250	/// Sets the id.
251	pub fn id(mut self, id: impl Into<String>) -> Self {
252		self.id = Some(id.into());
253		self
254	}
255
256	/// Sets the title.
257	pub fn title(mut self, title: impl Into<String>) -> Self {
258		self.title = Some(title.into());
259		self
260	}
261
262	/// Sets the role.
263	pub fn role(mut self, role: impl Into<String>) -> Self {
264		self.role = Some(role.into());
265		self
266	}
267
268	/// Sets whether the program listens (no acks or events when not).
269	pub const fn listen(mut self, listen: bool) -> Self {
270		self.listen = listen;
271		self
272	}
273
274	/// Reopens a closed inline surface with this id.
275	pub const fn adopt(mut self, adopt: bool) -> Self {
276		self.adopt = adopt;
277		self
278	}
279
280	/// Sets whether the session's close keeps the surface on screen.
281	pub const fn keep(mut self, keep: bool) -> Self {
282		self.keep = keep;
283		self
284	}
285}
286
287/// A surface of a session, as [`Session::open`] returned it (only valid
288/// with that session; another session's handle panics).
289#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
290pub struct Surface(usize);
291
292/// What [`Session::next`] returns.
293#[derive(Debug)]
294pub enum Input<M> {
295	/// A key press or a paste.
296	Key(Key),
297	/// An event no handler took.
298	Event(Event),
299	/// The message of the handler an event was routed to, with the event.
300	Msg(M, Event),
301}
302
303/// One open (or closed) surface.
304struct State<M> {
305	/// The surface id.
306	id:         String,
307	/// Whether acks come.
308	listen:     bool,
309	/// Keep on the session's close.
310	keep:       bool,
311	/// Still open.
312	open:       bool,
313	/// The last frame's sequence number.
314	seq:        u64,
315	/// The highest frame acked.
316	acked:      u64,
317	/// The view as last sent.
318	sent:       Doc,
319	/// An adopted surface from outside this session needs its unknown main
320	/// removed.
321	reset_main: bool,
322	/// A view rendered while out of credits.
323	pending:    Option<Doc>,
324	/// View ops and raw ops waiting for credit.
325	queued:     Vec<Op>,
326	/// The handlers of the view last rendered.
327	routes:     Routes<M>,
328}
329
330impl<M> State<M> {
331	/// Whether a frame must wait for an ack.
332	const fn blocked(&self, credits: u32) -> bool {
333		self.listen && self.seq.saturating_sub(self.acked) >= credits as u64
334	}
335}
336
337/// The writing half: the terminal, the encoder and the recording.
338struct Out {
339	/// The terminal.
340	term:   Box<dyn Terminal>,
341	/// Chunks under the `apc` limit.
342	enc:    Encoder,
343	/// The recording, when on.
344	record: Option<Recorder>,
345}
346
347impl Out {
348	/// Encodes, records and writes one message.
349	fn send(&mut self, message: &Message) -> Result<(), Error> {
350		let bytes = self.enc.encode(message)?;
351		if let Some(record) = &mut self.record {
352			let body = match message {
353				Message::Blob(_) => Value::String(message.body()?),
354				_ => serde_json::from_str(&message.body()?)?,
355			};
356			record.log("out", message.verb(), &message.params(), body);
357		}
358		self.term.write(&bytes)?;
359		Ok(())
360	}
361
362	/// Sends pending changes, committing their state only after the write
363	/// succeeds.
364	fn flush<M>(&mut self, state: &mut State<M>) -> Result<(), Error> {
365		let mut ops = state
366			.pending
367			.as_ref()
368			.map_or_else(Vec::new, |doc| state.sent.ops(doc, &state.id));
369		if state.reset_main && (state.pending.is_some() || !state.queued.is_empty()) {
370			ops.insert(0, Op::Del { id: "main".into() });
371		}
372		let queued_at = ops.len();
373		ops.append(&mut state.queued);
374		if !ops.is_empty() {
375			let mut message = Message::Frame(Frame { sf: state.id.clone(), s: state.seq + 1, ops });
376			if let Err(err) = self.send(&message) {
377				if let Message::Frame(frame) = &mut message {
378					state.queued = frame.ops.split_off(queued_at);
379				}
380				return Err(err);
381			}
382			state.seq += 1;
383			state.reset_main = false;
384		}
385		if let Some(doc) = state.pending.take() {
386			state.sent = doc;
387		}
388		Ok(())
389	}
390}
391
392/// A connection to a terminal that speaks TSP. Dropping it closes it
393/// (surfaces, modes, tty).
394pub struct Session<M = ()> {
395	/// The writing half.
396	out:      Out,
397	/// Splits TSP out of the input.
398	parser:   Parser,
399	/// Turns key bytes into keys.
400	keys:     Decoder,
401	/// What the terminal supports.
402	caps:     Capabilities,
403	/// Every surface opened, by [`Surface`] index.
404	surfaces: Vec<State<M>>,
405	/// The number of the next default surface id.
406	next_id:  u32,
407	/// Inputs ready for [`Session::next`].
408	inbox:    VecDeque<Input<M>>,
409	/// Replies not yet claimed.
410	replies:  VecDeque<Reply>,
411	/// Blob ids sent this session.
412	blobs:    HashSet<String>,
413	/// The input modes enabled: bracketed paste, kitty keys.
414	modes:    (bool, bool),
415	/// When input last arrived.
416	last_in:  Instant,
417	/// Closed already.
418	closed:   bool,
419}
420
421impl<M> Session<M> {
422	/// Connects to the process's terminal (Unix and Windows only): `Ok(None)`
423	/// when TSP isn't available (`TERN_TSP=0`, stdin or stdout not a tty,
424	/// inside tmux, screen or zellij, or the terminal answers DA1 first or not
425	/// in time).
426	///
427	/// # Errors
428	/// When the tty can't be switched to raw mode or I/O fails.
429	#[cfg(any(unix, windows))]
430	pub fn connect(options: Options) -> Result<Option<Self>, Error> {
431		if !available() {
432			return Ok(None);
433		}
434		let tty = Tty::open()?;
435		Self::with_terminal(tty, options)
436	}
437
438	/// Runs the handshake over `term` (already in raw mode): writes `hello`
439	/// and a DA1 request and waits for the reply. `Ok(None)`, with the
440	/// terminal restored, when DA1 answers first or nothing answers in time.
441	///
442	/// # Errors
443	/// When I/O fails or the recording can't be opened.
444	pub fn with_terminal(
445		term: impl Terminal + 'static,
446		options: Options,
447	) -> Result<Option<Self>, Error> {
448		let record = options.record.as_deref().map(Recorder::open).transpose()?;
449		let mut session = Self {
450			out:      Out { term: Box::new(term), enc: Encoder::default(), record },
451			parser:   Parser::new(),
452			keys:     Decoder::new(),
453			caps:     Capabilities::default(),
454			surfaces: Vec::new(),
455			next_id:  1,
456			inbox:    VecDeque::new(),
457			replies:  VecDeque::new(),
458			blobs:    HashSet::new(),
459			modes:    (false, false),
460			last_in:  Instant::now(),
461			closed:   false,
462		};
463		let app = options.app.clone().or_else(exe_name);
464		match session.handshake(app, &options) {
465			Ok(true) => {},
466			Ok(false) => {
467				session.closed = true;
468				session.out.term.restore()?;
469				return Ok(None);
470			},
471			Err(err) => {
472				session.closed = true;
473				let _ = session.out.term.restore();
474				return Err(err);
475			},
476		}
477		let (paste, kitty) = (options.bracketed_paste, options.kitty_keyboard);
478		let mut enable = Vec::new();
479		if paste {
480			enable.extend_from_slice(b"\x1b[?2004h");
481		}
482		if kitty {
483			enable.extend_from_slice(b"\x1b[>1u");
484		}
485		session.modes = (paste, kitty);
486		session.out.term.modes(paste, kitty);
487		if !enable.is_empty() {
488			session.out.term.write(&enable)?;
489		}
490		Ok(Some(session))
491	}
492
493	/// Sends `hello` + DA1 and waits: whether the hello reply came first.
494	fn handshake(&mut self, app: Option<String>, options: &Options) -> Result<bool, Error> {
495		let hello =
496			Query::Hello { app, ver: options.version.clone(), features: options.features.clone() };
497		let mut bytes = self.out.enc.encode(&Message::Query(hello.clone()))?;
498		bytes.extend_from_slice(b"\x1b[c");
499		if let Some(record) = &mut self.out.record {
500			record.log("out", "q", &[], serde_json::to_value(&hello)?);
501		}
502		self.out.term.write(&bytes)?;
503		let deadline = Instant::now() + options.timeout;
504		let mut buf = [0u8; 4096];
505		loop {
506			let left = deadline.saturating_duration_since(Instant::now());
507			if left.is_zero() {
508				return Ok(false);
509			}
510			let Some(n) = self.out.term.read(&mut buf, Some(left))? else {
511				continue;
512			};
513			if n == 0 {
514				return Ok(false);
515			}
516			self.last_in = Instant::now();
517			let mut accepted = false;
518			for item in self.parser.feed(&buf[..n]) {
519				match item {
520					Item::Reply(Reply::Hello(hello)) if !accepted => {
521						self.record_in("r", hello.raw.clone());
522						self.caps = Capabilities {
523							term:          hello.term,
524							version:       hello.ver,
525							kinds:         hello.kinds,
526							features:      hello.features,
527							apc:           hello.apc,
528							credits:       hello.credits,
529							cols:          hello.cols,
530							cell:          hello.cell,
531							dark:          hello.dark,
532							reduce_motion: hello.reduce_motion,
533							hour12:        hello.hour12,
534						};
535						self.out.enc.set_limit(self.caps.apc);
536						accepted = true;
537					},
538					Item::Da1 if !accepted => return Ok(false),
539					other => self.item(other)?,
540				}
541			}
542			if accepted {
543				return Ok(true);
544			}
545		}
546	}
547
548	/// What the terminal supports.
549	pub const fn caps(&self) -> &Capabilities {
550		&self.caps
551	}
552
553	/// Opens a surface.
554	///
555	/// # Errors
556	/// When writing fails.
557	pub fn open(&mut self, options: SurfaceOptions) -> Result<Surface, Error> {
558		let id = options.id.clone().unwrap_or_else(|| {
559			let id = format!("s{}", self.next_id);
560			self.next_id += 1;
561			id
562		});
563		self.out.send(&Message::Open(Open {
564			id:     id.clone(),
565			mode:   Some(options.mode),
566			title:  options.title,
567			role:   options.role,
568			listen: (!options.listen).then_some(false),
569			adopt:  options.adopt.then_some(true),
570		}))?;
571		let previous = options
572			.adopt
573			.then(|| {
574				self
575					.surfaces
576					.iter()
577					.rposition(|state| state.id == id && !state.open)
578			})
579			.flatten();
580		let (sent, seq) = previous.map_or_else(
581			|| (Doc::new(), 0),
582			|i| {
583				let state = &mut self.surfaces[i];
584				(std::mem::take(&mut state.sent), state.seq)
585			},
586		);
587		self.surfaces.push(State {
588			id,
589			listen: options.listen,
590			keep: options.keep,
591			open: true,
592			seq,
593			acked: seq,
594			sent,
595			reset_main: options.adopt && previous.is_none(),
596			pending: None,
597			queued: Vec::new(),
598			routes: Routes::default(),
599		});
600		Ok(Surface(self.surfaces.len() - 1))
601	}
602
603	/// The id of `sf`.
604	///
605	/// # Panics
606	/// When `sf` comes from another session.
607	pub fn surface_id(&self, sf: Surface) -> &str {
608		&self.surfaces[sf.0].id
609	}
610
611	/// Whether `sf` is still open (not closed, and not `gone`).
612	///
613	/// # Panics
614	/// When `sf` comes from another session.
615	pub fn is_open(&self, sf: Surface) -> bool {
616		self.surfaces[sf.0].open
617	}
618
619	/// Renders `view` on `sf`: sends the difference from the last view sent,
620	/// or, while out of credits, stores it for the frame an ack releases.
621	/// Its handlers replace the last view's. A bare node or list is `main`'s
622	/// children.
623	///
624	/// # Errors
625	/// [`Error::DuplicateId`] (nothing is sent), a closed surface, or I/O.
626	pub fn render(&mut self, sf: Surface, view: impl Into<View<M>>) -> Result<(), Error> {
627		let (doc, routes) = Doc::build(view.into())?;
628		let credits = self.caps.credits;
629		let state = open(&mut self.surfaces, sf)?;
630		state.routes = routes;
631		state.pending = Some(doc);
632		if state.blocked(credits) {
633			// Pick up acks already waiting, which may release the frame.
634			return self.pump(Duration::ZERO);
635		}
636		self.out.flush(state)
637	}
638
639	/// Sends `ops` on `sf` in one frame, after anything waiting, or queues
640	/// them while out of credits (the raw escape hatch).
641	///
642	/// # Errors
643	/// A closed surface, or I/O.
644	pub fn send(&mut self, sf: Surface, ops: Vec<Op>) -> Result<(), Error> {
645		let credits = self.caps.credits;
646		let state = open(&mut self.surfaces, sf)?;
647		state.queued.extend(ops);
648		if state.blocked(credits) {
649			return Ok(());
650		}
651		self.out.flush(state)
652	}
653
654	/// Moves the caret to field `id`, or nowhere.
655	///
656	/// # Errors
657	/// As [`send`](Self::send).
658	pub fn focus(&mut self, sf: Surface, id: Option<&str>) -> Result<(), Error> {
659		self.send(sf, vec![Op::Focus { id: id.map(str::to_owned) }])
660	}
661
662	/// Scrolls node `id` into view.
663	///
664	/// # Errors
665	/// As [`send`](Self::send).
666	pub fn reveal(&mut self, sf: Surface, id: &str, at: RevealAt) -> Result<(), Error> {
667		self.send(sf, vec![Op::Reveal { id: id.to_owned(), at }])
668	}
669
670	/// Scrolls the container around node `id`.
671	///
672	/// # Errors
673	/// As [`send`](Self::send).
674	pub fn scroll(&mut self, sf: Surface, id: &str, by: ScrollBy) -> Result<(), Error> {
675		self.send(sf, vec![Op::Scroll { id: id.to_owned(), by }])
676	}
677
678	/// Hints that node `id` won't change soon.
679	///
680	/// # Errors
681	/// As [`send`](Self::send).
682	pub fn settle(&mut self, sf: Surface, id: &str) -> Result<(), Error> {
683		self.send(sf, vec![Op::Settle { id: id.to_owned() }])
684	}
685
686	/// Hands the pane back to the grid.
687	///
688	/// # Errors
689	/// As [`send`](Self::send).
690	pub fn suspend(&mut self, sf: Surface) -> Result<(), Error> {
691		self.send(sf, vec![Op::Suspend])
692	}
693
694	/// Takes the pane again after [`suspend`](Self::suspend).
695	///
696	/// # Errors
697	/// As [`send`](Self::send).
698	pub fn resume(&mut self, sf: Surface) -> Result<(), Error> {
699		self.send(sf, vec![Op::Resume])
700	}
701
702	/// Installs or replaces stylesheet `name` of `sf`; `None` removes it.
703	///
704	/// # Errors
705	/// A closed surface, or I/O.
706	pub fn stylesheet(&mut self, sf: Surface, name: &str, css: Option<&str>) -> Result<(), Error> {
707		let id = open(&mut self.surfaces, sf)?.id.clone();
708		self.out.send(&Message::Sheet(Sheet {
709			sf:   Some(id),
710			name: name.to_owned(),
711			css:  css.map(str::to_owned),
712		}))
713	}
714
715	/// Sends the program palette of `sf` (its `sf` is filled in).
716	///
717	/// # Errors
718	/// A closed surface, or I/O.
719	pub fn palette(&mut self, sf: Surface, palette: Palette) -> Result<(), Error> {
720		let id = open(&mut self.surfaces, sf)?.id.clone();
721		self
722			.out
723			.send(&Message::Palette(Palette { sf: Some(id), ..palette }))
724	}
725
726	/// Closes `sf`: sends what still waits (credits or not), then `x`.
727	///
728	/// # Errors
729	/// A closed surface, or I/O.
730	pub fn close_surface(&mut self, sf: Surface, keep: bool) -> Result<(), Error> {
731		let state = open(&mut self.surfaces, sf)?;
732		let frame = self.out.flush(state);
733		let id = state.id.clone();
734		let close = self.out.send(&Message::Close(Close { id, keep }));
735		if close.is_ok() {
736			state.open = false;
737			state.pending = None;
738			state.queued.clear();
739			if keep {
740				state.sent.retain_main();
741			} else {
742				state.sent = Doc::new();
743			}
744		}
745		frame.and(close)
746	}
747
748	/// Sends a blob once per session and returns its id.
749	///
750	/// # Errors
751	/// [`Error::BlobTooLarge`] over 16 MiB, or I/O.
752	pub fn blob(&mut self, data: Vec<u8>, mime: Option<&str>) -> Result<String, Error> {
753		let blob = Blob::new(data, mime.map(str::to_owned))?;
754		let id = blob.id.clone();
755		if !self.blobs.contains(&id) {
756			self.out.send(&Message::Blob(blob))?;
757			self.blobs.insert(id.clone());
758		}
759		Ok(id)
760	}
761
762	/// Asks which of `ids` Tern still holds. Input arriving meanwhile waits
763	/// for [`next`](Self::next).
764	///
765	/// # Errors
766	/// [`Error::Timeout`] without a reply in 2 s, or I/O.
767	pub fn blobs(&mut self, ids: Vec<String>) -> Result<Vec<String>, Error> {
768		self.out.send(&Message::Query(Query::Blobs { ids }))?;
769		let deadline = Instant::now() + QUERY_TIMEOUT;
770		loop {
771			if let Some(at) = self
772				.replies
773				.iter()
774				.position(|r| matches!(r, Reply::Blobs(_)))
775				&& let Some(Reply::Blobs(reply)) = self.replies.remove(at)
776			{
777				return Ok(reply.have);
778			}
779			let left = deadline.saturating_duration_since(Instant::now());
780			if left.is_zero() {
781				return Err(Error::Timeout("the blobs reply"));
782			}
783			self.read(Some(left))?;
784		}
785	}
786
787	/// Waits up to `timeout` (`None`: until something arrives) for the next
788	/// key, unhandled event, or handler message. `Ok(None)` when the time is
789	/// up.
790	///
791	/// # Errors
792	/// [`Error::InputClosed`] at the end of input, or I/O.
793	pub fn next(&mut self, timeout: Option<Duration>) -> Result<Option<Input<M>>, Error> {
794		let deadline = timeout.map(|t| Instant::now() + t);
795		loop {
796			if let Some(input) = self.inbox.pop_front() {
797				return Ok(Some(input));
798			}
799			if deadline.is_some_and(|d| Instant::now() >= d) && !self.read(Some(Duration::ZERO))? {
800				return Ok(self.inbox.pop_front());
801			}
802			let left = deadline.map(|d| d.saturating_duration_since(Instant::now()));
803			self.read(left)?;
804		}
805	}
806
807	/// Processes input for up to `timeout` without returning it: acks
808	/// release waiting frames; keys and events wait for
809	/// [`next`](Self::next).
810	///
811	/// # Errors
812	/// [`Error::InputClosed`] at the end of input, or I/O.
813	pub fn pump(&mut self, timeout: Duration) -> Result<(), Error> {
814		let deadline = Instant::now() + timeout;
815		loop {
816			let left = deadline.saturating_duration_since(Instant::now());
817			if !self.read(Some(left))? && Instant::now() >= deadline {
818				return Ok(());
819			}
820		}
821	}
822
823	/// Reads once, waiting at most `timeout` but releasing undecided input
824	/// after [`FLUSH_AFTER`] of quiet. Whether any input was handled.
825	fn read(&mut self, timeout: Option<Duration>) -> Result<bool, Error> {
826		let held = self.parser.pending() || self.keys.pending();
827		let flush_in = held.then(|| FLUSH_AFTER.saturating_sub(self.last_in.elapsed()));
828		let wait = match (timeout, flush_in) {
829			(Some(t), Some(f)) => Some(t.min(f)),
830			(t, f) => t.or(f),
831		};
832		let mut buf = [0u8; 8192];
833		match self.out.term.read(&mut buf, wait)? {
834			Some(0) => Err(Error::InputClosed),
835			Some(n) => {
836				self.last_in = Instant::now();
837				for item in self.parser.feed(&buf[..n]) {
838					self.item(item)?;
839				}
840				Ok(true)
841			},
842			None => {
843				if held && self.last_in.elapsed() >= FLUSH_AFTER {
844					for item in self.parser.flush() {
845						self.item(item)?;
846					}
847					for key in self.keys.flush() {
848						self.inbox.push_back(Input::Key(key));
849					}
850					return Ok(true);
851				}
852				Ok(false)
853			},
854		}
855	}
856
857	/// Handles one parsed input item.
858	fn item(&mut self, item: Item) -> Result<(), Error> {
859		match item {
860			Item::Keys(bytes) => {
861				for key in self.keys.feed(&bytes) {
862					self.inbox.push_back(Input::Key(key));
863				}
864			},
865			Item::Reply(reply) => {
866				self.record_in("r", reply.raw().clone());
867				self.replies.push_back(reply);
868			},
869			Item::Event(event) => {
870				self.record_in("e", event.raw().clone());
871				self.event(event)?;
872			},
873			Item::Da1 => {},
874		}
875		Ok(())
876	}
877
878	/// Records an incoming message.
879	fn record_in(&mut self, verb: &str, raw: serde_json::Map<String, Value>) {
880		if let Some(record) = &mut self.out.record {
881			record.log("in", verb, &[], Value::Object(raw));
882		}
883	}
884
885	/// The newest surface with id `sf`.
886	fn by_id(&self, sf: &str) -> Option<usize> {
887		self.surfaces.iter().rposition(|s| s.id == sf)
888	}
889
890	/// Handles an event: state updates, flow control, then routing.
891	fn event(&mut self, event: Event) -> Result<(), Error> {
892		match &event {
893			Event::Ack(ack) => {
894				if let Some(i) = self.by_id(&ack.sf) {
895					let credits = self.caps.credits;
896					let state = &mut self.surfaces[i];
897					state.acked = state.acked.max(ack.s);
898					if state.open && !state.blocked(credits) {
899						self.out.flush(state)?;
900					}
901				}
902				return Ok(());
903			},
904			Event::Resize(r) => {
905				self.caps.cols = r.cols;
906				if r.cell.is_some() {
907					self.caps.cell = r.cell;
908				}
909			},
910			Event::Theme(t) => self.caps.dark = t.dark,
911			Event::Motion(m) => self.caps.reduce_motion = m.reduce,
912			Event::Gone(g) => {
913				for state in &mut self.surfaces {
914					if g.ids.contains(&state.id) && g.sf.as_ref().is_none_or(|sf| *sf == state.id) {
915						state.open = false;
916					}
917				}
918			},
919			_ => {},
920		}
921		let msg = event
922			.sf()
923			.and_then(|sf| self.by_id(sf))
924			.and_then(|i| self.surfaces[i].routes.route(&event));
925		self.inbox.push_back(match msg {
926			Some(msg) => Input::Msg(msg, event),
927			None => Input::Event(event),
928		});
929		Ok(())
930	}
931
932	/// Closes the session: open surfaces (each as its options say), the
933	/// input modes, a short drain of late input, and the tty.
934	///
935	/// # Errors
936	/// The first I/O error; every step still runs.
937	pub fn close(mut self) -> Result<(), Error> {
938		self.shutdown()
939	}
940
941	/// The close sequence (idempotent).
942	fn shutdown(&mut self) -> Result<(), Error> {
943		if self.closed {
944			return Ok(());
945		}
946		self.closed = true;
947		let mut first: Option<Error> = None;
948		let mut note = |r: Result<(), Error>| {
949			if let Err(err) = r
950				&& first.is_none()
951			{
952				first = Some(err);
953			}
954		};
955		for i in 0..self.surfaces.len() {
956			if self.surfaces[i].open {
957				let keep = self.surfaces[i].keep;
958				note(self.close_surface(Surface(i), keep));
959			}
960		}
961		let reset = term::mode_reset(self.modes.0, self.modes.1);
962		if !reset.is_empty() {
963			note(self.out.term.write(reset).map_err(Error::from));
964		}
965		self.out.term.modes(false, false);
966		let until = Instant::now() + DRAIN;
967		let mut buf = [0u8; 4096];
968		loop {
969			let left = until.saturating_duration_since(Instant::now());
970			if left.is_zero() {
971				break;
972			}
973			match self.out.term.read(&mut buf, Some(left)) {
974				Ok(Some(n)) if n > 0 => {},
975				_ => break,
976			}
977		}
978		note(self.out.term.restore().map_err(Error::from));
979		first.map_or(Ok(()), Err)
980	}
981}
982
983impl<M> Drop for Session<M> {
984	fn drop(&mut self) {
985		if let Err(err) = self.shutdown() {
986			tracing::warn!(%err, "closing the TSP session failed");
987		}
988	}
989}
990
991/// The state of `sf`, when it is open.
992fn open<M>(surfaces: &mut [State<M>], sf: Surface) -> Result<&mut State<M>, Error> {
993	let state = &mut surfaces[sf.0];
994	if state.open {
995		Ok(state)
996	} else {
997		Err(Error::SurfaceClosed(state.id.clone()))
998	}
999}
1000
1001/// Whether the environment allows TSP on the process's terminal.
1002#[cfg(any(unix, windows))]
1003fn available() -> bool {
1004	let set = |name: &str| std::env::var_os(name).is_some_and(|v| !v.is_empty());
1005	if std::env::var_os("TERN_TSP").is_some_and(|v| v == "0") {
1006		return false;
1007	}
1008	if set("TMUX") || set("STY") || set("ZELLIJ") {
1009		return false;
1010	}
1011	term::is_tty()
1012}
1013
1014/// The executable's file name, without extension.
1015fn exe_name() -> Option<String> {
1016	let exe = std::env::current_exe().ok()?;
1017	Some(exe.file_stem()?.to_string_lossy().into_owned())
1018}