Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/scripts/winget-manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
import argparse
import hashlib
import pathlib

PACKAGE = "blendonl.mshell"
REPO = "https://github.com/blendonl/mshell"
MANIFEST_VERSION = "1.6.0"


def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest().upper()


def header(version, kind):
return [
f"# yaml-language-server: $schema=https://aka.ms/winget-manifest.{kind}.{MANIFEST_VERSION}.schema.json",
"",
f"PackageIdentifier: {PACKAGE}",
f"PackageVersion: {version}",
]


def footer(kind):
return [f"ManifestType: {kind}", f"ManifestVersion: {MANIFEST_VERSION}", ""]


def version_manifest(version):
return header(version, "version") + ["DefaultLocale: en-US"] + footer("version")


def installer_manifest(version, msi, product_code):
lines = header(version, "installer") + [
"InstallerType: wix",
"Scope: machine",
"UpgradeBehavior: install",
"InstallModes:",
" - interactive",
" - silent",
" - silentWithProgress",
]
if product_code:
lines.append(f"ProductCode: '{product_code}'")
lines += [
"Installers:",
" - Architecture: x64",
f" InstallerUrl: {REPO}/releases/download/v{version}/{msi.name}",
f" InstallerSha256: {sha256(msi)}",
]
return lines + footer("installer")


def locale_manifest(version):
return header(version, "defaultLocale") + [
"PackageLocale: en-US",
"Publisher: blendonl",
"PublisherUrl: https://github.com/blendonl",
f"PublisherSupportUrl: {REPO}/issues",
"PackageName: mshell",
f"PackageUrl: {REPO}",
"License: MIT",
f"LicenseUrl: {REPO}/blob/main/LICENSE",
"ShortDescription: A keyboard-driven tiling window manager that replaces explorer.exe as the Windows shell.",
"Moniker: mshell",
"Tags:",
" - tiling-window-manager",
" - window-manager",
" - shell",
" - lua",
f"ReleaseNotesUrl: {REPO}/releases/tag/v{version}",
] + footer("defaultLocale")


def main():
p = argparse.ArgumentParser()
p.add_argument("--version", required=True)
p.add_argument("--msi", required=True, type=pathlib.Path)
p.add_argument("--product-code", default="")
p.add_argument("--out", required=True, type=pathlib.Path)
a = p.parse_args()

out = a.out / "manifests" / PACKAGE[0].lower() / "blendonl" / "mshell" / a.version
out.mkdir(parents=True, exist_ok=True)

files = {
f"{PACKAGE}.yaml": version_manifest(a.version),
f"{PACKAGE}.installer.yaml": installer_manifest(a.version, a.msi, a.product_code),
f"{PACKAGE}.locale.en-US.yaml": locale_manifest(a.version),
}
for name, lines in files.items():
(out / name).write_text("\n".join(lines), encoding="utf-8")
print(out / name)


if __name__ == "__main__":
main()
26 changes: 24 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,27 @@ jobs:

- uses: actions/upload-artifact@v4
with:
name: mshell-win64
path: dist/*.zip
name: stage
path: dist/stage

msi:
needs: build
runs-on: windows-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: stage
path: dist/stage

- name: Build the installer
shell: pwsh
run: ./packaging/build-msi.ps1 -Version 0.0.1 -Stage dist/stage -Out dist/mshell-test.msi

- uses: actions/upload-artifact@v4
with:
name: mshell-msi
path: dist/*.msi
134 changes: 109 additions & 25 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,20 @@ concurrency:
cancel-in-progress: false

jobs:
release:
build:
if: "!contains(github.event.head_commit.message, '[skip release]')"
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install mingw-w64 and wixl
- name: Install mingw-w64
run: |
sudo apt-get update
sudo apt-get install -y gcc-mingw-w64-x86-64 wixl

command -v wixl >/dev/null || {
echo "::error::wixl is not on PATH after installing it"; exit 1; }
wixl --version
sudo apt-get install -y gcc-mingw-w64-x86-64

- name: Host-side unit tests
run: make test
Expand Down Expand Up @@ -83,27 +79,99 @@ jobs:
echo "--- release notes ---"
cat "$RUNNER_TEMP/notes.md"

- name: Build and package
run: make CFLAGS_EXTRA=-Werror dist msi
- name: Build and stage
run: make CFLAGS_EXTRA=-Werror dist

- name: Keep the release notes and the bumped files
run: cp Makefile CHANGELOG.md "$RUNNER_TEMP/notes.md" dist/

- uses: actions/upload-artifact@v4
with:
name: stage
path: dist/stage

- uses: actions/upload-artifact@v4
with:
name: release-meta
path: |
dist/Makefile
dist/CHANGELOG.md
dist/notes.md
dist/*-symbols.zip

msi:
needs: build
runs-on: windows-latest
env:
GH_TOKEN: ${{ github.token }}
MSHELL_SIGN_PFX_BASE64: ${{ secrets.MSHELL_SIGN_PFX_BASE64 }}
MSHELL_SIGN_PFX_PASSWORD: ${{ secrets.MSHELL_SIGN_PFX_PASSWORD }}
steps:
- uses: actions/checkout@v4

- uses: actions/download-artifact@v4
with:
name: stage
path: dist/stage

- name: Build the installer
shell: pwsh
run: >
./packaging/build-msi.ps1
-Version ${{ needs.build.outputs.version }}
-Stage dist/stage
-Out dist/mshell-${{ needs.build.outputs.version }}.msi

- uses: actions/upload-artifact@v4
with:
name: msi
path: dist/*.msi

publish:
needs: [build, msi]
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
NEW: ${{ needs.build.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/download-artifact@v4
with:
name: release-meta
path: dist

- uses: actions/download-artifact@v4
with:
name: msi
path: dist

- name: Check every artifact exists
env:
NEW: ${{ steps.version.outputs.version }}
run: |
for f in "dist/mshell-$NEW-win64.zip" \
"dist/mshell-$NEW-win64-symbols.zip" \
for f in "dist/mshell-$NEW-win64-symbols.zip" \
"dist/mshell-$NEW.msi"; do
test -s "$f" || { echo "::error::$f was not produced"; exit 1; }
done
ls -l "dist/mshell-$NEW-win64.zip" \
"dist/mshell-$NEW-win64-symbols.zip" \
"dist/mshell-$NEW.msi"
ls -l dist

- name: Write the winget manifests
run: |
sudo apt-get update
sudo apt-get install -y msitools
code=$(msiinfo export "dist/mshell-$NEW.msi" Property | awk -F'\t' '$1=="ProductCode"{print $2}' | tr -d '\r')
python3 .github/scripts/winget-manifest.py \
--version "$NEW" \
--msi "dist/mshell-$NEW.msi" \
--product-code "$code" \
--out dist/winget
(cd dist/winget && zip -r "../mshell-$NEW-winget.zip" manifests)

- name: Commit the bump back to main
id: commit
env:
NEW: ${{ steps.version.outputs.version }}
run: |
cp dist/Makefile dist/CHANGELOG.md .
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Makefile CHANGELOG.md
Expand All @@ -112,13 +180,29 @@ jobs:
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Publish
env:
NEW: ${{ steps.version.outputs.version }}
run: |
gh release create "v$NEW" \
--target "${{ steps.commit.outputs.sha }}" \
--title "mshell $NEW" \
--notes-file "$RUNNER_TEMP/notes.md" \
"dist/mshell-$NEW-win64.zip" \
--notes-file dist/notes.md \
"dist/mshell-$NEW.msi" \
"dist/mshell-$NEW-win64-symbols.zip" \
"dist/mshell-$NEW.msi"
"dist/mshell-$NEW-winget.zip"

winget:
needs: [build, publish]
runs-on: windows-latest
env:
GH_TOKEN: ${{ github.token }}
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
NEW: ${{ needs.build.outputs.version }}
steps:
- name: Submit to winget-pkgs
if: env.WINGET_TOKEN != ''
shell: pwsh
run: |
gh release download "v$env:NEW" -R "$env:GITHUB_REPOSITORY" -p "mshell-$env:NEW-winget.zip" -D winget
Expand-Archive "winget/mshell-$env:NEW-winget.zip" winget
Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
./wingetcreate.exe submit --token $env:WINGET_TOKEN "winget/manifests/b/blendonl/mshell/$env:NEW"
if ($LASTEXITCODE) { throw "wingetcreate failed ($LASTEXITCODE)" }
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,59 @@ All notable changes to mshell are documented here. This project adheres to

## Unreleased

### Changed

- **BREAKING: mshell installs from the MSI only.** `install.bat` and
`uninstall.bat` are gone, and so is the release zip. `mshell-<version>.msi`
installs to `C:\Program Files\mshell` and offers, as checkboxes on its
*Custom Setup* page: using mshell as your shell, the privileged helper's
sign-in task, the Windows tweaks (including the five policy values that
used to need an elevated `install.bat`), and the companion apps **mrun**,
**mnotify** and **mcapture**, which are now bundled. Upgrading renames the
running images and hands over to the new build without a reboot or a
sign-out; uninstalling from *Apps* removes the shell value, the task, the
companions and the tweaks. The MSI is built with WiX v5 on Windows
(`packaging/build-msi.ps1`).
- **`config.update` runs the MSI.** `Win+Shift+u` downloads the release's
`.msi`, checks it against the published SHA-256, runs it with
`msiexec /passive` and restarts into the new build.
- **mshell writes its own default config.** The first start without
`%APPDATA%\mshell\init.lua` copies the shipped one there with its
`.luarc.json`, and every start refreshes `%APPDATA%\mshell\meta`.
- **A new default `init.lua`.** `h/j/k/l` only work while `Win` is held; a bare
`Win` tap opens a one-shot leader menu — `g` go to a desktop, `m` send the
window to one, `w` window, `l` layout, `r` resize, `a` apps, `v` media,
`s` session, `x` mshell. `Win+Ctrl+1..9` takes the window to a desktop and
follows it. `Win+Return` is the terminal (the first of `wt`, `pwsh`, `cmd`
that launches), `Win+Space` the launcher (mrun when installed), `Win+q`
closes. mnotify and `mrun --daemon` start with the session when installed,
and `Win+Shift+s` / `Win+PrintScreen` take screenshots. The colours are
Catppuccin Mocha, the palette mrun and mnotify already ship with.
- **New defaults land beside your config.** When a release changes the shipped
`init.lua` and yours differs from it, mshell copies the new one to
`%APPDATA%\mshell\init.lua.new` and says so in a notification. Yours is
never overwritten.
- **The bundled companions are pinned.** `packaging/companions.json` names the
mrun, mnotify and mcapture release each MSI ships and its SHA-256, so a
companion release no longer changes an mshell build. `-LatestCompanions`
takes the newest instead.

### Added

- `config.restart` (`mshell.exe --msg restart`) hands over to the build that is
installed now, the way an update does.
- `--tweaks apply` and `--tweaks revert` take several groups:
`--tweaks apply input apps`.
- A welcome notification on the first start explains the leader menu and the
main chords.
- Code signing: with the `MSHELL_SIGN_PFX_BASE64` and
`MSHELL_SIGN_PFX_PASSWORD` secrets set, the release signs every unsigned
binary and the MSI. A signed `mshelld.exe` turns on the helper's
same-certificate check of its clients.
- winget: each release attaches `mshell-<version>-winget.zip` with the
`blendonl.mshell` manifests, and submits them to winget-pkgs when the
`WINGET_TOKEN` secret is set.

## 0.15.25 — 2026-10-15

- feat(power): configure the active power plan from `power.setup` and
Expand Down
Loading
Loading