A Rust TUI for lurker. https://luir.org
  • Rust 97.8%
  • Lua 2.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
J. Newing ada66527ef
All checks were successful
release / prepare (push) Successful in 2s
release / build (linux-arm64, aarch64-unknown-linux-gnu) (push) Successful in 2m15s
release / build (linux-arm64-static, aarch64-unknown-linux-musl) (push) Successful in 2m20s
release / build (linux-x86_64, x86_64-unknown-linux-gnu) (push) Successful in 2m11s
release / build (linux-x86_64-static, x86_64-unknown-linux-musl) (push) Successful in 2m14s
release / build (windows-x86_64, x86_64-pc-windows-gnu) (push) Successful in 2m37s
release / checksums (push) Successful in 4s
bump to 0.4.0
2026-09-18 10:30:48 -04:00
.forgejo/workflows build each target in its own toolchain image 2026-09-09 10:58:17 -04:00
assets fold the system buffer into the luir row 2026-09-15 09:39:59 -04:00
examples banners are just .ans files now 2026-09-12 16:19:25 -04:00
src stop history from messing up the nick list (#10) 2026-09-18 10:26:33 -04:00
tests stop history from messing up the nick list (#10) 2026-09-18 10:26:33 -04:00
.gitignore initial commit 2026-08-18 14:00:29 -04:00
build.rs notice a commit when stamping the version 2026-09-09 11:30:18 -04:00
Cargo.lock bump to 0.4.0 2026-09-18 10:30:48 -04:00
Cargo.toml bump to 0.4.0 2026-09-18 10:30:48 -04:00
LICENSE tidy the version on a tag, and add the licence 2026-09-09 11:29:08 -04:00
README.md op, voice and kick from the member list 2026-09-18 10:23:25 -04:00

MIT License Rust Protocol Platforms

luir

A clean, modern terminal client for Lurker, written in Rust.
The name is what it is: a Lurker UI, in Rust.

Read the docs · Get started · Try the demo

Table of Contents
  1. About The Project
  2. Getting Started
  3. Usage
  4. Protocol notes
  5. Development
  6. Roadmap
  7. Contributing
  8. License
  9. Contact
  10. Acknowledgments

About The Project

luir speaks the Lurker client protocol (v1) over WebSocket: server-authoritative read state, resume-on-reconnect from the last message id seen, backlog paging, drafts synced across devices, typing indicators, and nicklists.

                  panel_style = "boxes"  (the default)
╭─ networks ─────────────╮╭─ topic ─────────────────────────╮╭─ 5 members ──╮
│  luir                  ││ Lurker, a modern IRC bouncer    ││  @avery      │
│                        │╰─────────────────────────────────╯│  +kestrel    │
│ ■ libera (gold.libera… │╭─ #lurker ───────────────────────╮│   juniper    │
│   #lurker         ⠈    ││ 14:28:07  (avery) morning, the  ││   rowan      │
│   #rust        ⠉  ⠈  3 ││                   backlog       ││   you        │
│   ferris          ⠈  1 ││ 14:38:02        * kestrel edits ││              │
╰────────────────────────╯╰─────────────────────────────────╯╰──────────────╯
╭───────────────────────────────────────────────────────────────────────────╮
│ [15:12] [you] [Lag: 38ms ⣀⣀⣀⣀⣀⣀⠔⠑⢄⣀⣀⣀] [#lurker@libera]        [Act: 4,5] │
│ #lurker                                                                  │
╰───────────────────────────────────────────────────────────────────────────╯
                            panel_style = "rules"
╌ networks ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ #lurker ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ 5 members ╌╌
  luir                   ╎ Lurker, a modern IRC bouncer · docs.lurker.chat ╎  @avery
                         ╎╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎  +kestrel
 ■ libera (gold.libera…  ╎ 14:28:07        (avery) morning, the new        ╎   juniper
   #lurker         ⠈     ╎                         backlog resume landed   ╎   rowan
   #rust        ⠉  ⠈   3 ╎ 14:29:31      (kestrel) does it still cap at    ╎   you
   ferris          ⠈   1 ╎                         500?                    ╎
                         ╎ 14:38:02              * kestrel updates it      ╎
 □ oftc (kiwi.oftc.net)  ╎─────────────── new ─────────────────────────────╎
   #debian               ╎ 15:12:44      (kestrel) got it, thanks          ╎
 [15:12] [you] [Lag: 38ms ⣀⣀⣀⣀⣀⣀⠔⠑⢄⣀⣀⣀] [#lurker@libera]     [Act: 4,5]
 #lurker 

(back to top)

Built With

(back to top)

Getting Started

Prerequisites

Nothing, if you take a prebuilt binary. To build from source you need a Rust toolchain (1.98 or newer) and a C toolchain with CMake, which aws-lc-sys needs for the TLS backend and mlua needs to compile Lua.

Installation

Prebuilt binaries are attached to every release:

File For
luir-<version>-linux-x86_64.tar.gz Most Linux, glibc 2.18 or newer
luir-<version>-linux-x86_64-static.tar.gz Any Linux, statically linked
luir-<version>-linux-arm64.tar.gz Raspberry Pi 4/5, ARM servers
luir-<version>-linux-arm64-static.tar.gz The same, statically linked
luir-<version>-windows-x86_64.zip Windows 10 and 11

Unpack it and put luir somewhere on your PATH. SHA256SUMS is attached alongside them.

Or build it yourself:

git clone https://git.xfnet.org/jnewing/luir.git
cd luir
cargo install --path .

First run

luir

The first run writes an annotated config into ~/.config/luir/, split by concern:

config.lua   pulls the rest together, and holds `behavior`
server.lua   which Lurker to talk to, and how to log in
ui.lua       layout, marks, borders, spacing
theme.lua    colours
status.lua   the status bar
keys.lua     bindings and aliases

Set url in server.lua, then run it again.

The config is Lua: it runs at start-up and returns a table. config.lua requires the others, and the config directory is on package.path, so you can split it however you like, or collapse it back into one file:

return {
  server = require("server"),
  ui     = require("ui"),
  theme  = require("theme"),
}

Every option is documented inline in the file it lives in, which is the reference: there is no second copy here to fall out of date with it.

Being Lua means it can compute. theme.lua names its colours once and builds the rest from them; server.lua can read a password from the environment. Unknown keys are still rejected with the offending name and the valid alternatives, a syntax error reports its line, and a missing require names the file.

Logging in

On Lurker 2.3.0 or newer there is nothing to set up. The first time luir connects, it prints an address, tries to open it in your browser, and asks for a code:

  1. Open the address and sign in to Lurker.
  2. Approve luir. The page shows a code.
  3. Paste the code into luir.

That approves luir with OAuth, so it never handles your password. The token it is given never expires, and luir keeps it in auth.json beside the config, readable only by you. It works over SSH too: nothing has to reach back to the machine luir runs on, so open the address wherever you have a browser.

  • luir login approves luir again, replacing the token it holds.
  • luir logout revokes the token on the server and forgets it.
  • Revoking luir in Lurker under Settings → Authorized apps works too. luir notices on its next start and asks for approval again.

Older servers have no browser sign-in, so luir falls back to a username and password in server.lua, and mints a 30-day session token on every start:

username = "you",
password = "hunter2",

The same fallback applies on a newer server when luir starts with no terminal to ask in, such as from a script.

A token you already have takes priority over all of this: LURKER_TOKEN in the environment, server.token, or server.token_command, which runs any shell command that prints one (sh -c, or cmd.exe on Windows). API tokens from /settings/api-tokens fail with a 401, because they authenticate the MCP endpoint only.

Try it without a server

luir --demo

Runs the real interface against synthetic data. Useful for tuning a theme.

(back to top)

Usage

Keys

F1 shows the full list, and luir --list-actions prints it with whatever you have rebound.

Key
ctrl+k Jump to a buffer by name
alt+e Pick an emoticon and drop it in the composer
alt+t Pick a colour scheme, previewed live
alt+n Add a network
alt+shift+n Edit the selected network
alt+↑ / alt+↓ Previous / next buffer
alt+a Next buffer with unread activity
alt+← / alt+→, or alt+, / alt+. Previous / next network
pgup / pgdn Scroll, fetching older history at the top
alt+pgup / alt+pgdn Scroll the member list
alt+m Move the highlight into the member list
alt+shift+f Favourite this buffer, or drop it
F8 Show or hide the favourites sections
tab Complete a nick, channel, or command
F2 / F3 / F4 Toggle the sidebar, member list, topic
alt+1alt+9 Jump to a buffer by its sidebar number
F7 Connection charts: lag per network, and the outbound frame rate
F5 Reload the config file
ctrl+q Quit

Inside tmux, alt+← and alt+→ may never reach luir: a common tmux setup binds them to switching sessions without the prefix. alt+, and alt+. do the same job and are left alone. To check what tmux holds, run tmux list-keys -T root | grep -E 'M-(Left|Right)'. To give the arrows back, put unbind -n M-Left and unbind -n M-Right in .tmux.conf.

The composer takes the usual readline keys: ctrl+a/ctrl+e, ctrl+u/ctrl+k, ctrl+w, alt+b/alt+f, and / for input history.

Scroll position is anchored to a message, not to a line offset, so arriving messages and history pages loaded above never move the text you are reading.

The member list scrolls on its own, with alt+pgup and alt+pgdn or the wheel while the pointer is over it, and returns to the top when you change channel. Set ui.scrollbars to auto for a track beside it.

alt+m, or a click on a nick, moves the highlight into the list and gives it the keyboard. The status bar names the keys while it has them:

Key
, j k, tab Move the highlight
pgup / pgdn, home / end, g / G Move it further
enter, m Open a direct message with them
w /whois them
i Put their nick in the composer
o Op them, or deop them if they are
v Voice them, or devoice them if they are
K Start /kick <nick> in the composer, to add a reason and send
esc, q Give the keyboard back

o needs ops in that channel, and v and K need ops or halfops, so the status bar only names them to someone who has what they need.

Every action hands the keyboard back, since each of them ends with you typing or reading somewhere else, except o and v: a channel is often sorted out a few people at a time. The highlight stays where you left it until you change channel.

Commands

/help lists them all; /help join explains one.

/join /part /msg /me /notice /emote /nick /topic /away /back /addnetwork /whois /kick /ctcp /raw /names /list /buffer /close /clear /mark /markall /pin /unpin /note /mode /connect /disconnect /reconnect /delnetwork /highlight /unhighlight /theme /version /quit

Start a line with // to send text that begins with a slash.

/whois draws its reply as a block, in whichever buffer you asked from:

+--[ whois: synmuffin ]-----------------------------------
| host       synmuffin!~muffin@example.net
| name       Muffin of Syn
| account    synmuffin
| flags      bot, secure
| server     pine.fxnet.org (FXNet Montreal)
| channels   @#lurker #rust #chat
| idle       15m
| signed on  2026-01-01 14:02
+---------------------------------------------------------

It is open on the right, so a value too long for the pane wraps under its own column instead of breaking a border. behavior.whois_to sends it somewhere else if you would rather: "server" for the network's server buffer, or the name of a buffer to collect every one of them.

/part leaves the channel but keeps the window, so the scrollback is still there to read. The composer says #chat (parted) while you are off it, and anything typed is refused in that window rather than sent and bounced back as not_on_channel somewhere else. /join with no argument is not a thing, so the refusal names the channel: /join #chat puts you back. Add /close to drop the window entirely once you are done with it.

Themes

alt+t opens the theme picker. Type to filter, arrow up and down, and the whole client is drawn in whichever scheme the highlight is on, so you are looking at your own channels in it rather than at a swatch. Enter keeps it, escape puts back what you had.

Ninety-eight of them: luir's own, and the whole of NvChad's set under the names NvChad uses, so a theme you know from there is here under the name you know it by. Both separators work, since NvChad is not consistent about which it uses, and the names luir shipped before still resolve: dracula finds chadracula, monokai finds monekai, solarized finds solarized_dark. /theme <name> sets one without opening the picker.

Each is a base16 palette, mapped into luir's slots the same way for every scheme, so they stay coherent with each other and porting another one is a matter of sixteen colours. The palettes are NvChad's base_16 tables verbatim, which is what their own highlighting is built from, so a scheme here is the same palette it is there. It is a palette rather than a pixel match, though: luir has a sidebar and a nicklist where an editor has syntax, and the slots are spent differently.

The picker writes your choice to state.json beside the config, because the config is Lua with your own comments in it and this client is not going to rewrite that. The saved choice wins over theme.name in theme.lua, and says so on startup when the two disagree. /theme reset forgets it and hands the decision back to the config file.

A scheme is only a starting point. Anything set in theme.lua is laid over the top, so you can take a scheme and change the two colours you disagree with. The shipped theme.lua therefore sets nothing at all and is entirely comments: an empty table is what lets a scheme through.

If you have a theme.lua from before the schemes existed, it spells out all twenty-two colours, and those win, so the picker will appear to do nothing. The picker says so in its footer and names the count. Comment the colours out to get it back, or keep the ones you actually chose:

return {
  name   = "gruvbox",
  accent = "#7aa2f7",
}

Setting background = "default" is worth knowing about: it lets the terminal's own background through, so a transparent terminal stays transparent. The bundled schemes name a background instead, since that is much of what makes them look like themselves.

Banners

The client log opens with a banner, a different one each start by default. Each is a plain .ans file of ANSI art in the sixteen classic colours, and luir draws every colour as the theme colour that plays the same part: blue as the accent, red as the error colour, green as success, with the bright half lightened from them. So a banner follows whichever theme you pick.

Twelve are bundled: acid, chrome, circuit, fire, graffiti, halftone, inverse, neon, outline, overgrown, scanline and solid. banner_style in ui.lua names one, or stays random.

To add your own, save a .ans file in a banners folder beside the config, ~/.config/luir/banners/mine.ans say, and it joins the random pick or can be named as mine. One named like a bundled banner replaces it. Any ANSI editor will do, PabloDraw or Moebius for instance. Files in UTF-8 and in code page 437 both read, a row ends at a line break, and colours beyond the sixteen are ignored. startup_banner = false turns the banner off.

Emoticons and aliases

alt+e opens a picker over the bundled set (the shrug, the table flip, Lenny, and over three hundred others), filtered as you type. It searches keywords as well as names, so angry finds (╯°□°)╯︵ ┻━┻ even though nothing is called that. Enter drops the art into the composer at the cursor rather than sending it, since it is usually the middle of a sentence.

/emote <name> sends one outright, and takes text alongside it: /emote flip table nope sends nope (╯°□°)╯︵ ┻━┻.

Your own go in keys.lua, merged over the bundled set by name:

local emotes = {
  ["my shrug"] = \_(ツ)_/¯",
}

Aliases take $1..$9 and $*. A bundled set ships with the client, the services shorthands among them, and /help lists whatever is in force. Your own go in keys.lua, merged over the bundled set by name:

local aliases = {
  shrug = "/me shrugs",
  w     = "/whois $1",
}

Favourites

alt+shift+f on a channel or a direct message puts it in a section at the top of the sidebar, under the client log and the system buffer. Lurker keeps one flag and labels it two ways, so channels collect under favorites and people under friends, matching the web UI. /favorite and /unfavorite do the same thing by name.

The list lives on the server, so it is the same list the web UI shows and it survives a restart. It arrives once, in the connect burst, ahead of the buffers it names: a direct message is not in the snapshot at all and turns up later out of backlog. So the list is kept as the server sent it and matched again as buffers appear, rather than being resolved once and discarded. Nothing moves on screen until the server confirms it, which is the same rule the rest of the client follows. Closing a buffer takes it out of the list.

A favourite stays listed under its network as well, so the section is a shortcut rather than a move. When two networks have a channel of the same name, the section names the network beside each of them.

alt+↑ / alt+↓ walk the sidebar row by row, exactly as it is drawn. A favourite is drawn twice, so it is two stops: once in its section and once under its network. Only the row you are on is highlighted. The sidebar numbers behind alt+1alt+9 are row numbers and follow the same order.

/favorites reports what the server last sent and what came of it, which is the way to tell a list that never mentioned a buffer from one that mentioned it and could not be matched up.

F8 hides both sections, and ui.show_favorites sets where they start, on by default. Hiding them only hides them: a favourite is still a favourite, still listed under its network, and still a favourite in the web UI.

Mentions

Every message that highlights you is gathered into a mentions buffer near the top of the sidebar, from every network and channel, and each line says where it was said. Its count goes up while you are somewhere else and clears when you look.

What counts as a mention is Lurker's call rather than luir's: your nick, plus the highlight rules on your account, the same ones the web UI uses.

/highlight lists those rules, and adds one when given some text. The text matches anywhere in a message, unless -full makes it whole words or -regexp a regular expression. -matchcase, -network for just this network, and -channels #a,#b narrow it down, and -mask nick!user@host matches on who said it instead. /unhighlight removes one, by its number in the list or by its text. The rule for your own nick is Lurker's, and stays.

/highlight luir
/highlight -full -network deploy
/unhighlight 2

Mentions from before luir started come from the server's highlights feed, and scrolling up pages further back through it. A server without the feed still gets the ones that arrive while you are connected, and the buffer says so.

With nothing typed in the mentions buffer, and pick a mention and enter goes to where it was said. A click does the same. The channel opens with the mention a third of the way down, under what led up to it. One from further back than the channel has loaded brings in the conversation around it, and that stretch is held apart from new messages until you scroll down to them, or ctrl+end goes straight back to the newest.

ui.mentions = false goes without the buffer, and ui.mention_jump = false leaves the arrows, enter, and the mouse as they are everywhere else.

Charts

Three of them, all braille, all from readings the client already had:

  • {lagchart} in the status bar: the recent lag, one row.
  • {sendrate} in the status bar: frames going out, drawn only while something is going out. Colour is against the client's own send limit, so a large paste turns it red as the rate limiter starts holding frames back.
  • sidebar_activity: a dot per minute that had messages in it beside each channel and DM, so the busy ones are visible without opening them.

F7 opens the same data with room to read it: a chart per network and one for the outbound rate, six rows each. Six rows of braille carry twenty-four levels against a single row's four, which is the difference between knowing a spike happened and knowing how big it was.

Classic mode

style = "classic" in ui.lua switches the scrollback and status line to the ircII/BitchX conventions, while keeping the panels, scroll anchoring, and everything else:

[15:44] *** avery (avery@example.net) has joined #lurker
[15:44] <avery> morning, the new backlog resume landed
[15:45] * kestrel updates the client
[15:46] *** Signoff: rowan (Ping timeout)
 [15:46] [you(+iwx)] [#lurker(+nt)@libera] [Lag: 38ms] [Act: 4,5]

[Act:] lists the buffers with unread traffic by their sidebar number, and buffer_numbers = true shows those numbers in the sidebar so alt+4 takes you straight there. theme.lua carries a matching ANSI palette, commented out, that inherits your terminal's own colours.

The sidebar and nick list toggle with F2 and F3 in either style.

Networks

Each network is one row in the sidebar, ■ FXNet (core.fxnet.org): the network's name, the server it is connected to, and a mark coloured by whether it is connected. The row is selectable and opens the server's own messages, notices and numerics, and it is the row alt+shift+n edits. The host gives way first in a narrow pane, since the name is what tells two networks apart.

It leads its network even over a pinned channel, because it is the heading rather than a row beneath one. merge_server_row = false in ui.lua puts the name back on a heading of its own, with the server as the first row under it.

Lurker's own system buffer is folded away the same way, into the luir row: the client's log and the server's system messages in one scrollback, in the order they happened, with both their unread counts on the row. Reading it reads both. merge_system_row = false gives system its own row again.

Adding one

alt+n, or /addnetwork, opens a dialog with the fields Lurker's own asks for: name, host, port, TLS, nick, real name and SASL, with a server password, channels to join, commands to run on connect and the two connection toggles folded away under advanced options. The examples in it are FXNet, irc.fxnet.org on 6697, where this client was written and tested.

Tab and the arrows move, space flips a toggle or folds a section, ctrl+s saves from anywhere, and esc closes without sending. What is folded away is still submitted: a default you never unfolded is still the value. Enter moves to the next field rather than submitting, so a stray keystroke cannot fire the request; the buttons at the foot are what submits.

The commands list is built a line at a time: enter commits the line you are typing and starts another, backspace on an empty one takes the previous back to edit. They are raw IRC lines rather than slash commands, and WAIT 15 pauses that many seconds before the next.

Empty optional fields are left out of the request rather than sent as empty strings, so the server's own defaults still apply. The network is created over POST /api/networks and connects immediately.

Editing one

alt+shift+n opens the same dialog on a network you already have, filled in from the server's copy of it. It works from the network's own row in the sidebar and nowhere else: the form writes straight back to whatever it was opened on, and one row meaning one network is a clearer rule to hold than "whichever network the thing you are looking at happens to be on". Naming one outright works from anywhere, with /editnetwork <name>.

Only the fields you change are sent, over PATCH /api/networks/:id. That cuts both ways: emptying a field clears it, and a field the server never told us about is left alone rather than cleared by a blank the dialog invented. Passwords are never sent back to a client, so they show as unchanged and stay that way unless you type a new one. Changes apply on the next connection.

Deleting one

/delnetwork <name>, or the Delete network button at the foot of the editor, opens a confirmation that will not submit until the network's name is typed back into it. That is deliberate: DELETE /api/networks/:id is final, and the protocol does not say what becomes of the buffers, history and read state that hang off the network. Nothing is sent until the name matches.

To stop a network without losing it, /disconnect <name> is what you want. It quits with luir <version> https://luir.org, so the people in your channels see what you were on. Closing luir itself quits nothing, since Lurker stays connected for you. /reconnect and deleting a network quit with Lurker's own reasons, which the server does not let a client change.

(back to top)

Protocol notes

A few rules from the specification that shape the client's behaviour, in case they surprise you:

  • Nothing is rendered optimistically. A message you send appears only when the server echoes it back with self: true. If a send fails, the text is handed back to the composer rather than lost.
  • Read state is server-authoritative. Unread and highlight counts are rendered exactly as the server reports them; the client never counts locally.
  • Unknown frames and fields are ignored, so a newer server does not break this client. Frames with an unrecognised kind are logged and dropped.
  • The resume cursor excludes the system buffer, whose ids live in a separate space and would corrupt ?since. Its networkId is null rather than missing, and marking it read means sending that null explicitly; omitting the field is not the same thing.
  • Outbound frames are rate-limited below the server's 40/sec flood control, so a large paste cannot cost you the session.

The client tolerates variation in the tail fields of event types the documentation describes but does not fully enumerate: a kick victim or a renamed nick is looked up under several plausible field names and degrades to a readable line if none match.

Real servers vary from the documented frames in ways worth knowing if you are writing another client. A standalone instance sends networkId where the docs say id, channels where they say buffers, an away object rather than a string, and member modes as a list of letters rather than a prefix string. All four are accepted, alongside the documented spellings, and tests/protocol.rs pins both shapes.

(back to top)

Development

cargo test          # unit tests plus rendered-layout snapshots
cargo clippy --all-targets
luir --demo --log-file /tmp/lurker.log

Layout tests render the real widget tree to an off-screen buffer, so a regression in wrapping, scrolling, or pane sizing fails the suite.

Working out what a server is doing

luir --dbg writes everything to luir-debug.log beside the config, and names the path in the client log at startup. It records every frame in and out verbatim, with the file and line that logged it, which is what it takes to tell "the server did not answer" from "the server answered with something this client ignores":

grep -- '-> server' luir-debug.log        # what luir sent
grep -- '<- server' luir-debug.log        # what came back
grep favorite luir-debug.log              # one feature's whole conversation

--dbg also opens a debug buffer, right under the client log in the sidebar, holding the same lines as they are written. It saves a second terminal and a tail -f when what you are chasing is something you have to provoke by hand: watch the frames go out as you press the key. The buffer drops the nick gutter, since nobody is speaking in it and log lines are long enough already. The file is still the one to grep; the buffer is for watching. It carries the same scrollback cap as any other buffer, so a long session keeps the newest lines rather than all of them.

luir itself is at trace and everything else at warn, so the TLS and WebSocket crates do not bury the frames. LUIR_LOG overrides that if a narrower filter is wanted, and --log-file alone keeps the old info level. The log carries message content, so read it before sharing it, and that goes for the buffer too: it is on screen, and screenshots travel.

(back to top)

Roadmap

Nothing here is promised. These are the things that have come up and not been built yet.

  • Select and copy text out of a buffer. Mouse capture takes the terminal's own selection away, so this needs solving inside the client.
  • Spell check in the composer, off by default.
  • macOS builds. Blocked on Apple's SDK licence rather than on anything technical, since it cannot be cross-compiled from Linux CI.
  • Caching in the release workflow, to cut the build time.

(back to top)

Contributing

Bug reports are the useful thing right now, and /version prints exactly what a report needs: the build, the client protocol version, and what the server is speaking. luir --dbg gets the detail, though the log carries message content, so read it before sharing it.

If you want to send a patch, cargo test and cargo clippy --all-targets should both be clean, and the layout tests will tell you if a change moved something on screen that you did not mean to move.

(back to top)

License

Distributed under the MIT License. See LICENSE for more information.

(back to top)

Contact

Project link: https://git.xfnet.org/jnewing/luir

(back to top)

Acknowledgments

  • Lurker, for the bouncer and the protocol
  • NvChad, whose base16 palettes are the bundled colour schemes
  • ratatui, which draws all of this
  • ASCIImoji by jiggly, where many of the bundled emoticons come from, under the Beer-Ware License
  • FXNet, where it was written and tested
  • Best-README-Template, which this README follows

(back to top)