Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,30 @@ wins over the file, and `plannotator-tui config` prints whichever is in effect.
backgrounds plannotator-tui paints itself change; the document keeps your terminal's own
colours either way.

`[colors]` replaces individual roles of whichever palette is in force; a role left out keeps
its light or dark value. A colour is a name (`yellow`, `light-cyan`), a 256-colour index
(`238`, quoted or not) or `#rrggbb`. An unknown role or colour is an error that names it, and
`plannotator-tui config` prints the roles you set. Every role, with the dark palette's values
(apart from `selection`, which is reverse video by default):

```toml
[colors]
accent = "cyan" # focused borders and gutter, the open file, menu and picker frames
heading = "cyan" # every heading level
comment = "yellow" # comment titles, the compose box, annotated rows in the gutter
looks-good = "green"
delete = "red" # also the struck-through text
comment-bg = "58" # the tint under commented text
looks-good-bg = "22" # the tint under looks-good text
block = "236" # the focused block and the focused tree row
toolbar = "238" # the floating toolbar and the idle header buttons
cursor = "240" # the keyboard cursor cell
selection = "#45475a" # a character selection, a picker row, a menu row
```

`cursor` and `selection` become plain backgrounds when set: the text on them keeps the
terminal's colour.

### Keys

Every key is rebindable under `[keys]`: an action, and the keys that run it.
Expand Down
9 changes: 6 additions & 3 deletions crates/plannotator-tui/src/app/archive_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
use ratatui::Frame;
use ratatui::crossterm::event::{KeyEvent, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph};

use super::draw::picker_help;
use super::{App, Mode, label};
use crate::keys::{self, Action};
use crate::theme::palette;

impl App {
pub(super) fn archive_key(&mut self, key: KeyEvent) {
Expand Down Expand Up @@ -63,7 +64,7 @@ impl App {
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::new().fg(Color::Cyan))
.border_style(Style::new().fg(palette().accent))
.title(format!(" Archived annotations ({}) ", self.archive_items.len()))
.title_bottom(picker_help(
"select",
Expand Down Expand Up @@ -99,7 +100,9 @@ impl App {
let quote = item.annotation.anchor.original_text.split_whitespace().collect::<Vec<_>>().join(" ");
let body = item.annotation.body.split_whitespace().collect::<Vec<_>>().join(" ");
let style = if index == self.archive_cursor {
Style::new().fg(Color::Black).bg(Color::Cyan)
// Reversed accent: the row text takes the terminal background, which an accent
// picked to read as text on that background contrasts with.
Style::new().fg(palette().accent).add_modifier(Modifier::REVERSED)
} else {
Style::new()
};
Expand Down
24 changes: 13 additions & 11 deletions crates/plannotator-tui/src/app/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ const COMPOSE_WIDTH: u16 = 48;

fn accent(kind: Kind) -> Color {
match kind {
Kind::Comment => Color::Yellow,
Kind::LooksGood => Color::Green,
Kind::Delete => Color::Red,
Kind::Comment => palette().comment,
Kind::LooksGood => palette().looks_good,
Kind::Delete => palette().delete,
}
}

Expand Down Expand Up @@ -99,7 +99,8 @@ impl App {
fn draw_tree(&self, frame: &mut Frame, area: Rect) {
let Some(tree) = &self.tree else { return };
let focused = self.focus == Focus::Tree;
let border = if focused { Style::new().fg(Color::Cyan) } else { Style::new().fg(Color::DarkGray) };
let border =
if focused { Style::new().fg(palette().accent) } else { Style::new().fg(Color::DarkGray) };
let block = Block::default().borders(Borders::RIGHT).border_style(border);
let inner = block.inner(area);
frame.render_widget(block, area);
Expand All @@ -123,7 +124,7 @@ impl App {
};
let mut style = if row.is_dir { Style::new().dim() } else { Style::new() };
if open_path == Some(row.path.as_path()) {
style = style.bold().fg(Color::Cyan);
style = style.bold().fg(palette().accent);
}
let row_bg = (focused && i == self.tree_cursor).then(|| palette().block_bg);
if let Some(bg) = row_bg {
Expand All @@ -138,7 +139,7 @@ impl App {
Line::from(vec![
Span::styled(format!(" {name}"), style),
Span::styled(" ".repeat(pad), with_bg(Style::new())),
Span::styled(count, with_bg(Style::new().fg(Color::Yellow))),
Span::styled(count, with_bg(Style::new().fg(palette().comment))),
])
})
.collect();
Expand Down Expand Up @@ -179,7 +180,7 @@ impl App {
Kind::Comment => Style::new().bg(palette().comment_bg),
Kind::LooksGood => Style::new().bg(palette().approve_bg),
Kind::Delete => {
Style::new().fg(Color::Red).add_modifier(Modifier::CROSSED_OUT | Modifier::DIM)
Style::new().fg(palette().delete).add_modifier(Modifier::CROSSED_OUT | Modifier::DIM)
}
};
buf.set_style(Rect { x: doc.x + col as u16, y: screen_y, width: 1, height: 1 }, style);
Expand All @@ -204,8 +205,8 @@ impl App {
}

let marker = match (block == self.selected, row_has_annotation) {
(true, _) => Span::styled("▍", Style::new().fg(Color::Cyan)),
(false, true) => Span::styled("▍", Style::new().fg(Color::Yellow)),
(true, _) => Span::styled("▍", Style::new().fg(palette().accent)),
(false, true) => Span::styled("▍", Style::new().fg(palette().comment)),
(false, false) => Span::raw(" "),
};
buf.set_span(gutter.x, screen_y, &marker, 1);
Expand Down Expand Up @@ -285,7 +286,7 @@ impl App {
let boxed = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::new().fg(Color::Yellow))
.border_style(Style::new().fg(palette().comment))
.title(Span::styled(title.to_owned(), Style::new().dim()));
let inner = boxed.inner(rect);
frame.render_widget(boxed, rect);
Expand Down Expand Up @@ -395,7 +396,8 @@ impl App {
let label = format!(" {} ", label.as_deref().unwrap_or("Undo finish"));
let width = (label.width() as u16).min(area.width);
let rect = Rect { x: area.right() - width, y: area.y, width, height: area.height };
frame.render_widget(Paragraph::new(label.as_str()).style(Style::new().fg(Color::Cyan)), rect);
frame
.render_widget(Paragraph::new(label.as_str()).style(Style::new().fg(palette().accent)), rect);
self.geometry.undo_button = Some(rect);
area.width = area.width.saturating_sub(width);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/plannotator-tui/src/app/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ impl App {
}
Button::Review => {
self.geometry.review_button = Some(rect);
Style::new().fg(Color::Cyan).bg(palette().toolbar_bg)
Style::new().fg(palette().accent).bg(palette().toolbar_bg)
}
};
frame.buffer_mut().set_span(rect.x, rect.y, &Span::styled(label, style), rect.width);
Expand Down
4 changes: 2 additions & 2 deletions crates/plannotator-tui/src/app/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use anyhow::Result;
use ratatui::Frame;
use ratatui::crossterm::event::{KeyEvent, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph};
use unicode_width::UnicodeWidthStr as _;
Expand Down Expand Up @@ -180,7 +180,7 @@ impl App {
let boxed = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::new().fg(Color::Cyan))
.border_style(Style::new().fg(palette().accent))
.title(Span::styled(" review ", Style::new().dim()))
.title_bottom(Span::styled(help, Style::new().dim()));
let inner = boxed.inner(rect);
Expand Down
4 changes: 2 additions & 2 deletions crates/plannotator-tui/src/app/pick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use plannotator_tui_hosts::Message;
use ratatui::Frame;
use ratatui::crossterm::event::{KeyEvent, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph};
use unicode_width::UnicodeWidthStr as _;
Expand Down Expand Up @@ -145,7 +145,7 @@ impl App {
let boxed = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::new().fg(Color::Cyan))
.border_style(Style::new().fg(palette().accent))
.title(Span::styled(" which message? ", Style::new().dim()))
.title_bottom(Span::styled(
picker_help(
Expand Down
9 changes: 4 additions & 5 deletions crates/plannotator-tui/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,11 +337,10 @@ pub(crate) fn run_ui(build: impl FnOnce(usize) -> Result<App>) -> Result<()> {
// A config that fails to parse never kept the plain TUI from starting; it still does
// not. `plannotator-tui config` is where the error is reported.
let config = Config::load().unwrap_or_default();
crate::theme::install(crate::theme::resolve(
|key| std::env::var(key).ok(),
config.ui.theme,
crate::theme::detect,
)?);
crate::theme::install(
crate::theme::resolve(|key| std::env::var(key).ok(), config.ui.theme, crate::theme::detect)?,
&config.colors,
);
let mut terminal = ratatui::init();
execute!(stdout(), EnableMouseCapture)?;
let _ = execute!(stdout(), EnableBracketedPaste);
Expand Down
37 changes: 35 additions & 2 deletions crates/plannotator-tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::keys::Keymap;
use crate::theme::ThemeSetting;
use crate::theme::{Colors, ThemeSetting};

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct Config {
pub(crate) herdr: HerdrConfig,
pub(crate) ui: UiConfig,
pub(crate) keys: Keymap,
pub(crate) colors: Colors,
}

/// How the app looks. One key so far; the colours themselves are not configurable yet.
/// Which palette the app starts from; `[colors]` adjusts individual roles of it.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct UiConfig {
Expand Down Expand Up @@ -251,4 +252,36 @@ mod tests {
let text = Config::default().to_toml().expect("serializes");
assert_eq!(Config::parse(&text).expect("parses"), Config::default());
}

#[test]
fn colors_are_read_by_role_and_roundtrip() {
let config =
Config::parse("[colors]\naccent = \"magenta\"\nlooks-good-bg = \"#a6e3a1\"\nblock = \"236\"\n")
.expect("parses");
assert_eq!(config.colors.accent.map(|c| c.0), Some(ratatui::style::Color::Magenta));
assert_eq!(config.colors.looks_good_bg.map(|c| c.0), Some(ratatui::style::Color::Rgb(166, 227, 161)));
assert_eq!(config.colors.heading, None);
let text = config.to_toml().expect("serializes");
assert_eq!(Config::parse(&text).expect("reparses"), config);
}

#[test]
fn a_color_may_be_a_bare_index() {
let config = Config::parse("[colors]\nheading = 238\n").expect("parses");
assert_eq!(config.colors.heading.map(|c| c.0), Some(ratatui::style::Color::Indexed(238)));
let err = Config::parse("[colors]\nheading = 256\n").expect_err("out of range");
assert!(err.to_string().contains("256"), "{err}");
}

#[test]
fn an_unknown_color_role_error_names_the_role() {
let err = Config::parse("[colors]\nbackground = \"black\"\n").expect_err("rejected");
assert!(err.to_string().contains("background"), "{err}");
}

#[test]
fn an_unreadable_color_error_names_the_value() {
let err = Config::parse("[colors]\nheading = \"chartreuse\"\n").expect_err("rejected");
assert!(err.to_string().contains("chartreuse"), "{err}");
}
}
7 changes: 4 additions & 3 deletions crates/plannotator-tui/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use tui_markdown::{Options, StyleSheet};

use crate::doc::{BlockKind, Document};
use crate::srcmap::{LineOffsets, align};
use crate::theme::palette;
use crate::wrap::{Row, clip_line, wrap_line, wrap_table};

/// Rows of vertical space between blocks.
Expand All @@ -25,9 +26,9 @@ struct Styles;
impl StyleSheet for Styles {
fn heading(&self, level: u8) -> Style {
match level {
1 => Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
2 => Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
_ => Style::new().fg(Color::LightCyan).add_modifier(Modifier::BOLD),
1 => Style::new().fg(palette().heading).add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
2 => Style::new().fg(palette().heading).add_modifier(Modifier::BOLD),
_ => Style::new().fg(palette().subheading).add_modifier(Modifier::BOLD),
}
}
fn heading_marker(&self, _level: u8) -> &'static str {
Expand Down
Loading
Loading