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
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ one; without the flag the picker comes first, as it always has.
`plannotator-tui config` prints the file's path and the values in effect. The `herdr/`
directory in this repo is the development manifest; users should install Herdr Annotate.

The same file chooses the theme:

```toml
[ui]
theme = "auto" # auto (ask the terminal, default) | light | dark
```

On `auto` the viewer asks the terminal for its background colour once at startup and uses a
light palette when it finds one; a terminal that does not answer keeps the dark palette it
has always used. The question costs one round trip before the screen is drawn, and a key
pressed into that window is read along with the reply and lost, so set the theme outright if
you habitually type ahead. `light` and `dark` skip the
question, and `PLANNOTATOR_TUI_THEME=light|dark` does the same for one run — the variable
wins over the file, and `plannotator-tui config` prints whichever is in effect. Only the
backgrounds plannotator-tui paints itself change; the document keeps your terminal's own
colours either way.

Actions forwarded by Herdr Mirror default to a split beside the invoking remote
pane. Mirror does not preserve overlay presentation, and Herdr 0.8.2 opens an
overlay in its server's active tab, which can differ from the tab you are viewing.
Expand Down
3 changes: 3 additions & 0 deletions crates/plannotator-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pulldown-cmark = { version = "0.13.4", default-features = false }
ratatui = "0.30"
# highlight-code pulls syntect + a C oniguruma build; plain code blocks keep the build pure Rust.
tui-markdown = { version = "0.3.9", default-features = false }
# OSC 11 by hand is forty lines and a dozen terminal quirks; this reads the reply off its own
# tty handle, so it cannot race the event loop's reader. Three small crates, one author.
terminal-colorsaurus = "1.0.3"

[dev-dependencies]
rusqlite = { version = "0.31.0", features = ["bundled"] }
Expand Down
23 changes: 9 additions & 14 deletions crates/plannotator-tui/src/app/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph};
use unicode_width::UnicodeWidthStr;

use super::{App, Focus, GUTTER, Geometry, Mode, TOOLBAR, glyph, label};
use crate::theme::palette;
use crate::wrap::wrap_line;

const RAIL_WIDTH: u16 = 36;
Expand All @@ -22,12 +23,6 @@ const TREE_WIDTH: u16 = 28;
pub(super) const TREE_MIN_TOTAL_WIDTH: u16 = 120;
const COMPOSE_WIDTH: u16 = 48;

pub(crate) const COMMENT_BG: Color = Color::Indexed(58);
pub(crate) const APPROVE_BG: Color = Color::Indexed(22);
const BLOCK_BG: Color = Color::Indexed(236);
const TOOLBAR_BG: Color = Color::Indexed(238);
const CURSOR_BG: Color = Color::Indexed(240);

fn accent(kind: Kind) -> Color {
match kind {
Kind::Comment => Color::Yellow,
Expand Down Expand Up @@ -129,7 +124,7 @@ impl App {
if open_path == Some(row.path.as_path()) {
style = style.bold().fg(Color::Cyan);
}
let row_bg = (focused && i == self.tree_cursor).then_some(BLOCK_BG);
let row_bg = (focused && i == self.tree_cursor).then(|| palette().block_bg);
if let Some(bg) = row_bg {
style = style.bg(bg);
}
Expand Down Expand Up @@ -165,7 +160,7 @@ impl App {
if block == self.selected && !text_selection_active && self.pending.is_none() && doc_focused {
buf.set_style(
Rect { x: doc.x, y: screen_y, width: doc.width, height: 1 },
Style::new().bg(BLOCK_BG),
Style::new().bg(palette().block_bg),
);
}

Expand All @@ -180,8 +175,8 @@ impl App {
let Some(kind) = kind else { continue };
row_has_annotation = true;
let style = match kind {
Kind::Comment => Style::new().bg(COMMENT_BG),
Kind::LooksGood => Style::new().bg(APPROVE_BG),
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)
}
Expand All @@ -194,7 +189,7 @@ impl App {
let end = cols.end.min(usize::from(doc.width)) as u16;
if end > start {
let rect = Rect { x: doc.x + start, y: screen_y, width: end - start, height: 1 };
buf.set_style(rect, Style::new().add_modifier(Modifier::REVERSED));
buf.set_style(rect, palette().selection);
}
}

Expand All @@ -204,7 +199,7 @@ impl App {
&& row_index == self.cursor.0
{
let x = doc.x + (self.cursor.1.min(usize::from(doc.width).saturating_sub(1))) as u16;
buf.set_style(Rect { x, y: screen_y, width: 1, height: 1 }, Style::new().bg(CURSOR_BG));
buf.set_style(Rect { x, y: screen_y, width: 1, height: 1 }, palette().cursor);
}

let marker = match (block == self.selected, row_has_annotation) {
Expand Down Expand Up @@ -244,12 +239,12 @@ impl App {
let Some(rect) = self.float_origin(1, width) else { return };
frame.render_widget(Clear, rect);
let buf = frame.buffer_mut();
buf.set_style(rect, Style::new().bg(TOOLBAR_BG));
buf.set_style(rect, Style::new().bg(palette().toolbar_bg));
let mut x = rect.x + 1;
let mut spans = [0..0, 0..0, 0..0];
for ((label, item), span) in labels.iter().zip(TOOLBAR.iter()).zip(spans.iter_mut()) {
let w = label.width() as u16;
let style = Style::new().fg(accent(item.3)).bg(TOOLBAR_BG).bold();
let style = Style::new().fg(accent(item.3)).bg(palette().toolbar_bg).bold();
buf.set_span(x, rect.y, &Span::styled(label.as_str(), style), w);
*span = x..x + w;
x += w;
Expand Down
8 changes: 5 additions & 3 deletions crates/plannotator-tui/src/app/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ use unicode_width::UnicodeWidthStr;

use super::App;
use super::send::SendState;
use crate::theme::palette;

// These three set their own foreground as well as their background, so they read the same
// on a light terminal as on a dark one; only the idle button borrows the theme's grey.
const SEND_BG: Color = Color::Indexed(30);
const IDLE_BG: Color = Color::Indexed(238);
const SENT_BG: Color = Color::Indexed(22);
const BLOCKED_BG: Color = Color::Indexed(58);

Expand Down Expand Up @@ -70,7 +72,7 @@ impl App {
}
Button::Review => {
self.geometry.review_button = Some(rect);
Style::new().fg(Color::Cyan).bg(IDLE_BG)
Style::new().fg(Color::Cyan).bg(palette().toolbar_bg)
}
};
frame.buffer_mut().set_span(rect.x, rect.y, &Span::styled(label, style), rect.width);
Expand All @@ -81,7 +83,7 @@ impl App {
fn button_style(&self) -> Style {
match &self.send_state {
SendState::Ready if self.send_count() == 0 => {
Style::new().fg(Color::Gray).bg(IDLE_BG).add_modifier(Modifier::DIM)
Style::new().fg(palette().idle_fg).bg(palette().toolbar_bg).add_modifier(Modifier::DIM)
}
SendState::Ready => Style::new().fg(Color::Black).bg(SEND_BG).bold(),
SendState::Sent => Style::new().fg(Color::Black).bg(SENT_BG).bold(),
Expand Down
3 changes: 2 additions & 1 deletion crates/plannotator-tui/src/app/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph};
use unicode_width::UnicodeWidthStr as _;

use super::{App, Mode};
use crate::theme::palette;

#[cfg(test)]
mod tests;
Expand Down Expand Up @@ -190,7 +191,7 @@ impl App {
let style = if !self.action_applies(action) {
Style::new().dim()
} else if index == self.menu_cursor {
Style::new().reversed()
palette().selection
} else {
Style::new()
};
Expand Down
3 changes: 2 additions & 1 deletion crates/plannotator-tui/src/app/pick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use unicode_width::UnicodeWidthStr as _;

use super::{App, Mode, Open};
use crate::last::message_source;
use crate::theme::palette;

const PICK_MAX_WIDTH: u16 = 90;

Expand Down Expand Up @@ -160,7 +161,7 @@ impl App {
pick_rows.push((row, index));
let text =
fit(&pick_label(message, self.clock_offset), usize::from(inner.width).saturating_sub(1));
let style = if index == self.pick_cursor { Style::new().reversed() } else { Style::new() };
let style = if index == self.pick_cursor { palette().selection } else { Style::new() };
Line::from(Span::styled(format!(" {text}"), style))
})
.collect();
Expand Down
19 changes: 15 additions & 4 deletions crates/plannotator-tui/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ pub(crate) fn run(args: &[String]) -> Result<()> {
fn show_config() -> Result<()> {
let home = std::env::home_dir().unwrap_or_else(|| PathBuf::from("/"));
let path = crate::config::config_path(|k| std::env::var(k).ok(), &home);
let config = Config::load_from(&path)?;
let mut config = Config::load_from(&path)?;
// `PLANNOTATOR_TUI_THEME` overrides the file, so the file's value is not the effective one.
config.ui.theme = crate::theme::effective_setting(|key| std::env::var(key).ok(), config.ui.theme)?;
let state = if path.is_file() { "" } else { " (not present; defaults)" };
println!("# {}{state}", path.display());
print!("{}", config.to_toml()?);
Expand Down Expand Up @@ -257,6 +259,15 @@ fn interactive(path: &PathBuf) -> Result<()> {

/// Own the terminal for one app: `build` gets the document width the screen allows.
pub(crate) fn run_ui(build: impl FnOnce(usize) -> Result<App>) -> Result<()> {
// Settle the palette before the screen is ours: the background-colour query talks to
// the terminal directly, and it must not race the alternate screen or the event loop.
crate::theme::install(crate::theme::resolve(
|key| std::env::var(key).ok(),
// 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.
Config::load().map(|config| config.ui.theme).unwrap_or_default(),
crate::theme::detect,
)?);
let mut terminal = ratatui::init();
execute!(stdout(), EnableMouseCapture)?;
let _ = execute!(stdout(), EnableBracketedPaste);
Expand Down Expand Up @@ -355,7 +366,7 @@ fn snapshot(
menu: bool,
) -> Result<()> {
use ratatui::backend::TestBackend;
use ratatui::style::{Color, Modifier};
use ratatui::style::Modifier;
let mut terminal = ratatui::Terminal::new(TestBackend::new(cols, rows))?;
let mut app = open_app(path, doc_width(cols), false)?;
terminal.draw(|frame| app.draw(frame))?;
Expand All @@ -380,9 +391,9 @@ fn snapshot(
'%'
} else if style.add_modifier.contains(Modifier::CROSSED_OUT) {
'-'
} else if style.bg == Some(Color::Indexed(22)) {
} else if style.bg == Some(crate::theme::palette().approve_bg) {
'+'
} else if style.bg == Some(Color::Indexed(58)) {
} else if style.bg == Some(crate::theme::palette().comment_bg) {
'#'
} else {
' '
Expand Down
31 changes: 31 additions & 0 deletions crates/plannotator-tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,22 @@ use std::str::FromStr;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::theme::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,
}

/// How the app looks. One key so far; the colours themselves are not configurable yet.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct UiConfig {
/// `auto` asks the terminal for its background colour; `light` and `dark` skip the
/// question. `PLANNOTATOR_TUI_THEME` overrides whatever is written here.
pub(crate) theme: ThemeSetting,
}

/// How plannotator-tui opens inside Herdr.
Expand Down Expand Up @@ -213,6 +225,25 @@ mod tests {
);
}

#[test]
fn the_theme_defaults_to_asking_the_terminal() {
assert_eq!(Config::default().ui.theme, ThemeSetting::Auto);
assert_eq!(Config::parse("").expect("parses").ui.theme, ThemeSetting::Auto);
}

#[test]
fn a_configured_theme_is_read_and_leaves_the_rest_alone() {
let config = Config::parse("[ui]\ntheme = \"light\"\n").expect("parses");
assert_eq!(config.ui.theme, ThemeSetting::Light);
assert_eq!(config.herdr.placement, Placement::Overlay);
}

#[test]
fn an_unknown_theme_error_names_the_value() {
let err = Config::parse("[ui]\ntheme = \"solarized\"\n").expect_err("rejected");
assert!(err.to_string().contains("solarized"), "{err}");
}

#[test]
fn roundtrips_through_toml() {
let text = Config::default().to_toml().expect("serializes");
Expand Down
1 change: 1 addition & 0 deletions crates/plannotator-tui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod last;
mod layout;
mod srcmap;
mod store;
mod theme;
mod tree;
mod workspace_paths;
mod wrap;
Expand Down
Loading
Loading