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-07-31 22:31:23 +02:00
commit 0a3ddbdcaf
13 changed files with 661 additions and 0 deletions
+17
View File
@@ -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
+90
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
target
Cargo.lock
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "binocle"
version = "0.1.0"
description = "A TUI to do binary computations"
authors = ["Julien Thillard <julien.thillard38@gmail.com>"]
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
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) supersurviveur <julien.thillard38@gmail.com>
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.
+14
View File
@@ -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 <julien.thillard38@gmail.com>
This project is licensed under the MIT license ([LICENSE] or <http://opensource.org/licenses/MIT>)
[LICENSE]: ./LICENSE
+173
View File
@@ -0,0 +1,173 @@
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, hexadecimal::Hexadecimal, signed::Signed,
twos_complement::TwosComplement, unsigned::Unsigned,
};
#[derive(Debug, Default)]
pub struct App {
exit: bool,
character_index: usize,
input: String,
bit_size: usize,
parsed_input: BinaryRepr,
// Components
twos_complement: TwosComplement,
hexadecimal: Hexadecimal,
unsigned: Unsigned,
signed: Signed,
}
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) {
if input.is_ascii_hexdigit()
|| ((input == 'x' || input == 'o')
&& self.character_index == 1
&& self.input.starts_with('0'))
{
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) {
// TODO manage parsing errors
let (input, base) = if self.input.starts_with("0x") {
(&self.input[2..], 16)
} else if self.input.starts_with("0o") {
(&self.input[2..], 8)
} else if self.input.starts_with("0b") {
(&self.input[2..], 2)
} else {
(self.input.as_str(), 10)
};
if let Ok(parsed) = u128::from_str_radix(input, base) {
self.parsed_input = BinaryRepr::from_u128(parsed, self.bit_size);
} else {
return;
}
self.twos_complement.update(&self.parsed_input);
self.hexadecimal.update(&self.parsed_input);
self.unsigned.update(&self.parsed_input);
self.signed.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 output_layout = Layout::new(
Direction::Vertical,
[
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
],
);
let [twos_complement_area, hexadecimal_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.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.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);
}
}
+170
View File
@@ -0,0 +1,170 @@
use std::ops::{Add, Not, Sub};
#[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_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 mut digits: Vec<u8> = vec![0];
for bit in self.bits.iter().rev().skip(1) {
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();
}
if self.size != 0 && self.bits[self.size - 1] {
"-"
} else {
""
}
.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 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_u128(1, size)
}
}
impl BinaryRepr {
pub fn twos_complement(&self) -> Self {
!self + Self::from_u128(1, self.size)
}
}
+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 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_text = Text::from(("0x".to_owned() + &self.repr.to_hex()).yellow());
let input_block = Block::bordered()
.title(Line::from("hexadecimal".bold()))
.border_set(border::PLAIN);
Paragraph::new(input_text)
.block(input_block)
.render(area, buf);
}
}
+16
View File
@@ -0,0 +1,16 @@
#![deny(clippy::nursery, clippy::pedantic)]
use crate::app::App;
mod app;
mod binary_repr;
mod hexadecimal;
mod twos_complement;
mod unsigned;
mod signed;
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
ratatui::run(|terminal| App::default().run(terminal))?;
Ok(())
}
+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);
}
}