Compare commits
3
Commits
8f607afb3c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c14906e40 | ||
|
|
93e193f7fa | ||
|
|
b18dacec07 |
@@ -1,8 +1,13 @@
|
|||||||
/target
|
/target
|
||||||
/target-appimage
|
/target-appimage
|
||||||
|
/target-deb
|
||||||
/.cache
|
/.cache
|
||||||
/dist/*.AppImage
|
/dist/*.AppImage
|
||||||
/dist/rcalc.AppDir/
|
/dist/rcalc.AppDir/
|
||||||
|
/dist/deb-root/
|
||||||
|
/dist/*.deb
|
||||||
|
/dist/Packages
|
||||||
|
/dist/Packages.gz
|
||||||
/dist/.glibc-build.txt
|
/dist/.glibc-build.txt
|
||||||
.rcalc_last_bump
|
.rcalc_last_bump
|
||||||
/target-windows
|
/target-windows
|
||||||
|
|||||||
Generated
+1
-1
@@ -3904,7 +3904,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rcalc"
|
name = "rcalc"
|
||||||
version = "0.1.21"
|
version = "0.1.28"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"slint",
|
"slint",
|
||||||
"slint-build",
|
"slint-build",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rcalc"
|
name = "rcalc"
|
||||||
version = "0.1.21"
|
version = "0.1.28"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,70 @@ cargo run --release
|
|||||||
./dist/rcalc
|
./dist/rcalc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Install as a Debian package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build-deb.sh # → dist/rcalc_<version>-<rev>_<arch>.deb
|
||||||
|
sudo apt install ./dist/rcalc_*_amd64.deb
|
||||||
|
rcalc # or start it from the application menu
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary is compiled in an Ubuntu 22.04 container (Docker) by default, so the
|
||||||
|
package keeps a glibc 2.35 baseline and installs on Ubuntu 22.04 / 24.04 / 26.04.
|
||||||
|
`Depends:` are generated from the ELF with `dpkg-shlibdeps`.
|
||||||
|
|
||||||
|
Menu/taskbar icons come from `ico/` — every PNG there is placed into the
|
||||||
|
hicolor slot matching its real pixel size (file names do not matter), and the
|
||||||
|
remaining smaller sizes are downsampled from the biggest one. Without `ico/`
|
||||||
|
the icons are resized from `assets/icon.png`. The icon inside the window
|
||||||
|
(`ui/app.slint`) and the Windows `rcalc.exe` icon still use `assets/`.
|
||||||
|
|
||||||
|
Useful switches:
|
||||||
|
|
||||||
|
| Variable | Effect |
|
||||||
|
| --- | --- |
|
||||||
|
| `DEB_REVISION=` | packaging revision, `1` by default → `Version: 0.1.28-1` |
|
||||||
|
| `REPO=1` | also write `dist/Packages(.gz)` so `dist/` works as a local apt repo |
|
||||||
|
| `SKIP_DOCKER=1` | build on the host (needs this host's glibc on the target) |
|
||||||
|
| `PREBUILT=1` | package the existing `dist/rcalc`, no compile |
|
||||||
|
| `NO_STRIP=1` | keep symbols (bigger package) |
|
||||||
|
| `KEEP_STAGE=1` | leave `dist/deb-root/` for inspection |
|
||||||
|
| `DOCKER_IMAGE=` | other build image, e.g. `debian:12` for an older baseline |
|
||||||
|
| `DEB_MAINTAINER=` | `"Name <mail>"` in `Maintainer:` (default: git user) |
|
||||||
|
|
||||||
|
### Version and updates
|
||||||
|
|
||||||
|
Package version = `<version from Cargo.toml>-<DEB_REVISION>`, e.g. `0.1.28-1`.
|
||||||
|
|
||||||
|
* changed app code → bump `version` in `Cargo.toml` (it is also the window title);
|
||||||
|
* changed only packaging (icon, `.desktop`, `Depends`, rebuilt binary) → `DEB_REVISION=2`.
|
||||||
|
|
||||||
|
`dpkg` compares these in order (`0.1.28-1 < 0.1.28-2 < 0.1.29-1`, and
|
||||||
|
`0.1.9 < 0.1.10`), so every new build is an upgrade, never a downgrade. The
|
||||||
|
script prints the resulting upgrade path and warns if the version being built is
|
||||||
|
not newer than the installed one — an `apt upgrade` of such a package does nothing.
|
||||||
|
Release notes live in `packaging/deb/changelog` and are installed as
|
||||||
|
`/usr/share/doc/rcalc/changelog.Debian.gz`.
|
||||||
|
|
||||||
|
Update in place (config in `~/.config/rcalc/config.json` is a user file, it survives):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install ./dist/rcalc_0.1.28-2_amd64.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
Or keep `dist/` as a local repository and update with plain `apt` (build with
|
||||||
|
`REPO=1`, then point apt at the folder):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo 'deb [trusted=yes] file:/path/to/RCalc/dist ./' | sudo tee /etc/apt/sources.list.d/rcalc.list
|
||||||
|
sudo apt update && sudo apt install rcalc # later: sudo apt upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
> If `scripts/install-desktop.sh` was ever run, `~/.local/bin/rcalc` (a symlink to
|
||||||
|
> `dist/rcalc`) shadows `/usr/bin/rcalc` — remove it after switching to the package,
|
||||||
|
> otherwise `rcalc` in the terminal still starts the old binary. The script warns
|
||||||
|
> about this during the build.
|
||||||
|
|
||||||
### Icon (Linux / Wayland)
|
### Icon (Linux / Wayland)
|
||||||
|
|
||||||
On **Wayland** the window `icon` property is ignored — the desktop uses
|
On **Wayland** the window `icon` property is ignored — the desktop uses
|
||||||
@@ -27,8 +91,9 @@ clicking a random binary path, so KDE/GNOME can match `app_id=rcalc` to
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Modes: **Standard**, **Programmer**, **Convert**
|
- Modes: **Standard**, **Engineering**, **Programmer**, **Convert**, **CRC**, **Settings**
|
||||||
- Standard: `+ − × ÷`, `%`, `±`, `1/x`, `x²`, `√x`, memory
|
- Standard: `+ − × ÷`, `%`, `±`, `1/x`, `x²`, `√x`, memory, formula panel
|
||||||
|
- Engineering: trig functions, log/pow, DEG/RAD, SI prefixes
|
||||||
- Programmer: simultaneous **HEX / DEC / OCT / BIN**, sizes **QWORD / DWORD / DINT / WORD / INT / BYTE**,
|
- Programmer: simultaneous **HEX / DEC / OCT / BIN**, sizes **QWORD / DWORD / DINT / WORD / INT / BYTE**,
|
||||||
bitwise ops, clickable bits
|
bitwise ops, clickable bits
|
||||||
(WORD/DWORD — unsigned DEC; INT/DINT — signed)
|
(WORD/DWORD — unsigned DEC; INT/DINT — signed)
|
||||||
@@ -46,3 +111,7 @@ clicking a random binary path, so KDE/GNOME can match `app_id=rcalc` to
|
|||||||
- `src/main.rs` — UI wiring
|
- `src/main.rs` — UI wiring
|
||||||
- `ui/app.slint` — Slint interface
|
- `ui/app.slint` — Slint interface
|
||||||
- `dist/rcalc` — release binary
|
- `dist/rcalc` — release binary
|
||||||
|
- `scripts/build-deb.sh` + `packaging/deb/` — .deb package (control, desktop
|
||||||
|
file, AppStream metainfo, maintainer scripts)
|
||||||
|
- `scripts/build-appimage.sh` — portable AppImage
|
||||||
|
- `scripts/install-desktop.sh` — per-user launcher + icon (no root needed)
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let version = resolve_app_version();
|
let version = resolve_app_version();
|
||||||
@@ -38,82 +35,10 @@ fn embed_windows_icon() {
|
|||||||
|
|
||||||
/// Window / package version shown as `Rcalc-{version}`.
|
/// Window / package version shown as `Rcalc-{version}`.
|
||||||
///
|
///
|
||||||
/// On each `release` build the patch in `Cargo.toml` is incremented once
|
/// Always the version from `Cargo.toml`. Раньше каждая release-сборка
|
||||||
/// (`0.1.0` → `0.1.1` → …). If Cargo restarts the build script right after we
|
/// инкрементировала patch в Cargo.toml посреди сборки — из-за этого
|
||||||
/// rewrite `Cargo.toml`, a short mtime guard prevents a double bump.
|
/// имя AppImage (читается скриптом до сборки) отставало от версии,
|
||||||
|
/// зашитой в бинарник. Теперь версия меняется только вручную.
|
||||||
fn resolve_app_version() -> String {
|
fn resolve_app_version() -> String {
|
||||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".into())
|
||||||
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.
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
rcalc (0.1.28-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Upstream 0.1.28 (version from Cargo.toml), first Debian packaging.
|
||||||
|
* Binary is built in an Ubuntu 22.04 container (glibc 2.35 baseline), so the
|
||||||
|
package installs on Ubuntu 22.04 / 24.04 / 26.04.
|
||||||
|
* Ships /usr/bin/rcalc, /usr/share/applications/rcalc.desktop, hicolor icons
|
||||||
|
taken from ico/, AppStream metainfo and postinst/postrm cache refresh hooks.
|
||||||
|
* Depends are generated from the ELF with dpkg-shlibdeps.
|
||||||
|
* Build fix: clippy::approx_constant error in the convert round-trip test
|
||||||
|
(3.1415927 -> std::f32::consts::PI).
|
||||||
|
|
||||||
|
-- alexMB5300 <prom-ing@yandex.ru> Thu, 17 Sep 2026 19:12:31 +0300
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
Package: rcalc
|
||||||
|
Version: @VERSION@
|
||||||
|
Architecture: @ARCHITECTURE@
|
||||||
|
Maintainer: @MAINTAINER@
|
||||||
|
Section: math
|
||||||
|
Priority: optional
|
||||||
|
Installed-Size: @INSTALLED_SIZE@
|
||||||
|
Depends: @DEPENDS@
|
||||||
|
Recommends: libegl1, libgl1, libx11-6, libwayland-client0
|
||||||
|
Description: desktop calculator with programmer, engineering and convert tools
|
||||||
|
Calculator inspired by the Windows Calculator (standard mode), written in
|
||||||
|
Rust with the Slint UI toolkit. Modes: Standard, Engineering, Programmer,
|
||||||
|
Convert, CRC and Settings.
|
||||||
|
.
|
||||||
|
Programmer mode shows HEX / DEC / OCT / BIN at once, with QWORD / DWORD /
|
||||||
|
DINT / WORD / INT / BYTE word sizes, bitwise operators and clickable bits.
|
||||||
|
.
|
||||||
|
Convert mode maps a float32 to two 16-bit PLC words in every byte order
|
||||||
|
(AB CD / CD AB / BA DC / DC BA), approximates electronic gear ratios as
|
||||||
|
num/den and converts Char / Hex / Dec ASCII codes.
|
||||||
|
.
|
||||||
|
CRC mode computes Modbus RTU CRC-16 and Modbus ASCII LRC from a string of
|
||||||
|
hex bytes, e.g. "01 03 00 00 00 0A".
|
||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Maintainer script of the generated rcalc .deb (see scripts/build-deb.sh).
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
configure)
|
||||||
|
;;
|
||||||
|
abort-upgrade|abort-remove|abort-deconfigure)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "postinst called with unknown argument \`$1'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Refresh the XDG caches. Never fail the installation because of them: the
|
||||||
|
# tools may be absent (minimal systems, chroot, dpkg -x in a container).
|
||||||
|
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||||
|
update-desktop-database -q /usr/share/applications || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
|
||||||
|
gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v appstreamcli >/dev/null 2>&1; then
|
||||||
|
appstreamcli refresh --force >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Maintainer script of the generated rcalc .deb (see scripts/build-deb.sh).
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
remove|upgrade|disappear|failed-upgrade|abort-install|abort-upgrade|purge)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "postrm called with unknown argument \`$1'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Rebuild the caches after the files were (or are about to be) removed.
|
||||||
|
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||||
|
update-desktop-database -q /usr/share/applications || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
|
||||||
|
gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v appstreamcli >/dev/null 2>&1; then
|
||||||
|
appstreamcli refresh --force >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- @RELEASE_DATE@ = UTC date of the build; @VERSION@ comes from Cargo.toml. -->
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>rcalc.desktop</id>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<!-- LICENSE file is missing in the repository: NOASSERTION until it is added. -->
|
||||||
|
<project_license>NOASSERTION</project_license>
|
||||||
|
<name>Rcalc</name>
|
||||||
|
<summary>Calculator with programmer, engineering, convert and CRC modes</summary>
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
Desktop calculator inspired by the Windows Calculator. Standard mode uses
|
||||||
|
exact rational arithmetic, so 0.1 + 0.2 really gives 0.3.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Standard: + − × ÷, %, ±, 1/x, x², √x, memory, formula panel</li>
|
||||||
|
<li>Engineering: trigonometry, log/pow, DEG/RAD, SI prefixes</li>
|
||||||
|
<li>Programmer: simultaneous HEX / DEC / OCT / BIN, QWORD … BYTE word
|
||||||
|
sizes, bitwise ops, clickable bits</li>
|
||||||
|
<li>Convert: float32 to two 16-bit words (AB CD / CD AB / BA DC / DC BA),
|
||||||
|
gear ratio num/den, ASCII codes</li>
|
||||||
|
<li>CRC: Modbus RTU CRC-16 and Modbus ASCII LRC from hex bytes</li>
|
||||||
|
<li>Full keyboard input, three themes, three button sizes</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
<categories>
|
||||||
|
<category>Utility</category>
|
||||||
|
<category>Calculator</category>
|
||||||
|
<category>Science</category>
|
||||||
|
</categories>
|
||||||
|
<releases>
|
||||||
|
<release version="@VERSION@" date="@RELEASE_DATE@"/>
|
||||||
|
</releases>
|
||||||
|
<content_rating type="oars-1.1"/>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Rcalc
|
||||||
|
GenericName=Calculator
|
||||||
|
Comment=Calculator with Programmer, Engineering, Convert and CRC tools
|
||||||
|
Exec=rcalc
|
||||||
|
Icon=rcalc
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;Calculator;
|
||||||
|
Keywords=calculator;programmer;hex;bitwise;modbus;crc;lrc;
|
||||||
|
StartupNotify=true
|
||||||
|
# Wayland app_id / X11 WM_CLASS — must match set_xdg_app_id("rcalc") in src/main.rs
|
||||||
|
StartupWMClass=rcalc
|
||||||
@@ -101,6 +101,9 @@ package_appimage() {
|
|||||||
echo "Bundling libraries with linuxdeploy…"
|
echo "Bundling libraries with linuxdeploy…"
|
||||||
export APPIMAGE_EXTRACT_AND_RUN=1
|
export APPIMAGE_EXTRACT_AND_RUN=1
|
||||||
export OUTPUT="${IMAGE_NAME}"
|
export OUTPUT="${IMAGE_NAME}"
|
||||||
|
# Marker: mtime boundary to find the FRESH linuxdeploy output.
|
||||||
|
local marker="${OUT_DIR}/.pack-start"
|
||||||
|
touch "${marker}"
|
||||||
(
|
(
|
||||||
cd "${OUT_DIR}"
|
cd "${OUT_DIR}"
|
||||||
"${linuxdeploy}" \
|
"${linuxdeploy}" \
|
||||||
@@ -111,11 +114,15 @@ package_appimage() {
|
|||||||
--output appimage
|
--output appimage
|
||||||
)
|
)
|
||||||
|
|
||||||
# linuxdeploy drops the AppImage next to AppDir (dist/)
|
# linuxdeploy drops the AppImage next to AppDir (dist/). Only consider files
|
||||||
|
# NEWER than the marker: a blind `find | head -1` with several older
|
||||||
|
# *.AppImage in dist/ renamed an OLD image OVER the freshly built one
|
||||||
|
# (rcalc-0.1.28 got shipped with a 0.1.25 binary from the first build).
|
||||||
local built
|
local built
|
||||||
built="$(find "${OUT_DIR}" -maxdepth 1 -name '*.AppImage' ! -name 'linuxdeploy*' | head -1)"
|
built="$(find "${OUT_DIR}" -maxdepth 1 -name '*.AppImage' ! -name 'linuxdeploy*' -newer "${marker}" | head -1)"
|
||||||
|
rm -f "${marker}"
|
||||||
if [[ -z "$built" ]]; then
|
if [[ -z "$built" ]]; then
|
||||||
echo "error: AppImage was not produced" >&2
|
echo "error: AppImage was not produced (linuxdeploy failed?)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ "$(basename "$built")" != "${IMAGE_NAME}" ]]; then
|
if [[ "$(basename "$built")" != "${IMAGE_NAME}" ]]; then
|
||||||
|
|||||||
Executable
+439
@@ -0,0 +1,439 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build a Debian package (.deb) for rcalc.
|
||||||
|
#
|
||||||
|
# By default the release binary is compiled inside Ubuntu 22.04 (glibc 2.35) —
|
||||||
|
# the same baseline and the same cargo cache (target-appimage) as
|
||||||
|
# scripts/build-appimage.sh — so the package installs on Ubuntu 22.04 / 24.04
|
||||||
|
# and not only on this rolling host distro.
|
||||||
|
#
|
||||||
|
# Package layout (FHS):
|
||||||
|
# /usr/bin/rcalc stripped release binary
|
||||||
|
# /usr/share/applications/rcalc.desktop launcher (Exec=rcalc)
|
||||||
|
# /usr/share/icons/hicolor/*x*/apps/rcalc.png icons from ico/ (or assets/)
|
||||||
|
# /usr/share/metainfo/rcalc.appdata.xml AppStream metadata
|
||||||
|
# /usr/share/doc/rcalc/README.md
|
||||||
|
#
|
||||||
|
# Env switches:
|
||||||
|
# DEB_REVISION packaging revision appended to the Cargo.toml version
|
||||||
|
# (0.1.28 + 1 → "Version: 0.1.28-1"); bump it for updates that
|
||||||
|
# change only the package, not the app
|
||||||
|
# REPO=1 also generate dist/Packages(.gz) → dist/ usable as an apt repo
|
||||||
|
# SKIP_DOCKER=1 build on the host instead of Docker (fast, but the deb will
|
||||||
|
# then require this host's glibc — see DEPENDS below)
|
||||||
|
# PREBUILT=1 do not compile, package the existing dist/rcalc as-is
|
||||||
|
# NO_STRIP=1 keep symbols in the binary (much bigger package)
|
||||||
|
# KEEP_STAGE=1 leave dist/deb-root/ around for inspection
|
||||||
|
# DOCKER_IMAGE build container image (default: ubuntu:22.04)
|
||||||
|
# DEB_MAINTAINER "Name <mail>" (default: git user.name/user.email)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
PKG="rcalc"
|
||||||
|
OUT_DIR="${OUT_DIR:-${ROOT}/dist}"
|
||||||
|
META_DIR="${ROOT}/packaging/deb"
|
||||||
|
STAGE="${OUT_DIR}/deb-root"
|
||||||
|
ARCH="$(dpkg --print-architecture 2>/dev/null || uname -m)"
|
||||||
|
VERSION="$(grep -m1 '^version' "${ROOT}/Cargo.toml" | sed 's/.*"\(.*\)".*/\1/')"
|
||||||
|
# Debian version = upstream (Cargo.toml) + packaging revision: "0.1.28-1".
|
||||||
|
# The revision lets you ship an update (fixed desktop file, new icon, rebuilt
|
||||||
|
# binary) WITHOUT touching Cargo.toml; dpkg compares 0.1.28-1 < 0.1.28-2 <
|
||||||
|
# 0.1.29-1 correctly. DEB_REVISION is the knob for that.
|
||||||
|
DEB_REVISION="${DEB_REVISION:-1}"
|
||||||
|
DEB_VERSION="${VERSION}-${DEB_REVISION}"
|
||||||
|
DEB_FILE="${OUT_DIR}/${PKG}_${DEB_VERSION}_${ARCH}.deb"
|
||||||
|
|
||||||
|
DOCKER_IMAGE="${DOCKER_IMAGE:-ubuntu:22.04}"
|
||||||
|
SKIP_DOCKER="${SKIP_DOCKER:-0}"
|
||||||
|
PREBUILT="${PREBUILT:-0}"
|
||||||
|
NO_STRIP="${NO_STRIP:-0}"
|
||||||
|
KEEP_STAGE="${KEEP_STAGE:-0}"
|
||||||
|
# Nested $( ) with quotes inside ${:- } is parsed unreliably by bash — resolve first.
|
||||||
|
GIT_NAME="$(git -C "${ROOT}" config user.name 2>/dev/null || true)"
|
||||||
|
GIT_MAIL="$(git -C "${ROOT}" config user.email 2>/dev/null || true)"
|
||||||
|
MAINTAINER="${DEB_MAINTAINER:-${GIT_NAME:-${PKG}-packaging} <${GIT_MAIL:-localhost}>}"
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
echo "error: required command not found: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- binary ----
|
||||||
|
|
||||||
|
build_binary_docker() {
|
||||||
|
need_cmd docker
|
||||||
|
echo "Building release binary in ${DOCKER_IMAGE} (glibc 2.35 baseline)…"
|
||||||
|
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
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
build_binary_host() {
|
||||||
|
echo "Building release binary on the host ($(ldd --version | head -1))…"
|
||||||
|
(cd "${ROOT}" && cargo build --release)
|
||||||
|
install -D -m755 "${ROOT}/target/release/${PKG}" "${OUT_DIR}/${PKG}"
|
||||||
|
ldd --version | head -1 > "${OUT_DIR}/.glibc-build.txt" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_binary() {
|
||||||
|
mkdir -p "${OUT_DIR}"
|
||||||
|
if [[ "${PREBUILT}" == "1" ]]; then
|
||||||
|
echo "PREBUILT=1: packaging ${OUT_DIR}/${PKG} as-is"
|
||||||
|
elif [[ "${SKIP_DOCKER}" == "1" ]]; then
|
||||||
|
build_binary_host
|
||||||
|
else
|
||||||
|
build_binary_docker
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "${OUT_DIR}/${PKG}" ]]; then
|
||||||
|
echo "error: ${OUT_DIR}/${PKG} missing — run without PREBUILT=1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- staging ---
|
||||||
|
|
||||||
|
stage_tree() {
|
||||||
|
rm -rf "${STAGE}"
|
||||||
|
mkdir -p \
|
||||||
|
"${STAGE}/DEBIAN" \
|
||||||
|
"${STAGE}/usr/bin" \
|
||||||
|
"${STAGE}/usr/share/applications" \
|
||||||
|
"${STAGE}/usr/share/metainfo" \
|
||||||
|
"${STAGE}/usr/share/doc/${PKG}"
|
||||||
|
|
||||||
|
install -m755 "${OUT_DIR}/${PKG}" "${STAGE}/usr/bin/${PKG}"
|
||||||
|
if [[ "${NO_STRIP}" != "1" ]] && command -v strip >/dev/null 2>&1; then
|
||||||
|
strip --strip-unneeded "${STAGE}/usr/bin/${PKG}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -m644 "${META_DIR}/${PKG}.desktop" \
|
||||||
|
"${STAGE}/usr/share/applications/${PKG}.desktop"
|
||||||
|
|
||||||
|
sed -e "s|@VERSION@|${VERSION}|" \
|
||||||
|
-e "s|@RELEASE_DATE@|$(date -u +%Y-%m-%d)|" \
|
||||||
|
"${META_DIR}/${PKG}.appdata.xml.in" \
|
||||||
|
> "${STAGE}/usr/share/metainfo/${PKG}.appdata.xml"
|
||||||
|
|
||||||
|
gzip -9 -c "${ROOT}/README.md" > "${STAGE}/usr/share/doc/${PKG}/README.md.gz"
|
||||||
|
echo "Binary: $(file -b "${STAGE}/usr/bin/${PKG}" | cut -d, -f1-3)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Icons for the desktop entry: /usr/share/icons/hicolor/<N>x<N>/apps/rcalc.png
|
||||||
|
#
|
||||||
|
# Source of truth, in order:
|
||||||
|
# 1. ico/*.png — ready-made sizes dropped in by hand. The REAL pixel size of
|
||||||
|
# each file decides which hicolor slot it fills (file names are ignored),
|
||||||
|
# and ico/ wins for the whole family: mixing ico/ artwork with assets/
|
||||||
|
# artwork would show a different picture depending on the icon size.
|
||||||
|
# 2. assets/icon.png resized to the standard sizes (needs ImageMagick).
|
||||||
|
# 3. the three PNGs bundled in assets/, as-is.
|
||||||
|
stage_icons() {
|
||||||
|
local dst="${STAGE}/usr/share/icons/hicolor"
|
||||||
|
local prepared=0 master="" master_size=0 size
|
||||||
|
|
||||||
|
local magick=""
|
||||||
|
for tool in magick convert; do
|
||||||
|
if command -v "${tool}" >/dev/null 2>&1; then
|
||||||
|
magick="${tool}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -d "${ROOT}/ico" ]] && [[ -n "${magick}" ]]; then
|
||||||
|
for f in "${ROOT}"/ico/*.png; do
|
||||||
|
[[ -f "${f}" ]] || continue
|
||||||
|
local w h
|
||||||
|
read -r w h <<<"$(identify -format '%w %h' "${f}" 2>/dev/null || echo '0 0')" || true
|
||||||
|
[[ "${w}" == "${h}" ]] && [[ "${w}" -gt 0 ]] || continue
|
||||||
|
if (( w > master_size )); then
|
||||||
|
master="${f}"
|
||||||
|
master_size="${w}"
|
||||||
|
fi
|
||||||
|
local slot="${dst}/${w}x${w}/apps/${PKG}.png"
|
||||||
|
if [[ ! -e "${slot}" ]]; then
|
||||||
|
mkdir -p "${dst}/${w}x${w}/apps"
|
||||||
|
install -m644 "${f}" "${slot}"
|
||||||
|
prepared=$((prepared + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( prepared > 0 )); then
|
||||||
|
# Fill the remaining standard sizes from the biggest ico/ file — never
|
||||||
|
# upscale, so a 512px source does not produce a blurry 1024px icon.
|
||||||
|
if [[ -n "${magick}" ]]; then
|
||||||
|
for size in 16 24 32 48 64 96 128 256 512; do
|
||||||
|
[[ -e "${dst}/${size}x${size}/apps/${PKG}.png" ]] && continue
|
||||||
|
(( size <= master_size )) || continue
|
||||||
|
mkdir -p "${dst}/${size}x${size}/apps"
|
||||||
|
"${magick}" "${master}" -resize "${size}x${size}" \
|
||||||
|
"${dst}/${size}x${size}/apps/${PKG}.png"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo "Icons: ${prepared} ready-made from ico/ (master ${master_size}px)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${magick}" ]]; then
|
||||||
|
for size in 16 24 32 48 64 128 256 512 1024; do
|
||||||
|
mkdir -p "${dst}/${size}x${size}/apps"
|
||||||
|
"${magick}" "${ROOT}/assets/icon.png" -resize "${size}x${size}" \
|
||||||
|
"${dst}/${size}x${size}/apps/${PKG}.png"
|
||||||
|
done
|
||||||
|
echo "Icons: resized from assets/icon.png (1024px master)"
|
||||||
|
else
|
||||||
|
install -D -m644 "${ROOT}/assets/icon-64.png" "${dst}/64x64/apps/${PKG}.png"
|
||||||
|
install -D -m644 "${ROOT}/assets/icon-256.png" "${dst}/256x256/apps/${PKG}.png"
|
||||||
|
install -D -m644 "${ROOT}/assets/icon.png" "${dst}/1024x1024/apps/${PKG}.png"
|
||||||
|
echo "Icons: assets/*.png as-is (ImageMagick not found)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ dependencies --
|
||||||
|
|
||||||
|
# Real library dependencies, read from the ELF itself. GL / X11 / Wayland
|
||||||
|
# libraries are dlopen()ed by winit at runtime, so they never show up in
|
||||||
|
# NEEDED — they are listed in Recommends in control.in instead.
|
||||||
|
compute_depends() {
|
||||||
|
local depends=""
|
||||||
|
if [[ "${USE_SHLIBDEPS:-1}" == "1" ]] && command -v dpkg-shlibdeps >/dev/null 2>&1; then
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
mkdir -p "${tmp}/debian"
|
||||||
|
# dpkg-shlibdeps insists on reading a debian/control next to it.
|
||||||
|
cat > "${tmp}/debian/control" <<EOF
|
||||||
|
Source: ${PKG}
|
||||||
|
|
||||||
|
Package: ${PKG}
|
||||||
|
Architecture: ${ARCH}
|
||||||
|
Depends: \${shlibs:Depends}
|
||||||
|
Description: temporary, for dpkg-shlibdeps
|
||||||
|
EOF
|
||||||
|
: > "${tmp}/debian/${PKG}.substvars"
|
||||||
|
(cd "${tmp}" && dpkg-shlibdeps -T"debian/${PKG}.substvars" "${STAGE}/usr/bin/${PKG}") \
|
||||||
|
>/dev/null 2>&1 || true
|
||||||
|
depends="$(sed -n 's/^shlibs:Depends=//p' "${tmp}/debian/${PKG}.substvars")"
|
||||||
|
rm -rf "${tmp}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${depends}" ]]; then
|
||||||
|
echo "warning: dpkg-shlibdeps gave nothing, using a hand-written Depends list" >&2
|
||||||
|
depends='libc6, libfontconfig1, libgcc-s1'
|
||||||
|
fi
|
||||||
|
printf '%s' "${depends}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- control ---
|
||||||
|
|
||||||
|
stage_control() {
|
||||||
|
local installed_size depends
|
||||||
|
installed_size="$(du -s -k --exclude="${STAGE}/DEBIAN" "${STAGE}" | awk '{print $1}')"
|
||||||
|
depends="$(compute_depends)"
|
||||||
|
|
||||||
|
sed -e "s|@VERSION@|${DEB_VERSION}|" \
|
||||||
|
-e "s|@ARCHITECTURE@|${ARCH}|" \
|
||||||
|
-e "s|@MAINTAINER@|${MAINTAINER}|" \
|
||||||
|
-e "s|@INSTALLED_SIZE@|${installed_size}|" \
|
||||||
|
-e "s|@DEPENDS@|${depends}|" \
|
||||||
|
"${META_DIR}/control.in" > "${STAGE}/DEBIAN/control"
|
||||||
|
|
||||||
|
# Maintainer scripts must be executables owned by root in the archive.
|
||||||
|
install -m755 "${META_DIR}/postinst" "${STAGE}/DEBIAN/postinst"
|
||||||
|
install -m755 "${META_DIR}/postrm" "${STAGE}/DEBIAN/postrm"
|
||||||
|
|
||||||
|
(cd "${STAGE}" && find . -path ./DEBIAN -prune -o -type f -print \
|
||||||
|
| sed 's|^\./||' | sort | xargs -r md5sum) > "${STAGE}/DEBIAN/md5sums"
|
||||||
|
|
||||||
|
echo "Version: ${DEB_VERSION} (upstream ${VERSION} + revision ${DEB_REVISION})"
|
||||||
|
echo "Architecture: ${ARCH}"
|
||||||
|
echo "Maintainer: ${MAINTAINER}"
|
||||||
|
echo "Installed-Size: ${installed_size} KB"
|
||||||
|
echo "Depends: ${depends}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------- version / update ---
|
||||||
|
|
||||||
|
# What "apt upgrade" needs: a version that is strictly greater than the one
|
||||||
|
# already installed, and a package name that does not change. Both are checked
|
||||||
|
# here so a release is never silently unpromotable.
|
||||||
|
check_version() {
|
||||||
|
case "${VERSION}" in
|
||||||
|
[0-9]*) ;;
|
||||||
|
*) echo "error: upstream version '${VERSION}' must start with a digit (dpkg rule)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
if [[ ! "${VERSION}" =~ ^[0-9A-Za-z.+-~:]+$ || ! "${DEB_REVISION}" =~ ^[0-9A-Za-z.+-~]+$ ]]; then
|
||||||
|
echo "error: bad version '${DEB_VERSION}' — allowed chars are letters, digits and . + - ~ :" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local installed
|
||||||
|
installed="$(dpkg-query -W -f='${Version}' "${PKG}" 2>/dev/null || true)"
|
||||||
|
if [[ -n "${installed}" ]]; then
|
||||||
|
if dpkg --compare-versions "${installed}" ge "${DEB_VERSION}"; then
|
||||||
|
echo "warning: ${PKG} ${installed} is ALREADY installed and is not older than ${DEB_VERSION}" >&2
|
||||||
|
echo " → 'apt upgrade' will do nothing. Bump Cargo.toml or DEB_REVISION." >&2
|
||||||
|
else
|
||||||
|
echo "Upgrade path: ${installed} → ${DEB_VERSION} (apt upgrade will pick this up)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Upgrade path: fresh install of ${DEB_VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# scripts/install-desktop.sh links ~/.local/bin/rcalc → dist/rcalc, and
|
||||||
|
# ~/.local/bin comes before /usr/bin in PATH: the packaged binary would be
|
||||||
|
# shadowed by an old copy even after a successful upgrade.
|
||||||
|
if [[ -e "${HOME}/.local/bin/${PKG}" ]]; then
|
||||||
|
echo "warning: ${HOME}/.local/bin/${PKG} exists and shadows /usr/bin/${PKG}" >&2
|
||||||
|
echo " (installed by scripts/install-desktop.sh — remove it after switching to the .deb)" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# /usr/share/doc/rcalc/changelog.Debian.gz — Debian policy expects a changelog,
|
||||||
|
# and it is the human-readable history of what every version changed.
|
||||||
|
# packaging/deb/changelog (kept in git) wins; otherwise one entry is generated
|
||||||
|
# from the current commit so the package is never changelog-less.
|
||||||
|
stage_changelog() {
|
||||||
|
local doc="${STAGE}/usr/share/doc/${PKG}"
|
||||||
|
mkdir -p "${doc}"
|
||||||
|
|
||||||
|
local plain="${doc}/changelog.Debian" # gzip → changelog.Debian.gz (Debian-side history)
|
||||||
|
if [[ -f "${META_DIR}/changelog" ]]; then
|
||||||
|
cp "${META_DIR}/changelog" "${plain}"
|
||||||
|
echo "Changelog: packaging/deb/changelog"
|
||||||
|
else
|
||||||
|
local githash gitsubject
|
||||||
|
githash="$(git -C "${ROOT}" rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||||
|
gitsubject="$(git -C "${ROOT}" log -1 --pretty=%s 2>/dev/null || echo packaging)"
|
||||||
|
{
|
||||||
|
printf '%s (%s) unstable; urgency=medium\n\n' "${PKG}" "${DEB_VERSION}"
|
||||||
|
printf ' * Upstream %s, packaged as %s.\n' "${VERSION}" "${DEB_VERSION}"
|
||||||
|
if [[ -f "${OUT_DIR}/.glibc-build.txt" ]]; then
|
||||||
|
printf ' * Built against %s\n' "$(cat "${OUT_DIR}/.glibc-build.txt")"
|
||||||
|
fi
|
||||||
|
printf ' * git %s: %s\n\n' "${githash}" "${gitsubject}"
|
||||||
|
printf ' -- %s %s\n' "${MAINTAINER}" "$(date -R)"
|
||||||
|
} > "${plain}"
|
||||||
|
echo "Changelog: generated (add packaging/deb/changelog to keep real history)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
gzip -9n "${plain}" # -n: no timestamp → reproducible .gz
|
||||||
|
rm -f "${plain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# REPO=1 → turn dist/ into a flat apt repository, so updates can be installed
|
||||||
|
# with plain `apt update && apt upgrade` instead of pointing at a .deb file.
|
||||||
|
make_repo() {
|
||||||
|
[[ "${REPO:-0}" == "1" ]] || return 0
|
||||||
|
need_cmd dpkg-scanpackages
|
||||||
|
(
|
||||||
|
cd "${OUT_DIR}"
|
||||||
|
# One pass: Packages is the index, Packages.gz the compressed copy apt reads.
|
||||||
|
# Keep both files: apt reads Packages.gz, humans diff/read Packages.
|
||||||
|
dpkg-scanpackages --multiversion . /dev/null > Packages 2>/dev/null
|
||||||
|
gzip -9n -c Packages > Packages.gz
|
||||||
|
)
|
||||||
|
echo "Repo index: ${OUT_DIR}/Packages.gz ($(grep -c '^Package:' "${OUT_DIR}/Packages") entries)"
|
||||||
|
echo " apt one-liner: echo 'deb [trusted=yes] file:${OUT_DIR} ./' \\"
|
||||||
|
echo " | sudo tee /etc/apt/sources.list.d/${PKG}.list"
|
||||||
|
echo " then: sudo apt update && sudo apt install ${PKG}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ build ---
|
||||||
|
|
||||||
|
build_package() {
|
||||||
|
need_cmd dpkg-deb
|
||||||
|
rm -f "${DEB_FILE}"
|
||||||
|
|
||||||
|
# The stage tree inherits the build user's umask (often 002 → group-writable).
|
||||||
|
# Debian expects 755 dirs / 644 data files, plus the exec bits on the binary
|
||||||
|
# and the maintainer scripts.
|
||||||
|
find "${STAGE}" -type d -exec chmod 0755 {} +
|
||||||
|
find "${STAGE}" -type f -exec chmod 0644 {} +
|
||||||
|
chmod 0755 "${STAGE}/usr/bin/${PKG}" \
|
||||||
|
"${STAGE}/DEBIAN/postinst" "${STAGE}/DEBIAN/postrm"
|
||||||
|
|
||||||
|
# --root-owner-group: the stage tree is owned by the build user, the archive
|
||||||
|
# must not be (dpkg rejects foreign uids unless forced).
|
||||||
|
dpkg-deb --root-owner-group --build "${STAGE}" "${DEB_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_package() {
|
||||||
|
local ok=1
|
||||||
|
|
||||||
|
# Fields straight from the archive (dpkg-deb --info output is localised).
|
||||||
|
dpkg-deb --field "${DEB_FILE}"
|
||||||
|
|
||||||
|
if command -v desktop-file-validate >/dev/null 2>&1; then
|
||||||
|
if desktop-file-validate "${STAGE}/usr/share/applications/${PKG}.desktop"; then
|
||||||
|
echo "desktop-file-validate: OK"
|
||||||
|
else
|
||||||
|
echo "warning: the desktop file did not validate" >&2
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# dist/rcalc can be stale: build.rs embeds APP_VERSION from Cargo.toml only at
|
||||||
|
# compile time, and the AppImage packaging had exactly that bug (old binary in
|
||||||
|
# a new image). Anything newer than the packaged binary means "rebuild it".
|
||||||
|
local stale
|
||||||
|
stale="$(find "${ROOT}/src" "${ROOT}/ui" "${ROOT}/build.rs" "${ROOT}/Cargo.toml" \
|
||||||
|
-newer "${OUT_DIR}/${PKG}" -print -quit 2>/dev/null || true)"
|
||||||
|
if [[ -n "${stale}" ]]; then
|
||||||
|
echo "warning: ${stale} is newer than the packaged binary" >&2
|
||||||
|
echo " (PREBUILT=1 / SKIP_DOCKER=1 reused an old dist/${PKG} — rebuild to be safe)" >&2
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Files in package: $(dpkg-deb -c "${DEB_FILE}" | grep -c '^-')"
|
||||||
|
|
||||||
|
if [[ "${KEEP_STAGE}" != "1" ]]; then
|
||||||
|
rm -rf "${STAGE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
ls -l "${DEB_FILE}"
|
||||||
|
[[ "${ok}" == "1" ]] || echo "(check the warnings above)"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_version
|
||||||
|
ensure_binary
|
||||||
|
stage_tree
|
||||||
|
stage_icons
|
||||||
|
stage_changelog
|
||||||
|
stage_control
|
||||||
|
build_package
|
||||||
|
make_repo
|
||||||
|
verify_package
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "OK: ${DEB_FILE}"
|
||||||
|
echo "Version: ${DEB_VERSION} (upstream ${VERSION} + revision ${DEB_REVISION})"
|
||||||
|
echo "Install: sudo apt install '${DEB_FILE}'"
|
||||||
|
echo "Next release: bump version in Cargo.toml (app changes) — or just"
|
||||||
|
echo " DEB_REVISION=2 ./scripts/build-deb.sh (packaging/icon only),"
|
||||||
|
echo " then install the new .deb: apt replaces the old one in place."
|
||||||
|
echo "Run: rcalc (or start it from the application menu)"
|
||||||
|
echo "Remove: sudo apt remove ${PKG}"
|
||||||
|
|
||||||
+90
-4
@@ -391,7 +391,9 @@ impl Converter {
|
|||||||
if !ch.is_ascii_hexdigit() {
|
if !ch.is_ascii_hexdigit() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.draft == "0" {
|
// A synced all-zero draft ("0", "0000") is a placeholder:
|
||||||
|
// first digit replaces it instead of being swallowed.
|
||||||
|
if self.draft.chars().all(|c| c == '0') {
|
||||||
self.draft = ch.to_string();
|
self.draft = ch.to_string();
|
||||||
} else if self.draft.len() < 4 {
|
} else if self.draft.len() < 4 {
|
||||||
self.draft.push(ch);
|
self.draft.push(ch);
|
||||||
@@ -478,6 +480,11 @@ impl Converter {
|
|||||||
if t.is_empty() || t == "-" || t == "." || t == "-." {
|
if t.is_empty() || t == "-" || t == "." || t == "-." {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
// Exponent being typed: `1E`, `1E+`, `1E-` — intermediate
|
||||||
|
// state, keep the previous value without flagging an error.
|
||||||
|
if t.ends_with('E') || t.ends_with("E+") || t.ends_with("E-") {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
match t.parse::<f32>() {
|
match t.parse::<f32>() {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
self.float_bits = v.to_bits();
|
self.float_bits = v.to_bits();
|
||||||
@@ -777,8 +784,20 @@ fn format_float(v: f32) -> String {
|
|||||||
"Inf".into()
|
"Inf".into()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let s = format!("{v}");
|
if v == 0.0 {
|
||||||
s
|
// Keep IEEE −0 visible (bits 0x8000_0000).
|
||||||
|
return if v.is_sign_negative() {
|
||||||
|
"-0".into()
|
||||||
|
} else {
|
||||||
|
"0".into()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Scientific notation for extremes: keeps f32::MAX readable and the
|
||||||
|
// draft re-parsable (`3.4028235e38`.parse::<f32>() works).
|
||||||
|
if v.abs() < 1e-10 || v.abs() >= 1e16 {
|
||||||
|
return format!("{v:e}");
|
||||||
|
}
|
||||||
|
format!("{v}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// IEEE float bytes in natural order A B C D (big-endian bit pattern),
|
/// IEEE float bytes in natural order A B C D (big-endian bit pattern),
|
||||||
@@ -839,7 +858,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn roundtrip_all_endians() {
|
fn roundtrip_all_endians() {
|
||||||
let samples = [0.0f32, -1.0, 3.1415927, 12345.678, f32::MIN_POSITIVE];
|
let samples = [0.0f32, -1.0, std::f32::consts::PI, 12345.678, f32::MIN_POSITIVE];
|
||||||
for e in Endian::all() {
|
for e in Endian::all() {
|
||||||
for &f in &samples {
|
for &f in &samples {
|
||||||
let (w0, w1) = float_to_words(f, e);
|
let (w0, w1) = float_to_words(f, e);
|
||||||
@@ -889,4 +908,71 @@ mod tests {
|
|||||||
assert_eq!(c.ascii_code(), b'A');
|
assert_eq!(c.ascii_code(), b'A');
|
||||||
assert_eq!(c.ascii_char_text(), "A");
|
assert_eq!(c.ascii_char_text(), "A");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_draft_exponent_in_progress() {
|
||||||
|
// Typing `1E` is an intermediate state, not an error.
|
||||||
|
let mut c = Converter::new();
|
||||||
|
c.clear_active(); // draft "0"
|
||||||
|
c.input_char('1');
|
||||||
|
c.input_char('E');
|
||||||
|
assert_eq!(c.error(), None, "typing `1E` must not flag invalid float");
|
||||||
|
c.input_char('5');
|
||||||
|
assert_eq!(c.error(), None);
|
||||||
|
assert_eq!(c.float_value(), 1e5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_draft_exponent_sign_in_progress() {
|
||||||
|
let mut c = Converter::new();
|
||||||
|
c.clear_active(); // draft "0"
|
||||||
|
c.input_char('1');
|
||||||
|
c.input_char('E');
|
||||||
|
c.input_char('-');
|
||||||
|
assert_eq!(c.error(), None, "typing `1E-` must not flag invalid float");
|
||||||
|
c.input_char('9');
|
||||||
|
assert_eq!(c.error(), None);
|
||||||
|
assert_eq!(c.float_value(), 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negative_zero_words() {
|
||||||
|
// IEEE -0.0 must round-trip through the word view (0x8000 0000).
|
||||||
|
let mut c = Converter::new();
|
||||||
|
c.clear_active(); // draft "0", float +0
|
||||||
|
c.input_char('-');
|
||||||
|
c.input_char('0');
|
||||||
|
assert_eq!(c.words(), (0x8000, 0x0000));
|
||||||
|
assert_eq!(c.float_text(), "-0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn word_edit_updates_float() {
|
||||||
|
let mut c = Converter::new();
|
||||||
|
c.clear_active(); // float = 0
|
||||||
|
c.set_active(ConvField::Word0);
|
||||||
|
for ch in "3F80".chars() {
|
||||||
|
c.input_char(ch);
|
||||||
|
}
|
||||||
|
assert_eq!(c.float_value(), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn huge_float_uses_scientific() {
|
||||||
|
// f32::MAX plain-decimal display is 39 chars; exponent form is
|
||||||
|
// readable and re-parsable by the float draft parser.
|
||||||
|
let t = format_float(f32::MAX);
|
||||||
|
assert!(t.contains('e'), "expected scientific notation, got {t}");
|
||||||
|
assert_eq!(t.parse::<f32>().unwrap(), f32::MAX);
|
||||||
|
|
||||||
|
let t = format_float(1e-20);
|
||||||
|
assert!(t.contains('e'), "expected scientific notation, got {t}");
|
||||||
|
assert_eq!(t.parse::<f32>().unwrap(), 1e-20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negative_zero_displayed() {
|
||||||
|
assert_eq!(format_float(-0.0), "-0");
|
||||||
|
assert_eq!(format_float(0.0), "0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+200
-38
@@ -77,18 +77,27 @@ impl Rational {
|
|||||||
Self { num: n, den: 1 }
|
Self { num: n, den: 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a decimal literal like `12`, `-3.5`, `0.001` into an exact fraction.
|
/// Parse a decimal literal like `12`, `-3.5`, `0.001`, `1.5e-9` into an
|
||||||
|
/// exact fraction (exponent shifts by exact powers of ten).
|
||||||
fn parse(s: &str) -> Result<Self, CalcError> {
|
fn parse(s: &str) -> Result<Self, CalcError> {
|
||||||
let s = s.trim();
|
let s = s.trim();
|
||||||
if s.is_empty() || s == "." || s == "-" || s == "-." {
|
if s.is_empty() || s == "." || s == "-" || s == "-." {
|
||||||
return Err(CalcError::Invalid);
|
return Err(CalcError::Invalid);
|
||||||
}
|
}
|
||||||
let neg = s.starts_with('-');
|
// Split off an optional exponent: `1e-15`, `2.5E+3`.
|
||||||
let body = if neg { &s[1..] } else { s };
|
let (body, exp) = match s.split_once(['e', 'E']) {
|
||||||
|
Some((b, e)) => {
|
||||||
|
let exp: i32 = e.parse().map_err(|_| CalcError::Invalid)?;
|
||||||
|
(b, exp)
|
||||||
|
}
|
||||||
|
None => (s, 0i32),
|
||||||
|
};
|
||||||
|
let neg = body.starts_with('-');
|
||||||
|
let b = if neg { &body[1..] } else { body };
|
||||||
|
|
||||||
let (int_part, frac_part) = match body.split_once('.') {
|
let (int_part, frac_part) = match b.split_once('.') {
|
||||||
Some((a, b)) => (a, b),
|
Some((a, c)) => (a, c),
|
||||||
None => (body, ""),
|
None => (b, ""),
|
||||||
};
|
};
|
||||||
if int_part.is_empty() && frac_part.is_empty() {
|
if int_part.is_empty() && frac_part.is_empty() {
|
||||||
return Err(CalcError::Invalid);
|
return Err(CalcError::Invalid);
|
||||||
@@ -107,6 +116,27 @@ impl Rational {
|
|||||||
.and_then(|v| v.checked_add(frac_val))
|
.and_then(|v| v.checked_add(frac_val))
|
||||||
.ok_or(CalcError::Overflow)?;
|
.ok_or(CalcError::Overflow)?;
|
||||||
let num = if neg { -num } else { num };
|
let num = if neg { -num } else { num };
|
||||||
|
|
||||||
|
// Apply the exponent with exact power-of-ten arithmetic:
|
||||||
|
// value·10^exp = num·10^exp / 10^frac_digits.
|
||||||
|
let (num, den) = if exp >= 0 {
|
||||||
|
let e = exp as usize;
|
||||||
|
if e >= frac_digits {
|
||||||
|
(
|
||||||
|
num.checked_mul(pow10(e - frac_digits)?)
|
||||||
|
.ok_or(CalcError::Overflow)?,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(num, den / pow10(e)?)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
num,
|
||||||
|
den.checked_mul(pow10((-exp) as usize)?)
|
||||||
|
.ok_or(CalcError::Overflow)?,
|
||||||
|
)
|
||||||
|
};
|
||||||
Self::new(num, den)
|
Self::new(num, den)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,9 +205,14 @@ impl Value {
|
|||||||
if !v.is_finite() {
|
if !v.is_finite() {
|
||||||
return Err(CalcError::Overflow);
|
return Err(CalcError::Overflow);
|
||||||
}
|
}
|
||||||
// Prefer exact integer when float is (almost) integral.
|
if v == 0.0 {
|
||||||
if v.fract().abs() < 1e-12 && v.abs() < 1e15 {
|
return Ok(Value::Rat(Rational::from_i128(0)));
|
||||||
return Ok(Value::Rat(Rational::from_i128(v.round() as i128)));
|
}
|
||||||
|
// Prefer exact integer when float is (almost) integral. Never snap a
|
||||||
|
// sub-unit magnitude to an integer: that turned 1e-16 into `0`.
|
||||||
|
let r = v.round();
|
||||||
|
if r != 0.0 && (v - r).abs() < 1e-12 && v.abs() < 1e15 {
|
||||||
|
return Ok(Value::Rat(Rational::from_i128(r as i128)));
|
||||||
}
|
}
|
||||||
Ok(Value::Float(v))
|
Ok(Value::Float(v))
|
||||||
}
|
}
|
||||||
@@ -394,7 +429,7 @@ impl Calculator {
|
|||||||
self.entry = d.to_string();
|
self.entry = d.to_string();
|
||||||
} else if self.entry == "-0" {
|
} else if self.entry == "-0" {
|
||||||
self.entry = format!("-{d}");
|
self.entry = format!("-{d}");
|
||||||
} else if digit_count(&self.entry) < 16 {
|
} else if digit_count(&self.entry) < 40 && significant_digits(&self.entry) < 16 {
|
||||||
self.entry.push(d);
|
self.entry.push(d);
|
||||||
}
|
}
|
||||||
self.entry_value = None;
|
self.entry_value = None;
|
||||||
@@ -911,6 +946,19 @@ fn tokenize(src: &str) -> Result<Vec<(Tok, Option<Value>)>, CalcError> {
|
|||||||
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
|
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
// Optional decimal exponent: `1e-9`, `2.5E+3`.
|
||||||
|
if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') {
|
||||||
|
let mut j = i + 1;
|
||||||
|
if j < chars.len() && (chars[j] == '+' || chars[j] == '-') {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if j < chars.len() && chars[j].is_ascii_digit() {
|
||||||
|
i = j;
|
||||||
|
while i < chars.len() && chars[i].is_ascii_digit() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let lit: String = chars[start..i].iter().collect();
|
let lit: String = chars[start..i].iter().collect();
|
||||||
let v = Value::from_entry(&lit)?;
|
let v = Value::from_entry(&lit)?;
|
||||||
out.push((Tok::Num, Some(v)));
|
out.push((Tok::Num, Some(v)));
|
||||||
@@ -984,6 +1032,15 @@ fn digit_count(s: &str) -> usize {
|
|||||||
s.chars().filter(|c| c.is_ascii_digit()).count()
|
s.chars().filter(|c| c.is_ascii_digit()).count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Significant digits (leading zeros don't count) — caps precision, not
|
||||||
|
/// magnitude, so `0.0000000000000001` (1e-16) can be typed.
|
||||||
|
fn significant_digits(s: &str) -> usize {
|
||||||
|
s.chars()
|
||||||
|
.filter(|c| c.is_ascii_digit())
|
||||||
|
.skip_while(|&c| c == '0')
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
fn gcd(mut a: u128, mut b: u128) -> u128 {
|
fn gcd(mut a: u128, mut b: u128) -> u128 {
|
||||||
while b != 0 {
|
while b != 0 {
|
||||||
let t = b;
|
let t = b;
|
||||||
@@ -1016,36 +1073,36 @@ fn format_rational(r: Rational) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn terminating_decimal(r: Rational) -> Option<String> {
|
fn terminating_decimal(r: Rational) -> Option<String> {
|
||||||
|
// Terminating iff den = 2^a · 5^b. Digit count after the point is
|
||||||
|
// k = max(a, b); scale num so the denominator becomes exactly 10^k.
|
||||||
let mut den = r.den;
|
let mut den = r.den;
|
||||||
|
let (mut a, mut b) = (0u32, 0u32);
|
||||||
while den % 2 == 0 {
|
while den % 2 == 0 {
|
||||||
den /= 2;
|
den /= 2;
|
||||||
|
a += 1;
|
||||||
}
|
}
|
||||||
while den % 5 == 0 {
|
while den % 5 == 0 {
|
||||||
den /= 5;
|
den /= 5;
|
||||||
|
b += 1;
|
||||||
}
|
}
|
||||||
if den != 1 {
|
if den != 1 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// Scale denominator to a power of 10.
|
let k = a.max(b);
|
||||||
let mut d = r.den;
|
let mut n = r.num.abs();
|
||||||
let mut n = r.num;
|
for _ in 0..(k - a) {
|
||||||
let neg = n < 0;
|
n = n.checked_mul(2)?;
|
||||||
n = n.abs();
|
}
|
||||||
while d % 10 != 0 {
|
for _ in 0..(k - b) {
|
||||||
if d % 2 == 0 {
|
n = n.checked_mul(5)?;
|
||||||
d *= 5;
|
}
|
||||||
n *= 5;
|
let mut d: i128 = 1;
|
||||||
} else if d % 5 == 0 {
|
for _ in 0..k {
|
||||||
d *= 2;
|
d = d.checked_mul(10)?;
|
||||||
n *= 2;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let int_part = n / d;
|
let int_part = n / d;
|
||||||
let mut frac = (n % d).to_string();
|
let mut frac = (n % d).to_string();
|
||||||
let width = d.to_string().len().saturating_sub(1);
|
while frac.len() < k as usize {
|
||||||
while frac.len() < width {
|
|
||||||
frac.insert(0, '0');
|
frac.insert(0, '0');
|
||||||
}
|
}
|
||||||
// trim trailing zeros
|
// trim trailing zeros
|
||||||
@@ -1057,24 +1114,26 @@ fn terminating_decimal(r: Rational) -> Option<String> {
|
|||||||
} else {
|
} else {
|
||||||
format!("{int_part}.{frac}")
|
format!("{int_part}.{frac}")
|
||||||
};
|
};
|
||||||
Some(if neg { format!("-{body}") } else { body })
|
Some(if r.num < 0 { format!("-{body}") } else { body })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_float(v: f64) -> String {
|
fn format_float(v: f64) -> String {
|
||||||
if !v.is_finite() {
|
if !v.is_finite() || v == 0.0 {
|
||||||
return "0".into();
|
return "0".into();
|
||||||
}
|
}
|
||||||
// Snap near-integers (guards any float path residue).
|
// Snap near-integers (guards any float path residue); never for |v| < 1,
|
||||||
if (v - v.round()).abs() < 1e-10 && v.abs() < 1e15 {
|
// otherwise tiny results like 1e-16 collapse to `0`.
|
||||||
return format!("{}", v.round() as i64);
|
let r = v.round();
|
||||||
|
if r != 0.0 && (v - r).abs() < 1e-10 && v.abs() < 1e15 {
|
||||||
|
return format!("{}", r as i64);
|
||||||
}
|
}
|
||||||
let s = format!("{:.12}", v);
|
// Very small / large magnitudes: scientific notation keeps the value
|
||||||
let s = s.trim_end_matches('0').trim_end_matches('.').to_string();
|
// visible and re-parseable (`Rational::parse` understands exponents).
|
||||||
if s.is_empty() || s == "-" {
|
if v.abs() < 1e-10 || v.abs() >= 1e16 {
|
||||||
"0".into()
|
return format!("{v:e}");
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
}
|
||||||
|
// Shortest representation that round-trips back to `v` exactly.
|
||||||
|
format!("{v}")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Rational {
|
impl fmt::Display for Rational {
|
||||||
@@ -1173,4 +1232,107 @@ mod tests {
|
|||||||
let r = Rational::parse("1.25").unwrap();
|
let r = Rational::parse("1.25").unwrap();
|
||||||
assert_eq!(r, Rational::new(5, 4).unwrap());
|
assert_eq!(r, Rational::new(5, 4).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- regression tests (rounding / formatting bugs) ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quarter_and_eighth_display() {
|
||||||
|
// terminating_decimal scaled the fraction wrong: 1/4 displayed as "0.5".
|
||||||
|
let mut c = Calculator::new();
|
||||||
|
c.input_digit('1');
|
||||||
|
c.set_op(Op::Div);
|
||||||
|
c.input_digit('4');
|
||||||
|
c.equals();
|
||||||
|
assert_eq!(c.display(), "0.25");
|
||||||
|
|
||||||
|
c.clear_all();
|
||||||
|
c.input_digit('1');
|
||||||
|
c.set_op(Op::Div);
|
||||||
|
c.input_digit('8');
|
||||||
|
c.equals();
|
||||||
|
assert_eq!(c.display(), "0.125");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn five_hundredths_display() {
|
||||||
|
// 0.05 must survive exact formatting (was displayed as "0.1").
|
||||||
|
let mut c = Calculator::new();
|
||||||
|
c.input_digit('0');
|
||||||
|
c.input_dot();
|
||||||
|
c.input_digit('0');
|
||||||
|
c.input_digit('5'); // 0.05
|
||||||
|
c.set_op(Op::Mul);
|
||||||
|
c.input_digit('1');
|
||||||
|
c.equals();
|
||||||
|
assert_eq!(c.display(), "0.05");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tiny_reciprocal_not_zero() {
|
||||||
|
// 1/9999999999999999 ≈ 1e-16 was snapped to "0" by format_float.
|
||||||
|
let mut c = Calculator::new();
|
||||||
|
for d in "9999999999999999".chars() {
|
||||||
|
c.input_digit(d);
|
||||||
|
}
|
||||||
|
c.reciprocal();
|
||||||
|
let disp = c.display();
|
||||||
|
assert!(disp != "0", "tiny reciprocal must not display as 0, got {disp}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tiny_result_reusable_standard() {
|
||||||
|
// Float result in scientific notation must be re-parsable as operand.
|
||||||
|
let mut c = Calculator::new();
|
||||||
|
for d in "9999999999999999".chars() {
|
||||||
|
c.input_digit(d);
|
||||||
|
}
|
||||||
|
c.reciprocal();
|
||||||
|
c.set_op(Op::Mul);
|
||||||
|
c.input_digit('2');
|
||||||
|
c.equals();
|
||||||
|
let disp = c.display();
|
||||||
|
assert!(disp != "0" && disp != "Invalid input", "got {disp}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sqrt_square_roundtrip_tiny() {
|
||||||
|
// √(1e-30) = 1e-15 was snapped to 0 by Value::from_f64.
|
||||||
|
let mut c = Calculator::new();
|
||||||
|
c.input_dot(); // "0."
|
||||||
|
for _ in 0..14 {
|
||||||
|
c.input_digit('0');
|
||||||
|
}
|
||||||
|
c.input_digit('1'); // 0.000000000000001 = 1e-15
|
||||||
|
c.square();
|
||||||
|
c.sqrt();
|
||||||
|
let disp = c.display();
|
||||||
|
assert!(disp != "0", "sqrt(1e-30) must not display as 0, got {disp}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rational_parse_exponent() {
|
||||||
|
// Scientific-notation entries must parse to exact fractions.
|
||||||
|
let r = Rational::parse("1e-15").unwrap();
|
||||||
|
assert_eq!(r, Rational::new(1, 1_000_000_000_000_000).unwrap());
|
||||||
|
let r = Rational::parse("1.5e3").unwrap();
|
||||||
|
assert_eq!(r, Rational::new(1500, 1).unwrap());
|
||||||
|
let r = Rational::parse("2.5e-2").unwrap();
|
||||||
|
assert_eq!(r, Rational::new(1, 40).unwrap());
|
||||||
|
let r = Rational::parse("-1E+3").unwrap();
|
||||||
|
assert_eq!(r, Rational::new(-1000, 1).unwrap());
|
||||||
|
assert!(Rational::parse("1e2e3").is_err());
|
||||||
|
assert!(Rational::parse("1e").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_type_one_e_minus_16() {
|
||||||
|
// Leading zeros must not eat the 16-significant-digit budget.
|
||||||
|
let mut c = Calculator::new();
|
||||||
|
c.input_dot();
|
||||||
|
for _ in 0..15 {
|
||||||
|
c.input_digit('0');
|
||||||
|
}
|
||||||
|
c.input_digit('1');
|
||||||
|
assert_eq!(c.display(), "0.0000000000000001");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-10
@@ -199,7 +199,7 @@ impl Engineering {
|
|||||||
self.entry = d.to_string();
|
self.entry = d.to_string();
|
||||||
} else if self.entry == "-0" {
|
} else if self.entry == "-0" {
|
||||||
self.entry = format!("-{d}");
|
self.entry = format!("-{d}");
|
||||||
} else if digit_count(&self.entry) < 16 {
|
} else if digit_count(&self.entry) < 40 && significant_digits(&self.entry) < 16 {
|
||||||
self.entry.push(d);
|
self.entry.push(d);
|
||||||
}
|
}
|
||||||
self.sync_expr();
|
self.sync_expr();
|
||||||
@@ -389,6 +389,26 @@ impl Engineering {
|
|||||||
self.sync_expr();
|
self.sync_expr();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert `× 10 ^` for scientific-notation entry: `1.5 [×10ˣ] 3 = 1500`.
|
||||||
|
/// Exponent may be negated with ± before `=` (`10 ^ −3` parses correctly).
|
||||||
|
pub fn mul_pow10(&mut self) {
|
||||||
|
if self.error.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.done {
|
||||||
|
// Continue from the result: 1500 [×10ˣ] 2 = 150000.
|
||||||
|
self.formula.clear();
|
||||||
|
self.done = false;
|
||||||
|
self.typing = true;
|
||||||
|
}
|
||||||
|
self.flush_entry();
|
||||||
|
self.formula.push_str(" × 10 ^ ");
|
||||||
|
self.entry = "0".into();
|
||||||
|
self.typing = false;
|
||||||
|
self.second = false;
|
||||||
|
self.sync_expr();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn equals(&mut self) {
|
pub fn equals(&mut self) {
|
||||||
if self.error.is_some() {
|
if self.error.is_some() {
|
||||||
return;
|
return;
|
||||||
@@ -550,20 +570,31 @@ fn digit_count(s: &str) -> usize {
|
|||||||
s.chars().filter(|c| c.is_ascii_digit()).count()
|
s.chars().filter(|c| c.is_ascii_digit()).count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Significant digits (leading zeros don't count) — caps precision, not
|
||||||
|
/// magnitude, so `0.0000000000000001` (1e-16) can be typed.
|
||||||
|
fn significant_digits(s: &str) -> usize {
|
||||||
|
s.chars()
|
||||||
|
.filter(|c| c.is_ascii_digit())
|
||||||
|
.skip_while(|&c| c == '0')
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
fn format_num(v: f64) -> String {
|
fn format_num(v: f64) -> String {
|
||||||
if !v.is_finite() {
|
if !v.is_finite() || v == 0.0 {
|
||||||
return "0".into();
|
return "0".into();
|
||||||
}
|
}
|
||||||
if (v - v.round()).abs() < 1e-10 && v.abs() < 1e15 {
|
// Snap near-integers; never for |v| < 1 (kept 10^−50 displayed as "0").
|
||||||
return format!("{}", v.round() as i64);
|
let r = v.round();
|
||||||
|
if r != 0.0 && (v - r).abs() < 1e-10 && v.abs() < 1e15 {
|
||||||
|
return format!("{}", r as i64);
|
||||||
}
|
}
|
||||||
let s = format!("{:.12}", v);
|
// Scientific notation for extremes; `entry.parse::<f64>()` and the
|
||||||
let s = s.trim_end_matches('0').trim_end_matches('.').to_string();
|
// tokenizer both accept it, so the result stays reusable.
|
||||||
if s.is_empty() || s == "-" {
|
if v.abs() < 1e-10 || v.abs() >= 1e16 {
|
||||||
"0".into()
|
return format!("{v:e}");
|
||||||
} else {
|
|
||||||
s
|
|
||||||
}
|
}
|
||||||
|
// Shortest round-trip representation.
|
||||||
|
format!("{v}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- expression evaluator ---
|
// --- expression evaluator ---
|
||||||
@@ -639,6 +670,20 @@ fn tokenize(src: &str) -> Result<Vec<Tok>, SciError> {
|
|||||||
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
|
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
// Optional decimal exponent: `1e-9`, `2.5E+3`. The standalone
|
||||||
|
// constant `e` stays an Ident (needs no digit right after).
|
||||||
|
if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') {
|
||||||
|
let mut j = i + 1;
|
||||||
|
if j < chars.len() && (chars[j] == '+' || chars[j] == '-') {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if j < chars.len() && chars[j].is_ascii_digit() {
|
||||||
|
i = j;
|
||||||
|
while i < chars.len() && chars[i].is_ascii_digit() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let lit: String = chars[start..i].iter().collect();
|
let lit: String = chars[start..i].iter().collect();
|
||||||
let v: f64 = lit.parse().map_err(|_| SciError::Invalid)?;
|
let v: f64 = lit.parse().map_err(|_| SciError::Invalid)?;
|
||||||
out.push(Tok::Num(v));
|
out.push(Tok::Num(v));
|
||||||
@@ -858,4 +903,91 @@ mod tests {
|
|||||||
e.equals();
|
e.equals();
|
||||||
assert_eq!(e.display(), "1024");
|
assert_eq!(e.display(), "1024");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mul_pow10_basic() {
|
||||||
|
// 1.5 ×10ˣ 3 = 1500 (× 10 ^ с приоритетом ^ над ×).
|
||||||
|
let mut e = Engineering::new();
|
||||||
|
e.input_digit('1');
|
||||||
|
e.input_dot();
|
||||||
|
e.input_digit('5');
|
||||||
|
e.mul_pow10();
|
||||||
|
e.input_digit('3');
|
||||||
|
e.equals();
|
||||||
|
assert_eq!(e.display(), "1500");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mul_pow10_negative_exponent() {
|
||||||
|
// 2.5 ×10ˣ ±2 = 0.025.
|
||||||
|
let mut e = Engineering::new();
|
||||||
|
e.input_digit('2');
|
||||||
|
e.input_dot();
|
||||||
|
e.input_digit('5');
|
||||||
|
e.mul_pow10();
|
||||||
|
e.input_digit('2');
|
||||||
|
e.negate();
|
||||||
|
e.equals();
|
||||||
|
assert_eq!(e.display(), "0.025");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mul_pow10_continues_result() {
|
||||||
|
// 2 × 3 = 6, затем 6 ×10ˣ 2 = 600.
|
||||||
|
let mut e = Engineering::new();
|
||||||
|
e.input_digit('2');
|
||||||
|
e.set_op("×");
|
||||||
|
e.input_digit('3');
|
||||||
|
e.equals();
|
||||||
|
e.mul_pow10();
|
||||||
|
e.input_digit('2');
|
||||||
|
e.equals();
|
||||||
|
assert_eq!(e.display(), "600");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ten_pow_negative_not_zero() {
|
||||||
|
// 10 ^ −50 ≈ 1e-50 was snapped to "0" by format_num.
|
||||||
|
let mut e = Engineering::new();
|
||||||
|
e.input_digit('1');
|
||||||
|
e.input_digit('0');
|
||||||
|
e.set_op("^");
|
||||||
|
e.input_digit('5');
|
||||||
|
e.input_digit('0');
|
||||||
|
e.negate();
|
||||||
|
e.equals();
|
||||||
|
let disp = e.display();
|
||||||
|
assert!(disp != "0", "10^−50 must not display as 0, got {disp}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tiny_float_result_continues_expression() {
|
||||||
|
// Result in scientific notation must be re-parsable by the tokenizer.
|
||||||
|
let mut e = Engineering::new();
|
||||||
|
e.input_digit('1');
|
||||||
|
e.input_digit('0');
|
||||||
|
e.set_op("^");
|
||||||
|
e.input_digit('1');
|
||||||
|
e.input_digit('6');
|
||||||
|
e.negate();
|
||||||
|
e.equals();
|
||||||
|
assert_eq!(e.display(), "1e-16");
|
||||||
|
e.set_op("+");
|
||||||
|
e.input_digit('1');
|
||||||
|
e.equals();
|
||||||
|
assert_eq!(e.display(), "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_type_one_e_minus_16() {
|
||||||
|
// Leading zeros must not eat the 16-significant-digit budget.
|
||||||
|
let mut e = Engineering::new();
|
||||||
|
e.input_digit('0');
|
||||||
|
e.input_dot();
|
||||||
|
for _ in 0..15 {
|
||||||
|
e.input_digit('0');
|
||||||
|
}
|
||||||
|
e.input_digit('1');
|
||||||
|
assert_eq!(e.display(), "0.0000000000000001");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -532,6 +532,7 @@ fn handle_engineering(calc: &mut Engineering, id: &str) {
|
|||||||
"eng:fact" => calc.insert_func("fact"),
|
"eng:fact" => calc.insert_func("fact"),
|
||||||
"eng:exp" => calc.insert_func("exp"),
|
"eng:exp" => calc.insert_func("exp"),
|
||||||
"eng:tenpow" => calc.insert_func("tenpow"),
|
"eng:tenpow" => calc.insert_func("tenpow"),
|
||||||
|
"eng:mul10pow" => calc.mul_pow10(),
|
||||||
"eng:log" => calc.insert_func("log"),
|
"eng:log" => calc.insert_func("log"),
|
||||||
"eng:ln" => calc.insert_func("ln"),
|
"eng:ln" => calc.insert_func("ln"),
|
||||||
"MC" => calc.memory_clear(),
|
"MC" => calc.memory_clear(),
|
||||||
|
|||||||
@@ -699,4 +699,35 @@ mod tests {
|
|||||||
assert_eq!(p.word_size(), WordSize::Dint);
|
assert_eq!(p.word_size(), WordSize::Dint);
|
||||||
assert_eq!(p.dec_text().replace(' ', ""), "-1");
|
assert_eq!(p.dec_text().replace(' ', ""), "-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shift_count_mods_word_size() {
|
||||||
|
// Locks hardware-style semantics: shift count is taken modulo the
|
||||||
|
// word size (1 << 100000000 → 1e8 % 64 == 0 → no shift).
|
||||||
|
let mut p = Programmer::new();
|
||||||
|
p.input_digit('1');
|
||||||
|
p.set_op(ProgOp::Lsh);
|
||||||
|
for ch in "100000000".chars() {
|
||||||
|
p.input_digit(ch);
|
||||||
|
}
|
||||||
|
p.equals();
|
||||||
|
assert_eq!(p.current_bits(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shift_out_of_word_gives_zero() {
|
||||||
|
// 0xFFFF_FFFF_FFFF_FFFF Lsh 63 → only bit 0 survives at bit 63.
|
||||||
|
let mut p = Programmer::new();
|
||||||
|
p.set_base(Base::Hex);
|
||||||
|
for ch in "FFFFFFFFFFFFFFFF".chars() {
|
||||||
|
p.input_digit(ch);
|
||||||
|
}
|
||||||
|
p.set_op(ProgOp::Lsh);
|
||||||
|
p.set_base(Base::Dec); // type the shift count in decimal
|
||||||
|
p.input_digit('6');
|
||||||
|
p.input_digit('3');
|
||||||
|
p.equals();
|
||||||
|
// (u64::MAX << 63) & u64::MAX = 0x8000_0000_0000_0000
|
||||||
|
assert_eq!(p.current_bits(), 0x8000_0000_0000_0000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-12
@@ -1211,7 +1211,7 @@ export component AppWindow inherits Window {
|
|||||||
label: root.eng-second ? "³√x" : "√x";
|
label: root.eng-second ? "³√x" : "√x";
|
||||||
clicked => { root.press(root.eng-second ? "eng:cbrt" : "eng:sqrt"); }
|
clicked => { root.press(root.eng-second ? "eng:cbrt" : "eng:sqrt"); }
|
||||||
}
|
}
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "÷"; clicked => { root.press("/"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "x*10^y"; clicked => { root.press("eng:mul10pow"); } }
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
FuncBtn {
|
FuncBtn {
|
||||||
@@ -1222,14 +1222,14 @@ export component AppWindow inherits Window {
|
|||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "1/x"; clicked => { root.press("eng:inv"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "1/x"; clicked => { root.press("eng:inv"); } }
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "|x|"; clicked => { root.press("eng:abs"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "|x|"; clicked => { root.press("eng:abs"); } }
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "n!"; clicked => { root.press("eng:fact"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "n!"; clicked => { root.press("eng:fact"); } }
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "×"; clicked => { root.press("*"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "÷"; clicked => { root.press("/"); } }
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "x^y"; clicked => { root.press("^"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "x^y"; clicked => { root.press("^"); } }
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "("; clicked => { root.press("("); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "("; clicked => { root.press("("); } }
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: ")"; clicked => { root.press(")"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: ")"; clicked => { root.press(")"); } }
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "CE"; clicked => { root.press("CE"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "CE"; clicked => { root.press("CE"); } }
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "−"; clicked => { root.press("-"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "×"; clicked => { root.press("*"); } }
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
FuncBtn {
|
FuncBtn {
|
||||||
@@ -1240,28 +1240,28 @@ export component AppWindow inherits Window {
|
|||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "7"; clicked => { root.press("7"); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "7"; clicked => { root.press("7"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "8"; clicked => { root.press("8"); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "8"; clicked => { root.press("8"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "9"; clicked => { root.press("9"); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "9"; clicked => { root.press("9"); } }
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "+"; clicked => { root.press("+"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "−"; clicked => { root.press("-"); } }
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "log"; clicked => { root.press("eng:log"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "log"; clicked => { root.press("eng:log"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "4"; clicked => { root.press("4"); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "4"; clicked => { root.press("4"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "5"; clicked => { root.press("5"); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "5"; clicked => { root.press("5"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "6"; clicked => { root.press("6"); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "6"; clicked => { root.press("6"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "±"; clicked => { root.press("neg"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "+"; clicked => { root.press("+"); } }
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "ln"; clicked => { root.press("eng:ln"); } }
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "ln"; clicked => { root.press("eng:ln"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "1"; clicked => { root.press("1"); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "1"; clicked => { root.press("1"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "2"; clicked => { root.press("2"); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "2"; clicked => { root.press("2"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "3"; clicked => { root.press("3"); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "3"; clicked => { root.press("3"); } }
|
||||||
EqBtn { min-h: UiStyle.btn-h-sm; clicked => { root.press("="); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "±"; clicked => { root.press("neg"); } }
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
|
FuncBtn { min-h: UiStyle.btn-h-sm; label: "exp"; clicked => { root.press("eng:exp"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "0"; clicked => { root.press("0"); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "0"; clicked => { root.press("0"); } }
|
||||||
DigitBtn { min-h: UiStyle.btn-h-sm; label: "."; clicked => { root.press("."); } }
|
DigitBtn { min-h: UiStyle.btn-h-sm; label: "."; clicked => { root.press("."); } }
|
||||||
FuncBtn { min-h: UiStyle.btn-h-sm; label: "exp"; clicked => { root.press("eng:exp"); } }
|
|
||||||
Rectangle { min-height: 32px; background: transparent; }
|
|
||||||
Rectangle { min-height: 32px; background: transparent; }
|
Rectangle { min-height: 32px; background: transparent; }
|
||||||
|
EqBtn { min-h: UiStyle.btn-h-sm; clicked => { root.press("="); } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1293,10 +1293,17 @@ export component AppWindow inherits Window {
|
|||||||
if root.mode == 0: GridLayout {
|
if root.mode == 0: GridLayout {
|
||||||
spacing: 6px;
|
spacing: 6px;
|
||||||
Row {
|
Row {
|
||||||
if root.std-panel == 0: FuncBtn { label: "%"; clicked => { root.press("%"); } }
|
// `if` внутри Row у GridLayout ломает раскладку Slint
|
||||||
if root.std-panel == 1: FuncBtn { label: "("; clicked => { root.press("("); } }
|
// (RepeatedItemTree::grid_layout_input_data) — кнопки теряются.
|
||||||
if root.std-panel == 0: FuncBtn { label: "CE"; clicked => { root.press("CE"); } }
|
// Поэтому одна кнопка с тернарником вместо пары условных.
|
||||||
if root.std-panel == 1: FuncBtn { label: ")"; clicked => { root.press(")"); } }
|
FuncBtn {
|
||||||
|
label: root.std-panel == 0 ? "%" : "(";
|
||||||
|
clicked => { root.press(root.std-panel == 0 ? "%" : "("); }
|
||||||
|
}
|
||||||
|
FuncBtn {
|
||||||
|
label: root.std-panel == 0 ? "CE" : ")";
|
||||||
|
clicked => { root.press(root.std-panel == 0 ? "CE" : ")"); }
|
||||||
|
}
|
||||||
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"); } }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user