Skip to content

Add the omaq-connect terminal client and prepare v1.0.0 - #60

Merged
HANCORE-linux merged 38 commits into
mainfrom
feat/cli
Oct 10, 2026
Merged

HANCORE-linux merged 38 commits into
mainfrom
feat/cli

Conversation

@HANCORE-linux

Copy link
Copy Markdown
Owner

Summary

  • omaq-connect. A terminal client that talks to the same helper as the bar, either beside the bar or on its own. It offers:
    • direct chats in split panes, with receipts, typing state and day dividers;
    • invitations, requests and safety codes;
    • files with progress;
    • emoji, a picker, and :nickname;
    • :help.
  • Helper.
    • It emits throttled file.progress events.
    • It sends a correlated file.failed when a direct accept cannot start, a file.state answer for incoming transfers, and contact.decided for every decision.
    • A cancel from the peer now only drops local state.
    • Protocol stays 16, and the bar ignores the new events.
  • Install and uninstall. install.sh creates ~/.local/bin/omaq-connect, and the uninstaller removes it together with the plugin.
  • Structure. The client is a package of focused modules. tests/cli_arch_test.py checks the import rule.
  • Docs. The README shows the bar plugin and the terminal client side by side, and the release metadata moves to 1.0.0.

Verification

  • make test, make helper, make arch, the no-Signal build, phase 2, phase 8, qmllint, omarchy plugin validate and git diff --check all pass.
  • The client runs 96 unit tests plus the PTY, two-home, split and invite end-to-end tests, also on Python 3.11.
  • A user-like terminal-only run on a second Omarchy machine covered install, disabling the bar, chatting, the refused update and an interactive uninstall that kept all data.

omaq-connect is a text-only OmaQ client for any terminal. It talks to the
helper over the existing socket like Service.qml, so the bar and the helper
stay unchanged.

- Direct chats with online state, typing, receipts and read sync
- Split views, :help, :quit/:qa and suggestions while typing a command
- Starts the helper when none runs and stops only a helper it started
- --link/--unlink manage ~/.local/bin/omaq-connect; the installers link it
  and the uninstaller removes a dangling link
- Tests: unit, pty, and two- and three-identity Tox end-to-end runs; the
  uninstall test now uses an isolated HOME
The composer picker now shows the 22 reaction emoji followed by 🤟
(composerEmojiSet). Reactions stay limited to the 22 emoji the helper
accepts. 🤟 has no bundled image, so the picker uses its existing Noto
Color Emoji text fallback.

Update the reviewed plaintext-policy hashes for pages/ChatPage.qml. The
computed writes and setProperty calls are unchanged.
- Friends: :invite with QR code and clipboard copy, :invite revoke,
  :redeem, :requests, :accept and :decline
- Input history, :clear, 15-second confirmations and a blinking cursor
- Focus marker and name pill, unread highlight and message preview
- Emoticons become emoji, Ctrl+K emoji picker, and emoji widths are
  measured at startup
- Text size shown after a terminal zoom, and rotating tips
- Fixes from the Fable 5.1 review: confirmation expiry, identity_changed
  broadcasts, linear composer scrolling, bar wording and errors, QR
  colors, clear correlation, picker search, friends-bar overflow, long
  links and the invite countdown
- README section, illustrated user guide section and stage notes
- :sendfile <path> sends a file to the open chat. Relative, ~ and quoted
  paths work, and one file per chat travels at a time, as in the bar.
- Incoming files stay paused until accepted: the chat shows the offer,
  :acceptfile saves it in the helper's download folder
  (~/Downloads/omaq by default), and :declinefile rejects it.
- :cancelfile stops an outgoing transfer, waiting for the transfer id
  when needed.
- Results use the bar's wording; the friends bar highlights a friend
  with a pending offer whose chat is off screen.
- Test helpers save downloads inside their own state folder, and the
  two-home test sends and receives real files and compares the bytes.
- A unit test keeps the wording copied from the bar in sync.
Both make test and make test-ci now run tests/cli_unit_test.py, so a
change that breaks omaq-connect fails the regular suite.
- The helper emits file.progress for direct transfers (never avatars)
  with done and size, at most every 250 ms and only when the percentage
  changes, so a file sends at most about 100 events.
- omaq-connect draws it as a bar on the transfer line, for example
  "Sending notes.pdf  ███████░░░ 70%", and keeps the plain line until
  the first event or with an older helper. Events must match the
  transfer id or request and the friend's key.
- The bar ignores the event for now.
- The file transfer test checks the size lookup; the two-home test
  sends a real 1.5 MB file and checks bytes, rising progress, and the
  drawn bar.
Terminal client:
- Input recall, the clear confirmation, offers and sends are bound to
  the contact's stable key. Rebinds, identity changes, locks and
  disconnects drop them, and a nospam change no longer resets state.
- The confirmation prompt stays visible. Notices that arrive behind it
  are shown with its result instead of expiring unseen.
- Messages with unknown delivery, failed messages, and messages that
  were sent but not saved survive history reloads. The bar's note
  "Message sent, but could not be saved locally." is shown.
- A reconnect to the same helper keeps transfers and checks them with
  file.status; incoming offers are confirmed through file.state. File
  commands act on the focused chat only.
- Foreign errors are no longer taken as the client's own. Accept and
  decline use their own request id and the helper's contact.decided
  result.
- A :quit during startup starts no helper, and the handshake is
  finished before quitting. The client says when it cannot confirm
  that OmaQ went offline. Exits are serialized per state directory.
- Small windows wrap safety codes by group and scroll help and
  overlays; Enter never runs a hidden help entry.

Helper:
- A cancel from the peer only drops local state. The extra Tox cancel
  was echoed back by toxcore and killed the next transfer in the slot.
- A failed direct file.accept sends file.failed (accept_failed, or
  transfer_unknown when the offer is gone).
- file.status with an incoming transfer id answers with file.state.
- Every contact.decide ends with a correlated contact.decided event.
- The progress throttle lives in file.c and is cleared when a
  transfer ends.

Tests cover each finding: unit tests, the file transfer C test, the
pty test, and the two-home test, which now resends right after a
decline in both directions.
cli/omaq.py is now only the entry point, with the loader, link
management and main(). The client code lives in cli/omaq_connect/.

The loader imports the package by path, so it also works under
python3 -I. It sets sys.dont_write_bytecode first, so no __pycache__
lands in the installed plugin tree, which the updater requires.

The installer fixture now copies the package, a new test checks that
--help leaves no bytecode cache, and CI compiles cli as well.
These parts move out of the package root unchanged:
- text and width handling
- the helper vocabulary
- palette, key decoding and layout
- the socket link and the presence locks
- terminal probes, helper process paths and system helpers

tests/cli_arch_test.py checks the import direction between modules.
It keeps socket, terminal, process and signal modules out of the pure
ones, and it forbids "from time import"-style imports that tests could
not patch. make test runs it.
The 334-line on_event chain and the command chain become tables from
names to methods. The handler bodies are unchanged. Guard conditions
stay in the handlers, and malformed event names are ignored as before.
FileTransfers owns offers, sends and the last result. It reaches the
rest of the client only through send, say, the friend list and a
reload callback. The friend binding rules become pure functions in
session.py.
Invites owns the invite link, the pending request, the request ids and
the action window. Overlays and the revoke confirmation stay in the
user interface and are reached through callbacks. Error handling keeps
its order.
History merging, finding a sent message, the unsaved-message note and
rebind detection become pure functions with their own tests.
"File transfer successful" now stays in the status line for 7.5
seconds. Warnings keep 15.
Rendering moves into view.py as Screen: the friends bar, panes,
messages, transfers, overlays, help, picker and hint line. Screen
reads state through the app and does no I/O. The command, key and tip
tables move into commands.py.
FileOffer and OutgoingSend replace the offer and send dicts. They keep
their own progress and status rules. Tests change only from item to
attribute access.
Invite and PendingRequest replace the invite and request dicts. The
invite knows whether it is still active.
Confirmation replaces the confirmation dict. It knows when it expires
and what it says when the answer is no. It starts the new modals.py.
Help, Picker and the invite, request and redeemed overlays share
handle(app, kind, value) and rows(screen, width, height). The overlays
share paging and Escape through an Overlay base class.

App.edit keeps the input order: lock, help, confirmation, picker,
overlay.

A help list opened again now starts without the visible range of the
previous one. Enter before its first frame no longer runs a command
whose row has not been drawn yet. This matches the first opening and
F06. A new test pins it.
Command parsing, colon suggestions and tab completion are now pure
functions next to the command tables. Tab completion keeps its state
in a Completion dataclass.
Conversation replaces the per-chat dict for items, loaded state,
history request and typing time.
Chat owns conversations, pending messages, unread counts, read marks,
history retries and clear requests, along with their events. App keeps
panes, focus and notices.

The event table now names the owning object, such as chat.on_history
or files.on_offer, which removes 16 forwarding methods. A test checks
that every table entry resolves.
HelperProcess starts the helper, reports a failed start from its log
and decides whether this client launched the running helper. App keeps
the connect, settle, exit and main loop flow.
Pane now owns typing, cursor keys, deletion and recall of earlier
lines. The layout keys become one table in keys.py. The emoji picker
inserts through the same path.
The properties from the first split are gone. Code and tests now use
app.files and app.invites directly.
App, its tables and its constants move into app.py with explicit
imports. __init__.py only re-exports the modules. The import rule test
covers app.py as the composition root.
The test read Panel.qml, ChatPage.qml and Service.qml without closing
them, so every run printed three ResourceWarnings.
test_r12_small_windows_wrap_codes_and_scroll looked for "ctrl+n" in
the whole frame. The bottom line starts with a random tip, and one of
the 21 tips names ctrl+n, so about one run in 21 failed. The test now
looks for the help text "New pane with the next friend", which no tip
uses, and passes with every initial tip.
:nickname <name> sends the helper's nickname.set with the client's own
request id, like the bar's nickname field. The client first checks the
name the way the bar does: 1 to 18 characters, no control characters.

:nickname alone shows the current name, taken from the snapshot and
from nickname events. Results, errors and the 10-second timeout use
the bar's wording. The command needs the full word.

A new Profile class in profile.py holds this state. The unit test
covers the name check, correlation, errors, the timeout and a dropped
link. The PTY test sets a name through the real helper and finds it
again after a restart.
terminal-client.md gains a Structure section. It covers the entry
point, the module groups, the import rule and its test. Its first entry
names the package, and How to check lists tests/cli_arch_test.py and
the current unit test count. CURRENT.md mentions the checked import
rule.
Sources of truth now lists cli/ as one more presentation adapter on the
helper socket. The entry says the client keeps its state and
presentation modules free of terminal, socket and process I/O, and must
not become a second protocol implementation.
Security now follows the introduction, with the message-flow diagram
folded. A two-column comparison with linked screenshots replaces the
four bar screenshots. It explains that both clients share one identity,
contacts and history, that the terminal client runs without the bar,
and that the updater needs the plugin enabled.

Install, Update and Uninstall are shorter. Uninstall now mentions the
omaq-connect command. The project policy is folded after Uninstall,
and the AUR note is gone. tests/update-order.sh now checks the two
comparison columns instead of four screenshot cells.
The uninstaller deletes ~/.local/bin/omaq-connect only when it is a
dangling link into the removed hancore.omaq plugin. Four new cases run
the real script: a dangling link into the plugin is removed, while a
link to a tree that still exists, a foreign link and a regular file
stay.
After the bar is disabled, the helper it started keeps running until
the user logs out. Quitting the terminal client therefore takes you
offline only from the next login on. A user-like run on a second
Omarchy machine confirmed this.
A cancellation by either side now leaves "✕ File transfer canceled:
name" in that chat, like the bar's dismissible file status. The line
is bound to the contact's key.

Esc on an empty input line hides it. A new offer or send, another
result, or a rebound number replaces it. The short status-line notice
stays as before.
Cursor keys, backspace and delete now follow the clusters the screen
draws. In terminals that draw ZWJ and skin-tone sequences as one
cluster, those sequences move and delete as one unit. Elsewhere they
still go piece by piece, as drawn.
The manifest moves to 1.0.0, still Protocol 16. CURRENT.md, the stage
index and the terminal-client stage now name 1.0.0 where they said
unreleased or feat/cli. The CURRENT.md date moves to 2026-10-11.
ShellCheck flags `A && B || C` as SC2015, and CI fails on it. The
check is now an explicit if with the same logic.
@HANCORE-linux
HANCORE-linux merged commit 4a82518 into main Oct 10, 2026
1 check passed
@HANCORE-linux
HANCORE-linux deleted the feat/cli branch October 11, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant