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, Waker},
57	ui::View,
58	wire::{
59		Blob, Cell, Close, Encoder, Event, Font, Frame, Message, Mode, Op, Open, 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/// How long a `tmux` command may run before it counts as failed.
71#[cfg(any(unix, windows))]
72const TMUX_TIMEOUT: Duration = Duration::from_millis(500);
73
74/// How to connect.
75#[derive(Clone, Debug)]
76pub struct Options {
77	/// The program's name (default: the executable's).
78	pub app:             Option<String>,
79	/// The program's version.
80	pub version:         Option<String>,
81	/// Program features (`edit`, `undo`, `send`).
82	pub features:        Vec<String>,
83	/// How long to wait for the `hello` reply.
84	pub timeout:         Duration,
85	/// Enable bracketed paste while connected.
86	pub bracketed_paste: bool,
87	/// Push the kitty keyboard flag 1 while connected.
88	pub kitty_keyboard:  bool,
89	/// Record every message as JSONL (default: `TERN_TSP_RECORD`).
90	pub record:          Option<PathBuf>,
91}
92
93impl Default for Options {
94	fn default() -> Self {
95		Self {
96			app:             None,
97			version:         None,
98			features:        Vec::new(),
99			timeout:         Duration::from_secs(1),
100			bracketed_paste: true,
101			kitty_keyboard:  true,
102			record:          std::env::var_os("TERN_TSP_RECORD")
103				.filter(|v| !v.is_empty())
104				.map(PathBuf::from),
105		}
106	}
107}
108
109impl Options {
110	/// The defaults.
111	pub fn new() -> Self {
112		Self::default()
113	}
114
115	/// Sets the program's name.
116	pub fn app(mut self, app: impl Into<String>) -> Self {
117		self.app = Some(app.into());
118		self
119	}
120
121	/// Sets the program's version.
122	pub fn version(mut self, version: impl Into<String>) -> Self {
123		self.version = Some(version.into());
124		self
125	}
126
127	/// Adds a program feature ([`crate::wire::feature`]).
128	pub fn feature(mut self, feature: impl Into<String>) -> Self {
129		self.features.push(feature.into());
130		self
131	}
132
133	/// Sets the handshake timeout.
134	pub const fn timeout(mut self, timeout: Duration) -> Self {
135		self.timeout = timeout;
136		self
137	}
138
139	/// Turns bracketed paste on or off.
140	pub const fn bracketed_paste(mut self, on: bool) -> Self {
141		self.bracketed_paste = on;
142		self
143	}
144
145	/// Turns the kitty keyboard flag on or off.
146	pub const fn kitty_keyboard(mut self, on: bool) -> Self {
147		self.kitty_keyboard = on;
148		self
149	}
150
151	/// Records to `path` (or not).
152	pub fn record(mut self, path: Option<PathBuf>) -> Self {
153		self.record = path;
154		self
155	}
156}
157
158/// What the terminal draws and supports, kept current by `resize`,
159/// `theme` and `motion` events.
160#[derive(Clone, Debug, Default, PartialEq)]
161pub struct Capabilities {
162	/// The terminal (`tern`).
163	pub term:          String,
164	/// Its version.
165	pub version:       String,
166	/// Every kind it draws.
167	pub kinds:         Vec<String>,
168	/// Terminal features.
169	pub features:      Vec<String>,
170	/// The largest body per message.
171	pub apc:           usize,
172	/// Frames that may be unacknowledged.
173	pub credits:       u32,
174	/// The pane's width in columns.
175	pub cols:          u32,
176	/// The cell size in pixels.
177	pub cell:          Option<Cell>,
178	/// Whether the terminal shows its dark appearance.
179	pub dark:          bool,
180	/// Whether Reduce Motion is on.
181	pub reduce_motion: bool,
182	/// Whether the user's system reads a 12-hour clock (`3:05 PM`); false
183	/// (24-hour) on terminals that don't say.
184	pub hour12:        bool,
185}
186
187impl Capabilities {
188	/// Whether the terminal draws `kind`.
189	pub fn has_kind(&self, kind: &str) -> bool {
190		self.kinds.iter().any(|k| k == kind)
191	}
192
193	/// Whether the terminal has `feature`.
194	pub fn has_feature(&self, feature: &str) -> bool {
195		self.features.iter().any(|f| f == feature)
196	}
197}
198
199/// How to open a surface.
200#[derive(Clone, Debug)]
201pub struct SurfaceOptions {
202	/// The id (default `s1`, `s2`, … per session).
203	pub id:       Option<String>,
204	/// The mode.
205	pub mode:     Mode,
206	/// Names the pane until the program sets its own title.
207	pub title:    Option<String>,
208	/// `data-surface` for stylesheets.
209	pub role:     Option<String>,
210	/// Whether the program reads its events (default `true`).
211	pub listen:   bool,
212	/// Reopen a closed inline surface with this id.
213	pub adopt:    bool,
214	/// Keep it on screen when the session closes it (default `true`).
215	pub keep:     bool,
216	/// Closing the pane loses nothing, so Tern closes it without asking
217	/// (default `false`).
218	pub closable: bool,
219	/// The base face (default mono; `Font::Sans` for apps).
220	pub font:     Option<Font>,
221}
222
223impl Default for SurfaceOptions {
224	fn default() -> Self {
225		Self {
226			id:       None,
227			mode:     Mode::default(),
228			title:    None,
229			role:     None,
230			listen:   true,
231			adopt:    false,
232			keep:     true,
233			closable: false,
234			font:     None,
235		}
236	}
237}
238
239impl SurfaceOptions {
240	/// A surface of `mode`, listening, kept on close.
241	pub fn new(mode: Mode) -> Self {
242		Self { mode, ..Self::default() }
243	}
244
245	/// An inline surface (a session program).
246	pub fn inline() -> Self {
247		Self::new(Mode::Inline)
248	}
249
250	/// A screen surface (a full-screen view).
251	pub fn screen() -> Self {
252		Self::new(Mode::Screen)
253	}
254
255	/// A flow surface (command output that stays in the scrollback).
256	pub fn flow() -> Self {
257		Self::new(Mode::Flow)
258	}
259
260	/// Sets the id.
261	pub fn id(mut self, id: impl Into<String>) -> Self {
262		self.id = Some(id.into());
263		self
264	}
265
266	/// Sets the title.
267	pub fn title(mut self, title: impl Into<String>) -> Self {
268		self.title = Some(title.into());
269		self
270	}
271
272	/// Sets the role.
273	pub fn role(mut self, role: impl Into<String>) -> Self {
274		self.role = Some(role.into());
275		self
276	}
277
278	/// Sets whether the program listens (no acks or events when not).
279	pub const fn listen(mut self, listen: bool) -> Self {
280		self.listen = listen;
281		self
282	}
283
284	/// Reopens a closed inline surface with this id.
285	pub const fn adopt(mut self, adopt: bool) -> Self {
286		self.adopt = adopt;
287		self
288	}
289
290	/// Sets whether the session's close keeps the surface on screen.
291	pub const fn keep(mut self, keep: bool) -> Self {
292		self.keep = keep;
293		self
294	}
295
296	/// Sets whether closing the pane skips Tern's "a program is running"
297	/// prompt.
298	pub const fn closable(mut self, closable: bool) -> Self {
299		self.closable = closable;
300		self
301	}
302
303	/// Sets the base face: `Font::Sans` draws people's text in the UI face
304	/// (machine strings stay mono).
305	pub const fn font(mut self, font: Font) -> Self {
306		self.font = Some(font);
307		self
308	}
309}
310
311/// A surface of a session, as [`Session::open`] returned it (only valid
312/// with that session; another session's handle panics).
313#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
314pub struct Surface(usize);
315
316/// What [`Session::next`] returns.
317#[derive(Debug)]
318pub enum Input<M> {
319	/// A key press or a paste.
320	Key(Key),
321	/// An event no handler took.
322	Event(Event),
323	/// The message of the handler an event was routed to, with the event.
324	Msg(M, Event),
325	/// An `OSC 5522` clipboard packet: the bytes after `5522;`, before the
326	/// terminator.
327	Clipboard(Vec<u8>),
328}
329
330/// One open (or closed) surface.
331struct State<M> {
332	/// The surface id.
333	id:         String,
334	/// Whether acks come.
335	listen:     bool,
336	/// Keep on the session's close.
337	keep:       bool,
338	/// Still open.
339	open:       bool,
340	/// The last frame's sequence number.
341	seq:        u64,
342	/// The highest frame acked.
343	acked:      u64,
344	/// The view as last sent.
345	sent:       Doc,
346	/// An adopted surface from outside this session needs its unknown main
347	/// removed.
348	reset_main: bool,
349	/// A view rendered while out of credits.
350	pending:    Option<Doc>,
351	/// View ops and raw ops waiting for credit.
352	queued:     Vec<Op>,
353	/// The handlers of the view last rendered.
354	routes:     Routes<M>,
355}
356
357impl<M> State<M> {
358	/// Whether a frame must wait for an ack.
359	const fn blocked(&self, credits: u32) -> bool {
360		self.listen && self.seq.saturating_sub(self.acked) >= credits as u64
361	}
362}
363
364/// The writing half: the terminal, the encoder and the recording.
365struct Out {
366	/// The terminal.
367	term:   Box<dyn Terminal>,
368	/// Chunks under the `apc` limit.
369	enc:    Encoder,
370	/// The recording, when on.
371	record: Option<Recorder>,
372}
373
374impl Out {
375	/// Encodes, records and writes one message.
376	fn send(&mut self, message: &Message) -> Result<(), Error> {
377		let bytes = self.enc.encode(message)?;
378		if let Some(record) = &mut self.record {
379			let body = match message {
380				Message::Blob(_) => Value::String(message.body()?),
381				_ => serde_json::from_str(&message.body()?)?,
382			};
383			record.log("out", message.verb(), &message.params(), body);
384		}
385		self.term.write(&bytes)?;
386		Ok(())
387	}
388
389	/// Sends pending changes, committing their state only after the write
390	/// succeeds.
391	fn flush<M>(&mut self, state: &mut State<M>) -> Result<(), Error> {
392		let mut ops = state
393			.pending
394			.as_ref()
395			.map_or_default(|doc| state.sent.ops(doc, &state.id));
396		if state.reset_main && (state.pending.is_some() || !state.queued.is_empty()) {
397			ops.insert(0, Op::Del { id: "main".into() });
398		}
399		let queued_at = ops.len();
400		ops.append(&mut state.queued);
401		if !ops.is_empty() {
402			let mut message = Message::Frame(Frame { sf: state.id.clone(), s: state.seq + 1, ops });
403			if let Err(err) = self.send(&message) {
404				if let Message::Frame(frame) = &mut message {
405					state.queued = frame.ops.split_off(queued_at);
406				}
407				return Err(err);
408			}
409			state.seq += 1;
410			state.reset_main = false;
411		}
412		if let Some(doc) = state.pending.take() {
413			state.sent = doc;
414		}
415		Ok(())
416	}
417}
418
419/// A connection to a terminal that speaks TSP. Dropping it closes it
420/// (surfaces, modes, tty).
421pub struct Session<M = ()> {
422	/// The writing half.
423	out:      Out,
424	/// Splits TSP out of the input.
425	parser:   Parser,
426	/// Turns key bytes into keys.
427	keys:     Decoder,
428	/// What the terminal supports.
429	caps:     Capabilities,
430	/// Every surface opened, by [`Surface`] index.
431	surfaces: Vec<State<M>>,
432	/// The number of the next default surface id.
433	next_id:  u32,
434	/// Inputs ready for [`Session::next`].
435	inbox:    VecDeque<Input<M>>,
436	/// Replies not yet claimed.
437	replies:  VecDeque<Reply>,
438	/// Blob ids sent this session.
439	blobs:    HashSet<String>,
440	/// The input modes enabled: bracketed paste, kitty keys.
441	modes:    (bool, bool),
442	/// When input last arrived.
443	last_in:  Instant,
444	/// The terminal's waker, when it has one.
445	waker:    Option<Waker>,
446	/// The tmux pane whose title updates are off while TSP is spoken.
447	tmux:     Option<TmuxPane>,
448	/// Closed already.
449	closed:   bool,
450}
451
452/// Runs a `tmux` command: its standard output, `None` when it fails.
453type RunTmux = fn(&[&str]) -> Option<String>;
454
455/// The tmux pane a session speaks TSP in.
456struct TmuxPane {
457	/// The pane id (`TMUX_PANE`, like `%3`).
458	pane: String,
459	/// Runs `tmux`.
460	run:  RunTmux,
461}
462
463impl TmuxPane {
464	/// Turns the pane's `allow-set-title` off (tmux makes every APC body the
465	/// title otherwise), or back to its inherited value: whether tmux agreed.
466	fn titles(&self, restore: bool) -> bool {
467		let args: &[&str] = if restore {
468			&["set-option", "-pu", "-t", &self.pane, "allow-set-title"]
469		} else {
470			&["set-option", "-p", "-t", &self.pane, "allow-set-title", "off"]
471		};
472		(self.run)(args).is_some()
473	}
474}
475
476/// How detection lets a session probe.
477#[cfg(any(unix, windows))]
478#[derive(Clone, Debug, PartialEq, Eq)]
479enum Probe {
480	/// `hello` + DA1.
481	Direct,
482	/// Inside tmux whose session Tern draws: `hello` alone, in this pane.
483	Tmux(String),
484}
485
486/// Whether and how the environment `env` lets a session probe: never with
487/// `TERN_TSP=0` or inside screen or zellij; inside tmux only when the
488/// session's `@tern-tsp` option, read through `run`, has a value.
489#[cfg(any(unix, windows))]
490fn probe(
491	env: impl Fn(&str) -> Option<std::ffi::OsString>,
492	run: impl FnOnce(&[&str]) -> Option<String>,
493) -> Option<Probe> {
494	let set = |name: &str| env(name).is_some_and(|v| !v.is_empty());
495	if env("TERN_TSP").is_some_and(|v| v == "0") || set("STY") || set("ZELLIJ") {
496		return None;
497	}
498	if !set("TMUX") {
499		return Some(Probe::Direct);
500	}
501	let pane = env("TMUX_PANE")
502		.filter(|v| !v.is_empty())?
503		.into_string()
504		.ok()?;
505	let watched = run(&["show-options", "-qv", "@tern-tsp"])?;
506	(!watched.trim().is_empty()).then_some(Probe::Tmux(pane))
507}
508
509/// Runs `tmux` from `PATH` with the inherited environment: its standard
510/// output, or `None` when it fails or outlives [`TMUX_TIMEOUT`].
511#[cfg(any(unix, windows))]
512fn run_tmux(args: &[&str]) -> Option<String> {
513	use std::{
514		io::Read as _,
515		process::{Command, Stdio},
516	};
517	let mut child = Command::new("tmux")
518		.args(args)
519		.stdin(Stdio::null())
520		.stdout(Stdio::piped())
521		.stderr(Stdio::null())
522		.spawn()
523		.ok()?;
524	let deadline = Instant::now() + TMUX_TIMEOUT;
525	loop {
526		match child.try_wait() {
527			Ok(Some(status)) if status.success() => {
528				let mut out = String::new();
529				child.stdout.take()?.read_to_string(&mut out).ok()?;
530				return Some(out);
531			},
532			Ok(Some(_)) => return None,
533			Ok(None) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(5)),
534			_ => {
535				let _ = child.kill();
536				let _ = child.wait();
537				return None;
538			},
539		}
540	}
541}
542
543impl<M> Session<M> {
544	/// Connects to the process's terminal (Unix and Windows only): `Ok(None)`
545	/// when TSP isn't available (`TERN_TSP=0`, stdin or stdout not a tty,
546	/// inside screen or zellij, inside tmux unless Tern draws its session, or
547	/// the terminal answers DA1 first or not in time). Inside tmux it sends
548	/// `hello` without DA1 (tmux answers DA1 itself) and turns the pane's
549	/// `allow-set-title` off until the session ends.
550	///
551	/// # Errors
552	/// When the tty can't be switched to raw mode or I/O fails.
553	#[cfg(any(unix, windows))]
554	pub fn connect(options: Options) -> Result<Option<Self>, Error> {
555		if !term::is_tty() {
556			return Ok(None);
557		}
558		let Some(probe) = probe(|name| std::env::var_os(name), run_tmux) else {
559			return Ok(None);
560		};
561		let tmux = match probe {
562			Probe::Direct => None,
563			Probe::Tmux(pane) => Some(TmuxPane { pane, run: run_tmux }),
564		};
565		let tty = Tty::open()?;
566		Self::start(tty, options, tmux)
567	}
568
569	/// Runs the handshake over `term` (already in raw mode): writes `hello`
570	/// and a DA1 request and waits for the reply. `Ok(None)`, with the
571	/// terminal restored, when DA1 answers first or nothing answers in time.
572	///
573	/// # Errors
574	/// When I/O fails or the recording can't be opened.
575	pub fn with_terminal(
576		term: impl Terminal + 'static,
577		options: Options,
578	) -> Result<Option<Self>, Error> {
579		Self::start(term, options, None)
580	}
581
582	/// [`with_terminal`](Self::with_terminal), inside the tmux pane `tmux`
583	/// when given: its title updates go off before `hello`, which goes out
584	/// without DA1, and come back when the session ends.
585	fn start(
586		term: impl Terminal + 'static,
587		options: Options,
588		tmux: Option<TmuxPane>,
589	) -> Result<Option<Self>, Error> {
590		let record = options.record.as_deref().map(Recorder::open).transpose()?;
591		let waker = term.waker();
592		let mut session = Self {
593			out: Out { term: Box::new(term), enc: Encoder::default(), record },
594			parser: Parser::new(),
595			keys: Decoder::new(),
596			caps: Capabilities::default(),
597			surfaces: Vec::new(),
598			next_id: 1,
599			inbox: VecDeque::new(),
600			replies: VecDeque::new(),
601			blobs: HashSet::new(),
602			modes: (false, false),
603			last_in: Instant::now(),
604			waker,
605			tmux: None,
606			closed: false,
607		};
608		if let Some(tmux) = tmux {
609			if !tmux.titles(false) {
610				session.closed = true;
611				session.out.term.restore()?;
612				return Ok(None);
613			}
614			session.tmux = Some(tmux);
615		}
616		let app = options.app.clone().or_else(exe_name);
617		match session.handshake(app, &options) {
618			Ok(true) => {},
619			Ok(false) => {
620				session.closed = true;
621				session.release_tmux();
622				session.out.term.restore()?;
623				return Ok(None);
624			},
625			Err(err) => {
626				session.closed = true;
627				session.release_tmux();
628				let _ = session.out.term.restore();
629				return Err(err);
630			},
631		}
632		let (paste, kitty) = (options.bracketed_paste, options.kitty_keyboard);
633		let mut enable = Vec::new();
634		if paste {
635			enable.extend_from_slice(b"\x1b[?2004h");
636		}
637		if kitty {
638			enable.extend_from_slice(b"\x1b[>1u");
639		}
640		session.modes = (paste, kitty);
641		session.out.term.modes(paste, kitty);
642		if !enable.is_empty() {
643			session.out.term.write(&enable)?;
644		}
645		Ok(Some(session))
646	}
647
648	/// Sends `hello` + DA1 (`hello` alone in tmux, where only the reply or
649	/// the timeout decides) and waits: whether the hello reply came first.
650	fn handshake(&mut self, app: Option<String>, options: &Options) -> Result<bool, Error> {
651		let hello =
652			Query::Hello { app, ver: options.version.clone(), features: options.features.clone() };
653		let mut bytes = self.out.enc.encode(&Message::Query(hello.clone()))?;
654		let da1 = self.tmux.is_none();
655		if da1 {
656			bytes.extend_from_slice(b"\x1b[c");
657		}
658		if let Some(record) = &mut self.out.record {
659			record.log("out", "q", &[], serde_json::to_value(&hello)?);
660		}
661		self.out.term.write(&bytes)?;
662		let deadline = Instant::now() + options.timeout;
663		let mut buf = [0u8; 4096];
664		loop {
665			let left = deadline.saturating_duration_since(Instant::now());
666			if left.is_zero() {
667				return Ok(false);
668			}
669			let Some(n) = self.out.term.read(&mut buf, Some(left))? else {
670				continue;
671			};
672			if n == 0 {
673				return Ok(false);
674			}
675			self.last_in = Instant::now();
676			let mut accepted = false;
677			for item in self.parser.feed(&buf[..n]) {
678				match item {
679					Item::Reply(Reply::Hello(hello)) if !accepted => {
680						self.record_in("r", hello.raw.clone());
681						self.caps = Capabilities {
682							term:          hello.term,
683							version:       hello.ver,
684							kinds:         hello.kinds,
685							features:      hello.features,
686							apc:           hello.apc,
687							credits:       hello.credits,
688							cols:          hello.cols,
689							cell:          hello.cell,
690							dark:          hello.dark,
691							reduce_motion: hello.reduce_motion,
692							hour12:        hello.hour12,
693						};
694						self.out.enc.set_limit(self.caps.apc);
695						accepted = true;
696					},
697					Item::Da1 if !accepted && da1 => return Ok(false),
698					other => self.item(other)?,
699				}
700			}
701			if accepted {
702				return Ok(true);
703			}
704		}
705	}
706
707	/// What the terminal supports.
708	pub const fn caps(&self) -> &Capabilities {
709		&self.caps
710	}
711
712	/// Opens a surface.
713	///
714	/// # Errors
715	/// When writing fails.
716	pub fn open(&mut self, options: SurfaceOptions) -> Result<Surface, Error> {
717		let id = options.id.clone().unwrap_or_else(|| {
718			let id = format!("s{}", self.next_id);
719			self.next_id += 1;
720			id
721		});
722		self.out.send(&Message::Open(Open {
723			id:       id.clone(),
724			mode:     Some(options.mode),
725			title:    options.title,
726			role:     options.role,
727			listen:   (!options.listen).then_some(false),
728			adopt:    options.adopt.then_some(true),
729			closable: options.closable.then_some(true),
730			font:     options.font,
731		}))?;
732		let previous = options
733			.adopt
734			.then(|| {
735				self
736					.surfaces
737					.iter()
738					.rposition(|state| state.id == id && !state.open)
739			})
740			.flatten();
741		let (sent, seq) = previous.map_or_else(
742			|| (Doc::new(), 0),
743			|i| {
744				let state = &mut self.surfaces[i];
745				(std::mem::take(&mut state.sent), state.seq)
746			},
747		);
748		self.surfaces.push(State {
749			id,
750			listen: options.listen,
751			keep: options.keep,
752			open: true,
753			seq,
754			acked: seq,
755			sent,
756			reset_main: options.adopt && previous.is_none(),
757			pending: None,
758			queued: Vec::new(),
759			routes: Routes::default(),
760		});
761		Ok(Surface(self.surfaces.len() - 1))
762	}
763
764	/// The id of `sf`.
765	///
766	/// # Panics
767	/// When `sf` comes from another session.
768	pub fn surface_id(&self, sf: Surface) -> &str {
769		&self.surfaces[sf.0].id
770	}
771
772	/// Whether `sf` is still open (not closed, and not `gone`).
773	///
774	/// # Panics
775	/// When `sf` comes from another session.
776	pub fn is_open(&self, sf: Surface) -> bool {
777		self.surfaces[sf.0].open
778	}
779
780	/// Renders `view` on `sf`: sends the difference from the last view sent,
781	/// or, while out of credits, stores it for the frame an ack releases.
782	/// Its handlers replace the last view's. A bare node or list is `main`'s
783	/// children.
784	///
785	/// # Errors
786	/// [`Error::DuplicateId`] (nothing is sent), a closed surface, or I/O.
787	pub fn render(&mut self, sf: Surface, view: impl Into<View<M>>) -> Result<(), Error> {
788		let (doc, routes) = Doc::build(view.into())?;
789		let credits = self.caps.credits;
790		let state = open(&mut self.surfaces, sf)?;
791		state.routes = routes;
792		state.pending = Some(doc);
793		if state.blocked(credits) {
794			// Pick up acks already waiting, which may release the frame.
795			return self.pump(Duration::ZERO);
796		}
797		self.out.flush(state)
798	}
799
800	/// Sends `ops` on `sf` in one frame, after anything waiting, or queues
801	/// them while out of credits (the raw escape hatch).
802	///
803	/// # Errors
804	/// A closed surface, or I/O.
805	pub fn send(&mut self, sf: Surface, ops: Vec<Op>) -> Result<(), Error> {
806		let credits = self.caps.credits;
807		let state = open(&mut self.surfaces, sf)?;
808		state.queued.extend(ops);
809		if state.blocked(credits) {
810			return Ok(());
811		}
812		self.out.flush(state)
813	}
814
815	/// Moves the caret to field `id`, or nowhere.
816	///
817	/// # Errors
818	/// As [`send`](Self::send).
819	pub fn focus(&mut self, sf: Surface, id: Option<&str>) -> Result<(), Error> {
820		// The caret ends where the last focus puts it: a focus still waiting
821		// for credit is stale and may name a node of a view Tern never got.
822		open(&mut self.surfaces, sf)?
823			.queued
824			.retain(|op| !matches!(op, Op::Focus { .. }));
825		self.send(sf, vec![Op::Focus { id: id.map(str::to_owned) }])
826	}
827
828	/// Scrolls node `id` into view.
829	///
830	/// # Errors
831	/// As [`send`](Self::send).
832	pub fn reveal(&mut self, sf: Surface, id: &str, at: RevealAt) -> Result<(), Error> {
833		self.send(sf, vec![Op::Reveal { id: id.to_owned(), at }])
834	}
835
836	/// Scrolls the container around node `id`.
837	///
838	/// # Errors
839	/// As [`send`](Self::send).
840	pub fn scroll(&mut self, sf: Surface, id: &str, by: ScrollBy) -> Result<(), Error> {
841		self.send(sf, vec![Op::Scroll { id: id.to_owned(), by }])
842	}
843
844	/// Hints that node `id` won't change soon.
845	///
846	/// # Errors
847	/// As [`send`](Self::send).
848	pub fn settle(&mut self, sf: Surface, id: &str) -> Result<(), Error> {
849		self.send(sf, vec![Op::Settle { id: id.to_owned() }])
850	}
851
852	/// Hands the pane back to the grid.
853	///
854	/// # Errors
855	/// As [`send`](Self::send).
856	pub fn suspend(&mut self, sf: Surface) -> Result<(), Error> {
857		self.send(sf, vec![Op::Suspend])
858	}
859
860	/// Takes the pane again after [`suspend`](Self::suspend).
861	///
862	/// # Errors
863	/// As [`send`](Self::send).
864	pub fn resume(&mut self, sf: Surface) -> Result<(), Error> {
865		self.send(sf, vec![Op::Resume])
866	}
867
868	/// Installs or replaces stylesheet `name` of `sf`; `None` removes it.
869	///
870	/// # Errors
871	/// A closed surface, or I/O.
872	pub fn stylesheet(&mut self, sf: Surface, name: &str, css: Option<&str>) -> Result<(), Error> {
873		let id = open(&mut self.surfaces, sf)?.id.clone();
874		self.out.send(&Message::Sheet(Sheet {
875			sf:   Some(id),
876			name: name.to_owned(),
877			css:  css.map(str::to_owned),
878		}))
879	}
880
881	/// Closes `sf`: sends what still waits (credits or not), then `x`.
882	///
883	/// # Errors
884	/// A closed surface, or I/O.
885	pub fn close_surface(&mut self, sf: Surface, keep: bool) -> Result<(), Error> {
886		let state = open(&mut self.surfaces, sf)?;
887		let frame = self.out.flush(state);
888		let id = state.id.clone();
889		let close = self.out.send(&Message::Close(Close { id, keep }));
890		if close.is_ok() {
891			state.open = false;
892			state.pending = None;
893			state.queued.clear();
894			if keep {
895				state.sent.retain_main();
896			} else {
897				state.sent = Doc::new();
898			}
899		}
900		frame.and(close)
901	}
902
903	/// Sends a blob once per session and returns its id.
904	///
905	/// # Errors
906	/// [`Error::BlobTooLarge`] over 16 MiB, or I/O.
907	pub fn blob(&mut self, data: Vec<u8>, mime: Option<&str>) -> Result<String, Error> {
908		let blob = Blob::new(data, mime.map(str::to_owned))?;
909		let id = blob.id.clone();
910		if !self.blobs.contains(&id) {
911			self.out.send(&Message::Blob(blob))?;
912			self.blobs.insert(id.clone());
913		}
914		Ok(id)
915	}
916
917	/// Asks which of `ids` Tern still holds. Input arriving meanwhile waits
918	/// for [`next`](Self::next).
919	///
920	/// # Errors
921	/// [`Error::Timeout`] without a reply in 2 s, or I/O.
922	pub fn blobs(&mut self, ids: Vec<String>) -> Result<Vec<String>, Error> {
923		self.out.send(&Message::Query(Query::Blobs { ids }))?;
924		let deadline = Instant::now() + QUERY_TIMEOUT;
925		loop {
926			if let Some(at) = self
927				.replies
928				.iter()
929				.position(|r| matches!(r, Reply::Blobs(_)))
930				&& let Some(Reply::Blobs(reply)) = self.replies.remove(at)
931			{
932				return Ok(reply.have);
933			}
934			let left = deadline.saturating_duration_since(Instant::now());
935			if left.is_zero() {
936				return Err(Error::Timeout("the blobs reply"));
937			}
938			self.read(Some(left))?;
939		}
940	}
941
942	/// The terminal's waker: [`Waker::wake`] from any thread makes the
943	/// current or next [`next`](Self::next) return `Ok(None)`. `None` when
944	/// the terminal has no waker.
945	pub fn waker(&self) -> Option<Waker> {
946		self.waker.clone()
947	}
948
949	/// Writes `bytes` to the terminal as they are (OSC sequences, mode
950	/// switches), outside any surface.
951	///
952	/// # Errors
953	/// When the output fails.
954	pub fn write_raw(&mut self, bytes: &[u8]) -> Result<(), Error> {
955		self.out.term.write(bytes)?;
956		Ok(())
957	}
958
959	/// Waits up to `timeout` (`None`: until something arrives) for the next
960	/// key, unhandled event, handler message, or clipboard packet.
961	/// `Ok(None)`: the timeout passed or the waker was woken.
962	///
963	/// # Errors
964	/// [`Error::InputClosed`] at the end of input, or I/O.
965	pub fn next(&mut self, timeout: Option<Duration>) -> Result<Option<Input<M>>, Error> {
966		let deadline = timeout.map(|t| Instant::now() + t);
967		loop {
968			if let Some(input) = self.inbox.pop_front() {
969				return Ok(Some(input));
970			}
971			if self.waker.as_ref().is_some_and(Waker::take) {
972				return Ok(None);
973			}
974			if deadline.is_some_and(|d| Instant::now() >= d) && !self.read(Some(Duration::ZERO))? {
975				return Ok(self.inbox.pop_front());
976			}
977			let left = deadline.map(|d| d.saturating_duration_since(Instant::now()));
978			self.read(left)?;
979		}
980	}
981
982	/// Processes input for up to `timeout` without returning it: acks
983	/// release waiting frames; keys and events wait for
984	/// [`next`](Self::next).
985	///
986	/// # Errors
987	/// [`Error::InputClosed`] at the end of input, or I/O.
988	pub fn pump(&mut self, timeout: Duration) -> Result<(), Error> {
989		let deadline = Instant::now() + timeout;
990		loop {
991			let left = deadline.saturating_duration_since(Instant::now());
992			if !self.read(Some(left))? && Instant::now() >= deadline {
993				return Ok(());
994			}
995		}
996	}
997
998	/// Reads once, waiting at most `timeout` but releasing undecided input
999	/// after [`FLUSH_AFTER`] of quiet. Whether any input was handled.
1000	fn read(&mut self, timeout: Option<Duration>) -> Result<bool, Error> {
1001		let held = self.parser.pending() || self.keys.pending();
1002		let flush_in = held.then(|| FLUSH_AFTER.saturating_sub(self.last_in.elapsed()));
1003		let wait = match (timeout, flush_in) {
1004			(Some(t), Some(f)) => Some(t.min(f)),
1005			(t, f) => t.or(f),
1006		};
1007		let mut buf = [0u8; 8192];
1008		match self.out.term.read(&mut buf, wait)? {
1009			Some(0) => Err(Error::InputClosed),
1010			Some(n) => {
1011				self.last_in = Instant::now();
1012				for item in self.parser.feed(&buf[..n]) {
1013					self.item(item)?;
1014				}
1015				Ok(true)
1016			},
1017			None => {
1018				if held && self.last_in.elapsed() >= FLUSH_AFTER {
1019					for item in self.parser.flush() {
1020						self.item(item)?;
1021					}
1022					for key in self.keys.flush() {
1023						self.inbox.push_back(Input::Key(key));
1024					}
1025					return Ok(true);
1026				}
1027				Ok(false)
1028			},
1029		}
1030	}
1031
1032	/// Handles one parsed input item.
1033	fn item(&mut self, item: Item) -> Result<(), Error> {
1034		match item {
1035			Item::Keys(bytes) => {
1036				for key in self.keys.feed(&bytes) {
1037					self.inbox.push_back(Input::Key(key));
1038				}
1039			},
1040			Item::Reply(reply) => {
1041				self.record_in("r", reply.raw().clone());
1042				self.replies.push_back(reply);
1043			},
1044			Item::Event(event) => {
1045				self.record_in("e", event.raw().clone());
1046				self.event(event)?;
1047			},
1048			Item::Da1 => {},
1049			Item::Osc5522(payload) => self.inbox.push_back(Input::Clipboard(payload)),
1050		}
1051		Ok(())
1052	}
1053
1054	/// Records an incoming message.
1055	fn record_in(&mut self, verb: &str, raw: serde_json::Map<String, Value>) {
1056		if let Some(record) = &mut self.out.record {
1057			record.log("in", verb, &[], Value::Object(raw));
1058		}
1059	}
1060
1061	/// The newest surface with id `sf`.
1062	fn by_id(&self, sf: &str) -> Option<usize> {
1063		self.surfaces.iter().rposition(|s| s.id == sf)
1064	}
1065
1066	/// Handles an event: state updates, flow control, then routing.
1067	fn event(&mut self, event: Event) -> Result<(), Error> {
1068		match &event {
1069			Event::Ack(ack) => {
1070				if let Some(i) = self.by_id(&ack.sf) {
1071					let credits = self.caps.credits;
1072					let state = &mut self.surfaces[i];
1073					state.acked = state.acked.max(ack.s);
1074					if state.open && !state.blocked(credits) {
1075						self.out.flush(state)?;
1076					}
1077				}
1078				return Ok(());
1079			},
1080			Event::Resize(r) => {
1081				self.caps.cols = r.cols;
1082				if r.cell.is_some() {
1083					self.caps.cell = r.cell;
1084				}
1085			},
1086			Event::Theme(t) => self.caps.dark = t.dark,
1087			Event::Motion(m) => self.caps.reduce_motion = m.reduce,
1088			Event::Gone(g) => {
1089				for state in &mut self.surfaces {
1090					if g.ids.contains(&state.id) && g.sf.as_ref().is_none_or(|sf| *sf == state.id) {
1091						state.open = false;
1092					}
1093				}
1094			},
1095			_ => {},
1096		}
1097		let msg = event
1098			.sf()
1099			.and_then(|sf| self.by_id(sf))
1100			.and_then(|i| self.surfaces[i].routes.route(&event));
1101		self.inbox.push_back(match msg {
1102			Some(msg) => Input::Msg(msg, event),
1103			None => Input::Event(event),
1104		});
1105		Ok(())
1106	}
1107
1108	/// Closes the session: open surfaces (each as its options say), the
1109	/// input modes, a short drain of late input, and the tty.
1110	///
1111	/// # Errors
1112	/// The first I/O error; every step still runs.
1113	pub fn close(mut self) -> Result<(), Error> {
1114		self.shutdown()
1115	}
1116
1117	/// Gives the tmux pane its title updates back, once TSP is over.
1118	fn release_tmux(&mut self) {
1119		if let Some(tmux) = self.tmux.take()
1120			&& !tmux.titles(true)
1121		{
1122			tracing::warn!(pane = %tmux.pane, "could not restore the tmux pane's allow-set-title");
1123		}
1124	}
1125
1126	/// The close sequence (idempotent).
1127	fn shutdown(&mut self) -> Result<(), Error> {
1128		if self.closed {
1129			return Ok(());
1130		}
1131		self.closed = true;
1132		let mut first: Option<Error> = None;
1133		let mut note = |r: Result<(), Error>| {
1134			if let Err(err) = r
1135				&& first.is_none()
1136			{
1137				first = Some(err);
1138			}
1139		};
1140		for i in 0..self.surfaces.len() {
1141			if self.surfaces[i].open {
1142				let keep = self.surfaces[i].keep;
1143				note(self.close_surface(Surface(i), keep));
1144			}
1145		}
1146		let reset = term::mode_reset(self.modes.0, self.modes.1);
1147		if !reset.is_empty() {
1148			note(self.out.term.write(reset).map_err(Error::from));
1149		}
1150		self.out.term.modes(false, false);
1151		let until = Instant::now() + DRAIN;
1152		let mut buf = [0u8; 4096];
1153		loop {
1154			let left = until.saturating_duration_since(Instant::now());
1155			if left.is_zero() {
1156				break;
1157			}
1158			match self.out.term.read(&mut buf, Some(left)) {
1159				Ok(Some(n)) if n > 0 => {},
1160				_ => break,
1161			}
1162		}
1163		self.release_tmux();
1164		note(self.out.term.restore().map_err(Error::from));
1165		first.map_or(Ok(()), Err)
1166	}
1167}
1168
1169impl<M> Drop for Session<M> {
1170	fn drop(&mut self) {
1171		if let Err(err) = self.shutdown() {
1172			tracing::warn!(%err, "closing the TSP session failed");
1173		}
1174	}
1175}
1176
1177/// The state of `sf`, when it is open.
1178fn open<M>(surfaces: &mut [State<M>], sf: Surface) -> Result<&mut State<M>, Error> {
1179	let state = &mut surfaces[sf.0];
1180	if state.open {
1181		Ok(state)
1182	} else {
1183		Err(Error::SurfaceClosed(state.id.clone()))
1184	}
1185}
1186
1187/// The executable's file name, without extension.
1188fn exe_name() -> Option<String> {
1189	let exe = std::env::current_exe().ok()?;
1190	Some(exe.file_stem()?.to_string_lossy().into_owned())
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195	use std::{cell::RefCell, collections::VecDeque, ffi::OsString, io, time::Duration};
1196
1197	use super::{Options, Session, TmuxPane};
1198	use crate::term::Terminal;
1199
1200	thread_local! {
1201		/// What the terminal was sent and which tmux commands ran, in order.
1202		static LOG: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
1203	}
1204
1205	fn log(line: String) {
1206		LOG.with(|log| log.borrow_mut().push(line));
1207	}
1208
1209	fn take_log() -> Vec<String> {
1210		LOG.with(|log| log.take())
1211	}
1212
1213	/// A tmux that agrees to every command.
1214	#[allow(clippy::unnecessary_wraps, reason = "a RunTmux")]
1215	fn tmux_ok(args: &[&str]) -> Option<String> {
1216		log(format!("tmux {}", args.join(" ")));
1217		Some(String::new())
1218	}
1219
1220	/// A tmux that fails every command.
1221	fn tmux_fails(args: &[&str]) -> Option<String> {
1222		log(format!("tmux {}", args.join(" ")));
1223		None
1224	}
1225
1226	/// A terminal that answers with `input`, chunk by chunk, and logs output.
1227	struct Script(VecDeque<&'static [u8]>);
1228
1229	impl Terminal for Script {
1230		fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
1231			log(format!("out {}", String::from_utf8_lossy(bytes)));
1232			Ok(())
1233		}
1234
1235		fn read(&mut self, buf: &mut [u8], _: Option<Duration>) -> io::Result<Option<usize>> {
1236			let Some(bytes) = self.0.pop_front() else {
1237				return Ok(None);
1238			};
1239			buf[..bytes.len()].copy_from_slice(bytes);
1240			Ok(Some(bytes.len()))
1241		}
1242	}
1243
1244	const HELLO: &[u8] = b"\x1b_tsp;r;{\"r\":\"hello\",\"v\":1,\"credits\":2}\x1b\\";
1245
1246	/// The pane `%3`, run by `run`.
1247	fn pane(run: super::RunTmux) -> TmuxPane {
1248		TmuxPane { pane: "%3".into(), run }
1249	}
1250
1251	/// An environment of `vars`.
1252	#[cfg(any(unix, windows))]
1253	fn env(vars: &'static [(&'static str, &'static str)]) -> impl Fn(&str) -> Option<OsString> {
1254		move |name| {
1255			vars
1256				.iter()
1257				.find(|(k, _)| *k == name)
1258				.map(|(_, v)| OsString::from(v))
1259		}
1260	}
1261
1262	#[cfg(any(unix, windows))]
1263	#[test]
1264	fn probe_follows_the_environment_and_the_tmux_option() {
1265		use super::{Probe, probe};
1266		let unreachable = |_: &[&str]| -> Option<String> { panic!("tmux ran") };
1267		assert_eq!(probe(env(&[]), unreachable), Some(Probe::Direct));
1268		assert_eq!(probe(env(&[("TMUX", "")]), unreachable), Some(Probe::Direct));
1269		let off: [&'static [(&str, &str)]; 4] = [
1270			&[("TERN_TSP", "0")],
1271			&[("STY", "1.pts")],
1272			&[("ZELLIJ", "0")],
1273			&[("TMUX", "/tmp/t,1,0"), ("TMUX_PANE", "%3"), ("TERN_TSP", "0")],
1274		];
1275		for vars in off {
1276			assert_eq!(probe(env(vars), unreachable), None, "{vars:?}");
1277		}
1278		let tmux = &[("TMUX", "/tmp/t,1,0"), ("TMUX_PANE", "%3")];
1279		let mut asked = Vec::new();
1280		let watched = probe(env(tmux), |args| {
1281			asked = args.iter().map(ToString::to_string).collect();
1282			Some("1\n".into())
1283		});
1284		assert_eq!(watched, Some(Probe::Tmux("%3".into())));
1285		assert_eq!(asked, ["show-options", "-qv", "@tern-tsp"]);
1286		assert_eq!(probe(env(tmux), |_| Some("\n".into())), None);
1287		assert_eq!(probe(env(tmux), |_| None), None);
1288		assert_eq!(probe(env(&[("TMUX", "/tmp/t,1,0")]), unreachable), None);
1289	}
1290
1291	#[test]
1292	fn tmux_sends_hello_alone_and_restores_titles_on_close() {
1293		take_log();
1294		let term = Script(VecDeque::from([&b"\x1b[?62;c"[..], HELLO]));
1295		let session = Session::<()>::start(term, Options::new().record(None), Some(pane(tmux_ok)))
1296			.unwrap()
1297			.expect("DA1 must not decide in tmux");
1298		session.close().unwrap();
1299		let log = take_log();
1300		assert_eq!(log[0], "tmux set-option -p -t %3 allow-set-title off");
1301		assert!(log[1].starts_with("out \x1b_tsp;q;"), "{log:?}");
1302		assert!(!log[1].contains("\x1b[c"), "{log:?}");
1303		assert_eq!(log.last().unwrap(), "tmux set-option -pu -t %3 allow-set-title");
1304	}
1305
1306	#[test]
1307	fn tmux_without_a_reply_restores_titles() {
1308		take_log();
1309		let options = Options::new()
1310			.record(None)
1311			.timeout(Duration::from_millis(20));
1312		let session = Session::<()>::start(Script(VecDeque::new()), options, Some(pane(tmux_ok)));
1313		assert!(session.unwrap().is_none());
1314		let log = take_log();
1315		assert_eq!(log.len(), 3, "{log:?}");
1316		assert_eq!(log[2], "tmux set-option -pu -t %3 allow-set-title");
1317	}
1318
1319	#[test]
1320	fn tmux_refusing_titles_off_means_no_tsp() {
1321		take_log();
1322		let session = Session::<()>::start(
1323			Script(VecDeque::from([HELLO])),
1324			Options::new().record(None),
1325			Some(pane(tmux_fails)),
1326		);
1327		assert!(session.unwrap().is_none());
1328		assert_eq!(take_log(), ["tmux set-option -p -t %3 allow-set-title off"]);
1329	}
1330}