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)]
149pub struct NullFormat {
150 pub text: String,
152 pub italic: bool,
154 pub background: bool,
156}
157
158impl Default for NullFormat {
159 fn default() -> Self {
160 Self {
161 text: "NULL".into(),
162 italic: true,
163 background: true,
164 }
165 }
166}
167
168#[derive(Clone, Debug, PartialEq, Eq)]
169pub struct ReplacementRule {
170 pub find: String,
171 pub replace: String,
172}
173
174impl ReplacementRule {
175 #[must_use]
177 pub fn new(find: impl Into<String>, replace: impl Into<String>) -> Self {
178 Self {
179 find: find.into(),
180 replace: replace.into(),
181 }
182 }
183}
184
185#[derive(Clone, Debug, Default, PartialEq, Eq)]
186pub struct ColumnOverride {
187 pub number: Option<NumberFormat>,
188 pub date: Option<DateFormat>,
189 pub boolean: Option<BooleanFormat>,
190 pub string: Option<StringFormat>,
191 pub null: Option<NullFormat>,
192 pub replacements: Option<Vec<ReplacementRule>>,
193 pub replacement_timing: Option<ReplacementTiming>,
194}
195
196#[derive(Clone, Debug, PartialEq, Eq)]
197pub struct ResolvedColumnFormat {
198 pub kind: ColumnKind,
199 pub number: NumberFormat,
200 pub date: DateFormat,
201 pub boolean: BooleanFormat,
202 pub string: StringFormat,
203 pub null: NullFormat,
204 pub replacements: Vec<ReplacementRule>,
205 pub replacement_timing: ReplacementTiming,
206}
207
208impl ResolvedColumnFormat {
209 #[must_use]
210 pub fn alignment(&self) -> TextAlignment {
211 match self.kind {
212 ColumnKind::Integer | ColumnKind::Decimal => self.number.alignment,
213 ColumnKind::Date => self.date.alignment,
214 ColumnKind::Boolean => self.boolean.alignment,
215 ColumnKind::Text => self.string.alignment,
216 ColumnKind::None => TextAlignment::Left,
217 }
218 }
219}
220
221#[derive(Clone, Debug, PartialEq, Eq)]
222pub struct KeyBinding {
223 pub key: String,
224 pub platform: bool,
225 pub shift: bool,
226 pub alt: bool,
227 pub control: bool,
228}
229
230impl KeyBinding {
231 pub fn matches(&self, ks: &gpui::Keystroke) -> bool {
238 let required = self.platform || self.shift || self.alt || self.control;
239 let actual =
240 ks.modifiers.platform || ks.modifiers.shift || ks.modifiers.alt || ks.modifiers.control;
241 if !required {
244 return self.key == ks.key && !actual;
245 }
246 self.key == ks.key
247 && self.platform == ks.modifiers.platform
248 && self.shift == ks.modifiers.shift
249 && self.alt == ks.modifiers.alt
250 && self.control == ks.modifiers.control
251 }
252}
253
254#[derive(Clone, Debug, PartialEq, Eq)]
255pub struct KeyBindings {
256 pub select_all: KeyBinding,
257 pub copy: KeyBinding,
258 pub copy_with_headers: KeyBinding,
259 pub page_up: KeyBinding,
260 pub page_down: KeyBinding,
261 pub context_menu_modifier_control: bool,
262 pub context_menu_modifier_alt: bool,
263}
264
265impl Default for KeyBindings {
266 fn default() -> Self {
267 Self {
268 select_all: KeyBinding {
269 key: "a".into(),
270 platform: true,
271 shift: false,
272 alt: false,
273 control: false,
274 },
275 copy: KeyBinding {
276 key: "c".into(),
277 platform: true,
278 shift: false,
279 alt: false,
280 control: false,
281 },
282 copy_with_headers: KeyBinding {
283 key: "c".into(),
284 platform: true,
285 shift: true,
286 alt: false,
287 control: false,
288 },
289 page_up: KeyBinding {
290 key: "pageup".into(),
291 platform: false,
292 shift: false,
293 alt: false,
294 control: false,
295 },
296 page_down: KeyBinding {
297 key: "pagedown".into(),
298 platform: false,
299 shift: false,
300 alt: false,
301 control: false,
302 },
303 context_menu_modifier_control: true,
304 context_menu_modifier_alt: false,
305 }
306 }
307}
308
309#[derive(Clone, Debug, PartialEq, Eq)]
310pub struct GridConfig {
311 pub key_bindings: KeyBindings,
312 pub default_number: NumberFormat,
313 pub default_date: DateFormat,
314 pub default_boolean: BooleanFormat,
315 pub default_string: StringFormat,
316 pub default_null: NullFormat,
319 pub default_replacements: Vec<ReplacementRule>,
320 pub replacement_timing: ReplacementTiming,
321 pub column_overrides: Vec<ColumnOverride>,
322}
323
324impl Default for GridConfig {
325 fn default() -> Self {
326 Self {
327 key_bindings: KeyBindings::default(),
328 default_number: NumberFormat::default(),
329 default_date: DateFormat::default(),
330 default_boolean: BooleanFormat::default(),
331 default_string: StringFormat::default(),
332 default_null: NullFormat::default(),
333 default_replacements: vec![],
334 replacement_timing: ReplacementTiming::AfterFormat,
335 column_overrides: vec![],
336 }
337 }
338}
339
340impl GridConfig {
341 #[must_use]
344 pub fn resolve(&self, col_idx: usize, kind: ColumnKind) -> ResolvedColumnFormat {
345 let o = self.column_overrides.get(col_idx);
346 ResolvedColumnFormat {
347 kind,
348 number: o.and_then(|o| o.number).unwrap_or(self.default_number),
349 date: o
350 .and_then(|o| o.date.clone())
351 .unwrap_or_else(|| self.default_date.clone()),
352 boolean: o
353 .and_then(|o| o.boolean.clone())
354 .unwrap_or_else(|| self.default_boolean.clone()),
355 string: o
356 .and_then(|o| o.string.clone())
357 .unwrap_or_else(|| self.default_string.clone()),
358 null: o
359 .and_then(|o| o.null.clone())
360 .unwrap_or_else(|| self.default_null.clone()),
361 replacements: o
362 .and_then(|o| o.replacements.clone())
363 .unwrap_or_else(|| self.default_replacements.clone()),
364 replacement_timing: o
365 .and_then(|o| o.replacement_timing)
366 .unwrap_or(self.replacement_timing),
367 }
368 }
369
370 #[must_use]
373 pub fn resolve_all(&self, columns: &[crate::data::Column]) -> Vec<ResolvedColumnFormat> {
374 columns
375 .iter()
376 .enumerate()
377 .map(|(i, c)| self.resolve(i, c.kind))
378 .collect()
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use gpui::Keystroke;
386
387 fn ks(key: &str, platform: bool, shift: bool, alt: bool, control: bool) -> Keystroke {
388 Keystroke {
389 key: key.into(),
390 modifiers: gpui::Modifiers {
391 platform,
392 shift,
393 alt,
394 control,
395 function: false,
396 },
397 ..Default::default()
398 }
399 }
400
401 #[test]
402 fn resolve_uses_defaults_without_override() {
403 let cfg = GridConfig::default();
404 let cols = vec![
405 crate::data::Column::new("a", ColumnKind::Text, 80.0),
406 crate::data::Column::new("b", ColumnKind::Integer, 80.0),
407 ];
408 let resolved = cfg.resolve_all(&cols);
409 assert_eq!(resolved.len(), 2);
410 assert_eq!(resolved[0].kind, ColumnKind::Text);
411 assert_eq!(resolved[1].kind, ColumnKind::Integer);
412 assert_eq!(resolved[0].number.alignment, TextAlignment::Right);
413 assert_eq!(resolved[0].string.alignment, TextAlignment::Left);
414 }
415
416 #[test]
417 fn resolve_uses_per_column_override() {
418 let cfg = GridConfig {
419 column_overrides: vec![
420 ColumnOverride {
421 number: Some(NumberFormat {
422 decimals: 4,
423 ..NumberFormat::default()
424 }),
425 ..Default::default()
426 },
427 ColumnOverride::default(),
428 ],
429 ..GridConfig::default()
430 };
431 let cols = vec![
432 crate::data::Column::new("a", ColumnKind::Decimal, 80.0),
433 crate::data::Column::new("b", ColumnKind::Decimal, 80.0),
434 ];
435 let resolved = cfg.resolve_all(&cols);
436 assert_eq!(resolved[0].number.decimals, 4);
437 assert_eq!(resolved[1].number.decimals, 2);
438 }
439
440 #[test]
441 fn key_binding_matches_exact_modifier_set() {
442 let binding = KeyBinding {
443 key: "c".into(),
444 platform: true,
445 shift: false,
446 alt: false,
447 control: false,
448 };
449 assert!(binding.matches(&ks("c", true, false, false, false)));
450 assert!(!binding.matches(&ks("c", true, false, true, false)));
452 assert!(!binding.matches(&ks("c", true, false, false, true)));
453 assert!(!binding.matches(&ks("x", true, false, false, false)));
455 }
456
457 #[test]
458 fn key_binding_with_no_required_modifier_only_matches_bare_key() {
459 let binding = KeyBinding {
460 key: "pagedown".into(),
461 platform: false,
462 shift: false,
463 alt: false,
464 control: false,
465 };
466 assert!(binding.matches(&ks("pagedown", false, false, false, false)));
467 assert!(!binding.matches(&ks("pagedown", true, false, false, false)));
468 }
469
470 #[test]
471 fn key_binding_with_alt_true_accepts_alt_modifier() {
472 let binding = KeyBinding {
473 key: "c".into(),
474 platform: true,
475 shift: false,
476 alt: true,
477 control: false,
478 };
479 assert!(binding.matches(&ks("c", true, false, true, false)));
480 assert!(!binding.matches(&ks("c", true, false, false, false)));
481 }
482}