This commit is contained in:
2026-08-01 18:05:10 +03:00
parent 8e24f19a07
commit 7d050f3eb2
10 changed files with 1145 additions and 120 deletions
+6
View File
@@ -1 +1,7 @@
/target
/target-appimage
/.cache
/dist/*.AppImage
/dist/rcalc.AppDir/
/dist/.glibc-build.txt
.rcalc_last_bump
Generated
+1 -1
View File
@@ -3904,7 +3904,7 @@ dependencies = [
[[package]]
name = "rcalc"
version = "0.1.0"
version = "0.1.4"
dependencies = [
"slint",
"slint-build",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rcalc"
version = "0.1.0"
version = "0.1.5"
edition = "2021"
build = "build.rs"
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=rcalc
GenericName=Calculator
Comment=Calculator with Programmer, Convert, CRC tools
Exec=rcalc
Icon=rcalc
Terminal=false
Categories=Utility;Calculator;Science;Engineering;
StartupNotify=true
StartupWMClass=rcalc
+90
View File
@@ -1,3 +1,93 @@
use std::env;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;
fn main() {
let version = resolve_app_version();
println!("cargo:rustc-env=APP_VERSION={version}");
slint_build::compile("ui/app.slint").expect("Slint build failed");
}
/// Window / package version shown as `Rcalc-{version}`.
///
/// On each `release` build the patch in `Cargo.toml` is incremented once
/// (`0.1.0` → `0.1.1` → …). If Cargo restarts the build script right after we
/// rewrite `Cargo.toml`, a short mtime guard prevents a double bump.
fn resolve_app_version() -> String {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let pkg = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".into());
let profile = env::var("PROFILE").unwrap_or_default();
if profile != "release" {
return pkg;
}
let cargo_toml = manifest_dir.join("Cargo.toml");
let guard_path = manifest_dir.join(".rcalc_last_bump");
let already = fs::read_to_string(&guard_path)
.map(|s| s.trim().to_string())
.unwrap_or_default();
// Cargo re-invoked us because we just rewrote Cargo.toml — keep version.
if already == pkg {
if let Ok(meta) = fs::metadata(&cargo_toml) {
if let Ok(modified) = meta.modified() {
if modified.elapsed().unwrap_or(Duration::MAX) < Duration::from_secs(15) {
return pkg;
}
}
}
}
let Some(next) = bump_patch(&pkg) else {
return pkg;
};
if let Err(e) = set_cargo_toml_version(&cargo_toml, &next) {
println!("cargo:warning=could not bump Cargo.toml version: {e}");
return pkg;
}
let _ = fs::write(&guard_path, &next);
next
}
fn bump_patch(version: &str) -> Option<String> {
let mut parts: Vec<&str> = version.split('.').collect();
if parts.is_empty() {
return None;
}
while parts.len() < 3 {
parts.push("0");
}
let patch: u64 = parts[2]
.split(['-', '+'])
.next()?
.parse()
.ok()?;
Some(format!("{}.{}.{}", parts[0], parts[1], patch + 1))
}
fn set_cargo_toml_version(path: &PathBuf, version: &str) -> std::io::Result<()> {
let raw = fs::read_to_string(path)?;
let mut out = String::with_capacity(raw.len() + 8);
let mut replaced = false;
for line in raw.lines() {
if !replaced && line.starts_with("version") && line.contains('=') {
out.push_str(&format!("version = \"{version}\""));
out.push('\n');
replaced = true;
} else {
out.push_str(line);
out.push('\n');
}
}
if !replaced {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"version field not found in Cargo.toml",
));
}
fs::write(path, out)
}
Vendored
BIN
View File
Binary file not shown.
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
# Build a portable rcalc AppImage.
#
# The release binary is compiled inside Ubuntu 22.04 (glibc 2.35) so the
# AppImage runs on most modern distros — not only on bleeding-edge hosts.
# AppImage itself does NOT bundle glibc; the Docker base sets the floor.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ARCH="$(uname -m)"
OUT_DIR="${ROOT}/dist"
APPDIR="${OUT_DIR}/rcalc.AppDir"
TOOLS_DIR="${ROOT}/.cache/appimage-tools"
VERSION="$(grep -m1 '^version' "${ROOT}/Cargo.toml" | sed 's/.*"\(.*\)".*/\1/')"
IMAGE_NAME="rcalc-${VERSION}-${ARCH}.AppImage"
DOCKER_IMAGE="${DOCKER_IMAGE:-ubuntu:22.04}"
# Set SKIP_DOCKER=1 to package the already-built host binary (same glibc floor as host).
SKIP_DOCKER="${SKIP_DOCKER:-0}"
mkdir -p "${OUT_DIR}" "${TOOLS_DIR}"
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "error: required command not found: $1" >&2
exit 1
}
}
download_tool() {
local url="$1" dest="$2"
if [[ -x "$dest" ]]; then
return 0
fi
echo "Downloading $(basename "$dest")…"
curl -fsSL -o "$dest" "$url"
chmod +x "$dest"
}
build_binary_docker() {
need_cmd docker
echo "Building release binary in ${DOCKER_IMAGE} (portable glibc)…"
mkdir -p "${HOME}/.cargo/registry" "${HOME}/.cargo/git"
docker run --rm \
-v "${ROOT}:/src" \
-v "${HOME}/.cargo/registry:/root/.cargo/registry" \
-v "${HOME}/.cargo/git:/root/.cargo/git" \
-w /src \
"${DOCKER_IMAGE}" \
bash -lc '
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq curl build-essential pkg-config libfontconfig1-dev ca-certificates
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
# shellcheck disable=SC1091
source "$HOME/.cargo/env"
export CARGO_TARGET_DIR=/src/target-appimage
cargo build --release
install -D -m755 /src/target-appimage/release/rcalc /src/dist/rcalc
ldd --version | head -1 > /src/dist/.glibc-build.txt
# Make artifacts writable by the host user
chown -R '"$(id -u):$(id -g)"' /src/dist/rcalc /src/dist/.glibc-build.txt /src/target-appimage || true
'
}
ensure_binary() {
if [[ "$SKIP_DOCKER" == "1" ]]; then
if [[ ! -x "${OUT_DIR}/rcalc" ]]; then
echo "Building on host (SKIP_DOCKER=1)…"
(cd "${ROOT}" && cargo build --release)
install -D -m755 "${ROOT}/target/release/rcalc" "${OUT_DIR}/rcalc"
fi
ldd --version | head -1 > "${OUT_DIR}/.glibc-build.txt" || true
else
build_binary_docker
fi
if [[ ! -x "${OUT_DIR}/rcalc" ]]; then
echo "error: dist/rcalc missing after build" >&2
exit 1
fi
}
package_appimage() {
need_cmd curl
local linuxdeploy="${TOOLS_DIR}/linuxdeploy-${ARCH}.AppImage"
download_tool \
"https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage" \
"${linuxdeploy}"
rm -rf "${APPDIR}"
mkdir -p "${APPDIR}/usr/bin" "${APPDIR}/usr/share/icons/hicolor/256x256/apps"
install -m755 "${OUT_DIR}/rcalc" "${APPDIR}/usr/bin/rcalc"
install -m644 "${ROOT}/assets/icon-256.png" "${APPDIR}/usr/share/icons/hicolor/256x256/apps/rcalc.png"
install -m644 "${ROOT}/assets/icon-256.png" "${APPDIR}/rcalc.png"
install -m644 "${ROOT}/assets/rcalc-appimage.desktop" "${APPDIR}/rcalc.desktop"
echo "Bundling libraries with linuxdeploy…"
export APPIMAGE_EXTRACT_AND_RUN=1
export OUTPUT="${IMAGE_NAME}"
(
cd "${OUT_DIR}"
"${linuxdeploy}" \
--appdir "${APPDIR}" \
--executable "${APPDIR}/usr/bin/rcalc" \
--desktop-file "${APPDIR}/rcalc.desktop" \
--icon-file "${APPDIR}/rcalc.png" \
--output appimage
)
# linuxdeploy drops the AppImage next to AppDir (dist/)
local built
built="$(find "${OUT_DIR}" -maxdepth 1 -name '*.AppImage' ! -name 'linuxdeploy*' | head -1)"
if [[ -z "$built" ]]; then
echo "error: AppImage was not produced" >&2
exit 1
fi
if [[ "$(basename "$built")" != "${IMAGE_NAME}" ]]; then
mv -f "$built" "${OUT_DIR}/${IMAGE_NAME}"
fi
chmod +x "${OUT_DIR}/${IMAGE_NAME}"
}
ensure_binary
package_appimage
echo
echo "OK: ${OUT_DIR}/${IMAGE_NAME}"
if [[ -f "${OUT_DIR}/.glibc-build.txt" ]]; then
echo "Built against: $(cat "${OUT_DIR}/.glibc-build.txt")"
fi
echo "Run: ${OUT_DIR}/${IMAGE_NAME}"
echo "Tip: on the other PC, chmod +x and execute — no install needed."
+857 -106
View File
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -11,7 +11,7 @@ use std::rc::Rc;
use convert::{ConvField, ConvPanel, Converter, Endian, RatioWidth};
use crc::CrcTool;
use engine::{Calculator, Op};
use engine::{Calculator, Op, StdPanel};
use programmer::{Base, ProgOp, Programmer};
use slint::{ComponentHandle, ModelRc, VecModel};
@@ -55,6 +55,7 @@ fn main() -> Result<(), slint::PlatformError> {
let ui = AppWindow::new()?;
// Must match the .desktop basename (rcalc.desktop → "rcalc"), before show/run.
slint::set_xdg_app_id("rcalc")?;
ui.set_app_title(format!("Rcalc-{}", env!("APP_VERSION")).into());
let state = Rc::new(RefCell::new(State::default()));
@@ -170,6 +171,10 @@ fn refresh(ui: &AppWindow, state: &mut State) {
ui.set_display_text(state.std.display().into());
ui.set_expression_text(state.std.expression().into());
ui.set_has_memory(state.std.has_memory());
ui.set_std_panel(match state.std.panel() {
StdPanel::Standard => 0,
StdPanel::Formula => 1,
});
}
Mode::Programmer => {
ui.set_display_text(state.prog.display().into());
@@ -266,6 +271,8 @@ fn handle_key(state: &mut State, id: &str) {
fn handle_standard(calc: &mut Calculator, id: &str) {
match id {
"panel:std" => calc.set_panel(StdPanel::Standard),
"panel:formula" => calc.set_panel(StdPanel::Formula),
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => {
calc.input_digit(id.chars().next().unwrap());
}
@@ -276,9 +283,12 @@ fn handle_standard(calc: &mut Calculator, id: &str) {
"/" | "÷" => calc.set_op(Op::Div),
"=" => calc.equals(),
"%" => calc.percent(),
"(" => calc.paren_open(),
")" => calc.paren_close(),
"CE" => calc.clear_entry(),
"C" => calc.clear_all(),
"BS" => calc.backspace(),
"1/x" | "x2" | "sqrt" if calc.panel() == StdPanel::Formula => {}
"1/x" => calc.reciprocal(),
"x2" => calc.square(),
"sqrt" => calc.sqrt(),
+26 -6
View File
@@ -294,7 +294,8 @@ component EndianChip inherits Rectangle {
}
export component AppWindow inherits Window {
title: "rcalc";
in-out property <string> app-title: "Rcalc";
title: root.app-title;
icon: @image-url("../assets/icon-256.png");
preferred-width: 420px;
preferred-height: 640px;
@@ -308,6 +309,8 @@ export component AppWindow inherits Window {
in property <string> display-text: "0";
in property <string> expression-text: "";
in property <bool> has-memory: false;
// 0 = classic standard, 1 = formula
in property <int> std-panel: 0;
in property <string> hex-text: "0";
in property <string> dec-text: "0";
@@ -1004,6 +1007,21 @@ export component AppWindow inherits Window {
}
}
if root.mode == 0: HorizontalLayout {
spacing: 6px;
height: 30px;
ModeTab {
label: "Ordinary";
active: root.std-panel == 0;
clicked => { root.press("panel:std"); }
}
ModeTab {
label: "Formula";
active: root.std-panel == 1;
clicked => { root.press("panel:formula"); }
}
}
if root.mode == 0: HorizontalLayout {
spacing: 6px;
height: 32px;
@@ -1017,15 +1035,17 @@ export component AppWindow inherits Window {
if root.mode == 0: GridLayout {
spacing: 6px;
Row {
FuncBtn { label: "%"; clicked => { root.press("%"); } }
FuncBtn { label: "CE"; clicked => { root.press("CE"); } }
if root.std-panel == 0: FuncBtn { label: "%"; clicked => { root.press("%"); } }
if root.std-panel == 1: FuncBtn { label: "("; clicked => { root.press("("); } }
if root.std-panel == 0: FuncBtn { label: "CE"; clicked => { root.press("CE"); } }
if root.std-panel == 1: FuncBtn { label: ")"; clicked => { root.press(")"); } }
FuncBtn { label: "C"; clicked => { root.press("C"); } }
FuncBtn { label: "⌫"; clicked => { root.press("BS"); } }
}
Row {
FuncBtn { label: "1/x"; clicked => { root.press("1/x"); } }
FuncBtn { label: "x²"; clicked => { root.press("x2"); } }
FuncBtn { label: "√x"; clicked => { root.press("sqrt"); } }
FuncBtn { label: "1/x"; enabled: root.std-panel == 0; clicked => { root.press("1/x"); } }
FuncBtn { label: "x²"; enabled: root.std-panel == 0; clicked => { root.press("x2"); } }
FuncBtn { label: "√x"; enabled: root.std-panel == 0; clicked => { root.press("sqrt"); } }
FuncBtn { label: "÷"; clicked => { root.press("/"); } }
}
Row {