Files
feresaria/feresectl/src/cli.rs
T
Samuel Onoja 2c9bb85f2c Add session-owned Pomodoro cycles and controls
Add configurable work and break intervals, pause/resume, CLI and activity-popup controls, and silent completion alerts. Use CLOCK_BOOTTIME deadlines so running timers include suspend without consuming paused time.

Refs #31
2026-10-20 15:19:55 +01:00

337 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
use serde_json::{Value, json};
/// Control a running Ferese session.
#[derive(Debug, Parser)]
#[command(version)]
pub struct Cli {
/// Print query results as JSON.
#[arg(short = 'j', long, global = true)]
pub json: bool,
/// Connect to this IPC socket instead of FERESE_SOCKET or the runtime default.
#[arg(long, global = true)]
pub socket: Option<PathBuf>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Show or hide the application launcher.
Launcher {
#[command(subcommand)]
action: LauncherAction,
},
/// Control the session’s work and break timer.
Pomodoro {
#[command(subcommand)]
action: crate::pomodoro::Action,
},
/// Publish bounded activity data owned by the invoking shell.
Activity {
#[command(subcommand)]
action: crate::activities::Action,
},
/// Run a command with activity publishing; preserve its actual exit status.
Run {
#[arg(long)]
title: Option<String>,
#[arg(long)]
cargo_json: bool,
#[arg(last = true, required = true)]
command: Vec<String>,
},
/// Show the active window.
#[command(alias = "get-focused-window")]
FocusedWindow,
/// List windows.
#[command(alias = "get-windows")]
Windows,
/// Show idle inhibitors.
#[command(alias = "get-idle-inhibition")]
IdleInhibition,
/// List workspaces and their output ownership.
#[command(alias = "get-workspaces")]
Workspaces,
/// List connected outputs and their applied configuration.
#[command(alias = "get-outputs")]
Outputs,
/// List configured keybindings.
#[command(alias = "get-keybindings")]
Keybindings,
/// Show session and inhibition state.
#[command(alias = "get-session-state")]
SessionState,
/// Toggle floating for the focused window.
ToggleFloating,
/// Toggle fullscreen for the focused window.
ToggleFullscreen,
/// Toggle maximization for the focused window.
ToggleMaximized,
/// Switch between scrolling and tree layout.
ToggleLayout,
/// Show or hide the workspace overview.
ToggleOverview,
/// Show or hide the keybinding guide.
ToggleKeybindingGuide,
/// Cycle the focused column width.
CycleColumnWidth,
/// Center the focused column.
CenterColumn,
/// Toggle the focused scrolling column between stacked windows and tabs.
ToggleColumnTabbed,
/// Join the focused window to the adjacent tab group.
JoinTab { direction: Direction },
/// Split the focused tab out beside its group.
LeaveTab,
/// Focus the next tab in the group.
NextTab,
/// Focus the previous tab in the group.
PreviousTab,
/// Join a neighboring column or tree tab group.
Consume,
/// Leave the focused column or tree tab group.
Expel,
/// Close the focused window.
Close,
/// Cycle the display layout.
ToggleDisplayMode,
/// List available output profiles.
OutputProfiles,
/// Confirm the pending output configuration.
OutputConfirm,
/// Revert the pending output configuration.
OutputRevert,
/// Reload the compositor configuration.
ReloadConfig,
/// Exit the compositor.
Exit,
/// Open the logout confirmation.
RequestLogout,
/// Return to the previous workspace.
WorkspaceBackAndForth,
/// Focus the previously focused window.
FocusLastWindow,
/// Focus the most recently focused window in the opposite tiled/floating layer on this workspace.
FocusFloating,
/// Focus the next recently used window.
FocusMruNext,
/// Focus the previous recently used window.
FocusMruPrevious,
/// Focus a window in a direction.
Focus { direction: Direction },
/// Move the focused window in a direction.
Move { direction: Direction },
/// Resize the focused window in a direction.
Resize { direction: Direction },
/// Activate a workspace by its one-based index.
Workspace {
#[arg(value_parser = clap::value_parser!(u32).range(1..))]
index: u32,
},
/// Move the focused window to a workspace.
MoveToWorkspace {
#[arg(value_parser = clap::value_parser!(u32).range(1..))]
index: u32,
},
/// Apply a display layout.
OutputLayout { layout: DisplayLayout },
/// Select an output profile, or auto.
OutputProfile { name: String },
/// Enable or disable the internal display.
OutputInternal { state: Switch },
/// Write a screenshot PNG to stdout.
Screenshot {
#[arg(short = 'g', long)]
geometry: Option<String>,
},
/// Write a window screenshot PNG to stdout.
ScreenshotWindow { window: u64 },
/// Launch desktop autostart entries.
Autostart,
/// Inspect or change the theme.
Theme {
#[command(subcommand)]
action: ThemeAction,
},
/// Inspect or retry the XWayland server.
Xwayland {
#[command(subcommand)]
action: XwaylandAction,
},
/// Inspect media players or control playback.
Media {
#[command(subcommand)]
action: Option<MediaAction>,
},
/// Follow compositor changes as newline-delimited JSON.
EventStream,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum Direction {
Left,
Right,
Up,
Down,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum DisplayLayout {
InternalOnly,
ExternalOnly,
Extend,
Mirror,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum Switch {
On,
Off,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum ThemeMode {
Light,
Dark,
Auto,
}
#[derive(Debug, Subcommand)]
pub enum ThemeAction {
/// Show the current theme.
#[command(alias = "status")]
Get,
/// Follow theme snapshots as newline-delimited JSON.
Subscribe,
/// Set the appearance mode.
Mode { mode: ThemeMode },
/// Resolve a configuration file without applying it.
Preview { path: PathBuf },
}
#[derive(Debug, Subcommand)]
pub enum XwaylandAction {
/// Show XWayland status.
Status,
/// Retry starting XWayland after a failure.
Retry,
}
#[derive(Debug, Subcommand)]
pub enum MediaAction {
/// Show players and the active player.
Get,
/// Toggle playback.
PlayPause,
/// Skip to the next track.
Next,
/// Return to the previous track.
Previous,
/// Raise the active player's window.
Raise,
/// Select the active player automatically.
Auto,
/// Prefer a particular player.
Pin { player: String },
/// Exclude a player from automatic selection.
Ignore { player: String },
/// Include a previously ignored player.
Unignore { player: String },
}
fn value_name(value: &impl ValueEnum) -> String {
value
.to_possible_value()
.expect("visible enum value")
.get_name()
.to_owned()
}
#[derive(Debug, Subcommand)]
pub enum LauncherAction {
Toggle,
}
impl Command {
pub fn ipc(&self) -> Option<(&'static str, Value)> {
let (command, args) = match self {
Self::Pomodoro { .. } | Self::Activity { .. } | Self::Run { .. } => return None,
Self::Launcher {
action: LauncherAction::Toggle,
} => ("toggle-launcher", json!({})),
Self::FocusedWindow => ("get-focused-window", json!({})),
Self::Windows => ("get-windows", json!({})),
Self::IdleInhibition => ("get-idle-inhibition", json!({})),
Self::Workspaces => ("get-workspaces", json!({})),
Self::Outputs => ("outputs", json!({})),
Self::Keybindings => ("get-keybindings", json!({})),
Self::SessionState => ("get-session-state", json!({})),
Self::ToggleFloating => ("toggle-floating", json!({})),
Self::ToggleFullscreen => ("toggle-fullscreen", json!({})),
Self::ToggleMaximized => ("toggle-maximized", json!({})),
Self::ToggleLayout => ("toggle-layout", json!({})),
Self::ToggleOverview => ("toggle-overview", json!({})),
Self::ToggleKeybindingGuide => ("toggle-keybinding-guide", json!({})),
Self::CycleColumnWidth => ("cycle-column-width", json!({})),
Self::CenterColumn => ("center-column", json!({})),
Self::ToggleColumnTabbed => ("toggle-column-tabbed", json!({})),
Self::JoinTab { direction } => ("join-tab", json!({"direction": value_name(direction)})),
Self::LeaveTab => ("leave-tab", json!({})),
Self::NextTab => ("next-tab", json!({})),
Self::PreviousTab => ("previous-tab", json!({})),
Self::Consume => ("consume", json!({})),
Self::Expel => ("expel", json!({})),
Self::Close => ("close", json!({})),
Self::ToggleDisplayMode => ("toggle-display-mode", json!({})),
Self::OutputProfiles => ("output-profiles", json!({})),
Self::OutputConfirm => ("output-confirm", json!({})),
Self::OutputRevert => ("output-revert", json!({})),
Self::ReloadConfig => ("reload-config", json!({})),
Self::Exit => ("exit", json!({})),
Self::RequestLogout => ("request-logout", json!({})),
Self::WorkspaceBackAndForth => ("workspace-back-and-forth", json!({})),
Self::FocusLastWindow => ("focus-last-window", json!({})),
Self::FocusFloating => ("focus-floating", json!({})),
Self::FocusMruNext => ("focus-mru-next", json!({})),
Self::FocusMruPrevious => ("focus-mru-previous", json!({})),
Self::Focus { direction } => ("focus", json!({"direction": value_name(direction)})),
Self::Move { direction } => ("move", json!({"direction": value_name(direction)})),
Self::Resize { direction } => ("resize", json!({"direction": value_name(direction)})),
Self::Workspace { index } => ("workspace", json!({"index": index})),
Self::MoveToWorkspace { index } => ("move-to-workspace", json!({"index": index})),
Self::OutputLayout { layout } => ("output-layout", json!({"layout": value_name(layout)})),
Self::OutputProfile { name } => ("output-profile", json!({"name": name})),
Self::OutputInternal { state } => ("output-internal", json!({"enabled": matches!(state, Switch::On)})),
Self::Screenshot {
geometry: Some(geometry),
} => ("screenshot", json!({"geometry": geometry})),
Self::Screenshot { geometry: None } => ("screenshot", json!({})),
Self::ScreenshotWindow { window } => ("screenshot-window", json!({"window": window})),
Self::Xwayland { action } => (
match action {
XwaylandAction::Status => ferese_ipc::xwayland::STATUS_COMMAND,
XwaylandAction::Retry => ferese_ipc::xwayland::RETRY_COMMAND,
},
json!({}),
),
Self::Media {
action: None | Some(MediaAction::Get),
} => ("media-get", json!({})),
Self::Media { action: Some(action) } => {
let args = match action {
MediaAction::Pin { player } => json!({"action": "pin", "player": player}),
MediaAction::Ignore { player } => json!({"action": "ignore", "player": player, "ignored": true}),
MediaAction::Unignore { player } => json!({"action": "ignore", "player": player, "ignored": false}),
MediaAction::PlayPause => json!({"action": "play-pause"}),
MediaAction::Next => json!({"action": "next"}),
MediaAction::Previous => json!({"action": "previous"}),
MediaAction::Raise => json!({"action": "raise"}),
MediaAction::Auto => json!({"action": "auto"}),
MediaAction::Get => unreachable!(),
};
("media-action", args)
}
Self::Autostart | Self::Theme { .. } | Self::EventStream => return None,
};
Some((command, args))
}
}