First commit
CI / fmt (push) Has been cancelled
CI / clippy (push) Has been cancelled
CI / doc (push) Has been cancelled
CI / test macos-latest (push) Has been cancelled
CI / test windows-latest (push) Has been cancelled

This commit is contained in:
2026-08-08 11:02:48 +02:00
committed by surv
commit 86c507df21
17 changed files with 1059 additions and 0 deletions
+249
View File
@@ -0,0 +1,249 @@
use std::io;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, MouseEvent};
use ratatui::{
DefaultTerminal, Frame,
layout::{Constraint, Direction, Layout, Rect},
style::Stylize,
symbols::border,
text::{Line, Text},
widgets::{Block, Paragraph, Widget},
};
use winnow::Parser;
use crate::{
binary::Binary, binary_repr::BinaryRepr, hexadecimal::Hexadecimal, parser, signed::Signed,
twos_complement::TwosComplement, unsigned::Unsigned,
};
#[derive(Debug, Default, PartialEq, Eq)]
pub enum Input {
#[default]
Number,
BitSize,
}
#[derive(Debug, Default)]
pub struct App {
exit: bool,
character_index: u16,
bit_index: u16,
input: String,
bit_size_input: String,
select: Input,
bit_size: usize,
parsed_input: BinaryRepr,
number_area: Rect,
size_area: Rect,
// Components
twos_complement: TwosComplement,
hexadecimal: Hexadecimal,
binary: Binary,
unsigned: Unsigned,
signed: Signed,
}
impl App {
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
self.bit_size = 32;
self.bit_size_input = "32".to_string();
self.input = "0".to_string();
self.character_index = 1;
self.bit_index = 2;
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()?;
}
Ok(())
}
fn draw(&mut self, frame: &mut Frame) {
let area = frame.area();
let buf = frame.buffer_mut();
let layout = Layout::new(
Direction::Vertical,
[Constraint::Fill(1), Constraint::Length(3)],
);
let [output_area, input_area] = area.layout(&layout);
let output_layout = Layout::new(
Direction::Vertical,
[
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
],
);
let [
twos_complement_area,
hexadecimal_area,
binary_area,
unsigned_area,
signed_area,
] = output_area.layout(&output_layout);
self.twos_complement.render(twos_complement_area, buf);
self.hexadecimal.render(hexadecimal_area, buf);
self.binary.render(binary_area, buf);
self.unsigned.render(unsigned_area, buf);
self.signed.render(signed_area, buf);
let input_layout = Layout::new(
Direction::Horizontal,
[Constraint::Fill(1), Constraint::Length(10)],
);
let [input_area, size_area] = input_area.layout(&input_layout);
let input_text = Text::from(self.input.clone().yellow());
let input_block = Block::bordered()
.title(Line::from("input".bold()))
.border_set(border::PLAIN);
Paragraph::new(input_text)
.block(input_block)
.render(input_area, buf);
let size_text = Text::from(self.bit_size_input.clone().yellow());
let size_block = Block::bordered()
.title(Line::from("size".bold()))
.border_set(border::PLAIN);
Paragraph::new(size_text)
.block(size_block)
.render(size_area, buf);
self.size_area = size_area;
self.number_area = input_area;
if self.select == Input::Number {
frame.set_cursor_position((input_area.x + 1 + self.character_index, input_area.y + 1));
} else if self.select == Input::BitSize {
frame.set_cursor_position((size_area.x + 1 + self.bit_index, size_area.y + 1));
}
}
fn handle_events(&mut self) -> io::Result<()> {
match event::read()? {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_event(key_event);
}
Event::Mouse(mouse_event) => self.handle_mouse_event(mouse_event),
_ => {}
}
Ok(())
}
fn handle_key_event(&mut self, key_event: KeyEvent) {
match key_event.code {
KeyCode::Char('q') => self.exit(),
KeyCode::Left => {
if self.select == Input::Number {
self.character_index = self.character_index.saturating_sub(1);
} else if self.select == Input::BitSize {
self.bit_index = self.bit_index.saturating_sub(1);
}
}
KeyCode::Right => {
if self.select == Input::Number {
self.character_index += 1;
self.character_index = self
.character_index
.min(u16::try_from(self.input.len()).unwrap());
} else if self.select == Input::BitSize {
self.bit_index += 1;
self.bit_index = self
.bit_index
.min(u16::try_from(self.bit_size_input.len()).unwrap());
}
}
KeyCode::Char(to_insert) => self.enter_char(to_insert),
KeyCode::Backspace => self.delete_char(),
KeyCode::Tab => {
self.select = if self.select == Input::Number {
Input::BitSize
} else {
Input::Number
}
}
_ => {}
}
match key_event.code {
KeyCode::Char(_) | KeyCode::Backspace => self.update(),
_ => {}
}
}
const fn handle_mouse_event(&mut self, mouse_event: MouseEvent) {
let position = ratatui::layout::Position {
x: mouse_event.column,
y: mouse_event.row,
};
if let event::MouseEventKind::Down(_button) = mouse_event.kind {
if self.size_area.contains(position) {
self.select = Input::BitSize;
} else if self.number_area.contains(position) {
self.select = Input::Number;
}
}
}
fn enter_char(&mut self, input: char) {
if self.select == Input::Number {
self.input.insert(self.character_index as usize, input);
self.character_index += 1;
} else if self.select == Input::BitSize && input.is_ascii_digit() {
self.bit_size_input.insert(self.bit_index as usize, input);
self.bit_index += 1;
}
}
fn delete_char(&mut self) {
if self.select == Input::Number {
let is_not_cursor_leftmost = self.character_index != 0;
if is_not_cursor_leftmost {
self.character_index -= 1;
self.input.remove(self.character_index as usize);
}
} else if self.select == Input::BitSize {
let is_not_cursor_leftmost = self.bit_index != 0;
if is_not_cursor_leftmost {
self.bit_index -= 1;
self.bit_size_input.remove(self.bit_index as usize);
}
}
}
fn update(&mut self) {
self.bit_size = self.bit_size_input.parse().unwrap_or(0);
let mut parser = parser::parser(self.bit_size);
if !self.input.is_empty()
&& let Ok(input) = parser.parse(&self.input)
{
self.parsed_input = input.into_binary_repr();
} else {
return;
}
self.twos_complement.update(&self.parsed_input);
self.hexadecimal.update(&self.parsed_input);
self.binary.update(&self.parsed_input);
self.unsigned.update(&self.parsed_input);
self.signed.update(&self.parsed_input);
}
const fn exit(&mut self) {
self.exit = true;
}
}
+45
View File
@@ -0,0 +1,45 @@
use ratatui::{
buffer::Buffer,
layout::Rect,
style::Stylize,
symbols::border,
text::Line,
widgets::{Block, Paragraph, Widget},
};
use crate::binary_repr::BinaryRepr;
#[derive(Debug, Default)]
pub struct Binary {
repr: BinaryRepr,
}
impl Binary {
pub fn update(&mut self, input: &BinaryRepr) {
self.repr = input.clone();
}
}
impl Widget for &Binary {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut out = Line::from("0x".to_owned().gray());
let binding = self.repr.to_binary();
let mut txt = binding.as_str();
let mut color = false;
for _ in 0..txt.len().div_ceil(8) {
if color {
out += txt[0..8.min(txt.len())].cyan();
} else {
out += txt[0..8.min(txt.len())].red();
}
txt = &txt[8.min(txt.len())..];
color = !color;
}
let input_block = Block::bordered()
.title(Line::from("binary".bold()))
.border_set(border::PLAIN);
Paragraph::new(out).block(input_block).render(area, buf);
}
}
+336
View File
@@ -0,0 +1,336 @@
use std::ops::{Add, Mul, Neg, Not, Shl, Sub};
use winnow::{
Parser,
ascii::{digit1, hex_digit1, oct_digit1},
combinator::{alt, preceded, repeat},
error::ContextError,
};
#[derive(Debug, Default, Clone)]
pub struct BinaryRepr {
pub bits: Vec<bool>,
pub size: usize,
}
impl BinaryRepr {
pub const fn new(bits: Vec<bool>, size: usize) -> Self {
Self { bits, size }
}
pub fn to_hex(&self) -> String {
let mut hex = String::new();
for chunk in self.bits.chunks(4) {
let nibble = chunk
.iter()
.enumerate()
.fold(0, |acc, (i, bit)| acc + (u8::from(*bit) << i));
hex.insert(
0,
[
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
][nibble as usize],
);
}
hex
}
pub fn to_binary(&self) -> String {
let mut bin = String::new();
for bit in self.bits.iter().rev() {
if *bit {
bin.push('1');
} else {
bin.push('0');
}
}
bin
}
pub fn to_unsigned(&self) -> String {
let mut digits: Vec<u8> = vec![0];
for bit in self.bits.iter().rev() {
let mut carry = u8::from(*bit);
for digit in &mut digits {
let value = *digit * 2 + carry;
*digit = value % 10;
carry = value / 10;
}
while carry > 0 {
digits.push(carry % 10);
carry /= 10;
}
}
while digits.len() > 1 && *digits.last().unwrap() == 0 {
digits.pop();
}
digits
.iter()
.rev()
.map(|digit| (digit + b'0') as char)
.collect()
}
pub fn to_signed(&self) -> String {
let negative = self.size != 0 && self.bits[self.size - 1];
// For negatives, take the two's-complement negation (invert + 1)
// over the full width to recover the true magnitude bits.
let mag_bits: Vec<bool> = if negative {
let mut inverted: Vec<bool> = self.bits[..self.size].iter().map(|b| !b).collect();
let mut carry = true; // adding 1
for bit in &mut inverted {
let sum = u8::from(*bit) + u8::from(carry);
*bit = sum & 1 == 1;
carry = sum > 1;
}
inverted
} else {
self.bits[..self.size].to_vec()
};
let mut digits: Vec<u8> = vec![0];
for bit in mag_bits.iter().rev() {
let mut carry = u8::from(*bit);
for digit in &mut digits {
let value = *digit * 2 + carry;
*digit = value % 10;
carry = value / 10;
}
while carry > 0 {
digits.push(carry % 10);
carry /= 10;
}
}
while digits.len() > 1 && *digits.last().unwrap() == 0 {
digits.pop();
}
let sign = if negative { "-" } else { "" };
sign.to_owned()
+ &digits
.iter()
.rev()
.map(|digit| (digit + b'0') as char)
.collect::<String>()
}
pub fn from_u128(mut value: u128, size: usize) -> Self {
Self::new(
(0..size)
.map(|_| {
let out = value & 1 != 0;
value >>= 1;
out
})
.collect(),
size,
)
}
}
impl Not for BinaryRepr {
type Output = Self;
fn not(self) -> Self::Output {
Self::new(self.bits.iter().map(|a| !a).collect(), self.size)
}
}
impl Not for &BinaryRepr {
type Output = BinaryRepr;
fn not(self) -> Self::Output {
!self.clone()
}
}
impl Neg for BinaryRepr {
type Output = Self;
fn neg(self) -> Self::Output {
let size = self.size;
!self + Self::from_u128(1, size)
}
}
impl Add<&Self> for BinaryRepr {
type Output = Self;
fn add(mut self, rhs: &Self) -> Self::Output {
assert_eq!(self.size, rhs.size);
let mut carry = false;
self.bits
.iter_mut()
.zip(rhs.bits.iter())
.for_each(|(a, b)| {
let half = *a ^ b;
let out = half ^ carry;
carry = (*a & b) || (half & carry);
*a = out;
});
self
}
}
impl Add for BinaryRepr {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
self + &rhs
}
}
impl Add<u8> for BinaryRepr {
type Output = Self;
fn add(self, rhs: u8) -> Self::Output {
let size = self.size;
self + Self::from_u128(u128::from(rhs), size)
}
}
impl Sub<&Self> for BinaryRepr {
type Output = Self;
fn sub(self, rhs: &Self) -> Self::Output {
assert_eq!(self.size, rhs.size);
let size = self.size;
self + &!rhs + Self::from_u128(1, size)
}
}
impl Sub for BinaryRepr {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
self - &rhs
}
}
impl Shl<usize> for BinaryRepr {
type Output = Self;
fn shl(mut self, rhs: usize) -> Self::Output {
for _ in 0..rhs {
self.bits.insert(0, false);
}
self
}
}
impl Mul<&Self> for BinaryRepr {
type Output = Self;
fn mul(self, rhs: &Self) -> Self::Output {
assert_eq!(self.size, rhs.size);
let mut result = Self::new(vec![false; self.size], self.size);
for i in 0..self.size {
if rhs.bits[i] {
result = result + (self.clone() << i);
}
}
result
}
}
impl Mul for BinaryRepr {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
self * &rhs
}
}
impl Mul<usize> for BinaryRepr {
type Output = Self;
fn mul(self, rhs: usize) -> Self::Output {
let mut result = Self::new(vec![false; self.size], self.size);
for i in 0..usize::BITS - rhs.leading_zeros() {
if rhs & (1 << i) != 0 {
result = result + (self.clone() << (i as usize));
}
}
result
}
}
impl BinaryRepr {
pub fn twos_complement(&self) -> Self {
!self + Self::from_u128(1, self.size)
}
}
pub fn parse_binary_repr<'i>(size: usize) -> impl Parser<&'i str, BinaryRepr, ContextError> {
move |input: &mut &str| {
alt((
preceded("0x", hex_digit1).map(|t: &str| {
let size = size.max(t.len() * 4);
let len = t.len() * 4;
let mut vec = vec![false; size];
t.chars().enumerate().for_each(|(i, c)| {
let nibble = if c <= '9' {
c as u8 - b'0'
} else {
c.to_lowercase().next().unwrap() as u8 - b'a' + 10
};
vec[len - i * 4 - 4] = nibble & 0b0001 == 1;
vec[len - i * 4 - 3] = nibble & 0b0010 == 2;
vec[len - i * 4 - 2] = nibble & 0b0100 == 4;
vec[len - i * 4 - 1] = nibble & 0b1000 == 8;
});
BinaryRepr::new(vec, size)
}),
preceded("0o", oct_digit1).map(|t: &str| {
let size = size.max(t.len() * 3);
let len = t.len() * 3;
let mut vec = vec![false; size];
t.chars().enumerate().for_each(|(i, c)| {
let nibble = c as u8 - b'0';
vec[len - i * 3 - 3] = nibble & 0b0001 == 1;
vec[len - i * 3 - 2] = nibble & 0b0010 == 2;
vec[len - i * 3 - 1] = nibble & 0b0100 == 4;
});
BinaryRepr::new(vec, size)
}),
preceded(
"0b",
repeat(1.., alt(('0', '1'))).map(|t: Vec<_>| {
let size = size.max(t.len());
let len = t.len();
let mut vec = vec![false; size];
t.into_iter()
.enumerate()
.for_each(|(i, c)| vec[len - i - 1] = c == '1');
BinaryRepr::new(vec, size)
}),
),
digit1.map(|t: &str| {
let mut result = BinaryRepr::new(vec![false; size], size);
for digit in t.chars() {
result = result * 10 + (digit as u8 - b'0');
}
result
}),
))
.parse_next(input)
}
}
+43
View File
@@ -0,0 +1,43 @@
use std::ops::{Add, Mul, Neg, Not, Sub};
use crate::binary_repr::BinaryRepr;
pub enum BinOp {
Plus,
Sub,
Mul,
Div,
}
pub enum UnaryOp {
Not,
Neg,
}
pub enum Expr {
Constant(BinaryRepr),
BinaryOp(Box<Self>, BinOp, Box<Self>),
UnaryOp(UnaryOp, Box<Self>),
Group(Box<Self>),
}
impl Expr {
pub fn into_binary_repr(self) -> BinaryRepr {
match self {
Self::Constant(binary_repr) => binary_repr,
Self::BinaryOp(expr, bin_op, expr1) => {
(match bin_op {
BinOp::Plus => <BinaryRepr as Add>::add,
BinOp::Sub => <BinaryRepr as Sub>::sub,
BinOp::Mul => <BinaryRepr as Mul>::mul,
BinOp::Div => todo!(),
})(expr.into_binary_repr(), expr1.into_binary_repr())
}
Self::UnaryOp(unary_op, expr) => (match unary_op {
UnaryOp::Neg => <BinaryRepr as Neg>::neg,
UnaryOp::Not => <BinaryRepr as Not>::not,
})(expr.into_binary_repr()),
Self::Group(group) => group.into_binary_repr(),
}
}
}
+45
View File
@@ -0,0 +1,45 @@
use ratatui::{
buffer::Buffer,
layout::Rect,
style::Stylize,
symbols::border,
text::{Line, Text},
widgets::{Block, Paragraph, Widget},
};
use crate::binary_repr::BinaryRepr;
#[derive(Debug, Default)]
pub struct Hexadecimal {
repr: BinaryRepr,
}
impl Hexadecimal {
pub fn update(&mut self, input: &BinaryRepr) {
self.repr = input.clone();
}
}
impl Widget for &Hexadecimal {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut out = Line::from("0x".to_owned().gray());
let binding = self.repr.to_hex();
let mut txt = binding.as_str();
let mut color = false;
for _ in 0..txt.len().div_ceil(2) {
if color {
out += txt[0..2.min(txt.len())].cyan();
} else {
out += txt[0..2.min(txt.len())].red();
}
txt = &txt[2.min(txt.len())..];
color = !color;
}
let input_block = Block::bordered()
.title(Line::from("hexadecimal".bold()))
.border_set(border::PLAIN);
Paragraph::new(out).block(input_block).render(area, buf);
}
}
+27
View File
@@ -0,0 +1,27 @@
#![deny(clippy::nursery, clippy::pedantic)]
use std::io::stdout;
use crate::app::App;
mod app;
mod binary_repr;
mod expr;
mod hexadecimal;
mod parser;
mod signed;
mod twos_complement;
mod unsigned;
mod binary;
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
crossterm::execute!(stdout(), crossterm::event::EnableMouseCapture)?;
ratatui::run(|terminal| App::default().run(terminal))?;
exit()
}
fn exit() -> color_eyre::Result<()> {
crossterm::execute!(stdout(), crossterm::event::DisableMouseCapture)?;
Ok(())
}
+45
View File
@@ -0,0 +1,45 @@
use winnow::Parser;
use winnow::ascii::multispace0;
use winnow::combinator::{Infix, Prefix, peek};
use winnow::combinator::{delimited, dispatch, expression, fail};
use winnow::error::ContextError;
use winnow::token::any;
use crate::binary_repr::parse_binary_repr;
use crate::expr::{BinOp, Expr, UnaryOp};
pub fn parser<'i>(size: usize) -> impl Parser<&'i str, Expr, ContextError> {
move |i: &mut &str| {
use Infix::Left;
expression(delimited(
multispace0,
dispatch! {peek(any);
'(' => delimited('(', parser(size).map(|e| Expr::Group(Box::new(e))), ')'),
_ => parse_binary_repr(size).map(Expr::Constant),
},
multispace0,
))
.prefix(delimited(
multispace0,
dispatch! {any;
'-' => Prefix(12, |_, a| Ok(Expr::UnaryOp(UnaryOp::Neg, Box::new(a)))),
'!' => Prefix(12, |_, a| Ok(Expr::UnaryOp(UnaryOp::Not, Box::new(a)))),
_ => fail,
},
multispace0,
))
.infix(delimited(
multispace0,
dispatch! {any;
'+' => Left(5, |_, a, b| Ok(Expr::BinaryOp(Box::new(a), BinOp::Plus, Box::new(b)))),
'-' => Left(5, |_, a, b| Ok(Expr::BinaryOp(Box::new(a), BinOp::Sub, Box::new(b)))),
'*' => Left(7, |_, a, b| Ok(Expr::BinaryOp(Box::new(a), BinOp::Mul, Box::new(b)))),
'/' => Left(7, |_, a, b| Ok(Expr::BinaryOp(Box::new(a), BinOp::Div, Box::new(b)))),
_ => fail,
},
multispace0,
))
.parse_next(i)
}
}
+35
View File
@@ -0,0 +1,35 @@
use ratatui::{
buffer::Buffer,
layout::Rect,
style::Stylize,
symbols::border,
text::{Line, Text},
widgets::{Block, Paragraph, Widget},
};
use crate::binary_repr::BinaryRepr;
#[derive(Debug, Default)]
pub struct Signed {
repr: BinaryRepr,
}
impl Signed {
pub fn update(&mut self, input: &BinaryRepr) {
self.repr = input.clone();
}
}
impl Widget for &Signed {
fn render(self, area: Rect, buf: &mut Buffer) {
let input_text = Text::from(self.repr.to_signed().yellow());
let input_block = Block::bordered()
.title(Line::from("signed".bold()))
.border_set(border::PLAIN);
Paragraph::new(input_text)
.block(input_block)
.render(area, buf);
}
}
+35
View File
@@ -0,0 +1,35 @@
use ratatui::{
buffer::Buffer,
layout::Rect,
style::Stylize,
symbols::border,
text::{Line, Text},
widgets::{Block, Paragraph, Widget},
};
use crate::binary_repr::BinaryRepr;
#[derive(Debug, Default)]
pub struct TwosComplement {
repr: BinaryRepr,
}
impl TwosComplement {
pub fn update(&mut self, input: &BinaryRepr) {
self.repr = input.twos_complement();
}
}
impl Widget for &TwosComplement {
fn render(self, area: Rect, buf: &mut Buffer) {
let input_text = Text::from(("0x".to_owned() + &self.repr.to_hex()).yellow());
let input_block = Block::bordered()
.title(Line::from("2's complement".bold()))
.border_set(border::PLAIN);
Paragraph::new(input_text)
.block(input_block)
.render(area, buf);
}
}
+35
View File
@@ -0,0 +1,35 @@
use ratatui::{
buffer::Buffer,
layout::Rect,
style::Stylize,
symbols::border,
text::{Line, Text},
widgets::{Block, Paragraph, Widget},
};
use crate::binary_repr::BinaryRepr;
#[derive(Debug, Default)]
pub struct Unsigned {
repr: BinaryRepr,
}
impl Unsigned {
pub fn update(&mut self, input: &BinaryRepr) {
self.repr = input.clone();
}
}
impl Widget for &Unsigned {
fn render(self, area: Rect, buf: &mut Buffer) {
let input_text = Text::from(self.repr.to_unsigned().yellow());
let input_block = Block::bordered()
.title(Line::from("unsigned".bold()))
.border_set(border::PLAIN);
Paragraph::new(input_text)
.block(input_block)
.render(area, buf);
}
}