red: better terminal interface

This commit is contained in:
Mark Poliakov 2023-11-20 11:21:25 +02:00
parent 546010762f
commit d030e0d6f1
10 changed files with 412 additions and 482 deletions

View File

@ -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::<String>();
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

View File

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

View File

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

View File

@ -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<String>,
status: Option<String>,
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("<unnamed>"));
term.clear(Clear::LineToEnd);
let name = buf.name().map(String::as_str).unwrap_or("<unnamed>");
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<S: Into<String>>(&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();
}
}

View File

@ -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<B: AsRef<[u8]>>(&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<u8> {
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();
}
}

49
red/src/term/input.rs Normal file
View File

@ -0,0 +1,49 @@
use std::io::{Read, Stdin};
use crate::error::InputError;
pub trait ReadChar {
fn read_char(&mut self) -> Result<char, InputError>;
}
impl ReadChar for Stdin {
fn read_char(&mut self) -> Result<char, InputError> {
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<usize> {
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
}
}

53
red/src/term/linux.rs Normal file
View File

@ -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<Self, io::Error> {
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<libc::winsize> = 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 _))
}

View File

@ -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<B: AsRef<[u8]>>(&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<char>;
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<Self, Error> {
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<char, Error> {
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();
}
}

View File

@ -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<Self> {
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<Self> {
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<libc::winsize> = 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<char>;
}
impl ReadChar for Stdin {
fn read_char(&mut self) -> Option<char> {
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<usize> {
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<O: Write>(out: &mut O) {
out.write_all(b"\x1B[?1049h").ok();
}
fn leave_alternate_mode<O: Write>(out: &mut O) {
out.write_all(b"\x1B[?1049l").ok();
}
fn clear_all<O: Write>(out: &mut O) {
out.write_all(b"\x1B[2J").ok();
}
fn clear_line<O: Write>(out: &mut O, what: u32) {
out.write_all(format!("\x1B[{}K", what).as_bytes()).ok();
}
fn move_cursor<O: Write>(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<O: Write>(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<O: Write>(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<B: AsRef<[u8]>>(&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<char> {
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);
}
}

30
red/src/term/ygg.rs Normal file
View File

@ -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<Self> {
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))
}