meow
This commit is contained in:
Generated
+27
@@ -563,6 +563,8 @@ dependencies = [
|
||||
"iced",
|
||||
"iced_layershell",
|
||||
"modules",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"services",
|
||||
"tokio",
|
||||
"toml",
|
||||
@@ -3352,6 +3354,12 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -3493,6 +3501,19 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "services"
|
||||
version = "0.1.0"
|
||||
@@ -4225,6 +4246,12 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
||||
@@ -16,6 +16,7 @@ chrono = "0.4"
|
||||
toml = "1.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
thiserror = "2"
|
||||
clap = { version = "4.6.6", features = ["derive", "help"] }
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use iced::{Font, Pixels};
|
||||
use iced_layershell::reexport::{Anchor, KeyboardInteractivity};
|
||||
use iced_layershell::settings::{LayerShellSettings, Settings, StartMode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Bar text font, matching quickshell's `CaskaydiaCove NFM`.
|
||||
const DEFAULT_FONT: &str = "CaskaydiaCove NFM";
|
||||
@@ -37,6 +38,9 @@ pub struct Display {
|
||||
pub padding: f32,
|
||||
/// Gap between modules in the bar, logical px.
|
||||
pub spacing: f32,
|
||||
/// Path to a base16 YAML scheme to theme the bar with; system theme if
|
||||
/// absent.
|
||||
pub theme: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for Display {
|
||||
@@ -48,6 +52,7 @@ impl Default for Display {
|
||||
default_text_size: DEFAULT_TEXT_SIZE,
|
||||
padding: DEFAULT_PADDING,
|
||||
spacing: DEFAULT_SPACING,
|
||||
theme: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ mod display;
|
||||
mod effect;
|
||||
mod messages;
|
||||
mod module;
|
||||
mod reusable_elements;
|
||||
mod service;
|
||||
mod wire;
|
||||
|
||||
@@ -17,5 +18,6 @@ pub use display::Display;
|
||||
pub use effect::ModuleEffect;
|
||||
pub use messages::*;
|
||||
pub use module::BarModule;
|
||||
pub use reusable_elements::pill;
|
||||
pub use service::Service;
|
||||
pub use wire::{Endpoint, Inbox, Namespace, Target, Wire};
|
||||
|
||||
@@ -19,9 +19,9 @@ impl ClockKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clock tick payload: the value plus the kind that produced it.
|
||||
/// Clock tick: the kind is the variant, the rendered time the parameter.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct ClockPayload {
|
||||
pub kind: ClockKind,
|
||||
pub value: String,
|
||||
pub enum ClockPayload {
|
||||
Mins(String),
|
||||
Seconds(String),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
use iced::{border, widget::container, Theme};
|
||||
|
||||
const REF_RADIUS: f32 = 3.5;
|
||||
|
||||
/// The quickshell pill: rounded, filled with the theme background (the
|
||||
/// darkest colour the palette offers) so it reads as a solid block over the
|
||||
/// transparent bar.
|
||||
pub fn pill(theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(theme.palette().background.into()),
|
||||
text_color: Some(theme.palette().text),
|
||||
border: border::rounded(REF_RADIUS),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ tracing-subscriber = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = {workspace = true}
|
||||
toml = {workspace =true}
|
||||
serde = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
# Modules + services
|
||||
modules = { path = "../modules" }
|
||||
services = { path = "../services" }
|
||||
|
||||
@@ -16,6 +16,7 @@ mod args;
|
||||
mod msg;
|
||||
mod popup;
|
||||
mod subscription;
|
||||
mod theme;
|
||||
mod update;
|
||||
mod view;
|
||||
|
||||
@@ -58,6 +59,9 @@ fn main() -> Result<(), iced_layershell::Error> {
|
||||
let (padding, spacing) = (disp.padding, disp.spacing);
|
||||
let settings = disp.settings();
|
||||
|
||||
// Optional base16 theme; `None` lets iced follow the system colour scheme.
|
||||
let theme = disp.theme.as_deref().map(theme::load);
|
||||
|
||||
daemon(
|
||||
move || {
|
||||
Bar::new(
|
||||
@@ -76,6 +80,7 @@ fn main() -> Result<(), iced_layershell::Error> {
|
||||
view::view,
|
||||
)
|
||||
.style(view::style)
|
||||
.theme(move |_: &Bar, _: iced::window::Id| theme.clone())
|
||||
.subscription(subscription::gather_subscriptions)
|
||||
.settings(settings)
|
||||
.run()
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Base16 (`base00`..`base0F`) scheme loaded from YAML into an iced [`Theme`].
|
||||
//!
|
||||
//! The sixteen slots map onto iced's six-colour [`Palette`]: `base00` is the
|
||||
//! background, `base05` the foreground, and the blue/green/yellow/red accents
|
||||
//! become primary/success/warning/danger. The remaining slots are unused.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use iced::theme::Palette;
|
||||
use iced::{Color, Theme};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// The base16 slots the palette needs; other keys (author, base01..) are
|
||||
/// ignored by serde.
|
||||
#[derive(Deserialize)]
|
||||
struct Base16 {
|
||||
scheme: Option<String>,
|
||||
base00: String,
|
||||
base05: String,
|
||||
base08: String,
|
||||
#[serde(rename = "base0A")]
|
||||
base0a: String,
|
||||
#[serde(rename = "base0B")]
|
||||
base0b: String,
|
||||
#[serde(rename = "base0D")]
|
||||
base0d: String,
|
||||
}
|
||||
|
||||
impl Base16 {
|
||||
fn into_theme(self) -> Theme {
|
||||
Theme::custom(
|
||||
self.scheme.unwrap_or_else(|| "base16".to_string()),
|
||||
Palette {
|
||||
background: hex(&self.base00),
|
||||
text: hex(&self.base05),
|
||||
primary: hex(&self.base0d),
|
||||
success: hex(&self.base0b),
|
||||
warning: hex(&self.base0a),
|
||||
danger: hex(&self.base08),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// `"RRGGBB"` (optionally `#`-prefixed) to a [`Color`].
|
||||
fn hex(s: &str) -> Color {
|
||||
let v = u32::from_str_radix(s.trim_start_matches('#'), 16)
|
||||
.unwrap_or_else(|_| panic!("base16 colour {s:?} is not RRGGBB hex"));
|
||||
Color::from_rgb8((v >> 16) as u8, (v >> 8) as u8, v as u8)
|
||||
}
|
||||
|
||||
/// Loads a base16 YAML scheme; panics with the offending path on a read or
|
||||
/// parse error, matching how the TOML config is loaded.
|
||||
pub fn load(path: &Path) -> Theme {
|
||||
let text = std::fs::read_to_string(path)
|
||||
.unwrap_or_else(|e| panic!("cannot read theme {}: {e}", path.display()));
|
||||
let scheme: Base16 = serde_yaml::from_str(&text)
|
||||
.unwrap_or_else(|e| panic!("cannot parse base16 theme {}: {e}", path.display()));
|
||||
scheme.into_theme()
|
||||
}
|
||||
@@ -87,7 +87,7 @@ fn bar_view(bar: &Bar) -> Element<'_, Message> {
|
||||
pub(crate) fn style(_bar: &Bar, theme: &Theme) -> iced::theme::Style {
|
||||
use iced::theme::Style;
|
||||
Style {
|
||||
background_color: theme.palette().background,
|
||||
background_color: iced::Color::TRANSPARENT,
|
||||
text_color: theme.palette().text,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
//! something is muted — the signal the quickshell Audio widget gives.
|
||||
|
||||
use iced::widget::{container, row, space, text};
|
||||
use iced::{border, Alignment, Element, Length, Task, Theme};
|
||||
use iced::{Alignment, Element, Length, Task};
|
||||
|
||||
use common::{AudioNode, BarModule, ModuleEffect, PipewireMsg, PipewireState, Service, Wire};
|
||||
use common::{pill, AudioNode, BarModule, ModuleEffect, PipewireMsg, PipewireState, Service, Wire};
|
||||
|
||||
/// Glyphs exactly as the Quickshell Audio widget draws them: the speaker is a
|
||||
/// Material `md-volume` icon, the mic a Font Awesome one.
|
||||
@@ -19,7 +19,6 @@ const MIC_MUTED: &str = "\u{f131}"; // fa-microphone_slash
|
||||
/// given — if the bar ever leaves 20 px, scale the two sizes here.
|
||||
const REF_SLOT_W: f32 = 24.0;
|
||||
const REF_SPACING: f32 = 4.0;
|
||||
const REF_RADIUS: f32 = 3.5;
|
||||
/// The speaker is re-drawn in the mono face, whose ink is 1168/1496 the size
|
||||
/// of the proportional face quickshell uses, so it is pre-compensated.
|
||||
const REF_SINK_SIZE: f32 = 20.0 * 1496.0 / 1168.0;
|
||||
@@ -95,12 +94,3 @@ fn glyph(node: AudioNode, on: &'static str, muted: &'static str) -> &'static str
|
||||
on
|
||||
}
|
||||
}
|
||||
|
||||
/// The quickshell pill: rounded, a shade lighter than the bar behind it.
|
||||
fn pill(theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(theme.extended_palette().background.weak.color.into()),
|
||||
border: border::rounded(REF_RADIUS),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
//! service to switch which kind it publishes; right-click opens a popup.
|
||||
|
||||
use iced::widget::{container, mouse_area, text};
|
||||
use iced::{Element, Task};
|
||||
use iced::{Element, Padding, Task};
|
||||
|
||||
use common::{BarModule, ClockKind, ClockMsg, ClockPayload, Endpoint, ModuleEffect, Service, Wire};
|
||||
use common::{
|
||||
pill, BarModule, ClockKind, ClockMsg, ClockPayload, Endpoint, ModuleEffect, Service, Wire,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use toml::Table;
|
||||
|
||||
@@ -88,6 +90,12 @@ impl BarModule for Clock {
|
||||
.on_press(Wire::module(me, self.id(), ClockMsg::ToggleSeconds))
|
||||
.on_right_press(Wire::module(me, self.id(), ClockMsg::OpenPopup)),
|
||||
)
|
||||
.style(pill)
|
||||
.padding(Padding {
|
||||
right: 3.0,
|
||||
left: 3.0,
|
||||
..Default::default()
|
||||
})
|
||||
.id(iced::widget::Id::from(self.id()))
|
||||
.into(),
|
||||
}
|
||||
@@ -96,7 +104,9 @@ impl BarModule for Clock {
|
||||
fn update(&mut self, msg: Wire) -> Task<ModuleEffect> {
|
||||
// The service publishes a raw `ClockPayload`; UI sends `ClockMsg`.
|
||||
if let Some(payload) = msg.downcast::<ClockPayload>() {
|
||||
self.value = payload.value.clone();
|
||||
self.value = match payload {
|
||||
ClockPayload::Mins(v) | ClockPayload::Seconds(v) => v.clone(),
|
||||
};
|
||||
return Task::none();
|
||||
}
|
||||
match msg.downcast::<ClockMsg>() {
|
||||
|
||||
@@ -76,7 +76,10 @@ impl BarModule for WeatherModule {
|
||||
}
|
||||
// Clock tick from the datetime service: track the current hour.
|
||||
if let Some(payload) = msg.downcast::<ClockPayload>() {
|
||||
self.hour = payload.value.split(':').next().and_then(|h| h.parse().ok());
|
||||
let value = match payload {
|
||||
ClockPayload::Mins(v) | ClockPayload::Seconds(v) => v,
|
||||
};
|
||||
self.hour = value.split(':').next().and_then(|h| h.parse().ok());
|
||||
self.refresh();
|
||||
}
|
||||
match msg.downcast::<WeatherKind>() {
|
||||
|
||||
@@ -10,17 +10,18 @@ use std::collections::BTreeSet;
|
||||
|
||||
use common::{BarModule, HyprlandEvent, HyprlandMsg, ModuleEffect, Service, Wire};
|
||||
use iced::border;
|
||||
use iced::theme::palette;
|
||||
use iced::widget::{container, row, space};
|
||||
use iced::{window, Color, Element, Length, Task};
|
||||
use iced::{window, Element, Length, Task, Theme};
|
||||
use serde::Deserialize;
|
||||
use toml::Table;
|
||||
|
||||
/// Focused workspace (gruvbox base10 / `Colors.primaryText`).
|
||||
const ACTIVE: Color = Color::from_rgb(0.9216, 0.8588, 0.6980);
|
||||
/// Workspace exists but is not focused (base06 / `Colors.textSecondary`).
|
||||
const EXISTS: Color = Color::from_rgb(0.4863, 0.4353, 0.3922);
|
||||
/// Workspace does not exist (base04 / `Colors.textDisabled`).
|
||||
const ABSENT: Color = Color::from_rgb(0.3137, 0.2863, 0.2706);
|
||||
/// How far each state's cube fades from the theme text colour toward the
|
||||
/// theme background. Opaque (mixed, not alpha) so cubes stay solid over the
|
||||
/// transparent bar instead of washing out.
|
||||
const FADE_ACTIVE: f32 = 0.0;
|
||||
const FADE_EXISTS: f32 = 0.55;
|
||||
const FADE_ABSENT: f32 = 0.8;
|
||||
|
||||
/// Look of the workspace row, from `[modules.workspaces]`; every field
|
||||
/// defaults to the value Quickshell's `Workspaces.qml` hardcodes, so an
|
||||
@@ -81,21 +82,24 @@ impl Workspaces {
|
||||
}
|
||||
|
||||
fn cube(&self, id: i32) -> Element<'_, Wire> {
|
||||
let (color, scale) = if self.active == Some(id) {
|
||||
(ACTIVE, self.cfg.scale_active)
|
||||
let (fade, scale) = if self.active == Some(id) {
|
||||
(FADE_ACTIVE, self.cfg.scale_active)
|
||||
} else if self.existing.contains(&id) {
|
||||
(EXISTS, self.cfg.scale_exists)
|
||||
(FADE_EXISTS, self.cfg.scale_exists)
|
||||
} else {
|
||||
(ABSENT, self.cfg.scale_absent)
|
||||
(FADE_ABSENT, self.cfg.scale_absent)
|
||||
};
|
||||
let radius = self.cfg.radius;
|
||||
let bar = container(space())
|
||||
.width(Length::Fixed(self.cfg.slot_w * scale))
|
||||
.height(Length::Fixed(self.cfg.slot_h * scale))
|
||||
.style(move |_| container::Style {
|
||||
background: Some(color.into()),
|
||||
border: border::rounded(radius),
|
||||
..Default::default()
|
||||
.style(move |theme: &Theme| {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
background: Some(palette::mix(p.text, p.background, fade).into()),
|
||||
border: border::rounded(radius),
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
container(bar)
|
||||
.center_x(Length::Fixed(self.cfg.slot_w))
|
||||
|
||||
@@ -11,7 +11,7 @@ use chrono::{DateTime, Local, Timelike};
|
||||
use iced::futures::{channel::mpsc, SinkExt};
|
||||
use iced::Subscription;
|
||||
|
||||
use common::{ClockKind, ClockPayload, Endpoint, Inbox, Wire};
|
||||
use common::{ClockPayload, Endpoint, Inbox, Wire};
|
||||
|
||||
/// Route-key namespace owned by this service; keys are `"clock.<kind>"`.
|
||||
pub const NAMESPACE: &str = "clock.";
|
||||
@@ -44,7 +44,7 @@ impl ClockTicker {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
if let Some(payload) = clock.tick() {
|
||||
tracing::debug!(key, kind = ?payload.kind, "publishing clock");
|
||||
tracing::debug!(key, ?payload, "publishing clock");
|
||||
let wire = Wire::topic(Endpoint::service(key), key, payload);
|
||||
if sender.send(wire).await.is_err() {
|
||||
tracing::warn!(key, "clock send failed; stopping service");
|
||||
@@ -63,16 +63,10 @@ impl ClockTicker {
|
||||
let value = chrono::Local::now();
|
||||
let meow = self.last;
|
||||
if meow.minute() != value.minute() {
|
||||
return Some(ClockPayload {
|
||||
kind: ClockKind::Mins,
|
||||
value: value.format("%H:%M:%S").to_string(),
|
||||
});
|
||||
return Some(ClockPayload::Mins(value.format("%H:%M:%S").to_string()));
|
||||
}
|
||||
if meow.second() != value.second() {
|
||||
return Some(ClockPayload {
|
||||
kind: ClockKind::Seconds,
|
||||
value: value.format("%H:%M:%S").to_string(),
|
||||
});
|
||||
return Some(ClockPayload::Seconds(value.format("%H:%M:%S").to_string()));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
scheme: "Gruvbox dark, medium"
|
||||
author: "https://github.com/morhetz/gruvbox"
|
||||
base00: "282828" # bg
|
||||
base01: "3c3836" # bg1
|
||||
base02: "504945" # bg2
|
||||
base03: "665c54" # bg3
|
||||
base04: "bdae93" # fg4
|
||||
base05: "d5c4a1" # fg
|
||||
base06: "ebdbb2" # fg2
|
||||
base07: "fbf1c7" # fg0
|
||||
base08: "fb4934" # red
|
||||
base09: "fe8019" # orange
|
||||
base0A: "fabd2f" # yellow
|
||||
base0B: "b8bb26" # green
|
||||
base0C: "8ec07c" # aqua
|
||||
base0D: "83a598" # blue
|
||||
base0E: "d3869b" # purple
|
||||
base0F: "d65d0e" # brown
|
||||
Reference in New Issue
Block a user