◈ Netsurfer / perl.gg / Bionic Reader

Bionic Reader

2024-07-02

This page walks through a fifty-eight character Perl one-liner that bolds the first forty percent of each word to mimic bionic reading. You will see how Perl splits input on spaces, counts characters, and wraps the result in terminal escape codes.

Whether the reading speed claim holds up is still up for debate, but building it in Perl is a clean exercise in text processing.

perl -040pE'$l=($l=y///c)>2?int$l*.4:1;s~.{$l}~\033[1m$&\033[0m~' file.txt
Fifty-eight characters. The script reads any plain text file and prints the words back to your terminal with the leading characters bolded.

The output looks like this (bold shown in brackets for clarity):

[Th]e [su]n [wa]s [shi]ning [bri]ghtly [as] [Ali]ce [wal]ked...
Your eyes lock onto the bold prefix, and your brain fills in the rest of the word. It might be a neat parlor trick, but the underlying mechanics are solid.

Part 1: THE SWITCHES

perl -040pE
Three command line flags handle the heavy lifting here:
SWITCH WHAT IT DOES ------ -------------------------------------------------- -0 Set the input record separator (what splits input) 40 Octal 40 = ASCII 32 = space character -p Read input, run code, print result (implicit loop) -E Enable modern features (like say)
The -040 flag is the clever part. Perl normally reads text line by line, which means it splits the input on newlines. Setting -0 to octal 40 changes the split point to a space character.

Each "line" that -p processes is actually a single word.

Part 2: THE LENGTH CALCULATION

$l=($l=y///c)>2?int$l*.4:1
This line packs a lot of logic into a small space. Let's untangle it from the inside out:
PIECE WHAT IT DOES ----------- -------------------------------------------- y///c Count characters in $_ $l=y///c Store the count in $l ($l=...)>2 Is the length greater than 2? int$l*.4 Yes: take 40% of length (truncated) 1 No: just use 1 $l=... Store final result back in $l
The y///c trick deserves its own explanation.

Part 3: THE COUNTING TRICK

y///c
The y/// operator (also called tr///) normally performs transliteration. With empty search and replace lists plus the /c flag, it becomes a character counter.
y/abc/xyz/ Replace a->x, b->y, c->z y/// Replace nothing with nothing (no-op) y///c Count how many "nothings" matched (all chars)
When both lists are empty, every character "matches." The /c flag returns the total count instead of performing a replacement.

It is the shortest way to get string length in a one-liner:

y///c # Returns length of $_ length # Same thing, more readable, more typing
In code golf, y///c wins.
.--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/

Part 4: THE TERNARY LOGIC

($l=y///c)>2 ? int$l*.4 : 1
A word by word breakdown:
IF the word is longer than 2 characters Bold 40% of it (rounded down) ELSE Bold just 1 character
Why the threshold? Short words like "a" or "be" do not need the bionic treatment. One bold character is enough for them. Longer words get proportional bolding.
WORD LENGTH 40% BOLD CHARS ------ ------ ---- ---------- a 1 0.4 1 (minimum) be 2 0.8 1 (minimum) the 3 1.2 1 word 4 1.6 1 hello 5 2.0 2 reading 7 2.8 2 beautiful 9 3.6 3
The int() function truncates the decimal. No rounding happens, just a clean chop.

Part 5: THE SUBSTITUTION

s~.{$l}~\033[1m$&\033[0m~
This wraps the first $l characters in ANSI bold codes:
PIECE WHAT IT DOES ----------- -------------------------------------------- s~~~ Substitution (~ delimiters) .{$l} Match exactly $l characters (any char) \033[1m ANSI escape: turn bold ON $& The matched text (first $l chars) \033[0m ANSI escape: reset formatting
The .{ $l } quantifier is the key. It matches exactly as many characters as we calculated. It is not greedy, and it is not minimal. It just matches that exact count.

Part 6: ANSI ESCAPE CODES

\033[1m Bold on \033[0m Reset (all formatting off)
These are terminal control sequences. \033 is the escape character (octal 33 equals decimal 27 equals ESC).

When your terminal sees ESC[1m, it starts rendering bold text. When it sees ESC[0m, it returns to normal formatting.

Other codes you might use:

\033[4m Underline \033[7m Reverse video \033[31m Red text \033[32m Green text \033[1;31m Bold red
The bionic reader uses just bold, but you could get creative with the rest.

Part 7: PUTTING IT TOGETHER

Let's trace through the word "reading":
INPUT: "reading " (note trailing space from -040) STEP 1: y///c counts 8 characters STEP 2: 8 > 2, so $l = int(8 * 0.4) = 3 STEP 3: s~.{3}~ matches "rea" STEP 4: Replacement wraps it: \033[1mrea\033[0m STEP 5: -p prints: "\033[1mrea\033[0mding " OUTPUT: "reading " with "rea" bold
The space gets included in the count (8 characters instead of 7), but it does not affect the visual much. Whitespace in bold looks the same as normal whitespace.

Part 8: THE EXPANDED VERSION

#!/usr/bin/env perl use feature qw|say|; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Set word-by-word input mode # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # $/ is input record separator # Space character makes each "line" a word $/ = ' '; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Process each word # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ while (<>) { # Count characters in current word my $length = y///c; # Calculate how many chars to bold # 40% for words > 2 chars, else just 1 my $bold_count = $length > 2 ? int($length * 0.4) : 1; # Wrap first N characters in ANSI bold s~.{$bold_count}~\033[1m$&\033[0m~; # Print the modified word print; }
The same logic runs here, but it takes twenty times the characters. The one-liner still wins on density.

Part 9: VARIATIONS

Bold 50% instead of 40%:
perl -040pE'$l=($l=y///c)>2?int$l*.5:1;s~.{$l}~\033[1m$&\033[0m~' file
Underline instead of bold:
perl -040pE'$l=($l=y///c)>2?int$l*.4:1;s~.{$l}~\033[4m$&\033[0m~' file
Colored (red) instead of bold:
perl -040pE'$l=($l=y///c)>2?int$l*.4:1;s~.{$l}~\033[31m$&\033[0m~' file
HTML output instead of terminal:
perl -040pE'$l=($l=y///c)>2?int$l*.4:1;s~.{$l}~<b>$&</b>~' file
That last one generates HTML you can paste anywhere.

Part 10: DOES IT ACTUALLY WORK?

Bionic reading is still controversial. Some studies show improvement, while others show no measurable difference. Your mileage will vary.

But as a Perl exercise, it covers a lot of ground:

* -0 flag for custom record separators * y///c as a character counter * Nested assignment in a ternary * **Dynamic quantifiers** in regex * ANSI escape sequences * The -p implicit loop
All of that fits into fifty-eight characters.

Whether it helps you read faster is beside the point. It definitely helps you appreciate Perl's density.

_____ | | | TXT | |_____| | V .-----. / \ | PERL | \ / '-----' | V _______ | | | [BI]O | | [NI]C | |_______|
perl.gg
✓ Document: Doneone-liners / 12.8 KiB▧