1use crate::data::Palette;
2use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::fs::File;
6use std::io::{self, Read};
7use std::path::PathBuf;
8use thiserror::Error;
9
10use std::convert::TryFrom;
11
12use super::keybinds::Keybinds;
13use super::layout::RunPluginOrAlias;
14use super::options::Options;
15use super::plugins::{PluginAliases, PluginsConfigError};
16use super::theme::{Themes, UiConfig};
17use crate::cli::{CliArgs, Command};
18use crate::envs::EnvironmentVariables;
19use crate::{home, setup};
20
21const DEFAULT_CONFIG_FILE_NAME: &str = "config.kdl";
22
23type ConfigResult = Result<Config, ConfigError>;
24
25#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
27pub struct Config {
28 pub keybinds: Keybinds,
29 pub options: Options,
30 pub themes: Themes,
31 pub plugins: PluginAliases,
32 pub ui: UiConfig,
33 pub env: EnvironmentVariables,
34 pub background_plugins: HashSet<RunPluginOrAlias>,
35}
36
37#[derive(Error, Debug)]
38pub struct KdlError {
39 pub error_message: String,
40 pub src: Option<NamedSource>,
41 pub offset: Option<usize>,
42 pub len: Option<usize>,
43 pub help_message: Option<String>,
44}
45
46impl KdlError {
47 pub fn add_src(mut self, src_name: String, src_input: String) -> Self {
48 self.src = Some(NamedSource::new(src_name, src_input));
49 self
50 }
51}
52
53impl std::fmt::Display for KdlError {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
55 write!(f, "Failed to parse Zellij configuration")
56 }
57}
58use std::fmt::Display;
59
60impl Diagnostic for KdlError {
61 fn source_code(&self) -> Option<&dyn SourceCode> {
62 match self.src.as_ref() {
63 Some(src) => Some(src),
64 None => None,
65 }
66 }
67 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
68 match &self.help_message {
69 Some(help_message) => Some(Box::new(help_message)),
70 None => Some(Box::new(format!("For more information, please see our configuration guide: https://zellij.dev/documentation/configuration.html")))
71 }
72 }
73 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
74 if let (Some(offset), Some(len)) = (self.offset, self.len) {
75 let label = LabeledSpan::new(Some(self.error_message.clone()), offset, len);
76 Some(Box::new(std::iter::once(label)))
77 } else {
78 None
79 }
80 }
81}
82
83#[derive(Error, Debug, Diagnostic)]
84pub enum ConfigError {
85 #[error("Deserialization error: {0}")]
87 KdlDeserializationError(#[from] kdl::KdlError),
88 #[error("KdlDeserialization error: {0}")]
89 KdlError(KdlError), #[error("Config error: {0}")]
91 Std(#[from] Box<dyn std::error::Error>),
92 #[error("IoError: {0}, File: {1}")]
94 IoPath(io::Error, PathBuf),
95 #[error("FromUtf8Error: {0}")]
97 FromUtf8(#[from] std::string::FromUtf8Error),
98 #[error("PluginsError: {0}")]
100 PluginsError(#[from] PluginsConfigError),
101 #[error("{0}")]
102 ConversionError(#[from] ConversionError),
103 #[error("{0}")]
104 DownloadError(String),
105}
106
107impl ConfigError {
108 pub fn new_kdl_error(error_message: String, offset: usize, len: usize) -> Self {
109 ConfigError::KdlError(KdlError {
110 error_message,
111 src: None,
112 offset: Some(offset),
113 len: Some(len),
114 help_message: None,
115 })
116 }
117 pub fn new_layout_kdl_error(error_message: String, offset: usize, len: usize) -> Self {
118 ConfigError::KdlError(KdlError {
119 error_message,
120 src: None,
121 offset: Some(offset),
122 len: Some(len),
123 help_message: Some(format!("For more information, please see our layout guide: https://zellij.dev/documentation/creating-a-layout.html")),
124 })
125 }
126}
127
128#[derive(Debug, Error)]
129pub enum ConversionError {
130 #[error("{0}")]
131 UnknownInputMode(String),
132}
133
134impl TryFrom<&CliArgs> for Config {
135 type Error = ConfigError;
136
137 fn try_from(opts: &CliArgs) -> ConfigResult {
138 if let Some(ref path) = opts.config {
139 let default_config = Config::from_default_assets()?;
140 return Config::from_path(path, Some(default_config));
141 }
142
143 if let Some(Command::Setup(ref setup)) = opts.command {
144 if setup.clean {
145 return Config::from_default_assets();
146 }
147 }
148
149 let config_dir = opts
150 .config_dir
151 .clone()
152 .or_else(home::find_default_config_dir);
153
154 if let Some(ref config) = config_dir {
155 let path = config.join(DEFAULT_CONFIG_FILE_NAME);
156 if path.exists() {
157 let default_config = Config::from_default_assets()?;
158 Config::from_path(&path, Some(default_config))
159 } else {
160 Config::from_default_assets()
161 }
162 } else {
163 Config::from_default_assets()
164 }
165 }
166}
167
168impl Config {
169 pub fn theme_config(&self, theme_name: Option<&String>) -> Option<Palette> {
170 match &theme_name {
171 Some(theme_name) => self.themes.get_theme(theme_name).map(|theme| theme.palette),
172 None => self.themes.get_theme("default").map(|theme| theme.palette),
173 }
174 }
175 pub fn from_default_assets() -> ConfigResult {
177 let cfg = String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?;
178 match Self::from_kdl(&cfg, None) {
179 Ok(config) => Ok(config),
180 Err(ConfigError::KdlError(kdl_error)) => Err(ConfigError::KdlError(
181 kdl_error.add_src("Default built-in-configuration".into(), cfg),
182 )),
183 Err(e) => Err(e),
184 }
185 }
186 pub fn from_path(path: &PathBuf, default_config: Option<Config>) -> ConfigResult {
187 match File::open(path) {
188 Ok(mut file) => {
189 let mut kdl_config = String::new();
190 file.read_to_string(&mut kdl_config)
191 .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?;
192 match Config::from_kdl(&kdl_config, default_config) {
193 Ok(config) => Ok(config),
194 Err(ConfigError::KdlDeserializationError(kdl_error)) => {
195 let error_message = match kdl_error.kind {
196 kdl::KdlErrorKind::Context("valid node terminator") => {
197 format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
198 "- Missing `;` after a node name, eg. { node; another_node; }",
199 "- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }",
200 "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
201 "- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }")
202 },
203 _ => {
204 String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error"))
205 },
206 };
207 let kdl_error = KdlError {
208 error_message,
209 src: Some(NamedSource::new(
210 path.as_path().as_os_str().to_string_lossy(),
211 kdl_config,
212 )),
213 offset: Some(kdl_error.span.offset()),
214 len: Some(kdl_error.span.len()),
215 help_message: None,
216 };
217 Err(ConfigError::KdlError(kdl_error))
218 },
219 Err(ConfigError::KdlError(kdl_error)) => {
220 Err(ConfigError::KdlError(kdl_error.add_src(
221 path.as_path().as_os_str().to_string_lossy().to_string(),
222 kdl_config,
223 )))
224 },
225 Err(e) => Err(e),
226 }
227 },
228 Err(e) => Err(ConfigError::IoPath(e, path.into())),
229 }
230 }
231 pub fn merge(&mut self, other: Config) -> Result<(), ConfigError> {
232 self.options = self.options.merge(other.options);
233 self.keybinds.merge(other.keybinds.clone());
234 self.themes = self.themes.merge(other.themes);
235 self.plugins.merge(other.plugins);
236 self.ui = self.ui.merge(other.ui);
237 self.env = self.env.merge(other.env);
238 Ok(())
239 }
240 pub fn config_file_path(opts: &CliArgs) -> Option<PathBuf> {
241 opts.config.clone().or_else(|| {
242 opts.config_dir
243 .clone()
244 .or_else(home::find_default_config_dir)
245 .map(|config_dir| config_dir.join(DEFAULT_CONFIG_FILE_NAME))
246 })
247 }
248 pub fn write_config_to_disk(config: String, opts: &CliArgs) -> Result<Config, Option<PathBuf>> {
249 Config::from_kdl(&config, None)
251 .map_err(|e| {
252 log::error!("Failed to parse config: {}", e);
253 None
254 })
255 .and_then(|parsed_config| {
256 let backed_up_file_name = Config::backup_current_config(&opts)?;
257 let config_file_path = Config::config_file_path(&opts).ok_or_else(|| {
258 log::error!("Config file path not found");
259 None
260 })?;
261 let config = match backed_up_file_name {
262 Some(backed_up_file_name) => {
263 format!(
264 "{}{}",
265 Config::autogen_config_message(backed_up_file_name),
266 config
267 )
268 },
269 None => config,
270 };
271 std::fs::write(&config_file_path, config.as_bytes()).map_err(|e| {
272 log::error!("Failed to write config: {}", e);
273 Some(config_file_path.clone())
274 })?;
275 let written_config = std::fs::read_to_string(&config_file_path).map_err(|e| {
276 log::error!("Failed to read written config: {}", e);
277 Some(config_file_path.clone())
278 })?;
279 let parsed_written_config =
280 Config::from_kdl(&written_config, None).map_err(|e| {
281 log::error!("Failed to parse written config: {}", e);
282 None
283 })?;
284 if parsed_written_config == parsed_config {
285 Ok(parsed_config)
286 } else {
287 log::error!("Configuration corrupted when writing to disk");
288 Err(Some(config_file_path))
289 }
290 })
291 }
292 pub fn write_config_to_disk_if_it_does_not_exist(config: String, opts: &CliArgs) -> bool {
294 if opts.config.is_none() {
295 home::try_create_home_config_dir();
298 }
299 match Config::config_file_path(opts) {
300 Some(config_file_path) => {
301 if config_file_path.exists() {
302 false
303 } else {
304 if let Err(e) = std::fs::write(&config_file_path, config.as_bytes()) {
305 log::error!("Failed to write config to disk: {}", e);
306 return false;
307 }
308 match std::fs::read_to_string(&config_file_path) {
309 Ok(written_config) => written_config == config,
310 Err(e) => {
311 log::error!("Failed to read written config: {}", e);
312 false
313 },
314 }
315 }
316 },
317 None => false,
318 }
319 }
320 fn find_free_backup_file_name(config_file_path: &PathBuf) -> Option<PathBuf> {
321 let mut backup_config_path = None;
322 let config_file_name = config_file_path
323 .file_name()
324 .and_then(|f| f.to_str())
325 .unwrap_or_else(|| DEFAULT_CONFIG_FILE_NAME);
326 for i in 0..100 {
327 let new_file_name = if i == 0 {
328 format!("{}.bak", config_file_name)
329 } else {
330 format!("{}.bak.{}", config_file_name, i)
331 };
332 let mut potential_config_path = config_file_path.clone();
333 potential_config_path.set_file_name(new_file_name);
334 if !potential_config_path.exists() {
335 backup_config_path = Some(potential_config_path);
336 break;
337 }
338 }
339 backup_config_path
340 }
341 fn backup_config_with_written_content_confirmation(
342 current_config: &str,
343 current_config_file_path: &PathBuf,
344 backup_config_path: &PathBuf,
345 ) -> bool {
346 let _ = std::fs::copy(current_config_file_path, &backup_config_path);
347 match std::fs::read_to_string(&backup_config_path) {
348 Ok(backed_up_config) => current_config == &backed_up_config,
349 Err(e) => {
350 log::error!(
351 "Failed to back up config file {}: {:?}",
352 backup_config_path.display(),
353 e
354 );
355 false
356 },
357 }
358 }
359 fn backup_current_config(opts: &CliArgs) -> Result<Option<PathBuf>, Option<PathBuf>> {
360 if let Some(config_file_path) = Config::config_file_path(&opts) {
362 match std::fs::read_to_string(&config_file_path) {
363 Ok(current_config) => {
364 let Some(backup_config_path) =
365 Config::find_free_backup_file_name(&config_file_path)
366 else {
367 log::error!("Failed to find a file name to back up the configuration to, ran out of files.");
368 return Err(None);
369 };
370 if Config::backup_config_with_written_content_confirmation(
371 ¤t_config,
372 &config_file_path,
373 &backup_config_path,
374 ) {
375 Ok(Some(backup_config_path))
376 } else {
377 log::error!(
378 "Failed to back up config file: {}",
379 backup_config_path.display()
380 );
381 Err(Some(backup_config_path))
382 }
383 },
384 Err(e) => {
385 if e.kind() == std::io::ErrorKind::NotFound {
386 Ok(None)
387 } else {
388 log::error!(
389 "Failed to read current config {}: {}",
390 config_file_path.display(),
391 e
392 );
393 Err(Some(config_file_path))
394 }
395 },
396 }
397 } else {
398 log::error!("No config file path found?");
399 Err(None)
400 }
401 }
402 fn autogen_config_message(backed_up_file_name: PathBuf) -> String {
403 format!("//\n// THIS FILE WAS AUTOGENERATED BY ZELLIJ, THE PREVIOUS FILE AT THIS LOCATION WAS COPIED TO: {}\n//\n\n", backed_up_file_name.display())
404 }
405}
406
407#[cfg(test)]
408mod config_test {
409 use super::*;
410 use crate::data::{InputMode, Palette, PaletteColor};
411 use crate::input::layout::RunPlugin;
412 use crate::input::options::{Clipboard, OnForceClose};
413 use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
414 use std::collections::{BTreeMap, HashMap};
415 use std::io::Write;
416 use tempfile::tempdir;
417
418 #[test]
419 fn try_from_cli_args_with_config() {
420 let arbitrary_config = PathBuf::from("nonexistent.yaml");
422 let opts = CliArgs {
423 config: Some(arbitrary_config),
424 ..Default::default()
425 };
426 println!("OPTS= {:?}", opts);
427 let result = Config::try_from(&opts);
428 assert!(result.is_err());
429 }
430
431 #[test]
432 fn try_from_cli_args_with_option_clean() {
433 use crate::setup::Setup;
435 let opts = CliArgs {
436 command: Some(Command::Setup(Setup {
437 clean: true,
438 ..Setup::default()
439 })),
440 ..Default::default()
441 };
442 let result = Config::try_from(&opts);
443 assert!(result.is_ok());
444 }
445
446 #[test]
447 fn try_from_cli_args_with_config_dir() {
448 let mut opts = CliArgs::default();
449 let tmp = tempdir().unwrap();
450 File::create(tmp.path().join(DEFAULT_CONFIG_FILE_NAME))
451 .unwrap()
452 .write_all(b"keybinds: invalid\n")
453 .unwrap();
454 opts.config_dir = Some(tmp.path().to_path_buf());
455 let result = Config::try_from(&opts);
456 assert!(result.is_err());
457 }
458
459 #[test]
460 fn try_from_cli_args_with_config_dir_without_config() {
461 let mut opts = CliArgs::default();
462 let tmp = tempdir().unwrap();
463 opts.config_dir = Some(tmp.path().to_path_buf());
464 let result = Config::try_from(&opts);
465 assert_eq!(result.unwrap(), Config::from_default_assets().unwrap());
466 }
467
468 #[test]
469 fn try_from_cli_args_default() {
470 let opts = CliArgs::default();
471 let result = Config::try_from(&opts);
472 assert_eq!(result.unwrap(), Config::from_default_assets().unwrap());
473 }
474
475 #[test]
476 fn can_define_options_in_configfile() {
477 let config_contents = r#"
478 simplified_ui true
479 theme "my cool theme"
480 default_mode "locked"
481 default_shell "/path/to/my/shell"
482 default_cwd "/path"
483 default_layout "/path/to/my/layout.kdl"
484 layout_dir "/path/to/my/layout-dir"
485 theme_dir "/path/to/my/theme-dir"
486 mouse_mode false
487 pane_frames false
488 mirror_session true
489 on_force_close "quit"
490 scroll_buffer_size 100000
491 copy_command "/path/to/my/copy-command"
492 copy_clipboard "primary"
493 copy_on_select false
494 scrollback_editor "/path/to/my/scrollback-editor"
495 session_name "my awesome session"
496 attach_to_session true
497 "#;
498 let config = Config::from_kdl(config_contents, None).unwrap();
499 assert_eq!(
500 config.options.simplified_ui,
501 Some(true),
502 "Option set in config"
503 );
504 assert_eq!(
505 config.options.theme,
506 Some(String::from("my cool theme")),
507 "Option set in config"
508 );
509 assert_eq!(
510 config.options.default_mode,
511 Some(InputMode::Locked),
512 "Option set in config"
513 );
514 assert_eq!(
515 config.options.default_shell,
516 Some(PathBuf::from("/path/to/my/shell")),
517 "Option set in config"
518 );
519 assert_eq!(
520 config.options.default_cwd,
521 Some(PathBuf::from("/path")),
522 "Option set in config"
523 );
524 assert_eq!(
525 config.options.default_layout,
526 Some(PathBuf::from("/path/to/my/layout.kdl")),
527 "Option set in config"
528 );
529 assert_eq!(
530 config.options.layout_dir,
531 Some(PathBuf::from("/path/to/my/layout-dir")),
532 "Option set in config"
533 );
534 assert_eq!(
535 config.options.theme_dir,
536 Some(PathBuf::from("/path/to/my/theme-dir")),
537 "Option set in config"
538 );
539 assert_eq!(
540 config.options.mouse_mode,
541 Some(false),
542 "Option set in config"
543 );
544 assert_eq!(
545 config.options.pane_frames,
546 Some(false),
547 "Option set in config"
548 );
549 assert_eq!(
550 config.options.mirror_session,
551 Some(true),
552 "Option set in config"
553 );
554 assert_eq!(
555 config.options.on_force_close,
556 Some(OnForceClose::Quit),
557 "Option set in config"
558 );
559 assert_eq!(
560 config.options.scroll_buffer_size,
561 Some(100000),
562 "Option set in config"
563 );
564 assert_eq!(
565 config.options.copy_command,
566 Some(String::from("/path/to/my/copy-command")),
567 "Option set in config"
568 );
569 assert_eq!(
570 config.options.copy_clipboard,
571 Some(Clipboard::Primary),
572 "Option set in config"
573 );
574 assert_eq!(
575 config.options.copy_on_select,
576 Some(false),
577 "Option set in config"
578 );
579 assert_eq!(
580 config.options.scrollback_editor,
581 Some(PathBuf::from("/path/to/my/scrollback-editor")),
582 "Option set in config"
583 );
584 assert_eq!(
585 config.options.session_name,
586 Some(String::from("my awesome session")),
587 "Option set in config"
588 );
589 assert_eq!(
590 config.options.attach_to_session,
591 Some(true),
592 "Option set in config"
593 );
594 }
595
596 #[test]
597 fn can_define_themes_in_configfile() {
598 let config_contents = r#"
599 themes {
600 dracula {
601 fg 248 248 242
602 bg 40 42 54
603 red 255 85 85
604 green 80 250 123
605 yellow 241 250 140
606 blue 98 114 164
607 magenta 255 121 198
608 orange 255 184 108
609 cyan 139 233 253
610 black 0 0 0
611 white 255 255 255
612 }
613 }
614 "#;
615 let config = Config::from_kdl(config_contents, None).unwrap();
616 let mut expected_themes = HashMap::new();
617 expected_themes.insert(
618 "dracula".into(),
619 Theme {
620 palette: Palette {
621 fg: PaletteColor::Rgb((248, 248, 242)),
622 bg: PaletteColor::Rgb((40, 42, 54)),
623 red: PaletteColor::Rgb((255, 85, 85)),
624 green: PaletteColor::Rgb((80, 250, 123)),
625 yellow: PaletteColor::Rgb((241, 250, 140)),
626 blue: PaletteColor::Rgb((98, 114, 164)),
627 magenta: PaletteColor::Rgb((255, 121, 198)),
628 orange: PaletteColor::Rgb((255, 184, 108)),
629 cyan: PaletteColor::Rgb((139, 233, 253)),
630 black: PaletteColor::Rgb((0, 0, 0)),
631 white: PaletteColor::Rgb((255, 255, 255)),
632 ..Default::default()
633 },
634 sourced_from_external_file: false,
635 },
636 );
637 let expected_themes = Themes::from_data(expected_themes);
638 assert_eq!(config.themes, expected_themes, "Theme defined in config");
639 }
640
641 #[test]
642 fn can_define_multiple_themes_including_hex_themes_in_configfile() {
643 let config_contents = r##"
644 themes {
645 dracula {
646 fg 248 248 242
647 bg 40 42 54
648 red 255 85 85
649 green 80 250 123
650 yellow 241 250 140
651 blue 98 114 164
652 magenta 255 121 198
653 orange 255 184 108
654 cyan 139 233 253
655 black 0 0 0
656 white 255 255 255
657 }
658 nord {
659 fg "#D8DEE9"
660 bg "#2E3440"
661 black "#3B4252"
662 red "#BF616A"
663 green "#A3BE8C"
664 yellow "#EBCB8B"
665 blue "#81A1C1"
666 magenta "#B48EAD"
667 cyan "#88C0D0"
668 white "#E5E9F0"
669 orange "#D08770"
670 }
671 }
672 "##;
673 let config = Config::from_kdl(config_contents, None).unwrap();
674 let mut expected_themes = HashMap::new();
675 expected_themes.insert(
676 "dracula".into(),
677 Theme {
678 palette: Palette {
679 fg: PaletteColor::Rgb((248, 248, 242)),
680 bg: PaletteColor::Rgb((40, 42, 54)),
681 red: PaletteColor::Rgb((255, 85, 85)),
682 green: PaletteColor::Rgb((80, 250, 123)),
683 yellow: PaletteColor::Rgb((241, 250, 140)),
684 blue: PaletteColor::Rgb((98, 114, 164)),
685 magenta: PaletteColor::Rgb((255, 121, 198)),
686 orange: PaletteColor::Rgb((255, 184, 108)),
687 cyan: PaletteColor::Rgb((139, 233, 253)),
688 black: PaletteColor::Rgb((0, 0, 0)),
689 white: PaletteColor::Rgb((255, 255, 255)),
690 ..Default::default()
691 },
692 sourced_from_external_file: false,
693 },
694 );
695 expected_themes.insert(
696 "nord".into(),
697 Theme {
698 palette: Palette {
699 fg: PaletteColor::Rgb((216, 222, 233)),
700 bg: PaletteColor::Rgb((46, 52, 64)),
701 black: PaletteColor::Rgb((59, 66, 82)),
702 red: PaletteColor::Rgb((191, 97, 106)),
703 green: PaletteColor::Rgb((163, 190, 140)),
704 yellow: PaletteColor::Rgb((235, 203, 139)),
705 blue: PaletteColor::Rgb((129, 161, 193)),
706 magenta: PaletteColor::Rgb((180, 142, 173)),
707 cyan: PaletteColor::Rgb((136, 192, 208)),
708 white: PaletteColor::Rgb((229, 233, 240)),
709 orange: PaletteColor::Rgb((208, 135, 112)),
710 ..Default::default()
711 },
712 sourced_from_external_file: false,
713 },
714 );
715 let expected_themes = Themes::from_data(expected_themes);
716 assert_eq!(config.themes, expected_themes, "Theme defined in config");
717 }
718
719 #[test]
720 fn can_define_eight_bit_themes() {
721 let config_contents = r#"
722 themes {
723 eight_bit_theme {
724 fg 248
725 bg 40
726 red 255
727 green 80
728 yellow 241
729 blue 98
730 magenta 255
731 orange 255
732 cyan 139
733 black 1
734 white 255
735 }
736 }
737 "#;
738 let config = Config::from_kdl(config_contents, None).unwrap();
739 let mut expected_themes = HashMap::new();
740 expected_themes.insert(
741 "eight_bit_theme".into(),
742 Theme {
743 palette: Palette {
744 fg: PaletteColor::EightBit(248),
745 bg: PaletteColor::EightBit(40),
746 red: PaletteColor::EightBit(255),
747 green: PaletteColor::EightBit(80),
748 yellow: PaletteColor::EightBit(241),
749 blue: PaletteColor::EightBit(98),
750 magenta: PaletteColor::EightBit(255),
751 orange: PaletteColor::EightBit(255),
752 cyan: PaletteColor::EightBit(139),
753 black: PaletteColor::EightBit(1),
754 white: PaletteColor::EightBit(255),
755 ..Default::default()
756 },
757 sourced_from_external_file: false,
758 },
759 );
760 let expected_themes = Themes::from_data(expected_themes);
761 assert_eq!(config.themes, expected_themes, "Theme defined in config");
762 }
763
764 #[test]
765 fn can_define_plugin_configuration_in_configfile() {
766 let config_contents = r#"
767 plugins {
768 tab-bar location="zellij:tab-bar"
769 status-bar location="zellij:status-bar"
770 strider location="zellij:strider"
771 compact-bar location="zellij:compact-bar"
772 session-manager location="zellij:session-manager"
773 welcome-screen location="zellij:session-manager" {
774 welcome_screen true
775 }
776 filepicker location="zellij:strider"
777 }
778 "#;
779 let config = Config::from_kdl(config_contents, None).unwrap();
780 let mut expected_plugin_configuration = BTreeMap::new();
781 expected_plugin_configuration.insert(
782 "tab-bar".to_owned(),
783 RunPlugin::from_url("zellij:tab-bar").unwrap(),
784 );
785 expected_plugin_configuration.insert(
786 "status-bar".to_owned(),
787 RunPlugin::from_url("zellij:status-bar").unwrap(),
788 );
789 expected_plugin_configuration.insert(
790 "strider".to_owned(),
791 RunPlugin::from_url("zellij:strider").unwrap(),
792 );
793 expected_plugin_configuration.insert(
794 "compact-bar".to_owned(),
795 RunPlugin::from_url("zellij:compact-bar").unwrap(),
796 );
797 expected_plugin_configuration.insert(
798 "session-manager".to_owned(),
799 RunPlugin::from_url("zellij:session-manager").unwrap(),
800 );
801 let mut welcome_screen_configuration = BTreeMap::new();
802 welcome_screen_configuration.insert("welcome_screen".to_owned(), "true".to_owned());
803 expected_plugin_configuration.insert(
804 "welcome-screen".to_owned(),
805 RunPlugin::from_url("zellij:session-manager")
806 .unwrap()
807 .with_configuration(welcome_screen_configuration),
808 );
809 expected_plugin_configuration.insert(
810 "filepicker".to_owned(),
811 RunPlugin::from_url("zellij:strider").unwrap(),
812 );
813 assert_eq!(
814 config.plugins,
815 PluginAliases::from_data(expected_plugin_configuration),
816 "Plugins defined in config"
817 );
818 }
819
820 #[test]
821 fn can_define_ui_configuration_in_configfile() {
822 let config_contents = r#"
823 ui {
824 pane_frames {
825 rounded_corners true
826 hide_session_name true
827 }
828 }
829 "#;
830 let config = Config::from_kdl(config_contents, None).unwrap();
831 let expected_ui_config = UiConfig {
832 pane_frames: FrameConfig {
833 rounded_corners: true,
834 hide_session_name: true,
835 },
836 };
837 assert_eq!(config.ui, expected_ui_config, "Ui config defined in config");
838 }
839
840 #[test]
841 fn can_define_env_variables_in_config_file() {
842 let config_contents = r#"
843 env {
844 RUST_BACKTRACE 1
845 SOME_OTHER_VAR "foo"
846 }
847 "#;
848 let config = Config::from_kdl(config_contents, None).unwrap();
849 let mut expected_env_config = HashMap::new();
850 expected_env_config.insert("RUST_BACKTRACE".into(), "1".into());
851 expected_env_config.insert("SOME_OTHER_VAR".into(), "foo".into());
852 assert_eq!(
853 config.env,
854 EnvironmentVariables::from_data(expected_env_config),
855 "Env variables defined in config"
856 );
857 }
858}