1use std::collections::VecDeque;
5use std::fs::File;
6use std::io::{BufWriter, Write};
7use std::path::{Path, PathBuf};
8use std::time::{Duration, Instant};
9
10use anyhow::{Context, Result};
11
12use crate::ansi::{TermEvent, Terminal};
13use crate::autologin::{AutoLogin, Step};
14use crate::dialer::Entry;
15use crate::screen::Screen;
16use crate::telnet::Telnet;
17use crate::transfer::{
18 self, Download, Kermit, Outcome, TrailerFilter, Transfer, Upload, XyProtocol, XyReceive, XySend,
19};
20pub use crate::transport::{AuthPrompt, ConnectOptions, NetEvent, PromptKind, Writer, spawn_connection};
21
22const PROMPT_QUIET: Duration = Duration::from_millis(250);
25
26const BUSY_WINDOW: Duration = Duration::from_secs(30);
28
29fn says_busy(screen: &Screen) -> bool {
31 const WORDS: [&str; 6] = ["busy", "no free node", "all nodes", "all lines", "try again later", "too many users"];
32 (0..screen.rows()).any(|y| {
33 let line = Screen::row_text(screen.row(y)).to_ascii_lowercase();
34 WORDS.iter().any(|w| line.contains(w))
35 })
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum State {
40 Connecting,
41 Connected,
42 Closed,
43}
44
45#[derive(Clone, Debug)]
47pub struct SessionConfig {
48 pub scrollback: usize,
49 pub known_hosts: PathBuf,
50 pub download_dir: PathBuf,
51 pub ssh_agent: bool,
53 pub ssh_key_dir: Option<PathBuf>,
55 pub modem: crate::modem::ModemConfig,
57}
58
59#[derive(Clone, Debug, PartialEq)]
61pub enum SessionEvent {
62 Connected,
64 Term(TermEvent),
65 Notice(String),
66 UploadRequested,
70 AuthPrompt,
73 TransferStarted,
74 TransferFinished(Outcome),
75}
76
77pub struct Session {
78 pub entry: Entry,
79 pub term: Terminal,
80 pub state: State,
81 pub error: Option<String>,
83 config: SessionConfig,
84 telnet: Option<Telnet>,
85 writer: Option<Writer>,
86 capture: Option<Capture>,
87 transfer: Option<Transfer>,
88 rip_shows: bool,
90 skypix_load: Option<u32>,
92 upload_wait: Option<Vec<u8>>,
95 auth_prompt: Option<AuthPrompt>,
96 login: AutoLogin,
97 quiet_since: Option<Instant>,
99 paced: VecDeque<u8>,
102 pace_credit: f64,
103 pace_at: Instant,
104 scan_tail: Vec<u8>,
107 trailer: Option<TrailerFilter>,
109 started: Instant,
110 connected_at: Option<Instant>,
111 events: Vec<SessionEvent>,
112 pub tap: Option<Vec<u8>>,
114}
115
116impl Session {
117 pub fn new(entry: Entry, cols: usize, rows: usize, config: SessionConfig) -> Session {
118 let login = AutoLogin::new(&entry);
119 let mut term = Terminal::new(cols, rows, config.scrollback);
120 term.encoding = entry.encoding;
121 term.screen.base_font = entry.font;
122 term.screen.base_ice = entry.ice.unwrap_or(false);
123 term.screen.ice_colors = term.screen.base_ice;
124 term.screen.look.fonts[0] = entry.font;
125 term.start_emulation();
127 let telnet = entry.protocol.is_telnet().then(|| Telnet::new(&entry.term_type, cols as u16, rows as u16));
128 Session {
129 entry,
130 term,
131 state: State::Connecting,
132 error: None,
133 config,
134 telnet,
135 writer: None,
136 capture: None,
137 transfer: None,
138 rip_shows: false,
139 skypix_load: None,
140 upload_wait: None,
141 auth_prompt: None,
142 login,
143 quiet_since: None,
144 paced: VecDeque::new(),
145 pace_credit: 0.0,
146 pace_at: Instant::now(),
147 scan_tail: Vec::new(),
148 trailer: None,
149 started: Instant::now(),
150 connected_at: None,
151 events: Vec::new(),
152 tap: None,
153 }
154 }
155
156 pub fn connect<F>(&self, sink: F)
159 where
160 F: Fn(NetEvent) + Send + 'static,
161 {
162 let opts = ConnectOptions {
163 cols: self.term.screen.cols() as u16,
164 rows: self.term.screen.rows() as u16,
165 known_hosts: self.config.known_hosts.clone(),
166 ssh_agent: self.config.ssh_agent,
167 ssh_key_dir: self.config.ssh_key_dir.clone(),
168 modem: self.config.modem.clone(),
169 };
170 spawn_connection(&self.entry, &opts, sink);
171 }
172
173 pub fn handle(&mut self, ev: NetEvent) {
174 match ev {
175 NetEvent::Connected(w) => {
176 self.writer = Some(w);
177 self.state = State::Connected;
178 self.connected_at = Some(Instant::now());
179 self.login.connected();
180 self.events.push(SessionEvent::Connected);
181 }
182 NetEvent::Data(bytes) => self.receive(&bytes),
183 NetEvent::Notice(msg) => self.events.push(SessionEvent::Notice(msg)),
184 NetEvent::Prompt(p) => {
185 self.auth_prompt = Some(p);
186 self.events.push(SessionEvent::AuthPrompt);
187 }
188 NetEvent::Closed(err) => {
189 self.auth_prompt = None;
190 self.state = State::Closed;
191 self.error = err;
192 self.writer = None;
193 self.end_transfer(Some(Outcome::Failed("the connection closed".into())));
194 }
195 }
196 }
197
198 fn receive(&mut self, bytes: &[u8]) {
199 let data = match &mut self.telnet {
200 Some(t) => {
201 let (mut data, mut reply) = (Vec::with_capacity(bytes.len()), Vec::new());
202 t.receive(bytes, &mut data, &mut reply);
203 if !reply.is_empty() {
204 self.write_raw(&reply);
205 }
206 data
207 }
208 None => bytes.to_vec(),
209 };
210 if self.entry.baud == 0 || self.transfer.is_some() && self.paced.is_empty() {
212 self.process(data);
213 self.quiet_since = Some(Instant::now());
214 } else {
215 if self.paced.is_empty() {
216 self.pace_at = Instant::now();
217 }
218 self.paced.extend(data);
219 self.pump();
220 }
221 }
222
223 pub fn pacing(&self) -> bool {
226 !self.paced.is_empty()
227 }
228
229 pub fn pump(&mut self) {
232 if self.paced.is_empty() {
233 return;
234 }
235 let now = Instant::now();
236 self.pace_credit += now.duration_since(self.pace_at).as_secs_f64() * self.entry.baud as f64 / 10.0;
237 self.pace_at = now;
238 let n = (self.pace_credit as usize).min(self.paced.len());
239 if n == 0 {
240 return;
241 }
242 self.pace_credit -= n as f64;
243 let chunk: Vec<u8> = self.paced.drain(..n).collect();
244 self.process(chunk);
245 if self.transfer.is_some() {
247 let rest: Vec<u8> = self.paced.drain(..).collect();
248 self.process(rest);
249 }
250 if self.paced.is_empty() {
251 self.pace_credit = 0.0;
252 }
253 self.quiet_since = Some(now);
254 }
255
256 fn check_login_prompt(&mut self) {
259 let Some(t) = self.quiet_since else { return };
260 if t.elapsed() < PROMPT_QUIET || self.transfer.is_some() || self.upload_wait.is_some() {
261 return;
262 }
263 self.quiet_since = None;
264 let screen = &self.term.screen;
265 let (x, y) = screen.cursor();
266 let row = screen.row(y);
267 let prompt = match self.term.text_codepage().filter(|cp| cp.screen_codes()) {
269 Some(cp) => row[..x.min(row.len())].iter().map(|c| cp.decode(c.ch)).collect(),
270 None => Screen::row_text(&row[..x.min(row.len())]),
271 };
272 if let Some(step) = self.login.answer(&prompt) {
273 self.send_login_step(step);
274 }
275 }
276
277 pub fn rip_press(&mut self, x: usize, y: usize) -> bool {
280 let out = self.term.rip_press(x, y);
281 if let Some(text) = &out {
282 self.send(text);
283 }
284 self.events.extend(self.term.take_events().into_iter().map(SessionEvent::Term));
285 out.is_some()
286 }
287
288 pub fn answer_rip(&mut self, answer: Option<String>) {
291 self.term.rip_answer(answer);
292 let replies = self.term.take_replies();
293 if !replies.is_empty() {
294 self.send(&replies);
295 }
296 self.events.extend(self.term.take_events().into_iter().map(SessionEvent::Term));
297 }
298
299 pub fn send_login(&mut self) -> bool {
302 match self.login.next_step() {
303 Some(step) => {
304 self.send_login_step(step);
305 true
306 }
307 None => false,
308 }
309 }
310
311 pub fn has_login(&self) -> bool {
313 self.login.has_login()
314 }
315
316 fn send_login_step(&mut self, step: Step) {
317 let (text, what) = match step {
318 Step::User(u) => (u, "user name"),
319 Step::Password(p) => (p, "password"),
320 };
321 let bytes = self.term.encoding.encode_text(&format!("{text}\r"));
322 self.send(&bytes);
323 self.events.push(SessionEvent::Notice(format!("Sent your {what}")));
324 }
325
326 fn process(&mut self, mut data: Vec<u8>) {
329 if let Some(f) = &mut self.trailer
330 && !f.strip(&mut data)
331 {
332 self.trailer = None;
333 }
334 if let Some(t) = &mut self.transfer {
335 let mut out = Vec::new();
336 t.feed(&data, &mut out);
337 self.send(&out);
338 self.end_transfer(None);
339 return;
340 }
341 if let Some(buf) = &mut self.upload_wait {
342 buf.extend_from_slice(&data);
343 return;
344 }
345
346 let mut scan = std::mem::take(&mut self.scan_tail);
347 let already_shown = scan.len();
348 scan.extend_from_slice(&data);
349 if let Some(i) = transfer::find(&scan, transfer::ZRQINIT) {
350 self.show(&scan[already_shown.min(i)..i]);
351 match Download::new(self.config.download_dir.clone()) {
352 Ok(d) => {
353 self.transfer = Some(Transfer::Download(d));
354 self.events.push(SessionEvent::TransferStarted);
355 self.process(scan[i..].to_vec());
356 }
357 Err(e) => self.events.push(SessionEvent::Notice(format!("Cannot start download: {e:#}"))),
358 }
359 return;
360 }
361 if let Some(i) = transfer::find(&scan, transfer::ZRINIT) {
362 self.show(&scan[already_shown.min(i)..i]);
363 self.upload_wait = Some(scan[i..].to_vec());
364 self.events.push(SessionEvent::UploadRequested);
365 return;
366 }
367 if let Some(i) = crate::kermit::find_send_init(&scan) {
369 self.show(&scan[already_shown.min(i)..i]);
370 self.transfer = Some(Transfer::Kermit(Kermit::receive(self.config.download_dir.clone())));
371 self.events.push(SessionEvent::TransferStarted);
372 self.process(scan[i..].to_vec());
373 return;
374 }
375 let keep = scan.len().min(transfer::ZRQINIT.len() - 1);
376 self.scan_tail = scan[scan.len() - keep..].to_vec();
377 self.show(&data);
378 }
379
380 fn show(&mut self, data: &[u8]) {
381 if data.is_empty() {
382 return;
383 }
384 if let Some(c) = &mut self.capture {
385 c.write(data, self.term.encoding);
386 }
387 if let Some(t) = &mut self.tap {
388 t.extend_from_slice(data);
389 }
390 self.term.feed(data);
391 let replies = self.term.take_replies();
392 if !replies.is_empty() {
393 self.send(&replies);
394 }
395 self.events.extend(self.term.take_events().into_iter().map(SessionEvent::Term));
396 if let Some((block, rest)) = self.term.take_rip_block() {
397 self.start_rip_block(&block);
398 self.process(rest);
399 }
400 if let Some((t, rest)) = self.term.take_skypix_transfer() {
401 let dir = match &self.term.syncterm.cache_dir {
403 Some(c) => c.join("SkyPix"),
404 None => self.config.download_dir.clone(),
405 };
406 let name = transfer::sanitize_file_name(t.name.as_bytes());
407 let started = std::fs::create_dir_all(&dir)
408 .map_err(anyhow::Error::from)
409 .and_then(|()| self.start_xy_download_to(XyProtocol::XmodemCrc, dir, Some(name)));
410 match started {
411 Ok(()) => self.skypix_load = Some(t.mode),
412 Err(e) => {
413 self.send(transfer::CANCEL);
414 self.events.push(SessionEvent::Notice(format!("SkyPix transfer: {e:#}")));
415 }
416 }
417 self.process(rest);
418 }
419 }
420
421 fn start_rip_block(&mut self, b: &crate::rip::BlockTransfer) {
424 let dir = match &self.term.syncterm.cache_dir {
425 Some(c) => c.join("RIP"),
426 None => self.config.download_dir.clone(),
427 };
428 let xy = match b.protocol {
429 0 => Some(XyProtocol::Xmodem),
430 1 => Some(XyProtocol::XmodemCrc),
431 2 | 3 => Some(XyProtocol::Xmodem1k),
432 5 => Some(XyProtocol::Ymodem),
433 6 => Some(XyProtocol::YmodemG),
434 _ => None,
435 };
436 let started: Result<()> = (|| {
437 if b.upload {
438 let path = b.name.as_ref().map(|n| dir.join(n)).filter(|p| p.is_file());
440 let Some(path) = path else { anyhow::bail!("the BBS asked for a file that isn't here") };
441 match (b.protocol, xy) {
442 (_, Some(p)) => self.start_xy_upload(p, vec![path]),
443 (4, _) => self.start_kermit_upload(vec![path]),
444 _ => {
445 self.transfer = Some(Transfer::Upload(Upload::new(vec![path])?));
446 self.events.push(SessionEvent::TransferStarted);
447 Ok(())
448 }
449 }
450 } else {
451 std::fs::create_dir_all(&dir)?;
452 match (b.protocol, xy) {
453 (_, Some(p)) => self.start_xy_download_to(p, dir.clone(), b.name.clone()),
454 (4, _) => {
455 self.transfer = Some(Transfer::Kermit(Kermit::receive(dir.clone())));
456 self.events.push(SessionEvent::TransferStarted);
457 Ok(())
458 }
459 _ => {
460 self.transfer = Some(Transfer::Download(Download::new(dir.clone())?));
461 self.events.push(SessionEvent::TransferStarted);
462 Ok(())
463 }
464 }
465 }
466 })();
467 match started {
468 Ok(()) => self.rip_shows = !b.upload && b.shows(),
469 Err(e) => {
470 self.send(&[0x18; 10]);
472 self.events.push(SessionEvent::Notice(format!("RIP file transfer: {e:#}")));
473 }
474 }
475 }
476
477 fn end_transfer(&mut self, force: Option<Outcome>) {
480 let done = self.transfer.as_ref().is_some_and(Transfer::is_finished);
481 if !done && force.is_none() {
482 return;
483 }
484 let Some(t) = self.transfer.take() else { return };
485 let (outcome, leftover) = t.finish();
486 let outcome = if done { outcome } else { force.unwrap_or(outcome) };
487 let show: Vec<PathBuf> = match &outcome {
489 Outcome::Completed(files) if std::mem::take(&mut self.rip_shows) => {
490 files.iter().filter(|f| f.extension().is_some_and(|e| e.eq_ignore_ascii_case("rip"))).cloned().collect()
491 }
492 _ => Vec::new(),
493 };
494 self.rip_shows = false;
495 if let (Some(mode), Outcome::Completed(files)) = (self.skypix_load.take(), &outcome)
497 && let Some(data) = files.first().and_then(|f| std::fs::read(f).ok())
498 && let Some(sky) = self.term.skypix_mut()
499 {
500 sky.load(mode, &data);
501 }
502 self.events.push(SessionEvent::TransferFinished(outcome));
503 self.scan_tail.clear();
504 self.trailer = Some(TrailerFilter::new());
505 for f in show {
506 match std::fs::read(&f) {
507 Ok(data) => self.show(&data),
508 Err(e) => log::warn!("showing {}: {e}", f.display()),
509 }
510 }
511 self.process(leftover);
512 }
513
514 pub fn take_events(&mut self) -> Vec<SessionEvent> {
515 std::mem::take(&mut self.events)
516 }
517
518 pub fn transfer(&self) -> Option<&Transfer> {
519 self.transfer.as_ref()
520 }
521
522 pub fn auth_prompt(&self) -> Option<&AuthPrompt> {
524 self.auth_prompt.as_ref()
525 }
526
527 pub fn answer_prompt(&mut self, answer: Option<String>) {
529 if let Some(p) = self.auth_prompt.take() {
530 p.answer(answer);
531 }
532 }
533
534 pub fn upload_requested(&self) -> bool {
536 self.upload_wait.is_some()
537 }
538
539 pub fn start_upload(&mut self, paths: Vec<PathBuf>) -> Result<()> {
542 if let Some(t) = &mut self.transfer {
543 for p in paths {
544 t.enqueue(p)?;
545 }
546 return Ok(());
547 }
548 let up = Upload::new(paths)?;
549 self.transfer = Some(Transfer::Upload(up));
550 self.events.push(SessionEvent::TransferStarted);
551 let waiting = self.upload_wait.take().unwrap_or_default();
553 self.process(waiting);
554 Ok(())
555 }
556
557 pub fn start_xy_upload(&mut self, proto: XyProtocol, paths: Vec<PathBuf>) -> Result<()> {
559 if self.transfer.is_some() {
560 anyhow::bail!("a transfer is already in progress");
561 }
562 let up = XySend::new(proto, paths)?;
563 self.upload_wait = None;
564 self.transfer = Some(Transfer::XyUpload(up));
565 self.events.push(SessionEvent::TransferStarted);
566 Ok(())
567 }
568
569 pub fn start_xy_download(&mut self, proto: XyProtocol, name: Option<String>) -> Result<()> {
572 self.start_xy_download_to(proto, self.config.download_dir.clone(), name)
573 }
574
575 fn start_xy_download_to(&mut self, proto: XyProtocol, dir: PathBuf, name: Option<String>) -> Result<()> {
576 if self.transfer.is_some() {
577 anyhow::bail!("a transfer is already in progress");
578 }
579 let mut t = Transfer::XyDownload(XyReceive::new(proto, dir, name));
580 let mut out = Vec::new();
581 t.tick(Instant::now(), &mut out);
582 self.transfer = Some(t);
583 self.send(&out);
584 self.events.push(SessionEvent::TransferStarted);
585 Ok(())
586 }
587
588 pub fn start_kermit_upload(&mut self, paths: Vec<PathBuf>) -> Result<()> {
590 if self.transfer.is_some() {
591 anyhow::bail!("a transfer is already in progress");
592 }
593 let mut t = Transfer::Kermit(Kermit::send(paths)?);
594 let mut out = Vec::new();
595 t.start(&mut out);
596 self.upload_wait = None;
597 self.transfer = Some(t);
598 self.send(&out);
599 self.events.push(SessionEvent::TransferStarted);
600 Ok(())
601 }
602
603 pub fn start_kermit_download(&mut self) -> Result<()> {
606 if self.transfer.is_some() {
607 anyhow::bail!("a transfer is already in progress");
608 }
609 self.transfer = Some(Transfer::Kermit(Kermit::receive(self.config.download_dir.clone())));
610 self.events.push(SessionEvent::TransferStarted);
611 Ok(())
612 }
613
614 pub fn decline_upload(&mut self) {
616 if self.upload_wait.take().is_some() {
617 self.send(transfer::CANCEL);
618 }
619 }
620
621 pub fn cancel_transfer(&mut self) {
622 if let Some(t) = &mut self.transfer {
623 let mut out = Vec::new();
624 t.cancel(&mut out);
625 self.send(&out);
626 self.end_transfer(None);
627 }
628 }
629
630 pub fn tick(&mut self) {
632 self.pump();
633 self.term.poll(Instant::now());
635 let replies = self.term.take_replies();
636 if !replies.is_empty() {
637 self.send(&replies);
638 }
639 self.events.extend(self.term.take_events().into_iter().map(SessionEvent::Term));
640 if let Some(t) = &mut self.transfer {
641 let mut out = Vec::new();
642 t.tick(Instant::now(), &mut out);
643 self.send(&out);
644 self.end_transfer(None);
645 }
646 self.check_login_prompt();
647 }
648
649 pub fn next_tick(&self) -> Option<Instant> {
651 self.term.next_poll()
652 }
653
654 pub fn send(&mut self, data: &[u8]) {
656 if data.is_empty() {
657 return;
658 }
659 let encoded = match &self.telnet {
660 Some(t) => t.encode(data),
661 None => data.to_vec(),
662 };
663 self.write_raw(&encoded);
664 }
665
666 fn write_raw(&mut self, data: &[u8]) {
667 if let Some(w) = &mut self.writer
668 && let Err(e) = w.write_all(data)
669 {
670 self.error = Some(e.to_string());
671 self.hang_up();
672 }
673 }
674
675 pub fn hang_up(&mut self) {
676 self.answer_prompt(None);
677 if let Some(w) = self.writer.take() {
678 w.shutdown();
679 }
680 self.state = State::Closed;
681 self.stop_capture();
682 }
683
684 pub fn should_redial(&self) -> bool {
689 if self.state != State::Closed {
690 return false;
691 }
692 match self.connected_at {
693 None => self.error.as_deref().is_some_and(|e| e.contains("connecting to")),
694 Some(t) => t.elapsed() < BUSY_WINDOW && says_busy(&self.term.screen),
695 }
696 }
697
698 pub fn was_connected(&self) -> bool {
700 self.connected_at.is_some()
701 }
702
703 pub fn elapsed(&self) -> Duration {
705 self.connected_at.unwrap_or(self.started).elapsed()
706 }
707
708 pub fn download_dir(&self) -> &Path {
709 &self.config.download_dir
710 }
711
712 pub fn capture_path(&self) -> Option<&Path> {
713 self.capture.as_ref().map(|c| c.path.as_path())
714 }
715
716 pub fn start_capture(&mut self, path: PathBuf) -> Result<()> {
719 if let Some(dir) = path.parent() {
720 std::fs::create_dir_all(dir)?;
721 }
722 let f = File::options()
723 .create(true)
724 .append(true)
725 .open(&path)
726 .with_context(|| format!("opening {}", path.display()))?;
727 let kind = CaptureKind::for_path(&path);
728 self.capture = Some(Capture { path, file: BufWriter::new(f), kind, text: TextState::Ground });
729 Ok(())
730 }
731
732 pub fn stop_capture(&mut self) {
733 if let Some(mut c) = self.capture.take() {
734 let _ = c.file.flush();
735 }
736 }
737}
738
739#[derive(Clone, Copy, Debug, PartialEq, Eq)]
741pub enum CaptureKind {
742 Raw,
744 Text,
746}
747
748impl CaptureKind {
749 pub fn for_path(path: &Path) -> CaptureKind {
750 match path.extension().and_then(|e| e.to_str()) {
751 Some(e) if e.eq_ignore_ascii_case("txt") || e.eq_ignore_ascii_case("log") => CaptureKind::Text,
752 _ => CaptureKind::Raw,
753 }
754 }
755}
756
757#[derive(Clone, Copy, Debug, PartialEq, Eq)]
758enum TextState {
759 Ground,
760 Escape,
761 Csi,
762 String,
764}
765
766struct Capture {
767 path: PathBuf,
768 file: BufWriter<File>,
769 kind: CaptureKind,
770 text: TextState,
771}
772
773impl Capture {
774 fn write(&mut self, data: &[u8], encoding: crate::ansi::Encoding) {
775 if self.kind == CaptureKind::Raw {
776 let _ = self.file.write_all(data);
777 return;
778 }
779 let utf8 = encoding == crate::ansi::Encoding::Utf8;
780 let mut out = Vec::with_capacity(data.len());
781 for &b in data {
782 self.text = match (self.text, b) {
783 (_, 0x18 | 0x1A) => TextState::Ground,
784 (TextState::Ground, 0x1B) => TextState::Escape,
785 (TextState::Ground, b'\n' | b'\t') => {
786 out.push(b);
787 TextState::Ground
788 }
789 (TextState::Ground, 0x0C) => {
790 out.extend_from_slice(b"\n\n");
791 TextState::Ground
792 }
793 (TextState::Ground, 0x00..=0x1F | 0x7F) => TextState::Ground,
794 (TextState::Ground, _) if utf8 || b < 0x80 => {
795 out.push(b);
796 TextState::Ground
797 }
798 (TextState::Ground, _) => {
799 let mut buf = [0; 4];
800 out.extend_from_slice(crate::cp437::to_unicode(b).encode_utf8(&mut buf).as_bytes());
801 TextState::Ground
802 }
803 (TextState::Escape, b'[') => TextState::Csi,
804 (TextState::Escape, b']' | b'P' | b'X' | b'^' | b'_') => TextState::String,
805 (TextState::Escape, 0x20..=0x2F) => TextState::Escape,
806 (TextState::Escape, _) => TextState::Ground,
807 (TextState::Csi, 0x40..=0x7E) => TextState::Ground,
808 (TextState::Csi, _) => TextState::Csi,
809 (TextState::String, 0x07) => TextState::Ground,
810 (TextState::String, 0x1B) => TextState::Escape,
811 (TextState::String, _) => TextState::String,
812 };
813 }
814 let _ = self.file.write_all(&out);
815 }
816}
817
818impl Drop for Session {
819 fn drop(&mut self) {
820 self.hang_up();
821 }
822}
823
824#[cfg(test)]
825mod tests {
826 use super::*;
827 use crate::ansi::Encoding;
828
829 #[test]
830 fn text_captures_keep_only_the_words() {
831 let path = std::env::temp_dir().join(format!("badterm-capture-{}.txt", std::process::id()));
832 let _ = std::fs::remove_file(&path);
833 assert_eq!(CaptureKind::for_path(&path), CaptureKind::Text);
834 assert_eq!(CaptureKind::for_path(Path::new("x.ans")), CaptureKind::Raw);
835 let f = File::create(&path).unwrap();
836 let mut c =
837 Capture { path: path.clone(), file: BufWriter::new(f), kind: CaptureKind::Text, text: TextState::Ground };
838 c.write(b"\x1b[1;3", Encoding::Cp437);
840 c.write(b"1mHello\x1b]2;title\x07\r\n\xc4\xc4 bye\x07", Encoding::Cp437);
841 c.file.flush().unwrap();
842 assert_eq!(std::fs::read_to_string(&path).unwrap(), "Hello\n\u{2500}\u{2500} bye");
843 let _ = std::fs::remove_file(&path);
844 }
845}