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
73 changes: 73 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: ci

on:
push:
branches: [main]
pull_request:

env:
CARGO_TERM_COLOR: always

jobs:
fmt:
name: rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --check

linux:
name: linux (test + clippy)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2

- name: cargo test (default features)
run: cargo test

- name: cargo test --features linux-x11
run: cargo test --features linux-x11

# Wayland has no runtime on CI; compile-check the module only.
- name: cargo check --features linux-wayland
run: cargo check --features linux-wayland

- name: cargo clippy --all-targets
run: cargo clippy --all-targets -- -D warnings

# The feature model supports a bare lib build; lint it too.
- name: cargo clippy --no-default-features --lib
run: cargo clippy --no-default-features --lib -- -D warnings

windows-gnu-cross:
name: windows-gnu (cross check)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-gnu
- uses: Swatinem/rust-cache@v2
# `check` type-checks the cfg(windows) backend without needing a MinGW
# linker to actually link a binary.
- name: cargo check --target x86_64-pc-windows-gnu
run: cargo check --target x86_64-pc-windows-gnu

windows:
name: windows (MSVC test)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# Builds the cfg(windows) backend for real. The daemon_lifecycle suite
# drives run_daemon via MockBackend, so it runs cross-platform.
- name: cargo test
run: cargo test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ target
# Contains mutation testing data
**/mutants.out*/

# Stray build artifacts that leaked into the repo root (never tracked).
/rust_out
/stdin

# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
Expand Down
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Changelog

All notable changes to Oxiwake are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

_Nothing yet._

## [0.1.0] - 2026-07-21

Initial public release.

### Added

- **CLI** (`ow`): the verbs `on`, `off`, `toggle`, `status`, and `doctor`, plus
the global flags `--json` / `-j` and `--verbose` / `-v` (repeatable). The
`on` / `toggle` verbs take `--display` (also keep the display on),
`--aggressive-lid` (add `handle-lid-switch`; usually needs privilege), and
`--reason <text>`.
- **Linux backends** (priority order): `systemd-logind` D-Bus inhibitor (main),
XDG Desktop Portal inhibit, GNOME SessionManager, KDE PowerDevil / Solid, and
`org.freedesktop.ScreenSaver` (idle-only). All ship on by default and ride on
raw `zbus` (fully synchronous, no async runtime). X11 XScreenSaver / DPMS is
available behind the `linux-x11` feature, and Wayland idle-inhibit behind the
`linux-wayland` feature.
- **Windows backends**: `PowerCreateRequest` + `PowerSetRequest` (preferred) with
a `SetThreadExecutionState` fallback, via `windows-sys`.
- **Daemon model**: a small detached daemon holds the RAII `WakeGuard` for the
lock's whole lifetime (the guard's `Drop` closes the logind inhibitor FD on
Linux / clears the power request on Windows), so the lock's lifetime is tied
to the guard's — leak-safe by construction. A singleton advisory lock
(`oxiwake.lock`) ensures two racing `ow on` invocations can never both reach
OS-lock acquisition, and a per-invocation `pending.{pid}.json` hand-off
(keyed on the CLI's own pid) lets concurrent `ow on` invocations never
clobber each other's request.
- **`ow doctor`**: honest environment + per-backend diagnostics. Each backend
reports `supported` (compiled in) and `available` (reachable now), plus the
**guarantees** it cannot promise — e.g. that a logind `block` on
`shutdown`/`sleep`/`handle-*` typically needs a PolicyKit privilege (`idle`
is the one routinely allowed unprivileged), and that on Windows Modern
Standby / battery system/execution requests can be terminated ~5 minutes
after the sleep timeout and user-initiated sleep clears requests.
- Cross-platform design docs in [`docs/setup.md`](docs/setup.md).

### Known limitations

- **Wayland acquire is unavailable.** The `zwp_idle_inhibit_manager_v1` protocol
requires a `wl_surface`, and a headless daemon has none, so the
`linux-wayland` feature compiles and is probed by `ow doctor` but does not
provide an acquire path. Wayland users should rely on the D-Bus backends
(logind / portal / ScreenSaver).
- **Backend selection falls back across the priority list.** Oxiwake tries each
compiled-in Linux backend in priority order and uses the first that acquires
the lock; it does not (and should not) special-case distro names.
- `crates.io` publication is planned but not yet done; v0.1.0 is build-from-source
(see the README's [Installation](README.md#installation) section).

[Unreleased]: https://github.com/freeoxide/wake/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/freeoxide/wake/releases/tag/v0.1.0
116 changes: 116 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Contributing to Oxiwake

Thanks for digging in. Oxiwake is a cross-platform "keep awake" CLI/daemon
(`ow`) written in Rust 2021 (MSRV **1.85**). This file covers how to build,
how to run the full verification matrix, the feature model, the shared
target-directory convention, where the architecture lives, and the commit
style the repo uses.

## Building & verifying

**Do not** check in a local `target/` directory. This repo uses a shared
target directory so concurrent builds (and multiple agents) don't fight:

```bash
export CARGO_TARGET_DIR=~/.cache/cargo-target
```

Put that in your shell profile so every `cargo` invocation shares one build
cache. There is deliberately no per-repo `target/`.

The full verification matrix (please run **all** of these before pushing):

```bash
# Formatting — must be clean.
cargo fmt --check

# Lint the default feature set (all D-Bus backends).
cargo clippy --all-targets -- -D warnings

# Lint with D-Bus backends OFF — guards against code that only compiles
# when the default features are on (e.g. a stray `mut`, an unused import).
cargo clippy --no-default-features --lib -- -D warnings

# Tests.
cargo test
cargo test --features linux-x11

# The Wayland feature must at least type-check (it has no acquire path; see
# "Known limitations" — a check, not a test, is the right bar).
cargo check --features linux-wayland

# The Windows backend + named-pipe transport type-check from Linux without a
# Windows toolchain. (Final linking needs mingw-w64; `cargo check` exercises
# the full windows-sys type-check without it.)
rustup target add x86_64-pc-windows-gnu
cargo check --target x86_64-pc-windows-gnu
```

Every one of these is expected to pass with zero warnings under `-D warnings`.
CI runs the same matrix, so if it's green locally it'll be green upstream.

### Runtime notes for tests

This host (and CI) has **no Polkit, no X server, no Windows runtime**. Every
test is therefore either pure logic or build-gated behind the correct
`cfg(feature = "...")` / `cfg(target_os = "...")` and **never** requires a live
D-Bus / X / Windows session. Daemon and acquire paths are exercised through a
`MockBackend`; platform-specific code lives behind the right `cfg`. If you add
a test that needs a real session, you've written the wrong test.

## Feature model

Oxiwake's feature model is "backend support", mirroring
[`docs/setup.md`](docs/setup.md):

- **Default** (`cargo build`): every D-Bus backend on Linux — `linux-logind`,
`linux-portal`, `linux-screensaver`, `linux-gnome`, `linux-kde` — all riding
on `zbus` (fully synchronous, no async runtime, no extra deps).
- **`linux-x11`** (opt-in): X11 XScreenSaver / DPMS via `x11rb`. Pulls a
heavier native dep, so it's off by default.
- **`linux-wayland`** (opt-in): Wayland idle-inhibit via `wayland-client` +
`wayland-protocols`. Compiles and is probed by `ow doctor`, but has no
acquire path — a headless daemon has no `wl_surface`. See "Known
limitations" in the [CHANGELOG](CHANGELOG.md).

Windows backends are unconditional on Windows (gated by `cfg(windows)`, not a
feature). Do not add a feature that re-points a backend to a different crate
without updating `docs/setup.md` to match.

## Where the architecture lives

Before changing anything load-bearing, read:

- **[`src/daemon.rs`](src/daemon.rs)** — the module-level doc-comments are the
authoritative description of the lock-ownership invariant (the RAII guard is
held for the daemon's whole lifetime; its `Drop` closes the logind FD /
clears the power request), the singleton file lock, and the per-invocation
`pending.{pid}.json` hand-off.
- **[`docs/setup.md`](docs/setup.md)** — the backend strategy, priority order,
dependency rationale, state/IPC layout, and the `ow doctor` checklists.
- **README "Project layout"** — a one-line-per-file map of `src/`.

If you change the backend priority order, the runtime file layout, or any D-Bus
signature, update all three (code doc-comments, `docs/setup.md`, README) so
they stay in sync.

## Commit style

This repo uses [Conventional Commits](https://www.conventionalcommits.org/).
See `git log --oneline` for the established prefixes:

- `feat:` — a new capability (e.g. `feat: implement Oxiwake keep-awake CLI/daemon`)
- `fix:` — a bug fix, often scoped (`fix(daemon): ...`, `fix(wayland): ...`,
`fix(windows): ...`, `fix(backend): ...`, `fix(platform): ...`)
- `docs:` — documentation only (`docs(setup): ...`)
- `test:` — test additions/fixes (`test(x11): ...`)
- `chore:` — tooling, CI, housekeeping

The scope, when present, names the area (`daemon`, `platform`, `backend`,
`wayland`, `x11`, `windows`, `setup`, …). Keep the subject line imperative and
under ~72 characters; put the "why" in the body.

## License

MIT. By contributing you agree your contributions are licensed under the same
terms.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ edition = "2021"
rust-version = "1.85"
description = "Keep your machine awake where the OS allows it — and tell you when it cannot."
license = "MIT"
repository = "https://github.com/freeoxide/oxiwake"
repository = "https://github.com/freeoxide/wake"
readme = "README.md"
authors = ["Freeoxide"]
keywords = ["caffeinate", "wake-lock", "systemd", "power", "keep-awake"]
categories = ["command-line-utilities"]

Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Oxiwake

[![CI](https://github.com/freeoxide/wake/actions/workflows/ci.yml/badge.svg)](https://github.com/freeoxide/wake/actions/workflows/ci.yml)

> Oxiwake keeps your machine awake where the OS allows it — and tells you when it cannot.

Oxiwake (`ow`) is a cross-platform "keep awake" CLI/daemon. It holds a wake lock
Expand Down Expand Up @@ -51,10 +53,17 @@ the core, leak-safe design.
State lives in:

```
Linux: $XDG_RUNTIME_DIR/oxiwake/{state.json, oxiwake.sock, pending.json}
Windows: %LOCALAPPDATA%\Freeoxide\Oxiwake\...
Linux: $XDG_RUNTIME_DIR/oxiwake/{state.json, oxiwake.sock, pending.{pid}.json, oxiwake.lock}
Windows: %LOCALAPPDATA%\Freeoxide\Oxiwake\{state.json, oxiwake.sock, pending.{pid}.json, oxiwake.lock}
```

`state.json` is the persistent lock snapshot; `pending.{pid}.json` is the
per-invocation (pid-keyed) request hand-off from CLI to daemon; `oxiwake.lock`
is the singleton advisory lock (`flock` on Linux, `LockFileEx` on Windows) the
daemon holds for its whole lifetime so two racing `ow on` invocations can never
both reach OS-lock acquisition. All of these live in a tmpfs and are wiped on
logout/reboot, which is fine for transient lock state.

## Usage

```
Expand Down Expand Up @@ -93,6 +102,21 @@ Options:
If `ow on` cannot take the lock (e.g. the OS refuses), it reports the **real**
reason rather than hanging.

## Installation

Oxiwake is at **v0.1.0**. There is no `crates.io` package yet (a publish is
planned), so the primary path today is **build from source**, covered in
[Building](#building) below. If you have a Rust toolchain, the quickest one-off
install is from git:

```bash
cargo install --git https://github.com/freeoxide/wake
```

Prebuilt binaries for each release are the intended future path and will be
attached to [GitHub Releases](https://github.com/freeoxide/wake/releases) once
available. Until then, build from source.

## Building

```bash
Expand Down
49 changes: 49 additions & 0 deletions src/backend/gnome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,52 @@ fn name_has_owner(conn: &zbus::blocking::Connection, name: &str) -> Result<bool>
.map_err(|e| OxiwakeError::DbusDecode(format!("NameHasOwner bool: {e}")))?;
Ok(owner)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn name_and_supported() {
let b = GnomeBackend::new();
assert_eq!(b.name(), NAME);
assert_eq!(b.name(), "gnome-session");
assert!(b.supported());
// Default() must agree with new() — both are no-I/O handles.
assert_eq!(GnomeBackend.name(), NAME);
}

#[test]
fn status_reports_suspend_and_idle_block() {
// status() is a static report (no D-Bus): suspend+idle scope, Block mode.
let s = GnomeBackend::new().status().expect("status");
assert_eq!(s.backend, NAME);
assert_eq!(s.mode, WakeMode::Block);
assert!(!s.display);
assert!(s.targets.contains(&WakeTarget::SystemSleep));
assert!(s.targets.contains(&WakeTarget::Idle));
// GNOME SessionManager does not field lid/shutdown here.
assert!(!s.targets.contains(&WakeTarget::LidSwitch));
}

#[test]
fn doctor_returns_ok_with_fixed_name_and_guarantees() {
// doctor() probes the session bus; on a host with no session bus (or no
// GNOME service) it must still return Ok with the fixed name/guarantees.
// `available` legitimately varies, so we assert only the stable fields.
let report = GnomeBackend::new().doctor().expect("doctor");
assert_eq!(report.backend, NAME);
assert!(report.supported);
let joined = report.guarantees.join(" | ");
// The flags bitmask caveat and the session-level (not kernel) caveat
// are the load-bearing guarantees — pin them so `ow doctor` stays honest.
assert!(
joined.contains("4|8") || joined.contains("4 | 8"),
"guarantees must cite the GNOME flags bitmask, got: {joined}"
);
assert!(
joined.contains("session-level"),
"guarantees must disclose session-level scope, got: {joined}"
);
}
}
Loading
Loading