diff --git a/crates/shepherd-launcher-ui/src/main.rs b/crates/shepherd-launcher-ui/src/main.rs index 054a3cf..706ee09 100644 --- a/crates/shepherd-launcher-ui/src/main.rs +++ b/crates/shepherd-launcher-ui/src/main.rs @@ -9,8 +9,10 @@ mod grid; mod state; mod tile; +use crate::client::CommandClient; use anyhow::Result; use clap::Parser; +use shepherd_api::{ErrorCode, ResponsePayload, ResponseResult}; use shepherd_util::default_socket_path; use std::path::PathBuf; use tracing_subscriber::EnvFilter; @@ -27,6 +29,10 @@ struct Args { /// Log level #[arg(short, long, default_value = "info")] log_level: String, + + /// Send StopCurrent to shepherdd and exit (for compositor keybindings) + #[arg(long)] + stop_current: bool, } fn main() -> Result<()> { @@ -35,8 +41,7 @@ fn main() -> Result<()> { // Initialize logging tracing_subscriber::fmt() .with_env_filter( - EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new(&args.log_level)), + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)), ) .init(); @@ -45,6 +50,33 @@ fn main() -> Result<()> { // Determine socket path with fallback to default let socket_path = args.socket.unwrap_or_else(default_socket_path); + if args.stop_current { + let runtime = tokio::runtime::Runtime::new()?; + runtime.block_on(async move { + let client = CommandClient::new(&socket_path); + match client.stop_current().await { + Ok(response) => match response.result { + ResponseResult::Ok(ResponsePayload::Stopped) => { + tracing::info!("StopCurrent succeeded"); + Ok(()) + } + ResponseResult::Err(err) if err.code == ErrorCode::NoActiveSession => { + tracing::debug!("No active session to stop"); + Ok(()) + } + ResponseResult::Err(err) => { + anyhow::bail!("StopCurrent failed: {}", err.message) + } + ResponseResult::Ok(payload) => { + anyhow::bail!("Unexpected StopCurrent response: {:?}", payload) + } + }, + Err(e) => anyhow::bail!("Failed to send StopCurrent: {}", e), + } + })?; + return Ok(()); + } + // Run GTK application let application = app::LauncherApp::new(socket_path); let exit_code = application.run(); diff --git a/docs/ai/history/2026-02-08 001 sway stopcurrent keybinds.md b/docs/ai/history/2026-02-08 001 sway stopcurrent keybinds.md new file mode 100644 index 0000000..6f6cb91 --- /dev/null +++ b/docs/ai/history/2026-02-08 001 sway stopcurrent keybinds.md @@ -0,0 +1,15 @@ +# Sway StopCurrent Keybinds + +Prompt summary: +- Move keyboard exit handling out of launcher UI code and into `sway.conf`. +- Keep controller home behavior as-is. +- Ensure "exit" uses the API (`StopCurrent`) rather than closing windows directly. + +Implemented summary: +- Added a `--stop-current` mode to `shepherd-launcher` that sends `StopCurrent` to shepherdd over IPC and exits. +- Added Sway keybindings for `Alt+F4`, `Ctrl+W`, and `Home` that execute `shepherd-launcher --stop-current`. +- Kept controller home behavior in launcher UI unchanged. + +Key files: +- `sway.conf` +- `crates/shepherd-launcher-ui/src/main.rs` diff --git a/sway.conf b/sway.conf index de67a74..15dd11e 100644 --- a/sway.conf +++ b/sway.conf @@ -11,6 +11,7 @@ exec_always dbus-update-activation-environment --systemd \ ### Variables set $launcher ./target/debug/shepherd-launcher set $hud ./target/debug/shepherd-hud +set $stop_current $launcher --stop-current ### Output configuration # Set up displays (adjust as needed for your hardware) @@ -129,8 +130,11 @@ focus_follows_mouse no # Hide any title/tab text by using minimal font size font pango:monospace 1 -# Prevent window closing via keybindings (no Alt+F4) -# Windows can only be closed by the application itself +# Session stop keybindings via shepherdd API (does not close windows directly) +# Handled in sway so they work regardless of which client currently has focus. +bindsym --locked Alt+F4 exec $stop_current +bindsym --locked Ctrl+w exec $stop_current +bindsym --locked Home exec $stop_current # Hide mouse cursor after inactivity seat * hide_cursor 5000