use std::{
collections::{HashSet, VecDeque},
path::PathBuf,
time::{Duration, Instant},
};
use serde_json::Value;
#[cfg(any(unix, windows))]
use crate::term::Tty;
use crate::{
Error,
input::{Item, Parser},
keys::{Decoder, Key},
reconcile::{Doc, Routes},
record::Recorder,
term::{self, Terminal},
ui::View,
wire::{
Blob, Cell, Close, Encoder, Event, Frame, Message, Mode, Op, Open, Palette, Query, Reply,
RevealAt, ScrollBy, Sheet,
},
};
const FLUSH_AFTER: Duration = Duration::from_millis(30);
const DRAIN: Duration = Duration::from_millis(50);
const QUERY_TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Clone, Debug)]
pub struct Options {
pub app: Option<String>,
pub version: Option<String>,
pub features: Vec<String>,
pub timeout: Duration,
pub bracketed_paste: bool,
pub kitty_keyboard: bool,
pub record: Option<PathBuf>,
}
impl Default for Options {
fn default() -> Self {
Self {
app: None,
version: None,
features: Vec::new(),
timeout: Duration::from_secs(1),
bracketed_paste: true,
kitty_keyboard: true,
record: std::env::var_os("TERN_TSP_RECORD")
.filter(|v| !v.is_empty())
.map(PathBuf::from),
}
}
}
impl Options {
pub fn new() -> Self {
Self::default()
}
pub fn app(mut self, app: impl Into<String>) -> Self {
self.app = Some(app.into());
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn feature(mut self, feature: impl Into<String>) -> Self {
self.features.push(feature.into());
self
}
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub const fn bracketed_paste(mut self, on: bool) -> Self {
self.bracketed_paste = on;
self
}
pub const fn kitty_keyboard(mut self, on: bool) -> Self {
self.kitty_keyboard = on;
self
}
pub fn record(mut self, path: Option<PathBuf>) -> Self {
self.record = path;
self
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Capabilities {
pub term: String,
pub version: String,
pub kinds: Vec<String>,
pub features: Vec<String>,
pub apc: usize,
pub credits: u32,
pub cols: u32,
pub cell: Option<Cell>,
pub dark: bool,
pub reduce_motion: bool,
pub hour12: bool,
}
impl Capabilities {
pub fn has_kind(&self, kind: &str) -> bool {
self.kinds.iter().any(|k| k == kind)
}
pub fn has_feature(&self, feature: &str) -> bool {
self.features.iter().any(|f| f == feature)
}
}
#[derive(Clone, Debug)]
pub struct SurfaceOptions {
pub id: Option<String>,
pub mode: Mode,
pub title: Option<String>,
pub role: Option<String>,
pub listen: bool,
pub adopt: bool,
pub keep: bool,
}
impl Default for SurfaceOptions {
fn default() -> Self {
Self {
id: None,
mode: Mode::default(),
title: None,
role: None,
listen: true,
adopt: false,
keep: true,
}
}
}
impl SurfaceOptions {
pub fn new(mode: Mode) -> Self {
Self { mode, ..Self::default() }
}
pub fn inline() -> Self {
Self::new(Mode::Inline)
}
pub fn screen() -> Self {
Self::new(Mode::Screen)
}
pub fn flow() -> Self {
Self::new(Mode::Flow)
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn role(mut self, role: impl Into<String>) -> Self {
self.role = Some(role.into());
self
}
pub const fn listen(mut self, listen: bool) -> Self {
self.listen = listen;
self
}
pub const fn adopt(mut self, adopt: bool) -> Self {
self.adopt = adopt;
self
}
pub const fn keep(mut self, keep: bool) -> Self {
self.keep = keep;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Surface(usize);
#[derive(Debug)]
pub enum Input<M> {
Key(Key),
Event(Event),
Msg(M, Event),
}
struct State<M> {
id: String,
listen: bool,
keep: bool,
open: bool,
seq: u64,
acked: u64,
sent: Doc,
reset_main: bool,
pending: Option<Doc>,
queued: Vec<Op>,
routes: Routes<M>,
}
impl<M> State<M> {
const fn blocked(&self, credits: u32) -> bool {
self.listen && self.seq.saturating_sub(self.acked) >= credits as u64
}
}
struct Out {
term: Box<dyn Terminal>,
enc: Encoder,
record: Option<Recorder>,
}
impl Out {
fn send(&mut self, message: &Message) -> Result<(), Error> {
let bytes = self.enc.encode(message)?;
if let Some(record) = &mut self.record {
let body = match message {
Message::Blob(_) => Value::String(message.body()?),
_ => serde_json::from_str(&message.body()?)?,
};
record.log("out", message.verb(), &message.params(), body);
}
self.term.write(&bytes)?;
Ok(())
}
fn flush<M>(&mut self, state: &mut State<M>) -> Result<(), Error> {
let mut ops = state
.pending
.as_ref()
.map_or_else(Vec::new, |doc| state.sent.ops(doc, &state.id));
if state.reset_main && (state.pending.is_some() || !state.queued.is_empty()) {
ops.insert(0, Op::Del { id: "main".into() });
}
let queued_at = ops.len();
ops.append(&mut state.queued);
if !ops.is_empty() {
let mut message = Message::Frame(Frame { sf: state.id.clone(), s: state.seq + 1, ops });
if let Err(err) = self.send(&message) {
if let Message::Frame(frame) = &mut message {
state.queued = frame.ops.split_off(queued_at);
}
return Err(err);
}
state.seq += 1;
state.reset_main = false;
}
if let Some(doc) = state.pending.take() {
state.sent = doc;
}
Ok(())
}
}
pub struct Session<M = ()> {
out: Out,
parser: Parser,
keys: Decoder,
caps: Capabilities,
surfaces: Vec<State<M>>,
next_id: u32,
inbox: VecDeque<Input<M>>,
replies: VecDeque<Reply>,
blobs: HashSet<String>,
modes: (bool, bool),
last_in: Instant,
closed: bool,
}
impl<M> Session<M> {
#[cfg(any(unix, windows))]
pub fn connect(options: Options) -> Result<Option<Self>, Error> {
if !available() {
return Ok(None);
}
let tty = Tty::open()?;
Self::with_terminal(tty, options)
}
pub fn with_terminal(
term: impl Terminal + 'static,
options: Options,
) -> Result<Option<Self>, Error> {
let record = options.record.as_deref().map(Recorder::open).transpose()?;
let mut session = Self {
out: Out { term: Box::new(term), enc: Encoder::default(), record },
parser: Parser::new(),
keys: Decoder::new(),
caps: Capabilities::default(),
surfaces: Vec::new(),
next_id: 1,
inbox: VecDeque::new(),
replies: VecDeque::new(),
blobs: HashSet::new(),
modes: (false, false),
last_in: Instant::now(),
closed: false,
};
let app = options.app.clone().or_else(exe_name);
match session.handshake(app, &options) {
Ok(true) => {},
Ok(false) => {
session.closed = true;
session.out.term.restore()?;
return Ok(None);
},
Err(err) => {
session.closed = true;
let _ = session.out.term.restore();
return Err(err);
},
}
let (paste, kitty) = (options.bracketed_paste, options.kitty_keyboard);
let mut enable = Vec::new();
if paste {
enable.extend_from_slice(b"\x1b[?2004h");
}
if kitty {
enable.extend_from_slice(b"\x1b[>1u");
}
session.modes = (paste, kitty);
session.out.term.modes(paste, kitty);
if !enable.is_empty() {
session.out.term.write(&enable)?;
}
Ok(Some(session))
}
fn handshake(&mut self, app: Option<String>, options: &Options) -> Result<bool, Error> {
let hello =
Query::Hello { app, ver: options.version.clone(), features: options.features.clone() };
let mut bytes = self.out.enc.encode(&Message::Query(hello.clone()))?;
bytes.extend_from_slice(b"\x1b[c");
if let Some(record) = &mut self.out.record {
record.log("out", "q", &[], serde_json::to_value(&hello)?);
}
self.out.term.write(&bytes)?;
let deadline = Instant::now() + options.timeout;
let mut buf = [0u8; 4096];
loop {
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Ok(false);
}
let Some(n) = self.out.term.read(&mut buf, Some(left))? else {
continue;
};
if n == 0 {
return Ok(false);
}
self.last_in = Instant::now();
let mut accepted = false;
for item in self.parser.feed(&buf[..n]) {
match item {
Item::Reply(Reply::Hello(hello)) if !accepted => {
self.record_in("r", hello.raw.clone());
self.caps = Capabilities {
term: hello.term,
version: hello.ver,
kinds: hello.kinds,
features: hello.features,
apc: hello.apc,
credits: hello.credits,
cols: hello.cols,
cell: hello.cell,
dark: hello.dark,
reduce_motion: hello.reduce_motion,
hour12: hello.hour12,
};
self.out.enc.set_limit(self.caps.apc);
accepted = true;
},
Item::Da1 if !accepted => return Ok(false),
other => self.item(other)?,
}
}
if accepted {
return Ok(true);
}
}
}
pub const fn caps(&self) -> &Capabilities {
&self.caps
}
pub fn open(&mut self, options: SurfaceOptions) -> Result<Surface, Error> {
let id = options.id.clone().unwrap_or_else(|| {
let id = format!("s{}", self.next_id);
self.next_id += 1;
id
});
self.out.send(&Message::Open(Open {
id: id.clone(),
mode: Some(options.mode),
title: options.title,
role: options.role,
listen: (!options.listen).then_some(false),
adopt: options.adopt.then_some(true),
}))?;
let previous = options
.adopt
.then(|| {
self
.surfaces
.iter()
.rposition(|state| state.id == id && !state.open)
})
.flatten();
let (sent, seq) = previous.map_or_else(
|| (Doc::new(), 0),
|i| {
let state = &mut self.surfaces[i];
(std::mem::take(&mut state.sent), state.seq)
},
);
self.surfaces.push(State {
id,
listen: options.listen,
keep: options.keep,
open: true,
seq,
acked: seq,
sent,
reset_main: options.adopt && previous.is_none(),
pending: None,
queued: Vec::new(),
routes: Routes::default(),
});
Ok(Surface(self.surfaces.len() - 1))
}
pub fn surface_id(&self, sf: Surface) -> &str {
&self.surfaces[sf.0].id
}
pub fn is_open(&self, sf: Surface) -> bool {
self.surfaces[sf.0].open
}
pub fn render(&mut self, sf: Surface, view: impl Into<View<M>>) -> Result<(), Error> {
let (doc, routes) = Doc::build(view.into())?;
let credits = self.caps.credits;
let state = open(&mut self.surfaces, sf)?;
state.routes = routes;
state.pending = Some(doc);
if state.blocked(credits) {
return self.pump(Duration::ZERO);
}
self.out.flush(state)
}
pub fn send(&mut self, sf: Surface, ops: Vec<Op>) -> Result<(), Error> {
let credits = self.caps.credits;
let state = open(&mut self.surfaces, sf)?;
state.queued.extend(ops);
if state.blocked(credits) {
return Ok(());
}
self.out.flush(state)
}
pub fn focus(&mut self, sf: Surface, id: Option<&str>) -> Result<(), Error> {
self.send(sf, vec![Op::Focus { id: id.map(str::to_owned) }])
}
pub fn reveal(&mut self, sf: Surface, id: &str, at: RevealAt) -> Result<(), Error> {
self.send(sf, vec![Op::Reveal { id: id.to_owned(), at }])
}
pub fn scroll(&mut self, sf: Surface, id: &str, by: ScrollBy) -> Result<(), Error> {
self.send(sf, vec![Op::Scroll { id: id.to_owned(), by }])
}
pub fn settle(&mut self, sf: Surface, id: &str) -> Result<(), Error> {
self.send(sf, vec![Op::Settle { id: id.to_owned() }])
}
pub fn suspend(&mut self, sf: Surface) -> Result<(), Error> {
self.send(sf, vec![Op::Suspend])
}
pub fn resume(&mut self, sf: Surface) -> Result<(), Error> {
self.send(sf, vec![Op::Resume])
}
pub fn stylesheet(&mut self, sf: Surface, name: &str, css: Option<&str>) -> Result<(), Error> {
let id = open(&mut self.surfaces, sf)?.id.clone();
self.out.send(&Message::Sheet(Sheet {
sf: Some(id),
name: name.to_owned(),
css: css.map(str::to_owned),
}))
}
pub fn palette(&mut self, sf: Surface, palette: Palette) -> Result<(), Error> {
let id = open(&mut self.surfaces, sf)?.id.clone();
self
.out
.send(&Message::Palette(Palette { sf: Some(id), ..palette }))
}
pub fn close_surface(&mut self, sf: Surface, keep: bool) -> Result<(), Error> {
let state = open(&mut self.surfaces, sf)?;
let frame = self.out.flush(state);
let id = state.id.clone();
let close = self.out.send(&Message::Close(Close { id, keep }));
if close.is_ok() {
state.open = false;
state.pending = None;
state.queued.clear();
if keep {
state.sent.retain_main();
} else {
state.sent = Doc::new();
}
}
frame.and(close)
}
pub fn blob(&mut self, data: Vec<u8>, mime: Option<&str>) -> Result<String, Error> {
let blob = Blob::new(data, mime.map(str::to_owned))?;
let id = blob.id.clone();
if !self.blobs.contains(&id) {
self.out.send(&Message::Blob(blob))?;
self.blobs.insert(id.clone());
}
Ok(id)
}
pub fn blobs(&mut self, ids: Vec<String>) -> Result<Vec<String>, Error> {
self.out.send(&Message::Query(Query::Blobs { ids }))?;
let deadline = Instant::now() + QUERY_TIMEOUT;
loop {
if let Some(at) = self
.replies
.iter()
.position(|r| matches!(r, Reply::Blobs(_)))
&& let Some(Reply::Blobs(reply)) = self.replies.remove(at)
{
return Ok(reply.have);
}
let left = deadline.saturating_duration_since(Instant::now());
if left.is_zero() {
return Err(Error::Timeout("the blobs reply"));
}
self.read(Some(left))?;
}
}
pub fn next(&mut self, timeout: Option<Duration>) -> Result<Option<Input<M>>, Error> {
let deadline = timeout.map(|t| Instant::now() + t);
loop {
if let Some(input) = self.inbox.pop_front() {
return Ok(Some(input));
}
if deadline.is_some_and(|d| Instant::now() >= d) && !self.read(Some(Duration::ZERO))? {
return Ok(self.inbox.pop_front());
}
let left = deadline.map(|d| d.saturating_duration_since(Instant::now()));
self.read(left)?;
}
}
pub fn pump(&mut self, timeout: Duration) -> Result<(), Error> {
let deadline = Instant::now() + timeout;
loop {
let left = deadline.saturating_duration_since(Instant::now());
if !self.read(Some(left))? && Instant::now() >= deadline {
return Ok(());
}
}
}
fn read(&mut self, timeout: Option<Duration>) -> Result<bool, Error> {
let held = self.parser.pending() || self.keys.pending();
let flush_in = held.then(|| FLUSH_AFTER.saturating_sub(self.last_in.elapsed()));
let wait = match (timeout, flush_in) {
(Some(t), Some(f)) => Some(t.min(f)),
(t, f) => t.or(f),
};
let mut buf = [0u8; 8192];
match self.out.term.read(&mut buf, wait)? {
Some(0) => Err(Error::InputClosed),
Some(n) => {
self.last_in = Instant::now();
for item in self.parser.feed(&buf[..n]) {
self.item(item)?;
}
Ok(true)
},
None => {
if held && self.last_in.elapsed() >= FLUSH_AFTER {
for item in self.parser.flush() {
self.item(item)?;
}
for key in self.keys.flush() {
self.inbox.push_back(Input::Key(key));
}
return Ok(true);
}
Ok(false)
},
}
}
fn item(&mut self, item: Item) -> Result<(), Error> {
match item {
Item::Keys(bytes) => {
for key in self.keys.feed(&bytes) {
self.inbox.push_back(Input::Key(key));
}
},
Item::Reply(reply) => {
self.record_in("r", reply.raw().clone());
self.replies.push_back(reply);
},
Item::Event(event) => {
self.record_in("e", event.raw().clone());
self.event(event)?;
},
Item::Da1 => {},
}
Ok(())
}
fn record_in(&mut self, verb: &str, raw: serde_json::Map<String, Value>) {
if let Some(record) = &mut self.out.record {
record.log("in", verb, &[], Value::Object(raw));
}
}
fn by_id(&self, sf: &str) -> Option<usize> {
self.surfaces.iter().rposition(|s| s.id == sf)
}
fn event(&mut self, event: Event) -> Result<(), Error> {
match &event {
Event::Ack(ack) => {
if let Some(i) = self.by_id(&ack.sf) {
let credits = self.caps.credits;
let state = &mut self.surfaces[i];
state.acked = state.acked.max(ack.s);
if state.open && !state.blocked(credits) {
self.out.flush(state)?;
}
}
return Ok(());
},
Event::Resize(r) => {
self.caps.cols = r.cols;
if r.cell.is_some() {
self.caps.cell = r.cell;
}
},
Event::Theme(t) => self.caps.dark = t.dark,
Event::Motion(m) => self.caps.reduce_motion = m.reduce,
Event::Gone(g) => {
for state in &mut self.surfaces {
if g.ids.contains(&state.id) && g.sf.as_ref().is_none_or(|sf| *sf == state.id) {
state.open = false;
}
}
},
_ => {},
}
let msg = event
.sf()
.and_then(|sf| self.by_id(sf))
.and_then(|i| self.surfaces[i].routes.route(&event));
self.inbox.push_back(match msg {
Some(msg) => Input::Msg(msg, event),
None => Input::Event(event),
});
Ok(())
}
pub fn close(mut self) -> Result<(), Error> {
self.shutdown()
}
fn shutdown(&mut self) -> Result<(), Error> {
if self.closed {
return Ok(());
}
self.closed = true;
let mut first: Option<Error> = None;
let mut note = |r: Result<(), Error>| {
if let Err(err) = r
&& first.is_none()
{
first = Some(err);
}
};
for i in 0..self.surfaces.len() {
if self.surfaces[i].open {
let keep = self.surfaces[i].keep;
note(self.close_surface(Surface(i), keep));
}
}
let reset = term::mode_reset(self.modes.0, self.modes.1);
if !reset.is_empty() {
note(self.out.term.write(reset).map_err(Error::from));
}
self.out.term.modes(false, false);
let until = Instant::now() + DRAIN;
let mut buf = [0u8; 4096];
loop {
let left = until.saturating_duration_since(Instant::now());
if left.is_zero() {
break;
}
match self.out.term.read(&mut buf, Some(left)) {
Ok(Some(n)) if n > 0 => {},
_ => break,
}
}
note(self.out.term.restore().map_err(Error::from));
first.map_or(Ok(()), Err)
}
}
impl<M> Drop for Session<M> {
fn drop(&mut self) {
if let Err(err) = self.shutdown() {
tracing::warn!(%err, "closing the TSP session failed");
}
}
}
fn open<M>(surfaces: &mut [State<M>], sf: Surface) -> Result<&mut State<M>, Error> {
let state = &mut surfaces[sf.0];
if state.open {
Ok(state)
} else {
Err(Error::SurfaceClosed(state.id.clone()))
}
}
#[cfg(any(unix, windows))]
fn available() -> bool {
let set = |name: &str| std::env::var_os(name).is_some_and(|v| !v.is_empty());
if std::env::var_os("TERN_TSP").is_some_and(|v| v == "0") {
return false;
}
if set("TMUX") || set("STY") || set("ZELLIJ") {
return false;
}
term::is_tty()
}
fn exe_name() -> Option<String> {
let exe = std::env::current_exe().ok()?;
Some(exe.file_stem()?.to_string_lossy().into_owned())
}