Poor Dude’s Janky Bluesky Feed Reader CLI Via atrrr

Have you ever wanted to see your favourite social media posts in your command line? No? Me neither, but at least hrbrmstr has a few months ago. Or to be honest, I don’t know which social media site he prefers, but Bluesky is currently my favourite. With the ease of use and algorithmic curation that I loved about Twitter before its demise and the super interesting and easy to work with AT protocol, which should make Bluesky “billionaire-proof”1, I’m hopeful that this social network it here to stay.

Recently, I have published the atrrr package with a few friends, so I thought I could remove the pesky Python part from hrbrmstr’s command line interface. Along the way, I also looked into how one can write a command line tool with R. I really love using command line tools2 and was always a bit disappointed that people don’t seem to write them in R. After spending some time on this, I have to say: it’s not that bad, especially given the packages docopt and cli, but it’s definitly a bit more manual than in Python.

But let’s have a look at the result first:

And here is of course the commented source code (also available as a GitHub Gist):


# Command line application Bluesky feed reader based on atrrr.
# Make executable with `chmod u+x rbsky`.
# If you are on macOS, you should replace the first line with:
# #!/usr/local/bin/Rscript
# Not sure how to make it work in Windows ¯\_(ツ)_/¯
# based on https://rud.is/b/2023/07/07/poor-dudes-janky-bluesky-feed-reader-cli-via-r-python/

library(lubridate, include.only = c("as.period", "interval"), 
        quietly = TRUE, warn.conflicts = FALSE)
if (!require("docopt", quietly = TRUE)) install.packages("docopt")

# function to displace time since a post was made
ago <- function(t) {
  as.period(Sys.time() - t) |>
    as.character() |>
    tolower() |>
    gsub("\\d+\\.\\d+s", "ago", x = _)

# docopt can produce some documentation when you run `rbsky -h`
doc <- "Usage: rbsky [-a ALGO] [-n NUM] [-t S] [-h]

-a --algorithm ALGO   algorithm used to sort the posts [e.g., \"reverse-chronological\"]
-n --n_posts NUM      Maximum number of records to return [default: 25]
-t --timeout S        Time to wait before displaying the next post [default. 0.5 seconds]
-h --help             show this help text"

# this line parses the arguments passed from the command line and makes sure the
# documentation is shown when `rbsky -h` is run
args <- docopt(doc)
if (is.null(args$n_posts)) args$n_posts <- 25L
if (is.null(args$timeout)) args$timeout <- 0.5

# get feed
feed <- get_own_timeline(algorithm = args$algorithm,
                         limit = as.integer(args$n_posts),
                         verbose = FALSE)

# print feed
for (i in seq_along(feed$uri)) {
  item <- feed[i, ]
    # headline from author • time since post
    cli_h1(c(col_blue(item$author_name), " • ",
    # text of post in italic (not all terminals support it)
    # print quoted text if available
    quote <- purrr::pluck(item, "embed_data", 1, "external")
    if (!is.null(quote)) {
      cli_blockquote("{quote$title}\n{quote$text}", citation = quote$uri)
    # display that posts contains image(s)
    imgs <- length(purrr::pluck(item, "embed_data", 1, "images"))
    if (imgs > 0) {
      cli_text(col_green("[{imgs} IMAGE{?S}]"))
    # new line before next post
  # wait a little before showing the next post

I added the location of the file to my PATH3 with export PATH="/home/johannes/bin/:$PATH" to make it run without typing e.g., Rscript rbsky or ./rbsky. And there you go. If you want to explore how to search and analyse posts from Bluesky and then post the results via R, have a look at the atrrr-pkgdown site: https://jbgruber.github.io/atrrr/.

  1. Once the protocol fulfils its vision that one can always take their follower network and posts to a different site using the protocol.↩︎

  2. I liked this summary of reasons to use them https://youtu.be/Q1dwzi5DKio.↩︎

  3. The PATH environment variable is the location of one or several directories that your system searches for executables.↩︎