├── .gitignore ├── test ├── .gitattributes ├── data │ ├── whitespace.txt │ ├── filenames.txt │ ├── grades.csv │ ├── passwd.txt │ ├── simple.xml │ ├── users.json │ ├── lorem.txt │ ├── website.html │ ├── aduros-website.html │ ├── jwz-rss2html.html │ └── aduros-rss.xml ├── local-import.js ├── standalone ├── rss2html └── run ├── .npmignore ├── .circleci └── config.yml ├── lib ├── line-iterator.js ├── csv-iterator.js ├── xml-iterator.js ├── print.js ├── json-iterator.js ├── cli.js ├── html-iterator.js └── generate.js ├── index.js ├── LICENSE.txt ├── Makefile ├── package.json ├── bin └── pjs ├── doc └── manual.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | man/ 3 | -------------------------------------------------------------------------------- /test/.gitattributes: -------------------------------------------------------------------------------- 1 | data/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.circleci 2 | /Makefile 3 | /dist 4 | /test 5 | -------------------------------------------------------------------------------- /test/data/whitespace.txt: -------------------------------------------------------------------------------- 1 | This 2 | file has 3 | trailing 4 | WHITESPACE ! 5 | -------------------------------------------------------------------------------- /test/local-import.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.method = function () { 4 | return 123; 5 | } 6 | -------------------------------------------------------------------------------- /test/data/filenames.txt: -------------------------------------------------------------------------------- 1 | data/not-found.txt 2 | run 3 | data/grades.csv 4 | data/not-found.txt 5 | data/users.json 6 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@3.0.0 4 | workflows: 5 | node-tests: 6 | jobs: 7 | - node/test 8 | -------------------------------------------------------------------------------- /test/data/grades.csv: -------------------------------------------------------------------------------- 1 | name,subject,grade 2 | Bob,physics,43 3 | Alice,biology,75 4 | Alice,physics,90 5 | David,biology,85 6 | Clara,physics,78 7 | -------------------------------------------------------------------------------- /lib/line-iterator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (input) { 4 | return require("readline").createInterface({ input }); 5 | } 6 | -------------------------------------------------------------------------------- /test/data/passwd.txt: -------------------------------------------------------------------------------- 1 | root:x:0:0:root:/root:/bin/bash 2 | daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin 3 | bin:x:2:2:bin:/bin:/usr/sbin/nologin 4 | sys:x:3:3:sys:/dev:/usr/sbin/nologin 5 | -------------------------------------------------------------------------------- /lib/csv-iterator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (input, {columns} = {}) { 4 | const csvParse = require("csv-parse"); 5 | return input.pipe(csvParse({columns})); 6 | }; 7 | -------------------------------------------------------------------------------- /lib/xml-iterator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (input, {selector}) { 4 | const htmlIterator = require("./html-iterator"); 5 | return htmlIterator(input, {selector, xmlMode: true}); 6 | }; 7 | -------------------------------------------------------------------------------- /test/data/simple.xml: -------------------------------------------------------------------------------- 1 | 2 | xxx 3 | yyy 4 | zzz 5 | abc 6 | 7 | 8 | 666 9 | 10 | -------------------------------------------------------------------------------- /lib/print.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** Print arguments, converting objects to JSON. */ 4 | module.exports = function (...messages) { 5 | messages = messages.map(o => (typeof o == "object") ? JSON.stringify(o, null, " ") : o); 6 | process.stdout.write(messages.join(" ")+"\n"); 7 | } 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | generate: require("./lib/generate"), 5 | print: require("./lib/print"), 6 | eachLine: require("./lib/line-iterator"), 7 | eachCsv: require("./lib/csv-iterator"), 8 | eachJson: require("./lib/json-iterator"), 9 | eachHtml: require("./lib/html-iterator"), 10 | eachXml: require("./lib/xml-iterator"), 11 | }; 12 | -------------------------------------------------------------------------------- /test/standalone: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pjs -f 2 | // 3 | // This is a standalone pjs script! 4 | 5 | BEFORE: { 6 | calledBefore = 100; 7 | calledBefore += 23; 8 | } 9 | 10 | _.toUpperCase(); 11 | 12 | AFTER: { 13 | if (calledBefore == 123) { 14 | print("Done!"); 15 | } else { 16 | throw new Error("Assert: Didn't call BEFORE?"); 17 | } 18 | } 19 | 20 | // vi: ft=javascript 21 | -------------------------------------------------------------------------------- /test/data/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 123, 3 | "items": [ 4 | {"name": {"first": "Winifred", "last": "Frost"}, "age": 42}, 5 | {"name": {"first": "Miles", "last": "Fernandez"}, "age": 15}, 6 | {"name": {"first": "Kennard", "last": "Floyd"}, "age": 20}, 7 | {"name": {"first": "Lonnie", "last": "Davis"}, "age": 78}, 8 | {"name": {"first": "Duncan", "last": "Poole"}, "age": 36} 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/data/lorem.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque pulvinar, odio 2 | a ultrices facilisis, nisi lorem tempus lectus, vel rutrum dolor odio a velit. 3 | Aliquam pharetra maximus justo, ac vestibulum massa molestie non. Fusce eget 4 | quam aliquam, viverra quam quis, maximus nulla. Mauris tincidunt ut tellus at 5 | tincidunt. Proin nec est mi. Fusce sollicitudin elit et consequat volutpat. In 6 | ac vehicula enim. 7 | -------------------------------------------------------------------------------- /test/data/website.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bob's Homepage 7 | 8 | 9 | 10 |

Welcome To My Geocities Homepage

11 | 12 |
13 | Under CONSTRUCTION 14 | 15 |
16 | 17 | 18 | 19 |
20 | Visitor counter: 1234 21 |
22 | 23 |

Guestbook

24 | 25 | Sign my Guestbook! 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Bruno Garcia 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /test/rss2html: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S pjs --xml item -f 2 | // 3 | // Example of how of using a self-contained script to convert rss to html 4 | // 5 | // Usage: curl https://cdn.jwz.org/blog/feed/ | ./rss2html > output.html 6 | 7 | BEFORE: ` 8 | 9 | 10 | 11 |

RSS Feeds (Generated on ${new Date()}

12 | ` 13 | 14 | const link = _.querySelector("link"); 15 | const title = _.querySelector("title"); 16 | const description = _.querySelector("description"); 17 | const author = _.querySelector("author"); 18 | const pubDate = _.querySelector("pubDate"); 19 | 20 | ` 21 |

${COUNT}. ${title.text}

22 |

Posted on ${pubDate.text}

23 |

${description.text}

24 | ` 25 | 26 | AFTER: ` 27 |

Total posts: ${COUNT}

28 | 29 | 30 | ` 31 | 32 | // vim: ft=javascript 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: man/pjs.1 2 | 3 | .PHONY: all 4 | 5 | man/pjs.1: doc/manual.md 6 | mkdir -p man 7 | echo " \n\ 8 | --- \n\ 9 | title: PJS(1) pjs \n\ 10 | author: Bruno Garcia \n\ 11 | date: `date '+%B, %Y'` \n\ 12 | --- \n\ 13 | " | cat - "$<" | pandoc --standalone -f markdown - -t man -o "$@" 14 | 15 | tarball: 16 | mkdir -p dist 17 | 18 | # Trim shebang and put it back after 19 | tail -n +2 bin/pjs > dist/pjs-entry.js 20 | node_modules/.bin/webpack --target node --entry ./dist/pjs-entry.js \ 21 | --mode production -o dist --output-filename pjs-packed.js 22 | echo '#!/usr/bin/env node' | cat - dist/pjs-packed.js > dist/pjs 23 | 24 | # Build standalone JS release 25 | chmod +x dist/pjs 26 | tar -cjf dist/pjs-latest.tar.bz2 -C dist pjs 27 | 28 | prepack: all 29 | # Ensure no untracked files 30 | [ -z "`git status --porcelain`" ] 31 | 32 | prepublish: prepack tarball 33 | scp dist/pjs-latest.tar.bz2 aduros:web/pjs 34 | -------------------------------------------------------------------------------- /lib/json-iterator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (input, {filter}) { 4 | const jq = require("jq-in-the-browser").default; 5 | const query = jq(filter); 6 | 7 | const JSONStream = require("JSONStream"); 8 | const parser = JSONStream.parse(); 9 | 10 | const { Transform } = require("stream"); 11 | const extractor = new Transform({ 12 | objectMode: true, 13 | 14 | transform (object, encoding, callback) { 15 | const elements = query(object); 16 | if (Array.isArray(elements)) { 17 | for (const element of elements) { 18 | this.push(element); 19 | } 20 | } else { 21 | this.push(elements); 22 | } 23 | callback(); 24 | }, 25 | }); 26 | 27 | parser.on("error", function (error) { 28 | extractor.emit("error", error); 29 | }); 30 | 31 | return input.pipe(parser).pipe(extractor); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const commander = require("commander"); 4 | const pkg = require("../package.json"); 5 | 6 | function collect (value, previous) { 7 | if (previous == null) { 8 | previous = []; 9 | } 10 | return previous.concat([value]); 11 | } 12 | 13 | module.exports = function () { 14 | return new commander.Command() 15 | .description(pkg.description+"\n\nFor complete documentation, run `man pjs` or visit https://github.com/aduros/pjs") 16 | .arguments("[script-text]") 17 | .arguments("[file...]") 18 | .option("-x, --explain", "Print generated program instead of running it") 19 | .option("-b, --before ", "Run script before parsing", collect) 20 | .option("-a, --after ", "Run script after parsing", collect) 21 | .option("-f, --file ", "Load script from a file") 22 | .option("-d, --delimiter ", "The delimiter for text parsing", "\\s+") 23 | .option("--csv", "Parse input as CSV") 24 | .option("--csv-header", "Parse input as CSV with a column header") 25 | .option("--json ", "Parse input as JSON") 26 | .option("--html ", "Parse input as HTML") 27 | .option("--xml ", "Parse input as XML") 28 | .version(pkg.version) 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pjs-tool", 3 | "version": "1.2.2", 4 | "description": "An awk-like command-line tool for processing text, CSV, JSON, HTML, and XML.", 5 | "directories": { 6 | "bin": "./bin", 7 | "doc": "./doc", 8 | "lib": "./lib", 9 | "man": "./man", 10 | "test": "./test" 11 | }, 12 | "author": "Bruno Garcia ", 13 | "license": "ISC", 14 | "homepage": "https://github.com/aduros/pjs", 15 | "bugs": "https://github.com/aduros/pjs/issues", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/aduros/pjs.git" 19 | }, 20 | "keywords": [ 21 | "shell", 22 | "cli", 23 | "pipe", 24 | "awk", 25 | "jq", 26 | "sort", 27 | "cut", 28 | "sed", 29 | "grep", 30 | "wc", 31 | "uniq", 32 | "csv", 33 | "json", 34 | "html", 35 | "xml" 36 | ], 37 | "scripts": { 38 | "prepack": "make prepack", 39 | "prepublishOnly": "make prepublish", 40 | "test": "test/run" 41 | }, 42 | "dependencies": { 43 | "JSONStream": "^1.3.5", 44 | "acorn": "^8.0.4", 45 | "ast-types": "^0.14.2", 46 | "astring": "^1.6.0", 47 | "commander": "^7.0.0", 48 | "css-select": "^3.1.2", 49 | "csv-parse": "^4.14.2", 50 | "domutils": "^2.4.4", 51 | "estraverse": "^5.2.0", 52 | "htmlparser2": "^6.0.0", 53 | "jq-in-the-browser": "^0.7.2" 54 | }, 55 | "devDependencies": { 56 | "webpack": "^5.16.0", 57 | "webpack-cli": "^4.4.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/html-iterator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (input, {selector, xmlMode}) { 4 | const { DomHandler } = require("domhandler"); 5 | const { WritableStream } = require("htmlparser2/lib/WritableStream"); 6 | const cssSelect = require("css-select"); 7 | 8 | function toObject (node) { 9 | const obj = { 10 | type: node.type, 11 | get text () { 12 | const { getText } = require("domutils"); 13 | return getText(this); 14 | }, 15 | get innerHTML () { 16 | const { getInnerHTML } = require("domutils"); 17 | return getInnerHTML(this); 18 | }, 19 | querySelector (selector) { 20 | const result = cssSelect.selectOne(selector, node, {xmlMode}); 21 | return (result != null) ? toObject(result) : null; 22 | }, 23 | querySelectorAll (selector) { 24 | return cssSelect.selectAll(selector, node, {xmlMode}).map(toObject); 25 | }, 26 | }; 27 | if (node.name != null) { 28 | obj.name = node.name; 29 | } 30 | if (node.data != null) { 31 | obj.data = node.data; 32 | } 33 | if (node.attribs != null) { 34 | obj.attr = node.attribs; 35 | } 36 | if (node.children != null) { 37 | obj.children = node.children.map(toObject); 38 | } 39 | return obj; 40 | } 41 | 42 | const promise = new Promise((resolve, reject) => { 43 | const handler = new DomHandler((err, dom) => { 44 | if (err != null) { 45 | reject(err); 46 | } else { 47 | const nodes = cssSelect.selectAll(selector, dom, {xmlMode}); 48 | resolve(nodes.map(toObject)); 49 | } 50 | }); 51 | 52 | const writable = new WritableStream(handler, { xmlMode }); 53 | if (typeof window != "undefined") { 54 | // Browser stream polyfills seem to require some manual intervention 55 | writable.on("finish", function () { 56 | writable._final(() => {}); 57 | }); 58 | } 59 | input.pipe(writable); 60 | }); 61 | 62 | return { 63 | [Symbol.asyncIterator]: () => { 64 | let n = 0; 65 | return { 66 | async next () { 67 | const elements = await promise; 68 | return n >= elements.length ? { done: true } : { value: elements[n++] }; 69 | } 70 | }; 71 | } 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /bin/pjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Disable strict mode 4 | // "use strict"; 5 | 6 | const fs = require("fs"); 7 | const cli = require("../lib/cli"); 8 | 9 | const program = cli().action(async (script, files, options) => { 10 | if (options.file != null) { 11 | if (script != null) { 12 | // When using -f, assume script was an input file name 13 | files.unshift(script); 14 | } 15 | script = fs.readFileSync(options.file == "-" ? 0 : options.file, "utf8"); 16 | 17 | // Trim shebang 18 | if (script.charAt(0) == "#") { 19 | const idx = script.indexOf("\n"); 20 | script = script.substring(idx); 21 | } 22 | } 23 | 24 | // Must include at least one script 25 | if (options.before == null && script == null && options.after == null) { 26 | program.help({error: true}); 27 | } 28 | 29 | const generate = require("../lib/generate"); 30 | var generated; 31 | try { 32 | let mode, markupSelector; 33 | if (options.csv || options.csvHeader) { 34 | mode = "csv"; 35 | } else if (options.json != null) { 36 | mode = "json"; 37 | } else if (options.html != null) { 38 | mode = "html"; 39 | markupSelector = options.html; 40 | } else if (options.xml != null) { 41 | mode = "xml"; 42 | markupSelector = options.xml; 43 | } 44 | 45 | generated = generate({ 46 | mode, 47 | beforeJs: options.before ? options.before.join("\n;") : null, 48 | lineJs: script, 49 | afterJs: options.after ? options.after.join("\n;") : null, 50 | inputStream: options.explain ? null : "__inputStream", 51 | 52 | textDelimiter: (options.delimiter != null) ? new RegExp(options.delimiter) : null, 53 | csvHeader: options.csvHeader, 54 | jsonFilter: options.json, 55 | markupSelector: markupSelector, 56 | }); 57 | 58 | } catch (error) { 59 | if (process.env.PJS_DEBUG) { 60 | console.error(error); 61 | } else { 62 | console.error("Parse error: "+error.message); 63 | } 64 | process.exit(1); 65 | } 66 | if (options.explain) { 67 | process.stdout.write(generated); 68 | } else { 69 | process.stdout.on("error", error => { 70 | // Ignore errors about closed pipes 71 | }); 72 | 73 | const pjsModule = require(".."); 74 | function customRequire (name) { 75 | // Override require() in order to always locate pjs-tool 76 | if (name == "pjs-tool") { 77 | return pjsModule; 78 | } 79 | 80 | // The original untouched require() 81 | const requireFn = (typeof __non_webpack_require__ != "undefined") 82 | ? __non_webpack_require__ 83 | : require; 84 | 85 | // Transform to resolve relative to the current directory 86 | name = requireFn.resolve(name, {paths: [process.cwd()]}); 87 | 88 | return requireFn(name); 89 | } 90 | 91 | const fn = new Function("__inputStream", "FILENAME", "require", "return " + generated); 92 | async function invokeFn (inputStream, filename) { 93 | try { 94 | await fn(inputStream, filename, customRequire); 95 | } catch (error) { 96 | console.error("An error occurred when running your script. Consider using --explain to help debug.\n"); 97 | console.error(error); 98 | process.exit(1); 99 | } 100 | } 101 | 102 | if (files.length > 0) { 103 | // Run on each file sequentially 104 | for (const file of files) { 105 | if (file == "-") { 106 | // Special case where passed filename was "-" 107 | await invokeFn(process.stdin, null); 108 | } else { 109 | await invokeFn(fs.createReadStream(file), file); 110 | } 111 | } 112 | } else { 113 | // Run on stdin 114 | await invokeFn(process.stdin, null); 115 | } 116 | } 117 | }); 118 | 119 | program.parse(); 120 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # 3 | # pjs unit tests. 4 | 5 | equals () { 6 | tmp="/tmp/test-pjs" 7 | echo -n "$1" | tail -n +2 > "$tmp" 8 | diff - "$tmp" 9 | rm "$tmp" 10 | } 11 | 12 | cd `dirname $(realpath "$0")` 13 | export PATH="`realpath ../bin`:$PATH" 14 | set -v 15 | 16 | seq 1 10 | pjs '_ > 5' | equals ' 17 | 6 18 | 7 19 | 8 20 | 9 21 | 10 22 | ' 23 | 24 | seq 1 10 | pjs '_ % 2 == 0' | equals ' 25 | 2 26 | 4 27 | 6 28 | 8 29 | 10 30 | ' 31 | 32 | cat data/lorem.txt | pjs '_.toUpperCase()' | equals ' 33 | LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. QUISQUE PULVINAR, ODIO 34 | A ULTRICES FACILISIS, NISI LOREM TEMPUS LECTUS, VEL RUTRUM DOLOR ODIO A VELIT. 35 | ALIQUAM PHARETRA MAXIMUS JUSTO, AC VESTIBULUM MASSA MOLESTIE NON. FUSCE EGET 36 | QUAM ALIQUAM, VIVERRA QUAM QUIS, MAXIMUS NULLA. MAURIS TINCIDUNT UT TELLUS AT 37 | TINCIDUNT. PROIN NEC EST MI. FUSCE SOLLICITUDIN ELIT ET CONSEQUAT VOLUTPAT. IN 38 | AC VEHICULA ENIM. 39 | ' 40 | 41 | cat data/lorem.txt | pjs --after 'LINES.slice(-3).join("\n")' | equals ' 42 | quam aliquam, viverra quam quis, maximus nulla. Mauris tincidunt ut tellus at 43 | tincidunt. Proin nec est mi. Fusce sollicitudin elit et consequat volutpat. In 44 | ac vehicula enim. 45 | ' 46 | 47 | cat data/lorem.txt | pjs 'if (_.length > m) { m = _.length; longest = _ }' \ 48 | --after 'longest' | equals ' 49 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque pulvinar, odio 50 | ' 51 | 52 | cat data/lorem.txt | pjs '{ count++ }' --after 'count' | equals ' 53 | 6 54 | ' 55 | 56 | cat data/lorem.txt | pjs --after 'COUNT' | equals ' 57 | 6 58 | ' 59 | 60 | # Not technically correct because empty lines are counted as words 61 | cat data/lorem.txt | pjs '{ words += $.length }' --after 'words' | equals ' 62 | 62 63 | ' 64 | 65 | # Not technically correct because words include punctuation 66 | cat data/lorem.txt | pjs --before 'let words = new Set()' \ 67 | 'for (let word of $) words.add(word)' --after 'words.size' | equals ' 68 | 55 69 | ' 70 | 71 | echo "Hello" | pjs -f ./standalone | equals ' 72 | HELLO 73 | Done! 74 | ' 75 | 76 | pjs -f ./standalone data/lorem.txt | equals ' 77 | LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. QUISQUE PULVINAR, ODIO 78 | A ULTRICES FACILISIS, NISI LOREM TEMPUS LECTUS, VEL RUTRUM DOLOR ODIO A VELIT. 79 | ALIQUAM PHARETRA MAXIMUS JUSTO, AC VESTIBULUM MASSA MOLESTIE NON. FUSCE EGET 80 | QUAM ALIQUAM, VIVERRA QUAM QUIS, MAXIMUS NULLA. MAURIS TINCIDUNT UT TELLUS AT 81 | TINCIDUNT. PROIN NEC EST MI. FUSCE SOLLICITUDIN ELIT ET CONSEQUAT VOLUTPAT. IN 82 | AC VEHICULA ENIM. 83 | Done! 84 | ' 85 | 86 | cat standalone | pjs -f - data/lorem.txt | equals ' 87 | LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. QUISQUE PULVINAR, ODIO 88 | A ULTRICES FACILISIS, NISI LOREM TEMPUS LECTUS, VEL RUTRUM DOLOR ODIO A VELIT. 89 | ALIQUAM PHARETRA MAXIMUS JUSTO, AC VESTIBULUM MASSA MOLESTIE NON. FUSCE EGET 90 | QUAM ALIQUAM, VIVERRA QUAM QUIS, MAXIMUS NULLA. MAURIS TINCIDUNT UT TELLUS AT 91 | TINCIDUNT. PROIN NEC EST MI. FUSCE SOLLICITUDIN ELIT ET CONSEQUAT VOLUTPAT. IN 92 | AC VEHICULA ENIM. 93 | Done! 94 | ' 95 | 96 | cat data/whitespace.txt | pjs '_.replace(/\s*$/, "")' - | equals ' 97 | This 98 | file has 99 | trailing 100 | WHITESPACE ! 101 | ' 102 | 103 | pjs -d : '$5' data/passwd.txt | equals ' 104 | /root 105 | /usr/sbin 106 | /bin 107 | /dev 108 | ' 109 | 110 | pjs -d : '$.slice(0, 2)' data/passwd.txt | equals ' 111 | [ 112 | "root", 113 | "x" 114 | ] 115 | [ 116 | "daemon", 117 | "x" 118 | ] 119 | [ 120 | "bin", 121 | "x" 122 | ] 123 | [ 124 | "sys", 125 | "x" 126 | ] 127 | ' 128 | 129 | pjs "" --after 'FILENAME+": "+COUNT' data/*.txt | equals ' 130 | data/filenames.txt: 5 131 | data/lorem.txt: 6 132 | data/passwd.txt: 4 133 | data/whitespace.txt: 4 134 | ' 135 | 136 | cat data/filenames.txt | pjs 'fs.existsSync(_)' | equals ' 137 | run 138 | data/grades.csv 139 | data/users.json 140 | ' 141 | 142 | cat data/filenames.txt | pjs 'fs.existsSync(_) && fs.statSync(_).size > 200' | equals ' 143 | run 144 | data/users.json 145 | ' 146 | 147 | cat data/filenames.txt | pjs --before 'let s = new Set()' '{ s.add(_) }' \ 148 | --after 's.size' | equals ' 149 | 4 150 | ' 151 | 152 | cat data/website.html | pjs --html 'title' '_.text.trim()' | equals ' 153 | Bob'\''s Homepage 154 | ' 155 | 156 | cat data/website.html | pjs --html '#counter' '_.text' | equals ' 157 | 1234 158 | ' 159 | 160 | cat data/website.html | pjs --html 'img[src]' '_.attr.src' | equals ' 161 | construction.gif 162 | dancing-baby.gif 163 | ' 164 | 165 | cat data/simple.xml | pjs --xml 'item[kind=foo]' --after COUNT | equals ' 166 | 2 167 | ' 168 | 169 | cat data/simple.xml | pjs --xml 'ITEM' --after COUNT | equals ' 170 | 1 171 | ' 172 | 173 | cat data/aduros-website.html | pjs --html 'h1,h2' '_.text' | equals ' 174 | aduros.com 175 | Hacking i3: Automatic Layout 176 | Introducing pjs 177 | Ramblings From a Dirty Apartment 178 | Returning to Text 179 | Hacking i3: Window Promoting 180 | A Blog Half Full 181 | Simple Clipboard Management 182 | Firefox Minimalism 183 | Media Companies are Complicit 184 | Access Recent Files From the Command Line 185 | ' 186 | 187 | cat data/aduros-website.html | pjs --html 'img' '_.attr.src' | equals ' 188 | /profile.jpg 189 | ' 190 | 191 | cat data/aduros-website.html | pjs --html 'ul.c-links a[target=_blank]' '_.attr.href' | equals ' 192 | https://aduros.com/index.xml 193 | https://twitter.com/b_garcia 194 | https://github.com/aduros 195 | https://linkedin.com/in/bruno-garcia-38223415 196 | ' 197 | 198 | cat data/aduros-website.html | pjs --html 'h2 a' \ 199 | '_.attr.href.includes("blog") ? _.attr.href : null' | equals ' 200 | https://aduros.com/blog/hacking-i3-automatic-layout/ 201 | https://aduros.com/blog/introducing-pjs/ 202 | https://aduros.com/blog/ramblings-from-a-dirty-apartment/ 203 | https://aduros.com/blog/returning-to-text/ 204 | https://aduros.com/blog/hacking-i3-window-promoting/ 205 | https://aduros.com/blog/a-blog-half-full/ 206 | https://aduros.com/blog/simple-clipboard-management/ 207 | https://aduros.com/blog/firefox-minimalism/ 208 | https://aduros.com/blog/media-companies-are-complicit/ 209 | https://aduros.com/blog/access-recent-files-from-the-command-line/ 210 | ' 211 | 212 | cat data/users.json | pjs --json '.version' _ | equals ' 213 | 123 214 | ' 215 | 216 | cat data/users.json | pjs --json '.items[].name' '_.first+" "+_.last' | equals ' 217 | Winifred Frost 218 | Miles Fernandez 219 | Kennard Floyd 220 | Lonnie Davis 221 | Duncan Poole 222 | ' 223 | 224 | cat data/users.json | pjs --json '.items[]' '_.age >= 21' | equals ' 225 | { 226 | "name": { 227 | "first": "Winifred", 228 | "last": "Frost" 229 | }, 230 | "age": 42 231 | } 232 | { 233 | "name": { 234 | "first": "Lonnie", 235 | "last": "Davis" 236 | }, 237 | "age": 78 238 | } 239 | { 240 | "name": { 241 | "first": "Duncan", 242 | "last": "Poole" 243 | }, 244 | "age": 36 245 | } 246 | ' 247 | 248 | cat data/users.json | pjs --json '.items[0:3]' '_.age' | equals ' 249 | 42 250 | 15 251 | 20 252 | ' 253 | 254 | # Test concatenated JSON streams 255 | cat data/users.json data/users.json data/users.json | pjs --json '.items[0:3]' '_.age' | equals ' 256 | 42 257 | 15 258 | 20 259 | 42 260 | 15 261 | 20 262 | 42 263 | 15 264 | 20 265 | ' 266 | 267 | echo | pjs --after 'require("./local-import").method()' | equals ' 268 | 123 269 | ' 270 | 271 | # Disable: Circle CI's env doesn't support -S, hmm 272 | # This file should have exactly one line different due to the timestamp 273 | # [ `./rss2html data/jwz.xml | diff - data/jwz-rss2html.html | grep '^>' | wc -l` = 1 ] 274 | 275 | set +v 276 | echo "All tests passed." 277 | -------------------------------------------------------------------------------- /lib/generate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const estraverse = require("estraverse"); 4 | const b = require("ast-types").builders; 5 | 6 | function parse (js, replacements) { 7 | const acorn = require("acorn"); 8 | const ast = acorn.parse(js, { 9 | ecmaVersion: 2020, 10 | allowReturnOutsideFunction: true, 11 | allowAwaitOutsideFunction: true, 12 | }); 13 | 14 | if (replacements) { 15 | function replace (node) { 16 | if (node.type == "Identifier" && node.name in replacements) { 17 | return replacements[node.name]; 18 | } 19 | } 20 | estraverse.replace(ast, { 21 | enter (node, parent) { 22 | if (node.type == "ExpressionStatement") { 23 | if (parent.type == "BlockStatement") { 24 | const statements = replace(node.expression); 25 | if (statements != null) { 26 | const idx = parent.body.indexOf(node); 27 | parent.body.splice(idx, 1, ...statements); 28 | return estraverse.VisitorOption.Skip; 29 | } 30 | } 31 | } else { 32 | return replace(node); 33 | } 34 | }, 35 | }); 36 | } 37 | 38 | return ast; 39 | } 40 | 41 | function replaceLastExpression (nodes) { 42 | // Remove empty statements and flatten block statements 43 | let lastExpressionStatement = null, lastExpressionStatementIdx = -1; 44 | for (let ii = 0; ii < nodes.length; ++ii) { 45 | const node = nodes[ii]; 46 | if (node.type == "EmptyStatement") { 47 | nodes.splice(ii--, 1); 48 | } else if (node.type == "BlockStatement") { 49 | nodes.splice(ii, 1, ...node.body); 50 | ii += node.body.length-1; // Skip over the statements in this block 51 | } else if (node.type == "ExpressionStatement") { 52 | lastExpressionStatement = node; 53 | lastExpressionStatementIdx = ii; 54 | } else { 55 | lastExpressionStatement = null; 56 | lastExpressionStatementIdx = -1; 57 | } 58 | } 59 | 60 | if (lastExpressionStatement != null) { 61 | nodes[lastExpressionStatementIdx] = b.variableDeclaration("var", 62 | [b.variableDeclarator(b.identifier("_result"), lastExpressionStatement.expression)]); 63 | return true; 64 | } 65 | 66 | return false; 67 | } 68 | 69 | module.exports = opts => { 70 | const beforeJs = opts.beforeJs; 71 | const lineJs = opts.lineJs; 72 | const afterJs = opts.afterJs; 73 | 74 | const mode = opts.mode; 75 | const csvHeader = opts.csvHeader; 76 | const jsonFilter = opts.jsonFilter; 77 | const markupSelector = opts.markupSelector; 78 | const textDelimiter = opts.textDelimiter; 79 | const inputStream = opts.inputStream || "process.stdin"; 80 | 81 | const initConsts = []; 82 | const initVars = []; 83 | 84 | // List of AST nodes 85 | let beforeStatements = []; 86 | let afterStatements = []; 87 | 88 | // Features detected by static analysis 89 | const implicitRequires = new Set(); 90 | const implicitAssigns = new Set(); 91 | let hasDollar = false; 92 | let hasCOUNT = false; 93 | let hasLINES = false; 94 | 95 | function transformAst (ast) { 96 | return estraverse.replace(ast, { 97 | enter (node) { 98 | switch (node.type) { 99 | case "Identifier": 100 | if (node.name == "$") { 101 | hasDollar = true; 102 | 103 | } else if (node.name.match(/\$\d+/)) { 104 | // Transform $123 to $[123] 105 | hasDollar = true; 106 | const idx = parseInt(node.name.substr(1), 10); 107 | return b.memberExpression(b.identifier("$"), b.literal(idx), true); 108 | 109 | } else if (node.name == "COUNT") { 110 | hasCOUNT = true; 111 | 112 | } else if (node.name == "LINES") { 113 | hasLINES = true; 114 | 115 | } else { 116 | switch (node.name) { 117 | // Generated by: 118 | // curl https://raw.githubusercontent.com/sindresorhus/builtin-modules/master/builtin-modules.json | pjs --json '*' '`case "${_}":`' 119 | case "assert": case "async_hooks": case "buffer": case "child_process": 120 | case "cluster": case "console": case "constants": case "crypto": case "dgram": 121 | case "dns": case "domain": case "events": case "fs": case "http": case "http2": 122 | case "https": case "inspector": case "module": case "net": case "os": 123 | case "path": case "perf_hooks": case "process": case "punycode": 124 | case "querystring": case "readline": case "repl": case "stream": 125 | case "string_decoder": case "timers": case "tls": case "trace_events": 126 | case "tty": case "url": case "util": case "v8": case "vm": case "wasi": 127 | case "worker_threads": case "zlib": 128 | implicitRequires.add(node.name); 129 | break; 130 | } 131 | } 132 | break; 133 | 134 | case "UpdateExpression": 135 | if (node.argument.type == "Identifier") { 136 | implicitAssigns.add(node.argument.name); 137 | } 138 | break; 139 | 140 | case "AssignmentExpression": 141 | if (node.left.type == "Identifier") { 142 | implicitAssigns.add(node.left.name); 143 | } 144 | break; 145 | 146 | case "LabeledStatement": 147 | switch (node.label.name) { 148 | case "BEFORE": 149 | beforeStatements.push(transformAst(node.body)); 150 | return b.emptyStatement(); 151 | case "AFTER": 152 | afterStatements.push(transformAst(node.body)); 153 | return b.emptyStatement(); 154 | } 155 | break; 156 | } 157 | } 158 | }); 159 | } 160 | 161 | let lineAst = transformAst(parse(lineJs || "")); 162 | if (replaceLastExpression(lineAst.body)) { 163 | lineAst.body = lineAst.body.concat(parse(` 164 | if (_result === true) { 165 | print(_); 166 | } else if (_result !== false && _result != null) { 167 | print(_result); 168 | } 169 | `).body); 170 | } 171 | 172 | if (beforeJs) { 173 | const ast = transformAst(parse(beforeJs)); 174 | beforeStatements = beforeStatements.concat(ast.body); 175 | } 176 | if (replaceLastExpression(beforeStatements)) { 177 | beforeStatements = beforeStatements.concat(parse(` 178 | if (_result != null) { 179 | print(_result); 180 | } 181 | `).body); 182 | } 183 | 184 | if (afterJs) { 185 | const ast = transformAst(parse(afterJs)); 186 | afterStatements = afterStatements.concat(ast.body); 187 | } 188 | if (replaceLastExpression(afterStatements)) { 189 | afterStatements = afterStatements.concat(parse(` 190 | if (_result != null) { 191 | print(_result); 192 | } 193 | `).body); 194 | } 195 | 196 | if (hasDollar && !mode) { 197 | const preamble = parse("const $ = _.trim().split(DELIM)", { 198 | DELIM: b.literal(textDelimiter), 199 | }).body; 200 | lineAst.body = preamble.concat(lineAst.body); 201 | } 202 | 203 | if (hasCOUNT) { 204 | const preamble = parse("++COUNT").body; 205 | lineAst.body = preamble.concat(lineAst.body); 206 | // initVars.push(b.variableDeclarator(b.identifier("COUNT"), b.literal(0))); 207 | implicitAssigns.add("COUNT"); // Better error handling this way 208 | } 209 | 210 | if (hasLINES) { 211 | const preamble = parse("LINES.push(_)").body; 212 | lineAst.body = preamble.concat(lineAst.body); 213 | initConsts.push(b.variableDeclarator(b.identifier("LINES"), b.arrayExpression([]))); 214 | } 215 | 216 | for (const name of implicitRequires) { 217 | const expr = b.callExpression(b.identifier("require"), [b.literal(name)]); 218 | initConsts.push(b.variableDeclarator(b.identifier(name), expr)); 219 | } 220 | for (const name of implicitAssigns) { 221 | initVars.push(b.variableDeclarator(b.identifier(name), b.literal(0))); 222 | } 223 | 224 | let iterateJs; 225 | let iterateOpts; 226 | switch (mode) { 227 | case "csv": 228 | iterateJs = ` 229 | const {eachCsv, print} = require("pjs-tool"); 230 | BEFORE; 231 | `; 232 | 233 | if (csvHeader) { 234 | iterateJs += `for await (const _ of eachCsv(${inputStream}, {columns: true}))`; 235 | } else { 236 | iterateJs += `for await (const $ of eachCsv(${inputStream}))`; 237 | } 238 | iterateJs += "{ LINE; }"; 239 | break; 240 | 241 | case "json": 242 | iterateJs = ` 243 | const {eachJson, print} = require("pjs-tool"); 244 | BEFORE; 245 | for await (const _ of eachJson(${inputStream}, {filter: JSON_FILTER})) { 246 | LINE; 247 | } 248 | `; 249 | break; 250 | 251 | case "html": 252 | iterateJs = ` 253 | const {eachHtml, print} = require("pjs-tool"); 254 | BEFORE; 255 | for await (const _ of eachHtml(${inputStream}, {selector: MARKUP_SELECTOR})) { 256 | LINE; 257 | } 258 | `; 259 | break; 260 | 261 | case "xml": 262 | iterateJs = ` 263 | const {eachXml, print} = require("pjs-tool"); 264 | BEFORE; 265 | for await (const _ of eachXml(${inputStream}, {selector: MARKUP_SELECTOR})) { 266 | LINE; 267 | } 268 | `; 269 | break; 270 | 271 | default: 272 | iterateJs = ` 273 | const {eachLine, print} = require("pjs-tool"); 274 | BEFORE; 275 | for await (const _ of eachLine(${inputStream})) { 276 | LINE; 277 | } 278 | `; 279 | break; 280 | } 281 | 282 | // Write init consts and vars 283 | const initStatements = []; 284 | if (initConsts.length > 0) { 285 | initStatements.push(b.variableDeclaration("const", initConsts)); 286 | } 287 | if (initVars.length > 0) { 288 | initStatements.push(b.variableDeclaration("let", initVars)); 289 | } 290 | 291 | const wrapper = parse(` 292 | (async function () { 293 | INIT; 294 | ${iterateJs} 295 | AFTER; 296 | })(); 297 | `, { 298 | INIT: initStatements, 299 | BEFORE: beforeStatements, 300 | JSON_FILTER: jsonFilter != null ? b.literal(jsonFilter) : null, 301 | MARKUP_SELECTOR: markupSelector != null ? b.literal(markupSelector) : null, 302 | LINE: lineAst.body, 303 | AFTER: afterStatements, 304 | }); 305 | 306 | const astring = require("astring"); 307 | return astring.generate(wrapper, { indent: " " }); 308 | } 309 | -------------------------------------------------------------------------------- /doc/manual.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | pjs - pipe using explainable, vanilla JavaScript 4 | 5 | # SYNOPSIS 6 | 7 | **pjs** [*options*] [\--] '*script text*' [file ...] 8 | **pjs** [*options*] **-f** *script-file* [\--] [file ...] 9 | 10 | # DESCRIPTION 11 | 12 | **pjs** is a tool for processing text, CSV, JSON, HTML, and XML. 13 | 14 | pjs lets you write small and powerful JavaScript programs, similar to awk/sed/grep. It works by 15 | generating a complete JS program from the provided script, and feeding it each line of standard 16 | input. The statically generated program can be reviewed with `--explain`. 17 | 18 | See the EXAMPLES section below to see what pjs can do. 19 | 20 | # OPTIONS 21 | 22 | `-x, --explain` 23 | : Print the generated JS program instead of running it. This is useful for debugging or simply 24 | understanding what pjs is doing. The outputted program can be run directly in NodeJS. 25 | 26 | `-b, --before ` 27 | : Run a script before the input data is read. This can be used to initialize variables, or do 28 | anything else that should be done on startup. Can be specified multiple times. 29 | 30 | `-a, --after ` 31 | : Run a script after all the input data is read. This can be used to aggregate a summary, or 32 | perform anything else that should be done on completion. Can be specified multiple times. 33 | 34 | `-f, --file ` 35 | : Load script text from a file instead of the command-line. 36 | 37 | `-d, --delimiter ` 38 | : The delimiter for text parsing. This is a regular expression passed to `String.prototype.split()` 39 | used to split each line of input data into fields. The fields can be accessed by the `$` array 40 | variable. Defaults to `\s+`. 41 | 42 | `--csv` 43 | : Parse the input data as CSV (comma separated values). Quotes and escape sequences in the input are 44 | correctly handled. When using this option the `_` built-in variable is unavailable. 45 | 46 | `--csv-header` 47 | : Like `--csv`, but the first row is considered a column header. When using this option the `$` 48 | built-in variable is unavailable, and the `_` built-in variable is a mapping of column names to the 49 | row's values. 50 | 51 | `--json ` 52 | : Parse the input data as JSON, iterating over objects that match the given `jq`-like filter. The 53 | input may contain one or multiple concatenated JSON objects. When using this option the `_` built-in 54 | variable contains a JSON object. For a list of implemented filter syntax, see 55 | https://www.npmjs.com/package/jq-in-the-browser#supported-features 56 | 57 | `--html ` 58 | : Parse the input data as HTML, iterating over elements that match the given CSS selector. The 59 | parser is forgiving with malformed HTML. The `_` built-in variable will contain an object with the 60 | keys: `type`, `name`, `attr`, `children`, `text`, and `innerHTML`. It also contains methods 61 | `querySelector()` and `querySelectorAll()` for further querying using CSS selectors. For a list of 62 | implemented selector syntax, see https://www.npmjs.com/package/css-select#supported-selectors 63 | 64 | `--xml ` 65 | : Same as `--html`, but tags and attributes are considered case-sensitive. 66 | 67 | `-V, --version` 68 | : Print the version number. 69 | 70 | `-h, --help` 71 | : Print command-line options. The output of this option is less detailed than this document. 72 | 73 | # JAVASCRIPT REFERENCE 74 | 75 | ## Built-in Variables 76 | 77 | `_` (underscore) 78 | : The current line or object being processed. With `--json`, `--html`, or `--csv-header` this is an 79 | object, otherwise it is a string. 80 | 81 | `$` (dollar) 82 | : An array containing the fields of the current line. The field delimiter can be set with 83 | `--delimiter`. As a convenience, any references to `$N` where N is a number are translated to 84 | `$[N]`. 85 | 86 | `COUNT` 87 | : A numeric counter that is incremented with each line. 88 | 89 | `LINES` 90 | : An array containing all the lines that were processed. 91 | 92 | `FILENAME` 93 | : The name of the current input file, or null if reading from stdin. 94 | 95 | `print()` 96 | : Prints arguments to standard output. Objects are converted to JSON. 97 | 98 | ## Last Expression Handling 99 | 100 | If the last statement in the script is an expression, it will be used to filter or transform the 101 | output. If the last expression evaluates to `true`, the line is printed unmodified. If the last 102 | expression is a value, that value is printed instead. If the last expression evaluates to false or 103 | null, nothing is output. 104 | 105 | Sometimes output is never desired. In those cases either make sure the last expression is false or 106 | null, or wrap the expression in curly braces to make it a block statement. 107 | 108 | ## Implicit Imports 109 | 110 | Using any built-in NodeJS module (eg: `fs`) will automatically import it with `require()`. 111 | 112 | ## Implicit Variable Initialization 113 | 114 | Assigning to an undeclared variable will automatically insert a variable declaration. The initial 115 | value of these implicit variables is always 0. For other values or types, declare them explicitly in 116 | `--before`. 117 | 118 | ## Before/After Labels 119 | 120 | The JavaScript loop labels `BEFORE:` and `AFTER:` can be used to mark expressions that will be run 121 | as if they were passed separately to `--before` or `--after`. This can be useful in conjunction with 122 | `--file` to keep everything in one script file, or if you just prefer awk-like syntax. 123 | 124 | # EXAMPLES 125 | 126 | **Remember**: You can run any of these examples with `--explain` to inspect the generated program. 127 | `pjs` syntax is meant to be powerful, but never mysterious. 128 | 129 | ## Transforming Examples 130 | 131 | Convert a file to upper-case: 132 | 133 | ```sh 134 | cat document.txt | pjs '_.toUpperCase()' 135 | ``` 136 | 137 | Remove trailing whitespace from each line in a file: 138 | 139 | ```sh 140 | cat document.txt | pjs '_.replace(/\s*$/, "")' 141 | ``` 142 | 143 | Print the second field of each line (in this example, the PIDs): 144 | 145 | ```sh 146 | ps aux | pjs '$1' 147 | ``` 148 | 149 | Print all fields after the 10th (in this example, the process names): 150 | 151 | ```sh 152 | ps aux | pjs '$.slice(10).join(" ")' 153 | ``` 154 | 155 | ## Filtering Examples 156 | 157 | Given a list of numbers, print only numbers greater than 5: 158 | 159 | ```sh 160 | seq 1 10 | pjs '_ > 5' 161 | ``` 162 | 163 | Given a list of numbers, print only even numbers: 164 | 165 | ```sh 166 | seq 1 10 | pjs '_ % 2 == 0' 167 | ``` 168 | 169 | Given a list of filenames, print the files that actually exist: 170 | 171 | ```sh 172 | cat filenames.txt | pjs 'fs.existsSync(_)' 173 | ``` 174 | 175 | Given a list of filenames, print the files that are under one kilobyte in size: 176 | 177 | ```sh 178 | cat filenames.txt | pjs 'fs.statSync(_).size < 1000' 179 | ``` 180 | 181 | Print the last 10 lines of a file (like `tail`): 182 | 183 | ```sh 184 | cat document.txt | pjs --after 'LINES.slice(-10).join("\n")' 185 | ``` 186 | 187 | Print every other line of a file: 188 | 189 | ```sh 190 | cat document.txt | pjs 'COUNT % 2 == 1' 191 | ``` 192 | 193 | ## Summarizing Examples 194 | 195 | Manually count the lines in the input (like `wc -l`): 196 | 197 | ```sh 198 | cat filenames.txt | pjs '{ count++ }' --after 'count' 199 | ``` 200 | 201 | Same as above, but using the built-in `COUNT` variable: 202 | 203 | ```sh 204 | cat filenames.txt | pjs --after 'COUNT' 205 | ``` 206 | 207 | Count the *unique* lines in the input: 208 | 209 | ```sh 210 | cat filenames.txt | pjs --before 'let s = new Set()' '{ s.add(_) }' --after 's.size' 211 | ``` 212 | 213 | Manually sort the lines of the input (like `sort`) 214 | 215 | ```sh 216 | cat filenames.txt | pjs --before 'let lines = []' '{ lines.push(_) }' \ 217 | --after 'lines.sort().join("\n")' 218 | ``` 219 | 220 | Same as above, but using the built-in `LINES` variable: 221 | 222 | ```sh 223 | cat filenames.txt | pjs --after 'LINES.sort().join("\n")' 224 | ``` 225 | 226 | ## CSV Examples 227 | 228 | Given a `grades.csv` file that looks like this: 229 | 230 | ```csv 231 | name,subject,grade 232 | Bob,physics,43 233 | Alice,biology,75 234 | Alice,physics,90 235 | David,biology,85 236 | Clara,physics,78 237 | ``` 238 | 239 | Print only the third column: 240 | 241 | ```sh 242 | cat grades.csv | pjs --csv '$2' 243 | ``` 244 | 245 | Print the grades using the column header: 246 | 247 | ```sh 248 | cat grades.csv | pjs --csv-header '_.grade' 249 | ``` 250 | 251 | Print the names of students taking biology: 252 | 253 | ```sh 254 | cat grades.csv | pjs --csv-header '_.subject == "biology" && _.name' 255 | ``` 256 | 257 | Print the average grade across all courses: 258 | 259 | ```sh 260 | cat grades.csv | pjs --csv-header '{ sum += Number(_.grade) }' --after 'sum/COUNT' 261 | ``` 262 | 263 | ## JSON Examples 264 | 265 | Given a `users.json` file that looks like this: 266 | 267 | ```json 268 | { 269 | "version": 123, 270 | "items": [ 271 | {"name": {"first": "Winifred", "last": "Frost"}, "age": 42}, 272 | {"name": {"first": "Miles", "last": "Fernandez"}, "age": 15}, 273 | {"name": {"first": "Kennard", "last": "Floyd"}, "age": 20}, 274 | {"name": {"first": "Lonnie", "last": "Davis"}, "age": 78}, 275 | {"name": {"first": "Duncan", "last": "Poole"}, "age": 36} 276 | ] 277 | } 278 | ``` 279 | 280 | Print the value of the "version" field: 281 | 282 | ```sh 283 | cat users.json | pjs --json '.version' _ 284 | ``` 285 | 286 | Print the full name of each user: 287 | 288 | ```sh 289 | cat users.json | pjs --json '.items[].name' '_.first+" "+_.last' 290 | ``` 291 | 292 | Print the users that are older than 21: 293 | 294 | ```sh 295 | cat users.json | pjs --json '.items[]' '_.age >= 21' 296 | ``` 297 | 298 | Print the ages of the first 3 users only: 299 | 300 | ```sh 301 | cat users.json | pjs --json '.items[0:3]' '_.age' 302 | ``` 303 | 304 | Query a web API for users: 305 | 306 | ```sh 307 | curl -A "" 'https://www.instagram.com/web/search/topsearch/?query=John' | 308 | pjs --json '.users[].user' '`@${_.username} (${_.full_name})`' 309 | ``` 310 | 311 | ## HTML/XML Examples 312 | 313 | Print the text of all `

` and `

` elements on a web page: 314 | 315 | ```sh 316 | curl https://aduros.com | pjs --html 'h1,h2' '_.text' 317 | ``` 318 | 319 | Print the URLs of all images on a web page: 320 | 321 | ```sh 322 | curl https://aduros.com | pjs --html 'img' '_.attr.src' 323 | ``` 324 | 325 | Scrape headlines off a news site using a complex CSS selector: 326 | 327 | ```sh 328 | curl https://news.ycombinator.com | pjs '_.text' \ 329 | --html 'table table tr:nth-last-of-type(n+2) td:nth-child(3)' 330 | ``` 331 | 332 | Print a count of all external links: 333 | 334 | ```sh 335 | curl https://aduros.com | pjs --html 'a[target=_blank]' --after COUNT 336 | ``` 337 | 338 | Print all links in `

` elements with URLs containing the word "blog": 339 | 340 | ```sh 341 | curl https://aduros.com | pjs --html 'h2 a' '_.attr.href.includes("blog") && _.attr.href' 342 | ``` 343 | 344 | Print a readable summary of an RSS feed: 345 | 346 | ```sh 347 | curl https://aduros.com/index.xml | pjs --xml 'item' \ 348 | '_.querySelector("title").text + " --> " + _.querySelector("link").text' 349 | ``` 350 | 351 | ## Advanced Examples 352 | 353 | Bulk rename \*.jpeg files to \*.jpg: 354 | 355 | ```sh 356 | find -name '*.jpeg' | pjs 'let f = path.parse(_); 357 | fs.renameSync(_, path.join(f.dir, f.name+".jpg"))' 358 | ``` 359 | 360 | Print the longest line in the input: 361 | 362 | ```sh 363 | cat document.txt | pjs 'if (_.length > m) { m = _.length; longest = _ }' --after 'longest' 364 | ``` 365 | 366 | Count the words in the input: 367 | 368 | ```sh 369 | cat document.txt | pjs '{ words += $.length }' --after 'words' 370 | ``` 371 | 372 | Count the *unique* words in the input: 373 | 374 | ```sh 375 | cat document.txt | pjs --before 'let words = new Set()' \ 376 | 'for (let word of $) words.add(word)' --after 'words.size' 377 | ``` 378 | 379 | Using a script file instead of command-line arguments: 380 | 381 | ```sh 382 | echo ' 383 | BEFORE: { 384 | print("Starting up!") 385 | } 386 | _.toUpperCase() 387 | AFTER: "Total lines: "+COUNT 388 | ' > my-uppercase.js 389 | 390 | cat document.txt | pjs -f my-uppercase.js 391 | ``` 392 | 393 | Adding a shebang to the above script to make it self-executable: 394 | 395 | ```sh 396 | echo "#!/usr/bin/env -S pjs -f" | cat - my-uppercase.js > my-uppercase 397 | chmod +x my-uppercase 398 | 399 | ./my-uppercase document.txt 400 | ``` 401 | 402 | Completely scrape an entire online store, outputting a JSON stream for later processing: 403 | 404 | ```sh 405 | for page in `seq 1 50`; do 406 | 407 | >&2 echo "Scraping page $page..." 408 | curl -s "http://books.toscrape.com/catalogue/page-$page.html" | 409 | pjs --html '.product_pod h3 a' '"http://books.toscrape.com/catalogue/"+_.attr.href' | 410 | 411 | while read url; do 412 | >&2 echo "Scraping item details from $url" 413 | curl -s "$url" | pjs --html '.product_page' 'JSON.stringify({ 414 | title: _.querySelector(".product_main h1").text, 415 | description: _.querySelector("#product_description + p").text})' 416 | done 417 | done 418 | ``` 419 | 420 | # BUGS 421 | 422 | Please report bugs on GitHub: https://github.com/aduros/pjs/issues 423 | 424 | # SEE ALSO 425 | 426 | Website: https://github.com/aduros/pjs 427 | 428 | Related projects: [pyp](https://github.com/hauntsaninja/pyp), [nip](https://github.com/kolodny/nip), 429 | awk. Pyp and its `--explain` was a major inspiration for this project. 430 | -------------------------------------------------------------------------------- /test/data/aduros-website.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | aduros.com 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 62 | 63 |
64 | 65 | 66 | 67 | 68 |
69 |
70 |

Hacking i3: Automatic Layout

71 | 77 |
78 |
79 | (Day 20 of 30 Days of Blogging) 80 | Another post about making a good window manager even better! 81 | Window layouts in i3 basically come in two types: horizontally or vertically stacked. By combining those two you can arrange windows however you want. 82 | The default layout in i3 is horizontal, which means when you open up too many windows it ends up looking like this: 83 | (The video could not be played) 84 | Of course what you’re supposed to prevent everything getting squished like that is to split windows to the opposite layout so they grow in the other direction. 85 |
86 | 87 | Read more 88 | 89 |
90 | 91 | 92 | 93 | 94 |
95 |
96 |

Introducing pjs

97 | 103 |
104 |
105 | (Day 19 of 30 Days of Blogging) 106 | I’ve been working on a small project that can basically be summed up as awk for JS developers. pjs is a command-line tool for processing text-based files by writing snippets of JavaScript. 107 | For example, converting each line to upper-case: 108 | cat document.txt | pjs '_.toUpperCase()' Or filtering lines: 109 | seq 10 | pjs '_ % 2 == 0' And a whole bunch of other stuff, including support for streaming CSV and JSON. 110 |
111 | 112 | Read more 113 | 114 |
115 | 116 | 117 | 118 | 119 |
120 |
121 |

Ramblings From a Dirty Apartment

122 | 128 |
129 |
130 | (Day 18 of 30 Days of Blogging) 131 | I started reading some philosophy disguised as a housekeeping manual called Home Comforts and it’s been pretty illuminating. 132 | I’ve always loathed housework. I’m not exactly sure why. It could be the feeling of futility from the cycle of cleaning something that will soon become dirty again. Or maybe my pseudo-nomadism/quasi-Buddhism prevents me from attaching much importance to objects and spaces. Or maybe like many I internalize that housework is women’s work, or that valued work is paid work. 133 |
134 | 135 | Read more 136 | 137 |
138 | 139 | 140 | 141 | 142 |
143 |
144 |

Returning to Text

145 | 151 |
152 |
153 | (Day 17 of 30 Days of Blogging) 154 | I’ve been replacing the way I store personal todo lists and notes. Previously I was using Trello. Before that I used Tiddlywiki for a couple years. I used to work with a guy that swore by simply using a text file, so I started giving that a shot. 155 | In theory this approach has some major perks: 156 | It’s easy to add stuff: Just start typing. 157 |
158 | 159 | Read more 160 | 161 |
162 | 163 | 164 | 165 | 166 |
167 |
168 |

Hacking i3: Window Promoting

169 | 175 |
176 |
177 | (Day 16 of 30 Days of Blogging) 178 | One of the only things I miss from when I used xmonad many years ago was being able to hit a keybind to swap the currently focused window with the “master” window. I think the default keybind for that in xmonad is alt-Enter. 179 | i3 doesn’t have the concept of a master window, but if we consider the master window to just be the largest window, the same effect can be achieved: 180 |
181 | 182 | Read more 183 | 184 |
185 | 186 | 187 | 188 | 189 |
190 |
191 |

A Blog Half Full

192 | 198 |
199 |
200 | I’m halfway through 30 Days of Blogging. 201 | When I was a kid I really wanted to be a cartoonist. I wanted it so much that my first “job” was doing a daily comic strip for the local newspaper. I did that for 10 months straight, 6 days a week. At the end I was so sick of it I never wanted to pick up a pencil ever again. 202 | Hopefully blogging doesn’t end up the same way! 203 |
204 | 205 | Read more 206 | 207 |
208 | 209 | 210 | 211 | 212 |
213 |
214 |

Simple Clipboard Management

215 | 221 |
222 |
223 | (Day 14 of 30 Days of Blogging) 224 | Greenclip is really useful for recording your clipboard history and showing a menu to switch between items. 225 | Along the same lines I had an idea to write a tiny script that allows editing of the clipboard in vim. This has been handy when I need to quickly fix up some text before pasting it into a GUI application: 226 | #!/bin/sh -e file=`mktemp /tmp/clipboard-XXX` xsel --clipboard > "$file" xterm -e "$EDITOR-c 'set nofixeol' \"$file\"" xsel --clipboard < "$file" rm "$file" The nofixeol option prevents vim from adding a newline to the end of the clipboard, which is usually not desired. 227 |
228 | 229 | Read more 230 | 231 |
232 | 233 | 234 | 235 | 236 |
237 |
238 |

Firefox Minimalism

239 | 245 |
246 |
247 | (Day 13 of 30 Days of Blogging) 248 | I’ve been working on an opinionated and ultra-minimalist Firefox theme that pairs nicely with Tridactyl. I thought I’d share a few notes here. 249 | But first some pretty screenshots! Here’s what you’re greeted with when you open a Firefox window: 250 | Nope, it’s not a terminal, that text prompt is the address bar. 251 | Here’s what it looks like with a page in a single tab: 252 |
253 | 254 | Read more 255 | 256 |
257 | 258 | 259 | 260 | 261 |
262 |
263 |

Media Companies are Complicit

264 | 270 |
271 |
272 | (Day 12 of 30 Days of Blogging) 273 | Facebook, Twitter, and other media companies shouldn’t be applauded for finally deplatforming the American president after profiting off controversy for so many years. 274 | It took up until the very day that the American president lost the election for TV outlets to finally start calling out his bullshit. It took an act of terrorism to force social media’s hand against the communities allowed to grow on their platforms. 275 |
276 | 277 | Read more 278 | 279 |
280 | 281 | 282 | 283 | 284 |
285 |
286 |

Access Recent Files From the Command Line

287 | 293 |
294 |
295 | (Day 11 of 30 Days of Blogging) 296 | If you don’t already use Z you absolutely need to check it out. Z is a CLI tool that cd’s to a recently used directory, so that typing z foo will cd /my/deeply/nested/project/foobar7, as long as you’ve cd’ed into foobar7 sometime in the past. 297 | Z is great for jumping to recent directories… but is there an equivalent for opening recently used files? There’s fasd, but the way it works seems a bit too magical to me. 298 |
299 | 300 | Read more 301 | 302 |
303 | 304 | 305 | 306 | 321 |
322 | 387 | 388 | 389 | 390 |
391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pjs 2 | 3 | **pjs** is a command-line tool for filtering and transforming text, similar to `awk`. You provide it 4 | powerful one-line snippets written in vanilla JavaScript. It supports many input formats, including 5 | plain text, CSV, JSON, HTML, and XML. 6 | 7 | pjs works by generating a complete JS program from the provided script, and feeding it each line of 8 | standard input. The statically generated program can be reviewed with `--explain`. 9 | 10 | See the [examples](#examples) section below to see what pjs can do. For complete documentation, read 11 | the [manual](doc/manual.md) or run `man pjs`. 12 | 13 | # Installing 14 | 15 | Install the `pjs` command with `npm`: 16 | 17 | ```sh 18 | npm install -g pjs-tool 19 | ``` 20 | 21 | If `npm` is not available on your environment, you can download a [standalone 22 | executable](https://aduros.com/pjs/pjs-latest.tar.bz2). You will still need `node` installed. 23 | 24 | # Examples 25 | 26 | Click on an example to run it in your browser at the [pjs playground](https://aduros.com/pjs). 27 | 28 | ## Transforming Examples 29 | 30 | Convert a file to upper-case: 31 | 32 | [`cat input.txt | pjs '_.toUpperCase()'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20'_.toUpperCase()'%22%2C%22input%22%3A%22There%20once%20was%20a%20man%20from%20Nantucket%5CnWho%20kept%20all%20his%20cash%20in%20a%20bucket.%5CnBut%20his%20daughter%20named%20Nan%2C%5CnRan%20away%20with%20a%20man%5CnAnd%20as%20for%20the%20bucket%2C%20Nantucket.%22%7D) 33 | 34 | Print the second field of each line (in this example, the PIDs): 35 | 36 | [`ps aux | pjs '$1'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20'%241'%22%2C%22input%22%3A%22USER%20%20%20%20%20%20%20%20%20PID%20%25CPU%20%25MEM%20%20%20%20VSZ%20%20%20RSS%20TTY%20%20%20%20%20%20STAT%20START%20%20%20TIME%20COMMAND%5Cnsyslog%20%20%20%20%20%20%20643%20%200.0%20%200.0%20221124%20%204760%20%3F%20%20%20%20%20%20%20%20Ssl%20%20Jan28%20%20%200%3A00%20%2Fusr%2Fsbin%2Frsyslogd%20-n%20-iNONE%5Cnroot%20%20%20%20%20%20%20%20%20653%20%200.0%20%200.1%20%2018172%20%208712%20%3F%20%20%20%20%20%20%20%20Ss%20%20%20Jan28%20%20%200%3A04%20%2Flib%2Fsystemd%2Fsystemd-logind%5Cnroot%20%20%20%20%20%20%20%20%20654%20%200.2%20%200.1%20274952%20%209944%20%3F%20%20%20%20%20%20%20%20Ssl%20%20Jan28%20%20%202%3A40%20%2Fusr%2Fsbin%2Fthermald%20--systemd%20--d%5Cnroot%20%20%20%20%20%20%20%20%20655%20%200.0%20%200.1%20393916%2012676%20%3F%20%20%20%20%20%20%20%20Ssl%20%20Jan28%20%20%200%3A07%20%2Fusr%2Flibexec%2Fudisks2%2Fudisksd%5Cnroot%20%20%20%20%20%20%20%20%20657%20%200.0%20%200.1%20%2015208%20%208124%20%3F%20%20%20%20%20%20%20%20Ss%20%20%20Jan28%20%20%200%3A00%20%2Fsbin%2Fwpa_supplicant%20-u%20-s%20-O%20%2Fr%5Cnavahi%20%20%20%20%20%20%20%20666%20%200.0%20%200.0%20%20%209224%20%20%20324%20%3F%20%20%20%20%20%20%20%20S%20%20%20%20Jan28%20%20%200%3A00%20avahi-daemon%3A%20chroot%20helper%5Cnroot%20%20%20%20%20%20%20%20%20710%20%200.0%20%200.1%20306056%20%208344%20%3F%20%20%20%20%20%20%20%20SLsl%20Jan28%20%20%200%3A02%20%2Fusr%2Fsbin%2Flightdm%5Cnroot%20%20%20%20%20%20%20%20%20715%20%200.0%20%200.1%20315100%20%209992%20%3F%20%20%20%20%20%20%20%20Ssl%20%20Jan28%20%20%200%3A00%20%2Fusr%2Fsbin%2FModemManager%22%7D) 37 | 38 | Print all fields after the 10th (in this example, the process names): 39 | 40 | [`ps aux | pjs '$.slice(10).join(" ")'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20'%24.slice(10).join(%5C%22%20%5C%22)'%22%2C%22input%22%3A%22USER%20%20%20%20%20%20%20%20%20PID%20%25CPU%20%25MEM%20%20%20%20VSZ%20%20%20RSS%20TTY%20%20%20%20%20%20STAT%20START%20%20%20TIME%20COMMAND%5Cnsyslog%20%20%20%20%20%20%20643%20%200.0%20%200.0%20221124%20%204760%20%3F%20%20%20%20%20%20%20%20Ssl%20%20Jan28%20%20%200%3A00%20%2Fusr%2Fsbin%2Frsyslogd%20-n%20-iNONE%5Cnroot%20%20%20%20%20%20%20%20%20653%20%200.0%20%200.1%20%2018172%20%208712%20%3F%20%20%20%20%20%20%20%20Ss%20%20%20Jan28%20%20%200%3A04%20%2Flib%2Fsystemd%2Fsystemd-logind%5Cnroot%20%20%20%20%20%20%20%20%20654%20%200.2%20%200.1%20274952%20%209944%20%3F%20%20%20%20%20%20%20%20Ssl%20%20Jan28%20%20%202%3A40%20%2Fusr%2Fsbin%2Fthermald%20--systemd%20--d%5Cnroot%20%20%20%20%20%20%20%20%20655%20%200.0%20%200.1%20393916%2012676%20%3F%20%20%20%20%20%20%20%20Ssl%20%20Jan28%20%20%200%3A07%20%2Fusr%2Flibexec%2Fudisks2%2Fudisksd%5Cnroot%20%20%20%20%20%20%20%20%20657%20%200.0%20%200.1%20%2015208%20%208124%20%3F%20%20%20%20%20%20%20%20Ss%20%20%20Jan28%20%20%200%3A00%20%2Fsbin%2Fwpa_supplicant%20-u%20-s%20-O%20%2Fr%5Cnavahi%20%20%20%20%20%20%20%20666%20%200.0%20%200.0%20%20%209224%20%20%20324%20%3F%20%20%20%20%20%20%20%20S%20%20%20%20Jan28%20%20%200%3A00%20avahi-daemon%3A%20chroot%20helper%5Cnroot%20%20%20%20%20%20%20%20%20710%20%200.0%20%200.1%20306056%20%208344%20%3F%20%20%20%20%20%20%20%20SLsl%20Jan28%20%20%200%3A02%20%2Fusr%2Fsbin%2Flightdm%5Cnroot%20%20%20%20%20%20%20%20%20715%20%200.0%20%200.1%20315100%20%209992%20%3F%20%20%20%20%20%20%20%20Ssl%20%20Jan28%20%20%200%3A00%20%2Fusr%2Fsbin%2FModemManager%22%7D) 41 | 42 | Remove trailing whitespace from each line in a file: 43 | 44 | `cat document.txt | pjs '_.replace(/\s*$/, "")'` 45 | 46 | ## Filtering Examples 47 | 48 | Given a list of numbers, print only numbers greater than 5: 49 | 50 | [`seq 1 10 | pjs '_ > 5'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20'_%20%3E%205'%22%2C%22input%22%3A%221%5Cn2%5Cn3%5Cn4%5Cn5%5Cn6%5Cn7%5Cn8%5Cn9%5Cn10%22%7D) 51 | 52 | Given a list of numbers, print only even numbers: 53 | 54 | [`seq 1 10 | pjs '_ % 2 == 0'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20'_%20%25%202%20%3D%3D%200'%22%2C%22input%22%3A%221%5Cn2%5Cn3%5Cn4%5Cn5%5Cn6%5Cn7%5Cn8%5Cn9%5Cn10%22%7D) 55 | 56 | Print the last 4 lines of a file (like `tail`): 57 | 58 | [`seq 1 10 | pjs --after 'LINES.slice(-4).join("\n")'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--after%20'LINES.slice(-4).join(%5C%22%5C%5Cn%5C%22)'%22%2C%22input%22%3A%221%5Cn2%5Cn3%5Cn4%5Cn5%5Cn6%5Cn7%5Cn8%5Cn9%5Cn10%22%7D) 59 | 60 | Print every other line of a file: 61 | 62 | [`cat input.txt | pjs 'COUNT % 2 == 1'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20'COUNT%20%25%202%20%3D%3D%201'%22%2C%22input%22%3A%22There%20once%20was%20a%20man%20from%20Nantucket%5CnWho%20kept%20all%20his%20cash%20in%20a%20bucket.%5CnBut%20his%20daughter%20named%20Nan%2C%5CnRan%20away%20with%20a%20man%5CnAnd%20as%20for%20the%20bucket%2C%20Nantucket.%22%7D) 63 | 64 | Given a list of filenames, print the files that actually exist: 65 | 66 | `cat filenames.txt | pjs 'fs.existsSync(_)'` 67 | 68 | Given a list of filenames, print the files that are under one kilobyte in size: 69 | 70 | `cat filenames.txt | pjs 'fs.statSync(_).size < 1000'` 71 | 72 | ## Summarizing Examples 73 | 74 | Manually count the lines in the input (like `wc -l`): 75 | 76 | [`cat input.txt | pjs '{ count++ }' --after 'count'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20'%7B%20count%2B%2B%20%7D'%20--after%20'count'%22%2C%22input%22%3A%22There%20once%20was%20a%20man%20from%20Nantucket%5CnWho%20kept%20all%20his%20cash%20in%20a%20bucket.%5CnBut%20his%20daughter%20named%20Nan%2C%5CnRan%20away%20with%20a%20man%5CnAnd%20as%20for%20the%20bucket%2C%20Nantucket.%22%7D) 77 | 78 | Same as above, but using the built-in `COUNT` variable: 79 | 80 | [`cat input.txt | pjs --after 'COUNT'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--after%20'COUNT'%22%2C%22input%22%3A%22There%20once%20was%20a%20man%20from%20Nantucket%5CnWho%20kept%20all%20his%20cash%20in%20a%20bucket.%5CnBut%20his%20daughter%20named%20Nan%2C%5CnRan%20away%20with%20a%20man%5CnAnd%20as%20for%20the%20bucket%2C%20Nantucket.%22%7D) 81 | 82 | Count the *unique* lines in the input: 83 | 84 | [`cat input.txt | pjs --before 'let s = new Set()' '{ s.add(_) }' --after 's.size'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--before%20'let%20s%20%3D%20new%20Set()'%20'%7B%20s.add(_)%20%7D'%20--after%20's.size'%22%2C%22input%22%3A%22Laid%20back%2C%5CnLaid%20back%2C%5CnLaid%20back%20we'll%20give%20you%20play%20back.%5CnLaid%20back%2C%5CnLaid%20back%2C%5CnLaid%20back%20I'll%20give%20you%20play%20back.%22%7D) 85 | 86 | Manually sort the lines of the input (like `sort`) 87 | 88 | [`cat input.txt | pjs --before 'let lines = []' '{ lines.push(_) }' --after 'lines.sort().join("\n")'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--before%20'let%20lines%20%3D%20%5B%5D'%20'%7B%20lines.push(_)%20%7D'%20--after%20'lines.sort().join(%5C%22%5C%5Cn%5C%22)'%22%2C%22input%22%3A%22There%20once%20was%20a%20man%20from%20Nantucket%5CnWho%20kept%20all%20his%20cash%20in%20a%20bucket.%5CnBut%20his%20daughter%20named%20Nan%2C%5CnRan%20away%20with%20a%20man%5CnAnd%20as%20for%20the%20bucket%2C%20Nantucket.%22%7D) 89 | 90 | Same as above, but using the built-in `LINES` variable: 91 | 92 | [`cat input.txt | pjs --after 'LINES.sort().join("\n")'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--after%20'LINES.sort().join(%5C%22%5C%5Cn%5C%22)'%22%2C%22input%22%3A%22There%20once%20was%20a%20man%20from%20Nantucket%5CnWho%20kept%20all%20his%20cash%20in%20a%20bucket.%5CnBut%20his%20daughter%20named%20Nan%2C%5CnRan%20away%20with%20a%20man%5CnAnd%20as%20for%20the%20bucket%2C%20Nantucket.%22%7D) 93 | 94 | ## CSV Examples 95 | 96 | Given a `grades.csv` file that looks like this: 97 | 98 | ```csv 99 | name,subject,grade 100 | Bob,physics,43 101 | Alice,biology,75 102 | Alice,physics,90 103 | David,biology,85 104 | Clara,physics,78 105 | ``` 106 | 107 | Print only the third column: 108 | 109 | [`cat grades.csv | pjs --csv '$2'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--csv%20'%242'%22%2C%22input%22%3A%22name%2Csubject%2Cgrade%5CnBob%2Cphysics%2C43%5CnAlice%2Cbiology%2C75%5CnAlice%2Cphysics%2C90%5CnDavid%2Cbiology%2C85%5CnClara%2Cphysics%2C78%22%7D) 110 | 111 | Print the grades using the column header: 112 | 113 | [`cat grades.csv | pjs --csv-header '_.grade'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--csv-header%20'_.grade'%22%2C%22input%22%3A%22name%2Csubject%2Cgrade%5CnBob%2Cphysics%2C43%5CnAlice%2Cbiology%2C75%5CnAlice%2Cphysics%2C90%5CnDavid%2Cbiology%2C85%5CnClara%2Cphysics%2C78%22%7D) 114 | 115 | Print the names of students taking biology: 116 | 117 | [`cat grades.csv | pjs --csv-header '_.subject == "biology" && _.name'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--csv-header%20'_.subject%20%3D%3D%20%5C%22biology%5C%22%20%26%26%20_.name'%22%2C%22input%22%3A%22name%2Csubject%2Cgrade%5CnBob%2Cphysics%2C43%5CnAlice%2Cbiology%2C75%5CnAlice%2Cphysics%2C90%5CnDavid%2Cbiology%2C85%5CnClara%2Cphysics%2C78%22%7D) 118 | 119 | Print the average grade across all courses: 120 | 121 | [`cat grades.csv | pjs --csv-header '{ sum += Number(_.grade) }' --after 'sum/COUNT'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--csv-header%20'%7B%20sum%20%2B%3D%20Number(_.grade)%20%7D'%20--after%20'sum%2FCOUNT'%22%2C%22input%22%3A%22name%2Csubject%2Cgrade%5CnBob%2Cphysics%2C43%5CnAlice%2Cbiology%2C75%5CnAlice%2Cphysics%2C90%5CnDavid%2Cbiology%2C85%5CnClara%2Cphysics%2C78%22%7D) 122 | 123 | ## JSON Examples 124 | 125 | Given a `users.json` file that looks like this: 126 | 127 | ```json 128 | { 129 | "version": 123, 130 | "items": [ 131 | {"name": {"first": "Winifred", "last": "Frost"}, "age": 42}, 132 | {"name": {"first": "Miles", "last": "Fernandez"}, "age": 15}, 133 | {"name": {"first": "Kennard", "last": "Floyd"}, "age": 20}, 134 | {"name": {"first": "Lonnie", "last": "Davis"}, "age": 78}, 135 | {"name": {"first": "Duncan", "last": "Poole"}, "age": 36} 136 | ] 137 | } 138 | ``` 139 | 140 | Print the value of the "version" field: 141 | 142 | [`cat users.json | pjs --json '.version' _`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--json%20'.version'%20_%22%2C%22input%22%3A%22%7B%5Cn%20%20%5C%22version%5C%22%3A%20123%2C%5Cn%20%20%5C%22items%5C%22%3A%20%5B%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Winifred%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Frost%5C%22%7D%2C%20%5C%22age%5C%22%3A%2042%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Miles%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Fernandez%5C%22%7D%2C%20%5C%22age%5C%22%3A%2015%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Kennard%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Floyd%5C%22%7D%2C%20%5C%22age%5C%22%3A%2020%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Lonnie%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Davis%5C%22%7D%2C%20%5C%22age%5C%22%3A%2078%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Duncan%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Poole%5C%22%7D%2C%20%5C%22age%5C%22%3A%2036%7D%5Cn%20%20%5D%5Cn%7D%22%7D) 143 | 144 | Print the full name of each user: 145 | 146 | [`cat users.json | pjs --json '.items[].name' '_.first+" "+_.last'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--json%20'.items%5B%5D.name'%20'_.first%2B%5C%22%20%5C%22%2B_.last'%22%2C%22input%22%3A%22%7B%5Cn%20%20%5C%22version%5C%22%3A%20123%2C%5Cn%20%20%5C%22items%5C%22%3A%20%5B%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Winifred%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Frost%5C%22%7D%2C%20%5C%22age%5C%22%3A%2042%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Miles%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Fernandez%5C%22%7D%2C%20%5C%22age%5C%22%3A%2015%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Kennard%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Floyd%5C%22%7D%2C%20%5C%22age%5C%22%3A%2020%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Lonnie%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Davis%5C%22%7D%2C%20%5C%22age%5C%22%3A%2078%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Duncan%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Poole%5C%22%7D%2C%20%5C%22age%5C%22%3A%2036%7D%5Cn%20%20%5D%5Cn%7D%22%7D) 147 | 148 | Print the users that are older than 21: 149 | 150 | [`cat users.json | pjs --json '.items[]' '_.age >= 21'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--json%20'.items%5B%5D'%20'_.age%20%3E%3D%2021'%22%2C%22input%22%3A%22%7B%5Cn%20%20%5C%22version%5C%22%3A%20123%2C%5Cn%20%20%5C%22items%5C%22%3A%20%5B%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Winifred%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Frost%5C%22%7D%2C%20%5C%22age%5C%22%3A%2042%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Miles%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Fernandez%5C%22%7D%2C%20%5C%22age%5C%22%3A%2015%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Kennard%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Floyd%5C%22%7D%2C%20%5C%22age%5C%22%3A%2020%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Lonnie%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Davis%5C%22%7D%2C%20%5C%22age%5C%22%3A%2078%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Duncan%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Poole%5C%22%7D%2C%20%5C%22age%5C%22%3A%2036%7D%5Cn%20%20%5D%5Cn%7D%22%7D) 151 | 152 | Print the ages of the first 3 users only: 153 | 154 | [`cat users.json | pjs --json '.items[0:3]' '_.age'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--json%20'.items%5B0%3A3%5D'%20'_.age'%22%2C%22input%22%3A%22%7B%5Cn%20%20%5C%22version%5C%22%3A%20123%2C%5Cn%20%20%5C%22items%5C%22%3A%20%5B%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Winifred%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Frost%5C%22%7D%2C%20%5C%22age%5C%22%3A%2042%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Miles%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Fernandez%5C%22%7D%2C%20%5C%22age%5C%22%3A%2015%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Kennard%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Floyd%5C%22%7D%2C%20%5C%22age%5C%22%3A%2020%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Lonnie%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Davis%5C%22%7D%2C%20%5C%22age%5C%22%3A%2078%7D%2C%5Cn%20%20%20%20%7B%5C%22name%5C%22%3A%20%7B%5C%22first%5C%22%3A%20%5C%22Duncan%5C%22%2C%20%5C%22last%5C%22%3A%20%5C%22Poole%5C%22%7D%2C%20%5C%22age%5C%22%3A%2036%7D%5Cn%20%20%5D%5Cn%7D%22%7D) 155 | 156 | Query a web API for users: 157 | 158 | ```sh 159 | curl -A "" 'https://www.instagram.com/web/search/topsearch/?query=John' | 160 | pjs --json '.users[].user' '`@${_.username} (${_.full_name})`' 161 | ``` 162 | 163 | ## HTML/XML Examples 164 | 165 | Print the text of all `

` and `

` elements on a web page: 166 | 167 | [`cat page.html | pjs --html 'h1,h2' '_.text'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--html%20'h1%2Ch2'%20'_.text'%22%2C%22input%22%3A%22%3Chtml%3E%5Cn%3Cbody%3E%5Cn%20%20%3Ch1%3EWelcome%20To%20My%20Geocities%20Homepage%3C%2Fh1%3E%5Cn%5Cn%20%20%3Cdiv%3E%5Cn%20%20%20%20Under%20CONSTRUCTION%5Cn%20%20%20%20%3Cimg%20src%3D%5C%22construction.gif%5C%22%3E%5Cn%20%20%3C%2Fdiv%3E%5Cn%5Cn%20%20%3Cimg%20src%3D%5C%22dancing-baby.gif%5C%22%3E%5Cn%5Cn%20%20%3Cdiv%3E%5Cn%20%20%20%20Visitor%20counter%3A%20%3Cspan%20id%3D%5C%22counter%5C%22%3E1234%3C%2Fspan%3E%5Cn%20%20%3C%2Fdiv%3E%5Cn%5Cn%20%20%3Ch1%3EGuestbook%3C%2Fh1%3E%5Cn%5Cn%20%20Sign%20my%20%3Ca%20href%3D%5C%22guestbook.html%5C%22%3EGuestbook!%3C%2Fa%3E%5Cn%3C%2Fbody%3E%5Cn%3C%2Fhtml%3E%22%7D) 168 | 169 | Print the URLs of all images on a web page: 170 | 171 | [`cat page.html | pjs --html 'img' '_.attr.src'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--html%20'img'%20'_.attr.src'%22%2C%22input%22%3A%22%3Chtml%3E%5Cn%3Cbody%3E%5Cn%20%20%3Ch1%3EWelcome%20To%20My%20Geocities%20Homepage%3C%2Fh1%3E%5Cn%5Cn%20%20%3Cdiv%3E%5Cn%20%20%20%20Under%20CONSTRUCTION%5Cn%20%20%20%20%3Cimg%20src%3D%5C%22construction.gif%5C%22%3E%5Cn%20%20%3C%2Fdiv%3E%5Cn%5Cn%20%20%3Cimg%20src%3D%5C%22dancing-baby.gif%5C%22%3E%5Cn%5Cn%20%20%3Cdiv%3E%5Cn%20%20%20%20Visitor%20counter%3A%20%3Cspan%20id%3D%5C%22counter%5C%22%3E1234%3C%2Fspan%3E%5Cn%20%20%3C%2Fdiv%3E%5Cn%5Cn%20%20%3Ch1%3EGuestbook%3C%2Fh1%3E%5Cn%5Cn%20%20Sign%20my%20%3Ca%20href%3D%5C%22guestbook.html%5C%22%3EGuestbook!%3C%2Fa%3E%5Cn%3C%2Fbody%3E%5Cn%3C%2Fhtml%3E%22%7D) 172 | 173 | Scrape headlines off a news site using a complex CSS selector: 174 | 175 | ```sh 176 | curl https://news.ycombinator.com | pjs '_.text' \ 177 | --html 'table table tr:nth-last-of-type(n+2) td:nth-child(3)' 178 | ``` 179 | 180 | Print all links in `

` elements with URLs containing the word "blog": 181 | 182 | ```sh 183 | curl https://aduros.com | pjs --html 'h2 a' '_.attr.href.includes("blog") && _.attr.href' 184 | ``` 185 | 186 | Print a readable summary of an RSS feed: 187 | 188 | ```sh 189 | curl https://aduros.com/index.xml | pjs --xml 'item' \ 190 | '_.querySelector("title").text + " --> " + _.querySelector("link").text' 191 | ``` 192 | 193 | ## Advanced Examples 194 | 195 | Bulk rename \*.jpeg files to \*.jpg: 196 | 197 | ```sh 198 | find -name '*.jpeg' | pjs 'let f = path.parse(_); 199 | fs.renameSync(_, path.join(f.dir, f.name+".jpg"))' 200 | ``` 201 | 202 | Print the longest line in the input: 203 | 204 | [`cat input.txt | pjs 'if (_.length > m) { m = _.length; longest = _ }' --after 'longest'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20'if%20(_.length%20%3E%20m)%20%7B%20m%20%3D%20_.length%3B%20longest%20%3D%20_%20%7D'%20--after%20'longest'%22%2C%22input%22%3A%22There%20once%20was%20a%20man%20from%20Nantucket%5CnWho%20kept%20all%20his%20cash%20in%20a%20bucket.%5CnBut%20his%20daughter%20named%20Nan%2C%5CnRan%20away%20with%20a%20man%5CnAnd%20as%20for%20the%20bucket%2C%20Nantucket.%22%7D) 205 | 206 | Count the words in the input: 207 | 208 | [`cat input.txt | pjs '{ words += $.length }' --after 'words'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20'%7B%20words%20%2B%3D%20%24.length%20%7D'%20--after%20'words'%22%2C%22input%22%3A%22There%20once%20was%20a%20man%20from%20Nantucket%5CnWho%20kept%20all%20his%20cash%20in%20a%20bucket.%5CnBut%20his%20daughter%20named%20Nan%2C%5CnRan%20away%20with%20a%20man%5CnAnd%20as%20for%20the%20bucket%2C%20Nantucket.%22%7D) 209 | 210 | Count the *unique* words in the input: 211 | 212 | [`cat input.txt | pjs --before 'let words = new Set()' 'for (let word of $) words.add(word)' --after 'words.size'`](https://aduros.com/pjs/#%7B%22command%22%3A%22pjs%20--before%20'let%20words%20%3D%20new%20Set()'%20'for%20(let%20word%20of%20%24)%20words.add(word)'%20--after%20'words.size'%22%2C%22input%22%3A%22There%20once%20was%20a%20man%20from%20Nantucket%5CnWho%20kept%20all%20his%20cash%20in%20a%20bucket.%5CnBut%20his%20daughter%20named%20Nan%2C%5CnRan%20away%20with%20a%20man%5CnAnd%20as%20for%20the%20bucket%2C%20Nantucket.%22%7D) 213 | 214 | Using a script file instead of command-line arguments: 215 | 216 | ```sh 217 | echo ' 218 | BEFORE: { 219 | print("Starting up!") 220 | } 221 | _.toUpperCase() 222 | AFTER: "Total lines: "+COUNT 223 | ' > my-uppercase.js 224 | 225 | cat document.txt | pjs -f my-uppercase.js 226 | ``` 227 | 228 | Adding a shebang to the above script to make it self-executable: 229 | 230 | ```sh 231 | echo "#!/usr/bin/env -S pjs -f" | cat - my-uppercase.js > my-uppercase 232 | chmod +x my-uppercase 233 | 234 | ./my-uppercase document.txt 235 | ``` 236 | 237 | Completely scrape an entire online store, outputting a JSON stream for later processing: 238 | 239 | ```sh 240 | for page in `seq 1 50`; do 241 | 242 | >&2 echo "Scraping page $page..." 243 | curl -s "http://books.toscrape.com/catalogue/page-$page.html" | 244 | pjs --html '.product_pod h3 a' '"http://books.toscrape.com/catalogue/"+_.attr.href' | 245 | 246 | while read url; do 247 | >&2 echo "Scraping item details from $url" 248 | curl -s "$url" | pjs --html '.product_page' 'JSON.stringify({ 249 | title: _.querySelector(".product_main h1").text, 250 | description: _.querySelector("#product_description + p").text})' 251 | done 252 | done 253 | ``` 254 | -------------------------------------------------------------------------------- /test/data/jwz-rss2html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

RSS Feeds (Generated on Fri Jan 22 2021 19:06:56 GMT-0300 (Brasilia Standard Time)

6 | 7 | 8 |

1. Flash2K

9 |

Posted on Thu, 21 Jan 2021 23:35:06 +0000

10 |

As Adobe Flash stops running, so do some railroads in China: The railroad system in Dalian, northern China, collapsed citywide on Tuesday for up to 20 hours after the Adobe Flash programing software stopped running. Adobe had announced as early as 2017 that it would cease support for the multimedia software on Dec. 30 last year. The American software company eventually ended the operation of all Flash content on Tuesday. Tuesday's chaos arose after China Railway Shenyang failed to deactivate Flash in time, leading to a complete shutdown of its railroads in Dalian, Liaoning province. Staffers were reportedly unable to view train operation diagrams, formulate train sequencing schedules and arrange shunting plans. Authorities fixed the issue by installing a pirated version of Flash at 4:30 a.m. the following day. Why didn't they just run it on archive.org? Also: did Adobe push out Flash updates with time bombs in them? I assumed the deadline just meant they were going to remove the download link!

11 | 12 | 13 |

2. Put Bernie Anywhere

14 |

Posted on Thu, 21 Jan 2021 20:19:04 +0000

15 |

Enter an address or location and bernie will appear there. These worked out pretty well: This one is the world champ, though. Dan_Case:

16 | 17 | 18 |

3. Zombie Movies Prepared You for the Pandemic

19 |

Posted on Thu, 21 Jan 2021 17:01:40 +0000

20 |

[Research reveals] that an individual's enjoyment of horror films could have better prepared them for the COVID-19 pandemic as opposed to others who do not enjoy frightening entertainment. "My latest research collaboration was unique in that my colleagues wanted to identify factors beyond personality that contributed to people's psychological preparedness and resilience in the face of the pandemic," Johnson explained. "After factoring out personality influences, which were actually quite strong, we found that the more movies about zombies, alien invasions and apocalyptic pandemics people had seen prior to COVID-19, the better they dealt with the actual, current pandemic. These kinds of movies apparently serve as mental rehearsal for actual events. To me, this implicates an even more important message about stories in general -- whether in books, movies or plays. Stories are not just entertainment, but preparation for life." Personally, I never want to see a god damned zombie movie again as long as I live, but I was kind of there already even before the pandemic, because I was so sick to death of how terrible The Walking Dead and its spinoffs were. But I will make an exception for the first zombie movie that focuses on zombie denialists.

21 | 22 | 23 |

4. Googlyshark

24 |

Posted on Thu, 21 Jan 2021 03:27:29 +0000

25 |

SWinstonSchool:

26 | 27 | 28 |

5. Tainted Punchcard

29 |

Posted on Wed, 20 Jan 2021 22:51:12 +0000

30 |

AndyRileyish: I hand-punched Tainted Love into this strip of card because lockdown.

31 | 32 | 33 |

6. Witch Court

34 |

Posted on Wed, 20 Jan 2021 22:31:20 +0000

35 |

'Live' tweeting historical witch trials. Modern English, period sources. An ex-witch called Deliverance Hobbs is now bewitched because she confessed. The spectral form of another witch keeps appearing and beating her with iron rods as punishment. [...] A witch had an inch-long teat on her belly. She claimed it was a hernia, but it looked recently-sucked, with a hole at the tip. When it was squeezed, "white milky matter" came out. She also had three smaller teats on her genitals. A girl claimed that Rose Cullender, the witch with the milky teat on her belly and three extra teats on her vulva, had been coming to her bed at night, one time bringing a huge dog with her. [...] A child caught an invisible mouse and threw it into the fire where it exploded with a flash like gunpowder. No one but the child saw a mouse, but everyone saw the flash. [...] A bee flew into a child's face. The child vomited up a large iron nail. The child later confirmed that the bee had been carrying the nail and had "forced it into her mouth". [...] The child got sick. One night a strange toad fell out of her blanket and ran along the hearth. A boy picked it up with tongs and held it in the fire, where it exploded like a gunshot. The child had been very sick, but after the magic toad exploded in the fire she completely recovered. [...] The boy told the Devil he would commit to a life of deceit if the Devil would grant him one strange superpower: the boy asked for his saliva to be given the power to scald dogs as though it were boiling water. The Devil granted the boy his wish (to "make his spittle scald a dog"). The boy spat on a dog and a large amount of scalding hot water poured all over it. The possessed boy admitted to having spat a torrent of boiling water onto the dog. His honesty and remorse caused the Devil to leave his body with a terrifyingly loud noise. [...] A witch riding a goat was able to carry 15 or 16 children in one trip by taking a long wooden pole, sticking one end into the goat's anus and seating the children all along the length of the pole. [...] Asked where precisely on her body the Devil sucked her blood, the witch said the Devil sucked at a location he had chosen himself, just slightly above her anus. Because of the Devil's constant sucking at that spot, there is now a teat-like growth on the witch's body, near her anus. Elizabeth asked the Devil why he sucked her blood from the teat just above her anus. He said that it nourished him. [...] A witch's blood is infected with a "poisonous ferment". If a witch gives you a hard glare while thinking spiteful thoughts, pestilential spirits will shoot out of her eyes and infect you. Nature works by "subtle streams" of "minute particles". An example would be the jets of pestilence which shoot out of witch's eyes via by means of their malicious imagination and cause "dangerous and strange alterations" in those weak enough to be affected. The contagious witch-curse can be airborne - spread into the air by a witch's glaring eyes. The pestilence can also be transmitted in other more obvious ways such as the victim being hit by a witch or being given a poisoned apple. [...] Some say, why would the Devil waste his time running errands for a "silly old woman"? But if the Devil is wicked then he probably also uses his time unwisely. When we hear about the crazy behaviour of spirits and familiars, some people ask why the devil would frolic so ludicrously. Perhaps witches are only visited by spirits or demons who are very junior, low ranking or disgraced. [...] The idea that witches can change their bodies into animals is no harder to believe than the idea that the thoughts or imagination of a pregnant woman can cause her foetus to have real and monstrous birth defects, which is of course a widely credited fact.

36 | 37 | 38 |

7. yjkX

39 |

Posted on Wed, 20 Jan 2021 20:52:28 +0000

40 |

I sincerely hope that today's regime change can mark a return to my blog's core competency: fart jokes and tentacles.

41 | 42 | 43 |

8. Wikipedia: Repository of all Human Knowledge

44 |

Posted on Wed, 20 Jan 2021 19:24:23 +0000

45 |

The Great Wikipedia Titty Scandal: This is the story of a Wikipedia administrator gone mad with 80,000 boob pages. Digging into Neelix's history, however, his fellow administrators couldn't believe what they found. He hadn't just created a handful of redirects, as the original report described; he'd quietly created thousands upon thousands of new redirects, each one a chaotic, if not offensive, permutation of the word "tits" and "boobs." For example, he created redirects for "tittypumper," "tittypumpers," "tit pump," "pump titties," "pumping boobies" and hundreds more for "breast pump." In fact, for seemingly every Wikipedia article related to breasts, he did something similar. [...] "I especially don't see the value of creating pages with titles like 'titty banged,' 'frenchfucking,' 'licks boobs,' 'boobyfeeding,' 'a trip down mammary lane' and so on. Wikipedia is not censored, but we're also not Urban Dictionary," added Ivanvector. [...] "I've just gone through all 80,000 page creations, and he was creating nonsense like 'anti-trousers' years ago," added Iridescent. "This isn't anything new, it's just the first time it's come to light."

46 | 47 | 48 |

9. Dear Music Video Directors:

49 |

Posted on Tue, 19 Jan 2021 17:40:52 +0000

50 |

By my extremely scientific survey, 10% of all music videos made in 2020 have used the following filter. Stop. It is not charming like an 808 handclap. It is irritating like an airhorn sample. I don't know which video-editing software came with this as one of the stock old-timey presets, but FFS, stop using it. Bonus points here for the unnecessarily burned-in pillarboxing.

51 | 52 | 53 |

10. The Internet was a Mistake

54 |

Posted on Mon, 18 Jan 2021 20:59:19 +0000

55 |

TeenageStepdad:

56 | 57 | 58 |

11. Apparently I have violated Facebook's Community Standards.

59 |

Posted on Mon, 18 Jan 2021 16:32:53 +0000

60 |

Note that my Facebook account has zero friends, zero posts, zero photos, and has made zero comments in the last 4+ years. It only still exists so that I can admin our business pages. "We may be unable to review your account. We apologize for any inconvenience "

61 | 62 | 63 |

12. Spare the air.

64 |

Posted on Mon, 18 Jan 2021 07:35:12 +0000

65 |

So many people have died in Los Angeles County that officials have suspended air-quality regulations that limit the number of cremations. Health officials and the L.A. County coroner requested the change because the current death rate is "more than double that of pre-pandemic years, leading to hospitals, funeral homes and crematoriums exceeding capacity, without the ability to process the backlog," the South Coast Air Quality Management District said Sunday.

66 | 67 | 68 |

13. Periodic wellness check on Pablo Escobar's Cocaine Hippos:

69 |

Posted on Mon, 18 Jan 2021 02:22:11 +0000

70 |

Doin' fine. 'When he was shot dead in 1993,, most of the animals were shipped away, but four hippos were left to fend for themselves in a pond. Although nobody knows exactly how many there are, estimates put the total number between 80 and 100, making them the largest invasive species on the planet. Scientists forecast that the number of hippos will swell to almost 1,500 by 2040. They conclude, that at that point, environmental impacts will be irreversible and numbers impossible to control. "Nobody likes the idea of shooting a hippo, but we have to accept that no other strategy is going to work," [...] Environmentalists have been trying to sterilise the hippos for years [...] Male hippos have retractable testes and females' reproductive organs are even harder to find, according to scientists. "We didn't understand the female anatomy," said David Echeverri Lopez, a government environmentalist. "We tried to sterilise females on several occasions and were always unsuccessful." He is also playing an impossible game of catch-up. Mr Echeverri told The Telegraph that he is able to castrate roughly a hippo per year, whereas scientists estimate that the population grows by 10 percent annually. [...] "Relocation might have been possible 30 years ago, when there were only four hippos," said Dr Castelblanco-Martínez. "Castration could also have been effective if officials had provided sufficient resources for the programme early on, but a cull is now the only option."

71 | 72 | 73 |

14. Space Monkey

74 |

Posted on Sun, 17 Jan 2021 20:57:54 +0000

75 |

How did I not know about this until today?? In 2016, former astronaut Mark Kelly sent his twin brother and ISS commander Scott Kelly a gorilla suit for their birthday.

76 | 77 | 78 |

15. Today is Johnny Mnemonic day.

79 |

Posted on Sun, 17 Jan 2021 17:49:37 +0000

80 |

Thursday, January 17th, 2021. That's right, today's Thursday. It says so right there. "Fax the images to Newark." I wrote a review of it ten months ago -- I saw it at Alamo shortly before lockdown, which probably means that Johnny Mnemonic was the last movie I saw in a theatre.

81 | 82 | 83 |

16. WTF, certbot

84 |

Posted on Sun, 17 Jan 2021 16:50:38 +0000

85 |

A few weeks ago, my Let's Encrypt cron job started complaining that certbot-auto is no longer supported on CentOS 7.7. Ummmm thaaaaanks? So I changed "certbot-auto" to "certbot" but now it's saying: Attempting to parse the version 1.9.0 renewal configuration file found at /etc/letsencrypt/renewal/jwz.org.conf with version 0.38.0 of Certbot. This might not work. How is this shit supposed to work? What am I expected to do on CentOS 7.7? Certbot 0.38.0 is the latest version in yum.

86 | 87 | 88 |

17. 10K September

89 |

Posted on Sat, 16 Jan 2021 19:06:45 +0000

90 |

Today is day 10,000 of The September That Never Ended. The Internet: Mistakes Were Made.™

91 | 92 | 93 |

18. Stealing Your Private YouTube Videos, One Frame at a Time

94 |

Posted on Sat, 16 Jan 2021 03:40:42 +0000

95 |

Turns out you can exfiltrate every possible thumbnail of a private video via an Adwords account.

96 | 97 | 98 |

19. I told you so, 2021 edition

99 |

Posted on Sat, 16 Jan 2021 03:11:57 +0000

100 |

Cinnamon-screensaver got popped, again. If you are not running XScreenSaver on Linux, then it is safe to assume that your screen does not lock. The latest: 2021: Mash keys on the virtual keyboard to unlock Cinnamon-screensaver. Previously: CVE-2019-3010, Privilege escalation in Oracle Solaris screen saver fork. CVE-2015-7496: Hold ESC to unlock Gnome-session GDM. CVE-2014-1949, MDVSA-2015:162: Press Menu key then ESC in Cinnamon-screensaver, get shell. Hold down keys, unlock Cinnamon-screensaver. Hold enter, unlock Gnome-screensaver. You will recall that in 2004, which is now seventeen years ago, I wrote a document explaining why I made the design trade-offs that I did in XScreenSaver, and in that document I predicted this exact bug as my example of, "this is what will happen if you don't do it this way." And they went and made that happen. Repeatedly. Every time this bug is re-introduced, someone pipes up and says something like, "So what, it was a bug, they've fixed it." That's really missing the point. The point is not that such a bug existed, but that such a bug was even possible. The real bug here is that the design of the system even permits this class of bug. It is unconscionable that someone designing a critical piece of security infrastructure would design the system in such a way that it does not fail safe. Especially when I have given them nearly 30 years of prior art demonstrating how to do it right, and a two-decades-old document clearly explaining What Not To Do that coincidentally used this very bug as its illustrative strawman! These bugs are a shameful embarrassment of design -- as opposed to merely bad code. This same bug keeps cropping up in these other screen lockers for several reasons. Writing security-critical code is hard. Most people can't do it. Locking and authentication is an OS-level problem. And while X11 is at the heart of the OS of a Linux desktop computer, it was designed with no security to speak of, and so lockers have to run as normal, unprivileged, user-level applications. That makes the problem even harder. This mistake of the X11 architecture can never, ever be fixed. X11 is too old, too ossified, and has too many quagmire-trapped stakeholders to ever make any meaningful changes to it again. That's why people keep trying to replace X11 -- and failing, because it's too entrenched. As always, these bugs are terrible because bad security is worse than no security. If you knew for a fact that your screen didn't lock, you would behave appropriately. Maybe you'd log out when you walked away. Maybe you wouldn't use that computer for certain things. But a security placebo makes you behave as if it's secure when in fact it is not. One of the infuriating parts of these recurring bugs is that the screen-locker part of XScreenSaver isn't even the fun part! I do not enjoy working on it. I never have. I added it in response to demand and necessity, not because it sounded like a good time. I started and continue this project as an outlet for making art. I'd much rather be spending my time pushing triangles. Sigh. And in not-at-all-unrelated news: Just to add insult to injury, it has recently come to my attention that not only are Gnome-screensaver, Mate-screensaver and Cinnamon-screensaver buggy and insecure dumpster fires, but they are also in violation of my license and infringing my copyright. XScreenSaver was released under the BSD license, one of the oldest and most permissive of the free software licenses. It turns out, the Gnome-screensaver authors copied large parts of XScreenSaver into their program, removed the BSD license and slapped a GPL license on my code instead -- and also removed my name. Rude. If they had asked me, "can you dual-license this code", I might have said yes. If they had asked, "can we strip your name off and credit your work as (C) William Jon McCann instead"... probably not. Mate-screensaver and Cinnamon-screensaver, being forks and descendants of Gnome-screensaver, have inherited this license violation and continue to perpetuate it. Every Linux distro is shipping this copyright- and license-infringing code. I eagerly await hearing how they're going to make this right.

101 | 102 | 103 |

20. Billboard

104 |

Posted on Sat, 16 Jan 2021 00:24:30 +0000

105 |

"Wow, I sure do hate this weed-and-bongs billboard outside my window." [ Monkey's paw curls ] Now there's an anti-abortion billboard too.

106 | 107 | 108 |

21. Sony Scopeman

109 |

Posted on Thu, 14 Jan 2021 03:37:18 +0000

110 |

Niklas Fauth: Hardware Design files of a replacement mainboard for the Sony Watchman FD-10. This turns it into a bluetooth and WiFi-enabled vector display. In "Audio" mode, the ESP32 acts as a bluetooth speaker. Play back audio files from your smartphone or laptop to hear and see the soundwaves. You can change the size by adjusting the playback volume. In "Video" mode, the ESP32 renders the result of the Lorenz Attractor equation. You can change the simulation speed using the "Tune" knob.

111 | 112 | 113 |

22. Facebook Is Showing Military Gear Ads Next To Insurrection Posts

114 |

Posted on Thu, 14 Jan 2021 01:58:11 +0000

115 |

Facebook has been running ads for body armor, gun holsters, and other military equipment next to content promoting election misinformation and news about the attempted coup at the US Capitol, despite internal warnings from concerned employees. In the aftermath of an attempted insurrection by President Donald Trump's supporters last week at the US Capitol building, Facebook has served up ads for defense products to accounts that follow extremist content, according to the Tech Transparency Project, a nonprofit watchdog group. Those ads -- which include New Year's specials for specialized body armor plates, rifle enhancements, and shooting targets -- were all delivered to a TTP Facebook account used to monitor right-wing content that could incite violence. [...] These ads for tactical gear, which were flagged internally by employees as potentially problematic, show Facebook has been profiting from content that amplifies political and cultural discord in the US. "Facebook has spent years facilitating fringe voices who use the platform to organize and amplify calls for violence," said TTP Director Katie Paul. "As if that weren't enough, Facebook's advertising microtargeting is directing domestic extremists toward weapons accessories and armor that can make their militarized efforts more effective, all while Facebook profits." [...] During Monday's interview, Sandberg also addressed the proliferation of hate-related content on Facebook. "I think there's a false belief that we somehow profit, that people somehow want to see this content," she said. "That's just not true." [cue laughter]

116 | 117 | 118 |

23. US Coast Guard recovers stolen tiki boat after extremely low-speed chase

119 |

Posted on Thu, 14 Jan 2021 01:00:47 +0000

120 |

"The person aboard showed signs of intoxication and was taken into custody."

121 | 122 | 123 |

24. The Scarlet F

124 |

Posted on Thu, 14 Jan 2021 00:35:22 +0000

125 |

Facebook Warns Employees Not to Wear Company Gear In Public After Banning Trump: Facebook's internal security team has warned employees not to wear or carry any Facebook-branded gear in public. "In light of recent events, and to err on the side of caution, global security is encouraging everyone to avoid wearing or carrying Facebook-branded items at this time," the memo said without apparently mentioning Trump by name. [...] It's no surprise that social media companies are worried about backlash from Trump's cadre of dangerous extremists. Though one can't help but wonder where we'd be today had companies like Facebook and Twitter taken Trump's authoritarian language more seriously over the past four years. [...] Facebook and Twitter may have done the right thing by banning Trump after the Capitol coup attempt, but it's way too late to call leadership at these companies anything but complicit in the damage Trump has done to the country. There would very likely be no Trump presidency without Twitter and Facebook.

126 | 127 | 128 |

25. House EVS

129 |

Posted on Thu, 14 Jan 2021 00:02:29 +0000

130 |

I don't think I've seen the House voting UI before! The aesthetic is very Star Trek TOS. kaikahele: On just my 11th day as Hawaii's newest member of Congress I have voted YEA to impeach the President of the United States. He must be held accountable for inciting violent & deadly insurrection on our democracy & our nations capitol. We must remove him from office. ProPublica: Voting in the House has not changed much since an electronic voting system was first used on January 23, 1973, with the goal of making voting periods shorter. The latest version of the EVS system was installed in 2004. There are 47 voting stations around the House chamber,, and members can use any of them to vote using a card they carry. Few votes are held in the format that existed decades ago, when individual names were read aloud, but electronic voting isn't mandatory. Lawmakers can come down to the well of the chamber, in front of the clerks, and use a system of color-coded cards to write their votes and hand them to a tally clerk. house.gov: Before electronic voting was introduced, this well-worn object, two Veeder-Root manual counters welded together, was often the center of attention on the Floor of the House. 1985. 1972. 1928 I wonder if the cards are considered "secrets" or if they just have the name encoded on them in plain-text.

131 | 132 | 133 |

26. Recent Movies and TV

134 |

Posted on Tue, 12 Jan 2021 06:29:14 +0000

135 |

Mank: Wow, I loved this -- it's gorgeously shot, the cast are amazing, the flashback structure mirrors Kane in a very cool way.... And I can't imagine how this got made. Was the pitch, "We've got that film major market cornered! Well... not just any film major, but the ones who have deep knowledge about the players in the studio system in the 30s. Long tail. Oh and it's black and white." If you aren't a total movie nerd will you even understand what's going on without regular Wikipedia pauses? Anyway it's fucking brilliant, and the best technical recreation of a movie from that era that I've ever seen. They even included the cigarette burns and reel-change flutters! Kingfish calls this "painting the feet" -- "When you paint the little pilot's feet, and then glue closed the cockpit of the model airplane, and only you know, for all time, that his little feets are painted." Tenet: I remember that sinking feeling when they finally found the MacGuffin. "Fuck, that means it's only half over??" You know how they say Trump is a poor person's idea of a rich person? This movie is a stupid person's idea of clever. Much like Inception. I mean, Bill and Ted 3 had better use of its cosmology. The camera tricks with the backwards fights weren't even any good, or even comprehensible. And a backwards person sitting in a forwards car makes it backward? The stupid, it burns like inexplicable frostbite. This movie was better when it was the Sugar Water video by Cibo Matto, which was mercifully only 4 minutes long. And had better physics. The Flight Attendant: She wakes up in a hotel in a foreign country with a corpse in her bed and isn't sure if she murdered him, like you do. It started off much stronger than it finished, but it was ok. Mad Max Fury Road, Black & Chrome Edit: In memoriam of the passing of Toecutter, I watched the black and white version, which I hadn't seen before. It's brilliant. Interestingly, not entirely black and white, but sometimes graded in blues instead. I think it also would have worked really well as a silent movie (well, not silent -- but not a "talkie". Score and effects, but title cards instead of dialog!) J. R. "Bob" Dobbs and the Church of the Subgenius: This was fun a fun documentary about a cult that was very important to me in my formative years. The Queen's Gambit: Poorly socialized addict plays chess, pouts. I loved this. Superintelligence: An AI trying to decide whether to wipe out humanity uses Melissa McCarthy as a guinea pig, and then the plot kind of just gives up on all that and the AI tries to set her up on a date instead, while the DOD and NSA peek in the window like the ineffectual chaperone from an 80s movie? It's fluff, but I enjoy her style of comedy and this is more of that. Planes, Trains and Automobiles: I watched an actual Thanksgiving movie, near Thanksgiving. I forgot how funny this was! I was still chuckling about it the next day. Lego Star Wars Holiday Special: I've never watched any of the other Lego movies, but this was one of the worst, least-funny things I've ever seen. I didn't last 15 minutes. I was expecting Robot Chicken. It is not that. The Stand-In: Drew Barrymore chews the scenery as two different shitheads. It's predictable but kinda funny. Archenemy: Hobo Superman helps some kids beat up some drug dealers. It's ok. Radium Girls: It's depressing, and then their faces fall off. Capitalism! Castle Freak (2020): Breaking my rule about remakes, this was a pretty entertaining horror movie. It went Lovecraft, whereas the original didn't (even though Stuart Gordon, director of the original, made all the Lovecraft movies.) Castle Freak (1995): I couldn't remember if I had ever seen the original. It is complete garbage, probably Gordon's worse. And... that's something. Wonder Woman 84: Oh mah gawd this was soooo bad. Let us never speak of it again. Parallel: Tech shitheads find a magic mirror to parallel universes and act like shitheads about it, taking advantage in the most shitheaddy ways they can imagine. Pass. Soul: It's really good. Tesla (2020): This was fantastic. It had a fourth-wall-breaking ahistorical aspect to it, like the I, Tonya of alternating current. (There's even skating.)

136 | 137 | 138 |

27. jwz mixtape 224

139 |

Posted on Tue, 12 Jan 2021 02:38:37 +0000

140 |

Please enjoy jwz mixtape 224.

141 | 142 | 143 |

28. Modern Retro Computer Terminals

144 |

Posted on Mon, 11 Jan 2021 23:49:42 +0000

145 |

Oriol Ferrer Mesià:

146 | 147 | 148 |

29. Meanwhile, Cate Blanchett

149 |

Posted on Mon, 11 Jan 2021 20:03:23 +0000

150 |

Cate Blanchett wins permission for meditation room at her haunted £4.9m Sussex mansion despite discovery of bat colony.

151 | 152 | 153 |

30. House Democrats introduce impeachment resolution

154 |

Posted on Mon, 11 Jan 2021 18:03:28 +0000

155 |

Charging Trump with incitement of insurrection: House Democrats formally introduced their resolution to impeach President Donald Trump on Monday, [PDF, text] charging him with "incitement of insurrection" for his role in last week's riots at the US Capitol. The single impeachment article, which was introduced when the House gaveled into a brief pro-forma session Monday, points to Trump's repeated false claims that he won the election and his speech to the crowd on January 6 before pro-Trump rioters breached the Capitol. It also cited Trump's call with the Georgia Republican secretary of state where the President urged him to "find" enough votes for Trump to win the state. "In all this, President Trump gravely endangered the security of the United States and its institutions of Government," the resolution says. "He threatened the integrity of the democratic system, interfered with the peaceful transition of power, and imperiled a coequal branch of Government. He thereby betrayed his trust as President, to the manifest injury of the people of the United States." The resolution, which was introduced by Democrats David Cicilline of Rhode Island, Jamie Raskin of Maryland and Ted Lieu of California, also cited the Constitution's 14th Amendment, noting that it "prohibits any person who has 'engaged in insurrection or rebellion against' the United States" from holding office. What does the party of "unity and moving on" have to say? Let's check in: Democrats on Monday sought to take up a resolution from Raskin urging Pence and the Cabinet to invoke the 25th Amendment. Hoyer asked for unanimous consent to bring up the resolution, but West Virginia GOP Rep. Alex Mooney objected to the request. Pelosi has said the Democrats will move to bring the resolution for a floor vote on Tuesday. [...] A Senate impeachment trial beginning on January 20 -- Biden's inauguration -- would grind the chamber to a halt, unable to confirm nominees or enact legislation until the trial was finished. One option being considered is waiting until later to send the articles to the Senate: House Democratic Whip James Clyburn said on CNN's "State of the Union" Sunday the House might wait until after Biden's first 100 days in office before sending the impeachment articles to the Senate to begin the trial. But Hoyer's comments Monday seemed to suggest that was an unlikely move, since it would cut against Democrats' argument that removing Trump is an urgent priority. I don't get the "urgency" argument -- my understanding is that there is no path to removing him from office before Jan 20 via impeachment. Only the 25th Amendment can do that. And Pence -- the guy who Trump's goons intended to lynch on Wednesday -- won't do that.

156 | 157 | 158 |

Total posts: 30

159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /test/data/aduros-rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | aduros.com 5 | https://aduros.com/ 6 | Recent content on aduros.com 7 | Hugo -- gohugo.io 8 | en-us 9 | Bruno Garcia <b@aduros.com> 10 | Bruno Garcia <b@aduros.com> 11 | Wed, 20 Jan 2021 11:04:04 +0000 12 | 13 | Hacking i3: Automatic Layout 14 | https://aduros.com/blog/hacking-i3-automatic-layout/ 15 | Wed, 20 Jan 2021 11:04:04 +0000 16 | Bruno Garcia <b@aduros.com> 17 | https://aduros.com/blog/hacking-i3-automatic-layout/ 18 | <p>(Day 20 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 19 | <p>Another post about making a <a href="https://i3wm.org">good window manager</a> even better!</p> 20 | <p>Window layouts in i3 basically come in two types: horizontally or vertically stacked. By combining 21 | those two you can arrange windows however you want.</p> 22 | <p>The default layout in i3 is horizontal, which means when you open up too many windows it ends up 23 | looking like this:</p> 24 | <video width="100%" loop muted autoplay playsinline class="shadow"> 25 | <source src="before.mp4" type="video/mp4"> 26 | <source src="before.webm" type="video/webm"> 27 | <p>(The video could not be played)</p> 28 | </video> 29 | 30 | <p>Of course what you&rsquo;re supposed to prevent everything getting squished like that is to split windows 31 | to the opposite layout so they grow in the other direction. But that requires a <em>key press</em>, and we 32 | don&rsquo;t use tiling WMs to manually position windows, dammit!</p> 33 | <p>Luckily i3 has a capable API and others have used it to automatically perform that splitting, such 34 | as <a href="https://github.com/olemartinorg/i3-alternating-layout">i3-alternating-layout</a> and 35 | <a href="https://github.com/Chimrod/i3_workspaces">i3_workspaces</a>. After playing with those I didn&rsquo;t 36 | really find the idea of binary tree layouts very practical. Maybe it&rsquo;s my 13&rdquo; laptop screen, but 37 | windows just get too small.</p> 38 | <p>What I really wanted was a two-column layout that splits the screen in half, with a vertical stack 39 | of windows on each side. Well, after a lot of API struggling:</p> 40 | <video width="100%" loop muted autoplay playsinline class="shadow"> 41 | <source src="after.mp4" type="video/mp4"> 42 | <source src="after.webm" type="video/webm"> 43 | <p>(The video could not be played)</p> 44 | </video> 45 | 46 | <p>No windows were manually split in this video 😮 Just pure, sweet terminal spam. Moving windows 47 | between the columns works as you&rsquo;d expect. You can still overload the screen with too many windows, 48 | but hey, a short and wide terminal is at least still readable.</p> 49 | <p>Here&rsquo;s the source for that:</p> 50 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python3" data-lang="python3"><span style="color:#75715e">#!/usr/bin/env python3</span> 51 | <span style="color:#75715e">#</span> 52 | <span style="color:#75715e"># Automatically splits windows so workspaces are laid out in 2 columns.</span> 53 | 54 | <span style="color:#f92672">from</span> i3ipc <span style="color:#66d9ef">import</span> Connection, Event 55 | 56 | COLUMNS <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span> 57 | 58 | <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">move_container</span> (con1, con2): 59 | con2<span style="color:#f92672">.</span>command(<span style="color:#e6db74">&#34;mark __column-layout&#34;</span>); 60 | con1<span style="color:#f92672">.</span>command(<span style="color:#e6db74">&#34;move window to mark __column-layout&#34;</span>) 61 | con2<span style="color:#f92672">.</span>command(<span style="color:#e6db74">&#34;unmark __column-layout&#34;</span>); 62 | 63 | <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">layout</span> (i3, event): 64 | <span style="color:#66d9ef">if</span> event<span style="color:#f92672">.</span>change <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;close&#34;</span>: 65 | <span style="color:#66d9ef">for</span> reply <span style="color:#f92672">in</span> i3<span style="color:#f92672">.</span>get_workspaces(): 66 | <span style="color:#66d9ef">if</span> reply<span style="color:#f92672">.</span>focused: 67 | workspace <span style="color:#f92672">=</span> i3<span style="color:#f92672">.</span>get_tree()<span style="color:#f92672">.</span>find_by_id(reply<span style="color:#f92672">.</span>ipc_data[<span style="color:#e6db74">&#34;id&#34;</span>])<span style="color:#f92672">.</span>workspace() 68 | 69 | <span style="color:#66d9ef">if</span> len(workspace<span style="color:#f92672">.</span>nodes) <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">and</span> len(workspace<span style="color:#f92672">.</span>nodes[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>nodes) <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>: 70 | child <span style="color:#f92672">=</span> workspace<span style="color:#f92672">.</span>nodes[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>nodes[<span style="color:#ae81ff">0</span>] 71 | move_container(child, workspace) 72 | <span style="color:#66d9ef">else</span>: 73 | window <span style="color:#f92672">=</span> i3<span style="color:#f92672">.</span>get_tree()<span style="color:#f92672">.</span>find_by_id(event<span style="color:#f92672">.</span>container<span style="color:#f92672">.</span>id) 74 | <span style="color:#66d9ef">if</span> window <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>: 75 | workspace <span style="color:#f92672">=</span> window<span style="color:#f92672">.</span>workspace() 76 | <span style="color:#66d9ef">if</span> workspace <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span> <span style="color:#f92672">and</span> len(workspace<span style="color:#f92672">.</span>nodes) <span style="color:#f92672">&gt;=</span> COLUMNS: 77 | <span style="color:#66d9ef">for</span> node <span style="color:#f92672">in</span> workspace<span style="color:#f92672">.</span>nodes: 78 | <span style="color:#66d9ef">if</span> node<span style="color:#f92672">.</span>layout <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;splitv&#34;</span>: 79 | node<span style="color:#f92672">.</span>command(<span style="color:#e6db74">&#34;splitv&#34;</span>) 80 | 81 | i3 <span style="color:#f92672">=</span> Connection() 82 | i3<span style="color:#f92672">.</span>on(Event<span style="color:#f92672">.</span>WINDOW_NEW, layout) 83 | i3<span style="color:#f92672">.</span>on(Event<span style="color:#f92672">.</span>WINDOW_CLOSE, layout) 84 | i3<span style="color:#f92672">.</span>on(Event<span style="color:#f92672">.</span>WINDOW_MOVE, layout) 85 | i3<span style="color:#f92672">.</span>main() 86 | </code></pre></div><p>As always, make sure you have python3 and the latest version of i3ipc installed.</p> 87 | <p>For larger and wider screens you might experiment increasing <code>COLUMNS</code> to 3. I tend to stick with 2 88 | even when I had a desktop computer, my workflow usually ends up being <em>[thing I&rsquo;m working on]</em> on 89 | the left column and a bunch of terminals on the right.</p> 90 | <p>If you&rsquo;re still reading this far, you&rsquo;ll probably like these other posts on i3:</p> 91 | <ul> 92 | <li><a href="../hacking-i3-window-promoting">Hacking i3: Window Promoting</a></li> 93 | <li><a href="../hacking-i3-window-swallowing">Hacking i3: Window Swallowing</a></li> 94 | </ul> 95 | 96 | 97 | 98 | 99 | Introducing pjs 100 | https://aduros.com/blog/introducing-pjs/ 101 | Tue, 19 Jan 2021 20:45:27 +0000 102 | Bruno Garcia <b@aduros.com> 103 | https://aduros.com/blog/introducing-pjs/ 104 | <p>(Day 19 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 105 | <p>I&rsquo;ve been working on a small project that can basically be summed up as <code>awk</code> for JS developers. 106 | <a href="https://github.com/aduros/pjs"><code>pjs</code></a> is a command-line tool for processing text-based files by 107 | writing snippets of JavaScript.</p> 108 | <p>For example, converting each line to upper-case:</p> 109 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-sh" data-lang="sh">cat document.txt | pjs <span style="color:#e6db74">&#39;_.toUpperCase()&#39;</span> 110 | </code></pre></div><p>Or filtering lines:</p> 111 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-sh" data-lang="sh">seq <span style="color:#ae81ff">10</span> | pjs <span style="color:#e6db74">&#39;_ % 2 == 0&#39;</span> 112 | </code></pre></div><p>And a whole bunch of <a href="https://github.com/aduros/pjs#examples">other stuff</a>, including support for 113 | streaming CSV and JSON.</p> 114 | <p>Under the hood it does some interesting things with static analysis and AST transformation to 115 | support all the magic. The magic can always be explained by running pjs with <code>--explain</code> to see the 116 | generated program.</p> 117 | <p>I would say v1.0 is nearly finished, ETA being a confident &ldquo;soon&rdquo;, and probably will be done by the 118 | time anyone reads this.</p> 119 | 120 | 121 | 122 | 123 | Ramblings From a Dirty Apartment 124 | https://aduros.com/blog/ramblings-from-a-dirty-apartment/ 125 | Mon, 18 Jan 2021 20:56:21 +0000 126 | Bruno Garcia <b@aduros.com> 127 | https://aduros.com/blog/ramblings-from-a-dirty-apartment/ 128 | <p>(Day 18 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 129 | <p>I started reading some philosophy disguised as a housekeeping manual called <a href="https://www.amazon.com/Home-Comforts-Science-Keeping-House/dp/0743272862">Home 130 | Comforts</a> and it&rsquo;s been 131 | pretty illuminating.</p> 132 | <p>I&rsquo;ve always loathed housework. I&rsquo;m not exactly sure why. It could be the feeling of futility from 133 | the cycle of cleaning something that will soon become dirty again. Or maybe my 134 | pseudo-nomadism/quasi-Buddhism prevents me from attaching much importance to objects and spaces. Or 135 | maybe like many I internalize that housework is women&rsquo;s work, or that valued work is <em>paid</em> work.</p> 136 | <p>Whatever the reason, it&rsquo;s bullshit. Many qualities I admire in others and try to cultivate in myself 137 | are practiced by housework. Self-reliance, independence, problem-solving, discipline, preparedness, 138 | even mindfulness.</p> 139 | <p>To a degree these are qualities that are also developed by computer programming. I&rsquo;m pretty good at 140 | one and not the other, why? One difference is that people will give you a lot of money for 141 | programming. But monetary rewards are starting to motivate me less and less these past few years.</p> 142 | <p>If I put half as much time organizing my home as I do my <code>$HOME</code> I&rsquo;d probably be better off. 143 | Perhaps housework is the most rewarding task one can perform, because it doesn&rsquo;t depend on an 144 | employer and the results are immediate.</p> 145 | <p>But housework is <em>boring</em>. Yeah, so is programming like 99% of the time. Besides, maybe boredom is 146 | <a href="../implementing-a-slow-life">underrated</a>.</p> 147 | 148 | 149 | 150 | 151 | Returning to Text 152 | https://aduros.com/blog/returning-to-text/ 153 | Sun, 17 Jan 2021 21:21:16 +0000 154 | Bruno Garcia <b@aduros.com> 155 | https://aduros.com/blog/returning-to-text/ 156 | <p>(Day 17 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 157 | <p>I&rsquo;ve been replacing the way I store personal todo lists and notes. Previously I was using Trello. 158 | Before that I used <a href="https://tiddlywiki.com/">Tiddlywiki</a> for a couple years. I used to work with a 159 | guy that swore by simply using a text file, so I started giving that a shot.</p> 160 | <p>In theory this approach has some major perks:</p> 161 | <ul> 162 | <li>It&rsquo;s easy to add stuff: Just start typing.</li> 163 | <li>Just as importantly, it&rsquo;s easy to delete stuff.</li> 164 | <li>Searching and transforming text files is a solved problem.</li> 165 | <li>You own your data. It&rsquo;s on your own hardware and is in a format that won&rsquo;t be obsolete in our 166 | lifetime.</li> 167 | </ul> 168 | <p>Something that may be obvious to others that I discovered only recently is that <em>you need to be able 169 | to get to your notebook.txt quickly</em>. It should be instantly accessible on a global keybind. It 170 | isn&rsquo;t good enough to open your editor, file-open, navigate to notebook.txt, scroll down to where you 171 | were before&hellip; Once I took care of that I&rsquo;m finding the text file approach much more natural.</p> 172 | <p>I&rsquo;m using the <a href="https://github.com/vimwiki/vimwiki">vimwiki</a> plugin, but honestly I don&rsquo;t use most of 173 | the &ldquo;wiki&rdquo; features and just use it for markdown syntax conveniences.</p> 174 | <p>The only downside I occasionally feel is the lack of access on my phone. Though not so much over the 175 | last few months with the lockdowns and all. I&rsquo;ve thought about self-hosting something like Etherpad 176 | that would sync on both my devices, but&hellip; meh. Sometimes low tech is best tech.</p> 177 | 178 | 179 | 180 | 181 | Hacking i3: Window Promoting 182 | https://aduros.com/blog/hacking-i3-window-promoting/ 183 | Sat, 16 Jan 2021 12:04:44 +0000 184 | Bruno Garcia <b@aduros.com> 185 | https://aduros.com/blog/hacking-i3-window-promoting/ 186 | <p>(Day 16 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 187 | <p>One of the only things I miss from when I used <a href="https://xmonad.org/">xmonad</a> many years ago was 188 | being able to hit a keybind to swap the currently focused window with the &ldquo;master&rdquo; window. I think 189 | the default keybind for that in xmonad is alt-Enter.</p> 190 | <p><a href="https://i3wm.org/">i3</a> doesn&rsquo;t have the concept of a master window, but if we consider the master 191 | window to just be the largest window, the same effect can be achieved:</p> 192 | <video width="100%" loop muted autoplay playsinline class="shadow"> 193 | <source src="demo.mp4" type="video/mp4"> 194 | <source src="demo.webm" type="video/webm"> 195 | <p>(The video could not be played)</p> 196 | </video> 197 | 198 | <p>Simple, but often useful. Here&rsquo;s the <code>promote-window</code> script that implements it:</p> 199 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python3" data-lang="python3"><span style="color:#75715e">#!/usr/bin/env python3</span> 200 | <span style="color:#75715e">#</span> 201 | <span style="color:#75715e"># Promotes the focused window by swapping it with the largest window.</span> 202 | 203 | <span style="color:#f92672">from</span> i3ipc <span style="color:#66d9ef">import</span> Connection, Event 204 | 205 | <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">find_biggest_window</span> (container): 206 | max_leaf <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span> 207 | max_area <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span> 208 | <span style="color:#66d9ef">for</span> leaf <span style="color:#f92672">in</span> container<span style="color:#f92672">.</span>leaves(): 209 | rect <span style="color:#f92672">=</span> leaf<span style="color:#f92672">.</span>rect 210 | area <span style="color:#f92672">=</span> rect<span style="color:#f92672">.</span>width <span style="color:#f92672">*</span> rect<span style="color:#f92672">.</span>height 211 | <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> leaf<span style="color:#f92672">.</span>focused <span style="color:#f92672">and</span> area <span style="color:#f92672">&gt;</span> max_area: 212 | max_area <span style="color:#f92672">=</span> area 213 | max_leaf <span style="color:#f92672">=</span> leaf 214 | <span style="color:#66d9ef">return</span> max_leaf 215 | 216 | i3 <span style="color:#f92672">=</span> Connection() 217 | 218 | <span style="color:#66d9ef">for</span> reply <span style="color:#f92672">in</span> i3<span style="color:#f92672">.</span>get_workspaces(): 219 | <span style="color:#66d9ef">if</span> reply<span style="color:#f92672">.</span>focused: 220 | workspace <span style="color:#f92672">=</span> i3<span style="color:#f92672">.</span>get_tree()<span style="color:#f92672">.</span>find_by_id(reply<span style="color:#f92672">.</span>ipc_data[<span style="color:#e6db74">&#34;id&#34;</span>]) 221 | master <span style="color:#f92672">=</span> find_biggest_window(workspace) 222 | i3<span style="color:#f92672">.</span>command(<span style="color:#e6db74">&#34;swap container with con_id </span><span style="color:#e6db74">%s</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">%</span> master<span style="color:#f92672">.</span>id) 223 | </code></pre></div><p>Make sure you have python3 with i3ipc, and then add this your i3 config:</p> 224 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">bindsym $mod+p exec --no-startup-id ~/.config/i3/promote-window 225 | </code></pre></div><p>No big-brain Haskell required! If you read this far and found this interesting you might also like 226 | <a href="../hacking-i3-window-swallowing">Hacking i3: Window Swallowing</a>.</p> 227 | 228 | 229 | 230 | 231 | A Blog Half Full 232 | https://aduros.com/blog/a-blog-half-full/ 233 | Fri, 15 Jan 2021 20:34:14 +0000 234 | Bruno Garcia <b@aduros.com> 235 | https://aduros.com/blog/a-blog-half-full/ 236 | <p>I&rsquo;m halfway through <a href="https://aduros.com/blog/30-days-of-blogging">30 Days of Blogging</a>.</p> 237 | <p>When I was a kid I <em>really</em> wanted to be a cartoonist. I wanted it so much that my first &ldquo;job&rdquo; was 238 | doing a daily comic strip for the local newspaper. I did that for 10 months straight, 6 days a week. 239 | At the end I was so sick of it I never wanted to pick up a pencil ever again.</p> 240 | <p>Hopefully blogging doesn&rsquo;t end up the same way!</p> 241 | <p>In other news, one post this week got picked up on <a href="https://news.ycombinator.com/item?id=25732862">Hacker 242 | News</a> which was a nice little ego boost. I have no 243 | idea how much of that translated to new readership since this site doesn&rsquo;t track users. This is 244 | absolutely for the best.</p> 245 | <p>Just a couple more weeks, let&rsquo;s get it.</p> 246 | 247 | <div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"> 248 | <iframe src="https://www.youtube.com/embed/KxGRhd_iWuE" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe> 249 | </div> 250 | 251 | 252 | 253 | 254 | 255 | Simple Clipboard Management 256 | https://aduros.com/blog/simple-clipboard-management/ 257 | Thu, 14 Jan 2021 23:53:42 +0000 258 | Bruno Garcia <b@aduros.com> 259 | https://aduros.com/blog/simple-clipboard-management/ 260 | <p>(Day 14 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 261 | <p><a href="https://github.com/erebe/greenclip">Greenclip</a> is really useful for recording your clipboard history 262 | and showing a menu to switch between items.</p> 263 | <p>Along the same lines I had an idea to write a tiny script that allows editing of the clipboard in 264 | vim. This has been handy when I need to quickly fix up some text before pasting it into a GUI 265 | application:</p> 266 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-sh" data-lang="sh"><span style="color:#75715e">#!/bin/sh -e 267 | </span><span style="color:#75715e"></span> 268 | file<span style="color:#f92672">=</span><span style="color:#e6db74">`</span>mktemp /tmp/clipboard-XXX<span style="color:#e6db74">`</span> 269 | 270 | xsel --clipboard &gt; <span style="color:#e6db74">&#34;</span>$file<span style="color:#e6db74">&#34;</span> 271 | xterm -e <span style="color:#e6db74">&#34;</span>$EDITOR<span style="color:#e6db74"> -c &#39;set nofixeol&#39; \&#34;</span>$file<span style="color:#e6db74">\&#34;&#34;</span> 272 | xsel --clipboard &lt; <span style="color:#e6db74">&#34;</span>$file<span style="color:#e6db74">&#34;</span> 273 | 274 | rm <span style="color:#e6db74">&#34;</span>$file<span style="color:#e6db74">&#34;</span> 275 | </code></pre></div><p>The <code>nofixeol</code> option prevents vim from adding a newline to the end of the clipboard, which is 276 | usually not desired.</p> 277 | 278 | 279 | 280 | 281 | Firefox Minimalism 282 | https://aduros.com/blog/firefox-minimalism/ 283 | Wed, 13 Jan 2021 19:31:34 +0000 284 | Bruno Garcia <b@aduros.com> 285 | https://aduros.com/blog/firefox-minimalism/ 286 | <p>(Day 13 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 287 | <p>I&rsquo;ve been working on an opinionated and ultra-minimalist Firefox theme that pairs nicely with 288 | <a href="https://github.com/tridactyl/tridactyl">Tridactyl</a>. I thought I&rsquo;d share a few notes here.</p> 289 | <p>But first some pretty screenshots! Here&rsquo;s what you&rsquo;re greeted with when you open a Firefox window:</p> 290 | <div style="text-align: center"> 291 | <img src="screenshot1.png" class="shadow"> 292 | </div> 293 | 294 | <p>Nope, it&rsquo;s not a terminal, that text prompt is the address bar.</p> 295 | <p>Here&rsquo;s what it looks like with a page in a single tab:</p> 296 | <div style="text-align: center"> 297 | <img src="screenshot2.png" class="shadow"> 298 | </div> 299 | 300 | <p>The tab bar is hidden until there are multiple tabs. I find this helps me focus during certain tasks 301 | where I have a browser window with just one site I&rsquo;m working on.</p> 302 | <p>Here&rsquo;s a slightly busier example with three tabs open:</p> 303 | <div style="text-align: center"> 304 | <img src="screenshot3.png" class="shadow"> 305 | </div> 306 | 307 | <p>One of the tabs is playing audio, which is shown with a yellow underline.</p> 308 | <p>In order to fit on this blog, all of these screenshots are much lower resolution than I actually 309 | browse with. But even with windows at low resolutions you can see it&rsquo;s pretty readable.</p> 310 | <h2 id="trimmed-fat">Trimmed Fat</h2> 311 | <p>Default browser UIs can be messy, and some things were obvious candidates to remove. Other things 312 | were less obvious but since the theme assumes hotkey usage, basically everything that doesn&rsquo;t 313 | communicate important information gets hidden:</p> 314 | <ul> 315 | <li>Navigation buttons (back, forward, home, reload): I use hotkeys or the right-click popup.</li> 316 | <li>Page action buttons (bookmark, show info, add to Pocket, etc.): Either I have hotkeys or I never 317 | use these features.</li> 318 | <li>Tab favicons: No branding and more room for the title.</li> 319 | <li>The hamburger menu: The alt key can be pressed to temporarily show the menu bar. Most of the 320 | things in this menu can be hotkeyed (open dev tools, Firefox preferences, logins, etc.)</li> 321 | <li>Extension buttons: I only use uBlock and Tridactyl.</li> 322 | <li>Downloads button: Actually, I kept it because it&rsquo;s handy to see download progress. It&rsquo;s kind of 323 | annoying that it doesn&rsquo;t auto-hide after the downloads are finished. I might remove it.</li> 324 | <li>Page scrollbars: You can usually infer your position in a page just by looking at the content. 325 | Tridactyl has vim-like hotkeys for going to the bottom and top of the page.</li> 326 | </ul> 327 | <h2 id="changed-habits">Changed Habits</h2> 328 | <p>I used to be one of those people with 50+ tabs open, persisted across every session. Maybe once a 329 | week or so I would do a tab purge.</p> 330 | <p>Using a &ldquo;one-line&rdquo; theme quickly discourages that habit. These days I rely more on multiple windows. 331 | When I start a new &ldquo;task&rdquo;, I open a new window on a different desktop. When done I close the window 332 | and all the tabs are gone. If I need to go back to some site I found before, I use browser history 333 | or bookmarks.</p> 334 | <p>There are still some quirks to work out but once it&rsquo;s ready I&rsquo;ll throw the theme up somewhere on 335 | <a href="https://github.com/aduros">Github</a>!</p> 336 | 337 | 338 | 339 | 340 | Media Companies are Complicit 341 | https://aduros.com/blog/media-companies-are-complicit/ 342 | Tue, 12 Jan 2021 20:19:51 +0000 343 | Bruno Garcia <b@aduros.com> 344 | https://aduros.com/blog/media-companies-are-complicit/ 345 | <p>(Day 12 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 346 | <p>Facebook, Twitter, and other media companies shouldn&rsquo;t be applauded for finally deplatforming the 347 | American president after profiting off controversy for so many years.</p> 348 | <p>It took up <em>until the very day</em> that the American president lost the election for TV outlets to 349 | finally start calling out his bullshit. It took an act of terrorism to force social media&rsquo;s hand 350 | against the communities allowed to grow on their platforms.</p> 351 | <p>Extremist users are engaged users, and businesses know that. These companies don&rsquo;t stand for 352 | anything besides number of active users and ad impressions. They and their advertisers have way too 353 | much power. We&rsquo;re seeing now that social media companies have the power to do what politics couldn&rsquo;t 354 | do in 4 years, suppress Trump. This should be cause for some concern.</p> 355 | <p>We can&rsquo;t entrust media companies with the common good because their actions are moral-less and will 356 | always follow the shifting public sentiment which informs advertising. In the coming weeks some 357 | insurrectionists may be jailed and scapegoated. Maybe even the president himself. But don&rsquo;t forget 358 | about the profiteering that helped create them and how we allow it to continue.</p> 359 | 360 | 361 | 362 | 363 | Access Recent Files From the Command Line 364 | https://aduros.com/blog/access-recent-files-from-the-command-line/ 365 | Mon, 11 Jan 2021 22:08:04 +0000 366 | Bruno Garcia <b@aduros.com> 367 | https://aduros.com/blog/access-recent-files-from-the-command-line/ 368 | <p>(Day 11 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 369 | <p>If you don&rsquo;t already use <a href="https://github.com/rupa/z">Z</a> you absolutely need to check it out. Z is a 370 | CLI tool that cd&rsquo;s to a recently used directory, so that typing <code>z foo</code> will <code>cd /my/deeply/nested/project/foobar7</code>, as long as you&rsquo;ve cd&rsquo;ed into foobar7 sometime in the past.</p> 371 | <p>Z is great for jumping to recent directories&hellip; but is there an equivalent for opening recently used 372 | <em>files</em>? There&rsquo;s <a href="https://github.com/clvv/fasd">fasd</a>, but the way it works seems a bit too magical 373 | to me. It&rsquo;s also limited in that files are often opened from other programs, instead of directly 374 | from the shell.</p> 375 | <p>Every desktop environment since Windows 95 already has a &ldquo;Recent Files&rdquo; list for their file manager. 376 | On Linux, this is often handled by GTK. Conveniently, GTK provides a simple API for accessing the 377 | recent file list, and we can use it to write a small script that reads or writes to that list:</p> 378 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python3" data-lang="python3"><span style="color:#75715e">#!/usr/bin/env python3</span> 379 | <span style="color:#75715e">#</span> 380 | <span style="color:#75715e"># file-history: Read or write to GTK&#39;s recent file list.</span> 381 | 382 | <span style="color:#f92672">import</span> gi 383 | <span style="color:#f92672">import</span> os 384 | <span style="color:#f92672">import</span> re 385 | <span style="color:#f92672">import</span> sys 386 | 387 | gi<span style="color:#f92672">.</span>require_version(<span style="color:#e6db74">&#34;Gtk&#34;</span>, <span style="color:#e6db74">&#34;3.0&#34;</span>) 388 | <span style="color:#f92672">from</span> gi.repository <span style="color:#66d9ef">import</span> Gtk, Gio, GLib 389 | 390 | manager <span style="color:#f92672">=</span> Gtk<span style="color:#f92672">.</span>RecentManager<span style="color:#f92672">.</span>get_default() 391 | 392 | <span style="color:#66d9ef">if</span> len(sys<span style="color:#f92672">.</span>argv) <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">1</span>: 393 | <span style="color:#75715e"># Add the given files to the recent file list</span> 394 | <span style="color:#66d9ef">for</span> file <span style="color:#f92672">in</span> sys<span style="color:#f92672">.</span>argv[<span style="color:#ae81ff">1</span>:]: 395 | uri <span style="color:#f92672">=</span> Gio<span style="color:#f92672">.</span>File<span style="color:#f92672">.</span>new_for_path(file)<span style="color:#f92672">.</span>get_uri() 396 | manager<span style="color:#f92672">.</span>add_item(uri) 397 | GLib<span style="color:#f92672">.</span>idle_add(Gtk<span style="color:#f92672">.</span>main_quit) 398 | Gtk<span style="color:#f92672">.</span>main() 399 | 400 | <span style="color:#66d9ef">else</span>: 401 | <span style="color:#75715e"># Print the recent file list, starting with most recently used</span> 402 | home <span style="color:#f92672">=</span> re<span style="color:#f92672">.</span>compile(<span style="color:#e6db74">&#34;^&#34;</span><span style="color:#f92672">+</span>os<span style="color:#f92672">.</span>environ[<span style="color:#e6db74">&#34;HOME&#34;</span>]<span style="color:#f92672">+</span><span style="color:#e6db74">&#34;/&#34;</span>) 403 | <span style="color:#66d9ef">for</span> item <span style="color:#f92672">in</span> sorted(manager<span style="color:#f92672">.</span>get_items(), key<span style="color:#f92672">=</span><span style="color:#66d9ef">lambda</span> x: x<span style="color:#f92672">.</span>get_modified(), reverse<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>): 404 | <span style="color:#66d9ef">if</span> item<span style="color:#f92672">.</span>exists(): 405 | print(home<span style="color:#f92672">.</span>sub(<span style="color:#e6db74">&#34;~/&#34;</span>, item<span style="color:#f92672">.</span>get_uri_display())) 406 | </code></pre></div><p>Running <code>file-history</code> with arguments will add those files to the recent file list. Running 407 | <code>file-history</code> with no arguments will print the recent file list to stdout. Now we can go to town 408 | combining this with other scripts.</p> 409 | <h2 id="reading-history">Reading History</h2> 410 | <p>Let&rsquo;s write a key binding in zsh that displays the recent file history in an 411 | <a href="https://github.com/junegunn/fzf">fzf</a> menu.</p> 412 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-zsh" data-lang="zsh">_bind_recent <span style="color:#f92672">()</span> <span style="color:#f92672">{</span> 413 | local res<span style="color:#f92672">=</span><span style="color:#e6db74">`</span>file-history | fzf --reverse --height 40% --prompt <span style="color:#e6db74">&#34;Hist&gt; &#34;</span> | sed <span style="color:#e6db74">&#34;s|^~/|</span>$HOME<span style="color:#e6db74">/|&#34;</span><span style="color:#e6db74">`</span> 414 | <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$res<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span> 415 | LBUFFER<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$LBUFFER<span style="color:#e6db74"> </span><span style="color:#e6db74">${</span>(q)res<span style="color:#e6db74">}</span><span style="color:#e6db74"> &#34;</span> 416 | <span style="color:#66d9ef">fi</span> 417 | zle reset-prompt 418 | <span style="color:#f92672">}</span> 419 | zle -N _bind_recent 420 | bindkey <span style="color:#e6db74">&#39;^j&#39;</span> _bind_recent 421 | </code></pre></div><p>Now pressing ctrl-J will allow you to pick a recent file and add it to the end of the command line. 422 | I use this all the time for processing Firefox downloads. After downloading a zip or whatever, I 423 | just go to a terminal and type <code>unzip &lt;ctrl-J&gt;</code>. Since the list is sorted, the recently downloaded 424 | zip is the first item in the list and I just press enter to expand out to <code>unzip ~/Downloads/LongFilename.zip</code>.</p> 425 | <p>Another possibility is integrating with <a href="https://i3wm.org/">i3</a> or another window manager, so that 426 | pressing alt-J shows a recent file menu:</p> 427 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">bindsym $mod+j exec --no-startup-id file-history | rofi -dmenu -p Hist | 428 | sed &#34;s|^~/|$HOME/|&#34; | xargs -d &#39;\n&#39; xdg-open 429 | </code></pre></div><h2 id="writing-history">Writing History</h2> 430 | <p>The other half of this is populating the recent file history with useful data. Graphical GTK apps 431 | will already do this. Ideally we want every file we &ldquo;open&rdquo; from the CLI to be added to the history 432 | too.</p> 433 | <p>Some possibilities:</p> 434 | <ul> 435 | <li>For users of CLI file managers like <a href="https://github.com/gokcehan/lf">LF</a> or Ranger, call 436 | <code>file-history &lt;selected-file&gt;</code> as part of the file open handler.</li> 437 | <li>Write a shell alias that wraps <code>xdg-open</code> and calls <code>file-history</code>. I added this to my <a href="https://github.com/aduros/dotfiles/blob/master/home/bin/o">general 438 | purpose file opener</a>.</li> 439 | <li>Write a vim autocmd to sends all opened files to <code>file-history</code>. Personally I don&rsquo;t do this since 440 | vim already has an internal file history, but it might be useful for some.</li> 441 | </ul> 442 | <p>So far this setup has been working pretty nicely for me! Not quite as life-changing as Z, but I 443 | still use it on a daily basis.</p> 444 | 445 | 446 | 447 | 448 | XTerm: It's Better Than You Thought 449 | https://aduros.com/blog/xterm-its-better-than-you-thought/ 450 | Sun, 10 Jan 2021 19:29:19 +0000 451 | Bruno Garcia <b@aduros.com> 452 | https://aduros.com/blog/xterm-its-better-than-you-thought/ 453 | <p>(Day 10 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 454 | <p>A couple months back I switched my terminal from xfce4-terminal to the venerable xterm. For some 455 | reason I always put xterm in the same bucket as xclock, xmessage, or any other prehistoric command 456 | starting with X that comes pre-installed on any graphical Linux distribution.</p> 457 | <p>It was surprising to learn that xterm is still very much <a href="https://invisible-island.net/xterm/xterm.log.html">actively 458 | developed</a>. Even more surprisingly, it turns out 459 | xterm has <a href="https://lwn.net/Articles/751763/">incredibly low input latency</a> compared to modern 460 | terminals. This is easy to test at home, try typing in xterm compared to any other terminal and feel 461 | how much snappier it is.</p> 462 | <p>The lower latency alone is worth the price of admission in my opinion, so I went about configuring 463 | xterm as my default terminal. The configuration goes in <code>~/.Xresources</code> and you need to run <code>xrdb ~/.Xresources</code> after every change, or <a href="https://github.com/aduros/dotfiles/blob/eab476fc62e74e46cb41bb5c094cede7a28a014f/home/.config/nvim/options.vim#L27">make vim do 464 | it</a>.</p> 465 | <h2 id="basic-configuration">Basic Configuration</h2> 466 | <p>Here are some &ldquo;modern&rdquo; sensible defaults I ended up landing on:</p> 467 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">! Sensible defaults 468 | XTerm.vt100.locale: false 469 | XTerm.vt100.utf8: true 470 | XTerm.vt100.scrollTtyOutput: false 471 | XTerm.vt100.scrollKey: true 472 | XTerm.vt100.bellIsUrgent: true 473 | XTerm.vt100.metaSendsEscape: true 474 | </code></pre></div><p>And here are some visual styling options, not including colors:</p> 475 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">! Styling 476 | XTerm.vt100.faceName: DejaVu Sans Mono 477 | XTerm.vt100.boldMode: false 478 | XTerm.vt100.faceSize: 11 479 | XTerm.vt100.internalBorder: 16 480 | XTerm.borderWidth: 0 481 | </code></pre></div><p>XTerm supports key binding, but the syntax is non-obvious:</p> 482 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">XTerm.vt100.translations: #override \n\ 483 | Ctrl Shift &lt;Key&gt;N: scroll-back(1, halfpage) \n\ 484 | Ctrl Shift &lt;Key&gt;T: scroll-forw(1, halfpage) \n\ 485 | Ctrl Shift &lt;Key&gt;C: copy-selection(CLIPBOARD) \n\ 486 | Ctrl Shift &lt;Key&gt;V: insert-selection(CLIPBOARD) 487 | </code></pre></div><p>This allows copying and pasting to the clipboard (not just the X selection) with shift-ctrl-C and V. 488 | It also allows scrolling up and down with shift-ctrl-N and T (you can switch this to K and J to 489 | match vim keys in Qwerty).</p> 490 | <h2 id="url-handling">URL Handling</h2> 491 | <p>So now we have a pretty usable setup, but there&rsquo;s one more incredibly useful feature that was hard 492 | to figure out: opening URLs in the browser. We could of course select the URL and copy-paste, but 493 | there&rsquo;s a better way.</p> 494 | <p>XTerm has a configuration option called <code>printerCommand</code> which is a command that is piped all the 495 | text currently visible in the terminal. As the name suggests, it&rsquo;s meant to be used to implement 496 | printing to physical paper, but we can save the trees and hijack it to instead scan the screen for 497 | URLs and open the browser:</p> 498 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-sh" data-lang="sh"><span style="color:#75715e">#!/bin/sh -e 499 | </span><span style="color:#75715e"></span> 500 | grep -Eo <span style="color:#e6db74">&#39;\bhttps?://\S+\b&#39;</span> | 501 | uniq | 502 | ifne rofi -dmenu -i -p <span style="color:#e6db74">&#34;Open URL&#34;</span> -auto-select | 503 | xargs xdg-open 504 | </code></pre></div><p>This greps for URLs, removes consecutive duplicates with <code>uniq</code>, and displays a 505 | <a href="https://github.com/davatorium/rofi">rofi</a> menu to choose between them if there were multiple URLs. 506 | <code>ifne</code> is included in <a href="https://packages.debian.org/unstable/utils/moreutils">moreutils</a>. Put this 507 | script in an executable file called <code>select-url</code> in your <code>$PATH</code> and then add this to <code>.Xresources</code>:</p> 508 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">XTerm.vt100.printerCommand: select-url 509 | 510 | XTerm.vt100.translations: #override \n\ 511 | ... 512 | Ctrl Shift &lt;Key&gt;W: print(noAttrs, noNewLine) 513 | </code></pre></div><p>Now when you press shift-ctrl-W, any URL shown in the terminal will open in the browser. You don&rsquo;t 514 | have to select anything or use your mouse at all, nice!</p> 515 | <p>Someday it would be great to improve <code>select-url</code> to also scan for email addresses. Maybe during the 516 | next pandemic&hellip;</p> 517 | <h2 id="peeking-at-the-alternate-screen">Peeking at the Alternate Screen</h2> 518 | <p>Sometimes you open a fullscreen application like vim or a man page and you need to refer back to 519 | some text on the shell. Use this keybind to toggle back and forth:</p> 520 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">XTerm.vt100.translations: #override \n\ 521 | ... 522 | Ctrl Shift &lt;Key&gt;H: set-altscreen(toggle) 523 | </code></pre></div><p>You can even use it view a previously opened vim or man page after you close out of it!</p> 524 | <h2 id="opening-new-terminals-at-the-current-directory">Opening New Terminals at the Current Directory</h2> 525 | <p>There&rsquo;s a keybind action called <code>spawn-new-terminal()</code> that can be used for this, but even better is 526 | using <a href="https://github.com/schischi/xcwd">xcwd</a> to get the working directory of any currently focused 527 | window. Then you can put this in your i3 config for example:</p> 528 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">bindsym $mod+Return exec --no-startup-id cd &#34;`xcwd`&#34; &amp;&amp; xterm 529 | </code></pre></div><h2 id="wish-list">Wish List</h2> 530 | <p>XTerm is missing a few small features:</p> 531 | <ul> 532 | <li>Text reflow when the terminal is resized.</li> 533 | <li>Fallback fonts don&rsquo;t seem to always work. Maybe I&rsquo;m missing a config option?</li> 534 | <li>Transparency not natively supported. I don&rsquo;t care about transparency but maybe it&rsquo;s important to 535 | some people.</li> 536 | <li>Occasionally strange flickering with <a href="https://github.com/ibhagwan/picom">picom</a>, possibly a bug 537 | with picom?</li> 538 | </ul> 539 | <p>In the end this wasn&rsquo;t enough to stop me from using xterm, but the lack of text reflow still irks me 540 | from time to time. Overall, I&rsquo;ve been pleasantly surprised with xterm after taking the time to 541 | configure it.</p> 542 | 543 | 544 | 545 | 546 | Remote Movie Nights With Popcord 547 | https://aduros.com/blog/remote-movie-nights-with-popcord/ 548 | Sat, 09 Jan 2021 19:30:50 +0000 549 | Bruno Garcia <b@aduros.com> 550 | https://aduros.com/blog/remote-movie-nights-with-popcord/ 551 | <p>(Day 9 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 552 | <p>Several months ago during the height of the 2020 quarantine I released 553 | <a href="https://popcord.aduros.com">Popcord</a>, a Chrome extension for watching videos with friends remotely. 554 | It&rsquo;s designed to be used while on voice chat with friends and family, and simply handles 555 | synchronizing the playback position and state of the movie you&rsquo;re all watching together. I&rsquo;m pretty 556 | proud of how it turned out!</p> 557 | <div style="text-align: center"> 558 | <img src="screenshot.png" class="shadow"> 559 | </div> 560 | 561 | <p>Under the hood the system is made up of 3 different pieces: The browser extension, a web application, 562 | and a websocket server.</p> 563 | <h2 id="browser-extension">Browser Extension</h2> 564 | <p>This is the actual extension downloaded from the Chrome web store. It has code for interacting with 565 | the HTML5 <code>&lt;video&gt;</code> elements on the page, and the minimal UI popup with the button to connect to the 566 | websocket server. It also exposes API hooks to invite links hosted by the web application hosted at 567 | <a href="https://popcord.aduros.com">https://popcord.aduros.com</a>.</p> 568 | <p>I&rsquo;d like to port a version to 569 | <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions">Firefox</a> someday, but I 570 | never got around to it.</p> 571 | <h2 id="web-application">Web Application</h2> 572 | <p>This is an Express webapp written in node that serves the invite URLs that look like this: 573 | <a href="https://popcord.aduros.com/invite/h5fryqv2sk?u=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DRRNanbmD1xk">https://popcord.aduros.com/invite/h5fryqv2sk?u=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DRRNanbmD1xk</a>. 574 | Those pages also contain a bit of JS that talks to the browser extension to prepare the redirect, 575 | such as checking for the presence of the extension and prompting for permissions to run on the 576 | destination domain (in this example youtube.com).</p> 577 | <p>There&rsquo;s no database or persistence of any kind, since all the needed state is contained in the 578 | invite URLs: Both the final destination URL, and the room token.</p> 579 | <h2 id="websocket-server">Websocket Server</h2> 580 | <p>Once the invite link is clicked through, the extension goes to the destination URL and connects to 581 | the websocket server using the given room token. The websocket server is just a simple controller 582 | that groups up clients by room token and allows them to relay messages to each other. When one 583 | client sends a command like &ldquo;pause the video&rdquo; the server broadcasts it to all other clients in the 584 | same room.</p> 585 | <h2 id="hosting">Hosting</h2> 586 | <p>Both the webapp and websocket server are deployed as systemd services running on the same EC2 587 | machine that hosts this blog, though they could be separated in the future should the need arise. 588 | They both run isolated in <a href="https://firejail.wordpress.com/">firejail</a> sandboxes for security.</p> 589 | <p><a href="https://popcord.aduros.com">Try Popcord</a> the next time you&rsquo;re away from home or want to watch a 590 | movie with friends far away! If you find an issue, please report it on the <a href="https://github.com/aduros/popcord">Github 591 | repository</a> or email me.</p> 592 | 593 | 594 | 595 | 596 | What's My IP Address? 597 | https://aduros.com/blog/whats-my-ip-address/ 598 | Fri, 08 Jan 2021 20:15:22 +0000 599 | Bruno Garcia <b@aduros.com> 600 | https://aduros.com/blog/whats-my-ip-address/ 601 | <p>(Day 8 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 602 | <p>Sometimes you need to determine your own IP address. There&rsquo;s <code>ip addr</code> (the new <code>ifconfig</code>), which 603 | will tell you your local network IP. What about your external Internet-addressible IP? Use this:</p> 604 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-sh" data-lang="sh">dig @resolver4.opendns.com myip.opendns.com +short 605 | </code></pre></div><p>This is faster than <code>curl icanhazip.com</code> or equivalent URLs, and more reliable too.</p> 606 | 607 | 608 | 609 | 610 | Implementing a Slow Life 611 | https://aduros.com/blog/implementing-a-slow-life/ 612 | Thu, 07 Jan 2021 15:45:28 +0000 613 | Bruno Garcia <b@aduros.com> 614 | https://aduros.com/blog/implementing-a-slow-life/ 615 | <p>(Day 7 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 616 | <p>There&rsquo;s a <a href="https://www.theguardian.com/science/2014/jul/03/electric-shock-preferable-to-thinking-says-study">famous 617 | experiment</a> 618 | where subjects (typically poor university students) are left alone in a small, empty room with blank 619 | walls and no windows. They are asked to sit at a table and &ldquo;entertain themselves with their own 620 | thoughts&rdquo; for 15 minutes. No music, no phone, no TV. On the table is a button that applies a mild 621 | electric shock. It turns out, many of us will press that button, often multiple times. Why?</p> 622 | <p>Like with any psychological experiment, we shouldn&rsquo;t be too quick to come to conclusions. The 623 | conclusion here being that sometimes we prefer pain to boredom. Speaking personally however, 624 | I consider this to be true. I&rsquo;d probably hit that button (at least at first out of curiosity, and 625 | probably afterwards out of boredom). I certainly hit that figurative button multiple times a day 626 | whenever I&rsquo;m bored at home.</p> 627 | <p>In the same way that eating broccoli is delicious, but totally boring to someone who has had their 628 | level of taste stimulation calibrated to sugary and salty foods, I&rsquo;m considering the ways my own 629 | mental stimulation is over-calibrated to junk.</p> 630 | <p>I don&rsquo;t think there&rsquo;s anything wrong with enjoying junk food, or being highly stimulated. Only that 631 | perhaps it shouldn&rsquo;t be one&rsquo;s default state of being.</p> 632 | <p>Let&rsquo;s talk about 3 small things I think should be enjoyed responsibly in a Slow Life: Multi-tasking, 633 | social media, and cars.</p> 634 | <h2 id="multi-tasking">Multi-tasking</h2> 635 | <p>Some people say that multi-tasking is a myth, but I&rsquo;ve met many people who can do it quite well. 636 | They never do it by <em>actually</em> performing multiple tasks simultaneously though. Multi-tasking works 637 | by rapidly switching focus from task to task. When you train multi-tasking you&rsquo;re really training to 638 | reduce the cost of all that context switching. This mode of thinking can be useful, but it can also 639 | be over-trained and isn&rsquo;t very compatible with boredom.</p> 640 | <h2 id="social-media">Social media</h2> 641 | <p>There&rsquo;s absolutely nothing wrong with using social media, having a social media presence, and 642 | talking to others on social media. It&rsquo;s still junk food. Treat it more like enjoying a glass of wine 643 | (or your drug of choice) in the evening, and less like needing to get drunk throughout the day.</p> 644 | <h2 id="cars">Cars</h2> 645 | <p>Ok, so here&rsquo;s where I go off the rails a bit. I cannot stand cars. Whether I&rsquo;m riding in a car, or 646 | walking on the street next to passing cars, or sitting somewhere and hearing traffic, for some 647 | reason cars keep me mentally stimulated juuust enough to be draining. I&rsquo;m not even talking about the 648 | ecological impact of cars, which is also important. It&rsquo;s something deeper&hellip; possibly the prevalence 649 | of cars combined with how much of modern life is involuntarily structured around them. As soon as I 650 | can better articulate my car beef I may write another post about it. Anyways, drive cars, but don&rsquo;t 651 | live in or around them.</p> 652 | 653 | 654 | 655 | 656 | Generating Strong Passwords 657 | https://aduros.com/blog/generating-strong-passwords/ 658 | Wed, 06 Jan 2021 22:02:26 +0000 659 | Bruno Garcia <b@aduros.com> 660 | https://aduros.com/blog/generating-strong-passwords/ 661 | <p>(Day 6 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 662 | <p>APG is a command line program for generating random passwords. On Ubuntu/Debian it can be installed 663 | with <code>sudo apt install apg</code>.</p> 664 | <p>Here are the flags I use with it:</p> 665 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">apg -a <span style="color:#ae81ff">1</span> -n <span style="color:#ae81ff">1</span> -m <span style="color:#ae81ff">25</span> -x <span style="color:#ae81ff">30</span> -M SNCL -d -E <span style="color:#e6db74">&#34;&#39;\&#34;\`\\&#34;</span> 666 | </code></pre></div><p><code>-a 1</code>: Specifies to use true random instead of the default mode of pronounceable passwords. The 667 | assumption is that these passwords are going to be remembered by a password manager and not a brain.</p> 668 | <p><code>-n 1</code>: Only generate one password, by default apg will generate multiple.</p> 669 | <p><code>-m 25 -x 30</code>: Generate a password between 25 and 30 characters long. Probably overkill, but I love 670 | me some entropy.</p> 671 | <p><code>-M SNCL</code>: Guarantees the password will contain at least one symbol, number, capital, and lowercase 672 | character. Handy for websites that enforce passwords with these characters.</p> 673 | <p><code>-E &quot;'\&quot;\`\\&quot;</code>: Prevents the password from containing quotes and backslashes, meaning passwords 674 | are always safe to paste into a string literal in source code.</p> 675 | <p>The output of this command can be piped into <code>xsel</code> to copy it to the clipboard. See 676 | <a href="https://github.com/aduros/dotfiles/blob/eab476fc62e74e46cb41bb5c094cede7a28a014f/home/bin/generate-password">generate-password</a> 677 | for what that looks like.</p> 678 | <p>Note: apg hasn&rsquo;t been updated since 2003 and its creator has disappeared from the Internet. If 679 | there&rsquo;s some other password generator I should be using do <a href="mailto:b@aduros.com">let me know</a>, but I 680 | haven&rsquo;t found anything that matches apg&rsquo;s features. Although perhaps using software that&rsquo;s 681 | &ldquo;finished&rdquo; and built to last isn&rsquo;t necessarily a bad thing.</p> 682 | 683 | 684 | 685 | 686 | Hacking i3: Window Swallowing 687 | https://aduros.com/blog/hacking-i3-window-swallowing/ 688 | Tue, 05 Jan 2021 21:54:40 +0000 689 | Bruno Garcia <b@aduros.com> 690 | https://aduros.com/blog/hacking-i3-window-swallowing/ 691 | <p>(Day 5 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 692 | <p>Tiling window managers are a big reason why I use Linux. Not having to manually drag windows around 693 | is absolutely game changing, especially on a laptop with a small screen.</p> 694 | <p>Tiling WMs work by resizing other windows when a new window is opened, so that no windows ever 695 | overlap. Most of the time this is perfect, but there&rsquo;s a certain case where it&rsquo;s not so great, which 696 | is when opening a GUI application from a terminal to quickly view a file:</p> 697 | <video width="100%" loop muted autoplay playsinline class="shadow"> 698 | <source src="before.mp4" type="video/mp4"> 699 | <source src="before.webm" type="video/webm"> 700 | <p>(The video could not be played)</p> 701 | </video> 702 | 703 | <p>Notice how the original terminal that opened <a href="https://github.com/muennich/sxiv">sxiv</a> (a bad name 704 | for a good image viewer) remains open, taking up space and not showing anything useful. Even worse, 705 | we can&rsquo;t close that terminal without also killing the image window.</p> 706 | <p>In this case what you really want is to have the image window &ldquo;on top&rdquo; of the terminal window, 707 | hiding it until it closes. I think <a href="https://dwm.suckless.org/">DWM</a> was the first to implement this 708 | type of feature, calling it &ldquo;window swallowing&rdquo;. I use <a href="https://i3wm.org/">i3</a>, so I tried some 709 | other solutions like <a href="https://github.com/jamesofarrell/i3-swallow">i3-swallow</a> and 710 | <a href="https://github.com/salman-abedin/devour">devour</a>. Unfortunately nothing I found really worked 100%, 711 | having layout flickering or restoring the terminal window to the wrong spot.</p> 712 | <p>It occurred to me that i3 has a feature which I never use that could work here: tabs. So I wrote a 713 | script which creates a temporary tab layout on the current window in a way that replicates 714 | &ldquo;swallowing&rdquo;. No flickering or jank. As a small bonus, you can still view the terminal window if 715 | needed by switching tabs.</p> 716 | <p>The final step is hiding the i3 tab buttons in the config. This is optional but I find i3 tab 717 | buttons pretty ugly so I turn them off by putting <code>font pango:mono 0</code> in the config. Unfortunately 718 | this is a dirty hack as setting the font size seems to be the only way to shrink the tab bar. This 719 | is the same font that i3 uses to display error messages, so don&rsquo;t mess up! Or comment it out if you 720 | need to see errors.</p> 721 | <p>Here&rsquo;s the final result:</p> 722 | <video width="100%" loop muted autoplay playsinline class="shadow"> 723 | <source src="after.mp4" type="video/mp4"> 724 | <source src="after.webm" type="video/webm"> 725 | <p>(The video could not be played)</p> 726 | </video> 727 | 728 | <p>Beautiful 😍 All you need to do is prefix a command with <code>i3-tabbed</code> to let it do its thing. This 729 | can be made automatic by using a shell alias: <code>alias sxiv=&quot;i3-tabbed sxiv&quot;</code>.</p> 730 | <p>If you want to <a href="https://github.com/aduros/dotfiles/blob/master/home/bin/i3-tabbed">grab the script</a>, 731 | make sure you have python3 and i3ipc which can be installed with <code>pip install i3ipc</code>.</p> 732 | 733 | 734 | 735 | 736 | TODO-based Development 737 | https://aduros.com/blog/todo-based-development/ 738 | Mon, 04 Jan 2021 14:06:31 +0000 739 | Bruno Garcia <b@aduros.com> 740 | https://aduros.com/blog/todo-based-development/ 741 | <p>(Day 4 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 742 | <p>I use TODO comments a lot when working on small projects. Projects where the &ldquo;issue tracker&rdquo; is just 743 | a text file in my editor, and mainly to stub out empty methods that I&rsquo;ll implement later. TODOs are 744 | easy to insert, and they move with the source code. The main drawback is that they usually get lost 745 | and end up as bitrot.</p> 746 | <p>Keeping track of TODOs by searching the codebase is pretty straightforward. It can even be 747 | integrated nicely in vim:</p> 748 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-sh" data-lang="sh">rg --vimgrep --only-matching <span style="color:#e6db74">&#39;\b(TODO|FIXME)[:;.,]? *(.*)&#39;</span> --replace <span style="color:#e6db74">&#39;$2&#39;</span> 749 | </code></pre></div><p>That will present all comments of the form <code>// TODO: fix Y2K bug</code> in vim&rsquo;s quickfix list.</p> 750 | <p>Even better is if we can sort the TODOs by date:</p> 751 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-sh" data-lang="sh">rg --vimgrep --only-matching <span style="color:#e6db74">&#39;\b(TODO|FIXME)\s*\(([\d-]*?)\)[:;.,]? *(.*)&#39;</span> <span style="color:#ae81ff">\ 752 | </span><span style="color:#ae81ff"></span> --replace <span style="color:#e6db74">&#39; $2 | $3&#39;</span> -- <span style="color:#e6db74">&#34;</span>$@<span style="color:#e6db74">&#34;</span> | sort --field-separator <span style="color:#e6db74">&#34;:&#34;</span> --key <span style="color:#ae81ff">4</span> --reverse 753 | </code></pre></div><p>This requires adding the date inside the comments like this: <code>// TODO(2021-01-04): Implement foobar</code>. Inserting the date can be made automatic in vim by using an abbreviation:</p> 754 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-vim" data-lang="vim"><span style="color:#a6e22e">inoreabbrev</span> &lt;<span style="color:#a6e22e">expr</span>&gt; <span style="color:#a6e22e">TODO</span> <span style="color:#e6db74">&#34;TODO(&#34;</span>.<span style="color:#a6e22e">strftime</span>(<span style="color:#e6db74">&#34;%F&#34;</span>).<span style="color:#e6db74">&#34;)&#34;</span><span style="color:#960050;background-color:#1e0010"> 755 | </span></code></pre></div><p>Then whenever you type <code>TODO</code> in insert mode it expands to include the date. No plugins required! If 756 | you found this interesting check out the full <a href="https://github.com/aduros/dotfiles/blob/eab476fc62e74e46cb41bb5c094cede7a28a014f/home/.config/nvim/init.vim#L291">vim 757 | config</a> 758 | and the simple <a href="https://github.com/aduros/dotfiles/blob/eab476fc62e74e46cb41bb5c094cede7a28a014f/home/bin/todo">todo 759 | script</a> 760 | it relies on.</p> 761 | 762 | 763 | 764 | 765 | Experiments With Rockstar 766 | https://aduros.com/blog/experiments-with-rockstar/ 767 | Sun, 03 Jan 2021 17:24:10 +0000 768 | Bruno Garcia <b@aduros.com> 769 | https://aduros.com/blog/experiments-with-rockstar/ 770 | <p>(Day 3 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 771 | <p>I recently stumbled across a toy programming language called <a href="https://codewithrockstar.com">Rockstar</a>, 772 | in which programs are also heavy metal lyrics. Basically COBOL for metalheads.</p> 773 | <p>The syntax can be almost completely poetic, even with programming features with numeric literals, 774 | functions, arrays, and arithmetic hidden in its text.</p> 775 | <p>The grand-daddy of esoteric programming languages is 776 | <a href="https://en.wikipedia.org/wiki/Brainfuck">Brainfuck</a>, in which programs have only 8 commands and are 777 | almost completely unreadable.</p> 778 | <p>Thanks to copious amounts of Covid-related free time, I present the world&rsquo;s most useless program: A 779 | Rockstar program that interprets and executes a Brainfuck program. The <a href="https://github.com/aduros/brainrock/blob/master/interpreter-debug.rock">&ldquo;debug&rdquo; 780 | version</a> tries to be more 781 | readable, but here&rsquo;s the &ldquo;optimized&rdquo; version in its 100 lines of glory/horror:</p> 782 | <div class="highlight"><pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-text" data-lang="text">Brainfuck was deceiving a 783 | knife was decorated red 784 | A killer is on every earth 785 | You were I 786 | Life was loneliness 787 | Let freedom be life without you 788 | Rock steadily 789 | 790 | Rock the memories 791 | Let my heart be life 792 | Listen to the drums 793 | Let your song be life 794 | 795 | Unite steadily into the earth 796 | 797 | Heaven takes the virtuous 798 | If the virtuous is a killer 799 | Give back life 800 | 801 | Give back the virtuous with you 802 | 803 | Hell takes rock 804 | If rock is life 805 | Give back a killer 806 | 807 | Give back rock with freedom 808 | 809 | My brain takes dreaming and waking 810 | Cast waking into darkness 811 | Give back darkness is dreaming 812 | 813 | My search takes your hands 814 | Put your hands into mine 815 | While mine 816 | Let your song be with your hands 817 | Let flight be the drums at your song 818 | If my brain taking flight, brainfuck 819 | Build mine up 820 | 821 | If my brain taking flight, knife 822 | Knock mine down 823 | 824 | 825 | 826 | Until nothing 827 | Let drugs be the drums at your song 828 | If drugs ain&#39;t right 829 | Break it down 830 | 831 | Let time be the memories at my heart 832 | If time is nothing 833 | Let time be life 834 | 835 | If my brain taking drugs, brainfuck and time is life 836 | My search taking you 837 | Take it to the top 838 | 839 | If my brain taking drugs, knife and time 840 | My search taking freedom 841 | Take it to the top 842 | 843 | Dire was bloody battle-cry 844 | If my brain taking drugs, dire 845 | Knock my heart down 846 | 847 | Softly was snowed in 848 | If my brain taking drugs, softly 849 | Build my heart up 850 | 851 | Forgetting was half way 852 | If my brain taking drugs, forgetting 853 | Let the memories at my heart be Heaven taking time 854 | 855 | Terror is this virus 856 | If my brain taking drugs, terror 857 | Let the memories at my heart be Hell taking time 858 | 859 | Regretful was your demise 860 | If my brain taking drugs, regretful 861 | Real is a retrospect 862 | If time is real 863 | Scream the earth 864 | Unite steadily into the earth 865 | 866 | If time ain&#39;t real 867 | Cast time into doubt 868 | Let the earth be with doubt 869 | 870 | 871 | Build your song up 872 | 873 | If the earth ain&#39;t nothing 874 | Whisper the earth... 875 | </code></pre></div><p>You can paste this monstrosity into the <a href="https://codewithrockstar.com/online">online Rockstar</a> 876 | interpreter, along with any Brainfuck program into the Input field. For example, Hello World: 877 | <code>++++++++[&gt;++++[&gt;++&gt;+++&gt;+++&gt;+&lt;&lt;&lt;&lt;-]&gt;+&gt;+&gt;-&gt;&gt;+[&lt;]&lt;-]&gt;&gt;.&gt;---.+++++++..+++.&gt;&gt;.&lt;-.&lt;.+++.------.--------.&gt;&gt;+.&gt;++.</code></p> 878 | <p>In theory it should be compatible with most Brainfuck programs, unlike the <a href="https://github.com/aduros/trollcat">last interpreter I 879 | wrote</a>. The Brainfuck comma instruction for character input 880 | isn&rsquo;t supported though. It might be possible by buffering a line of text and reading off the first 881 | character, but I&rsquo;ve already spent way too much time on this thing.</p> 882 | <p>Anyways, now I can put &ldquo;rockstar developer&rdquo; on my resume and confuse recruiters.</p> 883 | 884 | 885 | 886 | 887 | Final Thoughts on Flash 888 | https://aduros.com/blog/final-thoughts-on-flash/ 889 | Sat, 02 Jan 2021 12:10:09 +0000 890 | Bruno Garcia <b@aduros.com> 891 | https://aduros.com/blog/final-thoughts-on-flash/ 892 | <p>(Day 2 of <a href="../30-days-of-blogging">30 Days of Blogging</a>)</p> 893 | <p>On December 31 2020, Flash was finally, officially, 100% for real this time, discontinued. It&rsquo;s 894 | already been dead for ages of course, but damned if that won&rsquo;t stop me from adding my own hot take 895 | about Flash. I was involved with Flash game development from around 2008-2014 or so. I&rsquo;d like to 896 | note a few things I think the platform did well, and not so well.</p> 897 | <h1 id="things-flash-did-well">Things Flash did well</h1> 898 | <p><strong>Easily archivable</strong>. The .swf format contained all the code and assets for the application to run. 899 | Back in the day this made it really easy to host and share (and pirate) games. Today it also makes 900 | it easy to archive Flash games for historical purposes. When it comes to the web and appstore 901 | platforms, long-term availability is more iffy. Either games live as a mash of files on a single web 902 | server which will eventually go offline, or they only run on a certain version of phone which will 903 | be obsolete in a couple of years.</p> 904 | <p><strong>Backwards compatibility</strong>. In retrospect it was remarkable how Flash never had any breaking 905 | changes between versions. Old SWFs that were authored in the first ancient versions of Flash still 906 | work to this day. By comparison, pretty much all of <a href="https://aduros.com/games/">my early HTML5 907 | games</a> are broken today.</p> 908 | <p><strong>Syndication as a revenue model</strong> for small developers. The Flash game industry was decentralized. 909 | Though there were large game hubs like Miniclip, there were thousands of smaller Flash game sites. 910 | As a Flash game developer, you could license your game to a publisher to syndicate on their network 911 | of sites. You could sell licenses to multiple publishers. It wouldn&rsquo;t make you fabulously rich, but 912 | it was safe income that could fund a solo project or even a small team. Today there&rsquo;s probably like 913 | 2-3 hubs that your game can viably run on. The income potential in the top 1% is bonkers high, 914 | but if you&rsquo;re in the 99%, you get nothing.</p> 915 | <p><strong>It was weird</strong>. As a consequence of the platform&rsquo;s appeal to small developers, there was a lot of 916 | weird shit created on it. We still have experimental stuff on the web, but most of it is in the form 917 | of videos and images and non-interactive. It seems like the interactive weirdness has largely moved 918 | off the web platform and onto modding-friendly sandbox games like Roblox, VRChat, Minecraft, etc.</p> 919 | <h1 id="things-flash-did-poorly">Things Flash did poorly</h1> 920 | <p><strong>There were bugs</strong>. Oh man was it ever buggy, browsers crashed all the time. In hindsight this may 921 | have been as much Flash player&rsquo;s fault as it was the plugin API that browsers used to embed the 922 | Flash player. Either way it was hell.</p> 923 | <p><strong>The name &ldquo;Flash&rdquo; was overloaded</strong>. It was confusing that the name &ldquo;Flash&rdquo; was simultaneously used 924 | for (a) The Flash authoring environment (b) The Flash player runtime (c) The Flash browser plugin. 925 | Adobe tied their entire ecosystem around the Flash brand, but that backfired spectacularly once 926 | &ldquo;Flash&rdquo; started to become a taboo word. Adobe eventually renamed Flash-the-authoring-environment to 927 | Animate, but it was way too late.</p> 928 | <p><strong>Developer tools or lack thereof</strong>. Flash was very much a designer-oriented product, and their 929 | offering to developers who just wanted to do everything in a text editor was really lacking. Entire 930 | home-grown <a href="https://haxe.org">software projects</a> sprung up trying to make Flash more 931 | developer-friendly, that were constantly at odds with Adobe&rsquo;s designer-focused vision of doing 932 | things. As a developer this irked me, but in the post-Flash web stack perhaps things have swung too 933 | far in the direction of doing everything in a text editor. We have a million JS frameworks and tools 934 | for building applications with text but we no longer have the equivalent of a 935 | Flash-the-authoring-environment for bringing animations into those applications.</p> 936 | <p><strong>The Flash player should have been open sourced</strong>. I actually don&rsquo;t know if this would have made a 937 | difference, though it certainly would have helped fix my other gripes. I believe that Adobe could 938 | have made it viable since all their revenue came from Flash-the-authoring-environment anyways. 939 | There would still be value in an open source Flash player today for archival purposes. Adobe plz, I 940 | just want to be able to play Bloons TD when I&rsquo;m 85 on my iWindowsX 128-bit VR tablet.</p> 941 | 942 | 943 | 944 | 945 | 30 Days of Blogging 946 | https://aduros.com/blog/30-days-of-blogging/ 947 | Fri, 01 Jan 2021 18:35:32 +0000 948 | Bruno Garcia <b@aduros.com> 949 | https://aduros.com/blog/30-days-of-blogging/ 950 | <p>Happy new year!</p> 951 | <p>I&rsquo;ve had this domain kicking around for ages and have always wanted to do something more with it. 952 | So, I&rsquo;m making a commitment to write a blog post every single day for this month. This won&rsquo;t be easy 953 | for me. I do not write. Hell, most days I can barely even <em>read</em>. I worry about writing about the 954 | wrong thing, or too little, or too much. But I&rsquo;m also stubborn, so for the next 30 days we&rsquo;re just 955 | going to dive in.</p> 956 | <p>The past several months of the COVID quarantine has given me plenty of time to think, and time to 957 | work on side projects which will be interesting to write about here. That should fill about 2 weeks 958 | worth of posts. After that&hellip; well, let&rsquo;s find out.</p> 959 | <p>So here we go, new year, new blog. I haven&rsquo;t yet decided what I enjoy writing about, but I expect 960 | the posts will be mostly about tech, but also occasionally about travel, circus arts, spirituality. 961 | Really anything but politics.</p> 962 | 963 | 964 | 965 | 966 | 967 | --------------------------------------------------------------------------------