This commit is contained in:
workD12
2026-07-31 20:16:28 +03:00
commit 8e24f19a07
20 changed files with 10037 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
+1
View File
@@ -0,0 +1 @@
/target
Generated
+6226
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "rcalc"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[dependencies]
slint = "1.11"
[build-dependencies]
slint-build = "1.11"
+48
View File
@@ -0,0 +1,48 @@
# rcalc
Desktop calculator for Linux/Windows/macOS, inspired by the Windows Calculator (standard mode).
Built with Rust + [Slint](https://slint.dev).
## Run
```bash
cargo run --release
# or
./dist/rcalc
```
### Icon (Linux / Wayland)
On **Wayland** the window `icon` property is ignored — the desktop uses
`.desktop` + `app_id`. Install once:
```bash
./scripts/install-desktop.sh
```
Then start **rcalc** from the app menu (or `~/.local/bin/rcalc`), not by
clicking a random binary path, so KDE/GNOME can match `app_id=rcalc` to
`rcalc.desktop` and show the icon.
## Features
- Modes: **Standard**, **Programmer**, **Convert**
- Standard: `+ − × ÷`, `%`, `±`, `1/x`, `x²`, `√x`, memory
- Programmer: simultaneous **HEX / DEC / OCT / BIN**, sizes **QWORD / DWORD / DINT / WORD / INT / BYTE**,
bitwise ops, clickable bits
(WORD/DWORD — unsigned DEC; INT/DINT — signed)
- Convert: **float32 ↔ two 16-bit words** with endian **AB CD / CD AB / BA DC / DC BA**
- Convert → **Ratio**: float → minimal **num/den** for electronic gear (Byte≤255 / Word≤65535)
- Convert → **ASCII**: Char ↔ Hex ↔ Dec (control names for NUL…DEL)
- **CRC**: Modbus RTU CRC-16 + Modbus ASCII LRC from hex bytes (`01 03 00 00 00 0A`)
- Keyboard input
## Project layout
- `src/engine.rs` — standard calculator logic
- `src/programmer.rs` — programmer mode logic
- `src/convert.rs` — float32 / word endian convert
- `src/main.rs` — UI wiring
- `ui/app.slint` — Slint interface
- `dist/rcalc` — release binary
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 699 KiB

+13
View File
@@ -0,0 +1,13 @@
[Desktop Entry]
Type=Application
Name=rcalc
GenericName=Calculator
Comment=Calculator with Programmer, Convert, CRC tools
Exec=/home/alex/cloud/rust/rcalc/dist/rcalc
Icon=rcalc
Path=/home/alex/cloud/rust/rcalc
Terminal=false
Categories=Utility;Calculator;Science;Engineering;
StartupNotify=true
# Wayland app_id / X11 WM_CLASS — must match set_xdg_app_id("rcalc")
StartupWMClass=rcalc
+3
View File
@@ -0,0 +1,3 @@
fn main() {
slint_build::compile("ui/app.slint").expect("Slint build failed");
}
Vendored Executable
BIN
View File
Binary file not shown.
Vendored Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Install rcalc launcher + icon for Wayland/XDG desktops (KDE, GNOME, …).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BIN="${ROOT}/dist/rcalc"
ICON_SRC="${ROOT}/assets/icon-256.png"
DESKTOP_SRC="${ROOT}/assets/rcalc.desktop"
if [[ ! -x "$BIN" ]]; then
echo "Building release binary…"
(cd "$ROOT" && cargo build --release)
# Prefer project dist/, else cargo target
if [[ -x "$ROOT/dist/rcalc" ]]; then
BIN="$ROOT/dist/rcalc"
else
BIN="$(find "$ROOT" /tmp/cursor-sandbox-cache -path '*/release/rcalc' -type f 2>/dev/null | head -1 || true)"
fi
fi
if [[ ! -x "$BIN" ]]; then
echo "error: binary not found; run: cargo build --release && cp target/release/rcalc dist/" >&2
exit 1
fi
APP_DIR="${HOME}/.local/share/applications"
ICON_DIR="${HOME}/.local/share/icons/hicolor/256x256/apps"
mkdir -p "$APP_DIR" "$ICON_DIR" "${HOME}/.local/bin"
cp -f "$ICON_SRC" "${ICON_DIR}/rcalc.png"
ln -sfn "$BIN" "${HOME}/.local/bin/rcalc"
# Rewrite Exec to absolute binary path
sed "s|^Exec=.*|Exec=${BIN}|" "$DESKTOP_SRC" \
| sed "s|^Path=.*|Path=${ROOT}|" \
> "${APP_DIR}/rcalc.desktop"
chmod 644 "${APP_DIR}/rcalc.desktop"
# Refresh caches (ignore failures on minimal setups)
update-desktop-database "$APP_DIR" 2>/dev/null || true
gtk-update-icon-cache -f -t "${HOME}/.local/share/icons/hicolor" 2>/dev/null || true
echo "Installed:"
echo " launcher: ${APP_DIR}/rcalc.desktop"
echo " icon: ${ICON_DIR}/rcalc.png"
echo " symlink: ${HOME}/.local/bin/rcalc"
echo
echo "On Wayland: quit rcalc if running, then start it from the app menu"
echo "(or: rcalc). The dock should show the custom icon."
+892
View File
@@ -0,0 +1,892 @@
//! Float32 ↔ two 16-bit words with Modbus-style endian (AB CD).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Endian {
/// Big-endian: bytes A B C D as-is (Motorola / network order).
AbCd,
/// Word-swapped: C D A B.
CdAb,
/// Bytes swapped inside each word: B A D C.
BaDc,
/// Little-endian: D C B A (Intel).
DcBa,
}
impl Endian {
pub fn label(self) -> &'static str {
match self {
Endian::AbCd => "AB CD",
Endian::CdAb => "CD AB",
Endian::BaDc => "BA DC",
Endian::DcBa => "DC BA",
}
}
pub fn all() -> [Endian; 4] {
[Endian::AbCd, Endian::CdAb, Endian::BaDc, Endian::DcBa]
}
fn index(self) -> i32 {
match self {
Endian::AbCd => 0,
Endian::CdAb => 1,
Endian::BaDc => 2,
Endian::DcBa => 3,
}
}
pub fn from_index(i: i32) -> Self {
match i {
1 => Endian::CdAb,
2 => Endian::BaDc,
3 => Endian::DcBa,
_ => Endian::AbCd,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConvField {
Float,
Word0,
Word1,
AsciiChar,
AsciiHex,
AsciiDec,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConvPanel {
Words,
Ratio,
Ascii,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RatioWidth {
Byte,
Word,
}
impl RatioWidth {
pub fn label(self) -> &'static str {
match self {
RatioWidth::Byte => "Byte",
RatioWidth::Word => "Word",
}
}
pub fn max(self) -> u32 {
match self {
RatioWidth::Byte => 255,
RatioWidth::Word => 65535,
}
}
}
/// Relative-error precision presets for gear-ratio approximation.
pub const PRECISION_PRESETS: &[f64] = &[
1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9,
];
#[derive(Debug, Clone, Copy)]
pub struct RatioResult {
pub num: u32,
pub den: u32,
pub approx: f64,
pub rel_error: f64,
pub exact: bool,
}
#[derive(Debug, Clone)]
pub struct Converter {
endian: Endian,
float_bits: u32,
/// Text currently being edited in the active field.
draft: String,
active: ConvField,
error: Option<&'static str>,
panel: ConvPanel,
ratio_width: RatioWidth,
precision_idx: usize,
/// Current ASCII / Latin-1 code (0..=255).
ascii_code: u8,
}
impl Default for Converter {
fn default() -> Self {
Self::new()
}
}
impl Converter {
pub fn new() -> Self {
Self {
endian: Endian::AbCd,
float_bits: 1.0f32.to_bits(),
draft: "1".into(),
active: ConvField::Float,
error: None,
panel: ConvPanel::Words,
ratio_width: RatioWidth::Word,
precision_idx: 3, // 1e-6
ascii_code: b'A',
}
}
pub fn panel(&self) -> ConvPanel {
self.panel
}
pub fn set_panel(&mut self, panel: ConvPanel) {
self.commit_draft();
self.panel = panel;
self.active = match panel {
ConvPanel::Words => ConvField::Float,
ConvPanel::Ratio => ConvField::Float,
ConvPanel::Ascii => ConvField::AsciiChar,
};
self.sync_draft();
}
pub fn ratio_width(&self) -> RatioWidth {
self.ratio_width
}
pub fn set_ratio_width(&mut self, width: RatioWidth) {
self.ratio_width = width;
}
pub fn cycle_ratio_width(&mut self) {
self.ratio_width = match self.ratio_width {
RatioWidth::Byte => RatioWidth::Word,
RatioWidth::Word => RatioWidth::Byte,
};
}
pub fn precision_idx(&self) -> usize {
self.precision_idx
}
pub fn precision(&self) -> f64 {
PRECISION_PRESETS[self.precision_idx.min(PRECISION_PRESETS.len() - 1)]
}
pub fn precision_text(&self) -> String {
let p = self.precision();
format!("{p:.0e}")
}
pub fn cycle_precision(&mut self) {
self.precision_idx = (self.precision_idx + 1) % PRECISION_PRESETS.len();
}
pub fn ratio(&self) -> Option<RatioResult> {
let x = self.float_value() as f64;
approximate_ratio(x, self.ratio_width.max(), self.precision())
}
pub fn ratio_num_text(&self) -> String {
match self.ratio() {
Some(r) => format!("{}", r.num),
None => "—".into(),
}
}
pub fn ratio_den_text(&self) -> String {
match self.ratio() {
Some(r) => format!("{}", r.den),
None => "—".into(),
}
}
pub fn ratio_frac_text(&self) -> String {
match self.ratio() {
Some(r) => format!("{} / {}", r.num, r.den),
None => "—".into(),
}
}
pub fn ratio_approx_text(&self) -> String {
match self.ratio() {
Some(r) => format!("{:.10}", r.approx).trim_end_matches('0').trim_end_matches('.').to_string(),
None => "—".into(),
}
}
pub fn ratio_error_text(&self) -> String {
match self.ratio() {
Some(r) if r.exact => "exact".into(),
Some(r) => format!("{:.3e}", r.rel_error),
None => "—".into(),
}
}
pub fn endian(&self) -> Endian {
self.endian
}
pub fn endian_index(&self) -> i32 {
self.endian.index()
}
pub fn active(&self) -> ConvField {
self.active
}
pub fn error(&self) -> Option<&'static str> {
self.error
}
pub fn set_endian(&mut self, endian: Endian) {
self.commit_draft();
self.endian = endian;
self.sync_draft();
}
pub fn set_active(&mut self, field: ConvField) {
let ok = match self.panel {
ConvPanel::Words => matches!(
field,
ConvField::Float | ConvField::Word0 | ConvField::Word1
),
ConvPanel::Ratio => field == ConvField::Float,
ConvPanel::Ascii => matches!(
field,
ConvField::AsciiChar | ConvField::AsciiHex | ConvField::AsciiDec
),
};
if !ok || field == self.active {
return;
}
self.commit_draft();
self.active = field;
self.sync_draft();
}
pub fn ascii_code(&self) -> u8 {
self.ascii_code
}
pub fn ascii_char_text(&self) -> String {
if self.active == ConvField::AsciiChar {
return self.draft.clone();
}
format_ascii_glyph(self.ascii_code)
}
pub fn ascii_hex_text(&self) -> String {
if self.active == ConvField::AsciiHex {
return self.draft.clone();
}
format!("{:02X}", self.ascii_code)
}
pub fn ascii_dec_text(&self) -> String {
if self.active == ConvField::AsciiDec {
return self.draft.clone();
}
format!("{}", self.ascii_code)
}
pub fn ascii_name_text(&self) -> String {
ascii_control_name(self.ascii_code)
.map(|n| n.to_string())
.unwrap_or_else(|| {
if self.ascii_code.is_ascii_graphic() || self.ascii_code == b' ' {
format!("'{}'", self.ascii_code as char)
} else {
format!("0x{:02X}", self.ascii_code)
}
})
}
pub fn float_value(&self) -> f32 {
f32::from_bits(self.float_bits)
}
pub fn words(&self) -> (u16, u16) {
float_to_words(self.float_value(), self.endian)
}
pub fn float_text(&self) -> String {
if self.active == ConvField::Float {
return self.draft.clone();
}
format_float(self.float_value())
}
pub fn word0_text(&self) -> String {
if self.active == ConvField::Word0 {
return self.draft.clone();
}
format!("{:04X}", self.words().0)
}
pub fn word1_text(&self) -> String {
if self.active == ConvField::Word1 {
return self.draft.clone();
}
format!("{:04X}", self.words().1)
}
pub fn wire_bytes_text(&self) -> String {
let (w0, w1) = self.words();
let a = w0.to_be_bytes();
let b = w1.to_be_bytes();
format!("{:02X} {:02X} {:02X} {:02X}", a[0], a[1], b[0], b[1])
}
pub fn ieee_hex_text(&self) -> String {
format!("0x{:08X}", self.float_bits)
}
pub fn clear_active(&mut self) {
self.error = None;
self.draft = match self.active {
ConvField::Float => "0".into(),
ConvField::Word0 | ConvField::Word1 => "0".into(),
ConvField::AsciiChar => " ".into(),
ConvField::AsciiHex => "00".into(),
ConvField::AsciiDec => "0".into(),
};
let _ = self.apply_draft();
}
pub fn backspace(&mut self) {
self.error = None;
if self.active == ConvField::AsciiChar {
self.draft = " ".into();
self.ascii_code = b' ';
return;
}
self.draft.pop();
if self.draft.is_empty() {
self.draft = match self.active {
ConvField::AsciiHex => "0".into(),
_ => "0".into(),
};
}
let _ = self.apply_draft();
}
pub fn input_char(&mut self, ch: char) {
self.error = None;
match self.active {
ConvField::Float => {
let ch = ch.to_ascii_uppercase();
if !matches!(ch, '0'..='9' | '.' | '-' | '+' | 'E') {
return;
}
if self.draft == "0" && ch != '.' && ch != 'E' {
self.draft = ch.to_string();
} else if self.draft == "-0" && ch != '.' && ch != 'E' {
self.draft = format!("-{ch}");
} else {
self.draft.push(ch);
}
}
ConvField::Word0 | ConvField::Word1 => {
let ch = ch.to_ascii_uppercase();
if !ch.is_ascii_hexdigit() {
return;
}
if self.draft == "0" {
self.draft = ch.to_string();
} else if self.draft.len() < 4 {
self.draft.push(ch);
} else {
return;
}
}
ConvField::AsciiChar => {
if (ch as u32) > 255 {
return;
}
self.ascii_code = ch as u8;
self.draft = format_ascii_glyph(self.ascii_code);
return;
}
ConvField::AsciiHex => {
let ch = ch.to_ascii_uppercase();
if !ch.is_ascii_hexdigit() {
return;
}
if self.draft == "0" {
self.draft = ch.to_string();
} else if self.draft.len() < 2 {
self.draft.push(ch);
} else {
self.draft = ch.to_string();
}
}
ConvField::AsciiDec => {
if !ch.is_ascii_digit() {
return;
}
if self.draft == "0" {
self.draft = ch.to_string();
} else if self.draft.len() < 3 {
self.draft.push(ch);
} else {
self.draft = ch.to_string();
}
}
}
let _ = self.apply_draft();
}
pub fn negate_float(&mut self) {
if self.panel != ConvPanel::Words && self.panel != ConvPanel::Ratio {
return;
}
if self.active != ConvField::Float {
self.set_active(ConvField::Float);
}
if let Some(rest) = self.draft.strip_prefix('-') {
self.draft = rest.to_string();
if self.draft.is_empty() {
self.draft = "0".into();
}
} else if self.draft != "0" {
self.draft = format!("-{}", self.draft);
}
let _ = self.apply_draft();
}
fn commit_draft(&mut self) {
let _ = self.apply_draft();
self.sync_draft();
}
fn sync_draft(&mut self) {
self.draft = match self.active {
ConvField::Float => format_float(self.float_value()),
ConvField::Word0 => format!("{:04X}", self.words().0),
ConvField::Word1 => format!("{:04X}", self.words().1),
ConvField::AsciiChar => format_ascii_glyph(self.ascii_code),
ConvField::AsciiHex => format!("{:02X}", self.ascii_code),
ConvField::AsciiDec => format!("{}", self.ascii_code),
};
self.error = None;
}
fn apply_draft(&mut self) -> Result<(), ()> {
match self.active {
ConvField::Float => {
let t = self.draft.trim();
if t.is_empty() || t == "-" || t == "." || t == "-." {
return Ok(());
}
match t.parse::<f32>() {
Ok(v) => {
self.float_bits = v.to_bits();
self.error = None;
Ok(())
}
Err(_) => {
self.error = Some("Invalid float");
Err(())
}
}
}
ConvField::Word0 => {
let w0 = parse_word(&self.draft)?;
let w1 = self.words().1;
self.float_bits = words_to_float(w0, w1, self.endian).to_bits();
self.error = None;
Ok(())
}
ConvField::Word1 => {
let w0 = self.words().0;
let w1 = parse_word(&self.draft)?;
self.float_bits = words_to_float(w0, w1, self.endian).to_bits();
self.error = None;
Ok(())
}
ConvField::AsciiChar => {
let t = self.draft.trim();
if t.is_empty() {
self.ascii_code = b' ';
} else if let Some(c) = t.chars().next() {
if (c as u32) <= 255 {
self.ascii_code = c as u8;
} else {
self.error = Some("Not Latin-1");
return Err(());
}
}
self.error = None;
Ok(())
}
ConvField::AsciiHex => {
let cleaned: String = self
.draft
.chars()
.filter(|c| c.is_ascii_hexdigit())
.collect();
if cleaned.is_empty() {
return Ok(());
}
match u8::from_str_radix(&cleaned, 16) {
Ok(v) => {
self.ascii_code = v;
self.error = None;
Ok(())
}
Err(_) => {
self.error = Some("Invalid hex");
Err(())
}
}
}
ConvField::AsciiDec => {
let t = self.draft.trim();
if t.is_empty() {
return Ok(());
}
match t.parse::<u16>() {
Ok(v) if v <= 255 => {
self.ascii_code = v as u8;
self.error = None;
Ok(())
}
Ok(_) => {
self.error = Some("Max 255");
Err(())
}
Err(_) => {
self.error = Some("Invalid dec");
Err(())
}
}
}
}
}
}
fn format_ascii_glyph(code: u8) -> String {
if let Some(name) = ascii_control_name(code) {
format!("<{name}>")
} else if code == b' ' {
" ".into()
} else if code.is_ascii_graphic() {
(code as char).to_string()
} else {
// Latin-1 printable or other
match char::from_u32(code as u32) {
Some(c) if !c.is_control() => c.to_string(),
_ => format!("<{code:02X}>"),
}
}
}
fn ascii_control_name(code: u8) -> Option<&'static str> {
Some(match code {
0x00 => "NUL",
0x01 => "SOH",
0x02 => "STX",
0x03 => "ETX",
0x04 => "EOT",
0x05 => "ENQ",
0x06 => "ACK",
0x07 => "BEL",
0x08 => "BS",
0x09 => "TAB",
0x0A => "LF",
0x0B => "VT",
0x0C => "FF",
0x0D => "CR",
0x0E => "SO",
0x0F => "SI",
0x10 => "DLE",
0x11 => "DC1",
0x12 => "DC2",
0x13 => "DC3",
0x14 => "DC4",
0x15 => "NAK",
0x16 => "SYN",
0x17 => "ETB",
0x18 => "CAN",
0x19 => "EM",
0x1A => "SUB",
0x1B => "ESC",
0x1C => "FS",
0x1D => "GS",
0x1E => "RS",
0x1F => "US",
0x7F => "DEL",
_ => return None,
})
}
/// Best rational approximation of `x` with num, den ∈ [0..=max_val], den ≥ 1.
/// Stops early when relative error ≤ `max_rel_err` (smallest such convergent preferred).
pub fn approximate_ratio(x: f64, max_val: u32, max_rel_err: f64) -> Option<RatioResult> {
if !x.is_finite() {
return None;
}
if x == 0.0 {
return Some(RatioResult {
num: 0,
den: 1,
approx: 0.0,
rel_error: 0.0,
exact: true,
});
}
let x = x.abs();
let max_val = max_val as u64;
let mut best = RatioResult {
num: 0,
den: 1,
approx: 0.0,
rel_error: 1.0,
exact: false,
};
let consider = |n: u64, d: u64, best: &mut RatioResult| {
if d == 0 || d > max_val || n > max_val {
return;
}
let approx = n as f64 / d as f64;
let rel = if x == 0.0 {
0.0
} else {
(approx - x).abs() / x
};
let exact = (approx - x).abs() <= f64::EPSILON * x.max(1.0) * 4.0;
if rel < best.rel_error
|| ((rel - best.rel_error).abs() < 1e-18 && d < best.den as u64)
|| ((rel - best.rel_error).abs() < 1e-18 && d == best.den as u64 && n < best.num as u64)
{
*best = RatioResult {
num: n as u32,
den: d as u32,
approx,
rel_error: rel,
exact,
};
}
};
// Continued fraction: h₋₂=0,k₋₂=1 ; h₋₁=1,k₋₁=0
let mut h2 = 0u64;
let mut k2 = 1u64;
let mut h1 = 1u64;
let mut k1 = 0u64;
let mut remaining = x;
for _ in 0..64 {
if best.rel_error <= max_rel_err {
break;
}
let a = remaining.floor() as u64;
let t_start = if a == 0 { 0 } else { 1 };
let mut last_h = h1;
let mut last_k = k1;
let mut fitted = false;
for t in t_start..=a {
let Some(hn) = h1.checked_mul(t).and_then(|v| v.checked_add(h2)) else {
fitted = false;
break;
};
let Some(kn) = k1.checked_mul(t).and_then(|v| v.checked_add(k2)) else {
fitted = false;
break;
};
if hn > max_val || kn > max_val {
let mut lo = t_start;
let mut hi = t.saturating_sub(1);
while lo < hi {
let mid = (lo + hi + 1) / 2;
let ok = h1
.checked_mul(mid)
.and_then(|v| v.checked_add(h2))
.zip(k1.checked_mul(mid).and_then(|v| v.checked_add(k2)))
.is_some_and(|(h, k)| h <= max_val && k <= max_val);
if ok {
lo = mid;
} else {
hi = mid - 1;
}
}
if a == 0 || lo >= t_start {
if let (Some(hn), Some(kn)) = (
h1.checked_mul(lo).and_then(|v| v.checked_add(h2)),
k1.checked_mul(lo).and_then(|v| v.checked_add(k2)),
) {
if kn > 0 && hn <= max_val && kn <= max_val {
consider(hn, kn, &mut best);
}
}
}
fitted = false;
break;
}
consider(hn, kn, &mut best);
last_h = hn;
last_k = kn;
fitted = true;
if best.rel_error <= max_rel_err {
break;
}
}
if best.rel_error <= max_rel_err {
break;
}
if fitted {
h2 = h1;
k2 = k1;
h1 = last_h;
k1 = last_k;
} else {
break;
}
let frac = remaining - a as f64;
if frac <= 1e-18 {
break;
}
remaining = 1.0 / frac;
}
Some(best)
}
fn parse_word(s: &str) -> Result<u16, ()> {
let cleaned: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
if cleaned.is_empty() {
return Ok(0);
}
u16::from_str_radix(&cleaned, 16).map_err(|_| ())
}
fn format_float(v: f32) -> String {
if !v.is_finite() {
return if v.is_nan() {
"NaN".into()
} else if v.is_sign_negative() {
"-Inf".into()
} else {
"Inf".into()
};
}
let s = format!("{v}");
s
}
/// IEEE float bytes in natural order A B C D (big-endian bit pattern),
/// then rearrange into two register words for the selected endian.
pub fn float_to_words(f: f32, endian: Endian) -> (u16, u16) {
let abcd = f.to_be_bytes(); // [A, B, C, D]
let wire = match endian {
Endian::AbCd => abcd,
Endian::CdAb => [abcd[2], abcd[3], abcd[0], abcd[1]],
Endian::BaDc => [abcd[1], abcd[0], abcd[3], abcd[2]],
Endian::DcBa => [abcd[3], abcd[2], abcd[1], abcd[0]],
};
let w0 = u16::from_be_bytes([wire[0], wire[1]]);
let w1 = u16::from_be_bytes([wire[2], wire[3]]);
(w0, w1)
}
pub fn words_to_float(w0: u16, w1: u16, endian: Endian) -> f32 {
let a = w0.to_be_bytes();
let b = w1.to_be_bytes();
let wire = [a[0], a[1], b[0], b[1]];
let abcd = match endian {
Endian::AbCd => wire,
Endian::CdAb => [wire[2], wire[3], wire[0], wire[1]],
Endian::BaDc => [wire[1], wire[0], wire[3], wire[2]],
Endian::DcBa => [wire[3], wire[2], wire[1], wire[0]],
};
f32::from_be_bytes(abcd)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_point_zero_abcd() {
let (w0, w1) = float_to_words(1.0, Endian::AbCd);
assert_eq!(w0, 0x3F80);
assert_eq!(w1, 0x0000);
assert_eq!(words_to_float(w0, w1, Endian::AbCd), 1.0);
}
#[test]
fn one_point_zero_dcba() {
let (w0, w1) = float_to_words(1.0, Endian::DcBa);
assert_eq!(w0, 0x0000);
assert_eq!(w1, 0x803F);
assert_eq!(words_to_float(w0, w1, Endian::DcBa), 1.0);
}
#[test]
fn one_point_zero_cdab() {
let (w0, w1) = float_to_words(1.0, Endian::CdAb);
assert_eq!(w0, 0x0000);
assert_eq!(w1, 0x3F80);
assert_eq!(words_to_float(w0, w1, Endian::CdAb), 1.0);
}
#[test]
fn roundtrip_all_endians() {
let samples = [0.0f32, -1.0, 3.1415927, 12345.678, f32::MIN_POSITIVE];
for e in Endian::all() {
for &f in &samples {
let (w0, w1) = float_to_words(f, e);
let back = words_to_float(w0, w1, e);
assert_eq!(back.to_bits(), f.to_bits(), "endian {:?}", e);
}
}
}
#[test]
fn pi_word_ratio() {
let r = approximate_ratio(std::f64::consts::PI, 65535, 1e-6).unwrap();
assert_eq!((r.num, r.den), (355, 113));
}
#[test]
fn half_byte_ratio() {
let r = approximate_ratio(0.5, 255, 1e-9).unwrap();
assert_eq!((r.num, r.den), (1, 2));
}
#[test]
fn gear_ratio_example() {
// 17/23 electronic gear style
let r = approximate_ratio(17.0 / 23.0, 255, 1e-9).unwrap();
assert_eq!((r.num, r.den), (17, 23));
}
#[test]
fn ascii_char_to_codes() {
let mut c = Converter::new();
c.set_panel(ConvPanel::Ascii);
c.set_active(ConvField::AsciiChar);
c.input_char('A');
assert_eq!(c.ascii_code(), 0x41);
assert_eq!(c.ascii_hex_text(), "41");
assert_eq!(c.ascii_dec_text(), "65");
}
#[test]
fn ascii_hex_to_char() {
let mut c = Converter::new();
c.set_panel(ConvPanel::Ascii);
c.set_active(ConvField::AsciiHex);
c.input_char('4');
c.input_char('1');
assert_eq!(c.ascii_code(), b'A');
assert_eq!(c.ascii_char_text(), "A");
}
}
+168
View File
@@ -0,0 +1,168 @@
//! Modbus RTU CRC-16 and Modbus ASCII LRC.
#[derive(Debug, Clone, Default)]
pub struct CrcTool {
/// Raw text from the input field (hex bytes, spaces optional).
input: String,
}
#[derive(Debug, Clone)]
pub struct CrcView {
pub input: String,
pub parsed_hex: String,
pub byte_count: usize,
pub rtu_crc_lo_hi: String,
pub rtu_crc_word: String,
pub rtu_frame: String,
pub ascii_lrc: String,
pub ascii_frame: String,
pub error: String,
}
impl CrcTool {
pub fn new() -> Self {
Self {
input: "01 03 00 00 00 0A".into(),
}
}
pub fn input(&self) -> &str {
&self.input
}
pub fn set_input(&mut self, text: impl Into<String>) {
self.input = text.into();
}
pub fn view(&self) -> CrcView {
match parse_hex_bytes(&self.input) {
Ok(bytes) => {
let crc = modbus_crc16(&bytes);
let lo = (crc & 0xFF) as u8;
let hi = (crc >> 8) as u8;
let lrc = modbus_lrc(&bytes);
let parsed_hex = format_bytes(&bytes);
let mut frame = bytes.clone();
frame.push(lo);
frame.push(hi);
let ascii_payload: String = bytes.iter().map(|b| format!("{b:02X}")).collect();
let ascii_frame = format!(":{ascii_payload}{lrc:02X}\\r\\n");
CrcView {
input: self.input.clone(),
parsed_hex,
byte_count: bytes.len(),
rtu_crc_lo_hi: format!("{lo:02X} {hi:02X}"),
rtu_crc_word: format!("0x{crc:04X}"),
rtu_frame: format_bytes(&frame),
ascii_lrc: format!("{lrc:02X}"),
ascii_frame,
error: String::new(),
}
}
Err(e) => CrcView {
input: self.input.clone(),
parsed_hex: String::new(),
byte_count: 0,
rtu_crc_lo_hi: "—".into(),
rtu_crc_word: "—".into(),
rtu_frame: "—".into(),
ascii_lrc: "—".into(),
ascii_frame: "—".into(),
error: e.into(),
},
}
}
}
/// Modbus RTU CRC-16 (poly 0xA001, init 0xFFFF). Returned value is host u16;
/// on the wire it is sent low-byte first.
pub fn modbus_crc16(data: &[u8]) -> u16 {
let mut crc = 0xFFFFu16;
for &b in data {
crc ^= u16::from(b);
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
crc
}
/// Modbus ASCII LRC: two's complement of the sum of bytes.
pub fn modbus_lrc(data: &[u8]) -> u8 {
let sum = data.iter().fold(0u8, |acc, &b| acc.wrapping_add(b));
(!sum).wrapping_add(1)
}
fn format_bytes(data: &[u8]) -> String {
data.iter()
.map(|b| format!("{b:02X}"))
.collect::<Vec<_>>()
.join(" ")
}
/// Accepts `01 03 00 00`, `01-03-00`, `0x01,0x03`, or continuous `01030000`.
fn parse_hex_bytes(text: &str) -> Result<Vec<u8>, &'static str> {
let cleaned: String = text
.chars()
.filter(|c| c.is_ascii_hexdigit())
.collect::<String>()
.to_ascii_uppercase();
if cleaned.is_empty() {
return Ok(Vec::new());
}
if cleaned.len() % 2 != 0 {
return Err("Odd number of hex digits");
}
let mut out = Vec::with_capacity(cleaned.len() / 2);
let bytes = cleaned.as_bytes();
for i in (0..bytes.len()).step_by(2) {
let hi = from_hex(bytes[i])?;
let lo = from_hex(bytes[i + 1])?;
out.push((hi << 4) | lo);
}
Ok(out)
}
fn from_hex(b: u8) -> Result<u8, &'static str> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'A'..=b'F' => Ok(b - b'A' + 10),
b'a'..=b'f' => Ok(b - b'a' + 10),
_ => Err("Invalid hex"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_modbus_crc() {
// 01 03 00 00 00 0A → CRC lo hi = C5 CD
let data = [0x01u8, 0x03, 0x00, 0x00, 0x00, 0x0A];
let crc = modbus_crc16(&data);
assert_eq!(crc & 0xFF, 0xC5);
assert_eq!(crc >> 8, 0xCD);
}
#[test]
fn known_modbus_lrc() {
let data = [0x01u8, 0x03, 0x00, 0x00, 0x00, 0x0A];
assert_eq!(modbus_lrc(&data), 0xF2);
}
#[test]
fn parse_spaced() {
let b = parse_hex_bytes("01 03 00 00 00 0A").unwrap();
assert_eq!(b, vec![0x01, 0x03, 0x00, 0x00, 0x00, 0x0A]);
}
}
+425
View File
@@ -0,0 +1,425 @@
//! Calculator logic (standard / Windows-like).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Op {
Add,
Sub,
Mul,
Div,
}
impl Op {
pub fn symbol(self) -> &'static str {
match self {
Op::Add => "+",
Op::Sub => "−",
Op::Mul => "×",
Op::Div => "÷",
}
}
fn apply(self, a: f64, b: f64) -> Result<f64, CalcError> {
let r = match self {
Op::Add => a + b,
Op::Sub => a - b,
Op::Mul => a * b,
Op::Div => {
if b == 0.0 {
return Err(CalcError::DivByZero);
}
a / b
}
};
if !r.is_finite() {
Err(CalcError::Overflow)
} else {
Ok(r)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CalcError {
DivByZero,
Overflow,
Invalid,
}
impl CalcError {
pub fn message(self) -> &'static str {
match self {
CalcError::DivByZero => "Cannot divide by zero",
CalcError::Overflow => "Overflow",
CalcError::Invalid => "Invalid input",
}
}
}
#[derive(Debug, Clone)]
pub struct Calculator {
/// Digits currently being entered (or last result as string).
entry: String,
/// True while the user is typing a new number.
typing: bool,
/// Left operand waiting for the next number / equals.
pending: Option<f64>,
/// Pending binary operator.
op: Option<Op>,
/// Expression line above the main display (e.g. "12 +").
expression: String,
/// Last error, if any.
error: Option<CalcError>,
/// Memory register.
memory: f64,
/// Whether memory has been set (for UI hint).
memory_set: bool,
}
impl Default for Calculator {
fn default() -> Self {
Self::new()
}
}
impl Calculator {
pub fn new() -> Self {
Self {
entry: "0".into(),
typing: false,
pending: None,
op: None,
expression: String::new(),
error: None,
memory: 0.0,
memory_set: false,
}
}
pub fn display(&self) -> &str {
if let Some(err) = self.error {
return err.message();
}
&self.entry
}
pub fn expression(&self) -> &str {
&self.expression
}
pub fn has_memory(&self) -> bool {
self.memory_set
}
pub fn clear_all(&mut self) {
*self = Self {
memory: self.memory,
memory_set: self.memory_set,
..Self::new()
};
}
pub fn clear_entry(&mut self) {
self.error = None;
self.entry = "0".into();
self.typing = false;
}
pub fn backspace(&mut self) {
if self.error.is_some() {
self.clear_entry();
return;
}
if !self.typing {
return;
}
self.entry.pop();
if self.entry.is_empty() || self.entry == "-" {
self.entry = "0".into();
self.typing = false;
}
}
pub fn input_digit(&mut self, d: char) {
debug_assert!(d.is_ascii_digit());
if self.error.is_some() {
self.clear_all();
}
if !self.typing {
self.entry = d.to_string();
self.typing = true;
return;
}
if self.entry == "0" {
self.entry = d.to_string();
} else if self.entry == "-0" {
self.entry = format!("-{d}");
} else if digit_count(&self.entry) < 16 {
self.entry.push(d);
}
}
pub fn input_dot(&mut self) {
if self.error.is_some() {
self.clear_all();
}
if !self.typing {
self.entry = "0.".into();
self.typing = true;
return;
}
if !self.entry.contains('.') {
self.entry.push('.');
}
}
pub fn negate(&mut self) {
if self.error.is_some() {
return;
}
if let Ok(v) = self.current_value() {
self.set_entry(-v);
self.typing = true;
}
}
pub fn percent(&mut self) {
if self.error.is_some() {
return;
}
let Ok(cur) = self.current_value() else {
return;
};
let result = match (self.pending, self.op) {
(Some(a), Some(Op::Add | Op::Sub)) => a * cur / 100.0,
_ => cur / 100.0,
};
self.set_entry(result);
self.typing = false;
}
pub fn reciprocal(&mut self) {
self.unary(|x| {
if x == 0.0 {
Err(CalcError::DivByZero)
} else {
Ok(1.0 / x)
}
});
}
pub fn square(&mut self) {
self.unary(|x| {
let r = x * x;
if r.is_finite() {
Ok(r)
} else {
Err(CalcError::Overflow)
}
});
}
pub fn sqrt(&mut self) {
self.unary(|x| {
if x < 0.0 {
Err(CalcError::Invalid)
} else {
Ok(x.sqrt())
}
});
}
pub fn set_op(&mut self, op: Op) {
if self.error.is_some() {
return;
}
if let Err(e) = self.commit_pending() {
self.fail(e);
return;
}
let Ok(v) = self.current_value() else {
return;
};
self.pending = Some(v);
self.op = Some(op);
self.expression = format!("{} {}", format_number(v), op.symbol());
self.typing = false;
}
pub fn equals(&mut self) {
if self.error.is_some() {
return;
}
let Some(op) = self.op else {
self.expression.clear();
self.typing = false;
return;
};
let Ok(b) = self.current_value() else {
return;
};
let a = self.pending.unwrap_or(b);
match op.apply(a, b) {
Ok(r) => {
self.expression = format!(
"{} {} {} =",
format_number(a),
op.symbol(),
format_number(b)
);
self.set_entry(r);
self.pending = None;
self.op = None;
self.typing = false;
}
Err(e) => self.fail(e),
}
}
// --- Memory ---
pub fn memory_clear(&mut self) {
self.memory = 0.0;
self.memory_set = false;
}
pub fn memory_recall(&mut self) {
if self.error.is_some() {
self.clear_all();
}
self.set_entry(self.memory);
self.typing = false;
}
pub fn memory_add(&mut self) {
if self.error.is_some() {
return;
}
if let Ok(v) = self.current_value() {
self.memory += v;
self.memory_set = true;
self.typing = false;
}
}
pub fn memory_sub(&mut self) {
if self.error.is_some() {
return;
}
if let Ok(v) = self.current_value() {
self.memory -= v;
self.memory_set = true;
self.typing = false;
}
}
pub fn memory_store(&mut self) {
if self.error.is_some() {
return;
}
if let Ok(v) = self.current_value() {
self.memory = v;
self.memory_set = true;
self.typing = false;
}
}
// --- internals ---
fn unary(&mut self, f: impl FnOnce(f64) -> Result<f64, CalcError>) {
if self.error.is_some() {
return;
}
let Ok(v) = self.current_value() else {
return;
};
match f(v) {
Ok(r) => {
self.expression.clear();
self.set_entry(r);
self.typing = false;
}
Err(e) => self.fail(e),
}
}
fn commit_pending(&mut self) -> Result<(), CalcError> {
let (Some(a), Some(op)) = (self.pending, self.op) else {
return Ok(());
};
if !self.typing {
return Ok(());
}
let b = self.current_value()?;
let r = op.apply(a, b)?;
self.set_entry(r);
self.pending = Some(r);
self.op = None;
Ok(())
}
fn current_value(&self) -> Result<f64, CalcError> {
self.entry
.parse::<f64>()
.map_err(|_| CalcError::Invalid)
}
fn set_entry(&mut self, v: f64) {
self.entry = format_number(v);
self.error = None;
}
fn fail(&mut self, e: CalcError) {
self.error = Some(e);
self.pending = None;
self.op = None;
self.expression.clear();
self.typing = false;
}
}
fn digit_count(s: &str) -> usize {
s.chars().filter(|c| c.is_ascii_digit()).count()
}
fn format_number(v: f64) -> String {
if !v.is_finite() {
return "0".into();
}
// Prefer integer display when close enough.
if v.fract().abs() < 1e-12 && v.abs() < 1e15 {
return format!("{}", v as i64);
}
let s = format!("{:.12}", v);
let s = s.trim_end_matches('0').trim_end_matches('.').to_string();
if s.is_empty() || s == "-" {
"0".into()
} else {
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_add() {
let mut c = Calculator::new();
c.input_digit('1');
c.input_digit('2');
c.set_op(Op::Add);
c.input_digit('3');
c.equals();
assert_eq!(c.display(), "15");
}
#[test]
fn div_by_zero() {
let mut c = Calculator::new();
c.input_digit('1');
c.set_op(Op::Div);
c.input_digit('0');
c.equals();
assert!(c.display().contains("zero"));
}
}
+370
View File
@@ -0,0 +1,370 @@
// Hide the extra console window on Windows GUI builds.
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
mod convert;
mod crc;
mod engine;
mod programmer;
use std::cell::RefCell;
use std::rc::Rc;
use convert::{ConvField, ConvPanel, Converter, Endian, RatioWidth};
use crc::CrcTool;
use engine::{Calculator, Op};
use programmer::{Base, ProgOp, Programmer};
use slint::{ComponentHandle, ModelRc, VecModel};
slint::include_modules!();
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Standard,
Programmer,
Convert,
Crc,
}
struct State {
mode: Mode,
std: Calculator,
prog: Programmer,
conv: Converter,
crc: CrcTool,
show_bits: bool,
win_profile: u32,
}
impl Default for State {
fn default() -> Self {
Self {
mode: Mode::Standard,
std: Calculator::new(),
prog: Programmer::new(),
conv: Converter::new(),
crc: CrcTool::new(),
show_bits: true,
win_profile: u32::MAX,
}
}
}
fn main() -> Result<(), slint::PlatformError> {
// Platform must exist first — calling set_xdg_app_id before AppWindow::new
// returns NoPlatform and the Wayland app_id stays unset (KDE shows the "W" icon).
let ui = AppWindow::new()?;
// Must match the .desktop basename (rcalc.desktop → "rcalc"), before show/run.
slint::set_xdg_app_id("rcalc")?;
let state = Rc::new(RefCell::new(State::default()));
refresh(&ui, &mut state.borrow_mut());
let ui_weak = ui.as_weak();
let state_keys = state.clone();
ui.on_key_pressed(move |id| {
let mut s = state_keys.borrow_mut();
handle_key(&mut s, id.as_str());
if let Some(ui) = ui_weak.upgrade() {
refresh(&ui, &mut s);
}
});
let ui_weak = ui.as_weak();
let state_crc = state.clone();
ui.on_crc_edited(move |text| {
let mut s = state_crc.borrow_mut();
s.crc.set_input(text.as_str());
if let Some(ui) = ui_weak.upgrade() {
apply_crc_results(&ui, &s);
}
});
ui.run()
}
fn bit_row_model(row: &[(bool, u32)]) -> ModelRc<BitInfo> {
let items: Vec<BitInfo> = row
.iter()
.map(|(on, idx)| BitInfo {
value: *on,
index: *idx as i32,
})
.collect();
ModelRc::new(VecModel::from(items))
}
fn window_profile(state: &State) -> u32 {
let mode = match state.mode {
Mode::Standard => 0,
Mode::Programmer => 1,
Mode::Convert => 2,
Mode::Crc => 3,
};
let bits = if state.show_bits {
state.prog.word_size().bits()
} else {
0
};
let panel = match state.conv.panel() {
ConvPanel::Words => 0,
ConvPanel::Ratio => 1,
ConvPanel::Ascii => 2,
};
mode * 1000 + bits * 10 + panel
}
fn fit_window(ui: &AppWindow, state: &mut State) {
let profile = window_profile(state);
if state.win_profile == profile {
return;
}
state.win_profile = profile;
let (w, h) = match state.mode {
Mode::Standard => (340.0, 560.0),
Mode::Programmer => {
let bit_h = if state.show_bits {
match state.prog.word_size().bits() {
64 => 120.0,
32 => 64.0,
16 => 36.0,
_ => 36.0,
}
} else {
0.0
};
(420.0, 720.0 + bit_h)
}
Mode::Convert => (420.0, 820.0),
Mode::Crc => (460.0, 560.0),
};
ui.window()
.set_size(slint::LogicalSize::new(w as f32, h as f32));
}
fn apply_crc_results(ui: &AppWindow, state: &State) {
let v = state.crc.view();
ui.set_crc_parsed(v.parsed_hex.into());
ui.set_crc_count(v.byte_count.to_string().into());
ui.set_crc_rtu_bytes(v.rtu_crc_lo_hi.into());
ui.set_crc_rtu_word(v.rtu_crc_word.into());
ui.set_crc_rtu_frame(v.rtu_frame.into());
ui.set_crc_ascii_lrc(v.ascii_lrc.into());
ui.set_crc_ascii_frame(v.ascii_frame.into());
ui.set_crc_error(v.error.into());
}
fn refresh(ui: &AppWindow, state: &mut State) {
ui.set_mode(match state.mode {
Mode::Standard => 0,
Mode::Programmer => 1,
Mode::Convert => 2,
Mode::Crc => 3,
});
fit_window(ui, state);
match state.mode {
Mode::Standard => {
ui.set_display_text(state.std.display().into());
ui.set_expression_text(state.std.expression().into());
ui.set_has_memory(state.std.has_memory());
}
Mode::Programmer => {
ui.set_display_text(state.prog.display().into());
ui.set_expression_text(state.prog.expression().into());
ui.set_has_memory(false);
ui.set_hex_text(state.prog.hex_text().into());
ui.set_dec_text(state.prog.dec_text().into());
ui.set_oct_text(state.prog.oct_text().into());
ui.set_bin_text(state.prog.bin_text().into());
ui.set_active_base(match state.prog.base() {
Base::Hex => 0,
Base::Dec => 1,
Base::Oct => 2,
Base::Bin => 3,
});
ui.set_word_size_label(state.prog.word_size().label().into());
ui.set_show_bits(state.show_bits);
let rows = state.prog.bit_rows();
ui.set_bit_row_count(rows.len() as i32);
ui.set_bit_row0(bit_row_model(rows.first().map(|r| r.as_slice()).unwrap_or(&[])));
ui.set_bit_row1(bit_row_model(rows.get(1).map(|r| r.as_slice()).unwrap_or(&[])));
ui.set_bit_row2(bit_row_model(rows.get(2).map(|r| r.as_slice()).unwrap_or(&[])));
ui.set_bit_row3(bit_row_model(rows.get(3).map(|r| r.as_slice()).unwrap_or(&[])));
}
Mode::Convert => {
ui.set_conv_panel(match state.conv.panel() {
ConvPanel::Words => 0,
ConvPanel::Ratio => 1,
ConvPanel::Ascii => 2,
});
ui.set_conv_endian(state.conv.endian_index());
ui.set_conv_active(match state.conv.active() {
ConvField::Float | ConvField::AsciiChar => 0,
ConvField::Word0 | ConvField::AsciiHex => 1,
ConvField::Word1 | ConvField::AsciiDec => 2,
});
ui.set_conv_float(state.conv.float_text().into());
ui.set_conv_word0(state.conv.word0_text().into());
ui.set_conv_word1(state.conv.word1_text().into());
ui.set_conv_bytes(state.conv.wire_bytes_text().into());
ui.set_conv_ieee(state.conv.ieee_hex_text().into());
ui.set_conv_error(state.conv.error().unwrap_or("").into());
ui.set_ratio_width(match state.conv.ratio_width() {
RatioWidth::Byte => 0,
RatioWidth::Word => 1,
});
ui.set_ratio_precision(state.conv.precision_text().into());
ui.set_ratio_num(state.conv.ratio_num_text().into());
ui.set_ratio_den(state.conv.ratio_den_text().into());
ui.set_ratio_frac(state.conv.ratio_frac_text().into());
ui.set_ratio_approx(state.conv.ratio_approx_text().into());
ui.set_ratio_error(state.conv.ratio_error_text().into());
ui.set_ascii_char(state.conv.ascii_char_text().into());
ui.set_ascii_hex(state.conv.ascii_hex_text().into());
ui.set_ascii_dec(state.conv.ascii_dec_text().into());
ui.set_ascii_name(state.conv.ascii_name_text().into());
}
Mode::Crc => {
ui.set_crc_input(state.crc.input().into());
apply_crc_results(ui, state);
}
}
}
fn handle_key(state: &mut State, id: &str) {
match id {
"mode:std" => {
state.mode = Mode::Standard;
return;
}
"mode:prog" => {
state.mode = Mode::Programmer;
return;
}
"mode:conv" => {
state.mode = Mode::Convert;
return;
}
"mode:crc" => {
state.mode = Mode::Crc;
return;
}
_ => {}
}
match state.mode {
Mode::Standard => handle_standard(&mut state.std, id),
Mode::Programmer => handle_programmer(state, id),
Mode::Convert => handle_convert(&mut state.conv, id),
Mode::Crc => {}
}
}
fn handle_standard(calc: &mut Calculator, id: &str) {
match id {
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => {
calc.input_digit(id.chars().next().unwrap());
}
"." | "," => calc.input_dot(),
"+" => calc.set_op(Op::Add),
"-" | "−" => calc.set_op(Op::Sub),
"*" | "×" => calc.set_op(Op::Mul),
"/" | "÷" => calc.set_op(Op::Div),
"=" => calc.equals(),
"%" => calc.percent(),
"CE" => calc.clear_entry(),
"C" => calc.clear_all(),
"BS" => calc.backspace(),
"1/x" => calc.reciprocal(),
"x2" => calc.square(),
"sqrt" => calc.sqrt(),
"neg" => calc.negate(),
"MC" => calc.memory_clear(),
"MR" => calc.memory_recall(),
"M+" => calc.memory_add(),
"M-" => calc.memory_sub(),
"MS" => calc.memory_store(),
_ => {}
}
}
fn handle_programmer(state: &mut State, id: &str) {
if id == "bits" {
state.show_bits = !state.show_bits;
return;
}
if let Some(rest) = id.strip_prefix("bit:") {
if let Ok(bit) = rest.parse::<u32>() {
state.prog.toggle_bit(bit);
}
return;
}
let calc = &mut state.prog;
match id {
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D"
| "E" | "F" | "a" | "b" | "c" | "d" | "e" | "f" => {
calc.input_digit(id.chars().next().unwrap());
}
"base:hex" => calc.set_base(Base::Hex),
"base:dec" => calc.set_base(Base::Dec),
"base:oct" => calc.set_base(Base::Oct),
"base:bin" => calc.set_base(Base::Bin),
"word" => calc.cycle_word_size(),
"+" => calc.set_op(ProgOp::Add),
"-" | "−" => calc.set_op(ProgOp::Sub),
"*" | "×" => calc.set_op(ProgOp::Mul),
"/" | "÷" => calc.set_op(ProgOp::Div),
"%" => calc.set_op(ProgOp::Mod),
"AND" => calc.set_op(ProgOp::And),
"OR" => calc.set_op(ProgOp::Or),
"XOR" => calc.set_op(ProgOp::Xor),
"NAND" => calc.set_op(ProgOp::Nand),
"NOR" => calc.set_op(ProgOp::Nor),
"Lsh" => calc.set_op(ProgOp::Lsh),
"Rsh" => calc.set_op(ProgOp::Rsh),
"NOT" => calc.not(),
"=" => calc.equals(),
"CE" => calc.clear_entry(),
"clear" => calc.clear_all(),
"BS" => calc.backspace(),
"neg" => calc.negate(),
_ => {}
}
}
fn handle_convert(conv: &mut Converter, id: &str) {
match id {
"panel:words" => conv.set_panel(ConvPanel::Words),
"panel:ratio" => conv.set_panel(ConvPanel::Ratio),
"panel:ascii" => conv.set_panel(ConvPanel::Ascii),
"endian:0" => conv.set_endian(Endian::AbCd),
"endian:1" => conv.set_endian(Endian::CdAb),
"endian:2" => conv.set_endian(Endian::BaDc),
"endian:3" => conv.set_endian(Endian::DcBa),
"rwidth:byte" => conv.set_ratio_width(RatioWidth::Byte),
"rwidth:word" => conv.set_ratio_width(RatioWidth::Word),
"rprec" => conv.cycle_precision(),
"conv:float" => conv.set_active(ConvField::Float),
"conv:w0" => conv.set_active(ConvField::Word0),
"conv:w1" => conv.set_active(ConvField::Word1),
"ascii:char" => conv.set_active(ConvField::AsciiChar),
"ascii:hex" => conv.set_active(ConvField::AsciiHex),
"ascii:dec" => conv.set_active(ConvField::AsciiDec),
"CE" | "clear" => conv.clear_active(),
"BS" => conv.backspace(),
"neg" => conv.negate_float(),
other => {
// Single-character input (digits, letters, space, punctuation).
let mut chars = other.chars();
if let (Some(ch), None) = (chars.next(), chars.next()) {
conv.input_char(ch);
}
}
}
}
+680
View File
@@ -0,0 +1,680 @@
//! Programmer calculator (Windows-like): integer bases + bitwise ops.
use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Base {
Hex,
Dec,
Oct,
Bin,
}
impl Base {
pub fn radix(self) -> u32 {
match self {
Base::Hex => 16,
Base::Dec => 10,
Base::Oct => 8,
Base::Bin => 2,
}
}
pub fn label(self) -> &'static str {
match self {
Base::Hex => "HEX",
Base::Dec => "DEC",
Base::Oct => "OCT",
Base::Bin => "BIN",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WordSize {
/// 64-bit (signed DEC).
Qword,
/// 32-bit unsigned (PLC DWORD).
Dword,
/// 32-bit signed (PLC DINT).
Dint,
/// 16-bit unsigned (PLC WORD).
Word,
/// 16-bit signed (PLC INT).
Int,
/// 8-bit (signed DEC).
Byte,
}
impl WordSize {
pub fn bits(self) -> u32 {
match self {
WordSize::Qword => 64,
WordSize::Dword | WordSize::Dint => 32,
WordSize::Word | WordSize::Int => 16,
WordSize::Byte => 8,
}
}
/// Signed two's-complement DEC display (INT / DINT / QWORD / BYTE).
pub fn is_signed(self) -> bool {
matches!(
self,
WordSize::Qword | WordSize::Dint | WordSize::Int | WordSize::Byte
)
}
pub fn label(self) -> &'static str {
match self {
WordSize::Qword => "QWORD",
WordSize::Dword => "DWORD",
WordSize::Dint => "DINT",
WordSize::Word => "WORD",
WordSize::Int => "INT",
WordSize::Byte => "BYTE",
}
}
pub fn cycle(self) -> Self {
match self {
WordSize::Qword => WordSize::Dword,
WordSize::Dword => WordSize::Dint,
WordSize::Dint => WordSize::Word,
WordSize::Word => WordSize::Int,
WordSize::Int => WordSize::Byte,
WordSize::Byte => WordSize::Qword,
}
}
fn mask(self) -> u64 {
match self.bits() {
64 => u64::MAX,
bits => (1u64 << bits) - 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgOp {
Add,
Sub,
Mul,
Div,
Mod,
And,
Or,
Xor,
Nand,
Nor,
Lsh,
Rsh,
}
impl ProgOp {
pub fn symbol(self) -> &'static str {
match self {
ProgOp::Add => "+",
ProgOp::Sub => "−",
ProgOp::Mul => "×",
ProgOp::Div => "÷",
ProgOp::Mod => "%",
ProgOp::And => "AND",
ProgOp::Or => "OR",
ProgOp::Xor => "XOR",
ProgOp::Nand => "NAND",
ProgOp::Nor => "NOR",
ProgOp::Lsh => "Lsh",
ProgOp::Rsh => "Rsh",
}
}
fn apply(self, a: u64, b: u64, size: WordSize) -> Result<u64, ProgError> {
let mask = size.mask();
let a = a & mask;
let b = b & mask;
let r = match self {
ProgOp::Add => a.wrapping_add(b),
ProgOp::Sub => a.wrapping_sub(b),
ProgOp::Mul => a.wrapping_mul(b),
ProgOp::Div => {
if b == 0 {
return Err(ProgError::DivByZero);
}
a / b
}
ProgOp::Mod => {
if b == 0 {
return Err(ProgError::DivByZero);
}
a % b
}
ProgOp::And => a & b,
ProgOp::Or => a | b,
ProgOp::Xor => a ^ b,
ProgOp::Nand => !(a & b),
ProgOp::Nor => !(a | b),
ProgOp::Lsh => {
let sh = (b as u32) % size.bits().max(1);
a << sh
}
ProgOp::Rsh => {
let sh = (b as u32) % size.bits().max(1);
a >> sh
}
};
Ok(r & mask)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgError {
DivByZero,
Invalid,
}
impl ProgError {
pub fn message(self) -> &'static str {
match self {
ProgError::DivByZero => "Cannot divide by zero",
ProgError::Invalid => "Invalid input",
}
}
}
#[derive(Debug, Clone)]
pub struct Programmer {
entry: String,
typing: bool,
value: u64,
pending: Option<u64>,
op: Option<ProgOp>,
expression: String,
error: Option<ProgError>,
base: Base,
word_size: WordSize,
}
impl Default for Programmer {
fn default() -> Self {
Self::new()
}
}
impl Programmer {
pub fn new() -> Self {
Self {
entry: "0".into(),
typing: false,
value: 0,
pending: None,
op: None,
expression: String::new(),
error: None,
base: Base::Dec,
word_size: WordSize::Qword,
}
}
pub fn base(&self) -> Base {
self.base
}
pub fn word_size(&self) -> WordSize {
self.word_size
}
pub fn expression(&self) -> &str {
&self.expression
}
pub fn display(&self) -> String {
if let Some(err) = self.error {
return err.message().into();
}
if self.typing {
format_grouped_entry(&self.entry, self.base)
} else {
format_value(self.value, self.base, self.word_size)
}
}
pub fn hex_text(&self) -> String {
format_value(self.current_bits(), Base::Hex, self.word_size)
}
pub fn dec_text(&self) -> String {
format_value(self.current_bits(), Base::Dec, self.word_size)
}
pub fn oct_text(&self) -> String {
format_value(self.current_bits(), Base::Oct, self.word_size)
}
pub fn bin_text(&self) -> String {
format_value(self.current_bits(), Base::Bin, self.word_size)
}
/// Bit rows for the clickable bit board (MSB-first within each row).
/// Each entry is `(is_set, bit_index)` with `bit_index` counted from LSB = 0.
pub fn bit_rows(&self) -> Vec<Vec<(bool, u32)>> {
let n = self.word_size.bits();
let v = self.current_bits();
let cols: u32 = if n <= 8 { 8 } else { 16 };
let mut rows = Vec::new();
let mut row = Vec::new();
for i in (0..n).rev() {
row.push(((v >> i) & 1 == 1, i));
if row.len() == cols as usize {
rows.push(std::mem::take(&mut row));
}
}
if !row.is_empty() {
rows.push(row);
}
rows
}
pub fn toggle_bit(&mut self, bit: u32) {
if bit >= self.word_size.bits() {
return;
}
if self.error.is_some() {
self.clear_entry();
}
let v = self.current_bits();
self.value = (v ^ (1u64 << bit)) & self.word_size.mask();
self.typing = false;
self.error = None;
self.entry = format_raw(self.value, self.base, self.word_size);
}
pub fn set_base(&mut self, base: Base) {
if self.error.is_some() {
self.clear_entry();
}
// Commit typed digits into value when switching bases.
if self.typing {
if let Ok(v) = parse_entry(&self.entry, self.base) {
self.value = v & self.word_size.mask();
}
self.typing = false;
}
self.base = base;
self.entry = format_raw(self.value, base, self.word_size);
}
pub fn cycle_word_size(&mut self) {
if self.typing {
if let Ok(v) = parse_entry(&self.entry, self.base) {
self.value = v;
}
self.typing = false;
}
self.word_size = self.word_size.cycle();
self.value &= self.word_size.mask();
if let Some(p) = self.pending.as_mut() {
*p &= self.word_size.mask();
}
self.entry = format_raw(self.value, self.base, self.word_size);
}
pub fn clear_all(&mut self) {
let base = self.base;
let word_size = self.word_size;
*self = Self::new();
self.base = base;
self.word_size = word_size;
}
pub fn clear_entry(&mut self) {
self.error = None;
self.entry = "0".into();
self.value = 0;
self.typing = false;
}
pub fn backspace(&mut self) {
if self.error.is_some() {
self.clear_entry();
return;
}
if !self.typing {
return;
}
self.entry.pop();
if self.entry.is_empty() {
self.entry = "0".into();
self.typing = false;
self.value = 0;
} else if let Ok(v) = parse_entry(&self.entry, self.base) {
self.value = v & self.word_size.mask();
}
}
pub fn input_digit(&mut self, ch: char) {
let ch = ch.to_ascii_uppercase();
if !is_valid_digit(ch, self.base) {
return;
}
if self.error.is_some() {
self.clear_all();
}
if !self.typing {
self.entry = ch.to_string();
self.typing = true;
} else if self.entry == "0" {
self.entry = ch.to_string();
} else {
let max_digits = max_digits(self.base, self.word_size);
if digit_len(&self.entry) >= max_digits {
return;
}
self.entry.push(ch);
}
match parse_entry(&self.entry, self.base) {
Ok(v) => {
let masked = v & self.word_size.mask();
// Reject overflow beyond word size (except wrapping interpretation of typed hex).
if v != masked && self.base != Base::Bin {
self.entry.pop();
if self.entry.is_empty() {
self.entry = "0".into();
self.typing = false;
}
return;
}
self.value = masked;
}
Err(_) => {
self.entry.pop();
}
}
}
pub fn negate(&mut self) {
if self.error.is_some() {
return;
}
let v = self.current_bits();
self.value = (!v).wrapping_add(1) & self.word_size.mask();
self.typing = false;
self.entry = format_raw(self.value, self.base, self.word_size);
}
pub fn not(&mut self) {
if self.error.is_some() {
return;
}
let v = self.current_bits();
self.value = (!v) & self.word_size.mask();
self.expression.clear();
self.typing = false;
self.entry = format_raw(self.value, self.base, self.word_size);
}
pub fn set_op(&mut self, op: ProgOp) {
if self.error.is_some() {
return;
}
if let Err(e) = self.commit_pending() {
self.fail(e);
return;
}
let v = self.current_bits();
self.pending = Some(v);
self.op = Some(op);
self.expression = format!(
"{} {}",
format_value(v, self.base, self.word_size),
op.symbol()
);
self.typing = false;
self.entry = format_raw(v, self.base, self.word_size);
}
pub fn equals(&mut self) {
if self.error.is_some() {
return;
}
let Some(op) = self.op else {
self.expression.clear();
self.typing = false;
return;
};
let b = self.current_bits();
let a = self.pending.unwrap_or(b);
match op.apply(a, b, self.word_size) {
Ok(r) => {
self.expression = format!(
"{} {} {} =",
format_value(a, self.base, self.word_size),
op.symbol(),
format_value(b, self.base, self.word_size)
);
self.value = r;
self.pending = None;
self.op = None;
self.typing = false;
self.entry = format_raw(r, self.base, self.word_size);
}
Err(e) => self.fail(e),
}
}
fn current_bits(&self) -> u64 {
if self.typing {
parse_entry(&self.entry, self.base)
.unwrap_or(self.value)
& self.word_size.mask()
} else {
self.value & self.word_size.mask()
}
}
fn commit_pending(&mut self) -> Result<(), ProgError> {
let (Some(a), Some(op)) = (self.pending, self.op) else {
return Ok(());
};
if !self.typing {
return Ok(());
}
let b = self.current_bits();
let r = op.apply(a, b, self.word_size)?;
self.value = r;
self.pending = Some(r);
self.op = None;
self.entry = format_raw(r, self.base, self.word_size);
Ok(())
}
fn fail(&mut self, e: ProgError) {
self.error = Some(e);
self.pending = None;
self.op = None;
self.expression.clear();
self.typing = false;
}
}
fn is_valid_digit(ch: char, base: Base) -> bool {
match base {
Base::Bin => matches!(ch, '0' | '1'),
Base::Oct => matches!(ch, '0'..='7'),
Base::Dec => ch.is_ascii_digit(),
Base::Hex => ch.is_ascii_hexdigit(),
}
}
fn digit_len(s: &str) -> usize {
s.chars().filter(|c| c.is_ascii_hexdigit()).count()
}
fn max_digits(base: Base, size: WordSize) -> usize {
let bits = size.bits() as f64;
match base {
Base::Bin => size.bits() as usize,
Base::Oct => (bits / 3.0).ceil() as usize,
Base::Dec => (bits * std::f64::consts::LN_2 / std::f64::consts::LN_10).ceil() as usize,
Base::Hex => (bits / 4.0).ceil() as usize,
}
}
fn parse_entry(entry: &str, base: Base) -> Result<u64, ProgError> {
let cleaned: String = entry
.chars()
.filter(|c| c.is_ascii_hexdigit())
.collect();
if cleaned.is_empty() {
return Ok(0);
}
u64::from_str_radix(&cleaned, base.radix()).map_err(|_| ProgError::Invalid)
}
fn format_raw(v: u64, base: Base, size: WordSize) -> String {
let v = v & size.mask();
match base {
Base::Dec => {
if size.is_signed() {
format!("{}", to_signed(v, size))
} else {
format!("{v}")
}
}
Base::Hex => format!("{:X}", v),
Base::Oct => format!("{:o}", v),
Base::Bin => format!("{:b}", v),
}
}
fn format_value(v: u64, base: Base, size: WordSize) -> String {
format_grouped_entry(&format_raw(v, base, size), base)
}
fn format_grouped_entry(raw: &str, base: Base) -> String {
// Keep leading minus for signed decimal.
let (sign, digits) = if let Some(rest) = raw.strip_prefix('-') {
("-", rest)
} else {
("", raw)
};
let digits: String = digits
.chars()
.filter(|c| c.is_ascii_hexdigit())
.collect::<String>()
.to_ascii_uppercase();
if digits.is_empty() {
return format!("{sign}0");
}
let group = match base {
Base::Bin => 4,
Base::Hex | Base::Oct => 4,
Base::Dec => 3,
};
let mut out = String::new();
for (i, ch) in digits.chars().rev().enumerate() {
if i > 0 && i % group == 0 {
out.push(' ');
}
out.push(ch);
}
let grouped: String = out.chars().rev().collect();
let mut result = String::new();
let _ = write!(result, "{sign}{grouped}");
result
}
fn to_signed(v: u64, size: WordSize) -> i64 {
let bits = size.bits();
if bits == 64 {
return v as i64;
}
let mask = size.mask();
let v = v & mask;
let sign_bit = 1u64 << (bits - 1);
if v & sign_bit != 0 {
(v | !mask) as i64
} else {
v as i64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_and() {
let mut p = Programmer::new();
p.set_base(Base::Hex);
p.input_digit('F');
p.input_digit('F');
p.set_op(ProgOp::And);
p.input_digit('0');
p.input_digit('F');
p.equals();
assert_eq!(p.current_bits(), 0x0F);
}
#[test]
fn simultaneous_bases() {
let mut p = Programmer::new();
p.input_digit('2');
p.input_digit('5');
p.input_digit('5');
assert_eq!(p.hex_text().replace(' ', ""), "FF");
assert_eq!(p.bin_text().replace(' ', ""), "11111111");
}
#[test]
fn toggle_bit_flips() {
let mut p = Programmer::new();
// QWORD → DWORD → DINT → WORD → INT → BYTE
for _ in 0..5 {
p.cycle_word_size();
}
assert_eq!(p.word_size(), WordSize::Byte);
p.toggle_bit(0);
p.toggle_bit(7);
assert_eq!(p.current_bits(), 0b1000_0001);
p.toggle_bit(0);
assert_eq!(p.current_bits(), 0b1000_0000);
}
#[test]
fn int_vs_word_dec() {
let mut p = Programmer::new();
p.set_base(Base::Hex);
// → DWORD → DINT → WORD
p.cycle_word_size();
p.cycle_word_size();
p.cycle_word_size();
assert_eq!(p.word_size(), WordSize::Word);
p.input_digit('F');
p.input_digit('F');
p.input_digit('F');
p.input_digit('F');
assert_eq!(p.dec_text().replace(' ', ""), "65535");
p.cycle_word_size(); // INT
assert_eq!(p.word_size(), WordSize::Int);
assert_eq!(p.dec_text().replace(' ', ""), "-1");
}
#[test]
fn dint_vs_dword_dec() {
let mut p = Programmer::new();
p.set_base(Base::Hex);
p.cycle_word_size(); // DWORD
assert_eq!(p.word_size(), WordSize::Dword);
for _ in 0..8 {
p.input_digit('F');
}
assert_eq!(p.dec_text().replace(' ', ""), "4294967295");
p.cycle_word_size(); // DINT
assert_eq!(p.word_size(), WordSize::Dint);
assert_eq!(p.dec_text().replace(' ', ""), "-1");
}
}
+1149
View File
File diff suppressed because it is too large Load Diff