- Rust 97.8%
- Lua 2.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
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
|
||
| .forgejo/workflows | ||
| assets | ||
| examples | ||
| src | ||
| tests | ||
| .gitignore | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
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
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 ›
Built With
- Rust
- ratatui and crossterm
- tokio and tokio-tungstenite
- rustls and reqwest
- mlua, for the Lua configuration
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:
- Open the address and sign in to Lurker.
- Approve luir. The page shows a code.
- 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 loginapproves luir again, replacing the token it holds.luir logoutrevokes 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.
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+1 … alt+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+1 … alt+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.
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
kindare logged and dropped. - The resume cursor excludes the system buffer, whose ids live in a
separate space and would corrupt
?since. ItsnetworkIdis 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.
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.
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.
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.
License
Distributed under the MIT License. See LICENSE for more
information.
Contact
Project link: https://git.xfnet.org/jnewing/luir
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