···2233// Builds the speech bubble (without trailing newline) and returns
44// it as one String.
55-pub fn render(text: &str, max_width: usize, min_width: usize) -> String {
55+pub fn render(text: &str, max_width: usize, min_width: usize, think: bool) -> String {
66 let wrapped_text: Vec<String> = textwrap::wrap(text, max_width)
77 .into_iter()
88 .map(|cow| cow.into_owned())
···2727 out.push('\n');
28282929 if wrapped_text.len() == 1 {
3030- // Single-line:
3131- // _______________________
3232- // < this is a single line >
3333- // -----------------------
3434- push_bubble_line(&mut out, &wrapped_text[0], inner, '<', '>');
3030+ // Single-line: < text > for speech, ( text ) for thought.
3131+ let (l, r) = if think { ('(', ')') } else { ('<', '>') };
3232+ push_bubble_line(&mut out, &wrapped_text[0], inner, l, r);
3533 } else {
3636- // Multi-line:
3737- // ___________________________________________________________
3838- // / this is a multi line text that will require rounding the \
3939- // | speech bubble corners. since it is more than two lines it |
4040- // \ will need to be extended with walls. /
4141- // -----------------------------------------------------------
3434+ // Multi-line: rounded speech corners + | walls,
3535+ // or all parens for thought (cowthink convention).
4236 let last = wrapped_text.len() - 1;
4337 for (i, line) in wrapped_text.iter().enumerate() {
4444- let (l, r) = if i == 0 {
3838+ let (l, r) = if think {
3939+ ('(', ')')
4040+ } else if i == 0 {
4541 ('/', '\\')
4642 } else if i == last {
4743 ('\\', '/')
+35-3
src/main.rs
···1212const MIN_BUBBLE_SIZE: usize = 20;
1313const MAX_BUBBLE_SIZE: usize = 60;
1414const MIN_CLONE_INNER: usize = 22;
1515+const DOLLY_MESSAGE_TAIL_POS: &[usize] = &[3];
1616+const DOLLYS_MESSAGE_TAIL_POS: &[usize] = &[3, 22];
15171618const MUSINGS: &[&str] = &[
1719 "I push, therefore I baa.",
···3638 /// Have Dolly share what's on her mind.
3739 #[arg(long)]
3840 muse: bool,
4141+4242+ /// Use a thought bubble instead of speech.
4343+ #[arg(long)]
4444+ think: bool,
3945}
40464147fn main() -> ExitCode {
···5056 };
51575258 let min_inner = if cli.clone { MIN_CLONE_INNER } else { 0 };
5353- let bubble: String = bubble::render(&message, message_width(), min_inner);
5959+ let bubble: String = bubble::render(&message, message_width(), min_inner, cli.think);
6060+6161+ let (art, tail_pos) = if cli.clone {
6262+ (DOLLYS, DOLLYS_MESSAGE_TAIL_POS)
6363+ } else {
6464+ (DOLLY, DOLLY_MESSAGE_TAIL_POS)
6565+ };
6666+ let tail = render_message_tail(tail_pos, cli.think);
54675568 // Lock stdout and use BufWriter to group writes to one syscall only.
5669 let stdout = io::stdout();
5770 let mut out = BufWriter::new(stdout.lock());
58715959- let art = if cli.clone { DOLLYS } else { DOLLY };
6060-6172 let _ = writeln!(out, "{bubble}");
7373+ let _ = out.write_all(tail.as_bytes());
6274 let _ = out.write_all(art.as_bytes());
63756476 match out.flush() {
···111123 .map(|d| d.as_millis())
112124 .unwrap_or(0);
113125 MUSINGS[(nanos as usize) % MUSINGS.len()]
126126+}
127127+128128+// Renders the two-line tail connecting bubble to Dolly's head. Each anchor
129129+// in `positions` starts a tail char at that column on line 1 and at col+1 on line 2.
130130+fn render_message_tail(positions: &[usize], think: bool) -> String {
131131+ let ch = if think { 'o' } else { '\\' };
132132+ let mut out = String::new();
133133+ for offset in 0..2 {
134134+ let mut col = 0;
135135+ for &pos in positions {
136136+ let target = pos + offset;
137137+ for _ in col..target {
138138+ out.push(' ');
139139+ }
140140+ out.push(ch);
141141+ col = target + 1;
142142+ }
143143+ out.push('\n');
144144+ }
145145+ out
114146}
115147116148// Computes the wrap width for the message inside the bubble.