261 lines
9.1 KiB
Rust
261 lines
9.1 KiB
Rust
//! barbar — a Wayland layer-shell status bar built on `iced` + `iced_layershell`.
|
|
//!
|
|
//! Renders a full-width bar pinned to the top of the screen via the
|
|
//! `wlr-layer-shell` protocol. Optionally target a specific output by
|
|
//! passing its name as the first CLI argument.
|
|
//!
|
|
//! Clicking any button spawns a separate LayerShell popup surface anchored
|
|
//! to that button's position on the bar.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use iced::widget::{column, container, text};
|
|
use iced::{window, Alignment, Element, Length, Rectangle, Subscription, Task, Theme};
|
|
use iced_layershell::daemon;
|
|
use iced_layershell::reexport::Anchor;
|
|
use iced_layershell::settings::{LayerShellSettings, Settings, StartMode};
|
|
use iced_layershell::to_layer_message;
|
|
|
|
use iced::futures::sink::SinkExt;
|
|
|
|
use crate::module::{BarModule, ModuleMsg};
|
|
use crate::subscriptions::clock::ClockTicker;
|
|
use crate::subscriptions::Subscription as ModuleSub;
|
|
use crate::subscriptions::SubscriptionPayloadKind;
|
|
|
|
mod module;
|
|
mod popup;
|
|
mod subscriptions;
|
|
|
|
/// Height of the bar in logical pixels.
|
|
const BAR_HEIGHT: u32 = 36;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Application state. Tracks which popup is open and owns the module registry.
|
|
struct Bar {
|
|
/// Which module's button triggered the open popup? None = closed.
|
|
active_popup: Option<String>,
|
|
/// Module registry keyed by stable module id.
|
|
modules: BTreeMap<&'static str, Box<dyn BarModule>>,
|
|
/// Surface ID of the currently open popup (for removal).
|
|
popup_id: Option<window::Id>,
|
|
/// Fan-out routing table: module id -> the subscription kinds it wants.
|
|
/// Built once at boot from `BarModule::subscriptions()`.
|
|
routes: BTreeMap<&'static str, Vec<ModuleSub>>,
|
|
}
|
|
|
|
impl Bar {
|
|
/// Builds the module registry. The daemon calls this once at boot.
|
|
fn new() -> Self {
|
|
let mut modules: BTreeMap<&'static str, Box<dyn BarModule>> = BTreeMap::new();
|
|
modules.insert("clock", Box::new(crate::module::clock::Clock::new()));
|
|
let routes = modules
|
|
.iter()
|
|
.map(|(id, module)| (*id, module.subscriptions()))
|
|
.collect();
|
|
Self {
|
|
active_popup: None,
|
|
modules,
|
|
popup_id: None,
|
|
routes,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Messages produced by user input, events and subscriptions.
|
|
#[to_layer_message(multi)]
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) enum Message {
|
|
Noop,
|
|
/// A routed module message; dispatch by `id` without naming its type.
|
|
Module(ModuleMsg),
|
|
/// A subscription event, pre-fanned by kind; `update` routes it.
|
|
Subscription(SubscriptionPayloadKind),
|
|
/// A bar module was clicked; requests a popup for that module.
|
|
/// Carries the module to run `view` from, and the element to stick to.
|
|
RequestPopup(String, String),
|
|
/// Internal: the widget-tree layout pass reported the laid-out bounds of
|
|
/// a module's clickable area; the popup is anchored there.
|
|
BoundsFound(String, Rectangle),
|
|
/// Closes the currently open popup.
|
|
ClosePopup(window::Id),
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Application wiring
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn namespace() -> String {
|
|
String::from("barbar")
|
|
}
|
|
|
|
fn main() -> Result<(), iced_layershell::Error> {
|
|
let start_mode = match std::env::args().nth(1) {
|
|
Some(output) => StartMode::TargetScreen(output),
|
|
None => StartMode::Active,
|
|
};
|
|
|
|
daemon(Bar::new, namespace, update, view)
|
|
.style(style)
|
|
.subscription(gather_subscriptions)
|
|
.settings(Settings {
|
|
layer_settings: LayerShellSettings {
|
|
size: Some((0, BAR_HEIGHT)),
|
|
exclusive_zone: BAR_HEIGHT as i32,
|
|
anchor: Anchor::Top | Anchor::Left | Anchor::Right,
|
|
start_mode,
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
})
|
|
.run()
|
|
}
|
|
|
|
fn gather_subscriptions(bar: &Bar) -> Subscription<Message> {
|
|
//TODO: Get a vec of subscription enum 'bar_wants' and spawn a batch depending on what the
|
|
//modules want.
|
|
let wants_clock = bar
|
|
.routes
|
|
.values()
|
|
.any(|kinds| kinds.iter().any(|k| matches!(k, ModuleSub::Clock(_))));
|
|
if !wants_clock {
|
|
return Subscription::none();
|
|
}
|
|
|
|
Subscription::run_with((), |_| {
|
|
iced::stream::channel(
|
|
0,
|
|
|mut sender: iced::futures::channel::mpsc::Sender<Message>| async move {
|
|
let mut clock = ClockTicker::new();
|
|
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
|
|
loop {
|
|
interval.tick().await;
|
|
for payload in clock.tick() {
|
|
if sender
|
|
.send(Message::Subscription(SubscriptionPayloadKind::Clock(
|
|
payload,
|
|
)))
|
|
.await
|
|
.is_err()
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
)
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Update
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn update(bar: &mut Bar, msg: Message) -> Task<Message> {
|
|
match msg {
|
|
Message::Module(m) => match bar.modules.get_mut(m.id) {
|
|
// The module produces its own (app-level) tasks, e.g. opening a
|
|
// popup for itself — nothing to map here.
|
|
Some(module) => module.update(m),
|
|
None => Task::none(),
|
|
},
|
|
|
|
Message::Subscription(kind) => match kind {
|
|
// Fan out by kind to every module whose route requests it. The
|
|
// payload kind is typed, so no downcast needed.
|
|
SubscriptionPayloadKind::Clock(payload) => bar
|
|
.modules
|
|
.iter_mut()
|
|
.filter(|(id, _)| {
|
|
bar.routes
|
|
.get(*id)
|
|
.is_some_and(|kinds| kinds.contains(&ModuleSub::Clock(payload.kind)))
|
|
})
|
|
.map(|(_, module)| module.update(ModuleMsg::new(module.id(), payload.clone())))
|
|
.fold(Task::none(), |acc, t| acc.chain(t)),
|
|
},
|
|
|
|
Message::RequestPopup(module_id, element_id) => {
|
|
// Toggle: close if already open, else open.
|
|
if bar.active_popup.as_deref() == Some(module_id.as_str()) {
|
|
popup::close_popup(bar)
|
|
} else {
|
|
// Query the widget tree for the element's laid-out bounds;
|
|
// the resulting `BoundsFound` opens the popup there.
|
|
popup::capture_bounds(module_id, element_id)
|
|
}
|
|
}
|
|
|
|
Message::BoundsFound(module_id, bounds) => popup::open_popup(bar, module_id, bounds),
|
|
|
|
Message::ClosePopup(id) => {
|
|
if bar.popup_id == Some(id) {
|
|
bar.popup_id = None;
|
|
bar.active_popup = None;
|
|
}
|
|
Task::done(Message::RemoveWindow(id))
|
|
}
|
|
|
|
Message::Noop => Task::none(),
|
|
|
|
// Forward multi-window mutations to their internal handlers.
|
|
_ => Task::none(),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// View — routed per-surface by window ID
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Dispatch view by which surface is being rendered.
|
|
fn view(bar: &Bar, id: window::Id) -> Element<'_, Message> {
|
|
// The bar is always created first. Any subsequently opened popup has a
|
|
// different IcedId we can distinguish. We compare against the bar's
|
|
// known ID (stored implicitly as "first window").
|
|
if bar.popup_id == Some(id) {
|
|
// This is the popup surface.
|
|
popup::view(bar)
|
|
} else {
|
|
// This is the main bar surface.
|
|
bar_view(bar)
|
|
}
|
|
}
|
|
|
|
/// Main bar widget tree. Each module renders itself; the app only lays the
|
|
/// modules out in a row. Popup requests are the module's own business: its
|
|
/// `update` returns the task that asks the app for a popup.
|
|
fn bar_view(bar: &Bar) -> Element<'_, Message> {
|
|
let mut children: Vec<Element<Message>> = Vec::new();
|
|
|
|
for (i, (_, module)) in bar.modules.iter().enumerate() {
|
|
children.push(module.view(None).map(Message::Module));
|
|
if i < bar.modules.len() - 1 {
|
|
children.push(text("|").size(14).into());
|
|
}
|
|
}
|
|
|
|
container(
|
|
column(children)
|
|
.width(Length::Fill)
|
|
.align_x(Alignment::Center),
|
|
)
|
|
.padding(8)
|
|
.align_x(Alignment::Center)
|
|
.into()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Style
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn style(_bar: &Bar, theme: &Theme) -> iced::theme::Style {
|
|
use iced::theme::Style;
|
|
Style {
|
|
background_color: theme.palette().background,
|
|
text_color: theme.palette().text,
|
|
}
|
|
}
|