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
42 changes: 32 additions & 10 deletions .github/workflows/bundle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@ on:
- ".github/workflows/bundle.yml"
- ".github/workflows/release.yml"
pull_request:
paths:
- "app/src-tauri/nsis/**"
- "app/src-tauri/Info.plist"
- "app/src-tauri/tauri.conf.json"
- "app/src-tauri/Cargo.toml"
- "app/src-tauri/build.rs"
- "scripts/sidecar.sh"
- "Cargo.toml"
- ".github/workflows/bundle.yml"
- ".github/workflows/release.yml"
workflow_dispatch:

permissions:
Expand All @@ -37,8 +27,38 @@ env:
CARGO_TERM_COLOR: always

jobs:
# `paths` on the trigger leaves a required check unreported for good; a skipped job counts as met.
changes:
name: what the branch touched
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
packaging: ${{ steps.look.outputs.packaging }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- id: look
env:
BASE: ${{ github.event.pull_request.base.sha }}
run: |
if [ -z "$BASE" ]; then
echo "packaging=true" >> "$GITHUB_OUTPUT"
exit 0
fi
watched='^(app/src-tauri/(nsis/|Info\.plist|tauri\.conf\.json|Cargo\.toml|build\.rs)|scripts/sidecar\.sh|Cargo\.toml$|\.github/workflows/(bundle|release)\.yml$)'
from=$(git merge-base "$BASE" HEAD)
if git diff --name-only --diff-filter=d "$from" HEAD | grep -qE "$watched"; then
echo "packaging=true" >> "$GITHUB_OUTPUT"
else
echo "packaging=false" >> "$GITHUB_OUTPUT"
fi

nsis:
name: the installer builds
needs: changes
if: needs.changes.outputs.packaging == 'true'
runs-on: windows-latest
timeout-minutes: 45
steps:
Expand Down Expand Up @@ -99,6 +119,8 @@ jobs:

macos:
name: the bundle builds
needs: changes
if: needs.changes.outputs.packaging == 'true'
runs-on: macos-latest
timeout-minutes: 60
env:
Expand Down
42 changes: 32 additions & 10 deletions .github/workflows/mutants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@ name: Mutants

on:
pull_request:
paths:
- crates/**
- app/src/**
- .cargo/mutants.toml
- app/stryker.config.json
- .github/workflows/mutants.yml

permissions:
contents: read
Expand All @@ -20,10 +14,35 @@ env:
CARGO_TERM_COLOR: always

jobs:
# `paths` on the trigger leaves a required check unreported for good; a skipped job counts as met.
changes:
name: what the branch touched
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
mutable: ${{ steps.look.outputs.mutable }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- id: look
env:
BASE: ${{ github.event.pull_request.base.sha }}
run: |
watched='^(crates/|app/src/|\.cargo/mutants\.toml$|app/stryker\.config\.json$|\.github/workflows/mutants\.yml$)'
from=$(git merge-base "$BASE" HEAD)
if git diff --name-only --diff-filter=d "$from" HEAD | grep -qE "$watched"; then
echo "mutable=true" >> "$GITHUB_OUTPUT"
else
echo "mutable=false" >> "$GITHUB_OUTPUT"
fi

crates-on-the-branch:
name: crates / the lines this branch changed
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
needs: changes
if: github.actor != 'dependabot[bot]' && needs.changes.outputs.mutable == 'true'
timeout-minutes: 90
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -83,7 +102,8 @@ jobs:
mac-on-the-branch:
name: mac / the lines this branch changed
runs-on: macos-latest
if: github.actor != 'dependabot[bot]'
needs: changes
if: github.actor != 'dependabot[bot]' && needs.changes.outputs.mutable == 'true'
timeout-minutes: 90
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -137,7 +157,8 @@ jobs:
settings-on-the-branch:
name: settings / the lines this branch changed
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
needs: changes
if: github.actor != 'dependabot[bot]' && needs.changes.outputs.mutable == 'true'
timeout-minutes: 90
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -201,7 +222,8 @@ jobs:
window-on-the-branch:
name: window / the lines this branch changed
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
needs: changes
if: github.actor != 'dependabot[bot]' && needs.changes.outputs.mutable == 'true'
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
Expand Down
15 changes: 14 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ We believe in:

1. **Fork** the repository.
2. **Choose the base branch:**
- `main` — active development. Version 2.0 lands here.
- `main` — active development. The 2.x line lands here.
- `v1-stable` — the frozen 1.4.x line. Security and crash fixes only, no new features.
3. **Create a branch** from it (`git checkout -b feature/my-improvement`).
4. **Make your changes** following the style guide below.
Expand All @@ -67,6 +67,19 @@ Found something confusing? Missing information? PRs welcome.

---

## Architecture

Two binaries, one package:

- `linkunbound-shell` → the resident: tray or menu bar, the picker, the notice; Windows and macOS run it for every link, and a second copy hands its link to the one already running and exits
- `linkunbound-settings` → the settings window, opened on demand, and the errands the resident sends it on with no window (`--look` asks the feed, `--update` installs)

**Windows.** A named pipe of the user's own (`linkunbound-<user>.sock`) links second instances to the resident, and holding its name is what keeps a second resident from starting. The installer writes the app's own ProgId, `RegisteredApplications` and `StartMenuInternet` keys; the settings window re-points them when the install moves and never recreates what the person took away; Windows itself owns the final choice through `UserChoice`, which no application may write.

**macOS.** The bundle's `CFBundleExecutable` is the resident, so Launch Services starts it — or talks to the running copy — for every link. Nothing arrives on the command line: links, documents, launches and reopens come in as Apple Events (`GURL`, `odoc`, `oapp`, `rapp`), and the launch event says whether the session started the app as a login item, which is what keeps the settings window closed at sign-in. A Unix socket under `~/Library/Application Support/LinkUnbound/` carries links handed over from a terminal. Default-browser registration goes through `NSWorkspace.setDefaultApplication` for `http`, `https` and the web document types, which the system confirms with its own prompt; the browser that held the links before is remembered and gets them back on unregistering. Login items use `SMAppService`. A browser is started through `open` either way — plainly for a bare link, as an instance of its own when a private window or a profile rides along — so Launch Services starts it and it answers for its own permissions rather than for LinkUnbound's. The web document types are declared as an alternate opener: double-clicking an `.html` keeps opening wherever it did until the person chooses. The app runs as `LSUIElement`, so it lives in the menu bar instead of the Dock, and the picker floats above every Space, full-screen apps included.

---

## Project Structure

LinkUnbound is a Cargo workspace. Two binaries ship in one package: the
Expand Down
6 changes: 4 additions & 2 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ macOS does not report which application opened a link, so the foreground app sta
What this means in practice:

- **Only the application name is read.** Not its windows, not its title, not
its contents. `explorer`, `cmd` and `powershell` are discarded, since they
are the shell itself rather than an app worth writing a rule about.
its contents. The shell counts as an application like any other: a link
opened from a file on the desktop reports `explorer`, and whether that is
worth a rule is yours to decide, not ours. The picker names the rule that
decided every time one does.
- **It is read at the moment a link arrives**, not continuously. LinkUnbound
does not watch which applications you use.
- **It reaches the disk only if you ask it to.** The name is used in memory to
Expand Down
Loading
Loading