Ask claude to translate the python to rust
This commit is contained in:
1474
Cargo.lock
generated
Normal file
1474
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -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"
|
||||
|
||||
40
README.md
40
README.md
@@ -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
|
||||

|
||||

|
||||
|
||||
373
src/main.rs
373
src/main.rs
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user