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

Merge pull request #497 from JakeStanger/feat/cairo

feat: new cairo module
This commit is contained in:
Jake Stanger 2024-04-17 20:49:59 +01:00 committed by GitHub
commit e6f7108862
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 622 additions and 24 deletions

41
src/clients/lua.rs Normal file
View file

@ -0,0 +1,41 @@
use mlua::Lua;
use std::ops::Deref;
use std::path::Path;
use tracing::{debug, error};
/// Wrapper around Lua instance
/// to create a singleton and handle initialization.
#[derive(Debug)]
pub struct LuaEngine {
lua: Lua,
}
impl LuaEngine {
pub fn new(config_dir: &Path) -> Self {
let lua = unsafe { Lua::unsafe_new() };
let user_init = config_dir.join("init.lua");
if user_init.exists() {
debug!("loading user init script");
if let Err(err) = lua.load(user_init).exec() {
error!("{err:?}");
}
}
debug!("loading internal init script");
if let Err(err) = lua.load(include_str!("../../lua/init.lua")).exec() {
error!("{err:?}");
}
Self { lua }
}
}
impl Deref for LuaEngine {
type Target = Lua;
fn deref(&self) -> &Self::Target {
&self.lua
}
}

View file

@ -1,11 +1,15 @@
use crate::{await_sync, Ironbar};
use color_eyre::Result;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
#[cfg(feature = "clipboard")]
pub mod clipboard;
#[cfg(feature = "workspaces")]
pub mod compositor;
#[cfg(feature = "cairo")]
pub mod lua;
#[cfg(feature = "music")]
pub mod music;
#[cfg(feature = "notifications")]
@ -27,6 +31,8 @@ pub struct Clients {
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
#[cfg(feature = "clipboard")]
clipboard: Option<Arc<clipboard::Client>>,
#[cfg(feature = "cairo")]
lua: Option<Rc<lua::LuaEngine>>,
#[cfg(feature = "music")]
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
#[cfg(feature = "notifications")]
@ -75,6 +81,13 @@ impl Clients {
Ok(client)
}
#[cfg(feature = "cairo")]
pub fn lua(&mut self, config_dir: &Path) -> Rc<lua::LuaEngine> {
self.lua
.get_or_insert_with(|| Rc::new(lua::LuaEngine::new(config_dir)))
.clone()
}
#[cfg(feature = "music")]
pub fn music(&mut self, client_type: music::ClientType) -> Arc<dyn music::MusicClient> {
self.music

View file

@ -2,6 +2,8 @@ mod common;
mod r#impl;
mod truncate;
#[cfg(feature = "cairo")]
use crate::modules::cairo::CairoModule;
#[cfg(feature = "clipboard")]
use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")]
@ -40,6 +42,8 @@ pub use self::truncate::TruncateMode;
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
#[cfg(feature = "cairo")]
Cairo(Box<CairoModule>),
#[cfg(feature = "clipboard")]
Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")]
@ -81,6 +85,8 @@ impl ModuleConfig {
}
match self {
#[cfg(feature = "cairo")]
Self::Cairo(module) => create!(module),
#[cfg(feature = "clipboard")]
Self::Clipboard(module) => create!(module),
#[cfg(feature = "clock")]

View file

@ -96,14 +96,18 @@ pub struct Ironbar {
bars: Rc<RefCell<Vec<Bar>>>,
clients: Rc<RefCell<Clients>>,
config: Rc<RefCell<Config>>,
config_dir: PathBuf,
}
impl Ironbar {
fn new() -> Self {
let (config, config_dir) = load_config();
Self {
bars: Rc::new(RefCell::new(vec![])),
clients: Rc::new(RefCell::new(Clients::new())),
config: Rc::new(RefCell::new(load_config())),
config: Rc::new(RefCell::new(config)),
config_dir,
}
}
@ -260,7 +264,7 @@ impl Ironbar {
/// Note this does *not* reload bars, which must be performed separately.
#[cfg(feature = "ipc")]
fn reload_config(&self) {
self.config.replace(load_config());
self.config.replace(load_config().0);
}
}
@ -270,20 +274,37 @@ fn start_ironbar() {
}
/// Loads the config file from disk.
fn load_config() -> Config {
let mut config = env::var("IRONBAR_CONFIG")
.map_or_else(
|_| ConfigLoader::new("ironbar").find_and_load(),
ConfigLoader::load,
)
.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
fn load_config() -> (Config, PathBuf) {
let config_path = env::var("IRONBAR_CONFIG");
Config::default()
});
let (config, directory) = if let Ok(config_path) = config_path {
let path = PathBuf::from(config_path);
(
ConfigLoader::load(&path),
path.parent()
.map(PathBuf::from)
.ok_or_else(|| Report::msg("Specified path has no parent")),
)
} else {
let config_loader = ConfigLoader::new("ironbar");
(
config_loader.find_and_load(),
config_loader.config_dir().map_err(Report::new),
)
};
let mut config = config.unwrap_or_else(|err| {
error!("Failed to load config: {}", err);
warn!("Falling back to the default config");
info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
Config::default()
});
let directory = directory
.and_then(|dir| dir.canonicalize().map_err(Report::new))
.unwrap_or_else(|_| env::current_dir().expect("to have current working directory"));
debug!("Loaded config file");
@ -297,7 +318,7 @@ fn load_config() -> Config {
}
}
config
(config, directory)
}
/// Gets the GDK `Display` instance.

198
src/modules/cairo.rs Normal file
View file

@ -0,0 +1,198 @@
use crate::config::CommonConfig;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, module_impl, spawn, try_send};
use cairo::{Format, ImageSurface};
use glib::translate::IntoGlibPtr;
use glib::Propagation;
use gtk::prelude::*;
use gtk::DrawingArea;
use mlua::{Error, Function, LightUserData};
use notify::event::ModifyKind;
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Watcher};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::mpsc::Receiver;
use tokio::time::sleep;
use tracing::{debug, error};
#[derive(Debug, Clone, Deserialize)]
pub struct CairoModule {
path: PathBuf,
#[serde(default = "default_frequency")]
frequency: u64,
#[serde(default = "default_size")]
width: u32,
#[serde(default = "default_size")]
height: u32,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
const fn default_size() -> u32 {
42
}
const fn default_frequency() -> u64 {
200
}
impl Module<gtk::Box> for CairoModule {
type SendMessage = ();
type ReceiveMessage = ();
module_impl!("cairo");
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: Receiver<Self::ReceiveMessage>,
) -> color_eyre::Result<()>
where
<Self as Module<gtk::Box>>::SendMessage: Clone,
{
let path = self.path.to_path_buf();
let tx = context.tx.clone();
spawn(async move {
let parent = path.parent().expect("to have parent path");
let mut watcher = recommended_watcher({
let path = path.clone();
move |res: notify::Result<Event>| match res {
Ok(event) if matches!(event.kind, EventKind::Modify(ModifyKind::Data(_))) => {
debug!("{event:?}");
if event.paths.first().is_some_and(|p| p == &path) {
try_send!(tx, ModuleUpdateEvent::Update(()));
}
}
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
_ => {}
}
})
.expect("Failed to create lua file watcher");
watcher
.watch(parent, RecursiveMode::NonRecursive)
.expect("Failed to start lua file watcher");
// avoid watcher from dropping
loop {
sleep(Duration::from_secs(1)).await;
}
});
// Lua needs to run synchronously with the GTK updates,
// so the controller does not handle the script engine.
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleParts<gtk::Box>>
where
<Self as Module<gtk::Box>>::SendMessage: Clone,
{
let id = context.id.to_string();
let container = gtk::Box::new(info.bar_position.orientation(), 0);
let surface = ImageSurface::create(Format::ARgb32, self.width as i32, self.height as i32)?;
let area = DrawingArea::new();
let lua = context
.ironbar
.clients
.borrow_mut()
.lua(&context.ironbar.config_dir);
// this feels kinda dirty,
// but it keeps draw functions separate in the global scope
let script = fs::read_to_string(&self.path)?
.replace("function draw", format!("function __draw_{id}").as_str());
lua.load(&script).exec()?;
{
let lua = lua.clone();
let id = id.clone();
let path = self.path.clone();
area.connect_draw(move |_, cr| {
let function: Function = lua
.load(include_str!("../../lua/draw.lua"))
.eval()
.expect("to be valid");
if let Err(err) = cr.set_source_surface(&surface, 0.0, 0.0) {
error!("{err}");
return Propagation::Stop;
}
let ptr = unsafe { cr.clone().into_glib_ptr().cast() };
// mlua needs a valid return type, even if we don't return anything
if let Err(err) =
function.call::<_, Option<bool>>((id.as_str(), LightUserData(ptr)))
{
match err {
Error::RuntimeError(message) => {
let message = message.split_once("]:").expect("to exist").1;
error!("[lua runtime error] {}:{message}", path.display())
}
_ => error!("{err}"),
}
return Propagation::Stop;
}
Propagation::Proceed
});
}
area.set_size_request(self.width as i32, self.height as i32);
container.add(&area);
glib::spawn_future_local(async move {
loop {
area.queue_draw();
glib::timeout_future(Duration::from_millis(self.frequency)).await;
}
});
glib_recv!(context.subscribe(), _ev => {
let res = fs::read_to_string(&self.path)
.map(|s| s.replace("function draw", format!("function __draw_{id}").as_str()));
match res {
Ok(script) => {
match lua.load(&script).exec() {
Ok(_) => {},
Err(Error::SyntaxError { message, ..}) => {
let message = message.split_once("]:").expect("to exist").1;
error!("[lua syntax error] {}:{message}", self.path.display())
},
Err(err) => error!("lua error: {err:?}")
}
},
Err(err) => error!("{err:?}")
}
});
Ok(ModuleParts {
widget: container,
popup: None,
})
}
}

View file

@ -16,6 +16,8 @@ use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup;
use crate::{glib_recv_mpsc, send, Ironbar};
#[cfg(feature = "cairo")]
pub mod cairo;
#[cfg(feature = "clipboard")]
pub mod clipboard;
/// Displays the current date and time.