diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4ead320
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,43 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Test (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ env:
+ QT_QPA_PLATFORM: offscreen
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ cache: pip
+
+ - name: Install Linux Qt runtime libs
+ if: runner.os == 'Linux'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libegl1 libgl1 libxkbcommon0 libdbus-1-3 libglib2.0-0
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+
+ - name: Run tests
+ run: pytest -m "not integration" -q
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index dc931ee..8b9e0b9 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -6,7 +6,11 @@ on:
- main
paths:
- docs/**
+ - VERSION
- .github/workflows/pages.yml
+ release:
+ types: [published]
+ workflow_dispatch:
permissions:
contents: write
@@ -18,6 +22,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
+ - name: Sync download links to current VERSION
+ run: |
+ VERSION="$(cat VERSION)"
+ sed -i -E "s/Datamosh-[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?-(linux|windows|macos)/Datamosh-${VERSION}-\2/g" docs/index.html
+ sed -i -E "s#// download v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?#// download v${VERSION}#g" docs/index.html
+ sed -i -E "s#(
v)[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?#\1${VERSION}#g" docs/index.html
+
- name: Deploy to gh-pages branch
uses: peaceiris/actions-gh-pages@v4
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 269450b..9278984 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -79,6 +79,22 @@ jobs:
Set-Content -Path VERSION -Value $version -NoNewline
"VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+ - name: Download FFmpeg for bundling
+ shell: pwsh
+ run: |
+ # Ship ffmpeg/ffprobe inside the Windows build so the app works on
+ # machines without ffmpeg on PATH (gui/ffmpeg_env.ensure_ffmpeg_on_path
+ # prepends the bundled "ffmpeg" dir at startup). GPL build is fine for a
+ # GPL-3.0 project. Pin to a specific release for reproducible builds.
+ $url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1/ffmpeg-7.1-essentials_build.zip"
+ Invoke-WebRequest -Uri $url -OutFile ffmpeg.zip
+ Expand-Archive -Path ffmpeg.zip -DestinationPath ffmpeg-zip -Force
+ New-Item -ItemType Directory -Force -Path ffmpeg-bin | Out-Null
+ $ff = Get-ChildItem -Path ffmpeg-zip -Recurse -Filter ffmpeg.exe | Select-Object -First 1
+ $fp = Get-ChildItem -Path ffmpeg-zip -Recurse -Filter ffprobe.exe | Select-Object -First 1
+ Copy-Item $ff.FullName ffmpeg-bin/ffmpeg.exe
+ Copy-Item $fp.FullName ffmpeg-bin/ffprobe.exe
+
- name: Build app
shell: pwsh
run: |
@@ -86,6 +102,8 @@ jobs:
--add-data "README.md;." `
--add-data "LICENSE;." `
--add-data "VERSION;." `
+ --add-binary "ffmpeg-bin/ffmpeg.exe;ffmpeg" `
+ --add-binary "ffmpeg-bin/ffprobe.exe;ffmpeg" `
main.py
- name: Install Inno Setup
diff --git a/.github/workflows/update_beta5_release.yml b/.github/workflows/update_beta5_release.yml
deleted file mode 100644
index b530dee..0000000
--- a/.github/workflows/update_beta5_release.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: Update Beta 5 Release Notes
-
-on:
- push:
- branches:
- - main
- paths:
- - RELEASE_BODY.md
- - .github/workflows/update_beta5_release.yml
-
-permissions:
- contents: write
-
-jobs:
- update-release:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Promote v1.1.0-beta.5 as current release
- uses: actions/github-script@v7
- with:
- script: |
- const fs = require("fs");
- const owner = context.repo.owner;
- const repo = context.repo.repo;
- const tag = "v1.1.0-beta.5";
- const body = fs.readFileSync("RELEASE_BODY.md", "utf8");
-
- const release = await github.rest.repos.getReleaseByTag({
- owner,
- repo,
- tag,
- });
-
- await github.rest.repos.updateRelease({
- owner,
- repo,
- release_id: release.data.id,
- name: "Datamosh v1.1.0 (Linux/macOS)",
- body,
- draft: false,
- prerelease: false,
- make_latest: "true",
- });
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b283257..6c01d55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,23 @@
All notable changes to this project are documented here.
-## [Unreleased] - 2026-02-17
+## [Unreleased]
+
+## [1.1.5] - 2026-02-28
+
+### Fixed
+- Temp directories from normalization and I-frame injection are now cleaned up when a clip is removed, preventing `/tmp` accumulation over long sessions.
+- Video info probing (fps, frame count, dimensions) moved off the Qt main thread — no more UI freeze after importing clips.
+- Timeline scroll wheel now pans; `Ctrl+wheel` zooms (was inverted vs. shortcut docs).
+- Clip list view now highlights the correct row when a timeline clip is clicked.
+- Settings panel controls are now disabled when no clip is selected (prevented silent no-op slider interactions).
+- "Cut At Playhead" context menu item disabled on empty timeline.
+- `UpdateWorker` exceptions no longer permanently block future update checks.
+- OpenCV video capture handle always released via `finally` (prevents file lock on Windows).
+- `RenderDialog` stops its render worker when the dialog is closed mid-render.
+- Drag-reorder MIME data parse is now validated; malformed drops return `False` cleanly.
+- Undo/redo stacks use `deque(maxlen=200)` for O(1) eviction.
+- Timeline hint text contrast raised to meet WCAG AA minimum.
## [1.1.4] - 2026-02-18
diff --git a/CLAUDE.md b/CLAUDE.md
index d1f6a39..4cde305 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -35,7 +35,7 @@ Tests are in `tests/` with markers `integration` (requires real video files), `g
## Architecture
-The application has two layers: a pure binary AVI manipulation engine (`mosh.py`, treat as stable/untouched) and a PySide6 GUI (`gui/`, `main.py`).
+The application has two layers: a pure binary AVI manipulation engine (`mosh.py`) and a PySide6 GUI (`gui/`, `main.py`). `mosh.py` is the most safety-critical code — change it with care and keep `tests/test_mosh*.py` green; it must still round-trip its own normalized output byte-for-byte.
### Core engine (`mosh.py`)
Operates directly on the RIFF/AVI binary format — no video decoding. Pipeline:
@@ -97,4 +97,4 @@ Multiple clips concatenated at chunk level, each tagged with `clip_id` for indep
- `ffmpeg` and `ffprobe` must be on PATH.
- `opencv-python-headless` is used for frame extraction; falls back to ffmpeg pipe if unavailable.
- Output format is always AVI (Xvid). MP4/WebM export not implemented.
-- `mosh.py` must not be modified — the GUI wraps it via `MoshWorker`.
+- `mosh.py` is the core engine, wrapped by `MoshWorker`. It may be modified, but carefully: keep its round-trip behavior intact and `tests/test_mosh*.py` green. idx1 is treated as advisory (used only for keyframe flags); output is capped at the 4 GB AVI limit with a clear error.
diff --git a/README.md b/README.md
index 0b3f1c5..057e2c9 100644
--- a/README.md
+++ b/README.md
@@ -157,6 +157,10 @@ pyinstaller --noconfirm --clean --windowed --name Datamosh \
## Keyboard Shortcuts
+- `Ctrl+N`: new project
+- `Ctrl+Shift+P`: open project (`.dmosh`)
+- `Ctrl+S`: save project
+- `Ctrl+Shift+S`: save project as
- `Ctrl+O`: open clips
- `Ctrl+Shift+O`: add clips
- `Ctrl+R`: render
@@ -192,6 +196,15 @@ Headless-safe full suite:
QT_QPA_PLATFORM=offscreen pytest -q
```
+## Projects
+
+Save your whole session — clips, per-clip mosh settings, timeline arrangement, cuts,
+and injected I-frames — to a `.dmosh` project file (`File > Save Project`, `Ctrl+S`).
+Reopening (`File > Open Project`, `Ctrl+Shift+P`) recreates the clips from their source
+paths and re-ingests them, so projects stay small and portable. Source media must still
+be present at its saved path; missing sources are reported and left empty. The window
+title shows the project name and a `*` when there are unsaved changes.
+
## Notes
- Current output target is AVI/Xvid.
diff --git a/ROADMAP.md b/ROADMAP.md
index 120ea07..de8a14e 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -14,6 +14,8 @@ Product direction for Datamosh GUI (timeline-first, experimental mosh workflow).
- Define expected pass/fail behavior and fallback messaging.
### 2. Import Strategy UI (Audit + Upgrade)
+> **Status: shipped in v1.1.1** — import options dialog with normalize-all vs. direct-AVI modes, preset/custom controls (width/height/GOP/qscale/audio), and a persistent import profile. Remaining work below is refinement only.
+
- Audit current import path (auto-normalize to Xvid) and verify edge-case behavior.
- Add an import options dialog before ingest:
- Keep original if compatible
@@ -47,6 +49,8 @@ Definition of done:
## Phase 3: UI Polish and Workflow Speed
### 5. Toolbar Refresh
+> **Status: shipped** — the toolbar is now icon-only (`ToolButtonIconOnly`) with tooltips and keyboard accelerators. Remaining work is a custom scalable icon set.
+
- Replace text actions with icon-first toolbar + tooltips.
- Keep keyboard shortcuts primary; toolbar as visual accelerator.
- Add scalable icon set for light/dark readability.
@@ -57,8 +61,12 @@ Definition of done:
- Background decode/analysis prioritization for active viewport.
## Near-Term Priority Order
-1. Example media suite + ingest reliability tests
-2. Import strategy dialog and profile system
+1. **Project persistence** — save/load `.dmosh` project files (timeline arrangement + per-clip settings + cuts + injected I-frames). Foundational: today all work is lost on close.
+2. Example media suite + ingest reliability tests
3. Layered timeline foundation
4. Opacity/blend modes
-5. Icon toolbar and final UI pass
+5. Custom scalable icon set for the toolbar
+
+### Already shipped
+- Import strategy dialog and persistent import profiles (v1.1.1)
+- Icon-first toolbar with tooltips and accelerators
diff --git a/docs/index.html b/docs/index.html
index 25239d7..c9393d1 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -659,25 +659,25 @@
Get Datamosh GUI
Linux
x86_64