1#![doc = include_str!("../README.md")]
2#![allow(dead_code)]
3#![allow(clippy::single_match)]
4#![allow(clippy::needless_doctest_main)]
5
6use fltk::{enums::*, prelude::*, *};
7use portable_pty::MasterPty;
8use std::cell::Cell as StdCell;
9use std::{
10 io::{self, Write},
11 str,
12 sync::{
13 atomic::{AtomicBool, Ordering},
14 Arc, Mutex,
15 },
16};
17mod canvas;
18pub mod cell_performer;
19mod cells;
20mod pty;
21mod styles;
22
23pub use canvas::TermCanvas;
24pub use cells::{Cell, CellBuffer, Style};
25use styles::*;
26
27const UP: &[u8] = if cfg!(not(target_os = "windows")) {
28 b"\x10"
29} else {
30 b"\x1b[A"
31};
32const DOWN: &[u8] = if cfg!(not(target_os = "windows")) {
33 b"\x0E"
34} else {
35 b"\x1b[B"
36};
37
38pub fn menu_cb(m: &mut impl MenuExt) {
39 if let Ok(mpath) = m.item_pathname(None) {
40 match mpath.as_str() {
41 "/Copy\t" => {
42 let mut term: group::Group = app::widget_from_id("term").unwrap();
43 term.do_callback();
44 }
45 "/Paste\t" => {
46 let term: group::Group = app::widget_from_id("term").unwrap();
47 app::paste_text(&term);
48 }
49 _ => (),
50 }
51 }
52}
53
54pub fn init_menu(m: &mut (impl MenuExt + 'static)) {
55 m.add(
56 "Copy\t",
57 Shortcut::Ctrl | Shortcut::Shift | 'c',
58 menu::MenuFlag::Normal,
59 menu_cb,
60 );
61 m.add(
62 "Paste\t",
63 Shortcut::Ctrl | Shortcut::Shift | 'v',
64 menu::MenuFlag::Normal,
65 menu_cb,
66 );
67}
68
69fn copy_selection_to_clipboard(
71 sel_arc: &Arc<Mutex<crate::canvas::Selection>>,
72 buffer: &Arc<Mutex<CellBuffer>>,
73 widget_width: i32,
74) {
75 if let Ok(ssel) = sel_arc.lock() {
76 if let (Some(mut a), Some(mut b)) = (ssel.start, ssel.end) {
77 if b < a {
78 std::mem::swap(&mut a, &mut b);
79 }
80 if let Ok(buf) = buffer.lock() {
81 let pad_x = 6;
82 let char_w = ((draw::width("M") as f32).ceil() as i32).max(1);
83 let cols = ((widget_width - 2 * pad_x).max(1) / char_w).max(1) as usize;
84 let snap = buf.snapshot();
85 let mut visual: Vec<Vec<char>> = Vec::new();
86 for line in snap.iter() {
87 let row: Vec<char> = line.iter().map(|c| c.ch).collect();
88 if row.is_empty() {
89 visual.push(Vec::new());
90 continue;
91 }
92 for chunk in row.chunks(cols) {
93 visual.push(chunk.to_vec());
94 }
95 }
96 let mut out = String::new();
97 for v in a.0..=b.0 {
98 if v >= visual.len() {
99 break;
100 }
101 let line = &visual[v];
102 let start = if v == a.0 { a.1.min(line.len()) } else { 0 };
103 let end = if v == b.0 {
104 (b.1 + 1).min(line.len())
105 } else {
106 line.len()
107 };
108 if start < end {
109 for ch in &line[start..end] {
110 out.push(*ch);
111 }
112 }
113 if v != b.0 {
114 out.push('\n');
115 }
116 }
117 if !out.is_empty() {
118 app::copy(&out);
119 app::copy2(&out);
120 }
121 }
122 }
123 }
124}
125
126fn scroll_to_bottom(scroll: &mut group::Scroll, canvas: &group::Group) {
128 let view_h = scroll.h();
129 let max_y = (canvas.h() - view_h).max(0);
130 scroll.scroll_to(scroll.xposition(), max_y);
131}
132
133fn paste_and_scroll(widget: &group::Group, scroll: &mut group::Scroll, canvas: &group::Group) {
135 app::paste_text(widget);
136 scroll_to_bottom(scroll, canvas);
137}
138
139fn is_copy_shortcut() -> bool {
141 let key = app::event_key();
142 app::event_state().contains(EventState::Ctrl)
143 && app::event_state().contains(EventState::Shift)
144 && key == Key::from_char('c')
145}
146
147fn is_paste_shortcut() -> bool {
149 let key = app::event_key();
150 (app::event_state().contains(EventState::Ctrl)
151 && app::event_state().contains(EventState::Shift)
152 && key == Key::from_char('v'))
153 || (app::event_state().contains(EventState::Shift) && key == Key::Insert)
154}
155
156pub struct PPTerm {
157 scroll: group::Scroll,
158 canvas: TermCanvas,
159 pty: Option<pty::PtyHandles>,
160 shutdown_flag: Arc<AtomicBool>,
161 buffer: Arc<Mutex<CellBuffer>>,
162 cols: u16,
163 rows: u16,
164 auto_follow: Arc<Mutex<bool>>,
165 shared_writer: Arc<Mutex<Option<Arc<Mutex<Box<dyn Write + Send>>>>>>,
167 shared_master: Arc<Mutex<Option<Arc<Mutex<Box<dyn MasterPty + Send>>>>>>,
168}
169
170impl Default for PPTerm {
171 fn default() -> Self {
172 PPTerm::new(0, 0, 0, 0, None)
173 }
174}
175
176impl PPTerm {
177 #[allow(clippy::too_many_arguments)]
179 fn new_with_cols_rows_internal<L: Into<Option<&'static str>>>(
180 x: i32,
181 y: i32,
182 w: i32,
183 h: i32,
184 label: L,
185 init_cols: u16,
186 init_rows: u16,
187 max_lines: usize,
188 defer_start: bool,
189 ) -> Self {
190 let mut scroll =
191 group::Scroll::new(x, y, w, h, label).with_type(group::ScrollType::Vertical);
192 scroll.set_id("term_group");
193 let buffer = Arc::new(Mutex::new(CellBuffer::new(max_lines, BLACK, WHITE)));
194 let mut canvas = TermCanvas::new(scroll.x(), scroll.y(), w, h, None);
195 canvas.set_id("term");
196 canvas.set_buffer(buffer.clone());
197 canvas.set_scroll(scroll.clone());
198 canvas.start_blink(0.6);
199 canvas.end();
200 let mut m = menu::MenuButton::default()
201 .with_type(menu::MenuButtonType::Popup3)
202 .with_id("pop2");
203 init_menu(&mut m);
204 scroll.end();
205
206 draw::set_font(Font::Courier, 14);
208 let sample = "MMMMMMMMMM";
209 let (sw, sh) = draw::measure(sample, false);
210 let char_w = (sw as f32 / 10.0).ceil() as i32;
211 let line_h = sh.max(14);
212
213 let pad_x = 6;
218 let pad_y = 4;
219 let actual_cols = if w > 0 {
220 ((w - 2 * pad_x).max(char_w) / char_w).max(10) as u16
221 } else {
222 init_cols.max(10)
223 };
224 let actual_rows = if h > 0 {
225 ((h - pad_y).max(line_h) / line_h).max(3) as u16
226 } else {
227 init_rows.max(3)
228 };
229
230 let shutdown_flag = Arc::new(AtomicBool::new(false));
231 let shared_writer: Arc<Mutex<Option<Arc<Mutex<Box<dyn Write + Send>>>>>> =
233 Arc::new(Mutex::new(None));
234 let shared_master: Arc<Mutex<Option<Arc<Mutex<Box<dyn MasterPty + Send>>>>>> =
235 Arc::new(Mutex::new(None));
236
237 let handles = if !defer_start {
239 let h = pty::start(
240 buffer.clone(),
241 actual_cols,
242 actual_rows,
243 shutdown_flag.clone(),
244 );
245 if let Some(ref hh) = h {
246 *shared_writer.lock().unwrap() = Some(hh.writer.clone());
247 *shared_master.lock().unwrap() = Some(hh.master_pty.clone());
248 }
249 h
250 } else {
251 None
252 };
253
254 scroll.resize_callback({
256 let mut canvas = canvas.clone();
257 let shared_master_cb = shared_master.clone();
258 let mut menu = m.clone();
259 move |_, x, y, w, h| {
260 menu.resize(x, y, w, h);
263 canvas.set_size(w, canvas.h());
264 let pad_x = 6;
265 let pad_y = 4;
266 let cols = ((w - 2 * pad_x).max(char_w) / char_w).max(10) as u16;
267 let rows = ((h - pad_y).max(line_h) / line_h).max(3) as u16;
268 if let Some(ref pty) = *shared_master_cb.lock().unwrap() {
269 let _ = pty::resize_pty(pty, cols, rows);
270 }
271 }
272 });
273
274 {
276 let selection_arc = canvas.selection_handle();
277 let buffer_for_copy = buffer.clone();
278 let mut scroll_for_input = scroll.clone();
279 let canvas_for_input = canvas.clone();
280 let shared_writer_ev = shared_writer.clone();
281 canvas.set_callback({
282 let sel_arc = selection_arc.clone();
283 let buf = buffer.clone();
284 move |g| {
285 copy_selection_to_clipboard(&sel_arc, &buf, g.w());
286 }
287 });
288 canvas.handle({
289 move |t, ev| match ev {
290 Event::Push => {
291 if app::event_button() == 1 {
292 t.take_focus().ok();
293 let (mx, my) = (app::event_x(), app::event_y());
294 let line_h = draw::height().max(14);
296 let char_w = ((draw::width("M") as f32).ceil() as i32).max(1);
297 let pad_x = 6;
298 let view_top = scroll_for_input.y();
299 let yoff = scroll_for_input.yposition();
300 let row_view = ((my - view_top).max(0) / line_h) as usize;
301 let row = row_view + (yoff / line_h).max(0) as usize;
302 let col = ((mx - t.x() - pad_x).max(0) / char_w) as usize;
303 if let Ok(mut sel) = selection_arc.lock() {
304 sel.start = Some((row, col));
305 sel.end = Some((row, col));
306 }
307 t.redraw();
308 true
309 } else {
310 false
311 }
312 }
313 Event::KeyDown => {
314 if is_copy_shortcut() {
315 copy_selection_to_clipboard(&selection_arc, &buffer_for_copy, t.w());
316 return true;
317 }
318 if is_paste_shortcut() {
319 paste_and_scroll(t, &mut scroll_for_input, &canvas_for_input);
320 return true;
321 }
322 let key = app::event_key();
323 let mods = app::event_state();
324 let has_shift = mods.contains(EventState::Shift);
325 let has_alt = mods.contains(EventState::Alt);
326 let has_ctrl = mods.contains(EventState::Ctrl);
327 let send = |bytes: &[u8]| {
328 if let Some(ref wr_arc) = *shared_writer_ev.lock().unwrap() {
329 if let Ok(mut w) = wr_arc.lock() {
330 if has_alt {
331 let _ = w.write_all(b"\x1b"); }
333 let _ = w.write_all(bytes);
334 }
335 }
336 };
337 let arrow_with_mods = |letter: u8| -> Vec<u8> {
338 let mut v = Vec::new();
339 let mut m = 1; if has_shift {
341 m += 1;
342 } if has_alt {
344 m += 2;
345 } if has_ctrl {
347 m += 4;
348 } if m == 1 {
350 v.extend_from_slice(&[0x1b, b'[', letter]);
351 } else {
352 v.extend_from_slice(b"\x1b[1;");
353 v.extend_from_slice(m.to_string().as_bytes());
354 v.push(letter);
355 }
356 v
357 };
358 match key {
359 #[cfg(windows)]
360 Key::BackSpace => {
361 if let Some(ref wr_arc) = *shared_writer_ev.lock().unwrap() {
362 if let Ok(mut w) = wr_arc.lock() {
363 let _ = w.write_all(b"\x7f");
364 }
365 }
366 }
367 Key::Up => {
368 let seq = if has_shift || has_alt || has_ctrl {
369 arrow_with_mods(b'A')
370 } else {
371 UP.to_vec()
372 };
373 send(&seq);
374 }
375 Key::Down => {
376 let seq = if has_shift || has_alt || has_ctrl {
377 arrow_with_mods(b'B')
378 } else {
379 DOWN.to_vec()
380 };
381 send(&seq);
382 }
383 Key::Left => {
384 let seq = arrow_with_mods(b'D');
385 send(&seq);
386 }
387 Key::Right => {
388 let seq = arrow_with_mods(b'C');
389 send(&seq);
390 }
391 Key::Home => {
393 if has_shift || has_alt || has_ctrl {
394 let seq = arrow_with_mods(b'H');
395 send(&seq);
396 } else {
397 send(b"\x1b[1~");
398 }
399 }
400 Key::End => {
401 if has_shift || has_alt || has_ctrl {
402 let seq = arrow_with_mods(b'F');
403 send(&seq);
404 } else {
405 send(b"\x1b[4~");
406 }
407 }
408 Key::PageUp => {
409 send(b"\x1b[5~");
410 }
411 Key::PageDown => {
412 send(b"\x1b[6~");
413 }
414 Key::Insert => {
415 send(b"\x1b[2~");
416 }
417 Key::Delete => {
418 send(b"\x1b[3~");
419 }
420 Key::Enter => {
421 send(b"\r");
422 }
423 Key::F1 => {
424 send(b"\x1bOP");
425 }
426 Key::F2 => {
427 send(b"\x1bOQ");
428 }
429 Key::F3 => {
430 send(b"\x1bOR");
431 }
432 Key::F4 => {
433 send(b"\x1bOS");
434 }
435 Key::F5 => {
436 send(b"\x1b[15~");
437 }
438 Key::F6 => {
439 send(b"\x1b[17~");
440 }
441 Key::F7 => {
442 send(b"\x1b[18~");
443 }
444 Key::F8 => {
445 send(b"\x1b[19~");
446 }
447 Key::F9 => {
448 send(b"\x1b[20~");
449 }
450 Key::F10 => {
451 send(b"\x1b[21~");
452 }
453 Key::F11 => {
454 send(b"\x1b[23~");
455 }
456 Key::F12 => {
457 send(b"\x1b[24~");
458 }
459 _ => {
460 let txt = app::event_text();
461 if !txt.is_empty() {
462 send(txt.as_bytes());
463 }
464 }
465 }
466 scroll_to_bottom(&mut scroll_for_input, &canvas_for_input);
468 true
469 }
470 Event::Drag => {
471 let (mx, my) = (app::event_x(), app::event_y());
472 let line_h = draw::height().max(14);
474 let char_w = ((draw::width("M") as f32).ceil() as i32).max(1);
475 let pad_x = 6;
476 let view_top = scroll_for_input.y();
477 let yoff = scroll_for_input.yposition();
478 let row_view = ((my - view_top).max(0) / line_h) as usize;
479 let row = row_view + (yoff / line_h).max(0) as usize;
480 let col = ((mx - t.x() - pad_x).max(0) / char_w) as usize;
481 if let Ok(mut sel) = selection_arc.lock() {
482 sel.end = Some((row, col));
483 }
484 let edge = 14; let mut new_y = scroll_for_input.yposition();
489 let vt = scroll_for_input.y();
491 let vb = vt + scroll_for_input.h();
492 if my <= vt + edge {
494 new_y = new_y.saturating_sub(line_h * 3);
495 } else if my >= vb - edge {
496 new_y = new_y.saturating_add(line_h * 2);
497 }
498 if new_y != scroll_for_input.yposition() {
499 let max_y = (canvas_for_input.h() - scroll_for_input.h()).max(0);
500 let new_y = new_y.clamp(0, max_y);
501 scroll_for_input.scroll_to(scroll_for_input.xposition(), new_y);
502 }
503 t.redraw();
504 true
505 }
506 Event::Released => {
507 if app::event_button() == 2 {
509 app::paste_text(t);
510 } else if app::event_button() == 3 {
511 m.popup();
512 }
513 true
514 }
515 Event::Paste => {
516 let mut pasted = String::new();
518 if let Some(cb) = app::event_clipboard() {
519 match cb {
520 app::ClipboardEvent::Text(s) => pasted = s,
521 app::ClipboardEvent::Image(_) => {}
522 }
523 }
524 if pasted.is_empty() {
525 pasted = app::event_text();
526 }
527 if !pasted.is_empty() {
528 if let Some(ref wr_arc) = *shared_writer_ev.lock().unwrap() {
529 if let Ok(mut w) = wr_arc.lock() {
530 let _ = w.write_all(pasted.as_bytes());
531 }
532 }
533 }
534 scroll_to_bottom(&mut scroll_for_input, &canvas_for_input);
536 true
537 }
538 Event::Focus => true,
539 Event::Unfocus => true,
540 _ => false,
541 }
542 });
543 }
544
545 let mut canvas_clone = canvas.clone();
547 let buffer_clone = buffer.clone();
548 let shared_master_cl = shared_master.clone();
549 let mut scroll_clone = scroll.clone();
550 let auto_follow_flag = Arc::new(Mutex::new(true));
551 let last_vlines = Arc::new(Mutex::new(0usize));
552 let last_vlines_cl = last_vlines.clone();
553 let auto_follow_flag_cl = auto_follow_flag.clone();
554
555 let prev_cols = StdCell::new(0u16);
557 let prev_rows = StdCell::new(0u16);
558 let prev_h = StdCell::new(0i32);
559
560 app::add_timeout3(0.05, move |h| {
562 if !scroll_clone.visible() {
564 app::repeat_timeout3(0.5, h);
565 return;
566 }
567
568 let pad_x = 6;
570 let pad_y = 4;
571 let cols_vis = ((canvas_clone.w() - 12).max(char_w) / char_w).max(1) as usize;
572 let cols = ((scroll_clone.w() - 2 * pad_x).max(char_w) / char_w).max(10) as u16;
573 let rows = ((scroll_clone.h() - pad_y).max(line_h) / line_h).max(3) as u16;
574
575 let dims_changed = cols != prev_cols.get() || rows != prev_rows.get();
577
578 let mut had_dirty = false;
580 let line_count: usize = if let Ok(mut buf) = buffer_clone.lock() {
581 let dirty_areas = buf.take_dirty_areas();
583 had_dirty = !dirty_areas.is_empty();
584
585 if had_dirty || dims_changed || prev_h.get() == 0 {
586 let snap = buf.snapshot();
587 let mut vlines = 0usize;
588 for line in snap.iter() {
589 let len = line.len().max(1);
590 vlines += len.div_ceil(cols_vis); }
592 vlines
593 } else {
594 *last_vlines_cl.lock().unwrap()
596 }
597 } else {
598 0usize
599 };
600
601 let desired_h = (line_count as i32 * line_h + pad_y).max(scroll_clone.h());
602
603 let mut changed = false;
605 if desired_h != prev_h.get() {
606 canvas_clone.set_size(scroll_clone.w(), desired_h);
607 prev_h.set(desired_h);
608 changed = true;
609 }
610
611 if cols != prev_cols.get() || rows != prev_rows.get() {
613 if let Some(ref pty) = *shared_master_cl.lock().unwrap() {
614 let _ = pty::resize_pty(pty, cols, rows);
615 }
616 prev_cols.set(cols);
617 prev_rows.set(rows);
618 changed = true;
619 }
620
621 if let Ok(mut buf) = buffer_clone.lock() {
623 buf.set_dimensions(cols as usize, rows as usize);
624 }
625
626 if *auto_follow_flag_cl.lock().unwrap() {
628 let mut new_output = false;
629 if let Ok(mut prev) = last_vlines_cl.lock() {
630 if line_count > *prev {
631 new_output = true;
632 }
633 *prev = line_count;
634 }
635 if new_output {
636 let view_h = scroll_clone.h();
637 let max_y = (canvas_clone.h() - view_h).max(0);
638 scroll_clone.scroll_to(scroll_clone.xposition(), max_y);
639 }
640 }
641
642 let next = if had_dirty || changed {
644 canvas_clone.redraw();
645 0.05
646 } else {
647 0.05
648 };
649 app::repeat_timeout3(next, h);
650 });
651
652 Self {
653 scroll,
654 canvas,
655 pty: handles,
656 shutdown_flag,
657 buffer,
658 cols: init_cols,
659 rows: init_rows,
660 auto_follow: auto_follow_flag,
661 shared_writer: shared_writer.clone(),
662 shared_master: shared_master.clone(),
663 }
664 }
665
666 pub fn new<L: Into<Option<&'static str>>>(x: i32, y: i32, w: i32, h: i32, label: L) -> Self {
668 Self::new_with_cols_rows_internal(x, y, w, h, label, 80, 24, 2000, false)
669 }
670
671 pub fn new_with_dims<L: Into<Option<&'static str>>>(
673 x: i32,
674 y: i32,
675 w: i32,
676 h: i32,
677 label: L,
678 cols: u16,
679 rows: u16,
680 ) -> Self {
681 Self::new_with_cols_rows_internal(x, y, w, h, label, cols, rows, 2000, false)
682 }
683
684 pub fn with_dimensions(cols: u16, rows: u16) -> Self {
687 Self::new_with_cols_rows_internal(0, 0, 0, 0, None, cols, rows, 2000, false)
688 }
689
690 pub fn with_dimensions_and_scrollback(cols: u16, rows: u16, scrollback_lines: usize) -> Self {
692 Self::new_with_cols_rows_internal(0, 0, 0, 0, None, cols, rows, scrollback_lines, false)
693 }
694
695 pub fn new_deferred<L: Into<Option<&'static str>>>(
698 x: i32,
699 y: i32,
700 w: i32,
701 h: i32,
702 label: L,
703 ) -> Self {
704 Self::new_with_cols_rows_internal(x, y, w, h, label, 80, 24, 2000, true)
705 }
706
707 pub fn new_with_dims_deferred<L: Into<Option<&'static str>>>(
709 x: i32,
710 y: i32,
711 w: i32,
712 h: i32,
713 label: L,
714 cols: u16,
715 rows: u16,
716 ) -> Self {
717 Self::new_with_cols_rows_internal(x, y, w, h, label, cols, rows, 2000, true)
718 }
719
720 pub fn with_dimensions_deferred(cols: u16, rows: u16) -> Self {
722 Self::new_with_cols_rows_internal(0, 0, 0, 0, None, cols, rows, 2000, true)
723 }
724
725 pub fn with_dimensions_and_scrollback_deferred(
726 cols: u16,
727 rows: u16,
728 scrollback_lines: usize,
729 ) -> Self {
730 Self::new_with_cols_rows_internal(0, 0, 0, 0, None, cols, rows, scrollback_lines, true)
731 }
732
733 pub fn start(&mut self) {
736 if self.pty.is_some() {
737 return;
738 }
739
740 draw::set_font(Font::Courier, 14);
742 let sample = "MMMMMMMMMM";
743 let (sw, sh) = draw::measure(sample, false);
744 let char_w = (sw as f32 / 10.0).ceil() as i32;
745 let line_h = sh.max(14);
746 let pad_x = 6;
747 let pad_y = 4;
748 let w = self.scroll.w();
749 let h = self.scroll.h();
750 let cols = if w > 0 {
751 ((w - 2 * pad_x).max(char_w) / char_w).max(10) as u16
752 } else {
753 self.cols.max(10)
754 };
755 let rows = if h > 0 {
756 ((h - pad_y).max(line_h) / line_h).max(3) as u16
757 } else {
758 self.rows.max(3)
759 };
760
761 let handles = pty::start(
762 self.buffer.clone(),
763 cols,
764 rows,
765 self.shutdown_flag.clone(),
766 );
767 if let Some(ref h) = handles {
768 *self.shared_writer.lock().unwrap() = Some(h.writer.clone());
769 *self.shared_master.lock().unwrap() = Some(h.master_pty.clone());
770 }
771 self.pty = handles;
772 }
773
774 pub fn widget(&self) -> &group::Scroll {
775 &self.scroll
776 }
777 pub fn redraw(&mut self) {
778 self.canvas.redraw();
779 }
780 pub fn buffer(&self) -> Arc<Mutex<CellBuffer>> {
781 self.buffer.clone()
782 }
783 pub fn shutdown(&self) {
784 self.shutdown_flag.store(true, Ordering::Relaxed);
785 }
786 pub fn set_auto_follow(&mut self, enabled: bool) {
787 if let Ok(mut v) = self.auto_follow.lock() {
788 *v = enabled;
789 }
790 }
791 pub fn auto_follow_handle(&self) -> Arc<Mutex<bool>> {
792 self.auto_follow.clone()
793 }
794
795 pub fn cols(&self) -> u16 {
797 self.cols
798 }
799 pub fn rows(&self) -> u16 {
801 self.rows
802 }
803
804 pub fn write_all(&self, s: &[u8]) -> Result<(), io::Error> {
806 if let Some(ref pty) = &self.pty {
807 match pty.writer.lock() {
808 Ok(mut w) => w.write_all(s),
809 Err(_) => Err(io::Error::new(
810 io::ErrorKind::Other,
811 "Failed to acquire writer lock",
812 )),
813 }
814 } else {
815 Err(io::Error::new(
816 io::ErrorKind::BrokenPipe,
817 "No writer available",
818 ))
819 }
820 }
821
822 pub fn set_dims(&mut self, cols: u16, rows: u16) -> Result<(), Box<dyn std::error::Error>> {
827 self.cols = cols;
828 self.rows = rows;
829 if let Some(ref p) = self.pty {
830 pty::resize_pty(&p.master_pty, cols, rows)
831 } else {
832 Ok(())
833 }
834 }
835}
836
837impl Drop for PPTerm {
838 fn drop(&mut self) {
839 self.shutdown();
840 if let Some(pty) = self.pty.take() {
841 std::thread::spawn(move || {
843 let _ = pty.thread_handle.join();
844 });
845 }
846 }
847}
848
849fltk::widget_extends!(PPTerm, group::Scroll, scroll);
850
851fn canvas_sel_mouse_to_vpos(t: &group::Group, mx: i32, my: i32) -> (usize, usize) {
852 let line_h = draw::height().max(14);
853 let char_w = ((draw::width("M") as f32).ceil() as i32).max(1);
854 let pad_x = 6;
855 let col = ((mx - t.x() - pad_x).max(0) / char_w) as usize;
856 let row = ((my - t.y()).max(0) / line_h) as usize;
857 (row, col)
858}