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
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,5 +141,11 @@ not as a reason for mechanical splitting.
- Drafts under `docs/temp/` are planning material unless explicitly promoted.
- Version changes must go through `tools/bump-version.sh`; keep both package manifests, root
`Cargo.lock`, packaging metadata, and tag/release policy aligned.
- Close a user-visible "I don't have that setting / this build" report only after the change is in
a tagged GitHub release. `main` is not what `arch-install.sh`, AUR `wayscriber-bin`, or other
packaged installs ship. `--version` reports the crate version, not the git hash, so bump with
`tools/bump-version.sh` in the same change as a user-visible overlay, settings, or config toggle
(or immediately before tagging that release). Otherwise two binaries can print the same
`wayscriber 0.9.x` and look identical.

See [tools/README.md](tools/README.md) for build, install, packaging, version, and release helpers.
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,16 @@ sh arch-install.sh
```

The dry run verifies the latest stable release, its checksum and install manifest, the
required Arch packages, and package ownership without installing files. The full run
installs allowlisted files under `/usr/local`, refuses to shadow a pacman-owned Wayscriber
installation, and does not start or restart the user service. To update this installation,
download a fresh script, rerun it, then restart `wayscriber.service` when ready.
required Arch packages, and package ownership without installing files. Both the dry run
and the full run refuse another Wayscriber copy that is not this install: an unmanaged
file under `/usr`, `~/.local/bin/wayscriber`, or a user unit / drop-in whose `ExecStart`
is not `/usr/local/bin/wayscriber`. A symlink that already resolves to the dest is not a
conflict. Pass `--replace-other` on those commands to remove the other copy first. If
`wayscriber.service` is already active, the installer restarts it so the overlay daemon
follows `/usr/local/bin/wayscriber`; pass `--no-restart` to leave the running unit alone.
`--version` is the crate version, not a git hash, so inspect the installed path and the
running service `ExecStart` after install. To update this installation, download a fresh
script and rerun it.

Remove the direct installation before moving to the AUR. Otherwise, `/usr/local/bin` and
`/usr/local/lib/systemd/user` continue to take priority over the package files under
Expand All @@ -299,10 +305,12 @@ yay -S wayscriber-bin
systemctl --user enable --now wayscriber.service
```

The uninstaller removes only unmanaged files in the direct install manifest, including
the same files from the legacy `/usr` location. It leaves pacman-owned files and your user
configuration and data unchanged, and refuses actual removal while the service is active
or enabled.
The uninstaller removes only unmanaged files in the direct install manifest under
`/usr/local`. It leaves pacman-owned files and your user configuration and data unchanged,
and refuses actual removal while the service is active or enabled. A leftover unmanaged
git build under `/usr` is left in place; pass `--remove-unmanaged-usr` only when that
`/usr` copy should go too. Default uninstall does not remove `~/.local/bin/wayscriber` or
a user unit / drop-in; the script warns if those still point at `/usr/local`.

### NixOS and Nix

Expand Down
31 changes: 26 additions & 5 deletions docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@ Run the install script:

This will:
1. Build the release binary
2. Copy it to `~/.local/bin/wayscriber`
3. Tell you how to add Hyprland keybind
2. Copy it to `/usr/bin/wayscriber` (or `$WAYSCRIBER_INSTALL_DIR`)
3. Refuse a second copy under `/usr/local/bin` or `~/.local/bin` unless you pass `--replace-other` or confirm
4. Optionally set up the systemd user service or a Hyprland keybind

If `/usr/local/bin/wayscriber` already exists from the direct Arch installer, keep only one
prefix. Overlay spawn follows the running daemon file, not whichever path you passed to
`--version` or `--about`.

### Manual Install

Expand Down Expand Up @@ -250,9 +255,15 @@ Press <kbd>Escape</kbd> (should hide overlay)
- If `wayscriber` is not found from the compositor, use the absolute path from `command -v wayscriber`

**Binary not found?**
- Check PATH: `echo $PATH | grep .local/bin`
- Add to PATH if missing (see Manual Install)
- Restart terminal after PATH change
- Check `type -a wayscriber` and the path you installed to (`/usr/bin`, `/usr/local/bin`, or `~/.local/bin`)
- If you used `./tools/install.sh`, the default dest is `/usr/bin/wayscriber` (or `$WAYSCRIBER_INSTALL_DIR`)
- If you copied the binary by hand, add `~/.local/bin` to PATH (see Manual Install) and restart the terminal

**Settings or `--about` do not match the binary you just built?**
- Two copies can exist: the direct installer uses `/usr/local`, source `tools/install.sh` uses `/usr/bin`, and a manual copy may live in `~/.local/bin`.
- Check `type -a wayscriber`, `systemctl --user show wayscriber.service -p ExecStart,MainPID,FragmentPath`, and `readlink -f /proc/$(systemctl --user show -p MainPID --value wayscriber.service)/exe`.
- `--version` is the crate version and can be identical on both copies. `--about` opens a window; use it on the path you intend to run, not as a substitute for the running daemon path.
- Keep one prefix. `./tools/install.sh --replace-other` removes the other known copies when installing to `/usr/bin`.

**Want different key?**
- Edit `bindings.lua` or `hyprland.conf`, depending on your Hyprland config format
Expand All @@ -263,6 +274,16 @@ Press <kbd>Escape</kbd> (should hide overlay)

## Uninstall

If you used `./tools/install.sh` (default `/usr/bin`):

```bash
sudo rm -f /usr/bin/wayscriber
systemctl --user disable --now wayscriber.service
# Remove the keybind from bindings.lua or hyprland.conf
```

If you copied the binary by hand to `~/.local/bin`:

```bash
rm ~/.local/bin/wayscriber
# Remove the keybind from bindings.lua or hyprland.conf
Expand Down
5 changes: 4 additions & 1 deletion docs/codebase-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ backend entry in `src/backend/mod.rs`.
action applier.
5. Overlay candidates and runtime helpers are created only by the pre-lock process broker. The
daemon owns generation/pidfd decisions while the broker owns wait/reap; overlay readiness is
accepted only after the child wins its lock and publishes matching process identity.
accepted only after the child wins its lock and publishes matching process identity. Startup
also warns when another `wayscriber` file exists on `PATH` or under `/usr`, `/usr/local`,
or `~/.local/bin`, because overlay spawn follows this process file rather than whichever
path the user inspected.
6. Queue renames, producer eventfds, signals, and child pidfds drive the loop without a lifecycle
polling tick. Shutdown invalidates readiness, terminates owned work, and joins listeners.

Expand Down
1 change: 1 addition & 0 deletions src/daemon/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

## Architecture
- `core.rs`, `control.rs`, and `types.rs` own daemon state and toggle/control behavior.
- `binary_conflict.rs` warns when another Wayscriber binary exists besides the running daemon.
- `overlay/` owns overlay process spawn/control.
- `tray/` owns tray integration and shortcut hint I/O.
- `setup.rs` and `global_shortcuts.rs` support daemon setup workflows.
Expand Down
186 changes: 186 additions & 0 deletions src/daemon/binary_conflict.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use std::collections::HashSet;
use std::env;
use std::path::{Path, PathBuf};

use log::warn;

use crate::env_vars::PATH_ENV;
use crate::paths::home_dir;

/// Prefixes this project actually installs to, including ones the packaged
/// user unit PATH (`/usr/local/bin:/usr/bin:/bin`) does not search.
///
/// `PATH` is scanned separately. Do not add cargo/nix/opt folders here unless
/// a Wayscriber installer writes them; those still show up when they are on
/// `PATH`.
fn well_known_wayscriber_binaries() -> Vec<PathBuf> {
let mut paths = vec![
PathBuf::from("/usr/bin/wayscriber"),
PathBuf::from("/usr/local/bin/wayscriber"),
];
if let Some(home) = home_dir() {
paths.push(home.join(".local").join("bin").join("wayscriber"));
}
paths
}

fn canonicalize_file(path: &Path) -> Option<PathBuf> {
if !path.is_file() {
return None;
}
path.canonicalize().ok()
}

/// Other `wayscriber` files on `PATH` or in well-known prefixes besides this process.
///
/// Overlay spawn follows `current_exe` first. A second copy is how `--version` or
/// `--about` on a different path can disagree with the running daemon.
pub(crate) fn other_installed_wayscriber_binaries(
current_exe: &Path,
path_env: Option<&str>,
extra_candidates: &[&Path],
) -> Vec<PathBuf> {
let Some(current) = canonicalize_file(current_exe) else {
return Vec::new();
};

let mut seen = HashSet::new();
seen.insert(current);
let mut others = Vec::new();

let mut consider = |candidate: &Path| {
let Some(canonical) = canonicalize_file(candidate) else {
return;
};
if seen.insert(canonical.clone()) {
others.push(canonical);
}
};

if let Some(path_env) = path_env {
for dir in env::split_paths(path_env) {
consider(&dir.join("wayscriber"));
}
}
for candidate in extra_candidates {
consider(candidate);
}

others
}

pub(super) fn warn_if_other_wayscriber_binaries() {
let Ok(exe) = env::current_exe() else {
return;
};
let path_env = env::var(PATH_ENV).ok();
let extras = well_known_wayscriber_binaries();
let extra_refs: Vec<&Path> = extras.iter().map(PathBuf::as_path).collect();
let others = other_installed_wayscriber_binaries(&exe, path_env.as_deref(), &extra_refs);
if others.is_empty() {
return;
}
let other_list = others
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
warn!(
"Another wayscriber binary exists besides this daemon ({}). Overlay spawn follows this process file; inspecting a different path with --version or --about can disagree. Other copies: {}",
exe.display(),
other_list
);
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::os::unix::fs::symlink;

fn write_file(path: &Path) {
fs::write(path, b"wayscriber-test-binary").unwrap();
}

#[test]
fn no_conflict_when_path_and_extras_are_the_same_file() {
let tmp = crate::test_temp::tempdir().unwrap();
let binary = tmp.path().join("wayscriber");
write_file(&binary);
let path_env = tmp.path().display().to_string();

assert!(
other_installed_wayscriber_binaries(&binary, Some(&path_env), &[&binary]).is_empty()
);
}

#[test]
fn reports_a_second_file_on_path() {
let tmp = crate::test_temp::tempdir().unwrap();
let current_dir = tmp.path().join("current");
let other_dir = tmp.path().join("other");
fs::create_dir(&current_dir).unwrap();
fs::create_dir(&other_dir).unwrap();
let current = current_dir.join("wayscriber");
let other = other_dir.join("wayscriber");
write_file(&current);
write_file(&other);
let path_env = format!("{}:{}", other_dir.display(), current_dir.display());

let others = other_installed_wayscriber_binaries(&current, Some(&path_env), &[]);
assert_eq!(others, vec![other.canonicalize().unwrap()]);
}

#[test]
fn reports_a_well_known_copy_even_when_path_matches_current() {
let tmp = crate::test_temp::tempdir().unwrap();
let current_dir = tmp.path().join("local");
let usr_dir = tmp.path().join("usr");
fs::create_dir(&current_dir).unwrap();
fs::create_dir(&usr_dir).unwrap();
let current = current_dir.join("wayscriber");
let usr = usr_dir.join("wayscriber");
write_file(&current);
write_file(&usr);
let path_env = current_dir.display().to_string();

let others = other_installed_wayscriber_binaries(&current, Some(&path_env), &[&usr]);
assert_eq!(others, vec![usr.canonicalize().unwrap()]);
}

#[test]
fn symlink_to_the_same_file_is_not_a_conflict() {
let tmp = crate::test_temp::tempdir().unwrap();
let current = tmp.path().join("wayscriber");
let alias = tmp.path().join("alias-wayscriber");
write_file(&current);
symlink(&current, &alias).unwrap();

assert!(other_installed_wayscriber_binaries(&current, None, &[&alias]).is_empty());
}

#[test]
fn missing_current_exe_yields_no_others() {
let tmp = crate::test_temp::tempdir().unwrap();
let missing = tmp.path().join("missing");
let other = tmp.path().join("wayscriber");
write_file(&other);

assert!(other_installed_wayscriber_binaries(&missing, None, &[&other]).is_empty());
}

#[test]
fn well_known_includes_resolved_user_local_bin() {
let tmp = crate::test_temp::tempdir().unwrap();
crate::test_env::with_env_var(
crate::env_vars::HOME_ENV,
Some(tmp.path().as_os_str()),
|| {
let paths = well_known_wayscriber_binaries();
assert!(paths.contains(&PathBuf::from("/usr/bin/wayscriber")));
assert!(paths.contains(&PathBuf::from("/usr/local/bin/wayscriber")));
assert!(paths.contains(&tmp.path().join(".local").join("bin").join("wayscriber")));
},
);
}
}
1 change: 1 addition & 0 deletions src/daemon/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ impl Daemon {
/// Run daemon with signal handling
pub fn run(&mut self) -> Result<()> {
info!("Starting wayscriber daemon");
super::binary_conflict::warn_if_other_wayscriber_binaries();
if self.freeze_on_show {
info!("Daemon activations will request frozen mode on show");
}
Expand Down
1 change: 1 addition & 0 deletions src/daemon/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Daemon mode implementation: background service with toggle activation

mod binary_conflict;
mod control;
mod core;
mod global_shortcuts;
Expand Down
2 changes: 2 additions & 0 deletions tools/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
## Coupled Changes
- Version and packaging scripts must stay aligned with `tools/README.md`, `packaging/`, `.github/`, `Cargo.toml`, and release docs.
- Install/reload scripts may affect setup docs and daemon service behavior.
- `install.sh` and the website `arch-install.sh` must refuse a second unmanaged prefix
(`/usr` vs `/usr/local`) unless the operator opts into replacing the other copy.

## Validation
- Run changed scripts directly when safe.
Expand Down
8 changes: 7 additions & 1 deletion tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ Helper scripts for development, installation, packaging, and release workflows.

- **install.sh** - Full installation script
- Builds and installs binary to `/usr/bin` (or `$WAYSCRIBER_INSTALL_DIR`)
- Refuses a second copy under `/usr/bin`, `/usr/local/bin`, or `~/.local/bin` unless
`--replace-other` is passed or you confirm on a TTY
- Sets up config directory with example config
- Optionally configures systemd service or Hyprland autostart
- Usage: `./tools/install.sh`
- Usage: `./tools/install.sh [--replace-other]`

- **install-configurator.sh** - Install configurator only
- Builds and installs wayscriber-configurator, its desktop entry, and icons
Expand All @@ -60,6 +62,8 @@ Helper scripts for development, installation, packaging, and release workflows.
- Updates Cargo.toml, configurator/Cargo.toml, the workspace Cargo.lock, PKGBUILD, and .SRCINFO
- flake.nix package version follows Cargo.toml automatically
- Auto-increments patch version if no version specified
- Use this in the same change as a user-visible overlay/settings/config toggle, or immediately
before tagging that release, so `--version` is not identical to the last shipped crate
- Supports MAJOR.MINOR.PATCH.HOTFIX for packaging-only hotfix releases
- Usage: `./tools/bump-version.sh [--dry-run] [new_version]`

Expand Down Expand Up @@ -100,6 +104,8 @@ Packaging-only hotfix policy:
- Strictly parses the installer's static allowlist as data; it never executes the installer, and unsupported manifest syntax fails closed
- Requires the archive file set, modes, and service command to match what the installer accepts
- Runs against the deployed installer during release packaging
- Live `https://wayscriber.com/arch-install.sh` is what CI fetches. Dual-prefix flags in a
local website checkout are not checked until that file is published.
- Usage: `./tools/check-arch-installer-manifest.sh --installer FILE --archive FILE`

When the tarball file manifest changes, build and check the new tarball locally, deploy
Expand Down
Loading
Loading