1use std::fmt;
17use std::sync::atomic::{AtomicBool, Ordering};
18use std::sync::mpsc::{channel, Receiver, Sender};
19
20type MsgChannel = Sender<Box<dyn Msg>>;
22type MsgReceiver = Receiver<Box<dyn Msg>>;
24use std::sync::{Arc, Mutex};
25use std::thread;
26use std::time::Duration;
27
28use crate::commands::{
29 BatchMsg, InterruptMsg, QuitMsg, RequestWindowSizeMsg, ResumeMsg, SequenceMsg, SuspendMsg,
30};
31use crate::cursor::CursorPositionMsg;
32use crate::environ::EnvMsg;
33use crate::exec::ExecMsg;
34use crate::focus::{BlurMsg, FocusMsg};
35use crate::key::{KeyMod, KeyPressMsg, KeyReleaseMsg};
36use crate::keyboard::KeyboardEnhancementsMsg;
37use crate::model::{Model, Msg};
38use crate::mouse::{MouseClickMsg, MouseMotionMsg, MouseReleaseMsg, MouseWheelMsg};
39use crate::options::ProgramOptions;
40use crate::paste::PasteMsg;
41use crate::profile::ColorProfileMsg;
42use crate::renderer::{PrintLineMsg, Renderer};
43use crate::screen::{ClearScreenMsg, WindowSizeMsg};
44use crate::tty::{disable_raw_mode, enable_raw_mode};
45use crossterm::terminal::size as term_size;
46
47pub const ERR_PROGRAM_PANIC: &str = "program experienced a panic";
49
50pub const ERR_PROGRAM_KILLED: &str = "program was killed";
52
53pub const ERR_INTERRUPTED: &str = "program was interrupted";
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum ProgramError {
60 Killed,
62 Interrupted,
64 Panic,
66}
67
68impl fmt::Display for ProgramError {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 ProgramError::Killed => write!(f, "{}", ERR_PROGRAM_KILLED),
72 ProgramError::Interrupted => write!(f, "{}", ERR_INTERRUPTED),
73 ProgramError::Panic => write!(f, "{}", ERR_PROGRAM_PANIC),
74 }
75 }
76}
77
78impl std::error::Error for ProgramError {}
79
80pub struct Program<M: Model> {
82 startup_buf: Arc<Mutex<Option<Vec<u8>>>>,
85 model: M,
86 options: ProgramOptions<M>,
87 renderer: Arc<Mutex<Box<dyn Renderer>>>,
88 msg_tx: Option<MsgChannel>,
89 finished: Arc<AtomicBool>,
90}
91
92impl<M: Model> Program<M> {
93 pub fn new(model: M) -> Self {
95 let (w, h) = term_size().unwrap_or((80, 24));
96 let env: Vec<String> = std::env::vars().map(|(k, v)| format!("{k}={v}")).collect();
97 Self {
98 startup_buf: Arc::new(Mutex::new(None)),
99 model,
100 options: ProgramOptions::default(),
101 renderer: Arc::new(Mutex::new(Box::new(
102 crate::cursed_renderer::new_cursed_renderer(
103 Box::new(std::io::stdout()),
104 &env,
105 w as usize,
106 h as usize,
107 ),
108 ))),
109 msg_tx: None,
110 finished: Arc::new(AtomicBool::new(false)),
111 }
112 }
113
114 pub fn with_options(mut self, options: ProgramOptions<M>) -> Self {
116 self.options = options;
117 self
118 }
119
120 pub fn send(&self, msg: Box<dyn Msg>) {
124 if let Some(tx) = &self.msg_tx {
125 let _ = tx.send(msg);
126 }
127 }
128
129 pub fn quit(&self) {
132 self.send(Box::new(QuitMsg));
133 }
134
135 pub fn kill(&mut self) {
139 let _ = disable_raw_mode();
142 self.finished.store(true, Ordering::SeqCst);
143 if let Some(tx) = &self.msg_tx {
144 let _ = tx.send(Box::new(QuitMsg));
145 }
146 }
147
148 pub fn wait(&self) {
150 while !self.finished.load(Ordering::SeqCst) {
151 thread::sleep(Duration::from_millis(10));
152 }
153 }
154
155 pub fn println(&self, args: &str) {
158 self.send(Box::new(PrintLineMsg {
159 message_body: args.to_string(),
160 }));
161 }
162
163 pub fn printf(&self, body: &str) {
166 self.send(Box::new(PrintLineMsg {
167 message_body: body.to_string(),
168 }));
169 }
170
171 fn handle_msg(&mut self, msg: Box<dyn Msg>, tx: &MsgChannel) -> Result<bool, ProgramError> {
173 let processed_msg = if let Some(ref filter) = self.options.filter {
174 match filter(&self.model, msg) {
175 Some(m) => m,
176 None => return Ok(false),
177 }
178 } else {
179 msg
180 };
181
182 if processed_msg.as_ref().as_any().is::<QuitMsg>() {
183 return Ok(true);
184 }
185
186 if processed_msg.as_ref().as_any().is::<InterruptMsg>() {
187 return Err(ProgramError::Interrupted);
188 }
189
190 if processed_msg.as_ref().as_any().is::<SuspendMsg>() {
191 let _ = disable_raw_mode();
194 self.send_resume_later(tx);
195 }
196
197 if processed_msg.as_ref().as_any().is::<ClearScreenMsg>() {
198 self.renderer.lock().unwrap().clear_screen();
199 } else if processed_msg
200 .as_ref()
201 .as_any()
202 .is::<crate::color::RequestBackgroundColorMsg>()
203 {
204 if let Ok(mut buf) = self.startup_buf.lock() {
207 if let Some(b) = buf.as_mut() {
208 b.extend_from_slice(
209 rusty_x_ansi::background::REQUEST_BACKGROUND_COLOR.as_bytes(),
210 );
211 }
212 }
213 } else if processed_msg
214 .as_ref()
215 .as_any()
216 .is::<crate::color::RequestForegroundColorMsg>()
217 {
218 if let Ok(mut buf) = self.startup_buf.lock() {
219 if let Some(b) = buf.as_mut() {
220 b.extend_from_slice(
221 rusty_x_ansi::background::REQUEST_FOREGROUND_COLOR.as_bytes(),
222 );
223 }
224 }
225 } else if processed_msg
226 .as_ref()
227 .as_any()
228 .is::<crate::color::RequestCursorColorMsg>()
229 {
230 if let Ok(mut buf) = self.startup_buf.lock() {
231 if let Some(b) = buf.as_mut() {
232 b.extend_from_slice(rusty_x_ansi::background::REQUEST_CURSOR_COLOR.as_bytes());
233 }
234 }
235 } else if let Some(cap) = processed_msg
236 .as_ref()
237 .as_any()
238 .downcast_ref::<crate::termcap::RequestCapabilityMsg>()
239 {
240 use std::io::Write as _;
244 let mut seq = String::from("\x1bP+q");
245 for b in cap.0.as_bytes() {
246 seq.push_str(&format!("{:02X}", b));
247 }
248 seq.push_str("\x1b\\");
249 let _ = std::io::stdout().write_all(seq.as_bytes());
250 return Ok(false);
251 } else if processed_msg
252 .as_ref()
253 .as_any()
254 .is::<crate::xterm::RequestTerminalVersionMsg>()
255 {
256 use std::io::Write as _;
260 let _ = std::io::stdout().write_all(b"\x1b[>q");
261 return Ok(false);
262 } else if processed_msg.as_ref().as_any().is::<RequestWindowSizeMsg>() {
263 if let Ok((w, h)) = term_size() {
264 let _ = tx.send(Box::new(WindowSizeMsg {
265 width: w as usize,
266 height: h as usize,
267 }));
268 }
269 return Ok(false);
271 } else if let Some(ws) = processed_msg
272 .as_ref()
273 .as_any()
274 .downcast_ref::<WindowSizeMsg>()
275 {
276 self.renderer.lock().unwrap().resize(ws.width, ws.height);
278 } else if let Some(exec_msg) = processed_msg.as_ref().as_any().downcast_ref::<ExecMsg>() {
279 let _ = disable_raw_mode();
280 let mut cmd = std::process::Command::new(&exec_msg.cmd);
281 cmd.args(&exec_msg.args);
282 let _ = cmd.status();
283 let _ = enable_raw_mode();
284 } else if let Some(print_msg) = processed_msg
285 .as_ref()
286 .as_any()
287 .downcast_ref::<PrintLineMsg>()
288 {
289 let _ = self
291 .renderer
292 .lock()
293 .unwrap()
294 .insert_above(print_msg.message_body.clone());
295 let view = self.model.view();
297 self.renderer.lock().unwrap().render(view);
298 return Ok(false);
299 } else if let Some(env) = processed_msg.as_ref().as_any().downcast_ref::<EnvMsg>() {
300 let _ = env;
301 } else if let Some(profile) = processed_msg
302 .as_ref()
303 .as_any()
304 .downcast_ref::<ColorProfileMsg>()
305 {
306 let p = match profile.profile {
307 crate::profile::ColorProfile::TrueColor => rusty_colorprofile::Profile::TrueColor,
308 crate::profile::ColorProfile::ANSI256 => rusty_colorprofile::Profile::Ansi256,
309 crate::profile::ColorProfile::ANSI => rusty_colorprofile::Profile::Ansi,
310 crate::profile::ColorProfile::Ascii => rusty_colorprofile::Profile::Ascii,
311 };
312 self.renderer.lock().unwrap().set_color_profile(p);
313 } else if let Some(_resume) = processed_msg.as_ref().as_any().downcast_ref::<ResumeMsg>() {
314 let _ = enable_raw_mode();
315 }
316
317 if processed_msg.as_ref().as_any().is::<BatchMsg>() {
326 let any = processed_msg.into_any();
327 let batch = *any.downcast::<BatchMsg>().unwrap();
328 let tx_clone = tx.clone();
329 thread::spawn(move || exec_batch_msg(batch, &tx_clone));
330 return Ok(false);
331 }
332
333 let mouse_msg = {
338 let any = processed_msg.as_ref().as_any();
339 if let Some(m) = any.downcast_ref::<crate::mouse::MouseClickMsg>() {
340 Some(crate::mouse::MouseMsg::Click(m.clone()))
341 } else if let Some(m) = any.downcast_ref::<crate::mouse::MouseMotionMsg>() {
342 Some(crate::mouse::MouseMsg::Motion(m.clone()))
343 } else if let Some(m) = any.downcast_ref::<crate::mouse::MouseReleaseMsg>() {
344 Some(crate::mouse::MouseMsg::Release(m.clone()))
345 } else {
346 any.downcast_ref::<crate::mouse::MouseWheelMsg>()
347 .map(|m| crate::mouse::MouseMsg::Wheel(m.clone()))
348 }
349 };
350 if let Some(mouse_msg) = mouse_msg {
351 let cmd = self.renderer.lock().unwrap().on_mouse(mouse_msg);
352 if let Some(c) = cmd {
353 let tx_clone = tx.clone();
354 thread::spawn(move || {
355 if let Some(new_msg) = c() {
356 let _ = tx_clone.send(new_msg);
357 }
358 });
359 }
360 }
361
362 if processed_msg.as_ref().as_any().is::<SequenceMsg>() {
363 let any = processed_msg.into_any();
364 let seq = *any.downcast::<SequenceMsg>().unwrap();
365 let tx_clone = tx.clone();
366 thread::spawn(move || exec_sequence_msg(seq, &tx_clone));
367 return Ok(false);
368 }
369
370 let cmd = self.model.update(&*processed_msg);
371 let view = self.model.view();
372 self.renderer.lock().unwrap().render(view);
373
374 if let Some(c) = cmd {
375 let tx_clone = tx.clone();
376 thread::spawn(move || {
377 if let Some(new_msg) = c() {
378 let _ = tx_clone.send(new_msg);
379 }
380 });
381 }
382 Ok(false)
383 }
384
385 fn send_resume_later(&self, tx: &MsgChannel) {
386 let tx = tx.clone();
387 thread::spawn(move || {
388 let _ = tx.send(Box::new(ResumeMsg));
389 });
390 }
391
392 pub fn run(mut self) -> Result<M, Box<dyn std::error::Error>> {
394 let _ = enable_raw_mode();
395 self.renderer.lock().unwrap().start();
396
397 {
403 use std::os::fd::AsRawFd as _;
404 let env = rusty_ultraviolet::Environ(
405 std::env::vars().map(|(k, v)| format!("{k}={v}")).collect(),
406 );
407 let profile = rusty_ultraviolet::terminal_screen::detect_color_profile(
408 Some(std::io::stdout().as_raw_fd()),
409 &env,
410 );
411 self.renderer
412 .lock()
413 .unwrap()
414 .set_color_profile(match profile {
415 rusty_ultraviolet::terminal_screen::ColorProfile::TrueColor => {
416 rusty_colorprofile::Profile::TrueColor
417 }
418 rusty_ultraviolet::terminal_screen::ColorProfile::Ansi256 => {
419 rusty_colorprofile::Profile::Ansi256
420 }
421 rusty_ultraviolet::terminal_screen::ColorProfile::Ansi => {
422 rusty_colorprofile::Profile::Ansi
423 }
424 _ => rusty_colorprofile::Profile::NoTty,
425 });
426 }
427
428 let (hard_tabs, backspace) = check_optimized_movements();
429 let map_nl = false;
432 self.renderer
433 .lock()
434 .unwrap()
435 .set_optimizations(hard_tabs, backspace, map_nl);
436
437 let (tx, rx): (MsgChannel, MsgReceiver) = channel();
438 self.msg_tx = Some(tx.clone());
439
440 let external_ctx = self.options.context.clone();
441
442 let input_tx = tx.clone();
446 thread::spawn(move || {
447 let reader: Box<dyn std::io::Read + Send> = Box::new(std::io::stdin());
448 let mut tr =
449 rusty_ultraviolet::terminal_reader::new_terminal_reader(reader, "xterm-256color");
450 tr.set_legacy(rusty_ultraviolet::LegacyKeyEncoding::default());
451 let (dec_tx, dec_rx) = std::sync::mpsc::channel::<rusty_ultraviolet::DecodedEvent>();
452 let streamer = std::thread::spawn(move || {
453 let _ = tr.stream_events(&dec_tx);
454 });
455 for ev in dec_rx {
456 if let Some(msg) = decoded_to_msg(ev) {
457 if input_tx.send(msg).is_err() {
458 break;
459 }
460 }
461 }
462 let _ = streamer.join();
463 });
464
465 if let Some(cmd) = self.model.init() {
467 let tx_clone = tx.clone();
468 thread::spawn(move || {
469 if let Some(msg) = cmd() {
470 let _ = tx_clone.send(msg);
471 }
472 });
473 }
474
475 if let Ok((w, h)) = term_size() {
477 let _ = tx.send(Box::new(WindowSizeMsg {
478 width: w as usize,
479 height: h as usize,
480 }));
481 }
482
483 let _ = tx.send(Box::new(EnvMsg::from_std()));
485
486 {
489 use std::os::fd::AsRawFd as _;
490 let env = rusty_ultraviolet::Environ(
491 std::env::vars().map(|(k, v)| format!("{k}={v}")).collect(),
492 );
493 let profile = rusty_ultraviolet::terminal_screen::detect_color_profile(
494 Some(std::io::stdout().as_raw_fd()),
495 &env,
496 );
497 let msg_profile = match profile {
498 rusty_ultraviolet::terminal_screen::ColorProfile::TrueColor => {
499 crate::profile::ColorProfile::TrueColor
500 }
501 rusty_ultraviolet::terminal_screen::ColorProfile::Ansi256 => {
502 crate::profile::ColorProfile::ANSI256
503 }
504 rusty_ultraviolet::terminal_screen::ColorProfile::Ansi => {
505 crate::profile::ColorProfile::ANSI
506 }
507 _ => crate::profile::ColorProfile::Ascii,
508 };
509 let _ = tx.send(Box::new(crate::profile::ColorProfileMsg {
510 profile: msg_profile,
511 }));
512 }
513
514 let query_sync = should_query_synchronized_output();
519 self.startup_buf = Arc::new(Mutex::new(if query_sync {
520 Some(b"\x1b[?2026$p\x1b[?2027$p".to_vec())
521 } else {
522 None
523 }));
524 let startup_buf = self.startup_buf.clone();
525
526 let initial_view = self.model.view();
530 self.renderer.lock().unwrap().render(initial_view);
531
532 let tick_renderer = self.renderer.clone();
535 let done = self.finished.clone();
536 let tick_buf = startup_buf.clone();
537 thread::spawn(move || {
538 let interval = Duration::from_millis(1000 / 60);
539 while !done.load(Ordering::SeqCst) {
540 thread::sleep(interval);
541 if let Some(buf) = tick_buf.lock().unwrap().take() {
542 use std::io::Write as _;
543 let _ = std::io::stdout().write_all(&buf);
544 }
545 let _ = tick_renderer.lock().unwrap().flush(false);
546 }
547 });
548
549 let result = loop {
551 if let Some(ctx) = &external_ctx {
553 if ctx.done() {
554 break Err(ProgramError::Killed);
555 }
556 }
557 match rx.recv_timeout(Duration::from_millis(50)) {
558 Ok(msg) => match self.handle_msg(msg, &tx) {
559 Ok(true) => break Ok(()),
560 Ok(false) => continue,
561 Err(e) => break Err(e),
562 },
563 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
564 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break Ok(()),
565 }
566 };
567
568 self.finished.store(true, Ordering::SeqCst);
569 let final_view = self.model.view();
572 self.renderer.lock().unwrap().render(final_view);
573 let _fr = self.renderer.lock().unwrap().flush(true);
579 let _cr = self.renderer.lock().unwrap().close();
580 let _ = disable_raw_mode();
581
582 match result {
583 Ok(()) => Ok(self.model),
584 Err(e) => Err(Box::new(e)),
585 }
586 }
587}
588
589fn exec_batch_msg(batch: BatchMsg, tx: &MsgChannel) {
597 let handles: Vec<_> = batch
598 .0
599 .into_iter()
600 .flatten()
601 .map(|cmd| {
602 let tx = tx.clone();
603 thread::spawn(move || {
604 if let Some(msg) = cmd() {
605 dispatch_msg(msg, &tx);
606 }
607 })
608 })
609 .collect();
610 for handle in handles {
611 let _ = handle.join();
612 }
613}
614
615fn exec_sequence_msg(seq: SequenceMsg, tx: &MsgChannel) {
621 for cmd in seq.0.into_iter().flatten() {
622 if let Some(msg) = cmd() {
623 dispatch_msg(msg, tx);
624 }
625 }
626}
627
628fn dispatch_msg(msg: Box<dyn Msg>, tx: &MsgChannel) {
633 if msg.as_ref().as_any().is::<BatchMsg>() {
634 let any = msg.into_any();
635 let batch = *any.downcast::<BatchMsg>().unwrap();
636 exec_batch_msg(batch, tx);
637 } else if msg.as_ref().as_any().is::<SequenceMsg>() {
638 let any = msg.into_any();
639 let seq = *any.downcast::<SequenceMsg>().unwrap();
640 exec_sequence_msg(seq, tx);
641 } else {
642 let _ = tx.send(msg);
643 }
644}
645
646fn should_query_synchronized_output() -> bool {
650 let term_type = std::env::var("TERM").unwrap_or_default();
651 let term_prog = std::env::var("TERM_PROGRAM").ok();
652 let ssh_tty = std::env::var("SSH_TTY").is_ok();
653 let wt_session = std::env::var("WT_SESSION").is_ok();
654
655 let ok_term_prog = term_prog.is_some();
656 wt_session
657 || term_type.contains("ghostty")
658 || term_type.contains("wezterm")
659 || (!ok_term_prog && !ssh_tty)
660 || (!ssh_tty && !term_prog.as_deref().unwrap_or("").contains("Apple"))
661 || term_type.contains("alacritty")
662 || term_type.contains("kitty")
663 || term_type.contains("rio")
664}
665
666fn decoded_to_msg(ev: rusty_ultraviolet::DecodedEvent) -> Option<Box<dyn Msg>> {
669 use rusty_ultraviolet::DecodedEvent as D;
670 match ev {
671 D::KeyPress(k) => Some(Box::new(KeyPressMsg(uv_key_to_key(k)))),
672 D::KeyRelease(k) => Some(Box::new(KeyReleaseMsg(uv_key_to_key(k)))),
673 D::MouseClick(m) => Some(Box::new(MouseClickMsg(uv_mouse_to_mouse(m)))),
674 D::MouseRelease(m) => Some(Box::new(MouseReleaseMsg(uv_mouse_to_mouse(m)))),
675 D::MouseWheel(m) => Some(Box::new(MouseWheelMsg(uv_mouse_to_mouse(m)))),
676 D::MouseMotion(m) => Some(Box::new(MouseMotionMsg(uv_mouse_to_mouse(m)))),
677 D::WindowSize(s) => Some(Box::new(WindowSizeMsg {
678 width: s.width,
679 height: s.height,
680 })),
681 D::Paste(s) => Some(Box::new(PasteMsg { content: s })),
682 D::Focus => Some(Box::new(FocusMsg)),
683 D::Blur => Some(Box::new(BlurMsg)),
684 D::KeyboardEnhancements(flags) => Some(Box::new(KeyboardEnhancementsMsg { flags })),
685 D::CursorPosition { x, y } => Some(Box::new(CursorPositionMsg {
686 x: x.max(0) as usize,
687 y: y.max(0) as usize,
688 })),
689 D::Capability(s) => Some(Box::new(crate::termcap::CapabilityMsg { content: s })),
693 D::TerminalVersion(s) => Some(Box::new(crate::xterm::TerminalVersionMsg { name: s })),
694 D::ForegroundColor(Some(c)) => Some(Box::new(crate::color::ForegroundColorMsg(c))),
695 D::BackgroundColor(Some(c)) => Some(Box::new(crate::color::BackgroundColorMsg(c))),
696 D::CursorColor(Some(c)) => Some(Box::new(crate::color::CursorColorMsg(c))),
697 _ => None,
698 }
699}
700
701fn uv_key_to_key(k: rusty_ultraviolet::Key) -> crate::key::Key {
703 use rusty_ultraviolet::key as uvk;
704 let code = match k.code {
705 uvk::KEY_UP => crate::key::KEY_UP,
706 uvk::KEY_DOWN => crate::key::KEY_DOWN,
707 uvk::KEY_LEFT => crate::key::KEY_LEFT,
708 uvk::KEY_RIGHT => crate::key::KEY_RIGHT,
709 uvk::KEY_PG_UP => crate::key::KEY_PG_UP,
710 uvk::KEY_PG_DOWN => crate::key::KEY_PG_DOWN,
711 uvk::KEY_HOME => crate::key::KEY_HOME,
712 uvk::KEY_END => crate::key::KEY_END,
713 uvk::KEY_ENTER => crate::key::KEY_ENTER,
714 uvk::KEY_TAB => crate::key::KEY_TAB,
715 uvk::KEY_BACKSPACE => crate::key::KEY_BACKSPACE,
716 uvk::KEY_ESCAPE => crate::key::KEY_ESCAPE,
717 uvk::KEY_SPACE => crate::key::KEY_SPACE,
718 _ => char::from_u32(k.code).unwrap_or('\0'),
721 };
722 crate::key::Key {
723 text: k.text.clone(),
724 mod_keys: KeyMod(k.mod_.0 as u8),
725 code,
726 shifted_code: char::from_u32(k.shifted_code),
727 base_code: char::from_u32(k.base_code),
728 is_repeat: k.is_repeat,
729 }
730}
731
732fn uv_mouse_to_mouse(m: rusty_ultraviolet::Mouse) -> crate::mouse::Mouse {
734 let button = match m.button.0 {
735 0 => crate::mouse::MouseButton::MouseNone,
736 1 => crate::mouse::MouseButton::MouseLeft,
737 2 => crate::mouse::MouseButton::MouseMiddle,
738 3 => crate::mouse::MouseButton::MouseRight,
739 4 => crate::mouse::MouseButton::MouseWheelUp,
740 5 => crate::mouse::MouseButton::MouseWheelDown,
741 6 => crate::mouse::MouseButton::MouseWheelLeft,
742 7 => crate::mouse::MouseButton::MouseWheelRight,
743 8 => crate::mouse::MouseButton::MouseBackward,
744 9 => crate::mouse::MouseButton::MouseForward,
745 10 => crate::mouse::MouseButton::MouseButton10,
746 _ => crate::mouse::MouseButton::MouseButton11,
747 };
748 crate::mouse::Mouse {
749 x: m.x.max(0) as usize,
750 y: m.y.max(0) as usize,
751 button,
752 mod_keys: KeyMod(m.mod_.0 as u8),
753 }
754}
755
756fn check_optimized_movements() -> (bool, bool) {
759 use std::os::fd::AsRawFd;
760 #[cfg(unix)]
761 {
762 let fd = std::io::stdin().as_raw_fd();
763 let mut t: libc::termios = unsafe { std::mem::zeroed() };
764 if unsafe { libc::tcgetattr(fd, &mut t) } != 0 {
765 return (false, false);
766 }
767 let hard_tabs = t.c_oflag & libc::TABDLY == libc::TAB0;
768 #[cfg(target_os = "macos")]
769 let backspace = t.c_lflag & libc::BSDLY == libc::BS0;
770 #[cfg(not(target_os = "macos"))]
771 let backspace = false;
772 (hard_tabs, backspace)
773 }
774 #[cfg(not(unix))]
775 {
776 let _ = ();
777 (true, true)
778 }
779}