0.1.4
This commit is contained in:
@@ -1 +1,7 @@
|
||||
/target
|
||||
/target-appimage
|
||||
/.cache
|
||||
/dist/*.AppImage
|
||||
/dist/rcalc.AppDir/
|
||||
/dist/.glibc-build.txt
|
||||
.rcalc_last_bump
|
||||
|
||||
Generated
+1
-1
@@ -3904,7 +3904,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rcalc"
|
||||
version = "0.1.0"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"slint",
|
||||
"slint-build",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rcalc"
|
||||
version = "0.1.0"
|
||||
version = "0.1.5"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
Binary file not shown.
Executable
+137
@@ -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."
|
||||
+862
-111
File diff suppressed because it is too large
Load Diff
+11
-1
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user