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" edition = "2024"
[dependencies] [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] colorspace, hsla / hsl
- [x] Vary colors, find balance so nothing draws excessive attention, but noticeable, though gradual, color evolution occurs - [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. 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 semantic clustering working -- static color. Faiss? enough? Word2Vec? Queries?
- [ ] Get sematic selection & filtering working, based on keywords, chosen posts - [ ] 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 - [ ] 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) - fallen? [twitterfall](https://twitterfall.com) inspiration. [hatnote's L2W](https://l2w.hatnote.com)
# Run it # 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 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 # What it looks like
![firehose screenshot](/res/Screenshot_20250222_162327.png) ![firehose screenshot](/res/Screenshot_20250222_162327.png)
![firehose asciinema](/res/ascii-1.gif) ![firehose asciinema](/res/ascii-1.gif)

View File

@@ -1,3 +1,372 @@
fn main() { use clap::Parser;
println!("Hello, world!"); 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(())
} }