sc
Scott avatar
ttwillsey

Cool Site Spotlight Raycast Script Command

Raycast

Part of the Raycast series

I like to highlight different cool or interesting sites by featuring them in my Cool Site Spotlight on my links page. I put a new one up every one or two weeks, and put a link to the last one in the Cool Sites Archive section of the links page.

The data for the current one is a simple JSON file called spotlight.json, and it looks like this:

spotlight.json
{
"Site": "https://html.earth/",
"Title": "html.earth: Markdown to HTML Converter & Site Generator",
"Description": "html.earth is a free site generator that converts Markdown to HTML. It comes with a front matter editor, custom styling field, and import & export options.",
"Image": "HtmlDotEarth-5F9FE361-5512-40BF-A78E-468190030F83"
}

Up until now, when it’s time to spotlight a new site, I’ve been grabbing a markdown link of the site title and URL using Hookmark and then using the title and URL from that to put in the Title and Site property values. I then run a Raycast script command to rename, resize, and reformat the site screenshot and put it in the appropriate directories in my own site’s local Astro project. Once I have the image name, I can put it in spotlight.json in the Image property value, minus the file extension, for reasons I’ll write about someday in a separate post.

Anyway, it’s been a fairly manual process, and I finally got tired of it yesterday. So I wrote a Raycast script command called “New Cool Site Spotlight” to do it for me.

Raycast New Cool Site Spotlight Script Command

As you can see, the script command takes one parameter: the URL of the site I want to feature in Cool Site Spotlight.

The first thing the script command does is look at spotlight.json in its current state and grabs the title and URL of the current site, which it formats into a Markdown link and puts on the clipboard. It also sticks a dash in front of it for good measure, since the list of previous cool sites IS actually a list, and Markdown list elements start with a dash. This is all handled by a function called create_cool_site_archive_link.

def create_cool_site_archive_link():
# Open the Spotlight JSON file
SPOTLIGHT_JSON_FILE = '/Users/scott/Sites/scottwillsey/src/data/spotlight.json'
with open(SPOTLIGHT_JSON_FILE, 'r') as f:
data = json.load(f)
# Get the 'Site' and 'Title' values
site = data.get('Site')
title = data.get('Title')
# Create a markdown link
markdown_link = f'- [{title}]({site})'
pyperclip.copy(markdown_link)

The script command then calls a function named get_page_info which uses the URL to grab the page and extract its title and description.

def get_page_info(url):
# Get the title and description from the page
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
title = soup.find('title').text
description = soup.find('meta', {'name': 'description'})
description = description['content'] if description else None
return title, description

Finally, the script command calls write_spotlight_json, which replaces the contents of spotlight.json with the new information.

def write_spotlight_json(title, description, url):
# Create the JSON for the new Cool Site Spotlight entry
IMAGE_DIR = '/Users/scott/Scripts/scottwillsey/scottwillsey-images/In'
SPOTLIGHT_JSON_FILE = '/Users/scott/Sites/scottwillsey/src/data/spotlight.json'
png_file = next((f for f in os.listdir(IMAGE_DIR) if f.endswith('.png')), None)
if png_file:
png_file = os.path.splitext(png_file)[0]
entry = {
"Site": url,
"Title": title,
"Description": description,
"Image": png_file
}
# If all values are present, write the JSON to the file
if all(entry.values()):
with open(SPOTLIGHT_JSON_FILE, 'w') as f:
json.dump(entry, f, indent=4)
return entry

One interesting thing you might notice is that it looks in /Users/scott/Scripts/scottwillsey/scottwillsey-images/In for any image file that ends in .png, which it then extracts the file name minus the extension from. Once it’s done that, it creates a dictionary named entry. If all keys in entry have corresponding values, it writes the dictionary as JSON into spotlight.json, replacing whatever was there previously.

Here’s the full New Cool Site Spotlight Raycast script command:

new-cool-site-spotlight.py
#!/Users/scott/Scripts/python/venv/bin/python
# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title New Cool Site Spotlight
# @raycast.mode fullOutput
# Optional parameters:
# @raycast.argument1 { "type": "text", "placeholder": "Cool Site URL" }
# @raycast.packageName Website
# Documentation:
# @raycast.description Create the JSON for Cool Site Spotlight for scottwillsey.com from a URL
# @raycast.author scott_willsey
# @raycast.authorURL https://raycast.com/scott_willsey
import sys
import json
import pyperclip
import requests
from bs4 import BeautifulSoup
import os
url = sys.argv[1]
def create_cool_site_archive_link():
# Open the Spotlight JSON file
SPOTLIGHT_JSON_FILE = '/Users/scott/Sites/scottwillsey/src/data/spotlight.json'
with open(SPOTLIGHT_JSON_FILE, 'r') as f:
data = json.load(f)
# Get the 'Site' and 'Title' values
site = data.get('Site')
title = data.get('Title')
# Create a markdown link
markdown_link = f'- [{title}]({site})'
pyperclip.copy(markdown_link)
def get_page_info(url):
# Get the title and description from the page
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
title = soup.find('title').text
description = soup.find('meta', {'name': 'description'})
description = description['content'] if description else None
return title, description
def write_spotlight_json(title, description, url):
# Create the JSON for the new Cool Site Spotlight entry
IMAGE_DIR = '/Users/scott/Scripts/scottwillsey/scottwillsey-images/In'
SPOTLIGHT_JSON_FILE = '/Users/scott/Sites/scottwillsey/src/data/spotlight.json'
png_file = next((f for f in os.listdir(IMAGE_DIR) if f.endswith('.png')), None)
if png_file:
png_file = os.path.splitext(png_file)[0]
entry = {
"Site": url,
"Title": title,
"Description": description,
"Image": png_file
}
# If all values are present, write the JSON to the file
if all(entry.values()):
with open(SPOTLIGHT_JSON_FILE, 'w') as f:
json.dump(entry, f, indent=4)
return entry
create_cool_site_archive_link()
title, description = get_page_info(url)
if not (title and description):
sys.exit(1)
print(write_spotlight_json(title, description, url))

You may still have questions about the image file, such as how I create it, why it’s in a directory called /Users/scott/Scripts/scottwillsey/scottwillsey-images/In, and what I mean when I say I rename and reformat it first. You may also wonder why all that isn’t part of THIS script command intead of being something I do separately still. I’ll write about that soon.

In the meantime, I’ve gone from manually tweaking a JSON file with multiple copy and paste edits when I want to update Cool Site Spotlight to simply copying a URL from the browser and running a Raycast script command by executing a keyboard command. It’s so much nicer now!

SSH Connections in Visual Studio Code

Mac

Part of the Mac series

Recently I needed to work on some script files on a server and wanted to edit them in Visual Studio Code. Visual Studio Code supports remote connections, including ssh connections. Unfortunately, some of these files require root privileges, and I can’t and don’t want to be able to ssh into this server as root.

What about sudo su once you’re logged in, you say?

The good news about configuring ssh connections is that you can set up RemoteCommand entries in your ssh config that will be executed upon connection. sudo su is a command. And the good news about Visual Studio Code is that it lets you specify an ssh config file to use for its ssh connections, so you can customize your Visual Studio Code ssh connections to perform a sudo su upon connection, if needed, while keeping your normal ssh config file free of such silliness so that this doesn’t happen in your normal terminal ssh sessions.1

First create a copy of your ~/.ssh/config file and name it something obvious. Mine is named config_vscode.

Terminal window
ll
total 168
drwx------@ 20 scott staff 640B Jan 21 08:47 .
drwxr-xr-x@ 76 scott staff 2.4K Jan 21 09:02 ..
-rw-r--r--@ 1 scott staff 6.0K Dec 18 2023 .DS_Store
drwx------ 3 scott staff 96B Jan 21 08:47 1Password
-rw-------@ 1 scott staff 1.7K Jan 10 14:32 config
-rw-------@ 1 scott staff 1.7K Dec 20 20:26 config_por
-rw-------@ 1 scott staff 948B Jan 10 14:58 config_vscode

Now edit your newly created created config_vscode (or whatever) file and add the following to the end of connections that need root access to files in Visual Studio Code:

Terminal window
RemoteCommand sudo su

This means a given connection that looks like this in ~/.ssh/config:

~/.ssh/config
Host server1
HostName server1.domain.com
Port 22
User serverperson
IdentityFile ~/.ssh/server1
IdentitiesOnly yes

Should look like this in ~/.ssh/config_vscode:

~/.ssh/config_vscode
Host server1
HostName server1.domain.com
Port 22
User serverperson
IdentityFile ~/.ssh/server1
IdentitiesOnly yes
RemoteCommand sudo su

Then in your Visual Studio Code user settings.json file, make sure to point to your custom ssh config and set enableRemoteCommand to true.

settings.json
"remote.SSH.configFile": "/Users/scott/.ssh/config_vscode",
"remote.SSH.enableRemoteCommand": true,

Also, in your Visual Studio Code defaultSettings.json, make sure the following is set to true:

defaultSettings.json
"remote.SSH.useLocalServer": true,

Then make your ssh connection in Visual Studio Code and be amazed as you now have root powers on files that previously refused to submit to your will.

Vscode SSH connections

Footnotes

  1. This assumes that your login user is in your server’s sudoers list and that sudoers don’t need to enter passwords to perform a sudo su.

The Notey McNoteness of Raycast Notes

Raycast

Part of the Raycast series

Raycast Floating Notes started off as a very simple text window you could dump stuff into. That’s all it was. One window, one note, and anything you put in there was always going to be there when you opened Raycast Floating Notes again. Then late last year, Raycast introduced Raycast Notes, a Floating Notes update and replacement (Meet the new Raycast Notes - Raycast Blog).

Raycast Notes Window

Raycast Notes is a big upgrade over Floating Notes for the simple reason that you can keep multiple notes… something Floating Notes just couldn’t do. Browse your notes and select the note to view using the menu icon at the top of the current note, or use ⌘P to pop up the list.

Raycast Notes Note List

You can also perform a bunch of actions in/on notes, such as window auto-sizing, zoom, copying, formatting, searching, creating a quicklink to the note, all with keyboard shortcuts, by clicking the ⌘ icon or using the ⌘K keyboard shortcut.

Raycast Notes Actions

Here’s the catch, though, at least for me – people who use Raycast want Raycast Notes to become the be-all, end-all of notes apps for them. There are people who genuinely want to replace Obsidian, Apple Notes, or Notion with Raycast Notes, and this has led to some minor annoyances for me. I don’t like the fact that pasting a URL into a note in Raycast Notes auto-formats it as a markdown link. Most of the time I just want a text link that I can see and copy as plaintext.

I understand why people want Raycast Notes to do all these things and have all these features, but honestly for me, I want a scratchpad, not a word processor. Raycast Notes is never going to replace Notion, Apple Notes, or Bear for me. It just can’t, and it’s going to be way too clunky if Raycast tries to do that.

I did learn that there’s a way to at least copy the links that Raycast Notes insists on formatting into markdown back out of the note as plaintext though. Highlight the link, hit ⇧⌘C to pop up the “Copy Selected Text As” menu, and choose “Plain Text”.

Raycast Notes Copy Text As

Aside from little quibbles like that, I do use Raycast Notes all the time to capture quick info or keep things I need floating on my screen while I work. It’s very much a quick reference, context-based tool for me, and for that use case, I do appreciate it.

Just quit trying to make it into the Microsoft Word of Markdown editors, people. 😄

Aichat Config for OpenAI and Anthropic APIs

There is a fascinating LLM CLI tool called aichat that lets you use all kinds of LLM models in the command line, including REPL mode, RAG mode (feeding it your documents and files of choice to use as a knowledgebase), and much more.

I was a little confused about how to configure it to give me a choice of OpenAI and Anthropic LLM models though. I kept breaking the config and generating error messages. Finally I stumbled across a post in the aichat repo’s closed issues that explained for OpenAI and Anthropic, you can configure their API and type information and skip configuring any model information. That way aichat will pull the list of available models from each API for you.

Here’s what my config looks like now:

~/Library/Application Support/aichat/config.yaml
model: claude:claude-3-5-sonnet-latest
stream: true
save: true
wrap: 85
wrap_code: false
clients:
- type: openai
api_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- type: claude
api_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

You can see I default to the Claude 3.5 Sonnet (latest) model. The nomenclature for the value of specified model is type:model-name. Then I have a clients section which lists two types of models: OpenAI and Claude (Anthropic).

Here’s the default Claude 3.5 Sonnet (which is notoriously shy about answering questions about model information):

Terminal window
scott@Midnight:~ aichat "what model are you using?"
I'm Claude, an AI assistant created by Anthropic. I aim to be direct and honest in my
communications, including about what I am.

But I could also specify a model manually, or set it in an environment variable:

Terminal window
scott@Midnight:~ aichat -m openai:gpt-4o "What LLM model are you using?"
I am based on OpenAI's GPT-4 model.

And in aichat REPL mode, you can get a list of models to choose from with the .model ⇥ command (that’s .model followed by a space and a tab).

Terminal window
scott@Midnight:~ aichat
Welcome to aichat 0.26.0
Type ".help" for additional help.
> | .model
openai:gpt-4o 128000 / 16384 | 2.5 / 10 👁
openai:gpt-4o-2024-11-20 128000 / 16384 | 2.5 / 10 👁
openai:gpt-4o-2024-08-06 128000 / 16384 | 2.5 / 10 👁
openai:chatgpt-4o-latest 128000 / 16384 | 5 / 15 👁
openai:gpt-4o-mini 128000 / 16384 | 0.15 / 0.6 👁
openai:gpt-4-turbo 128000 / 4096 | 10 / 30 👁
openai:o1-preview 128000 / 32768 | 15 / 60
openai:o1-mini 128000 / 65536 | 3 / 12
openai:o1 128000 / - | 15 / 60 👁
openai:gpt-3.5-turbo 16385 / 4096 | 0.5 / 1.5
claude:claude-3-5-sonnet-latest 200000 / 8192 | 3 / 15 👁
claude:claude-3-5-sonnet-20241022 200000 / 8192 | 3 / 15 👁
claude:claude-3-5-haiku-latest 200000 / 8192 | 0.8 / 4 👁
claude:claude-3-5-haiku-20241022 200000 / 8192 | 0.8 / 4 👁
claude:claude-3-opus-20240229 200000 / 4096 | 15 / 75 👁
claude:claude-3-sonnet-20240229 200000 / 4096 | 3 / 15 👁
claude:claude-3-haiku-20240307 200000 / 4096 | 0.25 / 1.25 👁

Arrow up and down to select the model you want and then hit return. This basically does the same as you typing .model type:model-name:

Terminal window
scott@Midnight:~ aichat
Welcome to aichat 0.26.0
Type ".help" for additional help.
> .model openai:gpt-4o
> what LLM model are you?
I am based on OpenAI's GPT-4.

There’s a lot that aichat can do that I haven’t even poked at yet. Honestly, I don’t have time and probably use cases to dig into it much more in the very near term, but it is definitely a very comprehensive CLI tool for LLM use.

Default Apps December 2024

Once upon a time,1 the Hemispheric Views podcast held the first ever Duel of the Defaults in which they decided who was the winner at using the most default apps for several categories. Then Robb Knight took it to 11 with his App Defaults page.

In the continued spirit of blogging about one’s default apps, here is my updated list for 2024.

Official Categories

  • 📮 Mail Server
  • 📨 Mail Client
  • 📝 Notes
    • Notion
    • Apple Notes for family note sharing
    • Bear for all my writing and podcast show notes
    • Raycast Floating Notes Window
  • To-Do
    • Reminders App
  • 📷 iPhone Photo Shooting
    • iOS Camera
  • 🟦 Photo Management
    • Photos App
  • 📅 Calendar
  • 📂 Cloud File Storage
    • iCloud Drive
  • 📖 RSS
  • 👩‍🦲 Contacts
    • Apple Contacts App
  • 🌐 Browser
    • Safari, Chrome for web work
  • 💬 Chat
    • Messages, Signal, Discord, Slack
  • 🔖 Bookmarks
  • 📑 Read It Later
    • Safari Read Later
  • 📜 Word Processing
    • Bear for all my writing and podcast show notes
    • Pages
    • Google Docs
    • Microsoft Word
  • 📈 Spreadsheets
    • Excel (I’m sorry, but Numbers is the most useless excuse for a spreadsheet app I’ve ever seen)
  • 📊 Presentations
    • Keynote
  • 🛒 Shopping List
    • Reminders
  • 🍴 Meal Planning & Recipes
  • 💰 Budgeting and Personal Finance
    • Spreadsheet
  • 📰 News
    • RSS
    • Mastodon
    • BlueSky
  • 🎵 Music
    • Apple Music
  • 🎤 Podcasts
  • 🔐 Password Management

Additional Categories

Footnotes

  1. November 2nd, 2023, in fact

BBEdit for Log Analysis

Mac

Part of the Mac series

Long-time Mac users will undoubtedly have heard of BBEdit. BBEdit is the favorite Mac text editor of many old Mac users1 for various types of writing, including programming, notes, articles, and basically any type of writing Mac nerds can do.

Personally, I wouldn’t program using BBEdit. Visual Studio Code and offshoots like Cursor are much better for that. I wouldn’t write this blog post in it. That’s what I use Bear for. And, in my humble-ish opinion, they’re much better at those tasks than BBEdit is.

But there is a specific use that BBEdit can’t be topped for on the Mac – log file analysis. It’s like BBEdit was made specifically for it.

Here’s a UTM firewall packet filter log containing all entries for a given day, downloaded right from the UTM’s log file interface.

BBEdit1

If I want to look at only entries for 192.168.5.5 and 192.168.5.109, I can open the Search dialog box with ⌘F and put in a regular expression to search for that.

BBEdit2

Right in the Search dialog box, before even hitting “Find All”, it shows me at the bottom how many matches there are – 3505 of them.

If I do hit “Find All”, it opens a new window containing those lines at the top of the window, and, if I click on one of them, the log file they are in with the highlighted line in view at the bottom of the window. It’s great for contextualizing matches.

BBEdit11

But what if I want a document that contains those matching lines and nothing else? That’s what the “Extract” button on the Search dialog box is for. By clicking that instead of “Find All”, a new document is created in BBEdit that contains just the matching lines named “Extracted occurrences of (insert regular expression search string here)”.

BBEdit3

I use this all the time to drill down further and further without having to edit any files or keep search results windows open. Let’s say I have a subset of IP addresses extracted, like anything in the 192.168.5.x range, and I want to see only lines that have a firewall action of “drop” in them. In other words, I want to ignore all cases where the firewall passed the traffic along and only see when it blocked it for those IP addresses.

Very simple. While viewing the document of extracted 192.168.5 matches, ⌘F again and enter a new regular expression looking for lines where action=“drop”. The search dialog shows 29 matches.

BBEdit6

Hit the “Extract” button and extract the 29 matches to a new document.

BBEdit8

Sometimes when viewing results like this I want blank lines between them so I use a regular expression find and replace when doing this, and click the “Replace All” button in the search dialog, which acts on the active document and replaces all matches with what I want – in my case, the same result lines but followed by 2 newlines after each of them.

BBEdit9

Much better for my tired old eyes.

BBEedit10

And now for some additional niceties of BBEdit search:

  1. You can save your searches and access them using the “g” button on the Search dialog to get a dropdown list to choose from.

BBEdit12

  1. You can also, as my examples in this post illustrate, pull up previously used searches from the search history feature by clicking the clock icon button on the search dialog, and choosing from the dropdown list.

BBEdit13

  1. There’s also a multi-file search you can open separately from regular search, with the same features mentioned above, using ⇧⌘F. It lets you choose the files to search in with great granularity, and it has the same “Find All”, “Replace All”, and “Extract” functions as the regular active file search function. This means you can extract all instances of a specific thing contained in multiple files to one new document.

BBEdit14

One important thing about composing a search in BBEdit if you do want to extract to a new document is that you need to compose your regular expression to include a full line and not just the exact search terms specified. For example, if you just search for “192.168.5” like this, and then Extract:

BBEdit16

You get this:

BBEdit15

Not very useful if you’re into context or understanding of any kind. But if you use a regular expression that looks like this (since in my case, the log is separate lines of logged events):

BBEdit17

Now you get the full line for each matching line:

BBEdit18

That’s because the search was created with a regular expression of ^.*192\.168\.5.*$, which looks for start of line, any characters any number of times followed by 192.168.5, followed by any number of any characters, followed by end of line. When using regular expressions as search terms, make sure that the Grep option next to “Matching:” in the search dialog box is checked so that the search term acts as a regular expression instead of a literal string.

I’m sure there’s much more that I haven’t even discovered about BBEdit and searching and extracting yet, but these are some things that I use all the time and find indispensable. If your job or hobby requires you to parse log files very often, I recommend a copy of BBEdit even if you never use it for anything else. It’s still very worth the price for that.

Footnotes

  1. I’m old too, so this is fair game. And it’s true. I don’t think many new and or young Mac users use or even know about BBEdit.

Astro 5 Upgrade

Part of the Astro series

Astro 5 is out, and it has a number of changes from Astro 4. The good news is, I was able to upgrade this site to it without making any changes at all. That doesn’t mean I don’t have any to make – I need to convert my Content Collections to the new Content Layer API at the very least, for example.

But the fact that it worked as is enabled me to update all my packages to the latest versions and then worry about updating specific implementation details later.

I do want to point out that if you have any sites using the Astro Starlight documentation site framework, you can’t upgrade to Astro 5 yet. It will not compile. There is a PR in place and the stellar Astro documentation team is all over it!

I’ll make notes about the actual changes I do make to this site to be fully Astro 5 compliant and write a post about it soon.