A sheep speaks.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial commit

Kevin aac9bc7e

+558
+1
.gitignore
··· 1 + /target
+269
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "anstream" 7 + version = "1.0.0" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" 10 + dependencies = [ 11 + "anstyle", 12 + "anstyle-parse", 13 + "anstyle-query", 14 + "anstyle-wincon", 15 + "colorchoice", 16 + "is_terminal_polyfill", 17 + "utf8parse", 18 + ] 19 + 20 + [[package]] 21 + name = "anstyle" 22 + version = "1.0.14" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" 25 + 26 + [[package]] 27 + name = "anstyle-parse" 28 + version = "1.0.0" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" 31 + dependencies = [ 32 + "utf8parse", 33 + ] 34 + 35 + [[package]] 36 + name = "anstyle-query" 37 + version = "1.1.5" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 40 + dependencies = [ 41 + "windows-sys", 42 + ] 43 + 44 + [[package]] 45 + name = "anstyle-wincon" 46 + version = "3.0.11" 47 + source = "registry+https://github.com/rust-lang/crates.io-index" 48 + checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 49 + dependencies = [ 50 + "anstyle", 51 + "once_cell_polyfill", 52 + "windows-sys", 53 + ] 54 + 55 + [[package]] 56 + name = "bitflags" 57 + version = "2.11.1" 58 + source = "registry+https://github.com/rust-lang/crates.io-index" 59 + checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" 60 + 61 + [[package]] 62 + name = "clap" 63 + version = "4.6.1" 64 + source = "registry+https://github.com/rust-lang/crates.io-index" 65 + checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" 66 + dependencies = [ 67 + "clap_builder", 68 + "clap_derive", 69 + ] 70 + 71 + [[package]] 72 + name = "clap_builder" 73 + version = "4.6.0" 74 + source = "registry+https://github.com/rust-lang/crates.io-index" 75 + checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" 76 + dependencies = [ 77 + "anstream", 78 + "anstyle", 79 + "clap_lex", 80 + "strsim", 81 + ] 82 + 83 + [[package]] 84 + name = "clap_derive" 85 + version = "4.6.1" 86 + source = "registry+https://github.com/rust-lang/crates.io-index" 87 + checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" 88 + dependencies = [ 89 + "heck", 90 + "proc-macro2", 91 + "quote", 92 + "syn", 93 + ] 94 + 95 + [[package]] 96 + name = "clap_lex" 97 + version = "1.1.0" 98 + source = "registry+https://github.com/rust-lang/crates.io-index" 99 + checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" 100 + 101 + [[package]] 102 + name = "colorchoice" 103 + version = "1.0.5" 104 + source = "registry+https://github.com/rust-lang/crates.io-index" 105 + checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" 106 + 107 + [[package]] 108 + name = "dollysay" 109 + version = "0.1.0" 110 + dependencies = [ 111 + "clap", 112 + "terminal_size", 113 + "textwrap", 114 + "unicode-width", 115 + ] 116 + 117 + [[package]] 118 + name = "errno" 119 + version = "0.3.14" 120 + source = "registry+https://github.com/rust-lang/crates.io-index" 121 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 122 + dependencies = [ 123 + "libc", 124 + "windows-sys", 125 + ] 126 + 127 + [[package]] 128 + name = "heck" 129 + version = "0.5.0" 130 + source = "registry+https://github.com/rust-lang/crates.io-index" 131 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 132 + 133 + [[package]] 134 + name = "is_terminal_polyfill" 135 + version = "1.70.2" 136 + source = "registry+https://github.com/rust-lang/crates.io-index" 137 + checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 138 + 139 + [[package]] 140 + name = "libc" 141 + version = "0.2.186" 142 + source = "registry+https://github.com/rust-lang/crates.io-index" 143 + checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" 144 + 145 + [[package]] 146 + name = "linux-raw-sys" 147 + version = "0.12.1" 148 + source = "registry+https://github.com/rust-lang/crates.io-index" 149 + checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" 150 + 151 + [[package]] 152 + name = "once_cell_polyfill" 153 + version = "1.70.2" 154 + source = "registry+https://github.com/rust-lang/crates.io-index" 155 + checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 156 + 157 + [[package]] 158 + name = "proc-macro2" 159 + version = "1.0.106" 160 + source = "registry+https://github.com/rust-lang/crates.io-index" 161 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 162 + dependencies = [ 163 + "unicode-ident", 164 + ] 165 + 166 + [[package]] 167 + name = "quote" 168 + version = "1.0.45" 169 + source = "registry+https://github.com/rust-lang/crates.io-index" 170 + checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" 171 + dependencies = [ 172 + "proc-macro2", 173 + ] 174 + 175 + [[package]] 176 + name = "rustix" 177 + version = "1.1.4" 178 + source = "registry+https://github.com/rust-lang/crates.io-index" 179 + checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" 180 + dependencies = [ 181 + "bitflags", 182 + "errno", 183 + "libc", 184 + "linux-raw-sys", 185 + "windows-sys", 186 + ] 187 + 188 + [[package]] 189 + name = "smawk" 190 + version = "0.3.2" 191 + source = "registry+https://github.com/rust-lang/crates.io-index" 192 + checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 193 + 194 + [[package]] 195 + name = "strsim" 196 + version = "0.11.1" 197 + source = "registry+https://github.com/rust-lang/crates.io-index" 198 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 199 + 200 + [[package]] 201 + name = "syn" 202 + version = "2.0.117" 203 + source = "registry+https://github.com/rust-lang/crates.io-index" 204 + checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" 205 + dependencies = [ 206 + "proc-macro2", 207 + "quote", 208 + "unicode-ident", 209 + ] 210 + 211 + [[package]] 212 + name = "terminal_size" 213 + version = "0.4.4" 214 + source = "registry+https://github.com/rust-lang/crates.io-index" 215 + checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" 216 + dependencies = [ 217 + "rustix", 218 + "windows-sys", 219 + ] 220 + 221 + [[package]] 222 + name = "textwrap" 223 + version = "0.16.2" 224 + source = "registry+https://github.com/rust-lang/crates.io-index" 225 + checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 226 + dependencies = [ 227 + "smawk", 228 + "unicode-linebreak", 229 + "unicode-width", 230 + ] 231 + 232 + [[package]] 233 + name = "unicode-ident" 234 + version = "1.0.24" 235 + source = "registry+https://github.com/rust-lang/crates.io-index" 236 + checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 237 + 238 + [[package]] 239 + name = "unicode-linebreak" 240 + version = "0.1.5" 241 + source = "registry+https://github.com/rust-lang/crates.io-index" 242 + checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 243 + 244 + [[package]] 245 + name = "unicode-width" 246 + version = "0.2.2" 247 + source = "registry+https://github.com/rust-lang/crates.io-index" 248 + checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" 249 + 250 + [[package]] 251 + name = "utf8parse" 252 + version = "0.2.2" 253 + source = "registry+https://github.com/rust-lang/crates.io-index" 254 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 255 + 256 + [[package]] 257 + name = "windows-link" 258 + version = "0.2.1" 259 + source = "registry+https://github.com/rust-lang/crates.io-index" 260 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 261 + 262 + [[package]] 263 + name = "windows-sys" 264 + version = "0.61.2" 265 + source = "registry+https://github.com/rust-lang/crates.io-index" 266 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 267 + dependencies = [ 268 + "windows-link", 269 + ]
+14
Cargo.toml
··· 1 + [package] 2 + name = "dollysay" 3 + version = "0.1.0" 4 + edition = "2024" 5 + description = "A cloned sheep speaks. cowsay for Tangled." 6 + license = "MIT" 7 + repository = "https://tangled.org/paeth.xyz/dollysay" 8 + readme = "README.md" 9 + 10 + [dependencies] 11 + clap = { version = "4", features = ["derive"] } 12 + textwrap = "0.16" 13 + terminal_size = "0.4" 14 + unicode-width = "0.2"
+7
LICENSE
··· 1 + Copyright 2026 paeth.xyz 2 + 3 + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 + 5 + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 + 7 + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+49
README.md
··· 1 + # dollysay 2 + 3 + A cloned sheep speaks. Inspired by cowsay, made for [Tangled](https://tangled.org). 4 + 5 + ``` 6 + __________________________ 7 + < I push, therefore I baa. > 8 + -------------------------- 9 + \ \ 10 + \ \ 11 + .~``~,,```-. .~``~,,```-. 12 + ( -~~- ) ( -~~- ) 13 + ( // OO \\ ) ( // OO \\ ) 14 + ( \__/ ) ( \__/ ) 15 + `~_.,~.__.~' `~_.,~.__.~' 16 + ``` 17 + 18 + ## Install 19 + 20 + You'll need a Rust toolchain. Install via [rustup](https://rustup.rs) if you don't have one. 21 + 22 + ```sh 23 + git clone <repo-url> dollysay 24 + cd dollysay 25 + cargo install --path . 26 + ``` 27 + 28 + That builds in release mode and installs the `dollysay` binary into `~/.cargo/bin/`, which rustup already puts on your `$PATH`. 29 + 30 + ## Use 31 + 32 + ```sh 33 + dollysay hello # speak a message 34 + echo "baa" | dollysay # read from stdin 35 + dollysay --clone "we are many" # two Dollys, one bubble 36 + dollysay --tip # hear what Dolly has to say 37 + ``` 38 + 39 + Run `dollysay --help` for the full flag list. 40 + 41 + ## Uninstall 42 + 43 + ```sh 44 + cargo uninstall dollysay 45 + ``` 46 + 47 + ## TODO 48 + 49 + - Add Dolly moods!
+7
dolly.txt
··· 1 + \ 2 + \ 3 + .~``~,,```. 4 + ( -~~- ) 5 + ( // OO \\ ) 6 + ( \__/ ) 7 + `~_.,~.__.~'
+7
dollys.txt
··· 1 + \ \ 2 + \ \ 3 + .~``~,,```-. .~``~,,```-. 4 + ( -~~- ) ( -~~- ) 5 + ( // OO \\ ) ( // OO \\ ) 6 + ( \__/ ) ( \__/ ) 7 + `~_.,~.__.~' `~_.,~.__.~'
+79
src/bubble.rs
··· 1 + use unicode_width::UnicodeWidthStr; 2 + 3 + // Builds the speech bubble (without trailing newline) and returns 4 + // it as one String. 5 + pub fn render(text: &str, max_width: usize, min_width: usize) -> String { 6 + let wrapped_text: Vec<String> = textwrap::wrap(text, max_width) 7 + .into_iter() 8 + .map(|cow| cow.into_owned()) 9 + .collect(); 10 + 11 + // TODO: Better handle long min widths when cloning Dolly. Fragile here. 12 + // inner width of bubble should be widest line, but never wider than 13 + // max_width and never narrower than min_width. 14 + let inner = wrapped_text 15 + .iter() 16 + // Crucial library win 17 + .map(|l| UnicodeWidthStr::width(l.as_str())) 18 + .max() 19 + .unwrap_or(0) 20 + .max(min_width.min(max_width)); 21 + 22 + let mut out = String::new(); 23 + 24 + // Top border of bubble. 25 + out.push(' '); 26 + out.push_str(&"_".repeat(inner + 2)); 27 + out.push('\n'); 28 + 29 + if wrapped_text.len() == 1 { 30 + // Single-line: 31 + // _______________________ 32 + // < this is a single line > 33 + // ----------------------- 34 + push_bubble_line(&mut out, &wrapped_text[0], inner, '<', '>'); 35 + } else { 36 + // Multi-line: 37 + // ___________________________________________________________ 38 + // / this is a multi line text that will require rounding the \ 39 + // | speech bubble corners. since it is more than two lines it | 40 + // \ will need to be extended with walls. / 41 + // ----------------------------------------------------------- 42 + let last = wrapped_text.len() - 1; 43 + for (i, line) in wrapped_text.iter().enumerate() { 44 + let (l, r) = if i == 0 { 45 + ('/', '\\') 46 + } else if i == last { 47 + ('\\', '/') 48 + } else { 49 + ('|', '|') 50 + }; 51 + push_bubble_line(&mut out, line, inner, l, r); 52 + } 53 + } 54 + 55 + // Bottom border of bubble. 56 + out.push(' '); 57 + out.push_str(&"-".repeat(inner + 2)); 58 + 59 + out 60 + } 61 + 62 + // Writes one bubble row: 63 + // left corner, " text... <spce padding>", right corner, newline. 64 + fn push_bubble_line(out: &mut String, line: &str, inner: usize, left: char, right: char) { 65 + out.push(left); 66 + out.push(' '); 67 + 68 + out.push_str(line); 69 + 70 + let pad = inner - UnicodeWidthStr::width(line); 71 + for _ in 0..pad { 72 + out.push(' '); 73 + } 74 + 75 + out.push(' '); 76 + out.push(right); 77 + 78 + out.push('\n'); 79 + }
+125
src/main.rs
··· 1 + mod bubble; 2 + 3 + use std::io::{self, BufWriter, IsTerminal, Read, Write}; 4 + use std::process::ExitCode; 5 + use std::time::{SystemTime, UNIX_EPOCH}; 6 + 7 + use clap::Parser; 8 + use terminal_size::{Width, terminal_size}; 9 + 10 + const DOLLY: &str = include_str!("../dolly.txt"); 11 + const DOLLYS: &str = include_str!("../dollys.txt"); 12 + const MIN_BUBBLE_SIZE: usize = 20; 13 + const MAX_BUBBLE_SIZE: usize = 60; 14 + const MIN_CLONE_INNER: usize = 22; 15 + 16 + const TIPS: &[&str] = &[ 17 + "I push, therefore I baa.", 18 + "Identity portable. Repos plural. Wool eternal.", 19 + ]; 20 + 21 + #[derive(Parser, Debug)] 22 + #[command( 23 + name = "dollysay", 24 + version, // use the version from Cargo.toml 25 + about = "A cloned sheep speaks. Inspired by cowsay, made for Tangled.org." 26 + )] 27 + struct Cli { 28 + /// The message. If absent, dollysay reads from stdin. 29 + #[arg(trailing_var_arg = true)] 30 + message: Vec<String>, 31 + 32 + /// Clone Dolly. 33 + #[arg(long)] 34 + clone: bool, 35 + 36 + /// Have Dolly share a random Tangled tip. 37 + #[arg(long)] 38 + tip: bool, 39 + } 40 + 41 + fn main() -> ExitCode { 42 + let cli = Cli::parse(); 43 + 44 + let message = match resolve_message(&cli) { 45 + Ok(m) => m, 46 + Err(e) => { 47 + eprintln!("dollysay: {e}"); 48 + return ExitCode::FAILURE; 49 + } 50 + }; 51 + 52 + let min_inner = if cli.clone { MIN_CLONE_INNER } else { 0 }; 53 + let bubble: String = bubble::render(&message, message_width(), min_inner); 54 + 55 + // Lock stdout and use BufWriter to group writes to one syscall only. 56 + let stdout = io::stdout(); 57 + let mut out = BufWriter::new(stdout.lock()); 58 + 59 + let art = if cli.clone { DOLLYS } else { DOLLY }; 60 + 61 + let _ = writeln!(out, "{bubble}"); 62 + let _ = out.write_all(art.as_bytes()); 63 + 64 + match out.flush() { 65 + Ok(()) => ExitCode::SUCCESS, 66 + Err(e) if e.kind() == io::ErrorKind::BrokenPipe => ExitCode::SUCCESS, 67 + Err(e) => { 68 + eprintln!("dollysay: {e}"); 69 + ExitCode::FAILURE 70 + } 71 + } 72 + } 73 + 74 + // Finds where the message text comes from: positional args or piped stdin, 75 + // or falls back to default string. 76 + fn resolve_message(cli: &Cli) -> io::Result<String> { 77 + // --tip wins over positional/stdin: the user is explicitly asking for one. 78 + if cli.tip { 79 + return Ok(random_tip().to_string()); 80 + } 81 + 82 + // positional args 83 + if !cli.message.is_empty() { 84 + return Ok(cli.message.join(" ")); 85 + } 86 + 87 + // stdin 88 + let stdin = io::stdin(); 89 + 90 + // Two cases for is_terminal(): 91 + // true -- user is just running `dollysay` with no args, so stdin is connected to the TTY. 92 + if stdin.is_terminal() { 93 + return Ok("Baa!".to_string()); 94 + } 95 + 96 + // false -- something is piped in to stdin (`echo baa | dollysay`). 97 + let mut buf = String::new(); 98 + stdin.lock().read_to_string(&mut buf)?; 99 + // The piping adds a trailing \n; trim it. 100 + Ok(buf.trim_end().to_string()) 101 + } 102 + 103 + // Picks a tip pseudo-randomly. Uses the system clock as the seed source so we 104 + // don't pull in `rand`. 105 + fn random_tip() -> &'static str { 106 + if TIPS.is_empty() { 107 + return "Baa!"; 108 + } 109 + let nanos = SystemTime::now() 110 + .duration_since(UNIX_EPOCH) 111 + .map(|d| d.as_millis()) 112 + .unwrap_or(0); 113 + TIPS[(nanos as usize) % TIPS.len()] 114 + } 115 + 116 + // Computes the wrap width for the message inside the bubble. 117 + fn message_width() -> usize { 118 + let term: usize = terminal_size() 119 + .map(|(Width(w), _)| w as usize) 120 + .unwrap_or(80); // stdout is not TTY (e.g. pipe), default here 121 + 122 + // Subtract 4 columns for the bubble's left/right borders + a margin. 123 + term.saturating_sub(4) 124 + .clamp(MIN_BUBBLE_SIZE, MAX_BUBBLE_SIZE) 125 + }