From 771c553571e89ab0be9f8951fde92a8c763ef9eb Mon Sep 17 00:00:00 2001 From: Mark Poliakov <mark@alnyan.me> Date: Sun, 2 Mar 2025 17:27:26 +0200 Subject: [PATCH] term/sysutils: alternate mode, cursor hide/show, top-like utility --- userspace/Cargo.lock | 20 ++ userspace/Cargo.toml | 1 + userspace/lib/cross/src/signal.rs | 7 +- userspace/lib/cross/src/sys/unix/mem.rs | 12 +- userspace/lib/cross/src/sys/unix/mod.rs | 11 +- userspace/lib/cross/src/sys/unix/pid.rs | 5 +- userspace/lib/cross/src/sys/unix/pipe.rs | 5 +- userspace/lib/cross/src/sys/unix/timer.rs | 22 +- userspace/lib/cross/src/sys/yggdrasil/mod.rs | 30 +- .../lib/cross/src/sys/yggdrasil/socket.rs | 19 +- userspace/lib/libterm/Cargo.toml | 8 + userspace/lib/libterm/src/lib.rs | 11 +- userspace/lib/libterm/src/tui.rs | 107 +++++++ userspace/sysutils/Cargo.toml | 7 +- userspace/sysutils/src/lib.rs | 23 -- userspace/sysutils/src/top.rs | 301 ++++++++++++++++++ userspace/sysutils/src/tst.rs | 2 +- userspace/term/src/main.rs | 17 +- userspace/term/src/state.rs | 56 +++- xtask/src/build/userspace.rs | 1 + 20 files changed, 582 insertions(+), 83 deletions(-) create mode 100644 userspace/lib/libterm/src/tui.rs create mode 100644 userspace/sysutils/src/top.rs diff --git a/userspace/Cargo.lock b/userspace/Cargo.lock index 08cd4320..81f49a52 100644 --- a/userspace/Cargo.lock +++ b/userspace/Cargo.lock @@ -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" diff --git a/userspace/Cargo.toml b/userspace/Cargo.toml index 24dec191..5013b78b 100644 --- a/userspace/Cargo.toml +++ b/userspace/Cargo.toml @@ -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 } diff --git a/userspace/lib/cross/src/signal.rs b/userspace/lib/cross/src/signal.rs index 9a67e22a..f2dff0ff 100644 --- a/userspace/lib/cross/src/signal.rs +++ b/userspace/lib/cross/src/signal.rs @@ -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) +} diff --git a/userspace/lib/cross/src/sys/unix/mem.rs b/userspace/lib/cross/src/sys/unix/mem.rs index c0ab126a..dfe87ebd 100644 --- a/userspace/lib/cross/src/sys/unix/mem.rs +++ b/userspace/lib/cross/src/sys/unix/mem.rs @@ -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()); } diff --git a/userspace/lib/cross/src/sys/unix/mod.rs b/userspace/lib/cross/src/sys/unix/mod.rs index 0327df77..6e3a52a2 100644 --- a/userspace/lib/cross/src/sys/unix/mod.rs +++ b/userspace/lib/cross/src/sys/unix/mod.rs @@ -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()) + } +} diff --git a/userspace/lib/cross/src/sys/unix/pid.rs b/userspace/lib/cross/src/sys/unix/pid.rs index ebed1cf2..f7bafb79 100644 --- a/userspace/lib/cross/src/sys/unix/pid.rs +++ b/userspace/lib/cross/src/sys/unix/pid.rs @@ -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; diff --git a/userspace/lib/cross/src/sys/unix/pipe.rs b/userspace/lib/cross/src/sys/unix/pipe.rs index c87cd6a1..5b597671 100644 --- a/userspace/lib/cross/src/sys/unix/pipe.rs +++ b/userspace/lib/cross/src/sys/unix/pipe.rs @@ -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; diff --git a/userspace/lib/cross/src/sys/unix/timer.rs b/userspace/lib/cross/src/sys/unix/timer.rs index b81df4ee..2f0bc519 100644 --- a/userspace/lib/cross/src/sys/unix/timer.rs +++ b/userspace/lib/cross/src/sys/unix/timer.rs @@ -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, diff --git a/userspace/lib/cross/src/sys/yggdrasil/mod.rs b/userspace/lib/cross/src/sys/yggdrasil/mod.rs index fb5e0a62..ada4edc8 100644 --- a/userspace/lib/cross/src/sys/yggdrasil/mod.rs +++ b/userspace/lib/cross/src/sys/yggdrasil/mod.rs @@ -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(()) } diff --git a/userspace/lib/cross/src/sys/yggdrasil/socket.rs b/userspace/lib/cross/src/sys/yggdrasil/socket.rs index 38d183a2..c5e7a4e6 100644 --- a/userspace/lib/cross/src/sys/yggdrasil/socket.rs +++ b/userspace/lib/cross/src/sys/yggdrasil/socket.rs @@ -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)) diff --git a/userspace/lib/libterm/Cargo.toml b/userspace/lib/libterm/Cargo.toml index b523352a..ebca0d21 100644 --- a/userspace/lib/libterm/Cargo.toml +++ b/userspace/lib/libterm/Cargo.toml @@ -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 diff --git a/userspace/lib/libterm/src/lib.rs b/userspace/lib/libterm/src/lib.rs index d8d8c95c..a9003db1 100644 --- a/userspace/lib/libterm/src/lib.rs +++ b/userspace/lib/libterm/src/lib.rs @@ -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), } } } diff --git a/userspace/lib/libterm/src/tui.rs b/userspace/lib/libterm/src/tui.rs new file mode 100644 index 00000000..57558371 --- /dev/null +++ b/userspace/lib/libterm/src/tui.rs @@ -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)) + } +} diff --git a/userspace/sysutils/Cargo.toml b/userspace/sysutils/Cargo.toml index 9b17d251..9c25671b 100644 --- a/userspace/sysutils/Cargo.toml +++ b/userspace/sysutils/Cargo.toml @@ -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" diff --git a/userspace/sysutils/src/lib.rs b/userspace/sysutils/src/lib.rs index f0e60b8d..cb015217 100644 --- a/userspace/sysutils/src/lib.rs +++ b/userspace/sysutils/src/lib.rs @@ -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 { diff --git a/userspace/sysutils/src/top.rs b/userspace/sysutils/src/top.rs new file mode 100644 index 00000000..eef9e296 --- /dev/null +++ b/userspace/sysutils/src/top.rs @@ -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}"); + } + } +} diff --git a/userspace/sysutils/src/tst.rs b/userspace/sysutils/src/tst.rs index 4712eec5..b8f2719b 100644 --- a/userspace/sysutils/src/tst.rs +++ b/userspace/sysutils/src/tst.rs @@ -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)); } diff --git a/userspace/term/src/main.rs b/userspace/term/src/main.rs index 526014dc..f93860ad 100644 --- a/userspace/term/src/main.rs +++ b/userspace/term/src/main.rs @@ -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; diff --git a/userspace/term/src/state.rs b/userspace/term/src/state.rs index ada52c4b..31f51341 100644 --- a/userspace/term/src/state.rs +++ b/userspace/term/src/state.rs @@ -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 diff --git a/xtask/src/build/userspace.rs b/xtask/src/build/userspace.rs index 4fe227c9..e75c47ff 100644 --- a/xtask/src/build/userspace.rs +++ b/xtask/src/build/userspace.rs @@ -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"),