1. Problem statement
When the user shares the screen (browser getDisplayMedia, OBS, Zoom, Teams,
etc.) the audience sees everything that crosses the X server, including:
- Toast notifications from
dunst — Slack/Element/Signal/Telegram/Discord
message previews, Thunderbird mail digests, notify-send from random user
scripts, USB/udiskie mount popups, network-manager-applet state changes,
Duplicati/nh backup notices, notify-send calls inside our own
i3-presentation-mode-adjacent scripts (power-profile-rofi,
dim-warning, toggle-logseq, …).
- The full
i3status-rust block row defined in
modules/apps/i3wm/status.nix:22-76 — net SSID, IP address, public
battery percentage, disk free, CPU temperature, time. Most of this is
effectively a privacy leak the moment a stranger sees the recording.
- Workspace urgency flashes from chat clients pinging while presenting,
driven by _NET_WM_STATE_DEMANDS_ATTENTION on the i3 bar.
The session also actively interferes with sharing:
xset is configured with a 3600s screensaver/DPMS in
modules/apps/i3wm/startup.nix:65-75. After an hour the audience sees
a black screen.
services.screen-locker (modules/apps/i3wm/services.nix:127-147) wires
xss-lock + xautolock with inactiveInterval = 15 minutes plus a
60-second dim-warning that drops the backlight to 80 nits before
locking. During a long screen share both fire — the screen dims, then
the lock screen appears mid-call.
- On
tpnix, services.logind.HandleLidSwitch = "suspend"
(modules/tpnix/services.nix:18) — closing the lid mid-share suspends
the host, which is fine policy outside a presentation but breaks
hand-offs between docked and undocked.
There is currently no toggle that addresses these as a coherent
unit. The user manually pauses dunst, manually xsets, and forgets one
of them every time.
2. Goal / intended behavior
Provide an opt-in, runtime-toggleable "presentation mode" that the
user enters before sharing the screen and exits afterwards.
Toggling presentation mode ON must, in a single keystroke, suppress
every notification surface that is allowed to draw on top of shared
content, and disable every idle/lock/sleep path that would interrupt a
live share. Toggling OFF must restore each setting to exactly the
value it had before, with no orphaned background processes.
The design must be:
- Transparent — the user can see whether the mode is on at any time
(initial: i3-presentation-mode status; future: i3status-rust block).
- Crash-safe — if the script dies mid-toggle the system never ends
up with notifications silently dropped forever or xautolock
permanently disabled.
- i3-restart-safe —
mod+Shift+r reloads i3 without breaking the
state. If presentation mode was ON before restart it stays ON.
- Reboot-safe (default OFF) — a fresh login starts with mode OFF
even if the user crashed/power-cycled while presenting.
3. Functional requirements
-
FR-1. A new Home Manager module
modules/apps/i3wm/presentation.nix exposes options under
gui.i3.presentation.* and adds a single binary,
i3-presentation-mode, to home.packages.
-
FR-2. Subcommands:
i3-presentation-mode on — enter mode (idempotent).
i3-presentation-mode off — leave mode (idempotent).
i3-presentation-mode toggle — flip based on state file.
i3-presentation-mode status — print on/off, exit 0 if ON,
1 if OFF (suitable as an i3status-rust custom block).
i3-presentation-mode resume — internal; re-applies live knobs if
the state file says ON. Wired into i3 startup (FR-9).
-
FR-3. State is a single file at
${XDG_RUNTIME_DIR}/i3-presentation-mode/state containing a JSON
blob with:
{ "active": true,
"started_at": "2026-04-30T18:31:00Z",
"saved": { "power_profile": "balanced", "bar_mode": "dock" } }
XDG_RUNTIME_DIR is tmpfs and clears at logout/reboot — gives the
reboot-safe default for free.
-
FR-4. Actions on enter (each toggle-able via gui.i3.presentation.actions.*):
| Key |
Default |
Mechanism |
pauseDunst |
true |
dunstctl set-paused true. Notification before pause via notify-send -u low -t 1500 "Presentation" "ON", then 0.3s sleep, then pause. Dunst queues messages received while paused; they are released on set-paused false. |
hideBar |
true |
i3-msg 'bar mode invisible' (no bar_id — applies to all bars; tray hides with bar). |
disableIdle |
true |
xset s off -dpms s noblank plus xautolock -disable plus systemd-inhibit --what=idle:sleep --who=presentation --why='screen sharing' --mode=block sleep infinity & (PID stored in state). The systemd inhibitor covers logind IdleAction paths that xset cannot reach. |
inhibitLidSuspend |
false |
tpnix only — adds handle-lid-switch:handle-lid-switch-ep:handle-lid-switch-docked to the systemd-inhibit --what list. Off by default to preserve the safety property "closing the lid suspends, period". |
setPowerProfile |
false |
Off by default because both hosts already pin Performance (modules/tpnix/services.nix:89-99, modules/system76/.*). Exposed for future hosts; when on, switches via system76-power profile performance or powerprofilesctl set performance and restores prior value on exit. |
hideCursor |
false |
Spawns unclutter --timeout 1 --jitter 5 (PID stored in state). Off by default; opt-in. |
notify |
true |
Pre/post notify-send so the user sees the transition on screen even though dunst gets paused immediately after. |
-
FR-5. Actions on exit: every action above is reversed in the
reverse order it was applied. Specifically:
dunstctl set-paused false.
i3-msg 'bar mode dock'.
xset s default +dpms (re-applies the i3-startup defaults of
3600s; presentation.nix recomputes the values from
gui.i3.idle.{screensaverSeconds,dpmsSeconds} rather than
hard-coding them).
xautolock -enable.
kill <inhibit-pid> for each saved PID.
kill <unclutter-pid> if hideCursor was on.
- Restore
power_profile from state if setPowerProfile was on.
- State file removed.
-
FR-6. Idempotence. Re-running on while ON: notify-send "already
on" at urgency=low, refresh started_at, no toggling of subsystems.
Re-running off while OFF: silent no-op.
-
FR-7. Crash safety. Each enter step is gated on "is the desired
end state already true?" — dunstctl is-paused, i3-msg -t get_bar_config | jq '.[0].mode', xset q | grep DPMS. The script
records each successful step into the state file before moving to
the next; off reads the state file and only reverses steps that
were actually applied. Partial-fail recovery: i3-presentation-mode off
while in a half-applied state still produces a coherent OFF.
-
FR-8. Default keybinding bound under
xsession.windowManager.i3.config.keybindings:
${mod}+Shift+F12 → exec --no-startup-id i3-presentation-mode toggle.
Configurable via gui.i3.presentation.keybinding (string, nullable —
setting null skips the binding entirely so users can wire their
own).
-
FR-9. i3 restart hook. A non-always = true startup entry:
i3-presentation-mode resume. Reads the state file; if active=true
re-asserts bar mode invisible, xset s off -dpms s noblank,
xautolock -disable, the systemd-inhibit lock, etc. Does not
re-pause dunst because dunst lives in a separate user service that
i3 restart doesn't touch — dunst stays paused across an i3 restart.
-
FR-10. Per-host enablement. New options live under
gui.i3.presentation. gui.i3.presentation.enable = true is set in
whichever module already defines gui.i3.* for the i3 hosts (today:
every host that imports flake.homeManagerModules.apps.i3-config).
Hosts that disable i3 will also disable this — gated by the existing
lib.mkIf i3Enabled pattern in modules/apps/i3wm/services.nix:18.
4. Non-functional requirements
- NFR-1. Implementation language: bash via
pkgs.writeShellApplication.
No new flake inputs. Runtime tools already in nixpkgs:
coreutils, dunst, i3, libnotify, xorg.xset, xautolock, systemd, jq (state file parsing). Optional: unclutter-xfixes, system76-power | power-profiles-daemon.
- NFR-2. No new system-level (
environment.systemPackages) entries
unless absolutely required — the script ships via Home Manager
(home.packages) like power-profile-rofi does
(modules/apps/i3wm/config.nix:417-423).
- NFR-3. No
lib.mkOverride 1100 or mkForce on existing options
— additive only.
- NFR-4. State file is
0600 and lives under $XDG_RUNTIME_DIR,
which is per-user tmpfs. No secrets are written.
- NFR-5. Honors the existing repo invariant:
nix flake check --accept-flake-config must pass after the change. The new module
declares its options, so any host that doesn't import the i3 module
is unaffected.
- NFR-6. No race with
xss-lock's sleep transfer (-l flag,
modules/apps/i3wm/services.nix:135). Sleep-transfer fires on
systemd PrepareForSleep and is separate from the idle path. The
presentation-mode systemd-inhibit uses --what=idle:sleep which
blocks the suspend path during a screen share — when the user
manually triggers suspend (mod+Shift+e →
systemctl suspend) the inhibit is released by kill during off,
and the lock-on-suspend safety property is preserved.
5. System interactions / data flow
┌─────────────────────────┐
keybind ───▶│ i3-presentation-mode │
(mod+Shift+F12) │ on | off | toggle | │
│ status | resume │
└────┬────────┬────────┬──┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌────────┐ ┌─────────────────────┐
│ dunstctl │ │ i3-msg │ │ xset / xautolock / │
│ set-paused │ │ bar │ │ systemd-inhibit / │
│ true|false │ │ mode │ │ unclutter / pp-ctl │
└─────────────────┘ └────────┘ └──────────┬──────────┘
│
state file ◀──────────────┘
$XDG_RUNTIME_DIR/i3-presentation-mode/state
External services touched: none. All effects are local user-session
state. No D-Bus calls beyond what dunstctl and systemd-inhibit
already do.
6. Module design (proposed file layout)
modules/apps/i3wm/
├── config.nix # unchanged (defines gui.i3.* base options)
├── keybindings.nix # unchanged
├── nixos.nix # unchanged
├── presentation.nix # NEW — script, options, keybinding, startup hook
├── scratchpad.nix # unchanged
├── services.nix # unchanged
├── startup.nix # unchanged (presentation hooks live in presentation.nix)
├── status.nix # later: optional custom block (out of scope for v1)
└── window-rules.nix # unchanged
Skeleton:
# modules/apps/i3wm/presentation.nix
{
flake.homeManagerModules.apps.i3-config =
{
config,
lib,
pkgs,
osConfig ? { },
...
}:
let
cfg = config.gui.i3.presentation;
mod = lib.attrByPath [ "xsession" "windowManager" "i3" "config" "modifier" ] "Mod4" config;
powerBackend = lib.attrByPath
[ "gui" "i3" "powerProfiles" "backend" ] "powerprofilesctl" osConfig;
presentationScript = pkgs.writeShellApplication {
name = "i3-presentation-mode";
runtimeInputs = [
pkgs.coreutils pkgs.dunst pkgs.i3 pkgs.libnotify
pkgs.xorg.xset pkgs.xautolock pkgs.systemd pkgs.jq
]
++ lib.optionals cfg.actions.hideCursor [ pkgs.unclutter-xfixes ]
++ lib.optionals cfg.actions.setPowerProfile (
if powerBackend == "system76-power"
then [ pkgs.system76-power pkgs.gawk pkgs.gnugrep ]
else [ pkgs.power-profiles-daemon ]);
text = ''…''; # see FR-2..FR-7
};
in {
options.gui.i3.presentation = {
enable = lib.mkOption {
type = lib.types.bool;
default = config.xsession.windowManager.i3.enable or false;
description = "Install the i3 presentation-mode toggle.";
};
keybinding = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "${mod}+Shift+F12";
description = "i3 keybinding that toggles presentation mode. null disables.";
};
actions = {
pauseDunst = lib.mkEnableOption "dunst pause" // { default = true; };
hideBar = lib.mkEnableOption "i3 bar hide" // { default = true; };
disableIdle = lib.mkEnableOption "idle/DPMS/xautolock/systemd-inhibit" // { default = true; };
inhibitLidSuspend = lib.mkEnableOption "logind lid-switch inhibit";
setPowerProfile = lib.mkEnableOption "force performance power profile";
hideCursor = lib.mkEnableOption "unclutter cursor hide";
notify = lib.mkEnableOption "transition notifications" // { default = true; };
};
};
config = lib.mkIf cfg.enable {
home.packages = [ presentationScript ];
xsession.windowManager.i3.config = {
keybindings = lib.mkIf (cfg.keybinding != null) (lib.mkOptionDefault {
"${cfg.keybinding}" =
"exec --no-startup-id ${lib.getExe presentationScript} toggle";
});
startup = lib.mkAfter [{
command = "${lib.getExe presentationScript} resume";
always = false;
notification = false;
}];
};
};
};
}
7. Alternatives considered
-
Per-host wrapper script under scripts/ — rejected. Other i3
helpers (toggle-logseq, power-profile-rofi,
i3-scratchpad-show-or-create) are all packaged via Home Manager
derivations beside modules/apps/i3wm/. Symmetry wins.
-
Replace dunst with mako / a Wayland flow — out of scope. This
repo is committed to X11 + dunst (modules/apps/dunst.nix:53,
modules/apps/i3wm/services.nix:111).
-
Auto-detect screen sharing via xdg-desktop-portal ScreenCast
signals — does not work for X11 in this setup. Browsers running
on i3 use the X11 getDisplayMedia path directly, not the portal.
xdg-desktop-portal-gtk is wired in
modules/system76/gsettings.nix / modules/tpnix/gsettings.nix only
for screenshot fallback. Auto-detect would require either OBS-only
process polling or X11 window-property scrapes, both fragile. Manual
toggle is the honest design.
-
Use a dedicated i3 mode (mode "presentation" like resize/gaps)
— rejected. Modes hijack the global keymap; the user wants to keep
using normal keybinds while presenting.
-
Use the caffeine/caffeine-ng package — overlapping but not
identical: caffeine only handles xset + screensaver, not dunst,
not i3 bar, not the bundled state we need.
-
Force force_display_urgency_hint 0 to silence workspace
flashes — force_display_urgency_hint is a config-time directive,
not a runtime command (verified in i3 user guide). Rewriting i3
config and reloading mid-presentation is too invasive. Pausing
dunst already silences the noise; the bar is hidden so the flash is
invisible regardless.
8. Test plan
9. Open questions / future work
- Q1. Should there be a rofi entry (next to
power-profile-rofi)
with explicit On / Off / Status choices? Proposed: yes, in v2.
Cheap to add once the script is in place.
- Q2. Should
programs.i3status-rust.bars.default.blocks grow a
custom block driven by i3-presentation-mode status so the bar
itself shows a red "PRES" marker when active (visible only when
hideBar = false)? Proposed: yes, in v2.
- Q3. Should we hook OBS / SimpleScreenRecorder lifecycle to
auto-toggle? Out of scope for v1; tracked separately.
- Q4. Should we expose a
gui.i3.presentation.profiles attrset
(e.g. talk, pair-programming, demo) where each profile
pre-configures different action sets? Probably premature; revisit
after a few months of v1 use.
- Q5. Should webcam / microphone state be displayed (e.g.
pactl list source-outputs polled into a status block)? Separate
RFC.
Labels (suggested)
type(enhancement)
area(home-manager)
host(system76)
host(tpnix)
priority(p3)
focus(hardening) — privacy/notification-leak surface
1. Problem statement
When the user shares the screen (browser
getDisplayMedia, OBS, Zoom, Teams,etc.) the audience sees everything that crosses the X server, including:
dunst— Slack/Element/Signal/Telegram/Discordmessage previews, Thunderbird mail digests,
notify-sendfrom random userscripts, USB/
udiskiemount popups, network-manager-applet state changes,Duplicati/
nhbackup notices,notify-sendcalls inside our owni3-presentation-mode-adjacent scripts (power-profile-rofi,dim-warning,toggle-logseq, …).i3status-rustblock row defined inmodules/apps/i3wm/status.nix:22-76—netSSID, IP address, publicbattery percentage, disk free, CPU temperature, time. Most of this is
effectively a privacy leak the moment a stranger sees the recording.
driven by
_NET_WM_STATE_DEMANDS_ATTENTIONon the i3 bar.The session also actively interferes with sharing:
xsetis configured with a 3600s screensaver/DPMS inmodules/apps/i3wm/startup.nix:65-75. After an hour the audience seesa black screen.
services.screen-locker(modules/apps/i3wm/services.nix:127-147) wiresxss-lock+xautolockwithinactiveInterval = 15minutes plus a60-second
dim-warningthat drops the backlight to 80 nits beforelocking. During a long screen share both fire — the screen dims, then
the lock screen appears mid-call.
tpnix,services.logind.HandleLidSwitch = "suspend"(
modules/tpnix/services.nix:18) — closing the lid mid-share suspendsthe host, which is fine policy outside a presentation but breaks
hand-offs between docked and undocked.
There is currently no toggle that addresses these as a coherent
unit. The user manually pauses dunst, manually
xsets, and forgets oneof them every time.
2. Goal / intended behavior
Provide an opt-in, runtime-toggleable "presentation mode" that the
user enters before sharing the screen and exits afterwards.
The design must be:
(initial:
i3-presentation-mode status; future: i3status-rust block).up with notifications silently dropped forever or
xautolockpermanently disabled.
mod+Shift+rreloads i3 without breaking thestate. If presentation mode was ON before restart it stays ON.
even if the user crashed/power-cycled while presenting.
3. Functional requirements
FR-1. A new Home Manager module
modules/apps/i3wm/presentation.nixexposes options undergui.i3.presentation.*and adds a single binary,i3-presentation-mode, tohome.packages.FR-2. Subcommands:
i3-presentation-mode on— enter mode (idempotent).i3-presentation-mode off— leave mode (idempotent).i3-presentation-mode toggle— flip based on state file.i3-presentation-mode status— printon/off, exit0if ON,1if OFF (suitable as an i3status-rust custom block).i3-presentation-mode resume— internal; re-applies live knobs ifthe state file says ON. Wired into i3 startup (FR-9).
FR-3. State is a single file at
${XDG_RUNTIME_DIR}/i3-presentation-mode/statecontaining a JSONblob with:
{ "active": true, "started_at": "2026-04-30T18:31:00Z", "saved": { "power_profile": "balanced", "bar_mode": "dock" } }XDG_RUNTIME_DIRistmpfsand clears at logout/reboot — gives thereboot-safe default for free.
FR-4. Actions on enter (each toggle-able via
gui.i3.presentation.actions.*):pauseDunsttruedunstctl set-paused true. Notification before pause vianotify-send -u low -t 1500 "Presentation" "ON", then 0.3s sleep, then pause. Dunst queues messages received while paused; they are released onset-paused false.hideBartruei3-msg 'bar mode invisible'(nobar_id— applies to all bars; tray hides with bar).disableIdletruexset s off -dpms s noblankplusxautolock -disableplussystemd-inhibit --what=idle:sleep --who=presentation --why='screen sharing' --mode=block sleep infinity &(PID stored in state). The systemd inhibitor covers logindIdleActionpaths thatxsetcannot reach.inhibitLidSuspendfalsetpnixonly — addshandle-lid-switch:handle-lid-switch-ep:handle-lid-switch-dockedto the systemd-inhibit--whatlist. Off by default to preserve the safety property "closing the lid suspends, period".setPowerProfilefalsemodules/tpnix/services.nix:89-99,modules/system76/.*). Exposed for future hosts; when on, switches viasystem76-power profile performanceorpowerprofilesctl set performanceand restores prior value on exit.hideCursorfalseunclutter --timeout 1 --jitter 5(PID stored in state). Off by default; opt-in.notifytruenotify-sendso the user sees the transition on screen even though dunst gets paused immediately after.FR-5. Actions on exit: every action above is reversed in the
reverse order it was applied. Specifically:
dunstctl set-paused false.i3-msg 'bar mode dock'.xset s default +dpms(re-applies the i3-startup defaults of3600s;
presentation.nixrecomputes the values fromgui.i3.idle.{screensaverSeconds,dpmsSeconds}rather thanhard-coding them).
xautolock -enable.kill <inhibit-pid>for each saved PID.kill <unclutter-pid>if hideCursor was on.power_profilefrom state ifsetPowerProfilewas on.FR-6. Idempotence. Re-running
onwhile ON:notify-send"alreadyon" at
urgency=low, refreshstarted_at, no toggling of subsystems.Re-running
offwhile OFF: silent no-op.FR-7. Crash safety. Each enter step is gated on "is the desired
end state already true?" —
dunstctl is-paused,i3-msg -t get_bar_config | jq '.[0].mode',xset q | grep DPMS. The scriptrecords each successful step into the state file before moving to
the next;
offreads the state file and only reverses steps thatwere actually applied. Partial-fail recovery:
i3-presentation-mode offwhile in a half-applied state still produces a coherent OFF.
FR-8. Default keybinding bound under
xsession.windowManager.i3.config.keybindings:${mod}+Shift+F12 → exec --no-startup-id i3-presentation-mode toggle.Configurable via
gui.i3.presentation.keybinding(string, nullable —setting
nullskips the binding entirely so users can wire theirown).
FR-9. i3 restart hook. A non-
always = truestartup entry:i3-presentation-mode resume. Reads the state file; ifactive=truere-asserts
bar mode invisible,xset s off -dpms s noblank,xautolock -disable, thesystemd-inhibitlock, etc. Does notre-pause dunst because dunst lives in a separate user service that
i3 restart doesn't touch — dunst stays paused across an i3 restart.
FR-10. Per-host enablement. New options live under
gui.i3.presentation.gui.i3.presentation.enable = trueis set inwhichever module already defines
gui.i3.*for the i3 hosts (today:every host that imports
flake.homeManagerModules.apps.i3-config).Hosts that disable i3 will also disable this — gated by the existing
lib.mkIf i3Enabledpattern inmodules/apps/i3wm/services.nix:18.4. Non-functional requirements
pkgs.writeShellApplication.No new flake inputs. Runtime tools already in nixpkgs:
coreutils, dunst, i3, libnotify, xorg.xset, xautolock, systemd, jq(state file parsing). Optional:unclutter-xfixes, system76-power | power-profiles-daemon.environment.systemPackages) entriesunless absolutely required — the script ships via Home Manager
(
home.packages) likepower-profile-rofidoes(
modules/apps/i3wm/config.nix:417-423).lib.mkOverride 1100ormkForceon existing options— additive only.
0600and lives under$XDG_RUNTIME_DIR,which is per-user
tmpfs. No secrets are written.nix flake check --accept-flake-configmust pass after the change. The new moduledeclares its options, so any host that doesn't import the i3 module
is unaffected.
xss-lock's sleep transfer (-lflag,modules/apps/i3wm/services.nix:135). Sleep-transfer fires onsystemd
PrepareForSleepand is separate from the idle path. Thepresentation-mode
systemd-inhibituses--what=idle:sleepwhichblocks the suspend path during a screen share — when the user
manually triggers suspend (
mod+Shift+e→systemctl suspend) the inhibit is released bykillduringoff,and the lock-on-suspend safety property is preserved.
5. System interactions / data flow
External services touched: none. All effects are local user-session
state. No D-Bus calls beyond what
dunstctlandsystemd-inhibitalready do.
6. Module design (proposed file layout)
Skeleton:
7. Alternatives considered
Per-host wrapper script under
scripts/— rejected. Other i3helpers (
toggle-logseq,power-profile-rofi,i3-scratchpad-show-or-create) are all packaged via Home Managerderivations beside
modules/apps/i3wm/. Symmetry wins.Replace dunst with
mako/ a Wayland flow — out of scope. Thisrepo is committed to X11 + dunst (
modules/apps/dunst.nix:53,modules/apps/i3wm/services.nix:111).Auto-detect screen sharing via xdg-desktop-portal
ScreenCastsignals — does not work for X11 in this setup. Browsers running
on i3 use the X11
getDisplayMediapath directly, not the portal.xdg-desktop-portal-gtkis wired inmodules/system76/gsettings.nix/modules/tpnix/gsettings.nixonlyfor screenshot fallback. Auto-detect would require either OBS-only
process polling or X11 window-property scrapes, both fragile. Manual
toggle is the honest design.
Use a dedicated i3 mode (
mode "presentation"like resize/gaps)— rejected. Modes hijack the global keymap; the user wants to keep
using normal keybinds while presenting.
Use the
caffeine/caffeine-ngpackage — overlapping but notidentical:
caffeineonly handlesxset+ screensaver, not dunst,not i3 bar, not the bundled state we need.
Force
force_display_urgency_hint 0to silence workspaceflashes —
force_display_urgency_hintis a config-time directive,not a runtime command (verified in i3 user guide). Rewriting i3
config and reloading mid-presentation is too invasive. Pausing
dunst already silences the noise; the bar is hidden so the flash is
invisible regardless.
8. Test plan
nix flake check --accept-flake-config --no-build --offlinepasses.
nix build .#nixosConfigurations.tpnix.config.system.build.toplevelbuilds.
nix build .#nixosConfigurations.system76.config.system.build.toplevelbuilds.
i3-presentation-mode statusprintsoffafter a fresh login.mod+Shift+F12(toggle on):dunstctl is-paused→true.i3-msg -t get_bar_config bar-0 | jq -r .mode→invisible.xset q | grep DPMS→DPMS is Disabled.xautolock -toggle(the introspection variant) confirmsdisabled.systemctl --user list-units 'session-*' --type=scopeshows theinhibit lock under
Inhibited:.mod+Shift+F12(toggle off): every check above reverses.notify-send -u normal "test" "queued"while ON. Confirmnothing pops. Toggle off. Confirm dunst replays the queued one
from history (
dunstctl history-pop).mod+Shift+r(i3 restart) while ON: bar comes back hidden,xsetknobs re-applied — verified byxset qandi3-msg -t get_bar_config.(xss-lock sleep-transfer path is intact). After unlocking, mode
is still ON.
cleared with tmpfs).
i3-presentation-mode ontwice in a row: second invocationlogs "already on", state file
started_atupdates.i3-presentation-mode offwhile never having toggled on:silent no-op, exit 0.
on(pkill -9 i3-presentation-mode) andthen run
off: every applied step is reversed; the partialstate is detectable via the per-step state file flags.
9. Open questions / future work
power-profile-rofi)with explicit
On / Off / Statuschoices? Proposed: yes, in v2.Cheap to add once the script is in place.
programs.i3status-rust.bars.default.blocksgrow acustomblock driven byi3-presentation-mode statusso the baritself shows a red "PRES" marker when active (visible only when
hideBar = false)? Proposed: yes, in v2.auto-toggle? Out of scope for v1; tracked separately.
gui.i3.presentation.profilesattrset(e.g.
talk,pair-programming,demo) where each profilepre-configures different action sets? Probably premature; revisit
after a few months of v1 use.
pactl list source-outputspolled into a status block)? SeparateRFC.
Labels (suggested)
type(enhancement)area(home-manager)host(system76)host(tpnix)priority(p3)focus(hardening)— privacy/notification-leak surface