From f23edd7d3fde9259dcef88bb8691c058a94004b7 Mon Sep 17 00:00:00 2001 From: supersurviveur Date: Fri, 31 Jul 2026 22:05:45 +0200 Subject: [PATCH] First commit --- .github/dependabot.yml | 17 +++++ .github/workflows/ci.yml | 90 +++++++++++++++++++++++++ .gitignore | 2 + Cargo.toml | 18 +++++ LICENSE | 21 ++++++ README.md | 14 ++++ src/app.rs | 138 +++++++++++++++++++++++++++++++++++++++ src/binary_repr.rs | 110 +++++++++++++++++++++++++++++++ src/hexadecimal.rs | 35 ++++++++++ src/main.rs | 14 ++++ src/twos_complement.rs | 35 ++++++++++ 11 files changed, 494 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/app.rs create mode 100644 src/binary_repr.rs create mode 100644 src/hexadecimal.rs create mode 100644 src/main.rs create mode 100644 src/twos_complement.rs 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..16659f1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[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" + +[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/src/app.rs b/src/app.rs new file mode 100644 index 0000000..140e805 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,138 @@ +use std::io; + +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Rect}, + style::Stylize, + symbols::border, + text::{Line, Text}, + widgets::{Block, Paragraph, Widget}, + DefaultTerminal, Frame, +}; + +use crate::{binary_repr::BinaryRepr, twos_complement::TwosComplement}; + +#[derive(Debug, Default)] +pub struct App { + exit: bool, + character_index: usize, + input: String, + + bit_size: usize, + parsed_input: BinaryRepr, + + // Components + twos_complement: TwosComplement, +} + +impl App { + pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { + self.bit_size = 32; + while !self.exit { + terminal.draw(|frame| self.draw(frame))?; + self.handle_events()?; + } + Ok(()) + } + + fn draw(&self, frame: &mut Frame) { + frame.render_widget(self, frame.area()); + } + + 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); + } + _ => {} + } + Ok(()) + } + + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Char('q') => self.exit(), + // KeyCode::Left => self.decrement_counter(), + // KeyCode::Right => self.increment_counter(), + KeyCode::Char(to_insert) => self.enter_char(to_insert), + KeyCode::Backspace => self.delete_char(), + _ => {} + } + match key_event.code { + KeyCode::Char(_) | KeyCode::Backspace => self.update(), + _ => {} + } + } + + fn enter_char(&mut self, input: char) { + self.input.push(input); + self.character_index += 1; + } + + fn delete_char(&mut self) { + let is_not_cursor_leftmost = self.character_index != 0; + if is_not_cursor_leftmost { + self.character_index -= 1; + self.input.remove(self.character_index); + } + } + + fn update(&mut self) { + self.parsed_input = + BinaryRepr::from_usize(self.input.parse::().unwrap(), self.bit_size); + self.twos_complement.update(&self.parsed_input); + } + + const fn exit(&mut self) { + self.exit = true; + } +} + +impl Widget for &App { + fn render(self, area: Rect, buf: &mut Buffer) { + let layout = Layout::new( + Direction::Vertical, + [Constraint::Fill(1), Constraint::Length(3)], + ); + let [output_area, input_area] = area.layout(&layout); + + // let title = Line::from(" Counter App Tutorial ".bold()); + // let instructions = Line::from(vec![ + // " Decrement ".into(), + // "".blue().bold(), + // " Increment ".into(), + // "".blue().bold(), + // " Quit ".into(), + // " ".blue().bold(), + // ]); + + self.twos_complement.render(output_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.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); + } +} diff --git a/src/binary_repr.rs b/src/binary_repr.rs new file mode 100644 index 0000000..433a80e --- /dev/null +++ b/src/binary_repr.rs @@ -0,0 +1,110 @@ +use std::ops::{Add, Not, Sub}; + +#[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 from_usize(mut value: usize, 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 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 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_usize(1, size) + } +} + +impl BinaryRepr { + pub fn twos_complement(&self) -> Self { + !self + Self::from_usize(1, self.size) + } +} diff --git a/src/hexadecimal.rs b/src/hexadecimal.rs new file mode 100644 index 0000000..44d9d81 --- /dev/null +++ b/src/hexadecimal.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 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); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..30c8f1e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,14 @@ +#![deny(clippy::nursery, clippy::pedantic)] + +use crate::app::App; + +mod app; +mod binary_repr; +mod twos_complement; +mod hexadecimal; + +fn main() -> color_eyre::Result<()> { + color_eyre::install()?; + ratatui::run(|terminal| App::default().run(terminal))?; + Ok(()) +} diff --git a/src/twos_complement.rs b/src/twos_complement.rs new file mode 100644 index 0000000..44d9d81 --- /dev/null +++ b/src/twos_complement.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 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); + } +}