red: better terminal interface
This commit is contained in:
parent
546010762f
commit
d030e0d6f1
@ -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
|
||||
|
@ -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> {
|
||||
|
@ -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),
|
||||
}
|
||||
|
117
red/src/main.rs
117
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<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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
49
red/src/term/input.rs
Normal 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
53
red/src/term/linux.rs
Normal 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 _))
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
30
red/src/term/ygg.rs
Normal 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))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user