Add 'userspace/' from commit '2b418dfb5cfc0673a3afda6eda5957abaaf7a8ff'
git-subtree-dir: userspace git-subtree-mainline: c4a5ad22c1fddf7ac8649939e2f1f3ae7aa0bb39 git-subtree-split: 2b418dfb5cfc0673a3afda6eda5957abaaf7a8ff
This commit is contained in:
commit
817f71f90f
2
userspace/.gitignore
vendored
Normal file
2
userspace/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
/extra.sh
|
7
userspace/.nlsp-settings/rust_analyzer.json
Normal file
7
userspace/.nlsp-settings/rust_analyzer.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"rust-analyzer.cargo.target": "aarch64-unknown-yggdrasil",
|
||||
"rust-analyzer.server.extraEnv": {
|
||||
"RUSTUP_TOOLCHAIN": "ygg-stage1",
|
||||
"RA_LOG": "debug"
|
||||
}
|
||||
}
|
1133
userspace/Cargo.lock
generated
Normal file
1133
userspace/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
userspace/Cargo.toml
Normal file
21
userspace/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[workspace]
|
||||
resolver = "1"
|
||||
members = [
|
||||
"init",
|
||||
"shell",
|
||||
"sysutils",
|
||||
"red",
|
||||
"colors",
|
||||
"term",
|
||||
"lib/libcolors",
|
||||
"lib/serde-ipc",
|
||||
"lib/libterm",
|
||||
"netutils"
|
||||
]
|
||||
|
||||
[patch.'https://git.alnyan.me/yggdrasil/yggdrasil-rt.git']
|
||||
yggdrasil-rt = { path = "../yggdrasil-rt" }
|
||||
[patch.'https://git.alnyan.me/yggdrasil/yggdrasil-abi.git']
|
||||
yggdrasil-abi = { path = "../abi" }
|
||||
[patch.'https://git.alnyan.me/yggdrasil/yggdrasil-abi-def.git']
|
||||
yggdrasil-abi-def = { path = "../abi-def" }
|
4
userspace/arch/aarch64/inittab
Normal file
4
userspace/arch/aarch64/inittab
Normal file
@ -0,0 +1,4 @@
|
||||
init:1:wait:/sbin/rc default
|
||||
logd:1:once:/sbin/logd
|
||||
|
||||
user:1:once:/sbin/login /dev/ttyS0
|
2
userspace/arch/x86_64/inittab
Normal file
2
userspace/arch/x86_64/inittab
Normal file
@ -0,0 +1,2 @@
|
||||
init:1:wait:/sbin/rc default
|
||||
logd:1:once:/sbin/logd
|
3
userspace/arch/x86_64/rc.d/99-colors
Executable file
3
userspace/arch/x86_64/rc.d/99-colors
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
/sbin/service start /bin/colors
|
15
userspace/colors/Cargo.toml
Normal file
15
userspace/colors/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "colors"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||
|
||||
[dependencies]
|
||||
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git", features = ["serde"] }
|
||||
serde-ipc = { path = "../lib/serde-ipc" }
|
||||
libcolors = { path = "../lib/libcolors", default_features = false }
|
||||
|
||||
flexbuffers = "2.0.0"
|
||||
lazy_static = "1.4.0"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
thiserror = "1.0.56"
|
90
userspace/colors/src/display.rs
Normal file
90
userspace/colors/src/display.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
os::yggdrasil::io::{
|
||||
device::{DeviceRequest, FdDeviceRequest},
|
||||
mapping::FileMapping,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
pub struct Display<'a> {
|
||||
#[allow(unused)]
|
||||
mapping: FileMapping<'a>,
|
||||
data: &'a mut [u32],
|
||||
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
||||
pub struct Point<T>(pub T, pub T);
|
||||
|
||||
impl<'a> Display<'a> {
|
||||
pub fn open() -> Result<Self, Error> {
|
||||
let file = OpenOptions::new().open("/dev/fb0")?;
|
||||
|
||||
unsafe {
|
||||
file.device_request(&mut DeviceRequest::AcquireDevice)?;
|
||||
}
|
||||
|
||||
let width = 640;
|
||||
let height = 480;
|
||||
|
||||
let mut mapping = FileMapping::new(file, 0, width * height * 4)?;
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts_mut(mapping.as_mut_ptr() as *mut u32, width * height)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
mapping,
|
||||
data,
|
||||
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub fn fill(&mut self, color: u32) {
|
||||
self.data.fill(color);
|
||||
}
|
||||
|
||||
pub fn row(&mut self, y: usize) -> &mut [u32] {
|
||||
&mut self.data[y * self.width..y * self.width + self.width]
|
||||
}
|
||||
|
||||
pub fn copy_from_slice(&mut self, slice: &[u32]) {
|
||||
if slice.len() != self.data.len() {
|
||||
panic!("Invalid copy source size");
|
||||
}
|
||||
|
||||
self.data.copy_from_slice(slice);
|
||||
}
|
||||
|
||||
pub fn blit_buffer(
|
||||
&mut self,
|
||||
source: &[u32],
|
||||
dst: Point<usize>,
|
||||
src: Point<usize>,
|
||||
w: usize,
|
||||
h: usize,
|
||||
src_stride: usize,
|
||||
) {
|
||||
for y in 0..h {
|
||||
let dst_offset = (y + src.1 + dst.1) * self.width + dst.0 + src.0;
|
||||
let src_offset = (y + src.1) * src_stride + src.0;
|
||||
|
||||
let src_chunk = &source[src_offset..src_offset + w];
|
||||
let dst_chunk = &mut self.data[dst_offset..dst_offset + w];
|
||||
|
||||
dst_chunk.copy_from_slice(src_chunk);
|
||||
}
|
||||
}
|
||||
}
|
7
userspace/colors/src/error.rs
Normal file
7
userspace/colors/src/error.rs
Normal file
@ -0,0 +1,7 @@
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Communication error: {0}")]
|
||||
IpcError(#[from] serde_ipc::Error),
|
||||
}
|
113
userspace/colors/src/input.rs
Normal file
113
userspace/colors/src/input.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Read,
|
||||
os::fd::{AsRawFd, RawFd},
|
||||
};
|
||||
|
||||
use libcolors::event::{KeyInput, KeyModifiers};
|
||||
use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
pub struct KeyboardInput(File);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct InputState {
|
||||
lshift: bool,
|
||||
rshift: bool,
|
||||
lctrl: bool,
|
||||
rctrl: bool,
|
||||
lalt: bool,
|
||||
ralt: bool,
|
||||
}
|
||||
|
||||
impl KeyboardInput {
|
||||
pub fn open() -> Result<Self, Error> {
|
||||
let file = File::open("/dev/kbd")?;
|
||||
Ok(Self(file))
|
||||
}
|
||||
|
||||
pub fn as_poll_fd(&self) -> RawFd {
|
||||
self.0.as_raw_fd()
|
||||
}
|
||||
|
||||
pub fn read_event(&mut self) -> Result<KeyboardKeyEvent, Error> {
|
||||
let mut buf = [0; 4];
|
||||
let len = self.0.read(&mut buf)?;
|
||||
|
||||
if len == 4 {
|
||||
Ok(KeyboardKeyEvent::from_bytes(buf))
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub fn update(&mut self, key: KeyboardKey, state: bool) {
|
||||
match key {
|
||||
KeyboardKey::LAlt => self.lalt = state,
|
||||
KeyboardKey::RAlt => self.ralt = state,
|
||||
KeyboardKey::LShift => self.lshift = state,
|
||||
KeyboardKey::RShift => self.rshift = state,
|
||||
KeyboardKey::LControl => self.lctrl = state,
|
||||
KeyboardKey::RControl => self.rctrl = state,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_shift(ch: u8) -> u8 {
|
||||
static DIGIT_TRANS: &[u8] = b")!@#$%^&*(";
|
||||
|
||||
match ch {
|
||||
ch if ch.is_ascii_lowercase() => ch.to_ascii_uppercase(),
|
||||
b'0'..=b'9' => DIGIT_TRANS[(ch - b'0') as usize],
|
||||
b';' => b':',
|
||||
b'\'' => b'"',
|
||||
b'/' => b'?',
|
||||
b'-' => b'_',
|
||||
b'=' => b'+',
|
||||
b',' => b'<',
|
||||
b'.' => b'>',
|
||||
b'`' => b'~',
|
||||
b'\\' => b'|',
|
||||
_ => ch,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_input(&self, key: KeyboardKey) -> KeyInput {
|
||||
let modifiers = self.modifiers();
|
||||
|
||||
let input = match (key, modifiers) {
|
||||
(KeyboardKey::Char(ch), KeyModifiers::NONE) => Some(ch as _),
|
||||
// TODO proper shift key translation
|
||||
(KeyboardKey::Char(ch), KeyModifiers::SHIFT) => Some(Self::translate_shift(ch) as _),
|
||||
(KeyboardKey::Tab, KeyModifiers::NONE) => Some('\t'),
|
||||
(KeyboardKey::Enter, KeyModifiers::NONE) => Some('\n'),
|
||||
(_, _) => None,
|
||||
};
|
||||
|
||||
KeyInput {
|
||||
modifiers,
|
||||
key,
|
||||
input,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modifiers(&self) -> KeyModifiers {
|
||||
KeyModifiers {
|
||||
shift: self.lshift || self.rshift,
|
||||
ctrl: self.lctrl || self.rctrl,
|
||||
alt: self.lalt || self.ralt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// impl FdEventSource<ServerEvent> for KeyboardInput {
|
||||
// fn poll_fd(&self) -> RawFd {
|
||||
// self.0.as_raw_fd()
|
||||
// }
|
||||
//
|
||||
// fn read_event(&mut self) -> ServerEvent {
|
||||
// }
|
||||
// }
|
586
userspace/colors/src/main.rs
Normal file
586
userspace/colors/src/main.rs
Normal file
@ -0,0 +1,586 @@
|
||||
#![feature(yggdrasil_os, rustc_private)]
|
||||
|
||||
// TODO rewrite and split this into meaningful components
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
os::{
|
||||
fd::{AsRawFd, RawFd},
|
||||
yggdrasil::io::{
|
||||
mapping::FileMapping, message_channel::ChannelPublisherId, poll::PollChannel,
|
||||
shared_memory::SharedMemory,
|
||||
},
|
||||
},
|
||||
process::{Command, ExitCode},
|
||||
};
|
||||
|
||||
use display::Display;
|
||||
use error::Error;
|
||||
use input::{InputState, KeyboardInput};
|
||||
use libcolors::{
|
||||
event::{Event, KeyModifiers, WindowEvent, WindowInfo},
|
||||
message::{ClientMessage, ServerMessage},
|
||||
};
|
||||
use serde_ipc::{Receiver, Sender};
|
||||
use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
|
||||
|
||||
pub mod display;
|
||||
pub mod error;
|
||||
pub mod input;
|
||||
|
||||
pub struct Window<'a> {
|
||||
window_id: u32,
|
||||
client_id: ChannelPublisherId,
|
||||
|
||||
surface_mapping: FileMapping<'a>,
|
||||
surface_data: &'a [u32],
|
||||
}
|
||||
|
||||
pub struct Frame {
|
||||
x: u32,
|
||||
y: u32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
|
||||
dirty: bool,
|
||||
|
||||
window: Option<u32>,
|
||||
}
|
||||
|
||||
pub struct Row {
|
||||
frames: Vec<Frame>,
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
pub struct ServerSender(Sender<ServerMessage>);
|
||||
|
||||
pub struct Server<'a, 'd> {
|
||||
display: Display<'d>,
|
||||
|
||||
input_state: InputState,
|
||||
|
||||
// Window management
|
||||
windows: BTreeMap<u32, Window<'a>>,
|
||||
rows: Vec<Row>,
|
||||
last_window_id: u32,
|
||||
focused_frame: Option<(usize, usize)>,
|
||||
// Outer frame
|
||||
padding: usize,
|
||||
background: u32,
|
||||
|
||||
// Event generators
|
||||
poll: PollChannel,
|
||||
receiver: Receiver<ClientMessage>,
|
||||
input: KeyboardInput,
|
||||
|
||||
// Comms
|
||||
sender: ServerSender,
|
||||
}
|
||||
|
||||
impl Row {
|
||||
pub fn new(x: u32, y: u32, w: u32, h: u32) -> Self {
|
||||
Self {
|
||||
frames: vec![],
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn balance_frames(&mut self) {
|
||||
if self.frames.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let spacing = 4;
|
||||
let wc = self.frames.len() as u32;
|
||||
|
||||
let w = (self.width - spacing * (wc - 1)) / wc;
|
||||
let h = self.height;
|
||||
let mut x = self.x;
|
||||
let y = self.y;
|
||||
|
||||
for frame in self.frames.iter_mut() {
|
||||
frame.dirty = true;
|
||||
frame.x = x;
|
||||
frame.y = y;
|
||||
frame.w = w;
|
||||
frame.h = h;
|
||||
|
||||
x += w + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place_frame(&mut self) -> &mut Frame {
|
||||
self.frames.push(Frame {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 0,
|
||||
h: 0,
|
||||
|
||||
dirty: true,
|
||||
window: None,
|
||||
});
|
||||
|
||||
self.balance_frames();
|
||||
|
||||
self.frames.last_mut().unwrap()
|
||||
}
|
||||
|
||||
pub fn remove_frame(&mut self, col: usize) {
|
||||
self.frames.remove(col);
|
||||
|
||||
self.balance_frames();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'd> Server<'a, 'd> {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
let mut poll = PollChannel::new()?;
|
||||
|
||||
let mut display = Display::open()?;
|
||||
let input = KeyboardInput::open()?;
|
||||
|
||||
let (sender, receiver) = serde_ipc::channel(libcolors::CHANNEL_NAME)?;
|
||||
let sender = ServerSender(sender);
|
||||
|
||||
poll.add(input.as_poll_fd())?;
|
||||
poll.add(receiver.as_poll_fd())?;
|
||||
|
||||
let background = 0xFFCCCCCC;
|
||||
display.fill(background);
|
||||
|
||||
Ok(Self {
|
||||
display,
|
||||
|
||||
input_state: InputState::default(),
|
||||
|
||||
poll,
|
||||
receiver,
|
||||
input,
|
||||
sender,
|
||||
|
||||
padding: 4,
|
||||
background,
|
||||
|
||||
windows: BTreeMap::new(),
|
||||
rows: vec![],
|
||||
last_window_id: 1,
|
||||
focused_frame: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_window(
|
||||
&mut self,
|
||||
client_id: ChannelPublisherId,
|
||||
) -> Result<(WindowInfo, RawFd), Error> {
|
||||
if self.rows.is_empty() {
|
||||
self.rows.push(Row::new(
|
||||
self.padding as _,
|
||||
self.padding as _,
|
||||
(self.display.width() - self.padding * 2) as _,
|
||||
(self.display.height() - self.padding * 2) as _,
|
||||
));
|
||||
}
|
||||
|
||||
// Create a frame
|
||||
let row = self.rows.last_mut().unwrap();
|
||||
let frame = row.place_frame();
|
||||
|
||||
// Create the actual window
|
||||
let window_id = self.last_window_id;
|
||||
self.last_window_id += 1;
|
||||
|
||||
let mapping_size = self.display.width() * self.display.height() * 4;
|
||||
let surface_shm = SharedMemory::new(mapping_size).unwrap();
|
||||
let fd = surface_shm.as_raw_fd();
|
||||
let mut surface_mapping = surface_shm.into_mapping().unwrap();
|
||||
|
||||
let surface_data = unsafe {
|
||||
std::slice::from_raw_parts_mut(
|
||||
surface_mapping.as_mut_ptr() as *mut u32,
|
||||
(frame.w * frame.h) as usize,
|
||||
)
|
||||
};
|
||||
|
||||
frame.window = Some(window_id);
|
||||
|
||||
let window = Window {
|
||||
window_id,
|
||||
client_id,
|
||||
surface_mapping,
|
||||
surface_data,
|
||||
};
|
||||
|
||||
self.windows.insert(window_id, window);
|
||||
|
||||
let info = WindowInfo {
|
||||
window_id,
|
||||
surface_stride: self.display.width() * 4,
|
||||
surface_mapping_size: mapping_size,
|
||||
width: frame.w,
|
||||
height: frame.h,
|
||||
};
|
||||
|
||||
self.display.fill(self.background);
|
||||
self.set_focused_window(window_id)?;
|
||||
self.flush_dirty_frames();
|
||||
|
||||
Ok((info, fd))
|
||||
}
|
||||
|
||||
fn remove_window(&mut self, window_id: u32) {
|
||||
// Find the window
|
||||
if !self.windows.contains_key(&window_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO this is ugly
|
||||
let mut res = None;
|
||||
for (i, row) in self.rows.iter().enumerate() {
|
||||
let j = row
|
||||
.frames
|
||||
.iter()
|
||||
.position(|f| f.window.map(|w| w == window_id).unwrap_or(false));
|
||||
|
||||
if let Some(j) = j {
|
||||
res = Some((i, j));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the frame
|
||||
if let Some((row, col)) = res {
|
||||
self.rows[row].remove_frame(col);
|
||||
self.display.fill(self.background);
|
||||
self.flush_dirty_frames();
|
||||
}
|
||||
|
||||
self.windows.remove(&window_id);
|
||||
|
||||
if self.focused_frame == res {
|
||||
self.focused_frame = None;
|
||||
|
||||
let new_focus = if let Some((row, col)) = res {
|
||||
// Focus some other frame in the same row
|
||||
if let Some(f_row) = self.rows.get(row) {
|
||||
let row_len = f_row.frames.len();
|
||||
|
||||
if col == 0 && row_len != 0 {
|
||||
Some((row, 1))
|
||||
} else if col > 0 {
|
||||
Some((row, col - 1))
|
||||
} else {
|
||||
// Empty row
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// No row exists
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// No frames?
|
||||
None
|
||||
};
|
||||
|
||||
self.set_focused_frame(new_focus);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_keyboard_event(&mut self, event: KeyboardKeyEvent) -> Result<(), Error> {
|
||||
let (key, state) = event.split();
|
||||
|
||||
self.input_state.update(key, state);
|
||||
|
||||
if state {
|
||||
let input = self.input_state.make_input(key);
|
||||
|
||||
// Non-window keys
|
||||
#[allow(clippy::single_match)]
|
||||
match (input.modifiers, input.key) {
|
||||
(KeyModifiers::ALT, KeyboardKey::Enter) => {
|
||||
// TODO do something with spawned child
|
||||
Command::new("/bin/term").spawn().ok();
|
||||
return Ok(());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Window keys
|
||||
if let Some((row, col)) = self.focused_frame {
|
||||
let row_len = self.rows[row].frames.len();
|
||||
|
||||
match (input.modifiers, input.key) {
|
||||
(KeyModifiers::ALT, KeyboardKey::Char(b'l')) => {
|
||||
if col + 1 < row_len {
|
||||
self.set_focused_frame(Some((row, col + 1)));
|
||||
} else {
|
||||
self.set_focused_frame(Some((row, 0)));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
(KeyModifiers::ALT, KeyboardKey::Char(b'h')) => {
|
||||
if col > 0 {
|
||||
self.set_focused_frame(Some((row, col - 1)));
|
||||
} else if row_len != 0 {
|
||||
self.set_focused_frame(Some((row, row_len - 1)));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((_, window)) = self.get_focused_window() {
|
||||
// Deliver event to the window
|
||||
self.sender
|
||||
.send_event(
|
||||
Event::WindowEvent(window.window_id, WindowEvent::KeyInput(input)),
|
||||
window.client_id,
|
||||
)
|
||||
.ok();
|
||||
} else {
|
||||
self.focused_frame = None;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_window(&self, window_id: u32) -> Option<(&Frame, &Window<'a>)> {
|
||||
let window = self.windows.get(&window_id)?;
|
||||
for row in self.rows.iter() {
|
||||
if let Some(f) = row
|
||||
.frames
|
||||
.iter()
|
||||
.find(|f| f.window.map(|w| w == window_id).unwrap_or(false))
|
||||
{
|
||||
return Some((f, window));
|
||||
}
|
||||
}
|
||||
// TODO Orphaned frame/window?
|
||||
None
|
||||
}
|
||||
|
||||
fn get_focused_window(&self) -> Option<(&Frame, &Window<'a>)> {
|
||||
let (row, col) = self.focused_frame?;
|
||||
|
||||
let frame = &self.rows[row].frames[col];
|
||||
let Some(window) = frame.window.and_then(|w| self.windows.get(&w)) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((frame, window))
|
||||
}
|
||||
|
||||
fn set_focused_frame(&mut self, focus: Option<(usize, usize)>) {
|
||||
if self.focused_frame == focus {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some((_, old_window)) = self.get_focused_window() {
|
||||
self.sender
|
||||
.send_event(
|
||||
Event::WindowEvent(old_window.window_id, WindowEvent::FocusChanged(false)),
|
||||
old_window.client_id,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
self.focused_frame = focus;
|
||||
|
||||
if let Some((row, col)) = focus {
|
||||
let Some(f_row) = self.rows.get(row) else {
|
||||
return;
|
||||
};
|
||||
let Some(frame) = f_row.frames.get(col) else {
|
||||
return;
|
||||
};
|
||||
let Some(window) = frame.window.and_then(|w| self.windows.get(&w)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.sender
|
||||
.send_event(
|
||||
Event::WindowEvent(window.window_id, WindowEvent::FocusChanged(true)),
|
||||
window.client_id,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_focused_window(&mut self, window_id: u32) -> Result<(), Error> {
|
||||
// TODO this is ugly
|
||||
let mut res = None;
|
||||
for (i, row) in self.rows.iter().enumerate() {
|
||||
let j = row
|
||||
.frames
|
||||
.iter()
|
||||
.position(|f| f.window.map(|w| w == window_id).unwrap_or(false));
|
||||
|
||||
if let Some(j) = j {
|
||||
res = Some((i, j));
|
||||
}
|
||||
}
|
||||
|
||||
self.set_focused_frame(res);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush_dirty_frames(&mut self) {
|
||||
for row in self.rows.iter() {
|
||||
for frame in row.frames.iter() {
|
||||
if !frame.dirty {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(window) = frame.window.and_then(|w| self.windows.get_mut(&w)) else {
|
||||
// TODO handle orphaned frame
|
||||
continue;
|
||||
};
|
||||
|
||||
let new_surface_data = unsafe {
|
||||
std::slice::from_raw_parts_mut(
|
||||
window.surface_mapping.as_mut_ptr() as *mut u32,
|
||||
(frame.w * frame.h) as usize,
|
||||
)
|
||||
};
|
||||
|
||||
window.surface_data = new_surface_data;
|
||||
|
||||
self.sender
|
||||
.send_event(
|
||||
Event::WindowEvent(
|
||||
window.window_id,
|
||||
WindowEvent::Resized {
|
||||
width: frame.w,
|
||||
height: frame.h,
|
||||
},
|
||||
),
|
||||
window.client_id,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_client_message(
|
||||
&mut self,
|
||||
client_id: ChannelPublisherId,
|
||||
message: ClientMessage,
|
||||
) -> Result<(), Error> {
|
||||
match message {
|
||||
ClientMessage::ClientHello => {
|
||||
debug_trace!("{:?}: ClientHello", client_id);
|
||||
// Echo the ID back
|
||||
self.sender
|
||||
.send_event(Event::ServerHello(client_id.into()), client_id)
|
||||
}
|
||||
ClientMessage::CreateWindow => {
|
||||
debug_trace!("{:?}: CreateWindow", client_id);
|
||||
let (info, shm_fd) = self.create_window(client_id)?;
|
||||
let window_id = info.window_id;
|
||||
|
||||
self.sender
|
||||
.send_event(Event::NewWindowInfo(info), client_id)?;
|
||||
self.sender.send_fd(shm_fd, client_id)?;
|
||||
self.sender.send_event(
|
||||
Event::WindowEvent(window_id, WindowEvent::RedrawRequested),
|
||||
client_id,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
ClientMessage::BlitWindow {
|
||||
window_id,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
} => {
|
||||
if let Some((frame, window)) = self.get_window(window_id) {
|
||||
let x = x.min(frame.w);
|
||||
let y = y.min(frame.h);
|
||||
let w = w.min(frame.w - x);
|
||||
let h = h.min(frame.h - y);
|
||||
|
||||
if w == 0 || h == 0 {
|
||||
// Invalid rectangle, skip it
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.display.blit_buffer(
|
||||
window.surface_data,
|
||||
display::Point(frame.x as _, frame.y as _),
|
||||
display::Point(x as _, y as _),
|
||||
w as _,
|
||||
h as _,
|
||||
frame.w as usize,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ClientMessage::DestroyWindow(window_id) => {
|
||||
debug_trace!("{:?}: DestroyWindow {}", client_id, window_id);
|
||||
self.remove_window(window_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_inner(mut self) -> Result<(), Error> {
|
||||
loop {
|
||||
match self.poll.wait(None)? {
|
||||
Some((fd, Ok(_))) if fd == self.input.as_poll_fd() => {
|
||||
let event = self.input.read_event()?;
|
||||
self.handle_keyboard_event(event)?;
|
||||
}
|
||||
Some((fd, Ok(_))) if fd == self.receiver.as_poll_fd() => {
|
||||
let (client_id, message) = self.receiver.receive_message()?;
|
||||
self.handle_client_message(client_id, message)?;
|
||||
}
|
||||
Some((_, Ok(_))) => {
|
||||
todo!()
|
||||
}
|
||||
Some((_, Err(error))) => {
|
||||
return Err(Error::from(error));
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(self) -> ExitCode {
|
||||
match self.run_inner() {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
debug_trace!("colors server finished with an error: {}", error);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerSender {
|
||||
pub fn send_event(&mut self, event: Event, client_id: ChannelPublisherId) -> Result<(), Error> {
|
||||
self.0
|
||||
.send_to(&ServerMessage::Event(event), client_id)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn send_fd(&mut self, fd: RawFd, client_id: ChannelPublisherId) -> Result<(), Error> {
|
||||
self.0.send_file(fd, client_id).map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let server = Server::new().unwrap();
|
||||
|
||||
server.run()
|
||||
}
|
BIN
userspace/etc/fonts/fixed-regular.ttf
Normal file
BIN
userspace/etc/fonts/fixed-regular.ttf
Normal file
Binary file not shown.
BIN
userspace/etc/fonts/regular.psfu
Normal file
BIN
userspace/etc/fonts/regular.psfu
Normal file
Binary file not shown.
1
userspace/etc/hosts
Normal file
1
userspace/etc/hosts
Normal file
@ -0,0 +1 @@
|
||||
127.0.0.1 localhost
|
1
userspace/etc/profile
Normal file
1
userspace/etc/profile
Normal file
@ -0,0 +1 @@
|
||||
set PATH /bin:/sbin
|
4
userspace/etc/rc.d/00-mount
Executable file
4
userspace/etc/rc.d/00-mount
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
/sbin/mount -t devfs /dev
|
||||
/sbin/mount -t sysfs /sys
|
3
userspace/etc/rc.d/10-resolver
Executable file
3
userspace/etc/rc.d/10-resolver
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
/sbin/service start -- /bin/dnsq -n 11.0.0.1 -s 127.0.0.1:53
|
3
userspace/etc/rc.d/44-netconf
Executable file
3
userspace/etc/rc.d/44-netconf
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
/sbin/dhcp-client eth0
|
20
userspace/init/Cargo.toml
Normal file
20
userspace/init/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "init"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[[bin]]
|
||||
name = "init"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "rc"
|
||||
path = "src/rc.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
serde_json = "1.0.108"
|
||||
yggdrasil-rt = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-rt.git" }
|
12
userspace/init/src/lib.rs
Normal file
12
userspace/init/src/lib.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct StartService {
|
||||
pub binary: String,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum InitMsg {
|
||||
StartService(StartService)
|
||||
}
|
199
userspace/init/src/main.rs
Normal file
199
userspace/init/src/main.rs
Normal file
@ -0,0 +1,199 @@
|
||||
#![feature(yggdrasil_os)]
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader},
|
||||
os::yggdrasil::io::message_channel::{MessageChannel, MessageReceiver},
|
||||
path::Path,
|
||||
process::{Command, ExitCode, Stdio},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use init::InitMsg;
|
||||
use yggdrasil_rt::debug_trace;
|
||||
|
||||
const INITTAB_PATH: &str = "/etc/inittab";
|
||||
|
||||
pub enum InitError {
|
||||
IoError(std::io::Error),
|
||||
OsError(yggdrasil_rt::Error),
|
||||
CustomError(String),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for InitError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IoError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<yggdrasil_rt::Error> for InitError {
|
||||
fn from(value: yggdrasil_rt::Error) -> Self {
|
||||
Self::OsError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for InitError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::IoError(e) => fmt::Debug::fmt(e, f),
|
||||
Self::OsError(e) => fmt::Debug::fmt(e, f),
|
||||
Self::CustomError(e) => fmt::Debug::fmt(e, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum RuleAction {
|
||||
Wait,
|
||||
Once,
|
||||
Boot,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Rule {
|
||||
#[allow(unused)]
|
||||
id: String,
|
||||
// runlevels ignored here
|
||||
action: RuleAction,
|
||||
program: String,
|
||||
arguments: Vec<String>,
|
||||
}
|
||||
|
||||
impl Rule {
|
||||
pub fn run(&self) -> Result<(), InitError> {
|
||||
let arguments: Vec<_> = self.arguments.iter().map(String::as_str).collect();
|
||||
let mut child = match self.action {
|
||||
RuleAction::Wait | RuleAction::Once => Command::new(&self.program)
|
||||
.args(&arguments)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?,
|
||||
RuleAction::Boot => todo!(),
|
||||
};
|
||||
|
||||
if self.action == RuleAction::Wait {
|
||||
child.wait()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RuleAction {
|
||||
type Err = InitError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"wait" => Ok(Self::Wait),
|
||||
"once" => Ok(Self::Once),
|
||||
"boot" => Ok(Self::Boot),
|
||||
_ => Err(InitError::CustomError(format!(
|
||||
"Unrecognized rule action: {:?}",
|
||||
s
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_rule(line: &str) -> Result<Rule, InitError> {
|
||||
let terms: Vec<_> = line.split(':').collect();
|
||||
if terms.len() != 4 {
|
||||
return Err(InitError::CustomError(format!(
|
||||
"Expected 4 terms in init rule, got {}",
|
||||
terms.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let id = terms[0].to_owned();
|
||||
// Runlevel (term 1) ignored
|
||||
let action = RuleAction::from_str(terms[2])?;
|
||||
|
||||
let command_terms: Vec<_> = terms[3].split(' ').collect();
|
||||
let program = command_terms[0].to_owned();
|
||||
let arguments = command_terms
|
||||
.into_iter()
|
||||
.skip(1)
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
|
||||
Ok(Rule {
|
||||
id,
|
||||
action,
|
||||
program,
|
||||
arguments,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_rules<P: AsRef<Path>>(path: P) -> Result<Vec<Rule>, InitError> {
|
||||
let file = BufReader::new(File::open(path)?);
|
||||
let mut rules = vec![];
|
||||
|
||||
for line in file.lines() {
|
||||
let line = line?;
|
||||
let line = line.trim();
|
||||
let (line, _) = line.split_once('#').unwrap_or((line, ""));
|
||||
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
rules.push(parse_rule(line)?);
|
||||
}
|
||||
|
||||
Ok(rules)
|
||||
}
|
||||
|
||||
fn handle_message(msg: InitMsg) -> io::Result<()> {
|
||||
match msg {
|
||||
InitMsg::StartService(init::StartService { binary, args }) => {
|
||||
Command::new(binary).args(args).spawn()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main_loop(channel: MessageChannel) -> io::Result<()> {
|
||||
let mut buf = [0; 1024];
|
||||
|
||||
loop {
|
||||
let (_, len) = channel.receive_message(&mut buf)?;
|
||||
if let Ok(msg) = serde_json::from_slice::<InitMsg>(&buf[..len]) {
|
||||
if let Err(err) = handle_message(msg) {
|
||||
debug_trace!("init::handle_message: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
debug_trace!("Userspace init starting");
|
||||
|
||||
let rules = match load_rules(INITTAB_PATH) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
debug_trace!("init: failed to load rules: {:?}", e);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let channel = MessageChannel::open("service-control", true).unwrap();
|
||||
|
||||
debug_trace!("Rules loaded");
|
||||
|
||||
for rule in rules {
|
||||
if let Err(err) = rule.run() {
|
||||
debug_trace!("rc: failed to execute rule {:?}: {:?}", rule, err);
|
||||
}
|
||||
}
|
||||
|
||||
match main_loop(channel) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
debug_trace!("init: main_loop returned {}", e);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
104
userspace/init/src/rc.rs
Normal file
104
userspace/init/src/rc.rs
Normal file
@ -0,0 +1,104 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::read_dir,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, ExitCode},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
const RC_DIR: &str = "/etc/rc.d";
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
enum Error {
|
||||
IncorrectUsage(String),
|
||||
IoError(std::io::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IoError(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Mode {
|
||||
Default,
|
||||
}
|
||||
|
||||
impl FromStr for Mode {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"default" => Ok(Self::Default),
|
||||
_ => Err(Error::IncorrectUsage(format!("Incorrect mode: {:?}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_script<P: AsRef<Path>>(path: P, arg: &str) -> Result<(), Error> {
|
||||
let path = path.as_ref();
|
||||
yggdrasil_rt::debug_trace!("rc: {:?} {}", path, arg);
|
||||
|
||||
// TODO run those in parallel, if allowed
|
||||
// TODO binfmt guessing
|
||||
let mut process = Command::new(path).arg(arg).spawn()?;
|
||||
|
||||
if !process.wait()?.success() {
|
||||
eprintln!("{:?}: Failed", path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_scripts<P: AsRef<Path>>(dir: P) -> Result<Vec<String>, Error> {
|
||||
let mut items = read_dir(dir)?
|
||||
.map(|item| item.map(|item| item.file_name().to_str().unwrap().to_owned()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
items.sort();
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn _main(mode: &str) -> Result<(), Error> {
|
||||
// TODO only 1 mode supported, so ignore it
|
||||
let _mode = Mode::from_str(mode)?;
|
||||
|
||||
let path = PathBuf::from(RC_DIR);
|
||||
let scripts = get_scripts(&path)?;
|
||||
|
||||
// Execute scripts in order
|
||||
for script in scripts {
|
||||
if let Err(err) = exec_script(path.join(&script), "start") {
|
||||
eprintln!("{}: {:?}", script, err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn usage(_cmd: &str) {
|
||||
eprintln!("Usage: ...");
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<_> = env::args().collect();
|
||||
if args.len() != 2 {
|
||||
eprintln!("Incorrect rc usage, expected 2 args, got {}", args.len());
|
||||
usage(&args[0]);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
match _main(&args[1]) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(Error::IncorrectUsage(e)) => {
|
||||
eprintln!("rc: incorrect usage: {:?}", e);
|
||||
usage(&args[0]);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("rc: {:?}", e);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
20
userspace/lib/libcolors/Cargo.toml
Normal file
20
userspace/lib/libcolors/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "libcolors"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||
|
||||
[dependencies]
|
||||
serde-ipc = { path = "../serde-ipc" }
|
||||
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git", features = ["serde"] }
|
||||
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
thiserror = "1.0.56"
|
||||
|
||||
# client_raqote
|
||||
raqote = { version = "0.8.3", default-features = false, optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
client_raqote = ["client", "raqote"]
|
||||
client = []
|
136
userspace/lib/libcolors/src/application/connection.rs
Normal file
136
userspace/lib/libcolors/src/application/connection.rs
Normal file
@ -0,0 +1,136 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
os::{
|
||||
fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
|
||||
yggdrasil::io::{message_channel::ChannelPublisherId, poll::PollChannel},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use serde_ipc::{Message, Receiver, Sender};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
event::Event,
|
||||
message::{ClientMessage, ServerMessage},
|
||||
};
|
||||
|
||||
pub struct Connection {
|
||||
sender: Sender<ClientMessage>,
|
||||
receiver: Receiver<ServerMessage>,
|
||||
event_queue: VecDeque<Event>,
|
||||
poll: PollChannel,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
let (sender, receiver) = serde_ipc::channel(crate::CHANNEL_NAME)?;
|
||||
let timeout = Duration::from_secs(1);
|
||||
let mut poll = PollChannel::new()?;
|
||||
let event_queue = VecDeque::new();
|
||||
|
||||
poll.add(receiver.as_raw_fd())?;
|
||||
|
||||
Ok(Self {
|
||||
sender,
|
||||
receiver,
|
||||
timeout,
|
||||
poll,
|
||||
event_queue,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_poll_fd(&self) -> RawFd {
|
||||
self.poll.as_raw_fd()
|
||||
}
|
||||
|
||||
pub fn receive_file(&mut self) -> Result<OwnedFd, Error> {
|
||||
loop {
|
||||
let Some((_, Ok(_))) = self.poll.wait(Some(self.timeout))? else {
|
||||
return Err(Error::CommunicationTimeout);
|
||||
};
|
||||
|
||||
// TODO ignore non-server messages
|
||||
let (_, msg) = self.receiver.receive_raw()?;
|
||||
|
||||
match msg {
|
||||
Message::File(fd) => {
|
||||
let file = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
break Ok(file);
|
||||
}
|
||||
Message::Data(ServerMessage::Event(event)) => {
|
||||
self.event_queue.push_back(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive_map<T, F: Fn(Event) -> Result<T, Event>>(
|
||||
&mut self,
|
||||
predicate: F,
|
||||
) -> Result<T, Error> {
|
||||
loop {
|
||||
let Some((_, Ok(_))) = self.poll.wait(Some(self.timeout))? else {
|
||||
return Err(Error::CommunicationTimeout);
|
||||
};
|
||||
|
||||
// Unless we're doing a request, the server should not send any FDs, so just drop
|
||||
// anything that's not a message
|
||||
let (_, Message::Data(ServerMessage::Event(event))) = self.receiver.receive_raw()?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match predicate(event) {
|
||||
Ok(val) => break Ok(val),
|
||||
Err(ev) => {
|
||||
// Predicate rejected the event
|
||||
self.event_queue.push_back(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive_event(&mut self) -> Result<Event, Error> {
|
||||
if let Some(event) = self.event_queue.pop_front() {
|
||||
return Ok(event);
|
||||
}
|
||||
|
||||
loop {
|
||||
match self.poll.wait(Some(self.timeout))? {
|
||||
Some((_, Ok(_))) => (),
|
||||
Some((_, Err(e))) => {
|
||||
todo!("Connection error: {:?}", e)
|
||||
}
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let (_, Message::Data(ServerMessage::Event(event))) = self.receiver.receive_raw()?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
break Ok(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send(&mut self, msg: ClientMessage) -> Result<(), Error> {
|
||||
self.sender
|
||||
.send_to(&msg, ChannelPublisherId::ZERO)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn connect(&mut self) -> Result<u32, Error> {
|
||||
self.sender
|
||||
.send_to(&ClientMessage::ClientHello, ChannelPublisherId::ZERO)?;
|
||||
|
||||
self.receive_map(|ev| {
|
||||
if let Event::ServerHello(id) = ev {
|
||||
Ok(id)
|
||||
} else {
|
||||
Err(ev)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
96
userspace/lib/libcolors/src/application/mod.rs
Normal file
96
userspace/lib/libcolors/src/application/mod.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
process::ExitCode,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
event::{Event, WindowEvent},
|
||||
message::ClientMessage,
|
||||
};
|
||||
|
||||
use self::{connection::Connection, window::Window};
|
||||
|
||||
pub mod connection;
|
||||
pub mod window;
|
||||
|
||||
pub struct Application<'a> {
|
||||
pub(crate) connection: Arc<Mutex<Connection>>,
|
||||
windows: BTreeMap<u32, Window<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Application<'a> {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
let mut connection = Connection::new()?;
|
||||
connection.connect()?;
|
||||
|
||||
Ok(Self {
|
||||
connection: Arc::new(Mutex::new(connection)),
|
||||
windows: BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_window(&mut self, window: Window<'a>) {
|
||||
assert!(!self.windows.contains_key(&window.id()));
|
||||
self.windows.insert(window.id(), window);
|
||||
}
|
||||
|
||||
fn run_inner(mut self) -> Result<ExitCode, Error> {
|
||||
loop {
|
||||
self.poll_events()?;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, event: Event) -> Result<(), Error> {
|
||||
if let Event::WindowEvent(window_id, ev) = event {
|
||||
if let Some(window) = self.windows.get_mut(&window_id) {
|
||||
window.handle_event(ev)?;
|
||||
} else {
|
||||
debug_trace!("Unexpected window_id received: {:?}", window_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn redraw(&mut self) -> Result<(), Error> {
|
||||
for (_, window) in self.windows.iter_mut() {
|
||||
// Inject RedrawRequested
|
||||
window.handle_event(WindowEvent::RedrawRequested)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn poll_events(&mut self) -> Result<(), Error> {
|
||||
let event = {
|
||||
let mut connection = self.connection.lock().unwrap();
|
||||
connection.receive_event()?
|
||||
};
|
||||
|
||||
self.handle_event(event)
|
||||
}
|
||||
|
||||
pub fn run(self) -> ExitCode {
|
||||
match self.run_inner() {
|
||||
Ok(exit) => exit,
|
||||
Err(e) => {
|
||||
debug_trace!("Application finished with error {:?}", e);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connection(&self) -> &Arc<Mutex<Connection>> {
|
||||
&self.connection
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Application<'_> {
|
||||
fn drop(&mut self) {
|
||||
let mut conn = self.connection.lock().unwrap();
|
||||
for (window_id, _) in self.windows.iter() {
|
||||
conn.send(ClientMessage::DestroyWindow(*window_id)).ok();
|
||||
}
|
||||
}
|
||||
}
|
225
userspace/lib/libcolors/src/application/window.rs
Normal file
225
userspace/lib/libcolors/src/application/window.rs
Normal file
@ -0,0 +1,225 @@
|
||||
use std::{
|
||||
os::yggdrasil::io::mapping::FileMapping,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
event::{Event, KeyInput, WindowEvent},
|
||||
message::ClientMessage,
|
||||
};
|
||||
|
||||
use super::{connection::Connection, Application};
|
||||
|
||||
pub trait OnCloseRequested = Fn() -> EventOutcome;
|
||||
pub trait OnKeyInput = Fn(KeyInput) -> EventOutcome;
|
||||
pub trait OnResized = Fn(u32, u32) -> EventOutcome;
|
||||
pub trait OnFocusChanged = Fn(bool) -> EventOutcome;
|
||||
|
||||
#[cfg(feature = "client_raqote")]
|
||||
pub trait OnRedrawRequested = Fn(&mut raqote::DrawTarget<&mut [u32]>);
|
||||
|
||||
#[cfg(not(feature = "client_raqote"))]
|
||||
pub trait OnRedrawRequested = Fn(&mut [u32]);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum EventOutcome {
|
||||
None,
|
||||
Redraw,
|
||||
Destroy,
|
||||
}
|
||||
|
||||
pub struct Window<'a> {
|
||||
connection: Arc<Mutex<Connection>>,
|
||||
window_id: u32,
|
||||
surface_mapping: FileMapping<'a>,
|
||||
#[cfg(feature = "client_raqote")]
|
||||
surface_draw_target: raqote::DrawTarget<&'a mut [u32]>,
|
||||
#[cfg(not(feature = "client_raqote"))]
|
||||
surface_data: &'a mut [u32],
|
||||
width: u32,
|
||||
height: u32,
|
||||
focused: bool,
|
||||
|
||||
on_close_requested: Box<dyn OnCloseRequested>,
|
||||
on_redraw_requested: Box<dyn OnRedrawRequested>,
|
||||
on_key_input: Box<dyn OnKeyInput>,
|
||||
on_resized: Box<dyn OnResized>,
|
||||
on_focus_changed: Box<dyn OnFocusChanged>,
|
||||
}
|
||||
|
||||
impl<'a> Window<'a> {
|
||||
pub fn new(application: &Application) -> Result<Self, Error> {
|
||||
let mut connection = application.connection.lock().unwrap();
|
||||
|
||||
connection.send(ClientMessage::CreateWindow)?;
|
||||
|
||||
let create_info = connection.receive_map(|r| match r {
|
||||
Event::NewWindowInfo(info) => Ok(info),
|
||||
_ => Err(r),
|
||||
})?;
|
||||
assert_eq!(create_info.surface_stride % 4, 0);
|
||||
let surface_shm_fd = connection.receive_file()?;
|
||||
let mut surface_mapping =
|
||||
FileMapping::new(surface_shm_fd, 0, create_info.surface_mapping_size)?;
|
||||
let surface_data = unsafe {
|
||||
std::slice::from_raw_parts_mut(
|
||||
surface_mapping.as_mut_ptr() as *mut u32,
|
||||
(create_info.width * create_info.height) as usize,
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(feature = "client_raqote")]
|
||||
let surface_draw_target = raqote::DrawTarget::from_backing(
|
||||
create_info.width as _,
|
||||
create_info.height as _,
|
||||
surface_data,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
connection: application.connection.clone(),
|
||||
window_id: create_info.window_id,
|
||||
width: create_info.width,
|
||||
height: create_info.height,
|
||||
surface_mapping,
|
||||
#[cfg(feature = "client_raqote")]
|
||||
surface_draw_target,
|
||||
#[cfg(not(feature = "client_raqote"))]
|
||||
surface_data,
|
||||
|
||||
focused: false,
|
||||
|
||||
on_close_requested: Box::new(|| {
|
||||
// Do nothing
|
||||
EventOutcome::Destroy
|
||||
}),
|
||||
#[cfg(feature = "client_raqote")]
|
||||
on_redraw_requested: Box::new(|dt| {
|
||||
dt.clear(SolidSource::from_unpremultiplied_argb(255, 127, 127, 127));
|
||||
}),
|
||||
#[cfg(not(feature = "client_raqote"))]
|
||||
on_redraw_requested: Box::new(|dt| {
|
||||
dt.fill(0xFF888888);
|
||||
}),
|
||||
on_key_input: Box::new(|_ev| EventOutcome::None),
|
||||
on_resized: Box::new(|_w, _h| EventOutcome::None),
|
||||
on_focus_changed: Box::new(|_| EventOutcome::None),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u32 {
|
||||
self.window_id
|
||||
}
|
||||
|
||||
pub fn set_on_key_input<H: OnKeyInput + 'static>(&mut self, handler: H) {
|
||||
self.on_key_input = Box::new(handler);
|
||||
}
|
||||
|
||||
pub fn set_on_resized<H: OnResized + 'static>(&mut self, handler: H) {
|
||||
self.on_resized = Box::new(handler);
|
||||
}
|
||||
|
||||
pub fn set_on_redraw_requested<H: OnRedrawRequested + 'static>(&mut self, handler: H) {
|
||||
self.on_redraw_requested = Box::new(handler);
|
||||
}
|
||||
|
||||
pub fn set_on_focus_changed<H: OnFocusChanged + 'static>(&mut self, handler: H) {
|
||||
self.on_focus_changed = Box::new(handler);
|
||||
}
|
||||
|
||||
pub fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub fn redraw(&mut self) -> Result<(), Error> {
|
||||
#[cfg(feature = "client_raqote")]
|
||||
{
|
||||
let dt = &mut self.surface_draw_target;
|
||||
(self.on_redraw_requested)(dt);
|
||||
}
|
||||
#[cfg(not(feature = "client_raqote"))]
|
||||
{
|
||||
(self.on_redraw_requested)(self.surface_data);
|
||||
}
|
||||
|
||||
// Blit
|
||||
self.blit_rect(0, 0, self.width, self.height)
|
||||
}
|
||||
|
||||
pub fn handle_event(&mut self, ev: WindowEvent) -> Result<EventOutcome, Error> {
|
||||
let outcome = match ev {
|
||||
WindowEvent::RedrawRequested => {
|
||||
self.redraw()?;
|
||||
EventOutcome::None
|
||||
}
|
||||
WindowEvent::Resized { width, height } => {
|
||||
let new_surface_data = unsafe {
|
||||
std::slice::from_raw_parts_mut(
|
||||
self.surface_mapping.as_mut_ptr() as *mut u32,
|
||||
(width * height) as usize,
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(feature = "client_raqote")]
|
||||
{
|
||||
let new_draw_target =
|
||||
raqote::DrawTarget::from_backing(width as _, height as _, new_surface_data);
|
||||
|
||||
self.surface_draw_target = new_draw_target;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "client_raqote"))]
|
||||
{
|
||||
self.surface_data = new_surface_data;
|
||||
}
|
||||
|
||||
(self.on_resized)(width, height)
|
||||
}
|
||||
WindowEvent::KeyInput(input) => (self.on_key_input)(input),
|
||||
WindowEvent::FocusChanged(focused) => {
|
||||
self.focused = focused;
|
||||
(self.on_focus_changed)(focused)
|
||||
}
|
||||
WindowEvent::CloseRequested => (self.on_close_requested)(),
|
||||
_ => EventOutcome::None,
|
||||
};
|
||||
|
||||
if outcome == EventOutcome::Redraw {
|
||||
self.redraw()?;
|
||||
}
|
||||
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
#[cfg(feature = "client_raqote")]
|
||||
pub fn as_draw_target(&mut self) -> &mut raqote::DrawTarget<&'a mut [u32]> {
|
||||
&mut self.surface_draw_target
|
||||
}
|
||||
|
||||
fn blit_rect(&mut self, x: u32, y: u32, w: u32, h: u32) -> Result<(), Error> {
|
||||
// Clip to self bounds
|
||||
let x = x.min(self.width);
|
||||
let y = y.min(self.height);
|
||||
let w = w.min(self.width - x);
|
||||
let h = h.min(self.height - y);
|
||||
|
||||
if w == 0 || h == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut connection = self.connection.lock().unwrap();
|
||||
connection.send(ClientMessage::BlitWindow {
|
||||
window_id: self.window_id,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
11
userspace/lib/libcolors/src/error.rs
Normal file
11
userspace/lib/libcolors/src/error.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use std::io;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Communication timeout")]
|
||||
CommunicationTimeout,
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("Communication error: {0:?}")]
|
||||
CommunicationError(#[from] serde_ipc::Error),
|
||||
}
|
90
userspace/lib/libcolors/src/event.rs
Normal file
90
userspace/lib/libcolors/src/event.rs
Normal file
@ -0,0 +1,90 @@
|
||||
pub use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct WindowInfo {
|
||||
pub window_id: u32,
|
||||
pub surface_stride: usize,
|
||||
pub surface_mapping_size: usize,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct KeyModifiers {
|
||||
pub ctrl: bool,
|
||||
pub shift: bool,
|
||||
pub alt: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct KeyEvent {
|
||||
pub modifiers: KeyModifiers,
|
||||
pub key: KeyboardKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct KeyInput {
|
||||
pub modifiers: KeyModifiers,
|
||||
pub key: KeyboardKey,
|
||||
pub input: Option<char>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum WindowEvent {
|
||||
KeyPressed(KeyEvent),
|
||||
KeyReleased(KeyEvent),
|
||||
KeyInput(KeyInput),
|
||||
Resized { width: u32, height: u32 },
|
||||
FocusChanged(bool),
|
||||
Destroyed,
|
||||
RedrawRequested,
|
||||
CloseRequested,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum Event {
|
||||
// Server events
|
||||
KeyboardKey(KeyboardKeyEvent),
|
||||
RedrawRequested,
|
||||
|
||||
// Window events
|
||||
WindowEvent(u32, WindowEvent),
|
||||
|
||||
// Request-responses
|
||||
ServerHello(u32),
|
||||
NewWindowInfo(WindowInfo),
|
||||
}
|
||||
|
||||
impl KeyModifiers {
|
||||
pub const SHIFT: Self = Self {
|
||||
shift: true,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
};
|
||||
|
||||
pub const CTRL: Self = Self {
|
||||
shift: false,
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
};
|
||||
|
||||
pub const CTRL_SHIFT: Self = Self {
|
||||
shift: true,
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
};
|
||||
|
||||
pub const ALT: Self = Self {
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
};
|
||||
|
||||
pub const NONE: Self = Self {
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
};
|
||||
}
|
12
userspace/lib/libcolors/src/lib.rs
Normal file
12
userspace/lib/libcolors/src/lib.rs
Normal file
@ -0,0 +1,12 @@
|
||||
#![feature(yggdrasil_os, rustc_private)]
|
||||
#![cfg_attr(feature = "client", feature(trait_alias))]
|
||||
|
||||
pub const CHANNEL_NAME: &str = "colors";
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
pub mod application;
|
||||
#[cfg(feature = "client")]
|
||||
pub mod error;
|
||||
|
||||
pub mod event;
|
||||
pub mod message;
|
30
userspace/lib/libcolors/src/message.rs
Normal file
30
userspace/lib/libcolors/src/message.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use crate::event::Event;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ServerMessage {
|
||||
Event(Event),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ClientMessage {
|
||||
ClientHello,
|
||||
CreateWindow,
|
||||
BlitWindow {
|
||||
window_id: u32,
|
||||
x: u32,
|
||||
y: u32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
},
|
||||
DestroyWindow(u32),
|
||||
}
|
||||
|
||||
impl From<(u32, ServerMessage)> for Event {
|
||||
fn from((_, msg): (u32, ServerMessage)) -> Self {
|
||||
let ServerMessage::Event(ev) = msg;
|
||||
|
||||
ev
|
||||
}
|
||||
}
|
12
userspace/lib/libterm/Cargo.toml
Normal file
12
userspace/lib/libterm/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "libterm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0.50"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.150"
|
82
userspace/lib/libterm/src/input.rs
Normal file
82
userspace/lib/libterm/src/input.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Read, Stdin},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||
pub enum TermKey {
|
||||
Char(char),
|
||||
Escape,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum InputError {
|
||||
#[error("Invalid UTF-8 prefix byte: {0:#x}")]
|
||||
InvalidPrefixByte(u8),
|
||||
#[error("Invalid UTF-8: {0}")]
|
||||
DecodeError(std::str::Utf8Error),
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(io::Error),
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for TermKey {
|
||||
fn from(value: char) -> Self {
|
||||
Self::Char(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TermKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Char('\t') => f.write_str("<tab>"),
|
||||
Self::Char(key) => fmt::Display::fmt(key, f),
|
||||
Self::Escape => f.write_str("<esc>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
228
userspace/lib/libterm/src/lib.rs
Normal file
228
userspace/lib/libterm/src/lib.rs
Normal file
@ -0,0 +1,228 @@
|
||||
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))]
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, stdin, stdout, Stdin, Stdout, Write},
|
||||
};
|
||||
|
||||
use self::{input::ReadChar, sys::RawMode};
|
||||
|
||||
#[cfg(target_os = "yggdrasil")]
|
||||
mod yggdrasil;
|
||||
#[cfg(target_os = "yggdrasil")]
|
||||
use yggdrasil as sys;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
#[cfg(unix)]
|
||||
use unix as sys;
|
||||
|
||||
mod input;
|
||||
|
||||
pub use input::{InputError, TermKey};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("Input error: {0}")]
|
||||
InputError(#[from] InputError),
|
||||
}
|
||||
|
||||
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<()>;
|
||||
fn raw_set_cursor_visible(&mut self, visible: bool) -> io::Result<()>;
|
||||
}
|
||||
|
||||
pub struct Term {
|
||||
stdin: Stdin,
|
||||
stdout: Stdout,
|
||||
raw: RawMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CursorStyle {
|
||||
Default,
|
||||
Line,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u32)]
|
||||
pub enum Color {
|
||||
Black = 0,
|
||||
Red = 1,
|
||||
Green = 2,
|
||||
Yellow = 3,
|
||||
Blue = 4,
|
||||
Magenta = 5,
|
||||
Cyan = 6,
|
||||
White = 7,
|
||||
Default = 9,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Clear {
|
||||
All,
|
||||
LineToEnd,
|
||||
}
|
||||
|
||||
impl RawTerminal for Stdout {
|
||||
fn raw_enter_alternate_mode(&mut self) -> io::Result<()> {
|
||||
self.write_all(b"\x1B[?1049h")
|
||||
}
|
||||
|
||||
fn raw_leave_alternate_mode(&mut self) -> io::Result<()> {
|
||||
self.write_all(b"\x1B[?1049l")
|
||||
}
|
||||
|
||||
fn raw_clear_all(&mut self) -> io::Result<()> {
|
||||
self.write_all(b"\x1B[2J")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn raw_set_cursor_visible(&mut self, visible: bool) -> io::Result<()> {
|
||||
if visible {
|
||||
write!(self, "\x1B[?25h")
|
||||
} else {
|
||||
write!(self, "\x1B[?25l")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)? };
|
||||
|
||||
stdout.raw_enter_alternate_mode()?;
|
||||
stdout.raw_clear_all()?;
|
||||
stdout.raw_move_cursor(0, 0)?;
|
||||
|
||||
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::from)
|
||||
}
|
||||
pub fn set_cursor_visible(&mut self, visible: bool) -> Result<(), Error> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
self.stdout
|
||||
.raw_set_cursor_visible(visible)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
#[cfg(target_os = "yggdrasil")]
|
||||
{
|
||||
let _ = visible;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
pub fn set_cursor_style(&mut self, style: CursorStyle) -> Result<(), Error> {
|
||||
self.stdout.raw_set_cursor_style(style).map_err(Error::from)
|
||||
}
|
||||
pub fn size(&self) -> Result<(usize, usize), Error> {
|
||||
unsafe { sys::terminal_size(&self.stdout).map_err(Error::from) }
|
||||
}
|
||||
|
||||
pub fn set_foreground(&mut self, color: Color) -> Result<(), Error> {
|
||||
self.stdout.raw_set_color(3, color).map_err(Error::from)
|
||||
}
|
||||
pub fn set_background(&mut self, color: Color) -> Result<(), Error> {
|
||||
self.stdout.raw_set_color(4, color).map_err(Error::from)
|
||||
}
|
||||
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::from)
|
||||
}
|
||||
pub fn reset_style(&mut self) -> Result<(), Error> {
|
||||
self.stdout.raw_set_style(0).map_err(Error::from)
|
||||
}
|
||||
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::from)
|
||||
}
|
||||
|
||||
pub fn read_key(&mut self) -> Result<TermKey, Error> {
|
||||
let ch = self.stdin.read_char().map_err(Error::from)?;
|
||||
|
||||
if ch == '\x1B' {
|
||||
return Ok(TermKey::Escape);
|
||||
}
|
||||
|
||||
Ok(TermKey::Char(ch))
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> Result<(), Error> {
|
||||
self.stdout.flush().map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Term {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self.raw.leave(&self.stdin);
|
||||
}
|
||||
self.stdout.raw_leave_alternate_mode().ok();
|
||||
}
|
||||
}
|
53
userspace/lib/libterm/src/unix.rs
Normal file
53
userspace/lib/libterm/src/unix.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use std::{
|
||||
io::{self, Stdin, Stdout},
|
||||
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 _))
|
||||
}
|
33
userspace/lib/libterm/src/yggdrasil.rs
Normal file
33
userspace/lib/libterm/src/yggdrasil.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use std::{
|
||||
io::{self, Stdin, Stdout},
|
||||
mem::MaybeUninit,
|
||||
os::yggdrasil::io::{
|
||||
device::{DeviceRequest, FdDeviceRequest},
|
||||
terminal::{update_terminal_options, 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 stdout.device_request(&mut req).is_err() {
|
||||
// Fallback
|
||||
return Ok((60, 20));
|
||||
}
|
||||
let DeviceRequest::GetTerminalSize(size) = req else {
|
||||
unreachable!();
|
||||
};
|
||||
let size = size.assume_init();
|
||||
Ok((size.columns, size.rows))
|
||||
}
|
10
userspace/lib/serde-ipc/Cargo.toml
Normal file
10
userspace/lib/serde-ipc/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "serde-ipc"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||
|
||||
[dependencies]
|
||||
flexbuffers = "2.0.0"
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
thiserror = "1.0.56"
|
142
userspace/lib/serde-ipc/src/lib.rs
Normal file
142
userspace/lib/serde-ipc/src/lib.rs
Normal file
@ -0,0 +1,142 @@
|
||||
#![feature(yggdrasil_os, rustc_private)]
|
||||
|
||||
use std::{
|
||||
io,
|
||||
marker::PhantomData,
|
||||
os::{
|
||||
fd::{AsRawFd, RawFd},
|
||||
yggdrasil::io::message_channel::{
|
||||
ChannelPublisherId, MessageChannel, MessageChannelReceiver, MessageChannelSender,
|
||||
MessageDestination, MessageReceiver, MessageSender, ReceivedMessageMetadata,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Serialization error: {0}")]
|
||||
SerializeError(flexbuffers::SerializationError),
|
||||
#[error("Deserialization error: {0}")]
|
||||
DeserializeError(flexbuffers::DeserializationError),
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(io::Error),
|
||||
}
|
||||
|
||||
pub struct Sender<M: Serialize> {
|
||||
inner: MessageChannelSender,
|
||||
_pd: PhantomData<M>,
|
||||
}
|
||||
|
||||
pub struct Receiver<M: DeserializeOwned> {
|
||||
inner: MessageChannelReceiver,
|
||||
buffer: [u8; 1024],
|
||||
_pd: PhantomData<M>,
|
||||
}
|
||||
|
||||
pub enum Message<T> {
|
||||
Data(T),
|
||||
File(RawFd),
|
||||
}
|
||||
|
||||
fn raw_send_message_to<T: Serialize, S: MessageSender>(
|
||||
sender: &S,
|
||||
msg: &T,
|
||||
id: ChannelPublisherId,
|
||||
) -> Result<(), Error> {
|
||||
let msg = flexbuffers::to_vec(msg)?;
|
||||
sender
|
||||
.send_message(&msg, MessageDestination::Specific(id.into()))
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
fn raw_send_file_to<F: AsRawFd, S: MessageSender>(
|
||||
sender: &S,
|
||||
file: &F,
|
||||
id: ChannelPublisherId,
|
||||
) -> Result<(), Error> {
|
||||
sender
|
||||
.send_raw_fd(file.as_raw_fd(), MessageDestination::Specific(id.into()))
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn channel<T: Serialize, U: DeserializeOwned>(
|
||||
name: &str,
|
||||
) -> Result<(Sender<T>, Receiver<U>), Error> {
|
||||
let raw = MessageChannel::open(name, true)?;
|
||||
let (raw_sender, raw_receiver) = raw.split();
|
||||
Ok((
|
||||
Sender {
|
||||
inner: raw_sender,
|
||||
_pd: PhantomData,
|
||||
},
|
||||
Receiver {
|
||||
inner: raw_receiver,
|
||||
buffer: [0; 1024],
|
||||
_pd: PhantomData,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
impl<T: Serialize> Sender<T> {
|
||||
pub fn send_to(&mut self, msg: &T, id: ChannelPublisherId) -> Result<(), Error> {
|
||||
raw_send_message_to(&self.inner, msg, id)
|
||||
}
|
||||
|
||||
pub fn send_file(&mut self, fd: RawFd, id: ChannelPublisherId) -> Result<(), Error> {
|
||||
raw_send_file_to(&self.inner, &fd, id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned> Receiver<T> {
|
||||
pub fn receive_message(&mut self) -> Result<(ChannelPublisherId, T), Error> {
|
||||
loop {
|
||||
let (id, message) = self.receive_raw()?;
|
||||
|
||||
if let Message::Data(data) = message {
|
||||
break Ok((id, data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive_raw(&mut self) -> Result<(ChannelPublisherId, Message<T>), Error> {
|
||||
let (id, metadata) = self.inner.receive_raw(&mut self.buffer)?;
|
||||
|
||||
match metadata {
|
||||
ReceivedMessageMetadata::Data(len) => {
|
||||
let msg = flexbuffers::from_slice(&self.buffer[..len])?;
|
||||
Ok((id, Message::Data(msg)))
|
||||
}
|
||||
ReceivedMessageMetadata::File(fd) => Ok((id, Message::File(fd))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_poll_fd(&self) -> RawFd {
|
||||
self.as_raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DeserializeOwned> AsRawFd for Receiver<T> {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.inner.as_raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<flexbuffers::SerializationError> for Error {
|
||||
fn from(value: flexbuffers::SerializationError) -> Self {
|
||||
Self::SerializeError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<flexbuffers::DeserializationError> for Error {
|
||||
fn from(value: flexbuffers::DeserializationError) -> Self {
|
||||
Self::DeserializeError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::IoError(value)
|
||||
}
|
||||
}
|
45
userspace/netutils/Cargo.toml
Normal file
45
userspace/netutils/Cargo.toml
Normal file
@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "netutils"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git", features = ["serde", "alloc", "bytemuck"] }
|
||||
|
||||
clap = { version = "4.3.19", features = ["std", "derive", "help", "usage"], default-features = false }
|
||||
thiserror = "1.0.50"
|
||||
bytemuck = { version = "1.14.0", features = ["derive"] }
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
rand = { git = "https://git.alnyan.me/yggdrasil/rand.git", branch = "alnyan/yggdrasil" }
|
||||
url = "2.5.0"
|
||||
clap-num = "1.1.1"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "netconf"
|
||||
path = "src/netconf.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "dhcp-client"
|
||||
path = "src/dhcp_client.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "nc"
|
||||
path = "src/netcat.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "http"
|
||||
path = "src/http.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "dnsq"
|
||||
path = "src/dnsq.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "ping"
|
||||
path = "src/ping.rs"
|
570
userspace/netutils/src/dhcp_client.rs
Normal file
570
userspace/netutils/src/dhcp_client.rs
Normal file
@ -0,0 +1,570 @@
|
||||
#![feature(yggdrasil_os)]
|
||||
|
||||
use std::os::{
|
||||
fd::AsRawFd,
|
||||
yggdrasil::io::{poll::PollChannel, raw_socket::RawSocket, timer::TimerFd},
|
||||
};
|
||||
use std::{io, mem::size_of, process::ExitCode, time::Duration};
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use netutils::{parse_udp_protocol, Error, NetConfig};
|
||||
use yggdrasil_abi::net::protocols::{
|
||||
EtherType, EthernetFrame, InetChecksum, IpProtocol, Ipv4Frame, UdpFrame,
|
||||
};
|
||||
use yggdrasil_abi::net::types::NetValueImpl;
|
||||
use yggdrasil_abi::net::{IpAddr, Ipv4Addr, MacAddress, SubnetAddr, SubnetV4Addr};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
|
||||
#[repr(C, packed)]
|
||||
struct DhcpMessageHeader {
|
||||
op: u8,
|
||||
htype: u8,
|
||||
hlen: u8,
|
||||
hops: u8,
|
||||
xid: u32,
|
||||
secs: u16,
|
||||
flags: u16,
|
||||
ciaddr: u32,
|
||||
yiaddr: u32,
|
||||
siaddr: u32,
|
||||
giaddr: u32,
|
||||
chaddr: [u8; 16],
|
||||
sname: [u8; 64],
|
||||
file: [u8; 128],
|
||||
cookie: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum DhcpMessageType {
|
||||
Offer,
|
||||
Discover,
|
||||
Request,
|
||||
Acknowledge,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DhcpOption {
|
||||
MessageType(DhcpMessageType),
|
||||
SubnetMask(u32),
|
||||
Router(Ipv4Addr),
|
||||
ServerId(Ipv4Addr),
|
||||
// ParameterRequestList(Vec<u8>),
|
||||
RequestedAddress(Ipv4Addr),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ParsedOption {
|
||||
Option(usize, DhcpOption),
|
||||
Unknown(usize),
|
||||
Pad,
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DhcpMessage {
|
||||
header: DhcpMessageHeader,
|
||||
options: Vec<DhcpOption>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DhcpOffer {
|
||||
router_address: Ipv4Addr,
|
||||
server_address: Ipv4Addr,
|
||||
your_address: Ipv4Addr,
|
||||
subnet_mask: u32,
|
||||
}
|
||||
|
||||
fn pad_to_align(buffer: &mut Vec<u8>) {
|
||||
let offset = buffer.len() % 4;
|
||||
for _ in offset..4 {
|
||||
buffer.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DhcpMessageType> for u8 {
|
||||
fn from(value: DhcpMessageType) -> u8 {
|
||||
match value {
|
||||
DhcpMessageType::Discover => 0x01,
|
||||
DhcpMessageType::Offer => 0x02,
|
||||
DhcpMessageType::Request => 0x03,
|
||||
DhcpMessageType::Acknowledge => 0x05,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for DhcpMessageType {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u8) -> Result<DhcpMessageType, Self::Error> {
|
||||
match value {
|
||||
0x01 => Ok(DhcpMessageType::Discover),
|
||||
0x02 => Ok(DhcpMessageType::Offer),
|
||||
0x03 => Ok(DhcpMessageType::Request),
|
||||
0x05 => Ok(DhcpMessageType::Acknowledge),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DhcpOption {
|
||||
fn write(&self, buffer: &mut Vec<u8>) {
|
||||
match self {
|
||||
Self::MessageType(ty) => {
|
||||
buffer.extend_from_slice(&[53, 0x01, (*ty).into()]);
|
||||
}
|
||||
// Self::ParameterRequestList(list) => todo!(),
|
||||
Self::RequestedAddress(addr) => {
|
||||
buffer.extend_from_slice(&[50, 0x04]);
|
||||
buffer.extend_from_slice(&u32::from(*addr).to_be_bytes());
|
||||
}
|
||||
Self::ServerId(addr) => {
|
||||
buffer.extend_from_slice(&[54, 0x04]);
|
||||
buffer.extend_from_slice(&u32::from(*addr).to_be_bytes());
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(data: &[u8]) -> Option<ParsedOption> {
|
||||
if data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if data[0] == 0 {
|
||||
return Some(ParsedOption::Pad);
|
||||
}
|
||||
if data[0] == 0xFF {
|
||||
return Some(ParsedOption::End);
|
||||
}
|
||||
|
||||
if data.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let opcode = data[0];
|
||||
let length = data[1] as usize;
|
||||
|
||||
if data.len() < 2 + length {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(ParsedOption::Option(
|
||||
2 + length,
|
||||
match opcode {
|
||||
// DHCP message type
|
||||
1 if length == 4 => {
|
||||
let value = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
|
||||
DhcpOption::SubnetMask(value)
|
||||
}
|
||||
3 if length == 4 => {
|
||||
let value = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
|
||||
DhcpOption::Router(value.into())
|
||||
}
|
||||
53 if length == 1 => {
|
||||
let ty = DhcpMessageType::try_from(data[2]).ok()?;
|
||||
DhcpOption::MessageType(ty)
|
||||
}
|
||||
54 if length == 4 => {
|
||||
let value = u32::from_be_bytes([data[2], data[3], data[4], data[5]]);
|
||||
DhcpOption::ServerId(value.into())
|
||||
}
|
||||
_ => return Some(ParsedOption::Unknown(2 + length)),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl DhcpMessage {
|
||||
fn to_vec(&self) -> Vec<u8> {
|
||||
let mut result = vec![];
|
||||
|
||||
result.extend_from_slice(bytemuck::bytes_of(&self.header));
|
||||
|
||||
for option in self.options.iter() {
|
||||
option.write(&mut result);
|
||||
pad_to_align(&mut result);
|
||||
}
|
||||
|
||||
// End option
|
||||
result.push(0xFF);
|
||||
pad_to_align(&mut result);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < size_of::<DhcpMessageHeader>() {
|
||||
return None;
|
||||
}
|
||||
let header: &DhcpMessageHeader =
|
||||
bytemuck::from_bytes(&data[..size_of::<DhcpMessageHeader>()]);
|
||||
|
||||
let mut options = vec![];
|
||||
|
||||
let mut offset = size_of::<DhcpMessageHeader>();
|
||||
loop {
|
||||
let data = &data[offset..];
|
||||
let option = DhcpOption::parse(data)?;
|
||||
|
||||
match option {
|
||||
ParsedOption::Option(len, value) => {
|
||||
options.push(value);
|
||||
offset += len;
|
||||
}
|
||||
ParsedOption::Unknown(len) => {
|
||||
offset += len;
|
||||
}
|
||||
ParsedOption::Pad => {
|
||||
offset += 1;
|
||||
}
|
||||
ParsedOption::End => break,
|
||||
}
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
header: *header,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
fn message_type(&self) -> Option<DhcpMessageType> {
|
||||
self.options.iter().find_map(|option| match option {
|
||||
DhcpOption::MessageType(ty) => Some(*ty),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn as_dhcp_acknowledge(&self, requested_address: Ipv4Addr) -> Option<()> {
|
||||
let ty = self.message_type()?;
|
||||
|
||||
if ty != DhcpMessageType::Acknowledge {
|
||||
return None;
|
||||
}
|
||||
let yiaddr = u32::from_be(self.header.yiaddr);
|
||||
if yiaddr == 0 {
|
||||
return None;
|
||||
}
|
||||
if Ipv4Addr::from(yiaddr) != requested_address {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn as_dhcp_offer(&self) -> Option<DhcpOffer> {
|
||||
let ty = self.message_type()?;
|
||||
if ty != DhcpMessageType::Offer {
|
||||
return None;
|
||||
}
|
||||
let yiaddr = u32::from_be(self.header.yiaddr);
|
||||
if yiaddr == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut router = None;
|
||||
let mut server = None;
|
||||
let mut subnet_mask = None;
|
||||
|
||||
for option in self.options.iter() {
|
||||
match option {
|
||||
DhcpOption::Router(address) if router.is_none() => {
|
||||
router.replace(*address);
|
||||
}
|
||||
DhcpOption::ServerId(address) if server.is_none() => {
|
||||
server.replace(*address);
|
||||
}
|
||||
DhcpOption::SubnetMask(mask) if subnet_mask.is_none() => {
|
||||
subnet_mask.replace(*mask);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let router_address = router?;
|
||||
let server_address = server?;
|
||||
let subnet_mask = subnet_mask?;
|
||||
|
||||
Some(DhcpOffer {
|
||||
server_address,
|
||||
router_address,
|
||||
your_address: Ipv4Addr::from(yiaddr),
|
||||
subnet_mask,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn send_split_packet(
|
||||
socket: &RawSocket,
|
||||
l2_frame: &[u8],
|
||||
l3_frame: &[u8],
|
||||
l4_frame: &[u8],
|
||||
l4_data: &[u8],
|
||||
) -> Result<(), io::Error> {
|
||||
let mut buffer =
|
||||
Vec::with_capacity(l2_frame.len() + l3_frame.len() + l4_frame.len() + l4_data.len());
|
||||
buffer.extend_from_slice(l2_frame);
|
||||
buffer.extend_from_slice(l3_frame);
|
||||
buffer.extend_from_slice(l4_frame);
|
||||
buffer.extend_from_slice(l4_data);
|
||||
|
||||
socket.send(&buffer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_udp_broadcast(
|
||||
socket: &RawSocket,
|
||||
source_mac: MacAddress,
|
||||
source_port: u16,
|
||||
destination_port: u16,
|
||||
data: &[u8],
|
||||
) -> Result<(), io::Error> {
|
||||
let ip_size = size_of::<Ipv4Frame>() + size_of::<UdpFrame>() + data.len();
|
||||
let udp_size = size_of::<UdpFrame>() + data.len();
|
||||
|
||||
let l2_frame = EthernetFrame {
|
||||
source_mac,
|
||||
destination_mac: MacAddress::BROADCAST,
|
||||
ethertype: EtherType::IPV4.to_network_order(),
|
||||
};
|
||||
let mut l3_frame = Ipv4Frame {
|
||||
source_address: u32::from(Ipv4Addr::UNSPECIFIED).to_network_order(),
|
||||
destination_address: u32::from(Ipv4Addr::BROADCAST).to_network_order(),
|
||||
protocol: IpProtocol::UDP,
|
||||
version_length: 0x45,
|
||||
total_length: u16::try_from(ip_size).unwrap().to_network_order(),
|
||||
dscp_flags: 0,
|
||||
flags_frag: 0x4000u16.to_network_order(),
|
||||
id: 0u16.to_network_order(),
|
||||
header_checksum: 0u16.to_network_order(),
|
||||
ttl: 64,
|
||||
};
|
||||
let l4_frame = UdpFrame {
|
||||
source_port: source_port.to_network_order(),
|
||||
destination_port: destination_port.to_network_order(),
|
||||
length: u16::try_from(udp_size).unwrap().to_network_order(),
|
||||
checksum: 0u16.to_network_order(),
|
||||
};
|
||||
|
||||
let mut ip_checksum = InetChecksum::new();
|
||||
ip_checksum.add_bytes(bytemuck::bytes_of(&l3_frame), true);
|
||||
let ip_checksum = ip_checksum.finish();
|
||||
|
||||
l3_frame.header_checksum = ip_checksum.to_network_order();
|
||||
|
||||
send_split_packet(
|
||||
socket,
|
||||
bytemuck::bytes_of(&l2_frame),
|
||||
bytemuck::bytes_of(&l3_frame),
|
||||
bytemuck::bytes_of(&l4_frame),
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
fn wait_for_dhcp_offer(
|
||||
poll: &mut PollChannel,
|
||||
timer: &mut TimerFd,
|
||||
socket: &RawSocket,
|
||||
) -> Result<DhcpOffer, Error> {
|
||||
let mut packet = [0; 4096];
|
||||
|
||||
let socket_fd = socket.as_raw_fd();
|
||||
let timer_fd = timer.as_raw_fd();
|
||||
|
||||
timer.start(Duration::from_secs(1))?;
|
||||
|
||||
loop {
|
||||
let (fd, result) = poll.wait(None)?.unwrap();
|
||||
result?;
|
||||
|
||||
match fd {
|
||||
fd if fd == socket_fd => {
|
||||
let len = socket.recv(&mut packet)?;
|
||||
|
||||
let Some((source, destination, data)) = parse_udp_protocol(&packet[..len]) else {
|
||||
continue;
|
||||
};
|
||||
if source.port() != 67 || destination.port() != 68 {
|
||||
continue;
|
||||
}
|
||||
let Some(msg) = DhcpMessage::parse(data) else {
|
||||
continue;
|
||||
};
|
||||
let Some(offer) = msg.as_dhcp_offer() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
return Ok(offer);
|
||||
}
|
||||
fd if fd == timer_fd => {
|
||||
return Err(Error::TimedOut);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_for_dhcp_acknowledge(
|
||||
poll: &mut PollChannel,
|
||||
timer: &mut TimerFd,
|
||||
socket: &RawSocket,
|
||||
requested_address: Ipv4Addr,
|
||||
) -> Result<(), Error> {
|
||||
let mut packet = [0; 4096];
|
||||
|
||||
let socket_fd = socket.as_raw_fd();
|
||||
let timer_fd = timer.as_raw_fd();
|
||||
|
||||
timer.start(Duration::from_secs(1))?;
|
||||
|
||||
loop {
|
||||
let (fd, result) = poll.wait(None)?.unwrap();
|
||||
result?;
|
||||
|
||||
match fd {
|
||||
fd if fd == socket_fd => {
|
||||
let len = socket.recv(&mut packet)?;
|
||||
|
||||
let Some((source, destination, data)) = parse_udp_protocol(&packet[..len]) else {
|
||||
continue;
|
||||
};
|
||||
if source.port() != 67 || destination.port() != 68 {
|
||||
continue;
|
||||
}
|
||||
let Some(msg) = DhcpMessage::parse(data) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if msg.as_dhcp_acknowledge(requested_address).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
fd if fd == timer_fd => {
|
||||
return Err(Error::TimedOut);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn attempt_request(
|
||||
poll: &mut PollChannel,
|
||||
timer: &mut TimerFd,
|
||||
socket: &RawSocket,
|
||||
iface_mac_bytes: [u8; 6],
|
||||
) -> Result<DhcpOffer, Error> {
|
||||
let iface_mac = MacAddress::from(iface_mac_bytes);
|
||||
|
||||
let transaction_id = rand::random::<u32>();
|
||||
let mut message = DhcpMessage {
|
||||
header: DhcpMessageHeader {
|
||||
op: 1,
|
||||
htype: 1,
|
||||
hlen: 6,
|
||||
xid: transaction_id.to_be(),
|
||||
chaddr: [0; 16],
|
||||
cookie: 0x63825363u32.to_be(),
|
||||
..DhcpMessageHeader::zeroed()
|
||||
},
|
||||
options: vec![DhcpOption::MessageType(DhcpMessageType::Discover)],
|
||||
};
|
||||
message.header.chaddr[..6].copy_from_slice(&iface_mac_bytes);
|
||||
let data = message.to_vec();
|
||||
|
||||
send_udp_broadcast(socket, iface_mac, 68, 67, &data)?;
|
||||
|
||||
let offer = wait_for_dhcp_offer(poll, timer, socket)?;
|
||||
|
||||
println!(
|
||||
"Got DHCP offer: address={} router={} server={} mask={}",
|
||||
offer.your_address,
|
||||
offer.router_address,
|
||||
offer.server_address,
|
||||
Ipv4Addr::from(offer.subnet_mask)
|
||||
);
|
||||
|
||||
// Send DHCP request
|
||||
let mut message = DhcpMessage {
|
||||
header: DhcpMessageHeader {
|
||||
op: 1,
|
||||
htype: 1,
|
||||
hlen: 6,
|
||||
xid: transaction_id.to_be(),
|
||||
chaddr: [0; 16],
|
||||
siaddr: u32::from(offer.server_address).to_be(),
|
||||
cookie: 0x63825363u32.to_be(),
|
||||
..DhcpMessageHeader::zeroed()
|
||||
},
|
||||
options: vec![
|
||||
DhcpOption::MessageType(DhcpMessageType::Request),
|
||||
DhcpOption::RequestedAddress(offer.your_address),
|
||||
DhcpOption::ServerId(offer.server_address),
|
||||
],
|
||||
};
|
||||
message.header.chaddr[..6].copy_from_slice(&iface_mac_bytes);
|
||||
let data = message.to_vec();
|
||||
|
||||
send_udp_broadcast(socket, iface_mac, 68, 67, &data)?;
|
||||
|
||||
wait_for_dhcp_acknowledge(poll, timer, socket, offer.your_address)?;
|
||||
|
||||
println!("Got DHCP acknowledge for {}", offer.your_address);
|
||||
|
||||
Ok(offer)
|
||||
}
|
||||
|
||||
fn request_address(interface: &str) -> Result<DhcpOffer, Error> {
|
||||
let mut poll = PollChannel::new()?;
|
||||
let mut timer = TimerFd::new(false)?;
|
||||
let socket = RawSocket::bind(interface)?;
|
||||
let iface_mac_bytes = socket.hardware_address()?;
|
||||
|
||||
poll.add(socket.as_raw_fd())?;
|
||||
poll.add(timer.as_raw_fd())?;
|
||||
|
||||
for _ in 0..5 {
|
||||
match attempt_request(&mut poll, &mut timer, &socket, iface_mac_bytes) {
|
||||
Ok(offer) => {
|
||||
return Ok(offer);
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Error: {}, trying again", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::TimedOut)
|
||||
}
|
||||
|
||||
fn configure_address(interface: &str, offer: DhcpOffer) -> Result<(), Error> {
|
||||
let mut netconf = NetConfig::open()?;
|
||||
netconf.set_interface_address(interface, IpAddr::V4(offer.your_address))?;
|
||||
netconf.add_route(
|
||||
interface,
|
||||
SubnetAddr::V4(SubnetV4Addr::UNSPECIFIED),
|
||||
Some(IpAddr::V4(offer.router_address)),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
|
||||
if args.len() != 2 {
|
||||
eprintln!("Usage: {} INTERFACE", args[0]);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let offer = match request_address(&args[1]) {
|
||||
Ok(offer) => offer,
|
||||
Err(error) => {
|
||||
eprintln!("Could not obtain an address from DHCP: {}", error);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
match configure_address(&args[1], offer) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("Could not assign offered address to {}: {}", args[1], error);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
260
userspace/netutils/src/dnsq.rs
Normal file
260
userspace/netutils/src/dnsq.rs
Normal file
@ -0,0 +1,260 @@
|
||||
#![feature(yggdrasil_os)]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
os::{
|
||||
fd::AsRawFd,
|
||||
yggdrasil::io::{poll::PollChannel, timer::TimerFd},
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
io,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
|
||||
process::ExitCode,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
use yggdrasil_abi::net::dns::{
|
||||
DnsClass, DnsMessage, DnsMethod, DnsRecordData, DnsReplyCode, DnsSerialize, DnsType,
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum Error {
|
||||
#[error("{0}")]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("Timed out")]
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Arguments {
|
||||
#[clap(short, long)]
|
||||
server: Option<SocketAddr>,
|
||||
#[clap(short, long)]
|
||||
nameservers: Vec<IpAddr>,
|
||||
#[clap(short, long, value_enum)]
|
||||
record_type: Option<RecordType>,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, ValueEnum)]
|
||||
enum RecordType {
|
||||
A,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum ServerError {
|
||||
#[error("{0}")]
|
||||
Error(#[from] Error),
|
||||
#[error("Invalid query")]
|
||||
InvalidQuery,
|
||||
#[error("Name does not exist")]
|
||||
NameDoesNotExist,
|
||||
}
|
||||
|
||||
fn handle_query(
|
||||
nameservers: &[SocketAddr],
|
||||
a_cache: &mut HashMap<String, Ipv4Addr>,
|
||||
ty: DnsType,
|
||||
class: DnsClass,
|
||||
name: &str,
|
||||
) -> Result<IpAddr, ServerError> {
|
||||
match (ty, class) {
|
||||
(DnsType::A, DnsClass::IN) => {
|
||||
if let Some(ip) = a_cache.get(name) {
|
||||
debug_trace!("Cache hit: {} -> {}", name, ip);
|
||||
return Ok(IpAddr::V4(*ip));
|
||||
}
|
||||
|
||||
debug_trace!("Cache miss: {}", name);
|
||||
let result = perform_query(nameservers, name, ty)?;
|
||||
|
||||
for answer in result.answers {
|
||||
let Some(name) = answer.name.0 else {
|
||||
continue;
|
||||
};
|
||||
|
||||
#[allow(clippy::single_match)]
|
||||
match (answer.ty, answer.class, answer.rdata) {
|
||||
(DnsType::A, DnsClass::IN, DnsRecordData::A(ip)) => {
|
||||
debug_trace!("Resolved: {} -> {}", name, ip);
|
||||
a_cache.insert(name, ip.into());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
a_cache
|
||||
.get(name)
|
||||
.copied()
|
||||
.map(IpAddr::V4)
|
||||
.ok_or(ServerError::NameDoesNotExist)
|
||||
}
|
||||
_ => Err(ServerError::InvalidQuery),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_server(addr: SocketAddr, nameservers: &[SocketAddr]) -> Result<(), Error> {
|
||||
// TODO cache TTL
|
||||
let mut a_cache = HashMap::new();
|
||||
let mut buffer = [0; 2048];
|
||||
let socket = UdpSocket::bind(addr)?;
|
||||
|
||||
loop {
|
||||
let (len, remote) = socket.recv_from(&mut buffer)?;
|
||||
let Some(message) = DnsMessage::parse(&buffer[..len]) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut answers = vec![];
|
||||
for (name, ty, class) in message
|
||||
.questions
|
||||
.iter()
|
||||
.filter_map(|q| q.name.0.as_deref().map(|name| (name, q.ty, q.class)))
|
||||
{
|
||||
match handle_query(nameservers, &mut a_cache, ty, class, name) {
|
||||
Ok(ip) => answers.push((name.to_owned(), ip.into())),
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reply_code = if answers.len() == message.questions.len() {
|
||||
DnsReplyCode::NO_ERROR
|
||||
} else {
|
||||
DnsReplyCode::NAME_ERROR
|
||||
};
|
||||
|
||||
let mut packet = vec![];
|
||||
let reply = DnsMessage::reply(reply_code, message.questions, answers, message.xid);
|
||||
reply.serialize(&mut packet);
|
||||
if let Err(error) = socket.send_to(&packet, remote) {
|
||||
debug_trace!("sendto: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_query(nameservers: &[SocketAddr], name: &str, ty: DnsType) -> Result<DnsMessage, Error> {
|
||||
let mut buffer = [0; 2048];
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")?;
|
||||
let mut timer = TimerFd::new(false)?;
|
||||
let mut poll = PollChannel::new()?;
|
||||
|
||||
let timer_fd = timer.as_raw_fd();
|
||||
let socket_fd = socket.as_raw_fd();
|
||||
|
||||
poll.add(socket_fd)?;
|
||||
poll.add(timer_fd)?;
|
||||
|
||||
for _ in 0..5 {
|
||||
let mut packet = vec![];
|
||||
let xid = rand::random();
|
||||
let cookie = rand::random();
|
||||
let query = DnsMessage::query(name, ty, xid, cookie);
|
||||
query.serialize(&mut packet);
|
||||
|
||||
for nameserver in nameservers.iter() {
|
||||
socket.send_to(&packet, nameserver)?;
|
||||
}
|
||||
timer.start(Duration::from_millis(500))?;
|
||||
|
||||
loop {
|
||||
let (poll_fd, result) = poll.wait(None)?.unwrap();
|
||||
result?;
|
||||
|
||||
match poll_fd {
|
||||
fd if fd == socket_fd => {
|
||||
let (len, _) = socket.recv_from(&mut buffer)?;
|
||||
let Some(message) = DnsMessage::parse(&buffer[..len]) else {
|
||||
continue;
|
||||
};
|
||||
if message.xid != xid {
|
||||
continue;
|
||||
}
|
||||
if message.method != DnsMethod::REPLY {
|
||||
eprintln!("WARN: server sent a QUERY instead of REPLY");
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(message);
|
||||
}
|
||||
fd if fd == timer_fd => {
|
||||
break;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::TimedOut)
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let mut args: Arguments = Arguments::parse();
|
||||
|
||||
if let Some(server) = args.server {
|
||||
let nameservers: Vec<_> = args
|
||||
.nameservers
|
||||
.into_iter()
|
||||
.map(|ip| SocketAddr::new(ip, 53))
|
||||
.collect();
|
||||
|
||||
if nameservers.is_empty() {
|
||||
eprintln!("-s requires one or more nameservers");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
match run_server(server, &nameservers) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {}", error);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
} else if let Some(name) = args.name {
|
||||
if args.nameservers.is_empty() {
|
||||
args.nameservers
|
||||
.push(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
||||
}
|
||||
let nameservers: Vec<_> = args
|
||||
.nameservers
|
||||
.into_iter()
|
||||
.map(|ip| SocketAddr::new(ip, 53))
|
||||
.collect();
|
||||
|
||||
let ty = match args.record_type.unwrap_or(RecordType::A) {
|
||||
RecordType::A => DnsType::A,
|
||||
};
|
||||
|
||||
let message = match perform_query(&nameservers, &name, ty) {
|
||||
Ok(message) => message,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {}", error);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
if message.reply_code == DnsReplyCode::NO_ERROR {
|
||||
for answer in message.answers.iter() {
|
||||
let name = answer.name.0.as_deref().unwrap_or("<NULL>");
|
||||
#[allow(clippy::single_match)]
|
||||
match (answer.ty, answer.class, &answer.rdata) {
|
||||
(DnsType::A, DnsClass::IN, DnsRecordData::A(address)) => {
|
||||
println!("{}: {}", name, address);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
} else {
|
||||
eprintln!("Server returned error: {}", message.reply_code);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
} else {
|
||||
eprintln!("Either -s LISTEN-ADDRESS or NAME must be specified");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
148
userspace/netutils/src/http.rs
Normal file
148
userspace/netutils/src/http.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, stdout, Read, Stdout, Write},
|
||||
net::TcpStream,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitCode,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use url::{Host, Url};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Arguments {
|
||||
#[clap(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
#[clap(subcommand)]
|
||||
method: Method,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Method {
|
||||
#[clap(arg_required_else_help = true)]
|
||||
Get {
|
||||
#[clap(help = "URL to GET")]
|
||||
url: Url,
|
||||
},
|
||||
}
|
||||
|
||||
enum Output {
|
||||
File(File),
|
||||
Stdout(Stdout),
|
||||
}
|
||||
|
||||
impl Output {
|
||||
fn open<P: AsRef<Path>>(path: Option<P>) -> Result<Self, io::Error> {
|
||||
match path {
|
||||
Some(path) => File::create(path).map(Self::File),
|
||||
None => Ok(Self::Stdout(stdout())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Output {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::File(file) => file.write(buf),
|
||||
Self::Stdout(file) => file.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::File(file) => file.flush(),
|
||||
Self::Stdout(file) => file.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_header<R: Read>(stream: &mut R) -> Result<Option<String>, io::Error> {
|
||||
let mut buf = [0];
|
||||
let mut line = Vec::new();
|
||||
loop {
|
||||
stream.read_exact(&mut buf[..1])?;
|
||||
if buf[0] == b'\r' {
|
||||
continue;
|
||||
}
|
||||
if buf[0] == b'\n' {
|
||||
break;
|
||||
}
|
||||
line.push(buf[0]);
|
||||
}
|
||||
if line.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let line = String::from_utf8(line).unwrap();
|
||||
Ok(Some(line))
|
||||
}
|
||||
|
||||
fn get(url: Url, output: Option<PathBuf>) -> Result<(), io::Error> {
|
||||
let mut output = Output::open(output)?;
|
||||
|
||||
let host = url.host().unwrap();
|
||||
|
||||
let request = format!(
|
||||
"GET {} HTTP/1.1\r\nHost: {}\r\nAccept: */*\r\n\r\n",
|
||||
url.path(),
|
||||
host
|
||||
);
|
||||
|
||||
let port = url.port_or_known_default().unwrap();
|
||||
let mut stream = match host {
|
||||
Host::Domain(hostname) => TcpStream::connect((hostname, port))?,
|
||||
Host::Ipv4(address) => TcpStream::connect((address, port))?,
|
||||
Host::Ipv6(address) => TcpStream::connect((address, port))?,
|
||||
};
|
||||
|
||||
eprintln!("Connecting to {}:{}...", host, port);
|
||||
|
||||
//let mut stream = TcpStream::connect(remote)?;
|
||||
let mut buf = [0; 2048];
|
||||
|
||||
for line in request.split('\n') {
|
||||
eprintln!("> {}", line.trim());
|
||||
}
|
||||
|
||||
stream.write_all(request.as_bytes())?;
|
||||
|
||||
let mut content_length = 0;
|
||||
|
||||
while let Some(header) = receive_header(&mut stream)? {
|
||||
let Some((key, value)) = header.split_once(':') else {
|
||||
continue;
|
||||
};
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
|
||||
if key == "Content-Length" {
|
||||
content_length = usize::from_str(value).unwrap();
|
||||
}
|
||||
eprintln!("< {}", header);
|
||||
}
|
||||
|
||||
while content_length != 0 {
|
||||
let limit = buf.len().min(content_length);
|
||||
let amount = stream.read(&mut buf[..limit])?;
|
||||
output.write_all(&buf[..amount])?;
|
||||
content_length -= amount;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Arguments::parse();
|
||||
|
||||
let result = match args.method {
|
||||
Method::Get { url } => get(url, args.output),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {}", error);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
171
userspace/netutils/src/lib.rs
Normal file
171
userspace/netutils/src/lib.rs
Normal file
@ -0,0 +1,171 @@
|
||||
#![feature(yggdrasil_os, rustc_private)]
|
||||
|
||||
use std::{
|
||||
io,
|
||||
mem::size_of,
|
||||
os::yggdrasil::io::message_channel::{
|
||||
MessageChannel, MessageChannelReceiver, MessageChannelSender, MessageDestination,
|
||||
MessageReceiver, MessageSender,
|
||||
},
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
use yggdrasil_abi::net::{
|
||||
netconfig::{InterfaceQuery, NetConfigRequest, NetConfigResult, RoutingInfo},
|
||||
protocols::{EtherType, EthernetFrame, IpProtocol, Ipv4Frame, UdpFrame},
|
||||
types::NetValueImpl,
|
||||
IpAddr, Ipv4Addr, MacAddress, SocketAddr, SubnetAddr,
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("NetConfig returned error: {0}")]
|
||||
NetConfError(Box<str>),
|
||||
#[error("Serialize/deserialize error: {0}")]
|
||||
SerializeError(#[from] serde_json::Error),
|
||||
#[error("Timed out")]
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
pub struct NetConfig {
|
||||
sender: MessageChannelSender,
|
||||
receiver: MessageChannelReceiver,
|
||||
buffer: [u8; 4096],
|
||||
}
|
||||
|
||||
impl NetConfig {
|
||||
pub fn open() -> Result<Self, Error> {
|
||||
let channel = MessageChannel::open("@kernel-netconf", true)?;
|
||||
let (sender, receiver) = channel.split();
|
||||
Ok(Self {
|
||||
sender,
|
||||
receiver,
|
||||
buffer: [0; 4096],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send(&mut self, request: &NetConfigRequest) -> Result<(), Error> {
|
||||
let bytes = serde_json::to_vec(&request)?;
|
||||
self.sender
|
||||
.send_message(&bytes, MessageDestination::Specific(0))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn request<'de, T: Deserialize<'de>>(
|
||||
&'de mut self,
|
||||
request: &NetConfigRequest,
|
||||
) -> Result<T, Error> {
|
||||
self.send(request)?;
|
||||
let (_sender, len) = self.receiver.receive_message(&mut self.buffer)?;
|
||||
let msg: NetConfigResult<T> = serde_json::from_slice(&self.buffer[..len])?;
|
||||
match msg {
|
||||
NetConfigResult::Ok(value) => Ok(value),
|
||||
NetConfigResult::Err(error) => Err(Error::NetConfError(error)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query_route(&mut self, address: IpAddr) -> Result<RoutingInfo, Error> {
|
||||
self.request(&NetConfigRequest::QueryRoute(address))
|
||||
}
|
||||
|
||||
pub fn query_arp(
|
||||
&mut self,
|
||||
interface_id: u32,
|
||||
address: IpAddr,
|
||||
perform_query: bool,
|
||||
) -> Result<MacAddress, Error> {
|
||||
self.request(&NetConfigRequest::QueryArp(
|
||||
interface_id,
|
||||
address,
|
||||
perform_query,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn set_interface_address<Q: Into<InterfaceQuery>>(
|
||||
&mut self,
|
||||
interface: Q,
|
||||
address: IpAddr,
|
||||
) -> Result<(), Error> {
|
||||
self.request(&NetConfigRequest::SetNetworkAddress {
|
||||
interface: interface.into(),
|
||||
address,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_route<Q: Into<InterfaceQuery>>(
|
||||
&mut self,
|
||||
interface: Q,
|
||||
subnet: SubnetAddr,
|
||||
gateway: Option<IpAddr>,
|
||||
) -> Result<(), Error> {
|
||||
self.request(&NetConfigRequest::AddRoute {
|
||||
interface: interface.into(),
|
||||
gateway,
|
||||
subnet,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_l2_protocol(packet: &[u8]) -> Option<(EthernetFrame, &[u8])> {
|
||||
if packet.len() < size_of::<EthernetFrame>() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let l2_frame: &EthernetFrame = bytemuck::from_bytes(&packet[..size_of::<EthernetFrame>()]);
|
||||
let l2_data = &packet[size_of::<EthernetFrame>()..];
|
||||
|
||||
Some((*l2_frame, l2_data))
|
||||
}
|
||||
|
||||
pub fn parse_ip_protocol(packet: &[u8]) -> Option<(IpProtocol, IpAddr, IpAddr, &[u8])> {
|
||||
let (l2_frame, l2_data) = parse_l2_protocol(packet)?;
|
||||
|
||||
match EtherType::from_network_order(l2_frame.ethertype) {
|
||||
EtherType::IPV4 if l2_data.len() >= size_of::<Ipv4Frame>() => {
|
||||
let l3_frame: &Ipv4Frame = bytemuck::from_bytes(&l2_data[..size_of::<Ipv4Frame>()]);
|
||||
|
||||
let source_addr = IpAddr::V4(Ipv4Addr::from(u32::from_network_order(
|
||||
l3_frame.source_address,
|
||||
)));
|
||||
let destination_addr = IpAddr::V4(Ipv4Addr::from(u32::from_network_order(
|
||||
l3_frame.destination_address,
|
||||
)));
|
||||
|
||||
Some((
|
||||
l3_frame.protocol,
|
||||
source_addr,
|
||||
destination_addr,
|
||||
&l2_data[l3_frame.header_length()..l3_frame.total_length()],
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_udp_protocol(packet: &[u8]) -> Option<(SocketAddr, SocketAddr, &[u8])> {
|
||||
let (protocol, source_ip, destination_ip, l3_data) = parse_ip_protocol(packet)?;
|
||||
|
||||
if protocol != IpProtocol::UDP || l3_data.len() < size_of::<UdpFrame>() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let l4_frame: &UdpFrame = bytemuck::from_bytes(&l3_data[..size_of::<UdpFrame>()]);
|
||||
let l4_data_size = core::cmp::min(
|
||||
l3_data.len() - size_of::<UdpFrame>(),
|
||||
l4_frame.data_length(),
|
||||
);
|
||||
|
||||
let source_addr = SocketAddr::new(source_ip, u16::from_network_order(l4_frame.source_port));
|
||||
let destination_addr = SocketAddr::new(
|
||||
destination_ip,
|
||||
u16::from_network_order(l4_frame.destination_port),
|
||||
);
|
||||
|
||||
Some((
|
||||
source_addr,
|
||||
destination_addr,
|
||||
&l3_data[size_of::<UdpFrame>()..size_of::<UdpFrame>() + l4_data_size],
|
||||
))
|
||||
}
|
164
userspace/netutils/src/netcat.rs
Normal file
164
userspace/netutils/src/netcat.rs
Normal file
@ -0,0 +1,164 @@
|
||||
#![feature(yggdrasil_os)]
|
||||
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::yggdrasil::io::poll::PollChannel;
|
||||
use std::{
|
||||
io::{self, stdin, stdout, Read, Write},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, TcpStream, UdpSocket},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Args {
|
||||
#[clap(short, long)]
|
||||
listen: bool,
|
||||
#[clap(short, long)]
|
||||
udp: bool,
|
||||
#[clap(short = 'p', long)]
|
||||
listen_port: Option<u16>,
|
||||
address: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
fn udp_client(address: SocketAddr) -> io::Result<()> {
|
||||
let mut stdin = stdin();
|
||||
let mut stdout = stdout();
|
||||
let mut poll = PollChannel::new()?;
|
||||
// TODO use ephemeral port?
|
||||
// TODO use socket.connect()
|
||||
let socket = UdpSocket::bind(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||
address.port(),
|
||||
))?;
|
||||
let mut buffer = [0; 512];
|
||||
|
||||
let socket_fd = socket.as_raw_fd();
|
||||
let stdin_fd = stdin.as_raw_fd();
|
||||
|
||||
poll.add(socket_fd)?;
|
||||
poll.add(stdin_fd)?;
|
||||
|
||||
loop {
|
||||
let (fd, status) = poll.wait(None)?.unwrap();
|
||||
status?;
|
||||
|
||||
match fd {
|
||||
fd if fd == socket_fd => {
|
||||
let (len, _) = socket.recv_from(&mut buffer)?;
|
||||
stdout.write_all(&buffer[..len])?;
|
||||
}
|
||||
fd if fd == stdin_fd => {
|
||||
let len = stdin.read(&mut buffer)?;
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
socket.send_to(&buffer[..len], address)?;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn udp_server(port: u16) -> io::Result<()> {
|
||||
let mut stdout = stdout();
|
||||
let socket = UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?;
|
||||
let mut buffer = [0; 2048];
|
||||
|
||||
loop {
|
||||
let (len, _) = socket.recv_from(&mut buffer)?;
|
||||
stdout.write_all(&buffer[..len])?;
|
||||
}
|
||||
}
|
||||
|
||||
fn tcp_client(address: SocketAddr) -> io::Result<()> {
|
||||
let mut stdin = stdin();
|
||||
let mut stdout = stdout();
|
||||
let mut poll = PollChannel::new()?;
|
||||
let mut socket = TcpStream::connect(address)?;
|
||||
let mut buffer = [0; 512];
|
||||
|
||||
let socket_fd = socket.as_raw_fd();
|
||||
let stdin_fd = stdin.as_raw_fd();
|
||||
|
||||
poll.add(socket_fd)?;
|
||||
poll.add(stdin_fd)?;
|
||||
|
||||
loop {
|
||||
let (fd, status) = poll.wait(None)?.unwrap();
|
||||
status?;
|
||||
|
||||
match fd {
|
||||
fd if fd == socket_fd => {
|
||||
let len = socket.read(&mut buffer)?;
|
||||
stdout.write_all(&buffer[..len])?;
|
||||
}
|
||||
fd if fd == stdin_fd => {
|
||||
let len = stdin.read(&mut buffer)?;
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
socket.write_all(&buffer[..len])?;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tcp_server(port: u16) -> io::Result<()> {
|
||||
let mut stdout = stdout();
|
||||
let listener = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?;
|
||||
let mut buffer = [0; 2048];
|
||||
|
||||
let (mut stream, _) = listener.accept()?;
|
||||
|
||||
loop {
|
||||
let len = stream.read(&mut buffer)?;
|
||||
stdout.write_all(&buffer[..len])?;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
|
||||
if args.listen && args.address.is_some() {
|
||||
eprintln!("--listen/-l and remote address cannot be used simultaneously");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let result = if args.listen {
|
||||
let Some(port) = args.listen_port else {
|
||||
eprintln!("--listen/-l requires --listen-port/-p PORT");
|
||||
return ExitCode::FAILURE;
|
||||
};
|
||||
|
||||
if args.udp {
|
||||
udp_server(port)
|
||||
} else {
|
||||
tcp_server(port)
|
||||
}
|
||||
} else {
|
||||
let Some(address) = args.address else {
|
||||
eprintln!("Remote address required");
|
||||
return ExitCode::FAILURE;
|
||||
};
|
||||
|
||||
if args.udp {
|
||||
udp_client(address)
|
||||
} else {
|
||||
tcp_client(address)
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {}", error);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
277
userspace/netutils/src/netconf.rs
Normal file
277
userspace/netutils/src/netconf.rs
Normal file
@ -0,0 +1,277 @@
|
||||
use std::{net::IpAddr, process::ExitCode, str::FromStr};
|
||||
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use netutils::{Error, NetConfig};
|
||||
use yggdrasil_abi::net::netconfig::{InterfaceInfo, InterfaceQuery, NetConfigRequest, RouteInfo};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub struct Arguments {
|
||||
#[clap(subcommand)]
|
||||
section: Section,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Section {
|
||||
#[clap(arg_required_else_help = true, about = "Route query and manipulation")]
|
||||
Route(#[clap(subcommand)] RouteAction),
|
||||
#[clap(
|
||||
arg_required_else_help = true,
|
||||
about = "Address query and manipulation"
|
||||
)]
|
||||
Addr(#[clap(subcommand)] AddrAction),
|
||||
#[clap(
|
||||
arg_required_else_help = true,
|
||||
about = "Link information query and manipulation"
|
||||
)]
|
||||
Link(#[clap(subcommand)] LinkAction),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct RouteAction {
|
||||
#[clap(subcommand)]
|
||||
command: RouteCommands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct AddrAction {
|
||||
#[clap(subcommand)]
|
||||
command: AddrCommands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct LinkAction {
|
||||
#[clap(subcommand)]
|
||||
command: LinkCommands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum RouteCommands {
|
||||
#[clap(about = "List routes")]
|
||||
List {
|
||||
#[clap(help = "Filter routes by given network interface")]
|
||||
interface: Option<String>,
|
||||
},
|
||||
#[clap(about = "Add a route", arg_required_else_help = true)]
|
||||
Add {
|
||||
#[clap(help = "Network interface the route is associated with")]
|
||||
interface: String,
|
||||
#[clap(
|
||||
help = "Subnetwork the route is directed to",
|
||||
value_parser = SubnetAddr::from_str
|
||||
)]
|
||||
subnet: SubnetAddr,
|
||||
#[clap(help = "Gateway the route should be reached through")]
|
||||
gateway: Option<IpAddr>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum AddrCommands {
|
||||
#[clap(about = "List network interface IP addresses")]
|
||||
List,
|
||||
#[clap(about = "Get interface IP address")]
|
||||
Get {
|
||||
#[clap(help = "Interface to query")]
|
||||
interface: String,
|
||||
},
|
||||
#[clap(about = "Set interface's IP address")]
|
||||
Set {
|
||||
#[clap(help = "Interface to assign address to")]
|
||||
interface: String,
|
||||
#[clap(help = "IPv4/IPv6 address for the interface")]
|
||||
address: IpAddr,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum LinkCommands {
|
||||
#[clap(about = "List network interfaces")]
|
||||
List,
|
||||
#[clap(about = "Describe a network interface")]
|
||||
Show {
|
||||
#[clap(help = "Interface to describe")]
|
||||
interface: String,
|
||||
},
|
||||
#[clap(about = "Get interface MAC address")]
|
||||
GetMac {
|
||||
#[clap(help = "Interface to describe")]
|
||||
interface: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SubnetAddr(yggdrasil_abi::net::SubnetAddr);
|
||||
|
||||
impl FromStr for SubnetAddr {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let inner =
|
||||
yggdrasil_abi::net::SubnetAddr::from_str(s).map_err(|_| "Invalid subnet format")?;
|
||||
Ok(Self(inner))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_route_action(nc: &mut NetConfig, action: RouteCommands) -> Result<(), Error> {
|
||||
match action {
|
||||
RouteCommands::List { interface: None } => {
|
||||
let list = nc.request::<Vec<RouteInfo>>(&NetConfigRequest::ListRoutes)?;
|
||||
|
||||
for route in list {
|
||||
print_route_info(&route);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
RouteCommands::List {
|
||||
interface: Some(interface),
|
||||
} => {
|
||||
let list = nc.request::<Vec<RouteInfo>>(&NetConfigRequest::DescribeRoutes(
|
||||
InterfaceQuery::ByName(interface.into_boxed_str()),
|
||||
))?;
|
||||
|
||||
for route in list {
|
||||
print_route_info(&route);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
RouteCommands::Add {
|
||||
interface,
|
||||
subnet,
|
||||
gateway,
|
||||
} => nc.request::<()>(&NetConfigRequest::AddRoute {
|
||||
interface: InterfaceQuery::ByName(interface.into_boxed_str()),
|
||||
gateway: gateway.map(Into::into),
|
||||
subnet: subnet.0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_addr_action(nc: &mut NetConfig, action: AddrCommands) -> Result<(), Error> {
|
||||
match action {
|
||||
AddrCommands::List => {
|
||||
let list = nc.request::<Vec<(Box<str>, u32)>>(&NetConfigRequest::ListInterfaces)?;
|
||||
|
||||
for (name, id) in list {
|
||||
let info = match nc.request::<InterfaceInfo>(&NetConfigRequest::DescribeInterface(
|
||||
InterfaceQuery::ById(id),
|
||||
)) {
|
||||
Ok(info) => info,
|
||||
Err(error) => {
|
||||
eprintln!("{}: {}", name, error);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(address) = info.address {
|
||||
println!("{}: {}", name, address);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
AddrCommands::Get { interface } => {
|
||||
let info = nc.request::<InterfaceInfo>(&NetConfigRequest::DescribeInterface(
|
||||
InterfaceQuery::ByName(interface.into_boxed_str()),
|
||||
))?;
|
||||
|
||||
if let Some(address) = info.address {
|
||||
println!("{}", address);
|
||||
} else {
|
||||
println!("<No address>");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
AddrCommands::Set { interface, address } => {
|
||||
nc.request::<()>(&NetConfigRequest::SetNetworkAddress {
|
||||
interface: InterfaceQuery::ByName(interface.into_boxed_str()),
|
||||
address: address.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_link_action(nc: &mut NetConfig, action: LinkCommands) -> Result<(), Error> {
|
||||
match action {
|
||||
LinkCommands::List => {
|
||||
let list = nc.request::<Vec<(Box<str>, u32)>>(&NetConfigRequest::ListInterfaces)?;
|
||||
|
||||
for (name, id) in list {
|
||||
let info = match nc.request::<InterfaceInfo>(&NetConfigRequest::DescribeInterface(
|
||||
InterfaceQuery::ById(id),
|
||||
)) {
|
||||
Ok(info) => info,
|
||||
Err(error) => {
|
||||
eprintln!("{}: {}", name, error);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
print_interface_info(&info);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
LinkCommands::Show { interface } => {
|
||||
let info = nc.request::<InterfaceInfo>(&NetConfigRequest::DescribeInterface(
|
||||
InterfaceQuery::ByName(interface.into_boxed_str()),
|
||||
))?;
|
||||
|
||||
print_interface_info(&info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
LinkCommands::GetMac { interface } => {
|
||||
let info = nc.request::<InterfaceInfo>(&NetConfigRequest::DescribeInterface(
|
||||
InterfaceQuery::ByName(interface.into_boxed_str()),
|
||||
))?;
|
||||
|
||||
println!("{}", info.mac);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_action(section: Section) -> Result<(), Error> {
|
||||
let mut nc = NetConfig::open()?;
|
||||
match section {
|
||||
Section::Route(RouteAction { command }) => run_route_action(&mut nc, command),
|
||||
Section::Addr(AddrAction { command }) => run_addr_action(&mut nc, command),
|
||||
Section::Link(LinkAction { command }) => run_link_action(&mut nc, command),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_interface_info(info: &InterfaceInfo) {
|
||||
println!("{}:", info.interface_name);
|
||||
println!(" Id: #{}", info.interface_id);
|
||||
if let Some(address) = info.address {
|
||||
println!(" Address: {}", address);
|
||||
} else {
|
||||
println!(" <No address>");
|
||||
}
|
||||
println!(" Hardware address: {}", info.mac);
|
||||
}
|
||||
|
||||
fn print_route_info(info: &RouteInfo) {
|
||||
print!("{}", info.subnet);
|
||||
if let Some(gw) = info.gateway {
|
||||
print!(" via {}", gw);
|
||||
}
|
||||
println!(" dev {}", info.interface_name);
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Arguments::parse();
|
||||
|
||||
match run_action(args.section) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {}", error);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
354
userspace/netutils/src/ping.rs
Normal file
354
userspace/netutils/src/ping.rs
Normal file
@ -0,0 +1,354 @@
|
||||
#![feature(yggdrasil_os, rustc_private)]
|
||||
|
||||
use std::{
|
||||
mem::size_of,
|
||||
os::{
|
||||
fd::AsRawFd,
|
||||
yggdrasil::{
|
||||
io::{poll::PollChannel, raw_socket::RawSocket, timer::TimerFd},
|
||||
signal::{set_signal_handler, Signal, SignalHandler},
|
||||
},
|
||||
},
|
||||
process::ExitCode,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use bytemuck::Zeroable;
|
||||
use clap::Parser;
|
||||
use netutils::{Error, NetConfig};
|
||||
use yggdrasil_abi::net::{
|
||||
protocols::{EtherType, EthernetFrame, IcmpV4Frame, InetChecksum, IpProtocol, Ipv4Frame},
|
||||
types::NetValueImpl,
|
||||
IpAddr, Ipv4Addr, MacAddress,
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[clap(
|
||||
help = "Time (ms) between a reply is received and the next request is sent",
|
||||
short,
|
||||
long,
|
||||
default_value_t = 1000,
|
||||
value_parser = valid_interval
|
||||
)]
|
||||
inteval: u32,
|
||||
#[clap(
|
||||
help = "Time (ms) after which the request is considered unanswered",
|
||||
short,
|
||||
long,
|
||||
default_value_t = 500,
|
||||
value_parser = valid_timeout,
|
||||
)]
|
||||
timeout: u32,
|
||||
#[clap(
|
||||
help = "Number of requests to perform",
|
||||
short,
|
||||
long,
|
||||
default_value_t = 10
|
||||
)]
|
||||
count: usize,
|
||||
#[clap(
|
||||
help = "Amount of bytes to include as data",
|
||||
short,
|
||||
long,
|
||||
default_value_t = 64,
|
||||
value_parser = valid_data_size
|
||||
)]
|
||||
data_size: usize,
|
||||
|
||||
#[clap(help = "Address to ping")]
|
||||
address: core::net::IpAddr,
|
||||
}
|
||||
|
||||
fn valid_interval(s: &str) -> Result<u32, String> {
|
||||
clap_num::number_range(s, 100, 10000)
|
||||
}
|
||||
|
||||
fn valid_timeout(s: &str) -> Result<u32, String> {
|
||||
clap_num::number_range(s, 100, 5000)
|
||||
}
|
||||
|
||||
fn valid_data_size(s: &str) -> Result<usize, String> {
|
||||
clap_num::number_range(s, 4, 128)
|
||||
}
|
||||
|
||||
struct PingRouting {
|
||||
interface_id: u32,
|
||||
source_ip: IpAddr,
|
||||
destination_ip: IpAddr,
|
||||
source_mac: MacAddress,
|
||||
gateway_mac: MacAddress,
|
||||
}
|
||||
|
||||
struct PingStats {
|
||||
packets_sent: usize,
|
||||
packets_received: usize,
|
||||
}
|
||||
|
||||
fn resolve_routing(address: IpAddr) -> Result<PingRouting, Error> {
|
||||
let mut nc = NetConfig::open()?;
|
||||
let routing = nc.query_route(address)?;
|
||||
let Some(source) = routing.source else {
|
||||
todo!();
|
||||
};
|
||||
let Some(gateway) = routing.gateway else {
|
||||
todo!();
|
||||
};
|
||||
|
||||
let gateway_mac = nc.query_arp(routing.interface_id, gateway, true)?;
|
||||
|
||||
Ok(PingRouting {
|
||||
interface_id: routing.interface_id,
|
||||
source_ip: source,
|
||||
destination_ip: routing.destination,
|
||||
source_mac: routing.source_mac,
|
||||
gateway_mac,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_ping_reply(
|
||||
packet: &[u8],
|
||||
local: Ipv4Addr,
|
||||
remote: Ipv4Addr,
|
||||
expect_l4_data: &[u8],
|
||||
expect_id: u16,
|
||||
expect_seq: u16,
|
||||
) -> bool {
|
||||
if packet.len() < size_of::<EthernetFrame>() + size_of::<Ipv4Frame>() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let l3_offset = size_of::<EthernetFrame>();
|
||||
|
||||
let l2_frame: &EthernetFrame = bytemuck::from_bytes(&packet[..l3_offset]);
|
||||
|
||||
if EtherType::from_network_order(l2_frame.ethertype) != EtherType::IPV4 {
|
||||
return false;
|
||||
}
|
||||
let l3_frame: &Ipv4Frame =
|
||||
bytemuck::from_bytes(&packet[l3_offset..l3_offset + size_of::<Ipv4Frame>()]);
|
||||
if l3_frame.protocol != IpProtocol::ICMP
|
||||
|| u32::from_network_order(l3_frame.source_address) != u32::from(remote)
|
||||
|| u32::from_network_order(l3_frame.destination_address) != u32::from(local)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let mut ip_checksum = InetChecksum::new();
|
||||
ip_checksum.add_value(l3_frame, true);
|
||||
let ip_checksum = ip_checksum.finish();
|
||||
|
||||
if ip_checksum != 0 {
|
||||
eprintln!("IP checksum mismatch: {:#06x}", ip_checksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
let l4_offset = l3_offset + l3_frame.header_length();
|
||||
let l4_size = l3_frame
|
||||
.total_length()
|
||||
.saturating_sub(l3_frame.header_length());
|
||||
if packet.len() < l4_offset + size_of::<IcmpV4Frame>() + expect_l4_data.len() {
|
||||
return false;
|
||||
}
|
||||
let l4_frame: &IcmpV4Frame =
|
||||
bytemuck::from_bytes(&packet[l4_offset..l4_offset + size_of::<IcmpV4Frame>()]);
|
||||
let l4_data = &packet[l4_offset + size_of::<IcmpV4Frame>()..l4_offset + l4_size];
|
||||
|
||||
if l4_frame.ty != 0 || l4_frame.code != 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let rest = u32::from_network_order(l4_frame.rest);
|
||||
let reply_id = (rest >> 16) as u16;
|
||||
let reply_seq = rest as u16;
|
||||
|
||||
if reply_id != expect_id || reply_seq != expect_seq {
|
||||
eprintln!(
|
||||
"ICMP seq/id mismatch: sent {}/{}, got {}/{}",
|
||||
expect_id, expect_seq, reply_id, reply_seq
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut icmp_checksum = InetChecksum::new();
|
||||
icmp_checksum.add_value(l4_frame, true);
|
||||
icmp_checksum.add_bytes(l4_data, true);
|
||||
let icmp_checksum = icmp_checksum.finish();
|
||||
|
||||
if icmp_checksum != 0 {
|
||||
eprintln!("ICMP checksum mismatch: {:#06x}", icmp_checksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
l4_data == expect_l4_data
|
||||
}
|
||||
|
||||
fn ping_once(
|
||||
socket: &mut RawSocket,
|
||||
poll: &mut PollChannel,
|
||||
timer: &mut TimerFd,
|
||||
info: &PingRouting,
|
||||
timeout: Duration,
|
||||
data_len: usize,
|
||||
id: u16,
|
||||
seq: u16,
|
||||
) -> Result<bool, Error> {
|
||||
let mut buffer = [0; 4096];
|
||||
|
||||
let source_ip = info.source_ip.into_ipv4().unwrap();
|
||||
let destination_ip = info.destination_ip.into_ipv4().unwrap();
|
||||
let mut l4_data = vec![];
|
||||
|
||||
l4_data.reserve(data_len);
|
||||
for _ in 0..data_len {
|
||||
l4_data.push(rand::random());
|
||||
}
|
||||
|
||||
let ip_len = (size_of::<Ipv4Frame>() + size_of::<IcmpV4Frame>() + data_len)
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
let l2_frame = EthernetFrame {
|
||||
source_mac: info.source_mac,
|
||||
destination_mac: info.gateway_mac,
|
||||
ethertype: EtherType::IPV4.to_network_order(),
|
||||
};
|
||||
let mut l3_frame = Ipv4Frame {
|
||||
source_address: u32::from(source_ip).to_network_order(),
|
||||
destination_address: u32::from(destination_ip).to_network_order(),
|
||||
protocol: IpProtocol::ICMP,
|
||||
version_length: 0x45,
|
||||
total_length: u16::to_network_order(ip_len),
|
||||
flags_frag: u16::to_network_order(0x4000),
|
||||
id: u16::to_network_order(0),
|
||||
ttl: 255,
|
||||
..Ipv4Frame::zeroed()
|
||||
};
|
||||
let mut l4_frame = IcmpV4Frame {
|
||||
ty: 8,
|
||||
code: 0,
|
||||
checksum: u16::to_network_order(0),
|
||||
rest: u32::to_network_order(((id as u32) << 16) | (seq as u32)),
|
||||
};
|
||||
|
||||
let mut ip_checksum = InetChecksum::new();
|
||||
ip_checksum.add_value(&l3_frame, true);
|
||||
l3_frame.header_checksum = ip_checksum.finish().to_network_order();
|
||||
|
||||
let mut icmp_checksum = InetChecksum::new();
|
||||
icmp_checksum.add_value(&l4_frame, true);
|
||||
icmp_checksum.add_bytes(&l4_data, true);
|
||||
l4_frame.checksum = icmp_checksum.finish().to_network_order();
|
||||
|
||||
let mut packet = vec![];
|
||||
packet.extend_from_slice(bytemuck::bytes_of(&l2_frame));
|
||||
packet.extend_from_slice(bytemuck::bytes_of(&l3_frame));
|
||||
packet.extend_from_slice(bytemuck::bytes_of(&l4_frame));
|
||||
packet.extend_from_slice(&l4_data);
|
||||
|
||||
timer.start(timeout)?;
|
||||
socket.send(&packet)?;
|
||||
|
||||
loop {
|
||||
let (fd, result) = poll.wait(None)?.unwrap();
|
||||
result?;
|
||||
|
||||
match fd {
|
||||
fd if fd == socket.as_raw_fd() => {
|
||||
// TODO
|
||||
let len = socket.recv(&mut buffer)?;
|
||||
if validate_ping_reply(&buffer[..len], source_ip, destination_ip, &l4_data, id, seq)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
fd if fd == timer.as_raw_fd() => {
|
||||
return Ok(false);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ping(
|
||||
address: IpAddr,
|
||||
times: usize,
|
||||
data_len: usize,
|
||||
interval: Duration,
|
||||
timeout: Duration,
|
||||
) -> Result<PingStats, Error> {
|
||||
let routing = resolve_routing(address)?;
|
||||
|
||||
let mut stats = PingStats {
|
||||
packets_sent: 0,
|
||||
packets_received: 0,
|
||||
};
|
||||
let mut poll = PollChannel::new()?;
|
||||
let mut timer = TimerFd::new(false)?;
|
||||
let mut socket = RawSocket::bind(routing.interface_id)?;
|
||||
|
||||
poll.add(timer.as_raw_fd())?;
|
||||
poll.add(socket.as_raw_fd())?;
|
||||
|
||||
let id = rand::random();
|
||||
for i in 0..times {
|
||||
if INTERRUPTED.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
|
||||
let result = ping_once(
|
||||
&mut socket,
|
||||
&mut poll,
|
||||
&mut timer,
|
||||
&routing,
|
||||
timeout,
|
||||
data_len,
|
||||
id,
|
||||
i as u16,
|
||||
)?;
|
||||
stats.packets_sent += 1;
|
||||
|
||||
if result {
|
||||
stats.packets_received += 1;
|
||||
println!("[{}/{}] {}: PONG", i + 1, times, address);
|
||||
}
|
||||
|
||||
std::thread::sleep(interval);
|
||||
}
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
fn interrupt(_: Signal) {
|
||||
INTERRUPTED.store(true, Ordering::Release);
|
||||
}
|
||||
|
||||
static INTERRUPTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn main() -> ExitCode {
|
||||
set_signal_handler(Signal::Interrupted, SignalHandler::Function(interrupt));
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
let stats = match ping(
|
||||
args.address.into(),
|
||||
args.count,
|
||||
args.data_size,
|
||||
Duration::from_millis(args.inteval.into()),
|
||||
Duration::from_millis(args.timeout.into()),
|
||||
) {
|
||||
Ok(stats) => stats,
|
||||
Err(error) => {
|
||||
eprintln!("ping: {}", error);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let loss = (stats.packets_sent - stats.packets_received) * 100 / stats.packets_sent;
|
||||
println!(
|
||||
"{} sent, {} received, {}% loss",
|
||||
stats.packets_sent, stats.packets_received, loss
|
||||
);
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
424
userspace/red/Cargo.lock
generated
Normal file
424
userspace/red/Cargo.lock
generated
Normal file
@ -0,0 +1,424 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-chain"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"match_cfg",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
|
||||
[[package]]
|
||||
name = "match_cfg"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"libc",
|
||||
"syslog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.192"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.192"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syslog"
|
||||
version = "6.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7434e95bcccce1215d30f4bf84fe8c00e8de1b9be4fb736d747ca53d36e7f96f"
|
||||
dependencies = [
|
||||
"error-chain",
|
||||
"hostname",
|
||||
"libc",
|
||||
"log",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
18
userspace/red/Cargo.toml
Normal file
18
userspace/red/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "red"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
libterm = { path = "../lib/libterm" }
|
||||
|
||||
thiserror = "1.0.50"
|
||||
unicode-width = "0.1.11"
|
||||
|
||||
[target.'cfg(not(target_os = "yggdrasil"))'.dependencies]
|
||||
libc = "0.2.150"
|
||||
crossterm = "0.27.0"
|
||||
syslog = "6.1.0"
|
543
userspace/red/src/buffer/mod.rs
Normal file
543
userspace/red/src/buffer/mod.rs
Normal file
@ -0,0 +1,543 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fmt::Write as FmtWrite,
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, BufWriter, Write as IoWrite},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use libterm::{Color, CursorStyle, Term};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
error::Error,
|
||||
line::{Line, TextLike},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct View {
|
||||
cursor_column: usize,
|
||||
cursor_row: usize,
|
||||
column_offset: usize,
|
||||
row_offset: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
offset_x: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum Mode {
|
||||
Normal,
|
||||
Insert,
|
||||
}
|
||||
|
||||
pub enum SetMode {
|
||||
Normal,
|
||||
InsertBefore,
|
||||
InsertAfter,
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
lines: Vec<Line>,
|
||||
dirty: bool,
|
||||
mode_dirty: bool,
|
||||
mode: Mode,
|
||||
view: View,
|
||||
name: Option<String>,
|
||||
path: Option<PathBuf>,
|
||||
modified: bool,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Normal => "NORMAL",
|
||||
Self::Insert => "INSERT",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn set_row(&mut self, row: usize) {
|
||||
self.cursor_row = row;
|
||||
|
||||
if self.cursor_row < self.row_offset {
|
||||
self.row_offset = self.cursor_row;
|
||||
} else if self.cursor_row >= self.row_offset + self.height {
|
||||
self.row_offset = self.cursor_row - self.height + 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_column(&mut self, config: &Config, col: usize, line: Option<&Line>) {
|
||||
let Some(line) = line else {
|
||||
self.column_offset = 0;
|
||||
self.cursor_column = 0;
|
||||
return;
|
||||
};
|
||||
|
||||
self.cursor_column = col;
|
||||
|
||||
if line.display_width(config.tab_width) < self.width {
|
||||
self.column_offset = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
let width_to_cursor = line
|
||||
.span(..self.cursor_column)
|
||||
.display_width(config.tab_width);
|
||||
|
||||
if width_to_cursor < self.column_offset {
|
||||
self.column_offset = width_to_cursor;
|
||||
} else if width_to_cursor >= self.column_offset + self.width {
|
||||
self.column_offset = width_to_cursor - self.width + 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.column_offset = 0;
|
||||
self.row_offset = 0;
|
||||
self.cursor_row = 0;
|
||||
self.cursor_column = 0;
|
||||
}
|
||||
|
||||
// pub fn resize(&mut self, width: usize, height: usize) {
|
||||
// self.width = width;
|
||||
// self.height = height;
|
||||
|
||||
// self.column_offset = 0;
|
||||
// self.row_offset = 0;
|
||||
// }
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
lines: vec![],
|
||||
dirty: true,
|
||||
mode_dirty: true,
|
||||
view: View::default(),
|
||||
mode: Mode::Normal,
|
||||
name: None,
|
||||
path: None,
|
||||
modified: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_lines<P: AsRef<Path>>(path: P) -> io::Result<Vec<Line>> {
|
||||
let input = BufReader::new(File::open(path)?);
|
||||
let lines = input.lines().collect::<Result<Vec<_>, _>>()?;
|
||||
let lines = lines
|
||||
.into_iter()
|
||||
.map(|line| Line::from_str(line.trim_end_matches('\n')))
|
||||
.collect();
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let name = path.file_name().and_then(OsStr::to_str).map(String::from);
|
||||
let lines = if path.exists() {
|
||||
Self::read_lines(path)?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
lines,
|
||||
name,
|
||||
path: Some(path.into()),
|
||||
mode: Mode::Normal,
|
||||
dirty: true,
|
||||
mode_dirty: true,
|
||||
view: View::default(),
|
||||
modified: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reopen<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let name = path.file_name().and_then(OsStr::to_str).map(String::from);
|
||||
let lines = if path.exists() {
|
||||
Self::read_lines(path)?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
self.lines = lines;
|
||||
self.modified = false;
|
||||
self.mode = Mode::Normal;
|
||||
self.dirty = true;
|
||||
self.mode_dirty = true;
|
||||
self.view.reset();
|
||||
|
||||
self.path = Some(path.into());
|
||||
self.name = name;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Result<(), Error> {
|
||||
let path = self.path.as_ref().ok_or(Error::NoPath)?;
|
||||
let mut writer = BufWriter::new(File::create(path).map_err(Error::WriteError)?);
|
||||
|
||||
for line in self.lines.iter() {
|
||||
writer
|
||||
.write_all(line.to_string().as_ref())
|
||||
.map_err(Error::WriteError)?;
|
||||
writer.write_all(b"\n").map_err(Error::WriteError)?;
|
||||
}
|
||||
|
||||
self.modified = false;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_path<P: AsRef<Path>>(&mut self, path: P) {
|
||||
let path = PathBuf::from(path.as_ref());
|
||||
let name = path.file_name().and_then(OsStr::to_str).map(String::from);
|
||||
|
||||
self.path = Some(path);
|
||||
self.name = name;
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, config: &Config, mode: SetMode) {
|
||||
let dst_mode = match mode {
|
||||
SetMode::Normal => Mode::Normal,
|
||||
SetMode::InsertAfter | SetMode::InsertBefore => Mode::Insert,
|
||||
};
|
||||
|
||||
if dst_mode == self.mode {
|
||||
return;
|
||||
}
|
||||
|
||||
self.mode = dst_mode;
|
||||
self.mode_dirty = true;
|
||||
match mode {
|
||||
SetMode::Normal => self.move_cursor(config, -1, 0),
|
||||
SetMode::InsertBefore => self.move_cursor(config, 0, 0),
|
||||
SetMode::InsertAfter => self.move_cursor(config, 1, 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<&String> {
|
||||
self.name.as_ref()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<&PathBuf> {
|
||||
self.path.as_ref()
|
||||
}
|
||||
|
||||
pub fn row_offset(&self) -> usize {
|
||||
self.view.row_offset
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.lines.is_empty()
|
||||
}
|
||||
|
||||
pub fn cursor_row(&self) -> usize {
|
||||
self.view.cursor_row
|
||||
}
|
||||
|
||||
pub fn set_position(&mut self, config: &Config, px: usize, py: usize) {
|
||||
self.dirty = true;
|
||||
|
||||
if self.lines.is_empty() {
|
||||
self.view.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to row
|
||||
self.view.set_row(py.min(self.lines.len() - 1));
|
||||
|
||||
// Set mode- and line-len-adjusted column
|
||||
if let Some(line) = self.lines.get(self.view.cursor_row)
|
||||
&& !line.is_empty()
|
||||
{
|
||||
match self.mode {
|
||||
// Limited by line.len()
|
||||
Mode::Normal => self
|
||||
.view
|
||||
.set_column(config, px.min(line.len() - 1), Some(line)),
|
||||
Mode::Insert => self.view.set_column(config, px.min(line.len()), Some(line)),
|
||||
}
|
||||
} else {
|
||||
self.view.set_column(config, 0, None);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_line_end(&mut self, config: &Config) {
|
||||
let len = self
|
||||
.lines
|
||||
.get(self.view.cursor_row)
|
||||
.map(Line::len)
|
||||
.unwrap_or(0);
|
||||
|
||||
self.set_position(config, len, self.view.cursor_row);
|
||||
}
|
||||
|
||||
pub fn to_first_line(&mut self, config: &Config) {
|
||||
let len = self
|
||||
.lines
|
||||
.get(self.view.cursor_row)
|
||||
.map(Line::len)
|
||||
.unwrap_or(0);
|
||||
|
||||
self.set_position(config, len, 0);
|
||||
}
|
||||
|
||||
pub fn to_last_line(&mut self, config: &Config) {
|
||||
if self.lines.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let len = self
|
||||
.lines
|
||||
.get(self.view.cursor_row)
|
||||
.map(Line::len)
|
||||
.unwrap_or(0);
|
||||
|
||||
self.set_position(config, len, self.lines.len() - 1);
|
||||
}
|
||||
|
||||
pub fn set_column(&mut self, config: &Config, x: usize) {
|
||||
self.set_position(config, x, self.view.cursor_row);
|
||||
}
|
||||
|
||||
pub fn move_cursor(&mut self, config: &Config, dx: isize, dy: isize) {
|
||||
let px = (self.view.cursor_column as isize + dx).max(0) as usize;
|
||||
let py = (self.view.cursor_row as isize + dy).max(0) as usize;
|
||||
|
||||
self.set_position(config, px, py);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, config: &Config, offset_x: usize, width: usize, height: usize) {
|
||||
self.dirty = true;
|
||||
self.view.height = height;
|
||||
self.view.width = width;
|
||||
self.view.offset_x = offset_x;
|
||||
|
||||
self.view.set_row(self.view.cursor_row);
|
||||
self.view.set_column(
|
||||
config,
|
||||
self.view.cursor_column,
|
||||
self.lines.get(self.view.cursor_row),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn display_cursor(&self, config: &Config) -> (usize, usize) {
|
||||
if self.lines.is_empty() {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// assert!(self.view.column_offset <= self.view.cursor_column);
|
||||
assert!(self.view.row_offset <= self.view.cursor_row);
|
||||
|
||||
let line = &self.lines[self.view.cursor_row];
|
||||
assert!(self.view.cursor_column <= line.len());
|
||||
|
||||
let column = line
|
||||
.span(..self.view.cursor_column)
|
||||
.display_width(config.tab_width);
|
||||
assert!(self.view.column_offset <= column);
|
||||
|
||||
(
|
||||
column - self.view.column_offset,
|
||||
self.view.cursor_row - self.view.row_offset,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.view.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.view.height
|
||||
}
|
||||
|
||||
pub fn is_modified(&self) -> bool {
|
||||
self.modified
|
||||
}
|
||||
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
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;
|
||||
|
||||
for &ch in span.iter() {
|
||||
if pos >= self.view.width {
|
||||
break;
|
||||
}
|
||||
|
||||
if ch == '\t' {
|
||||
let old_pos = pos;
|
||||
let new_pos = (pos + config.tab_width) & !(config.tab_width - 1);
|
||||
pos = new_pos;
|
||||
|
||||
for i in old_pos..new_pos {
|
||||
if i >= self.view.width {
|
||||
break;
|
||||
}
|
||||
if i == old_pos {
|
||||
term.set_foreground(Color::Blue)?;
|
||||
term.write_char('>').map_err(Error::TerminalFmtError)?;
|
||||
term.set_foreground(Color::Default)?;
|
||||
} else {
|
||||
term.write_char(' ').map_err(Error::TerminalFmtError)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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.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) -> Result<(), Error> {
|
||||
for (row, line) in self
|
||||
.lines
|
||||
.iter()
|
||||
.skip(self.view.row_offset)
|
||||
.take(self.view.height)
|
||||
.enumerate()
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
pub fn newline_after(&mut self, break_line: bool) {
|
||||
if self.lines.is_empty() {
|
||||
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)
|
||||
} else {
|
||||
Line::new()
|
||||
};
|
||||
self.lines.insert(self.view.cursor_row + 1, newline);
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, config: &Config, ch: char) {
|
||||
if self.lines.is_empty() {
|
||||
assert_eq!(self.view.cursor_row, 0);
|
||||
self.lines.push(Line::new());
|
||||
}
|
||||
|
||||
let line = &mut self.lines[self.view.cursor_row];
|
||||
line.insert(self.view.cursor_column, ch);
|
||||
self.move_cursor(config, 1, 0);
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn erase_backward(&mut self, config: &Config) {
|
||||
if self.lines.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.view.cursor_column == 0 {
|
||||
if self.view.cursor_row != 0 {
|
||||
let line = self.lines.remove(self.view.cursor_row);
|
||||
let prev_line = &mut self.lines[self.view.cursor_row - 1];
|
||||
|
||||
let len = prev_line.len();
|
||||
prev_line.extend(line);
|
||||
self.set_position(config, len, self.view.cursor_row - 1);
|
||||
self.modified = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let line = &mut self.lines[self.view.cursor_row];
|
||||
line.remove(self.view.cursor_column - 1);
|
||||
self.move_cursor(config, -1, 0);
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn erase_forward(&mut self) {
|
||||
if self.lines.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let line = &mut self.lines[self.view.cursor_row];
|
||||
if self.view.cursor_column == line.len() {
|
||||
return;
|
||||
}
|
||||
line.remove(self.view.cursor_column);
|
||||
self.dirty = true;
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn kill_line(&mut self, config: &Config) {
|
||||
if self.lines.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.lines.remove(self.view.cursor_row);
|
||||
self.move_cursor(config, 0, 1);
|
||||
self.modified = true;
|
||||
}
|
||||
|
||||
pub fn number_width(&mut self) -> usize {
|
||||
if self.lines.is_empty() {
|
||||
1
|
||||
} else {
|
||||
self.lines.len().ilog10() as usize + 1
|
||||
}
|
||||
}
|
||||
}
|
137
userspace/red/src/command.rs
Normal file
137
userspace/red/src/command.rs
Normal file
@ -0,0 +1,137 @@
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crate::{error::Error, State, buffer::{Buffer, SetMode}, config::Config};
|
||||
|
||||
pub type CommandFn = fn(&mut State, &[&str]) -> Result<(), Error>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Action {
|
||||
// Editing
|
||||
EraseBackward,
|
||||
InsertBefore,
|
||||
InsertAfter,
|
||||
NewlineBefore,
|
||||
NewlineAfter,
|
||||
BreakLine,
|
||||
KillLine,
|
||||
|
||||
// Movement
|
||||
MoveFirstLine,
|
||||
MoveLastLine,
|
||||
MoveCharPrev,
|
||||
MoveCharNext,
|
||||
MoveLineBack(usize),
|
||||
MoveLineForward(usize),
|
||||
MoveLineStart,
|
||||
MoveLineEnd,
|
||||
}
|
||||
|
||||
static COMMANDS: &[(&str, RangeInclusive<usize>, CommandFn)] = &[
|
||||
("w", 0..=1, cmd_write),
|
||||
("w!", 0..=1, cmd_force_write),
|
||||
("q", 0..=0, cmd_exit),
|
||||
("q!", 0..=0, cmd_force_exit),
|
||||
("e", 1..=1, cmd_edit),
|
||||
("e!", 0..=1, cmd_force_edit),
|
||||
];
|
||||
|
||||
// Commands
|
||||
fn cmd_write(state: &mut State, args: &[&str]) -> Result<(), Error> {
|
||||
if args.len() == 1 && state.buffer().is_modified() && state.buffer().path().is_some() {
|
||||
return Err(Error::UnsavedBuffer("Use :w! FILE to force write to another file"));
|
||||
}
|
||||
|
||||
cmd_force_write(state, args)
|
||||
}
|
||||
|
||||
fn cmd_force_write(state: &mut State, args: &[&str]) -> Result<(), Error> {
|
||||
let buffer = state.buffer_mut();
|
||||
if let Some(&path) = args.first() {
|
||||
buffer.set_path(path);
|
||||
}
|
||||
|
||||
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> {
|
||||
if state.buffer().is_modified() {
|
||||
return Err(Error::UnsavedBuffer("Use :e! [FILE] to open another file"));
|
||||
}
|
||||
|
||||
cmd_force_edit(state, args)
|
||||
}
|
||||
|
||||
fn cmd_force_edit(state: &mut State, args: &[&str]) -> Result<(), Error> {
|
||||
if let Some(&path) = args.first() {
|
||||
state.buffer_mut().reopen(path).map_err(Error::OpenError)
|
||||
} else if let Some(path) = state.buffer().path().cloned() {
|
||||
state.buffer_mut().reopen(path).map_err(Error::OpenError)
|
||||
} else {
|
||||
Err(Error::NoPath)
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_exit(state: &mut State, _args: &[&str]) -> Result<(), Error> {
|
||||
let buffer = state.buffer();
|
||||
|
||||
if buffer.is_modified() {
|
||||
return Err(Error::UnsavedBuffer("Use :q! to force exit"));
|
||||
}
|
||||
|
||||
state.exit();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_force_exit(state: &mut State, _args: &[&str]) -> Result<(), Error> {
|
||||
state.exit();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn execute(state: &mut State, command: String) -> Result<(), Error> {
|
||||
let words = command.split(' ').collect::<Vec<_>>();
|
||||
let Some((&cmd, args)) = words.split_first() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for (name, nargs, f) in COMMANDS.iter() {
|
||||
if *name == cmd {
|
||||
if !nargs.contains(&args.len()) {
|
||||
todo!();
|
||||
}
|
||||
|
||||
return f(state, args);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::UnknownCommand(cmd.into()))
|
||||
}
|
||||
|
||||
pub fn perform(buffer: &mut Buffer, config: &Config, action: Action) -> Result<(), Error> {
|
||||
match action {
|
||||
// Editing
|
||||
Action::EraseBackward => buffer.erase_backward(config),
|
||||
Action::InsertBefore => buffer.set_mode(config, SetMode::InsertBefore),
|
||||
Action::InsertAfter => buffer.set_mode(config, SetMode::InsertAfter),
|
||||
Action::NewlineBefore => buffer.newline_before(),
|
||||
Action::NewlineAfter => buffer.newline_after(false),
|
||||
Action::BreakLine => buffer.newline_after(true),
|
||||
Action::KillLine => buffer.kill_line(config),
|
||||
// Movement
|
||||
Action::MoveCharPrev => buffer.move_cursor(config, -1, 0),
|
||||
Action::MoveCharNext => buffer.move_cursor(config, 1, 0),
|
||||
Action::MoveLineBack(count) => buffer.move_cursor(config, 0, -(count as isize)),
|
||||
Action::MoveLineForward(count) => buffer.move_cursor(config, 0, count as isize),
|
||||
Action::MoveLineStart => buffer.set_column(config, 0),
|
||||
Action::MoveLineEnd => buffer.to_line_end(config),
|
||||
Action::MoveFirstLine => buffer.to_first_line(config),
|
||||
Action::MoveLastLine => buffer.to_last_line(config),
|
||||
}
|
||||
Ok(())
|
||||
}
|
66
userspace/red/src/config.rs
Normal file
66
userspace/red/src/config.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use crate::{
|
||||
buffer::Mode,
|
||||
command::Action,
|
||||
keymap::{bindn, bind1, KeyMap, KeySeq, PrefixNode},
|
||||
};
|
||||
|
||||
pub struct Config {
|
||||
// TODO must be a power of 2, lol
|
||||
pub tab_width: usize,
|
||||
pub number: bool,
|
||||
|
||||
pub nmap: KeyMap,
|
||||
pub imap: KeyMap,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
use Action::*;
|
||||
|
||||
let nmap = KeyMap::from_iter([
|
||||
bind1('i', [InsertBefore]),
|
||||
bind1('a', [InsertAfter]),
|
||||
bind1('h', [MoveCharPrev]),
|
||||
bind1('l', [MoveCharNext]),
|
||||
bind1('j', [MoveLineForward(1)]),
|
||||
bind1('J', [MoveLineForward(25)]),
|
||||
bind1('k', [MoveLineBack(1)]),
|
||||
bind1('K', [MoveLineBack(25)]),
|
||||
bindn(['g', 'g'], [MoveFirstLine]),
|
||||
bind1('G', [MoveLastLine]),
|
||||
bind1('I', [MoveLineStart, InsertBefore]),
|
||||
bind1('A', [MoveLineEnd, InsertAfter]),
|
||||
bind1('o', [NewlineAfter, MoveLineForward(1), InsertBefore]),
|
||||
bind1('O', [NewlineBefore, MoveLineBack(1), InsertBefore]),
|
||||
bindn(['d', 'd'], [KillLine]),
|
||||
]);
|
||||
|
||||
let imap = KeyMap::from_iter([
|
||||
bind1('\x7F', [EraseBackward]),
|
||||
bind1(
|
||||
'\n',
|
||||
[BreakLine, MoveLineForward(1), MoveLineStart, InsertBefore],
|
||||
),
|
||||
bind1(
|
||||
'\x0D',
|
||||
[BreakLine, MoveLineForward(1), MoveLineStart, InsertBefore],
|
||||
),
|
||||
]);
|
||||
|
||||
Self {
|
||||
tab_width: 4,
|
||||
number: true,
|
||||
nmap,
|
||||
imap,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn key_seq(&self, mode: Mode, seq: &KeySeq) -> Option<&PrefixNode<KeySeq, Vec<Action>>> {
|
||||
match mode {
|
||||
Mode::Normal => self.nmap.get(seq),
|
||||
Mode::Insert => self.imap.get(seq),
|
||||
}
|
||||
}
|
||||
}
|
22
userspace/red/src/error.rs
Normal file
22
userspace/red/src/error.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use std::{fmt, io};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
// I/O errors
|
||||
#[error("Could not open file: {0}")]
|
||||
OpenError(io::Error),
|
||||
#[error("Could not write file: {0}")]
|
||||
WriteError(io::Error),
|
||||
#[error("Buffer does not have a path")]
|
||||
NoPath,
|
||||
#[error("Buffer has unsaved changes: {0}")]
|
||||
UnsavedBuffer(&'static str),
|
||||
#[error("Invalid command, usage: {0}")]
|
||||
InvalidCommand(&'static str),
|
||||
#[error("Unknown command: `{0}`")]
|
||||
UnknownCommand(String),
|
||||
#[error("Terminal error: {0}")]
|
||||
TerminalError(#[from] libterm::Error),
|
||||
#[error("Terminal error: {0:?}")]
|
||||
TerminalFmtError(fmt::Error),
|
||||
}
|
50
userspace/red/src/keymap/key.rs
Normal file
50
userspace/red/src/keymap/key.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
use libterm::TermKey;
|
||||
|
||||
use super::map::Prefix;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct KeySeq(Vec<TermKey>);
|
||||
|
||||
impl KeySeq {
|
||||
pub fn one<I: Into<TermKey>>(k: I) -> Self {
|
||||
Self(vec![k.into()])
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
pub fn push<I: Into<TermKey>>(&mut self, k: I) {
|
||||
self.0.push(k.into());
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Prefix for KeySeq {
|
||||
fn prefix(&self) -> Option<Self> {
|
||||
self.0.split_last().map(|(_, vs)| vs.to_vec()).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Into<TermKey>, I: IntoIterator<Item = K>> From<I> for KeySeq {
|
||||
fn from(value: I) -> Self {
|
||||
Self(value.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for KeySeq {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (i, key) in self.0.iter().enumerate() {
|
||||
if i != 0 {
|
||||
f.write_char('+')?;
|
||||
}
|
||||
fmt::Display::fmt(key, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
100
userspace/red/src/keymap/map.rs
Normal file
100
userspace/red/src/keymap/map.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
pub trait Prefix: Sized + Eq + Hash + Clone {
|
||||
fn prefix(&self) -> Option<Self>;
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum PrefixNode<K: Prefix, V> {
|
||||
Prefix(HashSet<K>),
|
||||
Leaf(V),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PrefixMap<K: Prefix, V> {
|
||||
map: HashMap<K, PrefixNode<K, V>>,
|
||||
}
|
||||
|
||||
impl<K: Prefix, V> PrefixNode<K, V> {
|
||||
pub fn empty_prefix() -> Self {
|
||||
Self::Prefix(HashSet::new())
|
||||
}
|
||||
|
||||
pub fn add_suffix(&mut self, suffix: K) {
|
||||
if let Self::Prefix(set) = self {
|
||||
set.insert(suffix);
|
||||
} else {
|
||||
*self = Self::Prefix(HashSet::from_iter([suffix]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Prefix + fmt::Debug, V: fmt::Debug> fmt::Debug for PrefixNode<K, V> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Leaf(leaf) => f.debug_struct("Leaf").field("value", leaf).finish(),
|
||||
Self::Prefix(suffixes) => f.debug_struct("Prefix").field("suffixes", suffixes).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Prefix, V> PrefixMap<K, V> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: K, value: V) {
|
||||
if let Some(prefix) = key.prefix() {
|
||||
self.map
|
||||
.entry(prefix)
|
||||
.or_insert_with(PrefixNode::empty_prefix)
|
||||
.add_suffix(key.clone());
|
||||
}
|
||||
|
||||
// TODO remove all suffixes of `key`, if those exist
|
||||
self.map.insert(key, PrefixNode::Leaf(value));
|
||||
}
|
||||
|
||||
pub fn get<N>(&self, key: &N) -> Option<&PrefixNode<K, V>>
|
||||
where
|
||||
K: Borrow<N>,
|
||||
N: Eq + Hash + ?Sized,
|
||||
{
|
||||
self.map.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Prefix, V> FromIterator<(K, V)> for PrefixMap<K, V> {
|
||||
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
|
||||
let mut this = Self::new();
|
||||
for (k, v) in iter {
|
||||
this.insert(k, v);
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Prefix + fmt::Debug, V: fmt::Debug> fmt::Debug for PrefixMap<K, V> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut dm = f.debug_map();
|
||||
|
||||
for (k, v) in self.map.iter().filter_map(|(k, v)| {
|
||||
if let PrefixNode::Leaf(leaf) = v {
|
||||
Some((k, leaf))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
dm.entry(k, v);
|
||||
}
|
||||
|
||||
dm.finish()
|
||||
}
|
||||
}
|
61
userspace/red/src/keymap/mod.rs
Normal file
61
userspace/red/src/keymap/mod.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use std::{borrow::Borrow, hash::Hash};
|
||||
|
||||
use crate::command::Action;
|
||||
|
||||
use self::map::PrefixMap;
|
||||
pub use self::map::PrefixNode;
|
||||
|
||||
mod key;
|
||||
mod map;
|
||||
|
||||
pub use key::KeySeq;
|
||||
use libterm::TermKey;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeyMap {
|
||||
map: PrefixMap<KeySeq, Vec<Action>>,
|
||||
}
|
||||
|
||||
impl KeyMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
map: PrefixMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get<N>(&self, key: &N) -> Option<&PrefixNode<KeySeq, Vec<Action>>>
|
||||
where
|
||||
KeySeq: Borrow<N>,
|
||||
N: Eq + Hash + ?Sized,
|
||||
{
|
||||
self.map.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(KeySeq, Vec<Action>)> for KeyMap {
|
||||
fn from_iter<T: IntoIterator<Item = (KeySeq, Vec<Action>)>>(iter: T) -> Self {
|
||||
Self {
|
||||
map: PrefixMap::from_iter(iter),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bindn<I: Into<KeySeq>, V: IntoIterator<Item = Action>>(
|
||||
key: I,
|
||||
actions: V,
|
||||
) -> (KeySeq, Vec<Action>) {
|
||||
(key.into(), actions.into_iter().collect())
|
||||
}
|
||||
|
||||
pub fn bind1<I: Into<TermKey>, V: IntoIterator<Item = Action>>(
|
||||
key: I,
|
||||
actions: V,
|
||||
) -> (KeySeq, Vec<Action>) {
|
||||
(KeySeq::one(key), actions.into_iter().collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn from_iter() {}
|
||||
}
|
229
userspace/red/src/line.rs
Normal file
229
userspace/red/src/line.rs
Normal file
@ -0,0 +1,229 @@
|
||||
use std::{ops::Index, slice::SliceIndex};
|
||||
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub struct Line {
|
||||
data: Vec<char>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Span<'a>(&'a [char]);
|
||||
|
||||
pub trait TextLike: Index<usize, Output = char> + ToString {
|
||||
type Iter<'a>: Iterator<Item = &'a char>
|
||||
where
|
||||
Self: 'a;
|
||||
type Span<'a>: TextLike + 'a
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
fn display_width(&self, tab_width: usize) -> usize;
|
||||
fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_>;
|
||||
fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_>;
|
||||
|
||||
fn iter(&self) -> Self::Iter<'_>;
|
||||
}
|
||||
|
||||
// Line
|
||||
impl Line {
|
||||
pub fn new() -> Self {
|
||||
Self { data: vec![] }
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_str<S: AsRef<str>>(s: S) -> Self {
|
||||
let chars = s.as_ref().chars();
|
||||
Self {
|
||||
data: Vec::from_iter(chars),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_span(&self) -> Span {
|
||||
Span(self.data.as_ref())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.is_empty()
|
||||
}
|
||||
|
||||
pub fn split_off(&mut self, at: usize) -> Line {
|
||||
let data = self.data.split_off(at);
|
||||
Line { data }
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, at: usize, ch: char) {
|
||||
self.data.insert(at, ch);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, at: usize) {
|
||||
self.data.remove(at);
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, other: Line) {
|
||||
self.data.extend(other.data);
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Line {
|
||||
type Item = char;
|
||||
type IntoIter = std::vec::IntoIter<char>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.data.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl TextLike for Line {
|
||||
type Span<'a> = Span<'a>;
|
||||
type Iter<'a> = std::slice::Iter<'a, char>;
|
||||
|
||||
fn display_width(&self, tab_width: usize) -> usize {
|
||||
self.as_span().display_width(tab_width)
|
||||
}
|
||||
|
||||
fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_> {
|
||||
self.as_span().span(range)
|
||||
}
|
||||
|
||||
fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_> {
|
||||
self.as_span().skip_to_width(offset, tab_width)
|
||||
}
|
||||
|
||||
fn iter(&self) -> Self::Iter<'_> {
|
||||
self.data.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<usize> for Line {
|
||||
type Output = char;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.data[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Line {
|
||||
fn to_string(&self) -> String {
|
||||
self.as_span().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// Span
|
||||
impl<'s> TextLike for Span<'s> {
|
||||
type Iter<'a> = std::slice::Iter<'a, char> where 's: 'a;
|
||||
type Span<'a> = Span<'s> where 's: 'a;
|
||||
|
||||
fn display_width(&self, tab_width: usize) -> usize {
|
||||
self.0.iter().fold(0, |pos, &ch| match ch {
|
||||
'\t' => (pos + tab_width) & !(tab_width - 1),
|
||||
_ => pos + ch.width().unwrap_or(1),
|
||||
})
|
||||
}
|
||||
|
||||
fn span<R: SliceIndex<[char], Output = [char]>>(&self, range: R) -> Self::Span<'_> {
|
||||
Span(&self.0[range])
|
||||
}
|
||||
|
||||
fn skip_to_width(&self, offset: usize, tab_width: usize) -> Self::Span<'_> {
|
||||
let mut index = 0;
|
||||
let mut pos = 0;
|
||||
for &ch in self.0.iter() {
|
||||
if pos >= offset {
|
||||
break;
|
||||
}
|
||||
match ch {
|
||||
'\t' => pos = (pos + tab_width) & !(tab_width - 1),
|
||||
_ => pos += ch.width().unwrap_or(1),
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
self.span(index..)
|
||||
}
|
||||
|
||||
fn iter(&self) -> Self::Iter<'_> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<usize> for Span<'_> {
|
||||
type Output = char;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Span<'_> {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::line::{Span, TextLike};
|
||||
|
||||
use super::Line;
|
||||
|
||||
#[test]
|
||||
fn line_from_str() {
|
||||
// pure ASCII
|
||||
let text = "abc123\n\t xyz";
|
||||
let line = Line::from_str(text);
|
||||
assert_eq!(
|
||||
line.data,
|
||||
vec!['a', 'b', 'c', '1', '2', '3', '\n', '\t', ' ', 'x', 'y', 'z']
|
||||
);
|
||||
|
||||
// cyrillic unicode
|
||||
let text = "це тест123";
|
||||
let line = Line::from_str(text);
|
||||
assert_eq!(
|
||||
line.data,
|
||||
vec!['ц', 'е', ' ', 'т', 'е', 'с', 'т', '1', '2', '3']
|
||||
);
|
||||
|
||||
// japanese unicode
|
||||
let text = "1日本2";
|
||||
let line = Line::from_str(text);
|
||||
assert_eq!(line.data, vec!['1', '日', '本', '2']);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_to_string() {
|
||||
let line = Line {
|
||||
data: vec!['a', 'b', 'c', 'т', 'е', 'с', 'т', '1', '2', '3', '\n'],
|
||||
};
|
||||
assert_eq!(line.to_string().as_str(), "abcтест123\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_span() {
|
||||
// All span
|
||||
let line = Line::from_str("abcdef");
|
||||
assert_eq!(line.as_span(), Span(&['a', 'b', 'c', 'd', 'e', 'f']));
|
||||
|
||||
assert_eq!(line.span(..3), Span(&['a', 'b', 'c']));
|
||||
assert_eq!(line.span(..=3), Span(&['a', 'b', 'c', 'd']));
|
||||
|
||||
assert_eq!(line.span(..=3).span(2..), Span(&['c', 'd']));
|
||||
assert_eq!(line.span(2..=3), Span(&['c', 'd']));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_width() {
|
||||
// No tabs
|
||||
let line = Line::from_str("abcdef");
|
||||
assert_eq!(line.display_width(4), line.len());
|
||||
|
||||
// Tabs
|
||||
let line = Line::from_str("\ta\tbcdef");
|
||||
assert_eq!(line.display_width(4), 8 + 5);
|
||||
}
|
||||
}
|
371
userspace/red/src/main.rs
Normal file
371
userspace/red/src/main.rs
Normal file
@ -0,0 +1,371 @@
|
||||
#![feature(let_chains, rustc_private)]
|
||||
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))]
|
||||
#![allow(clippy::new_without_default)]
|
||||
|
||||
use std::{env, fmt::Write, path::Path};
|
||||
|
||||
use libterm::{Clear, Color, Term, TermKey};
|
||||
|
||||
use buffer::{Buffer, Mode, SetMode};
|
||||
use config::Config;
|
||||
use error::Error;
|
||||
use keymap::{KeySeq, PrefixNode};
|
||||
|
||||
pub mod buffer;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod keymap;
|
||||
pub mod line;
|
||||
pub mod term;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum TopMode {
|
||||
Normal,
|
||||
Command,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
term: Term,
|
||||
buffer: Buffer,
|
||||
command: String,
|
||||
message: Option<String>,
|
||||
status: Option<String>,
|
||||
key_seq: KeySeq,
|
||||
top_mode: TopMode,
|
||||
config: Config,
|
||||
running: bool,
|
||||
number_width: usize,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn open<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
|
||||
let config = Config::default();
|
||||
let mut buffer = match path {
|
||||
Some(path) => Buffer::open(path).unwrap(),
|
||||
None => Buffer::empty(),
|
||||
};
|
||||
let term = Term::open()?;
|
||||
|
||||
let (w, h) = term.size()?;
|
||||
if config.number {
|
||||
let nw = buffer.number_width() + 2;
|
||||
buffer.resize(&config, nw, w - nw - 1, h - 2);
|
||||
} else {
|
||||
buffer.resize(&config, 0, w - 1, h - 2);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
number_width: buffer.number_width(),
|
||||
top_mode: TopMode::Normal,
|
||||
message: None,
|
||||
status: None,
|
||||
command: String::new(),
|
||||
key_seq: KeySeq::empty(),
|
||||
running: true,
|
||||
buffer,
|
||||
term,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn buffer_mut(&mut self) -> &mut Buffer {
|
||||
&mut self.buffer
|
||||
}
|
||||
|
||||
pub fn exit(&mut self) {
|
||||
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)?;
|
||||
|
||||
if i + start == self.buffer.cursor_row() {
|
||||
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)
|
||||
.map_err(Error::TerminalFmtError)?;
|
||||
}
|
||||
|
||||
if i == self.buffer.height() {
|
||||
break;
|
||||
}
|
||||
|
||||
if i + start == self.buffer.cursor_row() {
|
||||
self.term.reset_style()?;
|
||||
}
|
||||
}
|
||||
|
||||
self.term.reset_style()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_modeline(&mut self) -> Result<(), Error> {
|
||||
self.term.set_cursor_position(self.buffer.height(), 0)?;
|
||||
|
||||
let bg = match (self.top_mode, self.buffer.mode()) {
|
||||
(TopMode::Normal, Mode::Normal) => Color::Yellow,
|
||||
(TopMode::Normal, Mode::Insert) => Color::Cyan,
|
||||
(TopMode::Command, _) => Color::Green,
|
||||
};
|
||||
|
||||
self.term.set_background(bg)?;
|
||||
self.term.set_foreground(Color::Black)?;
|
||||
|
||||
match self.top_mode {
|
||||
TopMode::Normal => {
|
||||
write!(self.term, " {} ", self.buffer.mode().as_str())
|
||||
.map_err(Error::TerminalFmtError)?;
|
||||
|
||||
if self.buffer.is_modified() {
|
||||
self.term.set_background(Color::Magenta)?;
|
||||
self.term.set_foreground(Color::Default)?;
|
||||
} else {
|
||||
self.term.set_foreground(Color::Green)?;
|
||||
self.term.set_background(Color::Default)?;
|
||||
}
|
||||
}
|
||||
TopMode::Command => {
|
||||
write!(self.term, " COMMAND ").map_err(Error::TerminalFmtError)?;
|
||||
|
||||
self.term.set_foreground(Color::Green)?;
|
||||
self.term.set_background(Color::Default)?;
|
||||
}
|
||||
}
|
||||
|
||||
let name = self
|
||||
.buffer
|
||||
.name()
|
||||
.map(String::as_str)
|
||||
.unwrap_or("<unnamed>");
|
||||
write!(self.term, " {}", name).map_err(Error::TerminalFmtError)?;
|
||||
self.term.clear(Clear::LineToEnd)?;
|
||||
self.term
|
||||
.set_cursor_position(self.buffer.height(), self.buffer.width() - 10)?;
|
||||
self.term.set_foreground(Color::White)?;
|
||||
write!(self.term, "{}", self.key_seq).map_err(Error::TerminalFmtError)?;
|
||||
|
||||
self.term.reset_style()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display(&mut self) -> Result<(), Error> {
|
||||
if self.buffer.is_dirty() {
|
||||
self.term.clear(Clear::All)?;
|
||||
}
|
||||
|
||||
if self.config.number && self.buffer.is_dirty() {
|
||||
self.display_number()?;
|
||||
}
|
||||
|
||||
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.write_str(msg).map_err(Error::TerminalFmtError)?;
|
||||
self.term.flush()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.display_modeline()?;
|
||||
|
||||
match self.top_mode {
|
||||
TopMode::Normal => {
|
||||
self.buffer
|
||||
.set_terminal_cursor(&self.config, &mut self.term)?;
|
||||
}
|
||||
TopMode::Command => {
|
||||
self.term.set_cursor_position(self.buffer.height() + 1, 0)?;
|
||||
write!(self.term, ":{}", self.command.as_str()).map_err(Error::TerminalFmtError)?;
|
||||
}
|
||||
}
|
||||
self.term.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_command(&mut self) -> Result<(), Error> {
|
||||
let cmd = self.command.clone();
|
||||
command::execute(self, cmd)
|
||||
}
|
||||
|
||||
fn handle_command_key(&mut self, key: TermKey) -> Result<(), Error> {
|
||||
match key {
|
||||
TermKey::Char('\n') | TermKey::Char('\x0D') => {
|
||||
self.top_mode = TopMode::Normal;
|
||||
self.handle_command()?;
|
||||
}
|
||||
TermKey::Char('\x7F') => {
|
||||
if self.command.is_empty() {
|
||||
self.top_mode = TopMode::Normal;
|
||||
} else {
|
||||
self.command.pop();
|
||||
}
|
||||
}
|
||||
TermKey::Escape => {
|
||||
self.top_mode = TopMode::Normal;
|
||||
}
|
||||
TermKey::Char(c) if c.is_ascii_graphic() || c == ' ' => self.command.push(c),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_mode_key(&mut self, mode: Mode, key: TermKey) -> Result<(), Error> {
|
||||
let buffer = &mut self.buffer;
|
||||
|
||||
self.key_seq.push(key);
|
||||
|
||||
match self.config.key_seq(mode, &self.key_seq) {
|
||||
Some(PrefixNode::Leaf(actions)) => {
|
||||
self.key_seq.clear();
|
||||
|
||||
for &action in actions {
|
||||
command::perform(buffer, &self.config, action)?;
|
||||
}
|
||||
}
|
||||
Some(PrefixNode::Prefix(_)) => {}
|
||||
None => {
|
||||
self.key_seq.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if self.buffer().mode() != Mode::Normal {
|
||||
self.status = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_normal_key(&mut self, key: TermKey) -> Result<(), Error> {
|
||||
match key {
|
||||
TermKey::Escape => {
|
||||
self.key_seq.clear();
|
||||
self.buffer.set_mode(&self.config, SetMode::Normal);
|
||||
Ok(())
|
||||
}
|
||||
TermKey::Char(':') => {
|
||||
self.key_seq.clear();
|
||||
self.command.clear();
|
||||
self.status = None;
|
||||
self.top_mode = TopMode::Command;
|
||||
Ok(())
|
||||
}
|
||||
_ => self.handle_mode_key(Mode::Normal, key),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_insert_key(&mut self, key: TermKey) -> Result<(), Error> {
|
||||
match key {
|
||||
TermKey::Escape => {
|
||||
self.buffer.set_mode(&self.config, SetMode::Normal);
|
||||
Ok(())
|
||||
}
|
||||
TermKey::Char(key)
|
||||
if !key.is_ascii() || key == ' ' || key == '\t' || key.is_ascii_graphic() =>
|
||||
{
|
||||
self.buffer.insert(&self.config, key);
|
||||
Ok(())
|
||||
}
|
||||
_ => self.handle_mode_key(Mode::Insert, key),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self) -> Result<(), Error> {
|
||||
if self.config.number {
|
||||
let nw = self.buffer.number_width();
|
||||
if nw != self.number_width {
|
||||
self.number_width = nw;
|
||||
let nw = nw + 2;
|
||||
let (w, h) = self.term.size()?;
|
||||
self.buffer.resize(&self.config, nw, w - nw - 1, h - 2);
|
||||
}
|
||||
}
|
||||
|
||||
self.display()?;
|
||||
|
||||
let key = self.term.read_key()?;
|
||||
|
||||
if self.message.is_some() {
|
||||
self.message = None;
|
||||
if key != TermKey::Char(':') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let result = match (self.top_mode, self.buffer.mode()) {
|
||||
(TopMode::Normal, Mode::Normal) => self.handle_normal_key(key),
|
||||
(TopMode::Normal, Mode::Insert) => self.handle_insert_key(key),
|
||||
(TopMode::Command, _) => self.handle_command_key(key),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => {
|
||||
self.message = Some(format!("Error: {}", e));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup(&mut self) {
|
||||
self.term.clear(Clear::All).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = env::args().collect::<Vec<_>>();
|
||||
if args.len() > 2 {
|
||||
eprintln!("Usage: red [FILE]");
|
||||
return;
|
||||
}
|
||||
|
||||
if !Term::is_tty() {
|
||||
eprintln!("Not a tty");
|
||||
return;
|
||||
}
|
||||
|
||||
let path = args.get(1);
|
||||
let mut state = State::open(path).unwrap();
|
||||
let error = loop {
|
||||
if !state.running {
|
||||
break None;
|
||||
}
|
||||
|
||||
if let Err(error) = state.update() {
|
||||
break Some(error);
|
||||
}
|
||||
};
|
||||
state.cleanup();
|
||||
|
||||
if let Some(error) = error {
|
||||
eprintln!("Error: {:?}", error);
|
||||
}
|
||||
}
|
0
userspace/red/src/term/common.rs
Normal file
0
userspace/red/src/term/common.rs
Normal file
0
userspace/red/src/term/input.rs
Normal file
0
userspace/red/src/term/input.rs
Normal file
0
userspace/red/src/term/linux.rs
Normal file
0
userspace/red/src/term/linux.rs
Normal file
0
userspace/red/src/term/mod.rs
Normal file
0
userspace/red/src/term/mod.rs
Normal file
1
userspace/rt
Symbolic link
1
userspace/rt
Symbolic link
@ -0,0 +1 @@
|
||||
../../sandbox/yggdrasil-rust/yggdrasil-rt
|
19
userspace/shell/Cargo.toml
Normal file
19
userspace/shell/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "shell"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.3.19", features = ["std", "derive"], default-features = false }
|
||||
thiserror = "1.0.50"
|
||||
nom = "7.1.3"
|
||||
|
||||
[target.'cfg(target_os = "yggdrasil")'.dependencies]
|
||||
yggdrasil-rt = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-rt.git" }
|
||||
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git" }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "*"
|
93
userspace/shell/src/builtins.rs
Normal file
93
userspace/shell/src/builtins.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use crate::{Error, Outcome};
|
||||
|
||||
pub type BuiltinCommand = fn(&[String], &mut HashMap<String, String>) -> Result<Outcome, Error>;
|
||||
|
||||
static BUILTINS: &[(&str, BuiltinCommand)] = &[
|
||||
("echo", b_echo),
|
||||
("set", b_set),
|
||||
("which", b_which),
|
||||
("exit", b_exit),
|
||||
];
|
||||
|
||||
pub fn get_builtin(name: &str) -> Option<BuiltinCommand> {
|
||||
BUILTINS
|
||||
.iter()
|
||||
.find_map(|&(key, value)| if key == name { Some(value) } else { None })
|
||||
}
|
||||
|
||||
fn b_which(args: &[String], _envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
|
||||
fn find_in_path(path: &str, program: &str) -> Option<String> {
|
||||
for entry in path.split(':') {
|
||||
let full_path = PathBuf::from(entry).join(program);
|
||||
|
||||
if full_path.exists() {
|
||||
return Some(full_path.to_str().unwrap().to_owned());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
if args.len() != 1 {
|
||||
eprintln!("which usage: which PROGRAM");
|
||||
return Ok(Outcome::Exited(1));
|
||||
}
|
||||
|
||||
let program = args[0].as_str();
|
||||
|
||||
let resolution = if program.starts_with('/') || program.starts_with('.') {
|
||||
if Path::new(program).exists() {
|
||||
Some(program.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if let Ok(path) = env::var("PATH") {
|
||||
find_in_path(&path, program)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match resolution {
|
||||
Some(path) => {
|
||||
println!("{}: {}", program, path);
|
||||
Ok(Outcome::Exited(0))
|
||||
}
|
||||
_ => Ok(Outcome::Exited(1)),
|
||||
}
|
||||
}
|
||||
|
||||
fn b_set(args: &[String], envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
|
||||
if args.len() != 2 {
|
||||
eprintln!("set usage: set VAR VALUE");
|
||||
return Ok(Outcome::Exited(1));
|
||||
}
|
||||
envs.insert(args[0].clone(), args[1].clone());
|
||||
Ok(Outcome::ok())
|
||||
}
|
||||
|
||||
fn b_echo(args: &[String], _envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
if i != 0 {
|
||||
print!(" ");
|
||||
}
|
||||
print!("{}", arg);
|
||||
}
|
||||
println!();
|
||||
Ok(Outcome::ok())
|
||||
}
|
||||
|
||||
fn b_exit(args: &[String], _envs: &mut HashMap<String, String>) -> Result<Outcome, Error> {
|
||||
match args.len() {
|
||||
0 => Ok(Outcome::ExitShell(ExitCode::SUCCESS)),
|
||||
_ => {
|
||||
eprintln!("Usage: exit [CODE]");
|
||||
Ok(Outcome::Exited(1))
|
||||
}
|
||||
}
|
||||
}
|
224
userspace/shell/src/main.rs
Normal file
224
userspace/shell/src/main.rs
Normal file
@ -0,0 +1,224 @@
|
||||
#![cfg_attr(not(unix), feature(yggdrasil_os))]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
fs::File,
|
||||
io::{self, stdin, stdout, BufRead, BufReader, Stdin, Write},
|
||||
os::fd::{FromRawFd, IntoRawFd, OwnedFd},
|
||||
path::Path,
|
||||
process::{ExitCode, Stdio},
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
mod builtins;
|
||||
mod parser;
|
||||
mod sys;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
IoError(#[from] io::Error),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Args {
|
||||
#[arg(short)]
|
||||
login: bool,
|
||||
script: Option<String>,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
pub enum Outcome {
|
||||
Exited(i32),
|
||||
Killed(i32),
|
||||
ExitShell(ExitCode),
|
||||
}
|
||||
|
||||
pub enum Input {
|
||||
Interactive(Stdin),
|
||||
File(BufReader<File>),
|
||||
}
|
||||
|
||||
impl Outcome {
|
||||
pub const fn ok() -> Self {
|
||||
Self::Exited(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn getline(&mut self, buf: &mut String) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Interactive(input) => {
|
||||
let mut stdout = stdout();
|
||||
print!("$ ");
|
||||
stdout.flush().ok();
|
||||
|
||||
input.read_line(buf)
|
||||
}
|
||||
Self::File(input) => input.read_line(buf),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_interactive(&self) -> bool {
|
||||
matches!(self, Self::Interactive(_))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO group pipeline commands into a single process group
|
||||
pub fn exec(
|
||||
interactive: bool,
|
||||
pipeline: &[parser::Command],
|
||||
env: &mut HashMap<String, String>,
|
||||
) -> Result<Outcome, Error> {
|
||||
// Pipeline "a | b | c" execution:
|
||||
//
|
||||
// 1. a.stdin = STDIN, a.stdout = pipe0
|
||||
// 2. b.stdin = pipe0, b.stdout = pipe1
|
||||
// 3. c.stdin = pipe1, c.stdout = STDOUT
|
||||
//
|
||||
// Pipe count: command count - 1
|
||||
|
||||
if pipeline.is_empty() {
|
||||
return Ok(Outcome::ok());
|
||||
}
|
||||
|
||||
if pipeline.len() == 1 {
|
||||
let command = &pipeline[0];
|
||||
let (cmd, args) = command.words.split_first().unwrap();
|
||||
|
||||
if let Some(builtin) = builtins::get_builtin(cmd) {
|
||||
return builtin(args, env);
|
||||
}
|
||||
}
|
||||
|
||||
let mut inputs = vec![];
|
||||
let mut outputs = vec![];
|
||||
let mut children = vec![];
|
||||
let mut pipe_fds = vec![];
|
||||
|
||||
inputs.push(Stdio::inherit());
|
||||
for _ in 1..pipeline.len() {
|
||||
let pipe = sys::create_pipe()?;
|
||||
|
||||
let read_fd = pipe.read.into_raw_fd();
|
||||
let write_fd = pipe.write.into_raw_fd();
|
||||
pipe_fds.push(unsafe { OwnedFd::from_raw_fd(read_fd) });
|
||||
pipe_fds.push(unsafe { OwnedFd::from_raw_fd(write_fd) });
|
||||
|
||||
let input = unsafe { Stdio::from_raw_fd(read_fd) };
|
||||
let output = unsafe { Stdio::from_raw_fd(write_fd) };
|
||||
|
||||
inputs.push(input);
|
||||
outputs.push(output);
|
||||
}
|
||||
outputs.push(Stdio::inherit());
|
||||
|
||||
assert_eq!(inputs.len(), outputs.len());
|
||||
assert_eq!(inputs.len(), pipeline.len());
|
||||
|
||||
let ios = inputs.drain(..).zip(outputs.drain(..));
|
||||
for (command, (input, output)) in pipeline.iter().zip(ios) {
|
||||
let (cmd, args) = command.words.split_first().unwrap();
|
||||
|
||||
let child = sys::exec_binary(interactive, cmd, args, env, input, output)?;
|
||||
|
||||
children.push(child);
|
||||
}
|
||||
|
||||
drop(pipe_fds);
|
||||
|
||||
for mut child in children.drain(..) {
|
||||
let status = child.wait()?;
|
||||
|
||||
if !status.success() {
|
||||
return Ok(Outcome::from(status));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Outcome::ok())
|
||||
}
|
||||
|
||||
fn run(mut input: Input, vars: &mut HashMap<String, String>) -> io::Result<ExitCode> {
|
||||
let mut line = String::new();
|
||||
|
||||
let code = loop {
|
||||
line.clear();
|
||||
|
||||
let len = input.getline(&mut line)?;
|
||||
|
||||
if len == 0 {
|
||||
break ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
let line = line.trim();
|
||||
let line = match line.split_once('#') {
|
||||
Some((line, _)) => line.trim(),
|
||||
None => line,
|
||||
};
|
||||
let cmd = parser::parse_line(vars, line).unwrap();
|
||||
|
||||
let q_code = match exec(input.is_interactive(), &cmd, vars) {
|
||||
Ok(Outcome::ExitShell(code)) => {
|
||||
break code;
|
||||
}
|
||||
Ok(Outcome::Killed(signal)) => {
|
||||
if input.is_interactive() {
|
||||
eprintln!("Killed: {}", signal);
|
||||
}
|
||||
signal as i32 + 128
|
||||
}
|
||||
Ok(Outcome::Exited(code)) => code % 256,
|
||||
Err(e) => {
|
||||
eprintln!("{}: {}", "<command>", e);
|
||||
127
|
||||
}
|
||||
};
|
||||
|
||||
vars.insert("?".to_owned(), q_code.to_string());
|
||||
};
|
||||
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
fn run_file<P: AsRef<Path>>(path: P, env: &mut HashMap<String, String>) -> io::Result<ExitCode> {
|
||||
let input = BufReader::new(File::open(path)?);
|
||||
run(Input::File(input), env)
|
||||
}
|
||||
|
||||
fn run_stdin(env: &mut HashMap<String, String>) -> io::Result<ExitCode> {
|
||||
run(Input::Interactive(stdin()), env)
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
let mut vars = HashMap::new();
|
||||
|
||||
for (key, value) in env::vars() {
|
||||
vars.insert(key, value);
|
||||
}
|
||||
|
||||
if args.login {
|
||||
run_file("/etc/profile", &mut vars).ok();
|
||||
}
|
||||
|
||||
// Insert PATH to current process env
|
||||
if let Some(path) = vars.get("PATH") {
|
||||
env::set_var("PATH", path);
|
||||
}
|
||||
|
||||
let result = if let Some(script) = &args.script {
|
||||
run_file(script, &mut vars)
|
||||
} else {
|
||||
run_stdin(&mut vars)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("{:?}", e);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
157
userspace/shell/src/parser.rs
Normal file
157
userspace/shell/src/parser.rs
Normal file
@ -0,0 +1,157 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Command {
|
||||
pub words: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Token {
|
||||
Word(String),
|
||||
Pipe,
|
||||
}
|
||||
|
||||
fn lex_skip_whitespace(mut input: &[u8]) -> &[u8] {
|
||||
while input.first().map(u8::is_ascii_whitespace).unwrap_or(false) {
|
||||
input = &input[1..];
|
||||
}
|
||||
input
|
||||
}
|
||||
|
||||
pub fn lex_word(mut input: &[u8]) -> Result<(Token, &[u8]), &[u8]> {
|
||||
let mut buffer = String::new();
|
||||
while !input.is_empty() {
|
||||
if input[0].is_ascii_whitespace() || input[0] == b'"' {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.push(input[0] as char);
|
||||
input = &input[1..];
|
||||
}
|
||||
|
||||
Ok((Token::Word(buffer), input))
|
||||
}
|
||||
|
||||
pub fn lex_token(mut input: &[u8]) -> Result<(Option<Token>, &[u8]), &[u8]> {
|
||||
input = lex_skip_whitespace(input);
|
||||
|
||||
let Some(&ch) = input.first() else {
|
||||
return Ok((None, &[]));
|
||||
};
|
||||
|
||||
match ch {
|
||||
b'|' => Ok((Some(Token::Pipe), &input[1..])),
|
||||
b'"' => todo!(),
|
||||
_ => lex_word(input).map(|(x, y)| (Some(x), y)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lex_line(mut input: &[u8]) -> Result<Vec<Token>, &[u8]> {
|
||||
let mut res = Vec::new();
|
||||
while let (Some(token), output) = lex_token(input)? {
|
||||
res.push(token);
|
||||
input = output;
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn collect_pipeline(tokens: &[Token]) -> Vec<Command> {
|
||||
let mut pipeline = Vec::new();
|
||||
let mut current = None;
|
||||
|
||||
for token in tokens {
|
||||
match token {
|
||||
Token::Word(word) => {
|
||||
let current = current.get_or_insert_with(|| Command { words: vec![] });
|
||||
current.words.push(word.clone());
|
||||
}
|
||||
Token::Pipe => {
|
||||
if let Some(current) = current.take() {
|
||||
pipeline.push(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(current) = current {
|
||||
pipeline.push(current);
|
||||
}
|
||||
|
||||
pipeline
|
||||
}
|
||||
|
||||
pub fn parse_line(_env: &HashMap<String, String>, input: &str) -> Result<Vec<Command>, ()> {
|
||||
let tokens = lex_line(input.as_bytes()).map_err(|_| ())?;
|
||||
let pipeline = collect_pipeline(&tokens);
|
||||
Ok(pipeline)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::parser::{lex_line, lex_skip_whitespace, lex_token, Command, Token};
|
||||
|
||||
use super::collect_pipeline;
|
||||
|
||||
#[test]
|
||||
fn collect() {
|
||||
let tokens = lex_line(b"abc def | ghi jkl | mno pqr").unwrap();
|
||||
let pipeline = collect_pipeline(&tokens);
|
||||
|
||||
assert_eq!(
|
||||
&pipeline,
|
||||
&[
|
||||
Command {
|
||||
words: vec!["abc".to_owned(), "def".to_owned()]
|
||||
},
|
||||
Command {
|
||||
words: vec!["ghi".to_owned(), "jkl".to_owned()]
|
||||
},
|
||||
Command {
|
||||
words: vec!["mno".to_owned(), "pqr".to_owned()]
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_whitespace() {
|
||||
let w = b" \t\na";
|
||||
assert_eq!(lex_skip_whitespace(w), b"a");
|
||||
|
||||
let w = b"";
|
||||
assert_eq!(lex_skip_whitespace(w), b"");
|
||||
|
||||
let w = b"a";
|
||||
assert_eq!(lex_skip_whitespace(w), b"a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_tokens() {
|
||||
let w = b"abc def";
|
||||
assert_eq!(
|
||||
lex_line(w).unwrap(),
|
||||
vec![Token::Word("abc".to_owned()), Token::Word("def".to_owned())]
|
||||
);
|
||||
|
||||
let w = b"abc def | ghi jkl";
|
||||
assert_eq!(
|
||||
lex_line(w).unwrap(),
|
||||
vec![
|
||||
Token::Word("abc".to_owned()),
|
||||
Token::Word("def".to_owned()),
|
||||
Token::Pipe,
|
||||
Token::Word("ghi".to_owned()),
|
||||
Token::Word("jkl".to_owned()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token() {
|
||||
let w = b"abc def";
|
||||
let (t0, w) = lex_token(w).unwrap();
|
||||
assert_eq!(t0, Some(Token::Word("abc".to_owned())));
|
||||
let (t1, w) = lex_token(w).unwrap();
|
||||
assert_eq!(t1, Some(Token::Word("def".to_owned())));
|
||||
}
|
||||
}
|
11
userspace/shell/src/sys/mod.rs
Normal file
11
userspace/shell/src/sys/mod.rs
Normal file
@ -0,0 +1,11 @@
|
||||
#[cfg(unix)]
|
||||
pub mod unix;
|
||||
#[cfg(unix)]
|
||||
pub use unix as imp;
|
||||
|
||||
#[cfg(target_os = "yggdrasil")]
|
||||
pub mod yggdrasil;
|
||||
#[cfg(target_os = "yggdrasil")]
|
||||
pub use yggdrasil as imp;
|
||||
|
||||
pub use imp::*;
|
65
userspace/shell/src/sys/unix.rs
Normal file
65
userspace/shell/src/sys/unix.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
os::{
|
||||
fd::{FromRawFd, OwnedFd, RawFd},
|
||||
unix::process::{CommandExt, ExitStatusExt},
|
||||
},
|
||||
process::{Child, Command, ExitStatus, Stdio},
|
||||
};
|
||||
|
||||
use crate::Outcome;
|
||||
|
||||
pub struct Pipe {
|
||||
pub read: OwnedFd,
|
||||
pub write: OwnedFd,
|
||||
}
|
||||
|
||||
pub fn exec_binary(
|
||||
interactive: bool,
|
||||
binary: &str,
|
||||
args: &[String],
|
||||
env: &HashMap<String, String>,
|
||||
input: Stdio,
|
||||
output: Stdio,
|
||||
) -> Result<Child, io::Error> {
|
||||
let mut command = Command::new(binary);
|
||||
let mut command = command
|
||||
.args(args)
|
||||
.envs(env.iter())
|
||||
.stdin(input)
|
||||
.stdout(output);
|
||||
|
||||
if interactive {
|
||||
unsafe {
|
||||
command = command.process_group(0);
|
||||
}
|
||||
}
|
||||
|
||||
command.spawn()
|
||||
}
|
||||
|
||||
pub fn create_pipe() -> Result<Pipe, io::Error> {
|
||||
let mut fds = [0; 2];
|
||||
let (read, write) = unsafe {
|
||||
if libc::pipe(fds.as_mut_ptr()) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
(OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1]))
|
||||
};
|
||||
|
||||
Ok(Pipe { read, write })
|
||||
}
|
||||
|
||||
impl From<ExitStatus> for Outcome {
|
||||
fn from(value: ExitStatus) -> Self {
|
||||
if let Some(code) = value.code() {
|
||||
Self::Exited(code)
|
||||
} else if let Some(sig) = value.signal() {
|
||||
Self::Killed(sig)
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
58
userspace/shell/src/sys/yggdrasil.rs
Normal file
58
userspace/shell/src/sys/yggdrasil.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use std::os::yggdrasil::io::pipe;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
os::fd::OwnedFd,
|
||||
process::{Child, Command, ExitStatus, Stdio},
|
||||
};
|
||||
|
||||
use crate::Outcome;
|
||||
|
||||
pub struct Pipe {
|
||||
pub read: OwnedFd,
|
||||
pub write: OwnedFd,
|
||||
}
|
||||
|
||||
pub fn exec_binary(
|
||||
interactive: bool,
|
||||
binary: &str,
|
||||
args: &[String],
|
||||
env: &HashMap<String, String>,
|
||||
input: Stdio,
|
||||
output: Stdio,
|
||||
) -> Result<Child, io::Error> {
|
||||
use std::os::yggdrasil::process::CommandExt;
|
||||
let mut command = Command::new(binary);
|
||||
let mut command = command
|
||||
.args(args)
|
||||
.envs(env.iter())
|
||||
.stdin(input)
|
||||
.stdout(output);
|
||||
|
||||
if interactive {
|
||||
unsafe {
|
||||
command = command.process_group_raw(0);
|
||||
}
|
||||
}
|
||||
|
||||
command.spawn()
|
||||
}
|
||||
|
||||
pub fn create_pipe() -> Result<Pipe, io::Error> {
|
||||
let (read, write) = pipe::create_pipe_pair()?;
|
||||
Ok(Pipe { read, write })
|
||||
}
|
||||
|
||||
impl From<ExitStatus> for Outcome {
|
||||
fn from(value: ExitStatus) -> Self {
|
||||
use std::os::yggdrasil::process::ExitStatusExt;
|
||||
|
||||
if let Some(code) = value.code() {
|
||||
Self::Exited(code)
|
||||
} else if let Some(sig) = value.signal() {
|
||||
Self::Killed(sig as _)
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
91
userspace/sysutils/Cargo.toml
Normal file
91
userspace/sysutils/Cargo.toml
Normal file
@ -0,0 +1,91 @@
|
||||
[package]
|
||||
name = "sysutils"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
libterm = { path = "../lib/libterm" }
|
||||
yggdrasil-abi = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-abi.git", features = ["serde", "alloc", "bytemuck"] }
|
||||
|
||||
thiserror = "1.0.50"
|
||||
clap = { version = "4.3.19", features = ["std", "derive", "help", "usage"], default-features = false }
|
||||
# TODO own impl
|
||||
humansize = { version = "2.1.3", features = ["impl_style"] }
|
||||
rand = { git = "https://git.alnyan.me/yggdrasil/rand.git", branch = "alnyan/yggdrasil" }
|
||||
serde = { version = "1.0.193", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
sha2 = { version = "0.10.8", features = ["force-soft"] }
|
||||
|
||||
init = { path = "../init" }
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
# /sbin
|
||||
[[bin]]
|
||||
name = "mount"
|
||||
path = "src/mount.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "login"
|
||||
path = "src/login.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "service"
|
||||
path = "src/service.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "logd"
|
||||
path = "src/logd.rs"
|
||||
|
||||
# /bin
|
||||
[[bin]]
|
||||
name = "ls"
|
||||
path = "src/ls.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "mkdir"
|
||||
path = "src/mkdir.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "touch"
|
||||
path = "src/touch.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "rm"
|
||||
path = "src/rm.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "cat"
|
||||
path = "src/cat.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "hexd"
|
||||
path = "src/hexd.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "random"
|
||||
path = "src/random.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "dd"
|
||||
path = "src/dd.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "view"
|
||||
path = "src/view.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "chmod"
|
||||
path = "src/chmod.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "sha256sum"
|
||||
path = "src/sha256sum.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "sysmon"
|
||||
path = "src/sysmon.rs"
|
36
userspace/sysutils/src/cat.rs
Normal file
36
userspace/sysutils/src/cat.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::{self, stdout, BufReader, Read, Stdout, Write},
|
||||
path::Path,
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
fn cat_file<P: AsRef<Path>>(stdout: &mut Stdout, path: P) -> io::Result<()> {
|
||||
let mut buf = [0; 4096];
|
||||
let mut reader = BufReader::new(File::open(path)?);
|
||||
|
||||
loop {
|
||||
let count = reader.read(&mut buf)?;
|
||||
|
||||
if count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
stdout.write_all(&buf[..count])?;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let mut stdout = stdout();
|
||||
let mut exit = ExitCode::SUCCESS;
|
||||
|
||||
for arg in env::args().skip(1) {
|
||||
if let Err(error) = cat_file(&mut stdout, &arg) {
|
||||
eprintln!("{}: {}", arg, error);
|
||||
exit = ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
exit
|
||||
}
|
22
userspace/sysutils/src/chmod.rs
Normal file
22
userspace/sysutils/src/chmod.rs
Normal file
@ -0,0 +1,22 @@
|
||||
#![feature(rustc_private, yggdrasil_os)]
|
||||
use std::{
|
||||
env,
|
||||
os::yggdrasil::io::{self, FileMetadataUpdate, FileMetadataUpdateMode, RawFileMode},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<_> = env::args().collect();
|
||||
if args.len() != 3 {
|
||||
panic!();
|
||||
}
|
||||
|
||||
let mode_str = &args[1];
|
||||
let filename = &args[2];
|
||||
|
||||
let mode_oct = u32::from_str_radix(mode_str, 8).unwrap();
|
||||
|
||||
let update =
|
||||
FileMetadataUpdate::Permissions(RawFileMode::new(mode_oct), FileMetadataUpdateMode::Set);
|
||||
|
||||
io::update_metadata(filename, &update).unwrap();
|
||||
}
|
138
userspace/sysutils/src/dd.rs
Normal file
138
userspace/sysutils/src/dd.rs
Normal file
@ -0,0 +1,138 @@
|
||||
#![feature(yggdrasil_os)]
|
||||
use std::{
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use sysutils::{Input, Output};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(short)]
|
||||
source: Option<String>,
|
||||
#[arg(short)]
|
||||
destination: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
src_skip: Option<u64>,
|
||||
#[arg(long, default_value_t = 512)]
|
||||
src_bs: u64,
|
||||
#[arg(short, long, default_value_t = usize::MAX)]
|
||||
count: usize,
|
||||
|
||||
// TODO: remove this when pipes are a thing
|
||||
#[arg(short = 'x', long)]
|
||||
as_hex: bool,
|
||||
}
|
||||
|
||||
fn dump_block(offset: u64, data: &[u8]) {
|
||||
const WINDOW_SIZE: usize = 16;
|
||||
let window_count = (data.len() + WINDOW_SIZE) / WINDOW_SIZE;
|
||||
|
||||
for iw in 0..window_count {
|
||||
let off = iw * WINDOW_SIZE;
|
||||
let len = core::cmp::min(data.len() - off, WINDOW_SIZE);
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let window = &data[off..off + len];
|
||||
|
||||
print!("{:08X}: ", offset + off as u64);
|
||||
for i in 0..WINDOW_SIZE {
|
||||
if i < window.len() {
|
||||
print!("{:02X}", window[i]);
|
||||
} else {
|
||||
print!(" ");
|
||||
}
|
||||
|
||||
if i % 2 == 1 {
|
||||
print!(" ");
|
||||
}
|
||||
}
|
||||
|
||||
for &ch in window {
|
||||
if ch.is_ascii_graphic() || ch == b' ' {
|
||||
print!("{}", ch as char);
|
||||
} else {
|
||||
print!(".");
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
fn run<I: Read + Seek, O: Write>(
|
||||
mut input: I,
|
||||
mut output: O,
|
||||
src_position: u64,
|
||||
src_block_size: u64,
|
||||
mut count: usize,
|
||||
as_hex: bool,
|
||||
) -> io::Result<()> {
|
||||
let mut block = vec![0; src_block_size as usize];
|
||||
let mut offset = 0;
|
||||
|
||||
input.seek(SeekFrom::Start(src_position * src_block_size))?;
|
||||
|
||||
while count != 0 {
|
||||
let read_count = input.read(&mut block)?;
|
||||
|
||||
if read_count == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if as_hex {
|
||||
dump_block(
|
||||
(src_position + offset) * src_block_size,
|
||||
&block[..read_count],
|
||||
);
|
||||
} else {
|
||||
output.write_all(&block[..read_count])?;
|
||||
}
|
||||
|
||||
count -= 1;
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
|
||||
let src_path = args.source.as_deref().unwrap_or("-");
|
||||
let dst_path = args.destination.as_deref().unwrap_or("-");
|
||||
|
||||
if src_path == dst_path && src_path != "-" {
|
||||
eprintln!("Input and output cannot be the same: {}", src_path);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
if args.as_hex && dst_path != "-" {
|
||||
eprintln!("--as-hex requires stdout output");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let input = Input::open_str(src_path).unwrap();
|
||||
let output = Output::open_str(dst_path).unwrap();
|
||||
|
||||
let src_position = args.src_skip.unwrap_or(0);
|
||||
|
||||
match run(
|
||||
input,
|
||||
output,
|
||||
src_position,
|
||||
args.src_bs,
|
||||
args.count,
|
||||
args.as_hex,
|
||||
) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
77
userspace/sysutils/src/hexd.rs
Normal file
77
userspace/sysutils/src/hexd.rs
Normal file
@ -0,0 +1,77 @@
|
||||
use std::{
|
||||
env,
|
||||
io::{self, Read},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use sysutils::{Input, ToExitCode};
|
||||
|
||||
const WINDOW_SIZE: usize = 16;
|
||||
|
||||
fn do_line(offset: usize, line: &[u8]) {
|
||||
print!("{:08x} |", offset);
|
||||
|
||||
for i in 0..WINDOW_SIZE {
|
||||
if i % 2 == 0 {
|
||||
print!(" ");
|
||||
}
|
||||
if i < line.len() {
|
||||
print!("{:02X}", line[i]);
|
||||
} else {
|
||||
print!(" ");
|
||||
}
|
||||
}
|
||||
|
||||
print!(" | ");
|
||||
|
||||
for &ch in line {
|
||||
let ch = if ch.is_ascii_control() { b'.' } else { ch };
|
||||
print!("{}", ch as char);
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
fn do_file(path: &str) -> io::Result<()> {
|
||||
let mut input = Input::open_str(path)?;
|
||||
let mut buf = [0; WINDOW_SIZE];
|
||||
let mut offset = 0;
|
||||
|
||||
loop {
|
||||
let len = input.read(&mut buf)?;
|
||||
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
do_line(offset, &buf[..len]);
|
||||
offset += len;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn main() -> ExitCode {
|
||||
let args = env::args().collect::<Vec<_>>();
|
||||
|
||||
let result = match args.len() {
|
||||
0..=1 => do_file("-").to_exit_code(),
|
||||
2 => do_file(&args[1]).to_exit_code(),
|
||||
_ => {
|
||||
let mut result = 0;
|
||||
for arg in args[1..].iter() {
|
||||
println!("{}:", arg);
|
||||
if do_file(arg).to_exit_code() != 0 {
|
||||
result = 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
};
|
||||
|
||||
if result == 0 {
|
||||
ExitCode::SUCCESS
|
||||
} else {
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
194
userspace/sysutils/src/lib.rs
Normal file
194
userspace/sysutils/src/lib.rs
Normal file
@ -0,0 +1,194 @@
|
||||
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))]
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufRead, Read, Seek, SeekFrom, Write},
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod unix {
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct FileMode(u32);
|
||||
|
||||
impl From<u32> for FileMode {
|
||||
fn from(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FileMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
macro_rules! print_bit {
|
||||
($res:expr, $val:expr, $self:expr, $bit:expr) => {
|
||||
if $self.0 & $bit == $bit {
|
||||
$res = $val;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut buf = ['-'; 9];
|
||||
|
||||
print_bit!(buf[0], 'r', self, 0o400);
|
||||
print_bit!(buf[1], 'w', self, 0o200);
|
||||
print_bit!(buf[2], 'x', self, 0o100);
|
||||
print_bit!(buf[3], 'r', self, 0o040);
|
||||
print_bit!(buf[4], 'w', self, 0o020);
|
||||
print_bit!(buf[5], 'x', self, 0o010);
|
||||
print_bit!(buf[6], 'r', self, 0o004);
|
||||
print_bit!(buf[7], 'w', self, 0o002);
|
||||
print_bit!(buf[8], 'x', self, 0o001);
|
||||
|
||||
for ch in buf.iter() {
|
||||
fmt::Display::fmt(ch, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Ok(_) => 0,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Input {
|
||||
Stdin(io::Stdin),
|
||||
File(io::BufReader<File>),
|
||||
}
|
||||
|
||||
pub enum Output {
|
||||
Stdout(io::Stdout),
|
||||
File(io::BufWriter<File>),
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn open_str(arg: &str) -> io::Result<Self> {
|
||||
if arg == "-" {
|
||||
Ok(Self::Stdin(io::stdin()))
|
||||
} else {
|
||||
let file = File::open(arg)?;
|
||||
let reader = io::BufReader::new(file);
|
||||
Ok(Self::File(reader))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_line(&mut self, line: &mut String) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Stdin(f) => f.read_line(line),
|
||||
Self::File(f) => f.read_line(line),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Stdin> for Input {
|
||||
fn from(value: io::Stdin) -> Self {
|
||||
Self::Stdin(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for Input {
|
||||
fn from(value: File) -> Self {
|
||||
Self::File(io::BufReader::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Input {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Stdin(value) => value.read(buf),
|
||||
Self::File(value) => value.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Input {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
match self {
|
||||
Self::Stdin(_) => todo!(),
|
||||
Self::File(value) => value.seek(pos),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Output {
|
||||
pub fn open_str(arg: &str) -> io::Result<Self> {
|
||||
if arg == "-" {
|
||||
Ok(Self::Stdout(io::stdout()))
|
||||
} else {
|
||||
let file = File::create(arg)?;
|
||||
let writer = io::BufWriter::new(file);
|
||||
Ok(Self::File(writer))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Stdout> for Output {
|
||||
fn from(value: io::Stdout) -> Self {
|
||||
Self::Stdout(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for Output {
|
||||
fn from(value: File) -> Self {
|
||||
Self::File(io::BufWriter::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Output {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Stdout(value) => value.write(buf),
|
||||
Self::File(value) => value.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Stdout(value) => value.flush(),
|
||||
Self::File(value) => value.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Output {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
match self {
|
||||
Self::Stdout(_) => todo!(),
|
||||
Self::File(value) => value.seek(pos),
|
||||
}
|
||||
}
|
||||
}
|
18
userspace/sysutils/src/logd.rs
Normal file
18
userspace/sysutils/src/logd.rs
Normal file
@ -0,0 +1,18 @@
|
||||
#![feature(yggdrasil_os)]
|
||||
|
||||
use std::os::yggdrasil::io::message_channel::{MessageChannel, MessageReceiver};
|
||||
|
||||
fn main() {
|
||||
let channel = MessageChannel::open("log", true).unwrap();
|
||||
let mut buf = [0; 1024];
|
||||
|
||||
loop {
|
||||
let Ok((_, len)) = channel.receive_message(&mut buf) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(msg) = std::str::from_utf8(&buf[..len]) {
|
||||
debug_trace!("log::message: {:?}", msg);
|
||||
}
|
||||
}
|
||||
}
|
84
userspace/sysutils/src/login.rs
Normal file
84
userspace/sysutils/src/login.rs
Normal file
@ -0,0 +1,84 @@
|
||||
#![feature(yggdrasil_os, rustc_private)]
|
||||
|
||||
use std::{
|
||||
env,
|
||||
io::{self, stdin, stdout, BufRead, Write},
|
||||
os::{
|
||||
fd::AsRawFd,
|
||||
yggdrasil::io::{
|
||||
device::{DeviceRequest, FdDeviceRequest},
|
||||
terminal::start_terminal_session,
|
||||
},
|
||||
},
|
||||
process::{Command, ExitCode},
|
||||
};
|
||||
|
||||
fn login_readline<R: BufRead + AsRawFd>(
|
||||
reader: &mut R,
|
||||
buf: &mut String,
|
||||
_secret: bool,
|
||||
) -> Result<usize, io::Error> {
|
||||
reader.read_line(buf)
|
||||
}
|
||||
|
||||
fn login_as(username: &str, _password: &str) -> Result<(), io::Error> {
|
||||
let mut shell = Command::new("/bin/sh").arg("-l").spawn()?;
|
||||
println!("Hello {:?}", username);
|
||||
shell.wait()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn login_attempt(erase: bool) -> Result<(), io::Error> {
|
||||
let mut stdin = stdin().lock();
|
||||
let mut stdout = stdout();
|
||||
|
||||
if erase {
|
||||
print!("\x1b[1;1f\x1b[2J");
|
||||
stdout.flush().ok();
|
||||
}
|
||||
|
||||
let mut username = String::new();
|
||||
|
||||
print!("Username: ");
|
||||
stdout.flush().ok();
|
||||
if login_readline(&mut stdin, &mut username, false)? == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
login_as(username.trim(), "")
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<_> = env::args().skip(1).collect();
|
||||
if args.len() != 1 {
|
||||
eprintln!("Usage: /sbin/login TTY");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
let terminal = args[0].as_str();
|
||||
|
||||
// TODO check that `terminal` is a terminal
|
||||
debug_trace!("Starting a session at {}", terminal);
|
||||
if let Err(err) = unsafe { start_terminal_session(terminal) } {
|
||||
debug_trace!("Error: {:?}", err);
|
||||
eprintln!("Could not setup a session at {}: {:?}", terminal, err);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let mut attempt_number = 0;
|
||||
loop {
|
||||
debug_trace!("Login attempt {}", attempt_number);
|
||||
|
||||
// "Attach" the terminal
|
||||
unsafe {
|
||||
let pid = std::os::yggdrasil::process::id_ext();
|
||||
stdin()
|
||||
.device_request(&mut DeviceRequest::SetTerminalGroup(pid))
|
||||
.expect("Could not attach the terminal");
|
||||
}
|
||||
|
||||
if let Err(err) = login_attempt(attempt_number % 3 == 0) {
|
||||
eprintln!("login: {}", err.to_string());
|
||||
}
|
||||
attempt_number += 1;
|
||||
}
|
||||
}
|
215
userspace/sysutils/src/ls.rs
Normal file
215
userspace/sysutils/src/ls.rs
Normal file
@ -0,0 +1,215 @@
|
||||
#![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))]
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
fs::{read_dir, FileType, Metadata},
|
||||
io,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
#[cfg(target_os = "yggdrasil")]
|
||||
use std::os::yggdrasil::fs::MetadataExt;
|
||||
|
||||
use clap::Parser;
|
||||
use humansize::{FormatSize, BINARY};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(disable_help_flag = true)]
|
||||
pub struct Args {
|
||||
#[arg(short)]
|
||||
long: bool,
|
||||
#[arg(short, long)]
|
||||
human_readable: bool,
|
||||
|
||||
paths: Vec<String>,
|
||||
}
|
||||
|
||||
trait DisplayBit {
|
||||
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result;
|
||||
|
||||
fn display_with<'a, 'b>(&'a self, opts: &'b Args) -> DisplayWith<'a, 'b, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
DisplayWith { item: self, opts }
|
||||
}
|
||||
}
|
||||
|
||||
trait DisplaySizeBit: Sized {
|
||||
fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result;
|
||||
|
||||
fn display_size_with(self, opts: &Args) -> DisplaySizeWith<'_, Self> {
|
||||
DisplaySizeWith { size: self, opts }
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplaySizeBit for u64 {
|
||||
fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if opts.human_readable {
|
||||
fmt::Display::fmt(&self.format_size(BINARY), f)
|
||||
} else {
|
||||
fmt::Display::fmt(&self, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplaySizeWith<'a, T> {
|
||||
size: T,
|
||||
opts: &'a Args,
|
||||
}
|
||||
|
||||
struct DisplayWith<'a, 'b, T: DisplayBit> {
|
||||
item: &'a T,
|
||||
opts: &'b Args,
|
||||
}
|
||||
|
||||
impl<T: DisplayBit> fmt::Display for DisplayWith<'_, '_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.item.display_bit(self.opts, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DisplaySizeBit + Copy> fmt::Display for DisplaySizeWith<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.size.display_size_bit(self.opts, f)
|
||||
}
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
name: String,
|
||||
ty: Option<FileType>,
|
||||
attrs: Option<Metadata>,
|
||||
}
|
||||
|
||||
impl DisplayBit for Option<FileType> {
|
||||
fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ty) = self {
|
||||
if ty.is_dir() {
|
||||
f.write_str("d")
|
||||
} else if ty.is_symlink() {
|
||||
f.write_str("l")
|
||||
} else {
|
||||
f.write_str("-")
|
||||
}
|
||||
} else {
|
||||
f.write_str("?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "yggdrasil")]
|
||||
impl DisplayBit for Option<Metadata> {
|
||||
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let Some(attrs) = self else {
|
||||
return write!(f, "--------- {:<8}", "???");
|
||||
};
|
||||
|
||||
let mode = attrs.mode_ext();
|
||||
write!(f, "{} {:>8}", mode, attrs.len().display_size_with(opts))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl DisplayBit for Option<Metadata> {
|
||||
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use sysutils::unix::FileMode;
|
||||
|
||||
let Some(attrs) = self else {
|
||||
return write!(f, "--------- {:<8}", "???");
|
||||
};
|
||||
|
||||
let mode = FileMode::from(attrs.mode());
|
||||
write!(f, "{} {:>8}", mode, attrs.len().display_size_with(opts))
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayBit for Entry {
|
||||
fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if opts.long {
|
||||
write!(
|
||||
f,
|
||||
"{}{} {}",
|
||||
self.ty.display_with(opts),
|
||||
self.attrs.display_with(opts),
|
||||
self.name
|
||||
)
|
||||
} else {
|
||||
f.write_str(&self.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
fn invalid() -> Self {
|
||||
Self {
|
||||
name: "???".to_owned(),
|
||||
ty: None,
|
||||
attrs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_directory(path: &Path) -> io::Result<Vec<Entry>> {
|
||||
let mut entries = vec![];
|
||||
for entry in read_dir(path)? {
|
||||
let Ok(entry) = entry else {
|
||||
entries.push(Entry::invalid());
|
||||
continue;
|
||||
};
|
||||
|
||||
let os_filename = entry.file_name();
|
||||
let ty = entry.file_type().ok();
|
||||
let attrs = entry.path().metadata().ok();
|
||||
|
||||
entries.push(Entry {
|
||||
name: os_filename.to_string_lossy().to_string(),
|
||||
ty,
|
||||
attrs,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn list(opts: &Args, path: &Path) -> io::Result<()> {
|
||||
if path.is_dir() {
|
||||
let entries = list_directory(path)?;
|
||||
|
||||
for entry in entries {
|
||||
println!("{}", entry.display_with(opts));
|
||||
}
|
||||
} else {
|
||||
// TODO fetch info
|
||||
println!("{}", path.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_wrap<P: AsRef<Path>>(opts: &Args, path: P) {
|
||||
let path = path.as_ref();
|
||||
|
||||
match list(opts, path) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
eprintln!("{}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
if args.paths.is_empty() {
|
||||
list_wrap(&args, ".");
|
||||
} else {
|
||||
for path in args.paths.iter() {
|
||||
if args.paths.len() > 1 {
|
||||
println!("{}:", path);
|
||||
}
|
||||
list_wrap(&args, path);
|
||||
}
|
||||
}
|
||||
}
|
37
userspace/sysutils/src/mkdir.rs
Normal file
37
userspace/sysutils/src/mkdir.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(short)]
|
||||
parents: bool,
|
||||
#[clap(required = true)]
|
||||
dirs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
fn mkdir<P: AsRef<Path>>(path: P, parents: bool) -> Result<(), io::Error> {
|
||||
if parents {
|
||||
std::fs::create_dir_all(path)
|
||||
} else {
|
||||
std::fs::create_dir(path)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
let mut result = ExitCode::SUCCESS;
|
||||
|
||||
for arg in args.dirs {
|
||||
if let Err(e) = mkdir(&arg, args.parents) {
|
||||
eprintln!("{}: {}", arg.display(), e);
|
||||
result = ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
46
userspace/sysutils/src/mount.rs
Normal file
46
userspace/sysutils/src/mount.rs
Normal file
@ -0,0 +1,46 @@
|
||||
#![feature(rustc_private, yggdrasil_os)]
|
||||
use std::{
|
||||
os::yggdrasil::io::device::{mount_raw, MountOptions},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Args {
|
||||
#[arg(short)]
|
||||
ty: Option<String>,
|
||||
source: String,
|
||||
target: Option<String>,
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
|
||||
let source = if args.target.is_some() {
|
||||
Some(args.source.as_str())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let target = args.target.as_deref().unwrap_or(args.source.as_str());
|
||||
let filesystem = args.ty.as_deref();
|
||||
|
||||
// Permissions are not yet implemented, lol
|
||||
let result = unsafe {
|
||||
let options = MountOptions {
|
||||
source,
|
||||
filesystem,
|
||||
target,
|
||||
};
|
||||
|
||||
mount_raw(&options)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(err) => {
|
||||
eprintln!("mount: {:?}", err);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
27
userspace/sysutils/src/random.rs
Normal file
27
userspace/sysutils/src/random.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<_> = env::args().collect();
|
||||
|
||||
let (lower, upper, count) = match args.len() {
|
||||
1 => (0, u32::MAX, 1),
|
||||
2 => (0, args[1].parse().unwrap(), 1),
|
||||
3 => (args[1].parse().unwrap(), args[2].parse().unwrap(), 1),
|
||||
4 => (
|
||||
args[1].parse().unwrap(),
|
||||
args[2].parse().unwrap(),
|
||||
args[3].parse::<usize>().unwrap(),
|
||||
),
|
||||
_ => panic!("Incorrect usage"),
|
||||
};
|
||||
|
||||
assert!(lower < upper);
|
||||
|
||||
for _ in 0..count {
|
||||
let value: u32 = rand::random();
|
||||
let res =
|
||||
(((value as u64) * (upper - lower) as u64) / u32::MAX as u64 + lower as u64) as u32;
|
||||
|
||||
println!("{}", res);
|
||||
}
|
||||
}
|
71
userspace/sysutils/src/rm.rs
Normal file
71
userspace/sysutils/src/rm.rs
Normal file
@ -0,0 +1,71 @@
|
||||
#![feature(io_error_more)]
|
||||
use std::{io, fs, path::{Path, PathBuf}, process::ExitCode};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(short)]
|
||||
recurse: bool,
|
||||
#[clap(required = true)]
|
||||
paths: Vec<PathBuf>
|
||||
}
|
||||
|
||||
fn rm<P: AsRef<Path>>(path: P, recurse: bool) -> bool {
|
||||
let path = path.as_ref();
|
||||
println!("remove {}", path.display());
|
||||
|
||||
match fs::remove_file(path) {
|
||||
Err(e) if recurse && e.kind() == io::ErrorKind::IsADirectory => {
|
||||
let readdir = match fs::read_dir(path) {
|
||||
Ok(dir) => dir,
|
||||
Err(error) => {
|
||||
eprintln!("{}: {}", path.display(), error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let mut result = true;
|
||||
|
||||
for entry in readdir {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(error) => {
|
||||
eprintln!("{}/: {}", path.display(), error);
|
||||
result = false;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
result &= rm(entry.path(), recurse);
|
||||
}
|
||||
|
||||
if result {
|
||||
if let Err(error) = fs::remove_dir(path) {
|
||||
eprintln!("{}: {}", path.display(), error);
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("{}: {}", path.display(), e);
|
||||
false
|
||||
}
|
||||
Ok(_) => true
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
let mut result = ExitCode::SUCCESS;
|
||||
|
||||
for arg in args.paths {
|
||||
if !rm(arg, args.recurse) {
|
||||
result = ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
56
userspace/sysutils/src/service.rs
Normal file
56
userspace/sysutils/src/service.rs
Normal file
@ -0,0 +1,56 @@
|
||||
#![feature(yggdrasil_os, rustc_private)]
|
||||
|
||||
use std::{
|
||||
io,
|
||||
os::yggdrasil::io::message_channel::{MessageChannel, MessageDestination, MessageSender},
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
#[command(arg_required_else_help = true)]
|
||||
Start { name: Vec<String> },
|
||||
}
|
||||
|
||||
fn send_message(msg: init::InitMsg) -> io::Result<()> {
|
||||
let channel = MessageChannel::open("service-control", false)?;
|
||||
let msg = serde_json::to_string(&msg).unwrap();
|
||||
|
||||
channel.send_message(msg.as_bytes(), MessageDestination::Specific(0))
|
||||
}
|
||||
|
||||
fn run(command: Command) -> io::Result<()> {
|
||||
match command {
|
||||
Command::Start { name } => {
|
||||
let Some((binary, args)) = name.split_first() else {
|
||||
return Ok(());
|
||||
};
|
||||
let msg = init::InitMsg::StartService(init::StartService {
|
||||
binary: binary.to_owned(),
|
||||
args: args.into(),
|
||||
});
|
||||
|
||||
send_message(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
|
||||
match run(args.command) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
46
userspace/sysutils/src/sha256sum.rs
Normal file
46
userspace/sysutils/src/sha256sum.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::{self, Read},
|
||||
path::Path,
|
||||
process::ExitCode,
|
||||
};
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
fn do_digest<P: AsRef<Path>>(path: P) -> Result<(), io::Error> {
|
||||
let mut buf = [0; 4096];
|
||||
let mut file = File::open(path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
loop {
|
||||
let count = file.read(&mut buf)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buf[..count]);
|
||||
}
|
||||
|
||||
let result = hasher.finalize();
|
||||
for byte in result {
|
||||
print!("{:02x}", byte);
|
||||
}
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<_> = env::args().collect();
|
||||
if args.len() != 2 {
|
||||
eprintln!("TODO: {} FILENAME", args[0]);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
match do_digest(&args[1]) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("{}: {}", args[1], error);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
70
userspace/sysutils/src/sysmon.rs
Normal file
70
userspace/sysutils/src/sysmon.rs
Normal file
@ -0,0 +1,70 @@
|
||||
#![feature(yggdrasil_os, rustc_private)]
|
||||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
fmt::Write,
|
||||
os::yggdrasil::{get_system_info, SystemInfo, SystemMemoryStats},
|
||||
};
|
||||
|
||||
use humansize::FormatSize;
|
||||
use libterm::{Clear, Term};
|
||||
|
||||
fn get_memory_stats() -> SystemMemoryStats {
|
||||
let mut info = SystemInfo::MemoryStats(SystemMemoryStats::default());
|
||||
get_system_info(&mut info).unwrap();
|
||||
let SystemInfo::MemoryStats(stats) = info;
|
||||
stats
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Retrieve the stats once for total cap
|
||||
let stats = get_memory_stats();
|
||||
let total = stats.total_usable_pages;
|
||||
|
||||
let mut term = Term::open().unwrap();
|
||||
|
||||
let mut points = VecDeque::new();
|
||||
|
||||
loop {
|
||||
let (width, height) = term.size().unwrap();
|
||||
term.clear(Clear::All).ok();
|
||||
|
||||
let stats = get_memory_stats();
|
||||
|
||||
points.push_back(stats.allocated_pages);
|
||||
|
||||
let h = height - 1;
|
||||
|
||||
for (i, point) in points.iter().enumerate() {
|
||||
let v = (*point * h) / total;
|
||||
for j in 0..v {
|
||||
term.set_cursor_position(h - j - 1, i).ok();
|
||||
term.write_char('*').ok();
|
||||
}
|
||||
}
|
||||
|
||||
while points.len() >= width - 1 {
|
||||
points.pop_front();
|
||||
}
|
||||
|
||||
term.set_cursor_position(h, 1).ok();
|
||||
|
||||
let allocated_bytes = stats.allocated_pages * stats.page_size;
|
||||
let free_bytes = stats.free_pages * stats.page_size;
|
||||
let total_usable_bytes = stats.total_usable_pages * stats.page_size;
|
||||
|
||||
write!(
|
||||
term,
|
||||
"U: {} F: {} T: {} ({}%)",
|
||||
allocated_bytes.format_size(Default::default()),
|
||||
free_bytes.format_size(Default::default()),
|
||||
total_usable_bytes.format_size(Default::default()),
|
||||
100 * stats.allocated_pages / stats.total_usable_pages
|
||||
)
|
||||
.ok();
|
||||
|
||||
term.flush().ok();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
}
|
26
userspace/sysutils/src/touch.rs
Normal file
26
userspace/sysutils/src/touch.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use std::{env, fs::OpenOptions, io, path::Path, process::ExitCode};
|
||||
|
||||
fn touch<P: AsRef<Path>>(path: P) -> Result<(), io::Error> {
|
||||
OpenOptions::new().create(true).write(true).open(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let mut args = env::args();
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: {} FILES", args.next().unwrap());
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
let mut result = ExitCode::SUCCESS;
|
||||
|
||||
for arg in args.skip(1) {
|
||||
if let Err(err) = touch(&arg) {
|
||||
eprintln!("{}: {}", arg, err);
|
||||
result = ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
168
userspace/sysutils/src/view.rs
Normal file
168
userspace/sysutils/src/view.rs
Normal file
@ -0,0 +1,168 @@
|
||||
use std::{
|
||||
fmt::{self, Write},
|
||||
io,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use libterm::{Clear, Color, Term, TermKey};
|
||||
use sysutils::Input;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Terminal error: {0}")]
|
||||
TerminalError(#[from] libterm::Error),
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
#[error("Terminal output error: {0}")]
|
||||
FmtError(#[from] fmt::Error),
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
term: Term,
|
||||
source: Input,
|
||||
eof: bool,
|
||||
show_bar: bool,
|
||||
|
||||
cursor: usize,
|
||||
buffer: Vec<String>,
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn open(src: &str, show_bar: bool) -> Result<Self, Error> {
|
||||
let term = Term::open()?;
|
||||
let source = Input::open_str(src)?;
|
||||
|
||||
Ok(Self {
|
||||
term,
|
||||
source,
|
||||
cursor: 0,
|
||||
show_bar,
|
||||
buffer: vec![],
|
||||
eof: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_more(&mut self, height: usize) -> Result<(), Error> {
|
||||
if self.eof || self.cursor + height < self.buffer.len() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let need_lines = self.cursor + height + 1 - self.buffer.len();
|
||||
for _ in 0..need_lines {
|
||||
let mut line = String::new();
|
||||
let len = self.source.read_line(&mut line)?;
|
||||
if len == 0 {
|
||||
self.eof = true;
|
||||
break;
|
||||
}
|
||||
self.buffer.push(line.trim().to_owned());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display(&mut self, width: usize, height: usize) -> Result<(), Error> {
|
||||
// TODO don't redraw every time
|
||||
self.term.clear(Clear::All)?;
|
||||
self.term.set_cursor_visible(false)?;
|
||||
|
||||
let bar_height = self.show_bar as usize;
|
||||
|
||||
for y in 0..height - bar_height {
|
||||
let i = y + self.cursor;
|
||||
let Some(line) = self.buffer.get(i) else {
|
||||
break;
|
||||
};
|
||||
let limit = std::cmp::min(line.len(), width);
|
||||
|
||||
self.term.set_cursor_position(y, 0)?;
|
||||
write!(self.term, "{}", &line[..limit])?;
|
||||
}
|
||||
|
||||
if self.show_bar {
|
||||
let line_end = std::cmp::min(self.cursor + height, self.buffer.len());
|
||||
|
||||
self.term.set_cursor_position(height - 1, 0)?;
|
||||
|
||||
if self.eof {
|
||||
self.term.set_background(Color::Green)?;
|
||||
} else {
|
||||
self.term.set_background(Color::Yellow)?;
|
||||
}
|
||||
self.term.set_foreground(Color::Black)?;
|
||||
self.term.set_bright(true)?;
|
||||
|
||||
self.term.clear(Clear::LineToEnd)?;
|
||||
|
||||
if self.eof {
|
||||
let p = line_end * 100 / self.buffer.len();
|
||||
write!(
|
||||
self.term,
|
||||
"[{}..{}/{}] ({}%)",
|
||||
self.cursor,
|
||||
line_end,
|
||||
self.buffer.len(),
|
||||
p
|
||||
)?;
|
||||
} else {
|
||||
write!(self.term, "[{}..{}] ...", self.cursor, line_end)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.term.reset_style()?;
|
||||
self.term.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run(mut self) -> Result<(), Error> {
|
||||
loop {
|
||||
let (width, height) = self.term.size()?;
|
||||
|
||||
self.fetch_more(height)?;
|
||||
|
||||
self.display(width, height)?;
|
||||
|
||||
let key = self.term.read_key()?;
|
||||
|
||||
match key {
|
||||
TermKey::Char('q') => break,
|
||||
TermKey::Char('j') => {
|
||||
if !self.eof || self.cursor + height < self.buffer.len() {
|
||||
self.cursor += 1;
|
||||
}
|
||||
}
|
||||
TermKey::Char('k') => {
|
||||
if self.cursor != 0 {
|
||||
self.cursor -= 1;
|
||||
}
|
||||
}
|
||||
// TODO page down, page up
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for View {
|
||||
fn drop(&mut self) {
|
||||
self.term.set_cursor_visible(true).ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
#[arg(short, long)]
|
||||
no_bar: bool,
|
||||
|
||||
filename: String,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// TODO check if running in a terminal
|
||||
let args = Args::parse();
|
||||
let view = View::open(&args.filename, !args.no_bar).unwrap();
|
||||
view.run()
|
||||
}
|
12
userspace/term/Cargo.toml
Normal file
12
userspace/term/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "term"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Mark Poliakov <mark@alnyan.me>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1.14.0", features = ["derive"] }
|
||||
libcolors = { path = "../lib/libcolors", default_features = false, features = ["client"] }
|
||||
thiserror = "1.0.56"
|
112
userspace/term/src/attr.rs
Normal file
112
userspace/term/src/attr.rs
Normal file
@ -0,0 +1,112 @@
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[repr(usize)]
|
||||
pub enum Color {
|
||||
Black = 0,
|
||||
Red = 1,
|
||||
Green = 2,
|
||||
Yellow = 3,
|
||||
Blue = 4,
|
||||
Magenta = 5,
|
||||
Cyan = 6,
|
||||
White = 7,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct DisplayColor {
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CellAttributes {
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
pub bright: bool,
|
||||
}
|
||||
|
||||
impl DisplayColor {
|
||||
pub const BLACK: Self = Self::new(0, 0, 0);
|
||||
pub const WHITE: Self = Self::new(255, 255, 255);
|
||||
pub const DARK_GRAY: Self = Self::new(60, 60, 60);
|
||||
pub const LIGHT_GRAY: Self = Self::new(127, 127, 127);
|
||||
|
||||
pub const DARK_RED: Self = Self::new(160, 0, 0);
|
||||
pub const DARK_GREEN: Self = Self::new(0, 160, 0);
|
||||
pub const DARK_BLUE: Self = Self::new(0, 0, 160);
|
||||
|
||||
pub const DARK_YELLOW: Self = Self::new(160, 160, 0);
|
||||
pub const DARK_MAGENTA: Self = Self::new(160, 0, 160);
|
||||
pub const DARK_CYAN: Self = Self::new(0, 160, 160);
|
||||
|
||||
pub const LIGHT_RED: Self = Self::new(255, 0, 0);
|
||||
pub const LIGHT_GREEN: Self = Self::new(0, 255, 0);
|
||||
pub const LIGHT_BLUE: Self = Self::new(0, 0, 255);
|
||||
|
||||
pub const LIGHT_YELLOW: Self = Self::new(255, 255, 0);
|
||||
pub const LIGHT_MAGENTA: Self = Self::new(255, 0, 255);
|
||||
pub const LIGHT_CYAN: Self = Self::new(0, 255, 255);
|
||||
|
||||
pub const fn new(r: u8, g: u8, b: u8) -> Self {
|
||||
Self { r, g, b }
|
||||
}
|
||||
|
||||
pub const fn to_u32(self) -> u32 {
|
||||
0xFF000000 | ((self.r as u32) << 16) | ((self.g as u32) << 8) | (self.b as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn from_esc(v: u32) -> Self {
|
||||
match v {
|
||||
0 => Self::Black,
|
||||
1 => Self::Red,
|
||||
2 => Self::Green,
|
||||
3 => Self::Yellow,
|
||||
4 => Self::Blue,
|
||||
5 => Self::Magenta,
|
||||
6 => Self::Cyan,
|
||||
7 => Self::White,
|
||||
_ => Self::Black,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_display(self, bright: bool) -> DisplayColor {
|
||||
if bright {
|
||||
BRIGHT_COLOR_MAP[self as usize]
|
||||
} else {
|
||||
DIM_COLOR_MAP[self as usize]
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn to_rgba(self, bright: bool) -> SolidSource {
|
||||
// if bright {
|
||||
// BRIGHT_COLOR_MAP[self as usize]
|
||||
// } else {
|
||||
// COLOR_MAP[self as usize]
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
const DIM_COLOR_MAP: &[DisplayColor] = &[
|
||||
DisplayColor::BLACK,
|
||||
DisplayColor::DARK_RED,
|
||||
DisplayColor::DARK_GREEN,
|
||||
DisplayColor::DARK_YELLOW,
|
||||
DisplayColor::DARK_BLUE,
|
||||
DisplayColor::DARK_MAGENTA,
|
||||
DisplayColor::DARK_CYAN,
|
||||
DisplayColor::LIGHT_GRAY,
|
||||
];
|
||||
|
||||
const BRIGHT_COLOR_MAP: &[DisplayColor] = &[
|
||||
DisplayColor::DARK_GRAY,
|
||||
DisplayColor::LIGHT_RED,
|
||||
DisplayColor::LIGHT_GREEN,
|
||||
DisplayColor::LIGHT_YELLOW,
|
||||
DisplayColor::LIGHT_BLUE,
|
||||
DisplayColor::LIGHT_MAGENTA,
|
||||
DisplayColor::LIGHT_CYAN,
|
||||
DisplayColor::WHITE,
|
||||
];
|
7
userspace/term/src/error.rs
Normal file
7
userspace/term/src/error.rs
Normal file
@ -0,0 +1,7 @@
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Application error: {0:?}")]
|
||||
ApplicationError(#[from] libcolors::error::Error),
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
74
userspace/term/src/font.rs
Normal file
74
userspace/term/src/font.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use std::mem::size_of;
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
struct Align<T, D: ?Sized> {
|
||||
_align: T,
|
||||
data: D,
|
||||
}
|
||||
|
||||
static FONT_DATA: &Align<u32, [u8]> = &Align {
|
||||
_align: 0,
|
||||
data: *include_bytes!("../../etc/fonts/regular.psfu"),
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Pod, Zeroable, Clone, Copy)]
|
||||
struct PsfHeader {
|
||||
magic: u32,
|
||||
version: u32,
|
||||
header_size: u32,
|
||||
flags: u32,
|
||||
num_glyph: u32,
|
||||
bytes_per_glyph: u32,
|
||||
height: u32,
|
||||
width: u32,
|
||||
}
|
||||
|
||||
/// Represents a PSF-format font object
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PcScreenFont<'a> {
|
||||
header: &'a PsfHeader,
|
||||
glyph_data: &'a [u8],
|
||||
}
|
||||
|
||||
impl Default for PcScreenFont<'static> {
|
||||
fn default() -> Self {
|
||||
Self::from_bytes(&FONT_DATA.data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PcScreenFont<'a> {
|
||||
/// Constructs an instance of [PcScreenFont] from its byte representation
|
||||
pub fn from_bytes(bytes: &'a [u8]) -> Self {
|
||||
let header: &PsfHeader = bytemuck::from_bytes(&bytes[..size_of::<PsfHeader>()]);
|
||||
let glyph_data = &bytes[header.header_size as usize..];
|
||||
|
||||
Self { header, glyph_data }
|
||||
}
|
||||
|
||||
/// Returns the character width of the font
|
||||
#[inline]
|
||||
pub const fn width(&self) -> u32 {
|
||||
self.header.width
|
||||
}
|
||||
|
||||
/// Returns the character height of the font
|
||||
#[inline]
|
||||
pub const fn height(&self) -> u32 {
|
||||
self.header.height
|
||||
}
|
||||
|
||||
/// Returns the count of glyphs present in the font
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
#[inline]
|
||||
pub const fn len(&self) -> u32 {
|
||||
self.header.num_glyph
|
||||
}
|
||||
|
||||
/// Returns the data slice of a single glyph within the font
|
||||
#[inline]
|
||||
pub fn raw_glyph_data(&self, index: u32) -> &[u8] {
|
||||
&self.glyph_data[(index * self.header.bytes_per_glyph) as usize..]
|
||||
}
|
||||
}
|
350
userspace/term/src/main.rs
Normal file
350
userspace/term/src/main.rs
Normal file
@ -0,0 +1,350 @@
|
||||
#![feature(yggdrasil_os, rustc_private)]
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
os::{
|
||||
fd::{AsRawFd, FromRawFd, RawFd},
|
||||
yggdrasil::{
|
||||
io::{
|
||||
device::{DeviceRequest, FdDeviceRequest},
|
||||
poll::PollChannel,
|
||||
terminal::{create_pty, TerminalOptions, TerminalSize},
|
||||
},
|
||||
process::CommandExt,
|
||||
},
|
||||
},
|
||||
process::{Child, Command, ExitCode, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
use error::Error;
|
||||
use font::PcScreenFont;
|
||||
use libcolors::{
|
||||
application::{
|
||||
window::{EventOutcome, Window},
|
||||
Application,
|
||||
},
|
||||
event::{KeyModifiers, KeyboardKey},
|
||||
};
|
||||
use state::{Cursor, State};
|
||||
|
||||
pub mod attr;
|
||||
pub mod error;
|
||||
pub mod font;
|
||||
pub mod state;
|
||||
|
||||
struct DrawState {
|
||||
width: usize,
|
||||
force_redraw: bool,
|
||||
focus_changed: bool,
|
||||
focused: bool,
|
||||
old_cursor: Cursor,
|
||||
|
||||
font: PcScreenFont<'static>,
|
||||
}
|
||||
|
||||
pub struct Terminal<'a> {
|
||||
poll: PollChannel,
|
||||
conn_fd: RawFd,
|
||||
application: Application<'a>,
|
||||
state: Arc<Mutex<State>>,
|
||||
|
||||
pty_master: Arc<Mutex<File>>,
|
||||
pty_master_fd: RawFd,
|
||||
#[allow(unused)]
|
||||
shell: Child,
|
||||
}
|
||||
|
||||
impl DrawState {
|
||||
pub fn new(font: PcScreenFont<'static>, width: usize) -> Self {
|
||||
Self {
|
||||
width,
|
||||
force_redraw: true,
|
||||
focus_changed: false,
|
||||
focused: true,
|
||||
old_cursor: Cursor { row: 0, col: 0 },
|
||||
|
||||
font,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, dt: &mut [u32], state: &mut State) {
|
||||
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 fw = self.font.width() as usize;
|
||||
let fh = self.font.height() as usize;
|
||||
|
||||
let cursor_dirty = self.old_cursor != state.cursor;
|
||||
|
||||
if self.force_redraw {
|
||||
dt.fill(default_bg);
|
||||
}
|
||||
|
||||
if cursor_dirty {
|
||||
state.buffer.set_row_dirty(self.old_cursor.row);
|
||||
}
|
||||
|
||||
let bytes_per_line = (self.font.width() as usize + 7) / 8;
|
||||
for (i, row) in state.buffer.dirty_rows() {
|
||||
let cy = i * fh;
|
||||
|
||||
for (j, cell) in row.cells().enumerate() {
|
||||
let bg = cell.attrs.bg.to_display(false).to_u32();
|
||||
let fg = cell.attrs.fg.to_display(cell.attrs.bright).to_u32();
|
||||
|
||||
let cx = j * fw;
|
||||
|
||||
// Fill cell
|
||||
for y in 0..fh {
|
||||
let off = (cy + y) * self.width + cx;
|
||||
dt[off..off + fw].fill(bg);
|
||||
}
|
||||
|
||||
if cell.char == '\0' {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Draw character
|
||||
let mut c = cell.char as u32;
|
||||
if c >= self.font.len() {
|
||||
c = b'?' as u32;
|
||||
}
|
||||
let mut glyph = self.font.raw_glyph_data(c);
|
||||
let mut y = 0;
|
||||
|
||||
while y < self.font.height() as usize {
|
||||
let mut mask = 1 << (self.font.width() - 1);
|
||||
let mut x = 0;
|
||||
let row_off = (cy + y) * self.width;
|
||||
|
||||
while x < self.font.width() as usize {
|
||||
let v = if glyph[0] & mask != 0 { fg } else { bg };
|
||||
dt[row_off + cx + x] = v;
|
||||
mask >>= 1;
|
||||
x += 1;
|
||||
}
|
||||
glyph = &glyph[bytes_per_line..];
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check if there's a character under cursor
|
||||
let cx = state.cursor.col * fw;
|
||||
let cy = state.cursor.row * fh;
|
||||
|
||||
// Fill block cursor
|
||||
for y in 0..fh {
|
||||
let off = (cy + y) * self.width + cx;
|
||||
dt[off..off + fw].fill(default_fg);
|
||||
}
|
||||
|
||||
if !self.focused {
|
||||
// Remove cursor center
|
||||
for y in 1..fh - 1 {
|
||||
let off = (cy + y) * self.width + cx + 1;
|
||||
dt[off..off + fw - 2].fill(default_bg);
|
||||
}
|
||||
}
|
||||
|
||||
self.old_cursor = state.cursor;
|
||||
self.force_redraw = false;
|
||||
self.focus_changed = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Terminal<'a> {
|
||||
pub fn new(font: PcScreenFont<'static>) -> Result<Self, Error> {
|
||||
let mut app = Application::new()?;
|
||||
let mut window = Window::new(&app)?;
|
||||
let mut poll = PollChannel::new()?;
|
||||
|
||||
let width = window.width() as usize;
|
||||
let height = window.height() as usize;
|
||||
|
||||
let rows = height / font.height() as usize;
|
||||
let columns = width / font.width() as usize;
|
||||
|
||||
let termios = TerminalOptions::default();
|
||||
let (pty_master, pty_slave) = create_pty(Some(termios), TerminalSize { rows, columns })?;
|
||||
let pty_master_fd = pty_master.as_raw_fd();
|
||||
|
||||
// TODO I hate this
|
||||
let pty_master = Arc::new(Mutex::new(pty_master));
|
||||
let state = Arc::new(Mutex::new(State::new(columns, rows)));
|
||||
let draw_state = Arc::new(Mutex::new(DrawState::new(font, width)));
|
||||
|
||||
let state_c = state.clone();
|
||||
let draw_state_c = draw_state.clone();
|
||||
let pty_master_c = pty_master.clone();
|
||||
window.set_on_resized(move |width, height| {
|
||||
let mut s = state_c.lock().unwrap();
|
||||
let mut ds = draw_state_c.lock().unwrap();
|
||||
let pty_master = pty_master_c.lock().unwrap();
|
||||
|
||||
let width = width as usize;
|
||||
let height = height as usize;
|
||||
let rows = height / ds.font.height() as usize;
|
||||
let columns = width / ds.font.width() as usize;
|
||||
|
||||
let term_size = TerminalSize { rows, columns };
|
||||
unsafe {
|
||||
pty_master
|
||||
.device_request(&mut DeviceRequest::SetTerminalSize(term_size))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
s.resize(
|
||||
width / ds.font.width() as usize,
|
||||
height / ds.font.height() as usize,
|
||||
);
|
||||
ds.width = width;
|
||||
ds.force_redraw = true;
|
||||
|
||||
EventOutcome::Redraw
|
||||
});
|
||||
|
||||
let pty_master_c = pty_master.clone();
|
||||
window.set_on_key_input(move |ev| {
|
||||
let mut pty_master = pty_master_c.lock().unwrap();
|
||||
|
||||
// TODO error reporting from handlers
|
||||
if let Some(input) = ev.input {
|
||||
pty_master.write_all(&[input as u8]).unwrap();
|
||||
} else {
|
||||
match (ev.modifiers, ev.key) {
|
||||
(KeyModifiers::NONE, KeyboardKey::Escape) => {
|
||||
pty_master.write_all(&[b'\x1B']).unwrap();
|
||||
}
|
||||
(KeyModifiers::NONE, KeyboardKey::Backspace) => {
|
||||
pty_master.write_all(&[termios.chars.erase]).unwrap();
|
||||
}
|
||||
(KeyModifiers::CTRL, KeyboardKey::Char(b'c')) => {
|
||||
pty_master.write_all(&[termios.chars.interrupt]).unwrap();
|
||||
}
|
||||
(KeyModifiers::CTRL, KeyboardKey::Char(b'd')) => {
|
||||
pty_master.write_all(&[termios.chars.eof]).unwrap();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
EventOutcome::None
|
||||
});
|
||||
|
||||
let draw_state_c = draw_state.clone();
|
||||
window.set_on_focus_changed(move |focused| {
|
||||
let mut ds = draw_state_c.lock().unwrap();
|
||||
|
||||
ds.focused = focused;
|
||||
ds.focus_changed = true;
|
||||
|
||||
EventOutcome::Redraw
|
||||
});
|
||||
|
||||
let state_c = state.clone();
|
||||
window.set_on_redraw_requested(move |dt: &mut [u32]| {
|
||||
let mut s = state_c.lock().unwrap();
|
||||
let mut ds = draw_state.lock().unwrap();
|
||||
|
||||
ds.draw(dt, &mut s);
|
||||
});
|
||||
|
||||
app.add_window(window);
|
||||
|
||||
let conn_fd = app.connection().lock().unwrap().as_poll_fd();
|
||||
|
||||
poll.add(conn_fd)?;
|
||||
poll.add(pty_master_fd)?;
|
||||
|
||||
let pty_slave_fd = pty_slave.as_raw_fd();
|
||||
let shell = unsafe {
|
||||
Command::new("/bin/sh")
|
||||
.arg("-l")
|
||||
.stdin(Stdio::from_raw_fd(pty_slave_fd))
|
||||
.stdout(Stdio::from_raw_fd(pty_slave_fd))
|
||||
.stderr(Stdio::from_raw_fd(pty_slave_fd))
|
||||
.process_group_raw(0)
|
||||
.gain_terminal(0)
|
||||
.spawn()?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
poll,
|
||||
conn_fd,
|
||||
state,
|
||||
application: app,
|
||||
|
||||
pty_master,
|
||||
pty_master_fd,
|
||||
shell,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_shell(&mut self) -> Result<bool, Error> {
|
||||
let mut buf = [0; 8192];
|
||||
let mut pty = self.pty_master.lock().unwrap();
|
||||
let len = pty.read(&mut buf)?;
|
||||
|
||||
if len == 0 {
|
||||
ABORT.store(true, Ordering::Release);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut needs_redraw = false;
|
||||
let mut s = self.state.lock().unwrap();
|
||||
|
||||
for &ch in &buf[..len] {
|
||||
needs_redraw |= s.handle_shell_output(ch);
|
||||
}
|
||||
|
||||
Ok(needs_redraw)
|
||||
}
|
||||
|
||||
fn run_inner(mut self) -> Result<ExitCode, Error> {
|
||||
while !ABORT.load(Ordering::Acquire) {
|
||||
match self.poll.wait(None)? {
|
||||
Some((fd, Ok(_))) if fd == self.conn_fd => {
|
||||
self.application.poll_events()?;
|
||||
}
|
||||
Some((fd, Ok(_))) if fd == self.pty_master_fd => {
|
||||
let needs_redraw = self.handle_shell()?;
|
||||
|
||||
if needs_redraw {
|
||||
self.application.redraw()?;
|
||||
}
|
||||
}
|
||||
Some((_, Ok(_))) => {
|
||||
todo!()
|
||||
}
|
||||
Some((_, Err(error))) => return Err(Error::from(error)),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
pub fn run(self) -> ExitCode {
|
||||
match self.run_inner() {
|
||||
Ok(code) => code,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {}", error);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static ABORT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let font = PcScreenFont::default();
|
||||
let term = Terminal::new(font).unwrap();
|
||||
|
||||
term.run()
|
||||
}
|
373
userspace/term/src/state.rs
Normal file
373
userspace/term/src/state.rs
Normal file
@ -0,0 +1,373 @@
|
||||
use crate::attr::{CellAttributes, Color};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GridCell {
|
||||
pub char: char,
|
||||
pub attrs: CellAttributes,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GridRow {
|
||||
cols: Vec<GridCell>,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
rows: Vec<GridRow>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Cursor {
|
||||
pub row: usize,
|
||||
pub col: usize,
|
||||
}
|
||||
|
||||
enum EscapeState {
|
||||
Normal,
|
||||
Escape,
|
||||
Csi,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
pub buffer: Buffer,
|
||||
|
||||
esc_state: EscapeState,
|
||||
esc_args: Vec<u32>,
|
||||
|
||||
pub cursor: Cursor,
|
||||
#[allow(unused)]
|
||||
saved_cursor: Option<Cursor>,
|
||||
|
||||
pub default_attributes: CellAttributes,
|
||||
pub attributes: CellAttributes,
|
||||
}
|
||||
|
||||
impl GridCell {
|
||||
pub fn new(char: char, attrs: CellAttributes) -> Self {
|
||||
Self { char, attrs }
|
||||
}
|
||||
|
||||
pub fn empty(bg: Color) -> Self {
|
||||
Self {
|
||||
char: '\0',
|
||||
attrs: CellAttributes {
|
||||
fg: Color::Black,
|
||||
bg,
|
||||
bright: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GridRow {
|
||||
pub fn new(width: usize, bg: Color) -> Self {
|
||||
Self {
|
||||
cols: vec![GridCell::empty(bg); width],
|
||||
dirty: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_cell(&mut self, col: usize, char: char, attrs: CellAttributes) {
|
||||
self.cols[col] = GridCell::new(char, attrs);
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
|
||||
pub fn clear_dirty(&mut self) {
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
pub fn cells(&self) -> impl Iterator<Item = &GridCell> {
|
||||
self.cols.iter()
|
||||
}
|
||||
|
||||
fn clear(&mut self, bg: Color) {
|
||||
self.cols.fill(GridCell::empty(bg));
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
fn erase_to_right(&mut self, start: usize, bg: Color) {
|
||||
self.cols[start..].fill(GridCell::empty(bg));
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
fn resize(&mut self, width: usize, bg: Color) {
|
||||
self.cols.resize(width, GridCell::empty(bg));
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new(width: usize, height: usize, bg: Color) -> Self {
|
||||
Self {
|
||||
rows: vec![GridRow::new(width, bg); height],
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, bg: Color) {
|
||||
self.rows.fill(GridRow::new(self.width, bg));
|
||||
}
|
||||
|
||||
pub fn dirty_rows(&mut self) -> impl Iterator<Item = (usize, &mut GridRow)> {
|
||||
self.rows.iter_mut().enumerate().filter_map(|(i, row)| {
|
||||
if row.dirty {
|
||||
row.dirty = false;
|
||||
Some((i, row))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: usize, height: usize, bg: Color) {
|
||||
self.rows.resize(height, GridRow::new(width, bg));
|
||||
for row in self.rows.iter_mut() {
|
||||
row.resize(width, bg);
|
||||
}
|
||||
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
pub fn set_cell(&mut self, cur: Cursor, cell: GridCell) {
|
||||
self.rows[cur.row].cols[cur.col] = cell;
|
||||
self.rows[cur.row].dirty = true;
|
||||
}
|
||||
|
||||
pub fn scroll_once(&mut self, bg: Color) {
|
||||
for i in 1..self.height {
|
||||
self.rows[i - 1] = self.rows[i].clone();
|
||||
self.rows[i - 1].dirty = true;
|
||||
}
|
||||
self.rows[self.height - 1] = GridRow::new(self.width, bg);
|
||||
}
|
||||
|
||||
pub fn erase_row(&mut self, row: usize, bg: Color) {
|
||||
self.rows[row].clear(bg);
|
||||
}
|
||||
|
||||
pub fn set_row_dirty(&mut self, row: usize) {
|
||||
self.rows[row].dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
let default_attributes = CellAttributes {
|
||||
fg: Color::White,
|
||||
bg: Color::Black,
|
||||
bright: false,
|
||||
};
|
||||
|
||||
Self {
|
||||
buffer: Buffer::new(width, height, default_attributes.bg),
|
||||
|
||||
esc_args: Vec::new(),
|
||||
esc_state: EscapeState::Normal,
|
||||
|
||||
cursor: Cursor { row: 0, col: 0 },
|
||||
saved_cursor: None,
|
||||
|
||||
default_attributes,
|
||||
attributes: default_attributes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: usize, height: usize) {
|
||||
self.buffer
|
||||
.resize(width, height, self.default_attributes.bg);
|
||||
|
||||
if self.cursor.row >= height {
|
||||
self.cursor.row = height - 1;
|
||||
}
|
||||
|
||||
if self.cursor.col >= width {
|
||||
self.cursor.col = width - 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn putc_normal(&mut self, ch: u8) -> bool {
|
||||
let mut redraw = match ch {
|
||||
c if c >= 127 => {
|
||||
let attr = CellAttributes {
|
||||
fg: Color::Black,
|
||||
bg: Color::Red,
|
||||
bright: false,
|
||||
};
|
||||
self.buffer.set_cell(self.cursor, GridCell::new('?', attr));
|
||||
self.cursor.col += 1;
|
||||
true
|
||||
}
|
||||
b'\x1B' => {
|
||||
self.esc_state = EscapeState::Escape;
|
||||
self.esc_args.clear();
|
||||
self.esc_args.push(0);
|
||||
return false;
|
||||
}
|
||||
b'\r' => {
|
||||
self.buffer.rows[self.cursor.row].dirty = true;
|
||||
self.cursor.col = 0;
|
||||
true
|
||||
}
|
||||
b'\n' => {
|
||||
self.buffer.rows[self.cursor.row].dirty = true;
|
||||
self.cursor.row += 1;
|
||||
self.cursor.col = 0;
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
self.buffer
|
||||
.set_cell(self.cursor, GridCell::new(ch as char, self.attributes));
|
||||
self.cursor.col += 1;
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if self.cursor.col >= self.buffer.width {
|
||||
if self.cursor.row < self.buffer.height {
|
||||
self.buffer.rows[self.cursor.row].dirty = true;
|
||||
}
|
||||
|
||||
self.cursor.row += 1;
|
||||
self.cursor.col = 0;
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
while self.cursor.row >= self.buffer.height {
|
||||
self.buffer.scroll_once(self.default_attributes.bg);
|
||||
self.cursor.row -= 1;
|
||||
redraw = true;
|
||||
}
|
||||
|
||||
redraw
|
||||
}
|
||||
|
||||
fn handle_ctlseq(&mut self, c: u8) -> bool {
|
||||
let redraw = match c {
|
||||
// Move back one character
|
||||
b'D' => {
|
||||
if self.cursor.col > 0 {
|
||||
self.buffer.set_row_dirty(self.cursor.row);
|
||||
self.cursor.col -= 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
// Character attributes
|
||||
b'm' => match self.esc_args[0] {
|
||||
0 => {
|
||||
self.attributes = self.default_attributes;
|
||||
false
|
||||
}
|
||||
1 => {
|
||||
self.attributes.bright = true;
|
||||
false
|
||||
}
|
||||
30..=39 => {
|
||||
let vt_color = self.esc_args[0] % 10;
|
||||
if vt_color == 9 {
|
||||
self.attributes.fg = Color::Black;
|
||||
} else {
|
||||
self.attributes.fg = Color::from_esc(vt_color);
|
||||
}
|
||||
false
|
||||
}
|
||||
40..=49 => {
|
||||
let vt_color = self.esc_args[0] % 10;
|
||||
if vt_color == 9 {
|
||||
self.attributes.bg = Color::Black;
|
||||
} else {
|
||||
self.attributes.bg = Color::from_esc(vt_color);
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
// Move cursor to position
|
||||
b'f' => {
|
||||
let row = self.esc_args[0].clamp(1, self.buffer.height as u32) - 1;
|
||||
let col = self.esc_args[1].clamp(1, self.buffer.width as u32) - 1;
|
||||
|
||||
self.buffer.set_row_dirty(self.cursor.row);
|
||||
self.cursor = Cursor {
|
||||
row: row as _,
|
||||
col: col as _,
|
||||
};
|
||||
|
||||
true
|
||||
}
|
||||
// Clear rows/columns/screen
|
||||
b'J' => match self.esc_args[0] {
|
||||
// Erase lines down
|
||||
0 => false,
|
||||
// Erase lines up
|
||||
1 => false,
|
||||
// Erase all
|
||||
2 => {
|
||||
self.buffer.clear(self.attributes.bg);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
b'K' => match self.esc_args[0] {
|
||||
// Erase to right
|
||||
0 => {
|
||||
self.buffer.rows[self.cursor.row]
|
||||
.erase_to_right(self.cursor.col, self.attributes.bg);
|
||||
true
|
||||
}
|
||||
// Erase All
|
||||
2 => {
|
||||
self.buffer.erase_row(self.cursor.row, self.attributes.bg);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
|
||||
self.esc_state = EscapeState::Normal;
|
||||
redraw
|
||||
}
|
||||
|
||||
fn handle_ctlseq_byte(&mut self, c: u8) -> bool {
|
||||
match c {
|
||||
b'0'..=b'9' => {
|
||||
let arg = self.esc_args.last_mut().unwrap();
|
||||
*arg *= 10;
|
||||
*arg += (c - b'0') as u32;
|
||||
false
|
||||
}
|
||||
b';' => {
|
||||
self.esc_args.push(0);
|
||||
false
|
||||
}
|
||||
_ => self.handle_ctlseq(c),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_shell_output(&mut self, ch: u8) -> bool {
|
||||
match self.esc_state {
|
||||
EscapeState::Normal => self.putc_normal(ch),
|
||||
EscapeState::Escape => match ch {
|
||||
b'[' => {
|
||||
self.esc_state = EscapeState::Csi;
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
self.esc_state = EscapeState::Normal;
|
||||
false
|
||||
}
|
||||
},
|
||||
EscapeState::Csi => self.handle_ctlseq_byte(ch),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user