diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..f6fd310
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,282 @@
+name: Build Packages
+
+on:
+ workflow_dispatch:
+ pull_request:
+ push:
+ branches:
+ - main
+ - master
+ tags:
+ - "[0-9]*.[0-9]*.[0-9]*"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ windows-installer:
+ name: Windows installer
+ runs-on: windows-latest
+ timeout-minutes: 45
+ defaults:
+ run:
+ shell: pwsh
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Audit runner image
+ run: |
+ "ImageOS=$env:ImageOS"
+ "ImageVersion=$env:ImageVersion"
+ Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion, OsHardwareAbstractionLayer
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.26.2"
+ check-latest: true
+ cache: true
+
+ - name: Set up MSYS2
+ uses: msys2/setup-msys2@v2
+ with:
+ update: true
+ path-type: inherit
+ install: >-
+ mingw-w64-x86_64-gcc
+
+ - name: Set up Zig
+ uses: goto-bus-stop/setup-zig@v2
+ with:
+ version: "0.17.0-dev.215+8c5542bd3"
+
+ - name: Install Windows build tools
+ run: |
+ choco install llvm innosetup --yes --no-progress
+ $env:GITHUB_PATH | ForEach-Object {
+ Add-Content $_ "C:\Program Files\LLVM\bin"
+ Add-Content $_ "C:\msys64\mingw64\bin"
+ Add-Content $_ "C:\Program Files (x86)\Inno Setup 6"
+ }
+
+ - name: Set up GoReleaser
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ install-only: true
+ distribution: goreleaser
+ version: latest
+
+ - name: Audit toolchain
+ run: |
+ go version
+ goreleaser --version
+ clang --version
+ x86_64-w64-mingw32-gcc --version
+ zig version
+ iscc /?
+
+ - name: Test
+ run: |
+ New-Item -ItemType Directory -Force build\.cache\gobuildtmp, build\.cache\gocache | Out-Null
+ $env:TEMP=(Resolve-Path build\.cache\gobuildtmp).Path
+ $env:TMP=$env:TEMP
+ $env:GOTMPDIR=$env:TEMP
+ $env:GOCACHE=(Resolve-Path build\.cache\gocache).Path
+ $env:CGO_ENABLED='1'
+ $env:CC='clang --target=x86_64-w64-windows-gnu'
+ $env:CXX='clang++ --target=x86_64-w64-windows-gnu'
+ $env:GOFLAGS='-ldflags=-extld=x86_64-w64-mingw32-gcc'
+ go test ./...
+
+ - name: Check GoReleaser config
+ run: goreleaser check --config build\release\goreleaser-win-all.yaml
+
+ - name: Build Windows binaries
+ run: |
+ New-Item -ItemType Directory -Force build\.cache\gobuildtmp, build\.cache\gocache | Out-Null
+ $env:TEMP=(Resolve-Path build\.cache\gobuildtmp).Path
+ $env:TMP=$env:TEMP
+ $env:GOTMPDIR=$env:TEMP
+ $env:GOCACHE=(Resolve-Path build\.cache\gocache).Path
+ goreleaser build --snapshot --clean --config build\release\goreleaser-win-all.yaml
+
+ - name: Build Windows installer
+ run: .\build\scripts\build-installer.ps1
+
+ - name: Upload Windows artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: musicalc-windows
+ if-no-files-found: error
+ retention-days: 14
+ path: |
+ build/dist/musicalc_x64.exe
+ build/dist/musicalc_arm64.exe
+ build/dist/installer/*.exe
+ build/dist/artifacts.json
+ build/dist/metadata.json
+
+ linux-amd64-packages:
+ name: Linux AMD64 packages
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Audit runner image
+ run: |
+ echo "ImageOS=${ImageOS}"
+ echo "ImageVersion=${ImageVersion}"
+ lsb_release -a
+ uname -a
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.26.2"
+ check-latest: true
+ cache: true
+
+ - name: Install Linux AMD64 dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install --yes --no-install-recommends \
+ gcc \
+ g++ \
+ libasound2-dev \
+ libgl1-mesa-dev \
+ pkg-config \
+ rpm \
+ xorg-dev
+
+ - name: Set up GoReleaser
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ install-only: true
+ distribution: goreleaser
+ version: latest
+
+ - name: Audit toolchain
+ run: |
+ go version
+ goreleaser --version
+ gcc --version
+ g++ --version
+ pkg-config --version
+ rpm --version
+
+ - name: Test
+ run: go test ./...
+
+ - name: Check GoReleaser config
+ run: goreleaser check --config build/release/goreleaser-linux-amd64.yaml
+
+ - name: Build Linux AMD64 packages
+ run: goreleaser release --snapshot --clean --config build/release/goreleaser-linux-amd64.yaml --skip=publish
+
+ - name: Upload Linux AMD64 artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: musicalc-linux-amd64
+ if-no-files-found: error
+ retention-days: 14
+ path: |
+ build/dist/*.tar.gz
+ build/dist/*.deb
+ build/dist/*.rpm
+ build/dist/artifacts.json
+ build/dist/metadata.json
+
+ linux-arm64-packages:
+ name: Linux ARM64 packages
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Audit runner image
+ run: |
+ echo "ImageOS=${ImageOS}"
+ echo "ImageVersion=${ImageVersion}"
+ lsb_release -a
+ uname -a
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.26.2"
+ check-latest: true
+ cache: true
+
+ - name: Install Linux ARM64 cross dependencies
+ run: |
+ sudo dpkg --add-architecture arm64
+ sudo apt-get update
+ sudo apt-get install --yes --no-install-recommends \
+ gcc-aarch64-linux-gnu \
+ g++-aarch64-linux-gnu \
+ libasound2-dev:arm64 \
+ libgl1-mesa-dev:arm64 \
+ libxcursor-dev:arm64 \
+ libxi-dev:arm64 \
+ libxinerama-dev:arm64 \
+ libxrandr-dev:arm64 \
+ libx11-dev:arm64 \
+ libxxf86vm-dev:arm64 \
+ pkg-config \
+ rpm
+
+ - name: Set up GoReleaser
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ install-only: true
+ distribution: goreleaser
+ version: latest
+
+ - name: Audit toolchain
+ run: |
+ go version
+ goreleaser --version
+ aarch64-linux-gnu-gcc --version
+ aarch64-linux-gnu-g++ --version
+ pkg-config --version
+ rpm --version
+
+ - name: Check GoReleaser config
+ run: goreleaser check --config build/release/goreleaser-linux-arm64.yaml
+
+ - name: Build Linux ARM64 packages
+ run: goreleaser release --snapshot --clean --config build/release/goreleaser-linux-arm64.yaml --skip=publish
+
+ - name: Upload Linux ARM64 artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: musicalc-linux-arm64
+ if-no-files-found: error
+ retention-days: 14
+ path: |
+ build/dist/*.tar.gz
+ build/dist/*.deb
+ build/dist/*.rpm
+ build/dist/artifacts.json
+ build/dist/metadata.json
diff --git a/.gitignore b/.gitignore
index 6a14833..3b45e2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,10 +17,11 @@ musicalc
coverage.*
*.coverprofile
profile.cov
-installer
-installer/*
-dist
-dist/*
+/installer/
+/dist/
+build/dist/
+build/.cache/
+build/installer/out/
# Dependency directories (remove the comment below to include it)
# vendor/
@@ -38,7 +39,9 @@ go.work.sum
musicalc_linux_*
musicalc_win_*
-musicalc.apk
-
fyne-cross
-fyne-cross/*
\ No newline at end of file
+fyne-cross/*
+
+# Legacy local Go build/cache directories
+.gobuildtmp/
+.gocache/
diff --git a/README-INSTALLER.md b/README-INSTALLER.md
deleted file mode 100644
index f9c3761..0000000
--- a/README-INSTALLER.md
+++ /dev/null
@@ -1,108 +0,0 @@
-# Creating Windows Installer
-
-## Prerequisites
-
-1. **Inno Setup**: Download and install from https://jrsoftware.org/isdl.php
-
-## Building the Installer
-
-1. **Build the application first:**
- ```powershell
- go build -ldflags="-s -w" -o musicalc.exe
- ```
-
-2. **Open Inno Setup Compiler**
-
-3. **Open the script:**
- - File → Open → Select `musicalc.iss`
-
-4. **Compile:**
- - Build → Compile (or press F9)
-
-5. **Output:**
- - Installer will be created in `installer/MusiCalc-Setup-1.0.0.exe`
-
-## Customization
-
-Edit `musicalc.iss` to change:
-- `MyAppVersion` - Application version number
-- `MyAppPublisher` - Your name/company
-- `MyAppURL` - Your website/repository URL
-- `AppId` - Unique GUID (keep as-is or generate new one)
-
-## Testing
-
-1. Run the generated installer: `installer/MusiCalc-Setup-1.0.0.exe`
-2. Install to default location or custom directory
-3. Verify desktop shortcut (if selected)
-4. Test the application runs correctly
-5. Uninstall via Windows Settings → Apps
-
-## Distribution
-
-The installer file (`MusiCalc-Setup-1.0.0.exe`) is a single executable that can be distributed to users. It includes:
-- Application executable
-- App icon
-- Start menu shortcuts
-- Optional desktop shortcut
-- Uninstaller
-
-# Creating Linux Packages and Tarballs
-
-GoReleaser automates the creation of `.deb`, `.rpm`, and `.tar.gz` packages for distribution.
-
-1. **Install GoReleaser**
- ```bash
- go install github.com/goreleaser/goreleaser/v2@latest
- ```
-
-2. **Install Package Dependencies*
- See [Build Instructions](README-BUILD.md).
-
-3. **Building packages with goreleaser**
-
- For Linux AMD64:
-
- ```bash
- goreleaser check --config .goreleaser-linux-amd64.yaml
- # for snapshot
- goreleaser release --snapshot --clean --config .goreleaser-linux-amd64.yaml
- # for release
- goreleaser release --clean --config .goreleaser-linux-amd64.yaml --skip=publish
- ```
-
- For Linux ARM64:
-
- ```bash
- goreleaser check --config .goreleaser-linux-arm64.yaml
- # for snapshot
- goreleaser release --snapshot --clean --config .goreleaser-linux-arm64.yaml
- # for release
- goreleaser release --clean --config .goreleaser-linux-arm64.yaml --skip=publish
- ```
-
- For Windows AMD64/ARM64:
- ```bash
- goreleaser check --config .goreleaser-cross-win.yaml
- # for snapshot
- goreleaser release --snapshot --clean --config .goreleaser-cross-win.yaml
- # for release
- goreleaser release --clean --config .goreleaser-cross-win.yaml --skip=publish
- ```
-
-
- This creates packages in the `dist/` directory without requiring a Git tag.
-
-4. **Create a release (requires Git tag)**
- ```bash
- # Create and push a version tag
- git tag -a v0.1.0 -m "Release version 0.1.0"
- git push origin v0.1.0
-
- # Build and publish release using the gorelease release commands above
- ```
-
-**Generated artifacts**:
-- `.tar.gz` archive with binary, install script, icon, and desktop entry
-- `.deb` package for Debian/Ubuntu
-- `.rpm` package for Fedora/RHEL
diff --git a/README.md b/README.md
index e22eb28..3f68f38 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
A cross-platform music calculator application built with Go and Fyne. Provides real-time calculators for tempo, pitch, sampling, and time-stretching operations commonly used in music production and audio engineering.
-This project is heavily inspired by [MusicMath](https://dev.laurentcolson.com/musicmath.html), an excellent tool currently exclusive to the Apple ecosystem. This version aims to bring that same utility to Windows, Linux and Android, with a simpler look and feel. For macOS users looking for a polished commercial solution, MusicMath is highly recommended. MusicMath has long been a reference tool for music-related calculations, and this project draws inspiration from that work.
+This project is heavily inspired by [MusicMath](https://dev.laurentcolson.com/musicmath.html), an excellent tool currently exclusive to the Apple ecosystem. This version aims to bring that same utility to Windows and Linux, with a simpler look and feel. For macOS users looking for a polished commercial solution, MusicMath is highly recommended. MusicMath has long been a reference tool for music-related calculations, and this project draws inspiration from that work.
@@ -96,16 +96,16 @@ This application is provided for informational purposes only and is not guarante
## Documentation
- **[User Guide](README-USERGUIDE.md)** - How to use each calculator
-- **[Build Instructions](README-BUILD.md)** - How to build from source on Windows, Linux, and macOS
-- **[Creating Installers](README-INSTALLER.md)** - Creating Windows installers and Linux packages
-- **[Icon Resources](README-ICONS.md)** - Icon specifications and resources
-- **[Test Instructions](README-TESTS.md)** - How to run tests
+- **[Build Instructions](build/docs/README-BUILD.md)** - How to build from source on Windows, Linux, and macOS
+- **[Creating Installers](build/docs/README-INSTALLER.md)** - Creating Windows installers and Linux packages
+- **[Icon Resources](build/docs/README-ICONS.md)** - Icon specifications and resources
+- **[Test Instructions](build/docs/README-TESTS.md)** - How to run tests
## Requirements
-- Go 1.24.5 or later
+- Go 1.26.2 or later
- GCC/MinGW (for CGO support on Windows)
-- Fyne v2.7.1
+- Fyne v2.7.3
## License
diff --git a/VERSION b/VERSION
deleted file mode 100644
index 7fc2521..0000000
--- a/VERSION
+++ /dev/null
@@ -1 +0,0 @@
-0.8.6
diff --git a/build-android.sh b/build-android.sh
deleted file mode 100755
index 0c55488..0000000
--- a/build-android.sh
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/bin/bash
-
-# Exit immediately if a command exits with a non-zero status
-set -e
-
-# --- 1, 2, 3) Check Environment Variables ---
-check_env_var() {
- if [ -z "${!1}" ]; then
- echo "error: $1 is not set."
- exit 1
- fi
- if [ ! -d "${!1}" ]; then
- echo "error: $1 path does not exist: ${!1}"
- exit 1
- fi
-}
-
-check_env_var "ANDROID_NDK_HOME"
-check_env_var "ANDROID_HOME"
-check_env_var "ANDROID_SDK_ROOT"
-
-# --- 4, 5) Locate zipalign ---
-# Priority: 1. System Path, 2. build-tools (standard), 3. cmdline-tools (user request)
-if command -v zipalign >/dev/null 2>&1; then
- ZIPALIGN_PATH=$(command -v zipalign)
-else
- # Search for the highest version available in build-tools (Standard location)
- # or the user-specified cmdline-tools path
- SEARCH_PATHS=(
- "${ANDROID_HOME}/build-tools"
- "${ANDROID_HOME}/cmdline-tools/latest/bin"
- )
-
- ZIPALIGN_PATH=$(find "${SEARCH_PATHS[@]}" -name zipalign -type f 2>/dev/null | sort -V | tail -n 1)
-fi
-
-if [ -z "$ZIPALIGN_PATH" ] || [ ! -x "$ZIPALIGN_PATH" ]; then
- echo "error: zipalign not found in PATH or Android SDK structure."
- exit 1
-fi
-
-echo "Using zipalign: $ZIPALIGN_PATH"
-
-# --- Build Configuration ---
-export CGO_ENABLED=1
-export CGO_CFLAGS="-O3 -flto=auto -march=armv8-a+crc+crypto"
-# Force 16KB Page Alignment for Android 15+ compatibility
-export CGO_LDFLAGS="-O3 -flto=auto -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384"
-export GOFLAGS="-ldflags=-s -w"
-
-git pull && git checkout -- go.mod && git fetch --tags --force
-
-# --- Execution ---
-echo "Building APK..."
-fyne package --os android/arm64 --id com.github.bzeiss --release -icon icons/appicon.png
-
-# Ensure dist exists
-mkdir -p dist
-
-# Aligning
-echo "Aligning APK to 16KB boundary..."
-mv musicalc.apk dist/musicalc-unaligned.apk
-"$ZIPALIGN_PATH" -f -v -P 16 4 dist/musicalc-unaligned.apk dist/musicalc.apk
-
-# Verify alignment
-"$ZIPALIGN_PATH" -c -v -P 16 4 dist/musicalc.apk
-
-# --- 6) Smarter Alignment Check ---
-echo "Verifying 16KB ($2^{14}$) alignment of internal shared libraries..."
-cd dist
-unzip -p musicalc.apk lib/arm64-v8a/libmusicalc.so > tmp_lib.so
-
-# Extraction of alignment value using awk
-# We check the 'align' value of the LOAD segments
-ALIGNMENT=$(objdump -p tmp_lib.so | grep "LOAD" | awk '{print $NF}' | head -n 1)
-
-if [[ "$ALIGNMENT" == *"2**14"* ]] || [[ "$ALIGNMENT" == *"0x4000"* ]]; then
- echo "SUCCESS: Library is correctly aligned to 16KB ($ALIGNMENT)."
-else
- echo "FAILURE: Library alignment is $ALIGNMENT, expected 2**14 (16384)."
- rm tmp_lib.so
- exit 1
-fi
-
-rm tmp_lib.so
-echo "Build and Verification Complete: dist/musicalc.apk"
\ No newline at end of file
diff --git a/build-win.ps1 b/build-win.ps1
deleted file mode 100644
index 849b7be..0000000
--- a/build-win.ps1
+++ /dev/null
@@ -1,4 +0,0 @@
-$env:CGO_FLAGS="-O3 -flto=auto -march=x86-64-v3"
-$env:CGO_LDFLAGS="-O3 -flto=auto"
-$env:CGO_ENABLED=1
-go build -ldflags="-s -w" -o musicalc.exe
diff --git a/README-BUILD.md b/build/docs/README-BUILD.md
similarity index 64%
rename from README-BUILD.md
rename to build/docs/README-BUILD.md
index 9768ff3..587d3d2 100644
--- a/README-BUILD.md
+++ b/build/docs/README-BUILD.md
@@ -10,9 +10,10 @@
cd musicalc
```
-2. **Install MSYS2** (provides MinGW GCC compiler required for Fyne)
+2. **Install LLVM and MSYS2** (provides clang plus the MinGW linker required for Fyne)
```powershell
winget install -e --id MSYS2.MSYS2
+ winget install -e --id LLVM.LLVM
```
3. **Install GCC via MSYS2**
@@ -32,11 +33,12 @@
- Add new entry: `C:\msys64\mingw64\bin`
- Click OK and restart your terminal
-5. **Verify GCC installation**
+5. **Verify compiler installation**
```powershell
- gcc --version
+ clang --version
+ x86_64-w64-mingw32-gcc --version
```
- Should output GCC version information
+ Both commands should output version information.
6. **Install Go dependencies**
```powershell
@@ -45,19 +47,13 @@
7. **Build the application**
```powershell
- $env:CGO_FLAGS="-O3 -flto=auto -march=x86-64-v3"
- $env:CGO_LDFLAGS="-O3 -flto=auto"
- $env:CGO_ENABLED=1
- go build -ldflags="-s -w" -o musicalc.exe
- ```
- or for a production build without the console:
- ```powershell
- go build -ldflags="-s -w -H=windowsgui" -o musicalc.exe
+ .\build\scripts\build-win.ps1
```
+ The script injects the application version from the current Git tag. If the current commit is not exactly tagged, it injects a local dev version derived from the latest tag and commit.
8. **Run the application**
```powershell
- .\musicalc.exe
+ .\build\dist\musicalc.exe
```
## Linux
@@ -90,8 +86,9 @@
4. **Windows ARM64 Cross-compilation support (requires a more recent Ubuntu Server version for building)**
- Download Zig from: https://ziglang.org/download/
- - Put zig somewere into your environment PATH
- - test by calling "zig" and "zig c++"
+ - Put `zig` somewhere into your environment `PATH`
+ - Test by calling `zig` and `zig c++`
+ - The Windows GoReleaser config stores Zig cache data under `build/.cache/zig-global` and `build/.cache/zig-local` so build-generated files stay inside the project tree.
5. **Install Go dependencies**
```bash
@@ -100,6 +97,7 @@
6. **Build the application for Linux AMD64**
```bash
+ mkdir -p build/dist
export CGO_CFLAGS="-O3 -flto=auto -march=x86-64-v3"
export CGO_LDFLAGS="-O3 -flto=auto"
export CGO_ENABLED=1
@@ -107,10 +105,12 @@
export CXX=g++
export GOOS=linux
export GOARCH=amd64
- go build -ldflags="-s -w" -o musicalc_linux_amd64
+ VERSION="$(git describe --tags --exact-match 2>/dev/null || echo dev-$(git rev-parse --short HEAD))"
+ go build -ldflags="-s -w -X main.version=${VERSION}" -o build/dist/musicalc_linux_amd64
```
7. **Build the application for Linux ARM64**
```bash
+ mkdir -p build/dist
export CGO_CFLAGS="-O3 -flto=auto -march=armv8.4-a+crc+crypto -fomit-frame-pointer"
export CGO_LDFLAGS="-O3 -flto=auto -Wl,--gc-sections"
export CGO_ENABLED=1
@@ -118,11 +118,13 @@
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig
export GOOS=linux
export GOARCH=arm64
- go build -ldflags="-s -w" -o musicalc_linux_arm64
+ VERSION="$(git describe --tags --exact-match 2>/dev/null || echo dev-$(git rev-parse --short HEAD))"
+ go build -ldflags="-s -w -X main.version=${VERSION}" -o build/dist/musicalc_linux_arm64
```
8. **Build the application for Windows AMD64**
```bash
+ mkdir -p build/dist
export CGO_CFLAGS="-O3 -flto=auto -march=x86-64-v3 -m64"
export CGO_LDFLAGS="-O3 -flto=auto"
export CGO_ENABLED=1
@@ -130,46 +132,33 @@
export CXX=x86_64-w64-mingw32-g++
export GOOS=windows
export GOARCH=amd64
- go build -ldflags="-s -w" -o musicalc_win_amd64
+ VERSION="$(git describe --tags --exact-match 2>/dev/null || echo dev-$(git rev-parse --short HEAD))"
+ go build -ldflags="-s -w -X main.version=${VERSION}" -o build/dist/musicalc_win_amd64.exe
```
9. **Build the application for Windows ARM64**
```bash
+ mkdir -p build/dist
export CGO_CFLAGS="-O3 -mcpu=oryon_1 -fomit-frame-pointer"
export CGO_LDFLAGS="-O3"
export CGO_ENABLED=1
export CC="zig cc -target aarch64-windows-gnu"
export CXX="zig c++ -target aarch64-windows-gnu"
+ export ZIG_GLOBAL_CACHE_DIR=build/.cache/zig-global
+ export ZIG_LOCAL_CACHE_DIR=build/.cache/zig-local
export GOOS=windows
export GOARCH=arm64
- go build -ldflags="-s -w" -o musicalc_win_arm64
- ```
-
-10. **Build the application for Android ARM64**
- ```bash
- export ANDROID_NDK_HOME=/path/to/your/android-ndk
- export ANDROID_HOME=/path/to/your/android-sdk
- export ANDROID_SDK_ROOT=$ANDROID_HOME
- export PATH=$PATH:${ANDROID_HOME}/cmdline-tools/latest/bin
- ./build-android.sh
+ VERSION="$(git describe --tags --exact-match 2>/dev/null || echo dev-$(git rev-parse --short HEAD))"
+ go build -ldflags="-s -w -X main.version=${VERSION}" -o build/dist/musicalc_win_arm64.exe
```
- For the Android SDK and NDK, you can use the Android Studio SDK Manager to install them.
+10. **Run the application**
```bash
- sdkmanager --licenses # accept all licenses
- sdkmanager "platform-tools" "build-tools;36.1.0" "platforms;android-36" # choose the most recent version
- ```
-
-11. **Run the application**
- ```bash
- ./musicalc_xxx
+ ./build/dist/musicalc_xxx
```
## Requirements
-- Go 1.24.5 or later
-- GCC/MinGW (for CGO support on Windows)
-- Fyne v2.7.1
-
+- Go 1.26.2 or later
- GCC/MinGW (for CGO support on Windows)
-- Fyne v2.7.1
+- Fyne v2.7.3
diff --git a/README-ICONS.md b/build/docs/README-ICONS.md
similarity index 100%
rename from README-ICONS.md
rename to build/docs/README-ICONS.md
diff --git a/build/docs/README-INSTALLER.md b/build/docs/README-INSTALLER.md
new file mode 100644
index 0000000..76fb8f5
--- /dev/null
+++ b/build/docs/README-INSTALLER.md
@@ -0,0 +1,116 @@
+# Creating Windows Installer
+
+## Prerequisites
+
+1. **Inno Setup**: Download and install from https://jrsoftware.org/isdl.php
+
+## Building the Installer
+
+1. **Build the application first:**
+ ```powershell
+ .\build\scripts\build-win.ps1
+ ```
+
+2. **Open Inno Setup Compiler**
+
+3. **Compile the installer:**
+ ```powershell
+ .\build\scripts\build-installer.ps1
+ ```
+
+ For a release installer, first check out the exact version tag and then run:
+ ```powershell
+ .\build\scripts\build-installer.ps1 -Release
+ ```
+
+4. **Output:**
+ - Installer will be created in `build/dist/installer/`
+
+## Customization
+
+Edit `build/installer/musicalc.iss` to change:
+- `MyAppPublisher` - Your name/company
+- `MyAppURL` - Your website/repository URL
+- `AppId` - Unique GUID (keep as-is or generate new one)
+
+## Testing
+
+1. Run the generated installer from `build/dist/installer/`
+2. Install to default location or custom directory
+3. Verify desktop shortcut (if selected)
+4. Test the application runs correctly
+5. Uninstall via Windows Settings > Apps
+
+## Distribution
+
+The installer file is a single executable that can be distributed to users. It includes:
+- Application executable
+- App icon
+- Start menu shortcuts
+- Optional desktop shortcut
+- Uninstaller
+
+# Creating Linux Packages and Tarballs
+
+GoReleaser automates the creation of `.deb`, `.rpm`, and `.tar.gz` packages for distribution.
+
+1. **Install GoReleaser**
+ ```bash
+ go install github.com/goreleaser/goreleaser/v2@latest
+ ```
+
+2. **Install Package Dependencies**
+ See [Build Instructions](README-BUILD.md).
+
+3. **Building packages with GoReleaser**
+
+ For Linux AMD64:
+
+ ```bash
+ goreleaser check --config build/release/goreleaser-linux-amd64.yaml
+ # for snapshot
+ goreleaser release --snapshot --clean --config build/release/goreleaser-linux-amd64.yaml
+ # for release
+ goreleaser release --clean --config build/release/goreleaser-linux-amd64.yaml --skip=publish
+ ```
+
+ For Linux ARM64:
+
+ ```bash
+ goreleaser check --config build/release/goreleaser-linux-arm64.yaml
+ # for snapshot
+ goreleaser release --snapshot --clean --config build/release/goreleaser-linux-arm64.yaml
+ # for release
+ goreleaser release --clean --config build/release/goreleaser-linux-arm64.yaml --skip=publish
+ ```
+
+ For Windows AMD64/ARM64:
+ ```bash
+ goreleaser check --config build/release/goreleaser-win-all.yaml
+ # for snapshot
+ goreleaser release --snapshot --clean --config build/release/goreleaser-win-all.yaml
+ # for release
+ goreleaser release --clean --config build/release/goreleaser-win-all.yaml --skip=publish
+ ```
+
+ The Windows ARM64 build uses Zig for cross-compilation. The GoReleaser config redirects Zig cache files to `build/.cache/zig-global` and `build/.cache/zig-local`.
+
+ Snapshot builds create packages in the `build/dist/` directory without requiring an exact Git tag. Release builds must be run from an exact version tag.
+
+4. **Create a release (requires Git tag)**
+ ```bash
+ # Create and push tags manually; release tooling does not create or push tags.
+ git tag -a 0.1.0 -m "Release 0.1.0"
+ git push origin 0.1.0
+
+ # Validate the current exact tag and all GoReleaser configs.
+ python utils/release.py --check-only
+
+ # Run one release config. Publishing is skipped unless --publish is supplied.
+ python utils/release.py --release --config build/release/goreleaser-linux-amd64.yaml
+ ```
+
+**Generated artifacts**:
+- `.tar.gz` archive with binary, install script, icon, and desktop entry
+- `.deb` package for Debian/Ubuntu
+- `.rpm` package for Fedora/RHEL
diff --git a/README-TESTS.md b/build/docs/README-TESTS.md
similarity index 100%
rename from README-TESTS.md
rename to build/docs/README-TESTS.md
diff --git a/musicalc.iss b/build/installer/musicalc.iss
similarity index 76%
rename from musicalc.iss
rename to build/installer/musicalc.iss
index 6c22f5c..442e8e5 100644
--- a/musicalc.iss
+++ b/build/installer/musicalc.iss
@@ -2,7 +2,9 @@
; Music Calculator Application - Universal Architecture Version
#define MyAppName "MusiCalc"
-#define MyAppVersion "0.8.6"
+#ifndef MyAppVersion
+ #error MyAppVersion must be supplied by the build command, for example: ISCC.exe /DMyAppVersion=X.Y.Z build\installer\musicalc.iss
+#endif
#define MyAppPublisher "B. Zeiss"
#define MyAppURL "https://github.com/bzeiss/musicalc"
#define MyAppExeName "musicalc.exe"
@@ -19,9 +21,9 @@ AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
; PrivilegesRequired=lowest
-OutputDir=installer
+OutputDir=..\dist\installer
OutputBaseFilename=musicalc-setup-{#MyAppVersion}
-SetupIconFile=icons\appicon.ico
+SetupIconFile=..\..\icons\appicon.ico
Compression=lzma
SolidCompression=yes
WizardStyle=modern
@@ -42,14 +44,14 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
[Files]
; 1. Install AMD64 version on x64 systems
-Source: "dist\musicalc_x64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Check: IsX64; Flags: 64bit ignoreversion
+Source: "..\dist\musicalc_x64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Check: IsX64; Flags: 64bit ignoreversion
; 2. Install ARM64 version on ARM64 systems
-Source: "dist\musicalc_arm64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Check: IsArm64; Flags: 64bit ignoreversion
+Source: "..\dist\musicalc_arm64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}"; Check: IsArm64; Flags: 64bit ignoreversion
; Common files
-Source: "icons\appicon.ico"; DestDir: "{app}"; Flags: ignoreversion
-Source: "icons\appicon.png"; DestDir: "{app}\icons"; Flags: ignoreversion
+Source: "..\..\icons\appicon.ico"; DestDir: "{app}"; Flags: ignoreversion
+Source: "..\..\icons\appicon.png"; DestDir: "{app}\icons"; Flags: ignoreversion
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\appicon.ico"
@@ -68,4 +70,4 @@ end;
function IsX64: Boolean;
begin
Result := (ProcessorArchitecture = paX64);
-end;
\ No newline at end of file
+end;
diff --git a/musicalc-pkg.desktop b/build/packaging/musicalc-pkg.desktop
similarity index 83%
rename from musicalc-pkg.desktop
rename to build/packaging/musicalc-pkg.desktop
index f030e4c..7484dd6 100644
--- a/musicalc-pkg.desktop
+++ b/build/packaging/musicalc-pkg.desktop
@@ -6,4 +6,4 @@ Exec=/usr/bin/musicalc
Icon=musicalc
Terminal=false
Categories=Utility;Audio;
-StartupWMClass=MusiCalc v0.8.6
+StartupWMClass=com.musicalc
diff --git a/musicalc.desktop b/build/packaging/musicalc.desktop
similarity index 84%
rename from musicalc.desktop
rename to build/packaging/musicalc.desktop
index b6ee087..d4d35d4 100644
--- a/musicalc.desktop
+++ b/build/packaging/musicalc.desktop
@@ -6,4 +6,4 @@ Exec=/usr/local/bin/musicalc
Icon=musicalc
Terminal=false
Categories=Utility;Audio;
-StartupWMClass=MusiCalc v0.8.6
+StartupWMClass=com.musicalc
diff --git a/.goreleaser-linux-amd64.yaml b/build/release/goreleaser-linux-amd64.yaml
similarity index 80%
rename from .goreleaser-linux-amd64.yaml
rename to build/release/goreleaser-linux-amd64.yaml
index b12ff2f..d292511 100644
--- a/.goreleaser-linux-amd64.yaml
+++ b/build/release/goreleaser-linux-amd64.yaml
@@ -1,10 +1,6 @@
version: 2
project_name: musicalc
-
-before:
- hooks:
- - git pull && git checkout -- go.mod && git fetch --tags --force
-# - go mod tidy
+dist: build/dist
builds:
- id: linux-amd64
@@ -20,15 +16,17 @@ builds:
- amd64
goamd64: [v3]
ldflags:
- - -s -w
+ - -s -w -X main.version={{ .Version }}
# This creates the .tar.gz bundle
archives:
- files:
- - src: "install.sh"
+ - src: "build/scripts/install.sh"
+ dst: "install.sh"
- src: "icons/appicon.png"
dst: "icon.png"
- - src: "musicalc.desktop"
+ - src: "build/packaging/musicalc.desktop"
+ dst: "musicalc.desktop"
# This creates the .deb and .rpm "Installers"
nfpms:
@@ -46,5 +44,5 @@ nfpms:
dst: /usr/share/icons/hicolor/512x512/apps/musicalc.png
# This creates the Start Menu entry automatically (uses /usr/bin path)
- - src: musicalc-pkg.desktop
+ - src: build/packaging/musicalc-pkg.desktop
dst: /usr/share/applications/musicalc.desktop
diff --git a/.goreleaser-linux-arm64.yaml b/build/release/goreleaser-linux-arm64.yaml
similarity index 81%
rename from .goreleaser-linux-arm64.yaml
rename to build/release/goreleaser-linux-arm64.yaml
index ca10cd0..f8b69b0 100644
--- a/.goreleaser-linux-arm64.yaml
+++ b/build/release/goreleaser-linux-arm64.yaml
@@ -1,10 +1,6 @@
version: 2
project_name: musicalc
-
-before:
- hooks:
- - git pull && git checkout -- go.mod && git fetch --tags --force
-# - go mod tidy
+dist: build/dist
builds:
- id: linux-arm64
@@ -20,15 +16,17 @@ builds:
goarch:
- arm64
ldflags:
- - -s -w
+ - -s -w -X main.version={{ .Version }}
# This creates the .tar.gz bundle
archives:
- files:
- - src: "install.sh"
+ - src: "build/scripts/install.sh"
+ dst: "install.sh"
- src: "icons/appicon.png"
dst: "icon.png"
- - src: "musicalc.desktop"
+ - src: "build/packaging/musicalc.desktop"
+ dst: "musicalc.desktop"
# This creates the .deb and .rpm "Installers"
nfpms:
@@ -46,5 +44,5 @@ nfpms:
dst: /usr/share/icons/hicolor/512x512/apps/musicalc.png
# This creates the Start Menu entry automatically (uses /usr/bin path)
- - src: musicalc-pkg.desktop
+ - src: build/packaging/musicalc-pkg.desktop
dst: /usr/share/applications/musicalc.desktop
diff --git a/.goreleaser-win-all.yaml b/build/release/goreleaser-win-all.yaml
similarity index 65%
rename from .goreleaser-win-all.yaml
rename to build/release/goreleaser-win-all.yaml
index c39f416..1ae59d3 100644
--- a/.goreleaser-win-all.yaml
+++ b/build/release/goreleaser-win-all.yaml
@@ -1,24 +1,20 @@
version: 2
project_name: musicalc
-
-before:
- hooks:
- - git pull && git checkout -- go.mod && git fetch --tags --force
-# - go mod tidy
+dist: build/dist
builds:
- id: win-amd64
env:
- CGO_ENABLED=1
- - CC=x86_64-w64-mingw32-gcc
- - CXX=x86_64-w64-mingw32-g++
- - CGO_CFLAGS=-O3 -flto=auto -march=x86-64-v3 -m64
- - CGO_LDFLAGS=-O3 -flto=auto
+ - CC=clang --target=x86_64-w64-windows-gnu
+ - CXX=clang++ --target=x86_64-w64-windows-gnu
+ - CGO_CFLAGS=-O3 -march=x86-64-v3
+ - CGO_LDFLAGS=-O3
goos: [windows]
goarch: [amd64]
goamd64: [v3]
ldflags:
- - -s -w -H=windowsgui
+ - -s -w -H=windowsgui -extld=x86_64-w64-mingw32-gcc -X main.version={{ .Version }}
binary: musicalc_x64
# This keeps the binary at the top level of the dist folder
no_unique_dist_dir: true
@@ -28,12 +24,14 @@ builds:
- CGO_ENABLED=1
- CC=zig cc -target aarch64-windows-gnu
- CXX=zig c++ -target aarch64-windows-gnu
+ - ZIG_GLOBAL_CACHE_DIR=build/.cache/zig-global
+ - ZIG_LOCAL_CACHE_DIR=build/.cache/zig-local
- CGO_CFLAGS=-O3 -mcpu=oryon_1 -fomit-frame-pointer
- CGO_LDFLAGS=-O3
goos: [windows]
goarch: [arm64]
ldflags:
- - -s -w -H=windowsgui
+ - -s -w -H=windowsgui -X main.version={{ .Version }}
binary: musicalc_arm64
no_unique_dist_dir: true
@@ -44,4 +42,4 @@ builds:
# name_template: "{{ .Binary }}"
# # This prevents GoReleaser from adding default README/LICENSE files
# files:
-# - none*
\ No newline at end of file
+# - none*
diff --git a/build/scripts/build-installer.ps1 b/build/scripts/build-installer.ps1
new file mode 100644
index 0000000..6a76570
--- /dev/null
+++ b/build/scripts/build-installer.ps1
@@ -0,0 +1,42 @@
+param(
+ [switch]$Release
+)
+
+$ErrorActionPreference = "Stop"
+
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$projectRoot = Resolve-Path (Join-Path $scriptDir "..\..")
+$installerScript = Join-Path $projectRoot "build\installer\musicalc.iss"
+
+Push-Location $projectRoot
+try {
+ $version = git describe --tags --exact-match 2>$null
+ if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) {
+ if ($Release) {
+ throw "Release installer builds require HEAD to be exactly on a version tag."
+ }
+
+ $latestTag = git describe --tags --abbrev=0 2>$null
+ $shortCommit = git rev-parse --short HEAD
+ if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($shortCommit)) {
+ $version = "dev"
+ } elseif ([string]::IsNullOrWhiteSpace($latestTag)) {
+ $version = "dev-$shortCommit"
+ } else {
+ $version = "$latestTag-dev-$shortCommit"
+ }
+ }
+
+ if ($Release -and ($version -notmatch '^\d+\.\d+\.\d+$')) {
+ throw "Release tag '$version' is invalid. Expected MAJOR.MINOR.PATCH, for example 0.8.7."
+ }
+
+ iscc "/DMyAppVersion=$version" $installerScript
+ $exitCode = $LASTEXITCODE
+} finally {
+ Pop-Location
+}
+
+if ($exitCode -ne 0) {
+ exit $exitCode
+}
diff --git a/build/scripts/build-win.ps1 b/build/scripts/build-win.ps1
new file mode 100644
index 0000000..f121bb1
--- /dev/null
+++ b/build/scripts/build-win.ps1
@@ -0,0 +1,38 @@
+$ErrorActionPreference = "Stop"
+
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$projectRoot = Resolve-Path (Join-Path $scriptDir "..\..")
+$distDir = Join-Path $projectRoot "build\dist"
+
+New-Item -ItemType Directory -Force $distDir | Out-Null
+
+$env:CGO_ENABLED = "1"
+$env:CC = "clang --target=x86_64-w64-windows-gnu"
+$env:CXX = "clang++ --target=x86_64-w64-windows-gnu"
+$env:CGO_CFLAGS = "-O3 -march=x86-64-v3"
+$env:CGO_LDFLAGS = "-O3"
+
+Push-Location $projectRoot
+try {
+ $version = git describe --tags --exact-match 2>$null
+ if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($version)) {
+ $latestTag = git describe --tags --abbrev=0 2>$null
+ $shortCommit = git rev-parse --short HEAD
+ if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($shortCommit)) {
+ $version = "dev"
+ } elseif ([string]::IsNullOrWhiteSpace($latestTag)) {
+ $version = "dev-$shortCommit"
+ } else {
+ $version = "$latestTag-dev-$shortCommit"
+ }
+ }
+
+ go build -ldflags="-s -w -H=windowsgui -extld=x86_64-w64-mingw32-gcc -X main.version=$version" -o (Join-Path $distDir "musicalc.exe")
+ $exitCode = $LASTEXITCODE
+} finally {
+ Pop-Location
+}
+
+if ($exitCode -ne 0) {
+ exit $exitCode
+}
diff --git a/install.sh b/build/scripts/install.sh
old mode 100755
new mode 100644
similarity index 100%
rename from install.sh
rename to build/scripts/install.sh
diff --git a/go.mod b/go.mod
index c5981fb..221a341 100644
--- a/go.mod
+++ b/go.mod
@@ -1,50 +1,49 @@
module musicalc
-go 1.24.0
-
-toolchain go1.24.5
+go 1.26.2
require (
- fyne.io/fyne/v2 v2.7.1
+ fyne.io/fyne/v2 v2.7.3
github.com/chinenual/go-scala v1.2.0
github.com/faiface/beep v1.1.0
)
require (
- fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect
- github.com/BurntSushi/toml v1.5.0 // indirect
+ fyne.io/systray v1.12.1 // indirect
+ github.com/BurntSushi/toml v1.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.1 // indirect
- github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/fsnotify/fsnotify v1.10.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.2.0 // indirect
- github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
- github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
- github.com/go-text/render v0.2.0 // indirect
- github.com/go-text/typesetting v0.2.1 // indirect
- github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 // indirect
+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260406072232-3ac4aa2bb164 // indirect
+ github.com/go-text/render v0.2.1 // indirect
+ github.com/go-text/typesetting v0.3.4 // indirect
+ github.com/godbus/dbus/v5 v5.2.2 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
- github.com/hack-pad/safejs v0.1.0 // indirect
- github.com/hajimehoshi/oto v0.7.1 // indirect
+ github.com/hack-pad/safejs v0.1.1 // indirect
+ github.com/hajimehoshi/oto v1.0.2 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
- github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
+ github.com/nicksnyder/go-i18n/v2 v2.6.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.4.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.11.1 // indirect
- github.com/yuin/goldmark v1.7.8 // indirect
- golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
- golang.org/x/image v0.24.0 // indirect
- golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
- golang.org/x/net v0.48.0 // indirect
- golang.org/x/sys v0.39.0 // indirect
- golang.org/x/text v0.32.0 // indirect
+ github.com/yuin/goldmark v1.8.2 // indirect
+ golang.org/x/exp/shiny v0.0.0-20260410095643-746e56fc9e2f // indirect
+ golang.org/x/image v0.39.0 // indirect
+ golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b // indirect
+ golang.org/x/net v0.53.0 // indirect
+ golang.org/x/sys v0.43.0 // indirect
+ golang.org/x/text v0.36.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index d482c0a..ddd91e7 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,9 @@
-fyne.io/fyne/v2 v2.7.1 h1:ja7rNHWWEooha4XBIZNnPP8tVFwmTfwMJdpZmLxm2Zc=
-fyne.io/fyne/v2 v2.7.1/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
-fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI=
-fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
-github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
-github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+fyne.io/fyne/v2 v2.7.3 h1:xBT/iYbdnNHONWO38fZMBrVBiJG8rV/Jypmy4tVfRWE=
+fyne.io/fyne/v2 v2.7.3/go.mod h1:gu+dlIcZWSzKZmnrY8Fbnj2Hirabv2ek+AKsfQ2bBlw=
+fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ=
+fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/chinenual/go-scala v1.2.0 h1:FA9qlw2eJQA+jyWgJH9oQm+mGwewN93387pm/kuy3rM=
github.com/chinenual/go-scala v1.2.0/go.mod h1:Uc09g2xLaUHqx3sK+1h3E0R3hG+ZFMNkVVDtEJhi7F0=
@@ -17,8 +17,8 @@ github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
-github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
-github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fsnotify/fsnotify v1.10.0 h1:Xx/5Ydg9CeBDX/wi4VJqStNtohYjitZhhlHt4h3St1M=
+github.com/fsnotify/fsnotify v1.10.0/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
@@ -32,30 +32,32 @@ github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebK
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
-github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
-github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
-github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
-github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
-github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
-github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
-github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
-github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
-github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
+github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
+github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260406072232-3ac4aa2bb164 h1:ddtotcEXIT7dFhSeXIWjK8YMFYl5bB2NdYQwgv6Uqjk=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20260406072232-3ac4aa2bb164/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
+github.com/go-text/render v0.2.1 h1:qwHhxqGUjjg4L0XyJWj7M7bpY75NZM+kBpv2Yfw5mcg=
+github.com/go-text/render v0.2.1/go.mod h1:HCCAq8MUlm/WRcXshBb4K/n+IkjeXQ1c2Ba+yICSm0A=
+github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
+github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
+github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
+github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
+github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
+github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
-github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
-github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
+github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
+github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
-github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
+github.com/hajimehoshi/oto v1.0.2 h1:NxUZoQkV4UgbJobPIT/pVTcT/qpkd3YqEGRzPtk7Rc8=
+github.com/hajimehoshi/oto v1.0.2/go.mod h1:AARGdOaQIhMJ1fhKu7nMzEesM2/mE4KZ8A1K1VZozuQ=
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
@@ -72,8 +74,8 @@ github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
-github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
-github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
+github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
+github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -92,32 +94,35 @@ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqd
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
-github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
+github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp/shiny v0.0.0-20260410095643-746e56fc9e2f h1:CMCUocbbREagqundn9s7nFTY3lrmw+Pmi90x2nrbw+g=
+golang.org/x/exp/shiny v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:75UwHX2ZPO3acaGFaP5bhU5yJd6CzUV5v4rX5CiE9ag=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
-golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
+golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
+golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg=
-golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc=
+golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b h1:Qt2eaXcZ8x20iAcoZ6AceeMMtnjuPHvC51KRCH1DKSQ=
+golang.org/x/mobile v0.0.0-20260410095206-2cfb76559b7b/go.mod h1:5Fu78lew5ucMXt8w2KYcwvxu2rkC/liHzUvaoiI+H/M=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
-golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
-golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
-golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
+golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/ui/timecode.go b/internal/ui/timecode.go
index 121b934..4da6858 100644
--- a/internal/ui/timecode.go
+++ b/internal/ui/timecode.go
@@ -108,6 +108,13 @@ func NewTimecodeTab() fyne.CanvasObject {
resultTC, maxWidth, resultFrames, fpsLabel)
}
+ pushHistory := func(entry string) {
+ historyList = append([]string{entry}, historyList...)
+ historyText.SetText(strings.Join(historyList, "\n\n"))
+ historyText.Refresh()
+ historyScroll.ScrollToTop()
+ }
+
// Track previous FPS for conversion history
var previousFPS string
previousFPS = "30 fps"
@@ -156,10 +163,7 @@ func NewTimecodeTab() fyne.CanvasObject {
conversionEntry := fmt.Sprintf(" %s (%*df) @%s\n= %s (%*df) @%s",
oldTC, maxWidth, oldFrames, oldFpsLabel, newTC, maxWidth, newFrames, newFpsLabel)
- historyList = append(historyList, conversionEntry)
- historyText.SetText(strings.Join(historyList, "\n\n"))
- historyText.Refresh()
- historyScroll.ScrollToBottom()
+ pushHistory(conversionEntry)
// Timecode H:M:S:F values don't change, only recalculate display with new FPS
// No need to update entry fields since H:M:S:F stay the same
@@ -189,10 +193,7 @@ func NewTimecodeTab() fyne.CanvasObject {
fpsLabel := strings.Split(fpsSelect.Selected, " ")[0]
historyEntry := formatHistoryEntry(tc1, frames1, tc2, frames2, result.Timecode, result.TotalFrames, fpsLabel, "+")
- historyList = append(historyList, historyEntry)
- historyText.SetText(strings.Join(historyList, "\n\n"))
- historyText.Refresh()
- historyScroll.ScrollToBottom()
+ pushHistory(historyEntry)
// Update first timecode with result and reset second timecode
updating = true
@@ -223,10 +224,7 @@ func NewTimecodeTab() fyne.CanvasObject {
fpsLabel := strings.Split(fpsSelect.Selected, " ")[0]
historyEntry := formatHistoryEntry(tc1, frames1, tc2, frames2, result.Timecode, result.TotalFrames, fpsLabel, "-")
- historyList = append(historyList, historyEntry)
- historyText.SetText(strings.Join(historyList, "\n\n"))
- historyText.Refresh()
- historyScroll.ScrollToBottom()
+ pushHistory(historyEntry)
// Update first timecode with result and reset second timecode
updating = true
diff --git a/internal/ui/widgets/timecode_entry.go b/internal/ui/widgets/timecode_entry.go
index ac4fc09..25a3347 100644
--- a/internal/ui/widgets/timecode_entry.go
+++ b/internal/ui/widgets/timecode_entry.go
@@ -3,6 +3,7 @@ package widgets
import (
"fmt"
"strconv"
+ "time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/driver/mobile"
@@ -15,6 +16,8 @@ type TimecodeEntry struct {
OnComplete func() // Called when all digits are entered
OnOperationKey func(key fyne.KeyName) // Called when +/- keys are pressed
fields []string // Field values [frames, seconds, minutes, hours] - right to left
+ lastOpKey fyne.KeyName
+ lastOpAt time.Time
}
func NewTimecodeEntry(threeDigits bool) *TimecodeEntry {
@@ -41,15 +44,32 @@ func (e *TimecodeEntry) Keyboard() mobile.KeyboardType {
return mobile.NumberKeyboard
}
+func (e *TimecodeEntry) shouldHandleOperationKey(key fyne.KeyName) bool {
+ // Some platforms deliver both TypedRune ('-' / '+') and TypedKey (KeyMinus/KeyEqual)
+ // for a single keypress. De-dupe within a short window.
+ if e.lastOpKey == key {
+ if !e.lastOpAt.IsZero() && time.Since(e.lastOpAt) < 50*time.Millisecond {
+ return false
+ }
+ }
+ e.lastOpKey = key
+ e.lastOpAt = time.Now()
+ return true
+}
+
// TypedRune handles right-justified timecode entry with dot as field separator
func (e *TimecodeEntry) TypedRune(r rune) {
// Handle +/- as operation keys
if r == '+' || r == '-' {
if e.OnOperationKey != nil {
if r == '+' {
- e.OnOperationKey(fyne.KeyEqual)
+ if e.shouldHandleOperationKey(fyne.KeyEqual) {
+ e.OnOperationKey(fyne.KeyEqual)
+ }
} else {
- e.OnOperationKey(fyne.KeyMinus)
+ if e.shouldHandleOperationKey(fyne.KeyMinus) {
+ e.OnOperationKey(fyne.KeyMinus)
+ }
}
}
return
@@ -96,7 +116,9 @@ func (e *TimecodeEntry) TypedKey(k *fyne.KeyEvent) {
case fyne.KeyEqual, fyne.KeyMinus:
// Handle +/- operation keys
if e.OnOperationKey != nil {
- e.OnOperationKey(k.Name)
+ if e.shouldHandleOperationKey(k.Name) {
+ e.OnOperationKey(k.Name)
+ }
}
return
case fyne.KeyPeriod, fyne.KeyComma:
diff --git a/main.go b/main.go
index 0f8393b..f41c607 100644
--- a/main.go
+++ b/main.go
@@ -1,7 +1,6 @@
package main
import (
- _ "embed"
"musicalc/internal/ui"
"strings"
@@ -12,8 +11,7 @@ import (
"fyne.io/fyne/v2/widget"
)
-//go:embed VERSION
-var version string
+var version = "dev"
// CategoryInfo represents a category with tab indices
type CategoryInfo struct {
@@ -54,7 +52,7 @@ func main() {
"note2freq": "Note to Frequency",
"freq2note": "Frequency to Note",
"samplelength": "Sample Length",
- "alignment": "Multi-Mic Alignment Delay",
+ "alignment": "Alignment Delay",
}
var switchCategory func(int)
@@ -109,7 +107,7 @@ func main() {
{Name: "Time & Tempo", TabIndices: []int{0, 1, 2}},
{Name: "Frequency & Pitch", TabIndices: []int{3, 4}},
{Name: "Analysis", TabIndices: []int{5}},
- {Name: "Multi Mic", TabIndices: []int{6}},
+ {Name: "Multi-Mic", TabIndices: []int{6}},
}
// Tab heading keys for each global tab index
diff --git a/utils/release.py b/utils/release.py
index 497c6e0..270539b 100644
--- a/utils/release.py
+++ b/utils/release.py
@@ -1,254 +1,141 @@
#!/usr/bin/env python3
"""
-Release script for MusiCalc
-Interactive release workflow with version management and git automation
+Release helper for MusiCalc.
+
+Git tags are the only release version source. This script validates the
+current tag and runs GoReleaser without creating commits, tags, or pushes.
"""
-import sys
-import os
+import argparse
import re
import subprocess
+import sys
from pathlib import Path
-def find_project_root():
- """Find project root by looking for go.mod file"""
+DEFAULT_CONFIGS = [
+ Path("build/release/goreleaser-linux-amd64.yaml"),
+ Path("build/release/goreleaser-linux-arm64.yaml"),
+ Path("build/release/goreleaser-win-all.yaml"),
+]
+
+VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$")
+
+
+def find_project_root() -> Path:
current = Path.cwd().resolve()
-
- # Check current directory and all parents
for directory in [current] + list(current.parents):
if (directory / "go.mod").exists():
return directory
-
- # If not found, check if we're in a subdirectory with ../go.mod
+
script_dir = Path(__file__).parent.resolve()
for directory in [script_dir] + list(script_dir.parents):
if (directory / "go.mod").exists():
return directory
-
- print("✗ Error: Could not find project root (go.mod not found)")
- sys.exit(1)
-
-
-def read_current_version(project_root):
- """Read current version from VERSION file"""
- version_file = project_root / "VERSION"
- if version_file.exists():
- return version_file.read_text().strip()
- return None
-
-
-def update_version_file(project_root, new_version):
- """Update VERSION file"""
- version_file = project_root / "VERSION"
- version_file.write_text(new_version + "\n")
- print(f"✓ Updated VERSION: {new_version}")
-
-
-def update_inno_setup(project_root, new_version):
- """Update version in musicalc.iss Inno Setup script"""
- iss_file = project_root / "musicalc.iss"
-
- if not iss_file.exists():
- print(f"✗ Error: {iss_file} not found")
- return False
-
- content = iss_file.read_text(encoding='utf-8')
-
- # Update #define MyAppVersion
- pattern = r'(#define MyAppVersion\s+")[^"]+(")'
-
- # Check if pattern exists in file
- if not re.search(pattern, content):
- print(f"✗ Error: Could not find version pattern in {iss_file}")
- return False
-
- replacement = r'\g<1>' + new_version + r'\g<2>'
- new_content = re.sub(pattern, replacement, content)
-
- # Only write if content changed
- if new_content != content:
- iss_file.write_text(new_content, encoding='utf-8')
- print(f"✓ Updated musicalc.iss: {new_version}")
- else:
- print(f"✓ musicalc.iss already at version: {new_version}")
-
- return True
-
-
-def update_desktop_files(project_root, new_version):
- """Update version in .desktop files (StartupWMClass field)"""
- desktop_files = [
- project_root / "musicalc.desktop",
- project_root / "musicalc-pkg.desktop"
- ]
-
- success = True
- for desktop_file in desktop_files:
- if not desktop_file.exists():
- print(f"⚠ Warning: {desktop_file.name} not found, skipping")
- continue
-
- content = desktop_file.read_text(encoding='utf-8')
-
- # Update StartupWMClass=MusiCalc v0.8.4
- pattern = r'(StartupWMClass=MusiCalc v)[0-9.]+'
-
- # Check if pattern exists in file
- if not re.search(pattern, content):
- print(f"✗ Error: Could not find StartupWMClass pattern in {desktop_file.name}")
- success = False
- continue
-
- replacement = r'\g<1>' + new_version
- new_content = re.sub(pattern, replacement, content)
-
- # Only write if content changed
- if new_content != content:
- desktop_file.write_text(new_content, encoding='utf-8')
- print(f"✓ Updated {desktop_file.name}: {new_version}")
- else:
- print(f"✓ {desktop_file.name} already at version: {new_version}")
-
- return success
-
-
-def validate_version(version_str):
- """Validate version string format (e.g., 0.8.3)"""
- pattern = r'^\d+\.\d+\.\d+$'
- return re.match(pattern, version_str) is not None
-
-
-def run_command(cmd, cwd, description):
- """Run a shell command and return success status"""
- print(f"\n→ {description}...")
- try:
- result = subprocess.run(
- cmd,
- cwd=cwd,
- shell=True,
- check=True,
- capture_output=True,
- text=True
+
+ raise SystemExit("Error: could not find project root (go.mod not found)")
+
+
+def run(args: list[str], cwd: Path, description: str, capture: bool = False) -> str:
+ print(f"\n{description}...")
+ result = subprocess.run(
+ args,
+ cwd=cwd,
+ check=False,
+ capture_output=capture,
+ text=True,
+ )
+ if result.returncode != 0:
+ if capture and result.stderr:
+ print(result.stderr.strip())
+ raise SystemExit(f"Error: {description} failed")
+ if capture:
+ return result.stdout.strip()
+ return ""
+
+
+def exact_version_tag(project_root: Path) -> str:
+ tag = run(
+ ["git", "describe", "--tags", "--exact-match"],
+ project_root,
+ "Reading exact Git tag",
+ capture=True,
+ )
+ if not VERSION_RE.match(tag):
+ raise SystemExit(
+ f"Error: release tag '{tag}' is invalid; expected MAJOR.MINOR.PATCH, for example 0.8.7"
+ )
+ return tag
+
+
+def check_configs(project_root: Path, configs: list[Path]) -> None:
+ for config in configs:
+ run(
+ ["goreleaser", "check", "--config", str(config)],
+ project_root,
+ f"Checking {config}",
)
- if result.stdout:
- print(result.stdout.strip())
- print(f"✓ {description} completed")
- return True
- except subprocess.CalledProcessError as e:
- print(f"✗ Error: {description} failed")
- if e.stderr:
- print(e.stderr.strip())
- return False
-
-
-def main():
- # Find project root
+
+
+def release_configs(project_root: Path, configs: list[Path], skip_publish: bool) -> None:
+ for config in configs:
+ command = ["goreleaser", "release", "--clean", "--config", str(config)]
+ if skip_publish:
+ command.append("--skip=publish")
+ run(command, project_root, f"Running GoReleaser for {config}")
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Validate and run a tag-based MusiCalc release."
+ )
+ parser.add_argument(
+ "--config",
+ action="append",
+ type=Path,
+ dest="configs",
+ help="GoReleaser config to use. May be specified multiple times for --check-only; release mode requires exactly one.",
+ )
+ parser.add_argument(
+ "--check-only",
+ action="store_true",
+ help="Only validate the current tag and GoReleaser configs. This is the default unless --release is set.",
+ )
+ parser.add_argument(
+ "--release",
+ action="store_true",
+ help="Run GoReleaser after validation. Requires exactly one --config.",
+ )
+ parser.add_argument(
+ "--publish",
+ action="store_true",
+ help="Allow GoReleaser to publish. By default, publishing is skipped.",
+ )
+ return parser.parse_args()
+
+
+def main() -> None:
+ args = parse_args()
project_root = find_project_root()
- print(f"Project root: {project_root}")
- print()
-
- # Read current version
- current_version = read_current_version(project_root)
- if not current_version:
- print("✗ Error: VERSION file not found or empty")
- sys.exit(1)
-
- print(f"Current version: {current_version}")
- print()
-
- # Ask if user wants to keep or change version
- response = input(f"Keep version {current_version}? [Y/n]: ").strip().lower()
-
- new_version = current_version
- if response == 'n':
- while True:
- new_version = input("Enter new version (format: xx.yy.zz): ").strip()
- if validate_version(new_version):
- break
- print("✗ Invalid version format. Please use format like 0.8.4")
-
- # Update VERSION file
- update_version_file(project_root, new_version)
- else:
- print(f"Using version: {new_version}")
-
- print()
-
- # Run go mod tidy
- if not run_command("go mod tidy", project_root, "Running go mod tidy"):
- sys.exit(1)
-
- # Update musicalc.iss
- print()
- if not update_inno_setup(project_root, new_version):
- sys.exit(1)
-
- # Update .desktop files
- print()
- if not update_desktop_files(project_root, new_version):
- sys.exit(1)
-
- # Ask for commit message
- print()
- commit_message = input("Enter release commit message: ").strip()
- if not commit_message:
- commit_message = f"Release v{new_version}"
-
- # Git workflow
- print()
- print("═" * 50)
- print("Git Workflow")
- print("═" * 50)
-
- # Stage files
- if not run_command("git add VERSION musicalc.iss musicalc.desktop musicalc-pkg.desktop go.mod go.sum", project_root, "Staging files"):
- sys.exit(1)
-
- # Check if there are changes to commit
- try:
- result = subprocess.run(
- "git diff --cached --quiet",
- cwd=project_root,
- shell=True
+ configs = args.configs or DEFAULT_CONFIGS
+
+ tag = exact_version_tag(project_root)
+ print(f"Release version: {tag}")
+ print("Git tags are user-managed; this script will not create or push tags.")
+
+ check_configs(project_root, configs)
+
+ if args.check_only or not args.release:
+ print("\nValidation complete.")
+ return
+
+ if len(configs) != 1:
+ raise SystemExit(
+ "Error: release mode requires exactly one --config. Use --check-only to validate all configs."
)
- has_changes = result.returncode != 0
- except subprocess.CalledProcessError:
- has_changes = True
-
- # Commit and push if there are changes
- if has_changes:
- if not run_command(f'git commit -m "{commit_message}"', project_root, "Committing changes"):
- sys.exit(1)
-
- # Push commits
- if not run_command("git push", project_root, "Pushing commits to origin"):
- sys.exit(1)
- else:
- print("\n→ No changes to commit (version unchanged)")
- print("✓ Skipping commit step")
-
- # Create and push tag
- tag_name = f"{new_version}"
- if not run_command(f'git tag -a {tag_name} -m "Release {tag_name}"', project_root, f"Creating tag {tag_name}"):
- sys.exit(1)
-
- if not run_command(f"git push origin {tag_name}", project_root, f"Pushing tag {tag_name}"):
- sys.exit(1)
-
- # Success summary
- print()
- print("═" * 50)
- print("✓ Release Complete!")
- print("═" * 50)
- print(f"Version: {new_version}")
- print(f"Tag: {tag_name}")
- print(f"Commit: {commit_message}")
- print()
- print("Next steps:")
- print(" 1. Build the application: go build -ldflags=\"-s -w -H=windowsgui\" -o musicalc.exe")
- print(" 2. Test the application")
- print(" 3. Create installer (if using Inno Setup)")
+
+ release_configs(project_root, configs, skip_publish=not args.publish)
+ print("\nRelease commands completed.")
if __name__ == "__main__":