Words and universal macOS installer
This commit is contained in:
75
.github/workflows/ci.yml
vendored
75
.github/workflows/ci.yml
vendored
@@ -122,9 +122,74 @@ jobs:
|
||||
name: ${{ matrix.artifact }}-desktop
|
||||
path: target/${{ matrix.target }}/release/cagire-desktop.exe
|
||||
|
||||
release:
|
||||
universal-macos:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: cagire-macos-*
|
||||
path: artifacts
|
||||
|
||||
- name: Create universal CLI binary
|
||||
run: |
|
||||
lipo -create \
|
||||
artifacts/cagire-macos-x86_64/cagire \
|
||||
artifacts/cagire-macos-aarch64/cagire \
|
||||
-output cagire
|
||||
chmod +x cagire
|
||||
lipo -info cagire
|
||||
|
||||
- name: Create universal app bundle
|
||||
run: |
|
||||
cd artifacts/cagire-macos-aarch64-desktop
|
||||
unzip Cagire.app.zip
|
||||
cd ../cagire-macos-x86_64-desktop
|
||||
unzip Cagire.app.zip
|
||||
cd ../..
|
||||
cp -R artifacts/cagire-macos-aarch64-desktop/Cagire.app Cagire.app
|
||||
lipo -create \
|
||||
artifacts/cagire-macos-x86_64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
|
||||
artifacts/cagire-macos-aarch64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
|
||||
-output Cagire.app/Contents/MacOS/cagire-desktop
|
||||
lipo -info Cagire.app/Contents/MacOS/cagire-desktop
|
||||
zip -r Cagire.app.zip Cagire.app
|
||||
|
||||
- name: Build .pkg installer
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
|
||||
cp -R Cagire.app pkg-root/Applications/
|
||||
cp cagire pkg-root/usr/local/bin/
|
||||
pkgbuild --root pkg-root --identifier com.sova.cagire \
|
||||
--version "$VERSION" --install-location / \
|
||||
"Cagire-${VERSION}-universal.pkg"
|
||||
|
||||
- name: Upload universal CLI
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal
|
||||
path: cagire
|
||||
|
||||
- name: Upload universal app bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-desktop
|
||||
path: Cagire.app.zip
|
||||
|
||||
- name: Upload .pkg installer
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-pkg
|
||||
path: Cagire-*-universal.pkg
|
||||
|
||||
release:
|
||||
needs: [build, universal-macos]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
@@ -141,7 +206,13 @@ jobs:
|
||||
mkdir -p release
|
||||
for dir in artifacts/*/; do
|
||||
name=$(basename "$dir")
|
||||
if [[ "$name" == *-desktop ]]; then
|
||||
if [[ "$name" == "cagire-macos-universal-pkg" ]]; then
|
||||
cp "$dir"/*.pkg release/
|
||||
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
|
||||
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
|
||||
elif [[ "$name" == "cagire-macos-universal" ]]; then
|
||||
cp "$dir/cagire" "release/cagire-macos-universal"
|
||||
elif [[ "$name" == *-desktop ]]; then
|
||||
base="${name%-desktop}"
|
||||
if ls "$dir"/*.deb 1>/dev/null 2>&1; then
|
||||
cp "$dir"/*.deb "release/${base}-desktop.deb"
|
||||
|
||||
@@ -5,7 +5,10 @@ All notable changes to this project will be documented in this file.
|
||||
## [0.0.8] - 2026-06-05
|
||||
|
||||
### Added
|
||||
- Universal macOS `.pkg` installer in CI: combines Intel and Apple Silicon builds into fat binaries via `lipo`, then packages `Cagire.app` and CLI into a single `.pkg` installer. Releases now include `cagire-macos-universal`, `cagire-macos-universal-desktop.app.zip`, and `Cagire-<version>-universal.pkg`.
|
||||
- New themes: **Eden** (dark forest — black background with green-only palette, terminal aesthetic) and **Georges** (Commodore 64 palette on pure black background).
|
||||
- `bounce` word: ping-pong cycle through n items by step runs (e.g., `60 64 67 72 4 bounce` → 60 64 67 72 67 64 60 64...).
|
||||
- `wchoose` word: weighted random selection from n value/weight pairs (e.g., `60 0.6 64 0.3 67 0.1 3 wchoose`). Supports quotations.
|
||||
|
||||
### Improved
|
||||
- Sample library browser: search now shows folder names only (no files) while typing, sorted by fuzzy match score. After confirming search with Enter, folders can be expanded and collapsed normally. Esc clears the search filter before closing the panel. Left arrow on a file collapses the parent folder. Cursor and scroll position stay valid after expand/collapse operations.
|
||||
|
||||
@@ -72,6 +72,8 @@ pub enum Op {
|
||||
Cycle,
|
||||
PCycle,
|
||||
Choose,
|
||||
Bounce,
|
||||
WChoose,
|
||||
ChanceExec,
|
||||
ProbExec,
|
||||
Coin,
|
||||
|
||||
@@ -711,6 +711,61 @@ impl Forth {
|
||||
drain_select_run(count, idx, stack, outputs, cmd)?;
|
||||
}
|
||||
|
||||
Op::Bounce => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count == 0 {
|
||||
return Err("bounce count must be > 0".into());
|
||||
}
|
||||
let idx = if count == 1 {
|
||||
0
|
||||
} else {
|
||||
let period = 2 * (count - 1);
|
||||
let raw = ctx.runs % period;
|
||||
if raw < count { raw } else { period - raw }
|
||||
};
|
||||
drain_select_run(count, idx, stack, outputs, cmd)?;
|
||||
}
|
||||
|
||||
Op::WChoose => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count == 0 {
|
||||
return Err("wchoose count must be > 0".into());
|
||||
}
|
||||
let pairs_needed = count * 2;
|
||||
if stack.len() < pairs_needed {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let start = stack.len() - pairs_needed;
|
||||
let mut values = Vec::with_capacity(count);
|
||||
let mut weights = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let val = stack[start + i * 2].clone();
|
||||
let w = stack[start + i * 2 + 1].as_float()?;
|
||||
if w < 0.0 {
|
||||
return Err("wchoose: negative weight".into());
|
||||
}
|
||||
values.push(val);
|
||||
weights.push(w);
|
||||
}
|
||||
stack.truncate(start);
|
||||
let total: f64 = weights.iter().sum();
|
||||
if total <= 0.0 {
|
||||
return Err("wchoose: total weight must be > 0".into());
|
||||
}
|
||||
let threshold: f64 = self.rng.lock().gen::<f64>() * total;
|
||||
let mut cumulative = 0.0;
|
||||
let mut selected_idx = count - 1;
|
||||
for (i, &w) in weights.iter().enumerate() {
|
||||
cumulative += w;
|
||||
if threshold < cumulative {
|
||||
selected_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let selected = values.swap_remove(selected_idx);
|
||||
select_and_run(selected, stack, outputs, cmd)?;
|
||||
}
|
||||
|
||||
Op::ChanceExec | Op::ProbExec => {
|
||||
let threshold = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
|
||||
@@ -66,6 +66,8 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"cycle" => Op::Cycle,
|
||||
"pcycle" => Op::PCycle,
|
||||
"choose" => Op::Choose,
|
||||
"bounce" => Op::Bounce,
|
||||
"wchoose" => Op::WChoose,
|
||||
"every" => Op::Every,
|
||||
"chance" => Op::ChanceExec,
|
||||
"prob" => Op::ProbExec,
|
||||
|
||||
@@ -103,6 +103,26 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bounce",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(v1..vn n -- selected)",
|
||||
desc: "Ping-pong cycle through n items by step runs",
|
||||
example: "60 64 67 72 4 bounce",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "wchoose",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(v1 w1 v2 w2 ... n -- selected)",
|
||||
desc: "Weighted random pick from n value/weight pairs",
|
||||
example: "60 0.6 64 0.3 67 0.1 3 wchoose",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "always",
|
||||
aliases: &[],
|
||||
|
||||
@@ -176,3 +176,80 @@ fn logrand_requires_positive() {
|
||||
fn rand_equal_bounds() {
|
||||
expect_float("5.0 5.0 rand", 5.0);
|
||||
}
|
||||
|
||||
// bounce
|
||||
|
||||
#[test]
|
||||
fn bounce_sequence() {
|
||||
let expected = [60, 64, 67, 72, 67, 64, 60, 64];
|
||||
for (runs, &exp) in expected.iter().enumerate() {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = run_ctx("60 64 67 72 4 bounce", &ctx);
|
||||
assert_eq!(stack_int(&f), exp, "bounce at runs={}", runs);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounce_single() {
|
||||
for runs in 0..5 {
|
||||
let ctx = ctx_with(|c| c.runs = runs);
|
||||
let f = run_ctx("42 1 bounce", &ctx);
|
||||
assert_eq!(stack_int(&f), 42, "bounce single at runs={}", runs);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounce_zero_count() {
|
||||
expect_error("1 2 3 0 bounce", "bounce count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounce_underflow() {
|
||||
expect_error("1 2 5 bounce", "stack underflow");
|
||||
}
|
||||
|
||||
// wchoose
|
||||
|
||||
#[test]
|
||||
fn wchoose_all_weight_one_item() {
|
||||
for _ in 0..10 {
|
||||
let f = forth_seeded(42);
|
||||
f.evaluate("10 0.0 20 1.0 2 wchoose", &default_ctx())
|
||||
.unwrap();
|
||||
assert_eq!(stack_int(&f), 20);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wchoose_deterministic() {
|
||||
let f1 = forth_seeded(99);
|
||||
let f2 = forth_seeded(99);
|
||||
f1.evaluate("60 0.6 64 0.3 67 0.1 3 wchoose", &default_ctx())
|
||||
.unwrap();
|
||||
f2.evaluate("60 0.6 64 0.3 67 0.1 3 wchoose", &default_ctx())
|
||||
.unwrap();
|
||||
assert_eq!(f1.stack(), f2.stack());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wchoose_zero_count() {
|
||||
expect_error("0 wchoose", "wchoose count must be > 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wchoose_underflow() {
|
||||
expect_error("10 0.5 2 wchoose", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wchoose_negative_weight() {
|
||||
expect_error("10 -0.5 20 1.0 2 wchoose", "negative weight");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wchoose_quotation() {
|
||||
let f = forth_seeded(42);
|
||||
f.evaluate("{ 10 } 0.0 { 20 } 1.0 2 wchoose", &default_ctx())
|
||||
.unwrap();
|
||||
assert_eq!(stack_int(&f), 20);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user