use std::ops::Range;
use std::sync::Arc;
use ghostty_vt::render::RowId;
use ghostty_vt::style::Palette;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Rgb(pub u8, pub u8, pub u8);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Color {
#[default]
Default,
Indexed(u8),
Rgb(Rgb),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Underline {
#[default]
None,
Single,
Double,
Curly,
Dotted,
Dashed,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Style {
pub foreground: Color,
pub background: Color,
pub underline_color: Color,
pub bold: bool,
pub italic: bool,
pub faint: bool,
pub blink: bool,
pub inverse: bool,
pub invisible: bool,
pub underline: Underline,
pub strikethrough: bool,
pub overline: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct CellSpan {
pub text: Range<usize>,
pub column: u16,
pub width: u8,
pub spacer: bool,
pub hyperlink: Option<Arc<str>>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct StyleRun {
pub text: Range<usize>,
pub columns: Range<u16>,
pub style: Style,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct TerminalRow {
pub id: RowId,
pub revision: u64,
pub text: Arc<str>,
pub cells: Arc<[CellSpan]>,
pub runs: Arc<[StyleRun]>,
pub soft_wrapped: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CursorShape {
#[default]
Block,
Bar,
Underline,
Hollow,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Cursor {
pub column: u16,
pub row: u16,
pub visible: bool,
pub blinking: bool,
pub shape: CursorShape,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct TerminalColors {
pub(crate) foreground: Rgb,
pub(crate) background: Rgb,
pub(crate) cursor: Option<Rgb>,
pub(crate) selection_background: Option<Rgb>,
pub(crate) selection_foreground: Option<Rgb>,
pub(crate) palette: [Rgb; 256],
}
impl Default for TerminalColors {
fn default() -> Self {
Self {
foreground: Rgb(0xff, 0xff, 0xff),
background: Rgb(0x28, 0x2c, 0x34),
cursor: None,
selection_background: None,
selection_foreground: None,
palette: Palette::default().0.map(|c| Rgb(c.r, c.g, c.b)),
}
}
}
impl TerminalColors {
#[must_use]
pub fn new(foreground: Rgb, background: Rgb) -> Self {
Self {
foreground,
background,
..Self::default()
}
}
#[must_use]
pub fn with_cursor(mut self, cursor: Rgb) -> Self {
self.cursor = Some(cursor);
self
}
#[must_use]
pub fn with_selection(mut self, background: Rgb, foreground: Rgb) -> Self {
self.selection_background = Some(background);
self.selection_foreground = Some(foreground);
self
}
#[must_use]
pub fn with_palette_entry(mut self, index: u8, color: Rgb) -> Self {
self.palette[usize::from(index)] = color;
self
}
pub fn foreground(&self) -> Rgb {
self.foreground
}
pub fn background(&self) -> Rgb {
self.background
}
pub fn cursor(&self) -> Option<Rgb> {
self.cursor
}
pub fn selection_background(&self) -> Option<Rgb> {
self.selection_background
}
pub fn selection_foreground(&self) -> Option<Rgb> {
self.selection_foreground
}
pub fn palette(&self) -> &[Rgb; 256] {
&self.palette
}
pub fn theme_config(&self, theme: &crate::Theme) -> crate::ThemeConfig {
let surface = crate::terminal::colors::hsla(self.background);
let mut config = if crate::theme::lightness(surface) < 0.5 {
theme.dark.clone()
} else {
theme.light.clone()
};
config.surface = surface;
config.ink = crate::terminal::colors::hsla(self.foreground);
config
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct CellSelection {
pub start: (u16, u16),
pub end: (u16, u16),
pub rectangle: bool,
}
impl CellSelection {
#[must_use]
pub fn new(start: (u16, u16), end: (u16, u16), rectangle: bool) -> Self {
Self {
start,
end,
rectangle,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ScrollMetrics {
pub total_rows: usize,
pub offset: usize,
pub viewport_rows: usize,
}
impl ScrollMetrics {
#[must_use]
pub fn at_bottom(self) -> bool {
self.offset + self.viewport_rows >= self.total_rows
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct InputModes {
pub mouse_reporting: bool,
pub alternate_screen: bool,
pub bracketed_paste: bool,
pub focus_reporting: bool,
pub alternate_scroll: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Viewport {
columns: u16,
rows: u16,
cell_width: u32,
cell_height: u32,
}
impl Default for Viewport {
fn default() -> Self {
Self {
columns: 80,
rows: 24,
cell_width: 8,
cell_height: 16,
}
}
}
pub const MAX_GRID: u16 = 1000;
impl Viewport {
pub fn new(columns: u16, rows: u16, cell_width: u32, cell_height: u32) -> Option<Self> {
if columns == 0
|| rows == 0
|| columns > MAX_GRID
|| rows > MAX_GRID
|| cell_width == 0
|| cell_height == 0
|| cell_width > 4096
|| cell_height > 4096
{
return None;
}
Some(Self {
columns,
rows,
cell_width,
cell_height,
})
}
#[must_use]
pub fn columns(self) -> u16 {
self.columns
}
#[must_use]
pub fn rows(self) -> u16 {
self.rows
}
#[must_use]
pub fn cell_width(self) -> u32 {
self.cell_width
}
#[must_use]
pub fn cell_height(self) -> u32 {
self.cell_height
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct TerminalFrame {
pub revision: u64,
pub content_revision: u64,
pub viewport: Viewport,
pub rows: Arc<[Arc<TerminalRow>]>,
pub cursor: Cursor,
pub colors: TerminalColors,
pub scroll: ScrollMetrics,
pub modes: InputModes,
}
impl TerminalFrame {
#[must_use]
pub fn text(&self) -> String {
self.rows
.iter()
.map(|row| row.text.trim_end())
.collect::<Vec<_>>()
.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_colors_are_ghosttys_built_in_ones() {
let colors = TerminalColors::default();
assert_eq!(colors.background(), Rgb(0x28, 0x2c, 0x34));
assert_eq!(colors.foreground(), Rgb(0xff, 0xff, 0xff));
assert_eq!(colors.cursor(), None);
assert_eq!(colors.selection_background(), None);
assert_eq!(colors.selection_foreground(), None);
assert_eq!(colors.palette()[1], Rgb(0xcc, 0x66, 0x66));
}
#[test]
fn viewport_rejects_bad_sizes() {
assert!(Viewport::new(0, 1, 8, 16).is_none());
assert!(Viewport::new(1, 0, 8, 16).is_none());
assert!(Viewport::new(1001, 1, 8, 16).is_none());
assert!(Viewport::new(1, 1, 0, 16).is_none());
assert!(Viewport::new(80, 24, 8, 16).is_some());
}
#[test]
fn scroll_metrics_know_the_bottom() {
assert!(
ScrollMetrics {
total_rows: 30,
offset: 6,
viewport_rows: 24
}
.at_bottom()
);
assert!(
!ScrollMetrics {
total_rows: 30,
offset: 0,
viewport_rows: 24
}
.at_bottom()
);
}
}