term: more attribute support

This commit is contained in:
Mark Poliakov 2025-03-03 17:53:19 +02:00
parent 7485476caa
commit 3567b79e1d
17 changed files with 593 additions and 230 deletions

28
userspace/Cargo.lock generated
View File

@ -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",
]

View File

@ -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 }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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.

0
userspace/etc/term.toml Normal file
View File

View File

@ -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(())
}

View File

@ -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 {

View File

@ -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()

View File

@ -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");
}

View File

@ -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"

View File

@ -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<Self> {
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<Self, Self::Err> {
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<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_str(&v)
}
}
deserializer.deserialize_str(V)
}
}

View File

@ -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<Color> {
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<P: AsRef<Path>>(path: P) -> Self {
Self::load(path).unwrap_or_default()
}
fn load<P: AsRef<Path>>(path: P) -> Option<Self> {
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)
}
}

View File

@ -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<TrueTypeFont<'static>>,
pub italic: Arc<TrueTypeFont<'static>>,
pub bold: Arc<TrueTypeFont<'static>>,
pub bold_italic: Arc<TrueTypeFont<'static>>,
}
pub trait Font {
fn layout(&self) -> &FontLayout;
fn map_glyph<F: FnMut(usize, usize, f32)>(&mut self, ch: char, mapper: F);
fn map_glyph<F: FnMut(usize, usize, f32)>(&self, ch: char, mapper: F);
}
pub struct TrueTypeFont<'a> {
@ -43,7 +51,7 @@ impl Font for TrueTypeFont<'_> {
&self.layout
}
fn map_glyph<F: FnMut(usize, usize, f32)>(&mut self, ch: char, mut mapper: F) {
fn map_glyph<F: FnMut(usize, usize, f32)>(&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<Self, Error> {
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,
})
}
}

View File

@ -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<F: Font> {
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<F: Font> DrawState<F> {
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<F: Font> DrawState<F> {
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<F: Font> DrawState<F> {
}
impl Terminal<'_> {
pub fn new<F: Font + 'static>(font: F) -> Result<Self, Error> {
pub fn new() -> Result<Self, Error> {
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<Config> = 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<ExitCode, Error> {
let font = TrueTypeFont::load(&args.regular_font, args.font_size)?;
let term = Terminal::new(font)?;
fn run() -> Result<ExitCode, Error> {
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}");

View File

@ -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<Cursor>,
pub cursor_style: CursorStyle,
pub default_attributes: CellAttributes,
pub attributes: CellAttributes,
pub fg_index: Option<u32>,
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<F: FnMut(usize, &mut GridRow)>(&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,