1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-01 02:31:04 +02:00

Merge pull request #595 from JakeStanger/refactor/cli-change

Refactor/cli change
This commit is contained in:
Jake Stanger 2024-06-01 17:23:17 +01:00 committed by GitHub
commit c28de8d902
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 603 additions and 431 deletions

View file

@ -5,28 +5,38 @@ It also includes a command line interface, which can be used for interacting wit
# CLI # CLI
This is shipped as part of the `ironbar` binary. To view commands, you can use `ironbar --help`. This is shipped as part of the `ironbar` binary. To view commands, you can use `ironbar --help`.
You can also view help per-command, for example using `ironbar set --help`. You can also view help per sub-command or command, for example using `ironbar var --help` or `ironbar var set --help`.
Responses are handled by writing their type to stdout, followed by any value starting on the next line. The CLI supports plaintext and JSON output. Plaintext will:
Error responses are written to stderr in the same format.
- Print `ok` for empty success responses
- Print the returned body for success responses
- Print `error` to followed by the error on the next line for error responses. This is printed to `stderr`.
Example: Example:
```shell ```shell
$ ironbar set subject world $ ironbar var set subject world
ok ok
$ ironbar get subject $ ironbar var get subject
ok
world world
$ ironbar var get foo
error
Variable not found
``` ```
All error responses will cause the CLI to exit code 3.
# IPC # IPC
The server listens on a Unix socket. The server listens on a Unix socket.
This can usually be found at `/run/user/$UID/ironbar-ipc.sock`. The path is printed on startup, and can usually be found at `/run/user/$UID/ironbar-ipc.sock`.
Commands and responses are sent as JSON objects, denoted by their `type` key. Commands and responses are sent as JSON objects.
Commands will have a `command` key, and a `subcommand` key when part of a sub-command.
The message buffer is currently limited to `1024` bytes. The message buffer is currently limited to `1024` bytes.
Particularly large messages will be truncated or cause an error. Particularly large messages will be truncated or cause an error.
@ -47,7 +57,7 @@ Responds with `ok`.
```json ```json
{ {
"type": "ping" "command": "ping"
} }
``` ```
@ -59,7 +69,7 @@ Responds with `ok`.
```json ```json
{ {
"type": "inspect" "command": "inspect"
} }
``` ```
@ -73,48 +83,7 @@ Responds with `ok`.
```json ```json
{ {
"type": "reload" "command": "reload"
}
```
### `get`
Gets an [ironvar](ironvars) value.
Responds with `ok_value` if the value exists, otherwise `error`.
```json
{
"type": "get",
"key": "foo"
}
```
### `set`
Sets an [ironvar](ironvars) value.
Responds with `ok`.
```json
{
"type": "set",
"key": "foo",
"value": "bar"
}
```
### list
Gets a list of all [ironvar](ironvars) values.
Responds with `ok_value`.
Each key/value pair is on its own `\n` separated newline. The key and value are separated by a colon and space `: `.
```json
{
"type": "list"
} }
``` ```
@ -126,26 +95,113 @@ Responds with `ok` if the stylesheet exists, otherwise `error`.
```json ```json
{ {
"type": "load_css", "command": "load_css",
"path": "/path/to/style.css" "path": "/path/to/style.css"
} }
``` ```
### `set_visible` ### `var`
Sets a bar's visibility. Subcommand for controlling Ironvars.
#### `get`
Gets an [ironvar](ironvars) value.
Responds with `ok_value` if the value exists, otherwise `error`.
```json
{
"command": "var",
"subcommand": "get",
"key": "foo"
}
```
#### `set`
Sets an [ironvar](ironvars) value.
Responds with `ok`.
```json
{
"command": "var",
"subcommand": "set",
"key": "foo",
"value": "bar"
}
```
#### `list`
Gets a list of all [ironvar](ironvars) values.
Responds with `ok_value`.
Each key/value pair is on its own `\n` separated newline. The key and value are separated by a colon and space `: `.
```json
{
"command": "var",
"subcommand": "list"
}
```
### `bar`
#### `show`
Forces a bar to be shown, regardless of the current visibility state.
```json
{
"command": "bar",
"subcommand": "show",
"name": "bar-123"
}
```
#### `hide`
Forces a bar to be hidden, regardless of the current visibility state.
```json
{
"command": "bar",
"subcommand": "hide",
"name": "bar-123"
}
```
#### `set_visible`
Sets a bar's visibility to one of shown/hidden.
Responds with `ok` if the bar exists, otherwise `error`. Responds with `ok` if the bar exists, otherwise `error`.
```json ```json
{ {
"type": "set_visible", "command": "bar",
"bar_name": "bar-123", "subcommand": "set_visible",
"name": "bar-123",
"visible": true "visible": true
} }
``` ```
### `get_visible` #### `toggle_visible`
Toggles the current visibility state of a bar between shown and hidden.
```json
{
"command": "bar",
"subcommand": "toggle_visible",
"name": "bar-123"
}
```
#### `get_visible`
Gets a bar's visibility. Gets a bar's visibility.
@ -153,50 +209,82 @@ Responds with `ok_value` and the visibility (`true`/`false`) if the bar exists,
```json ```json
{ {
"type": "get_visible", "command": "bar",
"bar_name": "bar-123" "subcommand": "get_visible",
"name": "bar-123"
} }
``` ```
### `toggle_popup` #### `show_popup`
Toggles the open/closed state for a module's popup.
Since each bar only has a single popup, any open popup on the bar is closed.
Responds with `ok` if the popup exists, otherwise `error`.
```json
{
"type": "toggle_popup",
"bar_name": "bar-123",
"name": "clock"
}
```
### `open_popup`
Sets a module's popup open, regardless of its current state. Sets a module's popup open, regardless of its current state.
Since each bar only has a single popup, any open popup on the bar is closed. Since each bar only has a single popup, any open popup on the bar is closed.
Responds with `ok` if the popup exists, otherwise `error`. Responds with `ok` if the bar and widget exist, otherwise `error`.
```json ```json
{ {
"type": "open_popup", "command": "bar",
"bar_name": "bar-123", "subcommand": "show_popup",
"name": "clock" "name": "bar-123",
"widget_name": "clock"
} }
``` ```
### `close_popup` #### `hide_popup`
Sets the popup on a bar closed, regardless of which module it is open for. Sets the popup on a bar closed, regardless of which module it is open for.
Responds with `ok` if the popup exists, otherwise `error`. Responds with `ok` if the bar and widget exist, otherwise `error`.
```json ```json
{ {
"type": "close_popup", "command": "bar",
"subcommand": "hide_popup",
"bar_name": "bar-123"
}
```
#### `set_popup_visible`
Sets a popup's visibility to one of shown/hidden.
Responds with `ok` if the bar and widget exist, otherwise `error`.
```json
{
"command": "bar",
"subcommand": "set_popup_visible",
"name": "bar-123",
"widget_name": "clock",
"visible": true
}
```
#### `toggle_popup`
Toggles the open/closed state for a module's popup.
Since each bar only has a single popup, any open popup on the bar is closed.
Responds with `ok` if the bar and widget exist, otherwise `error`.
```json
{
"command": "bar",
"subcommand": "toggle_popup",
"bar_name": "bar-123",
"widget_name": "clock"
}
```
#### `get_popup_visible`
Gets the popup's current visibility state.
```json
{
"command": "bar",
"subcommand": "get_popup_visible",
"bar_name": "bar-123" "bar_name": "bar-123"
} }
``` ```

View file

@ -320,6 +320,15 @@ impl Bar {
Inner::Loaded { popup } => popup.clone(), Inner::Loaded { popup } => popup.clone(),
} }
} }
pub fn visible(&self) -> bool {
self.window.is_visible()
}
/// Sets the window visibility status
pub fn set_visible(&self, visible: bool) {
self.window.set_visible(visible)
}
} }
/// Creates a `gtk::Box` container to place widgets inside. /// Creates a `gtk::Box` container to place widgets inside.

View file

@ -1,7 +1,9 @@
use crate::error::ExitCode;
use crate::ipc::commands::Command; use crate::ipc::commands::Command;
use crate::ipc::responses::Response; use crate::ipc::responses::Response;
use clap::Parser; use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::process::exit;
#[derive(Parser, Debug, Serialize, Deserialize)] #[derive(Parser, Debug, Serialize, Deserialize)]
#[command(version)] #[command(version)]
@ -15,16 +17,44 @@ pub struct Args {
#[arg(long("print-schema"))] #[arg(long("print-schema"))]
pub print_schema: bool, pub print_schema: bool,
/// Print debug information to stderr
/// TODO: Make bar follow this too
#[arg(long)]
pub debug: bool,
/// Format to output the response as.
#[arg(short, long)]
pub format: Option<Format>,
/// `bar_id` argument passed by `swaybar_command`. /// `bar_id` argument passed by `swaybar_command`.
/// Not used. /// Not used.
#[arg(short('b'), hide(true))] #[arg(short('b'), hide(true))]
sway_bar_id: Option<String>, sway_bar_id: Option<String>,
} }
pub fn handle_response(response: Response) { #[derive(Debug, Serialize, Deserialize, Default, ValueEnum, Clone, Copy)]
match response { pub enum Format {
Response::Ok => println!("ok"), #[default]
Response::OkValue { value } => println!("ok\n{value}"), Plain,
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()), Json,
}
pub fn handle_response(response: Response, format: Format) {
let is_err = matches!(response, Response::Err { .. });
match format {
Format::Plain => match response {
Response::Ok => println!("ok"),
Response::OkValue { value } => println!("{value}"),
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()),
},
Format::Json => println!(
"{}",
serde_json::to_string(&response).expect("to be valid json")
),
}
if is_err {
exit(ExitCode::IpcResponseError as i32)
} }
} }

View file

@ -2,6 +2,7 @@
pub enum ExitCode { pub enum ExitCode {
GtkDisplay = 1, GtkDisplay = 1,
CreateBars = 2, CreateBars = 2,
IpcResponseError = 3,
} }
pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex"; pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex";

View file

@ -8,7 +8,7 @@ use tokio::net::UnixStream;
impl Ipc { impl Ipc {
/// Sends a command to the IPC server. /// Sends a command to the IPC server.
/// The server response is returned. /// The server response is returned.
pub async fn send(&self, command: Command) -> Result<Response> { pub async fn send(&self, command: Command, debug: bool) -> Result<Response> {
let mut stream = match UnixStream::connect(&self.path).await { let mut stream = match UnixStream::connect(&self.path).await {
Ok(stream) => Ok(stream), Ok(stream) => Ok(stream),
Err(err) => Err(Report::new(err) Err(err) => Err(Report::new(err)
@ -17,6 +17,11 @@ impl Ipc {
}?; }?;
let write_buffer = serde_json::to_vec(&command)?; let write_buffer = serde_json::to_vec(&command)?;
if debug {
eprintln!("REQUEST JSON: {}", serde_json::to_string(&command)?);
}
stream.write_all(&write_buffer).await?; stream.write_all(&write_buffer).await?;
let mut read_buffer = vec![0; 1024]; let mut read_buffer = vec![0; 1024];

View file

@ -1,20 +1,39 @@
use clap::ArgAction;
use std::path::PathBuf; use std::path::PathBuf;
use clap::Subcommand; use clap::{Args, Subcommand};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Subcommand, Debug, Serialize, Deserialize)] #[derive(Subcommand, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "command", rename_all = "snake_case")]
pub enum Command { pub enum Command {
/// Return "ok" /// Pong
Ping, Ping,
/// Open the GTK inspector /// Open the GTK inspector.
Inspect, Inspect,
/// Reload the config /// Reload the config.
Reload, Reload,
/// Load an additional CSS stylesheet.
/// The sheet is automatically hot-reloaded.
LoadCss {
/// The path to the sheet.
path: PathBuf,
},
/// Get and set reactive Ironvar values.
#[command(subcommand)]
Var(IronvarCommand),
/// Interact with a specific bar.
Bar(BarCommand),
}
#[derive(Subcommand, Debug, Serialize, Deserialize)]
#[serde(tag = "subcommand", rename_all = "snake_case")]
pub enum IronvarCommand {
/// Set an `ironvar` value. /// Set an `ironvar` value.
/// This creates it if it does not already exist, and updates it if it does. /// This creates it if it does not already exist, and updates it if it does.
/// Any references to this variable are automatically and immediately updated. /// Any references to this variable are automatically and immediately updated.
@ -34,49 +53,69 @@ pub enum Command {
/// Gets the current value of all `ironvar`s. /// Gets the current value of all `ironvar`s.
List, List,
}
/// Load an additional CSS stylesheet. #[derive(Args, Debug, Serialize, Deserialize)]
/// The sheet is automatically hot-reloaded. pub struct BarCommand {
LoadCss { /// The name of the bar.
/// The path to the sheet. pub name: String,
path: PathBuf,
},
/// Set the visibility of the bar with the given name. #[command(subcommand)]
#[serde(flatten)]
pub subcommand: BarCommandType,
}
#[derive(Subcommand, Debug, Serialize, Deserialize)]
#[serde(tag = "subcommand", rename_all = "snake_case")]
pub enum BarCommandType {
// == Visibility == \\
/// Force the bar to be shown, regardless of current visibility state.
Show,
/// Force the bar to be hidden, regardless of current visibility state.
Hide,
/// Set the bar's visibility state via an argument.
SetVisible { SetVisible {
///Bar name to target. /// The new visibility state.
bar_name: String, #[clap(
/// The visibility status. num_args(1),
#[arg(short, long)] require_equals(true),
action = ArgAction::Set,
)]
visible: bool, visible: bool,
}, },
/// Toggle the current visibility state between shown and hidden.
ToggleVisible,
/// Get the bar's visibility state.
GetVisible,
/// Get the visibility of the bar with the given name. // == Popup visibility == \\
GetVisible { /// Open a popup, regardless of current state.
/// Bar name to target. /// If opening this popup, and a different popup on the same bar is already open, the other is closed.
bar_name: String, ShowPopup {
/// The configured name of the widget.
widget_name: String,
}, },
/// Close a popup, regardless of current state.
HidePopup,
/// Set the popup's visibility state via an argument.
/// If opening this popup, and a different popup on the same bar is already open, the other is closed.
SetPopupVisible {
/// The configured name of the widget.
widget_name: String,
#[clap(
num_args(1),
require_equals(true),
action = ArgAction::Set,
)]
visible: bool,
},
/// Toggle a popup open/closed. /// Toggle a popup open/closed.
/// If opening this popup, and a different popup on the same bar is already open, the other is closed. /// If opening this popup, and a different popup on the same bar is already open, the other is closed.
TogglePopup { TogglePopup {
/// The name of the monitor the bar is located on. /// The configured name of the widget.
bar_name: String, widget_name: String,
/// The name of the widget.
name: String,
},
/// Open a popup, regardless of current state.
OpenPopup {
/// The name of the monitor the bar is located on.
bar_name: String,
/// The name of the widget.
name: String,
},
/// Close a popup, regardless of current state.
ClosePopup {
/// The name of the monitor the bar is located on.
bar_name: String,
}, },
/// Get the popup's current visibility state.
GetPopupVisible,
} }

View file

@ -6,7 +6,7 @@ mod server;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tracing::warn; use tracing::warn;
pub use commands::Command; pub use commands::*;
pub use responses::Response; pub use responses::Response;
#[derive(Debug)] #[derive(Debug)]

View file

@ -1,297 +0,0 @@
use std::fs;
use std::path::Path;
use std::rc::Rc;
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::Application;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::mpsc::{self, Receiver, Sender};
use tracing::{debug, error, info, warn};
use crate::ipc::{Command, Response};
use crate::modules::PopupButton;
use crate::style::load_css;
use crate::{glib_recv_mpsc, read_lock, send_async, spawn, try_send, write_lock, Ironbar};
use super::Ipc;
impl Ipc {
/// Starts the IPC server on its socket.
///
/// Once started, the server will begin accepting connections.
pub fn start(&self, application: &Application, ironbar: Rc<Ironbar>) {
let (cmd_tx, cmd_rx) = mpsc::channel(32);
let (res_tx, mut res_rx) = mpsc::channel(32);
let path = self.path.clone();
if path.exists() {
warn!("Socket already exists. Did Ironbar exit abruptly?");
warn!("Attempting IPC shutdown to allow binding to address");
Self::shutdown(&path);
}
spawn(async move {
info!("Starting IPC on {}", path.display());
let listener = match UnixListener::bind(&path) {
Ok(listener) => listener,
Err(err) => {
error!(
"{:?}",
Report::new(err).wrap_err("Unable to start IPC server")
);
return;
}
};
loop {
match listener.accept().await {
Ok((stream, _addr)) => {
if let Err(err) =
Self::handle_connection(stream, &cmd_tx, &mut res_rx).await
{
error!("{err:?}");
}
}
Err(err) => {
error!("{err:?}");
}
}
}
});
let application = application.clone();
glib_recv_mpsc!(cmd_rx, command => {
let res = Self::handle_command(command, &application, &ironbar);
try_send!(res_tx, res);
});
}
/// Takes an incoming connections,
/// reads the command message, and sends the response.
///
/// The connection is closed once the response has been written.
async fn handle_connection(
mut stream: UnixStream,
cmd_tx: &Sender<Command>,
res_rx: &mut Receiver<Response>,
) -> Result<()> {
let (mut stream_read, mut stream_write) = stream.split();
let mut read_buffer = vec![0; 1024];
let bytes = stream_read.read(&mut read_buffer).await?;
let command = serde_json::from_slice::<Command>(&read_buffer[..bytes])?;
debug!("Received command: {command:?}");
send_async!(cmd_tx, command);
let res = res_rx
.recv()
.await
.unwrap_or(Response::Err { message: None });
let res = serde_json::to_vec(&res)?;
stream_write.write_all(&res).await?;
stream_write.shutdown().await?;
Ok(())
}
/// Takes an input command, runs it and returns with the appropriate response.
///
/// This runs on the main thread, allowing commands to interact with GTK.
fn handle_command(
command: Command,
application: &Application,
ironbar: &Rc<Ironbar>,
) -> Response {
match command {
Command::Inspect => {
gtk::Window::set_interactive_debugging(true);
Response::Ok
}
Command::Reload => {
info!("Closing existing bars");
ironbar.bars.borrow_mut().clear();
let windows = application.windows();
for window in windows {
window.close();
}
let wl = ironbar.clients.borrow_mut().wayland();
let outputs = wl.output_info_all();
ironbar.reload_config();
for output in outputs {
match crate::load_output_bars(ironbar, application, &output) {
Ok(mut bars) => ironbar.bars.borrow_mut().append(&mut bars),
Err(err) => error!("{err:?}"),
}
}
Response::Ok
}
Command::Set { key, value } => {
let variable_manager = Ironbar::variable_manager();
let mut variable_manager = write_lock!(variable_manager);
match variable_manager.set(key, value) {
Ok(()) => Response::Ok,
Err(err) => Response::error(&format!("{err}")),
}
}
Command::Get { key } => {
let variable_manager = Ironbar::variable_manager();
let value = read_lock!(variable_manager).get(&key);
match value {
Some(value) => Response::OkValue { value },
None => Response::error("Variable not found"),
}
}
Command::List => {
let variable_manager = Ironbar::variable_manager();
let mut values = read_lock!(variable_manager)
.get_all()
.iter()
.map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default()))
.collect::<Vec<_>>();
values.sort();
let value = values.join("\n");
Response::OkValue { value }
}
Command::LoadCss { path } => {
if path.exists() {
load_css(path);
Response::Ok
} else {
Response::error("File not found")
}
}
Command::TogglePopup { bar_name, name } => {
let bar = ironbar.bar_by_name(&bar_name);
match bar {
Some(bar) => {
let popup = bar.popup();
let current_widget = popup.current_widget();
popup.hide();
let data = popup
.container_cache
.borrow()
.iter()
.find(|(_, value)| value.name == name)
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
match data {
Some((id, Some(button))) if current_widget != Some(id) => {
let button_id = button.popup_id();
if popup.is_visible() {
popup.hide();
} else {
popup.show(id, button_id);
}
Response::Ok
}
Some((_, None)) => Response::error("Module has no popup functionality"),
Some(_) => Response::Ok,
None => Response::error("Invalid module name"),
}
}
None => Response::error("Invalid bar name"),
}
}
Command::OpenPopup { bar_name, name } => {
let bar = ironbar.bar_by_name(&bar_name);
match bar {
Some(bar) => {
let popup = bar.popup();
// only one popup per bar, so hide if open for another widget
popup.hide();
let data = popup
.container_cache
.borrow()
.iter()
.find(|(_, value)| value.name == name)
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
match data {
Some((id, Some(button))) => {
let button_id = button.popup_id();
popup.show(id, button_id);
Response::Ok
}
Some((_, None)) => Response::error("Module has no popup functionality"),
None => Response::error("Invalid module name"),
}
}
None => Response::error("Invalid bar name"),
}
}
Command::ClosePopup { bar_name } => {
let bar = ironbar.bar_by_name(&bar_name);
match bar {
Some(bar) => {
let popup = bar.popup();
popup.hide();
Response::Ok
}
None => Response::error("Invalid bar name"),
}
}
Command::Ping => Response::Ok,
Command::SetVisible { bar_name, visible } => {
let windows = application.windows();
let found = windows
.iter()
.find(|window| window.widget_name() == bar_name);
if let Some(window) = found {
window.set_visible(visible);
Response::Ok
} else {
Response::error("Bar not found")
}
}
Command::GetVisible { bar_name } => {
let windows = application.windows();
let found = windows
.iter()
.find(|window| window.widget_name() == bar_name);
if let Some(window) = found {
Response::OkValue {
value: window.is_visible().to_string(),
}
} else {
Response::error("Bar not found")
}
}
}
}
/// Shuts down the IPC server,
/// removing the socket file in the process.
///
/// Note this is static as the `Ipc` struct is not `Send`.
pub fn shutdown<P: AsRef<Path>>(path: P) {
fs::remove_file(&path).ok();
}
}

84
src/ipc/server/bar.rs Normal file
View file

@ -0,0 +1,84 @@
use super::Response;
use crate::bar::Bar;
use crate::ipc::{BarCommand, BarCommandType};
use crate::modules::PopupButton;
use crate::Ironbar;
use std::rc::Rc;
pub fn handle_command(command: BarCommand, ironbar: &Rc<Ironbar>) -> Response {
let bar = ironbar.bar_by_name(&command.name);
let Some(bar) = bar else {
return Response::error("Invalid bar name");
};
use BarCommandType::*;
match command.subcommand {
Show => set_visible(&bar, true),
Hide => set_visible(&bar, false),
SetVisible { visible } => set_visible(&bar, visible),
ToggleVisible => set_visible(&bar, !bar.visible()),
GetVisible => Response::OkValue {
value: bar.visible().to_string(),
},
ShowPopup { widget_name } => show_popup(&bar, widget_name),
HidePopup => hide_popup(&bar),
SetPopupVisible {
widget_name,
visible,
} => {
if visible {
show_popup(&bar, widget_name)
} else {
hide_popup(&bar)
}
}
TogglePopup { widget_name } => {
if bar.popup().visible() {
hide_popup(&bar)
} else {
show_popup(&bar, widget_name)
}
}
GetPopupVisible => Response::OkValue {
value: bar.popup().visible().to_string(),
},
}
}
fn set_visible(bar: &Bar, visible: bool) -> Response {
bar.set_visible(visible);
Response::Ok
}
fn show_popup(bar: &Bar, widget_name: String) -> Response {
let popup = bar.popup();
// only one popup per bar, so hide if open for another widget
popup.hide();
let data = popup
.container_cache
.borrow()
.iter()
.find(|(_, value)| value.name == widget_name)
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
match data {
Some((id, Some(button))) => {
let button_id = button.popup_id();
popup.show(id, button_id);
Response::Ok
}
Some((_, None)) => Response::error("Module has no popup functionality"),
None => Response::error("Invalid module name"),
}
}
fn hide_popup(bar: &Bar) -> Response {
let popup = bar.popup();
popup.hide();
Response::Ok
}

38
src/ipc/server/ironvar.rs Normal file
View file

@ -0,0 +1,38 @@
use crate::ipc::commands::IronvarCommand;
use crate::ipc::Response;
use crate::{read_lock, write_lock, Ironbar};
pub fn handle_command(command: IronvarCommand) -> Response {
match command {
IronvarCommand::Set { key, value } => {
let variable_manager = Ironbar::variable_manager();
let mut variable_manager = write_lock!(variable_manager);
match variable_manager.set(key, value) {
Ok(()) => Response::Ok,
Err(err) => Response::error(&format!("{err}")),
}
}
IronvarCommand::Get { key } => {
let variable_manager = Ironbar::variable_manager();
let value = read_lock!(variable_manager).get(&key);
match value {
Some(value) => Response::OkValue { value },
None => Response::error("Variable not found"),
}
}
IronvarCommand::List => {
let variable_manager = Ironbar::variable_manager();
let mut values = read_lock!(variable_manager)
.get_all()
.iter()
.map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default()))
.collect::<Vec<_>>();
values.sort();
let value = values.join("\n");
Response::OkValue { value }
}
}
}

164
src/ipc/server/mod.rs Normal file
View file

@ -0,0 +1,164 @@
mod bar;
mod ironvar;
use std::fs;
use std::path::Path;
use std::rc::Rc;
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::Application;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::mpsc::{self, Receiver, Sender};
use tracing::{debug, error, info, warn};
use crate::ipc::{Command, Response};
use crate::style::load_css;
use crate::{glib_recv_mpsc, send_async, spawn, try_send, Ironbar};
use super::Ipc;
impl Ipc {
/// Starts the IPC server on its socket.
///
/// Once started, the server will begin accepting connections.
pub fn start(&self, application: &Application, ironbar: Rc<Ironbar>) {
let (cmd_tx, cmd_rx) = mpsc::channel(32);
let (res_tx, mut res_rx) = mpsc::channel(32);
let path = self.path.clone();
if path.exists() {
warn!("Socket already exists. Did Ironbar exit abruptly?");
warn!("Attempting IPC shutdown to allow binding to address");
Self::shutdown(&path);
}
spawn(async move {
info!("Starting IPC on {}", path.display());
let listener = match UnixListener::bind(&path) {
Ok(listener) => listener,
Err(err) => {
error!(
"{:?}",
Report::new(err).wrap_err("Unable to start IPC server")
);
return;
}
};
loop {
match listener.accept().await {
Ok((stream, _addr)) => {
if let Err(err) =
Self::handle_connection(stream, &cmd_tx, &mut res_rx).await
{
error!("{err:?}");
}
}
Err(err) => {
error!("{err:?}");
}
}
}
});
let application = application.clone();
glib_recv_mpsc!(cmd_rx, command => {
let res = Self::handle_command(command, &application, &ironbar);
try_send!(res_tx, res);
});
}
/// Takes an incoming connections,
/// reads the command message, and sends the response.
///
/// The connection is closed once the response has been written.
async fn handle_connection(
mut stream: UnixStream,
cmd_tx: &Sender<Command>,
res_rx: &mut Receiver<Response>,
) -> Result<()> {
let (mut stream_read, mut stream_write) = stream.split();
let mut read_buffer = vec![0; 1024];
let bytes = stream_read.read(&mut read_buffer).await?;
// FIXME: Error on invalid command
let command = serde_json::from_slice::<Command>(&read_buffer[..bytes])?;
debug!("Received command: {command:?}");
send_async!(cmd_tx, command);
let res = res_rx
.recv()
.await
.unwrap_or(Response::Err { message: None });
let res = serde_json::to_vec(&res)?;
stream_write.write_all(&res).await?;
stream_write.shutdown().await?;
Ok(())
}
/// Takes an input command, runs it and returns with the appropriate response.
///
/// This runs on the main thread, allowing commands to interact with GTK.
fn handle_command(
command: Command,
application: &Application,
ironbar: &Rc<Ironbar>,
) -> Response {
match command {
Command::Ping => Response::Ok,
Command::Inspect => {
gtk::Window::set_interactive_debugging(true);
Response::Ok
}
Command::Reload => {
info!("Closing existing bars");
ironbar.bars.borrow_mut().clear();
let windows = application.windows();
for window in windows {
window.close();
}
let wl = ironbar.clients.borrow_mut().wayland();
let outputs = wl.output_info_all();
ironbar.reload_config();
for output in outputs {
match crate::load_output_bars(ironbar, application, &output) {
Ok(mut bars) => ironbar.bars.borrow_mut().append(&mut bars),
Err(err) => error!("{err:?}"),
}
}
Response::Ok
}
Command::LoadCss { path } => {
if path.exists() {
load_css(path);
Response::Ok
} else {
Response::error("File not found")
}
}
Command::Var(cmd) => ironvar::handle_command(cmd),
Command::Bar(cmd) => bar::handle_command(cmd, ironbar),
}
}
/// Shuts down the IPC server,
/// removing the socket file in the process.
///
/// Note this is static as the `Ipc` struct is not `Send`.
pub fn shutdown<P: AsRef<Path>>(path: P) {
fs::remove_file(&path).ok();
}
}

View file

@ -85,11 +85,21 @@ fn run_with_args() {
match args.command { match args.command {
Some(command) => { Some(command) => {
if args.debug {
eprintln!("REQUEST: {command:?}")
}
let rt = create_runtime(); let rt = create_runtime();
rt.block_on(async move { rt.block_on(async move {
let ipc = ipc::Ipc::new(); let ipc = ipc::Ipc::new();
match ipc.send(command).await { match ipc.send(command, args.debug).await {
Ok(res) => cli::handle_response(res), Ok(res) => {
if args.debug {
eprintln!("RESPONSE: {res:?}")
}
cli::handle_response(res, args.format.unwrap_or_default())
}
Err(err) => error!("{err:?}"), Err(err) => error!("{err:?}"),
}; };
}); });
@ -239,6 +249,7 @@ impl Ironbar {
/// Gets a `usize` ID value that is unique to the entire Ironbar instance. /// Gets a `usize` ID value that is unique to the entire Ironbar instance.
/// This is just a static `AtomicUsize` that increments every time this function is called. /// This is just a static `AtomicUsize` that increments every time this function is called.
#[must_use]
pub fn unique_id() -> usize { pub fn unique_id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(1); static COUNTER: AtomicUsize = AtomicUsize::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed) COUNTER.fetch_add(1, Ordering::Relaxed)

View file

@ -382,7 +382,7 @@ impl ModuleFactory for BarModuleFactory {
} }
ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => { ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => {
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id); debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
if popup.is_visible() && popup.current_widget().unwrap_or_default() == id { if popup.visible() && popup.current_widget().unwrap_or_default() == id {
popup.hide(); popup.hide();
} else { } else {
popup.show(id, button_id); popup.show(id, button_id);
@ -455,7 +455,7 @@ impl ModuleFactory for PopupModuleFactory {
} }
ModuleUpdateEvent::TogglePopup(_) if !disable_popup => { ModuleUpdateEvent::TogglePopup(_) if !disable_popup => {
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id); debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
if popup.is_visible() && popup.current_widget().unwrap_or_default() == id { if popup.visible() && popup.current_widget().unwrap_or_default() == id {
popup.hide(); popup.hide();
} else { } else {
popup.show(id, button_id); popup.show(id, button_id);

View file

@ -229,7 +229,7 @@ impl Popup {
} }
/// Checks if the popup is currently visible /// Checks if the popup is currently visible
pub fn is_visible(&self) -> bool { pub fn visible(&self) -> bool {
self.window.is_visible() self.window.is_visible()
} }