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