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)]
64pub struct NumberFormat {
65 pub decimals: usize,
67 pub show_negative_red: bool,
70 pub negative_parentheses: bool,
73 pub thousands_separator: bool,
75 pub alignment: TextAlignment,
77}
78
79impl Default for NumberFormat {
80 fn default() -> Self {
81 Self {
82 decimals: 2,
83 show_negative_red: true,
84 negative_parentheses: false,
85 thousands_separator: true,
86 alignment: TextAlignment::Right,
87 }
88 }
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
92pub struct RelativeDateFormat {
93 pub units: Vec<RelativeUnit>,
94 pub max_components: usize,
95}
96
97impl Default for RelativeDateFormat {
98 fn default() -> Self {
99 Self {
100 units: vec![RelativeUnit::Year, RelativeUnit::Month, RelativeUnit::Day],
101 max_components: 1,
102 }
103 }
104}
105
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct DateFormat {
108 pub format: String,
109 pub timezone_offset_minutes: i32,
110 pub relative: Option<RelativeDateFormat>,
111 pub alignment: TextAlignment,
112}
113
114impl Default for DateFormat {
115 fn default() -> Self {
116 Self {
117 format: "%Y-%m-%d".into(),
118 timezone_offset_minutes: 0,
119 relative: None,
120 alignment: TextAlignment::Center,
121 }
122 }
123}
124
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct BooleanFormat {
127 pub true_text: String,
128 pub false_text: String,
129 pub alignment: TextAlignment,
130}
131
132impl Default for BooleanFormat {
133 fn default() -> Self {
134 Self {
135 true_text: "true".into(),
136 false_text: "false".into(),
137 alignment: TextAlignment::Center,
138 }
139 }
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
143pub struct StringFormat {
144 pub case: TextCase,
145 pub max_length: Option<usize>,
146 pub truncation: TruncationBehavior,
147 pub alignment: TextAlignment,
148}
149
150impl Default for StringFormat {
151 fn default() -> Self {
152 Self {
153 case: TextCase::None,
154 max_length: None,
155 truncation: TruncationBehavior::Ellipsis,
156 alignment: TextAlignment::Left,
157 }
158 }
159}
160
161#[derive(Clone, Debug, PartialEq, Eq)]
168pub struct NullFormat {
169 pub text: String,
171 pub italic: bool,
173 pub background: bool,
175}
176
177impl Default for NullFormat {
178 fn default() -> Self {
179 Self {
180 text: "NULL".into(),
181 italic: true,
182 background: true,
183 }
184 }
185}
186
187#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct ReplacementRule {
189 pub find: String,
190 pub replace: String,
191}
192
193impl ReplacementRule {
194 #[must_use]
196 pub fn new(find: impl Into<String>, replace: impl Into<String>) -> Self {
197 Self {
198 find: find.into(),
199 replace: replace.into(),
200 }
201 }
202}
203
204#[derive(Clone, Debug, Default, PartialEq, Eq)]
205pub struct ColumnOverride {
206 pub number: Option<NumberFormat>,
207 pub date: Option<DateFormat>,
208 pub boolean: Option<BooleanFormat>,
209 pub string: Option<StringFormat>,
210 pub null: Option<NullFormat>,
211 pub replacements: Option<Vec<ReplacementRule>>,
212 pub replacement_timing: Option<ReplacementTiming>,
213}
214
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub struct ResolvedColumnFormat {
217 pub kind: ColumnKind,
218 pub number: NumberFormat,
219 pub date: DateFormat,
220 pub boolean: BooleanFormat,
221 pub string: StringFormat,
222 pub null: NullFormat,
223 pub replacements: Vec<ReplacementRule>,
224 pub replacement_timing: ReplacementTiming,
225}
226
227impl ResolvedColumnFormat {
228 #[must_use]
229 pub fn alignment(&self) -> TextAlignment {
230 match self.kind {
231 ColumnKind::Integer | ColumnKind::Decimal => self.number.alignment,
232 ColumnKind::Date => self.date.alignment,
233 ColumnKind::Boolean => self.boolean.alignment,
234 ColumnKind::Text => self.string.alignment,
235 ColumnKind::None => TextAlignment::Left,
236 }
237 }
238}
239
240#[derive(Clone, Debug, PartialEq, Eq)]
241pub struct KeyBinding {
242 pub key: String,
243 pub platform: bool,
244 pub shift: bool,
245 pub alt: bool,
246 pub control: bool,
247}
248
249impl KeyBinding {
250 pub fn matches(&self, ks: &gpui::Keystroke) -> bool {
257 let required = self.platform || self.shift || self.alt || self.control;
258 let actual =
259 ks.modifiers.platform || ks.modifiers.shift || ks.modifiers.alt || ks.modifiers.control;
260 if !required {
263 return self.key == ks.key && !actual;
264 }
265 self.key == ks.key
266 && self.platform == ks.modifiers.platform
267 && self.shift == ks.modifiers.shift
268 && self.alt == ks.modifiers.alt
269 && self.control == ks.modifiers.control
270 }
271}
272
273#[derive(Clone, Debug, PartialEq, Eq)]
274pub struct KeyBindings {
275 pub select_all: KeyBinding,
276 pub copy: KeyBinding,
277 pub copy_with_headers: KeyBinding,
278 pub page_up: KeyBinding,
279 pub page_down: KeyBinding,
280 pub context_menu_modifier_control: bool,
281 pub context_menu_modifier_alt: bool,
282}
283
284impl Default for KeyBindings {
285 fn default() -> Self {
286 Self {
287 select_all: KeyBinding {
288 key: "a".into(),
289 platform: true,
290 shift: false,
291 alt: false,
292 control: false,
293 },
294 copy: KeyBinding {
295 key: "c".into(),
296 platform: true,
297 shift: false,
298 alt: false,
299 control: false,
300 },
301 copy_with_headers: KeyBinding {
302 key: "c".into(),
303 platform: true,
304 shift: true,
305 alt: false,
306 control: false,
307 },
308 page_up: KeyBinding {
309 key: "pageup".into(),
310 platform: false,
311 shift: false,
312 alt: false,
313 control: false,
314 },
315 page_down: KeyBinding {
316 key: "pagedown".into(),
317 platform: false,
318 shift: false,
319 alt: false,
320 control: false,
321 },
322 context_menu_modifier_control: true,
323 context_menu_modifier_alt: false,
324 }
325 }
326}
327
328#[derive(Clone, Debug, PartialEq, Eq)]
329pub struct GridConfig {
330 pub key_bindings: KeyBindings,
331 pub default_number: NumberFormat,
332 pub default_date: DateFormat,
333 pub default_boolean: BooleanFormat,
334 pub default_string: StringFormat,
335 pub default_null: NullFormat,
338 pub default_replacements: Vec<ReplacementRule>,
339 pub replacement_timing: ReplacementTiming,
340 pub column_overrides: Vec<ColumnOverride>,
341 pub empty_text: String,
345}
346
347impl Default for GridConfig {
348 fn default() -> Self {
349 Self {
350 key_bindings: KeyBindings::default(),
351 default_number: NumberFormat::default(),
352 default_date: DateFormat::default(),
353 default_boolean: BooleanFormat::default(),
354 default_string: StringFormat::default(),
355 default_null: NullFormat::default(),
356 default_replacements: vec![],
357 replacement_timing: ReplacementTiming::AfterFormat,
358 column_overrides: vec![],
359 empty_text: "No rows".into(),
360 }
361 }
362}
363
364impl GridConfig {
365 #[must_use]
368 pub fn resolve(&self, col_idx: usize, kind: ColumnKind) -> ResolvedColumnFormat {
369 let o = self.column_overrides.get(col_idx);
370 ResolvedColumnFormat {
371 kind,
372 number: o.and_then(|o| o.number).unwrap_or(self.default_number),
373 date: o
374 .and_then(|o| o.date.clone())
375 .unwrap_or_else(|| self.default_date.clone()),
376 boolean: o
377 .and_then(|o| o.boolean.clone())
378 .unwrap_or_else(|| self.default_boolean.clone()),
379 string: o
380 .and_then(|o| o.string.clone())
381 .unwrap_or_else(|| self.default_string.clone()),
382 null: o
383 .and_then(|o| o.null.clone())
384 .unwrap_or_else(|| self.default_null.clone()),
385 replacements: o
386 .and_then(|o| o.replacements.clone())
387 .unwrap_or_else(|| self.default_replacements.clone()),
388 replacement_timing: o
389 .and_then(|o| o.replacement_timing)
390 .unwrap_or(self.replacement_timing),
391 }
392 }
393
394 #[must_use]
397 pub fn resolve_all(&self, columns: &[crate::data::Column]) -> Vec<ResolvedColumnFormat> {
398 columns
399 .iter()
400 .enumerate()
401 .map(|(i, c)| self.resolve(i, c.kind))
402 .collect()
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 use gpui::Keystroke;
410
411 fn ks(key: &str, platform: bool, shift: bool, alt: bool, control: bool) -> Keystroke {
412 Keystroke {
413 key: key.into(),
414 modifiers: gpui::Modifiers {
415 platform,
416 shift,
417 alt,
418 control,
419 function: false,
420 },
421 ..Default::default()
422 }
423 }
424
425 #[test]
426 fn resolve_uses_defaults_without_override() {
427 let cfg = GridConfig::default();
428 let cols = vec![
429 crate::data::Column::new("a", ColumnKind::Text, 80.0),
430 crate::data::Column::new("b", ColumnKind::Integer, 80.0),
431 ];
432 let resolved = cfg.resolve_all(&cols);
433 assert_eq!(resolved.len(), 2);
434 assert_eq!(resolved[0].kind, ColumnKind::Text);
435 assert_eq!(resolved[1].kind, ColumnKind::Integer);
436 assert_eq!(resolved[0].number.alignment, TextAlignment::Right);
437 assert_eq!(resolved[0].string.alignment, TextAlignment::Left);
438 }
439
440 #[test]
441 fn resolve_uses_per_column_override() {
442 let cfg = GridConfig {
443 column_overrides: vec![
444 ColumnOverride {
445 number: Some(NumberFormat {
446 decimals: 4,
447 ..NumberFormat::default()
448 }),
449 ..Default::default()
450 },
451 ColumnOverride::default(),
452 ],
453 ..GridConfig::default()
454 };
455 let cols = vec![
456 crate::data::Column::new("a", ColumnKind::Decimal, 80.0),
457 crate::data::Column::new("b", ColumnKind::Decimal, 80.0),
458 ];
459 let resolved = cfg.resolve_all(&cols);
460 assert_eq!(resolved[0].number.decimals, 4);
461 assert_eq!(resolved[1].number.decimals, 2);
462 }
463
464 #[test]
465 fn key_binding_matches_exact_modifier_set() {
466 let binding = KeyBinding {
467 key: "c".into(),
468 platform: true,
469 shift: false,
470 alt: false,
471 control: false,
472 };
473 assert!(binding.matches(&ks("c", true, false, false, false)));
474 assert!(!binding.matches(&ks("c", true, false, true, false)));
476 assert!(!binding.matches(&ks("c", true, false, false, true)));
477 assert!(!binding.matches(&ks("x", true, false, false, false)));
479 }
480
481 #[test]
482 fn key_binding_with_no_required_modifier_only_matches_bare_key() {
483 let binding = KeyBinding {
484 key: "pagedown".into(),
485 platform: false,
486 shift: false,
487 alt: false,
488 control: false,
489 };
490 assert!(binding.matches(&ks("pagedown", false, false, false, false)));
491 assert!(!binding.matches(&ks("pagedown", true, false, false, false)));
492 }
493
494 #[test]
495 fn key_binding_with_alt_true_accepts_alt_modifier() {
496 let binding = KeyBinding {
497 key: "c".into(),
498 platform: true,
499 shift: false,
500 alt: true,
501 control: false,
502 };
503 assert!(binding.matches(&ks("c", true, false, true, false)));
504 assert!(!binding.matches(&ks("c", true, false, false, false)));
505 }
506}