roostdocs

Roost Script

Roost Script is Roost's JavaScript-style language. If you know JavaScript, you already know most of it. It runs the same commands as Roost Basic, with the same limits and safety rules.

Starting a script

A script is Roost Script when its first line is // roost script. script new tidy --js starts one, and the editor's language menu switches a script between Roost Basic and Roost Script (it changes that first line and the comments, not your code).

Roost Script
// roost script
const yt = tabs({ site: 'youtube.com' })
print(`You have ${yt.length} YouTube tabs`)

Script words and memory

Words after a script's name are in args: tidy youtube.com gives args[0] === 'youtube.com'. remember('name', value) keeps a value for the next run, and recall('name', default) reads it back.

Roost Script
// roost script
// visits: count how often I tidy a site
const site = args[0] ?? 'youtube.com'
const n = recall(site, 0) + 1
close(tabs({ site }))
remember(site, n)
print(`Tidied ${site} ${n} times`)

Commands are functions

Every Roost command is a function with the same name. Words become arguments; options go in a last { } object, where true gives a flag. Dashes become capitals.

Roost ScriptRuns
tabs()tabs
tabs({ site: 'x.com', older: '3d' })tabs --site x.com --older 3d
open('fruit bat diet', { here: true })open "fruit bat diet" --here
waitFor('Loading', { gone: true })wait-for Loading --gone
click('Reply', { nth: 2 })click Reply --nth 2

Values are quoted for you, so text from a page can never turn into another command.

Lists and pipes

Commands that find things return a list of items. Each tab has .title, .url, .site, .number, .pinned and more. To pipe a list into a command, pass it as an argument, or call the command on the list with a dot:

Roost Script
const old = tabs({ older: '3d', unpinned: true })
close(old)                          // $old | close
group(old, 'Old', { color: 'grey' })  // $old | group Old --color grey
links().grep(/pdf/i).head(3).open() // links | grep -i pdf | head 3 | open

Lists have the usual JavaScript methods (map, filter, find, some, every, reduce, sort, join, slice, includes…) plus Roost's grep, head, tail, uniq, first and last.

Commands with actions

Commands like chirp, wallet, torrent, session and script take an action as their first word. Write it after a dot:

Roost Script
chirp.say('#rust', 'hello from Roost')
const unread = chirp.unread()
print(wallet.balance())
torrent.pause('all')
session.save('research', { close: true })

Output

A command on a line by itself shows its output in Roost, just like in Roost Basic. Used as a value (const t = tabs()), it's quiet and returns what it found: a list of items, or text if it only printed one line.

The language

  • let, const (and var); if/else, switch, the ternary ? :
  • for (const x of list), for (const key in obj), for (;;), while, do … while, break, continue
  • Functions, arrow functions and closures; default and rest parameters; return
  • Objects, arrays, destructuring (const { title, url } = tab), ...spread
  • Templates (`Hi ${name}`), regular expressions (/bat/i), ?., ??, typeof, ===
  • try/catch/finally and throw new Error('why')
  • await is accepted but never needed: every command already waits until it's done.

Built in

print(…) console.log(…)Show values in Roost
wait('2s')Pause (500, '2s', '1m'; a minute at most)
fail('why') stop()Stop with a message; end the script
range(5) range(1, 4)Lists of numbers
has() exists() read() readAll() value()Look at the page you're on (see Page checks)
title() url() site(url) tabCount()The browser
hour() minute() weekday() today() now()The time
Math JSON Object.keys/values/entries/assign/fromEntries Array.isArray/from Date.now()The usual helpers
String() Number() Boolean() parseInt() parseFloat() isNaN()Conversions

Text has length, includes, startsWith, endsWith, indexOf, slice, split, replace, replaceAll, trim, toUpperCase, toLowerCase, padStart, padEnd, repeat, match and more.

Classes

Classes work like JavaScript's: fields, a constructor, methods, static members, extends, super(…) and super.method(…), new, this and instanceof. A class can extend Error for your own error types.

Roost Script
// roost script
class Tracker {
  seen = []
  static made = 0
  constructor(site) { this.site = site; Tracker.made++ }
  check() {
    const now = tabs({ site: this.site }).map(t => t.url)
    const fresh = now.filter(u => !this.seen.includes(u))
    this.seen.push(...fresh)
    return fresh
  }
}

class LoudTracker extends Tracker {
  check() {
    const fresh = super.check()
    if (fresh.length) notify(`${fresh.length} new on ${this.site}`)
    return fresh
  }
}

const t = new LoudTracker('news.example')
t.check()

this is the object a method was called on; arrow functions keep the this around them. Methods taken off an object (const f = obj.method) stay attached to it. Getters, setters and #private fields aren't supported: use methods and normal names.

Math and hashes

Math has JavaScript's functions, plus extras for lists of numbers (text like "$18.99" or "1,234" counts too):

Math.sum(list) Math.avg(list) Math.median(list) Math.stdev(list)Totals and averages
Math.clamp(x, lo, hi) Math.roundTo(x, 2) Math.percent(part, whole)Keeping numbers in range and tidy
Math.format(1234.5) → "1,234.50", (1234.5).toLocaleString()Numbers for people
Math.gcd Math.lcm Math.isPrime Math.lerp Math.randomInt(1, 6)The rest

Hashes give hex text: md5('hello'), sha1(), sha256(), sha384(), sha512(), or hash(text, 'sha512'). Use them to check text against a published checksum or to notice when part of a page changed (store the hash with remember). md5 and sha1 are fine for that, but not for anything secret.

Roost Script
// roost script
// terms-watch: tell me when a site's terms page changes
open(args[0], { here: true })
const now = sha256(read('main') || '')
if (recall('terms', '') && recall('terms') !== now) notify('The terms changed')
remember('terms', now)

What isn’t there

Roost Script is a small language with its own interpreter, not a JavaScript engine. It has no getters or setters, no #private fields, no async, generators, imports or eval, and no way to reach Node, the network or your files. Regular expressions must be simple: patterns that repeat a repeat (like (a+)+) are refused, because they can take forever.

Errors

A failed command throws an error you can catch; e.message says why. Mistakes in the script itself are shown with their line number before anything runs, and the editor marks them as you type.

Roost Script
try {
  click('Next page')
} catch (e) {
  print('No next page:', e.message)
}
Can’t be caught: Ctrl C, answering no to one of Roost’s questions, and the safety limits. They always stop the whole script.

At the prompt

js runs one line of Roost Script and shows its value. Variables made with let and const stay for the next js line.

Roost
roost:~$ js tabs().length
12
roost:~$ js const yt = tabs({ site: 'youtube.com' })
roost:~$ js yt.map(t => t.title)
["Lo-fi beats", "Bat documentary"]

A full example

Roost Script
// roost script
// deals: check the prices on my watch list and tell me about drops
const pages = bookmarks('Watch list')
const found = []
for (const p of pages) {
  open(p.url, { here: true })
  waitFor('$')
  const price = parseFloat((read('.price') || '').replace(/[^0-9.]/g, ''))
  if (price && price < 50) found.push(`${title()}: $${price}`)
}
if (found.length) notify(found.join(' · '))
else print('No deals today')