feat: support themes directory (#1577)

* feat: add serde struct for theme file

* feat: update client to load the theme palette

* feat: merge themes in the setup phase

* chore: delete debug message in test

* feat: add theme_dir options

* fix: boxing large enum variant
This commit is contained in:
Jae-Heon Ji 2022-07-24 21:30:58 +09:00 committed by GitHub
parent eacac9fe67
commit 09481aac19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 150 additions and 10 deletions

View file

@ -265,8 +265,8 @@ pub(crate) fn start_client(opts: CliArgs) {
options,
})) = opts.command.clone()
{
let config_options = match options {
Some(SessionCommand::Options(o)) => config_options.merge_from_cli(o.into()),
let config_options = match options.as_deref() {
Some(SessionCommand::Options(o)) => config_options.merge_from_cli(o.to_owned().into()),
None => config_options,
};

View file

@ -91,7 +91,7 @@ pub enum Sessions {
/// Change the behaviour of zellij
#[clap(subcommand, name = "options")]
options: Option<SessionCommand>,
options: Option<Box<SessionCommand>>,
},
/// Kill the specific session

View file

@ -11,7 +11,7 @@ use std::convert::{TryFrom, TryInto};
use super::keybinds::{Keybinds, KeybindsFromYaml};
use super::options::Options;
use super::plugins::{PluginsConfig, PluginsConfigError, PluginsConfigFromYaml};
use super::theme::{ThemesFromYaml, UiConfigFromYaml};
use super::theme::{ThemesFromYamlIntermediate, UiConfigFromYaml};
use crate::cli::{CliArgs, Command};
use crate::envs::EnvironmentVariablesFromYaml;
use crate::setup;
@ -26,7 +26,7 @@ pub struct ConfigFromYaml {
#[serde(flatten)]
pub options: Option<Options>,
pub keybinds: Option<KeybindsFromYaml>,
pub themes: Option<ThemesFromYaml>,
pub themes: Option<ThemesFromYamlIntermediate>,
#[serde(flatten)]
pub env: Option<EnvironmentVariablesFromYaml>,
#[serde(default)]
@ -39,7 +39,7 @@ pub struct ConfigFromYaml {
pub struct Config {
pub keybinds: Keybinds,
pub options: Options,
pub themes: Option<ThemesFromYaml>,
pub themes: Option<ThemesFromYamlIntermediate>,
pub plugins: PluginsConfig,
pub ui: Option<UiConfigFromYaml>,
pub env: EnvironmentVariablesFromYaml,

View file

@ -59,6 +59,10 @@ pub struct Options {
/// subdirectory of config dir
#[clap(long, value_parser)]
pub layout_dir: Option<PathBuf>,
/// Set the theme_dir, defaults to
/// subdirectory of config dir
#[clap(long, value_parser)]
pub theme_dir: Option<PathBuf>,
#[clap(long, value_parser)]
#[serde(default)]
/// Set the handling of mouse events (true or false)
@ -139,6 +143,7 @@ impl Options {
let default_shell = other.default_shell.or_else(|| self.default_shell.clone());
let default_layout = other.default_layout.or_else(|| self.default_layout.clone());
let layout_dir = other.layout_dir.or_else(|| self.layout_dir.clone());
let theme_dir = other.theme_dir.or_else(|| self.theme_dir.clone());
let theme = other.theme.or_else(|| self.theme.clone());
let on_force_close = other.on_force_close.or(self.on_force_close);
let scroll_buffer_size = other.scroll_buffer_size.or(self.scroll_buffer_size);
@ -156,6 +161,7 @@ impl Options {
default_shell,
default_layout,
layout_dir,
theme_dir,
mouse_mode,
pane_frames,
mirror_session,
@ -192,6 +198,7 @@ impl Options {
let default_shell = other.default_shell.or_else(|| self.default_shell.clone());
let default_layout = other.default_layout.or_else(|| self.default_layout.clone());
let layout_dir = other.layout_dir.or_else(|| self.layout_dir.clone());
let theme_dir = other.theme_dir.or_else(|| self.theme_dir.clone());
let theme = other.theme.or_else(|| self.theme.clone());
let on_force_close = other.on_force_close.or(self.on_force_close);
let scroll_buffer_size = other.scroll_buffer_size.or(self.scroll_buffer_size);
@ -209,6 +216,7 @@ impl Options {
default_shell,
default_layout,
layout_dir,
theme_dir,
mouse_mode,
pane_frames,
mirror_session,
@ -262,6 +270,7 @@ impl From<CliOptions> for Options {
default_shell: opts.default_shell,
default_layout: opts.default_layout,
layout_dir: opts.layout_dir,
theme_dir: opts.theme_dir,
mouse_mode: opts.mouse_mode,
pane_frames: opts.pane_frames,
mirror_session: opts.mirror_session,

View file

@ -2,15 +2,26 @@ use serde::{
de::{Error, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::{collections::HashMap, fmt};
use super::options::Options;
use super::{config::ConfigError, options::Options};
use crate::data::{Palette, PaletteColor};
use crate::shared::detect_theme_hue;
/// Intermediate deserialization of themes
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ThemesFromYaml(HashMap<String, Theme>);
pub struct ThemesFromYamlIntermediate(HashMap<String, Theme>);
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ThemesFromYaml {
pub themes: ThemesFromYamlIntermediate,
}
type ThemesFromYamlResult = Result<ThemesFromYaml, ConfigError>;
#[derive(Debug, Default, Clone, Copy, PartialEq, Deserialize, Serialize)]
pub struct UiConfigFromYaml {
@ -23,7 +34,12 @@ pub struct FrameConfigFromYaml {
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
struct Theme {
struct ThemeFromYaml {
palette: PaletteFromYaml,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Theme {
#[serde(flatten)]
palette: PaletteFromYaml,
}
@ -125,6 +141,30 @@ impl Default for PaletteColorFromYaml {
}
impl ThemesFromYaml {
pub fn from_path(theme_path: &Path) -> ThemesFromYamlResult {
let mut theme_file = File::open(&theme_path)
.or_else(|_| File::open(&theme_path.with_extension("yaml")))
.map_err(|e| ConfigError::IoPath(e, theme_path.into()))?;
let mut theme = String::new();
theme_file.read_to_string(&mut theme)?;
let theme: ThemesFromYaml = match serde_yaml::from_str(&theme) {
Err(e) => return Err(ConfigError::Serde(e)),
Ok(theme) => theme,
};
Ok(theme)
}
}
impl From<ThemesFromYaml> for ThemesFromYamlIntermediate {
fn from(yaml: ThemesFromYaml) -> Self {
yaml.themes
}
}
impl ThemesFromYamlIntermediate {
pub fn theme_config(self, opts: &Options) -> Option<Palette> {
let mut from_yaml = self;
match &opts.theme {
@ -182,3 +222,8 @@ impl From<PaletteColorFromYaml> for PaletteColor {
}
}
}
// The unit test location.
#[cfg(test)]
#[path = "./unit/theme_test.rs"]
mod theme_test;

View file

@ -0,0 +1,16 @@
# Dracula Theme
themes:
dracula:
# From https://github.com/dracula/zellij
bg: [40, 42, 54]
red: [255, 85, 85]
green: [80, 250, 123]
yellow: [241, 250, 140]
blue: [98, 114, 164]
magenta: [255, 121, 198]
orange: [255, 184, 108]
fg: [248, 248, 242]
cyan: [139, 233, 253]
black: [0, 0, 0]
white: [255, 255, 255]

View file

@ -0,0 +1,16 @@
# Nord theme
themes:
nord:
fg: [216, 222, 233] #D8DEE9
bg: [46, 52, 64] #2E3440
black: [59, 66, 82] #3B4252
red: [191, 97, 106] #BF616A
green: [163, 190, 140] #A3BE8C
yellow: [235,203,139] #EBCB8B
blue: [129, 161, 193] #81A1C1
magenta: [180, 142, 173] #B48EAD
cyan: [136, 192, 208] #88C0D0
white: [229, 233, 240] #E5E9F0
orange: [208, 135, 112] #D08770

View file

@ -0,0 +1,22 @@
use super::super::theme::*;
use std::path::PathBuf;
fn theme_test_dir(theme: String) -> PathBuf {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let theme_dir = root.join("src/input/unit/fixtures/themes");
theme_dir.join(theme)
}
#[test]
fn dracula_theme_is_ok() {
let path = theme_test_dir("dracula.yaml".into());
let theme = ThemesFromYaml::from_path(&path);
assert!(theme.is_ok());
}
#[test]
fn no_theme_is_err() {
let path = theme_test_dir("nonexistent.yaml".into());
let theme = ThemesFromYaml::from_path(&path);
assert!(theme.is_err());
}

View file

@ -8,6 +8,7 @@ use crate::{
config::{Config, ConfigError},
layout::{LayoutFromYaml, LayoutFromYamlIntermediate},
options::Options,
theme::ThemesFromYaml,
},
};
use clap::{Args, IntoApp};
@ -80,6 +81,10 @@ pub fn get_layout_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
config_dir.map(|dir| dir.join("layouts"))
}
pub fn get_theme_dir(config_dir: Option<PathBuf>) -> Option<PathBuf> {
config_dir.map(|dir| dir.join("themes"))
}
pub fn dump_asset(asset: &[u8]) -> std::io::Result<()> {
std::io::stdout().write_all(asset)?;
Ok(())
@ -213,7 +218,7 @@ impl Setup {
);
};
let config = if !clean {
let mut config = if !clean {
match Config::try_from(opts) {
Ok(config) => config,
Err(e) => {
@ -244,6 +249,24 @@ impl Setup {
},
};
if let Some(theme_dir) = config_options
.theme_dir
.clone()
.or_else(|| get_theme_dir(opts.config_dir.clone().or_else(find_default_config_dir)))
{
if theme_dir.is_dir() {
for entry in (theme_dir.read_dir()?).flatten() {
if let Some(extension) = entry.path().extension() {
if extension == "yaml" || extension == "yml" {
if let Ok(themes) = ThemesFromYaml::from_path(&entry.path()) {
config.themes = config.themes.map(|t| t.merge(themes.into()));
}
}
}
}
}
}
if let Some(Command::Setup(ref setup)) = &opts.command {
setup
.from_cli_with_options(opts, &config_options)
@ -333,6 +356,10 @@ impl Setup {
.layout_dir
.clone()
.or_else(|| get_layout_dir(config_dir.clone()));
let theme_dir = config_options
.theme_dir
.clone()
.or_else(|| get_theme_dir(config_dir.clone()));
let system_data_dir = PathBuf::from(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij");
let config_file = opts
.config
@ -386,6 +413,11 @@ impl Setup {
} else {
message.push_str("[LAYOUT DIR]: Not Found\n");
}
if let Some(theme_dir) = theme_dir {
writeln!(&mut message, "[THEME DIR]: {:?}", theme_dir).unwrap();
} else {
message.push_str("[THEME DIR]: Not Found\n");
}
writeln!(&mut message, "[SYSTEM DATA DIR]: {:?}", system_data_dir).unwrap();
writeln!(&mut message, "[ARROW SEPARATOR]: {}", ARROW_SEPARATOR).unwrap();