1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
|
use serde::{
de::{Error, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
use std::{
collections::{BTreeMap, HashMap},
fmt,
};
use crate::data::Styling;
#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
pub struct UiConfig {
pub pane_frames: FrameConfig,
}
impl UiConfig {
pub fn merge(&self, other: UiConfig) -> Self {
let mut merged = self.clone();
merged.pane_frames = merged.pane_frames.merge(other.pane_frames);
merged
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct FrameConfig {
pub rounded_corners: bool,
pub hide_session_name: bool,
pub title_alignment: crate::data::PaneTitleAlignment,
/// outer gap between panes, expressed in ROWS (see Style::pane_gap_y).
/// Columns are scaled by `gap_aspect` so the gap looks square.
pub gap: usize,
/// inner padding between a pane's frame and its content, per side.
/// Already resolved at parse time (including any aspect correction).
pub padding: crate::data::PanePadding,
/// Columns per row. Terminal cells are roughly twice as tall as they are
/// wide, so 1 row of gap needs ~2 columns to look like the same distance.
/// 0 is treated as 1 (no correction).
pub gap_aspect: usize,
/// How strongly to desaturate the CONTENT of unfocused panes, 0-100.
/// 0 disables it entirely (the default).
pub inactive_grayscale: u8,
/// Omit the auto-generated "Pane #N" placeholder title, so a pane shows no
/// name until the user sets one or the running program sets a title.
pub hide_default_pane_names: bool,
/// Collapse to the swap layout named `responsive_layout` when the terminal
/// is narrower than this many columns. 0 disables the whole feature.
pub responsive_min_cols: usize,
/// Name of the swap tiled layout to use when below the threshold.
pub responsive_layout: String,
/// Cap on how many entries the stacked-pane picker shows at once. The list
/// windows around the focused entry and reports the rest as "+N". 0 = show
/// every member, however many rows that takes.
pub stack_list_max_rows: usize,
}
/// Hand-written rather than derived: `gap_aspect` must default to 2 to match
/// the KDL parser's default. A derived Default would give 0, which makes a
/// parsed config disagree with a default-constructed one and shows up as
/// config round-trip diffs.
impl Default for FrameConfig {
fn default() -> Self {
FrameConfig {
rounded_corners: false,
hide_session_name: false,
title_alignment: Default::default(),
gap: 0,
padding: Default::default(),
gap_aspect: 2,
inactive_grayscale: 0,
hide_default_pane_names: false,
responsive_min_cols: 0,
responsive_layout: String::from("stacked"),
stack_list_max_rows: 0,
}
}
}
impl FrameConfig {
fn aspect(&self) -> usize {
self.gap_aspect.max(1)
}
/// (columns, rows) of outer gap
pub fn resolved_gap(&self) -> (usize, usize) {
(self.gap * self.aspect(), self.gap)
}
pub fn resolved_padding(&self) -> crate::data::PanePadding {
self.padding
}
}
impl FrameConfig {
pub fn merge(&self, other: FrameConfig) -> Self {
let mut merged = self.clone();
merged.rounded_corners = other.rounded_corners;
merged.hide_session_name = other.hide_session_name;
merged.title_alignment = other.title_alignment;
merged.gap = other.gap;
merged.padding = other.padding;
merged.gap_aspect = other.gap_aspect;
merged.inactive_grayscale = other.inactive_grayscale;
merged.hide_default_pane_names = other.hide_default_pane_names;
merged.responsive_min_cols = other.responsive_min_cols;
merged.responsive_layout = other.responsive_layout.clone();
merged.stack_list_max_rows = other.stack_list_max_rows;
merged
}
}
#[derive(Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct Themes(HashMap<String, Theme>);
impl fmt::Debug for Themes {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut stable_sorted = BTreeMap::new();
for (theme_name, theme) in self.0.iter() {
stable_sorted.insert(theme_name, theme);
}
write!(f, "{:#?}", stable_sorted)
}
}
impl Themes {
pub fn from_data(theme_data: HashMap<String, Theme>) -> Self {
Themes(theme_data)
}
pub fn insert(&mut self, theme_name: String, theme: Theme) {
self.0.insert(theme_name, theme);
}
pub fn merge(&self, mut other: Themes) -> Self {
let mut merged = self.clone();
for (name, theme) in other.0.drain() {
merged.0.insert(name, theme);
}
merged
}
pub fn get_theme(&self, theme_name: &str) -> Option<&Theme> {
self.0.get(theme_name)
}
pub fn inner(&self) -> &HashMap<String, Theme> {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Theme {
pub sourced_from_external_file: bool,
#[serde(flatten)]
pub palette: Styling,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct HexColor(u8, u8, u8);
impl From<HexColor> for (u8, u8, u8) {
fn from(e: HexColor) -> (u8, u8, u8) {
let HexColor(r, g, b) = e;
(r, g, b)
}
}
pub struct HexColorVisitor();
impl<'de> Visitor<'de> for HexColorVisitor {
type Value = HexColor;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a hex color in the format #RGB or #RRGGBB")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: Error,
{
if let Some(stripped) = s.strip_prefix('#') {
return self.visit_str(stripped);
}
if s.len() == 3 {
Ok(HexColor(
u8::from_str_radix(&s[0..1], 16).map_err(E::custom)? * 0x11,
u8::from_str_radix(&s[1..2], 16).map_err(E::custom)? * 0x11,
u8::from_str_radix(&s[2..3], 16).map_err(E::custom)? * 0x11,
))
} else if s.len() == 6 {
Ok(HexColor(
u8::from_str_radix(&s[0..2], 16).map_err(E::custom)?,
u8::from_str_radix(&s[2..4], 16).map_err(E::custom)?,
u8::from_str_radix(&s[4..6], 16).map_err(E::custom)?,
))
} else {
Err(Error::custom(
"Hex color must be of form \"#RGB\" or \"#RRGGBB\"",
))
}
}
}
impl<'de> Deserialize<'de> for HexColor {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(HexColorVisitor())
}
}
impl Serialize for HexColor {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(format!("{:02X}{:02X}{:02X}", self.0, self.1, self.2).as_str())
}
}
#[cfg(test)]
#[path = "./unit/theme_test.rs"]
mod theme_test;
|