Complete Development Guide · ChromeOS Linux

Perl on a Chromebook

Full Perl 5 development — CLI tools, GUI apps, and web services — running natively in the ChromeOS Linux container.

ChromeOS Linux (Crostini) Debian Bookworm Perl/Tk · Gtk3 · Mojolicious GUI via XWayland — no extra setup Termux Android option Cloud IDEs
§01

Overview & Your Options

A modern Chromebook is a capable Perl development machine. ChromeOS ships with a built-in Debian Linux container that runs Perl, all CPAN modules, GUI toolkits, and web servers — with GUI windows appearing natively as ChromeOS windows via XWayland. No virtual machine manager, no separate X11 server, no special setup is required.

Linux Container
Crostini · Debian Bookworm
The best option. Full Perl 5, apt packages, CPAN, GUI apps. Built into ChromeOS. Recommended.
Termux
Android app · ARM/x86
Perl in a terminal via the Play Store. CLI scripts only (no GUI). Great for devices without Linux support.
Cloud IDE
Replit · Gitpod · GitHub
Browser-based. No local install needed. Good for learning and sharing. Needs internet.

Why the Linux Container is the Right Choice

FeatureLinux ContainerTermuxCloud IDE
Perl version5.36+ (current Debian)5.36+Varies
CPAN modulesAll — apt + cpanmMost — cpanmPre-installed set
GUI apps (Tk, Gtk3)✓ Native XWayland windows✗ CLI only✗ None
File access✓ Chrome Files app integrationLimitedBrowser upload
Offline use✓ Fully offline✗ Internet needed
VS Code / editors✓ Full desktop IDEnano/vim onlyWeb-based
Compatibility: The Linux container requires a Chromebook from around 2017 or newer. Check Settings → Advanced → Developers → Linux development environment. If you see the option, your device supports it.
§02

Enable the Linux Container

ChromeOS's Linux container (codenamed Crostini) runs a real Debian Linux environment in a secure container. Its apps appear as regular ChromeOS windows, and its files appear in the Files app.

1
Open ChromeOS Settings

Click the clock in the bottom-right corner → click the gear icon → or press Alt Shift S and click Settings.

2
Navigate to Linux

In Settings, scroll down to AdvancedDevelopersLinux development environment → click Turn on.

3
Configure the container

Choose a disk size (10–20 GB recommended for Perl development with GUI apps). Click Install. The container downloads and configures itself — this takes 3–10 minutes on first run.

4
Open the Linux terminal

Once setup completes, a terminal window opens automatically. You can reopen it anytime from the app launcher — search for Terminal or look for the penguin icon. You are now in a Debian Linux shell.

bash — first look
whoami      # Your Linux username (matches your Google account)
uname -a    # Linux kernel version
cat /etc/debian_version   # Should show 12.x (Bookworm)
ls ~/       # Your home directory — empty to start
5
Enable disk sharing (recommended)

In ChromeOS Files app, right-click Downloads and select Share with Linux. Your Downloads folder then appears at /mnt/chromeos/MyFiles/Downloads in Linux — easy file transfer between ChromeOS and the container.

Terminal — penguin@penguin: ~
penguin@penguin
Debian GNU/Linux 12 (bookworm)
penguin@penguin
:~$ uname -a
Linux penguin 6.1.x-x86_64 #1 SMP ... x86_64 GNU/Linux
penguin@penguin
:~$ _
§03

Install Perl & Development Tools

The container already has a minimal Perl, but you need the full package with docs, build tools, and a module installer. Run these commands in the Terminal:

1
Update the package database
bash
sudo apt update && sudo apt upgrade -y
2
Install Perl, documentation, and build tools
bash
sudo apt install -y \
    perl \
    perl-doc \
    perldoc \
    cpanminus \
    build-essential \
    libssl-dev \
    libexpat1-dev

# Verify
perl -v          # Should show v5.36 or v5.38
which cpanm      # /usr/bin/cpanm
perldoc perl     # Opens the Perl manual (press q to exit)
3
Install GUI toolkits (for windowed apps)
bash
sudo apt install -y \
    perl-tk \
    libgtk3-perl \
    libwx-perl \
    libglib-perl

# Test Perl/Tk (will show a small window via XWayland)
perl -e '
    use Tk;
    my $mw = MainWindow->new;
    $mw->title("Perl/Tk works!");
    $mw->Label(-text => "Hello from Chromebook!")->pack(-pady => 20);
    $mw->Button(-text => "Close", -command => sub { exit })->pack;
    MainLoop;
'
4
Install web development modules
bash
cpanm Mojolicious
cpanm Plack
cpanm LWP::UserAgent
# Verify Mojolicious
perl -e 'use Mojolicious; print "Mojo $Mojolicious::VERSION\n"'
5
Install commonly-used CPAN modules
bash
cpanm \
    Modern::Perl \
    Try::Tiny \
    Path::Tiny \
    DateTime \
    JSON::PP \
    Text::CSV \
    DBI \
    DBD::SQLite
ARM Chromebooks: Most packages are available for ARM64 (aarch64). If cpanm fails to build a module, try sudo apt install p5-module-name first — many CPAN modules have pre-built Debian packages.
§04

Editors & IDE Setup

VS Code (Recommended)

VS Code for Linux installs inside the container and appears as a ChromeOS app. It's the best option for Perl development on Chromebook.

bash — install VS Code
# Method 1: apt repository (recommended — keeps it updated)
sudo apt install -y wget gpg
wget -qO- https://packages.microsoft.com/keys/microsoft.asc \
    | gpg --dearmor > /tmp/packages.microsoft.gpg
sudo install -D -o root -g root -m 644 \
    /tmp/packages.microsoft.gpg \
    /etc/apt/keyrings/packages.microsoft.gpg
sudo sh -c 'echo "deb [arch=amd64,arm64,armhf \
    signed-by=/etc/apt/keyrings/packages.microsoft.gpg] \
    https://packages.microsoft.com/repos/code stable main" \
    > /etc/apt/sources.list.d/vscode.list'
sudo apt update && sudo apt install -y code

# Launch it
code                    # Opens VS Code as a ChromeOS window
code myfile.pl          # Open a specific file

VS Code Perl Extensions

Open VS Code, press CtrlShiftX, and install these extensions:

Perl Navigator

bmewburn.perl-navigator — Syntax checking, auto-complete, go-to definition.

Perl (IntelliSense)

richterger.perl — IntelliSense, function signatures.

Perl Toolbox

Linting via perlcritic and perltidy formatting.

Perl::Critic

Code style checker integration.

bash — install Perl tools for VS Code
cpanm Perl::Critic
cpanm Perl::Tidy
sudo apt install -y perl-debug    # Enable the built-in Perl debugger

Gedit — Lightweight GUI Editor

bash
sudo apt install -y gedit gedit-plugins
# Enable Perl syntax highlighting in Edit → Preferences → Plugins
gedit myfile.pl &    # Opens as ChromeOS window

Vim / Neovim in the Terminal

bash
sudo apt install -y neovim
# For Perl syntax and autocomplete in Neovim:
cpanm Neovim::Ext    # Perl host provider for Neovim plugins

Your Project Directory

bash — recommended structure
mkdir -p ~/perl_projects/hello_world
mkdir -p ~/perl_projects/gui_apps
mkdir -p ~/perl_projects/web_apps

# This directory appears in the ChromeOS Files app under "Linux files"
# You can open any .pl file from there with a right-click → Open with Code
~/perl_projects/
    hello_world/
        hello.pl
        system_info.pl
    gui_apps/
        tk_notes.pl
        gtk3_calculator.pl
    web_apps/
        app.pl
        templates/
        Makefile.PL
§05

Development Workflow

Running Scripts

bash — essential commands
perl myscript.pl              # Run a script
perl -c myscript.pl           # Syntax-check only (no execution)
perl -w myscript.pl           # Extra warnings
perl -e 'print "Hello\n";'    # One-liner
perl -n -e 'print if /error/i' logfile.txt  # One-liner filter

chmod +x myscript.pl          # Make executable
./myscript.pl                 # Run directly (needs #!/usr/bin/perl shebang)

# Debugging
perl -d myscript.pl           # Interactive debugger
perl -d:Trace myscript.pl     # Trace every line executed

Chrome OS ↔ Linux File Sharing

bash
# ChromeOS Downloads folder (if shared with Linux):
ls /mnt/chromeos/MyFiles/Downloads/

# Copy a .pl file from Downloads to your project folder:
cp /mnt/chromeos/MyFiles/Downloads/script.pl ~/perl_projects/

# Save output to Downloads (opens in ChromeOS Files):
perl myscript.pl > /mnt/chromeos/MyFiles/Downloads/output.txt

The Interactive REPL — Reply

bash
cpanm Reply           # Install a Perl REPL (like irb for Ruby)
reply                 # Start interactive Perl shell
# Type any Perl expression and see the result immediately:
#   > 2 ** 32
#   4294967296
#   > "hello" . "world"
#   helloworld

Managing Modules with cpanm

bash
cpanm Module::Name             # Install
cpanm --uninstall Module::Name # Remove
cpanm --info Module::Name      # Version info
cpanm --look Module::Name      # Browse source
perl -MModule::Name -e 'print Module::Name->VERSION, "\n"'  # Check version
§06

Hello World — Your First Perl Script

bash — create and run
mkdir -p ~/perl_projects/hello_world
cd ~/perl_projects/hello_world
nano hello.pl          # or: code hello.pl
perlhello.pl
#!/usr/bin/perl
# =============================================================================
#  hello.pl  —  Your first Perl script on a Chromebook
#  Run with:  perl hello.pl
# =============================================================================
use strict;
use warnings;

# String output
print "Hello, Chromebook!\n";

# Formatted output with printf
my $name = "Perl";
my $year = (localtime)[5] + 1900;
printf "Welcome to %s development in %d.\n", $name, $year;

# Show where we're running
printf "Running on: %s\n",    $^O;            # linux
printf "Perl version: %s\n",  $];             # 5.038002 etc.
printf "Home dir: %s\n",      $ENV{HOME};     # /home/penguin

# Array
my @tools = qw(VS-Code Terminal nano vim);
print "\nYour Linux tools:\n";
printf "  %d. %s\n", $_ + 1, $tools[$_] for 0 .. $#tools;

# Hash
my %info = (
    OS       => "ChromeOS Linux (Crostini)",
    Distro   => "Debian",
    Shell    => $ENV{SHELL} // "bash",
    Editor   => "VS Code",
);
print "\nEnvironment:\n";
printf "  %-10s : %s\n", $_, $info{$_} for sort keys %info;

print "\nSetup complete — happy Perl coding!\n";
bash — run it
perl hello.pl
Output
Hello, Chromebook! Welcome to Perl development in 2025. Running on: linux Perl version: 5.038002 Home dir: /home/penguin Your Linux tools: 1. VS-Code 2. Terminal 3. nano 4. vim Environment: Distro : Debian Editor : VS Code OS : ChromeOS Linux (Crostini) Shell : /bin/bash Setup complete — happy Perl coding!
§07

CLI Scripts — Practical Tools

Perl excels at command-line tools. These scripts are immediately useful for Chromebook work.

Script 1: System Information Reporter

perlsysinfo.pl
#!/usr/bin/perl
# =============================================================================
#  sysinfo.pl  —  Chromebook Linux system information reporter
#  Run with:  perl sysinfo.pl
# =============================================================================
use strict;
use warnings;
use POSIX qw(uname);
use List::Util qw(sum);

sub divider { print "-" x 50, "\n" }
sub heading { divider(); print "  $_[0]\n"; divider() }

# ── Kernel & OS ───────────────────────────────────────────────────────────────
heading("System");
my @un = uname();
printf "  %-16s %s\n", "Kernel:", $un[2];
printf "  %-16s %s\n", "Architecture:", $un[4];
printf "  %-16s %s\n", "Hostname:", $un[1];

if (open my $fh, "<", "/etc/os-release") {
    while (<$fh>) {
        if (/^PRETTY_NAME="(.+)"/) {
            printf "  %-16s %s\n", "OS:", $1;
            last;
        }
    }
}

# ── Perl environment ──────────────────────────────────────────────────────────
heading("Perl");
printf "  %-16s %vd\n", "Version:", $^V;
printf "  %-16s %s\n",  "Executable:", $^X;
printf "  %-16s %s\n",  "Platform:", $^O;

# Count installed modules
my $mod_count = 0;
for my $dir (@INC) {
    next unless -d $dir;
    opendir(my $dh, $dir) or next;
    $mod_count += grep { /\.pm$/i } readdir($dh);
    closedir($dh);
}
printf "  %-16s %d\n", "Modules (.pm):", $mod_count;

# ── Memory ────────────────────────────────────────────────────────────────────
heading("Memory");
if (open my $mf, "<", "/proc/meminfo") {
    my %mem;
    while (<$mf>) {
        $mem{$1} = $2 if /^(\w+):\s+(\d+)/;
    }
    my $total = $mem{MemTotal}     // 0;
    my $free  = $mem{MemAvailable} // 0;
    my $used  = $total - $free;
    printf "  %-16s %.1f GB\n", "Total:",     $total / 1_048_576;
    printf "  %-16s %.1f GB\n", "Used:",      $used  / 1_048_576;
    printf "  %-16s %.1f GB\n", "Available:", $free  / 1_048_576;
}

# ── Disk ──────────────────────────────────────────────────────────────────────
heading("Disk — Home Partition");
my $df = `df -BM "$ENV{HOME}" 2>/dev/null | tail -1`;
if ($df =~ /(\d+)M\s+(\d+)M\s+(\d+)M/) {
    printf "  %-16s %d GB\n", "Total:",  $1 / 1024;
    printf "  %-16s %d GB\n", "Used:",   $2 / 1024;
    printf "  %-16s %d GB\n", "Free:",   $3 / 1024;
}

# ── Environment ───────────────────────────────────────────────────────────────
heading("Environment");
for my $var (qw(DISPLAY TERM SHELL HOME LANG)) {
    printf "  %-16s %s\n", "$var:", $ENV{$var} // "(not set)";
}

divider();
print "  Running in: ";
print -d "/run/user" ? "container\n" : "native Linux\n";

Script 2: Chromebook Linux Files Analyzer

perlfiles.pl
#!/usr/bin/perl
# =============================================================================
#  files.pl  —  Analyze a directory of files
#  Usage:  perl files.pl [directory]
#  Run on your home dir:  perl files.pl ~
# =============================================================================
use strict;
use warnings;
use File::Find;
use File::Basename;
use POSIX qw(floor);
use List::Util qw(sum max);

my $dir    = shift // $ENV{HOME};
my $hidden = grep { /^-a/ } @ARGV;   # -a flag shows hidden files

die "Directory '$dir' not found.\n" unless -d $dir;

my %stats;
my @large;
my $start = time;

find({
    no_chdir => 1,
    wanted   => sub {
        return unless -f $_;
        return if !$hidden && m{/\.};  # Skip hidden unless -a

        my $size = -s $_;
        my (undef, undef, $ext) = fileparse($_, qr/\.[^.]*/);
        $ext = lc($ext) || '(no ext)';

        $stats{count}++;
        $stats{total_bytes} += $size;
        $stats{by_ext}{$ext}{count}++;
        $stats{by_ext}{$ext}{bytes} += $size;
        push @large, [$_, $size] if $size > 1_048_576;  # > 1 MB
    },
}, $dir);

my $elapsed = time - $start;

# ── Summary ───────────────────────────────────────────────────────────────────
my $total_mb = ($stats{total_bytes} // 0) / 1_048_576;
printf "\n%-24s %s\n",   "Directory:",     $dir;
printf "%-24s %d\n",     "Files found:",   $stats{count} // 0;
printf "%-24s %.1f MB\n","Total size:",    $total_mb;
printf "%-24s %ds\n\n",  "Scan time:",     $elapsed;

# ── By extension ─────────────────────────────────────────────────────────────
print "By extension (top 15):\n";
printf "  %-12s %6s  %10s\n", "Extension", "Files", "Size";
printf "  %s\n", "-" x 35;

my @sorted_ext = sort {
    $stats{by_ext}{$b}{bytes} <=> $stats{by_ext}{$a}{bytes}
} keys %{ $stats{by_ext} };

for my $ext (@sorted_ext[0..14]) {
    last unless defined $ext;
    my $c = $stats{by_ext}{$ext}{count};
    my $b = $stats{by_ext}{$ext}{bytes};
    printf "  %-12s %6d  %8.1f MB\n", $ext, $c, $b / 1_048_576;
}

# ── Large files ───────────────────────────────────────────────────────────────
if (@large) {
    print "\nFiles larger than 1 MB:\n";
    for my $f (sort { $b->[1] <=> $a->[1] } @large) {
        printf "  %8.1f MB  %s\n", $f->[1] / 1_048_576,
            substr($f->[0], length($dir) + 1);
    }
}
§08

File Processing Scripts

CSV Report Generator

A practical script showing how to read CSV files, process data, and produce reports — useful for handling exported spreadsheet data in ChromeOS.

perlcsv_report.pl
#!/usr/bin/perl
# =============================================================================
#  csv_report.pl  —  CSV file analyzer and report generator
#  Usage:  perl csv_report.pl data.csv
#          perl csv_report.pl    (uses built-in demo data)
#  Modules: Text::CSV (cpanm Text::CSV)
# =============================================================================
use strict;
use warnings;
use List::Util qw(sum min max);

# ── Parse a simple CSV (no quotes/escaping for clarity) ──────────────────────
sub parse_csv_line {
    my ($line) = @_;
    chomp $line;
    return map { s/^\s+|\s+$//gr } split(/,/, $line);
}

# ── Demo data (sales records) ────────────────────────────────────────────────
my @demo_csv = (
    "Product,Category,Units,Price",
    "Widget Pro,Electronics,42,29.99",
    "Gadget Lite,Electronics,87,14.99",
    "Desk Lamp,Home,31,45.00",
    "USB Hub,Electronics,124,19.99",
    "Notebook,Stationery,200,5.99",
    "Pen Set,Stationery,315,8.49",
    "Monitor Stand,Home,28,79.99",
    "Keyboard,Electronics,56,49.99",
    "Mouse Pad,Electronics,93,12.99",
    "Bookend Set,Home,44,24.99",
);

# Use file if given, else demo
my @lines;
if (@ARGV && -f $ARGV[0]) {
    open(my $fh, "<", $ARGV[0]) or die "Cannot open: $!";
    @lines = <$fh>;
    close $fh;
} else {
    @lines = @demo_csv;
    print "(Using demo data — pass a CSV file as argument)\n\n";
}

# ── Parse header and rows ─────────────────────────────────────────────────────
my @header = parse_csv_line(shift @lines);
printf "Columns: %s\n\n", join(", ", @header);

my @rows;
for my $line (@lines) {
    my @fields = parse_csv_line($line);
    next unless @fields == @header;
    my %row;
    @row{@header} = @fields;
    push @rows, \%row;
}
printf "Records: %d\n\n", scalar @rows;

# ── Analysis ──────────────────────────────────────────────────────────────────
# Assume numeric columns are the last two (Units, Price)
my ($unit_col, $price_col) = @header[-2, -1];
my $name_col    = $header[0];
my $cat_col     = $header[1];

# Calculate revenue per row
my %by_category;
my @revenues;
for my $r (@rows) {
    my $rev = ($r->{$unit_col} // 0) * ($r->{$price_col} // 0);
    $r->{Revenue} = $rev;
    push @revenues, $rev;
    $by_category{ $r->{$cat_col} }{count}++;
    $by_category{ $r->{$cat_col} }{revenue} += $rev;
    $by_category{ $r->{$cat_col} }{units}   += $r->{$unit_col} // 0;
}

# ── Detailed table ────────────────────────────────────────────────────────────
printf "%-22s %-14s %7s %8s %10s\n",
       $name_col, $cat_col, $unit_col, $price_col, "Revenue";
print "-" x 65, "\n";
for my $r (sort { $b->{Revenue} <=> $a->{Revenue} } @rows) {
    printf "%-22s %-14s %7d %8.2f %10.2f\n",
           $r->{$name_col}, $r->{$cat_col},
           $r->{$unit_col} // 0, $r->{$price_col} // 0, $r->{Revenue};
}
print "-" x 65, "\n";
printf "%-22s %-14s %7d %8s %10.2f\n",
    "TOTAL", "", sum(map {$_->{$unit_col}} @rows),
    "", sum(@revenues);

# ── Summary by category ───────────────────────────────────────────────────────
print "\nBy Category:\n";
for my $cat (sort { $by_category{$b}{revenue} <=> $by_category{$a}{revenue} }
             keys %by_category) {
    printf "  %-14s  %3d products  %6d units  \$%8.2f revenue\n",
           $cat,
           $by_category{$cat}{count},
           $by_category{$cat}{units},
           $by_category{$cat}{revenue};
}

printf "\nTotal revenue: \$%.2f\n", sum(@revenues);
printf "Best product:  %s (\$%.2f)\n",
    (sort { $b->{Revenue} <=> $a->{Revenue} } @rows)[0]{$name_col},
    max(@revenues);
Output
(Using demo data — pass a CSV file as argument) Columns: Product, Category, Units, Price Records: 10 Product Category Units Price Revenue ... Pen Set Stationery 315 8.49 2674.35 Notebook Stationery 200 5.99 1198.00 USB Hub Electronics 124 19.99 2478.76 ... Total revenue: $13842.37 Best product: Pen Set ($2674.35)
§09

Modules & CPAN on Chromebook

Essential Modules for Chromebook Perl

CategoryModuleInstallUse
Modern PerlModern::PerlcpanmEnables strict, warnings, say, etc.
Error handlingTry::Tinycpanmtry/catch/finally blocks
File pathsPath::TinycpanmEasy file/dir manipulation
JSONJSON::PPBuilt-inJSON encode/decode
HTTP clientLWP::UserAgentcpanmHTTP requests
CSVText::CSVcpanmRead/write CSV files
DatesDateTimecpanmDate/time math and formatting
DatabaseDBI + DBD::SQLitecpanmSQLite database access
TestingTest::SimpleBuilt-inUnit testing
perl — module usage examples
#!/usr/bin/perl
use strict; use warnings;

# Modern::Perl enables useful features
use Modern::Perl '2023';   # Enables say, state, strict, warnings

say "say adds a newline automatically";   # Like print "\n"

state $count = 0;          # Persistent variable across calls

# Try::Tiny — clean error handling
use Try::Tiny;
try {
    die "something went wrong\n";
} catch {
    warn "Caught: $_";
} finally {
    say "This always runs";
};

# Path::Tiny — easy file operations
use Path::Tiny;
my $dir  = path("~")->absolute;   # Resolve ~ to full path
my @pls  = $dir->children(qr/\.pl$/);  # Find all .pl files
say "Perl scripts: " . scalar(@pls);

my $file = path("/tmp/test.txt");
$file->spew("Hello from Path::Tiny!\n");    # Write
say $file->slurp;                            # Read
$file->remove;                               # Delete

# JSON::PP — no install needed
use JSON::PP;
my $data = { name => "Chromebook", year => 2024, active => \1 };
my $json = encode_json($data);
say "JSON: $json";
my $back = decode_json($json);
say "Name: $back->{name}";

# DBD::SQLite — in-container database
use DBI;
my $dbh = DBI->connect("dbi:SQLite:dbname=/tmp/test.db", "", "");
$dbh->do("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT)");
$dbh->do("INSERT INTO notes (text) VALUES (?)", undef, "First note from Chromebook!");
my $rows = $dbh->selectall_arrayref("SELECT * FROM notes");
printf "DB record: %d — %s\n", $_->[0], $_->[1] for @$rows;
$dbh->disconnect;
unlink "/tmp/test.db";
§10

GUI Development on Chromebook

This is where Chromebook Linux shines: GUI apps from the Linux container run as first-class ChromeOS windows. They appear in the taskbar, can be pinned to the shelf, support full-screen mode, and integrate with the ChromeOS window manager — with no X11 server configuration needed.

How it works: ChromeOS runs a component called Sommelier which acts as an XWayland server. When you run a Perl/Tk or Gtk3 app in the container, Sommelier translates X11 calls to Wayland and renders the window in ChromeOS's compositor. The DISPLAY variable is set to :0 automatically when you open a Terminal.
bash — check GUI setup
# Verify DISPLAY is set (should be :0 or similar)
echo $DISPLAY

# If empty, set it manually:
export DISPLAY=:0

# Quick GUI test — a bare Perl/Tk window
perl -e '
    use Tk;
    my $mw = MainWindow->new;
    $mw->title("Chromebook GUI Test");
    $mw->geometry("300x120");
    $mw->Label(-text  => "✓ GUI works on ChromeOS!",
               -font  => "Helvetica 14 bold",
               -fg    => "#34c759")->pack(-pady => 20);
    $mw->Button(-text => "Close", -command => sub { exit })->pack;
    MainLoop;
'

Making Linux Apps Appear in the Launcher

bash — create a desktop entry
mkdir -p ~/.local/share/applications

cat > ~/.local/share/applications/perl-notes.desktop << 'EOF'
[Desktop Entry]
Name=Perl Notes
Comment=Perl/Tk Notes Application
Exec=perl /home/penguin/perl_projects/gui_apps/tk_notes.pl
Icon=text-editor
Terminal=false
Type=Application
Categories=Utility;
EOF

# Refresh the app list
update-desktop-database ~/.local/share/applications

After this, Perl Notes appears in the ChromeOS launcher alongside all other apps.

§11

Perl/Tk App — Chromebook Notes

A complete Perl/Tk sticky-notes application: create notes, store them as text files in your Linux home directory (visible in the ChromeOS Files app), and load them back. Demonstrates the full Chromebook development cycle.

perltk_notes.pl
#!/usr/bin/perl
# =============================================================================
#  tk_notes.pl  —  Chromebook Notes  (Perl/Tk)
#  Install:  sudo apt install perl-tk
#  Run:      perl tk_notes.pl
#
#  Notes are saved in ~/notes/ and are visible in the ChromeOS Files app.
# =============================================================================
use strict;
use warnings;
use Tk;
use Tk::Text;
use Tk::Listbox;
use File::Basename;
use Cwd qw(abs_path);

# ── State ──────────────────────────────────────────────────────────────────────
my $notes_dir   = "$ENV{HOME}/notes";
my $current_note = "";
my $modified     = 0;
my $status_msg   = "Ready";

mkdir $notes_dir unless -d $notes_dir;

# ── Main window ───────────────────────────────────────────────────────────────
my $mw = MainWindow->new;
$mw->title("Chromebook Notes");
$mw->geometry("760x560+60+40");
$mw->configure(-background => "#1e1e2e");
$mw->protocol("WM_DELETE_WINDOW", \&on_quit);

# ── Menu bar ──────────────────────────────────────────────────────────────────
my $menu = $mw->Menu;
$mw->configure(-menu => $menu);

my $file = $menu->cascade(-label => "File", -tearoff => 0);
$file->command(-label => "New Note",     -accelerator => "Ctrl+N", -command => \&cmd_new);
$file->command(-label => "Save",         -accelerator => "Ctrl+S", -command => \&cmd_save);
$file->command(-label => "Delete Note",  -command => \&cmd_delete);
$file->separator;
$file->command(-label => "Quit",         -accelerator => "Ctrl+Q", -command => \&on_quit);

my $edit = $menu->cascade(-label => "Edit", -tearoff => 0);
$edit->command(-label => "Cut",          -accelerator => "Ctrl+X", -command => sub { $mw->focus; $editor->event_generate("<>") });
$edit->command(-label => "Copy",         -accelerator => "Ctrl+C", -command => sub { $editor->event_generate("<>") });
$edit->command(-label => "Paste",        -accelerator => "Ctrl+V", -command => sub { $editor->event_generate("<>") });

# ── Layout: sidebar (notes list) + editor ─────────────────────────────────────
my $paned = $mw->Frame(-background => "#1e1e2e")->pack(-fill => "both", -expand => 1);

# Sidebar
my $sidebar = $paned->Frame(
    -background  => "#16161e",
    -borderwidth => 0,
    -width       => 200,
)->pack(-side => "left", -fill => "y");
$sidebar->packPropagate(0);

$sidebar->Label(
    -text       => "MY NOTES",
    -font       => "Helvetica 9",
    -foreground => "#5bc7f0",
    -background => "#16161e",
    -anchor     => "w",
)->pack(-fill => "x", -padx => 10, -pady => 8);

# Notes list with scrollbar
my $lb_frame = $sidebar->Frame(-background => "#16161e")->pack(-fill => "both", -expand => 1);
my $lb = $lb_frame->Listbox(
    -background       => "#16161e",
    -foreground       => "#e2d9c8",
    -selectbackground => "#2a3a5a",
    -selectforeground => "#5bc7f0",
    -font             => "Helvetica 11",
    -borderwidth      => 0,
    -relief           => "flat",
    -activestyle      => "none",
)->pack(-side => "left", -fill => "both", -expand => 1);
my $lb_scroll = $lb_frame->Scrollbar(-command => [$lb, "yview"], -width => 8)->pack(
    -side => "right", -fill => "y"
);
$lb->configure(-yscrollcommand => [$lb_scroll, "set"]);

# Sidebar buttons
my $btn_row = $sidebar->Frame(-background => "#16161e")->pack(-fill => "x", -padx => 8, -pady => 6);
$btn_row->Button(
    -text       => "+ New",
    -font       => "Helvetica 10",
    -background => "#2a4a8a",
    -foreground => "white",
    -relief     => "flat",
    -padx       => 6, -pady => 3,
    -command    => \&cmd_new,
)->pack(-side => "left", -padx => 2);
$btn_row->Button(
    -text       => "⌫ Delete",
    -font       => "Helvetica 10",
    -background => "#4a1a1a",
    -foreground => "#e08080",
    -relief     => "flat",
    -padx       => 6, -pady => 3,
    -command    => \&cmd_delete,
)->pack(-side => "left", -padx => 2);

# ── Editor area ────────────────────────────────────────────────────────────────
my $editor_frame = $paned->Frame(-background => "#1e1e2e")->pack(
    -side => "left", -fill => "both", -expand => 1
);

my $title_var = "";
my $title_entry = $editor_frame->Entry(
    -textvariable     => \$title_var,
    -font             => "Helvetica 14 bold",
    -background       => "#2a2a3e",
    -foreground       => "#e2d9c8",
    -insertbackground => "white",
    -relief           => "flat",
    -borderwidth      => 4,
)->pack(-fill => "x", -padx => 8, -pady => 6);

my $editor_box = $editor_frame->Frame(-background => "#1e1e2e")->pack(
    -fill => "both", -expand => 1, -padx => 8
);
my $editor = $editor_box->Text(
    -font             => "Helvetica 12",
    -background       => "#1e1e2e",
    -foreground       => "#e2d9c8",
    -insertbackground => "white",
    -selectbackground => "#264f78",
    -relief           => "flat",
    -padx             => 4, -pady => 4,
    -wrap             => "word",
    -undo             => 1,
)->pack(-side => "left", -fill => "both", -expand => 1);
my $ed_scroll = $editor_box->Scrollbar(-command => [$editor, "yview"], -width => 8)->pack(
    -side => "right", -fill => "y"
);
$editor->configure(-yscrollcommand => [$ed_scroll, "set"]);

# Toolbar below editor
my $toolbar = $editor_frame->Frame(-background => "#16161e")->pack(-fill => "x", -pady => 4);
for my $info (
    ["Save  Ctrl+S", \&cmd_save,  "#2a4a2a", "#6fcf97"],
    ["Undo  Ctrl+Z", sub { $editor->edit_undo }, "#2a2a3a", "#9b9380"],
) {
    my ($label, $cb, $bg, $fg) = @$info;
    $toolbar->Button(
        -text => $label, -command => $cb,
        -background => $bg, -foreground => $fg,
        -font => "Helvetica 10", -relief => "flat",
        -padx => 10, -pady => 4,
    )->pack(-side => "left", -padx => 4);
}

# Status bar
my $status_bar = $mw->Frame(-background => "#5bc7f0", -height => 22)->pack(-fill => "x", -side => "bottom");
$status_bar->Label(
    -textvariable => \$status_msg,
    -font         => "Helvetica 10",
    -foreground   => "#0a1a2a",
    -background   => "#5bc7f0",
    -anchor       => "w",
)->pack(-side => "left", -padx => 10);
$status_bar->Label(
    -text       => "Notes dir: $notes_dir",
    -font       => "Helvetica 10",
    -foreground => "#0a1a2a",
    -background => "#5bc7f0",
)->pack(-side => "right", -padx => 10);

# ── Keyboard shortcuts ─────────────────────────────────────────────────────────
$mw->bind("", \&cmd_new);
$mw->bind("", \&cmd_save);
$mw->bind("", \&on_quit);

# ── Track modifications ────────────────────────────────────────────────────────
$editor->bind("<>", sub {
    if ($editor->edit_modified) {
        $modified = 1;
        $mw->title("Chromebook Notes  •");
    }
});

# ── Listbox selection ──────────────────────────────────────────────────────────
$lb->bind("<>", sub {
    my ($idx) = $lb->curselection;
    return unless defined $idx;
    my $name = $lb->get($idx);
    load_note($name);
});

# ── Load note list on start ────────────────────────────────────────────────────
refresh_list();

# ═══════════════════════════════════════════════════════════════════════
# Subroutines
# ═══════════════════════════════════════════════════════════════════════
sub refresh_list {
    $lb->delete(0, "end");
    my @notes = sort glob("$notes_dir/*.txt");
    for my $f (@notes) {
        $lb->insert("end", basename($f, ".txt"));
    }
}

sub load_note {
    my ($name) = @_;
    my $path = "$notes_dir/$name.txt";
    return unless -f $path;

    if ($modified) {
        my $ans = $mw->messageBox(
            -type => "YesNo", -icon => "question",
            -message => "Save current note first?"
        );
        cmd_save() if $ans eq "Yes";
    }

    open(my $fh, "<", $path) or return;
    my $content = do { local $/; <$fh> };
    close($fh);

    my $title = (split /\n/, $content, 2)[0] =~ s/^#\s*//r;
    $title_var = $title;
    $editor->delete("1.0", "end");
    $editor->insert("1.0", $content);
    $editor->edit_modified(0);
    $current_note = $name;
    $modified     = 0;
    $mw->title("Chromebook Notes — $name");
    $status_msg = "Loaded: $name";
}

sub cmd_new {
    my $name = "note_" . time;
    $title_var = "New Note";
    $editor->delete("1.0", "end");
    $editor->insert("1.0", "# New Note\n\n");
    $editor->edit_modified(0);
    $current_note = $name;
    $modified     = 0;
    $mw->title("Chromebook Notes — new");
    $status_msg = "New note";
    $title_entry->focus;
}

sub cmd_save {
    my $name = $title_var;
    $name =~ s/[^\w\s-]//g;
    $name =~ s/\s+/_/g;
    $name = lc($name) || $current_note || "untitled_" . time;
    my $path    = "$notes_dir/$name.txt";
    my $content = $editor->get("1.0", "end");
    open(my $fh, ">", $path) or do {
        $status_msg = "ERROR: cannot save";
        return;
    };
    print $fh $content;
    close($fh);
    $editor->edit_modified(0);
    $current_note = $name;
    $modified     = 0;
    $mw->title("Chromebook Notes — $name");
    $status_msg = "Saved: $name.txt";
    refresh_list();
}

sub cmd_delete {
    my ($idx) = $lb->curselection;
    return unless defined $idx;
    my $name = $lb->get($idx);
    my $ans  = $mw->messageBox(
        -type => "YesNo", -icon => "question",
        -message => "Delete note '$name'?"
    );
    if ($ans eq "Yes") {
        unlink "$notes_dir/$name.txt";
        $editor->delete("1.0", "end");
        $title_var    = "";
        $current_note = "";
        $modified     = 0;
        $mw->title("Chromebook Notes");
        refresh_list();
        $status_msg = "Deleted: $name";
    }
}

sub on_quit {
    cmd_save() if $modified;
    exit;
}

MainLoop;
§12

Gtk3 App — System Monitor

A Gtk3 application that displays live CPU, memory, and disk information, updating every second via a GLib timer. Demonstrates Gtk3's grid layout, markup labels, progress bars, and the Glib::Timeout repeating timer.

perlgtk3_sysmon.pl
#!/usr/bin/perl
# =============================================================================
#  gtk3_sysmon.pl  —  Chromebook System Monitor  (Gtk3)
#  Install:  sudo apt install libgtk3-perl
#  Run:      perl gtk3_sysmon.pl
# =============================================================================
use strict;
use warnings;
use Gtk3 '-init';
use Glib qw(TRUE FALSE);

# ── Read /proc/stat for CPU usage ─────────────────────────────────────────────
my @prev_cpu = (0) x 8;
sub read_cpu_pct {
    open my $f, '<', '/proc/stat' or return 0;
    my $line = <$f>; close $f;
    my (undef, @fields) = split /\s+/, $line;
    my $idle    = $fields[3] + $fields[4];  # idle + iowait
    my $total   = 0; $total += $_ for @fields;
    my $d_idle  = $idle  - $prev_cpu[3] - $prev_cpu[4];
    my $d_total = $total - $prev_cpu[7];
    @prev_cpu   = (@fields[0..6], $total);
    return 0 if $d_total == 0;
    return int(100 - 100 * $d_idle / $d_total);
}

sub read_mem {
    open my $f, '<', '/proc/meminfo' or return (0,0);
    my %m;
    while (<$f>) { $m{$1} = $2 if /^(\w+):\s+(\d+)/ }
    close $f;
    my $total  = $m{MemTotal}     // 1;
    my $avail  = $m{MemAvailable} // 0;
    return ($total, $total - $avail);
}

sub read_disk {
    my $df = `df -BM "$ENV{HOME}" 2>/dev/null | tail -1`;
    return (1,0) unless $df;
    my @f = split /\s+/, $df;
    return ((($f[1] =~ s/M//r) || 1), (($f[2] =~ s/M//r) || 0));
}

# ── CSS ────────────────────────────────────────────────────────────────────────
my $css = Gtk3::CssProvider->new;
$css->load_from_data(q{
    window { background-color: #09090f; }
    .title  { color: #5bc7f0; font-size: 22px; font-weight: bold; }
    .metric { color: #e2d9c8; font-size: 14px; font-family: monospace; }
    .label  { color: #9b9380; font-size: 12px; }
    .value-ok   { color: #6fcf97; font-size: 16px; font-weight: bold; font-family: monospace; }
    .value-warn { color: #f0a030; font-size: 16px; font-weight: bold; font-family: monospace; }
    .value-high { color: #eb5757; font-size: 16px; font-weight: bold; font-family: monospace; }
    progressbar trough { min-height: 12px; border-radius: 6px; background: #1e1e30; }
    progressbar progress { border-radius: 6px; }
    progressbar.ok   progress { background: #6fcf97; }
    progressbar.warn progress { background: #f0a030; }
    progressbar.high progress { background: #eb5757; }
    .card { background-color: #0f0f1c; border-radius: 8px; padding: 16px; }
    separator { background: #1e1e30; }
});
Gtk3::StyleContext::add_provider_for_screen(
    Gtk3::Gdk::Screen::get_default(),
    $css, Gtk3::STYLE_PROVIDER_PRIORITY_APPLICATION
);

# ── Main window ───────────────────────────────────────────────────────────────
my $win = Gtk3::Window->new('toplevel');
$win->set_title('Chromebook System Monitor');
$win->set_default_size(440, 360);
$win->set_resizable(FALSE);
$win->signal_connect(destroy => sub { Gtk3->main_quit });

my $root = Gtk3::Box->new('vertical', 0);
$root->set_margin_start(20); $root->set_margin_end(20);
$root->set_margin_top(16);   $root->set_margin_bottom(16);
$win->add($root);

# Title
my $title = Gtk3::Label->new('');
$title->set_markup('System Monitor');
$title->set_margin_bottom(16);
$root->pack_start($title, FALSE, FALSE, 0);

# ── Build a metric card ────────────────────────────────────────────────────────
my (%bars, %val_labels, %pct_labels);

sub make_card {
    my ($parent, $id, $title_text, $unit) = @_;

    my $frame = Gtk3::Frame->new;
    $frame->get_style_context->add_class('card');
    $frame->set_shadow_type('none');
    $frame->set_margin_bottom(10);
    $parent->pack_start($frame, FALSE, FALSE, 0);

    my $vbox = Gtk3::Box->new('vertical', 6);
    $vbox->set_margin_start(16); $vbox->set_margin_end(16);
    $vbox->set_margin_top(12);   $vbox->set_margin_bottom(12);
    $frame->add($vbox);

    # Header row: title + value
    my $header = Gtk3::Box->new('horizontal', 0);
    $vbox->pack_start($header, FALSE, FALSE, 0);

    my $lbl = Gtk3::Label->new('');
    $lbl->set_markup("$title_text");
    $lbl->set_halign('start');
    $header->pack_start($lbl, TRUE, TRUE, 0);

    my $val = Gtk3::Label->new('— %');
    $val->set_halign('end');
    $val_labels{$id} = $val;
    $header->pack_end($val, FALSE, FALSE, 0);

    # Progress bar
    my $bar = Gtk3::ProgressBar->new;
    $bar->set_fraction(0);
    $bar->get_style_context->add_class('ok');
    $bars{$id} = $bar;
    $vbox->pack_start($bar, FALSE, FALSE, 0);

    # Details row
    my $details = Gtk3::Label->new('');
    $details->set_halign('start');
    $details->set_margin_top(2);
    $pct_labels{$id} = $details;
    $vbox->pack_start($details, FALSE, FALSE, 0);

    return $frame;
}

make_card($root, 'cpu',  'CPU',    '%');
make_card($root, 'mem',  'Memory', 'GB');
make_card($root, 'disk', 'Disk',   'GB');

# Last-updated label
my $updated = Gtk3::Label->new('');
$updated->set_halign('end');
$updated->set_margin_top(4);
$root->pack_end($updated, FALSE, FALSE, 0);

# ── Update function ────────────────────────────────────────────────────────────
sub update_display {
    # CPU
    my $cpu_pct = read_cpu_pct();
    my $cpu_cls = $cpu_pct > 80 ? 'high' : $cpu_pct > 50 ? 'warn' : 'ok';
    my $cpu_col = $cpu_pct > 80 ? '#eb5757' : $cpu_pct > 50 ? '#f0a030' : '#6fcf97';
    $bars{cpu}->set_fraction($cpu_pct / 100);
    $bars{cpu}->get_style_context->remove_class($_) for qw(ok warn high);
    $bars{cpu}->get_style_context->add_class($cpu_cls);
    $val_labels{cpu}->set_markup("${cpu_pct}%");
    $pct_labels{cpu}->set_markup("Utilisation across all cores");

    # Memory
    my ($mem_total_kb, $mem_used_kb) = read_mem();
    my $mem_pct  = $mem_total_kb ? int(100 * $mem_used_kb / $mem_total_kb) : 0;
    my $mem_cls  = $mem_pct > 85 ? 'high' : $mem_pct > 65 ? 'warn' : 'ok';
    my $mem_col  = $mem_pct > 85 ? '#eb5757' : $mem_pct > 65 ? '#f0a030' : '#6fcf97';
    my $mem_used = sprintf "%.1f", $mem_used_kb  / 1_048_576;
    my $mem_tot  = sprintf "%.1f", $mem_total_kb / 1_048_576;
    $bars{mem}->set_fraction($mem_pct / 100);
    $bars{mem}->get_style_context->remove_class($_) for qw(ok warn high);
    $bars{mem}->get_style_context->add_class($mem_cls);
    $val_labels{mem}->set_markup("${mem_pct}%");
    $pct_labels{mem}->set_markup("${mem_used} GB used of ${mem_tot} GB");

    # Disk
    my ($disk_tot, $disk_used) = read_disk();
    my $disk_pct = $disk_tot ? int(100 * $disk_used / $disk_tot) : 0;
    my $disk_cls = $disk_pct > 90 ? 'high' : $disk_pct > 70 ? 'warn' : 'ok';
    my $disk_col = $disk_pct > 90 ? '#eb5757' : $disk_pct > 70 ? '#f0a030' : '#6fcf97';
    my $dtg      = sprintf "%.1f", $disk_tot  / 1024;
    my $dug      = sprintf "%.1f", $disk_used / 1024;
    $bars{disk}->set_fraction($disk_pct / 100);
    $bars{disk}->get_style_context->remove_class($_) for qw(ok warn high);
    $bars{disk}->get_style_context->add_class($disk_cls);
    $val_labels{disk}->set_markup("${disk_pct}%");
    $pct_labels{disk}->set_markup("${dug} GB used of ${dtg} GB (home)");

    # Timestamp
    my $time_str = scalar localtime;
    $updated->set_markup("Updated: $time_str");

    return TRUE;  # Keep the timer running
}

# ── Start timer — update every 1000ms ────────────────────────────────────────
update_display();
Glib::Timeout->add(1000, \&update_display);

$win->show_all;
Gtk3->main;
§13

Web Development — Mojolicious

Mojolicious is a full-featured, modern Perl web framework. On a Chromebook, you run the web server in the Linux container and access it from the Chrome browser on the same machine. The server runs at http://localhost:3000 and is visible directly in Chrome — no port forwarding needed.

bash — install and test
cpanm Mojolicious
# Quick test:
perl -e 'use Mojolicious; print "Mojo $Mojolicious::VERSION\n"'

Minimal Web App

perlapp.pl
#!/usr/bin/perl
# =============================================================================
#  app.pl  —  Minimal Mojolicious web application
#  Run:     perl app.pl daemon
#  Visit:   http://localhost:3000
# =============================================================================
use Mojolicious::Lite -signatures;

# Route: GET /
get '/' => sub ($c) {
    $c->render(text => 'Hello from Perl + Mojolicious on your Chromebook!');
};

# Route: GET /hello/:name
get '/hello/:name' => sub ($c) {
    my $name = $c->param('name');
    $c->render(text => "Hello, $name!");
};

# Route: GET /json
get '/json' => sub ($c) {
    $c->render(json => {
        message  => 'Hello from Perl',
        platform => 'Chromebook Linux',
        perl     => $^V . "",
        time     => time(),
    });
};

app->start;
bash — run it
perl app.pl daemon        # Starts on http://localhost:3000
# Press Ctrl+C to stop

Complete Notes Web App with Templates

perlnotes_web.pl
#!/usr/bin/perl
# =============================================================================
#  notes_web.pl  —  Mojolicious notes web application
#  Run:     perl notes_web.pl daemon
#  Visit:   http://localhost:3000
#  Notes stored in: ~/notes/ (same dir as the Tk app)
# =============================================================================
use Mojolicious::Lite -signatures;
use File::Basename;
use Cwd 'abs_path';

my $notes_dir = "$ENV{HOME}/notes";
mkdir $notes_dir unless -d $notes_dir;

# ── Helper: read all notes ────────────────────────────────────────────────────
helper notes => sub ($c) {
    my @files = sort glob("$notes_dir/*.txt");
    return [ map {
        my $name    = basename($_, ".txt");
        my $preview = "";
        if (open my $fh, "<", $_) {
            $preview = <$fh> // "";
            chomp $preview;
            $preview =~ s/^#\s*//;
            close $fh;
        }
        { name => $name, preview => $preview }
    } @files ];
};

# ── Routes ────────────────────────────────────────────────────────────────────
# List all notes
get '/' => sub ($c) {
    $c->render(template => 'index', notes => $c->notes);
};

# View a note
get '/note/:name' => sub ($c) {
    my $name = $c->param('name');
    $name =~ s/[^\w\-]//g;
    my $path = "$notes_dir/$name.txt";
    my $body = "";
    if (open my $fh, "<", $path) { local $/; $body = <$fh>; }
    $c->render(template => 'note', name => $name, body => $body);
};

# Save a note (POST)
post '/save' => sub ($c) {
    my $name = $c->param('name') // "untitled";
    my $body = $c->param('body') // "";
    $name =~ s/[^\w\-]//g;
    $name ||= "note_" . time;
    open(my $fh, ">", "$notes_dir/$name.txt") and do { print $fh $body; close $fh; };
    $c->redirect_to("/note/$name");
};

# Delete a note
post '/delete/:name' => sub ($c) {
    my $name = $c->param('name');
    $name =~ s/[^\w\-]//g;
    unlink "$notes_dir/$name.txt";
    $c->redirect_to('/');
};

# New note form
get '/new' => sub ($c) {
    $c->render(template => 'note', name => 'new_note', body => "# New Note\n\n");
};

app->start;

# ── Inline templates ──────────────────────────────────────────────────────────
__DATA__
@@ layouts/main.html.ep


Chromebook Notes


<%= content %>
@@ index.html.ep % layout 'main';

Chromebook Notes

<%= scalar @{ $notes } %> note(s) stored in ~/notes/

% if (!@{$notes}) {

Create your first note →

% } @@ note.html.ep % layout 'main';
% if ($name ne 'new_note') {
% }
Access from Chrome: After running perl notes_web.pl daemon in the Linux terminal, open Chrome on your Chromebook and go to http://localhost:3000. The web app runs on the same machine and the connection is immediate — no network involved.
§14

HTTP Client Scripts

Perl's LWP::UserAgent (or Mojolicious's Mojo::UserAgent) make it easy to fetch web data from your Chromebook scripts.

perlweb_fetch.pl
#!/usr/bin/perl
# =============================================================================
#  web_fetch.pl  —  HTTP client examples using LWP and Mojo
#  Install:  cpanm LWP::UserAgent Mojolicious
#  Run:      perl web_fetch.pl
# =============================================================================
use strict;
use warnings;
use LWP::UserAgent;
use JSON::PP;

my $ua = LWP::UserAgent->new(
    agent   => "PerlChromebook/1.0",
    timeout => 10,
);

# ── Fetch plain text ──────────────────────────────────────────────────────────
print "=== Plain text fetch ===\n";
my $res = $ua->get("https://httpbin.org/get");
if ($res->is_success) {
    my $data = decode_json($res->content);
    printf "Your IP:    %s\n",    $data->{origin}        // "unknown";
    printf "User-Agent: %s\n",    $data->{headers}{Host} // "";
} else {
    print "Error: ", $res->status_line, "\n";
}

# ── Fetch and parse JSON API ──────────────────────────────────────────────────
print "\n=== JSON API ===\n";
$res = $ua->get("https://api.github.com/repos/torvalds/linux");
if ($res->is_success) {
    my $repo = decode_json($res->content);
    printf "Repo:        %s\n", $repo->{full_name}    // "";
    printf "Stars:       %d\n", $repo->{stargazers_count} // 0;
    printf "Description: %s\n", $repo->{description}  // "";
} else {
    print "Error: ", $res->status_line, "\n";
}

# ── Save a file ───────────────────────────────────────────────────────────────
print "\n=== Download file ===\n";
my $url  = "https://raw.githubusercontent.com/nicowillis/nicowillis/main/README.md";
my $path = "/tmp/fetched.txt";
$res = $ua->get($url);
if ($res->is_success) {
    open(my $fh, ">", $path) or die $!;
    print $fh $res->content;
    close $fh;
    printf "Saved %d bytes to %s\n", -s $path, $path;
} else {
    # Graceful fallback if URL doesn't exist
    printf "Skipped download (status: %s)\n", $res->status_line;
}

# ── Using Mojo::UserAgent (async-capable) ─────────────────────────────────────
print "\n=== Mojo::UserAgent ===\n";
eval {
    require Mojo::UserAgent;
    my $mua  = Mojo::UserAgent->new;
    my $json = $mua->get("https://httpbin.org/uuid")->result->json;
    printf "UUID from Mojo::UA: %s\n", $json->{uuid} // "(unavailable)";
};
print "Mojo::UserAgent not installed\n" if $@;
§15

Termux — Perl via Android Apps

Termux is a terminal emulator and Linux environment for Android. On Chromebooks that have the Google Play Store but do not support the Linux container (or as a supplement), Termux gives you a functional Perl environment in a terminal — but without GUI support.

Limitation: Termux runs as an Android app. There is no X11 display available, so Perl/Tk, Gtk3, and other GUI toolkits do not work. You can run CLI scripts and web servers only. Use the Linux container for GUI development.

Install and Configure Termux

bash — inside Termux
# Install Termux from: https://f-droid.org/packages/com.termux/
# (F-Droid version is maintained; Play Store version is outdated)

# Once inside Termux:
pkg update && pkg upgrade -y

# Install Perl
pkg install perl

# Verify
perl -v     # Should show v5.36 or newer

# Install build tools for CPAN modules
pkg install build-essential openssl libcrypt

# Install cpanminus
curl -L https://cpanmin.us | perl - App::cpanminus
# or:
pkg install perl-cpanminus

# Install common modules
cpanm Modern::Perl Try::Tiny JSON::PP LWP::UserAgent

# For web development — Mojolicious works in Termux!
cpanm Mojolicious

Termux + Mojolicious Web Server

A Mojolicious app in Termux is accessible from Chrome browser running on the same Chromebook at http://localhost:3000 — a nice workflow for web development without the Linux container.

bash — run from Termux
# Save the minimal web app to ~/app.pl
# (use nano app.pl or termux-open to edit)
perl app.pl daemon   # Starts server

# Access from Chrome on the Chromebook:
# http://localhost:3000

Useful Termux Extras

bash
# Share files with ChromeOS via Termux's storage
termux-setup-storage   # Sets up ~/storage/ pointing to Android shared storage

# Your Chromebook Downloads:
ls ~/storage/downloads/
cp ~/storage/downloads/script.pl ~/

# Use a proper terminal font and color scheme
pkg install termux-styling   # Sets colors and fonts

# SSH into your Chromebook's Linux container from Termux:
pkg install openssh
ssh penguin@100.115.92.202   # IP of the Linux container (check ifconfig)
§16

Cloud IDEs — Browser-Based Perl

For quick experiments, sharing code, or using a Chromebook with limited storage, browser-based IDEs work well for Perl. These require internet access but need no local installation.

Replit

bash — replit workflow
# 1. Go to https://replit.com and sign in
# 2. Click "+ Create Repl" → choose "Perl" as language
# 3. Write and run Perl in the browser — full CPAN access via replit.nix

# .replit config for Perl:
# language = "perl5"
# run = "perl main.pl"

GitHub Codespaces

bash — in a Codespace terminal
# Codespaces runs Ubuntu with Perl pre-installed
# 1. Go to github.com → any repo → click "Code" → "Codespaces" → "+"
# 2. In the Codespace terminal:
perl -v
sudo apt install -y cpanminus perl-tk
cpanm Mojolicious

# Configure your .devcontainer/devcontainer.json for Perl:
# {
#   "image": "mcr.microsoft.com/devcontainers/base:debian",
#   "features": {},
#   "postCreateCommand": "sudo apt-get install -y perl cpanminus"
# }

Comparison

ServicePerl VersionCPANOfflineFree tier
Replit5.32+LimitedNoYes
GitHub Codespaces5.34+Full via apt+cpanmNo60h/month
Gitpod5.30+FullNo50h/month
play.perl.orgLatestSelected modulesNoYes (online REPL)
Recommendation: Use the Linux container for any serious work. Cloud IDEs are best for quick experiments, learning, and sharing code snippets with others.
§17

Tips & Best Practices

ChromeOS + Linux Integration

  • Right-click any .pl file in the Files app → Open with → Linux apps to run or edit it directly.
  • Pin the Terminal to the ChromeOS shelf for one-click access.
  • Files in ~/ show as Linux files in the ChromeOS Files app — drag and drop works between Linux and ChromeOS.
  • To increase the Linux disk size: Settings → Advanced → Developers → Linux → Disk size.
  • Linux apps can be added to the ChromeOS launcher: right-click the app icon in the shelf → Pin to shelf.

Performance Tips

  • Chromebooks with 4 GB RAM: avoid running both the Linux container and many Chrome tabs simultaneously.
  • Use cpanm --notest Module::Name to skip test suites — much faster module installs.
  • For large file processing, use File::Find instead of globbing, and process files line-by-line instead of slurping.
  • Suspend/resume the Linux container without losing state: Settings → Advanced → Developers → Linux → Shut down Linux — or just close all Linux windows.

ARM Chromebooks (Snapdragon / MediaTek)

  • The container runs aarch64 (ARM64) Linux — all Perl and most CPAN modules are supported.
  • If cpanm fails to compile a module with XS (C) code, try sudo apt install libmodulename-perl first for the Debian pre-built version.
  • Perl/Tk (perl-tk) and libgtk3-perl both have ARM64 Debian packages — they install and run correctly.

Backup Your Perl Environment

bash
# List all manually installed apt packages (save this list)
apt list --manual-installed 2>/dev/null | grep -v "automatic" > ~/installed_packages.txt

# List all cpanm-installed modules
cpanm --list-installed > ~/cpan_modules.txt

# Backup your Perl projects to ChromeOS Downloads
cp -r ~/perl_projects /mnt/chromeos/MyFiles/Downloads/perl_backup_$(date +%Y%m%d)/
§18

Quick Cheat Sheet

bash — setup
# Full Perl setup for ChromeOS Linux (run once after enabling Linux):
sudo apt update && sudo apt upgrade -y
sudo apt install -y perl perl-doc cpanminus build-essential \
    perl-tk libgtk3-perl libwx-perl libglib-perl wget code
cpanm Mojolicious Modern::Perl Try::Tiny Path::Tiny \
    DateTime JSON::PP LWP::UserAgent Text::CSV DBI DBD::SQLite
bash — daily commands
perl script.pl             # Run
perl -c script.pl          # Syntax check
perl -d script.pl          # Debug
perl -e 'say "hi"'         # One-liner (needs Modern::Perl or use feature)
code .                     # Open VS Code in current directory
reply                      # Perl REPL

# GUI apps — DISPLAY is set automatically in ChromeOS Terminal
perl tk_notes.pl           # Opens as ChromeOS window via XWayland
perl gtk3_sysmon.pl        # Same

# Web server
perl app.pl daemon         # Start at localhost:3000
perl app.pl daemon -l http://*:8080  # Custom port
perl — modern boilerplate
#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use open ':std', ':encoding(UTF-8)';
use feature qw(say state);  # or: use Modern::Perl;

# ... your code ...
GoalCommand
Check if Linux enabledSettings → Advanced → Developers
Open Linux terminalLauncher → Terminal (penguin icon)
Open Linux filesFiles app → Linux files
Share Downloads with LinuxFiles → right-click Downloads → Share with Linux
Run a GUI Perl appperl myapp.pl (window appears in ChromeOS)
Run web serverperl app.pl daemon → Chrome → localhost:3000
Pin Linux app to shelfRight-click app in shelf → Pin to shelf
Increase Linux disk sizeSettings → Developers → Linux → Disk size