Skip to content

Commit 7854d87

Browse files
committed
feat: add custom quotes
1 parent 5104520 commit 7854d87

File tree

5 files changed

+171
-49
lines changed

5 files changed

+171
-49
lines changed

cbundl.toml

+12
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,19 @@ enable = true
77

88
[banner.quote]
99
enable = true
10+
pick = "custom"
1011

1112
[formatter]
1213
# enable = true
1314
# path = "clang-format"
15+
16+
[[quote]]
17+
text = """
18+
Use a gun. And if that don't work...
19+
use more gun.
20+
"""
21+
author = "Dr. Dell Conagher"
22+
23+
[[quote]]
24+
text = "Democracy prevails once more."
25+
author = "Democracy Officer"

src/banner.rs

+1-5
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,7 @@ impl Banner {
5656
writeln!(out, " *")?;
5757

5858
if let Some(quotes) = self.quotes.as_ref() {
59-
let quote = if self.deterministic {
60-
quotes.get(0).expect("we dont have a single quote :'(")
61-
} else {
62-
quotes.random()
63-
};
59+
let quote = quotes.random();
6460

6561
writeln!(out, " *")?;
6662
quote.lines().try_for_each(|x| writeln!(out, " * {x}"))?;

src/cli.rs

+20-8
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,27 @@ pub fn run() -> Result<()> {
1919

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

22+
let bundler = Bundler {};
23+
24+
let quotes = config.enable_quote.then_some(Quotes {
25+
deterministic: config.deterministic,
26+
picker: config.quote_picker,
27+
custom_quotes: config.custom_quotes,
28+
});
29+
30+
let banner = (!config.no_banner).then_some(Banner {
31+
deterministic: config.deterministic,
32+
quotes,
33+
});
34+
35+
let formatter = (!config.no_format).then_some(Formatter {
36+
exe: config.formatter,
37+
});
38+
2239
let mut pipeline = Pipeline {
23-
bundler: Bundler {},
24-
banner: (!config.no_banner).then_some(Banner {
25-
quotes: config.enable_quote.then_some(Quotes {}),
26-
deterministic: config.deterministic,
27-
}),
28-
formatter: (!config.no_format).then_some(Formatter {
29-
exe: config.formatter,
30-
}),
40+
bundler,
41+
banner,
42+
formatter,
3143
};
3244

3345
let bundle = pipeline.process(&sources)?;

src/config.rs

+31-11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::consts::{
1111
CRATE_DESCRIPTION, DEFAULT_CONFIG_FILES, DEFAULT_FORMATTER, LONG_VERSION, SHORT_VERSION,
1212
};
1313
use crate::display::display_path;
14+
use crate::quotes::{CustomQuote, QuotePicker};
1415

1516
#[derive(Debug, Clone, Parser)]
1617
#[command(
@@ -70,6 +71,9 @@ struct File {
7071
bundle: Option<BundleSection>,
7172
banner: Option<BannerSection>,
7273
formatter: Option<FormatterSection>,
74+
75+
#[serde(rename = "quote")]
76+
quotes: Vec<CustomQuote>,
7377
}
7478

7579
#[derive(Debug, Clone, Deserialize)]
@@ -89,6 +93,9 @@ struct BannerSection {
8993
#[derive(Debug, Clone, Deserialize)]
9094
struct QuoteSection {
9195
enable: Option<bool>,
96+
97+
#[serde(rename = "pick")]
98+
picker: Option<QuotePicker>,
9299
}
93100

94101
#[derive(Debug, Clone, Deserialize)]
@@ -109,22 +116,16 @@ impl File {
109116
Some(x)
110117
}
111118

112-
fn read_many<'a, I>(paths: I) -> Option<Self>
119+
fn read_many<'a, I>(paths: I) -> Option<Result<Self>>
113120
where
114121
I: Iterator<Item = &'a Path>,
115122
{
116123
for path in paths {
117124
match Self::read(path) {
118125
Some(r) => {
119-
match r
120-
.with_context(|| format!("failed to read config `{}`", display_path(path)))
121-
{
122-
Ok(x) => return Some(x),
123-
Err(e) => {
124-
warn!("{e:#}");
125-
continue;
126-
}
127-
}
126+
return Some(r.with_context(|| {
127+
format!("failed to read config `{}`", display_path(path))
128+
}))
128129
}
129130
None => continue,
130131
}
@@ -168,6 +169,8 @@ pub struct Config {
168169

169170
pub no_banner: bool,
170171
pub enable_quote: bool,
172+
pub quote_picker: QuotePicker,
173+
pub custom_quotes: Vec<CustomQuote>,
171174

172175
pub no_format: bool,
173176
pub formatter: PathBuf,
@@ -188,7 +191,12 @@ impl Config {
188191

189192
Some(x)
190193
} else {
191-
File::read_many(DEFAULT_CONFIG_FILES.iter().copied().map(Path::new))
194+
let default_config_files = DEFAULT_CONFIG_FILES.iter().copied().map(Path::new);
195+
196+
match File::read_many(default_config_files) {
197+
Some(x) => Some(x?),
198+
None => None,
199+
}
192200
};
193201

194202
let deterministic = args
@@ -231,6 +239,16 @@ impl Config {
231239
.and_then(|x| x.enable)
232240
.unwrap_or(true);
233241

242+
let quote_picker = file
243+
.as_ref()
244+
.and_then(|x| x.banner.as_ref())
245+
.and_then(|x| x.quote.as_ref())
246+
.and_then(|x| x.picker.as_ref())
247+
.cloned()
248+
.unwrap_or(QuotePicker::All);
249+
250+
let custom_quotes = file.as_ref().map(|x| x.quotes.clone()).unwrap_or_default();
251+
234252
let no_format = args
235253
.flag("no_format")
236254
.or_else(|| {
@@ -260,6 +278,8 @@ impl Config {
260278

261279
no_banner,
262280
enable_quote,
281+
quote_picker,
282+
custom_quotes,
263283

264284
no_format,
265285
formatter,

src/quotes.rs

+107-25
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,151 @@
11
use std::marker::PhantomData;
2-
use std::slice::Iter as SliceIter;
2+
use std::vec::IntoIter as VecIter;
33

4-
use rand::seq::SliceRandom;
4+
use rand::seq::IteratorRandom;
5+
use serde::Deserialize;
6+
7+
#[derive(Clone)]
8+
enum QuoteInner<'a> {
9+
Builtin(&'static BuiltInQuote),
10+
Custom(&'a CustomQuote),
11+
}
512

613
#[derive(Clone)]
714
pub struct Quote<'a> {
8-
_marker: PhantomData<&'a ()>,
9-
quote: &'static BuiltInQuote,
15+
inner: QuoteInner<'a>,
1016
}
1117

1218
impl Quote<'_> {
13-
fn new(quote: &'static BuiltInQuote) -> Self {
14-
Self {
15-
_marker: PhantomData,
16-
quote,
17-
}
18-
}
19-
2019
pub fn lines(&self) -> QuoteLinesIter<'_> {
20+
let inner = match self.inner {
21+
// Don't listen to clippy, this is very much necessary so we can obtain `VecIter`.
22+
#[allow(clippy::unnecessary_to_owned)]
23+
QuoteInner::Builtin(quote) => quote.text.to_vec().into_iter(),
24+
25+
QuoteInner::Custom(quote) => quote
26+
.text
27+
.lines()
28+
.map(|x| x.trim_end())
29+
.collect::<Vec<&str>>()
30+
.into_iter(),
31+
};
32+
2133
QuoteLinesIter {
22-
inner: self.quote.text.iter(),
34+
_marker: PhantomData,
35+
inner,
2336
}
2437
}
2538

2639
pub fn author(&self) -> &str {
27-
self.quote.author
40+
match self.inner {
41+
QuoteInner::Builtin(x) => x.author,
42+
QuoteInner::Custom(x) => &x.author,
43+
}
2844
}
2945
}
3046

3147
#[derive(Clone)]
3248
pub struct QuoteLinesIter<'a> {
33-
inner: SliceIter<'a, &'a str>,
49+
_marker: PhantomData<&'a ()>,
50+
inner: VecIter<&'a str>,
3451
}
3552

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

3956
fn next(&mut self) -> Option<Self::Item> {
40-
self.inner.next().copied()
57+
self.inner.next()
4158
}
4259

4360
fn size_hint(&self) -> (usize, Option<usize>) {
4461
self.inner.size_hint()
4562
}
4663
}
4764

65+
#[derive(Debug, Clone, Deserialize)]
66+
#[serde(rename_all = "snake_case")]
67+
pub enum QuotePicker {
68+
All,
69+
Custom,
70+
Builtin,
71+
}
72+
73+
#[derive(Debug, Clone, Deserialize)]
74+
pub struct CustomQuote {
75+
pub text: String,
76+
pub author: String,
77+
}
78+
4879
#[derive(Debug, Clone)]
49-
pub struct Quotes {}
80+
pub struct Quotes {
81+
pub deterministic: bool,
82+
pub picker: QuotePicker,
83+
pub custom_quotes: Vec<CustomQuote>,
84+
}
5085

5186
impl Quotes {
52-
pub fn get(&self, i: usize) -> Option<Quote<'_>> {
53-
BUILT_IN_QUOTES.get(i).map(Quote::new)
87+
fn get_builtin_quote(&self) -> Quote<'_> {
88+
let inner = if self.deterministic {
89+
&BUILT_IN_QUOTES[0]
90+
} else {
91+
choose_random(BUILT_IN_QUOTES.iter()).unwrap()
92+
};
93+
94+
Quote {
95+
inner: QuoteInner::Builtin(inner),
96+
}
97+
}
98+
99+
fn get_custom_quote(&self) -> Option<Quote<'_>> {
100+
let inner = if self.deterministic {
101+
self.custom_quotes.first()
102+
} else {
103+
choose_random(self.custom_quotes.iter())
104+
}?;
105+
106+
Some(Quote {
107+
inner: QuoteInner::Custom(inner),
108+
})
109+
}
110+
111+
fn get_any_quote(&self) -> Quote<'_> {
112+
if self.deterministic {
113+
Quote {
114+
inner: QuoteInner::Builtin(&BUILT_IN_QUOTES[0]),
115+
}
116+
} else {
117+
let builtin = BUILT_IN_QUOTES.iter().map(|x| Quote {
118+
inner: QuoteInner::Builtin(x),
119+
});
120+
121+
let custom = self.custom_quotes.iter().map(|x| Quote {
122+
inner: QuoteInner::Custom(x),
123+
});
124+
125+
let quotes = builtin.chain(custom);
126+
127+
// SAFETY: `builtin` has at least one element, so `quotes` must also have
128+
// at least one element.
129+
choose_random(quotes).unwrap().clone()
130+
}
54131
}
55132

56133
pub fn random(&self) -> Quote<'_> {
57-
Quote::new(choose_random_quote())
134+
match self.picker {
135+
QuotePicker::All => self.get_any_quote(),
136+
QuotePicker::Builtin => self.get_builtin_quote(),
137+
QuotePicker::Custom => self
138+
.get_custom_quote()
139+
.unwrap_or_else(|| self.get_builtin_quote()),
140+
}
58141
}
59142
}
60143

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

63-
fn choose_random_quote() -> &'static BuiltInQuote {
64-
let mut rng = rand::thread_rng();
65-
66-
BUILT_IN_QUOTES
67-
.choose(&mut rng)
68-
.expect("we have no quotes :(")
146+
fn choose_random<T, I>(iter: I) -> Option<T>
147+
where
148+
I: Iterator<Item = T>,
149+
{
150+
iter.choose(&mut rand::thread_rng())
69151
}

0 commit comments

Comments
 (0)