diff --git a/red/src/buffer/mod.rs b/red/src/buffer/mod.rs index e999e7ee..57450ee5 100644 --- a/red/src/buffer/mod.rs +++ b/red/src/buffer/mod.rs @@ -1,7 +1,8 @@ use std::{ ffi::OsStr, fs::File, - io::{self, BufRead, BufReader, BufWriter, Write}, + io::{self, BufRead, BufReader, BufWriter, Write as IoWrite}, + fmt::Write as FmtWrite, path::{Path, PathBuf}, }; @@ -11,7 +12,7 @@ use crate::{ config::Config, error::Error, line::{Line, TextLike}, - term::{Color, CursorStyle, Term, Terminal}, + term::{Color, CursorStyle, Term}, }; #[derive(Default)] @@ -335,9 +336,9 @@ impl Buffer { self.dirty } - fn display_line(&self, config: &Config, term: &mut Term, row: usize, line: &Line) { + fn display_line(&self, config: &Config, term: &mut Term, row: usize, line: &Line) -> Result<(), Error> { let mut pos = 0; - term.set_cursor_position(row, self.view.offset_x); + term.set_cursor_position(row, self.view.offset_x)?; let span = line.skip_to_width(self.view.column_offset, config.tab_width); let long_line = span.display_width(config.tab_width) > self.view.width; @@ -357,32 +358,32 @@ impl Buffer { break; } if i == old_pos { - term.set_foreground(Color::Blue); - term.put_byte(b'>'); - term.set_foreground(Color::Default); + term.set_foreground(Color::Blue)?; + term.write_char('>').map_err(Error::TerminalFmtError)?; + term.set_foreground(Color::Default)?; } else { - term.put_byte(b' '); + term.write_char(' ').map_err(Error::TerminalFmtError)?; } } } else { - // TODO optimize later - let s = std::iter::once(ch).collect::(); - term.put_bytes(s.as_str()); + write!(term, "{}", ch).map_err(Error::TerminalFmtError)?; pos += ch.width().unwrap_or(1); } } if long_line { - term.set_cursor_position(row, self.view.width + self.view.offset_x); - term.set_foreground(Color::Black); - term.set_background(Color::White); - term.put_byte(b'>'); - term.set_foreground(Color::Default); - term.set_background(Color::Default); + term.set_cursor_position(row, self.view.width + self.view.offset_x)?; + term.set_foreground(Color::Black)?; + term.set_background(Color::White)?; + term.write_char('>').map_err(Error::TerminalFmtError)?; + term.set_foreground(Color::Default)?; + term.set_background(Color::Default)?; } + + Ok(()) } - pub fn display(&mut self, config: &Config, term: &mut Term) { + pub fn display(&mut self, config: &Config, term: &mut Term) -> Result<(), Error> { for (row, line) in self .lines .iter() @@ -390,11 +391,28 @@ impl Buffer { .take(self.view.height) .enumerate() { - self.display_line(config, term, row, line); + self.display_line(config, term, row, line)?; } + + Ok(()) + } + + pub fn set_terminal_cursor(&mut self, config: &Config, term: &mut Term) -> Result<(), Error> { + let (x, y) = self.display_cursor(config); + if self.mode_dirty { + match self.mode { + Mode::Normal => term.set_cursor_style(CursorStyle::Default)?, + Mode::Insert => term.set_cursor_style(CursorStyle::Line)?, + } + } + term.set_cursor_position(y, x + self.view.offset_x)?; + self.mode_dirty = false; + + Ok(()) } pub fn newline_before(&mut self) { + self.modified = true; self.lines.insert(self.view.cursor_row, Line::new()); } @@ -403,6 +421,7 @@ impl Buffer { self.lines.push(Line::new()); return; } + self.modified = true; let newline = if break_line { self.lines[self.view.cursor_row].split_off(self.view.cursor_column) @@ -437,6 +456,7 @@ impl Buffer { let len = prev_line.len(); prev_line.extend(line); self.set_position(config, len, self.view.cursor_row - 1); + self.modified = true; } return; } @@ -461,18 +481,6 @@ impl Buffer { self.modified = true; } - pub fn set_terminal_cursor(&mut self, config: &Config, term: &mut Term) { - let (x, y) = self.display_cursor(config); - if self.mode_dirty { - match self.mode { - Mode::Normal => term.set_cursor_style(CursorStyle::Default), - Mode::Insert => term.set_cursor_style(CursorStyle::Line), - } - } - term.set_cursor_position(y, x + self.view.offset_x); - self.mode_dirty = false; - } - pub fn number_width(&mut self) -> usize { if self.lines.len() == 0 { 1 diff --git a/red/src/command.rs b/red/src/command.rs index ea09d90e..1af14e24 100644 --- a/red/src/command.rs +++ b/red/src/command.rs @@ -47,7 +47,14 @@ fn cmd_force_write(state: &mut State, args: &[&str]) -> Result<(), Error> { buffer.set_path(path); } - buffer.save() + buffer.save()?; + + if let Some(name) = buffer.name() { + let status = format!("{:?} written", name); + state.set_status(status); + } + + Ok(()) } fn cmd_edit(state: &mut State, args: &[&str]) -> Result<(), Error> { diff --git a/red/src/error.rs b/red/src/error.rs index 69c22454..acde170b 100644 --- a/red/src/error.rs +++ b/red/src/error.rs @@ -1,4 +1,4 @@ -use std::io; +use std::{io, fmt}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -13,6 +13,19 @@ pub enum Error { UnsavedBuffer(&'static str), #[error("Invalid command, usage: {0}")] InvalidCommand(&'static str), - #[error("Unknown command: {0:?}")] + #[error("Unknown command: `{0}`")] UnknownCommand(String), + #[error("Terminal error: {0:?}")] + TerminalError(io::Error), + #[error("Terminal error: {0:?}")] + TerminalFmtError(fmt::Error), + #[error("Terminal input error: {0:?}")] + InputError(InputError) +} + +#[derive(Debug)] +pub enum InputError { + InvalidPrefixByte(u8), + DecodeError(std::str::Utf8Error), + IoError(io::Error), } diff --git a/red/src/main.rs b/red/src/main.rs index ac2e8c30..58e7305a 100644 --- a/red/src/main.rs +++ b/red/src/main.rs @@ -1,15 +1,13 @@ #![feature(let_chains, rustc_private)] #![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_raw_fd, yggdrasil_os))] -use std::{env, path::Path, fmt::Write}; +use std::{env, fmt::Write, path::Path}; use buffer::{Buffer, Mode, SetMode}; use config::Config; use error::Error; use term::{Clear, Color, Term}; -use crate::term::Terminal; - pub mod buffer; pub mod command; pub mod config; @@ -17,7 +15,7 @@ pub mod error; pub mod line; pub mod term; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq)] pub enum TopMode { Normal, Command, @@ -28,14 +26,15 @@ pub struct State { buffer: Buffer, command: String, message: Option, + status: Option, top_mode: TopMode, config: Config, running: bool, number_width: usize, } -fn display_modeline(term: &mut Term, top_mode: TopMode, buf: &Buffer) { - term.set_cursor_position(buf.height(), 0); +fn display_modeline(term: &mut Term, top_mode: TopMode, buf: &Buffer) -> Result<(), Error> { + term.set_cursor_position(buf.height(), 0)?; let bg = match (top_mode, buf.mode()) { (TopMode::Normal, Mode::Normal) => Color::Yellow, @@ -43,36 +42,35 @@ fn display_modeline(term: &mut Term, top_mode: TopMode, buf: &Buffer) { (TopMode::Command, _) => Color::Green, }; - term.set_background(bg); - term.set_foreground(Color::Black); + term.set_background(bg)?; + term.set_foreground(Color::Black)?; match top_mode { TopMode::Normal => { - term.put_byte(b' '); - term.put_bytes(buf.mode().as_str()); - term.put_byte(b' '); + write!(term, " {} ", buf.mode().as_str()).map_err(Error::TerminalFmtError)?; if buf.is_modified() { - term.set_background(Color::Magenta); - term.set_foreground(Color::Default); + term.set_background(Color::Magenta)?; + term.set_foreground(Color::Default)?; } else { - term.set_foreground(Color::Green); - term.set_background(Color::Default); + term.set_foreground(Color::Green)?; + term.set_background(Color::Default)?; } } TopMode::Command => { - term.put_bytes(b" COMMAND "); + write!(term, " COMMAND ").map_err(Error::TerminalFmtError)?; - term.set_foreground(Color::Green); - term.set_background(Color::Default); + term.set_foreground(Color::Green)?; + term.set_background(Color::Default)?; } } - term.put_byte(b' '); - term.put_bytes(buf.name().map(String::as_str).unwrap_or("")); - term.clear(Clear::LineToEnd); + let name = buf.name().map(String::as_str).unwrap_or(""); + write!(term, " {}", name).map_err(Error::TerminalFmtError)?; + term.clear(Clear::LineToEnd)?; + term.reset_style()?; - term.reset_style(); + Ok(()) } impl State { @@ -82,9 +80,9 @@ impl State { Some(path) => Buffer::open(path).unwrap(), None => Buffer::empty(), }; - let term = Term::open(); + let term = Term::open()?; - let (w, h) = term.size(); + let (w, h) = term.size()?; if config.number { let nw = buffer.number_width() + 2; buffer.resize(&config, nw, w - nw - 1, h - 2); @@ -96,6 +94,7 @@ impl State { number_width: buffer.number_width(), top_mode: TopMode::Normal, message: None, + status: None, command: String::new(), running: true, buffer, @@ -116,24 +115,25 @@ impl State { self.running = false; } + pub fn set_status>(&mut self, status: S) { + self.status.replace(status.into()); + } + fn display_number(&mut self) -> Result<(), Error> { let start = self.buffer.row_offset(); let end = self.buffer.len(); for i in 0.. { - self.term.set_cursor_position(i, 0); + self.term.set_cursor_position(i, 0)?; if i + start == self.buffer.cursor_row() { - self.term.set_bright(true); - self.term.set_foreground(Color::Yellow); + self.term.set_bright(true)?; + self.term.set_foreground(Color::Yellow)?; } if i + start < end { - write!(self.term, " {0:1$} ", i + start + 1, self.number_width).ok(); - } else { - for _ in 0..self.number_width + 2 { - self.term.put_byte(b' '); - } + write!(self.term, " {0:1$} ", i + start + 1, self.number_width) + .map_err(Error::TerminalFmtError)?; } if i == self.buffer.height() { @@ -141,45 +141,55 @@ impl State { } if i + start == self.buffer.cursor_row() { - self.term.reset_style(); + self.term.reset_style()?; } } - self.term.reset_style(); + self.term.reset_style()?; Ok(()) } fn display(&mut self) -> Result<(), Error> { if self.buffer.is_dirty() { - self.term.clear(Clear::All); + self.term.clear(Clear::All)?; } if self.config.number && self.buffer.is_dirty() { self.display_number()?; } - self.buffer.display(&self.config, &mut self.term); + self.buffer.display(&self.config, &mut self.term)?; + + if self.top_mode != TopMode::Command { + if let Some(status) = &self.status { + self.term + .set_cursor_position(self.buffer().height() + 1, 0)?; + self.term + .write_str(status.as_str()) + .map_err(Error::TerminalFmtError)?; + } + } if let Some(msg) = &self.message { - self.term.set_cursor_position(self.buffer.height(), 0); - self.term.put_bytes(msg); - self.term.flush(); + self.term.set_cursor_position(self.buffer.height(), 0)?; + self.term.write_str(msg).map_err(Error::TerminalFmtError)?; + self.term.flush()?; return Ok(()); } - display_modeline(&mut self.term, self.top_mode, &self.buffer); + display_modeline(&mut self.term, self.top_mode, &self.buffer)?; match self.top_mode { - TopMode::Normal => self - .buffer - .set_terminal_cursor(&self.config, &mut self.term), + TopMode::Normal => { + self.buffer + .set_terminal_cursor(&self.config, &mut self.term)?; + } TopMode::Command => { - self.term.set_cursor_position(self.buffer.height() + 1, 0); - self.term.put_byte(b':'); - self.term.put_bytes(self.command.as_bytes()); + self.term.set_cursor_position(self.buffer.height() + 1, 0)?; + write!(self.term, ":{}", self.command.as_str()).map_err(Error::TerminalFmtError)?; } } - self.term.flush(); + self.term.flush()?; Ok(()) } @@ -220,6 +230,7 @@ impl State { } ':' => { self.command.clear(); + self.status = None; self.top_mode = TopMode::Command; Ok(()) } @@ -231,6 +242,10 @@ impl State { } } + if self.buffer().mode() != Mode::Normal { + self.status = None; + } + Ok(()) } } @@ -265,16 +280,14 @@ impl State { if nw != self.number_width { self.number_width = nw; let nw = nw + 2; - let (w, h) = self.term.size(); + let (w, h) = self.term.size()?; self.buffer.resize(&self.config, nw, w - nw - 1, h - 2); } } self.display()?; - let Some(key) = self.term.read_key() else { - return Ok(()); - }; + let key = self.term.read_key()?; if self.message.is_some() { self.message = None; @@ -299,7 +312,7 @@ impl State { } pub fn cleanup(&mut self) { - self.term.clear(Clear::All); + self.term.clear(Clear::All).ok(); } } diff --git a/red/src/term/common.rs b/red/src/term/common.rs index 91db63d0..e69de29b 100644 --- a/red/src/term/common.rs +++ b/red/src/term/common.rs @@ -1,85 +0,0 @@ -use std::io::{stdin, stdout, Read, Stdin, Stdout, Write}; - -use crossterm::{ - cursor, execute, queue, style, - terminal::{ - self, disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, - LeaveAlternateScreen, - }, - tty::IsTty, - ExecutableCommand, -}; - -use super::Terminal; - -pub struct Term { - stdin: Stdin, - stdout: Stdout, -} - -impl Terminal for Term { - fn is_tty() -> bool { - stdout().is_tty() - } - - fn open() -> Self { - let stdin = stdin(); - let mut stdout = stdout(); - - execute!( - stdout, - EnterAlternateScreen, - Clear(ClearType::All), - cursor::MoveTo(0, 0) - ) - .unwrap(); - enable_raw_mode().unwrap(); - - Self { stdin, stdout } - } - - fn set_cursor_position(&mut self, row: usize, column: usize) { - queue!(self.stdout, cursor::MoveTo(column as _, row as _)).ok(); - } - fn set_cursor_visible(&mut self, visible: bool) { - if visible { - queue!(self.stdout, cursor::Show).ok(); - } else { - queue!(self.stdout, cursor::Hide).ok(); - } - } - fn size(&self) -> (usize, usize) { - let (w, h) = terminal::size().unwrap(); - (w as _, h as _) - } - - fn put_bytes>(&mut self, s: B) { - self.stdout.write(s.as_ref()).ok(); - } - fn put_byte(&mut self, ch: u8) { - self.put_bytes(&[ch]); - } - fn flush(&mut self) { - self.stdout.flush().ok(); - } - fn clear(&mut self) { - queue!(self.stdout, Clear(ClearType::All)).ok(); - } - - fn read_key(&mut self) -> Option { - let mut buf = [0; 1]; - let len = self.stdin.read(&mut buf).unwrap(); - if len != 0 { - Some(buf[0]) - } else { - None - } - } -} - -impl Drop for Term { - fn drop(&mut self) { - disable_raw_mode().ok(); - execute!(self.stdout, LeaveAlternateScreen).ok(); - } -} diff --git a/red/src/term/input.rs b/red/src/term/input.rs new file mode 100644 index 00000000..720f3de1 --- /dev/null +++ b/red/src/term/input.rs @@ -0,0 +1,49 @@ +use std::io::{Read, Stdin}; + +use crate::error::InputError; + +pub trait ReadChar { + fn read_char(&mut self) -> Result; +} + +impl ReadChar for Stdin { + fn read_char(&mut self) -> Result { + let mut buf = [0; 4]; + self.read_exact(&mut buf[..1]) + .map_err(InputError::IoError)?; + + let len = utf8_len_prefix(buf[0]).ok_or(InputError::InvalidPrefixByte(buf[0]))?; + + if len != 0 { + self.read_exact(&mut buf[1..=len]) + .map_err(InputError::IoError)?; + } + + // TODO optimize + let s = core::str::from_utf8(&buf[..len + 1]).map_err(InputError::DecodeError)?; + Ok(s.chars().next().unwrap()) + } +} + +const fn utf8_len_prefix(l: u8) -> Option { + let mask0 = 0b10000000; + let val0 = 0; + let mask1 = 0b11100000; + let val1 = 0b11000000; + let mask2 = 0b11110000; + let val2 = 0b11100000; + let mask3 = 0b11111000; + let val3 = 0b11110000; + + if l & mask3 == val3 { + Some(3) + } else if l & mask2 == val2 { + Some(2) + } else if l & mask1 == val1 { + Some(1) + } else if l & mask0 == val0 { + Some(0) + } else { + None + } +} diff --git a/red/src/term/linux.rs b/red/src/term/linux.rs new file mode 100644 index 00000000..1bd1328d --- /dev/null +++ b/red/src/term/linux.rs @@ -0,0 +1,53 @@ +use std::{ + io::{Stdin, Stdout, self}, + mem::MaybeUninit, + os::fd::AsRawFd, +}; + +pub struct RawMode { + saved_termios: libc::termios, +} + +impl RawMode { + pub unsafe fn enter(stdin: &Stdin) -> Result { + let mut old = MaybeUninit::uninit(); + + if libc::tcgetattr(stdin.as_raw_fd(), old.as_mut_ptr()) != 0 { + return Err(io::Error::last_os_error()); + } + + let old = old.assume_init(); + let mut new = old; + new.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN); + new.c_iflag &= !(libc::IGNBRK + | libc::BRKINT + | libc::PARMRK + | libc::ISTRIP + | libc::INLCR + | libc::IGNCR + | libc::ICRNL + | libc::IXON); + new.c_oflag &= !libc::OPOST; + new.c_cflag &= !(libc::PARENB | libc::CSIZE); + new.c_cflag |= libc::CS8; + + if libc::tcsetattr(stdin.as_raw_fd(), libc::TCSANOW, &new) != 0 { + return Err(io::Error::last_os_error()); + } + + Ok(Self { saved_termios: old }) + } + + pub unsafe fn leave(&self, stdin: &Stdin) { + libc::tcsetattr(stdin.as_raw_fd(), libc::TCSANOW, &self.saved_termios); + } +} + +pub unsafe fn terminal_size(stdout: &Stdout) -> io::Result<(usize, usize)> { + let mut size: MaybeUninit = MaybeUninit::uninit(); + if libc::ioctl(stdout.as_raw_fd(), libc::TIOCGWINSZ, size.as_mut_ptr()) != 0 { + return Err(io::Error::last_os_error()); + } + let size = size.assume_init(); + Ok((size.ws_col as _, size.ws_row as _)) +} diff --git a/red/src/term/mod.rs b/red/src/term/mod.rs index e72e7d6b..71546a26 100644 --- a/red/src/term/mod.rs +++ b/red/src/term/mod.rs @@ -1,12 +1,37 @@ -// #[cfg(not(target_os = "yggdrasil"))] -// pub mod common; -// -// #[cfg(not(target_os = "yggdrasil"))] -// pub use common::Term; +use std::{io::{Stdout, Write, Stdin, stdin, stdout, self}, fmt}; -pub mod simple; +use crate::{error::Error, term::input::ReadChar}; -pub use simple::Term; +use self::sys::RawMode; + +#[cfg(target_os = "yggdrasil")] +mod ygg; +#[cfg(target_os = "yggdrasil")] +use ygg as sys; + +#[cfg(not(target_os = "yggdrasil"))] +mod linux; +#[cfg(not(target_os = "yggdrasil"))] +use linux as sys; + +mod input; + +pub trait RawTerminal { + fn raw_enter_alternate_mode(&mut self) -> io::Result<()>; + fn raw_leave_alternate_mode(&mut self) -> io::Result<()>; + fn raw_clear_all(&mut self) -> io::Result<()>; + fn raw_clear_line(&mut self, what: u32) -> io::Result<()>; + fn raw_move_cursor(&mut self, row: usize, column: usize) -> io::Result<()>; + fn raw_set_cursor_style(&mut self, style: CursorStyle) -> io::Result<()>; + fn raw_set_color(&mut self, fgbg: u32, color: Color) -> io::Result<()>; + fn raw_set_style(&mut self, what: u32) -> io::Result<()>; +} + +pub struct Term { + stdin: Stdin, + stdout: Stdout, + raw: RawMode, +} #[derive(Debug, Clone, Copy)] pub enum CursorStyle { @@ -34,26 +59,128 @@ pub enum Clear { LineToEnd, } -pub trait Terminal { - fn is_tty() -> bool; - fn open() -> Self; +impl RawTerminal for Stdout { + fn raw_enter_alternate_mode(&mut self) -> io::Result<()> { + self.write_all(b"\x1B[?1049h") + } - // Cursor & size - fn set_cursor_position(&mut self, row: usize, column: usize); - fn set_cursor_visible(&mut self, visible: bool); - fn set_cursor_style(&mut self, style: CursorStyle); - fn size(&self) -> (usize, usize); + fn raw_leave_alternate_mode(&mut self) -> io::Result<()> { + self.write_all(b"\x1B[?1049l") + } - // Display - fn put_bytes>(&mut self, s: B); - fn put_byte(&mut self, ch: u8); - fn set_foreground(&mut self, color: Color); - fn set_background(&mut self, color: Color); - fn set_bright(&mut self, bright: bool); - fn reset_style(&mut self); - fn flush(&mut self); - fn clear(&mut self, clear: Clear); + fn raw_clear_all(&mut self) -> io::Result<()> { + self.write_all(b"\x1B[2J") + } - // Input - fn read_key(&mut self) -> Option; + fn raw_clear_line(&mut self, what: u32) -> io::Result<()> { + write!(self, "\x1B[{}K", what) + } + + fn raw_move_cursor(&mut self, row: usize, column: usize) -> io::Result<()> { + write!(self, "\x1B[{};{}f", row + 1, column + 1) + } + + fn raw_set_cursor_style(&mut self, style: CursorStyle) -> io::Result<()> { + // TODO yggdrasil support for cursor styles + #[cfg(not(target_os = "yggdrasil"))] + { + match style { + CursorStyle::Default => self.write_all(b"\x1B[0 q")?, + CursorStyle::Line => self.write_all(b"\x1B[6 q")?, + } + } + #[cfg(target_os = "yggdrasil")] + { + let _ = style; + } + Ok(()) + } + + fn raw_set_color(&mut self, fgbg: u32, color: Color) -> io::Result<()> { + write!(self, "\x1B[{}{}m", fgbg, color as u32) + } + + fn raw_set_style(&mut self, what: u32) -> io::Result<()> { + write!(self, "\x1B[{}m", what) + } +} + +impl Term { + pub fn is_tty() -> bool { + // TODO + true + } + + pub fn open() -> Result { + let stdin = stdin(); + let mut stdout = stdout(); + + // Set stdin to raw mode + let raw = unsafe { RawMode::enter(&stdin).map_err(Error::TerminalError)? }; + + stdout.raw_enter_alternate_mode().map_err(Error::TerminalError)?; + stdout.raw_clear_all().map_err(Error::TerminalError)?; + stdout.raw_move_cursor(0, 0).map_err(Error::TerminalError)?; + + Ok(Self { stdin, stdout, raw }) + } + + pub fn set_cursor_position(&mut self, row: usize, column: usize) -> Result<(), Error> { + self.stdout.raw_move_cursor(row, column).map_err(Error::TerminalError) + } + pub fn set_cursor_visible(&mut self, _visible: bool) -> Result<(), Error> { + Ok(()) + } + pub fn set_cursor_style(&mut self, style: CursorStyle) -> Result<(), Error> { + self.stdout.raw_set_cursor_style(style).map_err(Error::TerminalError) + } + pub fn size(&self) -> Result<(usize, usize), Error> { + unsafe { sys::terminal_size(&self.stdout).map_err(Error::TerminalError) } + } + + pub fn set_foreground(&mut self, color: Color) -> Result<(), Error> { + self.stdout.raw_set_color(3, color).map_err(Error::TerminalError) + } + pub fn set_background(&mut self, color: Color) -> Result<(), Error> { + self.stdout.raw_set_color(4, color).map_err(Error::TerminalError) + } + pub fn set_bright(&mut self, bright: bool) -> Result<(), Error> { + if bright { + self.stdout.raw_set_style(2) + } else { + self.stdout.raw_set_style(22) + }.map_err(Error::TerminalError) + } + pub fn reset_style(&mut self) -> Result<(), Error> { + self.stdout.raw_set_style(0).map_err(Error::TerminalError) + } + pub fn clear(&mut self, clear: Clear) -> Result<(), Error> { + match clear { + Clear::All => self.stdout.raw_clear_all(), + Clear::LineToEnd => self.stdout.raw_clear_line(0), + }.map_err(Error::TerminalError) + } + + pub fn read_key(&mut self) -> Result { + self.stdin.read_char().map_err(Error::InputError) + } + + pub fn flush(&mut self) -> Result<(), Error> { + self.stdout.flush().map_err(Error::TerminalError) + } +} + +impl fmt::Write for Term { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.stdout.write_all(s.as_bytes()).map_err(|_| fmt::Error::default()) + } +} + +impl Drop for Term { + fn drop(&mut self) { + unsafe { + self.raw.leave(&self.stdin); + } + self.stdout.raw_leave_alternate_mode().ok(); + } } diff --git a/red/src/term/simple.rs b/red/src/term/simple.rs deleted file mode 100644 index 44fafb9a..00000000 --- a/red/src/term/simple.rs +++ /dev/null @@ -1,285 +0,0 @@ -use std::{ - io::{stdin, stdout, Read, Stdin, Stdout, Write}, - mem::MaybeUninit, fmt, -}; - -use super::{Clear, Color, CursorStyle, Terminal}; - -struct RawMode { - #[cfg(not(target_os = "yggdrasil"))] - saved_termios: libc::termios, - #[cfg(target_os = "yggdrasil")] - saved_termios: std::os::yggdrasil::io::TerminalOptions, -} - -pub struct Term { - stdin: Stdin, - stdout: Stdout, - raw: RawMode, -} - -#[cfg(target_os = "yggdrasil")] -impl RawMode { - unsafe fn enter(stdin: &Stdin) -> Option { - use std::os::yggdrasil::io::TerminalOptions; - - let saved_termios = std::os::yggdrasil::io::update_terminal_options(stdin, |_| { - TerminalOptions::raw_input() - }) - .ok()?; - - Some(Self { saved_termios }) - } - - unsafe fn leave(&self, stdin: &Stdin) { - std::os::yggdrasil::io::update_terminal_options(stdin, |_| self.saved_termios.clone()).ok(); - } -} - -#[cfg(not(target_os = "yggdrasil"))] -impl RawMode { - unsafe fn enter(stdin: &Stdin) -> Option { - use std::os::fd::AsRawFd; - - let mut old = MaybeUninit::uninit(); - - if libc::tcgetattr(stdin.as_raw_fd(), old.as_mut_ptr()) != 0 { - return None; - } - - let old = old.assume_init(); - let mut new = old; - new.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN); - new.c_iflag &= !(libc::IGNBRK - | libc::BRKINT - | libc::PARMRK - | libc::ISTRIP - | libc::INLCR - | libc::IGNCR - | libc::ICRNL - | libc::IXON); - new.c_oflag &= !libc::OPOST; - new.c_cflag &= !(libc::PARENB | libc::CSIZE); - new.c_cflag |= libc::CS8; - - if libc::tcsetattr(stdin.as_raw_fd(), libc::TCSANOW, &new) != 0 { - return None; - } - - Some(Self { saved_termios: old }) - } - - unsafe fn leave(&self, stdin: &Stdin) { - use std::os::fd::AsRawFd; - libc::tcsetattr(stdin.as_raw_fd(), libc::TCSANOW, &self.saved_termios); - } -} - -#[cfg(not(target_os = "yggdrasil"))] -unsafe fn terminal_size(stdout: &Stdout) -> std::io::Result<(usize, usize)> { - use std::os::fd::AsRawFd; - let mut size: MaybeUninit = MaybeUninit::uninit(); - if libc::ioctl(stdout.as_raw_fd(), libc::TIOCGWINSZ, size.as_mut_ptr()) != 0 { - todo!(); - } - let size = size.assume_init(); - Ok((size.ws_col as _, size.ws_row as _)) -} - -#[cfg(target_os = "yggdrasil")] -unsafe fn terminal_size(stdout: &Stdout) -> std::io::Result<(usize, usize)> { - use std::os::yggdrasil::io::{DeviceRequest, FdDeviceRequest}; - let mut req = DeviceRequest::GetTerminalSize(MaybeUninit::uninit()); - if let Err(_) = stdout.device_request(&mut req) { - // Fallback - return Ok((60, 20)); - } - let DeviceRequest::GetTerminalSize(size) = req else { - unreachable!(); - }; - let size = size.assume_init(); - Ok((size.columns, size.rows)) -} - -pub trait ReadChar { - fn read_char(&mut self) -> Option; -} - -impl ReadChar for Stdin { - fn read_char(&mut self) -> Option { - let mut buf = [0; 4]; - self.read_exact(&mut buf[..1]).ok()?; - - let len = utf8_len_prefix(buf[0])?; - - if len != 0 { - self.read_exact(&mut buf[1..=len]).ok()?; - } - - // TODO optimize - let s = core::str::from_utf8(&buf[..len + 1]).ok()?; - s.chars().next() - } -} - -const fn utf8_len_prefix(l: u8) -> Option { - let mask0 = 0b10000000; - let val0 = 0; - let mask1 = 0b11100000; - let val1 = 0b11000000; - let mask2 = 0b11110000; - let val2 = 0b11100000; - let mask3 = 0b11111000; - let val3 = 0b11110000; - - if l & mask3 == val3 { - Some(3) - } else if l & mask2 == val2 { - Some(2) - } else if l & mask1 == val1 { - Some(1) - } else if l & mask0 == val0 { - Some(0) - } else { - None - } -} - -impl Term { - fn enter_alternate_mode(out: &mut O) { - out.write_all(b"\x1B[?1049h").ok(); - } - - fn leave_alternate_mode(out: &mut O) { - out.write_all(b"\x1B[?1049l").ok(); - } - - fn clear_all(out: &mut O) { - out.write_all(b"\x1B[2J").ok(); - } - - fn clear_line(out: &mut O, what: u32) { - out.write_all(format!("\x1B[{}K", what).as_bytes()).ok(); - } - - fn move_cursor(out: &mut O, row: usize, column: usize) { - out.write_all(format!("\x1B[{};{}f", row + 1, column + 1).as_bytes()) - .ok(); - } - - fn set_cursor_style_raw(out: &mut O, style: CursorStyle) { - // TODO yggdrasil support for cursor styles - #[cfg(not(target_os = "yggdrasil"))] - { - match style { - CursorStyle::Default => out.write_all(b"\x1B[0 q"), - CursorStyle::Line => out.write_all(b"\x1B[6 q"), - } - .ok(); - } - } - - fn set_color(out: &mut O, fgbg: u32, color: Color) { - out.write_all(format!("\x1B[{}{}m", fgbg, color as u32).as_bytes()) - .ok(); - } -} - -impl Terminal for Term { - fn is_tty() -> bool { - // TODO - true - } - - fn open() -> Self { - let stdin = stdin(); - let mut stdout = stdout(); - - // Set stdin to raw mode - let raw = unsafe { RawMode::enter(&stdin).unwrap() }; // unsafe { Self::enable_raw(&stdin).unwrap() }; - Self::enter_alternate_mode(&mut stdout); - Self::clear_all(&mut stdout); - Self::move_cursor(&mut stdout, 0, 0); - - Self { stdin, stdout, raw } - } - - fn set_cursor_position(&mut self, row: usize, column: usize) { - Self::move_cursor(&mut self.stdout, row, column) - } - fn set_cursor_visible(&mut self, _visible: bool) {} - fn set_cursor_style(&mut self, style: CursorStyle) { - Self::set_cursor_style_raw(&mut self.stdout, style); - } - fn size(&self) -> (usize, usize) { - unsafe { terminal_size(&self.stdout).unwrap() } - // #[cfg(target_os = "yggdrasil")] - // { - // (80, 30) - // } - // #[cfg(not(target_os = "yggdrasil"))] - // { - // (80, 25) - // } - } - - fn put_bytes>(&mut self, s: B) { - self.stdout.write_all(s.as_ref()).ok(); - } - fn put_byte(&mut self, ch: u8) { - self.put_bytes([ch]); - } - fn set_foreground(&mut self, color: Color) { - Self::set_color(&mut self.stdout, 3, color) - } - fn set_background(&mut self, color: Color) { - Self::set_color(&mut self.stdout, 4, color) - } - fn set_bright(&mut self, bright: bool) { - if bright { - self.stdout.write_all(b"\x1B[1m").ok(); - } else { - self.stdout.write_all(b"\x1B[22m").ok(); - } - } - fn reset_style(&mut self) { - self.stdout.write_all(b"\x1B[0m").ok(); - } - fn flush(&mut self) { - self.stdout.flush().ok(); - } - fn clear(&mut self, clear: Clear) { - match clear { - Clear::All => Self::clear_all(&mut self.stdout), - Clear::LineToEnd => Self::clear_line(&mut self.stdout, 0), - } - } - - fn read_key(&mut self) -> Option { - self.stdin.read_char() - - // let mut buf = [0; 1]; - // let len = self.stdin.read(&mut buf).unwrap(); - // if len != 0 { - // Some(buf[0]) - // } else { - // None - // } - } -} - -impl fmt::Write for Term { - fn write_str(&mut self, s: &str) -> fmt::Result { - self.put_bytes(s); - Ok(()) - } -} - -impl Drop for Term { - fn drop(&mut self) { - unsafe { - self.raw.leave(&self.stdin); - } - Self::leave_alternate_mode(&mut self.stdout); - } -} diff --git a/red/src/term/ygg.rs b/red/src/term/ygg.rs new file mode 100644 index 00000000..c6c34f1a --- /dev/null +++ b/red/src/term/ygg.rs @@ -0,0 +1,30 @@ +use std::{ + io::{self, Stdin, Stdout}, + mem::MaybeUninit, + os::yggdrasil::io::{update_terminal_options, DeviceRequest, FdDeviceRequest, TerminalOptions}, +}; + +pub struct RawMode(TerminalOptions); + +impl RawMode { + pub unsafe fn enter(stdin: &Stdin) -> io::Result { + update_terminal_options(stdin, |_| TerminalOptions::raw_input()).map(RawMode) + } + + pub unsafe fn leave(&self, stdin: &Stdin) { + update_terminal_options(stdin, |_| self.0).ok(); + } +} + +pub unsafe fn terminal_size(stdout: &Stdout) -> io::Result<(usize, usize)> { + let mut req = DeviceRequest::GetTerminalSize(MaybeUninit::uninit()); + if let Err(_) = stdout.device_request(&mut req) { + // Fallback + return Ok((60, 20)); + } + let DeviceRequest::GetTerminalSize(size) = req else { + unreachable!(); + }; + let size = size.assume_init(); + Ok((size.columns, size.rows)) +}