diff --git a/userspace/Cargo.lock b/userspace/Cargo.lock index 111e2493..d2979777 100644 --- a/userspace/Cargo.lock +++ b/userspace/Cargo.lock @@ -2422,6 +2422,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2728,7 +2737,9 @@ dependencies = [ "log", "logsink", "rusttype", + "serde", "thiserror", + "toml", ] [[package]] @@ -2832,11 +2843,26 @@ dependencies = [ "zerovec", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -2845,6 +2871,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] diff --git a/userspace/Cargo.toml b/userspace/Cargo.toml index 5013b78b..2b058758 100644 --- a/userspace/Cargo.toml +++ b/userspace/Cargo.toml @@ -38,6 +38,7 @@ sha2 = { version = "0.10.8" } chrono = { version = "0.4.31", default-features = false } postcard = { version = "1.1.1", features = ["alloc"] } tui = { version = "0.19.0", default-features = false } +toml = "0.8.20" raqote = { version = "0.8.3", default-features = false } diff --git a/userspace/etc/fonts/fixed-bold-italic.ttf b/userspace/etc/fonts/fixed-bold-italic.ttf new file mode 100644 index 00000000..85eb01be Binary files /dev/null and b/userspace/etc/fonts/fixed-bold-italic.ttf differ diff --git a/userspace/etc/fonts/fixed-bold.ttf b/userspace/etc/fonts/fixed-bold.ttf new file mode 100644 index 00000000..9997cdaa Binary files /dev/null and b/userspace/etc/fonts/fixed-bold.ttf differ diff --git a/userspace/etc/fonts/fixed-italic.ttf b/userspace/etc/fonts/fixed-italic.ttf new file mode 100644 index 00000000..5302690d Binary files /dev/null and b/userspace/etc/fonts/fixed-italic.ttf differ diff --git a/userspace/etc/fonts/licenses.txt b/userspace/etc/fonts/licenses.txt index 6bed11a1..74d42233 100644 --- a/userspace/etc/fonts/licenses.txt +++ b/userspace/etc/fonts/licenses.txt @@ -1,4 +1,4 @@ -fixed-regular.ttf (Liberation Mono): +fixed-***.ttf (Liberation Mono): Digitized data copyright (c) 2010 Google Corporation with Reserved Font Arimo, Tinos and Cousine. diff --git a/userspace/etc/term.toml b/userspace/etc/term.toml new file mode 100644 index 00000000..e69de29b diff --git a/userspace/lib/libterm/src/lib.rs b/userspace/lib/libterm/src/lib.rs index a9003db1..24c8408e 100644 --- a/userspace/lib/libterm/src/lib.rs +++ b/userspace/lib/libterm/src/lib.rs @@ -123,7 +123,7 @@ impl RawTerminal for Stdout { } fn raw_set_cursor_style(&mut self, style: CursorStyle) -> io::Result<()> { - // TODO yggdrasil support for cursor styles + // TODO term does not support spaces in ctl-seqs #[cfg(not(target_os = "yggdrasil"))] { match style { @@ -133,7 +133,10 @@ impl RawTerminal for Stdout { } #[cfg(target_os = "yggdrasil")] { - let _ = style; + match style { + CursorStyle::Default => self.write_all(b"\x1B[0q")?, + CursorStyle::Line => self.write_all(b"\x1B[6q")?, + } } Ok(()) } diff --git a/userspace/lib/libterm/src/tui.rs b/userspace/lib/libterm/src/tui.rs index 57558371..89e0a41d 100644 --- a/userspace/lib/libterm/src/tui.rs +++ b/userspace/lib/libterm/src/tui.rs @@ -45,7 +45,7 @@ impl tui::backend::Backend for Term { if bold { self.stdout.raw_set_style(1)?; } else { - self.stdout.raw_set_style(0)?; + self.stdout.raw_set_style(22)?; } self.stdout.raw_set_color(3, color)?; } else { diff --git a/userspace/red/src/buffer/mod.rs b/userspace/red/src/buffer/mod.rs index 8c4d133a..facb363d 100644 --- a/userspace/red/src/buffer/mod.rs +++ b/userspace/red/src/buffer/mod.rs @@ -426,6 +426,15 @@ impl Buffer { } pub fn display(&mut self, config: &Config, term: &mut Term) -> Result<(), Error> { + match self.mode { + Mode::Normal => { + term.set_cursor_style(CursorStyle::Default)?; + } + Mode::Insert => { + term.set_cursor_style(CursorStyle::Line)?; + } + } + for (row, line) in self .lines .iter() diff --git a/userspace/sysutils/src/tst.rs b/userspace/sysutils/src/tst.rs index b8f2719b..1fc2ae2d 100644 --- a/userspace/sysutils/src/tst.rs +++ b/userspace/sysutils/src/tst.rs @@ -1,23 +1,34 @@ -use std::{thread, time::Duration}; - fn main() { - let mut threads = vec![]; - for i in 0..4 { - let jh = thread::Builder::new() - .name(format!("tst-thread-{i}")) - .spawn(move || { - let current = thread::current(); - for _ in 0..100 { - println!("Hi from thread {:?}", current.name()); - thread::sleep(Duration::from_secs(1)); - } - }) - .unwrap(); + const COLORS: &[(usize, &str)] = &[ + (1, "Red"), + (2, "Green"), + (3, "Yellow"), + (4, "Blue"), + (5, "Magenta"), + (6, "Cyan"), + (7, "White"), + ]; - threads.push(jh); + println!( + "{:<8} \x1B[1m{:<8}\x1B[22m \x1B[3m{:<8} \x1B[1m{:<8}\x1B[0m", + "Normal", "Bold", "Italic", "Bold/it" + ); + for &(color, text) in COLORS { + print!("\x1B[3{color}m"); + // Normal + print!("{text:<8} "); + // Bold + print!("\x1B[1m"); + print!("{text:<8} "); + print!("\x1B[22m"); + // Italic + print!("\x1B[3m"); + print!("{text:<8} "); + // Bold/italic + print!("\x1B[1m"); + print!("{text:<8}"); + println!("\x1B[0m"); } - for thread in threads.into_iter() { - thread.join().unwrap(); - } + println!("\x1B[4m\x1B[1mUnderlined\x1B[0m, \x1B[9m\x1B[3mStrikethrough\x1B[0m? \x1B[4m\x1B[9mBOTH!!!\x1B[0m"); } diff --git a/userspace/term/Cargo.toml b/userspace/term/Cargo.toml index 6564d036..9401e1c3 100644 --- a/userspace/term/Cargo.toml +++ b/userspace/term/Cargo.toml @@ -12,5 +12,7 @@ logsink.workspace = true log.workspace = true thiserror.workspace = true clap.workspace = true +serde.workspace = true +toml.workspace = true rusttype = "0.9.3" diff --git a/userspace/term/src/attr.rs b/userspace/term/src/attr.rs index 498c1c19..65100dce 100644 --- a/userspace/term/src/attr.rs +++ b/userspace/term/src/attr.rs @@ -1,19 +1,15 @@ -#[derive(Clone, Copy, Debug)] -#[repr(usize)] -pub enum Color { - Black = 0, - Red = 1, - Green = 2, - Yellow = 3, - Blue = 4, - Magenta = 5, - Cyan = 6, - White = 7, -} +use std::{fmt, str::FromStr}; + +use serde::{ + de::{Unexpected, Visitor}, + Deserialize, +}; + +use crate::config::Config; #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[repr(C)] -pub struct DisplayColor { +pub struct Color { pub r: u8, pub g: u8, pub b: u8, @@ -23,90 +19,86 @@ pub struct DisplayColor { pub struct CellAttributes { pub fg: Color, pub bg: Color, - pub bright: bool, + pub bold: bool, + pub italic: bool, + pub underlined: bool, + pub strikethrough: bool, } -impl DisplayColor { - pub const BLACK: Self = Self::new(0, 0, 0); - pub const WHITE: Self = Self::new(255, 255, 255); - pub const DARK_GRAY: Self = Self::new(60, 60, 60); - pub const LIGHT_GRAY: Self = Self::new(127, 127, 127); - - pub const DARK_RED: Self = Self::new(160, 0, 0); - pub const DARK_GREEN: Self = Self::new(0, 160, 0); - pub const DARK_BLUE: Self = Self::new(0, 0, 160); - - pub const DARK_YELLOW: Self = Self::new(160, 160, 0); - pub const DARK_MAGENTA: Self = Self::new(160, 0, 160); - pub const DARK_CYAN: Self = Self::new(0, 160, 160); - - pub const LIGHT_RED: Self = Self::new(255, 0, 0); - pub const LIGHT_GREEN: Self = Self::new(0, 255, 0); - pub const LIGHT_BLUE: Self = Self::new(0, 0, 255); - - pub const LIGHT_YELLOW: Self = Self::new(255, 255, 0); - pub const LIGHT_MAGENTA: Self = Self::new(255, 0, 255); - pub const LIGHT_CYAN: Self = Self::new(0, 255, 255); - +impl Color { pub const fn new(r: u8, g: u8, b: u8) -> Self { Self { r, g, b } } + pub const fn from_u32(v: u32) -> Self { + let r = (v >> 16) as u8; + let g = (v >> 8) as u8; + let b = v as u8; + Self { r, g, b } + } + pub const fn to_u32(self) -> u32 { 0xFF000000 | ((self.r as u32) << 16) | ((self.g as u32) << 8) | (self.b as u32) } -} -impl Color { - pub fn from_esc(v: u32) -> Self { - match v { - 0 => Self::Black, - 1 => Self::Red, - 2 => Self::Green, - 3 => Self::Yellow, - 4 => Self::Blue, - 5 => Self::Magenta, - 6 => Self::Cyan, - 7 => Self::White, - _ => Self::Black, - } - } - - pub fn to_display(self, bright: bool) -> DisplayColor { - if bright { - BRIGHT_COLOR_MAP[self as usize] + pub fn from_escape(config: &Config, bold: bool, esc: u32) -> Option { + let map = if bold { + &config.colors.bold } else { - DIM_COLOR_MAP[self as usize] - } + &config.colors.dim + }; + map.lookup_escape(esc) } - - // pub fn to_rgba(self, bright: bool) -> SolidSource { - // if bright { - // BRIGHT_COLOR_MAP[self as usize] - // } else { - // COLOR_MAP[self as usize] - // } - // } } -const DIM_COLOR_MAP: &[DisplayColor] = &[ - DisplayColor::BLACK, - DisplayColor::DARK_RED, - DisplayColor::DARK_GREEN, - DisplayColor::DARK_YELLOW, - DisplayColor::DARK_BLUE, - DisplayColor::DARK_MAGENTA, - DisplayColor::DARK_CYAN, - DisplayColor::LIGHT_GRAY, -]; +impl FromStr for Color { + type Err = &'static str; -const BRIGHT_COLOR_MAP: &[DisplayColor] = &[ - DisplayColor::DARK_GRAY, - DisplayColor::LIGHT_RED, - DisplayColor::LIGHT_GREEN, - DisplayColor::LIGHT_YELLOW, - DisplayColor::LIGHT_BLUE, - DisplayColor::LIGHT_MAGENTA, - DisplayColor::LIGHT_CYAN, - DisplayColor::WHITE, -]; + fn from_str(s: &str) -> Result { + let Some(s) = s.strip_prefix("#") else { + return Err("Color should start with a `#`"); + }; + if s.len() != 6 { + return Err("Color should be 6 hex digits"); + } + let value = u32::from_str_radix(s, 16).map_err(|_| "Invalid color value")?; + Ok(Color::from_u32(value)) + } +} + +impl<'de> Deserialize<'de> for Color { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct V; + + impl<'de> Visitor<'de> for V { + type Value = Color; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a hex color in format `#XXYYZZ`") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if let Ok(color) = Color::from_str(v) { + Ok(color) + } else { + Err(E::invalid_value(Unexpected::Str(v), &"#XXYYZZ")) + } + } + + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + self.visit_str(&v) + } + } + + deserializer.deserialize_str(V) + } +} diff --git a/userspace/term/src/config.rs b/userspace/term/src/config.rs new file mode 100644 index 00000000..baa32a25 --- /dev/null +++ b/userspace/term/src/config.rs @@ -0,0 +1,115 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use serde::Deserialize; + +use crate::attr::Color; + +#[derive(Deserialize)] +pub struct FontConfig { + pub regular: PathBuf, + pub italic: PathBuf, + pub bold: PathBuf, + pub bold_italic: PathBuf, + + pub size: usize, +} + +#[derive(Deserialize)] +pub struct ColorsGroupConfig { + pub red: Color, + pub green: Color, + pub blue: Color, + pub yellow: Color, + pub magenta: Color, + pub cyan: Color, + pub white: Color, + pub black: Color, +} + +#[derive(Deserialize)] +pub struct ColorsConfig { + pub bold: ColorsGroupConfig, + pub dim: ColorsGroupConfig, +} + +#[derive(Default, Deserialize)] +pub struct Config { + pub fonts: FontConfig, + pub colors: ColorsConfig, +} + +impl Default for FontConfig { + fn default() -> Self { + Self { + regular: PathBuf::from("/etc/fonts/fixed-regular.ttf"), + italic: PathBuf::from("/etc/fonts/fixed-italic.ttf"), + bold: PathBuf::from("/etc/fonts/fixed-bold.ttf"), + bold_italic: PathBuf::from("/etc/fonts/fixed-bold-italic.ttf"), + + size: 16, + } + } +} + +impl Default for ColorsConfig { + fn default() -> Self { + Self { + dim: ColorsGroupConfig { + black: Color::from_u32(0x000000), + white: Color::from_u32(0xCCCCCC), + red: Color::from_u32(0xCC0000), + green: Color::from_u32(0x00CC00), + blue: Color::from_u32(0x0000CC), + yellow: Color::from_u32(0xCCCC00), + magenta: Color::from_u32(0xCC00CC), + cyan: Color::from_u32(0x00CCCC), + }, + bold: ColorsGroupConfig { + black: Color::from_u32(0x666666), + white: Color::from_u32(0xFFFFFF), + red: Color::from_u32(0xFF0000), + green: Color::from_u32(0x00FF00), + blue: Color::from_u32(0x0000FF), + yellow: Color::from_u32(0xFFFF00), + magenta: Color::from_u32(0xFF00FF), + cyan: Color::from_u32(0x00FFFF), + }, + } + } +} + +impl ColorsGroupConfig { + pub fn lookup_escape(&self, esc: u32) -> Option { + match esc { + 0 => Some(self.black), + 1 => Some(self.red), + 2 => Some(self.green), + 3 => Some(self.yellow), + 4 => Some(self.blue), + 5 => Some(self.magenta), + 6 => Some(self.cyan), + 7 => Some(self.white), + _ => None, + } + } +} + +impl Config { + pub fn load_or_default>(path: P) -> Self { + Self::load(path).unwrap_or_default() + } + + fn load>(path: P) -> Option { + let path = path.as_ref(); + let data = fs::read_to_string(path) + .inspect_err(|error| log::warn!("{path:?}: {error}")) + .ok()?; + let this = toml::from_str(&data) + .inspect_err(|error| log::warn!("{path:?}: {error}")) + .ok()?; + Some(this) + } +} diff --git a/userspace/term/src/font.rs b/userspace/term/src/font.rs index d120537d..e1ff2492 100644 --- a/userspace/term/src/font.rs +++ b/userspace/term/src/font.rs @@ -1,10 +1,18 @@ -use std::{fs, path::Path}; +use std::{fs, path::Path, sync::Arc}; -use crate::error::Error; +use crate::{error::Error, CONFIG}; + +#[derive(Clone)] +pub struct Fonts { + pub regular: Arc>, + pub italic: Arc>, + pub bold: Arc>, + pub bold_italic: Arc>, +} pub trait Font { fn layout(&self) -> &FontLayout; - fn map_glyph(&mut self, ch: char, mapper: F); + fn map_glyph(&self, ch: char, mapper: F); } pub struct TrueTypeFont<'a> { @@ -43,7 +51,7 @@ impl Font for TrueTypeFont<'_> { &self.layout } - fn map_glyph(&mut self, ch: char, mut mapper: F) { + fn map_glyph(&self, ch: char, mut mapper: F) { let glyph = self .inner .glyph(ch) @@ -68,3 +76,31 @@ pub struct FontLayout { pub width: usize, pub height: usize, } + +impl Fonts { + pub fn from_config() -> Result { + let config = &*CONFIG; + let regular = Arc::new(TrueTypeFont::load( + &config.fonts.regular, + config.fonts.size, + )?); + let italic = TrueTypeFont::load(&config.fonts.italic, config.fonts.size) + .map(Arc::new) + .inspect_err(|error| log::error!("{}: {error}", config.fonts.italic.display())) + .unwrap_or_else(|_| regular.clone()); + let bold = TrueTypeFont::load(&config.fonts.bold, config.fonts.size) + .map(Arc::new) + .inspect_err(|error| log::error!("{}: {error}", config.fonts.bold.display())) + .unwrap_or_else(|_| regular.clone()); + let bold_italic = TrueTypeFont::load(&config.fonts.bold_italic, config.fonts.size) + .map(Arc::new) + .inspect_err(|error| log::error!("{}: {error}", config.fonts.bold_italic.display())) + .unwrap_or_else(|_| italic.clone()); + Ok(Self { + regular, + italic, + bold, + bold_italic, + }) + } +} diff --git a/userspace/term/src/main.rs b/userspace/term/src/main.rs index 2578ac27..219756b8 100644 --- a/userspace/term/src/main.rs +++ b/userspace/term/src/main.rs @@ -3,6 +3,7 @@ use std::{ fs::File, io::{Read, Write}, + mem, os::{ fd::{self, AsRawFd, FromRawFd, IntoRawFd, RawFd}, yggdrasil::{ @@ -15,17 +16,16 @@ use std::{ rt::io::device, }, }, - path::PathBuf, process::{Child, Command, ExitCode, Stdio}, sync::{ atomic::{AtomicBool, Ordering}, - Arc, Mutex, + Arc, LazyLock, Mutex, }, }; -use clap::Parser; +use config::Config; use error::Error; -use font::{Font, TrueTypeFont}; +use font::{Font, Fonts}; use libcolors::{ application::{ window::{EventOutcome, Window}, @@ -34,21 +34,23 @@ use libcolors::{ event::KeyModifiers, input::Key, }; -use state::{Cursor, State}; +use state::{Cursor, CursorStyle, GridCell, State}; pub mod attr; +pub mod config; pub mod error; pub mod font; pub mod state; -struct DrawState { +struct DrawState { width: usize, force_redraw: bool, focus_changed: bool, focused: bool, old_cursor: Cursor, + old_cursor_style: CursorStyle, - font: F, + fonts: Fonts, } pub struct Terminal<'a> { @@ -63,31 +65,92 @@ pub struct Terminal<'a> { shell: Child, } -impl DrawState { - pub fn new(font: F, width: usize) -> Self { +impl DrawState { + pub fn new(fonts: Fonts, width: usize) -> Self { Self { width, - font, + fonts, force_redraw: true, focus_changed: false, focused: true, + old_cursor_style: CursorStyle::Block, old_cursor: Cursor { row: 0, col: 0 }, } } - pub fn draw(&mut self, dt: &mut [u32], state: &mut State) { + fn draw_character( + dt: &mut [u32], + cx: usize, + cy: usize, + fw: usize, + fh: usize, + stride: usize, + cell: &GridCell, + fonts: &Fonts, + invert: bool, + ) { fn blend(x: u8, y: u8, f: f32) -> u8 { let v = f * (x as f32) + (1.0 - f) * (y as f32); v as u8 } - let default_fg = state.default_attributes.fg.to_display(false).to_u32(); - let default_bg = state.default_attributes.bg.to_display(false).to_u32(); - let font_layout = self.font.layout(); + let mut bg = cell.attrs.bg; + let mut fg = cell.attrs.fg; + if invert { + mem::swap(&mut bg, &mut fg); + } + + // Fill cell + for y in 0..fh { + let off = (cy + y) * stride + cx; + dt[off..off + fw].fill(bg.to_u32()); + } + + if cell.char == '\0' { + return; + } + + let c = cell.char as char; + let font = match (cell.attrs.bold, cell.attrs.italic) { + (true, true) => &fonts.bold_italic, + (false, true) => &fonts.italic, + (true, false) => &fonts.bold, + (false, false) => &fonts.regular, + }; + + font.map_glyph(c, |x, y, v| { + let v = v.min(1.0); + let r = blend(fg.r, bg.r, v); + let g = blend(fg.g, bg.g, v); + let b = blend(fg.b, bg.b, v); + let color = (b as u32) | ((g as u32) << 8) | ((r as u32) << 16) | 0xFF000000; + + dt[(cy + y) * stride + cx + x] = color; + }); + + if cell.attrs.underlined { + let s = (cy + fh - 1) * stride + cx; + let d = &mut dt[s..s + fw]; + d.fill(fg.to_u32()); + } + if cell.attrs.strikethrough { + let s = (cy + fh / 2) * stride + cx; + let d = &mut dt[s..s + fw]; + d.fill(fg.to_u32()); + } + } + + pub fn draw(&mut self, dt: &mut [u32], state: &mut State) { + let default_fg = state.default_attributes.fg.to_u32(); + let default_bg = state.default_attributes.bg.to_u32(); + let font_layout = self.fonts.regular.layout(); let fw = font_layout.width; let fh = font_layout.height; - let cursor_dirty = self.old_cursor != state.cursor; + let cursor_dirty = (self.old_cursor != state.cursor) + || (self.old_cursor_style != state.cursor_style) + || state.cursor_dirty; + state.cursor_dirty = false; if self.force_redraw { dt.fill(default_bg); @@ -99,54 +162,53 @@ impl DrawState { let scroll = state.adjust_scroll(); let cursor_visible = scroll == 0 && state.cursor_visible; + state.buffer.visible_rows_mut(scroll, |i, row| { let cy = i * fh; - for (j, cell) in row.cells().enumerate() { - let bg = cell.attrs.bg.to_display(false); - let fg = cell.attrs.fg.to_display(cell.attrs.bright); - let cx = j * fw; - - // Fill cell - for y in 0..fh { - let off = (cy + y) * self.width + cx; - dt[off..off + fw].fill(bg.to_u32()); - } - - if cell.char == '\0' { - continue; - } - - let c = cell.char as char; - self.font.map_glyph(c, |x, y, v| { - let v = (v * 2.0).min(1.0); - let r = blend(fg.r, bg.r, v); - let g = blend(fg.g, bg.g, v); - let b = blend(fg.b, bg.b, v); - let color = (b as u32) | ((g as u32) << 8) | ((r as u32) << 16) | 0xFF000000; - - dt[(cy + y) * self.width + cx + x] = color; - }); + Self::draw_character(dt, cx, cy, fw, fh, self.width, cell, &self.fonts, false); } }); - // TODO check if there's a character under cursor - if cursor_visible { + if cursor_visible && cursor_dirty { let cx = state.cursor.col * fw; let cy = state.cursor.row * fh; - // Fill block cursor - for y in 0..fh { - let off = (cy + y) * self.width + cx; - dt[off..off + fw].fill(default_fg); - } + // Character under cursor + let cell = state.buffer.cell(state.cursor.row, state.cursor.col); - if !self.focused { - // Remove cursor center - for y in 1..fh - 1 { - let off = (cy + y) * self.width + cx + 1; - dt[off..off + fw - 2].fill(default_bg); + let fg = cell.attrs.fg.to_u32(); + + match state.cursor_style { + CursorStyle::Bar if self.focused => { + for y in 0..fh { + let off = (cy + y) * self.width + cx; + dt[off] = fg; + } + } + CursorStyle::Underline if self.focused => { + let off = (cy + fh - 1) * self.width + cx; + dt[off..off + fw].fill(fg); + } + // Block, focused + CursorStyle::Block if self.focused => { + Self::draw_character(dt, cx, cy, fw, fh, self.width, cell, &self.fonts, true); + } + // Block, not focused (default not focused) + _ => { + Self::draw_character(dt, cx, cy, fw, fh, self.width, cell, &self.fonts, false); + + // Draw outline + for y in 0..fh { + let off = (cy + y) * self.width + cx; + if y == 0 || y == fh - 1 { + dt[off..off + fw].fill(fg); + } else { + dt[off] = default_fg; + dt[off + fw - 1] = fg; + } + } } } @@ -159,7 +221,9 @@ impl DrawState { } impl Terminal<'_> { - pub fn new(font: F) -> Result { + pub fn new() -> Result { + let fonts = Fonts::from_config()?; + let mut app = Application::new()?; let mut window = Window::new(&app)?; let mut poll = PollChannel::new()?; @@ -167,7 +231,7 @@ impl Terminal<'_> { let width = window.width() as usize; let height = window.height() as usize; - let font_layout = font.layout(); + let font_layout = fonts.regular.layout(); let rows = height / font_layout.height; let columns = width / font_layout.width; @@ -179,7 +243,7 @@ impl Terminal<'_> { // TODO I hate this let pty_master = Arc::new(Mutex::new(pty_master)); let state = Arc::new(Mutex::new(State::new(columns, rows))); - let draw_state = Arc::new(Mutex::new(DrawState::new(font, width))); + let draw_state = Arc::new(Mutex::new(DrawState::new(fonts, width))); let state_c = state.clone(); let draw_state_c = draw_state.clone(); @@ -191,7 +255,7 @@ impl Terminal<'_> { let width = width as usize; let height = height as usize; - let font_layout = ds.font.layout(); + let font_layout = ds.fonts.regular.layout(); let rows = height / font_layout.height; let columns = width / font_layout.width; @@ -241,9 +305,11 @@ impl Terminal<'_> { need_redraw = s.scroll_end(); } (KeyModifiers::CTRL, Key::Char(b'l')) => { - s.clear(); + if !s.alternate { + s.clear(); + } pty_master.write_all(&[0x0C]).unwrap(); - need_redraw = true; + need_redraw = !s.alternate; } (KeyModifiers::SHIFT, Key::PageUp) => { need_redraw = s.scroll_up(); @@ -379,33 +445,18 @@ impl Terminal<'_> { } static ABORT: AtomicBool = AtomicBool::new(false); +static CONFIG: LazyLock = LazyLock::new(|| Config::load_or_default("/etc/term.conf")); -#[derive(Debug, Parser)] -struct Args { - #[clap( - long, - help = "TTF font to use", - default_value = "/etc/fonts/fixed-regular.ttf" - )] - regular_font: PathBuf, - #[clap( - long, - help = "Font height in pixels (only for TTF fonts)", - default_value_t = 16 - )] - font_size: usize, -} - -fn run(args: &Args) -> Result { - let font = TrueTypeFont::load(&args.regular_font, args.font_size)?; - let term = Terminal::new(font)?; +fn run() -> Result { + LazyLock::force(&CONFIG); + let term = Terminal::new()?; Ok(term.run()) } fn main() -> ExitCode { - let args = Args::parse(); logsink::setup_logging(false); - match run(&args) { + + match run() { Ok(code) => code, Err(error) => { log::error!("{error}"); diff --git a/userspace/term/src/state.rs b/userspace/term/src/state.rs index d5e5ef58..557b209d 100644 --- a/userspace/term/src/state.rs +++ b/userspace/term/src/state.rs @@ -1,6 +1,9 @@ use std::collections::VecDeque; -use crate::attr::{CellAttributes, Color}; +use crate::{ + attr::{CellAttributes, Color}, + CONFIG, +}; #[derive(Clone, Copy, Debug)] pub struct GridCell { @@ -44,6 +47,13 @@ struct Utf8Decoder { len: usize, } +#[derive(PartialEq)] +pub enum CursorStyle { + Block, + Underline, + Bar, +} + pub struct State { pub buffer: Buffer, @@ -58,8 +68,11 @@ pub struct State { #[allow(unused)] saved_cursor: Option, + pub cursor_style: CursorStyle, pub default_attributes: CellAttributes, pub attributes: CellAttributes, + pub fg_index: Option, + pub cursor_dirty: bool, } impl Utf8Decoder { @@ -87,22 +100,25 @@ impl GridCell { Self { char, attrs } } - pub fn empty(bg: Color) -> Self { + pub fn empty(fg: Color, bg: Color) -> Self { Self { char: '\0', attrs: CellAttributes { - fg: Color::Black, + fg, bg, - bright: false, + bold: false, + italic: false, + underlined: false, + strikethrough: false, }, } } } impl GridRow { - pub fn new(width: usize, bg: Color) -> Self { + pub fn new(width: usize, fg: Color, bg: Color) -> Self { Self { - cols: vec![GridCell::empty(bg); width], + cols: vec![GridCell::empty(fg, bg); width], dirty: true, } } @@ -124,26 +140,26 @@ impl GridRow { self.cols.iter() } - fn clear(&mut self, bg: Color) { - self.cols.fill(GridCell::empty(bg)); + fn clear(&mut self, fg: Color, bg: Color) { + self.cols.fill(GridCell::empty(fg, bg)); self.dirty = true; } - fn erase_to_right(&mut self, start: usize, bg: Color) { - self.cols[start..].fill(GridCell::empty(bg)); + fn erase_to_right(&mut self, start: usize, fg: Color, bg: Color) { + self.cols[start..].fill(GridCell::empty(fg, bg)); self.dirty = true; } - fn resize(&mut self, width: usize, bg: Color) { - self.cols.resize(width, GridCell::empty(bg)); + fn resize(&mut self, width: usize, fg: Color, bg: Color) { + self.cols.resize(width, GridCell::empty(fg, bg)); self.dirty = true; } } impl Buffer { - pub fn new(width: usize, height: usize, bg: Color) -> Self { + pub fn new(width: usize, height: usize, fg: Color, bg: Color) -> Self { Self { - rows: vec![GridRow::new(width, bg); height], + rows: vec![GridRow::new(width, fg, bg); height], scrollback: VecDeque::new(), scrollback_limit: 1024, width, @@ -151,8 +167,8 @@ impl Buffer { } } - pub fn clear(&mut self, bg: Color) { - self.rows.fill(GridRow::new(self.width, bg)); + pub fn clear(&mut self, fg: Color, bg: Color) { + self.rows.fill(GridRow::new(self.width, fg, bg)); } pub fn iter_rows_mut(&mut self, scroll: usize, mut handler: F) { @@ -185,10 +201,10 @@ impl Buffer { }); } - pub fn resize(&mut self, width: usize, height: usize, bg: Color) { - self.rows.resize(height, GridRow::new(width, bg)); + pub fn resize(&mut self, width: usize, height: usize, fg: Color, bg: Color) { + self.rows.resize(height, GridRow::new(width, fg, bg)); for row in self.rows.iter_mut() { - row.resize(width, bg); + row.resize(width, fg, bg); } self.width = width; @@ -200,7 +216,11 @@ impl Buffer { self.rows[cur.row].dirty = true; } - pub fn scroll_once(&mut self, bg: Color) { + pub fn cell(&self, row: usize, col: usize) -> &GridCell { + &self.rows[row].cols[col] + } + + pub fn scroll_once(&mut self, fg: Color, bg: Color) { self.scrollback.push_front(self.rows[0].clone()); if self.scrollback.len() >= self.scrollback_limit { self.scrollback.pop_back(); @@ -210,11 +230,11 @@ impl Buffer { self.rows[i - 1] = self.rows[i].clone(); self.rows[i - 1].dirty = true; } - self.rows[self.height - 1] = GridRow::new(self.width, bg); + self.rows[self.height - 1] = GridRow::new(self.width, fg, bg); } - pub fn erase_row(&mut self, row: usize, bg: Color) { - self.rows[row].clear(bg); + pub fn erase_row(&mut self, row: usize, fg: Color, bg: Color) { + self.rows[row].clear(fg, bg); } pub fn set_row_dirty(&mut self, row: usize) { @@ -231,14 +251,18 @@ impl Buffer { impl State { pub fn new(width: usize, height: usize) -> Self { + let config = &*CONFIG; let default_attributes = CellAttributes { - fg: Color::White, - bg: Color::Black, - bright: false, + fg: config.colors.dim.white, + bg: config.colors.dim.black, + bold: false, + italic: false, + underlined: false, + strikethrough: false, }; Self { - buffer: Buffer::new(width, height, default_attributes.bg), + buffer: Buffer::new(width, height, default_attributes.fg, default_attributes.bg), utf8_decode: Utf8Decoder::default(), esc_args: Vec::new(), @@ -252,17 +276,34 @@ impl State { default_attributes, attributes: default_attributes, + fg_index: Some(7), + cursor_style: CursorStyle::Block, + cursor_dirty: false, + } + } + + fn update_fg_color(&mut self) { + if let Some(fg_index) = self.fg_index { + self.attributes.fg = Color::from_escape(&*CONFIG, self.attributes.bold, fg_index) + .unwrap_or(self.default_attributes.fg); } } pub fn clear(&mut self) { - self.buffer.clear(self.attributes.bg); + self.buffer + .clear(self.default_attributes.fg, self.attributes.bg); + self.cursor_dirty = true; } pub fn resize(&mut self, width: usize, height: usize) { - self.buffer - .resize(width, height, self.default_attributes.bg); + self.buffer.resize( + width, + height, + self.default_attributes.fg, + self.default_attributes.bg, + ); self.scroll = 0; + self.cursor_dirty = true; if self.cursor.row >= height { self.cursor.row = height - 1; @@ -326,7 +367,8 @@ impl State { } while self.cursor.row >= self.buffer.height { - self.buffer.scroll_once(self.default_attributes.bg); + self.buffer + .scroll_once(self.default_attributes.fg, self.default_attributes.bg); self.cursor.row -= 1; redraw = true; } @@ -358,10 +400,22 @@ impl State { 1049 => { // Leave alternate mode self.alternate = false; + self.clear(); + self.cursor = Cursor { col: 0, row: 0 }; + self.cursor_dirty = true; true } _ => false, }, + 'q' => { + match self.esc_args.get(0).copied().unwrap_or(0) { + 3 | 4 => self.cursor_style = CursorStyle::Underline, + 5 | 6 => self.cursor_style = CursorStyle::Bar, + _ => self.cursor_style = CursorStyle::Block, + } + self.cursor_dirty = true; + true + } // Move back one character 'D' => { @@ -375,29 +429,79 @@ impl State { } // Character attributes 'm' => match self.esc_args[0] { + // Reset 0 => { self.attributes = self.default_attributes; + self.fg_index = Some(7); false } + // Bold 1 => { - self.attributes.bright = true; + self.attributes.bold = true; + self.update_fg_color(); false } + // Faint (should be decreased intensity, but here just normal) + 2 => { + self.attributes.bold = false; + self.update_fg_color(); + false + } + // Italicized + 3 => { + self.attributes.italic = true; + false + } + // Underlined + 4 => { + self.attributes.underlined = true; + false + } + // Strikethrough + 9 => { + self.attributes.strikethrough = true; + false + } + // Normal (neither bold nor faint) + 22 => { + self.attributes.bold = false; + self.update_fg_color(); + false + } + // Not italicized + 23 => { + self.attributes.italic = false; + false + } + // Not underlined + 24 => { + self.attributes.underlined = false; + false + } + // Not strikethrough + 29 => { + self.attributes.strikethrough = false; + false + } + // Foreground color 30..=39 => { let vt_color = self.esc_args[0] % 10; if vt_color == 9 { - self.attributes.fg = Color::Black; + self.fg_index = Some(7); } else { - self.attributes.fg = Color::from_esc(vt_color); + self.fg_index = Some(vt_color); } + self.update_fg_color(); false } + // Background color 40..=49 => { let vt_color = self.esc_args[0] % 10; if vt_color == 9 { - self.attributes.bg = Color::Black; + self.attributes.bg = self.default_attributes.bg; } else { - self.attributes.bg = Color::from_esc(vt_color); + self.attributes.bg = Color::from_escape(&*CONFIG, false, vt_color) + .unwrap_or(self.default_attributes.bg); } false } @@ -413,6 +517,7 @@ impl State { row: row as _, col: col as _, }; + self.cursor_dirty = true; true } @@ -420,6 +525,7 @@ impl State { 'H' => { self.buffer.set_row_dirty(self.cursor.row); self.cursor = Cursor { row: 0, col: 0 }; + self.cursor_dirty = true; true } // Clear rows/columns/screen @@ -430,7 +536,9 @@ impl State { 1 => false, // Erase all 2 => { - self.buffer.clear(self.attributes.bg); + self.buffer + .clear(self.default_attributes.fg, self.attributes.bg); + self.cursor_dirty = true; true } _ => false, @@ -438,13 +546,20 @@ impl State { 'K' => match self.esc_args[0] { // Erase to right 0 => { - self.buffer.rows[self.cursor.row] - .erase_to_right(self.cursor.col, self.attributes.bg); + self.buffer.rows[self.cursor.row].erase_to_right( + self.cursor.col, + self.default_attributes.fg, + self.attributes.bg, + ); true } // Erase All 2 => { - self.buffer.erase_row(self.cursor.row, self.attributes.bg); + self.buffer.erase_row( + self.cursor.row, + self.default_attributes.fg, + self.attributes.bg, + ); true } _ => false,