BitBar, SuperDuper, and library books
August 23, 2020 at 3:57 PM by Dr. Drang
Jason Snell’s recent post on BitBar inspired me to build a couple of menu bar notices of my own.
The first was a rewrite of the SuperDuper notice I used to use with a similar menu bar utility called AnyBar. It puts a thumbs up in the menu bar if the most recent scheduled SuperDuper backup was successful, and a thumbs down if it wasn’t. Clicking on the item brings up a summary of SuperDuper’s log file:
The log summary is in gray text because it doesn’t do anythingβit’s like a series of disabled menu items.
The second BitBar notice is a little more complicated. It gets the status of items my family has checked out or on hold at our local library and presents them in the menu. If a checked-out item is due soon (or overdue) or if an item on hold is ready to be picked up, the book item in the menu bar is red; otherwise, it’s blue. Either way, the items are listed in the submenus.1
In addition to the submenus, this one also allows some items in the menu to be chosen. If an item is due soon, it will be enabled in the menu, and choosing it will open Safari to the library’s login page. This speeds up renewing a book if I want to keep it longer.
BitBar uses the daunting words “plugin” and “API” to describe the code that configures these menu bar items, but they’re just programs that write lines of text to standard output. If you’ve ever written any kind of program in any language, you can write a BitBar plugin. There are several rules for the output, but the main ones are:
- Multiple lines will be cycled through over and over.
- If your output contains a line consisting only of
---
, the lines below it will appear in the dropdown for that plugin, but won’t appear in the menu bar itself.- Lines beginning with
--
will appear in submenus.
You save the programs in a folder that you set up when you run BitBar the first time (before you have any plugins written). I use ~/.bitbar
.
My SuperDuper plugin, named superduper.6h.py
in accordance with the BitBar plugin naming format, is this Python script:
python:
1: #!/usr/bin/python3
2:
3: import os
4:
5: # Where the SuperDuper! log files are.
6: logdir = (os.environ["HOME"] +
7: "/Library/Application Support/" +
8: "SuperDuper!/Scheduled Copies/" +
9: "Smart Update Backup from Macintosh HD.sdsp/Logs/")
10:
11: def sdinfo(s):
12: "Return just the timestamp and process information from a SuperDuper line."
13: parts = s.split('|')
14: ratespot = parts[3].find("at an effective transfer rate")
15: if ratespot > -1:
16: parts[3] = parts[3][:ratespot]
17: detailspot = parts[3].find("(")
18: if detailspot > -1:
19: parts[3] = parts[3][:detailspot]
20: return "%s: %s" % (parts[1].strip(), parts[3].strip(' \\\n'))
21:
22: # Get the last log file.
23: logfiles = [x for x in os.listdir(logdir) if x[-5:] == 'sdlog']
24: logfiles.sort()
25: lastlog = logdir + logfiles[-1]
26:
27: # Collect data for BitBar
28: notices = []
29: with open(lastlog) as f:
30: for line in f:
31: for signal in ["Started on", "PHASE:", "Copied", "Cloned", "Copy complete"]:
32: if signal in line:
33: notices.append(sdinfo(line))
34:
35: # Format output for BitBar
36: if "Copy complete." in notices[-1]:
37: print("ππ»")
38: else:
39: print("ππ»")
40: print("---")
41: print("\n".join(notices))
The bulk of this script was taken from the predecessor to my AnyBar script, which I wrote for GeekTool.2 It reads through the long SuperDuper log file and plucks out just the lines of interest, putting them in the list notices
.
The last section, Lines 36β41, looks at the last item in notices
to see if the backup finished. If it did, it prints a ππ»; if not, it prints a ππ». Then comes the ---
separator in Line 40 to divide the menu title from the stuff in the dropdown. Finally, all the items in notices
are printed line by line to populate the menu itself.
The library plugin, library.3h.py
, consists of the following Python code, most of which was copied from an old script that sends me a daily email with the status of the family library accounts.
python:
1: #!/usr/bin/python3
2:
3: import mechanize
4: from bs4 import BeautifulSoup
5: from datetime import timedelta, datetime
6: import re
7: import textwrap
8:
9: # Family library cards
10: cardList = [
11: {'patron' : 'Dad', 'code' : '12345678901234', 'pin' : '1234'},
12: {'patron' : 'Mom', 'code' : '98765432109876', 'pin' : '9876'},
13: {'patron' : 'Son1', 'code' : '91827364555555', 'pin' : '4321'},
14: {'patron' : 'Son2', 'code' : '11223344556677', 'pin' : '5678'}]
15:
16:
17: # The login URL for the library's account information.
18: lURL = 'https://library.naperville-lib.org/iii/cas/login?service=https%3A%2F%2Flibrary.naperville-lib.org%3A443%2Fpatroninfo~S1%2FIIITICKET&scope=1'
19:
20: # Initialize the lists of checked-out and on-hold items.
21: checkedOut = []
22: onHold = []
23:
24: # Dates to compare with due dates. "Soon" is 2 days from today.
25: today = datetime.now()
26: soon = datetime.now() + timedelta(2)
27:
28: # Function that returns a truncated string
29: def shortened(s, length=30):
30: try:
31: out = s[:s.index(' / ')]
32: except ValueError:
33: out = s
34: out = out.strip()
35: lines = textwrap.wrap(out, width=length)
36: if len(lines) > 1:
37: return lines[0] + 'β¦'
38: else:
39: return out
40:
41: # Go through each card, collecting the lists of items.
42: for card in cardList:
43: # Open a browser and login
44: br = mechanize.Browser()
45: br.set_handle_robots(False)
46: br.open(lURL)
47: br.select_form(nr=0)
48: br.form['code'] = card['code']
49: br.form['pin'] = card['pin']
50: br.submit()
51:
52: # We're now on either the page for checked-out items or for holds.
53: # Get the URL and figure out which page we're on.
54: pURL = br.response().geturl()
55: if pURL[-5:] == 'items': # checked-out items
56: cHtml = br.response().read() # get the HTML
57: br.follow_link(text_regex='requests? \(holds?\)') # go to holds
58: hHtml = br.response().read() # get the HTML
59: elif pURL[-5:] == 'holds': # holds
60: hHtml = hHtml = br.response().read() # get the HTML
61: br.follow_link(text_regex='currently checked out') # go to checked-out
62: cHtml = br.response().read() # get the HTML
63: else:
64: continue
65:
66: # Parse the HTML.
67: cSoup = BeautifulSoup(cHtml, features="html5lib")
68: hSoup = BeautifulSoup(hHtml, features="html5lib")
69:
70: # Collect the table rows that contain the items.
71: loans = cSoup.findAll('tr', {'class' : 'patFuncEntry'})
72: holds = hSoup.findAll('tr', {'class' : 'patFuncEntry'})
73:
74: # Due dates and pickup dates are of the form mm-dd-yy.
75: itemDate = re.compile(r'\d\d-\d\d-\d\d')
76:
77: # Go through each row of checked out items, keeping only the title and due date.
78: for item in loans:
79: # The title is everything before the spaced slash in the patFuncTitle
80: # string. Some titles have a patFuncVol span after the title string;
81: # that gets filtered out by contents[0]. Interlibrary loans
82: # don't appear as links, so there's no <a></a> inside the patFuncTitle
83: # item.
84: title = item.find('td', {'class' : 'patFuncTitle'}).text
85:
86: # The due date is somewhere in the patFuncStatus cell.
87: dueString = itemDate.findall(item.find('td', {'class' : 'patFuncStatus'}).contents[0])[0]
88: due = datetime.strptime(dueString, '%m-%d-%y')
89: # Add the item to the checked out list. Arrange tuple so items
90: # get sorted by due date.
91: checkedOut.append((due, card['patron'], title))
92:
93: # Go through each row of holds, keeping only the title and place in line.
94: for item in holds:
95: # Again, the title is everything before the spaced slash. Interlibrary loans
96: # are holds that don't appear as links, so there's no <a></a> inside the
97: # patFuncTitle item.
98: title = item.find('td', {'class' : 'patFuncTitle'}).text
99:
100: # The book's status in the hold queue will be either:
101: # 1. 'n of m holds'
102: # 2. 'Ready. Must be picked up by mm-dd-yy' (obsolete?)
103: # 3. 'DUE mm-dd-yy'
104: # 4. 'IN TRANSIT'
105: status = item.find('td', {'class' : 'patFuncStatus'}).contents[0].strip()
106: n = status.split()[0]
107: if n.isdigit(): # possibility 1
108: n = int(n)
109: status = status.replace(' holds', '')
110: elif n[:5].lower() == 'ready' or n[:3].lower() == 'due': # possibilities 2 & 3
111: n = -1
112: readyString = itemDate.findall(status)[0]
113: ready = datetime.strptime(readyString, '%m-%d-%y')
114: status = 'Ready<br/> ' + ready.strftime('%b %d')
115: else: # possibility 4
116: n = 0
117:
118: # Add the item to the on hold list. Arrange tuple so items
119: # get sorted by position in queue. The position is faked for
120: # items ready for pickup and in transit within the library.
121: onHold.append((n, card['patron'], title, status))
122:
123:
124: # Sort the lists.
125: checkedOut.sort()
126: onHold.sort()
127:
128: # Assemble the information for the menu
129: checkedAlert = False
130: holdAlert = False
131: checkedLines = []
132: holdLines = []
133: for item in checkedOut:
134: suffix = ''
135: if item[0] <= soon:
136: suffix = f'|href="{lURL}"'
137: checkedAlert = True
138: checkedLines.append(item[0].strftime('--%b %-d ') + shortened(item[2]) + suffix)
139: for item in onHold:
140: suffix = ''
141: if "Ready" in item[3]:
142: suffix = '|color=#700000'
143: holdAlert = True
144: holdLines.append('--' + shortened(item[2]) + suffix)
145:
146: # Print the information in BitBar format
147: if checkedAlert or holdAlert:
148: print('π')
149: else:
150: print('π')
151: print('---')
152: if checkedAlert:
153: print('Checked out|color=#CC0000')
154: else:
155: print('Checked out')
156: print('\n'.join(checkedLines))
157: if holdAlert:
158: print('Holds|color=#700000')
159: else:
160: print('Holds')
161: print('\n'.join(holdLines))
The BitBar-specific stuff starts on Line 128. It’s more complicated than the SuperDuper script, in that:
- The character used for the menu title is based on two criteria instead of just one.
- It has submenus, the items of which are distinguished by a
--
prefix. - It uses the
|color=#700000
suffix to make overdue or ready items red. I’ve found BitBar’s coloring of items somewhat sketchy. Often the items start off black and don’t turn red until I’ve moved the mouse over them, away from them, and then back over them again. - It uses the
|href=
suffix to enable the menu item and cause Safari to open the given URL when chosen.
Still, there’s not much to the BitBar part of either of these scripts. Now I’m wondering what other informational scripts I have that can be converted into BitBar form.
-
No, the Nero Wolfe video wasn’t actually overdue. Because of the pandemic, our library quarantines returned items for a few days before updating their status in the system and reshelving. So until the Current Situation is over, this notice will be red more than it should be. ↩
-
Seems longer than six years since I used GeekTool; I stopped because I usually have too many windows open to see information written on the desktop. ↩