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

feat!: improve CLI structure, add new commands

This splits most CLI/IPC commands into two categories:

- `var` for ironvars
- `bar` for controlling individual bars.

It also introduces more commands for visibility, as well as breaking existing ones.

New commands:

- `show`
- `hide`
- `toggle_visible`
- `set_popup_visible`
- `get_popup_visible`

Lastly, the implementation of some existing commands has been improved.

BREAKING CHANGE: All IPC commands have changed. Namely, `type` has been changed to `command`, and bar/var related commands are now under a `subcommand`. The full spec can be found on the wiki.

BREAKING CHANGE: Several CLI commands are now located under the `var` and `bar` categories. Usage of any commands to get/set Ironvars or control bar visibility will need to be updated.

BREAKING CHANGE: The `open_popup` and `close_popup` IPC commands are now called `show_popup` and `hide_popup` respectively.

BREAKING CHANGE: The popup `name` argument has been renamed to `widget_name` on all IPC commands.

BREAKING CHANGE: The `set-visibility` CLI command now takes a `true`/`false` positional argument in place of the `-v` flag.

BREAKING CHANGE: `ok_value` responses will no longer print `ok` as the first line when using the CLI
This commit is contained in:
Jake Stanger 2024-05-18 17:17:26 +01:00
parent 8dda49477b
commit 9dd711235f
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
8 changed files with 544 additions and 419 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,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();
}
}