term/sysutils: alternate mode, cursor hide/show, top-like utility
This commit is contained in:
parent
59b34fb269
commit
771c553571
20
userspace/Cargo.lock
generated
20
userspace/Cargo.lock
generated
@ -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"
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
107
userspace/lib/libterm/src/tui.rs
Normal file
107
userspace/lib/libterm/src/tui.rs
Normal 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))
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
301
userspace/sysutils/src/top.rs
Normal file
301
userspace/sysutils/src/top.rs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user