commit 0f1194aecaed3981d8333a0c38dbf50165382d20 Author: supersurviveur Date: Fri Jul 31 22:05:45 2026 +0200 First commit diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c2fabe1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + # Maintain dependencies for Cargo + - package-ecosystem: "cargo" + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + # Maintain dependencies for GitHub Actions + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..653741c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - master + - develop + +env: + CARGO_TERM_COLOR: always + +# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel +# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + fmt: + name: fmt + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: check formatting + run: cargo fmt -- --check + - name: Cache Cargo dependencies + uses: Swatinem/rust-cache@v2 + clippy: + name: clippy + runs-on: ubuntu-latest + permissions: + contents: read + checks: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Run clippy action + uses: clechasseur/rs-clippy-check@v3 + - name: Cache Cargo dependencies + uses: Swatinem/rust-cache@v2 + doc: + # run docs generation on nightly rather than stable. This enables features like + # https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an + # API be documented as only available in some specific platforms. + name: doc + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust nightly + uses: dtolnay/rust-toolchain@nightly + - name: Run cargo doc + run: cargo doc --no-deps --all-features + env: + RUSTDOCFLAGS: --cfg docsrs + test: + runs-on: ${{ matrix.os }} + name: test ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest] + steps: + # if your project needs OpenSSL, uncomment this to fix Windows builds. + # it's commented out by default as the install command takes 5-10m. + # - run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append + # if: runner.os == 'Windows' + # - run: vcpkg install openssl:x64-windows-static-md + # if: runner.os == 'Windows' + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + # enable this ci template to run regardless of whether the lockfile is checked in or not + - name: cargo generate-lockfile + if: hashFiles('Cargo.lock') == '' + run: cargo generate-lockfile + - name: cargo test --locked + run: cargo test --locked --all-features --all-targets + - name: Cache Cargo dependencies + uses: Swatinem/rust-cache@v2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d692672 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "binocle" +version = "0.1.0" +description = "A TUI to do binary computations" +authors = ["Julien Thillard "] +license = "MIT" +edition = "2024" + +[dependencies] +color-eyre = "0.6" +crossterm = "0.29" +ratatui = "0.30" +winnow = "1.0.4" + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5ffb709 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) supersurviveur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb76d32 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# binocle + +This is a [Ratatui] app generated by the [Hello World template]. + +[Ratatui]: https://ratatui.rs +[Hello World Template]: https://github.com/ratatui/templates/tree/main/hello-world + +## License + +Copyright (c) supersurviveur + +This project is licensed under the MIT license ([LICENSE] or ) + +[LICENSE]: ./LICENSE diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f216078 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +edition = "2024" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..efd40e0 --- /dev/null +++ b/src/app.rs @@ -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; + } +} diff --git a/src/binary.rs b/src/binary.rs new file mode 100644 index 0000000..37d5584 --- /dev/null +++ b/src/binary.rs @@ -0,0 +1,38 @@ +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::Stylize, + symbols::border, + text::Line, + widgets::{Block, Paragraph, Widget, Wrap}, +}; + +use crate::{binary_repr::BinaryRepr, color::colorize}; + +#[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 input_block = Block::bordered() + .title(Line::from("binary".bold())) + .border_set(border::PLAIN); + + let mut line = Line::from("0b".gray()); + + let mut binding = self.repr.to_binary(); + colorize(&binding, 8, &mut line); + Paragraph::new(line) + .wrap(Wrap { trim: false }) + .block(input_block) + .render(area, buf); + } +} diff --git a/src/binary_repr.rs b/src/binary_repr.rs new file mode 100644 index 0000000..f7b5efe --- /dev/null +++ b/src/binary_repr.rs @@ -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, + pub size: usize, +} + +impl BinaryRepr { + pub const fn new(bits: Vec, 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 = 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 = if negative { + let mut inverted: Vec = 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 = 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::() + } + + 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 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 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 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) + } +} diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..7321c6d --- /dev/null +++ b/src/color.rs @@ -0,0 +1,19 @@ +use ratatui::{style::Stylize, text::Line}; + +pub fn colorize<'a>(mut txt: &'a str, width: usize, out: &mut Line<'a>) { + let mut color = false; + if !txt.len().is_multiple_of(width) { + *out += txt[..txt.len() % width].red(); + color = !color; + txt = &txt[txt.len() % width..]; + } + for _ in 0..txt.len() / width { + if color { + *out += txt[0..width].cyan(); + } else { + *out += txt[0..width].red(); + } + txt = &txt[width..]; + color = !color; + } +} diff --git a/src/expr.rs b/src/expr.rs new file mode 100644 index 0000000..87fb1e3 --- /dev/null +++ b/src/expr.rs @@ -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, BinOp, Box), + UnaryOp(UnaryOp, Box), + Group(Box), +} + +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 => ::add, + BinOp::Sub => ::sub, + BinOp::Mul => ::mul, + BinOp::Div => todo!(), + })(expr.into_binary_repr(), expr1.into_binary_repr()) + } + Self::UnaryOp(unary_op, expr) => (match unary_op { + UnaryOp::Neg => ::neg, + UnaryOp::Not => ::not, + })(expr.into_binary_repr()), + Self::Group(group) => group.into_binary_repr(), + } + } +} diff --git a/src/hexadecimal.rs b/src/hexadecimal.rs new file mode 100644 index 0000000..934e537 --- /dev/null +++ b/src/hexadecimal.rs @@ -0,0 +1,38 @@ +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::Stylize, + symbols::border, + text::Line, + widgets::{Block, Paragraph, Widget, Wrap}, +}; + +use crate::{binary_repr::BinaryRepr, color::colorize}; + +#[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 input_block = Block::bordered() + .title(Line::from("hexadecimal".bold())) + .border_set(border::PLAIN); + + let mut line = Line::from("0x".gray()); + + let binding = self.repr.to_hex(); + colorize(&binding, 2, &mut line); + Paragraph::new(line) + .wrap(Wrap { trim: false }) + .block(input_block) + .render(area, buf); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..97e03a5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,28 @@ +#![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; +mod color; + +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(()) +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..06ef60f --- /dev/null +++ b/src/parser.rs @@ -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) + } +} diff --git a/src/signed.rs b/src/signed.rs new file mode 100644 index 0000000..f48745f --- /dev/null +++ b/src/signed.rs @@ -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); + } +} diff --git a/src/twos_complement.rs b/src/twos_complement.rs new file mode 100644 index 0000000..f26faed --- /dev/null +++ b/src/twos_complement.rs @@ -0,0 +1,38 @@ +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::Stylize, + symbols::border, + text::Line, + widgets::{Block, Paragraph, Widget, Wrap}, +}; + +use crate::{binary_repr::BinaryRepr, color::colorize}; + +#[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_block = Block::bordered() + .title(Line::from("2's complement".bold())) + .border_set(border::PLAIN); + + let mut line = Line::from("0x".gray()); + + let binding = self.repr.to_hex(); + colorize(&binding, 2, &mut line); + Paragraph::new(line) + .wrap(Wrap { trim: false }) + .block(input_block) + .render(area, buf); + } +} diff --git a/src/unsigned.rs b/src/unsigned.rs new file mode 100644 index 0000000..ef02790 --- /dev/null +++ b/src/unsigned.rs @@ -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); + } +}