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"
|
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"
|
||||||
|
|||||||
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] 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
|
||||||

|

|
||||||

|

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