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 pub empty_text: String,
326}
327
328impl Default for GridConfig {
329 fn default() -> Self {
330 Self {
331 key_bindings: KeyBindings::default(),
332 default_number: NumberFormat::default(),
333 default_date: DateFormat::default(),
334 default_boolean: BooleanFormat::default(),
335 default_string: StringFormat::default(),
336 default_null: NullFormat::default(),
337 default_replacements: vec![],
338 replacement_timing: ReplacementTiming::AfterFormat,
339 column_overrides: vec![],
340 empty_text: "No rows".into(),
341 }
342 }
343}
344
345impl GridConfig {
346 #[must_use]
349 pub fn resolve(&self, col_idx: usize, kind: ColumnKind) -> ResolvedColumnFormat {
350 let o = self.column_overrides.get(col_idx);
351 ResolvedColumnFormat {
352 kind,
353 number: o.and_then(|o| o.number).unwrap_or(self.default_number),
354 date: o
355 .and_then(|o| o.date.clone())
356 .unwrap_or_else(|| self.default_date.clone()),
357 boolean: o
358 .and_then(|o| o.boolean.clone())
359 .unwrap_or_else(|| self.default_boolean.clone()),
360 string: o
361 .and_then(|o| o.string.clone())
362 .unwrap_or_else(|| self.default_string.clone()),
363 null: o
364 .and_then(|o| o.null.clone())
365 .unwrap_or_else(|| self.default_null.clone()),
366 replacements: o
367 .and_then(|o| o.replacements.clone())
368 .unwrap_or_else(|| self.default_replacements.clone()),
369 replacement_timing: o
370 .and_then(|o| o.replacement_timing)
371 .unwrap_or(self.replacement_timing),
372 }
373 }
374
375 #[must_use]
378 pub fn resolve_all(&self, columns: &[crate::data::Column]) -> Vec<ResolvedColumnFormat> {
379 columns
380 .iter()
381 .enumerate()
382 .map(|(i, c)| self.resolve(i, c.kind))
383 .collect()
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use gpui::Keystroke;
391
392 fn ks(key: &str, platform: bool, shift: bool, alt: bool, control: bool) -> Keystroke {
393 Keystroke {
394 key: key.into(),
395 modifiers: gpui::Modifiers {
396 platform,
397 shift,
398 alt,
399 control,
400 function: false,
401 },
402 ..Default::default()
403 }
404 }
405
406 #[test]
407 fn resolve_uses_defaults_without_override() {
408 let cfg = GridConfig::default();
409 let cols = vec![
410 crate::data::Column::new("a", ColumnKind::Text, 80.0),
411 crate::data::Column::new("b", ColumnKind::Integer, 80.0),
412 ];
413 let resolved = cfg.resolve_all(&cols);
414 assert_eq!(resolved.len(), 2);
415 assert_eq!(resolved[0].kind, ColumnKind::Text);
416 assert_eq!(resolved[1].kind, ColumnKind::Integer);
417 assert_eq!(resolved[0].number.alignment, TextAlignment::Right);
418 assert_eq!(resolved[0].string.alignment, TextAlignment::Left);
419 }
420
421 #[test]
422 fn resolve_uses_per_column_override() {
423 let cfg = GridConfig {
424 column_overrides: vec![
425 ColumnOverride {
426 number: Some(NumberFormat {
427 decimals: 4,
428 ..NumberFormat::default()
429 }),
430 ..Default::default()
431 },
432 ColumnOverride::default(),
433 ],
434 ..GridConfig::default()
435 };
436 let cols = vec![
437 crate::data::Column::new("a", ColumnKind::Decimal, 80.0),
438 crate::data::Column::new("b", ColumnKind::Decimal, 80.0),
439 ];
440 let resolved = cfg.resolve_all(&cols);
441 assert_eq!(resolved[0].number.decimals, 4);
442 assert_eq!(resolved[1].number.decimals, 2);
443 }
444
445 #[test]
446 fn key_binding_matches_exact_modifier_set() {
447 let binding = KeyBinding {
448 key: "c".into(),
449 platform: true,
450 shift: false,
451 alt: false,
452 control: false,
453 };
454 assert!(binding.matches(&ks("c", true, false, false, false)));
455 assert!(!binding.matches(&ks("c", true, false, true, false)));
457 assert!(!binding.matches(&ks("c", true, false, false, true)));
458 assert!(!binding.matches(&ks("x", true, false, false, false)));
460 }
461
462 #[test]
463 fn key_binding_with_no_required_modifier_only_matches_bare_key() {
464 let binding = KeyBinding {
465 key: "pagedown".into(),
466 platform: false,
467 shift: false,
468 alt: false,
469 control: false,
470 };
471 assert!(binding.matches(&ks("pagedown", false, false, false, false)));
472 assert!(!binding.matches(&ks("pagedown", true, false, false, false)));
473 }
474
475 #[test]
476 fn key_binding_with_alt_true_accepts_alt_modifier() {
477 let binding = KeyBinding {
478 key: "c".into(),
479 platform: true,
480 shift: false,
481 alt: true,
482 control: false,
483 };
484 assert!(binding.matches(&ks("c", true, false, true, false)));
485 assert!(!binding.matches(&ks("c", true, false, false, false)));
486 }
487}