feat(status-bar): add multiple tips (#848)

* feat(status-bar): add draft for multiple tips

* feat: add TIPS_MAP

* Simplified 'tip' function.

* chore: update file structure

* feat(status-bar): update method of Tip rendering

* feat(status-bar): change type of tip in State

* refactor(status-bar): related to random tip selection

* feat(status-bar): add simple local cache for testing

* feat(status-bar): add cache system for tip data

* Add mpadir to wasm for plugin to access zellij temp folder.

* refactor(status-bar): update cache and utils

* fix(status-bar): update file read error

* refactor(status-bar): update macros

* chore(status-bar): delete test data

* chore(status-bar): update missing fixes

* feat(status-bar): add detailed error

* style: make clippy
This commit is contained in:
Jae-Heon Ji 2021-12-10 02:53:46 +09:00 committed by GitHub
parent 2096cafe1d
commit d79060f69a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 353 additions and 203 deletions

5
Cargo.lock generated
View file

@ -2082,6 +2082,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"ansi_term 0.12.1", "ansi_term 0.12.1",
"colored", "colored",
"lazy_static",
"rand 0.8.4",
"serde",
"serde_json",
"thiserror",
"zellij-tile", "zellij-tile",
"zellij-tile-utils", "zellij-tile-utils",
] ]

View file

@ -8,5 +8,10 @@ license = "MIT"
[dependencies] [dependencies]
colored = "2" colored = "2"
ansi_term = "0.12" ansi_term = "0.12"
lazy_static = "1.4.0"
rand = "0.8.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0.30"
zellij-tile = { path = "../../zellij-tile" } zellij-tile = { path = "../../zellij-tile" }
zellij-tile-utils = { path = "../../zellij-tile-utils" } zellij-tile-utils = { path = "../../zellij-tile-utils" }

View file

@ -1,5 +1,6 @@
mod first_line; mod first_line;
mod second_line; mod second_line;
mod tip;
use ansi_term::Style; use ansi_term::Style;
@ -11,6 +12,7 @@ use first_line::{ctrl_keys, superkey};
use second_line::{ use second_line::{
fullscreen_panes_to_hide, keybinds, locked_fullscreen_panes_to_hide, text_copied_hint, fullscreen_panes_to_hide, keybinds, locked_fullscreen_panes_to_hide, text_copied_hint,
}; };
use tip::utils::get_cached_tip_name;
// for more of these, copy paste from: https://en.wikipedia.org/wiki/Box-drawing_character // for more of these, copy paste from: https://en.wikipedia.org/wiki/Box-drawing_character
static ARROW_SEPARATOR: &str = ""; static ARROW_SEPARATOR: &str = "";
@ -19,6 +21,7 @@ static MORE_MSG: &str = " ... ";
#[derive(Default)] #[derive(Default)]
struct State { struct State {
tabs: Vec<TabInfo>, tabs: Vec<TabInfo>,
tip_name: String,
mode_info: ModeInfo, mode_info: ModeInfo,
diplay_text_copied_hint: bool, diplay_text_copied_hint: bool,
} }
@ -131,6 +134,8 @@ fn color_elements(palette: Palette) -> ColoredElements {
impl ZellijPlugin for State { impl ZellijPlugin for State {
fn load(&mut self) { fn load(&mut self) {
// TODO: Should be able to choose whether to use the cache through config.
self.tip_name = get_cached_tip_name();
set_selectable(false); set_selectable(false);
subscribe(&[ subscribe(&[
EventType::ModeUpdate, EventType::ModeUpdate,
@ -190,7 +195,7 @@ impl ZellijPlugin for State {
second_line = if self.diplay_text_copied_hint { second_line = if self.diplay_text_copied_hint {
text_copied_hint(&self.mode_info.palette) text_copied_hint(&self.mode_info.palette)
} else { } else {
keybinds(&self.mode_info, cols) keybinds(&self.mode_info, &self.tip_name, cols)
} }
} }
} }
@ -208,7 +213,7 @@ impl ZellijPlugin for State {
second_line = if self.diplay_text_copied_hint { second_line = if self.diplay_text_copied_hint {
text_copied_hint(&self.mode_info.palette) text_copied_hint(&self.mode_info.palette)
} else { } else {
keybinds(&self.mode_info, cols) keybinds(&self.mode_info, &self.tip_name, cols)
} }
} }
} }
@ -216,7 +221,7 @@ impl ZellijPlugin for State {
second_line = if self.diplay_text_copied_hint { second_line = if self.diplay_text_copied_hint {
text_copied_hint(&self.mode_info.palette) text_copied_hint(&self.mode_info.palette)
} else { } else {
keybinds(&self.mode_info, cols) keybinds(&self.mode_info, &self.tip_name, cols)
} }
} }
} }

View file

@ -5,7 +5,10 @@ use ansi_term::{
}; };
use zellij_tile::prelude::*; use zellij_tile::prelude::*;
use crate::{LinePart, MORE_MSG}; use crate::{
tip::{data::TIPS, TipFn},
LinePart, MORE_MSG,
};
fn full_length_shortcut( fn full_length_shortcut(
is_first_shortcut: bool, is_first_shortcut: bool,
@ -82,194 +85,6 @@ fn first_word_shortcut(
len, len,
} }
} }
fn quicknav_full(palette: Palette) -> LinePart {
let text_first_part = " Tip: ";
let alt = "Alt";
let text_second_part = " + ";
let new_pane_shortcut = "<n>";
let text_third_part = " => open new pane. ";
let second_alt = "Alt";
let text_fourth_part = " + ";
let brackets_navigation = "<[]";
let text_fifth_part = " or ";
let hjkl_navigation = "hjkl>";
let text_sixths_part = " => navigate between panes. ";
let third_alt = "Alt";
let text_seventh_parth = " + ";
let increase_decrease_parth = "<+->";
let text_eighth_parth = " => increase/decrease pane size.";
let len = text_first_part.chars().count()
+ alt.chars().count()
+ text_second_part.chars().count()
+ new_pane_shortcut.chars().count()
+ text_third_part.chars().count()
+ second_alt.chars().count()
+ text_fourth_part.chars().count()
+ brackets_navigation.chars().count()
+ text_fifth_part.chars().count()
+ hjkl_navigation.chars().count()
+ text_sixths_part.chars().count()
+ third_alt.chars().count()
+ text_seventh_parth.chars().count()
+ increase_decrease_parth.chars().count()
+ text_eighth_parth.chars().count();
let green_color = match palette.green {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
let orange_color = match palette.orange {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
LinePart {
part: format!(
"{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}",
text_first_part,
Style::new().fg(orange_color).bold().paint(alt),
text_second_part,
Style::new().fg(green_color).bold().paint(new_pane_shortcut),
text_third_part,
Style::new().fg(orange_color).bold().paint(second_alt),
text_fourth_part,
Style::new()
.fg(green_color)
.bold()
.paint(brackets_navigation),
text_fifth_part,
Style::new().fg(green_color).bold().paint(hjkl_navigation),
text_sixths_part,
Style::new().fg(orange_color).bold().paint(third_alt),
text_seventh_parth,
Style::new()
.fg(green_color)
.bold()
.paint(increase_decrease_parth),
text_eighth_parth,
),
len,
}
}
fn quicknav_medium(palette: Palette) -> LinePart {
let text_first_part = " Tip: ";
let alt = "Alt";
let text_second_part = " + ";
let new_pane_shortcut = "<n>";
let text_third_part = " => new pane. ";
let second_alt = "Alt";
let text_fourth_part = " + ";
let brackets_navigation = "<[]";
let text_fifth_part = " or ";
let hjkl_navigation = "hjkl>";
let text_sixths_part = " => navigate. ";
let third_alt = "Alt";
let text_seventh_parth = " + ";
let increase_decrease_parth = "<+->";
let text_eighth_parth = " => resize pane. ";
let len = text_first_part.chars().count()
+ alt.chars().count()
+ text_second_part.chars().count()
+ new_pane_shortcut.chars().count()
+ text_third_part.chars().count()
+ second_alt.chars().count()
+ text_fourth_part.chars().count()
+ brackets_navigation.chars().count()
+ text_fifth_part.chars().count()
+ hjkl_navigation.chars().count()
+ text_sixths_part.chars().count()
+ third_alt.chars().count()
+ text_seventh_parth.chars().count()
+ increase_decrease_parth.chars().count()
+ text_eighth_parth.chars().count();
let green_color = match palette.green {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
let orange_color = match palette.orange {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
LinePart {
part: format!(
"{}{}{}{}{}{}{}{}{}{}{}{}{}{}{}",
text_first_part,
Style::new().fg(orange_color).bold().paint(alt),
text_second_part,
Style::new().fg(green_color).bold().paint(new_pane_shortcut),
text_third_part,
Style::new().fg(orange_color).bold().paint(second_alt),
text_fourth_part,
Style::new()
.fg(green_color)
.bold()
.paint(brackets_navigation),
text_fifth_part,
Style::new().fg(green_color).bold().paint(hjkl_navigation),
text_sixths_part,
Style::new().fg(orange_color).bold().paint(third_alt),
text_seventh_parth,
Style::new()
.fg(green_color)
.bold()
.paint(increase_decrease_parth),
text_eighth_parth,
),
len,
}
}
fn quicknav_short(palette: Palette) -> LinePart {
let text_first_part = " QuickNav: ";
let alt = "Alt";
let text_second_part = " + ";
let new_pane_shortcut = "n";
let text_third_part = "/";
let brackets_navigation = "[]";
let text_fifth_part = "/";
let hjkl_navigation = "hjkl";
let text_sixth_part = "/";
let increase_decrease_part = "+-";
let len = text_first_part.chars().count()
+ alt.chars().count()
+ text_second_part.chars().count()
+ new_pane_shortcut.chars().count()
+ text_third_part.chars().count()
+ brackets_navigation.chars().count()
+ text_fifth_part.chars().count()
+ hjkl_navigation.chars().count()
+ text_sixth_part.chars().count()
+ increase_decrease_part.chars().count();
let green_color = match palette.green {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
let orange_color = match palette.orange {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
LinePart {
part: format!(
"{}{}{}{}{}{}{}{}{}{}",
text_first_part,
Style::new().fg(orange_color).bold().paint(alt),
text_second_part,
Style::new().fg(green_color).bold().paint(new_pane_shortcut),
text_third_part,
Style::new()
.fg(green_color)
.bold()
.paint(brackets_navigation),
text_fifth_part,
Style::new().fg(green_color).bold().paint(hjkl_navigation),
text_sixth_part,
Style::new()
.fg(green_color)
.bold()
.paint(increase_decrease_part),
),
len,
}
}
fn locked_interface_indication(palette: Palette) -> LinePart { fn locked_interface_indication(palette: Palette) -> LinePart {
let locked_text = " -- INTERFACE LOCKED -- "; let locked_text = " -- INTERFACE LOCKED -- ";
@ -318,9 +133,9 @@ fn select_pane_shortcut(is_first_shortcut: bool, palette: Palette) -> LinePart {
} }
} }
fn full_shortcut_list(help: &ModeInfo) -> LinePart { fn full_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart {
match help.mode { match help.mode {
InputMode::Normal => quicknav_full(help.palette), InputMode::Normal => tip(help.palette),
InputMode::Locked => locked_interface_indication(help.palette), InputMode::Locked => locked_interface_indication(help.palette),
_ => { _ => {
let mut line_part = LinePart::default(); let mut line_part = LinePart::default();
@ -337,9 +152,9 @@ fn full_shortcut_list(help: &ModeInfo) -> LinePart {
} }
} }
fn shortened_shortcut_list(help: &ModeInfo) -> LinePart { fn shortened_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart {
match help.mode { match help.mode {
InputMode::Normal => quicknav_medium(help.palette), InputMode::Normal => tip(help.palette),
InputMode::Locked => locked_interface_indication(help.palette), InputMode::Locked => locked_interface_indication(help.palette),
_ => { _ => {
let mut line_part = LinePart::default(); let mut line_part = LinePart::default();
@ -356,10 +171,10 @@ fn shortened_shortcut_list(help: &ModeInfo) -> LinePart {
} }
} }
fn best_effort_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart { fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> LinePart {
match help.mode { match help.mode {
InputMode::Normal => { InputMode::Normal => {
let line_part = quicknav_short(help.palette); let line_part = tip(help.palette);
if line_part.len <= max_len { if line_part.len <= max_len {
line_part line_part
} else { } else {
@ -397,16 +212,19 @@ fn best_effort_shortcut_list(help: &ModeInfo, max_len: usize) -> LinePart {
} }
} }
pub fn keybinds(help: &ModeInfo, max_width: usize) -> LinePart { pub fn keybinds(help: &ModeInfo, tip_name: &str, max_width: usize) -> LinePart {
let full_shortcut_list = full_shortcut_list(help); // It is assumed that there is at least one TIP data in the TIPS HasMap.
let tip_body = TIPS.get(tip_name).unwrap();
let full_shortcut_list = full_shortcut_list(help, tip_body.full);
if full_shortcut_list.len <= max_width { if full_shortcut_list.len <= max_width {
return full_shortcut_list; return full_shortcut_list;
} }
let shortened_shortcut_list = shortened_shortcut_list(help); let shortened_shortcut_list = shortened_shortcut_list(help, tip_body.medium);
if shortened_shortcut_list.len <= max_width { if shortened_shortcut_list.len <= max_width {
return shortened_shortcut_list; return shortened_shortcut_list;
} }
best_effort_shortcut_list(help, max_width) best_effort_shortcut_list(help, tip_body.short, max_width)
} }
pub fn text_copied_hint(palette: &Palette) -> LinePart { pub fn text_copied_hint(palette: &Palette) -> LinePart {

View file

@ -0,0 +1,119 @@
use std::collections::{HashMap, HashSet};
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zellij_tile::prelude::get_zellij_version;
#[derive(Debug, Serialize, Deserialize)]
pub struct Metadata {
zellij_version: String,
cached_data: HashMap<String, usize>,
}
#[derive(Debug)]
pub struct LocalCache {
path: PathBuf,
metadata: Metadata,
}
pub type LocalCacheResult = Result<LocalCache, LocalCacheError>;
#[derive(Error, Debug)]
pub enum LocalCacheError {
// Io error
#[error("IoError: {0}")]
Io(#[from] io::Error),
// Io error with path context
#[error("IoError: {0}, File: {1}")]
IoPath(io::Error, PathBuf),
// Deserialization error
#[error("Deserialization error: {0}")]
Serde(#[from] serde_json::Error),
}
impl LocalCache {
fn from_json(json_cache: &str) -> Result<Metadata, LocalCacheError> {
match serde_json::from_str::<Metadata>(json_cache) {
Ok(metadata) => Ok(metadata),
Err(err) => {
if json_cache.is_empty() {
return Ok(Metadata {
zellij_version: get_zellij_version(),
cached_data: HashMap::new(),
});
}
Err(LocalCacheError::Serde(err))
}
}
}
pub fn new(path: PathBuf) -> LocalCacheResult {
match OpenOptions::new()
.read(true)
.create(true)
.open(path.as_path())
{
Ok(mut file) => {
let mut json_cache = String::new();
file.read_to_string(&mut json_cache)
.map_err(LocalCacheError::Io)?;
let metadata = LocalCache::from_json(&json_cache)?;
Ok(LocalCache { path, metadata })
}
Err(e) => Err(LocalCacheError::IoPath(e, path)),
}
}
pub fn flush(&mut self) -> Result<(), LocalCacheError> {
match serde_json::to_string(&self.metadata) {
Ok(json_cache) => {
let mut file = File::create(self.path.as_path())
.map_err(|e| LocalCacheError::IoPath(e, self.path.clone()))?;
file.write_all(json_cache.as_bytes())
.map_err(LocalCacheError::Io)?;
Ok(())
}
Err(e) => Err(LocalCacheError::Serde(e)),
}
}
pub fn clear(&mut self) -> Result<(), LocalCacheError> {
self.metadata.cached_data.clear();
self.flush()
}
pub fn get_version(&self) -> &String {
&self.metadata.zellij_version
}
pub fn set_version<S: Into<String>>(&mut self, version: S) {
self.metadata.zellij_version = version.into();
}
pub fn is_empty(&self) -> bool {
self.metadata.cached_data.is_empty()
}
pub fn get_cached_data(&self) -> &HashMap<String, usize> {
&self.metadata.cached_data
}
pub fn get_cached_data_set(&self) -> HashSet<String> {
self.get_cached_data().keys().cloned().collect()
}
pub fn caching<S: Into<String>>(&mut self, key: S) -> Result<(), LocalCacheError> {
let key = key.into();
if let Some(item) = self.metadata.cached_data.get_mut(&key) {
*item += 1;
} else {
self.metadata.cached_data.insert(key, 1);
}
self.flush()
}
}

View file

@ -0,0 +1,2 @@
pub const DEFAULT_CACHE_FILE_PATH: &str = "/tmp/status-bar-tips.cache";
pub const MAX_CACHE_HITS: usize = 10;

View file

@ -0,0 +1,100 @@
use std::collections::HashMap;
use ansi_term::{
unstyled_len, ANSIString, ANSIStrings,
Color::{Fixed, RGB},
Style,
};
use lazy_static::lazy_static;
use crate::{tip::TipBody, LinePart};
use zellij_tile::prelude::*;
use zellij_tile_utils::palette_match;
macro_rules! strings {
($ANSIStrings:expr) => {{
let strings: &[ANSIString<'static>] = $ANSIStrings;
let ansi_strings = ANSIStrings(strings);
LinePart {
part: format!("{}", ansi_strings),
len: unstyled_len(&ansi_strings),
}
}};
}
lazy_static! {
pub static ref TIPS: HashMap<&'static str, TipBody> = HashMap::from([(
"quicknav",
TipBody {
short: quicknav_short,
medium: quicknav_medium,
full: quicknav_full,
}
)]);
}
fn quicknav_full(palette: Palette) -> LinePart {
let green_color = palette_match!(palette.green);
let orange_color = palette_match!(palette.orange);
strings!(&[
Style::new().paint(" Tip: "),
Style::new().fg(orange_color).bold().paint("Alt"),
Style::new().paint(" + "),
Style::new().fg(green_color).bold().paint("<n>"),
Style::new().paint(" => open new pane. "),
Style::new().fg(orange_color).bold().paint("Alt"),
Style::new().paint(" + "),
Style::new().fg(green_color).bold().paint("<[]"),
Style::new().paint(" or "),
Style::new().fg(green_color).bold().paint("hjkl>"),
Style::new().paint(" => navigate between panes. "),
Style::new().fg(orange_color).bold().paint("Alt"),
Style::new().paint(" + "),
Style::new().fg(green_color).bold().paint("<+->"),
Style::new().paint(" => increase/decrease pane size."),
])
}
fn quicknav_medium(palette: Palette) -> LinePart {
let green_color = palette_match!(palette.green);
let orange_color = palette_match!(palette.orange);
strings!(&[
Style::new().paint(" Tip: "),
Style::new().fg(orange_color).bold().paint("Alt"),
Style::new().paint(" + "),
Style::new().fg(green_color).bold().paint("<n>"),
Style::new().paint(" => new pane. "),
Style::new().fg(orange_color).bold().paint("Alt"),
Style::new().paint(" + "),
Style::new().fg(green_color).bold().paint("<[]"),
Style::new().paint(" or "),
Style::new().fg(green_color).bold().paint("hjkl>"),
Style::new().paint(" => navigate. "),
Style::new().fg(orange_color).bold().paint("Alt"),
Style::new().paint(" + "),
Style::new().fg(green_color).bold().paint("<+->"),
Style::new().paint(" => resize pane."),
])
}
fn quicknav_short(palette: Palette) -> LinePart {
let green_color = palette_match!(palette.green);
let orange_color = palette_match!(palette.orange);
strings!(&[
Style::new().paint(" QuickNav: "),
Style::new().fg(orange_color).bold().paint("Alt"),
Style::new().paint(" + "),
Style::new().fg(green_color).bold().paint("n"),
Style::new().paint("/"),
Style::new().fg(green_color).bold().paint("[]"),
Style::new().paint("/"),
Style::new().fg(green_color).bold().paint("hjkl"),
Style::new().paint("/"),
Style::new().fg(green_color).bold().paint("+-"),
])
}

View file

@ -0,0 +1,16 @@
pub mod cache;
pub mod consts;
pub mod data;
pub mod utils;
use crate::LinePart;
use zellij_tile::prelude::*;
pub type TipFn = fn(Palette) -> LinePart;
#[derive(Debug)]
pub struct TipBody {
pub short: TipFn,
pub medium: TipFn,
pub full: TipFn,
}

View file

@ -0,0 +1,68 @@
use std::path::PathBuf;
use rand::prelude::{IteratorRandom, SliceRandom};
use zellij_tile::prelude::get_zellij_version;
use super::cache::LocalCache;
use super::consts::{DEFAULT_CACHE_FILE_PATH, MAX_CACHE_HITS};
use super::data::TIPS;
macro_rules! get_name_and_caching {
($cache:expr) => {{
let name = get_random_tip_name();
$cache.caching(name.clone()).unwrap();
return name;
}};
($cache:expr, $from:expr) => {{
let name = $from.choose(&mut rand::thread_rng()).unwrap().to_string();
$cache.caching(name.clone()).unwrap();
return name;
}};
}
pub fn get_random_tip_name() -> String {
TIPS.keys()
.choose(&mut rand::thread_rng())
.unwrap()
.to_string()
}
pub fn get_cached_tip_name() -> String {
let mut local_cache = LocalCache::new(PathBuf::from(DEFAULT_CACHE_FILE_PATH)).unwrap();
let zellij_version = get_zellij_version();
if zellij_version.ne(local_cache.get_version()) {
local_cache.set_version(zellij_version);
local_cache.clear().unwrap();
}
if local_cache.is_empty() {
get_name_and_caching!(local_cache);
}
let usable_tips = local_cache
.get_cached_data()
.iter()
.filter(|(_, &v)| v < MAX_CACHE_HITS)
.map(|(k, _)| k.to_string())
.collect::<Vec<String>>();
if usable_tips.is_empty() {
let cached_set = local_cache.get_cached_data_set();
let diff = TIPS
.keys()
.cloned()
.filter(|k| !cached_set.contains(&k.to_string()))
.collect::<Vec<&str>>();
if !diff.is_empty() {
get_name_and_caching!(local_cache, diff);
} else {
local_cache.clear().unwrap();
get_name_and_caching!(local_cache);
}
} else {
get_name_and_caching!(local_cache, usable_tips);
}
}

View file

@ -29,7 +29,7 @@ use crate::{
}; };
use zellij_utils::{ use zellij_utils::{
consts::{VERSION, ZELLIJ_PROJ_DIR}, consts::{VERSION, ZELLIJ_PROJ_DIR, ZELLIJ_TMP_DIR},
errors::{ContextType, PluginContext}, errors::{ContextType, PluginContext},
}; };
use zellij_utils::{ use zellij_utils::{
@ -272,6 +272,8 @@ fn start_plugin(
.unwrap() .unwrap()
.map_dir("/data", &plugin_own_data_dir) .map_dir("/data", &plugin_own_data_dir)
.unwrap() .unwrap()
.map_dir("/tmp", ZELLIJ_TMP_DIR.as_path())
.unwrap()
.stdin(Box::new(input)) .stdin(Box::new(input))
.stdout(Box::new(output)) .stdout(Box::new(output))
.stderr(Box::new(stderr)) .stderr(Box::new(stderr))

View file

@ -5,6 +5,16 @@ macro_rules! rgb {
}; };
} }
#[macro_export]
macro_rules! palette_match {
($palette_color:expr) => {
match $palette_color {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
}
};
}
#[macro_export] #[macro_export]
macro_rules! style { macro_rules! style {
($fg:expr, $bg:expr) => { ($fg:expr, $bg:expr) => {