1084 lines
50 KiB
Markdown
1084 lines
50 KiB
Markdown
# Design: Tally - Cross-Platform Calculator
|
||
|
||
## References
|
||
|
||
- #[[file:.kiro/specs/calculator/requirements.md]]
|
||
|
||
---
|
||
|
||
## 1. System Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Frontends │
|
||
├─────────────┬──────────────────┬────────────────────────────┤
|
||
│ CLI │ TUI │ Android │
|
||
│ (Zig) │ (Zig+libvaxis) │ (Kotlin/Compose) │
|
||
└──────┬──────┴────────┬─────────┴──────────────┬─────────────┘
|
||
│ │ │
|
||
│ Zig import │ Zig import │ JNI (C ABI)
|
||
│ │ │
|
||
┌──────▼───────────────▼────────────────────────▼─────────────┐
|
||
│ Engine (Zig Library) │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ parser │ evaluator │ programmer │ struct_layout │
|
||
│ │ │ │ │
|
||
│ tokens │ standard │ bitwise ops │ abi_profiles │
|
||
│ ast │ financial │ base conv │ layout_compute │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
The engine is a pure Zig library with no I/O. Frontends depend on the engine; the engine depends on nothing outside `std`.
|
||
|
||
### Build Graph
|
||
|
||
```
|
||
build.zig (workspace root)
|
||
|- engine/build.zig -> produces: static lib (.a), C header, shared lib (.so/.dylib/.dll)
|
||
|- cli/build.zig -> produces: statically-linked binary
|
||
|- tui/build.zig -> produces: statically-linked binary (links libvaxis)
|
||
|- android targets -> produces: libtally.so for arm64-v8a, x86_64
|
||
(consumed by android/ Gradle project for APK packaging)
|
||
```
|
||
|
||
**Build system philosophy:**
|
||
- `zig build` is the single entry point for all compilation. No Makefile, no CMake, no shell scripts.
|
||
- `zig build` (no args) -> builds engine + CLI + TUI for the host platform.
|
||
- `zig build -Dtarget=aarch64-linux-android` -> cross-compiles engine .so for Android arm64.
|
||
- `zig build test` -> runs all engine unit tests with coverage reporting.
|
||
- The `android/` Gradle project handles only APK packaging and Compose UI compilation - it does not invoke Zig. Prebuilt .so files are committed or produced by a prior `zig build` step.
|
||
|
||
**Toolchain management (mise):**
|
||
- `.mise.toml` at project root declares: Zig version, any other needed tools.
|
||
- `mise install` provisions the full dev environment. No other manual setup required.
|
||
- CI uses the same `.mise.toml` for reproducibility.
|
||
|
||
---
|
||
|
||
## 2. Engine Design
|
||
|
||
### 2.1 Module Breakdown
|
||
|
||
| Module | Responsibility |
|
||
|--------|---------------|
|
||
| `parser.zig` | Tokenizer + Pratt parser -> AST |
|
||
| `ast.zig` | AST node definitions |
|
||
| `evaluator.zig` | Walk AST, produce results |
|
||
| `programmer.zig` | Integer operations, base conversion, bit manipulation |
|
||
| `struct_layout.zig` | Struct DSL parser, layout computation, ABI profiles |
|
||
| `units.zig` | Unit conversion tables and resolver |
|
||
| `financial.zig` | CAGR, TVM, compound interest |
|
||
| `types.zig` | Shared types (Value, Error, etc.) |
|
||
| `engine.zig` | Public API surface (Zig-native) |
|
||
| `c_api.zig` | `extern "C"` wrappers for JNI/FFI consumers |
|
||
|
||
### 2.2 Core Data Types
|
||
|
||
```zig
|
||
/// Result of any calculation
|
||
pub const Value = union(enum) {
|
||
integer: Integer,
|
||
float: f64,
|
||
boolean: bool,
|
||
struct_layout: StructLayoutResult,
|
||
multi_base: MultiBaseResult, // programmer mode result
|
||
};
|
||
|
||
pub const Integer = struct {
|
||
/// Raw bits stored in u64, interpretation depends on context
|
||
raw: u64,
|
||
bit_width: BitWidth,
|
||
signedness: Signedness,
|
||
};
|
||
|
||
pub const BitWidth = enum { bits8, bits16, bits32, bits64 };
|
||
pub const Signedness = enum { signed, unsigned };
|
||
|
||
pub const MultiBaseResult = struct {
|
||
value: Integer,
|
||
decimal_signed: []const u8, // formatted string
|
||
decimal_unsigned: []const u8,
|
||
hex: []const u8,
|
||
octal: []const u8,
|
||
binary: []const u8,
|
||
bit_pattern: [64]u1, // individual bits, index 0 = LSB
|
||
};
|
||
```
|
||
|
||
### 2.3 AST Nodes
|
||
|
||
```zig
|
||
pub const Expr = union(enum) {
|
||
number_literal: NumberLiteral,
|
||
unary_op: UnaryOp,
|
||
binary_op: BinaryOp,
|
||
function_call: FunctionCall,
|
||
variable: []const u8,
|
||
assignment: Assignment,
|
||
};
|
||
|
||
pub const NumberLiteral = struct {
|
||
value: f64,
|
||
/// For programmer mode: preserve original base and integer value
|
||
int_value: ?u64,
|
||
base: Base,
|
||
};
|
||
|
||
pub const Base = enum { decimal, hex, octal, binary };
|
||
|
||
pub const BinaryOp = struct {
|
||
op: Operator,
|
||
left: *Expr,
|
||
right: *Expr,
|
||
};
|
||
|
||
pub const Operator = enum {
|
||
add, sub, mul, div, mod, pow,
|
||
bit_and, bit_or, bit_xor, shift_left,
|
||
shift_right_logical, shift_right_arithmetic,
|
||
rotate_left, rotate_right,
|
||
};
|
||
```
|
||
|
||
### 2.4 Parser Design
|
||
|
||
**Approach: Pratt parser (top-down operator precedence)**
|
||
|
||
Precedence table (low -> high):
|
||
|
||
| Level | Operators |
|
||
|-------|-----------|
|
||
| 1 | `\|` (OR), `or` |
|
||
| 2 | `xor` (XOR keyword) |
|
||
| 3 | `&` (AND), `and` |
|
||
| 4 | `<<`, `>>`, `>>>`, `rol`, `ror` |
|
||
| 5 | `+`, `-` |
|
||
| 6 | `*`, `/`, `%` |
|
||
| 7 | `^`, `**` (exponentiation, always) |
|
||
| 8 | Unary `-`, `~` (NOT), `not` |
|
||
| 9 | Function calls, parentheses |
|
||
|
||
**`^` is always exponentiation.** There is no mode-dependent operator
|
||
overloading. XOR is a keyword (`xor`), consistent with the existing
|
||
`rol`/`ror` keyword operators. Bitwise ops have both symbolic and
|
||
keyword forms: `&`/`and`, `|`/`or`, `~`/`not`. XOR is keyword-only
|
||
(`xor`) since `^` is reserved for power. Every operator means the same
|
||
thing in standard and programmer modes.
|
||
|
||
**No implicit multiplication**: adjacency (e.g. `2pi`) is an error;
|
||
use explicit `*`. Spaces within hex/oct/bin literals are separators
|
||
(e.g. `0xFF FF`).
|
||
|
||
### 2.5 Evaluation
|
||
|
||
The evaluator maintains an `Environment`:
|
||
|
||
```zig
|
||
pub const Environment = struct {
|
||
mode: Mode,
|
||
variables: std.StringHashMap(Value),
|
||
history: std.ArrayList(HistoryEntry),
|
||
programmer_config: ProgrammerConfig,
|
||
|
||
pub const Mode = enum { standard, programmer, financial };
|
||
|
||
pub const ProgrammerConfig = struct {
|
||
bit_width: BitWidth = .bits64,
|
||
signedness: Signedness = .signed,
|
||
display_endian: Endianness = .little,
|
||
};
|
||
};
|
||
```
|
||
|
||
Standard mode evaluates to `f64`. Programmer mode evaluates to `Integer` (exact, truncated to bit width). Financial functions return `f64` with optional step-by-step breakdown.
|
||
|
||
### 2.6 Number Display Formatting
|
||
|
||
The engine provides raw values; frontends apply display formatting. The engine includes a formatting module that produces display strings and raw (clipboard-friendly) strings separately.
|
||
|
||
#### Decimal Formatting
|
||
|
||
| Context | Display | Clipboard |
|
||
|---------|---------|-----------|
|
||
| Standard result | `4,294,967,295` | `4294967295` |
|
||
| Financial result | `$1,234,567.89` | `1234567.89` |
|
||
| Large decimals | `4,294,967,295` (never scientific unless > 15 digits of precision) | `4294967295` |
|
||
|
||
**Scientific notation threshold**: Only use scientific notation when the number exceeds what can be meaningfully displayed in decimal (> 15 significant digits, or absolute value > 10^15 or < 10^-15). Otherwise, always show full decimal with commas. This is a deliberate departure from calculators that jump to scientific notation at 10+ digits.
|
||
|
||
#### Programmer Mode Formatting
|
||
|
||
Values are displayed with visual grouping separators to aid readability, but clipboard copies the raw value without separators.
|
||
|
||
| Base | Display (value view) | Display (memory view) | Clipboard |
|
||
|------|---------------------|----------------------|-----------|
|
||
| Hex | `FF FF FF FF` | `FF FF FF FF` | `0xFFFFFFFF` |
|
||
| Bin | `1111 1111 1111 1111 1111 1111 1111 1111` | same | `0b11111111111111111111111111111111` |
|
||
| Oct | `0o37_777_777_777` | - | `0o37777777777` |
|
||
| Dec | `4,294,967,295` | - | `4294967295` |
|
||
|
||
Rationale for hex grouping:
|
||
- **Display uses space per byte** (`FF FF FF FF`): matches hex editors, debuggers, `xxd` output. Consistent with binary's nibble-space grouping. Universally readable.
|
||
- These are two different views of the same data: the *value* (what you'd write in source code) vs. the *memory* (how it sits in RAM). For this calculator, both use the same space-separated format.
|
||
|
||
Binary always groups by nibble (4 bits) with spaces - universally expected by programmers.
|
||
|
||
#### Formatting Data Structure
|
||
|
||
```zig
|
||
pub const FormattedValue = struct {
|
||
/// Human-readable display string (with commas, underscores, spaces)
|
||
display: []const u8,
|
||
/// Machine/clipboard string (raw, no separators, includes prefix)
|
||
raw: []const u8,
|
||
};
|
||
```
|
||
|
||
All frontends:
|
||
- Show `display` in the UI
|
||
- Copy `raw` to clipboard on long-press (Android) or yank (TUI)
|
||
|
||
---
|
||
|
||
## 3. Struct Layout Engine
|
||
|
||
### 3.1 Mini-DSL Grammar
|
||
|
||
```
|
||
struct_def := "struct" "{" field_list "}"
|
||
field_list := (field ";")*
|
||
field := type_name identifier
|
||
type_name := "u8" | "u16" | "u32" | "u64"
|
||
| "i8" | "i16" | "i32" | "i64"
|
||
| "f32" | "f64" | "bool" | "ptr"
|
||
| "[" number "]" type_name // arrays
|
||
identifier := [a-zA-Z_][a-zA-Z0-9_]*
|
||
```
|
||
|
||
### 3.2 ABI Profile Interface
|
||
|
||
```zig
|
||
pub const AbiProfile = struct {
|
||
name: []const u8,
|
||
pointer_size: u8, // bytes
|
||
max_alignment: u8, // maximum alignment enforced
|
||
|
||
/// Given a field type, return its (size, alignment) in bytes
|
||
field_layout: *const fn (field_type: FieldType) -> struct { size: u8, alignment: u8 },
|
||
|
||
/// Struct-level alignment (usually max of all field alignments)
|
||
struct_alignment: *const fn (fields: []const FieldInfo) -> u8,
|
||
|
||
/// Whether tail padding is added to round up to struct alignment
|
||
tail_padding: bool,
|
||
};
|
||
|
||
// Built-in profiles
|
||
pub const x86_64_sysv = AbiProfile{ ... }; // default
|
||
pub const x86_64_win64 = AbiProfile{ ... }; // future
|
||
pub const packed = AbiProfile{ ... }; // no padding
|
||
```
|
||
|
||
### 3.3 Layout Computation Algorithm
|
||
|
||
```
|
||
function compute_layout(fields, abi):
|
||
offset = 0
|
||
struct_align = 1
|
||
result_fields = []
|
||
|
||
for each field in fields:
|
||
(size, align) = abi.field_layout(field.type)
|
||
struct_align = max(struct_align, align)
|
||
|
||
# Insert padding for alignment
|
||
padding = (align - (offset % align)) % align
|
||
if padding > 0:
|
||
emit_padding(offset, padding)
|
||
offset += padding
|
||
|
||
# Place field
|
||
emit_field(field.name, field.type, offset, size)
|
||
offset += size
|
||
|
||
# Tail padding
|
||
if abi.tail_padding:
|
||
tail = (struct_align - (offset % struct_align)) % struct_align
|
||
if tail > 0:
|
||
emit_padding(offset, tail)
|
||
offset += tail
|
||
|
||
return StructLayout {
|
||
total_size: offset,
|
||
alignment: struct_align,
|
||
fields: result_fields,
|
||
}
|
||
```
|
||
|
||
### 3.4 Layout Result Structure
|
||
|
||
```zig
|
||
pub const StructLayoutResult = struct {
|
||
total_size: usize,
|
||
alignment: u8,
|
||
fields: []const LayoutField,
|
||
bytes: []const ByteInfo, // byte-by-byte map for visualization
|
||
abi_name: []const u8,
|
||
};
|
||
|
||
pub const LayoutField = struct {
|
||
name: []const u8,
|
||
type_name: []const u8,
|
||
offset: usize,
|
||
size: usize,
|
||
alignment: u8,
|
||
padding_before: usize,
|
||
};
|
||
|
||
pub const ByteInfo = union(enum) {
|
||
field: struct { field_index: usize, byte_within_field: usize },
|
||
padding: struct { before_field_index: usize },
|
||
tail_padding: void,
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Unit Conversion Engine
|
||
|
||
### 4.1 Architecture
|
||
|
||
Units are organized by category. Each category has a canonical base unit. All conversions go through the base unit (from -> base -> to), except temperature which uses formula-based conversion.
|
||
|
||
```zig
|
||
pub const UnitCategory = enum {
|
||
length, mass, temperature, time, digital_storage,
|
||
speed, area, volume, energy, pressure, data_rate, angle,
|
||
};
|
||
|
||
pub const UnitDef = struct {
|
||
name: []const u8, // canonical name (e.g., "km")
|
||
aliases: []const []const u8, // alternatives (e.g., "kilometer", "kilometers")
|
||
category: UnitCategory,
|
||
/// Conversion to base unit: value_in_base = value * factor + offset
|
||
/// For most units, offset = 0. Temperature uses both.
|
||
to_base_factor: f64,
|
||
to_base_offset: f64,
|
||
from_base_factor: f64,
|
||
from_base_offset: f64,
|
||
};
|
||
```
|
||
|
||
### 4.2 Conversion Resolution
|
||
|
||
The parser recognizes the pattern `<expr> <unit> to <unit>`:
|
||
- Tokenizer identifies known unit names after a numeric expression
|
||
- `to` keyword triggers conversion mode
|
||
- Both units must be in the same category (error otherwise)
|
||
|
||
For expressions like `5 kg + 3 lb`, the parser:
|
||
1. Identifies `5 kg` as a unit-qualified value
|
||
2. Sees `+ 3 lb` - converts `3 lb` to `kg` (left-hand unit wins)
|
||
3. Evaluates as `5 + 1.36078 = 6.36078 kg`
|
||
|
||
### 4.3 Adding New Units
|
||
|
||
New units require only a table entry - no parser changes:
|
||
|
||
```zig
|
||
// In units/length.zig
|
||
pub const length_units = [_]UnitDef{
|
||
.{ .name = "m", .aliases = &.{"meter", "meters"}, .to_base_factor = 1.0, ... },
|
||
.{ .name = "km", .aliases = &.{"kilometer", "kilometers"}, .to_base_factor = 1000.0, ... },
|
||
.{ .name = "mi", .aliases = &.{"mile", "miles"}, .to_base_factor = 1609.344, ... },
|
||
// ... add new entries here
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Financial Module
|
||
|
||
### 5.1 CAGR
|
||
|
||
```
|
||
CAGR = (end_value / start_value) ^ (1 / periods) - 1
|
||
```
|
||
|
||
### 5.2 TVM (Time Value of Money)
|
||
|
||
The five TVM variables: N (periods), I/Y (interest rate per period), PV (present value), PMT (payment), FV (future value).
|
||
|
||
Relationship:
|
||
```
|
||
PV * (1 + r)^N + PMT * [((1 + r)^N - 1) / r] + FV = 0
|
||
```
|
||
where `r = I/Y / 100`.
|
||
|
||
Solving for the unknown variable:
|
||
- **FV, PV, PMT**: algebraic (closed-form)
|
||
- **N**: logarithmic solution
|
||
- **I/Y**: Newton-Raphson iterative solver (no closed form)
|
||
|
||
---
|
||
|
||
## 6. C API (for Android/FFI)
|
||
|
||
```zig
|
||
// c_api.zig - extern "C" exports
|
||
//
|
||
// SAFETY: All string inputs use pointer + length (not null-terminated).
|
||
// This avoids buffer overread vulnerabilities and allows embedded nulls.
|
||
// All returned strings include a length - caller must free with tally_result_free().
|
||
|
||
pub const CalcString = extern struct {
|
||
ptr: [*]const u8,
|
||
len: usize,
|
||
};
|
||
|
||
pub const CalcResult = extern struct {
|
||
/// JSON-encoded result. Null if error.
|
||
json_ptr: ?[*]u8,
|
||
json_len: usize,
|
||
/// Error message if json_ptr is null.
|
||
error_ptr: ?[*]u8,
|
||
error_len: usize,
|
||
};
|
||
|
||
/// Evaluate an expression, return JSON result.
|
||
/// Caller must free the result with tally_result_free().
|
||
export fn tally_eval(
|
||
expr_ptr: [*]const u8,
|
||
expr_len: usize,
|
||
mode: c_int, // 0=standard, 1=programmer, 2=financial
|
||
config_ptr: ?[*]const u8, // optional JSON config (null if none)
|
||
config_len: usize, // 0 if no config
|
||
) callconv(.c) CalcResult;
|
||
|
||
/// Compute struct layout, return JSON result.
|
||
export fn tally_struct_layout(
|
||
def_ptr: [*]const u8,
|
||
def_len: usize,
|
||
abi_ptr: [*]const u8, // "sysv", "win64", "packed"
|
||
abi_len: usize,
|
||
endian: c_int, // 0=little, 1=big
|
||
) callconv(.c) CalcResult;
|
||
|
||
/// Convert a value between units, return JSON result.
|
||
export fn tally_convert(
|
||
value: f64,
|
||
from_ptr: [*]const u8,
|
||
from_len: usize,
|
||
to_ptr: [*]const u8,
|
||
to_len: usize,
|
||
) callconv(.c) CalcResult;
|
||
|
||
/// Free a CalcResult's buffers.
|
||
export fn tally_result_free(result: *CalcResult) callconv(.c) void;
|
||
|
||
/// Get version string (static lifetime, do not free).
|
||
export fn tally_version(out_len: *usize) callconv(.c) [*]const u8;
|
||
```
|
||
|
||
Design notes:
|
||
- **No null-terminated strings.** All inputs take `(ptr, len)` pairs. This prevents buffer overread, allows binary data in expressions if ever needed, and is the idiomatic C pattern for length-known strings.
|
||
- **`CalcResult` struct** bundles success/error in a single return. Caller checks `json_ptr != null` for success.
|
||
- **JSON result format** keeps the Android/FFI boundary simple - Kotlin parses JSON natively. On the JNI side, the Kotlin bridge reads `(ptr, len)` into a `ByteArray` and decodes UTF-8.
|
||
|
||
---
|
||
|
||
## 7. CLI Design
|
||
|
||
```
|
||
tally [OPTIONS] <expression>
|
||
tally struct [OPTIONS] <definition | --file path>
|
||
tally convert <value> <from_unit> <to_unit>
|
||
tally cagr <start_value> <end_value> <periods>
|
||
tally tvm [--n N] [--rate R] [--pv PV] [--pmt PMT] [--fv FV]
|
||
|
||
Options:
|
||
-p, --programmer Programmer mode
|
||
-b, --bits <WIDTH> Bit width: 8, 16, 32, 64 (default: 64)
|
||
--base <BASE> Output base: dec, hex, oct, bin, all (default: all)
|
||
--json JSON output
|
||
--abi <ABI> Struct ABI: sysv (default), packed
|
||
--endian <E> Endianness: little (default), big
|
||
-h, --help Show help
|
||
--version Show version
|
||
```
|
||
|
||
### CLI Output Examples
|
||
|
||
Standard:
|
||
```
|
||
$ tally "2^32 - 1"
|
||
4,294,967,295
|
||
```
|
||
|
||
Programmer:
|
||
```
|
||
$ tally -p "0xFF & 0x0F"
|
||
dec(signed): 15
|
||
dec(unsigned): 15
|
||
hex: 0x0000_0000_0000_000F
|
||
oct: 0o17
|
||
bin: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111
|
||
bits: [64]
|
||
```
|
||
|
||
Struct:
|
||
```
|
||
$ tally struct "struct { u32 x; u8 flags; u16 id; u64 ts; }"
|
||
ABI: x86-64 System V | Endian: little
|
||
|
||
Offset Size Field Type Align Padding
|
||
────── ──── ────────── ───── ───── ───────
|
||
0 4 x u32 4
|
||
4 1 flags u8 1
|
||
5 1 (padding) 1 byte
|
||
6 2 id u16 2
|
||
8 8 ts u64 8
|
||
|
||
Total: 16 bytes (alignment: 8, padding: 1 byte, tail: 0)
|
||
|
||
Memory map:
|
||
00: [xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx] x
|
||
04: [ffff ffff] [········] [iiii iiii iiii iiii] flags, pad, id
|
||
08: [tttt tttt tttt tttt tttt tttt tttt tttt ts
|
||
0C: tttt tttt tttt tttt tttt tttt tttt tttt]
|
||
```
|
||
|
||
Unit conversion:
|
||
```
|
||
$ tally convert 100 km miles
|
||
62.1371 miles
|
||
|
||
$ tally "72 F to C"
|
||
22.2222 °C
|
||
|
||
$ tally "5 kg + 3 lb"
|
||
6.36078 kg
|
||
```
|
||
|
||
---
|
||
|
||
## 8. TUI Layout
|
||
|
||
### 8.1 Standard Mode
|
||
```
|
||
┌─ Tally ─────────────────────── [Standard] [Programmer] [Financial] ─┐
|
||
│ │
|
||
│ History: │
|
||
│ 2^10 = 1024 │
|
||
│ sqrt(144) = 12 │
|
||
│ Ans * 2 = 24 │
|
||
│ │
|
||
│ ───────────────────────────────────────────────────────────────── │
|
||
│ > 3 * pi + 1_ │
|
||
│ │
|
||
│ = 10.42477796... │
|
||
│ │
|
||
└───────────────────────────── [d]ec [h]ex [o]ct [b]in ── q:quit ── ?:help┘
|
||
```
|
||
|
||
### 8.2 Programmer Mode
|
||
```
|
||
┌─ Tally ─────────────────────── [Standard] [Programmer] [Financial] [Convert] ─┐
|
||
│ Bit Width: [64] Signed: [yes] Endian: [LE] │
|
||
│ │
|
||
│ ┌─ Bit Pattern (63 -> 0) ─────────────────────────────────────────────────────┐ │
|
||
│ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │ │
|
||
│ │ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 │ │
|
||
│ └─────────────────────────────────────────────────────────────────────────────┘ │
|
||
│ cursor: bit 3 ▲ │
|
||
│ │
|
||
│ Signed: 255 │
|
||
│ Unsigned: 255 │
|
||
│ Hex: 0x0000_0000_0000_00FF │
|
||
│ Oct: 0o377 │
|
||
│ Bin: 0000 0000 0000 0000 0000 0000 1111 1111 │
|
||
│ │
|
||
│ > 0xFF & 0x0F_ │
|
||
│ = 15 │
|
||
│ │
|
||
└──── arrows:nav Space:toggle w:width s:sign e:endian y:yank -- Tab:mode ----┘
|
||
```
|
||
|
||
**NOTE: The wireframe above (8.2) is superseded by the editable-fields
|
||
design below. Keeping the old wireframe for reference during transition.**
|
||
|
||
#### 8.2.1 Programmer Mode - Editable Fields (REVISED DESIGN)
|
||
|
||
The programmer mode is an **editable multi-field form** where every
|
||
representation of the value is live-editable. A change in any field
|
||
instantly updates all others. Closer to integer.exposed than a
|
||
traditional calculator with a prompt.
|
||
|
||
Fields (navigable with Up/Down):
|
||
1. Bit grid - arrow keys move, Space toggles individual bits
|
||
2. DEC(s) - editable signed decimal
|
||
3. DEC(u) - editable unsigned decimal
|
||
4. HEX - editable hex (space-separated bytes: FF 00 AB CD)
|
||
5. ASCII - read-only (derived) view of the bytes, one glyph per byte,
|
||
MSB first, aligned column-for-column under the HEX row directly above
|
||
it. Printable bytes (0x20-0x7E) show as themselves; every other byte
|
||
shows as `.`, matching the `xxd` hex-dump convention (e.g. `.ELF`,
|
||
`...ascii`). Not part of the Up/Down focus cycle since it has nothing
|
||
to edit.
|
||
6. OCT - editable octal
|
||
7. BIN - editable binary (nibble-separated: 1111 0000)
|
||
8. Expression - full expression evaluator (& | ^ ~ << >> etc.)
|
||
|
||
Navigation:
|
||
- Up/Down moves focus between fields
|
||
- When a text field is focused, typing replaces its content;
|
||
value updates on Enter (or live as you type - TBD)
|
||
- When bit grid is focused: left/right/up/down navigate, Space toggles
|
||
- Tab cycles between Standard/Programmer mode
|
||
- Ctrl-W cycles bit width (8/16/32/64/128)
|
||
- Ctrl-E toggles endianness display (LE/BE)
|
||
|
||
Bit width is a non-destructive display lens:
|
||
- The stored value keeps full precision (u128). Cycling the width never
|
||
truncates it, so narrowing to a smaller width and then widening again
|
||
restores the hidden upper bits.
|
||
- The DEC/HEX/OCT/BIN/ASCII rows and the bit grid show the value masked to
|
||
the current width.
|
||
- While the stored value has bits set beyond the current width, a warning
|
||
line is shown ("value exceeds N bits - showing low bits (widen to
|
||
restore)"). It is a warning, not an error: the value is still usable.
|
||
- Explicit value edits (typing a decimal digit, toggling a bit, editing a
|
||
hex/oct/bin digit) commit to the current width, clearing any hidden upper
|
||
bits. Only width cycling is non-destructive; deliberately editing at a
|
||
narrow width means you are now working at that width.
|
||
|
||
Endianness:
|
||
- Toggle with Ctrl-E; header shows current setting (`Endian: LE` / `Endian: BE`)
|
||
- Default: big-endian. The HEX row's primary job is to show the value in hex,
|
||
so by default it reads MSB-first and matches DEC/OCT/BIN. Little-endian is
|
||
the opt-in "how it sits in x86-64 memory" view.
|
||
- Affects: HEX byte display order and ASCII byte order only. In LE the display
|
||
bytes are reversed (e.g. 0xDEADBEEF -> `EF BE AD DE`).
|
||
- Does NOT affect: the underlying value, DEC, OCT, or BIN display, nor the
|
||
clipboard `raw` form of HEX (which stays the canonical MSB-first number).
|
||
- Rationale for BE default (revised from an earlier LE default): defaulting to
|
||
LE would reverse the HEX row so it no longer matches the number shown in the
|
||
DEC/BIN rows directly beside it, which is surprising in a calculator context.
|
||
|
||
Text/ASCII literals:
|
||
- Single-quoted strings in expressions: `'ELF'`, `'RIFF'`, `'a'`
|
||
- Each character becomes one byte packed into the integer
|
||
- Packing order follows endianness setting:
|
||
- BE: first char -> most significant used byte (`'AB'` = 0x4142)
|
||
- LE: first char -> least significant byte (`'AB'` = 0x4241)
|
||
- Max chars = bit_width / 8 (1 for 8-bit, 2 for 16, 4 for 32, 8 for 64)
|
||
- Non-ASCII characters are an error
|
||
- Useful for: magic numbers (`'ELF'`), protocol tags, inspecting
|
||
how string bytes pack into integer registers
|
||
|
||
IEEE 754 float interpretation:
|
||
- A sub-view within programmer mode (toggle with a key, e.g. Ctrl-F)
|
||
- Reinterprets the current bit pattern as an IEEE 754 float
|
||
- Supported formats:
|
||
- float32 (1 sign + 8 exponent + 23 significand, bias 127)
|
||
- float64 (1 sign + 11 exponent + 52 significand, bias 1023)
|
||
- Bit grid color-codes the three fields:
|
||
- Sign bit: one color (e.g. pink)
|
||
- Exponent: another color (e.g. cyan)
|
||
- Significand: another color (e.g. green)
|
||
- Display below the bit grid shows:
|
||
- Decimal value (exact)
|
||
- Formula: (-1)^s * 2^(e-bias) * 1.mantissa
|
||
- Classification: normal, denormal, zero, infinity, NaN
|
||
- ULP: delta to next representable value
|
||
- Raw fields: sign=0, exp=127 (unbiased: 0), frac=0x000000
|
||
- Input: can type a decimal float value (e.g. `3.14`) and it
|
||
shows the closest representable bit pattern, noting if rounding
|
||
occurred
|
||
- Toggling bits in the grid updates the float interpretation live
|
||
- Does NOT change bit_width setting - float32 uses 32-bit view,
|
||
float64 uses 64-bit view (auto-switches width when toggling
|
||
float format)
|
||
|
||
### 8.3 Struct Visualizer (sub-view of Programmer Mode)
|
||
```
|
||
┌─ Tally ────────────────── [Programmer > Struct Layout] ─────────────┐
|
||
│ ABI: x86-64 SysV Endian: LE │
|
||
│ │
|
||
│ Definition: │
|
||
│ struct { u32 x; u8 flags; u16 id; u64 ts; } │
|
||
│ │
|
||
│ ┌─ Memory Map ─────────────────────────────────────────────────────┐ │
|
||
│ │ 00 01 02 03 │ 04 │ 05 │ 06 07 │ 08 09 0A 0B 0C 0D 0E 0F │ │ │
|
||
│ │ ████████████ │ ▓▓ │ ░░ │ ▒▒▒▒▒ │ ░░░░░░░░░░░░░░░░░░░░░░░ │ │ │
|
||
│ │ x │flag│pad │ id │ ts │ │ │
|
||
│ └───────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Field Type Offset Size Align Pad Before │
|
||
│ x u32 0 4 4 0 │
|
||
│ flags u8 4 1 1 0 │
|
||
│ id u16 6 2 2 1 │
|
||
│ ts u64 8 8 8 0 │
|
||
│ │
|
||
│ Total: 16 bytes | Alignment: 8 | Padding: 1 byte (6.25%) │
|
||
│ Packed would be: 15 bytes │
|
||
└──── a:abi e:endian Enter:edit struct Esc:back ─────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Android UI Design
|
||
|
||
The Android app uses a fundamentally different interaction model from the TUI. Where the TUI is keyboard-driven with a prompt, Android is **touch-first with purpose-built input surfaces** for each mode. No text-cursor-in-a-prompt paradigm - instead, tappable buttons, interactive grids, and form fields.
|
||
|
||
### 9.1 Navigation & Shell
|
||
|
||
Bottom navigation bar with 4 destinations: **Standard**, **Programmer**, **Financial**, **Convert**.
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ ┌─────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ Mode Content Area │ │
|
||
│ │ │ │
|
||
│ └─────────────────────────────────┘ │
|
||
│ │
|
||
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
|
||
│ 🔢 Standard 💻 Programmer 💰 Finance 🔄 Convert │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### 9.2 Standard Calculator Screen
|
||
|
||
Classic calculator layout. Expression builds at top, result previews live below it, button grid fills the bottom half.
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ Expression area (scrollable) │
|
||
│ ┌─────────────────────────────────┐ │
|
||
│ │ 2 × (3 + 4) │ │ ← expression in natural format
|
||
│ │ = 14 │ │ ← live result preview (dimmed)
|
||
│ └─────────────────────────────────┘ │
|
||
│ │
|
||
│ History peek: last 2-3 results │
|
||
│ ┌─────────────────────────────────┐ │
|
||
│ │ 5^2 = 25 sqrt(144) = 12│ │
|
||
│ └─────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────┐ │
|
||
│ │ sin │ cos │ tan │ ( │ ) │ ⌫ │ │ ← function row (swipeable for more)
|
||
│ ├─────┼─────┼─────┼─────┼─────┼───┤ │
|
||
│ │ 7 │ 8 │ 9 │ ÷ │ ^ │ │ │
|
||
│ ├─────┼─────┼─────┼─────┼─────┤ │ │
|
||
│ │ 4 │ 5 │ 6 │ × │ % │ │ │
|
||
│ ├─────┼─────┼─────┼─────┼─────┤ │ │
|
||
│ │ 1 │ 2 │ 3 │ - │ sqrt│ │ │
|
||
│ ├─────┼─────┼─────┼─────┼─────┤ │ │
|
||
│ │ 0 │ . │ Ans │ + │ = │ │ │
|
||
│ └─────┴─────┴─────┴─────┴─────┘ │ │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
**Key interactions:**
|
||
- Tap buttons to build expression (no keyboard typing required)
|
||
- Long-press `=` to copy result
|
||
- Swipe function row for more functions (log, ln, abs, etc.)
|
||
- Tap history items to recall into expression
|
||
- Pull-down to expand full history view
|
||
|
||
### 9.3 Programmer Mode Screen
|
||
|
||
Split into three vertical zones: **value display**, **bit grid**, and **input buttons**.
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ ┌─── Bit Width ────────────────────┐ │
|
||
│ │ [ 8 ] [ 16 ] [ 32 ] [●64] │ │ ← segmented control
|
||
│ └──────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─── Value Display ────────────────┐ │
|
||
│ │ DEC 4,294,967,295 │ │ ← tappable to switch primary
|
||
│ │ HEX 0xFFFF_FFFF │ │
|
||
│ │ OCT 0o37_777_777_777 │ │
|
||
│ │ BIN 1111 1111 1111 1111 11.. │ │
|
||
│ │ SIGN -1 (signed) │ │
|
||
│ └──────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─── Bit Grid ─────────────────────┐ │
|
||
│ │ 31 30 29 28 │ 27 26 25 24 │ │ ← bit index labels
|
||
│ │ 1 1 1 1 │ 1 1 1 1 │ │ ← tappable cells
|
||
│ │ 23 22 21 20 │ 19 18 17 16 │ │
|
||
│ │ 1 1 1 1 │ 1 1 1 1 │ │
|
||
│ │ 15 14 13 12 │ 11 10 9 8 │ │
|
||
│ │ 1 1 1 1 │ 1 1 1 1 │ │
|
||
│ │ 7 6 5 4 │ 3 2 1 0 │ │
|
||
│ │ 1 1 1 1 │ 1 1 1 1 │ │
|
||
│ └──────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─── Operators ────────────────────┐ │
|
||
│ │ AND│ OR │XOR │NOT │ << │ >> │ROL │ │ ← bitwise op buttons
|
||
│ ├────┼────┼────┼────┼────┼────┼────┤ │
|
||
│ │ A │ B │ C │ D │ E │ F │ │ │ ← hex input
|
||
│ ├────┼────┼────┼────┼────┼────┤ │ │
|
||
│ │ 7 │ 8 │ 9 │ 0x│ 0b│CLR │ │ │ ← digits + base prefix
|
||
│ ├────┼────┼────┼────┼────┼────┤ │ │
|
||
│ │ 4 │ 5 │ 6 │ + │ - │ = │ │ │
|
||
│ ├────┼────┼────┼────┼────┼────┤ │ │
|
||
│ │ 1 │ 2 │ 3 │ × │ ÷ │ ⌫ │ │ │
|
||
│ └────┴────┴────┴────┴────┴────┘ │ │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
**Key interactions:**
|
||
- Tap any bit in the grid to toggle it - all displays update instantly
|
||
- Haptic pulse on bit toggle
|
||
- Tap DEC/HEX/OCT/BIN label to make that the primary input base
|
||
- Bit grid scrolls horizontally for 64-bit (or collapses to 4 rows of 16)
|
||
- FAB or toolbar button for "Struct Layout" sub-screen
|
||
|
||
### 9.4 Struct Layout Screen (Programmer sub-screen)
|
||
|
||
Accessed via a button within Programmer mode. Uses a **form-based struct builder** rather than raw text DSL (though raw text input is available via a toggle).
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ ← Back to Programmer │
|
||
│ │
|
||
│ ┌─── ABI / Settings ──────────────┐ │
|
||
│ │ ABI: [x86-64 SysV ▾] Endian: [LE ▾]│
|
||
│ └──────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─── Fields ───────────────────────┐ │
|
||
│ │ ┌────────┬──────────┬─────────┐ │ │
|
||
│ │ │ Type ▾ │ Name │ ⊖ │ │ │ ← each row is a field
|
||
│ │ ├────────┼──────────┼─────────┤ │ │
|
||
│ │ │ u32 │ x │ ⊖ │ │ │
|
||
│ │ │ u8 │ flags │ ⊖ │ │ │
|
||
│ │ │ u16 │ id │ ⊖ │ │ │
|
||
│ │ │ u64 │ ts │ ⊖ │ │ │
|
||
│ │ └────────┴──────────┴─────────┘ │ │
|
||
│ │ [ + Add Field ] │ │
|
||
│ │ [ ✏️ Raw DSL input toggle ] │ │
|
||
│ └──────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─── Memory Map ───────────────────┐ │
|
||
│ │ ██████████ ▓▓ ░░ ▒▒▒▒ ░░░░░░░░░ │ │ ← color-coded byte bar
|
||
│ │ x(4) fl p id ts(8) │ │ ← labels below
|
||
│ └──────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─── Summary ──────────────────────┐ │
|
||
│ │ Total: 16 bytes Align: 8 │ │
|
||
│ │ Padding: 1 byte (6.25%) │ │
|
||
│ │ Packed: 15 bytes │ │
|
||
│ └──────────────────────────────────┘ │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
**Key interactions:**
|
||
- Type dropdown for each field (Material exposed dropdown menu)
|
||
- Swipe-to-delete or tap ⊖ to remove a field
|
||
- Drag handles to reorder fields (see padding change in real-time)
|
||
- Memory map is a custom Canvas composable with tappable segments (tap a segment -> highlights field info)
|
||
- Toggle for raw DSL text input (for copy-paste from code)
|
||
- Share button: export struct layout as text or image
|
||
|
||
### 9.5 Financial Mode Screen
|
||
|
||
Card-based interface with function selector at top.
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ ┌─── Function ────────────────────┐ │
|
||
│ │ [●CAGR] [TVM] [Compound] [PV] │ │ ← scrollable chip row
|
||
│ └─────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─── CAGR Inputs ────────────────┐ │
|
||
│ │ │ │
|
||
│ │ Start Value │ │
|
||
│ │ ┌─────────────────────────┐ │ │
|
||
│ │ │ 10,000 │ │ │ ← text field
|
||
│ │ └─────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ End Value │ │
|
||
│ │ ┌─────────────────────────┐ │ │
|
||
│ │ │ 25,000 │ │ │
|
||
│ │ └─────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ Periods (years) │ │
|
||
│ │ ┌─────────────────────────┐ │ │
|
||
│ │ │ 5 │ │ │
|
||
│ │ └─────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ [ Calculate ] │ │
|
||
│ └─────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─── Result ─────────────────────┐ │
|
||
│ │ CAGR = 20.11% │ │
|
||
│ │ │ │
|
||
│ │ Formula: │ │
|
||
│ │ (25000/10000)^(1/5) - 1 │ │
|
||
│ │ = 2.5^0.2 - 1 │ │
|
||
│ │ = 0.2011... │ │
|
||
│ └─────────────────────────────────┘ │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
**Key interactions:**
|
||
- Chip row selects function type, input form adapts
|
||
- For TVM: 5 input fields, user leaves one blank -> that's the solve target
|
||
- Compute triggers engine call, result animates in
|
||
- Result card shows formula breakdown (expandable)
|
||
- Long-press result to copy
|
||
|
||
### 9.6 Convert Mode Screen
|
||
|
||
Designed for fast, one-handed unit conversion.
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ ┌─── Category ────────────────────┐ │
|
||
│ │ [Length] [Mass] [Temp] [Time] ▸ │ │ ← scrollable chip row
|
||
│ └─────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ 100 │ │ ← large editable number
|
||
│ │ kilometers [▾] │ │ ← unit selector dropdown
|
||
│ │ │ │
|
||
│ │ ⇅ (swap) │ │ ← tap to swap from/to
|
||
│ │ │ │
|
||
│ │ 62.1371 │ │ ← live result (updates as you type)
|
||
│ │ miles [▾] │ │ ← unit selector dropdown
|
||
│ │ │ │
|
||
│ └─────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─── Keypad ─────────────────────┐ │
|
||
│ │ 7 │ 8 │ 9 │ │ │
|
||
│ ├─────┼─────┼─────┤ ⌫ │ │
|
||
│ │ 4 │ 5 │ 6 │ │ │
|
||
│ ├─────┼─────┼─────┼─────────────┤ │
|
||
│ │ 1 │ 2 │ 3 │ │ │
|
||
│ ├─────┼─────┼─────┤ C │ │
|
||
│ │ 0 │ . │ ± │ │ │
|
||
│ └─────┴─────┴─────┴─────────────┘ │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
**Key interactions:**
|
||
- Conversion updates live as user types (no "calculate" button needed)
|
||
- Swap button rotates from/to units with an animation
|
||
- Unit selectors open a searchable bottom sheet (for categories with many units)
|
||
- Category chips filter which units appear in the selectors
|
||
- Recent conversions remembered
|
||
|
||
### 9.7 Tablet Layout (landscape / large screen)
|
||
|
||
On tablets, modes use a two-pane layout:
|
||
|
||
```
|
||
┌─────────────────────┬──────────────────────────────┐
|
||
│ │ │
|
||
│ Input / Controls │ Results / Visualization │
|
||
│ │ │
|
||
│ (button grid, │ (history, bit grid, │
|
||
│ form fields, │ memory map, formula │
|
||
│ struct editor) │ breakdown) │
|
||
│ │ │
|
||
└─────────────────────┴──────────────────────────────┘
|
||
│ 🔢 Standard 💻 Programmer 💰 Finance 🔄 Convert │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 9.8 Android Architecture (Technical)
|
||
|
||
```
|
||
┌──────────────────────────────────────┐
|
||
│ Compose UI Layer │
|
||
│ ├── StandardScreen │
|
||
│ ├── ProgrammerScreen │
|
||
│ │ ├── BitGridComposable │
|
||
│ │ └── StructVisualizerScreen │
|
||
│ ├── FinancialScreen │
|
||
│ └── ConvertScreen │
|
||
├──────────────────────────────────────┤
|
||
│ ViewModel Layer │
|
||
│ ├── CalculatorViewModel │
|
||
│ ├── ProgrammerViewModel │
|
||
│ ├── FinancialViewModel │
|
||
│ ├── ConvertViewModel │
|
||
│ └── manages state, calls engine │
|
||
├──────────────────────────────────────┤
|
||
│ JNI Bridge (TallyEngine.kt) │
|
||
│ ├── external fun evaluate(...) │
|
||
│ ├── external fun structLayout(...) │
|
||
│ ├── external fun convert(...) │
|
||
│ └── JSON serialization/deser │
|
||
├──────────────────────────────────────┤
|
||
│ Native .so (libtally.so) │
|
||
│ └── C ABI from engine/c_api.zig │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
The JNI bridge sends expression strings down and receives JSON results back. This keeps the boundary thin and debuggable.
|
||
|
||
---
|
||
|
||
## 10. Error Handling Strategy
|
||
|
||
```zig
|
||
pub const CalcError = error{
|
||
// Parser errors
|
||
UnexpectedToken,
|
||
UnmatchedParen,
|
||
InvalidNumber,
|
||
UnknownFunction,
|
||
UnknownVariable,
|
||
|
||
// Evaluation errors
|
||
DivisionByZero,
|
||
Overflow,
|
||
InvalidOperandType,
|
||
DomainError, // e.g., sqrt(-1)
|
||
|
||
// Struct layout errors
|
||
InvalidType,
|
||
InvalidFieldName,
|
||
DuplicateFieldName,
|
||
StructTooLarge,
|
||
|
||
// Financial errors
|
||
InsufficientParameters,
|
||
ConvergenceFailure, // TVM Newton-Raphson didn't converge
|
||
|
||
// System
|
||
OutOfMemory,
|
||
};
|
||
|
||
pub const ErrorInfo = struct {
|
||
err: CalcError,
|
||
message: []const u8,
|
||
position: ?usize, // character position in input where error occurred
|
||
context: []const u8, // snippet of input around error
|
||
};
|
||
```
|
||
|
||
All engine functions return `CalcError!Result`. Frontends translate these into user-facing messages with position highlighting.
|
||
|
||
---
|
||
|
||
## 11. Key Design Decisions
|
||
|
||
| # | Decision | Rationale |
|
||
|---|----------|-----------|
|
||
| 1 | Pratt parser over recursive descent | Easier to extend precedence levels; cleaner for infix with mixed prefix operators |
|
||
| 2 | `u64` storage for all programmer integers | Simplest representation; bit_width used for masking/interpretation, not storage |
|
||
| 3 | JSON over C ABI boundary | Avoids complex struct marshaling; Kotlin/Swift handle JSON natively; small perf cost acceptable for calculator |
|
||
| 4 | Separate parser for struct DSL | Struct grammar is different enough from expressions; keeps both parsers simple |
|
||
| 5 | ABI as interface/vtable | Adding Windows x64 or ARM is implementing one struct; no changes to layout algorithm |
|
||
| 6 | libvaxis for TUI | Most mature Zig TUI; supports Windows/Mac/Linux; active development; used by Ghostty |
|
||
| 7 | `^` always means power; XOR is the `xor` keyword | Avoids mode-dependent operator overloading; every operator means the same thing in both modes; consistent with existing `rol`/`ror` keyword operators |
|
||
| 8 | Statically-linked desktop binaries | Zero dependencies for end user; Zig makes this trivial |
|
||
| 9 | Unit conversion via base-unit normalization | Simple to implement, easy to extend; only temperature needs special case (offset) |
|
||
| 10 | Android: button grids, not text prompts | Touch-first UX; no keyboard needed for basic use; purpose-built surfaces per mode beat a generic expression prompt |
|
||
| 11 | Android: form-based struct builder + raw toggle | Most users won't want to type DSL syntax on a phone; visual builder is more natural; power users can toggle to raw text |
|
||
| 12 | Android: live conversion without "Calculate" button | Conversions are instant; eliminating a tap makes the UX feel responsive and direct |
|