1use 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
64const FLUSH_AFTER: Duration = Duration::from_millis(30);
66const DRAIN: Duration = Duration::from_millis(50);
68const QUERY_TIMEOUT: Duration = Duration::from_secs(2);
70#[cfg(any(unix, windows))]
72const TMUX_TIMEOUT: Duration = Duration::from_millis(500);
73
74#[derive(Clone, Debug)]
76pub struct Options {
77 pub app: Option<String>,
79 pub version: Option<String>,
81 pub features: Vec<String>,
83 pub timeout: Duration,
85 pub bracketed_paste: bool,
87 pub kitty_keyboard: bool,
89 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 pub fn new() -> Self {
112 Self::default()
113 }
114
115 pub fn app(mut self, app: impl Into<String>) -> Self {
117 self.app = Some(app.into());
118 self
119 }
120
121 pub fn version(mut self, version: impl Into<String>) -> Self {
123 self.version = Some(version.into());
124 self
125 }
126
127 pub fn feature(mut self, feature: impl Into<String>) -> Self {
129 self.features.push(feature.into());
130 self
131 }
132
133 pub const fn timeout(mut self, timeout: Duration) -> Self {
135 self.timeout = timeout;
136 self
137 }
138
139 pub const fn bracketed_paste(mut self, on: bool) -> Self {
141 self.bracketed_paste = on;
142 self
143 }
144
145 pub const fn kitty_keyboard(mut self, on: bool) -> Self {
147 self.kitty_keyboard = on;
148 self
149 }
150
151 pub fn record(mut self, path: Option<PathBuf>) -> Self {
153 self.record = path;
154 self
155 }
156}
157
158#[derive(Clone, Debug, Default, PartialEq)]
161pub struct Capabilities {
162 pub term: String,
164 pub version: String,
166 pub kinds: Vec<String>,
168 pub features: Vec<String>,
170 pub apc: usize,
172 pub credits: u32,
174 pub cols: u32,
176 pub cell: Option<Cell>,
178 pub dark: bool,
180 pub reduce_motion: bool,
182 pub hour12: bool,
185}
186
187impl Capabilities {
188 pub fn has_kind(&self, kind: &str) -> bool {
190 self.kinds.iter().any(|k| k == kind)
191 }
192
193 pub fn has_feature(&self, feature: &str) -> bool {
195 self.features.iter().any(|f| f == feature)
196 }
197}
198
199#[derive(Clone, Debug)]
201pub struct SurfaceOptions {
202 pub id: Option<String>,
204 pub mode: Mode,
206 pub title: Option<String>,
208 pub role: Option<String>,
210 pub listen: bool,
212 pub adopt: bool,
214 pub keep: bool,
216 pub closable: bool,
219 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 pub fn new(mode: Mode) -> Self {
242 Self { mode, ..Self::default() }
243 }
244
245 pub fn inline() -> Self {
247 Self::new(Mode::Inline)
248 }
249
250 pub fn screen() -> Self {
252 Self::new(Mode::Screen)
253 }
254
255 pub fn flow() -> Self {
257 Self::new(Mode::Flow)
258 }
259
260 pub fn id(mut self, id: impl Into<String>) -> Self {
262 self.id = Some(id.into());
263 self
264 }
265
266 pub fn title(mut self, title: impl Into<String>) -> Self {
268 self.title = Some(title.into());
269 self
270 }
271
272 pub fn role(mut self, role: impl Into<String>) -> Self {
274 self.role = Some(role.into());
275 self
276 }
277
278 pub const fn listen(mut self, listen: bool) -> Self {
280 self.listen = listen;
281 self
282 }
283
284 pub const fn adopt(mut self, adopt: bool) -> Self {
286 self.adopt = adopt;
287 self
288 }
289
290 pub const fn keep(mut self, keep: bool) -> Self {
292 self.keep = keep;
293 self
294 }
295
296 pub const fn closable(mut self, closable: bool) -> Self {
299 self.closable = closable;
300 self
301 }
302
303 pub const fn font(mut self, font: Font) -> Self {
306 self.font = Some(font);
307 self
308 }
309}
310
311#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
314pub struct Surface(usize);
315
316#[derive(Debug)]
318pub enum Input<M> {
319 Key(Key),
321 Event(Event),
323 Msg(M, Event),
325 Clipboard(Vec<u8>),
328}
329
330struct State<M> {
332 id: String,
334 listen: bool,
336 keep: bool,
338 open: bool,
340 seq: u64,
342 acked: u64,
344 sent: Doc,
346 reset_main: bool,
349 pending: Option<Doc>,
351 queued: Vec<Op>,
353 routes: Routes<M>,
355}
356
357impl<M> State<M> {
358 const fn blocked(&self, credits: u32) -> bool {
360 self.listen && self.seq.saturating_sub(self.acked) >= credits as u64
361 }
362}
363
364struct Out {
366 term: Box<dyn Terminal>,
368 enc: Encoder,
370 record: Option<Recorder>,
372}
373
374impl Out {
375 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 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
419pub struct Session<M = ()> {
422 out: Out,
424 parser: Parser,
426 keys: Decoder,
428 caps: Capabilities,
430 surfaces: Vec<State<M>>,
432 next_id: u32,
434 inbox: VecDeque<Input<M>>,
436 replies: VecDeque<Reply>,
438 blobs: HashSet<String>,
440 modes: (bool, bool),
442 last_in: Instant,
444 waker: Option<Waker>,
446 tmux: Option<TmuxPane>,
448 closed: bool,
450}
451
452type RunTmux = fn(&[&str]) -> Option<String>;
454
455struct TmuxPane {
457 pane: String,
459 run: RunTmux,
461}
462
463impl TmuxPane {
464 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#[cfg(any(unix, windows))]
478#[derive(Clone, Debug, PartialEq, Eq)]
479enum Probe {
480 Direct,
482 Tmux(String),
484}
485
486#[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#[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 #[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 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 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 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 pub const fn caps(&self) -> &Capabilities {
709 &self.caps
710 }
711
712 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 pub fn surface_id(&self, sf: Surface) -> &str {
769 &self.surfaces[sf.0].id
770 }
771
772 pub fn is_open(&self, sf: Surface) -> bool {
777 self.surfaces[sf.0].open
778 }
779
780 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 return self.pump(Duration::ZERO);
796 }
797 self.out.flush(state)
798 }
799
800 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 pub fn focus(&mut self, sf: Surface, id: Option<&str>) -> Result<(), Error> {
820 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 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 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 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 pub fn suspend(&mut self, sf: Surface) -> Result<(), Error> {
857 self.send(sf, vec![Op::Suspend])
858 }
859
860 pub fn resume(&mut self, sf: Surface) -> Result<(), Error> {
865 self.send(sf, vec![Op::Resume])
866 }
867
868 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 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 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 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 pub fn waker(&self) -> Option<Waker> {
946 self.waker.clone()
947 }
948
949 pub fn write_raw(&mut self, bytes: &[u8]) -> Result<(), Error> {
955 self.out.term.write(bytes)?;
956 Ok(())
957 }
958
959 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 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 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 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 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 fn by_id(&self, sf: &str) -> Option<usize> {
1063 self.surfaces.iter().rposition(|s| s.id == sf)
1064 }
1065
1066 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 pub fn close(mut self) -> Result<(), Error> {
1114 self.shutdown()
1115 }
1116
1117 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 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
1177fn 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
1187fn 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 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 #[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 fn tmux_fails(args: &[&str]) -> Option<String> {
1222 log(format!("tmux {}", args.join(" ")));
1223 None
1224 }
1225
1226 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 fn pane(run: super::RunTmux) -> TmuxPane {
1248 TmuxPane { pane: "%3".into(), run }
1249 }
1250
1251 #[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}