1use crate::data::Styling;
2
3#[cfg(not(target_family = "wasm"))]
4use crate::data::{LayoutInfo, LayoutWithError};
5
6use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9use std::fs::File;
10use std::io::{self, Read};
11#[cfg(not(target_family = "wasm"))]
12use std::path::Path;
13use std::path::PathBuf;
14use thiserror::Error;
15
16use std::convert::TryFrom;
17
18use super::keybinds::Keybinds;
19use super::layout::RunPluginOrAlias;
20use super::options::Options;
21use super::plugins::{PluginAliases, PluginsConfigError};
22use super::theme::{Themes, UiConfig};
23use super::web_client::WebClientConfig;
24use crate::cli::{CliArgs, Command};
25use crate::envs::EnvironmentVariables;
26use crate::{home, setup};
27
28pub const DEFAULT_CONFIG_FILE_NAME: &str = "config.kdl";
29
30type ConfigResult = Result<Config, ConfigError>;
31
32#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
34pub struct Config {
35 pub keybinds: Keybinds,
36 pub options: Options,
37 pub themes: Themes,
38 pub plugins: PluginAliases,
39 pub ui: UiConfig,
40 pub env: EnvironmentVariables,
41 pub background_plugins: HashSet<RunPluginOrAlias>,
42 pub web_client: WebClientConfig,
43}
44
45#[derive(Error, Debug, Serialize, Deserialize)]
46pub struct KdlError {
47 pub error_message: String,
48 #[serde(skip)]
49 pub src: Option<NamedSource<String>>,
50 pub offset: Option<usize>,
51 pub len: Option<usize>,
52 pub help_message: Option<String>,
53}
54
55impl Clone for KdlError {
56 fn clone(&self) -> Self {
57 KdlError {
58 error_message: self.error_message.clone(),
59 src: None, offset: self.offset,
61 len: self.len,
62 help_message: self.help_message.clone(),
63 }
64 }
65}
66
67impl PartialEq for KdlError {
68 fn eq(&self, other: &Self) -> bool {
69 self.error_message == other.error_message
71 && self.offset == other.offset
72 && self.len == other.len
73 && self.help_message == other.help_message
74 }
75}
76
77impl Eq for KdlError {}
78
79impl KdlError {
80 pub fn add_src(mut self, src_name: String, src_input: String) -> Self {
81 self.src = Some(NamedSource::new(src_name, src_input));
82 self
83 }
84}
85
86impl std::fmt::Display for KdlError {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
88 write!(f, "Failed to parse Zellij configuration")
89 }
90}
91use std::fmt::Display;
92
93impl Diagnostic for KdlError {
94 fn source_code(&self) -> Option<&dyn SourceCode> {
95 match self.src.as_ref() {
96 Some(src) => Some(src),
97 None => None,
98 }
99 }
100 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
101 match &self.help_message {
102 Some(help_message) => Some(Box::new(help_message)),
103 None => Some(Box::new(format!("For more information, please see our configuration guide: https://zellij.dev/documentation/configuration.html")))
104 }
105 }
106 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
107 if let (Some(offset), Some(len)) = (self.offset, self.len) {
108 let label = LabeledSpan::new(Some(self.error_message.clone()), offset, len);
109 Some(Box::new(std::iter::once(label)))
110 } else {
111 None
112 }
113 }
114}
115
116#[derive(Error, Debug, Diagnostic)]
117pub enum ConfigError {
118 #[error("Deserialization error: {0}")]
120 KdlDeserializationError(#[from] kdl::KdlError),
121 #[error("KdlDeserialization error: {0}")]
122 KdlError(KdlError), #[error("Config error: {0}")]
124 Std(#[from] Box<dyn std::error::Error>),
125 #[error("IoError: {0}, File: {1}")]
127 IoPath(io::Error, PathBuf),
128 #[error("FromUtf8Error: {0}")]
130 FromUtf8(#[from] std::string::FromUtf8Error),
131 #[error("PluginsError: {0}")]
133 PluginsError(#[from] PluginsConfigError),
134 #[error("{0}")]
135 ConversionError(#[from] ConversionError),
136 #[error("{0}")]
137 DownloadError(String),
138 #[error("failed to block on async task")]
139 Async(#[from] std::io::Error),
140}
141
142impl ConfigError {
143 pub fn new_kdl_error(error_message: String, offset: usize, len: usize) -> Self {
144 ConfigError::KdlError(KdlError {
145 error_message,
146 src: None,
147 offset: Some(offset),
148 len: Some(len),
149 help_message: None,
150 })
151 }
152 pub fn new_layout_kdl_error(error_message: String, offset: usize, len: usize) -> Self {
153 ConfigError::KdlError(KdlError {
154 error_message,
155 src: None,
156 offset: Some(offset),
157 len: Some(len),
158 help_message: Some(format!("For more information, please see our layout guide: https://zellij.dev/documentation/creating-a-layout.html")),
159 })
160 }
161}
162
163#[derive(Debug, Error)]
164pub enum ConversionError {
165 #[error("{0}")]
166 UnknownInputMode(String),
167}
168
169impl TryFrom<&CliArgs> for Config {
170 type Error = ConfigError;
171
172 fn try_from(opts: &CliArgs) -> ConfigResult {
173 if let Some(ref path) = opts.config {
174 let default_config = Config::from_default_assets()?;
175 return Config::from_path(path, Some(default_config));
176 }
177
178 if let Some(Command::Setup(ref setup)) = opts.command {
179 if setup.clean {
180 return Config::from_default_assets();
181 }
182 }
183
184 let config_dir = opts
185 .config_dir
186 .clone()
187 .or_else(home::find_default_config_dir);
188
189 if let Some(ref config) = config_dir {
190 let path = config.join(DEFAULT_CONFIG_FILE_NAME);
191 if path.exists() {
192 let default_config = Config::from_default_assets()?;
193 Config::from_path(&path, Some(default_config))
194 } else {
195 Config::from_default_assets()
196 }
197 } else {
198 Config::from_default_assets()
199 }
200 }
201}
202
203impl Config {
204 pub fn theme_config(&self, theme_name: Option<&String>) -> Option<Styling> {
205 match &theme_name {
206 Some(theme_name) => self.themes.get_theme(theme_name).map(|theme| theme.palette),
207 None => self.themes.get_theme("default").map(|theme| theme.palette),
208 }
209 }
210 pub fn from_default_assets() -> ConfigResult {
212 let cfg = String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?;
213 match Self::from_kdl(&cfg, None) {
214 Ok(config) => Ok(config),
215 Err(ConfigError::KdlError(kdl_error)) => Err(ConfigError::KdlError(
216 kdl_error.add_src("Default built-in-configuration".into(), cfg),
217 )),
218 Err(e) => Err(e),
219 }
220 }
221 pub fn from_path(path: &PathBuf, default_config: Option<Config>) -> ConfigResult {
222 match File::open(path) {
223 Ok(mut file) => {
224 let mut kdl_config = String::new();
225 file.read_to_string(&mut kdl_config)
226 .map_err(|e| ConfigError::IoPath(e, path.to_path_buf()))?;
227 match Config::from_kdl(&kdl_config, default_config) {
228 Ok(config) => Ok(config),
229 Err(ConfigError::KdlDeserializationError(kdl_error)) => {
230 let error_message = match kdl_error.kind {
231 kdl::KdlErrorKind::Context("valid node terminator") => {
232 format!("Failed to deserialize KDL node. \nPossible reasons:\n{}\n{}\n{}\n{}",
233 "- Missing `;` after a node name, eg. { node; another_node; }",
234 "- Missing quotations (\") around an argument node eg. { first_node \"argument_node\"; }",
235 "- Missing an equal sign (=) between node arguments on a title line. eg. argument=\"value\"",
236 "- Found an extraneous equal sign (=) between node child arguments and their values. eg. { argument=\"value\" }")
237 },
238 _ => {
239 String::from(kdl_error.help.unwrap_or("Kdl Deserialization Error"))
240 },
241 };
242 let kdl_error = KdlError {
243 error_message,
244 src: Some(NamedSource::new(
245 path.as_path().as_os_str().to_string_lossy(),
246 kdl_config,
247 )),
248 offset: Some(kdl_error.span.offset()),
249 len: Some(kdl_error.span.len()),
250 help_message: None,
251 };
252 Err(ConfigError::KdlError(kdl_error))
253 },
254 Err(ConfigError::KdlError(kdl_error)) => {
255 Err(ConfigError::KdlError(kdl_error.add_src(
256 path.as_path().as_os_str().to_string_lossy().to_string(),
257 kdl_config,
258 )))
259 },
260 Err(e) => Err(e),
261 }
262 },
263 Err(e) => Err(ConfigError::IoPath(e, path.into())),
264 }
265 }
266 pub fn merge(&mut self, other: Config) -> Result<(), ConfigError> {
267 self.options = self.options.merge(other.options);
268 self.keybinds.merge(other.keybinds.clone());
269 self.themes = self.themes.merge(other.themes);
270 self.plugins.merge(other.plugins);
271 self.ui = self.ui.merge(other.ui);
272 self.env = self.env.merge(other.env);
273 Ok(())
274 }
275 pub fn config_file_path(opts: &CliArgs) -> Option<PathBuf> {
276 opts.config.clone().or_else(|| {
277 opts.config_dir
278 .clone()
279 .or_else(|| {
280 home::try_create_home_config_dir();
281 home::find_default_config_dir()
282 })
283 .map(|config_dir| config_dir.join(DEFAULT_CONFIG_FILE_NAME))
284 })
285 }
286 pub fn default_config_file_path() -> Option<PathBuf> {
287 home::find_default_config_dir().map(|config_dir| config_dir.join(DEFAULT_CONFIG_FILE_NAME))
288 }
289 pub fn write_config_to_disk(
290 config: String,
291 config_file_path: &PathBuf,
292 ) -> Result<Config, Option<PathBuf>> {
293 let config_file_path = config_file_path.clone();
295 Config::from_kdl(&config, None)
296 .map_err(|e| {
297 log::error!("Failed to parse config: {}", e);
298 None
299 })
300 .and_then(|parsed_config| {
301 let backed_up_file_name = Config::backup_current_config(&config_file_path)?;
302 let config = match backed_up_file_name {
303 Some(backed_up_file_name) => {
304 format!(
305 "{}{}",
306 Config::autogen_config_message(backed_up_file_name),
307 config
308 )
309 },
310 None => config,
311 };
312 std::fs::write(&config_file_path, config.as_bytes()).map_err(|e| {
313 log::error!("Failed to write config: {}", e);
314 Some(config_file_path.clone())
315 })?;
316 let written_config = std::fs::read_to_string(&config_file_path).map_err(|e| {
317 log::error!("Failed to read written config: {}", e);
318 Some(config_file_path.clone())
319 })?;
320 let parsed_written_config =
321 Config::from_kdl(&written_config, None).map_err(|e| {
322 log::error!("Failed to parse written config: {}", e);
323 None
324 })?;
325 if parsed_written_config == parsed_config {
326 Ok(parsed_config)
327 } else {
328 log::error!("Configuration corrupted when writing to disk");
329 Err(Some(config_file_path))
330 }
331 })
332 }
333 pub fn write_config_to_disk_if_it_does_not_exist(
335 config: String,
336 config_file_path: &Option<PathBuf>,
337 ) -> bool {
338 let Some(config_file_path) = config_file_path.clone() else {
339 log::error!("Could not find file path to write config");
340 return false;
341 };
342 if config_file_path.exists() {
343 false
344 } else {
345 if let Err(e) = std::fs::write(&config_file_path, config.as_bytes()) {
346 log::error!("Failed to write config to disk: {}", e);
347 return false;
348 }
349 match std::fs::read_to_string(&config_file_path) {
350 Ok(written_config) => written_config == config,
351 Err(e) => {
352 log::error!("Failed to read written config: {}", e);
353 false
354 },
355 }
356 }
357 }
358 fn find_free_backup_file_name(config_file_path: &PathBuf) -> Option<PathBuf> {
359 let mut backup_config_path = None;
360 let config_file_name = config_file_path
361 .file_name()
362 .and_then(|f| f.to_str())
363 .unwrap_or_else(|| DEFAULT_CONFIG_FILE_NAME);
364 for i in 0..100 {
365 let new_file_name = if i == 0 {
366 format!("{}.bak", config_file_name)
367 } else {
368 format!("{}.bak.{}", config_file_name, i)
369 };
370 let mut potential_config_path = config_file_path.clone();
371 potential_config_path.set_file_name(new_file_name);
372 if !potential_config_path.exists() {
373 backup_config_path = Some(potential_config_path);
374 break;
375 }
376 }
377 backup_config_path
378 }
379 fn backup_config_with_written_content_confirmation(
380 current_config: &str,
381 current_config_file_path: &PathBuf,
382 backup_config_path: &PathBuf,
383 ) -> bool {
384 let _ = std::fs::copy(current_config_file_path, &backup_config_path);
385 match std::fs::read_to_string(&backup_config_path) {
386 Ok(backed_up_config) => current_config == &backed_up_config,
387 Err(e) => {
388 log::error!(
389 "Failed to back up config file {}: {:?}",
390 backup_config_path.display(),
391 e
392 );
393 false
394 },
395 }
396 }
397 fn backup_current_config(
398 config_file_path: &PathBuf,
399 ) -> Result<Option<PathBuf>, Option<PathBuf>> {
400 match std::fs::read_to_string(&config_file_path) {
403 Ok(current_config) => {
404 let Some(backup_config_path) =
405 Config::find_free_backup_file_name(&config_file_path)
406 else {
407 log::error!("Failed to find a file name to back up the configuration to, ran out of files.");
408 return Err(None);
409 };
410 if Config::backup_config_with_written_content_confirmation(
411 ¤t_config,
412 &config_file_path,
413 &backup_config_path,
414 ) {
415 Ok(Some(backup_config_path))
416 } else {
417 log::error!(
418 "Failed to back up config file: {}",
419 backup_config_path.display()
420 );
421 Err(Some(backup_config_path))
422 }
423 },
424 Err(e) => {
425 if e.kind() == std::io::ErrorKind::NotFound {
426 Ok(None)
427 } else {
428 log::error!(
429 "Failed to read current config {}: {}",
430 config_file_path.display(),
431 e
432 );
433 Err(Some(config_file_path.clone()))
434 }
435 },
436 }
437 }
438 fn autogen_config_message(backed_up_file_name: PathBuf) -> String {
439 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())
440 }
441}
442
443#[cfg(not(target_family = "wasm"))]
444pub async fn watch_config_file_changes<F, Fut>(
445 config_file_path: PathBuf,
446 config_dir: Option<&Path>,
447 on_config_change: F,
448) where
449 F: Fn(Config) -> Fut + Send + 'static,
450 Fut: std::future::Future<Output = ()> + Send,
451{
452 use crate::setup::Setup;
463 use notify::{self, Config as WatcherConfig, Event, PollWatcher, RecursiveMode, Watcher};
464 use std::time::Duration;
465 use tokio::sync::mpsc;
466
467 fn cli_args_for_config(config_file_path: &Path, config_dir: Option<&Path>) -> CliArgs {
468 let mut cli_args_for_config = CliArgs::default();
469 cli_args_for_config.config = Some(config_file_path.to_path_buf());
470 cli_args_for_config.config_dir = config_dir.map(Path::to_path_buf);
471 cli_args_for_config
472 }
473
474 fn load_config_and_theme_dir(
475 config_file_path: &Path,
476 config_dir: Option<&Path>,
477 ) -> Option<(Config, Option<PathBuf>)> {
478 let cli_args_for_config = cli_args_for_config(config_file_path, config_dir);
479 Setup::from_cli_args(&cli_args_for_config)
480 .map(|(config, _, config_options, _, _)| {
481 let theme_dir = config_options.theme_dir.or_else(|| {
482 let config_dir = config_dir
483 .map(Path::to_path_buf)
484 .or_else(home::find_default_config_dir);
485 home::get_theme_dir(config_dir).filter(|dir| dir.exists())
486 });
487 (config, theme_dir)
488 })
489 .ok()
490 }
491
492 fn event_is_for_config_file(event: &Event, config_file_path: &Path) -> bool {
493 event.paths.iter().any(|path| path == config_file_path)
494 }
495
496 fn event_is_in_theme_dir(event: &Event, theme_dir: Option<&Path>) -> bool {
497 theme_dir.map_or(false, |theme_dir| {
498 event.paths.iter().any(|path| path.starts_with(theme_dir))
499 })
500 }
501
502 async fn reload_config_after_change<F, Fut>(
503 config_file_path: &Path,
504 config_dir: Option<&Path>,
505 watched_theme_dir: Option<&Path>,
506 on_config_change: &F,
507 ) -> Option<bool>
508 where
509 F: Fn(Config) -> Fut + Send + 'static,
510 Fut: std::future::Future<Output = ()> + Send,
511 {
512 tokio::time::sleep(Duration::from_millis(100)).await;
513
514 if !config_file_path.exists() {
515 return None;
516 }
517
518 let (new_config, new_theme_dir) =
519 match load_config_and_theme_dir(config_file_path, config_dir) {
520 Some(loaded) => loaded,
521 None => {
522 log::error!("Failed to reload config from {:?}", config_file_path);
523 return None;
524 },
525 };
526 on_config_change(new_config).await;
527 Some(new_theme_dir.as_deref() != watched_theme_dir)
528 }
529
530 loop {
531 if config_file_path.exists() {
532 let watched_theme_dir =
533 load_config_and_theme_dir(config_file_path.as_path(), config_dir)
534 .and_then(|(_, theme_dir)| theme_dir);
535 let (tx, mut rx) = mpsc::unbounded_channel();
536
537 let mut watcher = match PollWatcher::new(
538 move |res: Result<Event, notify::Error>| {
539 let _ = tx.send(res);
540 },
541 WatcherConfig::default().with_poll_interval(Duration::from_secs(1)),
542 ) {
543 Ok(watcher) => watcher,
544 Err(e) => {
545 log::error!("Failed to create config watcher: {}", e);
546 break;
547 },
548 };
549
550 if let Err(e) = watcher.watch(&config_file_path, RecursiveMode::NonRecursive) {
551 log::error!("Failed to watch config file {:?}: {}", config_file_path, e);
552 break;
553 }
554
555 if let Some(watched_theme_dir) = &watched_theme_dir {
556 if let Err(e) = watcher.watch(watched_theme_dir, RecursiveMode::NonRecursive) {
557 log::error!(
558 "Failed to watch theme dir {:?}, continuing without it: {}",
559 watched_theme_dir,
560 e,
561 );
562 }
563 }
564
565 while let Some(event_result) = rx.recv().await {
566 let event = match event_result {
567 Ok(event) => event,
568 Err(e) => {
569 log::error!("Config watcher event error: {}", e);
570 break;
571 },
572 };
573
574 if event_is_for_config_file(&event, config_file_path.as_path()) {
575 if event.kind.is_remove() {
576 break;
577 }
578
579 if event.kind.is_create() || event.kind.is_modify() {
580 if reload_config_after_change(
581 config_file_path.as_path(),
582 config_dir,
583 watched_theme_dir.as_deref(),
584 &on_config_change,
585 )
586 .await
587 .unwrap_or(false)
588 {
589 break;
590 }
591 }
592 } else if event_is_in_theme_dir(&event, watched_theme_dir.as_deref())
593 && (event.kind.is_remove() || event.kind.is_create() || event.kind.is_modify())
594 {
595 let should_restart_watcher = reload_config_after_change(
596 config_file_path.as_path(),
597 config_dir,
598 watched_theme_dir.as_deref(),
599 &on_config_change,
600 )
601 .await
602 .unwrap_or(true);
603 if should_restart_watcher {
604 break;
605 }
606 }
607 }
608 }
609
610 while !config_file_path.exists() {
611 tokio::time::sleep(Duration::from_secs(3)).await;
612 }
613 }
614}
615
616#[cfg(not(target_family = "wasm"))]
617pub async fn watch_layout_dir_changes<F, Fut>(
618 layout_dir: PathBuf,
619 default_layout_name: Option<String>,
620 on_layout_change: F,
621) where
622 F: Fn(Vec<LayoutInfo>, Vec<LayoutWithError>) -> Fut + Send + 'static,
623 Fut: std::future::Future<Output = ()> + Send,
624{
625 use crate::input::layout::Layout;
626 use notify::{self, Config as WatcherConfig, Event, PollWatcher, RecursiveMode, Watcher};
627 use std::time::Duration;
628 use tokio::sync::mpsc;
629
630 loop {
631 if layout_dir.exists() {
632 let (tx, mut rx) = mpsc::unbounded_channel();
633
634 let mut watcher = match PollWatcher::new(
635 move |res: Result<Event, notify::Error>| {
636 let _ = tx.send(res);
637 },
638 WatcherConfig::default().with_poll_interval(Duration::from_secs(1)),
639 ) {
640 Ok(watcher) => watcher,
641 Err(_) => break,
642 };
643
644 if watcher
645 .watch(&layout_dir, RecursiveMode::Recursive)
646 .is_err()
647 {
648 break;
649 }
650
651 while let Some(event_result) = rx.recv().await {
652 match event_result {
653 Ok(event) => {
654 if event.kind.is_remove()
655 || event.kind.is_create()
656 || event.kind.is_modify()
657 {
658 tokio::time::sleep(Duration::from_millis(100)).await;
659
660 if !layout_dir.exists() {
661 break;
662 }
663
664 let (layouts, layout_errors) = Layout::list_available_layouts(
665 Some(layout_dir.clone()),
666 &default_layout_name,
667 );
668 on_layout_change(layouts, layout_errors).await;
669 }
670 },
671 Err(_) => break,
672 }
673 }
674 }
675
676 while !layout_dir.exists() {
677 tokio::time::sleep(Duration::from_secs(3)).await;
678 }
679 }
680}
681
682#[cfg(test)]
683mod config_test {
684 use super::*;
685 use crate::data::{InputMode, Palette, PaletteColor, StyleDeclaration, Styling};
686 use crate::input::layout::RunPlugin;
687 use crate::input::options::{Clipboard, OnForceClose};
688 use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
689 use std::collections::{BTreeMap, HashMap};
690 use std::io::Write;
691 use tempfile::tempdir;
692
693 #[test]
694 fn try_from_cli_args_with_config() {
695 let arbitrary_config = PathBuf::from("nonexistent.yaml");
697 let opts = CliArgs {
698 config: Some(arbitrary_config),
699 ..Default::default()
700 };
701 println!("OPTS= {:?}", opts);
702 let result = Config::try_from(&opts);
703 assert!(result.is_err());
704 }
705
706 #[test]
707 fn try_from_cli_args_with_option_clean() {
708 use crate::setup::Setup;
710 let opts = CliArgs {
711 command: Some(Command::Setup(Setup {
712 clean: true,
713 ..Setup::default()
714 })),
715 ..Default::default()
716 };
717 let result = Config::try_from(&opts);
718 assert!(result.is_ok());
719 }
720
721 #[test]
722 fn try_from_cli_args_with_config_dir() {
723 let mut opts = CliArgs::default();
724 let tmp = tempdir().unwrap();
725 File::create(tmp.path().join(DEFAULT_CONFIG_FILE_NAME))
726 .unwrap()
727 .write_all(b"keybinds: invalid\n")
728 .unwrap();
729 opts.config_dir = Some(tmp.path().to_path_buf());
730 let result = Config::try_from(&opts);
731 assert!(result.is_err());
732 }
733
734 #[test]
735 fn try_from_cli_args_with_config_dir_without_config() {
736 let mut opts = CliArgs::default();
737 let tmp = tempdir().unwrap();
738 opts.config_dir = Some(tmp.path().to_path_buf());
739 let result = Config::try_from(&opts);
740 assert_eq!(result.unwrap(), Config::from_default_assets().unwrap());
741 }
742
743 #[test]
744 fn try_from_cli_args_default() {
745 let opts = CliArgs::default();
746 let result = Config::try_from(&opts);
747 assert_eq!(result.unwrap(), Config::from_default_assets().unwrap());
748 }
749
750 #[test]
751 fn can_define_options_in_configfile() {
752 let config_contents = r#"
753 simplified_ui true
754 theme "my cool theme"
755 default_mode "locked"
756 default_shell "/path/to/my/shell"
757 default_cwd "/path"
758 default_layout "/path/to/my/layout.kdl"
759 layout_dir "/path/to/my/layout-dir"
760 theme_dir "/path/to/my/theme-dir"
761 mouse_mode false
762 pane_frames false
763 mirror_session true
764 on_force_close "quit"
765 scroll_buffer_size 100000
766 copy_command "/path/to/my/copy-command"
767 copy_clipboard "primary"
768 copy_on_select false
769 scrollback_editor "/path/to/my/scrollback-editor"
770 session_name "my awesome session"
771 attach_to_session true
772 "#;
773 let config = Config::from_kdl(config_contents, None).unwrap();
774 assert_eq!(
775 config.options.simplified_ui,
776 Some(true),
777 "Option set in config"
778 );
779 assert_eq!(
780 config.options.theme,
781 Some(String::from("my cool theme")),
782 "Option set in config"
783 );
784 assert_eq!(
785 config.options.default_mode,
786 Some(InputMode::Locked),
787 "Option set in config"
788 );
789 assert_eq!(
790 config.options.default_shell,
791 Some(PathBuf::from("/path/to/my/shell")),
792 "Option set in config"
793 );
794 assert_eq!(
795 config.options.default_cwd,
796 Some(PathBuf::from("/path")),
797 "Option set in config"
798 );
799 assert_eq!(
800 config.options.default_layout,
801 Some(PathBuf::from("/path/to/my/layout.kdl")),
802 "Option set in config"
803 );
804 assert_eq!(
805 config.options.layout_dir,
806 Some(PathBuf::from("/path/to/my/layout-dir")),
807 "Option set in config"
808 );
809 assert_eq!(
810 config.options.theme_dir,
811 Some(PathBuf::from("/path/to/my/theme-dir")),
812 "Option set in config"
813 );
814 assert_eq!(
815 config.options.mouse_mode,
816 Some(false),
817 "Option set in config"
818 );
819 assert_eq!(
820 config.options.pane_frames,
821 Some(false),
822 "Option set in config"
823 );
824 assert_eq!(
825 config.options.mirror_session,
826 Some(true),
827 "Option set in config"
828 );
829 assert_eq!(
830 config.options.on_force_close,
831 Some(OnForceClose::Quit),
832 "Option set in config"
833 );
834 assert_eq!(
835 config.options.scroll_buffer_size,
836 Some(100000),
837 "Option set in config"
838 );
839 assert_eq!(
840 config.options.copy_command,
841 Some(String::from("/path/to/my/copy-command")),
842 "Option set in config"
843 );
844 assert_eq!(
845 config.options.copy_clipboard,
846 Some(Clipboard::Primary),
847 "Option set in config"
848 );
849 assert_eq!(
850 config.options.copy_on_select,
851 Some(false),
852 "Option set in config"
853 );
854 assert_eq!(
855 config.options.scrollback_editor,
856 Some(PathBuf::from("/path/to/my/scrollback-editor")),
857 "Option set in config"
858 );
859 assert_eq!(
860 config.options.session_name,
861 Some(String::from("my awesome session")),
862 "Option set in config"
863 );
864 assert_eq!(
865 config.options.attach_to_session,
866 Some(true),
867 "Option set in config"
868 );
869 }
870
871 #[test]
872 fn can_define_themes_in_configfile() {
873 let config_contents = r#"
874 themes {
875 dracula {
876 fg 248 248 242
877 bg 40 42 54
878 red 255 85 85
879 green 80 250 123
880 yellow 241 250 140
881 blue 98 114 164
882 magenta 255 121 198
883 orange 255 184 108
884 cyan 139 233 253
885 black 0 0 0
886 white 255 255 255
887 }
888 }
889 "#;
890 let config = Config::from_kdl(config_contents, None).unwrap();
891 let mut expected_themes = HashMap::new();
892 expected_themes.insert(
893 "dracula".into(),
894 Theme {
895 palette: Palette {
896 fg: PaletteColor::Rgb((248, 248, 242)),
897 bg: PaletteColor::Rgb((40, 42, 54)),
898 red: PaletteColor::Rgb((255, 85, 85)),
899 green: PaletteColor::Rgb((80, 250, 123)),
900 yellow: PaletteColor::Rgb((241, 250, 140)),
901 blue: PaletteColor::Rgb((98, 114, 164)),
902 magenta: PaletteColor::Rgb((255, 121, 198)),
903 orange: PaletteColor::Rgb((255, 184, 108)),
904 cyan: PaletteColor::Rgb((139, 233, 253)),
905 black: PaletteColor::Rgb((0, 0, 0)),
906 white: PaletteColor::Rgb((255, 255, 255)),
907 ..Default::default()
908 }
909 .into(),
910 sourced_from_external_file: false,
911 },
912 );
913 let expected_themes = Themes::from_data(expected_themes);
914 assert_eq!(config.themes, expected_themes, "Theme defined in config");
915 }
916
917 #[test]
918 fn can_define_multiple_themes_including_hex_themes_in_configfile() {
919 let config_contents = r##"
920 themes {
921 dracula {
922 fg 248 248 242
923 bg 40 42 54
924 red 255 85 85
925 green 80 250 123
926 yellow 241 250 140
927 blue 98 114 164
928 magenta 255 121 198
929 orange 255 184 108
930 cyan 139 233 253
931 black 0 0 0
932 white 255 255 255
933 }
934 nord {
935 fg "#D8DEE9"
936 bg "#2E3440"
937 black "#3B4252"
938 red "#BF616A"
939 green "#A3BE8C"
940 yellow "#EBCB8B"
941 blue "#81A1C1"
942 magenta "#B48EAD"
943 cyan "#88C0D0"
944 white "#E5E9F0"
945 orange "#D08770"
946 }
947 }
948 "##;
949 let config = Config::from_kdl(config_contents, None).unwrap();
950 let mut expected_themes = HashMap::new();
951 expected_themes.insert(
952 "dracula".into(),
953 Theme {
954 palette: Palette {
955 fg: PaletteColor::Rgb((248, 248, 242)),
956 bg: PaletteColor::Rgb((40, 42, 54)),
957 red: PaletteColor::Rgb((255, 85, 85)),
958 green: PaletteColor::Rgb((80, 250, 123)),
959 yellow: PaletteColor::Rgb((241, 250, 140)),
960 blue: PaletteColor::Rgb((98, 114, 164)),
961 magenta: PaletteColor::Rgb((255, 121, 198)),
962 orange: PaletteColor::Rgb((255, 184, 108)),
963 cyan: PaletteColor::Rgb((139, 233, 253)),
964 black: PaletteColor::Rgb((0, 0, 0)),
965 white: PaletteColor::Rgb((255, 255, 255)),
966 ..Default::default()
967 }
968 .into(),
969 sourced_from_external_file: false,
970 },
971 );
972 expected_themes.insert(
973 "nord".into(),
974 Theme {
975 palette: Palette {
976 fg: PaletteColor::Rgb((216, 222, 233)),
977 bg: PaletteColor::Rgb((46, 52, 64)),
978 black: PaletteColor::Rgb((59, 66, 82)),
979 red: PaletteColor::Rgb((191, 97, 106)),
980 green: PaletteColor::Rgb((163, 190, 140)),
981 yellow: PaletteColor::Rgb((235, 203, 139)),
982 blue: PaletteColor::Rgb((129, 161, 193)),
983 magenta: PaletteColor::Rgb((180, 142, 173)),
984 cyan: PaletteColor::Rgb((136, 192, 208)),
985 white: PaletteColor::Rgb((229, 233, 240)),
986 orange: PaletteColor::Rgb((208, 135, 112)),
987 ..Default::default()
988 }
989 .into(),
990 sourced_from_external_file: false,
991 },
992 );
993 let expected_themes = Themes::from_data(expected_themes);
994 assert_eq!(config.themes, expected_themes, "Theme defined in config");
995 }
996
997 #[test]
998 fn can_define_eight_bit_themes() {
999 let config_contents = r#"
1000 themes {
1001 eight_bit_theme {
1002 fg 248
1003 bg 40
1004 red 255
1005 green 80
1006 yellow 241
1007 blue 98
1008 magenta 255
1009 orange 255
1010 cyan 139
1011 black 1
1012 white 255
1013 }
1014 }
1015 "#;
1016 let config = Config::from_kdl(config_contents, None).unwrap();
1017 let mut expected_themes = HashMap::new();
1018 expected_themes.insert(
1019 "eight_bit_theme".into(),
1020 Theme {
1021 palette: Palette {
1022 fg: PaletteColor::EightBit(248),
1023 bg: PaletteColor::EightBit(40),
1024 red: PaletteColor::EightBit(255),
1025 green: PaletteColor::EightBit(80),
1026 yellow: PaletteColor::EightBit(241),
1027 blue: PaletteColor::EightBit(98),
1028 magenta: PaletteColor::EightBit(255),
1029 orange: PaletteColor::EightBit(255),
1030 cyan: PaletteColor::EightBit(139),
1031 black: PaletteColor::EightBit(1),
1032 white: PaletteColor::EightBit(255),
1033 ..Default::default()
1034 }
1035 .into(),
1036 sourced_from_external_file: false,
1037 },
1038 );
1039 let expected_themes = Themes::from_data(expected_themes);
1040 assert_eq!(config.themes, expected_themes, "Theme defined in config");
1041 }
1042
1043 #[test]
1044 fn can_define_style_for_theme_with_hex() {
1045 let config_contents = r##"
1046 themes {
1047 named_theme {
1048 text_unselected {
1049 base "#DCD7BA"
1050 emphasis_0 "#DCD7CD"
1051 emphasis_1 "#DCD8DD"
1052 emphasis_2 "#DCD899"
1053 emphasis_3 "#ACD7CD"
1054 background "#1F1F28"
1055 }
1056 text_selected {
1057 base "#16161D"
1058 emphasis_0 "#16161D"
1059 emphasis_1 "#16161D"
1060 emphasis_2 "#16161D"
1061 emphasis_3 "#16161D"
1062 background "#9CABCA"
1063 }
1064 ribbon_unselected {
1065 base "#DCD7BA"
1066 emphasis_0 "#7FB4CA"
1067 emphasis_1 "#A3D4D5"
1068 emphasis_2 "#7AA89F"
1069 emphasis_3 "#DCD819"
1070 background "#252535"
1071 }
1072 ribbon_selected {
1073 base "#16161D"
1074 emphasis_0 "#181820"
1075 emphasis_1 "#1A1A22"
1076 emphasis_2 "#2A2A37"
1077 emphasis_3 "#363646"
1078 background "#76946A"
1079 }
1080 table_title {
1081 base "#DCD7BA"
1082 emphasis_0 "#7FB4CA"
1083 emphasis_1 "#A3D4D5"
1084 emphasis_2 "#7AA89F"
1085 emphasis_3 "#DCD819"
1086 background "#252535"
1087 }
1088 table_cell_unselected {
1089 base "#DCD7BA"
1090 emphasis_0 "#DCD7CD"
1091 emphasis_1 "#DCD8DD"
1092 emphasis_2 "#DCD899"
1093 emphasis_3 "#ACD7CD"
1094 background "#1F1F28"
1095 }
1096 table_cell_selected {
1097 base "#16161D"
1098 emphasis_0 "#181820"
1099 emphasis_1 "#1A1A22"
1100 emphasis_2 "#2A2A37"
1101 emphasis_3 "#363646"
1102 background "#76946A"
1103 }
1104 list_unselected {
1105 base "#DCD7BA"
1106 emphasis_0 "#DCD7CD"
1107 emphasis_1 "#DCD8DD"
1108 emphasis_2 "#DCD899"
1109 emphasis_3 "#ACD7CD"
1110 background "#1F1F28"
1111 }
1112 list_selected {
1113 base "#16161D"
1114 emphasis_0 "#181820"
1115 emphasis_1 "#1A1A22"
1116 emphasis_2 "#2A2A37"
1117 emphasis_3 "#363646"
1118 background "#76946A"
1119 }
1120 frame_unselected {
1121 base "#DCD8DD"
1122 emphasis_0 "#7FB4CA"
1123 emphasis_1 "#A3D4D5"
1124 emphasis_2 "#7AA89F"
1125 emphasis_3 "#DCD819"
1126 }
1127 frame_selected {
1128 base "#76946A"
1129 emphasis_0 "#C34043"
1130 emphasis_1 "#C8C093"
1131 emphasis_2 "#ACD7CD"
1132 emphasis_3 "#DCD819"
1133 }
1134 exit_code_success {
1135 base "#76946A"
1136 emphasis_0 "#76946A"
1137 emphasis_1 "#76946A"
1138 emphasis_2 "#76946A"
1139 emphasis_3 "#76946A"
1140 }
1141 exit_code_error {
1142 base "#C34043"
1143 emphasis_0 "#C34043"
1144 emphasis_1 "#C34043"
1145 emphasis_2 "#C34043"
1146 emphasis_3 "#C34043"
1147 }
1148 }
1149 }
1150 "##;
1151
1152 let config = Config::from_kdl(config_contents, None).unwrap();
1153 let mut expected_themes = HashMap::new();
1154 expected_themes.insert(
1155 "named_theme".into(),
1156 Theme {
1157 sourced_from_external_file: false,
1158 palette: Styling {
1159 text_unselected: StyleDeclaration {
1160 base: PaletteColor::Rgb((220, 215, 186)),
1161 emphasis_0: PaletteColor::Rgb((220, 215, 205)),
1162 emphasis_1: PaletteColor::Rgb((220, 216, 221)),
1163 emphasis_2: PaletteColor::Rgb((220, 216, 153)),
1164 emphasis_3: PaletteColor::Rgb((172, 215, 205)),
1165 background: PaletteColor::Rgb((31, 31, 40)),
1166 },
1167 text_selected: StyleDeclaration {
1168 base: PaletteColor::Rgb((22, 22, 29)),
1169 emphasis_0: PaletteColor::Rgb((22, 22, 29)),
1170 emphasis_1: PaletteColor::Rgb((22, 22, 29)),
1171 emphasis_2: PaletteColor::Rgb((22, 22, 29)),
1172 emphasis_3: PaletteColor::Rgb((22, 22, 29)),
1173 background: PaletteColor::Rgb((156, 171, 202)),
1174 },
1175 ribbon_unselected: StyleDeclaration {
1176 base: PaletteColor::Rgb((220, 215, 186)),
1177 emphasis_0: PaletteColor::Rgb((127, 180, 202)),
1178 emphasis_1: PaletteColor::Rgb((163, 212, 213)),
1179 emphasis_2: PaletteColor::Rgb((122, 168, 159)),
1180 emphasis_3: PaletteColor::Rgb((220, 216, 25)),
1181 background: PaletteColor::Rgb((37, 37, 53)),
1182 },
1183 ribbon_selected: StyleDeclaration {
1184 base: PaletteColor::Rgb((22, 22, 29)),
1185 emphasis_0: PaletteColor::Rgb((24, 24, 32)),
1186 emphasis_1: PaletteColor::Rgb((26, 26, 34)),
1187 emphasis_2: PaletteColor::Rgb((42, 42, 55)),
1188 emphasis_3: PaletteColor::Rgb((54, 54, 70)),
1189 background: PaletteColor::Rgb((118, 148, 106)),
1190 },
1191 table_title: StyleDeclaration {
1192 base: PaletteColor::Rgb((220, 215, 186)),
1193 emphasis_0: PaletteColor::Rgb((127, 180, 202)),
1194 emphasis_1: PaletteColor::Rgb((163, 212, 213)),
1195 emphasis_2: PaletteColor::Rgb((122, 168, 159)),
1196 emphasis_3: PaletteColor::Rgb((220, 216, 25)),
1197 background: PaletteColor::Rgb((37, 37, 53)),
1198 },
1199 table_cell_unselected: StyleDeclaration {
1200 base: PaletteColor::Rgb((220, 215, 186)),
1201 emphasis_0: PaletteColor::Rgb((220, 215, 205)),
1202 emphasis_1: PaletteColor::Rgb((220, 216, 221)),
1203 emphasis_2: PaletteColor::Rgb((220, 216, 153)),
1204 emphasis_3: PaletteColor::Rgb((172, 215, 205)),
1205 background: PaletteColor::Rgb((31, 31, 40)),
1206 },
1207 table_cell_selected: StyleDeclaration {
1208 base: PaletteColor::Rgb((22, 22, 29)),
1209 emphasis_0: PaletteColor::Rgb((24, 24, 32)),
1210 emphasis_1: PaletteColor::Rgb((26, 26, 34)),
1211 emphasis_2: PaletteColor::Rgb((42, 42, 55)),
1212 emphasis_3: PaletteColor::Rgb((54, 54, 70)),
1213 background: PaletteColor::Rgb((118, 148, 106)),
1214 },
1215 list_unselected: StyleDeclaration {
1216 base: PaletteColor::Rgb((220, 215, 186)),
1217 emphasis_0: PaletteColor::Rgb((220, 215, 205)),
1218 emphasis_1: PaletteColor::Rgb((220, 216, 221)),
1219 emphasis_2: PaletteColor::Rgb((220, 216, 153)),
1220 emphasis_3: PaletteColor::Rgb((172, 215, 205)),
1221 background: PaletteColor::Rgb((31, 31, 40)),
1222 },
1223 list_selected: StyleDeclaration {
1224 base: PaletteColor::Rgb((22, 22, 29)),
1225 emphasis_0: PaletteColor::Rgb((24, 24, 32)),
1226 emphasis_1: PaletteColor::Rgb((26, 26, 34)),
1227 emphasis_2: PaletteColor::Rgb((42, 42, 55)),
1228 emphasis_3: PaletteColor::Rgb((54, 54, 70)),
1229 background: PaletteColor::Rgb((118, 148, 106)),
1230 },
1231 frame_unselected: Some(StyleDeclaration {
1232 base: PaletteColor::Rgb((220, 216, 221)),
1233 emphasis_0: PaletteColor::Rgb((127, 180, 202)),
1234 emphasis_1: PaletteColor::Rgb((163, 212, 213)),
1235 emphasis_2: PaletteColor::Rgb((122, 168, 159)),
1236 emphasis_3: PaletteColor::Rgb((220, 216, 25)),
1237 ..Default::default()
1238 }),
1239 frame_selected: StyleDeclaration {
1240 base: PaletteColor::Rgb((118, 148, 106)),
1241 emphasis_0: PaletteColor::Rgb((195, 64, 67)),
1242 emphasis_1: PaletteColor::Rgb((200, 192, 147)),
1243 emphasis_2: PaletteColor::Rgb((172, 215, 205)),
1244 emphasis_3: PaletteColor::Rgb((220, 216, 25)),
1245 ..Default::default()
1246 },
1247 exit_code_success: StyleDeclaration {
1248 base: PaletteColor::Rgb((118, 148, 106)),
1249 emphasis_0: PaletteColor::Rgb((118, 148, 106)),
1250 emphasis_1: PaletteColor::Rgb((118, 148, 106)),
1251 emphasis_2: PaletteColor::Rgb((118, 148, 106)),
1252 emphasis_3: PaletteColor::Rgb((118, 148, 106)),
1253 ..Default::default()
1254 },
1255 exit_code_error: StyleDeclaration {
1256 base: PaletteColor::Rgb((195, 64, 67)),
1257 emphasis_0: PaletteColor::Rgb((195, 64, 67)),
1258 emphasis_1: PaletteColor::Rgb((195, 64, 67)),
1259 emphasis_2: PaletteColor::Rgb((195, 64, 67)),
1260 emphasis_3: PaletteColor::Rgb((195, 64, 67)),
1261 ..Default::default()
1262 },
1263 ..Default::default()
1264 },
1265 },
1266 );
1267 let expected_themes = Themes::from_data(expected_themes);
1268 assert_eq!(config.themes, expected_themes, "Theme defined in config")
1269 }
1270
1271 #[test]
1272 fn omitting_required_style_errors() {
1273 let config_contents = r##"
1274 themes {
1275 named_theme {
1276 text_unselected {
1277 base "#DCD7BA"
1278 emphasis_1 "#DCD8DD"
1279 emphasis_2 "#DCD899"
1280 emphasis_3 "#ACD7CD"
1281 background "#1F1F28"
1282 }
1283 }
1284 }
1285 "##;
1286
1287 let config = Config::from_kdl(config_contents, None);
1288 assert!(config.is_err());
1289 if let Err(ConfigError::KdlError(KdlError {
1290 error_message,
1291 src: _,
1292 offset: _,
1293 len: _,
1294 help_message: _,
1295 })) = config
1296 {
1297 assert_eq!(error_message, "Missing theme color: emphasis_0")
1298 }
1299 }
1300
1301 #[test]
1302 fn partial_declaration_of_styles_defaults_omitted() {
1303 let config_contents = r##"
1304 themes {
1305 named_theme {
1306 text_unselected {
1307 base "#DCD7BA"
1308 emphasis_0 "#DCD7CD"
1309 emphasis_1 "#DCD8DD"
1310 emphasis_2 "#DCD899"
1311 emphasis_3 "#ACD7CD"
1312 background "#1F1F28"
1313 }
1314 }
1315 }
1316 "##;
1317
1318 let config = Config::from_kdl(config_contents, None).unwrap();
1319 let mut expected_themes = HashMap::new();
1320 expected_themes.insert(
1321 "named_theme".into(),
1322 Theme {
1323 sourced_from_external_file: false,
1324 palette: Styling {
1325 text_unselected: StyleDeclaration {
1326 base: PaletteColor::Rgb((220, 215, 186)),
1327 emphasis_0: PaletteColor::Rgb((220, 215, 205)),
1328 emphasis_1: PaletteColor::Rgb((220, 216, 221)),
1329 emphasis_2: PaletteColor::Rgb((220, 216, 153)),
1330 emphasis_3: PaletteColor::Rgb((172, 215, 205)),
1331 background: PaletteColor::Rgb((31, 31, 40)),
1332 },
1333 ..Default::default()
1334 },
1335 },
1336 );
1337 let expected_themes = Themes::from_data(expected_themes);
1338 assert_eq!(config.themes, expected_themes, "Theme defined in config")
1339 }
1340
1341 #[test]
1342 fn can_define_plugin_configuration_in_configfile() {
1343 let config_contents = r#"
1344 plugins {
1345 tab-bar location="zellij:tab-bar"
1346 status-bar location="zellij:status-bar"
1347 strider location="zellij:strider"
1348 compact-bar location="zellij:compact-bar"
1349 session-manager location="zellij:session-manager"
1350 welcome-screen location="zellij:session-manager" {
1351 welcome_screen true
1352 }
1353 filepicker location="zellij:strider"
1354 }
1355 "#;
1356 let config = Config::from_kdl(config_contents, None).unwrap();
1357 let mut expected_plugin_configuration = BTreeMap::new();
1358 expected_plugin_configuration.insert(
1359 "tab-bar".to_owned(),
1360 RunPlugin::from_url("zellij:tab-bar").unwrap(),
1361 );
1362 expected_plugin_configuration.insert(
1363 "status-bar".to_owned(),
1364 RunPlugin::from_url("zellij:status-bar").unwrap(),
1365 );
1366 expected_plugin_configuration.insert(
1367 "strider".to_owned(),
1368 RunPlugin::from_url("zellij:strider").unwrap(),
1369 );
1370 expected_plugin_configuration.insert(
1371 "compact-bar".to_owned(),
1372 RunPlugin::from_url("zellij:compact-bar").unwrap(),
1373 );
1374 expected_plugin_configuration.insert(
1375 "session-manager".to_owned(),
1376 RunPlugin::from_url("zellij:session-manager").unwrap(),
1377 );
1378 let mut welcome_screen_configuration = BTreeMap::new();
1379 welcome_screen_configuration.insert("welcome_screen".to_owned(), "true".to_owned());
1380 expected_plugin_configuration.insert(
1381 "welcome-screen".to_owned(),
1382 RunPlugin::from_url("zellij:session-manager")
1383 .unwrap()
1384 .with_configuration(welcome_screen_configuration),
1385 );
1386 expected_plugin_configuration.insert(
1387 "filepicker".to_owned(),
1388 RunPlugin::from_url("zellij:strider").unwrap(),
1389 );
1390 assert_eq!(
1391 config.plugins,
1392 PluginAliases::from_data(expected_plugin_configuration),
1393 "Plugins defined in config"
1394 );
1395 }
1396
1397 #[test]
1398 fn can_define_ui_configuration_in_configfile() {
1399 let config_contents = r#"
1400 ui {
1401 pane_frames {
1402 rounded_corners true
1403 hide_session_name true
1404 }
1405 }
1406 "#;
1407 let config = Config::from_kdl(config_contents, None).unwrap();
1408 let expected_ui_config = UiConfig {
1409 pane_frames: FrameConfig {
1410 rounded_corners: true,
1411 hide_session_name: true,
1412 },
1413 };
1414 assert_eq!(config.ui, expected_ui_config, "Ui config defined in config");
1415 }
1416
1417 #[test]
1418 fn can_define_env_variables_in_config_file() {
1419 let config_contents = r#"
1420 env {
1421 RUST_BACKTRACE 1
1422 SOME_OTHER_VAR "foo"
1423 }
1424 "#;
1425 let config = Config::from_kdl(config_contents, None).unwrap();
1426 let mut expected_env_config = HashMap::new();
1427 expected_env_config.insert("RUST_BACKTRACE".into(), "1".into());
1428 expected_env_config.insert("SOME_OTHER_VAR".into(), "foo".into());
1429 assert_eq!(
1430 config.env,
1431 EnvironmentVariables::from_data(expected_env_config),
1432 "Env variables defined in config"
1433 );
1434 }
1435}