Skip navigation

In my M-F newsletter today I mentioned an awesome Rust-based HTML/JS/CSS minifier library that also include batteries for a few other languages.

There was no R port, so I made one using {rextendr}. The {rextendr} package makes is as easy to use Rust code in R packages as {Rcpp} does C/C++ code.

It was as simple as adding some dependencies to the Rust Cargo.toml file and then adding one Rust function to the main lib.rs file, and writing a thin wrapper function ({rextendr} can do that, too, but I wanted some default function parameters) for the shim. It took almost no time, and now you, too, can use the utility:

library(minifyhtml)

'
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  <!-- COMMENT -->
  <style>
    * { color: black; }
  </style>
  <title>TITTLE</title>
  </head>
  <body>
    <p>
       Some text
    </p>
    <script>
      console.log("This is a console log message.");
    </script>
  </body>
</html>
' -> src

cat(minify(src))
## <html xmlns=http://www.w3.org/1999/xhtml><meta charset=UTF-8><meta content=width=device-width,initial-scale=1 name=viewport><style>* { color: black; }</style><title>TITTLE</title><body><p>Some text</p><script>console.log("This is a console log message.");</script>

FIN

I have to work out one kink (due to developing on arm64 macOS) and the utility will also be able to minify CSS and JS embedded in HTML.

You can find {minifyhtml} on GitHub.

Just a quick post to note that I’ll still be doing long-form, single-topic posts here on the blog, but I’ve also started an M-F daily Substack newsletter [free], that introduces 1-3(ish) topics per-issue on stuff I find during my daily RSS trawls.

It’s a mix of tech, Mac, linux, data science, science-science, food, and some odds-and-ends that folks seem to find interesting so far.

There are a few issues up in the archive, so you can get a feel for what it’s all aboot.

I may drop some podcasts into there as well.

I came across a neat site that uses a Golang wasm function called from javascript on the page to help you see if your GitHub public SSH keys are “safe”. What does “safe” mean? This is what the function checks for (via that site):

Recommended key sizes are as follows:

  • For the RSA algorithm at least 2048, recommended 4096
  • The DSA algorithm should not be used
  • For the ECDSA algorithm, it should be 521
  • For the ED25519, the key size should be 256 or larger

The site also provides links to standards and guides to support the need for stronger keys.

I threw together a small R package — {pubcheck} — to check local keys, keys in a character vector, and keys residing in GitHub. One function will even check the GitHub keys of all the GitHub users a given account is following:

Local file

library(pubcheck)
library(tidyverse)

check_ssh_pub_key("~/.ssh/id_rsa.pub") |> 
  mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 
  knitr::kable()
key algo len status
ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… rsa 2048 ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096

A GitHub user

check_gh_user_keys(c("hrbrmstr", "mikemahoney218")) |> 
  mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 
  knitr::kable()
user key algo len status
hrbrmstr ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… rsa 2048 ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096
hrbrmstr ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… rsa 2048 ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096
hrbrmstr ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… rsa 2048 ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096
mikemahoney218 ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… rsa 4096 ✅ Key is safe
mikemahoney218 ssh-ed25519 AAAAC3NzaC1lZDI1NT… ed25519 256 ✅ Key is safe
mikemahoney218 ssh-ed25519 AAAAC3NzaC1lZDI1NT… ed25519 256 ✅ Key is safe
mikemahoney218 ssh-ed25519 AAAAC3NzaC1lZDI1NT… ed25519 256 ✅ Key is safe

Keys of all the users a GitHub account is following

check_gh_following("koenrh") |> 
  mutate(key = ifelse(is.na(key), NA_character_, sprintf("%s…", substr(key, 1, 30)))) |> 
  knitr::kable()
user key algo len status
framer NA NA NA NA
jurre ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… rsa 2048 ✅ Key is safe; For the RSA algorithm at least 2048, recommended 4096

What’s it like out there?

I processed my followers list and had some interesting results:

library(pubcheck)
library(hrbragg)
library(tidyverse)

# this takes a while as the # of users is > 500!
res <- check_gh_following("hrbrmstr")

res |> 
  count(user) |> 
  arrange(n) |> 
  count(n, name = "n_users") |> 
  mutate(csum = cumsum(n_users)) |> 
  ggplot() +
  geom_line(
    aes(n, csum)
  ) +
  geom_point(
    aes(n, csum)
  ) + 
  scale_x_continuous(breaks = 1:21) +
  scale_y_comma() +
  labs(
    x = "# Keys In User GH", y = "# Users",
    title = "Cumulative Plot Of User/Key Counts [n=522 users]",
    subtitle = "A handful of users have >=10 keys configured in GitHub; one has 21!!"
  ) +
  theme_cs(grid="XY")

res |> 
  count(algo, len, status) |> 
  mutate(kind = ifelse(is.na(status), "No SSH keys in account", sprintf("%s:%s\n%s", algo, len, status))) |> 
  mutate(kind = fct_reorder(gsub("[;,]", "\n", kind), n, identity)) |> 
  ggplot() +
  geom_col(
    aes(n, kind),
    width = 0.65, 
    fill = "steelblue", 
    color = NA
  ) +
  scale_x_comma(position = "top") +
  labs(
    x = NULL, y = NULL,
    title = "SSH Key Summary For GH Users hrbrmstr Is Following"
  ) +
  theme_cs(grid="X") +
  theme(plot.title.position = "plot")

FIN

Whether you use the website or the R package, it’d be a good idea to check on your SSH keys at least annually.

The morning before work was super productive and there is a nigh-complete DSL for ESC/POS commands along with the ability to just print {ggplot2}/{grid} object.

I changed the package name to {escpos} since it is no longer just plot object focused, and the DSL looks a bit like this:

library(stringi)
library(hrbrthemes)
library(ggplot2)
library(escpos)

ggplot() +
  geom_point(
    data = mtcars,
    aes(wt, mpg),
    color = "red"
  ) +
  labs(
    title = "A good title"
  ) +
  theme_ipsum_es(grid="XY") -> gg

epson_ip = "HOSTNAME_OR_IP_OF_YOUR_PRINTER"

escpos(epson_ip) |>
  pos_bold("on") %>%
  pos_align("center") %>%
  pos_size("2x") %>%
  pos_underline("2dot") %>%
  pos_plaintext("This Is A Title") %>%
  pos_lf(2) |>
  pos_underline("off") %>%
  pos_size("normal") %>%
  pos_align("left") %>%
  pos_bold("off") %>%
  pos_font("b") %>%
  pos_plaintext(
    stringi::stri_rand_lipsum(1)
  ) |>
  pos_lf(2) |>
  pos_font("a") %>%
  pos_plaintext(
    paste0(capture.output(
      str(mtcars, width = 40, strict.width = "cut")
    ), collapse = "\n")
  ) |>
  pos_lf(2L) |>
  pos_plot(gg, color = TRUE) %>%
  pos_lf(2L) |>
  pos_font("c") %>%
  pos_plaintext(
    stringi::stri_rand_lipsum(1, start_lipsum = FALSE)
  ) |>
  pos_lf(3) |>
  pos_cut() %>%
  pos_print()

full capabilities ESC/POS printing

FIN

I still need to make a more generic options “setter” (i.e. so one can set multiple modes in one function call), and I think supporting some kind of markdown/HTML subset to make it easier just to specify that without using the full DSL would be helpful. More updates over the coming weeks!

At the end of March, I caught a fleeting tweet that showcased an Epson thermal receipt printer generating a new “ticket” whenever a new GitHub issue was filed on a repository. @aschmelyun documents it well in this blog post. It’s a pretty cool hack, self-contained on a Pi Zero.

Andrew’s project birthed an idea: could I write an R package that will let me plot {ggplot2}/{grid} objects to it? The form factor of the receipt printer is tiny (~280 “pixels” wide), but the near infinite length of the paper means one can play with some data visualizations that cannot be done in other formats (and it would be cool to be able to play with other content to print to it in and outside of R).

One of the features that makes Andrew’s hack extra cool is that he used an Epson receipt printer model that was USB connected. I don’t see the need to dedicate and extra piece of plastic, metal, and silicon to manage the printing experience, especially since I already have a big linux server where I run personal, large scale data science jobs. I ended up getting a used (lots of restaurants close down each week) Epson TM-T88V off of eBay since it has Ethernet and is supports ESC/POS commands.

After unpacking it, I needed to get it on the local network. There are many guides out there for this, but this one sums up the process pretty well:

  • Plug the printer in and reset it
  • Hook up a system directly to it (Ethernet to Ethernet)
  • Configure your system to use the Epson default IP addressing scheme
  • Access the web setup page
  • Configure it to work on your network
  • Disconnect and restart the printer

To make sure everything worked, I grabbed one of the (weirdly) many projects on GitHub that provided a means for formatting graphics files to an ESC/POS compatible raster bitmap and processed a simple R-generated png to it, then used netcat to shunt the binary blob over to the printer on the default port of 9100.

I did some initial experiments with {magick}, pulling the graphics bits out of generated plots and then wrapping some R code around doing the conversion. It was clunky and tedious, and I knew there had to be a better way, so I hunted for some C/C++, Rust, or Go code that already did the conversion and found png2escpos by The Working Group. However, I’ve switched to png2pos by Petr Kutalek as the dithering it does won’t require the R user to produce only black-and-white plots for them to look good.

I thought about implementing a graphics device to support any R graphics output, but there are enough methods to convert a base R plot to a grid/grob object that I decided to mimic the functionality of ggsave() and make a ggpos() function. The comment annotations in the code snippet below walk you through the extremely basic process:

ggpos <- function(plot = ggplot2::last_plot(),
                  host_pos,
                  port = 9100L,
                  scale = 2,
                  width = 280,
                  height = 280,
                  units = "px",
                  dpi = 144,
                  bg = "white",
                  ...) {

  # we generate a png file using ggsave()

  png_file <- tempfile(fileext = ".png")

  ggplot2::ggsave(
    filename = png_file,
    plot = plot,
    scale = scale,
    width = width,
    height = height,
    units = units,
    dpi = dpi,
    bg = bg,
    ...
  )

  # we call an internal C function to convert the generated png file to an ESC/POS raster bitmap file

  res <- png_to_raster(png_file)

  if (res != "") { # if the conversion ended up generating a new file

    # read in the raw bytes

    escpos_raster <- stringi::stri_read_raw(res)

    # open up a binary socket to the printer 

    socketConnection(
      host = host_pos,
      port = port,
      open = "a+b"
    ) -> con

    on.exit(close(con))

    # shunt all the bytes over to it

    writeBin(
      object = escpos_raster,
      con = con,
      useBytes = TRUE
    )

  }

  invisible(res)

}

The only work I needed to do on the original C code was to have it output directly to a file instead of stdout.

Now, plotting to the printer is as straightforward as:

library(ggplot2)
library(escpos)

ggplot(mtcars) +
  geom_point(
    aes(wt, mpg)
  ) +
  labs(
    title = "Test of {ggpos}"
  ) +
  theme_ipsum_es(grid="XY") +
  theme(
    panel.grid.major.x = element_line(color = "black"),
    panel.grid.major.y = element_line(color = "black")
  ) -> gg

ggpos(gg, host_pos = HOSTNAME_OR_IP_ADDRESS_OF_YOUR_PRINTER)

That code produces this output (I’m still getting the hang of ripping the spooled paper off this thing):

ggplot receipt

This is the whole thing in action:

One of the 2022 #30DayChartChallenge topics was “part-to-whole”, so I rejiggered my treemap entry into a very long plot that would make CVS cashiers feel quite inferior.

You can find {escpos} over on GitHub.

FIN

One big caveat for this is that these printers have a tiny memory buffer, so very long, complex plots aren’t going to work out of the box. I had to break up my faceted heatmaps info individual ones and shunt them over one-by-one.

I’ll be switching over the the new C library soon, and adding a small DSL to handle text formatting and printing from R (the device has 2 fonts and almost no styles). I’ve even threatened to make a ShinyPOS application, but we’ll see how the motivation for that goes.

Kick the tyres and let me know if you end up using the package (+ share your creation to the 🌎).

RStudio’s macOS Electron build is coming along quite nicely and is blazing fast on Apple Silicon.

I like to install the dailies, well, daily!; and, of late, RStudio and Quarto are joined at the hip. As a result, I regularly found myself having to manually update Quarto CLI right after updating RStudio, so I made a small zsh script that will use the new RStudio dailies JSON API to grab the latest daily, and the GitHub JSON API to grab the latest Quarto release, then install them both.

Caveats and limitations are in the repo.

via Zack Whittaker on Twitter…

Download Tor Browser: http://torproject.org/download

Open this URL in Tor to access BBC (🇺🇦): https://www.bbcweb3hytmzhn5d532owbu6oqadra5z3ar726vq5kgwwn6aucdccrad.onion/ukrainian

Open this URL in Tor to access BBC (🇷🇺): https://www.bbcweb3hytmzhn5d532owbu6oqadra5z3ar726vq5kgwwn6aucdccrad.onion/russian

Ukrainian President Volodymyr Zelenskyy addresses the nation on a live TV broadcast in Kyiv, Ukraine, on Feb. 22, 2022.

Ukrainian Presidential Press Office via AP
Ukrainian President Volodymyr Zelenskyy addresses the nation on a live TV broadcast in Kyiv, Ukraine, on Feb. 22, 2022.

Here is the full transcript of Ukrainian President Volodymyr Zelenskyy’s video address early on Feb. 24 local time warning that a Russian invasion could cause tens of thousands of deaths.

“Today I initiated a phone call with the president of the Russian federation. The result was silence. Though the silence should be in Donbass. That’s why I want to address today the people of Russia. I am addressing you not as a president, I am addressing you as a citizen of Ukraine. More than 2,000 km of the common border is dividing us. Along this border your troops are stationed, almost 200,000 soldiers, thousands of military vehicles. Your leaders approved them to make a step forward, to the territory of another country. And this step can be the beginning of a big war on European continent.

We know for sure that we don’t need the war. Not a Cold War, not a hot war. Not a hybrid one. But if we’ll be attacked by the [enemy] troops, if they try to take our country away from us, our freedom, our lives, the lives of our children, we will defend ourselves. Not attack, but defend ourselves. And when you will be attacking us, you will see our faces, not our backs, but our faces.

The war is a big disaster, and this disaster has a high price. With every meaning of this word. People lose money, reputation, quality of life, they lose freedom. But the main thing is that people lose their loved ones, they lose themselves.

They told you that Ukraine is posing a threat to Russia. It was not the case in the past, not in the present, it’s not going to be in the future. You are demanding security guarantees from NATO, but we also demand security guarantees. Security for Ukraine from you, from Russia and other guarantees of the Budapest memorandum.

But our main goal is peace in Ukraine and the safety of our people, Ukrainians. For that we are ready to have talks with anybody, including you, in any format, on any platform. The war will deprive [security] guarantees from everybody — nobody will have guarantees of security anymore. Who will suffer the most from it? The people. Who doesn’t want it the most? The people! Who can stop it? The people. But are there those people among you? I am sure.

I know that they [the Russian state] won’t show my address on Russian TV, but Russian people have to see it. They need to know the truth, and the truth is that it is time to stop now, before it is too late. And if the Russian leaders don’t want to sit with us behind the table for the sake of peace, maybe they will sit behind the table with you. Do Russians want the war? I would like to know the answer. But the answer depends only on you, citizens of the Russian Federation.”