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