cli arguments, clippy passes
This commit is contained in:
parent
b4da358591
commit
a100c9be52
8 changed files with 528 additions and 958 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
target
|
||||||
|
result
|
821
Cargo.lock
generated
821
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
21
Cargo.toml
21
Cargo.toml
|
@ -5,4 +5,23 @@ edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nix = { version = "0.30.1", features = ["term", "process", "fs"], default-features = false }
|
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
6
flake.lock
generated
|
@ -22,11 +22,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1751271578,
|
"lastModified": 1751792365,
|
||||||
"narHash": "sha256-P/SQmKDu06x8yv7i0s8bvnnuJYkxVGBWLWHaU+tt4YY=",
|
"narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "3016b4b15d13f3089db8a41ef937b13a9e33a8df",
|
"rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
11
flake.nix
11
flake.nix
|
@ -13,12 +13,19 @@
|
||||||
}: let
|
}: let
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
in {
|
in rec {
|
||||||
packages.${system} = {
|
packages.${system} = {
|
||||||
default = pkgs.callPackage ./package.nix {
|
default = pkgs.callPackage ./package.nix {
|
||||||
naersk = pkgs.callPackage naersk {};
|
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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
makeWrapper,
|
makeWrapper,
|
||||||
naersk,
|
naersk,
|
||||||
pkg-config,
|
pkg-config,
|
||||||
|
upx,
|
||||||
wayland,
|
wayland,
|
||||||
xorg,
|
xorg,
|
||||||
...
|
...
|
||||||
|
@ -33,8 +34,10 @@ naersk.buildPackage rec {
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkg-config
|
pkg-config
|
||||||
makeWrapper
|
makeWrapper
|
||||||
|
upx
|
||||||
];
|
];
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
|
upx --lzma $out/bin/${meta.mainProgram}
|
||||||
wrapProgram "$out/bin/${meta.mainProgram}" --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath buildInputs}"
|
wrapProgram "$out/bin/${meta.mainProgram}" --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath buildInputs}"
|
||||||
'';
|
'';
|
||||||
meta = {
|
meta = {
|
||||||
|
|
510
src/lib.rs
510
src/lib.rs
|
@ -1,99 +1,205 @@
|
||||||
use iced::widget::text_input::Id;
|
//! this crate runs a terminal
|
||||||
use iced::widget::{column, row, scrollable, text, text_input};
|
|
||||||
use iced::{Element, Font, Task, keyboard};
|
|
||||||
|
|
||||||
|
#![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::pty::{ForkptyResult, forkpty};
|
||||||
use nix::unistd::write;
|
use nix::unistd::write;
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Write};
|
use std::io::{self, Read as _};
|
||||||
use std::os::unix::io::{AsFd, OwnedFd};
|
use std::os::unix::io::{AsFd as _, OwnedFd};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::{error, fmt, thread, time as core_time};
|
||||||
|
|
||||||
fn spawn_pty_with_shell(default_shell: String) -> OwnedFd {
|
/// whether to enable verbose logging; see `Flags::verbose`
|
||||||
match unsafe { forkpty(None, None) } {
|
static mut VERBOSE: bool = false;
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_from_fd(fd: &OwnedFd) -> Option<Vec<u8>> {
|
/// shell path; see `Flags::shell`
|
||||||
let mut read_buffer = [0; 65536];
|
static mut SHELL: Option<String> = None;
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// events to be passed to `Model::update`
|
||||||
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
|
Exit,
|
||||||
|
KeyPressed(keyboard::Key),
|
||||||
Tick,
|
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 {
|
pub struct Model {
|
||||||
screen_buffer: [u8; 65536],
|
/// location of cursor in user input line
|
||||||
screen_buffer_index: usize,
|
|
||||||
cursor_index: usize,
|
cursor_index: usize,
|
||||||
fd: OwnedFd,
|
/// fd of pty
|
||||||
stdin: OwnedFd,
|
fd: Option<OwnedFd>,
|
||||||
|
/// user input line
|
||||||
input: String,
|
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 {
|
impl Model {
|
||||||
fn new(
|
/// applies needed side effects when taking an input char
|
||||||
screen_buffer: [u8; 65536],
|
#[expect(
|
||||||
screen_buffer_index: usize,
|
clippy::arithmetic_side_effects,
|
||||||
cursor_index: usize,
|
reason = "cursor_index is bound checked"
|
||||||
fd: OwnedFd,
|
)]
|
||||||
stdin: OwnedFd,
|
fn input_char(&mut self, chr: char) {
|
||||||
input: String,
|
if self.cursor_index == self.input.len() {
|
||||||
) -> Self {
|
self.input.push_str(chr.to_string().as_str());
|
||||||
Model {
|
} else {
|
||||||
screen_buffer,
|
self.input.insert(self.cursor_index, chr);
|
||||||
screen_buffer_index,
|
|
||||||
cursor_index,
|
|
||||||
fd,
|
|
||||||
stdin,
|
|
||||||
input,
|
|
||||||
}
|
}
|
||||||
|
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> {
|
pub fn update(&mut self, msg: Msg) -> Task<Msg> {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::Tick => match read_from_fd(&self.fd) {
|
Msg::Exit => return window::get_latest().and_then(window::close),
|
||||||
Some(red) => self.update_screen_buffer(&red),
|
Msg::KeyPressed(key) => {
|
||||||
None => (),
|
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),
|
||||||
},
|
},
|
||||||
Msg::KeyPressed(key) => match key {
|
|
||||||
keyboard::Key::Character(c) => {
|
|
||||||
self.input_char(c.chars().nth(0).unwrap());
|
|
||||||
}
|
|
||||||
keyboard::Key::Named(keyboard::key::Named::Enter) => {
|
keyboard::Key::Named(keyboard::key::Named::Enter) => {
|
||||||
self.input.push('\n');
|
self.input.push('\n');
|
||||||
let mut write_buffer = self.input.as_bytes().to_vec();
|
let write_buffer = self.input.as_bytes().to_vec();
|
||||||
write(self.fd.as_fd(), &mut write_buffer);
|
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.input = String::new();
|
||||||
self.cursor_index = 0;
|
self.cursor_index = 0;
|
||||||
}
|
}
|
||||||
|
@ -101,91 +207,249 @@ impl Model {
|
||||||
self.input_char(' ');
|
self.input_char(' ');
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
|
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
|
||||||
if self.cursor_index <= 0 {
|
if self.cursor_index == 0 {
|
||||||
self.cursor_index = 0;
|
self.cursor_index = 0;
|
||||||
} else {
|
} else {
|
||||||
self.cursor_index -= 1;
|
self.cursor_index -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
|
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
|
||||||
if self.cursor_index >= self.input.len() - 1 {
|
if self.cursor_index >= self.input.len() {
|
||||||
self.cursor_index = self.input.len() - 1;
|
self.cursor_index = self.input.len();
|
||||||
} else {
|
} else {
|
||||||
self.cursor_index += 1;
|
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();
|
||||||
iced::Task::<Msg>::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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self) -> Element<'_, Msg> {
|
/// reads from the pty and adds it to the buffer
|
||||||
let (left, right) =
|
#[expect(
|
||||||
match String::from_utf8(self.screen_buffer[..self.screen_buffer_index].to_vec())
|
clippy::arithmetic_side_effects,
|
||||||
.unwrap()
|
clippy::indexing_slicing,
|
||||||
.rsplit_once('\n')
|
reason = "all is bound checked"
|
||||||
{
|
)]
|
||||||
Some(tup) => (tup.0.to_string(), tup.1.to_string()),
|
fn update_screen_buffer(&mut self, vec: &[u8]) -> Result<(), Error> {
|
||||||
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;
|
let offset = self.screen_buffer_index;
|
||||||
for (i, chr) in vec.iter().enumerate() {
|
for (i, chr) in vec.iter().enumerate() {
|
||||||
self.screen_buffer[i + offset] = chr.clone();
|
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;
|
self.screen_buffer_index += 1;
|
||||||
}
|
}
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_char(&mut self, c: char) {
|
/// view logic for model\
|
||||||
if self.cursor_index == self.input.len() {
|
/// TODO add wide char support\
|
||||||
self.input.push_str(c.to_string().as_str());
|
/// 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 {
|
} else {
|
||||||
self.input.insert(self.cursor_index, c);
|
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))
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
self.cursor_index += 1;
|
]
|
||||||
|
])
|
||||||
|
.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Model {
|
impl Default for Model {
|
||||||
|
#[inline]
|
||||||
|
#[expect(clippy::undocumented_unsafe_blocks, reason = "clippy be trippin")]
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut me = Self::new(
|
let mut me = Self {
|
||||||
[0; 65536],
|
screen_buffer: [0; 0x4000],
|
||||||
0,
|
screen_buffer_index: 0,
|
||||||
0,
|
cursor_index: 0,
|
||||||
spawn_pty_with_shell("/home/mtgmonkey/.nix-profile/bin/dash".to_string()),
|
fd: None,
|
||||||
std::io::stdin().as_fd().try_clone_to_owned().unwrap(),
|
input: String::new(),
|
||||||
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;
|
let mut nored = true;
|
||||||
while nored {
|
while nored {
|
||||||
let red = read_from_fd(&me.fd);
|
let red = read_from_option_fd(me.fd.as_ref());
|
||||||
match red {
|
if let Ok(red) = red {
|
||||||
Some(red) => {
|
|
||||||
nored = false;
|
nored = false;
|
||||||
me.update_screen_buffer(&red);
|
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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
48
src/main.rs
48
src/main.rs
|
@ -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 {
|
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)
|
.theme(Model::theme)
|
||||||
.default_font(iced::Font::MONOSPACE)
|
.default_font(iced::Font::MONOSPACE)
|
||||||
.decorations(false)
|
.decorations(false)
|
||||||
.subscription(Model::subscription)
|
.subscription(Model::subscription)
|
||||||
.run()
|
.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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue