Skip to content

Commit dc5644d

Browse files
committed
feat(theme): add sway bar color integration
1 parent 7483643 commit dc5644d

10 files changed

Lines changed: 266 additions & 11 deletions

File tree

‎Cargo.lock‎

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ signal-hook = "0.3"
7676
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
7777
smart-default = "0.7"
7878
sunrise = "3.0"
79+
swayipc = "4.0.0"
7980
swayipc-async = "3.0.0"
8081
thiserror = "2.0"
8182
tokio-util = { version = "0.7", features = ["codec"] }

‎cspell.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ words:
145145
- retval
146146
- rofi
147147
- rofication
148+
- RRGGBB
148149
- rsplit
149150
- rtattr
150151
- rtattrs

‎doc/themes.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,53 @@ cpu_boost_on = "ON"
125125
cpu_boost_off = "OFF"
126126
```
127127

128+
If you define colors in`~/.Xresources` like `*background: #aabbcc` you can use them like:
129+
130+
```toml
131+
[theme]
132+
theme = "solarized-dark"
133+
[theme.overrides]
134+
# Example: redefine `idle_bg` with the background color
135+
idle_bg = "x:background"
136+
```
137+
138+
If you use sway, you can also reference a bar color like:
139+
140+
```toml
141+
[theme]
142+
theme = "solarized-dark"
143+
[theme.overrides]
144+
# Example: redefine `critical` colors
145+
critical_bg = "sway:urgent_workspace_bg"
146+
critical_fg = "sway:urgent_workspace_text"
147+
```
148+
149+
These are the available sway colors:
150+
151+
* `active_workspace_bg`
152+
* `active_workspace_border`
153+
* `active_workspace_text`
154+
* `background`
155+
* `binding_mode_bg`
156+
* `binding_mode_border`
157+
* `binding_mode_text`
158+
* `focused_background`
159+
* `focused_separator`
160+
* `focused_statusline`
161+
* `focused_workspace_bg`
162+
* `focused_workspace_border`
163+
* `focused_workspace_text`
164+
* `inactive_workspace_bg`
165+
* `inactive_workspace_border`
166+
* `inactive_workspace_text`
167+
* `separator`
168+
* `statusline`
169+
* `urgent_workspace_bg`
170+
* `urgent_workspace_border`
171+
* `urgent_workspace_text`
172+
173+
You can see the current values for your bar with `swaymsg -t get_bar_config <bar-name>`.
174+
128175
# Available theme overrides
129176

130177
All `bg` and `fg` overrides are either

‎src/main.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ fn main() {
4242
bar.run_event_loop(restart).await?;
4343
Ok(())
4444
});
45+
4546
if let Err(error) = result {
4647
let error_widget = Widget::new()
4748
.with_text(error.to_string().pango_escaped())

‎src/themes.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
//! Theme specification
2+
13
pub mod color;
24
pub mod separator;
3-
pub mod xresources;
45

56
use std::fmt;
67
use std::ops::{Deref, DerefMut};

‎src/themes/color.rs‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
//! Color specification
2+
//!
3+
//! Colors can be specified in any of these forms:
4+
//!
5+
//! - `none` or an empty string for no color.
6+
//! - `auto` to let the output determine the color.
7+
//! - `#RRGGBB[AA]`. The alpha byte ranges from `00` (transparent) to `FF` (opaque).
8+
//! Alpha is optional and defaults to `FF`.
9+
//! - `hsv:H:S:V[:A]` for an HSVA color. Hue is in degrees; saturation, value,
10+
//! and optional alpha are percentages from 0 to 100. Alpha defaults to 100.
11+
//! - `x:name` to look up a color in `~/.Xresources`. For example,
12+
//! `x:background` uses the value of `*background`.
13+
//! - `sway:name` to use a color from the active sway bar configuration. For
14+
//! example, `sway:urgent_workspace_bg` uses the bar's urgent workspace
15+
//! background color. See [`sway`] for more details.
16+
17+
pub mod sway;
18+
pub mod xresources;
19+
120
use crate::errors::*;
221
use serde::de::{self, Deserializer, Visitor};
322
use serde::{Deserialize, Serialize, Serializer};
@@ -218,10 +237,15 @@ impl FromStr for Color {
218237
Color::Hsva(Hsva::new(h, s / 100., v / 100., (a / 100. * 255.) as u8))
219238
} else if color.starts_with("x:") {
220239
let name = color.split_at(2).1;
221-
super::xresources::get_color(name)?
240+
xresources::get_color(name)?
222241
.or_error(|| format!("color '{name}' not defined in ~/.Xresources"))?
223242
.parse()
224243
.or_error(|| format!("invalid color definition '{name}'"))?
244+
} else if color.starts_with("sway:") {
245+
let name = color.split_at(5).1;
246+
sway::get_color(name)?
247+
.parse()
248+
.or_error(|| format!("invalid color definition '{name}'"))?
225249
} else {
226250
let err_msg = || format!("'{color}' is not a valid RGBA color");
227251
let rgb = color.get(1..7).or_error(err_msg)?;

‎src/themes/color/sway.rs‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//! Sway colors
2+
//!
3+
//! Sway colors are picked up using sway IPC for the current bar.
4+
//!
5+
//! These are the available sway colors:
6+
//!
7+
//! * `active_workspace_bg`
8+
//! * `active_workspace_border`
9+
//! * `active_workspace_text`
10+
//! * `background`
11+
//! * `binding_mode_bg`
12+
//! * `binding_mode_border`
13+
//! * `binding_mode_text`
14+
//! * `focused_background`
15+
//! * `focused_separator`
16+
//! * `focused_statusline`
17+
//! * `focused_workspace_bg`
18+
//! * `focused_workspace_border`
19+
//! * `focused_workspace_text`
20+
//! * `inactive_workspace_bg`
21+
//! * `inactive_workspace_border`
22+
//! * `inactive_workspace_text`
23+
//! * `separator`
24+
//! * `statusline`
25+
//! * `urgent_workspace_bg`
26+
//! * `urgent_workspace_border`
27+
//! * `urgent_workspace_text`
28+
//!
29+
//! You can see the current values for your bar with `swaymsg -t get_bar_config <bar-name>`.
30+
31+
use swayipc::ColorableBarPart;
32+
33+
use std::sync::LazyLock;
34+
35+
use crate::errors::*;
36+
37+
#[cfg(not(test))]
38+
use std::{fs::read_to_string, os::unix::process::parent_id, path::Path};
39+
40+
#[cfg(not(test))]
41+
static COLORS: LazyLock<Result<ColorableBarPart>> =
42+
LazyLock::new(|| {
43+
let mut swayipc_connection =
44+
swayipc::Connection::new().error("Failed to open swayipc connection")?;
45+
46+
let mut current_pid = parent_id().to_string();
47+
while current_pid != "1" {
48+
let cmdline = read_to_string(Path::new("/proc").join(&current_pid).join("cmdline"))
49+
.or_error(|| format!("Failed to read /proc/{current_pid}/cmdline"))?;
50+
let cmdline_parts: Vec<_> = cmdline.split('\0').collect();
51+
if let Some(bar_id) = cmdline_parts
52+
.iter()
53+
.position(|&s| s == "-b" || s == "--bar_id")
54+
.and_then(|pos| cmdline_parts.get(pos + 1))
55+
{
56+
return Ok(swayipc_connection
57+
.get_bar_config(bar_id)
58+
.error("Failed to get swaybar config")?
59+
.colors);
60+
}
61+
let stat = read_to_string(Path::new("/proc").join(&current_pid).join("stat"))
62+
.or_error(|| format!("Failed to read /proc/{current_pid}/stat"))?;
63+
64+
current_pid =
65+
stat.split(' ').nth(3).unwrap().parse().or_error(|| {
66+
format!("Failed to parse parent PID from /proc/{current_pid}/stat")
67+
})?;
68+
}
69+
Err(Error::new(
70+
"Unable to find swaybar process in parent process tree",
71+
))
72+
});
73+
74+
#[cfg(test)]
75+
use tests::COLORS;
76+
77+
pub fn get_color(name: &str) -> Result<String> {
78+
COLORS.as_ref().map_err(Clone::clone).and_then(|colors| {
79+
Ok(match name {
80+
"background" => colors.background.clone(),
81+
"statusline" => colors.statusline.clone(),
82+
"separator" => colors.separator.clone(),
83+
"focused_background" => colors.focused_background.clone(),
84+
"focused_statusline" => colors.focused_statusline.clone(),
85+
"focused_separator" => colors.focused_separator.clone(),
86+
"focused_workspace_border" => colors.focused_workspace_border.clone(),
87+
"focused_workspace_bg" => colors.focused_workspace_bg.clone(),
88+
"focused_workspace_text" => colors.focused_workspace_text.clone(),
89+
"inactive_workspace_border" => colors.inactive_workspace_border.clone(),
90+
"inactive_workspace_bg" => colors.inactive_workspace_bg.clone(),
91+
"inactive_workspace_text" => colors.inactive_workspace_text.clone(),
92+
"active_workspace_border" => colors.active_workspace_border.clone(),
93+
"active_workspace_bg" => colors.active_workspace_bg.clone(),
94+
"active_workspace_text" => colors.active_workspace_text.clone(),
95+
"urgent_workspace_border" => colors.urgent_workspace_border.clone(),
96+
"urgent_workspace_bg" => colors.urgent_workspace_bg.clone(),
97+
"urgent_workspace_text" => colors.urgent_workspace_text.clone(),
98+
"binding_mode_border" => colors.binding_mode_border.clone(),
99+
"binding_mode_bg" => colors.binding_mode_bg.clone(),
100+
"binding_mode_text" => colors.binding_mode_text.clone(),
101+
_ => {
102+
return Err(Error::new(format!(
103+
"color '{}' is not a valid sway color name",
104+
name
105+
)));
106+
}
107+
})
108+
})
109+
}
110+
111+
#[cfg(test)]
112+
mod tests {
113+
114+
use super::*;
115+
116+
pub(crate) static COLORS: LazyLock<Result<ColorableBarPart>> = LazyLock::new(|| {
117+
let json_str = include_str!("../../../testdata/sway_colors.json");
118+
serde_json::from_str(json_str).error("Failed to parse sway colors JSON")
119+
});
120+
121+
#[test]
122+
fn test_deserializing_xcolors() {
123+
use crate::themes::color::*;
124+
let mut parsed_color = "sway:separator".parse::<Color>().unwrap();
125+
assert_eq!(
126+
parsed_color,
127+
Color::Rgba(Rgba {
128+
r: 0x93,
129+
g: 0xa1,
130+
b: 0xa1,
131+
a: 0xff
132+
})
133+
);
134+
parsed_color = "sway:focused_workspace_border".parse::<Color>().unwrap();
135+
assert_eq!(
136+
parsed_color,
137+
Color::Rgba(Rgba {
138+
r: 0x00,
139+
g: 0x88,
140+
b: 0xcc,
141+
a: 0xff,
142+
})
143+
);
144+
}
145+
}
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,25 +68,25 @@ mod tests {
6868

6969
#[test]
7070
fn test_deserializing_xcolors() {
71-
use super::super::color::*;
71+
use crate::themes::color::*;
7272
let mut parsed_color = "x:color4".parse::<Color>().unwrap();
7373
assert_eq!(
7474
parsed_color,
7575
Color::Rgba(Rgba {
76-
r: 254,
77-
g: 237,
78-
b: 218,
79-
a: 255
76+
r: 0xfe,
77+
g: 0xed,
78+
b: 0xda,
79+
a: 0xff
8080
})
8181
);
8282
parsed_color = "x:background".parse::<Color>().unwrap();
8383
assert_eq!(
8484
parsed_color,
8585
Color::Rgba(Rgba {
86-
r: 238,
87-
g: 51,
88-
b: 170,
89-
a: 153,
86+
r: 0xee,
87+
g: 0x33,
88+
b: 0xaa,
89+
a: 0x99,
9090
})
9191
);
9292
}

‎testdata/sway_colors.json‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"background": "#222222ff",
3+
"statusline": "#ddddddff",
4+
"separator": "#93a1a1ff",
5+
"focused_background": "#222222ff",
6+
"focused_statusline": "#ddddddff",
7+
"focused_separator": "#93a1a1ff",
8+
"focused_workspace_border": "#0088ccff",
9+
"focused_workspace_bg": "#0088ccff",
10+
"focused_workspace_text": "#ffffffff",
11+
"inactive_workspace_border": "#333333ff",
12+
"inactive_workspace_bg": "#333333ff",
13+
"inactive_workspace_text": "#888888ff",
14+
"active_workspace_border": "#333333ff",
15+
"active_workspace_bg": "#333333ff",
16+
"active_workspace_text": "#ffffffff",
17+
"urgent_workspace_border": "#2f343aff",
18+
"urgent_workspace_bg": "#900000ff",
19+
"urgent_workspace_text": "#ffffffff",
20+
"binding_mode_border": "#2f343aff",
21+
"binding_mode_bg": "#900000ff",
22+
"binding_mode_text": "#ffffffff"
23+
}

0 commit comments

Comments
 (0)