Skip to content

Commit ab24e2b

Browse files
authored
Merge pull request #1728 from squidowl/feat/1693
Add `palette` support for nickname colors
2 parents 0a33c74 + 1318d33 commit ab24e2b

File tree

10 files changed

+166
-140
lines changed

10 files changed

+166
-140
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ Added:
99
- `buffer.server_messages.away` theme setting to control how automated away messages appear
1010
- Drafts are remembered across Halloy sessions. Can be disabled with `buffer.text_input.persist`
1111
- Animate typing dots
12+
- `buffer.nickname.color` now supports `{ palette = ["#RRGGBB", ...] }` for nickname colors from a fixed set
1213

1314
Changed:
1415

1516
- Moved `typing` settings from `buffer.channel.typing` to `buffer.typing` to clarify that they appliy to queries as well as channels
17+
- Moved nicklist nickname settings from `buffer.channel.nicklist` to `buffer.nickname` (`away` and `color`)
1618

1719
Fixed:
1820

@@ -25,6 +27,9 @@ Fixed:
2527
- `typing` settings for buffers could get in a stuck state without any way to control them
2628
- +typing=done should not be sent when the message is sent
2729

30+
Removed:
31+
- `buffer.channel.message.nickname_color` in favor of `buffer.nickname.color`
32+
2833
Thanks:
2934

3035
- Contributions: @furudean, @omentic, @KaiKorla, @achille

data/src/appearance/theme.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,29 @@ pub fn randomize_color(original_color: Color, seed: &str) -> Color {
546546
from_hsl(randomized_hsl)
547547
}
548548

549+
pub fn nickname_color(
550+
original_color: Color,
551+
kind: &crate::buffer::Color,
552+
seed: Option<&str>,
553+
) -> Color {
554+
match (kind, seed) {
555+
(crate::buffer::Color::Solid, _) | (_, None) => original_color,
556+
(crate::buffer::Color::Unique, Some(seed)) => {
557+
randomize_color(original_color, seed)
558+
}
559+
(crate::buffer::Color::Palette(colors), Some(seed)) => {
560+
if colors.is_empty() {
561+
return original_color;
562+
}
563+
564+
let index =
565+
(seahash::hash(seed.as_bytes()) % colors.len() as u64) as usize;
566+
567+
colors[index]
568+
}
569+
}
570+
}
571+
549572
pub fn to_hsl(color: Color) -> Okhsl {
550573
let mut hsl = Okhsl::from_color(to_rgb(color));
551574
if hsl.saturation.is_nan() {

data/src/buffer.rs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ use core::fmt;
22
use std::str::FromStr;
33

44
use chrono::Locale;
5+
use iced_core::Color as IcedColor;
56
use serde::{Deserialize, Deserializer, Serialize};
67

78
pub mod timestamp;
89

910
pub use self::timestamp::Timestamp;
11+
use crate::appearance::theme::hex_to_color;
1012
use crate::serde::deserialize_strftime_date;
1113
use crate::target::{self, Target};
1214
use crate::{Server, channel, config, message};
@@ -239,12 +241,56 @@ impl Brackets {
239241
}
240242
}
241243

242-
#[derive(Debug, Clone, Copy, Default, Deserialize)]
243-
#[serde(rename_all = "kebab-case")]
244+
#[derive(Debug, Clone, Default)]
244245
pub enum Color {
245246
Solid,
246247
#[default]
247248
Unique,
249+
Palette(Vec<IcedColor>),
250+
}
251+
252+
impl<'de> Deserialize<'de> for Color {
253+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
254+
where
255+
D: Deserializer<'de>,
256+
{
257+
#[derive(Deserialize)]
258+
#[serde(untagged)]
259+
enum Repr {
260+
String(String),
261+
Palette { palette: Vec<String> },
262+
}
263+
264+
match Repr::deserialize(deserializer)? {
265+
Repr::String(value) => match value.as_str() {
266+
"solid" => Ok(Self::Solid),
267+
"unique" => Ok(Self::Unique),
268+
_ => Err(serde::de::Error::custom(format!(
269+
"unknown color: {value}",
270+
))),
271+
},
272+
Repr::Palette { palette } => {
273+
if palette.is_empty() {
274+
return Err(serde::de::Error::custom(
275+
"palette must contain at least one hex color",
276+
));
277+
}
278+
279+
let colors = palette
280+
.into_iter()
281+
.map(|hex| {
282+
hex_to_color(&hex).ok_or_else(|| {
283+
serde::de::Error::custom(format!(
284+
"invalid hex color in palette: {hex}",
285+
))
286+
})
287+
})
288+
.collect::<Result<Vec<_>, _>>()?;
289+
290+
Ok(Self::Palette(colors))
291+
}
292+
}
293+
}
248294
}
249295

250296
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
@@ -287,6 +333,50 @@ impl Resize {
287333
}
288334
}
289335

336+
#[cfg(test)]
337+
mod tests {
338+
use super::Color;
339+
340+
#[derive(Debug, serde::Deserialize)]
341+
struct Root {
342+
color: Color,
343+
}
344+
345+
#[test]
346+
fn color_deserializes_palette() {
347+
let root: Root = toml::from_str(
348+
r##"color = { palette = ["#112233", "#445566", "#778899"] }"##,
349+
)
350+
.expect("valid palette color");
351+
352+
match root.color {
353+
Color::Palette(colors) => assert_eq!(colors.len(), 3),
354+
_ => panic!("expected palette color"),
355+
}
356+
}
357+
358+
#[test]
359+
fn color_rejects_empty_palette() {
360+
let err = toml::from_str::<Root>(r#"color = { palette = [] }"#)
361+
.expect_err("empty palette should be rejected");
362+
363+
assert!(
364+
err.to_string()
365+
.contains("palette must contain at least one")
366+
);
367+
}
368+
369+
#[test]
370+
fn color_rejects_invalid_palette_hex() {
371+
let err = toml::from_str::<Root>(
372+
r##"color = { palette = ["#112233", "not-a-color"] }"##,
373+
)
374+
.expect_err("invalid palette hex should be rejected");
375+
376+
assert!(err.to_string().contains("invalid hex color in palette"));
377+
}
378+
}
379+
290380
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
291381
#[serde(rename_all = "kebab-case")]
292382
pub enum SkinTone {

data/src/config/buffer/channel.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
use serde::Deserialize;
22

33
use super::NicknameClickAction;
4-
use crate::buffer::Color;
54
use crate::channel::Position;
6-
use crate::config::buffer::{AccessLevelFormat, Away};
5+
use crate::config::buffer::AccessLevelFormat;
76
use crate::isupport;
87
use crate::serde::deserialize_u32_positive_integer;
98

@@ -34,7 +33,6 @@ impl ChannelNameCasing {
3433
#[derive(Debug, Clone, Deserialize)]
3534
#[serde(default)]
3635
pub struct Message {
37-
pub nickname_color: Color,
3836
pub show_emoji_reacts: bool,
3937
#[serde(deserialize_with = "deserialize_u32_positive_integer")]
4038
pub max_reaction_display: u32,
@@ -45,7 +43,6 @@ pub struct Message {
4543
impl Default for Message {
4644
fn default() -> Self {
4745
Self {
48-
nickname_color: Color::default(),
4946
show_emoji_reacts: true,
5047
max_reaction_display: 5,
5148
max_reaction_chars: 64,
@@ -56,10 +53,8 @@ impl Default for Message {
5653
#[derive(Debug, Clone, Deserialize)]
5754
#[serde(default)]
5855
pub struct Nicklist {
59-
pub away: Away,
6056
pub enabled: bool,
6157
pub position: Position,
62-
pub color: Color,
6358
pub width: Option<f32>,
6459
pub alignment: Alignment,
6560
pub show_access_levels: AccessLevelFormat,
@@ -69,10 +64,8 @@ pub struct Nicklist {
6964
impl Default for Nicklist {
7065
fn default() -> Self {
7166
Self {
72-
away: Away::default(),
7367
enabled: true,
7468
position: Position::default(),
75-
color: Color::default(),
7669
width: None,
7770
alignment: Alignment::default(),
7871
show_access_levels: AccessLevelFormat::default(),

docs/configuration/buffer.md

Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,6 @@ channel_name_casing = "lowercase"
7979

8080
Message settings within a channel buffer.
8181

82-
#### `nickname_color`
83-
84-
Nickname colors in the message. `"unique"` generates colors by randomizing the hue, while keeping the saturation and lightness from the theme's nickname color.
85-
86-
```toml
87-
# Type: string
88-
# Values: "solid", "unique"
89-
# Default: "unique"
90-
91-
[buffer.channel.message]
92-
nickname_color = "unique"
93-
```
94-
9582
#### `show_emoji_reacts`
9683

9784
Whether to display emoji reactions on messages (if [IRCv3 React](https://ircv3.net/specs/client-tags/react) is supported by the server).
@@ -150,39 +137,6 @@ Horizontal alignment of nicknames.
150137
alignment = "left"
151138
```
152139

153-
#### `away`
154-
155-
Controls the appearance of away nicknames.
156-
157-
```toml
158-
# Type: string or object
159-
# Values: "dimmed", "none" or { dimmed = float }
160-
# Default: "dimmed"
161-
[buffer.channel.nicklist]
162-
away = "dimmed"
163-
164-
# with custom dimming alpha value (0.0-1.0)
165-
[buffer.channel.nicklist]
166-
away = { dimmed = 0.5 }
167-
168-
# no away indication
169-
[buffer.channel.nicklist]
170-
away = "none"
171-
```
172-
173-
#### `color`
174-
175-
Nickname colors in the nicklist. `"unique"` generates colors by randomizing the hue, while keeping the saturation and lightness from the theme's nickname color.
176-
177-
```toml
178-
# Type: string
179-
# Values: "solid", "unique"
180-
# Default: "unique"
181-
182-
[buffer.channel.nicklist]
183-
color = "unique"
184-
```
185-
186140
#### `enabled`
187141

188142
Control if nicklist should be shown or not by default.
@@ -815,15 +769,18 @@ brackets = { left = "<", right = ">" }
815769

816770
### `color`
817771

818-
Nickname colors in a channel buffer. `"unique"` generates colors by randomizing the hue, while keeping the saturation and lightness from the theme's nickname color.
772+
Nickname colors across nickname UI, including channel messages, nicklists, and nickname-related controls. `"unique"` generates colors by randomizing the hue, while keeping the saturation and lightness from the theme's nickname color. `{ palette = [...] }` assigns each nickname one of the provided hex colors.
819773

820774
```toml
821-
# Type: string
822-
# Values: "solid", "unique"
775+
# Type: string or object
776+
# Values: "solid", "unique", or { palette = ["#RRGGBB", ...] }
823777
# Default: "unique"
824778

825779
[buffer.nickname]
826780
color = "unique"
781+
782+
[buffer.nickname]
783+
color = { palette = ["#B11E3A", "#2A7FFF", "#1E9E5A"] }
827784
```
828785

829786
### `offline`

src/appearance/theme/selectable_text.rs

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ pub fn server(
123123
pub fn nicklist_nickname(theme: &Theme, config: &Config, user: &User) -> Style {
124124
nickname_style(
125125
theme,
126-
config.buffer.channel.nicklist.color,
126+
&config.buffer.nickname.color,
127127
user,
128-
config.buffer.channel.nicklist.away.is_away(user.is_away()),
128+
config.buffer.nickname.away.is_away(user.is_away()),
129129
false,
130130
)
131131
}
@@ -138,7 +138,7 @@ pub fn nickname(
138138
) -> Style {
139139
nickname_style(
140140
theme,
141-
config.buffer.channel.message.nickname_color,
141+
&config.buffer.nickname.color,
142142
user,
143143
config
144144
.buffer
@@ -155,28 +155,19 @@ pub fn topic_nickname(
155155
user: &User,
156156
is_offline: bool,
157157
) -> Style {
158-
nickname_style(
159-
theme,
160-
config.buffer.channel.message.nickname_color,
161-
user,
162-
None,
163-
is_offline,
164-
)
158+
nickname_style(theme, &config.buffer.nickname.color, user, None, is_offline)
165159
}
166160

167161
fn nickname_style(
168162
theme: &Theme,
169-
kind: data::buffer::Color,
163+
kind: &data::buffer::Color,
170164
user: &User,
171165
is_away: Option<buffer::Away>,
172166
is_offline: bool,
173167
) -> Style {
174-
let seed = match kind {
175-
data::buffer::Color::Solid => None,
176-
data::buffer::Color::Unique => Some(user.seed()),
177-
};
178-
179-
let color = text::nickname(theme, seed, is_away, is_offline).color;
168+
let color =
169+
text::nickname(theme, kind, Some(user.seed()), is_away, is_offline)
170+
.color;
180171

181172
Style {
182173
color,

src/appearance/theme/text.rs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use data::appearance::theme::randomize_color;
1+
use data::appearance::theme::nickname_color;
22
use data::config::buffer;
33
use iced::widget::text::{Catalog, Style, StyleFn};
44

@@ -96,9 +96,10 @@ pub fn url(theme: &Theme) -> Style {
9696
}
9797
}
9898

99-
pub fn nickname<T: AsRef<str>>(
99+
pub fn nickname(
100100
theme: &Theme,
101-
seed: Option<T>,
101+
kind: &data::buffer::Color,
102+
seed: Option<&str>,
102103
is_away: Option<buffer::Away>,
103104
is_offline: bool,
104105
) -> Style {
@@ -121,14 +122,8 @@ pub fn nickname<T: AsRef<str>>(
121122

122123
let nickname = theme.styles().buffer.nickname;
123124

124-
// If we have a seed we randomize the color based on the seed before adding any alpha value.
125-
let color = match seed {
126-
Some(seed) => calculate_alpha_color(randomize_color(
127-
nickname.color,
128-
seed.as_ref(),
129-
)),
130-
None => calculate_alpha_color(nickname.color),
131-
};
125+
let color =
126+
calculate_alpha_color(nickname_color(nickname.color, kind, seed));
132127

133128
Style { color: Some(color) }
134129
}

0 commit comments

Comments
 (0)