Skip to content

Repository files navigation

oslo

A POSIX shell in Rust that also speaks Lua, with a structured pipeline that scripts written before it existed provably cannot reach. Linux only.

oslo                       # a prompt
oslo script.sh arg1        # run a shell script
oslo build.lua             # a Lua script — same command, no flag
oslo -c 'echo hello'       # run a command

oslo

Every feature has its own page and its own recording in docs/features, each made by a script in scripts/demo so it can be made again after the code changes. This page is the tour; those pages are the reference.


Two languages, one prompt

Shift+Tab switches between shell and Lua in place — your line, your cursor and your history stay where they are. Each language keeps its own history, completion and colouring.

bresilla@tron | I | sh  > ls -la | grep rust
bresilla@tron | I | lua > for _, f in ipairs(sh.ls(".")) do print(f.name) end

A ! prefix runs one Lua line from a shell prompt without changing mode. History keeps what it always had — !!, !$, !5, !-2, !1:2..4 — and a space is how you say you meant Lua: !5 is event five, ! 5 + 5 is ten. → two-languages-one-prompt.md

Structured pipelines

A command produces two things: text for you, rows for the next command. The pipe decides which before anything runs, by reading what each stage declares — never by looking at the bytes.

df | where 'free < 1e9' | sort-by free
ps | where 'not is_kernel' | first 5 | cols name
ls | where 'size > 1000' | sort-by size | cols name size_human
ps | group-by is_kernel | count                      # and distinct, stats
cat /etc/passwd | parse '{user}:{x}:{uid}:{rest}' | where 'uid > 1000' | get user

Filters are Lua, not a dialect invented for the occasion, so the escape hatch is the same language as the filter. An ordinary command may sit at either end — where the tools stop, what they made is handed on as bytes:

kubectl get pods -o json | from json | where 'status.phase == "Running"' | cols name
ps | first 5 | to json | jq .

structured-pipelines.md

Stream coordinates

A stage can address what the stage before it printed, by position — the job xargs exists for, without xargs:

cat hosts.txt | ssh {0:0} uptime               # line 0, word 0 of what `cat` printed
cat hosts.txt | ping {*:0}                     # word 0 of every line — one process, many arguments
cat one.txt   | echo "ran {%0:0} on {%0:1}"    # {%n} is the stage; {n} is what it printed

Every value arrives as one argument, so a filename with a space stays one filename. → stream-coordinates.md

POSIX, where it counts

432 corpus scripts run under oslo and under bash and compared byte for byte, plus a rule that every extension oslo added is unreachable from shell written before oslo existed. That second half is a build failure rather than a promise: tests/posix_stays_on_the_byte_path.rs runs the whole corpus and requires zero structured edges. → posix-fidelity.md

The rest

The prompt named segments with priorities, gathered once — and a segment may animate
What a line leaves behind the prompt replaced by what was run, drawable by another program
The line editor oslo owns the row it edits — buffer, layout, redraw, keymaps
Ghost suggestions the grey continuation, five sources you order yourself
Prediction and repair a model of what you run: what comes next, and what you meant
Completion the dropdown, matching as a transform rather than a prefix test, and carapace specs
The Lua interpreter Lua in pure Rust — what lets a static musl binary speak it with no C toolchain
Your own tools register_tool, builtins and autoloaded functions from Lua
Hooks thirty-two moments a config can attach to
Timers oslo.after and oslo.every — the only things that mean "later"
Asking for something thirteen widgets, at a prompt, from any shell, or from Lua
The terminal what oslo tells the terminal, and what it asks it
What gets written down the log, the outcomes, and what is deliberately not recorded
The history finder full-screen search with scopes that narrow and widen
Profiles · syncing keeping an agent's commands out of yours; two machines agreeing
Where you have been directory tracking, cd -N, cd root
Directory environments .env.lua per project, with an allow gate and an undo record
nix, as data · the calculator every nix --json answer as a Lua table; math '3 km in miles'
rm, and the things that can bite recoverable at the prompt, POSIX in a script
Scratches · plugins · secrets sessions that outlive a terminal; somebody else's Lua; values kept encrypted
Colours · drawing · nav every role settable; the output widgets; the filesystem navigator
Abbreviations · macros · argc gco becomes git checkout ; one store for all of it; options declared in comments
Interrupt escape · runtime features the job that will not take a Ctrl-C; turning things off at runtime
The control socket another program asking this shell a question — or moving it — in Lua, bound only when asked

Configuration

One file, ~/.config/oslo/init.lua, and it is Lua rather than a settings dialect:

oslo.suggest.sh_sources = { "history", "completion", "path" }
oslo.completion.fuzzy   = "smart"
oslo.keys["ctrl-g"]     = function(line) return line.text .. " --help" end
oslo.on.pre_cmd(function(c) if c.argv[1] == "rm" then print("careful") end end)

oslo config inspects and edits it. Every setting, hook and key is on the page for the feature it belongs to; the table above is the map.

This repository carries its own copy in config/, and make configs installs it — config/* becomes ~/.config/oslo/*. That is the one command between editing the config in a checkout and the shell reading it.

The hooks

Thirty-two moments a config can attach to, named pre-, post- or on-. oslo hook list prints the current set; hooks.md says what each one is handed and what a return value means.

Tools

Twelve of them — macros, config, profile, history, direnv, make, hook, lua-api, plugin, scratch, userin, secret — each with its own help. A script of the same name always wins.

Building

You do not have oslo yet, so the build is a script:

scripts/build.sh            # static musl release, every feature — the binary to use
scripts/build.sh --minimal  # static release, none of the optional features
scripts/build.sh --native   # this machine's target, for a quick local binary
nix build                   # the same static musl binary, through the flake

Once you have it, the build is .make.lua and the shell runs it:

oslo make            # every recipe, with what each of them says it does
oslo make build      # the same static release
oslo make dev        # a plain debug build, for iterating
oslo make verify     # fmt, line limits, README paths, tests, clippy, rustdoc — all of it
oslo make install    # to $PREFIX/bin and /usr/bin

At an oslo prompt in this directory, make alone is enough — the builtin hands the word over to the program everywhere else. There is no Makefile: scripts/build.sh exists precisely because .make.lua cannot build the shell that reads it.

Optional features

All ten are off by default, and off for the same reason: a shell that is going to be /bin/sh should carry what every session needs and nothing else. scripts/build.sh turns them on; the published release artifact is the default build.

Each cost is what turning that one feature off takes back out of the full build, measured on the static musl binary — 5,201,664 bytes with none of them, 6,403,264 with all eleven:

feature costs brings
argc +300 KB a script declares its options in comments and the shell parses them; the only one that vendors a parser
vista +297 KB the model: predict as a suggestion source, oslo.repair, oslo.predict.*, and the correction after a mistyped line
direnv +140 KB .env.lua read on arrival in a directory, the direnv builtin, oslo.direnv
secrets +108 KB the filing: oslo secret, several stores, secret run, the lazy variable, the hooks. No crypto of its own
math +96 KB math '3 km in miles' and oslo.math — dimensions, so 3 km + 2 s is a refusal
plugin +80 KB oslo plugin — installing somebody else's Lua. oslo.db and the pre-cmd veto are in every build
crypt +72 KB the built-in mechanism, so a fresh install encrypts without being told anything
nix +48 KB oslo.nix — every nix --json answer as a Lua table, and flake-output completion
scratch +44 KB named sessions that outlive their terminal, and the key that finds them
make +28 KB .make.lua — recipes with dependencies and staleness, the oslo make tool and the make builtin
spec +20 KB a .yaml per command in carapace-spec format, found by name; the completion model it fills is in every build

crypt implies secrets, so the two can only be removed together: 180 KB for the pair.

scripts/build.sh --minimal     # static release, none of them

There are no others, and in particular none that serve the test suite — --all-features turns on exactly the eleven above. A config is written to work either way, because a build without the feature simply does not have the name:

oslo.keys["f4"] = function(line)
  return oslo.repair and oslo.repair(line.text) or line.text
end

Every .rs file is under 600 lines, enforced by scripts/check-loc.sh.

To make it the system /bin/sh — the symlink, the dpkg diversion that survives a dash upgrade, what to check afterwards and how to undo all of it — see docs/default-shell.md.

Known gaps

coproc, select, associative arrays, and process substitution on a system with no /dev/fd. Each is listed with its cause and what to write instead in docs/known-gaps.md, and each one says so rather than quietly doing something else — a syntax error naming the construct, or a builtin refusing the option.

Licence

MIT.

About

POSIX shell that speaks Lua too and has structured pipes

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages