474 lines
15 KiB
Rust
474 lines
15 KiB
Rust
use std::collections::HashMap;
|
|
use std::fmt;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
use cursive::{
|
|
Cursive, Printer, Vec2,
|
|
event::{Event, EventResult},
|
|
utils::markup::StyledString,
|
|
theme::{Color, Theme, BorderStyle, ColorStyle, ColorType},
|
|
traits::{View,Boxable,Identifiable},
|
|
view::ViewWrapper,
|
|
views::{LinearLayout, TextView, DummyView, PaddedView, Panel, ViewBox},
|
|
};
|
|
|
|
use libcitadel::terminal::{TerminalPalette, Base16Scheme};
|
|
|
|
use crate::tree::{TreeView, Placement};
|
|
|
|
#[derive(Clone)]
|
|
pub struct ThemeHandler {
|
|
saved_palette: TerminalPalette,
|
|
theme: Theme,
|
|
}
|
|
|
|
impl ThemeHandler {
|
|
|
|
fn set_palette_color(theme: &mut Theme, name: &str, rgb: (u16, u16, u16)) {
|
|
theme.palette.set_color(name, Color::Rgb(rgb.0 as u8, rgb.1 as u8, rgb.2 as u8))
|
|
}
|
|
|
|
pub fn generate_base16_theme(base16: &Base16Scheme) -> Theme {
|
|
let mut theme = Theme::default();
|
|
theme.shadow = false;
|
|
theme.borders = BorderStyle::Outset;
|
|
let mapping = [
|
|
(0x0, "background"),
|
|
(0x1, "shadow"),
|
|
(0x0, "view"),
|
|
(0x5, "primary"),
|
|
(0xC, "secondary"),
|
|
(0x3, "tertiary"),
|
|
(0x8, "title_primary"),
|
|
(0xA, "title_secondary"),
|
|
(0x2, "highlight"),
|
|
(0x3, "highlight_inactive"),
|
|
];
|
|
for pair in &mapping {
|
|
Self::set_palette_color(&mut theme, pair.1, base16.color(pair.0).rgb());
|
|
}
|
|
theme
|
|
}
|
|
|
|
const SCHEME_CONF_PATH: &'static str = "/storage/citadel-state/realms-base16.conf";
|
|
const DEFAULT_SCHEME: &'static str = "default-dark";
|
|
|
|
pub fn save_base16_theme(base16: &Base16Scheme) {
|
|
if let Err(e) = fs::write(Self::SCHEME_CONF_PATH, base16.slug()) {
|
|
warn!("Error writing color scheme file ({}): {}", Self::SCHEME_CONF_PATH, e);
|
|
}
|
|
}
|
|
|
|
pub fn load_base16_scheme() -> Option<Base16Scheme> {
|
|
let path = Path::new(Self::SCHEME_CONF_PATH);
|
|
if path.exists() {
|
|
fs::read_to_string(path).ok().and_then(|ref s| Base16Scheme::by_name(s).cloned())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn load_base16_theme() -> Theme {
|
|
let path = Path::new(Self::SCHEME_CONF_PATH);
|
|
let mut scheme = Base16Scheme::by_name(Self::DEFAULT_SCHEME).unwrap();
|
|
if path.exists() {
|
|
if let Ok(scheme_name) = fs::read_to_string(path) {
|
|
if let Some(sch) = Base16Scheme::by_name(&scheme_name) {
|
|
scheme = sch;
|
|
}
|
|
}
|
|
}
|
|
Self::generate_base16_theme(scheme)
|
|
}
|
|
}
|
|
|
|
pub struct ThemeChooser {
|
|
inner: ViewBox,
|
|
}
|
|
|
|
impl ThemeChooser {
|
|
|
|
pub fn open(s: &mut Cursive) {
|
|
let initial = ThemeHandler::load_base16_scheme();
|
|
let chooser = Self::new(initial, |s,v| {
|
|
ThemeHandler::save_base16_theme(v);
|
|
let theme = ThemeHandler::generate_base16_theme(v);
|
|
s.set_theme(theme);
|
|
});
|
|
s.add_layer(chooser.with_id("theme-chooser"));
|
|
}
|
|
|
|
pub fn new<F>(initial: Option<Base16Scheme>, cb: F) -> Self
|
|
where F: 'static + Fn(&mut Cursive, &Base16Scheme)
|
|
{
|
|
let select = Self::create_tree_view(initial.clone(), cb);
|
|
let content = Self::create_content(initial, select);
|
|
let inner = ViewBox::boxed(content);
|
|
ThemeChooser { inner }
|
|
}
|
|
|
|
fn create_content<V: View>(initial: Option<Base16Scheme>, select: V) -> impl View {
|
|
let left = LinearLayout::vertical()
|
|
.child(TextView::new(StyledString::styled("Press Enter to change theme.\n 'q' or Esc to close panel", ColorStyle::tertiary())))
|
|
.child(DummyView)
|
|
.child(PaddedView::new((0,0,1,1),select));
|
|
|
|
|
|
let mut preview = ThemePreview::new();
|
|
if let Some(ref scheme) = initial {
|
|
preview.set_scheme(scheme.clone());
|
|
}
|
|
|
|
let right = Panel::new(PaddedView::new((1,1,0,0), preview.with_id("theme-preview")));//.title("Preview");
|
|
|
|
let layout = LinearLayout::horizontal()
|
|
.child(left)//PaddedView::new((0,0,0,2),left))
|
|
.child(DummyView.fixed_width(1))
|
|
.child(right);
|
|
|
|
let padded = PaddedView::new((1,1,1,1), layout);
|
|
Panel::new(padded)
|
|
.title("Choose a theme")
|
|
}
|
|
|
|
fn create_tree_view<F>(initial: Option<Base16Scheme>, cb: F) -> impl View
|
|
where F: 'static + Fn(&mut Cursive, &Base16Scheme)
|
|
{
|
|
let mut tree = TreeView::new()
|
|
.on_select(Self::on_tree_select)
|
|
.on_collapse(Self::on_tree_collapse)
|
|
.on_submit(move |s,idx| {
|
|
let item = Self::call_on_tree(s, |v| v.borrow_item(idx).cloned());
|
|
if let Some(TreeItem::ColorScheme(ref scheme)) = item {
|
|
(cb)(s, scheme);
|
|
}
|
|
});
|
|
|
|
Self::populate_tree(initial, &mut tree);
|
|
tree.with_id("theme-tree")
|
|
}
|
|
|
|
fn populate_tree(initial: Option<Base16Scheme>, tree: &mut TreeView<TreeItem>) {
|
|
let schemes = Base16Scheme::all_schemes();
|
|
let mut category_rows = HashMap::new();
|
|
let mut last_row = 0;
|
|
for scheme in &schemes {
|
|
last_row = Self::add_scheme_to_tree(initial.as_ref(), tree, last_row, scheme, &mut category_rows);
|
|
}
|
|
}
|
|
|
|
fn add_scheme_to_tree(initial: Option<&Base16Scheme>, tree: &mut TreeView<TreeItem>, last_row: usize, scheme: &Base16Scheme, category_rows: &mut HashMap<&str,usize>) -> usize {
|
|
let item = TreeItem::scheme(scheme);
|
|
let mut last_row = last_row;
|
|
let is_initial = initial.map(|s| s.slug() == scheme.slug()).unwrap_or(false);
|
|
|
|
if let Some(category) = scheme.category() {
|
|
let is_initial_category = initial.map(|sc| sc.category() == scheme.category()).unwrap_or(false);
|
|
let category_row = Self::get_category_row(!is_initial_category, tree, &mut last_row, category, category_rows);
|
|
if let Some(new_row) = tree.insert_item(item, Placement::LastChild, category_row) {
|
|
if is_initial {
|
|
tree.set_selected_row(new_row);
|
|
tree.scroll_to(category_row);
|
|
}
|
|
}
|
|
} else {
|
|
last_row = tree.insert_item(item, Placement::After, last_row)
|
|
.expect("newly added colorscheme row is not visible");
|
|
|
|
if is_initial {
|
|
tree.set_selected_row(last_row);
|
|
}
|
|
}
|
|
last_row
|
|
}
|
|
|
|
fn get_category_row<'a>(collapsed: bool, tree: &mut TreeView<TreeItem>, last_row: &mut usize, category: &'a str, category_rows: &mut HashMap<&'a str, usize>) -> usize {
|
|
let row = category_rows.entry(category).or_insert_with(|| {
|
|
let new_row = tree.insert_item(TreeItem::category(category, collapsed), Placement::After, *last_row)
|
|
.expect("newly added category row is not visible");
|
|
if collapsed {
|
|
tree.collapse_item(new_row);
|
|
}
|
|
*last_row = new_row;
|
|
new_row
|
|
});
|
|
*row
|
|
}
|
|
|
|
|
|
fn on_tree_select(s: &mut Cursive, idx: usize) {
|
|
let selected = Self::call_on_tree(s, |v| v.borrow_item(idx).cloned());
|
|
|
|
if let Some(item) = selected {
|
|
if let TreeItem::ColorScheme(scheme) = item {
|
|
s.call_on_id("theme-preview", |v: &mut ThemePreview| v.set_scheme(scheme));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn on_tree_collapse(s: &mut Cursive, row: usize, is_collapsed: bool, _: usize) {
|
|
Self::call_on_tree(s, |v| {
|
|
if let Some(item) = v.borrow_item_mut(row) {
|
|
if let TreeItem::Category(ref _name, ref mut collapsed) = *item {
|
|
*collapsed = is_collapsed;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn call_on_tree<F,R>(s: &mut Cursive, cb:F) -> R
|
|
where F: FnOnce(&mut TreeView<TreeItem>) -> R
|
|
|
|
{
|
|
s.call_on_id("theme-tree", cb)
|
|
.expect("call_on_id(theme-tree)")
|
|
}
|
|
|
|
fn toggle_expand_item(&self) -> EventResult {
|
|
EventResult::with_cb(|s| {
|
|
Self::call_on_tree(s, |v| {
|
|
if let Some(row) = v.row() {
|
|
Self::toggle_item_collapsed(v, row);
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
fn toggle_item_collapsed(v: &mut TreeView<TreeItem>, row: usize) {
|
|
if let Some(item) = v.borrow_item_mut(row) {
|
|
if let TreeItem::Category(_name, collapsed) = item {
|
|
let was_collapsed = *collapsed;
|
|
*collapsed = !was_collapsed;
|
|
v.set_collapsed(row, !was_collapsed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ViewWrapper for ThemeChooser {
|
|
type V = dyn View;
|
|
fn with_view<F: FnOnce(&Self::V) -> R, R>(&self, f: F) -> Option<R> { Some(f(&*self.inner)) }
|
|
fn with_view_mut<F: FnOnce(&mut Self::V) -> R, R>(&mut self, f: F) -> Option<R> { Some(f(&mut *self.inner)) }
|
|
|
|
fn wrap_on_event(&mut self, event: Event) -> EventResult {
|
|
match event {
|
|
Event::Char(' ') => self.toggle_expand_item(),
|
|
Event::Char('o') => self.toggle_expand_item(),
|
|
event => self.inner.on_event(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PreviewHelper<'a,'b> {
|
|
printer: Printer<'a,'b>,
|
|
scheme: Base16Scheme,
|
|
offset: Vec2,
|
|
}
|
|
|
|
impl <'a,'b> PreviewHelper<'a,'b> {
|
|
fn new(printer: Printer<'a,'b>, scheme: Base16Scheme) -> Self {
|
|
PreviewHelper {
|
|
printer, scheme, offset: Vec2::zero()
|
|
}
|
|
|
|
}
|
|
fn color(&self, idx: usize) -> ColorType {
|
|
let (r,g,b) = self.scheme.terminal_palette_color(idx).rgb();
|
|
ColorType::Color(Color::Rgb(r as u8, g as u8, b as u8))
|
|
}
|
|
|
|
fn color_fg(&self) -> ColorType {
|
|
let (r,g,b) = self.scheme.terminal_foreground().rgb();
|
|
ColorType::Color(Color::Rgb(r as u8, g as u8, b as u8))
|
|
}
|
|
fn color_bg(&self) -> ColorType {
|
|
let (r,g,b) = self.scheme.terminal_background().rgb();
|
|
ColorType::Color(Color::Rgb(r as u8, g as u8, b as u8))
|
|
}
|
|
|
|
fn draw(mut self, color: ColorType, text: &str) -> Self {
|
|
let style = ColorStyle::new(color,self.color_bg());
|
|
|
|
self.printer.with_color(style, |printer| {
|
|
printer.print(self.offset, text);
|
|
});
|
|
self.offset.x += text.len();
|
|
|
|
self
|
|
}
|
|
|
|
fn vtype(self, text: &str) -> Self {
|
|
let color = self.color(3);
|
|
self.draw(color, text)
|
|
}
|
|
|
|
fn konst(self, text: &str) -> Self {
|
|
let color = self.color(1);
|
|
self.draw(color, text)
|
|
}
|
|
|
|
|
|
fn func(self, text: &str) -> Self {
|
|
let color = self.color(4);
|
|
self.draw(color, text)
|
|
}
|
|
|
|
fn string(self, text: &str) -> Self {
|
|
let color = self.color(2);
|
|
self.draw(color, text)
|
|
}
|
|
|
|
fn keyword(self, text: &str) -> Self {
|
|
let color = self.color(5);
|
|
self.draw(color, text)
|
|
}
|
|
|
|
fn comment(self, text: &str) -> Self {
|
|
let color = self.color(8);
|
|
self.draw(color, text)
|
|
}
|
|
|
|
fn text(self, text: &str) -> Self {
|
|
let color = self.color_fg();
|
|
self.draw(color, text)
|
|
}
|
|
|
|
fn nl(mut self) -> Self {
|
|
self.offset.x = 0;
|
|
self.offset.y += 1;
|
|
self
|
|
}
|
|
}
|
|
|
|
struct ThemePreview {
|
|
last_size: Vec2,
|
|
scheme: Option<Base16Scheme>,
|
|
}
|
|
|
|
impl ThemePreview {
|
|
fn new() -> ThemePreview {
|
|
ThemePreview { scheme: None, last_size: Vec2::zero() }
|
|
}
|
|
|
|
fn set_scheme(&mut self, scheme: Base16Scheme) {
|
|
self.scheme = Some(scheme);
|
|
}
|
|
|
|
fn color(&self, idx: usize) -> ColorType {
|
|
let (r,g,b) = self.scheme.as_ref().unwrap().color(idx).rgb();
|
|
ColorType::Color(Color::Rgb(r as u8, g as u8, b as u8))
|
|
}
|
|
|
|
fn terminal_color(&self, idx: usize) -> ColorType {
|
|
let (r,g,b) = self.scheme.as_ref().unwrap().terminal_palette_color(idx).rgb();
|
|
ColorType::Color(Color::Rgb(r as u8, g as u8, b as u8))
|
|
}
|
|
|
|
fn terminal_style(&self, fg: usize, bg: usize) -> ColorStyle {
|
|
ColorStyle::new(self.terminal_color(fg), self.terminal_color(bg))
|
|
}
|
|
|
|
fn style(&self, fg: usize, bg: usize) -> ColorStyle {
|
|
ColorStyle::new(self.color(fg), self.color(bg))
|
|
}
|
|
|
|
fn draw_background(&self, printer: &Printer) {
|
|
let color = self.terminal_style(5, 0);
|
|
printer.with_color(color, |printer| {
|
|
for i in 0..self.last_size.y {
|
|
printer.print_hline((0,i), self.last_size.x, " ");
|
|
}
|
|
});
|
|
}
|
|
|
|
fn draw_colorbar(&self, printer: &Printer) {
|
|
let text_color = self.style(3, 0);
|
|
for i in 0..16 {
|
|
let color = self.style(3, i);
|
|
printer.with_color(text_color, |printer| {
|
|
printer.print((i*3, 0), &format!(" {:X} ", i));
|
|
});
|
|
printer.with_color(color, |printer| {
|
|
printer.print((i*3, 1), " ");
|
|
});
|
|
}
|
|
for i in 8..16 {
|
|
let color = self.terminal_style(5, i);
|
|
let x = (i - 8) * 6;
|
|
printer.with_color(color, |printer| {
|
|
printer.print_hline((x, 2), 6, " ");
|
|
});
|
|
|
|
}
|
|
}
|
|
|
|
fn draw_text(&self, printer: &Printer) {
|
|
let scheme = self.scheme.as_ref().unwrap().clone();
|
|
let name = scheme.name().to_owned();
|
|
let printer = printer.offset((4, 5));
|
|
PreviewHelper::new(printer, scheme)
|
|
.comment("/**").nl()
|
|
.comment(" * An example of how this color scheme").nl()
|
|
.comment(" * might look in a text editor with syntax").nl()
|
|
.comment(" * highlighting.").nl()
|
|
.comment(" */").nl()
|
|
.nl()
|
|
.func("#include ").string("<stdio.h>").nl()
|
|
.func("#include ").string("<stdlib.h>").nl()
|
|
.nl()
|
|
.vtype("static char").text(" theme[] = ").string(&format!("\"{}\"", name)).text(";").nl()
|
|
.nl()
|
|
.vtype("int").text(" main(").vtype("int").text(" argc, ").vtype("char").text(" **argv) {").nl()
|
|
.text(" printf(").string("\"Hello, ").keyword("%s").text("!").keyword("\\n").string("\"").text(", theme);").nl()
|
|
.text(" exit(").konst("0").text(");").nl()
|
|
.text("}")
|
|
.nl()
|
|
.nl();
|
|
}
|
|
|
|
}
|
|
|
|
|
|
impl View for ThemePreview {
|
|
fn draw(&self, printer: &Printer) {
|
|
if self.scheme.is_some() {
|
|
self.draw_background(&printer);
|
|
self.draw_colorbar(&printer.offset((2, 1)));
|
|
self.draw_text(&printer);
|
|
}
|
|
}
|
|
|
|
fn layout(&mut self, size: Vec2) {
|
|
self.last_size = size;
|
|
}
|
|
|
|
fn required_size(&mut self, _constraint: Vec2) -> Vec2 {
|
|
Vec2::new(52, 30)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone,Debug)]
|
|
enum TreeItem {
|
|
Category(String, bool),
|
|
ColorScheme(Base16Scheme),
|
|
}
|
|
|
|
impl fmt::Display for TreeItem {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "{}", match self {
|
|
TreeItem::Category(ref s, _) => s.as_str(),
|
|
TreeItem::ColorScheme(ref scheme) => scheme.name(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl TreeItem {
|
|
fn category(name: &str, collapsed: bool) -> Self {
|
|
TreeItem::Category(name.to_string(), collapsed)
|
|
}
|
|
fn scheme(scheme: &Base16Scheme) -> Self {
|
|
TreeItem::ColorScheme(scheme.clone())
|
|
}
|
|
}
|