Ask claude to translate the python to rust

This commit is contained in:
Greg Shuflin
2025-07-13 22:07:45 -07:00
parent 38abb08eee
commit a3b42ea3e1
4 changed files with 1896 additions and 2 deletions

1474
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,3 +4,14 @@ version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
colored = "2.0"
clap = { version = "4.0", features = ["derive"] }
futures-util = "0.3"
url = "2.4"
log = "0.4"
env_logger = "0.10"
rand = "0.8"

View File

@@ -10,6 +10,7 @@ A semantically colored feed of bluesky skeets, for your perspective, someone els
- [x] colorspace, hsla / hsl
- [x] Vary colors, find balance so nothing draws excessive attention, but noticeable, though gradual, color evolution occurs
And, for ergonomic use to communicate meaning. Color for cluster / direction. Brightness, inverse to length, for ease of reading, uniformity of gestalt. Saturation for content searching versus scaffolding metadata. Errors in grey too.
- [x] Port to Rust for better performance and native compilation
- [ ] Get semantic clustering working -- static color. Faiss? enough? Word2Vec? Queries?
- [ ] Get sematic selection & filtering working, based on keywords, chosen posts
- [ ] Get db/profiles/queries/modes set up -- so every taste selection input can inform any future use
@@ -17,10 +18,49 @@ A semantically colored feed of bluesky skeets, for your perspective, someone els
- fallen? [twitterfall](https://twitterfall.com) inspiration. [hatnote's L2W](https://l2w.hatnote.com)
# Run it
## Rust version (recommended)
```bash
# Build and run
cargo run
# With options
cargo run -- --count 10 --filters "+love,-hate" --cfilters "+create"
# Install and run
cargo install --path .
bsky-firehose-term --help
```
## Python version
```bash
uv run --with websockets,colorama,munch,fire python3 bluesky-simple-print.py
```
## Command-line options
Both versions support the same filtering options:
- `--skips`: Skip certain types (comma-separated)
- `--only`: Only show certain types (comma-separated)
- `--cfilters`: Commit type filters (+include,-skip format)
- `--filters`: Text filters (+include,-skip format)
- `--count`: Stop after N posts
Examples:
```bash
# Show only posts containing "rust" or "programming"
cargo run -- --filters "+rust,+programming"
# Skip posts containing "spam"
cargo run -- --filters "-spam"
# Show only commit types containing "create"
cargo run -- --cfilters "+create"
# Show only 5 posts
cargo run -- --count 5
```
# What it looks like
![firehose screenshot](/res/Screenshot_20250222_162327.png)
![firehose asciinema](/res/ascii-1.gif)

View File

@@ -1,3 +1,372 @@
fn main() {
println!("Hello, world!");
use clap::Parser;
use colored::*;
use futures_util::StreamExt;
use log::{debug, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
#[derive(Debug, Deserialize, Serialize)]
struct Post {
kind: Option<String>,
#[serde(rename = "type")]
type_: Option<String>,
time_us: Option<u64>,
did: Option<String>,
commit: Option<CommitData>,
}
#[derive(Debug, Deserialize, Serialize)]
struct CommitData {
#[serde(rename = "type")]
type_: Option<String>,
operation: Option<String>,
record: Option<RecordData>,
}
#[derive(Debug, Deserialize, Serialize)]
struct RecordData {
text: Option<String>,
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Skip certain types (comma-separated)
#[arg(long, default_value = "")]
skips: String,
/// Only show certain types (comma-separated)
#[arg(long, default_value = "")]
only: String,
/// Commit type filters (+include,-skip format)
#[arg(long, default_value = "")]
cfilters: String,
/// Text filters (+include,-skip format)
#[arg(long, default_value = "")]
filters: String,
/// Stop after N posts
#[arg(long)]
count: Option<usize>,
}
struct BlueskyFirehosePrinter;
impl BlueskyFirehosePrinter {
fn new() -> Self {
Self
}
fn hsv_to_rgb(h: f64, s: f64, v: f64) -> (u8, u8, u8) {
let c = v * s;
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
let m = v - c;
let (r, g, b) = if h < 60.0 {
(c, x, 0.0)
} else if h < 120.0 {
(x, c, 0.0)
} else if h < 180.0 {
(0.0, c, x)
} else if h < 240.0 {
(0.0, x, c)
} else if h < 300.0 {
(x, 0.0, c)
} else {
(c, 0.0, x)
};
(
((r + m) * 255.0) as u8,
((g + m) * 255.0) as u8,
((b + m) * 255.0) as u8,
)
}
fn create_color_from_hsv(h: f64, s: f64, v: f64) -> colored::Color {
let (r, g, b) = Self::hsv_to_rgb(h, s, v);
colored::Color::TrueColor { r, g, b }
}
fn extract_post_text(&self, post: &Post) -> String {
if let Some(ref commit) = post.commit {
if let Some(ref record) = commit.record {
if let Some(ref text) = record.text {
return text.chars().take(200).collect();
}
}
}
format!("{:?}", post)
}
async fn connect_and_print(
&self,
websocket_url: &str,
skips: &[String],
onlys: &[String],
count: Option<usize>,
cfilters: &HashMap<String, Vec<String>>,
fkeeps: &[String],
fdrops: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
let url = url::Url::parse(websocket_url)?;
let (ws_stream, _) = connect_async(url).await?;
println!("Connected to {}", websocket_url);
let (mut _write, mut read) = ws_stream.split();
let mut n = 0;
while let Some(msg) = read.next().await {
match msg {
Ok(Message::Text(text)) => {
match serde_json::from_str::<Post>(&text) {
Ok(mut post) => {
// Handle missing fields
if post.kind.is_none() {
post.kind = post.type_.clone();
}
// Apply filters
if !onlys.is_empty() {
if let Some(ref kind) = post.kind {
if !onlys.contains(kind) {
continue;
}
}
}
if let Some(ref kind) = post.kind {
if skips.contains(kind) {
continue;
}
}
let ts = post.time_us.unwrap_or(0) / 10_000;
// Generate HSV color based on timestamp
let h = ((ts as f64 / 4.0) % 255.0) / 255.0 * 360.0;
let mut s = 0.8;
let mut v = 0.8;
let mut text = String::new();
// Handle commit type filtering and text extraction
if post.kind.as_deref() == Some("commit") {
if let Some(ref commit) = post.commit {
if let Some(ref commit_type) = commit.type_ {
// Apply commit type filters
if let Some(excludes) = cfilters.get("-") {
if excludes.iter().any(|w| commit_type.contains(w)) {
continue;
}
}
if let Some(includes) = cfilters.get("+") {
if !includes.iter().any(|w| commit_type.contains(w)) {
continue;
}
}
}
if let Some(ref record) = commit.record {
if let Some(ref record_text) = record.text {
// Apply text filters
if !fdrops.is_empty() {
if fdrops.iter().any(|w| record_text.contains(w)) {
continue;
}
}
if !fkeeps.is_empty() {
if !fkeeps.iter().any(|w| record_text.contains(w)) {
continue;
}
}
text = record_text.clone();
// Adjust brightness based on text length
v = 1.0
- (text.len() as f64)
.ln()
.min(16.0 * 256.0_f64.ln())
/ (16.0 * 256.0_f64.ln());
} else {
text = format!("commit.record={:?}", record);
}
}
}
} else {
text = self.extract_post_text(&post);
s = 0.8;
v = 120.0 / 255.0;
}
// Create colors
let text_color = Self::create_color_from_hsv(h, s, v);
// Info color (dimmed version)
let info_s = s * 0.1;
let info_v = (v * 0.4).max(0.3);
let info_color = Self::create_color_from_hsv(h, info_s, info_v);
// Print colored output
let hsv_str = format!("{:.1}:{:.1}:{:.1}", h / 360.0, s, v);
let _post_id = post.did.unwrap_or_else(|| {
format!("r:{}", rand::random::<f64>())
});
let type_str = post.type_.as_deref().unwrap_or("None");
let kind_str = post.kind.as_deref().unwrap_or("None");
if post.type_.as_deref() == Some("com") {
let commit_type = post
.commit
.as_ref()
.and_then(|c| c.type_.as_deref())
.unwrap_or("None");
let operation = post
.commit
.as_ref()
.and_then(|c| c.operation.as_deref())
.unwrap_or("None");
println!(
"{}{}|type:{}|{}{}{}|hsv:{} type:{} kind:{} commit.type={} operation={}",
format!("{}", ts).color(info_color),
format!("").color(info_color),
type_str.color(info_color),
text.color(text_color),
format!("").color(info_color),
format!("").color(info_color),
hsv_str.color(info_color),
type_str.color(info_color),
kind_str.color(info_color),
commit_type.color(info_color),
operation.color(info_color)
);
} else {
println!(
"{}{}|type:{}|{}{}|hsv:{} type:{} kind:{}",
format!("{}", ts).color(info_color),
format!("").color(info_color),
type_str.color(info_color),
text.color(text_color),
format!("").color(info_color),
hsv_str.color(info_color),
type_str.color(info_color),
kind_str.color(info_color)
);
}
if let Some(max_count) = count {
n += 1;
if n >= max_count {
return Ok(());
}
}
}
Err(e) => {
debug!("JSON decode error: {}", e);
println!("err:{}", text);
}
}
}
Ok(Message::Close(_)) => {
println!("WebSocket closed");
break;
}
Err(e) => {
warn!("WebSocket error: {}", e);
break;
}
_ => {}
}
}
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let args = Args::parse();
// Parse arguments similar to Python version
let skips: Vec<String> = if args.skips.is_empty() {
vec![]
} else {
args.skips.split(',').map(|s| s.to_string()).collect()
};
let onlys: Vec<String> = if args.only.is_empty() {
vec![]
} else {
args.only.split(',').map(|s| s.to_string()).collect()
};
// Parse cfilters (+include,-skip format)
let mut cfilters = HashMap::new();
if !args.cfilters.is_empty() {
let mut includes = Vec::new();
let mut excludes = Vec::new();
for filter in args.cfilters.split(',') {
if filter.starts_with('+') {
includes.push(filter[1..].to_string());
} else if filter.starts_with('-') {
excludes.push(filter[1..].to_string());
}
}
if !includes.is_empty() {
cfilters.insert("+".to_string(), includes);
}
if !excludes.is_empty() {
cfilters.insert("-".to_string(), excludes);
}
}
// Parse text filters
let mut fkeeps = Vec::new();
let mut fdrops = Vec::new();
if !args.filters.is_empty() {
for filter in args.filters.split(',') {
if filter.starts_with('+') {
fkeeps.push(filter[1..].to_string());
} else if filter.starts_with('-') {
fdrops.push(filter[1..].to_string());
}
}
}
const BLUESKY_FIREHOSE_WS: &str =
"wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post";
let printer = BlueskyFirehosePrinter::new();
match printer
.connect_and_print(
BLUESKY_FIREHOSE_WS,
&skips,
&onlys,
args.count,
&cfilters,
&fkeeps,
&fdrops,
)
.await
{
Ok(()) => println!("done"),
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
Ok(())
}