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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Bug reports and pull requests are welcome. Read [CONTRIBUTING.md](https://github
Thanks to everyone who has sent a pull request:

- [@mariusrueve](https://github.com/mariusrueve) for the Conda and Pixi instructions ([#9](https://github.com/borghei/ink/pull/9)) and the [conda-forge package](https://github.com/conda-forge/ink-md-feedstock) behind them
- [@narma](https://github.com/narma) for honouring `--width full` and wide column counts in the reader ([#34](https://github.com/borghei/ink/pull/34))

## License

Expand Down
5 changes: 3 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ Every setting is optional. These are the defaults:
# terminal, or the name of a custom theme. Unset: match the terminal background.
# theme = "dark"

# Max text width in columns, 20 to 1000. Unset: the terminal width, up to 120.
# Plain output uses 80.
# Max text width: 20 to 1000 columns, or "narrow" (60), "wide" (100) or
# "full" (the whole reader pane). Unset: the terminal width, up to 120.
# Plain output uses 80 unless a column count is set.
# width = 90

# Line spacing: compact, normal, relaxed
Expand Down
18 changes: 8 additions & 10 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::selection::{self, Pos, SelMode, Selection};
use crate::stats;
use crate::theme;
use crate::toc::TocState;
use crate::Args;
use crate::{Args, Width};
use anyhow::Result;
use comrak::{parse_document, Arena};
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
Expand Down Expand Up @@ -2377,10 +2377,10 @@ fn build_tab(
// it at term_width - 4 (a requested --width wider than the terminal would
// otherwise overflow and be clipped at the right edge).
let available_width = term_width.saturating_sub(4).max(8);
let max_content_width = if args.full_width {
available_width
} else {
args.width.unwrap_or(120).clamp(8, available_width)
let max_content_width = match args.width {
Width::Full => available_width,
Width::Columns(n) => n.clamp(8, available_width),
Width::Auto => 120.min(available_width),
};
let center_margin = if term_width > max_content_width + 4 {
((term_width - max_content_width) / 2) as usize
Expand Down Expand Up @@ -2846,8 +2846,7 @@ mod section_tests {
Args {
inputs: Vec::new(),
theme: "dark".into(),
width: None,
full_width: false,
width: Width::Auto,
slides: false,
plain: false,
watch: false,
Expand Down Expand Up @@ -3091,8 +3090,7 @@ mod section_tests {
for terminal_width in [80, 200, 320] {
for toc_visible in [false, true] {
let mut args = test_args();
args.full_width = true;
args.width = Some(60); // A configured width must not limit full-width mode.
args.width = Width::Full;
args.toc = toc_visible;
let pane_width = effective_width(terminal_width, toc_visible);
let tab = build_tab(source.as_str(), "t.md", &args, pane_width, 0, &graphics);
Expand Down Expand Up @@ -3120,7 +3118,7 @@ mod section_tests {
(140, 80, 2, 76),
] {
let mut args = test_args();
args.width = Some(requested);
args.width = Width::Columns(requested);
let tab = build_tab(source.as_str(), "t.md", &args, terminal_width, 0, &graphics);
let first = tab.plain.iter().find(|line| !line.is_empty()).unwrap();
assert_eq!(first.len() - first.trim_start().len(), margin);
Expand Down
133 changes: 100 additions & 33 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ pub struct Cli {
pub theme: String,

/// Max rendering width in columns, 20-1000 (or: narrow, wide, full)
#[arg(short, long, value_parser = parse_width)]
pub width: Option<WidthArg>,
#[arg(short, long, value_parser = Width::parse)]
pub width: Option<Width>,

/// Presentation mode (split on ---)
#[arg(short, long)]
Expand Down Expand Up @@ -101,30 +101,50 @@ pub struct Cli {
pub ascii: bool,
}

/// A parsed `--width` value.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WidthArg {
/// A column count (`narrow` = 60, `wide` = 100).
/// How wide to lay out text: `--width` or the config file's `width`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Width {
/// Nothing asked for: the reader caps text at 120 columns, plain output
/// renders 80.
#[default]
Auto,
/// A column count in [`config::WIDTH_RANGE`] (`narrow` = 60, `wide` = 100).
/// The reader still keeps it inside the document pane.
Columns(u16),
/// `full`: no width cap.
/// `full`: the whole document pane in the reader; 80 columns in plain
/// output, where there is no pane.
Full,
}

/// `--width` parser: a column count in [`config::WIDTH_RANGE`] or a keyword.
fn parse_width(s: &str) -> Result<WidthArg, String> {
let (lo, hi) = (config::WIDTH_RANGE.start(), config::WIDTH_RANGE.end());
match s {
"narrow" => Ok(WidthArg::Columns(60)),
"wide" => Ok(WidthArg::Columns(100)),
"full" => Ok(WidthArg::Full),
_ => s
.parse::<u16>()
.ok()
.filter(|n| config::WIDTH_RANGE.contains(n))
.map(WidthArg::Columns)
.ok_or_else(|| {
format!("expected a column count from {lo} to {hi}, or one of: narrow, wide, full")
}),
impl Width {
/// Parse a column count in [`config::WIDTH_RANGE`] or a keyword
/// (`narrow`, `wide`, `full`). Shared by `--width` and the config file.
pub fn parse(s: &str) -> Result<Width, String> {
let (lo, hi) = (config::WIDTH_RANGE.start(), config::WIDTH_RANGE.end());
match s {
"narrow" => Ok(Width::Columns(60)),
"wide" => Ok(Width::Columns(100)),
"full" => Ok(Width::Full),
_ => s
.parse::<u16>()
.ok()
.filter(|n| config::WIDTH_RANGE.contains(n))
.map(Width::Columns)
.ok_or_else(|| {
format!(
"expected a column count from {lo} to {hi}, or one of: narrow, wide, full"
)
}),
}
}

/// The explicit column count, if one was asked for.
pub fn columns(self) -> Option<u16> {
match self {
Width::Columns(n) => Some(n),
Width::Auto | Width::Full => None,
}
}
}

Expand Down Expand Up @@ -200,13 +220,14 @@ pub enum ConfigAction {
}

/// Resolved arguments for the app.
///
/// Library users should build it with `..Default::default()` so that fields
/// added later do not break their code.
#[derive(Clone)]
pub struct Args {
pub inputs: Vec<String>,
pub theme: String,
pub width: Option<u16>,
/// Use the entire document pane in the interactive reader.
pub full_width: bool,
pub width: Width,
pub slides: bool,
pub plain: bool,
pub watch: bool,
Expand All @@ -219,6 +240,28 @@ pub struct Args {
pub clipboard: crate::clipboard::ClipboardMode,
}

/// The values `ink` uses with no flags and no config file, except that
/// `plain` is on: a library caller has no terminal for the reader.
impl Default for Args {
fn default() -> Self {
Args {
inputs: Vec::new(),
theme: "auto".to_string(),
width: Width::Auto,
slides: false,
plain: true,
watch: false,
toc: false,
images: crate::image::ImageMode::LocalOnly,
image_protocol: crate::graphics::ProtocolChoice::Auto,
frontmatter: false,
spacing: Spacing::Normal,
mouse_capture: true,
clipboard: crate::clipboard::ClipboardMode::Auto,
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)]
pub enum Spacing {
Compact,
Expand Down Expand Up @@ -369,7 +412,6 @@ pub fn run() -> Result<()> {
inputs,
theme,
width,
full_width: cli.width == Some(WidthArg::Full),
slides: cli.slides,
// The interactive reader needs a terminal: with stdout piped or
// redirected (`ink file.md | cat`), render plain output instead.
Expand Down Expand Up @@ -648,12 +690,11 @@ pub(crate) fn mouse_capture(
}
}

fn resolve_width(width: &Option<WidthArg>, config: &Option<config::Config>) -> Option<u16> {
match width {
Some(WidthArg::Columns(n)) => Some(*n),
Some(WidthArg::Full) => None,
None => config.as_ref().and_then(|c| c.width),
}
/// The `--width` flag wins over the config file's `width`.
fn resolve_width(width: &Option<Width>, config: &Option<config::Config>) -> Width {
width
.or_else(|| config.as_ref().and_then(|c| c.width))
.unwrap_or_default()
}

fn config_init(force: bool) -> Result<()> {
Expand All @@ -680,7 +721,7 @@ pub(crate) const STARTER_CONFIG: &str = r#"# ink configuration
# Color theme: dark, light, dracula, catppuccin, nord, tokyo-night, gruvbox, solarized, terminal
# theme = "catppuccin"

# Max rendering width in columns (or use --width on the CLI)
# Max rendering width: columns, or "narrow", "wide", "full" (or --width)
# width = 90

# Line spacing: compact, normal, relaxed
Expand Down Expand Up @@ -888,6 +929,32 @@ mod tests {
config::parse_config(toml, "test").config
}

#[test]
fn width_flag_wins_over_the_config_file() {
let c = cfg("width = \"full\"");
assert_eq!(resolve_width(&None, &c), Width::Full);
assert_eq!(
resolve_width(&Some(Width::Columns(70)), &c),
Width::Columns(70)
);
assert_eq!(
resolve_width(&Some(Width::Full), &cfg("width = 90")),
Width::Full
);
assert_eq!(resolve_width(&None, &None), Width::Auto);
}

#[test]
fn width_keywords_parse_the_same_everywhere() {
assert_eq!(Width::parse("full"), Ok(Width::Full));
assert_eq!(Width::parse("wide"), Ok(Width::Columns(100)));
assert_eq!(Width::parse("narrow"), Ok(Width::Columns(60)));
assert_eq!(Width::parse("140"), Ok(Width::Columns(140)));
assert!(Width::parse("19").is_err());
assert!(Width::parse("1001").is_err());
assert!(Width::parse("huge").is_err());
}

#[test]
fn no_mouse_flag_parses_before_and_after_a_subcommand() {
assert!(
Expand Down
74 changes: 61 additions & 13 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use std::path::{Path, PathBuf};
#[derive(Debug, Deserialize)]
pub struct Config {
pub theme: Option<String>,
pub width: Option<u16>,
/// Parsed by hand in [`parse_config`]: a number or a `--width` keyword.
#[serde(skip)]
pub width: Option<crate::cli::Width>,
pub spacing: Option<String>,
pub toc: Option<bool>,
pub frontmatter: Option<bool>,
Expand Down Expand Up @@ -162,15 +164,37 @@ pub fn parse_config(content: &str, path: &str) -> ConfigLoad {
warnings: &mut warnings,
};

let mut width: Option<u16> = w.take(&mut table, "", "width");
if let Some(n) = width.filter(|n| !WIDTH_RANGE.contains(n)) {
w.push(format!(
"`width` = {n} is out of range ({}..={}); ignoring it",
WIDTH_RANGE.start(),
WIDTH_RANGE.end()
));
width = None;
}
// A column count or a `--width` keyword (`narrow`, `wide`, `full`).
let width = match table.remove("width") {
None => None,
Some(toml::Value::Integer(n)) => {
match u16::try_from(n).ok().filter(|n| WIDTH_RANGE.contains(n)) {
Some(n) => Some(crate::cli::Width::Columns(n)),
None => {
w.push(format!(
"`width` = {n} is out of range ({}..={}); ignoring it",
WIDTH_RANGE.start(),
WIDTH_RANGE.end()
));
None
}
}
}
Some(toml::Value::String(s)) => match crate::cli::Width::parse(&s) {
Ok(width) => Some(width),
Err(e) => {
w.push(format!("`width` = \"{s}\": {e}; ignoring it"));
None
}
},
Some(other) => {
w.push(format!(
"`width`: expected a column count or one of: narrow, wide, full, found {}; ignoring it",
other.type_str()
));
None
}
};
let mut spacing: Option<String> = w.take(&mut table, "", "spacing");
if let Some(s) = spacing.as_deref().filter(|s| !SPACING_VALUES.contains(s)) {
w.push(format!(
Expand Down Expand Up @@ -275,6 +299,7 @@ pub fn set_theme(name: &str) -> anyhow::Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::Width;

fn platform() -> Option<PathBuf> {
Some(PathBuf::from("platform-config"))
Expand Down Expand Up @@ -302,7 +327,7 @@ mod tests {

#[test]
fn bad_value_warns_and_keeps_the_rest() {
let src = "width = \"wide\"\ntheme = \"nord\"\n[keybindings]\npreset = \"vim\"\n";
let src = "width = \"huge\"\ntheme = \"nord\"\n[keybindings]\npreset = \"vim\"\n";
let load = parse_config(src, "cfg.toml");
let cfg = load.config.expect("rest of the config still applies");
assert_eq!(cfg.width, None);
Expand All @@ -316,7 +341,7 @@ mod tests {
fn unknown_keys_warn_by_name() {
let src = "widht = 90\nwidth = 90\n[behavior]\nmouse_capure = false\n";
let load = parse_config(src, "cfg.toml");
assert_eq!(load.config.unwrap().width, Some(90));
assert_eq!(load.config.unwrap().width, Some(Width::Columns(90)));
let all = load.warnings.join("\n");
assert!(
all.contains("unknown config key `widht` in cfg.toml"),
Expand All @@ -326,6 +351,29 @@ mod tests {
assert_eq!(load.warnings.len(), 2);
}

#[test]
fn width_takes_the_cli_keywords() {
for (src, want) in [
("width = \"full\"", Width::Full),
("width = \"wide\"", Width::Columns(100)),
("width = \"narrow\"", Width::Columns(60)),
] {
let load = parse_config(src, "c");
assert!(load.warnings.is_empty(), "{src}: {:?}", load.warnings);
assert_eq!(load.config.unwrap().width, Some(want), "{src}");
}
}

#[test]
fn width_of_the_wrong_type_warns() {
let load = parse_config("width = true\ntheme = \"nord\"\n", "c");
let cfg = load.config.unwrap();
assert_eq!(cfg.width, None);
assert_eq!(cfg.theme.as_deref(), Some("nord"));
assert_eq!(load.warnings.len(), 1, "{:?}", load.warnings);
assert!(load.warnings[0].contains("`width`"), "{:?}", load.warnings);
}

#[test]
fn out_of_range_values_warn_and_fall_back() {
let load = parse_config("width = 5\nspacing = \"bogus\"\ntheme = 5\n", "c");
Expand All @@ -344,7 +392,7 @@ mod tests {
"c",
);
assert!(load.warnings.is_empty(), "{:?}", load.warnings);
assert_eq!(load.config.unwrap().width, Some(90));
assert_eq!(load.config.unwrap().width, Some(Width::Columns(90)));
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ pub mod toc;
pub mod watch;
pub mod wikilink;

pub use cli::{Args, Cli, Commands, ConfigAction, Spacing};
pub use cli::{Args, Cli, Commands, ConfigAction, Spacing, Width};
Loading
Loading