Modernize feresectl and add bounded IPC event streaming
This commit is contained in:
1 parent
cfdd2a148a
commit
97026a0db7
39 files changed
+1671
-254
No files matched your search
@@ -0,0 +1,285 @@
|
||||
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 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,
|
||||
/// Pull a window into the focused column.
|
||||
Consume,
|
||||
/// Move a window out of the focused column.
|
||||
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 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()
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn ipc(&self) -> Option<(&'static str, Value)> {
|
||||
let (command, args) = match self {
|
||||
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::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::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))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user