term/sysutils: alternate mode, cursor hide/show, top-like utility

This commit is contained in:
Mark Poliakov 2025-03-02 17:27:26 +02:00
parent 59b34fb269
commit 771c553571
20 changed files with 582 additions and 83 deletions

20
userspace/Cargo.lock generated
View File

@ -285,6 +285,12 @@ dependencies = [
"wayland-client",
]
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.2.14"
@ -1359,6 +1365,7 @@ version = "0.1.0"
dependencies = [
"libc",
"thiserror",
"tui",
"yggdrasil-rt",
]
@ -2690,6 +2697,7 @@ dependencies = [
"serde_json",
"sha2",
"thiserror",
"tui",
"yggdrasil-abi",
"yggdrasil-rt",
]
@ -2867,6 +2875,18 @@ version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "tui"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
dependencies = [
"bitflags 1.3.2",
"cassowary",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "typed-arena"
version = "2.0.2"

View File

@ -37,6 +37,7 @@ env_logger = "0.11.5"
sha2 = { version = "0.10.8" }
chrono = { version = "0.4.31", default-features = false }
postcard = { version = "1.1.1", features = ["alloc"] }
tui = { version = "0.19.0", default-features = false }
raqote = { version = "0.8.3", default-features = false }

View File

@ -1,6 +1,11 @@
use crate::sys;
use std::io;
use crate::sys;
pub fn set_sigint_handler(handler: fn()) {
sys::set_sigint_handler(handler);
}
pub fn send_kill(pid: u32) -> io::Result<()> {
sys::send_kill(pid)
}

View File

@ -52,16 +52,8 @@ impl sys::FileMapping for FileMappingImpl {
if write {
flags |= libc::PROT_WRITE;
}
let pointer = unsafe {
libc::mmap(
null_mut(),
size,
flags,
libc::MAP_SHARED,
fd.as_raw_fd(),
0,
)
};
let pointer =
unsafe { libc::mmap(null_mut(), size, flags, libc::MAP_SHARED, fd.as_raw_fd(), 0) };
if pointer == libc::MAP_FAILED {
return Err(io::Error::last_os_error());
}

View File

@ -6,7 +6,7 @@ pub mod socket;
pub mod term;
pub mod timer;
use std::{ffi::c_int, sync::Mutex};
use std::{ffi::c_int, io, sync::Mutex};
pub use mem::{FileMappingImpl, SharedMemoryImpl};
pub use pid::PidFdImpl;
@ -29,3 +29,12 @@ pub fn set_sigint_handler(handler: fn()) {
*SIGINT_HANDLER.lock().unwrap() = handler;
unsafe { libc::signal(libc::SIGINT, sigint_proxy as usize) };
}
pub fn send_kill(pid: u32) -> io::Result<()> {
let res = unsafe { libc::kill(pid as c_int, libc::SIGKILL) };
if res == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}

View File

@ -1,4 +1,7 @@
use std::{io, os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}};
use std::{
io,
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
};
use crate::sys::PidFd;

View File

@ -1,4 +1,7 @@
use std::{io::{self, Read, Write}, os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}};
use std::{
io::{self, Read, Write},
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
};
use crate::sys::Pipe;

View File

@ -1,5 +1,9 @@
use std::{
io, mem::size_of, os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}, ptr::null_mut, time::Duration
io,
mem::size_of,
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
ptr::null_mut,
time::Duration,
};
use crate::sys::TimerFd;
@ -10,24 +14,24 @@ pub struct TimerFdImpl {
impl TimerFd for TimerFdImpl {
fn new() -> io::Result<Self> {
let fd = unsafe { libc::timerfd_create(libc::CLOCK_MONOTONIC, libc::TFD_NONBLOCK | libc::TFD_CLOEXEC) };
let fd = unsafe {
libc::timerfd_create(
libc::CLOCK_MONOTONIC,
libc::TFD_NONBLOCK | libc::TFD_CLOEXEC,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
Ok(Self {
fd
})
Ok(Self { fd })
}
fn start(&mut self, timeout: Duration) -> io::Result<()> {
let tv_sec = timeout.as_secs() as _;
let tv_nsec = timeout.subsec_nanos().into();
let spec = libc::itimerspec {
it_value: libc::timespec {
tv_sec,
tv_nsec,
},
it_value: libc::timespec { tv_sec, tv_nsec },
it_interval: libc::timespec {
tv_sec: 0,
tv_nsec: 0,

View File

@ -1,18 +1,30 @@
pub mod poll;
pub mod timer;
pub mod mem;
pub mod pid;
pub mod pipe;
pub mod term;
pub mod poll;
pub mod socket;
pub mod mem;
pub mod term;
pub mod timer;
pub use poll::PollImpl;
pub use timer::TimerFdImpl;
use std::io;
pub use mem::{FileMappingImpl, SharedMemoryImpl};
pub use pid::PidFdImpl;
pub use pipe::PipeImpl;
pub use poll::PollImpl;
pub use socket::{BorrowedAddressImpl, LocalPacketSocketImpl, OwnedAddressImpl};
pub use term::RawStdinImpl;
pub use socket::{LocalPacketSocketImpl, OwnedAddressImpl, BorrowedAddressImpl};
pub use mem::{SharedMemoryImpl, FileMappingImpl};
pub use timer::TimerFdImpl;
pub fn set_sigint_handler(_handler: fn()) {
pub fn set_sigint_handler(_handler: fn()) {}
pub fn send_kill(pid: u32) -> io::Result<()> {
use runtime::rt::{
process::{ProcessId, Signal},
sys as syscall,
};
let target = unsafe { ProcessId::from_raw(pid) };
unsafe { syscall::send_signal(target, Signal::Killed) }.map_err(io::Error::from)?;
Ok(())
}

View File

@ -30,7 +30,6 @@ pub enum OwnedAddressImpl {
Anonymous(u64),
}
impl<'a> BorrowedAddressImpl<'a> {
pub fn to_sys(&self) -> LocalSocketAddress<'a> {
match *self {
@ -59,14 +58,14 @@ impl sys::OwnedAddress for OwnedAddressImpl {
fn as_borrowed(&self) -> Self::Borrowed<'_> {
match self {
Self::Named(path) => BorrowedAddressImpl::Named(path.as_path()),
&Self::Anonymous(id) => BorrowedAddressImpl::Anonymous(id)
&Self::Anonymous(id) => BorrowedAddressImpl::Anonymous(id),
}
}
fn from_borrowed(borrowed: &Self::Borrowed<'_>) -> Self {
match *borrowed {
BorrowedAddressImpl::Named(path) => Self::Named(path.into()),
BorrowedAddressImpl::Anonymous(id) => Self::Anonymous(id)
BorrowedAddressImpl::Anonymous(id) => Self::Anonymous(id),
}
}
}
@ -127,7 +126,7 @@ impl LocalPacketSocket for LocalPacketSocketImpl {
source: Some(&mut address),
payload: data,
ancillary: None,
ancillary_len: 0
ancillary_len: 0,
};
let len = unsafe { yggdrasil_rt::sys::receive_message(self.as_raw_fd(), &mut message) }?;
let address: LocalSocketAddress =
@ -135,16 +134,13 @@ impl LocalPacketSocket for LocalPacketSocketImpl {
Ok((len, OwnedAddressImpl::from_sys(&address)))
}
fn receive_with_ancillary(
&self,
data: &mut [u8],
) -> io::Result<(usize, Option<OwnedFd>)> {
fn receive_with_ancillary(&self, data: &mut [u8]) -> io::Result<(usize, Option<OwnedFd>)> {
let mut ancillary = [0; 32];
let mut message = MessageHeaderMut {
source: None,
payload: data,
ancillary: Some(&mut ancillary),
ancillary_len: 0
ancillary_len: 0,
};
let len = unsafe { yggdrasil_rt::sys::receive_message(self.as_raw_fd(), &mut message) }?;
let anc_len = message.ancillary_len;
@ -162,11 +158,12 @@ impl LocalPacketSocket for LocalPacketSocketImpl {
source: Some(&mut address),
payload: data,
ancillary: Some(&mut ancillary),
ancillary_len: 0
ancillary_len: 0,
};
let len = unsafe { yggdrasil_rt::sys::receive_message(self.as_raw_fd(), &mut message) }?;
let anc_len = message.ancillary_len;
let address: LocalSocketAddress = wire::from_slice(&address).map_err(yggdrasil_rt::Error::from)?;
let address: LocalSocketAddress =
wire::from_slice(&address).map_err(yggdrasil_rt::Error::from)?;
let address = OwnedAddressImpl::from_sys(&address);
let fd = read_ancillary(&ancillary[..anc_len])?;
Ok((len, fd, address))

View File

@ -8,11 +8,19 @@ edition = "2021"
[dependencies]
thiserror.workspace = true
tui = { workspace = true, optional = true }
[target.'cfg(unix)'.dependencies]
libc = "0.2.150"
[target.'cfg(target_os = "yggdrasil")'.dependencies]
yggdrasil-rt.workspace = true
[dev-dependencies]
tui.workspace = true
[features]
default = []
[lints]
workspace = true

View File

@ -1,7 +1,10 @@
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))]
use std::{
fmt, fs::File, io::{self, stdin, stdout, IsTerminal, Read, Stdin, Stdout, Write}, os::fd::{AsRawFd, RawFd}
fmt,
fs::File,
io::{self, stdin, stdout, IsTerminal, Read, Stdin, Stdout, Write},
os::fd::{AsRawFd, RawFd},
};
pub use self::{input::ReadChar, sys::RawMode};
@ -17,6 +20,8 @@ mod unix;
use unix as sys;
mod input;
#[cfg(any(feature = "tui", rust_analyzer))]
mod tui;
pub use input::{InputError, TermKey};
@ -43,14 +48,14 @@ pub trait RawTerminal {
enum TermInput {
Stdin(Stdin),
File(File)
File(File),
}
impl Read for TermInput {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Stdin(stdin) => stdin.read(buf),
Self::File(file) => file.read(buf)
Self::File(file) => file.read(buf),
}
}
}

View File

@ -0,0 +1,107 @@
use std::io::{self, Write};
use tui::{buffer, layout::Rect, style::Color};
use crate::{Color as TColor, RawTerminal, Term};
fn to_attributes(color: Color) -> Option<(TColor, bool)> {
Some(match color {
Color::Reset | Color::Indexed(_) | Color::Rgb(_, _, _) => return None,
Color::Red => (TColor::Red, false),
Color::LightRed => (TColor::Red, true),
Color::Green => (TColor::Green, false),
Color::LightGreen => (TColor::Green, true),
Color::Blue => (TColor::Blue, false),
Color::LightBlue => (TColor::Blue, true),
Color::Yellow => (TColor::Yellow, false),
Color::LightYellow => (TColor::Yellow, true),
Color::Magenta => (TColor::Magenta, false),
Color::LightMagenta => (TColor::Magenta, true),
Color::Cyan => (TColor::Cyan, false),
Color::LightCyan => (TColor::Cyan, true),
Color::Gray => (TColor::White, false),
Color::White => (TColor::White, true),
Color::Black => (TColor::Black, false),
Color::DarkGray => (TColor::Black, true),
})
}
impl tui::backend::Backend for Term {
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a buffer::Cell)>,
{
let mut fg = Color::Reset;
let mut bg = Color::Reset;
let mut last_pos: Option<(u16, u16)> = None;
for (x, y, cell) in content {
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
self.stdout.raw_move_cursor(y as usize, x as usize)?;
}
last_pos = Some((x, y));
if cell.fg != fg {
if let Some((color, bold)) = to_attributes(cell.fg) {
if bold {
self.stdout.raw_set_style(1)?;
} else {
self.stdout.raw_set_style(0)?;
}
self.stdout.raw_set_color(3, color)?;
} else {
self.stdout.raw_set_style(0)?;
}
}
if cell.bg != bg || cell.fg != fg {
if let Some((color, _)) = to_attributes(cell.bg) {
self.stdout.raw_set_color(4, color)?;
} else {
self.stdout.raw_set_color(4, TColor::Black)?;
}
}
fg = cell.fg;
bg = cell.bg;
self.stdout.write_all(cell.symbol.as_bytes())?;
}
self.stdout.raw_set_style(0)?;
Ok(())
}
fn clear(&mut self) -> Result<(), io::Error> {
self.stdout.raw_clear_all()
}
fn flush(&mut self) -> Result<(), io::Error> {
self.stdout.flush()
}
fn size(&self) -> Result<Rect, io::Error> {
let (w, h) = self.stdout.raw_size()?;
Ok(Rect {
x: 0,
y: 0,
width: w as u16,
height: h as u16 - 1,
})
}
fn show_cursor(&mut self) -> Result<(), io::Error> {
self.stdout.raw_set_cursor_visible(true)
}
fn hide_cursor(&mut self) -> Result<(), io::Error> {
self.stdout.raw_set_cursor_visible(false)
}
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
self.stdout.raw_move_cursor(y as usize, x as usize)
}
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
Ok((0, 0))
}
}

View File

@ -7,7 +7,7 @@ authors = ["Mark Poliakov <mark@alnyan.me>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
libterm.workspace = true
libterm = { workspace = true, features = ["tui"] }
cross.workspace = true
logsink.workspace = true
libutil.workspace = true
@ -20,6 +20,7 @@ serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
chrono.workspace = true
tui.workspace = true
# Own regex implementation?
regex = "1.11.1"
@ -137,6 +138,10 @@ path = "src/lspci.rs"
name = "ps"
path = "src/ps.rs"
[[bin]]
name = "top"
path = "src/top.rs"
[[bin]]
name = "tst"
path = "src/tst.rs"

View File

@ -48,34 +48,11 @@ pub mod unix {
}
}
// pub struct Syslog {
// #[cfg(target_os = "yggdrasil")]
// channel: std::os::yggdrasil::io::message_channel::MessageChannel,
// }
// TODO replace this
pub trait ToExitCode {
fn to_exit_code(&self) -> i32;
}
// #[cfg(target_os = "yggdrasil")]
// impl Syslog {
// pub fn open() -> io::Result<Self> {
// use std::os::yggdrasil::io::message_channel::MessageChannel;
//
// let channel = MessageChannel::open("log", false)?;
//
// Ok(Self { channel })
// }
//
// pub fn send_str(&mut self, msg: &str) -> io::Result<()> {
// use std::os::yggdrasil::io::message_channel::{MessageDestination, MessageSender};
//
// self.channel
// .send_message(msg.as_bytes(), MessageDestination::Specific(0))
// }
// }
impl<T> ToExitCode for io::Result<T> {
fn to_exit_code(&self) -> i32 {
match self {

View File

@ -0,0 +1,301 @@
#![feature(yggdrasil_os)]
use std::{
collections::HashSet,
fs, io,
os::fd::AsRawFd,
path::{Path, PathBuf},
time::Duration,
};
use cross::io::{Poll, TimerFd};
use libterm::{Term, TermKey};
use tui::{
layout::{Constraint, Layout},
style::{Color, Style},
text::{Span, Spans},
widgets::{Cell, Paragraph, Row, Table},
Frame, Terminal,
};
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
enum Error {
Io(#[from] io::Error),
Term(#[from] libterm::Error),
}
struct ProcessInfo {
pid: u32,
name: String,
threads: Vec<ThreadInfo>,
}
struct ThreadInfo {
tid: u32,
name: String,
}
struct State {
processes: Vec<ProcessInfo>,
expanded: HashSet<u32>,
selection: usize,
}
fn list_ids<P: AsRef<Path>>(path: P) -> io::Result<Vec<u32>> {
let mut list = vec![];
for entry in fs::read_dir(path)? {
let Ok(entry) = entry else {
continue;
};
let filename = entry.file_name();
let Some(name) = filename.to_str() else {
continue;
};
let Ok(pid) = name.parse() else {
continue;
};
list.push(pid);
}
Ok(list)
}
fn process_info(pid: u32) -> io::Result<ProcessInfo> {
let base = PathBuf::from("/sys/proc").join(format!("{pid}"));
let name = fs::read_to_string(base.join("name"))?;
let threads = threads(base).unwrap_or_default();
Ok(ProcessInfo { pid, name, threads })
}
fn processes() -> io::Result<Vec<ProcessInfo>> {
let pids = list_ids("/sys/proc")?;
let processes = pids
.into_iter()
.filter_map(|pid| process_info(pid).ok())
.collect();
Ok(processes)
}
fn thread_info<P: AsRef<Path>>(pid_path: P, tid: u32) -> io::Result<ThreadInfo> {
let base = pid_path.as_ref().join(format!("{tid}"));
let name = fs::read_to_string(base.join("name"))?;
Ok(ThreadInfo { tid, name })
}
fn threads<P: AsRef<Path>>(pid_path: P) -> io::Result<Vec<ThreadInfo>> {
let pid_path = pid_path.as_ref();
let tids = list_ids(pid_path)?;
let threads = tids
.into_iter()
.filter_map(|tid| thread_info(pid_path, tid).ok())
.collect();
Ok(threads)
}
impl State {
fn new() -> Result<Self, Error> {
let processes = processes()?;
Ok(Self {
processes,
expanded: HashSet::new(),
selection: 0,
})
}
fn next(&mut self) {
if self.processes.is_empty() {
self.selection = 0;
return;
}
if self.selection < self.processes.len() - 1 {
self.selection += 1;
}
}
fn prev(&mut self) {
if self.selection > 0 {
self.selection -= 1;
}
}
fn toggle_selected(&mut self) {
if let Some(selected) = self.processes.get(self.selection) {
let pid = selected.pid;
if self.expanded.contains(&pid) {
self.expanded.remove(&pid);
} else {
self.expanded.insert(pid);
}
}
}
fn kill_selected(&self) -> Result<(), Error> {
if let Some(selected) = self.processes.get(self.selection) {
cross::signal::send_kill(selected.pid)?;
}
Ok(())
}
fn refresh(&mut self) -> Result<(), Error> {
let selected_pid = self
.processes
.get(self.selection)
.map(|process| process.pid);
self.processes = processes()?;
let new_selection = selected_pid
.and_then(|pid| self.processes.iter().position(|process| process.pid == pid));
self.selection = new_selection.unwrap_or(0);
Ok(())
}
fn render(&self, f: &mut Frame<Term>) {
const KEYS: &[(&str, &str)] = &[
(" j ", "Down"),
(" k ", "Up"),
(" Enter ", "Toggle"),
(" K ", "Kill"),
];
let rects = Layout::default()
.constraints([Constraint::Percentage(99), Constraint::Min(1)].as_ref())
.split(f.size());
let normal_style = Style::default();
let selected_style = Style::default().bg(Color::Cyan).fg(Color::Black);
let hint_style = Style::default().bg(Color::Blue).fg(Color::White);
let key_style = Style::default().bg(Color::Cyan).fg(Color::Black);
let rows = self
.processes
.iter()
.enumerate()
.map(|(i, process)| {
let style = if i == self.selection {
selected_style
} else {
normal_style
};
let mut rows = vec![Row::new(vec![
Cell::from(format!("{}", process.pid)),
Cell::from(""),
Cell::from(process.name.as_str()),
])
.height(1)
.style(style)];
if self.expanded.contains(&process.pid) {
let count = process.threads.len();
for (i, thread) in process.threads.iter().enumerate() {
let ch = if i == count - 1 { "" } else { "" };
rows.push(
Row::new(vec![
Cell::from(ch),
Cell::from(format!("{}", thread.tid)),
Cell::from(thread.name.as_str()),
])
.style(style),
);
}
}
rows
})
.flatten();
let header = Row::new(vec![
Cell::from("PID"),
Cell::from("TID"),
Cell::from("NAME"),
])
.height(1)
.style(hint_style);
let table = Table::new(rows)
.widths(&[
Constraint::Min(6),
Constraint::Min(6),
Constraint::Percentage(100),
])
.highlight_symbol(">>")
.header(header);
let mut status_spans = vec![];
for (i, &(key, hint)) in KEYS.iter().enumerate() {
if i != 0 {
status_spans.push(Span::from(" "));
}
status_spans.push(Span::styled(key, key_style));
status_spans.push(Span::styled(hint, hint_style));
}
let status = Paragraph::new(vec![Spans::from(status_spans)]);
f.render_widget(table, rects[0]);
f.render_widget(status, rects[1]);
}
}
fn run() -> Result<(), Error> {
let mut state = State::new()?;
let mut poll = Poll::new()?;
let mut timer = TimerFd::new()?;
let mut running = true;
let term = Term::open()?;
let term_fd = term.input_fd();
poll.add(&timer)?;
poll.add(&term_fd)?;
let mut term = Terminal::new(term)?;
timer.start(Duration::from_secs(1))?;
while running {
term.draw(|f| state.render(f))?;
match poll.wait(None)?.unwrap() {
fd if fd == timer.as_raw_fd() => {
timer.start(Duration::from_secs(1))?;
state.refresh()?;
}
fd if fd == term_fd => {
let key = term.backend_mut().read_key()?;
match key {
TermKey::Escape | TermKey::Char('q') => {
running = false;
}
TermKey::Char('\n') => {
state.toggle_selected();
}
TermKey::Char('K') => {
if let Err(error) = state.kill_selected() {
log::error!("Kill: {error}");
}
}
TermKey::Char('k') => {
state.prev();
}
TermKey::Char('j') => {
state.next();
}
_ => (),
}
}
_ => unreachable!(),
}
}
Ok(())
}
fn main() {
logsink::setup_logging(false);
match run() {
Ok(()) => (),
Err(error) => {
eprintln!("{error}");
}
}
}

View File

@ -7,7 +7,7 @@ fn main() {
.name(format!("tst-thread-{i}"))
.spawn(move || {
let current = thread::current();
for _ in 0..10 {
for _ in 0..100 {
println!("Hi from thread {:?}", current.name());
thread::sleep(Duration::from_secs(1));
}

View File

@ -76,6 +76,11 @@ impl<F: Font> DrawState<F> {
}
pub fn draw(&mut self, dt: &mut [u32], state: &mut State) {
fn blend(x: u8, y: u8, f: f32) -> u8 {
let v = f * (x as f32) + (1.0 - f) * (y as f32);
v as u8
}
let default_fg = state.default_attributes.fg.to_display(false).to_u32();
let default_bg = state.default_attributes.bg.to_display(false).to_u32();
let font_layout = self.font.layout();
@ -93,12 +98,12 @@ impl<F: Font> DrawState<F> {
}
let scroll = state.adjust_scroll();
let cursor_visible = scroll == 0;
let cursor_visible = scroll == 0 && state.cursor_visible;
state.buffer.visible_rows_mut(scroll, |i, row| {
let cy = i * fh;
for (j, cell) in row.cells().enumerate() {
let bg = cell.attrs.bg.to_display(false).to_u32();
let bg = cell.attrs.bg.to_display(false);
let fg = cell.attrs.fg.to_display(cell.attrs.bright);
let cx = j * fw;
@ -106,7 +111,7 @@ impl<F: Font> DrawState<F> {
// Fill cell
for y in 0..fh {
let off = (cy + y) * self.width + cx;
dt[off..off + fw].fill(bg);
dt[off..off + fw].fill(bg.to_u32());
}
if cell.char == '\0' {
@ -116,9 +121,9 @@ impl<F: Font> DrawState<F> {
let c = cell.char as char;
self.font.map_glyph(c, |x, y, v| {
let v = (v * 2.0).min(1.0);
let r = fg.r as f32 * v;
let g = fg.g as f32 * v;
let b = fg.b as f32 * v;
let r = blend(fg.r, bg.r, v);
let g = blend(fg.g, bg.g, v);
let b = blend(fg.b, bg.b, v);
let color = (b as u32) | ((g as u32) << 8) | ((r as u32) << 16) | 0xFF000000;
dt[(cy + y) * self.width + cx + x] = color;

View File

@ -28,10 +28,14 @@ pub struct Cursor {
pub col: usize,
}
struct CsiState {
byte: u8,
}
enum EscapeState {
Normal,
Escape,
Csi,
Csi(CsiState),
}
#[derive(Default)]
@ -49,6 +53,8 @@ pub struct State {
scroll: usize,
pub cursor: Cursor,
pub cursor_visible: bool,
pub alternate: bool,
#[allow(unused)]
saved_cursor: Option<Cursor>,
@ -239,6 +245,8 @@ impl State {
esc_state: EscapeState::Normal,
cursor: Cursor { row: 0, col: 0 },
cursor_visible: true,
alternate: false,
scroll: 0,
saved_cursor: None,
@ -298,6 +306,11 @@ impl State {
}
};
if self.alternate {
self.cursor.col = self.cursor.col.min(self.buffer.width - 1);
self.cursor.row = self.cursor.row.min(self.buffer.height - 1);
}
if self.cursor.col >= self.buffer.width {
if self.cursor.row < self.buffer.height {
self.buffer.rows[self.cursor.row].dirty = true;
@ -317,8 +330,35 @@ impl State {
redraw
}
fn handle_ctlseq(&mut self, c: char) -> bool {
fn handle_ctlseq(&mut self, byte: u8, c: char) -> bool {
let redraw = match c {
'h' if byte == b'?' => match self.esc_args.get(0).copied().unwrap_or(0) {
25 => {
// Cursor visible
self.cursor_visible = true;
true
}
1049 => {
// Enter alternate mode
self.alternate = true;
true
}
_ => false,
},
'l' if byte == b'?' => match self.esc_args.get(0).copied().unwrap_or(0) {
25 => {
// Cursor not visible
self.cursor_visible = false;
true
}
1049 => {
// Leave alternate mode
self.alternate = false;
true
}
_ => false,
},
// Move back one character
'D' => {
if self.cursor.col > 0 {
@ -406,8 +446,12 @@ impl State {
redraw
}
fn handle_ctlseq_byte(&mut self, c: char) -> bool {
fn handle_ctlseq_byte(&mut self, byte: u8, c: char) -> bool {
match c {
'?' if byte == 0 => {
self.esc_state = EscapeState::Csi(CsiState { byte: b'?' });
false
}
c if let Some(digit) = c.to_digit(10) => {
let arg = self.esc_args.last_mut().unwrap();
*arg *= 10;
@ -418,7 +462,7 @@ impl State {
self.esc_args.push(0);
false
}
_ => self.handle_ctlseq(c),
_ => self.handle_ctlseq(byte, c),
}
}
@ -428,7 +472,7 @@ impl State {
EscapeState::Normal => self.putc_normal(ch),
EscapeState::Escape => match ch {
'[' => {
self.esc_state = EscapeState::Csi;
self.esc_state = EscapeState::Csi(CsiState { byte: 0 });
false
}
_ => {
@ -436,7 +480,7 @@ impl State {
false
}
},
EscapeState::Csi => self.handle_ctlseq_byte(ch),
EscapeState::Csi(CsiState { byte }) => self.handle_ctlseq_byte(byte, ch),
}
} else {
false

View File

@ -49,6 +49,7 @@ const PROGRAMS: &[(&str, &str)] = &[
("sleep", "bin/sleep"),
("lspci", "bin/lspci"),
("ps", "bin/ps"),
("top", "bin/top"),
("tst", "bin/tst"),
// netutils
("netconf", "sbin/netconf"),