mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-04-19 19:34:24 +02:00
feat: new custom module
Allows basic modules to be created from a config object, including popup content.
This commit is contained in:
parent
e23e691bc6
commit
3750124d8c
10 changed files with 326 additions and 54 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
@ -518,9 +518,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4"
|
||||
checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-flags",
|
||||
|
@ -530,9 +530,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199"
|
||||
checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
|
@ -545,15 +545,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c"
|
||||
checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea"
|
||||
checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -762,15 +762,6 @@ version = "1.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.24"
|
||||
|
@ -1154,9 +1145,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa"
|
||||
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"cxx-build",
|
||||
|
@ -1267,9 +1258,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.0.6"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d6112e8f37b59803ac47a42d14f1f3a59bbf72fc6857ffc5be455e28a691f8e"
|
||||
checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
|
@ -1462,9 +1453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
|
|
|
@ -32,7 +32,7 @@ regex = "1.6.0"
|
|||
stray = { version = "0.1.2" }
|
||||
dirs = "4.0.0"
|
||||
walkdir = "2.3.2"
|
||||
notify = "5.0.0"
|
||||
notify = { version = "5.0.0", default-features = false }
|
||||
mpd_client = "1.0.0"
|
||||
swayipc-async = { version = "2.0.1" }
|
||||
sysinfo = "0.26.4"
|
||||
|
|
25
examples/custom.corn
Normal file
25
examples/custom.corn
Normal file
|
@ -0,0 +1,25 @@
|
|||
let {
|
||||
$power_menu = {
|
||||
type = "custom"
|
||||
class = "power-menu"
|
||||
|
||||
bar = [ { type = "button" name="power-btn" label = "" exec = "popup:toggle" } ]
|
||||
|
||||
popup = [ {
|
||||
type = "box"
|
||||
orientation = "vertical"
|
||||
widgets = [
|
||||
{ type = "label" name = "header" label = "Power menu" }
|
||||
{
|
||||
type = "box"
|
||||
widgets = [
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" exec = "!shutdown now" }
|
||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" exec = "!reboot" }
|
||||
]
|
||||
}
|
||||
]
|
||||
} ]
|
||||
}
|
||||
} in {
|
||||
end = [ { type = "clock" } $power_menu ]
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
use crate::bridge_channel::BridgeChannel;
|
||||
use crate::config::{BarPosition, ModuleConfig};
|
||||
use crate::modules::custom::ExecEvent;
|
||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||
use crate::modules::mpd::{PlayerCommand, SongUpdate};
|
||||
use crate::modules::workspaces::WorkspaceUpdate;
|
||||
|
@ -236,6 +237,9 @@ fn add_modules(
|
|||
ModuleConfig::Launcher(module) => {
|
||||
add_module!(module, id, "launcher", LauncherUpdate, ItemEvent);
|
||||
}
|
||||
ModuleConfig::Custom(module) => {
|
||||
add_module!(module, id, "custom", (), ExecEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::modules::clock::ClockModule;
|
||||
use crate::modules::custom::CustomModule;
|
||||
use crate::modules::focused::FocusedModule;
|
||||
use crate::modules::launcher::LauncherModule;
|
||||
use crate::modules::mpd::MpdModule;
|
||||
|
@ -28,6 +29,7 @@ pub enum ModuleConfig {
|
|||
Launcher(LauncherModule),
|
||||
Script(ScriptModule),
|
||||
Focused(FocusedModule),
|
||||
Custom(CustomModule),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
|
|
@ -6,6 +6,7 @@ mod icon;
|
|||
mod logging;
|
||||
mod modules;
|
||||
mod popup;
|
||||
mod script;
|
||||
mod style;
|
||||
mod sway;
|
||||
mod wayland;
|
||||
|
|
241
src/modules/custom.rs
Normal file
241
src/modules/custom.rs
Normal file
|
@ -0,0 +1,241 @@
|
|||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::{ButtonGeometry, Popup};
|
||||
use crate::script::exec_command;
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, Label, Orientation};
|
||||
use serde::Deserialize;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct CustomModule {
|
||||
/// Container class name
|
||||
class: Option<String>,
|
||||
/// Widgets to add to the bar container
|
||||
bar: Vec<Widget>,
|
||||
/// Widgets to add to the popup container
|
||||
popup: Option<Vec<Widget>>,
|
||||
}
|
||||
|
||||
/// Attempts to parse an `Orientation` from `String`
|
||||
fn try_get_orientation(orientation: String) -> Result<Orientation> {
|
||||
match orientation.to_lowercase().as_str() {
|
||||
"horizontal" | "h" => Ok(Orientation::Horizontal),
|
||||
"vertical" | "v" => Ok(Orientation::Vertical),
|
||||
_ => Err(Report::msg("Invalid orientation string in config")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget attributes
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Widget {
|
||||
/// Type of GTK widget to add
|
||||
#[serde(rename = "type")]
|
||||
widget_type: WidgetType,
|
||||
widgets: Option<Vec<Widget>>,
|
||||
label: Option<String>,
|
||||
name: Option<String>,
|
||||
class: Option<String>,
|
||||
exec: Option<String>,
|
||||
orientation: Option<String>,
|
||||
}
|
||||
|
||||
/// Supported GTK widget types
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum WidgetType {
|
||||
Box,
|
||||
Label,
|
||||
Button,
|
||||
}
|
||||
|
||||
impl Widget {
|
||||
/// Creates this widget and adds it to the parent container
|
||||
fn add_to(self, parent: >k::Box, tx: Sender<ExecEvent>, bar_orientation: Orientation) {
|
||||
match self.widget_type {
|
||||
WidgetType::Box => parent.add(&self.into_box(tx, bar_orientation)),
|
||||
WidgetType::Label => parent.add(&self.into_label()),
|
||||
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Box` from this widget
|
||||
fn into_box(self, tx: Sender<ExecEvent>, bar_orientation: Orientation) -> gtk::Box {
|
||||
let mut builder = gtk::Box::builder();
|
||||
|
||||
if let Some(name) = self.name {
|
||||
builder = builder.name(&name);
|
||||
}
|
||||
|
||||
if let Some(orientation) = self.orientation {
|
||||
builder = builder
|
||||
.orientation(try_get_orientation(orientation).unwrap_or(Orientation::Horizontal));
|
||||
}
|
||||
|
||||
let container = builder.build();
|
||||
|
||||
if let Some(class) = self.class {
|
||||
container.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
if let Some(widgets) = self.widgets {
|
||||
widgets
|
||||
.into_iter()
|
||||
.for_each(|widget| widget.add_to(&container, tx.clone(), bar_orientation))
|
||||
}
|
||||
|
||||
container
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Label` from this widget
|
||||
fn into_label(self) -> Label {
|
||||
let mut builder = Label::builder().use_markup(true);
|
||||
|
||||
if let Some(name) = self.name {
|
||||
builder = builder.name(&name);
|
||||
}
|
||||
|
||||
let label = builder.build();
|
||||
|
||||
if let Some(text) = self.label {
|
||||
label.set_markup(&text);
|
||||
}
|
||||
|
||||
if let Some(class) = self.class {
|
||||
label.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
label
|
||||
}
|
||||
|
||||
/// Creates a `gtk::Button` from this widget
|
||||
fn into_button(self, tx: Sender<ExecEvent>, bar_orientation: Orientation) -> Button {
|
||||
let mut builder = Button::builder();
|
||||
|
||||
if let Some(name) = self.name {
|
||||
builder = builder.name(&name);
|
||||
}
|
||||
|
||||
let button = builder.build();
|
||||
|
||||
if let Some(text) = self.label {
|
||||
let label = Label::new(None);
|
||||
label.set_use_markup(true);
|
||||
label.set_markup(&text);
|
||||
button.add(&label);
|
||||
}
|
||||
|
||||
if let Some(class) = self.class {
|
||||
button.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
if let Some(exec) = self.exec {
|
||||
button.connect_clicked(move |button| {
|
||||
tx.try_send(ExecEvent {
|
||||
cmd: exec.clone(),
|
||||
geometry: Popup::button_pos(button, bar_orientation),
|
||||
})
|
||||
.expect("Failed to send exec message");
|
||||
});
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecEvent {
|
||||
cmd: String,
|
||||
geometry: ButtonGeometry,
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for CustomModule {
|
||||
type SendMessage = ();
|
||||
type ReceiveMessage = ExecEvent;
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
println!("{:?}", event);
|
||||
if event.cmd.starts_with('!') {
|
||||
debug!("executing command: '{}'", &event.cmd[1..]);
|
||||
if let Err(err) = exec_command(&event.cmd[1..]) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
} else if event.cmd == "popup:toggle" {
|
||||
tx.send(ModuleUpdateEvent::TogglePopup(event.geometry))
|
||||
.await
|
||||
.expect("Failed to send open popup event");
|
||||
} else if event.cmd == "popup:open" {
|
||||
tx.send(ModuleUpdateEvent::OpenPopup(event.geometry))
|
||||
.await
|
||||
.expect("Failed to send open popup event");
|
||||
} else if event.cmd == "popup:close" {
|
||||
tx.send(ModuleUpdateEvent::ClosePopup)
|
||||
.await
|
||||
.expect("Failed to send open popup event");
|
||||
} else {
|
||||
error!("Received invalid command: '{}'", event.cmd);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleWidget<gtk::Box>> {
|
||||
let orientation = info.bar_position.get_orientation();
|
||||
let container = gtk::Box::builder().orientation(orientation).build();
|
||||
|
||||
if let Some(ref class) = self.class {
|
||||
container.style_context().add_class(class)
|
||||
}
|
||||
|
||||
self.bar.clone().into_iter().for_each(|widget| {
|
||||
widget.add_to(&container, context.controller_tx.clone(), orientation)
|
||||
});
|
||||
|
||||
let popup = self.into_popup(context.controller_tx, context.popup_rx);
|
||||
|
||||
Ok(ModuleWidget {
|
||||
widget: container,
|
||||
popup,
|
||||
})
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: Sender<Self::ReceiveMessage>,
|
||||
_rx: glib::Receiver<Self::SendMessage>,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let container = gtk::Box::builder().name("popup-custom").build();
|
||||
|
||||
if let Some(class) = self.class {
|
||||
container
|
||||
.style_context()
|
||||
.add_class(format!("popup-{class}").as_str())
|
||||
}
|
||||
|
||||
if let Some(popup) = self.popup {
|
||||
popup
|
||||
.into_iter()
|
||||
.for_each(|widget| widget.add_to(&container, tx.clone(), Orientation::Horizontal));
|
||||
}
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
/// Clicking the widget opens a popup containing the current time
|
||||
/// with second-level precision and a calendar.
|
||||
pub mod clock;
|
||||
pub mod custom;
|
||||
pub mod focused;
|
||||
pub mod launcher;
|
||||
pub mod mpd;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use color_eyre::{eyre::Report, eyre::Result, eyre::WrapErr, Section};
|
||||
use crate::script::exec_command;
|
||||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
use std::process::Command;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::time::sleep;
|
||||
use tracing::{error, instrument};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ScriptModule {
|
||||
|
@ -37,7 +37,7 @@ impl Module<Label> for ScriptModule {
|
|||
let path = self.path.clone();
|
||||
spawn(async move {
|
||||
loop {
|
||||
match run_script(&path) {
|
||||
match exec_command(&path) {
|
||||
Ok(stdout) => tx
|
||||
.send(ModuleUpdateEvent::Update(stdout))
|
||||
.await
|
||||
|
@ -74,29 +74,3 @@ impl Module<Label> for ScriptModule {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn run_script(path: &str) -> Result<String> {
|
||||
let output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(path)
|
||||
.output()
|
||||
.wrap_err("Failed to get script output")?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8(output.stdout)
|
||||
.map(|output| output.trim().to_string())
|
||||
.wrap_err("Script stdout not valid UTF-8")?;
|
||||
|
||||
Ok(stdout)
|
||||
} else {
|
||||
let stderr = String::from_utf8(output.stderr)
|
||||
.map(|output| output.trim().to_string())
|
||||
.wrap_err("Script stderr not valid UTF-8")?;
|
||||
|
||||
Err(Report::msg(stderr)
|
||||
.wrap_err("Script returned non-zero error code")
|
||||
.suggestion("Check the path to your script")
|
||||
.suggestion("Check the script for errors"))
|
||||
}
|
||||
}
|
||||
|
|
35
src/script.rs
Normal file
35
src/script.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use color_eyre::eyre::WrapErr;
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use std::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
/// Attempts to execute a given command.
|
||||
/// If the command returns status 0,
|
||||
/// the `stdout` is returned.
|
||||
/// Otherwise, an `Err` variant
|
||||
/// containing the `stderr` is returned.
|
||||
#[instrument]
|
||||
pub fn exec_command(command: &str) -> Result<String> {
|
||||
let output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(command)
|
||||
.output()
|
||||
.wrap_err("Failed to get script output")?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8(output.stdout)
|
||||
.map(|output| output.trim().to_string())
|
||||
.wrap_err("Script stdout not valid UTF-8")?;
|
||||
|
||||
Ok(stdout)
|
||||
} else {
|
||||
let stderr = String::from_utf8(output.stderr)
|
||||
.map(|output| output.trim().to_string())
|
||||
.wrap_err("Script stderr not valid UTF-8")?;
|
||||
|
||||
Err(Report::msg(stderr)
|
||||
.wrap_err("Script returned non-zero error code")
|
||||
.suggestion("Check the path to your script")
|
||||
.suggestion("Check the script for errors"))
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue