From 8ebeb8dec9c30e59d0eec019969f1d43167daab5 Mon Sep 17 00:00:00 2001 From: chris west Date: Sun, 13 Nov 2022 21:43:27 -0800 Subject: [PATCH] add color themes --- CHANGELOG.md | 44 +++++++++- README.md | 2 + doc/phetch.1 | 90 ++++++++++++++++++- doc/phetch.1.md | 75 ++++++++++++++++ phetch.conf | 20 +++++ src/args.rs | 22 ++++- src/color.rs | 121 ------------------------- src/config.rs | 55 +++++++++++- src/help.rs | 77 +++++++++++++++- src/lib.rs | 2 +- src/main.rs | 17 +++- src/menu.rs | 45 +++++----- src/theme.rs | 228 ++++++++++++++++++++++++++++++++++++++++++++++++ src/ui.rs | 25 ++++-- src/ui/mode.rs | 3 + 15 files changed, 660 insertions(+), 166 deletions(-) create mode 100644 phetch.conf delete mode 100644 src/color.rs create mode 100644 src/theme.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index df3ea1b..f87bcfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,43 @@ ## v1.2.0 (dev) -This release adds a few new config options, for your convenience: +phetch is all about fun colors, but your options are limited. You +can turn off colors with the `NO_COLOR` env variable or you can +leave them on. That's it. + +Well, not anymore. As of `v1.2`, phetch now supports themes. + +### Themes + +Themes are simple files with the same format as `phetch.conf`: + + $ cat ~/.config/phetch/default.theme + # Color Scheme + ## UI + ui.cursor white bold + ui.number magenta + ui.menu yellow + ui.text white + + ## Items + item.text cyan + item.menu blue + item.error red + item.search white + item.telnet grey + item.external green + item.download white underline + item.media green underline + item.unsupported whitebg red + +Create your theme file and launch phetch with `-t FILE`, or set +the `theme FILE` option in your `~/.config/phetch/phetch.conf` + +You can see available colors and learn more about themes by opening +phetch's help - press `h` then `7` to get there quickly. + +### Config Options + +This release also adds a few new config options, for your convenience: - `scroll` controls how many lines to jump by when paging up/down. If set to 0 (the new default), you'll jump by an entire screen. @@ -8,9 +45,10 @@ This release adds a few new config options, for your convenience: or not. By default it's false, but one might find it handy to set to `true` if hosting, say, a Gopher-powered music server. -And a bonus: +### Keyboard Shortcuts -- `R` keyboard shortcut to refresh the current page. +Last but not least, you can now reload the current URL by pressing `R`. +Handy when developing your own Gopherhole! ## v1.1.0 diff --git a/README.md b/README.md index 5d4912c..f2ed8c4 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ the gophersphere. -c, --config FILE Use instead of ~/.config/phetch/phetch.conf -C, --no-config Don't use any config file + -t, --theme FILE Use FILE for color theme or print current theme. + --print-theme Print current theme. -h, --help Show this screen -v, --version Show phetch version diff --git a/doc/phetch.1 b/doc/phetch.1 index 930918e..514f7c7 100644 --- a/doc/phetch.1 +++ b/doc/phetch.1 @@ -5,7 +5,7 @@ .nh .ad l .\" Begin generated content: -.TH "PHETCH" "1" "2022-11-13" +.TH "PHETCH" "1" "2022-11-14" .P .SH NAME .P @@ -110,6 +110,16 @@ Use \fIFILE\fR instead of \fI~/.\&config/phetch/phetch.\&conf\fR Do not use any config file.\& .P .RE +\fB-t\fR, \fB--theme\fR \fIFILE\fR +.RS 4 +Use \fIFILE\fR for color theme.\& +.P +.RE +\fB--print-theme\fR +.RS 4 +Print current theme.\& +.P +.RE \fB-e\fR, \fB--encoding\fR \fIENCODING\fR .RS 4 Render text views in CP437 or UTF8 (default) encoding.\& @@ -335,6 +345,80 @@ encoding utf8 # Wrap text at N columns\&. 0 = off (--wrap) wrap 0 + +# How many lines to page up/down by? 0 = full screen +scroll 0 + +# Path to theme file, if you want to use one +theme ~/\&.config/phetch/dark\&.theme +.fi +.RE +.P +.SH THEMES +.P +You can change phetch'\&s color scheme by supplying your own theme +file with `--theme`/`-t` or by setting `theme FILE` in your +phetch.\&conf.\& +.P +You can also view the current theme with: +.P +.RS 4 +$ phetch --print-theme +.P +.RE +Theme files look like this: +.P +.nf +.RS 4 +ui\&.cursor white bold +ui\&.number magenta +ui\&.menu yellow +ui\&.text white +item\&.text cyan +item\&.menu blue +item\&.error red +item\&.search white +item\&.telnet grey +item\&.external green +item\&.download white underline +item\&.media green underline +item\&.unsupported whitebg red +.fi +.RE +.P +Valid colors for use in phetch themes: +.P +.nf +.RS 4 +bold +underline + +grey +red +green +yellow +blue +magenta +cyan +white +black + +darkred +darkgreen +darkyellow +darkblue +darkmagenta +darkcyan +darkwhite + +blackbg +redbg +greenbg +yellowbg +bluebg +magentabg +cyanbg +whitebg .fi .RE .P @@ -346,6 +430,10 @@ using the `-m` command line flag.\& To test it out, visit a compatible Gopher server or check out the "gopher types" help page by lauching \fBphetch\fR and then pressing `ctrl-h` then `3`.\& .P +By default \fBphetch\fR will prompt you when you try to open a media file, +but you can change this behavior by starting it with `--autoplay`/`-a` +or by setting `autoplayer true` in your config file.\& +.P .SH ABOUT .P \fBphetch\fR is maintained by chris west, and released under the MIT license.\& diff --git a/doc/phetch.1.md b/doc/phetch.1.md index 6c6adee..9fffc79 100644 --- a/doc/phetch.1.md +++ b/doc/phetch.1.md @@ -73,6 +73,12 @@ If no URL is given, however, *phetch* will launch and open its default *-C*, *--no-config* Do not use any config file. +*-t*, *--theme* _FILE_ + Use _FILE_ for color theme. + +*--print-theme* + Print current theme. + *-e*, *--encoding* _ENCODING_ Render text views in CP437 or UTF8 (default) encoding. @@ -238,6 +244,71 @@ wrap 0 # How many lines to page up/down by? 0 = full screen scroll 0 + +# Path to theme file, if you want to use one +theme ~/.config/phetch/dark.theme +``` + +# THEMES + +You can change phetch's color scheme by supplying your own theme +file with `--theme`/`-t` or by setting `theme FILE` in your +phetch.conf. + +You can also view the current theme with: + + $ phetch --print-theme + +Theme files look like this: + +``` +ui.cursor white bold +ui.number magenta +ui.menu yellow +ui.text white +item.text cyan +item.menu blue +item.error red +item.search white +item.telnet grey +item.external green +item.download white underline +item.media green underline +item.unsupported whitebg red +``` + +Valid colors for use in phetch themes: + +``` +bold +underline + +grey +red +green +yellow +blue +magenta +cyan +white +black + +darkred +darkgreen +darkyellow +darkblue +darkmagenta +darkcyan +darkwhite + +blackbg +redbg +greenbg +yellowbg +bluebg +magentabg +cyanbg +whitebg ``` # MEDIA PLAYER SUPPORT @@ -248,6 +319,10 @@ using the `-m` command line flag. To test it out, visit a compatible Gopher server or check out the "gopher types" help page by lauching *phetch* and then pressing `ctrl-h` then `3`. +By default *phetch* will prompt you when you try to open a media file, +but you can change this behavior by starting it with `--autoplay`/`-a` +or by setting `autoplayer true` in your config file. + # ABOUT *phetch* is maintained by chris west, and released under the MIT license. diff --git a/phetch.conf b/phetch.conf new file mode 100644 index 0000000..ed45d23 --- /dev/null +++ b/phetch.conf @@ -0,0 +1,20 @@ +tls true +emoji true + +# Color Scheme +## UI +ui.cursor white bold +ui.number magenta +ui.menu yellow +ui.text white + +## Items +item.text cyan +item.menu blue +item.error red +item.search white +item.telnet grey +item.external green +item.download white underline +item.media green underline +item.unsupported whitebg red diff --git a/src/args.rs b/src/args.rs index 0bed262..a39830e 100644 --- a/src/args.rs +++ b/src/args.rs @@ -131,6 +131,18 @@ pub fn parse>(args: &[T]) -> Result { iter.next(); // skip arg } arg if arg.starts_with("--config=") || arg.starts_with("-config=") => {} + "-t" | "--theme" | "-theme" => { + if let Some(arg) = iter.next() { + cfg.theme = config::load_file(arg.as_ref()) + .map_err(|e| ArgError::new(format!("error loading theme: {}", e)))? + .theme; + } else { + return Err(ArgError::new("need a theme file")); + } + } + "--print-theme" => { + cfg.mode = Mode::PrintTheme; + } "-s" | "--tls" | "-tls" => { if set_notls { return Err(ArgError::new("can't set both --tls and --no-tls")); @@ -196,17 +208,17 @@ pub fn parse>(args: &[T]) -> Result { } "-a" | "--autoplay" | "-autoplay" => { if set_nomedia { - return Err(ArgError::new("can't set both --no-media and --autoplay")) + return Err(ArgError::new("can't set both --no-media and --autoplay")); } if set_noautoplay { - return Err(ArgError::new("can't set both --autoplay and --no-autoplay")) + return Err(ArgError::new("can't set both --autoplay and --no-autoplay")); } set_autoplay = true; cfg.autoplay = true; } "-A" | "--no-autoplay" | "-no-autoplay" => { if set_autoplay { - return Err(ArgError::new("can't set both --autoplay and --no-autoplay")) + return Err(ArgError::new("can't set both --autoplay and --no-autoplay")); } cfg.autoplay = false; set_noautoplay = true; @@ -238,7 +250,9 @@ pub fn parse>(args: &[T]) -> Result { #[cfg(not(test))] { - if !atty::is(atty::Stream::Stdout) && cfg.mode != Mode::Raw { + if !atty::is(atty::Stream::Stdout) + && !matches!(cfg.mode, Mode::Raw | Mode::Print | Mode::PrintTheme) + { cfg.mode = Mode::NoTTY; } } diff --git a/src/color.rs b/src/color.rs deleted file mode 100644 index ffd795f..0000000 --- a/src/color.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Terminal colors. -//! Provides a macro to color text as well as sturcts to get their -//! raw ansi codes. - -use std::fmt; - -/// Shortcut to produce a String colored with one or more colors. -/// Example: -/// ```ignore -/// let s = color_string!("Red string", Red); -/// let x = color_string!("Hyperlink-ish", Blue, Underline); -macro_rules! color_string { - ($s:expr, $( $color:ident ),+) => {{ - if *crate::NO_COLOR { - $s.to_string() - } else { - let mut out = String::from("\x1b["); - $( out.push_str(crate::color::$color::code()); out.push_str(";"); )+ - out.push('m'); - out.push_str(&$s); - out.push_str(crate::color::Reset.as_ref()); - out.replace(";m", "m") - } - }}; -} - -/// Shortcut to produce a color's ANSI escape code. Don't forget to Reset! -/// ```ignore -/// let mut o = String::new(); -/// o.push_str(color!(Blue)); -/// o.push_str(color!(Underline)); -/// o.push_str("Hyperlinkish."); -/// o.push_str(color!(Reset)); -macro_rules! color { - ($color:ident) => { - if *crate::NO_COLOR { - "" - } else { - crate::color::$color.as_ref() - } - }; -} - -/// Create a color:: struct that can be used with format!. -/// Example: -/// ```ignore -/// define_color(Red, 91); -/// define_color(Reset, 0); -/// -/// println!("{}Error: {}{}", color::Red, msg, color::Reset); -macro_rules! define_color { - ($color:ident, $code:literal) => { - #[allow(missing_docs)] - pub struct $color; - impl fmt::Display for $color { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_ref()) - } - } - impl $color { - #[allow(missing_docs)] - #[inline] - pub fn code() -> &'static str { - concat!($code) - } - } - impl AsRef for $color { - #[inline] - fn as_ref(&self) -> &'static str { - concat!("\x1b[", $code, "m") - } - } - }; -} - -define_color!(Reset, 0); -define_color!(Bold, 1); -define_color!(Underline, 4); - -define_color!(Grey, 90); -define_color!(Red, 91); -define_color!(Green, 92); -define_color!(Yellow, 93); -define_color!(Blue, 94); -define_color!(Magenta, 95); -define_color!(Cyan, 96); -define_color!(White, 97); - -define_color!(Black, 30); -define_color!(DarkRed, 31); -define_color!(DarkGreen, 32); -define_color!(DarkYellow, 33); -define_color!(DarkBlue, 34); -define_color!(DarkMagenta, 35); -define_color!(DarkCyan, 36); -define_color!(DarkWhite, 37); - -define_color!(BlackBG, 40); -define_color!(RedBG, 41); -define_color!(GreenBG, 42); -define_color!(YellowBG, 43); -define_color!(BlueBG, 44); -define_color!(MagentaBG, 45); -define_color!(CyanBG, 46); -define_color!(WhiteBG, 47); - -#[cfg(test)] -mod tests { - #[test] - fn test_colors() { - assert_eq!(color_string!("Error", Red), "\x1b[91mError\x1b[0m"); - assert_eq!( - color_string!("Fancy Pants", Blue, Underline), - "\x1b[94;4mFancy Pants\x1b[0m" - ); - assert_eq!( - color_string!("Super-duper-fancy-pants", Magenta, Underline, Bold, BlueBG), - "\x1b[95;4;1;44mSuper-duper-fancy-pants\x1b[0m" - ) - } -} diff --git a/src/config.rs b/src/config.rs index b278485..e0867f9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,12 @@ //! An example default config is provided but unused by this module. use { - crate::{encoding::Encoding, phetchdir, ui}, + crate::{ + encoding::Encoding, + phetchdir, + theme::{to_color, Theme}, + ui, + }, std::{ collections::HashMap, fs::OpenOptions, @@ -58,6 +63,24 @@ wrap 0 # How many lines to page up/down by? 0 = full screen scroll 0 + +# Path to theme file, if any +theme ~/.config/phetch/pink.theme + +# Inline Theme +ui.cursor white bold +ui.number magenta +ui.menu yellow +ui.text white +item.text cyan +item.menu blue +item.error red +item.search white +item.telnet grey +item.external green +item.download white underline +item.media green underline +item.unsupported whitebg red "; /// Not all the config options are available in the phetch.conf. We @@ -87,6 +110,8 @@ pub struct Config { pub wrap: usize, /// Scroll by how many lines? 0 = full screen pub scroll: usize, + /// Color Scheme + pub theme: Theme, } impl Default for Config { @@ -103,6 +128,7 @@ impl Default for Config { mode: ui::Mode::default(), wrap: 0, scroll: 0, + theme: Theme::default(), } } } @@ -200,6 +226,33 @@ fn parse(text: &str) -> Result { cfg.encoding = Encoding::from_str(val) .map_err(|e| error!("{} on line {}: {:?}", e, linenum, line))?; } + + "theme" => { + let homevar = std::env::var("HOME"); + if homevar.is_err() && val.contains('~') { + return Err(error!("$HOME not set, can't decode `~`")); + } + cfg.theme = load_file(&val.replace('~', &homevar.unwrap())) + .map_err(|e| error!("error loading theme: {}", e))? + .theme + } + + // color scheme + "ui.cursor" => cfg.theme.ui_cursor = to_color(val), + "ui.number" => cfg.theme.ui_number = to_color(val), + "ui.menu" => cfg.theme.ui_menu = to_color(val), + "ui.text" => cfg.theme.ui_text = to_color(val), + + "item.text" => cfg.theme.item_text = to_color(val), + "item.menu" => cfg.theme.item_menu = to_color(val), + "item.error" => cfg.theme.item_error = to_color(val), + "item.search" => cfg.theme.item_search = to_color(val), + "item.telnet" => cfg.theme.item_telnet = to_color(val), + "item.external" => cfg.theme.item_external = to_color(val), + "item.download" => cfg.theme.item_download = to_color(val), + "item.media" => cfg.theme.item_media = to_color(val), + "item.unsupported" => cfg.theme.item_unsupported = to_color(val), + _ => return Err(error!("Unknown key on line {}: {}", linenum, key)), } keys.insert(key, true); diff --git a/src/help.rs b/src/help.rs index 2b2bdc3..f25a5a6 100644 --- a/src/help.rs +++ b/src/help.rs @@ -10,6 +10,7 @@ pub fn lookup(name: &str) -> Option { "history" => history::as_raw_menu(), "bookmarks" => bookmarks::as_raw_menu(), "help/config" => format!("{}{}", HEADER, CONFIG), + "help/themes" => format!("{}{}", HEADER, THEMES), "help/keys" => format!("{}{}", HEADER, KEYS), "help/nav" => format!("{}{}", HEADER, NAV), "help/types" => format!("{}{}", HEADER, TYPES), @@ -84,6 +85,7 @@ i 1bookmarks /help/bookmarks phetch 1history /help/history phetch 1phetch.conf /help/config phetch +1themes /help/themes phetch i i ~ * ~ i @@ -122,7 +124,7 @@ ia show history i ir view raw source iw toggle wide mode -ie toggle encoding (cp437 & utf8) +ie toggle encoding iq quit phetch ih show help i @@ -238,6 +240,79 @@ iwide no i i# show emoji status indicators iemoji no +i +i# cp437 or utf8 encoding +iencoding utf8 +i +i# wrap text at N cols. 0 = off +iwrap 0 +i +i# page up/down by N lines. +i# 0 = full screen +iscroll 0 +i +i# path to theme file, if any +itheme ~/.config/phetch/fun.theme +"; + +const THEMES: &str = " +i ** themes ** +i +iyou can change phetch's color +ischeme by supplying your own +itheme file with --theme/-t or +iby setting `theme FILE` in +iyour phetch.conf. +i +iyou can also view the current +itheme with: +i +i$ phetch --print-theme +i +itheme files look like this: +i +iui.cursor white bold +iui.number magenta +iui.menu yellow +iui.text white +iitem.text cyan +iitem.menu blue +iitem.error red +iitem.search white +iitem.telnet grey +iitem.external green +iitem.download white underline +iitem.media green underline +iitem.unsupported whitebg red +i +ivalid colors: +i +ibold +iunderline +igrey +ired +igreen +iyellow +iblue +imagenta +icyan +iwhite +iblack +idarkred +idarkgreen +idarkyellow +idarkblue +idarkmagenta +idarkcyan +idarkwhite +iblackbg +iredbg +igreenbg +iyellowbg +ibluebg +imagentabg +icyanbg +iwhitebg "; const TYPES: &str = " diff --git a/src/lib.rs b/src/lib.rs index 6740d29..60a4977 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,7 @@ extern crate lazy_static; #[macro_use] pub mod utils; #[macro_use] -pub mod color; +pub mod theme; pub mod args; pub mod bookmarks; pub mod config; diff --git a/src/main.rs b/src/main.rs index b55ed2e..72dd013 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ use phetch::{ - args, color, gopher, menu, terminal, + args, + config::{Config, SharedConfig}, + gopher, menu, terminal, theme, ui::{Mode, UI}, }; use std::{ @@ -26,6 +28,7 @@ fn run() -> Result<(), Box> { Mode::Raw => return print_raw(&cfg.start, cfg.tls, cfg.tor), Mode::Version => return print_version(), Mode::Help => return print_usage(), + Mode::PrintTheme => return print_theme(cfg), Mode::NoTTY => return print_plain(&cfg.start, cfg.tls, cfg.tor), Mode::Print => cfg.wide = true, Mode::Run => {} @@ -91,6 +94,8 @@ Options: -c, --config FILE Use instead of ~/.config/phetch/phetch.conf -C, --no-config Don't use any config file + -t, --theme FILE Use FILE for color theme or print current theme. + --print-theme Print current theme. -h, --help Show this screen -v, --version Show phetch version @@ -118,7 +123,7 @@ fn print_plain(url: &str, tls: bool, tor: bool) -> Result<(), Box> { let response = gopher::response_to_string(&response); match typ { gopher::Type::Menu => { - let menu = menu::parse(url, response); + let menu = menu::parse(url, response, SharedConfig::default()); for line in menu.lines() { out.push_str(line.text()); out.push('\n'); @@ -136,6 +141,12 @@ fn print_plain(url: &str, tls: bool, tor: bool) -> Result<(), Box> { Ok(()) } +/// Print current theme as plaintext +fn print_theme(cfg: Config) -> Result<(), Box> { + println!("{}", cfg.theme.to_string()); + Ok(()) +} + /// Put the terminal into raw mode, enter the alternate screen, and /// setup the panic handler. fn setup_terminal() { @@ -156,7 +167,7 @@ fn cleanup_terminal() { write!( stdout, "{}{}{}{}{}", - color::Reset, + theme::color::Reset, terminal::ClearAll, terminal::Goto(1, 1), terminal::ShowCursor, diff --git a/src/menu.rs b/src/menu.rs index b357aaa..af1f6b9 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -52,6 +52,8 @@ pub struct Menu { wide: bool, /// Scroll by how many lines? scroll: usize, + /// Global config + config: Config, } /// Represents a line in a Gopher menu. Provides the actual text of @@ -258,7 +260,7 @@ impl Menu { wide: config.read().unwrap().wide, scroll: config.read().unwrap().scroll, mode: config.read().unwrap().mode, - ..parse(url, response) + ..parse(url, response, config.clone()) } } @@ -369,26 +371,27 @@ impl Menu { for line in iter { out.push_str(&left_margin); + let config = self.config.read().unwrap(); if line.typ == Type::Info { out.push_str(" "); } else { if line.link == self.link && self.show_cursor() { - out.push_str(color!(Bold)); + out.push_str(&config.theme.ui_cursor); out.push('*'); - out.push_str(color!(Reset)); + out.push_str(reset_color!()); } else { out.push(' '); } out.push(' '); - out.push_str(color!(Magenta)); + out.push_str(&config.theme.ui_number); if line.link < 9 { out.push(' '); } let num = (line.link + 1).to_string(); out.push_str(&num); out.push_str(". "); - out.push_str(color!(Reset)); + out.push_str(reset_color!()); } // truncate long lines, instead of wrapping @@ -396,28 +399,25 @@ impl Menu { // color the line if line.typ.is_media() { - out.push_str(color!(Underline)); - out.push_str(color!(Green)); + out.push_str(&config.theme.item_media); } else if line.typ.is_download() { - out.push_str(color!(Underline)); - out.push_str(color!(White)); + out.push_str(&config.theme.item_download); } else if !line.typ.is_supported() { - out.push_str(color!(WhiteBG)); - out.push_str(color!(Red)); + out.push_str(&self.config.read().unwrap().theme.item_unsupported); } else { out.push_str(match line.typ { - Type::Text => color!(Cyan), - Type::Menu => color!(Blue), - Type::Info => color!(Yellow), - Type::HTML => color!(Green), - Type::Error => color!(Red), - Type::Telnet => color!(Grey), - Type::Search => color!(White), - _ => color!(Red), + Type::Text => &config.theme.item_text, + Type::Menu => &config.theme.item_menu, + Type::Info => &config.theme.ui_menu, + Type::HTML => &config.theme.item_external, + Type::Error => &config.theme.item_error, + Type::Telnet => &config.theme.item_telnet, + Type::Search => &config.theme.item_search, + _ => &config.theme.item_error, }); } out.push_str(&text); - out.push_str(color!(Reset)); + out.push_str(reset_color!()); // clear rest of line out.push_str(terminal::ClearUntilNewline.as_ref()); @@ -935,7 +935,7 @@ impl Menu { } /// Parse gopher response into a Menu object. -pub fn parse(url: &str, raw: String) -> Menu { +pub fn parse(url: &str, raw: String, config: Config) -> Menu { let mut spans = vec![]; let mut links = vec![]; let mut longest = 0; @@ -982,6 +982,7 @@ pub fn parse(url: &str, raw: String) -> Menu { tor: false, wide: false, scroll: 0, + config: config, } } @@ -1054,7 +1055,7 @@ mod tests { macro_rules! parse { ($s:expr) => { - parse("test", $s.to_string()) + parse("test", $s.to_string(), Config::default()) }; } diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..53d467c --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,228 @@ +//! Terminal color scheme. +//! Provides the Theme struct and functions/macros for making use of it. + +/// Provides a shortcut to the Reset color code. +pub mod color { + use std::fmt; + + /// Can be used with fmt calls to reset to terminal defaults. + pub struct Reset; + + impl fmt::Display for Reset { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\x1b[0m") + } + } +} + +/// Use with push_str() or something. +macro_rules! reset_color { + () => { + "\x1b[0m" + }; +} + +/// Color scheme for UI and menu items. +#[derive(Debug)] +pub struct Theme { + // UI Colors + /// The * cursor that appears next to the selected menu item. + pub ui_cursor: String, + /// The Number that appears to the left of a menu item. + pub ui_number: String, + /// The text in a menu. + pub ui_menu: String, + /// The color of the text content in a document. + pub ui_text: String, + + // Menu Item Colors + /// Text document. + pub item_text: String, + /// Another menu. + pub item_menu: String, + /// Something went wrong. + pub item_error: String, + /// Gopher search prompt + pub item_search: String, + /// Telnet item. MUDs and stuff. + pub item_telnet: String, + /// External link. HTTP, usually. + pub item_external: String, + /// Binary file that can be downloaded to disk. + pub item_download: String, + /// Media that can be opened, like an image or mp3. + pub item_media: String, + /// An unknown or unsupported Gopher type. + pub item_unsupported: String, +} + +impl Default for Theme { + fn default() -> Theme { + Theme { + ui_cursor: to_color("white bold").into(), + ui_number: to_color("magenta").into(), + ui_menu: to_color("yellow").into(), + ui_text: to_color("white").into(), + + item_text: to_color("cyan").into(), + item_menu: to_color("blue").into(), + item_error: to_color("red").into(), + item_search: to_color("white").into(), + item_telnet: to_color("grey").into(), + item_external: to_color("green").into(), + item_download: to_color("white underline").into(), + item_media: to_color("green underline").into(), + item_unsupported: to_color("whitebg red").into(), + } + } +} + +impl Theme { + /// Return theme file for this theme + pub fn to_string(&self) -> String { + format!( + "# phetch theme +ui.cursor {ui_cursor} +ui.number {ui_number} +ui.menu {ui_menu} +ui.text {ui_text} + +item.text {item_text} +item.menu {item_menu} +item.error {item_error} +item.search {item_search} +item.telnet {item_telnet} +item.external {item_external} +item.download {item_download} +item.media {item_media} +item.unsupported {item_unsupported}", + ui_cursor = to_words(&self.ui_cursor), + ui_number = to_words(&self.ui_number), + ui_menu = to_words(&self.ui_menu), + ui_text = to_words(&self.ui_text), + item_text = to_words(&self.item_text), + item_menu = to_words(&self.item_menu), + item_error = to_words(&self.item_error), + item_search = to_words(&self.item_search), + item_telnet = to_words(&self.item_telnet), + item_external = to_words(&self.item_external), + item_download = to_words(&self.item_download), + item_media = to_words(&self.item_media), + item_unsupported = to_words(&self.item_unsupported), + ) + } +} + +/// Convert a string like "blue underline" or "red" into a color code. +pub fn to_color>(line: S) -> String { + let parts = line.as_ref().split(' ').collect::>(); + + if parts.is_empty() { + return "".into(); + } + + let mut out = String::from("\x1b["); + let len = parts.len(); + + for (i, part) in parts.iter().enumerate() { + out.push_str(&color_code(part).to_string()); + if i < len - 1 { + out.push(';'); + } + } + out.push('m'); + + out +} + +/// Convert color code like "\x1b[91m" into something like "red" +pub fn to_words>(code: S) -> String { + code.as_ref() + .replace("\x1b[", "") + .replace('m', "") + .split(';') + .map(color_word) + .collect::>() + .join(" ") +} + +fn color_code(color: &str) -> usize { + match color { + "bold" => 1, + "underline" => 4, + "grey" => 90, + "red" => 91, + "green" => 92, + "yellow" => 93, + "blue" => 94, + "magenta" => 95, + "cyan" => 96, + "white" => 97, + "black" => 30, + "darkred" => 31, + "darkgreen" => 32, + "darkyellow" => 33, + "darkblue" => 34, + "darkmagenta" => 35, + "darkcyan" => 36, + "darkwhite" => 37, + "blackbg" => 40, + "redbg" => 41, + "greenbg" => 42, + "yellowbg" => 43, + "bluebg" => 44, + "magentabg" => 45, + "cyanbg" => 46, + "whitebg" => 47, + _ => 0, + } +} + +fn color_word(code: &str) -> &'static str { + match code { + "1" => "bold", + "4" => "underline", + "90" => "grey", + "91" => "red", + "92" => "green", + "93" => "yellow", + "94" => "blue", + "95" => "magenta", + "96" => "cyan", + "97" => "white", + "30" => "black", + "31" => "darkred", + "32" => "darkgreen", + "33" => "darkyellow", + "34" => "darkblue", + "35" => "darkmagenta", + "36" => "darkcyan", + "37" => "darkwhite", + "40" => "blackbg", + "41" => "redbg", + "42" => "greenbg", + "43" => "yellowbg", + "44" => "bluebg", + "45" => "magentabg", + "46" => "cyanbg", + "47" => "whitebg", + _ => "white", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_color_scheme() { + let mut theme = Theme::default(); + theme.ui_cursor = to_color("bold").into(); + theme.ui_menu = to_color("red").into(); + theme.item_menu = to_color("blue underline").into(); + + assert_eq!("\u{1b}[1m", theme.ui_cursor); + assert_eq!("\u{1b}[91m", theme.ui_menu); + assert_eq!("\u{1b}[94;4m", theme.item_menu); + } +} diff --git a/src/ui.rs b/src/ui.rs index afe9947..c250ce0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -17,7 +17,7 @@ mod view; pub use self::{action::Action, mode::Mode, view::View}; use crate::{ - bookmarks, color, + bookmarks, config::{Config, SharedConfig}, encoding::Encoding, gopher::{self, Type}, @@ -25,7 +25,7 @@ use crate::{ menu::Menu, terminal, text::Text, - utils, BUG_URL, + theme, utils, BUG_URL, }; use std::{ io::{stdin, stdout, Result, Write}, @@ -153,7 +153,12 @@ impl UI { self.status.clear(); } if let Err(e) = self.process_action(action) { - self.set_status(&format!("{}{}{}", color::Red, e, terminal::HideCursor)); + self.set_status(&format!( + "{}{}{}", + &self.config.read().unwrap().theme.item_error, + e, + terminal::HideCursor + )); } } @@ -203,7 +208,9 @@ impl UI { if typ.is_media() && self.config.read().unwrap().media.is_some() { self.dirty = true; - return if self.config.read().unwrap().autoplay || self.confirm(&format!("Open in media player? {}", url)) { + return if self.config.read().unwrap().autoplay + || self.confirm(&format!("Open in media player? {}", url)) + { utils::open_media(self.config.read().unwrap().media.as_ref().unwrap(), url) } else { Ok(()) @@ -339,7 +346,7 @@ impl UI { label, ".".repeat(i), terminal::ClearUntilNewline, - color::Reset, + theme::color::Reset, terminal::ShowCursor, ); stdout().flush().expect(ERR_STDOUT); @@ -414,7 +421,7 @@ impl UI { terminal::Goto(self.cols() - len as u16, self.rows()), status .iter() - .map(|s| color_string!(s, Bold, White)) + .map(|s| theme::to_color("bold white") + s + reset_color!()) .collect::>() .join(" "), )) @@ -430,7 +437,7 @@ impl UI { terminal::ClearCurrentLine, self.status, self.render_conn_status().unwrap_or_else(|| "".into()), - color::Reset, + theme::color::Reset, ) } @@ -454,7 +461,7 @@ impl UI { write!( out, "{}{}{}{} [Y/n]: {}", - color::Reset, + theme::color::Reset, terminal::Goto(1, rows), terminal::ClearCurrentLine, question, @@ -479,7 +486,7 @@ impl UI { write!( out, "{}{}{}{}{}{}", - color::Reset, + theme::color::Reset, terminal::Goto(1, rows), terminal::ClearCurrentLine, prompt, diff --git a/src/ui/mode.rs b/src/ui/mode.rs index 960e406..fe7623f 100644 --- a/src/ui/mode.rs +++ b/src/ui/mode.rs @@ -21,6 +21,9 @@ pub enum Mode { /// Show command line help. /// phetch --help Help, + /// Print current theme + /// phetch --theme + PrintTheme, } impl Default for Mode {