Skip to content

Commit

Permalink
feat: add custom quotes
Browse files Browse the repository at this point in the history
  • Loading branch information
threadexio committed Jan 2, 2025
1 parent 5104520 commit 7854d87
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 49 deletions.
12 changes: 12 additions & 0 deletions cbundl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,19 @@ enable = true

[banner.quote]
enable = true
pick = "custom"

[formatter]
# enable = true
# path = "clang-format"

[[quote]]
text = """
Use a gun. And if that don't work...
use more gun.
"""
author = "Dr. Dell Conagher"

[[quote]]
text = "Democracy prevails once more."
author = "Democracy Officer"
6 changes: 1 addition & 5 deletions src/banner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,7 @@ impl Banner {
writeln!(out, " *")?;

if let Some(quotes) = self.quotes.as_ref() {
let quote = if self.deterministic {
quotes.get(0).expect("we dont have a single quote :'(")
} else {
quotes.random()
};
let quote = quotes.random();

writeln!(out, " *")?;
quote.lines().try_for_each(|x| writeln!(out, " * {x}"))?;
Expand Down
28 changes: 20 additions & 8 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,27 @@ pub fn run() -> Result<()> {

let sources = Sources::new(config.entry)?;

let bundler = Bundler {};

let quotes = config.enable_quote.then_some(Quotes {
deterministic: config.deterministic,
picker: config.quote_picker,
custom_quotes: config.custom_quotes,
});

let banner = (!config.no_banner).then_some(Banner {
deterministic: config.deterministic,
quotes,
});

let formatter = (!config.no_format).then_some(Formatter {
exe: config.formatter,
});

let mut pipeline = Pipeline {
bundler: Bundler {},
banner: (!config.no_banner).then_some(Banner {
quotes: config.enable_quote.then_some(Quotes {}),
deterministic: config.deterministic,
}),
formatter: (!config.no_format).then_some(Formatter {
exe: config.formatter,
}),
bundler,
banner,
formatter,
};

let bundle = pipeline.process(&sources)?;
Expand Down
42 changes: 31 additions & 11 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::consts::{
CRATE_DESCRIPTION, DEFAULT_CONFIG_FILES, DEFAULT_FORMATTER, LONG_VERSION, SHORT_VERSION,
};
use crate::display::display_path;
use crate::quotes::{CustomQuote, QuotePicker};

#[derive(Debug, Clone, Parser)]
#[command(
Expand Down Expand Up @@ -70,6 +71,9 @@ struct File {
bundle: Option<BundleSection>,
banner: Option<BannerSection>,
formatter: Option<FormatterSection>,

#[serde(rename = "quote")]
quotes: Vec<CustomQuote>,
}

#[derive(Debug, Clone, Deserialize)]
Expand All @@ -89,6 +93,9 @@ struct BannerSection {
#[derive(Debug, Clone, Deserialize)]
struct QuoteSection {
enable: Option<bool>,

#[serde(rename = "pick")]
picker: Option<QuotePicker>,
}

#[derive(Debug, Clone, Deserialize)]
Expand All @@ -109,22 +116,16 @@ impl File {
Some(x)
}

fn read_many<'a, I>(paths: I) -> Option<Self>
fn read_many<'a, I>(paths: I) -> Option<Result<Self>>
where
I: Iterator<Item = &'a Path>,
{
for path in paths {
match Self::read(path) {
Some(r) => {
match r
.with_context(|| format!("failed to read config `{}`", display_path(path)))
{
Ok(x) => return Some(x),
Err(e) => {
warn!("{e:#}");
continue;
}
}
return Some(r.with_context(|| {
format!("failed to read config `{}`", display_path(path))
}))
}
None => continue,
}
Expand Down Expand Up @@ -168,6 +169,8 @@ pub struct Config {

pub no_banner: bool,
pub enable_quote: bool,
pub quote_picker: QuotePicker,
pub custom_quotes: Vec<CustomQuote>,

pub no_format: bool,
pub formatter: PathBuf,
Expand All @@ -188,7 +191,12 @@ impl Config {

Some(x)
} else {
File::read_many(DEFAULT_CONFIG_FILES.iter().copied().map(Path::new))
let default_config_files = DEFAULT_CONFIG_FILES.iter().copied().map(Path::new);

match File::read_many(default_config_files) {
Some(x) => Some(x?),
None => None,
}
};

let deterministic = args
Expand Down Expand Up @@ -231,6 +239,16 @@ impl Config {
.and_then(|x| x.enable)
.unwrap_or(true);

let quote_picker = file
.as_ref()
.and_then(|x| x.banner.as_ref())
.and_then(|x| x.quote.as_ref())
.and_then(|x| x.picker.as_ref())
.cloned()
.unwrap_or(QuotePicker::All);

let custom_quotes = file.as_ref().map(|x| x.quotes.clone()).unwrap_or_default();

let no_format = args
.flag("no_format")
.or_else(|| {
Expand Down Expand Up @@ -260,6 +278,8 @@ impl Config {

no_banner,
enable_quote,
quote_picker,
custom_quotes,

no_format,
formatter,
Expand Down
132 changes: 107 additions & 25 deletions src/quotes.rs
Original file line number Diff line number Diff line change
@@ -1,69 +1,151 @@
use std::marker::PhantomData;
use std::slice::Iter as SliceIter;
use std::vec::IntoIter as VecIter;

use rand::seq::SliceRandom;
use rand::seq::IteratorRandom;
use serde::Deserialize;

#[derive(Clone)]
enum QuoteInner<'a> {
Builtin(&'static BuiltInQuote),
Custom(&'a CustomQuote),
}

#[derive(Clone)]
pub struct Quote<'a> {
_marker: PhantomData<&'a ()>,
quote: &'static BuiltInQuote,
inner: QuoteInner<'a>,
}

impl Quote<'_> {
fn new(quote: &'static BuiltInQuote) -> Self {
Self {
_marker: PhantomData,
quote,
}
}

pub fn lines(&self) -> QuoteLinesIter<'_> {
let inner = match self.inner {
// Don't listen to clippy, this is very much necessary so we can obtain `VecIter`.
#[allow(clippy::unnecessary_to_owned)]
QuoteInner::Builtin(quote) => quote.text.to_vec().into_iter(),

QuoteInner::Custom(quote) => quote
.text
.lines()
.map(|x| x.trim_end())
.collect::<Vec<&str>>()
.into_iter(),
};

QuoteLinesIter {
inner: self.quote.text.iter(),
_marker: PhantomData,
inner,
}
}

pub fn author(&self) -> &str {
self.quote.author
match self.inner {
QuoteInner::Builtin(x) => x.author,
QuoteInner::Custom(x) => &x.author,
}
}
}

#[derive(Clone)]
pub struct QuoteLinesIter<'a> {
inner: SliceIter<'a, &'a str>,
_marker: PhantomData<&'a ()>,
inner: VecIter<&'a str>,
}

impl<'a> Iterator for QuoteLinesIter<'a> {
type Item = &'a str;

fn next(&mut self) -> Option<Self::Item> {
self.inner.next().copied()
self.inner.next()
}

fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuotePicker {
All,
Custom,
Builtin,
}

#[derive(Debug, Clone, Deserialize)]
pub struct CustomQuote {
pub text: String,
pub author: String,
}

#[derive(Debug, Clone)]
pub struct Quotes {}
pub struct Quotes {
pub deterministic: bool,
pub picker: QuotePicker,
pub custom_quotes: Vec<CustomQuote>,
}

impl Quotes {
pub fn get(&self, i: usize) -> Option<Quote<'_>> {
BUILT_IN_QUOTES.get(i).map(Quote::new)
fn get_builtin_quote(&self) -> Quote<'_> {
let inner = if self.deterministic {
&BUILT_IN_QUOTES[0]
} else {
choose_random(BUILT_IN_QUOTES.iter()).unwrap()
};

Quote {
inner: QuoteInner::Builtin(inner),
}
}

fn get_custom_quote(&self) -> Option<Quote<'_>> {
let inner = if self.deterministic {
self.custom_quotes.first()
} else {
choose_random(self.custom_quotes.iter())
}?;

Some(Quote {
inner: QuoteInner::Custom(inner),
})
}

fn get_any_quote(&self) -> Quote<'_> {
if self.deterministic {
Quote {
inner: QuoteInner::Builtin(&BUILT_IN_QUOTES[0]),
}
} else {
let builtin = BUILT_IN_QUOTES.iter().map(|x| Quote {
inner: QuoteInner::Builtin(x),
});

let custom = self.custom_quotes.iter().map(|x| Quote {
inner: QuoteInner::Custom(x),
});

let quotes = builtin.chain(custom);

// SAFETY: `builtin` has at least one element, so `quotes` must also have
// at least one element.
choose_random(quotes).unwrap().clone()
}
}

pub fn random(&self) -> Quote<'_> {
Quote::new(choose_random_quote())
match self.picker {
QuotePicker::All => self.get_any_quote(),
QuotePicker::Builtin => self.get_builtin_quote(),
QuotePicker::Custom => self
.get_custom_quote()
.unwrap_or_else(|| self.get_builtin_quote()),
}
}
}

include!(concat!(env!("OUT_DIR"), "/quotes.rs"));

fn choose_random_quote() -> &'static BuiltInQuote {
let mut rng = rand::thread_rng();

BUILT_IN_QUOTES
.choose(&mut rng)
.expect("we have no quotes :(")
fn choose_random<T, I>(iter: I) -> Option<T>
where
I: Iterator<Item = T>,
{
iter.choose(&mut rand::thread_rng())
}

0 comments on commit 7854d87

Please sign in to comment.