Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ Feel free to take a look at the provided color schemes for reference.
* `end_separator`
* `start_separator`

Theme overrides can also be set per block via `[block.theme_overrides]`. Since
separators are drawn *between* blocks, a separator takes the settings from the
theme of the block it precedes, and `end_separator` takes them from the last
block's theme.

# Available icon overrides

These can be directly set to a string containing the desired unicode codepoint(s) or use a TOML escape sequence like `"\uf0f3"` for up to 4-nibble codepoints and `"\U0001f312"` for up to 8-nibble codepoints.
Expand Down
8 changes: 8 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use crate::formatting::value::Value;
use crate::protocol::i3bar_block::I3BarBlock;
use crate::protocol::i3bar_event::{self, I3BarEvent};
use crate::signals::Signal;
use crate::themes::Theme;
use crate::widget::{State, Widget};

const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
Expand Down Expand Up @@ -142,6 +143,10 @@ enum RequestCmd {
struct RenderedBlock {
pub segments: Vec<I3BarBlock>,
pub merge_with_next: bool,
/// The block's theme with its `theme_overrides` applied, so that
/// per-block separator settings can be used when the separators are
/// rendered (which happens across blocks, not inside one).
pub theme: Arc<Theme>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -289,6 +294,8 @@ impl BarState {
.error_fullscreen_format
.with_default_config(&self.config.error_fullscreen_format);

let theme = shared_config.theme.clone();

let block = Block {
id: self.blocks.len(),
name: block_config.config.name(),
Expand All @@ -313,6 +320,7 @@ impl BarState {
self.blocks_render_cache.push(RenderedBlock {
segments: Vec::new(),
merge_with_next: block_config.common.merge_with_next,
theme,
});

Ok(())
Expand Down
153 changes: 140 additions & 13 deletions src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,25 @@ pub fn init(never_pause: bool) {
}

pub(crate) fn print_blocks<B>(blocks: &[B], config: &SharedConfig)
where
B: Borrow<RenderedBlock>,
{
let rendered_blocks = render_blocks(blocks, config);
println!("{},", serde_json::to_string(&rendered_blocks).unwrap());
}

/// Separators are rendered *between* blocks, so they cannot be styled by any
/// single block's widget rendering. Each separator takes its settings
/// (`separator`, `separator_fg`, `separator_bg`, `start_separator`) from the
/// theme of the block it precedes; `end_separator` takes them from the last
/// block's theme. Blocks without `theme_overrides` carry the global theme, so
/// this reduces to the old behavior when no per-block overrides are set.
fn render_blocks<B>(blocks: &[B], config: &SharedConfig) -> Vec<I3BarBlock>
where
B: Borrow<RenderedBlock>,
{
let mut prev_last_bg = Color::None;
let mut prev_theme: Option<std::sync::Arc<crate::themes::Theme>> = None;
let mut rendered_blocks = vec![];

// The right most block should never be alternated
Expand All @@ -48,6 +63,7 @@ where
let RenderedBlock {
mut segments,
merge_with_next,
theme,
} = widgets;

for segment in &mut segments {
Expand All @@ -65,25 +81,25 @@ where
alt = !alt;
}

let separator = match &config.theme.start_separator {
Separator::Custom(_) if i == 0 => &config.theme.start_separator,
_ => &config.theme.separator,
let separator = match &theme.start_separator {
Separator::Custom(_) if i == 0 => &theme.start_separator,
_ => &theme.separator,
};

if let Separator::Custom(separator) = separator {
if !prev_merge_with_next {
// The first widget's BG is used to get the FG color for the current separator
let sep_fg = if config.theme.separator_fg == Color::Auto {
let sep_fg = if theme.separator_fg == Color::Auto {
segments.first().unwrap().background
} else {
config.theme.separator_fg
theme.separator_fg
};

// The separator's BG is the last block's last widget's BG
let sep_bg = if config.theme.separator_bg == Color::Auto {
let sep_bg = if theme.separator_bg == Color::Auto {
prev_last_bg
} else {
config.theme.separator_bg
theme.separator_bg
};

let separator = I3BarBlock {
Expand All @@ -107,23 +123,25 @@ where

prev_merge_with_next = merge_with_next;
prev_last_bg = segments.last().unwrap().background;
prev_theme = Some(theme);

rendered_blocks.extend(segments);
}

if let Separator::Custom(end_separator) = &config.theme.end_separator {
let end_theme = prev_theme.as_deref().unwrap_or(&config.theme);
if let Separator::Custom(end_separator) = &end_theme.end_separator {
// The separator's FG is the last block's last widget's BG
let sep_fg = if config.theme.separator_fg == Color::Auto {
let sep_fg = if end_theme.separator_fg == Color::Auto {
prev_last_bg
} else {
config.theme.separator_fg
end_theme.separator_fg
};

// The separator has no background color
let sep_bg = if config.theme.separator_bg == Color::Auto {
let sep_bg = if end_theme.separator_bg == Color::Auto {
Color::None
} else {
config.theme.separator_bg
end_theme.separator_bg
};

let separator = I3BarBlock {
Expand All @@ -136,5 +154,114 @@ where
rendered_blocks.push(separator);
}

println!("{},", serde_json::to_string(&rendered_blocks).unwrap());
rendered_blocks
}

#[cfg(test)]
mod tests {
use super::*;
use crate::themes::Theme;
use std::str::FromStr as _;
use std::sync::Arc;

fn theme_with(f: impl FnOnce(&mut Theme)) -> Arc<Theme> {
let mut theme = Theme::default();
f(&mut theme);
Arc::new(theme)
}

fn block(text: &str, theme: &Arc<Theme>) -> RenderedBlock {
RenderedBlock {
segments: vec![I3BarBlock {
full_text: text.into(),
..Default::default()
}],
merge_with_next: false,
theme: theme.clone(),
}
}

#[test]
fn per_block_separator_overrides() {
let config = SharedConfig::default();
let theme_a = theme_with(|t| {
t.separator = Separator::Custom("|A|".into());
});
let theme_b = theme_with(|t| {
t.separator = Separator::Custom("|B|".into());
t.separator_fg = Color::from_str("#123456").unwrap();
});

let out = render_blocks(&[block("a", &theme_a), block("b", &theme_b)], &config);

let texts: Vec<&str> = out.iter().map(|b| b.full_text.as_str()).collect();
assert_eq!(texts, ["|A|", "a", "|B|", "b"]);
// the separator preceding a block takes that block's theme
assert_eq!(out[2].color, Color::from_str("#123456").unwrap());
}

#[test]
fn per_block_start_and_end_separators() {
let config = SharedConfig::default();
let first = theme_with(|t| {
t.start_separator = Separator::Custom("<start>".into());
});
let last = theme_with(|t| {
t.end_separator = Separator::Custom("<end>".into());
});

let out = render_blocks(&[block("a", &first), block("b", &last)], &config);

let texts: Vec<&str> = out.iter().map(|b| b.full_text.as_str()).collect();
assert_eq!(texts, ["<start>", "a", "b", "<end>"]);
}

#[test]
fn global_theme_when_no_overrides() {
let config = SharedConfig::default();
let theme = Arc::new(Theme::default());

let out = render_blocks(&[block("a", &theme), block("b", &theme)], &config);

// default theme uses native separators: no separator segments injected
let texts: Vec<&str> = out.iter().map(|b| b.full_text.as_str()).collect();
assert_eq!(texts, ["a", "b"]);
}

/// A global custom separator, one block overriding it (with its own fg),
/// and the last block providing end_separator — mirrors a live-tested
/// configuration.
#[test]
fn mixed_global_and_block_overrides() {
let global = theme_with(|t| {
t.separator = Separator::Custom("|G|".into());
});
let config = SharedConfig {
theme: global.clone(),
..Default::default()
};
// per-block themes start as a copy of the global theme with the
// block's overrides applied on top, as in BarState::spawn_block
let theme_a = theme_with(|t| {
t.separator = Separator::Custom(">>A".into());
t.separator_fg = Color::from_str("#ff0000").unwrap();
});
let theme_c = theme_with(|t| {
t.separator = Separator::Custom("|G|".into());
t.end_separator = Separator::Custom("<<END".into());
});

let out = render_blocks(
&[
block("A", &theme_a),
block("B", &global),
block("C", &theme_c),
],
&config,
);

let texts: Vec<&str> = out.iter().map(|b| b.full_text.as_str()).collect();
assert_eq!(texts, [">>A", "A", "|G|", "B", "|G|", "C", "<<END"]);
assert_eq!(out[0].color, Color::from_str("#ff0000").unwrap());
}
}
Loading