1use std::{
4 io::{self, IsTerminal, Write},
5 time::Duration,
6};
7
8use crate::{
9 ClearRegion, Color, Command, CommandWriter, CursorAppearance, CursorMove, Event, EventSource,
10 FocusChange, KeyCode, KeyEvent, KeyEventState, KeyKind, KeyboardEnhancementFlags,
11 KeyboardEnhancementQuery, MediaKeyCode, ModifierKeyCode, Modifiers, MouseButton, MouseEvent,
12 MouseKind, PixelSize, Position, RawModeControl, TerminalOutput, TerminalQuery, TerminalSize,
13 TerminalStyle, TextAttribute, TextAttributes, Underline, UnderlineStyle, WindowSize,
14 command::write_control_string,
15};
16use crossterm::{
17 cursor,
18 event::{
19 self, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
20 EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent,
21 KeyCode as CrosstermKeyCode, KeyEventKind, KeyEventState as CrosstermKeyEventState,
22 KeyModifiers, KeyboardEnhancementFlags as CrosstermKeyboardEnhancementFlags,
23 MediaKeyCode as CrosstermMediaKeyCode, ModifierKeyCode as CrosstermModifierKeyCode,
24 MouseButton as CrosstermMouseButton, MouseEventKind, PopKeyboardEnhancementFlags,
25 PushKeyboardEnhancementFlags,
26 },
27 queue,
28 style::{
29 Attribute, Color as CrosstermColor, Print, ResetColor, SetAttribute, SetBackgroundColor,
30 SetForegroundColor, SetUnderlineColor,
31 },
32 terminal::{
33 self, BeginSynchronizedUpdate, Clear, ClearType, DisableLineWrap, EnableLineWrap,
34 EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, ScrollDown, ScrollUp,
35 SetSize, SetTitle,
36 },
37};
38
39pub struct CrosstermBackend<W> {
41 writer: W,
42}
43
44impl<W> CrosstermBackend<W> {
45 pub const fn new(writer: W) -> Self {
46 Self { writer }
47 }
48
49 pub const fn writer(&self) -> &W {
50 &self.writer
51 }
52
53 pub fn writer_mut(&mut self) -> &mut W {
54 &mut self.writer
55 }
56
57 pub fn into_inner(self) -> W {
58 self.writer
59 }
60}
61
62impl<W: Write> CommandWriter for CrosstermBackend<W> {
63 fn write_command(&mut self, command: Command<'_>) -> io::Result<()> {
64 match command {
65 Command::MoveCursor(movement) => write_cursor_move(&mut self.writer, movement),
66 Command::SaveCursorPosition => queue!(self.writer, cursor::SavePosition),
67 Command::RestoreCursorPosition => queue!(self.writer, cursor::RestorePosition),
68 Command::SetCursorVisible(true) => queue!(self.writer, cursor::Show),
69 Command::SetCursorVisible(false) => queue!(self.writer, cursor::Hide),
70 Command::SetCursorBlinking(true) => queue!(self.writer, cursor::EnableBlinking),
71 Command::SetCursorBlinking(false) => queue!(self.writer, cursor::DisableBlinking),
72 Command::SetCursorAppearance(appearance) => {
73 queue!(self.writer, convert_cursor_appearance(appearance))
74 }
75 Command::SetAlternateScreen(true) => queue!(self.writer, EnterAlternateScreen),
76 Command::SetAlternateScreen(false) => queue!(self.writer, LeaveAlternateScreen),
77 Command::SetBracketedPaste(true) => queue!(self.writer, EnableBracketedPaste),
78 Command::SetBracketedPaste(false) => queue!(self.writer, DisableBracketedPaste),
79 Command::SetFocusReporting(true) => queue!(self.writer, EnableFocusChange),
80 Command::SetFocusReporting(false) => queue!(self.writer, DisableFocusChange),
81 Command::SetMouseCapture(true) => queue!(self.writer, EnableMouseCapture),
82 Command::SetMouseCapture(false) => queue!(self.writer, DisableMouseCapture),
83 Command::PushKeyboardEnhancement(flags) => queue!(
84 self.writer,
85 PushKeyboardEnhancementFlags(convert_keyboard_enhancement_flags(flags))
86 ),
87 Command::PopKeyboardEnhancement => {
88 queue!(self.writer, PopKeyboardEnhancementFlags)
89 }
90 Command::Clear(region) => queue!(self.writer, Clear(convert_clear_region(region))),
91 Command::Scroll(rows) if rows > 0 => {
92 queue!(self.writer, ScrollUp(offset(rows.unsigned_abs())?))
93 }
94 Command::Scroll(rows) if rows < 0 => {
95 queue!(self.writer, ScrollDown(offset(rows.unsigned_abs())?))
96 }
97 Command::Scroll(_) => Ok(()),
98 Command::SetSize(size) => queue!(
99 self.writer,
100 SetSize(coordinate(size.columns())?, coordinate(size.rows())?)
101 ),
102 Command::SetTitle(title) => queue!(self.writer, SetTitle(title.as_str())),
103 Command::SetLineWrap(true) => queue!(self.writer, EnableLineWrap),
104 Command::SetLineWrap(false) => queue!(self.writer, DisableLineWrap),
105 Command::SetSynchronizedUpdate(true) => queue!(self.writer, BeginSynchronizedUpdate),
106 Command::SetSynchronizedUpdate(false) => queue!(self.writer, EndSynchronizedUpdate),
107 Command::SetStyle(style) => write_style(&mut self.writer, style),
108 Command::ResetStyle => queue!(self.writer, SetAttribute(Attribute::Reset), ResetColor),
109 Command::SetHyperlink(Some(link)) => write_hyperlink_start(&mut self.writer, link),
110 Command::SetHyperlink(None) => super::ansi::write_hyperlink_end(&mut self.writer),
111 Command::ApplicationProgram(payload) => {
112 write_control_string(&mut self.writer, b"\x1b_", payload)
113 }
114 Command::DeviceControl(payload) => {
115 write_control_string(&mut self.writer, b"\x1bP", payload)
116 }
117 Command::LineFeed => self.writer.write_all(b"\n"),
118 Command::CarriageReturnLineFeed => self.writer.write_all(b"\r\n"),
119 Command::Print(text) => queue!(self.writer, Print(text.as_str())),
120 }
121 }
122}
123
124impl<W: Write> TerminalOutput for CrosstermBackend<W> {
125 fn flush(&mut self) -> io::Result<()> {
126 self.writer.flush()
127 }
128}
129
130impl<W: Write + IsTerminal> RawModeControl for CrosstermBackend<W> {
131 fn is_interactive(&self) -> bool {
132 io::stdin().is_terminal() && self.writer.is_terminal()
133 }
134
135 fn enable_raw_mode(&mut self) -> io::Result<()> {
136 terminal::enable_raw_mode()
137 }
138
139 fn disable_raw_mode(&mut self) -> io::Result<()> {
140 terminal::disable_raw_mode()
141 }
142}
143
144impl<W> TerminalQuery for CrosstermBackend<W> {
145 fn terminal_size(&mut self) -> io::Result<TerminalSize> {
146 let (columns, rows) = terminal::size()?;
147 Ok(TerminalSize::new(usize::from(columns), usize::from(rows)))
148 }
149
150 fn cursor_position(&mut self) -> io::Result<Position> {
151 let (column, row) = cursor::position()?;
152 Ok(Position::new(usize::from(column), usize::from(row)))
153 }
154
155 fn window_size(&mut self) -> io::Result<WindowSize> {
156 match terminal::window_size() {
157 Ok(size) => Ok(convert_window_size(size)),
158 Err(error) if error.kind() == io::ErrorKind::Unsupported => self
159 .terminal_size()
160 .map(|cells| WindowSize::new(cells, None)),
161 Err(error) => Err(error),
162 }
163 }
164
165 fn raw_mode_enabled(&mut self) -> io::Result<bool> {
166 terminal::is_raw_mode_enabled()
167 }
168}
169
170impl<W> KeyboardEnhancementQuery for CrosstermBackend<W> {
171 fn supports_keyboard_enhancement(&mut self) -> io::Result<bool> {
172 terminal::supports_keyboard_enhancement()
173 }
174}
175
176fn convert_window_size(size: terminal::WindowSize) -> WindowSize {
177 let cells = TerminalSize::new(usize::from(size.columns), usize::from(size.rows));
178 let pixels = (size.width != 0 && size.height != 0)
179 .then(|| PixelSize::new(usize::from(size.width), usize::from(size.height)));
180 WindowSize::new(cells, pixels)
181}
182
183impl<W> EventSource for CrosstermBackend<W> {
184 fn read_event(&mut self) -> io::Result<Event> {
185 Ok(convert_event(event::read()?))
186 }
187
188 fn poll_event(&mut self) -> io::Result<Option<Event>> {
189 self.poll_event_timeout(Duration::ZERO)
190 }
191
192 fn poll_event_timeout(&mut self, timeout: Duration) -> io::Result<Option<Event>> {
193 if event::poll(timeout)? {
194 Ok(Some(convert_event(event::read()?)))
195 } else {
196 Ok(None)
197 }
198 }
199}
200
201fn convert_keyboard_enhancement_flags(
202 requested: KeyboardEnhancementFlags,
203) -> CrosstermKeyboardEnhancementFlags {
204 let mut flags = CrosstermKeyboardEnhancementFlags::empty();
205 if requested.contains(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) {
206 flags |= CrosstermKeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES;
207 }
208 if requested.contains(KeyboardEnhancementFlags::REPORT_EVENT_TYPES) {
209 flags |= CrosstermKeyboardEnhancementFlags::REPORT_EVENT_TYPES;
210 }
211 if requested.contains(KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS) {
212 flags |= CrosstermKeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS;
213 }
214 if requested.contains(KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES) {
215 flags |= CrosstermKeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
216 }
217 flags
218}
219
220fn coordinate(value: usize) -> io::Result<u16> {
221 u16::try_from(value).map_err(|_| {
222 io::Error::new(
223 io::ErrorKind::InvalidInput,
224 "terminal coordinate exceeds u16",
225 )
226 })
227}
228
229fn offset(value: u32) -> io::Result<u16> {
230 u16::try_from(value)
231 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "terminal offset exceeds u16"))
232}
233
234fn write_cursor_move(writer: &mut impl Write, movement: CursorMove) -> io::Result<()> {
235 match movement {
236 CursorMove::To(position) => queue!(
237 writer,
238 cursor::MoveTo(coordinate(position.column())?, coordinate(position.row())?)
239 ),
240 CursorMove::ToColumn(column) => queue!(writer, cursor::MoveToColumn(coordinate(column)?)),
241 CursorMove::ToRow(row) => queue!(writer, cursor::MoveToRow(coordinate(row)?)),
242 CursorMove::By { columns, rows } => {
243 if rows < 0 {
244 queue!(writer, cursor::MoveUp(offset(rows.unsigned_abs())?))?;
245 } else if rows > 0 {
246 queue!(writer, cursor::MoveDown(offset(rows.unsigned_abs())?))?;
247 }
248 if columns < 0 {
249 queue!(writer, cursor::MoveLeft(offset(columns.unsigned_abs())?))
250 } else if columns > 0 {
251 queue!(writer, cursor::MoveRight(offset(columns.unsigned_abs())?))
252 } else {
253 Ok(())
254 }
255 }
256 CursorMove::ToNextLine(lines) => {
257 queue!(writer, cursor::MoveToNextLine(coordinate(lines)?))
258 }
259 CursorMove::ToPreviousLine(lines) => {
260 queue!(writer, cursor::MoveToPreviousLine(coordinate(lines)?))
261 }
262 }
263}
264
265fn convert_cursor_appearance(appearance: CursorAppearance) -> cursor::SetCursorStyle {
266 match appearance {
267 CursorAppearance::UserDefault => cursor::SetCursorStyle::DefaultUserShape,
268 CursorAppearance::BlinkingBlock => cursor::SetCursorStyle::BlinkingBlock,
269 CursorAppearance::SteadyBlock => cursor::SetCursorStyle::SteadyBlock,
270 CursorAppearance::BlinkingUnderline => cursor::SetCursorStyle::BlinkingUnderScore,
271 CursorAppearance::SteadyUnderline => cursor::SetCursorStyle::SteadyUnderScore,
272 CursorAppearance::BlinkingBar => cursor::SetCursorStyle::BlinkingBar,
273 CursorAppearance::SteadyBar => cursor::SetCursorStyle::SteadyBar,
274 }
275}
276
277fn convert_clear_region(region: ClearRegion) -> ClearType {
278 match region {
279 ClearRegion::Screen => ClearType::All,
280 ClearRegion::ScreenAndScrollback => ClearType::Purge,
281 ClearRegion::BeforeCursor => ClearType::FromCursorUp,
282 ClearRegion::AfterCursor => ClearType::FromCursorDown,
283 ClearRegion::Line => ClearType::CurrentLine,
284 ClearRegion::AfterCursorInLine => ClearType::UntilNewLine,
285 }
286}
287
288fn write_style(writer: &mut impl Write, style: TerminalStyle) -> io::Result<()> {
289 queue!(writer, SetAttribute(Attribute::Reset), ResetColor)?;
290 if let Some(color) = style.foreground {
291 queue!(writer, SetForegroundColor(convert_color(color)))?;
292 }
293 if let Some(color) = style.background {
294 queue!(writer, SetBackgroundColor(convert_color(color)))?;
295 }
296 write_attributes(writer, style.attributes)?;
297 write_underline(writer, style.underline)
298}
299
300fn write_underline(writer: &mut impl Write, underline: Option<Underline>) -> io::Result<()> {
301 let Some(underline) = underline else {
302 return Ok(());
303 };
304 let attribute = match underline.get_style() {
305 UnderlineStyle::Single => Attribute::Underlined,
306 UnderlineStyle::Double => Attribute::DoubleUnderlined,
307 UnderlineStyle::Curly => Attribute::Undercurled,
308 UnderlineStyle::Dotted => Attribute::Underdotted,
309 UnderlineStyle::Dashed => Attribute::Underdashed,
310 };
311 queue!(writer, SetAttribute(attribute))?;
312 if let Some(color) = underline.get_color() {
313 queue!(writer, SetUnderlineColor(convert_color(color)))?;
314 }
315 Ok(())
316}
317
318fn write_hyperlink_start(
319 writer: &mut impl Write,
320 link: crate::TerminalHyperlink<'_>,
321) -> io::Result<()> {
322 super::ansi::write_hyperlink_start(writer, link)
323}
324
325fn convert_color(color: Color) -> CrosstermColor {
326 match color {
327 Color::Ansi(0) => CrosstermColor::Black,
328 Color::Ansi(1) => CrosstermColor::DarkRed,
329 Color::Ansi(2) => CrosstermColor::DarkGreen,
330 Color::Ansi(3) => CrosstermColor::DarkYellow,
331 Color::Ansi(4) => CrosstermColor::DarkBlue,
332 Color::Ansi(5) => CrosstermColor::DarkMagenta,
333 Color::Ansi(6) => CrosstermColor::DarkCyan,
334 Color::Ansi(7) => CrosstermColor::Grey,
335 Color::Ansi(8) => CrosstermColor::DarkGrey,
336 Color::Ansi(9) => CrosstermColor::Red,
337 Color::Ansi(10) => CrosstermColor::Green,
338 Color::Ansi(11) => CrosstermColor::Yellow,
339 Color::Ansi(12) => CrosstermColor::Blue,
340 Color::Ansi(13) => CrosstermColor::Magenta,
341 Color::Ansi(14) => CrosstermColor::Cyan,
342 Color::Ansi(15) => CrosstermColor::White,
343 Color::Ansi(index) | Color::Ansi256(index) => CrosstermColor::AnsiValue(index),
344 Color::Rgb(red, green, blue) => CrosstermColor::Rgb {
345 r: red,
346 g: green,
347 b: blue,
348 },
349 }
350}
351
352fn write_attributes(writer: &mut impl Write, attributes: TextAttributes) -> io::Result<()> {
353 for attribute in attributes {
354 let attribute = match attribute {
355 TextAttribute::Bold => Attribute::Bold,
356 TextAttribute::Dim => Attribute::Dim,
357 TextAttribute::Italic => Attribute::Italic,
358 TextAttribute::SlowBlink => Attribute::SlowBlink,
359 TextAttribute::RapidBlink => Attribute::RapidBlink,
360 TextAttribute::Reversed => Attribute::Reverse,
361 TextAttribute::Hidden => Attribute::Hidden,
362 TextAttribute::CrossedOut => Attribute::CrossedOut,
363 TextAttribute::Fraktur => Attribute::Fraktur,
364 TextAttribute::Framed => Attribute::Framed,
365 TextAttribute::Encircled => Attribute::Encircled,
366 TextAttribute::Overlined => Attribute::OverLined,
367 };
368 queue!(writer, SetAttribute(attribute))?;
369 }
370 Ok(())
371}
372
373fn convert_event(event: CrosstermEvent) -> Event {
374 match event {
375 CrosstermEvent::Key(key) => Event::Key(KeyEvent {
376 code: convert_key_code(key.code),
377 modifiers: convert_modifiers(key.modifiers),
378 kind: match key.kind {
379 KeyEventKind::Press => KeyKind::Press,
380 KeyEventKind::Repeat => KeyKind::Repeat,
381 KeyEventKind::Release => KeyKind::Release,
382 },
383 state: convert_key_state(key.state),
384 }),
385 CrosstermEvent::Mouse(mouse) => Event::Mouse(MouseEvent {
386 kind: match mouse.kind {
387 MouseEventKind::Down(button) => MouseKind::Down(convert_mouse_button(button)),
388 MouseEventKind::Up(button) => MouseKind::Up(convert_mouse_button(button)),
389 MouseEventKind::Drag(button) => MouseKind::Drag(convert_mouse_button(button)),
390 MouseEventKind::Moved => MouseKind::Moved,
391 MouseEventKind::ScrollDown => MouseKind::ScrollDown,
392 MouseEventKind::ScrollUp => MouseKind::ScrollUp,
393 MouseEventKind::ScrollLeft => MouseKind::ScrollLeft,
394 MouseEventKind::ScrollRight => MouseKind::ScrollRight,
395 },
396 position: Position::new(usize::from(mouse.column), usize::from(mouse.row)),
397 modifiers: convert_modifiers(mouse.modifiers),
398 }),
399 CrosstermEvent::Paste(text) => Event::Paste(text),
400 CrosstermEvent::FocusGained => Event::Focus(FocusChange::Gained),
401 CrosstermEvent::FocusLost => Event::Focus(FocusChange::Lost),
402 CrosstermEvent::Resize(columns, rows) => {
403 Event::Resize(TerminalSize::new(usize::from(columns), usize::from(rows)))
404 }
405 }
406}
407
408fn convert_key_code(code: CrosstermKeyCode) -> KeyCode {
409 match code {
410 CrosstermKeyCode::Char(character) => KeyCode::Char(character),
411 CrosstermKeyCode::Enter => KeyCode::Enter,
412 CrosstermKeyCode::Tab => KeyCode::Tab,
413 CrosstermKeyCode::BackTab => KeyCode::BackTab,
414 CrosstermKeyCode::Backspace => KeyCode::Backspace,
415 CrosstermKeyCode::Delete => KeyCode::Delete,
416 CrosstermKeyCode::Esc => KeyCode::Escape,
417 CrosstermKeyCode::Insert => KeyCode::Insert,
418 CrosstermKeyCode::Home => KeyCode::Home,
419 CrosstermKeyCode::End => KeyCode::End,
420 CrosstermKeyCode::PageUp => KeyCode::PageUp,
421 CrosstermKeyCode::PageDown => KeyCode::PageDown,
422 CrosstermKeyCode::Up => KeyCode::Up,
423 CrosstermKeyCode::Down => KeyCode::Down,
424 CrosstermKeyCode::Left => KeyCode::Left,
425 CrosstermKeyCode::Right => KeyCode::Right,
426 CrosstermKeyCode::F(number) => KeyCode::Function(number),
427 CrosstermKeyCode::Null => KeyCode::Null,
428 CrosstermKeyCode::CapsLock => KeyCode::CapsLock,
429 CrosstermKeyCode::ScrollLock => KeyCode::ScrollLock,
430 CrosstermKeyCode::NumLock => KeyCode::NumLock,
431 CrosstermKeyCode::PrintScreen => KeyCode::PrintScreen,
432 CrosstermKeyCode::Pause => KeyCode::Pause,
433 CrosstermKeyCode::Menu => KeyCode::Menu,
434 CrosstermKeyCode::KeypadBegin => KeyCode::KeypadBegin,
435 CrosstermKeyCode::Media(code) => KeyCode::Media(convert_media_key(code)),
436 CrosstermKeyCode::Modifier(code) => KeyCode::Modifier(convert_modifier_key(code)),
437 }
438}
439
440fn convert_modifiers(modifiers: KeyModifiers) -> Modifiers {
441 let mut converted = Modifiers::NONE;
442 for (source, target) in [
443 (KeyModifiers::SHIFT, Modifiers::SHIFT),
444 (KeyModifiers::CONTROL, Modifiers::CONTROL),
445 (KeyModifiers::ALT, Modifiers::ALT),
446 (KeyModifiers::SUPER, Modifiers::SUPER),
447 (KeyModifiers::HYPER, Modifiers::HYPER),
448 (KeyModifiers::META, Modifiers::META),
449 ] {
450 if modifiers.contains(source) {
451 converted = converted | target;
452 }
453 }
454 converted
455}
456
457fn convert_key_state(state: CrosstermKeyEventState) -> KeyEventState {
458 let mut converted = KeyEventState::NONE;
459 for (source, target) in [
460 (CrosstermKeyEventState::KEYPAD, KeyEventState::KEYPAD),
461 (CrosstermKeyEventState::CAPS_LOCK, KeyEventState::CAPS_LOCK),
462 (CrosstermKeyEventState::NUM_LOCK, KeyEventState::NUM_LOCK),
463 ] {
464 if state.contains(source) {
465 converted = converted | target;
466 }
467 }
468 converted
469}
470
471fn convert_media_key(code: CrosstermMediaKeyCode) -> MediaKeyCode {
472 match code {
473 CrosstermMediaKeyCode::Play => MediaKeyCode::Play,
474 CrosstermMediaKeyCode::Pause => MediaKeyCode::Pause,
475 CrosstermMediaKeyCode::PlayPause => MediaKeyCode::PlayPause,
476 CrosstermMediaKeyCode::Reverse => MediaKeyCode::Reverse,
477 CrosstermMediaKeyCode::Stop => MediaKeyCode::Stop,
478 CrosstermMediaKeyCode::FastForward => MediaKeyCode::FastForward,
479 CrosstermMediaKeyCode::Rewind => MediaKeyCode::Rewind,
480 CrosstermMediaKeyCode::TrackNext => MediaKeyCode::TrackNext,
481 CrosstermMediaKeyCode::TrackPrevious => MediaKeyCode::TrackPrevious,
482 CrosstermMediaKeyCode::Record => MediaKeyCode::Record,
483 CrosstermMediaKeyCode::LowerVolume => MediaKeyCode::LowerVolume,
484 CrosstermMediaKeyCode::RaiseVolume => MediaKeyCode::RaiseVolume,
485 CrosstermMediaKeyCode::MuteVolume => MediaKeyCode::MuteVolume,
486 }
487}
488
489fn convert_modifier_key(code: CrosstermModifierKeyCode) -> ModifierKeyCode {
490 match code {
491 CrosstermModifierKeyCode::LeftShift => ModifierKeyCode::LeftShift,
492 CrosstermModifierKeyCode::LeftControl => ModifierKeyCode::LeftControl,
493 CrosstermModifierKeyCode::LeftAlt => ModifierKeyCode::LeftAlt,
494 CrosstermModifierKeyCode::LeftSuper => ModifierKeyCode::LeftSuper,
495 CrosstermModifierKeyCode::LeftHyper => ModifierKeyCode::LeftHyper,
496 CrosstermModifierKeyCode::LeftMeta => ModifierKeyCode::LeftMeta,
497 CrosstermModifierKeyCode::RightShift => ModifierKeyCode::RightShift,
498 CrosstermModifierKeyCode::RightControl => ModifierKeyCode::RightControl,
499 CrosstermModifierKeyCode::RightAlt => ModifierKeyCode::RightAlt,
500 CrosstermModifierKeyCode::RightSuper => ModifierKeyCode::RightSuper,
501 CrosstermModifierKeyCode::RightHyper => ModifierKeyCode::RightHyper,
502 CrosstermModifierKeyCode::RightMeta => ModifierKeyCode::RightMeta,
503 CrosstermModifierKeyCode::IsoLevel3Shift => ModifierKeyCode::IsoLevel3Shift,
504 CrosstermModifierKeyCode::IsoLevel5Shift => ModifierKeyCode::IsoLevel5Shift,
505 }
506}
507
508fn convert_mouse_button(button: CrosstermMouseButton) -> MouseButton {
509 match button {
510 CrosstermMouseButton::Left => MouseButton::Left,
511 CrosstermMouseButton::Right => MouseButton::Right,
512 CrosstermMouseButton::Middle => MouseButton::Middle,
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519 use crossterm::event::{KeyEvent as CrosstermKeyEvent, MouseEvent as CrosstermMouseEvent};
520
521 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
522 haystack
523 .windows(needle.len())
524 .any(|window| window == needle)
525 }
526
527 #[test]
528 fn key_kind_code_and_modifiers_are_normalized() {
529 let event = convert_event(CrosstermEvent::Key(CrosstermKeyEvent::new_with_kind(
530 CrosstermKeyCode::Char('x'),
531 KeyModifiers::CONTROL | KeyModifiers::ALT,
532 KeyEventKind::Release,
533 )));
534
535 assert_eq!(
536 event,
537 Event::Key(KeyEvent {
538 code: KeyCode::Char('x'),
539 modifiers: Modifiers::CONTROL.union(Modifiers::ALT),
540 kind: KeyKind::Release,
541 state: KeyEventState::NONE,
542 })
543 );
544 }
545
546 #[test]
547 fn enhanced_keyboard_information_is_preserved() {
548 let event = convert_event(CrosstermEvent::Key(
549 CrosstermKeyEvent::new_with_kind_and_state(
550 CrosstermKeyCode::Modifier(CrosstermModifierKeyCode::RightSuper),
551 KeyModifiers::SHIFT
552 | KeyModifiers::SUPER
553 | KeyModifiers::HYPER
554 | KeyModifiers::META,
555 KeyEventKind::Repeat,
556 CrosstermKeyEventState::KEYPAD
557 | CrosstermKeyEventState::CAPS_LOCK
558 | CrosstermKeyEventState::NUM_LOCK,
559 ),
560 ));
561
562 assert_eq!(
563 event,
564 Event::Key(KeyEvent {
565 code: KeyCode::Modifier(ModifierKeyCode::RightSuper),
566 modifiers: Modifiers::SHIFT
567 .union(Modifiers::SUPER)
568 .union(Modifiers::HYPER)
569 .union(Modifiers::META),
570 kind: KeyKind::Repeat,
571 state: KeyEventState::KEYPAD | KeyEventState::CAPS_LOCK | KeyEventState::NUM_LOCK,
572 })
573 );
574 assert_eq!(
575 convert_key_code(CrosstermKeyCode::Media(CrosstermMediaKeyCode::TrackNext)),
576 KeyCode::Media(MediaKeyCode::TrackNext)
577 );
578 }
579
580 #[test]
581 fn semantic_commands_cover_terminal_control_without_exposing_crossterm() {
582 let mut backend = CrosstermBackend::new(Vec::new());
583 backend
584 .write_command(Command::MoveCursor(CursorMove::By {
585 columns: -2,
586 rows: 3,
587 }))
588 .expect("relative cursor move encodes");
589 backend
590 .write_command(Command::Clear(ClearRegion::ScreenAndScrollback))
591 .expect("scrollback clear encodes");
592 backend
593 .write_command(Command::SetSynchronizedUpdate(true))
594 .expect("synchronized update encodes");
595 backend
596 .write_command(Command::SetSynchronizedUpdate(false))
597 .expect("synchronized update end encodes");
598 backend
599 .write_command(Command::SetHyperlink(Some(crate::TerminalHyperlink {
600 uri: "https://example.invalid/\u{1b}",
601 parameters: &[crate::HyperlinkParameter {
602 key: "id:unsafe",
603 value: "value",
604 }],
605 })))
606 .expect("hyperlink encodes");
607 backend
608 .write_command(Command::ApplicationProgram(
609 crate::ControlString::try_from("Ga=T;AAAA").expect("valid APC payload"),
610 ))
611 .expect("application command encodes");
612
613 let output = backend.into_inner();
614 assert!(contains(&output, b"\x1b[3B\x1b[2D"));
615 assert!(contains(&output, b"\x1b[3J"));
616 assert!(contains(&output, b"id%3Aunsafe=value"));
617 assert!(contains(&output, b"https://example.invalid/%1B"));
618 assert!(contains(&output, b"\x1b_Ga=T;AAAA\x1b\\"));
619 }
620
621 #[test]
622 fn paste_focus_resize_and_mouse_do_not_leak_crossterm_types() {
623 assert_eq!(
624 convert_event(CrosstermEvent::Paste("text".to_owned())),
625 Event::Paste("text".to_owned())
626 );
627 assert_eq!(
628 convert_event(CrosstermEvent::FocusLost),
629 Event::Focus(FocusChange::Lost)
630 );
631 assert_eq!(
632 convert_event(CrosstermEvent::Resize(120, 40)),
633 Event::Resize(TerminalSize::new(120, 40))
634 );
635 assert_eq!(
636 convert_event(CrosstermEvent::Mouse(CrosstermMouseEvent {
637 kind: MouseEventKind::Drag(CrosstermMouseButton::Left),
638 column: 7,
639 row: 9,
640 modifiers: KeyModifiers::SHIFT,
641 })),
642 Event::Mouse(MouseEvent {
643 kind: MouseKind::Drag(MouseButton::Left),
644 position: Position::new(7, 9),
645 modifiers: Modifiers::SHIFT,
646 })
647 );
648 }
649
650 #[test]
651 fn absent_pixel_geometry_does_not_erase_cell_geometry() {
652 let size = convert_window_size(terminal::WindowSize {
653 columns: 80,
654 rows: 24,
655 width: 0,
656 height: 0,
657 });
658 assert_eq!(size.cells(), TerminalSize::new(80, 24));
659 assert_eq!(size.pixels(), None);
660
661 let size = convert_window_size(terminal::WindowSize {
662 columns: 80,
663 rows: 24,
664 width: 1600,
665 height: 960,
666 });
667 assert_eq!(size.pixels(), Some(PixelSize::new(1600, 960)));
668 }
669}