1use std::{mem::MaybeUninit, ptr::NonNull};
4
5use crate::{
6 alloc::{Allocator, Object},
7 error::{Error, Result, from_optional_result_uninit, from_result},
8 ffi::{self, TerminalData as Data, TerminalOption as Opt},
9 key,
10 screen::{GridRef, Screen, TrackedGridRef},
11 style::{self, RgbColor},
12};
13
14#[doc(inline)]
15pub use ffi::{SizeReportSize, TerminalScrollbar as Scrollbar};
16
17#[derive(Debug)]
230pub struct Terminal<'alloc: 'cb, 'cb> {
231 pub(crate) inner: Object<'alloc, ffi::TerminalImpl>,
232 vtable: Box<VTable<'alloc, 'cb>>,
235}
236
237#[derive(Clone, Copy, Debug)]
239pub struct Options {
240 pub cols: u16,
242 pub rows: u16,
244 pub max_scrollback: usize,
246}
247
248impl From<Options> for ffi::TerminalOptions {
249 fn from(value: Options) -> Self {
250 Self {
251 cols: value.cols,
252 rows: value.rows,
253 max_scrollback: value.max_scrollback,
254 }
255 }
256}
257
258#[repr(u32)]
260#[derive(Clone, Copy, Debug, PartialEq, Eq, int_enum::IntEnum)]
261#[non_exhaustive]
262pub enum CursorStyle {
263 Bar = ffi::TerminalCursorStyle::BAR,
265 Block = ffi::TerminalCursorStyle::BLOCK,
267 Underline = ffi::TerminalCursorStyle::UNDERLINE,
269 BlockHollow = ffi::TerminalCursorStyle::BLOCK_HOLLOW,
271}
272
273impl<'alloc: 'cb, 'cb> Terminal<'alloc, 'cb> {
274 pub fn new(opts: Options) -> Result<Self> {
276 unsafe { Self::new_inner(std::ptr::null(), opts) }
278 }
279
280 pub fn new_with_alloc<'ctx: 'alloc>(
285 alloc: &'alloc Allocator<'ctx>,
286 opts: Options,
287 ) -> Result<Self> {
288 unsafe { Self::new_inner(alloc.to_raw(), opts) }
290 }
291
292 unsafe fn new_inner(alloc: *const ffi::Allocator, opts: Options) -> Result<Self> {
293 let mut raw: ffi::Terminal = std::ptr::null_mut();
294 let result = unsafe { ffi::ghostty_terminal_new(alloc, &raw mut raw, opts.into()) };
295 from_result(result)?;
296 Ok(Self {
297 inner: Object::new(raw)?,
298 vtable: Box::new(VTable::default()),
299 })
300 }
301
302 pub fn vt_write(&mut self, data: &[u8]) {
316 unsafe { ffi::ghostty_terminal_vt_write(self.inner.as_raw(), data.as_ptr(), data.len()) }
317 }
318
319 pub fn resize(
330 &mut self,
331 cols: u16,
332 rows: u16,
333 cell_width_px: u32,
334 cell_height_px: u32,
335 ) -> Result<()> {
336 let result = unsafe {
337 ffi::ghostty_terminal_resize(
338 self.inner.as_raw(),
339 cols,
340 rows,
341 cell_width_px,
342 cell_height_px,
343 )
344 };
345 from_result(result)
346 }
347
348 pub fn reset(&mut self) {
354 unsafe { ffi::ghostty_terminal_reset(self.inner.as_raw()) }
355 }
356
357 pub fn scroll_viewport(&mut self, scroll: ScrollViewport) {
359 unsafe { ffi::ghostty_terminal_scroll_viewport(self.inner.as_raw(), scroll.into()) }
360 }
361
362 pub fn grid_ref(&self, point: Point) -> Result<GridRef<'_>> {
379 let mut grid_ref = ffi::sized!(ffi::GridRef);
380 let result = unsafe {
381 ffi::ghostty_terminal_grid_ref(self.inner.as_raw(), point.into(), &raw mut grid_ref)
382 };
383 from_result(result)?;
384 Ok(unsafe { GridRef::from_raw(grid_ref) })
385 }
386
387 pub fn track_grid_ref(&self, point: Point) -> Result<TrackedGridRef> {
403 let mut raw: ffi::TrackedGridRef = std::ptr::null_mut();
404 let result = unsafe {
405 ffi::ghostty_terminal_grid_ref_track(self.inner.as_raw(), point.into(), &raw mut raw)
406 };
407 from_result(result)?;
408
409 let inner = NonNull::new(raw).ok_or(Error::InvalidValue)?;
410 Ok(TrackedGridRef::new(inner, self.inner.ptr))
411 }
412
413 pub fn point_from_grid_ref(
429 &self,
430 grid_ref: &GridRef<'_>,
431 space: PointSpace,
432 ) -> Result<Option<PointCoordinate>> {
433 let mut point = MaybeUninit::<ffi::PointCoordinate>::zeroed();
434 let result = unsafe {
435 ffi::ghostty_terminal_point_from_grid_ref(
436 self.inner.as_raw(),
437 std::ptr::from_ref(&grid_ref.inner),
438 space.into_raw(),
439 point.as_mut_ptr(),
440 )
441 };
442
443 from_optional_result_uninit(result, point).map(|value| value.map(Into::into))
444 }
445
446 pub fn mode(&self, mode: Mode) -> Result<bool> {
448 let mut value = false;
449 let result = unsafe {
450 ffi::ghostty_terminal_mode_get(self.inner.as_raw(), mode.into(), &raw mut value)
451 };
452 from_result(result)?;
453 Ok(value)
454 }
455
456 pub fn set_mode(&mut self, mode: Mode, value: bool) -> Result<&mut Self> {
458 let result =
459 unsafe { ffi::ghostty_terminal_mode_set(self.inner.as_raw(), mode.into(), value) };
460 from_result(result)?;
461 Ok(self)
462 }
463
464 pub(crate) fn get<T>(&self, tag: ffi::TerminalData::Type) -> Result<T> {
465 let mut value = MaybeUninit::<T>::zeroed();
466 let result = unsafe {
467 ffi::ghostty_terminal_get(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
468 };
469 from_result(result)?;
470 Ok(unsafe { value.assume_init() })
472 }
473 pub(crate) fn get_optional<T>(&self, tag: ffi::TerminalData::Type) -> Result<Option<T>> {
474 let mut value = MaybeUninit::<T>::zeroed();
475 let result = unsafe {
476 ffi::ghostty_terminal_get(self.inner.as_raw(), tag, value.as_mut_ptr().cast())
477 };
478 from_optional_result_uninit(result, value)
479 }
480 pub(crate) fn set<T>(&self, tag: ffi::TerminalOption::Type, v: &T) -> Result<()> {
481 let result = unsafe {
482 ffi::ghostty_terminal_set(self.inner.as_raw(), tag, std::ptr::from_ref(v).cast())
483 };
484 from_result(result)
485 }
486 pub(crate) fn set_ptr(
489 &self,
490 tag: ffi::TerminalOption::Type,
491 ptr: *const std::ffi::c_void,
492 ) -> Result<()> {
493 let result = unsafe { ffi::ghostty_terminal_set(self.inner.as_raw(), tag, ptr) };
494 from_result(result)
495 }
496 pub(crate) fn set_optional<T>(
497 &self,
498 tag: ffi::TerminalOption::Type,
499 v: Option<&T>,
500 ) -> Result<()> {
501 let ptr = if let Some(v) = v {
502 std::ptr::from_ref(v)
503 } else {
504 std::ptr::null()
505 };
506
507 let result = unsafe { ffi::ghostty_terminal_set(self.inner.as_raw(), tag, ptr.cast()) };
508 from_result(result)
509 }
510
511 pub fn cols(&self) -> Result<u16> {
513 self.get(Data::COLS)
514 }
515 pub fn rows(&self) -> Result<u16> {
517 self.get(Data::ROWS)
518 }
519 pub fn cursor_x(&self) -> Result<u16> {
521 self.get(Data::CURSOR_X)
522 }
523 pub fn cursor_y(&self) -> Result<u16> {
525 self.get(Data::CURSOR_Y)
526 }
527 pub fn is_cursor_pending_wrap(&self) -> Result<bool> {
529 self.get(Data::CURSOR_PENDING_WRAP)
530 }
531 pub fn is_cursor_visible(&self) -> Result<bool> {
533 self.get(Data::CURSOR_VISIBLE)
534 }
535 pub fn cursor_style(&self) -> Result<style::Style> {
539 self.get::<ffi::Style>(Data::CURSOR_STYLE)
540 .and_then(std::convert::TryInto::try_into)
541 }
542 pub fn kitty_keyboard_flags(&self) -> Result<key::KittyKeyFlags> {
544 self.get::<ffi::KittyKeyFlags>(Data::KITTY_KEYBOARD_FLAGS)
545 .map(key::KittyKeyFlags::from_bits_retain)
546 }
547
548 pub fn scrollbar(&self) -> Result<Scrollbar> {
554 self.get(Data::SCROLLBAR)
555 }
556 pub fn active_screen(&self) -> Result<Screen> {
558 self.get(Data::ACTIVE_SCREEN)
559 }
560 pub fn is_mouse_tracking(&self) -> Result<bool> {
565 self.get(Data::MOUSE_TRACKING)
566 }
567 pub fn title(&self) -> Result<&str> {
573 let str = self.get::<ffi::String>(Data::TITLE)?;
574 let str = unsafe { std::slice::from_raw_parts(str.ptr, str.len) };
577 std::str::from_utf8(str).map_err(|_| Error::InvalidValue)
578 }
579
580 pub fn pwd(&self) -> Result<&str> {
586 let str = self.get::<ffi::String>(Data::PWD)?;
587 let str = unsafe { std::slice::from_raw_parts(str.ptr, str.len) };
590 std::str::from_utf8(str).map_err(|_| Error::InvalidValue)
591 }
592 pub fn total_rows(&self) -> Result<usize> {
594 self.get(Data::TOTAL_ROWS)
595 }
596 pub fn scrollback_rows(&self) -> Result<usize> {
598 self.get(Data::SCROLLBACK_ROWS)
599 }
600
601 pub fn fg_color(&self) -> Result<Option<RgbColor>> {
603 self.get_optional::<ffi::ColorRgb>(Data::COLOR_FOREGROUND)
604 .map(|v| v.map(Into::into))
605 }
606 pub fn default_fg_color(&self) -> Result<Option<RgbColor>> {
608 self.get_optional::<ffi::ColorRgb>(Data::COLOR_FOREGROUND_DEFAULT)
609 .map(|v| v.map(Into::into))
610 }
611 pub fn set_default_fg_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
613 self.set_optional(Opt::COLOR_FOREGROUND, v.map(ffi::ColorRgb::from).as_ref())?;
614 Ok(self)
615 }
616
617 pub fn bg_color(&self) -> Result<Option<RgbColor>> {
619 self.get_optional::<ffi::ColorRgb>(Data::COLOR_BACKGROUND)
620 .map(|v| v.map(Into::into))
621 }
622 pub fn default_bg_color(&self) -> Result<Option<RgbColor>> {
624 self.get_optional::<ffi::ColorRgb>(Data::COLOR_BACKGROUND_DEFAULT)
625 .map(|v| v.map(Into::into))
626 }
627 pub fn set_default_bg_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
629 self.set_optional(Opt::COLOR_BACKGROUND, v.map(ffi::ColorRgb::from).as_ref())?;
630 Ok(self)
631 }
632
633 pub fn cursor_color(&self) -> Result<Option<RgbColor>> {
635 self.get_optional::<ffi::ColorRgb>(Data::COLOR_CURSOR)
636 .map(|v| v.map(Into::into))
637 }
638 pub fn default_cursor_color(&self) -> Result<Option<RgbColor>> {
640 self.get_optional::<ffi::ColorRgb>(Data::COLOR_CURSOR_DEFAULT)
641 .map(|v| v.map(Into::into))
642 }
643 pub fn set_default_cursor_color(&mut self, v: Option<RgbColor>) -> Result<&mut Self> {
645 self.set_optional(Opt::COLOR_CURSOR, v.map(ffi::ColorRgb::from).as_ref())?;
646 Ok(self)
647 }
648
649 pub fn set_default_cursor_style(&mut self, v: Option<CursorStyle>) -> Result<&mut Self> {
653 self.set_optional(Opt::DEFAULT_CURSOR_STYLE, v.as_ref())?;
654 Ok(self)
655 }
656
657 pub fn set_default_cursor_blink(&mut self, v: Option<bool>) -> Result<&mut Self> {
661 self.set_optional(Opt::DEFAULT_CURSOR_BLINK, v.as_ref())?;
662 Ok(self)
663 }
664
665 pub fn color_palette(&self) -> Result<[RgbColor; 256]> {
667 self.get::<[ffi::ColorRgb; 256]>(Data::COLOR_PALETTE)
668 .map(|v| v.map(Into::into))
669 }
670 pub fn default_color_palette(&self) -> Result<[RgbColor; 256]> {
672 self.get::<[ffi::ColorRgb; 256]>(Data::COLOR_PALETTE_DEFAULT)
673 .map(|v| v.map(Into::into))
674 }
675 pub fn set_default_color_palette(&mut self, v: Option<[RgbColor; 256]>) -> Result<&mut Self> {
677 self.set_optional(
678 Opt::COLOR_PALETTE,
679 v.map(|v| v.map(ffi::ColorRgb::from)).as_ref(),
680 )?;
681 Ok(self)
682 }
683
684 pub fn set_apc_max_bytes(&mut self, max: Option<usize>) -> Result<&mut Self> {
689 self.set_optional(ffi::TerminalOption::APC_MAX_BYTES, max.as_ref())?;
690 Ok(self)
691 }
692
693 pub fn set_glyph_protocol_enabled(&mut self, enabled: bool) -> Result<&mut Self> {
698 self.set(ffi::TerminalOption::GLYPH_PROTOCOL, &enabled)?;
699 Ok(self)
700 }
701}
702impl Drop for Terminal<'_, '_> {
703 fn drop(&mut self) {
704 unsafe { ffi::ghostty_terminal_free(self.inner.as_raw()) }
705 }
706}
707
708#[derive(Clone, Copy, Debug, PartialEq, Eq)]
710pub enum Point {
711 Active(PointCoordinate),
713 Viewport(PointCoordinate),
715 Screen(PointCoordinate),
717 History(PointCoordinate),
719}
720
721impl From<Point> for ffi::Point {
722 fn from(value: Point) -> Self {
723 match value {
724 Point::Active(coord) => Self {
725 tag: ffi::PointTag::ACTIVE,
726 value: ffi::PointValue {
727 coordinate: coord.into(),
728 },
729 },
730 Point::Viewport(coord) => Self {
731 tag: ffi::PointTag::VIEWPORT,
732 value: ffi::PointValue {
733 coordinate: coord.into(),
734 },
735 },
736 Point::Screen(coord) => Self {
737 tag: ffi::PointTag::SCREEN,
738 value: ffi::PointValue {
739 coordinate: coord.into(),
740 },
741 },
742 Point::History(coord) => Self {
743 tag: ffi::PointTag::HISTORY,
744 value: ffi::PointValue {
745 coordinate: coord.into(),
746 },
747 },
748 }
749 }
750}
751
752#[derive(Clone, Copy, Debug, PartialEq, Eq)]
754pub enum PointSpace {
755 Active,
757 Viewport,
759 Screen,
761 History,
763}
764
765impl PointSpace {
766 pub(crate) fn into_raw(self) -> ffi::PointTag::Type {
767 match self {
768 Self::Active => ffi::PointTag::ACTIVE,
769 Self::Viewport => ffi::PointTag::VIEWPORT,
770 Self::Screen => ffi::PointTag::SCREEN,
771 Self::History => ffi::PointTag::HISTORY,
772 }
773 }
774}
775
776#[derive(Clone, Copy, Debug, PartialEq, Eq)]
778pub struct PointCoordinate {
779 pub x: u16,
781 pub y: u32,
783}
784impl From<PointCoordinate> for ffi::PointCoordinate {
785 fn from(value: PointCoordinate) -> Self {
786 let PointCoordinate { x, y } = value;
787 Self { x, y }
788 }
789}
790impl From<ffi::PointCoordinate> for PointCoordinate {
791 fn from(value: ffi::PointCoordinate) -> Self {
792 let ffi::PointCoordinate { x, y } = value;
793 Self { x, y }
794 }
795}
796
797#[derive(Clone, Copy, Debug, PartialEq, Eq)]
799pub enum ScrollViewport {
800 Top,
802 Bottom,
804 Delta(isize),
806}
807impl From<ScrollViewport> for ffi::TerminalScrollViewport {
808 fn from(value: ScrollViewport) -> Self {
809 match value {
810 ScrollViewport::Top => Self {
811 tag: ffi::TerminalScrollViewportTag::TOP,
812 value: ffi::TerminalScrollViewportValue::default(),
813 },
814 ScrollViewport::Bottom => Self {
815 tag: ffi::TerminalScrollViewportTag::BOTTOM,
816 value: ffi::TerminalScrollViewportValue::default(),
817 },
818 ScrollViewport::Delta(delta) => Self {
819 tag: ffi::TerminalScrollViewportTag::DELTA,
820 value: {
821 let mut v = ffi::TerminalScrollViewportValue::default();
822 v.delta = delta;
823 v
824 },
825 },
826 }
827 }
828}
829
830#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
832pub struct Mode(pub ffi::Mode);
833
834impl Mode {
835 #![expect(missing_docs, reason = "no upstream documentation provided")]
836 const ANSI_BIT: u16 = 1 << 15;
837
838 #[must_use]
840 pub const fn new(v: u16, kind: ModeKind) -> Self {
841 match kind {
842 ModeKind::Ansi => Self(v | Self::ANSI_BIT),
843 ModeKind::Dec => Self(v),
844 }
845 }
846
847 #[must_use]
849 pub const fn value(self) -> u16 {
850 (self.0) & 0x7fff
851 }
852
853 #[must_use]
855 pub const fn kind(self) -> ModeKind {
856 if (self.0) & Self::ANSI_BIT > 0 {
857 ModeKind::Ansi
858 } else {
859 ModeKind::Dec
860 }
861 }
862
863 pub const KAM: Self = Self::new(2, ModeKind::Ansi);
864 pub const INSERT: Self = Self::new(4, ModeKind::Ansi);
865 pub const SRM: Self = Self::new(12, ModeKind::Ansi);
866 pub const LINEFEED: Self = Self::new(20, ModeKind::Ansi);
867
868 pub const DECCKM: Self = Self::new(1, ModeKind::Dec);
869 pub const _132_COLUMN: Self = Self::new(3, ModeKind::Dec);
870 pub const SLOW_SCROLL: Self = Self::new(4, ModeKind::Dec);
871 pub const REVERSE_COLORS: Self = Self::new(5, ModeKind::Dec);
872 pub const ORIGIN: Self = Self::new(6, ModeKind::Dec);
873 pub const WRAPAROUND: Self = Self::new(7, ModeKind::Dec);
874 pub const AUTOREPEAT: Self = Self::new(8, ModeKind::Dec);
875 pub const X10_MOUSE: Self = Self::new(9, ModeKind::Dec);
876 pub const CURSOR_BLINKING: Self = Self::new(12, ModeKind::Dec);
877 pub const CURSOR_VISIBLE: Self = Self::new(25, ModeKind::Dec);
878 pub const ENABLE_MODE3: Self = Self::new(40, ModeKind::Dec);
879 pub const REVERSE_WRAP: Self = Self::new(45, ModeKind::Dec);
880 pub const ALT_SCREEN_LEGACY: Self = Self::new(47, ModeKind::Dec);
881 pub const KEYPAD_KEYS: Self = Self::new(66, ModeKind::Dec);
882 pub const LEFT_RIGHT_MARGIN: Self = Self::new(69, ModeKind::Dec);
883 pub const NORMAL_MOUSE: Self = Self::new(1000, ModeKind::Dec);
884 pub const BUTTON_MOUSE: Self = Self::new(1002, ModeKind::Dec);
885 pub const ANY_MOUSE: Self = Self::new(1003, ModeKind::Dec);
886 pub const FOCUS_EVENT: Self = Self::new(1004, ModeKind::Dec);
887 pub const UTF8_MOUSE: Self = Self::new(1005, ModeKind::Dec);
888 pub const SGR_MOUSE: Self = Self::new(1006, ModeKind::Dec);
889 pub const ALT_SCROLL: Self = Self::new(1007, ModeKind::Dec);
890 pub const URXVT_MOUSE: Self = Self::new(1015, ModeKind::Dec);
891 pub const SGR_PIXELS_MOUSE: Self = Self::new(1016, ModeKind::Dec);
892 pub const NUMLOCK_KEYPAD: Self = Self::new(1035, ModeKind::Dec);
893 pub const ALT_ESC_PREFIX: Self = Self::new(1036, ModeKind::Dec);
894 pub const ALT_SENDS_ESC: Self = Self::new(1039, ModeKind::Dec);
895 pub const REVERSE_WRAP_EXT: Self = Self::new(1045, ModeKind::Dec);
896 pub const ALT_SCREEN: Self = Self::new(1047, ModeKind::Dec);
897 pub const SAVE_CURSOR: Self = Self::new(1048, ModeKind::Dec);
898 pub const ALT_SCREEN_SAVE: Self = Self::new(1049, ModeKind::Dec);
899 pub const BRACKETED_PASTE: Self = Self::new(2004, ModeKind::Dec);
900 pub const SYNC_OUTPUT: Self = Self::new(2026, ModeKind::Dec);
901 pub const GRAPHEME_CLUSTER: Self = Self::new(2027, ModeKind::Dec);
902 pub const COLOR_SCHEME_REPORT: Self = Self::new(2031, ModeKind::Dec);
903 pub const IN_BAND_RESIZE: Self = Self::new(2048, ModeKind::Dec);
904}
905
906#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
908pub enum ModeKind {
909 Dec,
911 Ansi,
913}
914
915impl From<Mode> for ffi::Mode {
916 fn from(value: Mode) -> Self {
917 value.0
918 }
919}
920
921#[derive(Debug, Clone, Copy)]
926pub struct DeviceAttributes {
927 pub primary: PrimaryDeviceAttributes,
929 pub secondary: SecondaryDeviceAttributes,
931 pub tertiary: TertiaryDeviceAttributes,
933}
934
935impl From<DeviceAttributes> for ffi::DeviceAttributes {
936 fn from(value: DeviceAttributes) -> Self {
937 Self {
938 primary: value.primary.into(),
939 secondary: value.secondary.into(),
940 tertiary: value.tertiary.into(),
941 }
942 }
943}
944
945#[derive(Debug, Clone, Copy)]
949pub struct PrimaryDeviceAttributes(ffi::DeviceAttributesPrimary);
950
951impl PrimaryDeviceAttributes {
952 #[must_use]
963 pub const fn new(
964 conformance_level: ConformanceLevel,
965 features: &[DeviceAttributeFeature],
966 ) -> Self {
967 assert!(features.len() <= 64);
968
969 let mut f = [0u16; 64];
970 let mut i = 0;
971 while i < features.len() {
972 f[i] = features[i].0;
973 i += 1;
974 }
975
976 Self(ffi::DeviceAttributesPrimary {
977 conformance_level: conformance_level.0,
978 features: f,
979 num_features: features.len(),
980 })
981 }
982}
983
984impl From<PrimaryDeviceAttributes> for ffi::DeviceAttributesPrimary {
985 fn from(value: PrimaryDeviceAttributes) -> Self {
986 value.0
987 }
988}
989
990#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
993pub struct ConformanceLevel(pub u16);
994
995impl ConformanceLevel {
996 #![expect(clippy::doc_markdown, reason = "false positive")]
997 #![expect(missing_docs, reason = "self-explanatory")]
998 pub const VT100: Self = Self(ffi::DA_CONFORMANCE_VT100);
999 pub const VT101: Self = Self(ffi::DA_CONFORMANCE_VT101);
1000 pub const VT102: Self = Self(ffi::DA_CONFORMANCE_VT102);
1001 pub const VT125: Self = Self(ffi::DA_CONFORMANCE_VT125);
1002 pub const VT131: Self = Self(ffi::DA_CONFORMANCE_VT131);
1003 pub const VT132: Self = Self(ffi::DA_CONFORMANCE_VT132);
1004 pub const VT220: Self = Self(ffi::DA_CONFORMANCE_VT220);
1005 pub const VT240: Self = Self(ffi::DA_CONFORMANCE_VT240);
1006 pub const VT320: Self = Self(ffi::DA_CONFORMANCE_VT320);
1007 pub const VT340: Self = Self(ffi::DA_CONFORMANCE_VT340);
1008 pub const VT420: Self = Self(ffi::DA_CONFORMANCE_VT420);
1009 pub const VT510: Self = Self(ffi::DA_CONFORMANCE_VT510);
1010 pub const VT520: Self = Self(ffi::DA_CONFORMANCE_VT520);
1011 pub const VT525: Self = Self(ffi::DA_CONFORMANCE_VT525);
1012 pub const LEVEL_2: Self = Self(ffi::DA_CONFORMANCE_LEVEL_2);
1014 pub const LEVEL_3: Self = Self(ffi::DA_CONFORMANCE_LEVEL_3);
1016 pub const LEVEL_4: Self = Self(ffi::DA_CONFORMANCE_LEVEL_4);
1018 pub const LEVEL_5: Self = Self(ffi::DA_CONFORMANCE_LEVEL_5);
1020}
1021
1022#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1024pub struct DeviceAttributeFeature(pub u16);
1025
1026impl DeviceAttributeFeature {
1027 #![expect(missing_docs, reason = "no upstream documentation provided")]
1028 pub const COLUMNS_132: Self = Self(ffi::DA_FEATURE_COLUMNS_132);
1029 pub const PRINTER: Self = Self(ffi::DA_FEATURE_PRINTER);
1030 pub const REGIS: Self = Self(ffi::DA_FEATURE_REGIS);
1031 pub const SIXEL: Self = Self(ffi::DA_FEATURE_SIXEL);
1032 pub const SELECTIVE_ERASE: Self = Self(ffi::DA_FEATURE_SELECTIVE_ERASE);
1033 pub const USER_DEFINED_KEYS: Self = Self(ffi::DA_FEATURE_USER_DEFINED_KEYS);
1034 pub const NATIONAL_REPLACEMENT: Self = Self(ffi::DA_FEATURE_NATIONAL_REPLACEMENT);
1035 pub const TECHNICAL_CHARACTERS: Self = Self(ffi::DA_FEATURE_TECHNICAL_CHARACTERS);
1036 pub const LOCATOR: Self = Self(ffi::DA_FEATURE_LOCATOR);
1037 pub const TERMINAL_STATE: Self = Self(ffi::DA_FEATURE_TERMINAL_STATE);
1038 pub const WINDOWING: Self = Self(ffi::DA_FEATURE_WINDOWING);
1039 pub const HORIZONTAL_SCROLLING: Self = Self(ffi::DA_FEATURE_HORIZONTAL_SCROLLING);
1040 pub const ANSI_COLOR: Self = Self(ffi::DA_FEATURE_ANSI_COLOR);
1041 pub const RECTANGULAR_EDITING: Self = Self(ffi::DA_FEATURE_RECTANGULAR_EDITING);
1042 pub const ANSI_TEXT_LOCATOR: Self = Self(ffi::DA_FEATURE_ANSI_TEXT_LOCATOR);
1043 pub const CLIPBOARD: Self = Self(ffi::DA_FEATURE_CLIPBOARD);
1044}
1045
1046#[derive(Debug, Copy, Clone)]
1051pub struct SecondaryDeviceAttributes {
1052 pub device_type: DeviceType,
1054 pub firmware_version: u16,
1056 pub rom_cartridge: u16,
1058}
1059
1060impl From<SecondaryDeviceAttributes> for ffi::DeviceAttributesSecondary {
1061 fn from(value: SecondaryDeviceAttributes) -> Self {
1062 Self {
1063 device_type: value.device_type.0,
1064 firmware_version: value.firmware_version,
1065 rom_cartridge: value.rom_cartridge,
1066 }
1067 }
1068}
1069
1070#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1072pub struct DeviceType(pub u16);
1073
1074impl DeviceType {
1075 #![expect(missing_docs, reason = "self-explanatory")]
1076 pub const VT100: Self = Self(ffi::DA_DEVICE_TYPE_VT100);
1077 pub const VT220: Self = Self(ffi::DA_DEVICE_TYPE_VT220);
1078 pub const VT240: Self = Self(ffi::DA_DEVICE_TYPE_VT240);
1079 pub const VT330: Self = Self(ffi::DA_DEVICE_TYPE_VT330);
1080 pub const VT340: Self = Self(ffi::DA_DEVICE_TYPE_VT340);
1081 pub const VT320: Self = Self(ffi::DA_DEVICE_TYPE_VT320);
1082 pub const VT382: Self = Self(ffi::DA_DEVICE_TYPE_VT382);
1083 pub const VT420: Self = Self(ffi::DA_DEVICE_TYPE_VT420);
1084 pub const VT510: Self = Self(ffi::DA_DEVICE_TYPE_VT510);
1085 pub const VT520: Self = Self(ffi::DA_DEVICE_TYPE_VT520);
1086 pub const VT525: Self = Self(ffi::DA_DEVICE_TYPE_VT525);
1087}
1088
1089#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1094pub struct TertiaryDeviceAttributes {
1095 pub unit_id: u32,
1097}
1098
1099impl From<TertiaryDeviceAttributes> for ffi::DeviceAttributesTertiary {
1100 fn from(value: TertiaryDeviceAttributes) -> Self {
1101 Self {
1102 unit_id: value.unit_id,
1103 }
1104 }
1105}
1106
1107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1109#[repr(u32)]
1110#[expect(missing_docs, reason = "self-explanatory")]
1111pub enum ColorScheme {
1112 Light = ffi::ColorScheme::LIGHT,
1113 Dark = ffi::ColorScheme::DARK,
1114}
1115
1116macro_rules! handlers {
1157 {
1158 $(
1159 $(#[$fmeta:meta])*
1160 $vis:vis fn $name:ident(
1161 &mut self,
1162 tag = $tag:ident,
1163 from = $rawfnty:ident( $($rfname:ident: $rfty:ty),*$(,)? ) $(-> $rawrty:ty)?,
1164 $(#[$tmeta:meta])*
1165 to = $(<$lf:lifetime>)? $fnty:ident( $($fty:ty),*$(,)? ) $(-> $rty:ty)?,
1166 ) |$t:ident, $func:ident| $block:block
1167 )*
1168 } => {
1169 impl<'alloc, 'cb> $crate::terminal::Terminal<'alloc, 'cb> {$(
1171 $(#[$fmeta])*
1172 $vis fn $name(&mut self, f: impl $fnty<'alloc, 'cb>) -> $crate::error::Result<&mut Self> {
1175 unsafe extern "C" fn callback(
1176 t: $crate::ffi::Terminal,
1177 ud: *mut std::ffi::c_void,
1178 $($rfname: $rfty),*
1179 ) $(-> $rawrty)? {
1180 let vtable = unsafe { &mut *ud.cast::<VTable<'_, '_>>() };
1187
1188 let obj = $crate::alloc::Object::new(t).expect("received null terminal ptr in callback - this is a bug!");
1189 let mut term = ::core::mem::ManuallyDrop::new($crate::terminal::Terminal::<'_, '_> {
1192 inner: obj,
1193 vtable: ::core::default::Default::default(),
1194 });
1195 let $t: &$crate::terminal::Terminal = &term;
1196 let $func = vtable.$name.as_deref_mut()
1197 .expect("no handler set but callback is still called - this is a bug!");
1198 let ret = $block;
1199
1200 unsafe { ::core::ptr::drop_in_place(&mut term.vtable) };
1205
1206 ret
1207 }
1208
1209 self.vtable.$name = Some(::std::boxed::Box::new(f));
1210
1211 let userdata = std::ptr::from_mut::<VTable<'alloc, 'cb>>(self.vtable.as_mut())
1218 as *const ::std::ffi::c_void;
1219 self.set_ptr($crate::ffi::TerminalOption::USERDATA, userdata)?;
1220
1221 let callback_ptr: unsafe extern "C" fn(
1225 $crate::ffi::Terminal,
1226 *mut ::std::ffi::c_void,
1227 $($rfty),*
1228 ) $(-> $rawrty)? = callback;
1229
1230 let result = unsafe {
1231 $crate::ffi::ghostty_terminal_set(
1232 self.inner.as_raw(),
1233 $crate::ffi::TerminalOption::$tag,
1234 callback_ptr as *const ::std::ffi::c_void
1235 )
1236 };
1237 $crate::error::from_result(result)?;
1238 Ok(self)
1239 }
1240 )*}
1241 $(
1242 #[doc = concat!(
1243 "[Effect](Terminal#effects) callback type for [`Terminal::",
1244 stringify!($name),
1245 "`](Terminal::",
1246 stringify!($name),
1247 ").\n"
1248 )]
1249 $(#[$tmeta])*
1250 pub trait $fnty<'alloc, 'cb>:
1251 $(for<$lf>)? FnMut(
1252 &$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
1253 $($fty),*
1254 ) $(-> $rty)? + 'cb {}
1255
1256 impl<'alloc, 'cb, F> $fnty<'alloc, 'cb> for F
1257 where
1258 F: $(for<$lf>)? FnMut(
1259 &$($lf)? $crate::terminal::Terminal<'alloc, 'cb>,
1260 $($fty),*
1261 ) $(-> $rty)? + 'cb
1262 {}
1263 )*
1264
1265 struct VTable<'alloc, 'cb> {
1266 $($name: Option<::std::boxed::Box<dyn $fnty<'alloc, 'cb>>>),*
1267 }
1268
1269 impl ::core::fmt::Debug for VTable<'_, '_> {
1270 fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
1271 f.write_str("VTable {..}")
1272 }
1273 }
1274
1275 impl ::core::default::Default for VTable<'_, '_> {
1276 fn default() -> Self {
1277 Self {
1278 $($name: None),*
1279 }
1280 }
1281 }
1282 };
1283}
1284
1285handlers! {
1286 pub fn on_pty_write(
1289 &mut self,
1290 tag = WRITE_PTY,
1291 from = GhosttyTerminalWritePtyFn(ptr: *const u8, len: usize),
1292 to = <'t>PtyWriteFn(&'t [u8]),
1293 ) |term, func| {
1294 let data = unsafe { std::slice::from_raw_parts(ptr, len) };
1298 func(&term, data);
1299 }
1300
1301 pub fn on_bell(
1304 &mut self,
1305 tag = BELL,
1306 from = GhosttyTerminalBellFn(),
1307 to = BellFn(),
1308 ) |term, func| {
1309 func(&term);
1310 }
1311
1312 pub fn on_enquiry(
1315 &mut self,
1316 tag = ENQUIRY,
1317 from = GhosttyTerminalEnquiryFn() -> ffi::String,
1318 to = <'t>EnquiryFn() -> Option<&'t str>,
1319 ) |term, func| {
1320 func(&term).unwrap_or("").into()
1321 }
1322
1323 pub fn on_xtversion(
1327 &mut self,
1328 tag = XTVERSION,
1329 from = GhosttyTerminalXtversionFn() -> ffi::String,
1330 to = <'t>XtversionFn() -> Option<&'t str>,
1331 ) |term, func| {
1332 func(&term).unwrap_or("").into()
1333 }
1334
1335 pub fn on_title_changed(
1341 &mut self,
1342 tag = TITLE_CHANGED,
1343 from = GhosttyTerminalTitleChangedFn(),
1344 to = TitleChangedFn(),
1345 ) |term, func| {
1346 func(&term);
1347 }
1348
1349 pub fn on_pwd_changed(
1355 &mut self,
1356 tag = PWD_CHANGED,
1357 from = GhosttyTerminalPwdChangedFn(),
1358 to = PwdChangedFn(),
1359 ) |term, func| {
1360 func(&term);
1361 }
1362
1363 pub fn on_size(
1366 &mut self,
1367 tag = SIZE,
1368 from = GhosttyTerminalSizeFn(out: *mut ffi::SizeReportSize) -> bool,
1369 to = SizeFn() -> Option<SizeReportSize>,
1370 ) |term, func| {
1371 if let Some(size) = func(&term) {
1372 unsafe { *out = size };
1374 true
1375 } else {
1376 false
1377 }
1378 }
1379
1380 pub fn on_color_scheme(
1386 &mut self,
1387 tag = COLOR_SCHEME,
1388 from = GhosttyTerminalColorSchemeFn(out: *mut ffi::ColorScheme::Type) -> bool,
1389 to = ColorSchemeFn() -> Option<ColorScheme>,
1390 ) |term, func| {
1391 if let Some(size) = func(&term) {
1392 unsafe { *out = size as ffi::ColorScheme::Type };
1394 true
1395 } else {
1396 false
1397 }
1398 }
1399
1400 pub fn on_device_attributes(
1406 &mut self,
1407 tag = DEVICE_ATTRIBUTES,
1408 from = GhosttyTerminalDeviceAttributesFn(out: *mut ffi::DeviceAttributes) -> bool,
1409 to = DeviceAttributesFn() -> Option<DeviceAttributes>,
1410 ) |term, func| {
1411 if let Some(size) = func(&term) {
1412 unsafe { *out = size.into() };
1414 true
1415 } else {
1416 false
1417 }
1418 }
1419}
1420
1421#[cfg(test)]
1422mod tests {
1423 use super::*;
1424 use crate::RenderState;
1425 use crate::render::CursorVisualStyle;
1426 use std::cell::{Cell, RefCell};
1427 use std::mem::ManuallyDrop;
1428
1429 #[inline(never)]
1430 fn build_terminal<'cb>(callback_count: &'cb RefCell<usize>) -> Terminal<'static, 'cb> {
1431 let mut terminal = Terminal::new(Options {
1432 cols: 80,
1433 rows: 24,
1434 max_scrollback: 1000,
1435 })
1436 .expect("terminal should initialize");
1437
1438 terminal
1439 .on_device_attributes(move |_term| {
1440 *callback_count.borrow_mut() += 1;
1441 Some(DeviceAttributes {
1442 primary: PrimaryDeviceAttributes::new(
1443 ConformanceLevel::VT220,
1444 &[DeviceAttributeFeature::ANSI_COLOR],
1445 ),
1446 secondary: SecondaryDeviceAttributes {
1447 device_type: DeviceType::VT220,
1448 firmware_version: 1,
1449 rom_cartridge: 0,
1450 },
1451 tertiary: TertiaryDeviceAttributes { unit_id: 0 },
1452 })
1453 })
1454 .expect("callback should register");
1455
1456 terminal
1457 }
1458
1459 fn relocate_into_new_box<T>(value: T) -> (Box<T>, usize, usize) {
1462 let src = Box::new(ManuallyDrop::new(value));
1465 let src_addr = std::ptr::from_ref(&**src).cast::<T>() as usize;
1466
1467 unsafe {
1468 let dst_layout = std::alloc::Layout::new::<T>();
1469 let dst_ptr = std::alloc::alloc(dst_layout).cast::<T>();
1470 if dst_ptr.is_null() {
1471 std::alloc::handle_alloc_error(dst_layout);
1472 }
1473
1474 let dst_addr = dst_ptr as usize;
1475 assert_ne!(
1476 src_addr, dst_addr,
1477 "test setup failed: source and destination storage unexpectedly match"
1478 );
1479
1480 std::ptr::copy_nonoverlapping(std::ptr::from_ref(&**src).cast::<T>(), dst_ptr, 1);
1484
1485 std::alloc::dealloc(
1489 Box::into_raw(src).cast::<u8>(),
1490 std::alloc::Layout::new::<ManuallyDrop<T>>(),
1491 );
1492
1493 (Box::from_raw(dst_ptr), src_addr, dst_addr)
1496 }
1497 }
1498
1499 #[test]
1502 fn title_changed_callback_returns_correct_title() {
1503 let captured_title: RefCell<String> = RefCell::new(String::new());
1506 let callback_count: Cell<usize> = Cell::new(0);
1507
1508 let mut terminal = Terminal::new(Options {
1509 cols: 80,
1510 rows: 24,
1511 max_scrollback: 0,
1512 })
1513 .expect("terminal should initialize");
1514
1515 terminal
1516 .on_title_changed(|term| {
1517 callback_count.set(callback_count.get() + 1);
1518 let title = term
1519 .title()
1520 .expect("title() should succeed inside callback");
1521 *captured_title.borrow_mut() = title.to_owned();
1522 })
1523 .expect("callback should register");
1524
1525 terminal.vt_write(b"\x1b]2;Hello Effects\x1b\\");
1527 assert_eq!(callback_count.get(), 1);
1528 assert_eq!(*captured_title.borrow(), "Hello Effects");
1529
1530 terminal.vt_write(b"\x1b]2;Second Title\x1b\\");
1532 assert_eq!(callback_count.get(), 2);
1533 assert_eq!(*captured_title.borrow(), "Second Title");
1534 }
1535
1536 #[test]
1539 fn pwd_changed_callback_returns_correct_pwd() {
1540 let captured_pwd: RefCell<String> = RefCell::new(String::new());
1541 let callback_count: Cell<usize> = Cell::new(0);
1542
1543 let mut terminal = Terminal::new(Options {
1544 cols: 80,
1545 rows: 24,
1546 max_scrollback: 0,
1547 })
1548 .expect("terminal should initialize");
1549
1550 terminal
1551 .on_pwd_changed(|term| {
1552 callback_count.set(callback_count.get() + 1);
1553 let pwd = term.pwd().expect("pwd() should succeed inside callback");
1554 *captured_pwd.borrow_mut() = pwd.to_owned();
1555 })
1556 .expect("callback should register");
1557
1558 terminal.vt_write(b"\x1b]7;file://localhost/tmp/project\x1b\\");
1559 assert_eq!(callback_count.get(), 1);
1560 assert_eq!(*captured_pwd.borrow(), "file://localhost/tmp/project");
1561
1562 terminal.vt_write(b"\x1b]7;file://localhost/tmp/other\x1b\\");
1563 assert_eq!(callback_count.get(), 2);
1564 assert_eq!(*captured_pwd.borrow(), "file://localhost/tmp/other");
1565 }
1566
1567 #[test]
1568 fn default_cursor_reset_uses_configured_style_and_blink() {
1569 let mut terminal = Terminal::new(Options {
1570 cols: 80,
1571 rows: 24,
1572 max_scrollback: 0,
1573 })
1574 .expect("terminal should initialize");
1575 let mut render_state = RenderState::new().expect("render state should initialize");
1576
1577 terminal
1578 .set_default_cursor_style(Some(CursorStyle::Underline))
1579 .expect("default cursor style should update")
1580 .set_default_cursor_blink(Some(true))
1581 .expect("default cursor blink should update");
1582
1583 terminal.vt_write(b"\x1b[0 q");
1584 let snapshot = render_state
1585 .update(&terminal)
1586 .expect("render state should update");
1587
1588 assert_eq!(
1589 snapshot
1590 .cursor_visual_style()
1591 .expect("cursor style should be readable"),
1592 CursorVisualStyle::Underline
1593 );
1594 assert!(
1595 snapshot
1596 .cursor_blinking()
1597 .expect("cursor blink should be readable")
1598 );
1599 }
1600
1601 #[test]
1602 fn glyph_protocol_enabled_setting_updates() {
1603 let mut terminal = Terminal::new(Options {
1604 cols: 80,
1605 rows: 24,
1606 max_scrollback: 0,
1607 })
1608 .expect("terminal should initialize");
1609
1610 terminal
1611 .set_glyph_protocol_enabled(false)
1612 .expect("glyph protocol should disable")
1613 .set_glyph_protocol_enabled(true)
1614 .expect("glyph protocol should enable");
1615 }
1616
1617 #[test]
1620 fn callbacks_survive_explicit_relocation() {
1621 let callback_count = RefCell::new(0usize);
1622 let terminal = build_terminal(&callback_count);
1623 let (mut terminal, addr_before, addr_after) = relocate_into_new_box(terminal);
1624 assert_ne!(addr_before, addr_after);
1625
1626 terminal.vt_write(b"\x1b[c");
1628 assert_eq!(*callback_count.borrow(), 1);
1629 }
1630
1631 fn tiny_terminal() -> Terminal<'static, 'static> {
1632 Terminal::new(Options {
1633 cols: 8,
1634 rows: 3,
1635 max_scrollback: 100,
1636 })
1637 .expect("terminal should initialize")
1638 }
1639
1640 fn codepoint_at_tracked_ref(terminal: &Terminal<'_, '_>, tracked: &TrackedGridRef) -> u32 {
1641 let snapshot = tracked
1642 .snapshot(terminal)
1643 .expect("tracked snapshot should not fail")
1644 .expect("tracked ref should have a value");
1645 snapshot
1646 .cell()
1647 .expect("tracked snapshot should resolve to a cell")
1648 .codepoint()
1649 .expect("tracked snapshot cell should expose a codepoint")
1650 }
1651
1652 #[test]
1653 fn tracked_grid_ref_follows_scroll() {
1654 let mut terminal = tiny_terminal();
1655 terminal.vt_write(b"alpha\r\nbravo\r\ncharlie");
1656
1657 let tracked = terminal
1658 .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1659 .expect("tracked grid ref should initialize");
1660
1661 terminal.vt_write(b"\r\ndelta");
1662
1663 assert!(tracked.has_value());
1664 assert_eq!(
1665 codepoint_at_tracked_ref(&terminal, &tracked),
1666 u32::from('a')
1667 );
1668 assert_eq!(
1669 tracked
1670 .point(PointSpace::Screen)
1671 .expect("tracked point should resolve")
1672 .expect("tracked point should have a value")
1673 .x,
1674 0
1675 );
1676 }
1677
1678 #[test]
1679 fn tracked_grid_ref_reports_loss_and_can_set_point() {
1680 let mut terminal = tiny_terminal();
1681 terminal.vt_write(b"alpha\r\nbravo\r\ncharlie");
1682
1683 let mut tracked = terminal
1684 .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1685 .expect("tracked grid ref should initialize");
1686
1687 terminal.reset();
1688
1689 assert!(!tracked.has_value());
1690 assert!(
1691 tracked
1692 .snapshot(&terminal)
1693 .expect("missing tracked snapshot should not fail")
1694 .is_none()
1695 );
1696 assert!(
1697 tracked
1698 .point(PointSpace::Screen)
1699 .expect("missing tracked point should not fail")
1700 .is_none()
1701 );
1702
1703 terminal.vt_write(b"echo");
1704 tracked
1705 .set(&mut terminal, Point::Active(PointCoordinate { x: 0, y: 0 }))
1706 .expect("tracked grid ref should set to a new point");
1707
1708 assert!(tracked.has_value());
1709 assert_eq!(
1710 codepoint_at_tracked_ref(&terminal, &tracked),
1711 u32::from('e')
1712 );
1713 }
1714
1715 #[test]
1716 fn tracked_grid_ref_survives_terminal_drop() {
1717 let tracked = {
1718 let mut terminal = tiny_terminal();
1719 terminal.vt_write(b"alpha");
1720 terminal
1721 .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1722 .expect("tracked grid ref should initialize")
1723 };
1724
1725 assert!(!tracked.has_value());
1726 assert!(
1727 tracked
1728 .point(PointSpace::Screen)
1729 .expect("detached tracked point should not fail")
1730 .is_none()
1731 );
1732 }
1733
1734 #[test]
1735 fn tracked_grid_ref_rejects_different_terminal() {
1736 let mut first = tiny_terminal();
1737 first.vt_write(b"alpha");
1738 let mut second = tiny_terminal();
1739 second.vt_write(b"bravo");
1740
1741 let mut tracked = first
1742 .track_grid_ref(Point::Active(PointCoordinate { x: 0, y: 0 }))
1743 .expect("tracked grid ref should initialize");
1744
1745 assert!(matches!(
1746 tracked.snapshot(&second),
1747 Err(Error::InvalidValue)
1748 ));
1749 assert!(matches!(
1750 tracked.set(&mut second, Point::Active(PointCoordinate { x: 0, y: 0 })),
1751 Err(Error::InvalidValue)
1752 ));
1753 }
1754
1755 #[test]
1756 fn grid_ref_converts_back_to_point() {
1757 let mut terminal = tiny_terminal();
1758 terminal.vt_write(b"alpha");
1759
1760 let original = PointCoordinate { x: 1, y: 0 };
1761 let grid_ref = terminal
1762 .grid_ref(Point::Active(original))
1763 .expect("grid ref should resolve");
1764
1765 assert_eq!(
1766 terminal
1767 .point_from_grid_ref(&grid_ref, PointSpace::Active)
1768 .expect("grid ref point conversion should not fail")
1769 .expect("grid ref should be representable in active space"),
1770 original
1771 );
1772 }
1773}