1use crate::data::ColumnKind;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub enum TextAlignment {
13 Left,
14 Center,
15 Right,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub enum TextCase {
20 Upper,
21 Lower,
22 Title,
23 None,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27pub enum TruncationBehavior {
28 Ellipsis,
29 CutOff,
30 Wrap,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
34pub enum RelativeUnit {
35 Second,
36 Minute,
37 Hour,
38 Day,
39 Week,
40 Month,
41 Year,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
45pub enum ReplacementTiming {
46 BeforeFormat,
47 #[default]
48 AfterFormat,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct NumberFormat {
53 pub decimals: usize,
54 pub show_negative_red: bool,
55 pub negative_parentheses: bool,
56 pub thousands_separator: bool,
57 pub alignment: TextAlignment,
58}
59
60impl Default for NumberFormat {
61 fn default() -> Self {
62 Self {
63 decimals: 2,
64 show_negative_red: true,
65 negative_parentheses: false,
66 thousands_separator: true,
67 alignment: TextAlignment::Right,
68 }
69 }
70}
71
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct RelativeDateFormat {
74 pub units: Vec<RelativeUnit>,
75 pub max_components: usize,
76}
77
78impl Default for RelativeDateFormat {
79 fn default() -> Self {
80 Self {
81 units: vec![RelativeUnit::Year, RelativeUnit::Month, RelativeUnit::Day],
82 max_components: 1,
83 }
84 }
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct DateFormat {
89 pub format: String,
90 pub timezone_offset_minutes: i32,
91 pub relative: Option<RelativeDateFormat>,
92 pub alignment: TextAlignment,
93}
94
95impl Default for DateFormat {
96 fn default() -> Self {
97 Self {
98 format: "%Y-%m-%d".into(),
99 timezone_offset_minutes: 0,
100 relative: None,
101 alignment: TextAlignment::Center,
102 }
103 }
104}
105
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct BooleanFormat {
108 pub true_text: String,
109 pub false_text: String,
110 pub alignment: TextAlignment,
111}
112
113impl Default for BooleanFormat {
114 fn default() -> Self {
115 Self {
116 true_text: "true".into(),
117 false_text: "false".into(),
118 alignment: TextAlignment::Center,
119 }
120 }
121}
122
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct StringFormat {
125 pub case: TextCase,
126 pub max_length: Option<usize>,
127 pub truncation: TruncationBehavior,
128 pub alignment: TextAlignment,
129}
130
131impl Default for StringFormat {
132 fn default() -> Self {
133 Self {
134 case: TextCase::None,
135 max_length: None,
136 truncation: TruncationBehavior::Ellipsis,
137 alignment: TextAlignment::Left,
138 }
139 }
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
143pub struct ReplacementRule {
144 pub find: String,
145 pub replace: String,
146}
147
148impl ReplacementRule {
149 #[must_use]
151 pub fn new(find: impl Into<String>, replace: impl Into<String>) -> Self {
152 Self {
153 find: find.into(),
154 replace: replace.into(),
155 }
156 }
157}
158
159#[derive(Clone, Debug, Default, PartialEq, Eq)]
160pub struct ColumnOverride {
161 pub number: Option<NumberFormat>,
162 pub date: Option<DateFormat>,
163 pub boolean: Option<BooleanFormat>,
164 pub string: Option<StringFormat>,
165 pub replacements: Option<Vec<ReplacementRule>>,
166 pub replacement_timing: Option<ReplacementTiming>,
167}
168
169#[derive(Clone, Debug, PartialEq, Eq)]
170pub struct ResolvedColumnFormat {
171 pub kind: ColumnKind,
172 pub number: NumberFormat,
173 pub date: DateFormat,
174 pub boolean: BooleanFormat,
175 pub string: StringFormat,
176 pub replacements: Vec<ReplacementRule>,
177 pub replacement_timing: ReplacementTiming,
178}
179
180impl ResolvedColumnFormat {
181 #[must_use]
182 pub fn alignment(&self) -> TextAlignment {
183 match self.kind {
184 ColumnKind::Integer | ColumnKind::Decimal => self.number.alignment,
185 ColumnKind::Date => self.date.alignment,
186 ColumnKind::Boolean => self.boolean.alignment,
187 ColumnKind::Text => self.string.alignment,
188 ColumnKind::None => TextAlignment::Left,
189 }
190 }
191}
192
193#[derive(Clone, Debug, PartialEq, Eq)]
194pub struct KeyBinding {
195 pub key: String,
196 pub platform: bool,
197 pub shift: bool,
198 pub alt: bool,
199 pub control: bool,
200}
201
202impl KeyBinding {
203 pub fn matches(&self, ks: &gpui::Keystroke) -> bool {
210 let required = self.platform || self.shift || self.alt || self.control;
211 let actual =
212 ks.modifiers.platform || ks.modifiers.shift || ks.modifiers.alt || ks.modifiers.control;
213 if !required {
216 return self.key == ks.key && !actual;
217 }
218 self.key == ks.key
219 && self.platform == ks.modifiers.platform
220 && self.shift == ks.modifiers.shift
221 && self.alt == ks.modifiers.alt
222 && self.control == ks.modifiers.control
223 }
224}
225
226#[derive(Clone, Debug, PartialEq, Eq)]
227pub struct KeyBindings {
228 pub select_all: KeyBinding,
229 pub copy: KeyBinding,
230 pub copy_with_headers: KeyBinding,
231 pub page_up: KeyBinding,
232 pub page_down: KeyBinding,
233 pub context_menu_modifier_control: bool,
234 pub context_menu_modifier_alt: bool,
235}
236
237impl Default for KeyBindings {
238 fn default() -> Self {
239 Self {
240 select_all: KeyBinding {
241 key: "a".into(),
242 platform: true,
243 shift: false,
244 alt: false,
245 control: false,
246 },
247 copy: KeyBinding {
248 key: "c".into(),
249 platform: true,
250 shift: false,
251 alt: false,
252 control: false,
253 },
254 copy_with_headers: KeyBinding {
255 key: "c".into(),
256 platform: true,
257 shift: true,
258 alt: false,
259 control: false,
260 },
261 page_up: KeyBinding {
262 key: "pageup".into(),
263 platform: false,
264 shift: false,
265 alt: false,
266 control: false,
267 },
268 page_down: KeyBinding {
269 key: "pagedown".into(),
270 platform: false,
271 shift: false,
272 alt: false,
273 control: false,
274 },
275 context_menu_modifier_control: true,
276 context_menu_modifier_alt: false,
277 }
278 }
279}
280
281#[derive(Clone, Debug, PartialEq, Eq)]
282pub struct GridConfig {
283 pub key_bindings: KeyBindings,
284 pub default_number: NumberFormat,
285 pub default_date: DateFormat,
286 pub default_boolean: BooleanFormat,
287 pub default_string: StringFormat,
288 pub default_replacements: Vec<ReplacementRule>,
289 pub replacement_timing: ReplacementTiming,
290 pub column_overrides: Vec<ColumnOverride>,
291}
292
293impl Default for GridConfig {
294 fn default() -> Self {
295 Self {
296 key_bindings: KeyBindings::default(),
297 default_number: NumberFormat::default(),
298 default_date: DateFormat::default(),
299 default_boolean: BooleanFormat::default(),
300 default_string: StringFormat::default(),
301 default_replacements: vec![],
302 replacement_timing: ReplacementTiming::AfterFormat,
303 column_overrides: vec![],
304 }
305 }
306}
307
308impl GridConfig {
309 #[must_use]
312 pub fn resolve(&self, col_idx: usize, kind: ColumnKind) -> ResolvedColumnFormat {
313 let o = self.column_overrides.get(col_idx);
314 ResolvedColumnFormat {
315 kind,
316 number: o.and_then(|o| o.number).unwrap_or(self.default_number),
317 date: o
318 .and_then(|o| o.date.clone())
319 .unwrap_or_else(|| self.default_date.clone()),
320 boolean: o
321 .and_then(|o| o.boolean.clone())
322 .unwrap_or_else(|| self.default_boolean.clone()),
323 string: o
324 .and_then(|o| o.string.clone())
325 .unwrap_or_else(|| self.default_string.clone()),
326 replacements: o
327 .and_then(|o| o.replacements.clone())
328 .unwrap_or_else(|| self.default_replacements.clone()),
329 replacement_timing: o
330 .and_then(|o| o.replacement_timing)
331 .unwrap_or(self.replacement_timing),
332 }
333 }
334
335 #[must_use]
338 pub fn resolve_all(&self, columns: &[crate::data::Column]) -> Vec<ResolvedColumnFormat> {
339 columns
340 .iter()
341 .enumerate()
342 .map(|(i, c)| self.resolve(i, c.kind))
343 .collect()
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use gpui::Keystroke;
351
352 fn ks(key: &str, platform: bool, shift: bool, alt: bool, control: bool) -> Keystroke {
353 Keystroke {
354 key: key.into(),
355 modifiers: gpui::Modifiers {
356 platform,
357 shift,
358 alt,
359 control,
360 function: false,
361 },
362 ..Default::default()
363 }
364 }
365
366 #[test]
367 fn resolve_uses_defaults_without_override() {
368 let cfg = GridConfig::default();
369 let cols = vec![
370 crate::data::Column::new("a", ColumnKind::Text, 80.0),
371 crate::data::Column::new("b", ColumnKind::Integer, 80.0),
372 ];
373 let resolved = cfg.resolve_all(&cols);
374 assert_eq!(resolved.len(), 2);
375 assert_eq!(resolved[0].kind, ColumnKind::Text);
376 assert_eq!(resolved[1].kind, ColumnKind::Integer);
377 assert_eq!(resolved[0].number.alignment, TextAlignment::Right);
378 assert_eq!(resolved[0].string.alignment, TextAlignment::Left);
379 }
380
381 #[test]
382 fn resolve_uses_per_column_override() {
383 let cfg = GridConfig {
384 column_overrides: vec![
385 ColumnOverride {
386 number: Some(NumberFormat {
387 decimals: 4,
388 ..NumberFormat::default()
389 }),
390 ..Default::default()
391 },
392 ColumnOverride::default(),
393 ],
394 ..GridConfig::default()
395 };
396 let cols = vec![
397 crate::data::Column::new("a", ColumnKind::Decimal, 80.0),
398 crate::data::Column::new("b", ColumnKind::Decimal, 80.0),
399 ];
400 let resolved = cfg.resolve_all(&cols);
401 assert_eq!(resolved[0].number.decimals, 4);
402 assert_eq!(resolved[1].number.decimals, 2);
403 }
404
405 #[test]
406 fn key_binding_matches_exact_modifier_set() {
407 let binding = KeyBinding {
408 key: "c".into(),
409 platform: true,
410 shift: false,
411 alt: false,
412 control: false,
413 };
414 assert!(binding.matches(&ks("c", true, false, false, false)));
415 assert!(!binding.matches(&ks("c", true, false, true, false)));
417 assert!(!binding.matches(&ks("c", true, false, false, true)));
418 assert!(!binding.matches(&ks("x", true, false, false, false)));
420 }
421
422 #[test]
423 fn key_binding_with_no_required_modifier_only_matches_bare_key() {
424 let binding = KeyBinding {
425 key: "pagedown".into(),
426 platform: false,
427 shift: false,
428 alt: false,
429 control: false,
430 };
431 assert!(binding.matches(&ks("pagedown", false, false, false, false)));
432 assert!(!binding.matches(&ks("pagedown", true, false, false, false)));
433 }
434
435 #[test]
436 fn key_binding_with_alt_true_accepts_alt_modifier() {
437 let binding = KeyBinding {
438 key: "c".into(),
439 platform: true,
440 shift: false,
441 alt: true,
442 control: false,
443 };
444 assert!(binding.matches(&ks("c", true, false, true, false)));
445 assert!(!binding.matches(&ks("c", true, false, false, false)));
446 }
447}