mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-07-03 03:31:03 +02:00
Merge branch 'refs/heads/master' into develop
# Conflicts: # Cargo.toml # src/modules/volume.rs
This commit is contained in:
commit
3bf4e9b877
51 changed files with 867 additions and 475 deletions
|
@ -320,6 +320,15 @@ impl Bar {
|
|||
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.
|
||||
|
|
48
src/cli.rs
48
src/cli.rs
|
@ -1,7 +1,9 @@
|
|||
use crate::error::ExitCode;
|
||||
use crate::ipc::commands::Command;
|
||||
use crate::ipc::responses::Response;
|
||||
use clap::Parser;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::exit;
|
||||
|
||||
#[derive(Parser, Debug, Serialize, Deserialize)]
|
||||
#[command(version)]
|
||||
|
@ -9,16 +11,50 @@ pub struct Args {
|
|||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
|
||||
/// Prints the config JSON schema to `stdout`
|
||||
/// and exits.
|
||||
#[cfg(feature = "schema")]
|
||||
#[arg(long("print-schema"))]
|
||||
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`.
|
||||
/// Not used.
|
||||
#[arg(short('b'), hide(true))]
|
||||
sway_bar_id: Option<String>,
|
||||
}
|
||||
|
||||
pub fn handle_response(response: Response) {
|
||||
match response {
|
||||
Response::Ok => println!("ok"),
|
||||
Response::OkValue { value } => println!("ok\n{value}"),
|
||||
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()),
|
||||
#[derive(Debug, Serialize, Deserialize, Default, ValueEnum, Clone, Copy)]
|
||||
pub enum Format {
|
||||
#[default]
|
||||
Plain,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ impl ToplevelHandleData {
|
|||
#[derive(Debug, Default)]
|
||||
pub struct ToplevelHandleDataInner {
|
||||
initial_done: bool,
|
||||
closed: bool,
|
||||
output: Option<WlOutput>,
|
||||
|
||||
current_info: Option<ToplevelInfo>,
|
||||
|
@ -137,14 +138,17 @@ where
|
|||
}
|
||||
Event::OutputEnter { output } => lock!(data.inner).output = Some(output),
|
||||
Event::OutputLeave { output: _ } => lock!(data.inner).output = None,
|
||||
Event::Closed => state.remove_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
),
|
||||
Event::Done => {
|
||||
Event::Closed => {
|
||||
lock!(data.inner).closed = true;
|
||||
state.remove_handle(
|
||||
conn,
|
||||
qh,
|
||||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
Event::Done if !lock!(data.inner).closed => {
|
||||
{
|
||||
let pending_info = lock!(data.inner).pending_info.clone();
|
||||
lock!(data.inner).current_info = Some(pending_info);
|
||||
|
|
|
@ -16,6 +16,7 @@ use tracing::trace;
|
|||
/// see [here](script).
|
||||
/// For information on styling, please see the [styling guide](styling-guide).
|
||||
#[derive(Debug, Default, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct CommonConfig {
|
||||
/// Sets the unique widget name,
|
||||
/// allowing you to target it in CSS using `#name`.
|
||||
|
@ -160,6 +161,7 @@ pub struct CommonConfig {
|
|||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum TransitionType {
|
||||
None,
|
||||
Crossfade,
|
||||
|
@ -169,6 +171,7 @@ pub enum TransitionType {
|
|||
|
||||
#[derive(Debug, Default, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum ModuleOrientation {
|
||||
#[default]
|
||||
#[serde(alias = "h")]
|
||||
|
|
|
@ -38,11 +38,15 @@ use color_eyre::Result;
|
|||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use schemars::JsonSchema;
|
||||
|
||||
pub use self::common::{CommonConfig, ModuleOrientation, TransitionType};
|
||||
pub use self::truncate::TruncateMode;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
pub enum ModuleConfig {
|
||||
#[cfg(feature = "cairo")]
|
||||
Cairo(Box<CairoModule>),
|
||||
|
@ -123,6 +127,7 @@ impl ModuleConfig {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
pub enum MonitorConfig {
|
||||
Single(BarConfig),
|
||||
Multiple(Vec<BarConfig>),
|
||||
|
@ -130,6 +135,7 @@ pub enum MonitorConfig {
|
|||
|
||||
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
pub enum BarPosition {
|
||||
Top,
|
||||
Bottom,
|
||||
|
@ -144,6 +150,7 @@ impl Default for BarPosition {
|
|||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Copy, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
pub struct MarginConfig {
|
||||
#[serde(default)]
|
||||
pub bottom: i32,
|
||||
|
@ -162,6 +169,7 @@ pub struct MarginConfig {
|
|||
/// depending on your [use-case](#2-pick-your-use-case).
|
||||
///
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
pub struct BarConfig {
|
||||
/// A unique identifier for the bar, used for controlling it over IPC.
|
||||
/// If not set, uses a generated integer suffix.
|
||||
|
@ -298,6 +306,7 @@ impl Default for BarConfig {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
pub struct Config {
|
||||
/// A map of [ironvar](ironvar) keys and values
|
||||
/// to initialize Ironbar with on startup.
|
||||
|
|
|
@ -4,6 +4,7 @@ use serde::Deserialize;
|
|||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum EllipsizeMode {
|
||||
Start,
|
||||
Middle,
|
||||
|
@ -28,6 +29,7 @@ impl From<EllipsizeMode> for GtkEllipsizeMode {
|
|||
///
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum TruncateMode {
|
||||
/// Auto mode lets GTK decide when to ellipsize.
|
||||
///
|
||||
|
|
|
@ -8,6 +8,7 @@ use tokio::sync::mpsc;
|
|||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum DynamicBool {
|
||||
/// Either a script or variable, to be determined.
|
||||
Unknown(String),
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
pub enum ExitCode {
|
||||
GtkDisplay = 1,
|
||||
CreateBars = 2,
|
||||
IpcResponseError = 3,
|
||||
}
|
||||
|
||||
pub const ERR_MUTEX_LOCK: &str = "Failed to get lock on Mutex";
|
||||
|
|
|
@ -8,7 +8,7 @@ use tokio::net::UnixStream;
|
|||
impl Ipc {
|
||||
/// Sends a command to the IPC server.
|
||||
/// 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 {
|
||||
Ok(stream) => Ok(stream),
|
||||
Err(err) => Err(Report::new(err)
|
||||
|
@ -17,6 +17,11 @@ impl Ipc {
|
|||
}?;
|
||||
|
||||
let write_buffer = serde_json::to_vec(&command)?;
|
||||
|
||||
if debug {
|
||||
eprintln!("REQUEST JSON: {}", serde_json::to_string(&command)?);
|
||||
}
|
||||
|
||||
stream.write_all(&write_buffer).await?;
|
||||
|
||||
let mut read_buffer = vec![0; 1024];
|
||||
|
|
|
@ -1,20 +1,39 @@
|
|||
use clap::ArgAction;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Subcommand;
|
||||
use clap::{Args, Subcommand};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Subcommand, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[serde(tag = "command", rename_all = "snake_case")]
|
||||
pub enum Command {
|
||||
/// Return "ok"
|
||||
/// Pong
|
||||
Ping,
|
||||
|
||||
/// Open the GTK inspector
|
||||
/// Open the GTK inspector.
|
||||
Inspect,
|
||||
|
||||
/// Reload the config
|
||||
/// Reload the config.
|
||||
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.
|
||||
/// 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.
|
||||
|
@ -34,49 +53,69 @@ pub enum Command {
|
|||
|
||||
/// Gets the current value of all `ironvar`s.
|
||||
List,
|
||||
}
|
||||
|
||||
/// Load an additional CSS stylesheet.
|
||||
/// The sheet is automatically hot-reloaded.
|
||||
LoadCss {
|
||||
/// The path to the sheet.
|
||||
path: PathBuf,
|
||||
},
|
||||
#[derive(Args, Debug, Serialize, Deserialize)]
|
||||
pub struct BarCommand {
|
||||
/// The name of the bar.
|
||||
pub name: String,
|
||||
|
||||
/// 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 {
|
||||
///Bar name to target.
|
||||
bar_name: String,
|
||||
/// The visibility status.
|
||||
#[arg(short, long)]
|
||||
/// The new visibility state.
|
||||
#[clap(
|
||||
num_args(1),
|
||||
require_equals(true),
|
||||
action = ArgAction::Set,
|
||||
)]
|
||||
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.
|
||||
GetVisible {
|
||||
/// Bar name to target.
|
||||
bar_name: String,
|
||||
// == Popup visibility == \\
|
||||
/// Open a popup, regardless of current state.
|
||||
/// If opening this popup, and a different popup on the same bar is already open, the other is closed.
|
||||
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.
|
||||
/// If opening this popup, and a different popup on the same bar is already open, the other is closed.
|
||||
TogglePopup {
|
||||
/// The name of the monitor the bar is located on.
|
||||
bar_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,
|
||||
/// The configured name of the widget.
|
||||
widget_name: String,
|
||||
},
|
||||
/// Get the popup's current visibility state.
|
||||
GetPopupVisible,
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ mod server;
|
|||
use std::path::{Path, PathBuf};
|
||||
use tracing::warn;
|
||||
|
||||
pub use commands::Command;
|
||||
pub use commands::*;
|
||||
pub use responses::Response;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -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
84
src/ipc/server/bar.rs
Normal 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
38
src/ipc/server/ironvar.rs
Normal 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
164
src/ipc/server/mod.rs
Normal 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();
|
||||
}
|
||||
}
|
22
src/main.rs
22
src/main.rs
|
@ -76,13 +76,30 @@ fn main() {
|
|||
fn run_with_args() {
|
||||
let args = cli::Args::parse();
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
if args.print_schema {
|
||||
let schema = schemars::schema_for!(Config);
|
||||
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
match args.command {
|
||||
Some(command) => {
|
||||
if args.debug {
|
||||
eprintln!("REQUEST: {command:?}")
|
||||
}
|
||||
|
||||
let rt = create_runtime();
|
||||
rt.block_on(async move {
|
||||
let ipc = ipc::Ipc::new();
|
||||
match ipc.send(command).await {
|
||||
Ok(res) => cli::handle_response(res),
|
||||
match ipc.send(command, args.debug).await {
|
||||
Ok(res) => {
|
||||
if args.debug {
|
||||
eprintln!("RESPONSE: {res:?}")
|
||||
}
|
||||
|
||||
cli::handle_response(res, args.format.unwrap_or_default())
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
};
|
||||
});
|
||||
|
@ -232,6 +249,7 @@ impl Ironbar {
|
|||
|
||||
/// 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.
|
||||
#[must_use]
|
||||
pub fn unique_id() -> usize {
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
|
|
|
@ -18,6 +18,7 @@ use tokio::time::sleep;
|
|||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct CairoModule {
|
||||
/// The path to the Lua script to load.
|
||||
/// This can be absolute, or relative to the working directory.
|
||||
|
|
|
@ -17,6 +17,7 @@ use tokio::sync::{broadcast, mpsc};
|
|||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct ClipboardModule {
|
||||
/// The icon to show on the bar widget button.
|
||||
/// Supports [image](images) icons.
|
||||
|
|
|
@ -16,6 +16,7 @@ use crate::modules::{
|
|||
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct ClockModule {
|
||||
/// The format string to use for the date/time shown on the bar.
|
||||
/// Pango markup is supported.
|
||||
|
|
|
@ -6,6 +6,7 @@ use gtk::prelude::*;
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct BoxWidget {
|
||||
/// Widget name.
|
||||
///
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::{build, try_send};
|
|||
use super::{CustomWidget, CustomWidgetContext, ExecEvent, WidgetConfig};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct ButtonWidget {
|
||||
/// Widget name.
|
||||
///
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::image::ImageProvider;
|
|||
use super::{CustomWidget, CustomWidgetContext};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct ImageWidget {
|
||||
/// Widget name.
|
||||
///
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::dynamic_value::dynamic_string;
|
|||
use super::{CustomWidget, CustomWidgetContext};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct LabelWidget {
|
||||
/// Widget name.
|
||||
///
|
||||
|
|
|
@ -28,6 +28,7 @@ use tokio::sync::{broadcast, mpsc};
|
|||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct CustomModule {
|
||||
/// Modules and widgets to add to the bar container.
|
||||
///
|
||||
|
@ -45,6 +46,7 @@ pub struct CustomModule {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct WidgetConfig {
|
||||
/// One of a custom module native Ironbar module.
|
||||
#[serde(flatten)]
|
||||
|
@ -57,6 +59,7 @@ pub struct WidgetConfig {
|
|||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum WidgetOrModule {
|
||||
/// A custom-module specific basic widget
|
||||
Widget(Widget),
|
||||
|
@ -67,6 +70,7 @@ pub enum WidgetOrModule {
|
|||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum Widget {
|
||||
/// A container to place nested widgets inside.
|
||||
Box(BoxWidget),
|
||||
|
|
|
@ -13,6 +13,7 @@ use crate::{build, glib_recv_mpsc, spawn, try_send};
|
|||
use super::{CustomWidget, CustomWidgetContext};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct ProgressWidget {
|
||||
/// Widget name.
|
||||
///
|
||||
|
|
|
@ -16,6 +16,7 @@ use crate::{build, glib_recv_mpsc, spawn, try_send};
|
|||
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct SliderWidget {
|
||||
/// Widget name.
|
||||
///
|
||||
|
|
|
@ -12,6 +12,7 @@ use tokio::sync::mpsc;
|
|||
use tracing::debug;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct FocusedModule {
|
||||
/// Whether to show icon on the bar.
|
||||
///
|
||||
|
|
|
@ -9,6 +9,7 @@ use serde::Deserialize;
|
|||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct LabelModule {
|
||||
/// The text to show on the label.
|
||||
/// This is a [Dynamic String](dynamic-values#dynamic-string).
|
||||
|
|
|
@ -19,6 +19,7 @@ use tokio::sync::{broadcast, mpsc};
|
|||
use tracing::{debug, error, trace};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct LauncherModule {
|
||||
/// List of app IDs (or classes) to always show regardless of open state,
|
||||
/// in the order specified.
|
||||
|
|
|
@ -384,7 +384,7 @@ impl ModuleFactory for BarModuleFactory {
|
|||
}
|
||||
ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => {
|
||||
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();
|
||||
} else {
|
||||
popup.show(id, button_id);
|
||||
|
@ -457,7 +457,7 @@ impl ModuleFactory for PopupModuleFactory {
|
|||
}
|
||||
ModuleUpdateEvent::TogglePopup(_) if !disable_popup => {
|
||||
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();
|
||||
} else {
|
||||
popup.show(id, button_id);
|
||||
|
|
|
@ -4,6 +4,7 @@ use serde::Deserialize;
|
|||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Icons {
|
||||
/// Icon to display when playing.
|
||||
///
|
||||
|
@ -71,6 +72,7 @@ impl Default for Icons {
|
|||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum PlayerType {
|
||||
Mpd,
|
||||
Mpris,
|
||||
|
@ -83,6 +85,7 @@ impl Default for PlayerType {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct MusicModule {
|
||||
/// Type of player to connect to
|
||||
#[serde(default)]
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use glib::{Propagation, PropertySet};
|
||||
use glib::{markup_escape_text, Propagation, PropertySet};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Label, Orientation, Scale};
|
||||
use regex::Regex;
|
||||
|
@ -531,6 +531,7 @@ fn get_token_value(song: &Track, token: &str) -> String {
|
|||
"track" => song.track.map(|x| x.to_string()),
|
||||
_ => Some(token.to_string()),
|
||||
}
|
||||
.map(|str| markup_escape_text(str.as_str()).to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ use tokio::sync::mpsc::Receiver;
|
|||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct NotificationsModule {
|
||||
/// Whether to show the current notification count.
|
||||
///
|
||||
|
@ -29,6 +30,7 @@ pub struct NotificationsModule {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
struct Icons {
|
||||
/// Icon to show when the panel is closed, with no notifications.
|
||||
///
|
||||
|
@ -193,6 +195,7 @@ impl Module<Overlay> for NotificationsModule {
|
|||
if self.show_count {
|
||||
label.add_class("count");
|
||||
overlay.add_overlay(&label);
|
||||
overlay.set_overlay_pass_through(&label, true);
|
||||
}
|
||||
|
||||
let ctx = context.controller_tx.clone();
|
||||
|
|
|
@ -10,6 +10,7 @@ use tokio::sync::mpsc;
|
|||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct ScriptModule {
|
||||
/// Path to script to execute.
|
||||
///
|
||||
|
|
|
@ -14,6 +14,7 @@ use tokio::sync::mpsc;
|
|||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct SysInfoModule {
|
||||
/// List of strings including formatting tokens.
|
||||
/// For available tokens, see [below](#formatting-tokens).
|
||||
|
@ -51,6 +52,7 @@ pub struct SysInfoModule {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Intervals {
|
||||
/// The number of seconds between refreshing memory data.
|
||||
///
|
||||
|
@ -91,6 +93,7 @@ pub struct Intervals {
|
|||
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum Interval {
|
||||
All(u64),
|
||||
Individual(Intervals),
|
||||
|
|
|
@ -19,6 +19,7 @@ use tokio::sync::mpsc;
|
|||
use tracing::{debug, error, warn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct TrayModule {
|
||||
/// Requests that icons from the theme be used over the item-provided item.
|
||||
/// Most items only provide one or the other so this will have no effect in most circumstances.
|
||||
|
@ -38,7 +39,8 @@ pub struct TrayModule {
|
|||
/// **Valid options**: `top_to_bottom`, `bottom_to_top`, `left_to_right`, `right_to_left`
|
||||
/// <br>
|
||||
/// **Default**: `left_to_right` if bar is horizontal, `top_to_bottom` if bar is vertical
|
||||
#[serde(default, deserialize_with = "deserialize_orientation")]
|
||||
#[serde(default, deserialize_with = "deserialize_pack_direction")]
|
||||
#[cfg_attr(feature = "schema", schemars(schema_with = "schema_pack_direction"))]
|
||||
direction: Option<PackDirection>,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
|
@ -50,7 +52,7 @@ const fn default_icon_size() -> u32 {
|
|||
16
|
||||
}
|
||||
|
||||
fn deserialize_orientation<'de, D>(deserializer: D) -> Result<Option<PackDirection>, D::Error>
|
||||
fn deserialize_pack_direction<'de, D>(deserializer: D) -> Result<Option<PackDirection>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
|
@ -61,11 +63,24 @@ where
|
|||
"right_to_left" => Ok(PackDirection::Rtl),
|
||||
"top_to_bottom" => Ok(PackDirection::Ttb),
|
||||
"bottom_to_top" => Ok(PackDirection::Btt),
|
||||
_ => Err(serde::de::Error::custom("invalid value for orientation")),
|
||||
_ => Err(serde::de::Error::custom("invalid value for direction")),
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
fn schema_pack_direction(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
use schemars::JsonSchema;
|
||||
let mut schema: schemars::schema::SchemaObject = <String>::json_schema(gen).into();
|
||||
schema.enum_values = Some(vec![
|
||||
"top_to_bottom".into(),
|
||||
"bottom_to_top".into(),
|
||||
"left_to_right".into(),
|
||||
"right_to_left".into(),
|
||||
]);
|
||||
schema.into()
|
||||
}
|
||||
|
||||
impl Module<MenuBar> for TrayModule {
|
||||
type SendMessage = Event;
|
||||
type ReceiveMessage = ActivateRequest;
|
||||
|
|
|
@ -22,6 +22,7 @@ const HOUR: i64 = 60 * 60;
|
|||
const MINUTE: i64 = 60;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct UpowerModule {
|
||||
/// The format string to use for the widget button label.
|
||||
/// For available tokens, see [below](#formatting-tokens).
|
||||
|
|
|
@ -18,6 +18,7 @@ use std::collections::HashMap;
|
|||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct VolumeModule {
|
||||
/// Maximum value to allow volume sliders to reach.
|
||||
/// Pulse supports values > 100 but this may result in distortion.
|
||||
|
|
|
@ -15,6 +15,7 @@ use tracing::{debug, trace, warn};
|
|||
|
||||
#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum SortOrder {
|
||||
/// Shows workspaces in the order they're added
|
||||
Added,
|
||||
|
@ -31,6 +32,7 @@ impl Default for SortOrder {
|
|||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum Favorites {
|
||||
ByMonitor(HashMap<String, Vec<String>>),
|
||||
Global(Vec<String>),
|
||||
|
@ -43,6 +45,7 @@ impl Default for Favorites {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct WorkspacesModule {
|
||||
/// Map of actual workspace names to custom names.
|
||||
///
|
||||
|
|
|
@ -229,7 +229,7 @@ impl Popup {
|
|||
}
|
||||
|
||||
/// Checks if the popup is currently visible
|
||||
pub fn is_visible(&self) -> bool {
|
||||
pub fn visible(&self) -> bool {
|
||||
self.window.is_visible()
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ use tracing::{debug, error, trace, warn};
|
|||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum ScriptInput {
|
||||
String(String),
|
||||
Struct(Script),
|
||||
|
@ -21,6 +22,7 @@ pub enum ScriptInput {
|
|||
|
||||
#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum ScriptMode {
|
||||
Poll,
|
||||
Watch,
|
||||
|
@ -75,6 +77,7 @@ impl ScriptMode {
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Script {
|
||||
#[serde(default = "ScriptMode::default")]
|
||||
pub(crate) mode: ScriptMode,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue