0.1.4
This commit is contained in:
@@ -1 +1,7 @@
|
|||||||
/target
|
/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]]
|
[[package]]
|
||||||
name = "rcalc"
|
name = "rcalc"
|
||||||
version = "0.1.0"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"slint",
|
"slint",
|
||||||
"slint-build",
|
"slint-build",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rcalc"
|
name = "rcalc"
|
||||||
version = "0.1.0"
|
version = "0.1.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
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() {
|
fn main() {
|
||||||
|
let version = resolve_app_version();
|
||||||
|
println!("cargo:rustc-env=APP_VERSION={version}");
|
||||||
|
|
||||||
slint_build::compile("ui/app.slint").expect("Slint build failed");
|
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."
|
||||||
+857
-106
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 convert::{ConvField, ConvPanel, Converter, Endian, RatioWidth};
|
||||||
use crc::CrcTool;
|
use crc::CrcTool;
|
||||||
use engine::{Calculator, Op};
|
use engine::{Calculator, Op, StdPanel};
|
||||||
use programmer::{Base, ProgOp, Programmer};
|
use programmer::{Base, ProgOp, Programmer};
|
||||||
use slint::{ComponentHandle, ModelRc, VecModel};
|
use slint::{ComponentHandle, ModelRc, VecModel};
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ fn main() -> Result<(), slint::PlatformError> {
|
|||||||
let ui = AppWindow::new()?;
|
let ui = AppWindow::new()?;
|
||||||
// Must match the .desktop basename (rcalc.desktop → "rcalc"), before show/run.
|
// Must match the .desktop basename (rcalc.desktop → "rcalc"), before show/run.
|
||||||
slint::set_xdg_app_id("rcalc")?;
|
slint::set_xdg_app_id("rcalc")?;
|
||||||
|
ui.set_app_title(format!("Rcalc-{}", env!("APP_VERSION")).into());
|
||||||
|
|
||||||
let state = Rc::new(RefCell::new(State::default()));
|
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_display_text(state.std.display().into());
|
||||||
ui.set_expression_text(state.std.expression().into());
|
ui.set_expression_text(state.std.expression().into());
|
||||||
ui.set_has_memory(state.std.has_memory());
|
ui.set_has_memory(state.std.has_memory());
|
||||||
|
ui.set_std_panel(match state.std.panel() {
|
||||||
|
StdPanel::Standard => 0,
|
||||||
|
StdPanel::Formula => 1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Mode::Programmer => {
|
Mode::Programmer => {
|
||||||
ui.set_display_text(state.prog.display().into());
|
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) {
|
fn handle_standard(calc: &mut Calculator, id: &str) {
|
||||||
match id {
|
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" => {
|
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => {
|
||||||
calc.input_digit(id.chars().next().unwrap());
|
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.set_op(Op::Div),
|
||||||
"=" => calc.equals(),
|
"=" => calc.equals(),
|
||||||
"%" => calc.percent(),
|
"%" => calc.percent(),
|
||||||
|
"(" => calc.paren_open(),
|
||||||
|
")" => calc.paren_close(),
|
||||||
"CE" => calc.clear_entry(),
|
"CE" => calc.clear_entry(),
|
||||||
"C" => calc.clear_all(),
|
"C" => calc.clear_all(),
|
||||||
"BS" => calc.backspace(),
|
"BS" => calc.backspace(),
|
||||||
|
"1/x" | "x2" | "sqrt" if calc.panel() == StdPanel::Formula => {}
|
||||||
"1/x" => calc.reciprocal(),
|
"1/x" => calc.reciprocal(),
|
||||||
"x2" => calc.square(),
|
"x2" => calc.square(),
|
||||||
"sqrt" => calc.sqrt(),
|
"sqrt" => calc.sqrt(),
|
||||||
|
|||||||
+26
-6
@@ -294,7 +294,8 @@ component EndianChip inherits Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export component AppWindow inherits Window {
|
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");
|
icon: @image-url("../assets/icon-256.png");
|
||||||
preferred-width: 420px;
|
preferred-width: 420px;
|
||||||
preferred-height: 640px;
|
preferred-height: 640px;
|
||||||
@@ -308,6 +309,8 @@ export component AppWindow inherits Window {
|
|||||||
in property <string> display-text: "0";
|
in property <string> display-text: "0";
|
||||||
in property <string> expression-text: "";
|
in property <string> expression-text: "";
|
||||||
in property <bool> has-memory: false;
|
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> hex-text: "0";
|
||||||
in property <string> dec-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 {
|
if root.mode == 0: HorizontalLayout {
|
||||||
spacing: 6px;
|
spacing: 6px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -1017,15 +1035,17 @@ export component AppWindow inherits Window {
|
|||||||
if root.mode == 0: GridLayout {
|
if root.mode == 0: GridLayout {
|
||||||
spacing: 6px;
|
spacing: 6px;
|
||||||
Row {
|
Row {
|
||||||
FuncBtn { label: "%"; clicked => { root.press("%"); } }
|
if root.std-panel == 0: FuncBtn { label: "%"; clicked => { root.press("%"); } }
|
||||||
FuncBtn { label: "CE"; clicked => { root.press("CE"); } }
|
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: "C"; clicked => { root.press("C"); } }
|
||||||
FuncBtn { label: "⌫"; clicked => { root.press("BS"); } }
|
FuncBtn { label: "⌫"; clicked => { root.press("BS"); } }
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
FuncBtn { label: "1/x"; clicked => { root.press("1/x"); } }
|
FuncBtn { label: "1/x"; enabled: root.std-panel == 0; clicked => { root.press("1/x"); } }
|
||||||
FuncBtn { label: "x²"; clicked => { root.press("x2"); } }
|
FuncBtn { label: "x²"; enabled: root.std-panel == 0; clicked => { root.press("x2"); } }
|
||||||
FuncBtn { label: "√x"; clicked => { root.press("sqrt"); } }
|
FuncBtn { label: "√x"; enabled: root.std-panel == 0; clicked => { root.press("sqrt"); } }
|
||||||
FuncBtn { label: "÷"; clicked => { root.press("/"); } }
|
FuncBtn { label: "÷"; clicked => { root.press("/"); } }
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
Reference in New Issue
Block a user