cli arguments, clippy passes

This commit is contained in:
mtgmonkey 2025-07-08 07:53:14 -04:00
parent b4da358591
commit a100c9be52
8 changed files with 528 additions and 958 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target
result

821
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,4 +5,23 @@ edition = "2024"
[dependencies]
nix = { version = "0.30.1", features = ["term", "process", "fs"], default-features = false }
iced = { version = "0.13.1", features = ["advanced", "smol"] }
iced = { version = "0.13.1", features = ["advanced", "smol", "wgpu"], default-features = false }
bpaf = { version = "0.9.20", features = ["derive"], default-features = false }
[lints.clippy]
cargo = "deny"
complexity = "deny"
correctness = "deny"
nursery = "deny"
pedantic = "deny"
perf = "deny"
restriction = "deny"
style = "deny"
suspicious = "deny"
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

6
flake.lock generated
View file

@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1751271578,
"narHash": "sha256-P/SQmKDu06x8yv7i0s8bvnnuJYkxVGBWLWHaU+tt4YY=",
"lastModified": 1751792365,
"narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3016b4b15d13f3089db8a41ef937b13a9e33a8df",
"rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb",
"type": "github"
},
"original": {

View file

@ -13,12 +13,19 @@
}: let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
in rec {
packages.${system} = {
default = pkgs.callPackage ./package.nix {
naersk = pkgs.callPackage naersk {};
};
};
# nixosModules.${system}.default = ./module.nix;
devShells.${system}.default = pkgs.mkShell {
nativeBuildInputs =
[
pkgs.clippy
]
++ packages.${system}.default.buildInputs
++ packages.${system}.default.nativeBuildInputs;
};
};
}

View file

@ -9,6 +9,7 @@
makeWrapper,
naersk,
pkg-config,
upx,
wayland,
xorg,
...
@ -33,8 +34,10 @@ naersk.buildPackage rec {
nativeBuildInputs = [
pkg-config
makeWrapper
upx
];
postInstall = ''
upx --lzma $out/bin/${meta.mainProgram}
wrapProgram "$out/bin/${meta.mainProgram}" --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath buildInputs}"
'';
meta = {

View file

@ -1,191 +1,455 @@
use iced::widget::text_input::Id;
use iced::widget::{column, row, scrollable, text, text_input};
use iced::{Element, Font, Task, keyboard};
//! this crate runs a terminal
#![expect(
clippy::needless_return,
clippy::shadow_reuse,
clippy::blanket_clippy_restriction_lints,
clippy::must_use_candidate,
clippy::missing_trait_methods,
clippy::pattern_type_mismatch,
clippy::std_instead_of_alloc,
clippy::cargo_common_metadata,
clippy::multiple_crate_versions,
clippy::semicolon_outside_block,
static_mut_refs,
unused_doc_comments,
reason = ""
)]
use bpaf::Bpaf;
use iced::widget::{column, rich_text, row, scrollable, span, text};
use iced::{Element, Task, keyboard, time, window};
use nix::errno::Errno;
use nix::fcntl;
use nix::pty::{ForkptyResult, forkpty};
use nix::unistd::write;
use std::fs::File;
use std::io::{Read, Write};
use std::os::unix::io::{AsFd, OwnedFd};
use std::io::{self, Read as _};
use std::os::unix::io::{AsFd as _, OwnedFd};
use std::process::Command;
use std::{error, fmt, thread, time as core_time};
fn spawn_pty_with_shell(default_shell: String) -> OwnedFd {
match unsafe { forkpty(None, None) } {
Ok(fork_pty_res) => match fork_pty_res {
ForkptyResult::Parent { master, .. } => {
set_nonblock(&master);
master
}
ForkptyResult::Child => {
Command::new(&default_shell).spawn().unwrap();
std::thread::sleep(std::time::Duration::MAX);
std::process::exit(0);
}
},
Err(e) => panic!("failed to fork {:?}", e),
}
}
/// whether to enable verbose logging; see `Flags::verbose`
static mut VERBOSE: bool = false;
fn read_from_fd(fd: &OwnedFd) -> Option<Vec<u8>> {
let mut read_buffer = [0; 65536];
let mut file = File::from(fd.try_clone().unwrap());
file.flush();
let file = file.read(&mut read_buffer);
match file {
Ok(file) => Some(read_buffer[..file].to_vec()),
Err(_) => None,
}
}
fn set_nonblock(fd: &OwnedFd) {
let flags = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_GETFL).unwrap();
let mut flags =
nix::fcntl::OFlag::from_bits(flags & nix::fcntl::OFlag::O_ACCMODE.bits()).unwrap();
flags.set(nix::fcntl::OFlag::O_NONBLOCK, true);
nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_SETFL(flags)).unwrap();
}
/// shell path; see `Flags::shell`
static mut SHELL: Option<String> = None;
/// events to be passed to `Model::update`
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum Msg {
Exit,
KeyPressed(keyboard::Key),
Tick,
KeyPressed(iced::keyboard::Key),
}
/// errors for this program
#[non_exhaustive]
#[derive(Debug)]
enum Error {
/// out of bounds err while accessing a slice
IndexOutOfBounds,
/// io error
Io(io::Error),
/// nix crate error
Nix(NixError),
/// try to access a `File::from::<OwnedFd>()` without an `OwnedFd`
NoFileDescriptor,
/// impossible error
Unreachable,
}
impl fmt::Display for Error {
#[expect(clippy::min_ident_chars, reason = "it's in the docs like that")]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Nix(nix_error) => return write!(f, "{nix_error}"),
Self::Io(io_error) => return write!(f, "{io_error}"),
Self::NoFileDescriptor => return write!(f, "no file descriptor specified"),
Self::IndexOutOfBounds => return write!(f, "index out of bounds"),
Self::Unreachable => return write!(f, "unreachable error, panic"),
}
}
}
impl error::Error for Error {}
/// error wrapper for the `nix` crate
#[non_exhaustive]
#[derive(Debug)]
enum NixError {
/// an OS error
Errno(Errno),
/// the error when `OFlags::from_bits(..)` returns `None`
UnrecognisedFlag,
}
impl fmt::Display for NixError {
#[expect(clippy::min_ident_chars, reason = "it's in the docs like that")]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::UnrecognisedFlag => return write!(f, "unrecognised flag"),
Self::Errno(errno) => return write!(f, "bad fcntl argument. errno: {errno}"),
}
}
}
/// cli flags
#[derive(Debug, Clone, Bpaf)]
#[bpaf(options)]
pub struct Flags {
/// path to shell
#[bpaf(short('S'), long)]
shell: Option<String>,
/// whether to debug log
#[bpaf(short, long)]
verbose: bool,
/// whether to display version: TODO
#[expect(dead_code, reason = "TODO")]
#[bpaf(short, long)]
version: bool,
}
/// represents the terminal emulator\
/// example usage:
/// ```rust
/// iced::application("window title", Model::update, Model::view)
/// .theme(Model::theme)
/// .default_font(iced::Font::MONOSPACE)
/// .decorations(false)
/// .subscription(Model::subscription)
/// .run()
/// ```
pub struct Model {
screen_buffer: [u8; 65536],
screen_buffer_index: usize,
/// location of cursor in user input line
cursor_index: usize,
fd: OwnedFd,
stdin: OwnedFd,
/// fd of pty
fd: Option<OwnedFd>,
/// user input line
input: String,
/// all chars on screen
screen_buffer: [u8; 0x4000],
/// length of `screen_buffer`'s filled area
screen_buffer_index: usize,
/// path to shell
shell: String,
}
impl Model {
fn new(
screen_buffer: [u8; 65536],
screen_buffer_index: usize,
cursor_index: usize,
fd: OwnedFd,
stdin: OwnedFd,
input: String,
) -> Self {
Model {
screen_buffer,
screen_buffer_index,
cursor_index,
fd,
stdin,
input,
}
}
pub fn update(&mut self, msg: Msg) -> Task<Msg> {
match msg {
Msg::Tick => match read_from_fd(&self.fd) {
Some(red) => self.update_screen_buffer(&red),
None => (),
},
Msg::KeyPressed(key) => match key {
keyboard::Key::Character(c) => {
self.input_char(c.chars().nth(0).unwrap());
}
keyboard::Key::Named(keyboard::key::Named::Enter) => {
self.input.push('\n');
let mut write_buffer = self.input.as_bytes().to_vec();
write(self.fd.as_fd(), &mut write_buffer);
self.input = String::new();
self.cursor_index = 0;
}
keyboard::Key::Named(keyboard::key::Named::Space) => {
self.input_char(' ');
}
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
if self.cursor_index <= 0 {
self.cursor_index = 0;
} else {
self.cursor_index -= 1;
}
}
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
if self.cursor_index >= self.input.len() - 1 {
self.cursor_index = self.input.len() - 1;
} else {
self.cursor_index += 1;
}
}
_ => (),
},
};
iced::Task::<Msg>::none()
}
pub fn view(&self) -> Element<'_, Msg> {
let (left, right) =
match String::from_utf8(self.screen_buffer[..self.screen_buffer_index].to_vec())
.unwrap()
.rsplit_once('\n')
{
Some(tup) => (tup.0.to_string(), tup.1.to_string()),
None => (
String::new(),
String::from_utf8(self.screen_buffer[..self.screen_buffer_index].to_vec())
.unwrap()
.to_string(),
),
};
scrollable(column![text(left), row![text(right), text(&self.input),]]).into()
}
pub fn theme(&self) -> iced::Theme {
iced::Theme::GruvboxDark
}
pub fn subscription(&self) -> iced::Subscription<Msg> {
let tick = iced::time::every(iced::time::Duration::new(0, 1)).map(|_| Msg::Tick);
let key = keyboard::on_key_press(|key, _| Some(Msg::KeyPressed(key)));
iced::Subscription::batch(vec![tick, key])
}
fn update_screen_buffer(&mut self, vec: &Vec<u8>) {
let offset = self.screen_buffer_index;
for (i, chr) in vec.iter().enumerate() {
self.screen_buffer[i + offset] = chr.clone();
self.screen_buffer_index += 1;
}
}
fn input_char(&mut self, c: char) {
/// applies needed side effects when taking an input char
#[expect(
clippy::arithmetic_side_effects,
reason = "cursor_index is bound checked"
)]
fn input_char(&mut self, chr: char) {
if self.cursor_index == self.input.len() {
self.input.push_str(c.to_string().as_str());
self.input.push_str(chr.to_string().as_str());
} else {
self.input.insert(self.cursor_index, c);
self.input.insert(self.cursor_index, chr);
}
self.cursor_index += 1;
}
/// subscription logic for model
#[inline]
pub fn subscription(&self) -> iced::Subscription<Msg> {
let tick = time::every(time::Duration::new(0, 1)).map(|_| {
return Msg::Tick;
});
let key = keyboard::on_key_press(|key, _| {
return Some(Msg::KeyPressed(key));
});
return iced::Subscription::batch(vec![tick, key]);
}
/// theme logic for model
#[inline]
pub const fn theme(&self) -> iced::Theme {
return iced::Theme::GruvboxDark;
}
/// update logic for model
/// TODO fix pattern type mismatch
/// TODO add more keys
#[inline]
#[expect(
clippy::arithmetic_side_effects,
clippy::wildcard_enum_match_arm,
reason = "bounds checked"
)]
pub fn update(&mut self, msg: Msg) -> Task<Msg> {
match msg {
Msg::Exit => return window::get_latest().and_then(window::close),
Msg::KeyPressed(key) => {
match key {
keyboard::Key::Character(chr) => match chr.chars().nth(0) {
Some(chr) => self.input_char(chr),
None => return window::get_latest().and_then(window::close),
},
keyboard::Key::Named(keyboard::key::Named::Enter) => {
self.input.push('\n');
let write_buffer = self.input.as_bytes().to_vec();
if let Some(fd) = &self.fd {
match write(fd.as_fd(), &write_buffer) {
Ok(_) => (),
Err(error) => print_err(&Error::Nix(NixError::Errno(error))),
}
}
self.input = String::new();
self.cursor_index = 0;
}
keyboard::Key::Named(keyboard::key::Named::Space) => {
self.input_char(' ');
}
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
if self.cursor_index == 0 {
self.cursor_index = 0;
} else {
self.cursor_index -= 1;
}
}
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
if self.cursor_index >= self.input.len() {
self.cursor_index = self.input.len();
} else {
self.cursor_index += 1;
}
}
keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
self.cursor_index = 0;
}
keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
self.cursor_index = self.input.len();
}
_ => (),
}
return iced::Task::none();
}
Msg::Tick => {
let red = read_from_option_fd(self.fd.as_ref());
match red {
Ok(red) => {
if let Err(error) = self.update_screen_buffer(&red) {
print_err(&error);
}
}
Err(error) => print_err(&error),
}
return iced::Task::none();
}
}
}
/// reads from the pty and adds it to the buffer
#[expect(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
reason = "all is bound checked"
)]
fn update_screen_buffer(&mut self, vec: &[u8]) -> Result<(), Error> {
let offset = self.screen_buffer_index;
for (i, chr) in vec.iter().enumerate() {
let index = i + offset;
if index < self.screen_buffer.len() {
self.screen_buffer[index] = *chr;
} else {
return Err(Error::IndexOutOfBounds);
}
self.screen_buffer_index += 1;
}
return Ok(());
}
/// view logic for model\
/// TODO add wide char support\
/// TODO bound check
#[inline]
#[expect(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
clippy::string_slice,
reason = "TODO"
)]
pub fn view(&self) -> Element<'_, Msg> {
let (left, right) =
String::from_utf8_lossy(&self.screen_buffer[..self.screen_buffer_index])
.rsplit_once('\n')
.map_or_else(
|| {
return (
String::new(),
String::from_utf8_lossy(
&self.screen_buffer[..self.screen_buffer_index],
)
.to_string(),
);
},
|tup| {
return (tup.0.to_owned(), tup.1.to_owned());
},
);
return scrollable(column![
text(left),
row![
text(right),
text(&self.input[..(self.cursor_index)]),
if self.cursor_index < self.input.len() {
row![
if self.input[self.cursor_index..=self.cursor_index] == *" " {
text("_").color(iced::Color::from_rgb(f32::MAX, 0.0, 0.0))
} else {
text(&self.input[self.cursor_index..=self.cursor_index])
.color(iced::Color::from_rgb(f32::MAX, 0.0, 0.0))
},
text(&self.input[(self.cursor_index + 1)..])
]
} else {
row![
text(&self.input[self.cursor_index..]),
rich_text![
span("_")
.color(iced::Color::from_rgb(f32::MAX, 0.0, 0.0))
.background(iced::Color::from_rgb(f32::MAX, f32::MAX, 0.0))
]
]
}
]
])
.into();
}
}
impl Default for Model {
#[inline]
#[expect(clippy::undocumented_unsafe_blocks, reason = "clippy be trippin")]
fn default() -> Self {
let mut me = Self::new(
[0; 65536],
0,
0,
spawn_pty_with_shell("/home/mtgmonkey/.nix-profile/bin/dash".to_string()),
std::io::stdin().as_fd().try_clone_to_owned().unwrap(),
String::new(),
);
let mut me = Self {
screen_buffer: [0; 0x4000],
screen_buffer_index: 0,
cursor_index: 0,
fd: None,
input: String::new(),
/// SAFETY call *after* `init()`
shell: unsafe { SHELL.clone() }.map_or_else(
|| return String::from("/home/mtgmonkey/.nix-profile/bin/dash"),
|shell| return shell,
),
};
me.fd = spawn_pty_with_shell(&me.shell).ok();
let mut nored = true;
while nored {
let red = read_from_fd(&me.fd);
match red {
Some(red) => {
nored = false;
me.update_screen_buffer(&red);
let red = read_from_option_fd(me.fd.as_ref());
if let Ok(red) = red {
nored = false;
if let Err(error) = me.update_screen_buffer(&red) {
print_err(&error);
}
None => (),
}
}
me
return me;
}
}
/// # Safety
/// call *before* creating a `Model` because `Model::default()` relies on `SHELL`
/// call *before* `print_err()` because `print_err()` relies on `VERBOSE`
#[inline]
#[expect(clippy::undocumented_unsafe_blocks, reason = "clippy be trippin")]
pub unsafe fn init(flags: Flags) {
unsafe {
VERBOSE = flags.verbose;
}
unsafe {
SHELL = flags.shell;
}
}
/// spawns a pty with the specified shell program
#[expect(clippy::single_call_fn, reason = "abstraction")]
fn spawn_pty_with_shell(default_shell: &str) -> Result<OwnedFd, Error> {
// SAFETY: always safe unless the OS is out of ptys
// so it is always safe
match unsafe { forkpty(None, None) } {
Ok(fork_pty_res) => match fork_pty_res {
ForkptyResult::Parent { master, .. } => {
if let Err(error) = set_nonblock(&master) {
return Err(error);
}
return Ok(master);
}
ForkptyResult::Child => {
if let Err(error) = Command::new(default_shell).spawn() {
return Err(Error::Io(error));
}
thread::sleep(core_time::Duration::MAX);
return Err(Error::Unreachable);
}
},
Err(error) => return Err(Error::Nix(NixError::Errno(error))),
}
}
/// reads from an `&OwnedFd`
/// TODO check bounds
#[expect(
clippy::single_call_fn,
clippy::indexing_slicing,
reason = "abstraction"
)]
fn read_from_fd(fd: &OwnedFd) -> Result<Vec<u8>, Error> {
let mut read_buffer = [0; 0x4000];
#[expect(clippy::unwrap_used, reason = "platform-specific but fine")]
let mut file = File::from(fd.try_clone().unwrap());
let file = file.read(&mut read_buffer);
match file {
Ok(file) => return Ok(read_buffer[..file].to_vec()),
Err(error) => return Err(Error::Io(error)),
}
}
/// reads from an `Option<&OwnedFd>` if it's there
fn read_from_option_fd(maybe_fd: Option<&OwnedFd>) -> Result<Vec<u8>, Error> {
return maybe_fd.map_or(Err(Error::NoFileDescriptor), |fd| {
return read_from_fd(fd);
});
}
/// sets a `OwnedFd` as nonblocking.
#[expect(clippy::single_call_fn, reason = "abstraction")]
fn set_nonblock(fd: &OwnedFd) -> Result<(), Error> {
let flags = match fcntl::fcntl(fd, fcntl::FcntlArg::F_GETFL) {
Ok(flags) => flags,
Err(errno) => return Err(Error::Nix(NixError::Errno(errno))),
};
let flags = fcntl::OFlag::from_bits(flags & fcntl::OFlag::O_ACCMODE.bits());
match flags {
Some(mut flags) => {
flags.set(fcntl::OFlag::O_NONBLOCK, true);
if let Err(errno) = fcntl::fcntl(fd, fcntl::FcntlArg::F_SETFL(flags)) {
return Err(Error::Nix(NixError::Errno(errno)));
}
}
None => return Err(Error::Nix(NixError::UnrecognisedFlag)),
}
return Ok(());
}
/// if `VERBOSE` is `true`, logs errors
#[inline]
#[expect(
clippy::print_stdout,
clippy::undocumented_unsafe_blocks,
reason = "toggleable with VERBOSE option\n
clippy be buggin"
)]
fn print_err(error: &Error) {
/// SAFETY the only time VERBOSE is written to should be `init()`
if unsafe { VERBOSE } {
println!("[ERROR] {error}");
}
}

View file

@ -1,35 +1,27 @@
use rust_term::*;
//! this crate runs a terminal
#![expect(
clippy::needless_return,
clippy::cargo_common_metadata,
clippy::blanket_clippy_restriction_lints,
clippy::multiple_crate_versions,
unused_doc_comments,
reason = ""
)]
use rust_term::{Model, flags, init};
#[expect(clippy::undocumented_unsafe_blocks, reason = "clippy be trippin")]
fn main() -> iced::Result {
iced::application("test", Model::update, Model::view)
/// SAFETY call does occur *before* the initialization of a Model
/// SAFETY call does occur *before* any opportunity to call `print_err()`
unsafe {
init(flags().run());
};
return iced::application("test", Model::update, Model::view)
.theme(Model::theme)
.default_font(iced::Font::MONOSPACE)
.decorations(false)
.subscription(Model::subscription)
.run()
/*
let default_shell = "/home/mtgmonkey/.nix-profile/bin/dash".to_string();
let fd = spawn_pty_with_shell(default_shell);
let mut write_buffer = "tty\n".as_bytes().to_vec();
write(fd.as_fd(), &mut write_buffer);
loop {
let red = read_from_fd(&fd);
match red {
Some(red) => print!("{}", String::from_utf8(red).unwrap()),
None => {
let mut read_buffer = [0; 65536];
let mut file = File::from(std::io::stdin().as_fd().try_clone_to_owned().unwrap());
file.flush();
let file = file.read(&mut read_buffer);
println!(
"{}",
match file {
Ok(file) => write(fd.as_fd(), &read_buffer[..file]).unwrap(),
Err(_) => 0,
}
);
}
};
}
*/
.run();
}