mcclim/telegram.lisp
The panel
Sizes are in texture pixels. The surface's own rectangle decides how big the thing is in the world; this decides how much of it is bezel.
Two surfaces carry the panel and they are not the same shape: a wall is wide, the phone in the hand is tall. What differs between them is a handful of edges -- the panel's extent, where the well ends and the composer begins, how many characters fit on a line -- and those are specials, bound from the frame's geometry around every paint and every hit-test. Everything that does not depend on the shape stays a constant.
The phone's panel: the proportions of the slab in the hand, so the texture is not squeezed sideways onto it.
Telegram gives every participant a colour and so does this. The choice is a hash of the name, so a person keeps their colour between sessions without anything having to be stored.
What a frame paints
One immutable value, built on the console's thread and read on the game's. Publishing it is a single SETF of a slot, which is why no lock is needed: a repaint either sees the whole previous view or the whole next one.
While logging in: which answer the console is waiting for, and the words to put above the field. NIL once there is a conversation to show.
At the :QR stage, the code to draw: a square bit array, dark where 1.
Pictures
FFmpeg opens a still exactly the way it opens a film -- one packet, one frame -- so the decode path luv already has for video is also the one for a photograph, and no JPEG reader has to exist here. The word order turns around on the way through: libav packs red in the low byte, and a CLIM pattern wants it in the third.
The size photo should be drawn at, fitted into the transcript's box.
An extension FFmpeg will recognize from the name alone.
The demuxer probes the content anyway, but a plausible suffix costs nothing and makes a leftover temporary file identifiable.
:HEAD carries the avatar, the name, and the time; :BODY is a wrapped continuation; :PHOTO is a picture this client can locate but not yet draw.
text broken into lines of at most columns characters, on spaces when it
can and mid-word when a word is longer than the whole line.
The console
A thread, a mailbox, and a slot. The thread holds the only reference to
the roster that anything dereferences, and its Telegram connection is
bound thread-locally so that a console never disturbs whatever the
listener is doing with TELEGRAM.CLIENT:*connection*.
Photo id to a decoded pattern, or :UNAVAILABLE for one that will not decode -- which has to be remembered too, or a broken picture is refetched on every pass forever.
A film the console has fetched and the render thread has not started yet. Opening it makes GPU resources, so the console only downloads.
The last thing that went wrong, kept until something goes right. A
failure passed to one publish-console-view would otherwise be erased by
the very next publish, which is a good way never to see an error.
The login, when there is one to do: which answer is wanted next, and what has been gathered so far. NIL means logged in, or trying to be.
The code login in progress, when there is one. It holds a live connection and lives only as long as this process, which is the whole point: a code is good for about a minute, so a login that outlived a restart is not one to resume.
The QR code as a bit array, rebuilt whenever the token changes. The console thread encodes it so the render thread only ever draws squares.
One Telegram connection and roster, driven on its own thread, publishing a finished view for a McCLIM frame to paint.
Post a request to console's thread. Never blocks and never answers.
peer's recent history as finished, wrapped transcript lines.
Build the next view and hand it to whoever repaints.
What is wanted is whatever this view turns out to reference; a picture scrolled out of the transcript stops being worth a round trip.
Logging in
BEGIN-LOGIN returns a CODE-LOGIN holding an open connection, and COMPLETE-LOGIN and COMPLETE-PASSWORD are handed it back. The console keeps it in a slot, so the panel's state is which question it is asking and which login is asking it, and a player who quits between the code being sent and typed comes back to a clean phone rather than to a prompt for a code Telegram forgot about an hour ago. Only the finished authorization key reaches the disk. Credentials go their own way: the api_id and hash typed here are written to ~/.telegram.env, where the library already looks.
~/.telegram.env
The lines shown above the field for the login stage the console is at.
Drop any login in progress, closing the connection it was holding.
Anything before :CODE means the login in hand is not the one being finished, so let go of its connection rather than leaking it.
The connection is current and authorized: load and show conversations.
RESUME and COMPLETE-LOGIN bind the application identity only for their own extent; INVOKE needs it for every call after, so give this thread its own.
Encode the login's current token and publish it.
Start a QR login and show its code, or fall back to asking for a number.
One short wait for the phone to accept the code, then show whatever the login has now: the conversations, the password question, or a fresh code.
The phone said yes and the account has a password: same question as a code login's, answered over this connection.
Take answer, the player's reply at login stage, on the console's thread.
Each stage either moves to the next or, on failure, stays and says why.
A fresh identity from the file, replacing whatever the missing one left behind, so BEGIN-LOGIN sees the credentials just written.
A rejected identity is not the phone's fault: go back and ask for the credentials again rather than leaving the player stuck.
An expired or already-spent code cannot be retyped: this login is over, so let it go and ask for the number again rather than keeping the player at a question with no right answer.
Carry out one request on the console's own thread. Returns true when the published view should be rebuilt.
Adding a command the panel can ask for is a method here.
Fetch the whole film and leave it where the render thread will find it. Opening it here would make GPU resources on the wrong thread.
answer-console-login publishes as it goes.
Resume the stored session and load enough to show something -- or, when there is nothing to resume, start asking for what a login needs.
The connection is this thread's: TELEGRAM.CLIENT:*connection* is rebound
around the whole loop, so RESUME makes it current here and nowhere else.
The key is stale or was logged out elsewhere: start over.
Leave the manual API-ID prompt when credentials appeared asynchronously. The next console pass opens normally, using a stored session or QR login.
Download and decode a few of the pictures the last view asked for.
A handful per pass rather than all of them: a transcript full of photographs would otherwise stall the update poll behind a queue of downloads, and the pictures appearing over a second or two is the better failure.
Nothing to poll while logging in -- except a QR code, which is waiting on the phone rather than on the player.
Pictures are fetched after the view that named them, so the text is on the wall before the photographs land on it.
console's thread. Nothing in here touches McCLIM.
A dropped socket is ordinary: Telegram closes an idle connection and the next call notices. Forget it and the next pass resumes. Any login in progress owned that connection, so it is over too: let it go and clear its stage, or the loop would keep polling a dead socket at a question no answer can advance -- the panel stuck saying "reconnecting" forever.
Painting
Which of the two screens is showing, and what is half-typed. Both are the panel's own business and never leave the game thread.
How far back the transcript is pushed, in pixels above the bottom. Zero means pinned to the newest message, which is where a chat starts.
One bevelled panel, raised or recessed, optionally standing off the wall.
A little generated head. Telegram's own avatars are files this client cannot fetch yet; a hash of the name at least gives every speaker a stable face rather than a blank square.
The header is part of the screen, not part of the bezel: cream text on a lit stone frame has no contrast, and the device reads as one dark pane behind one raised surround.
How far back the transcript can be pushed before it runs out of history.
view's transcript as (LINE TOP height) triples, scroll pixels back.
Bottom-anchored like every chat: at scroll zero the newest line sits against the composer, and scrolling moves the whole column down to uncover older ones. Drawing and hit-testing both read this, which is the only way a click can land on the picture the player is actually looking at.
Lines that fall entirely outside the well are dropped; ones that straddle its edge are kept and clipped when drawn, so scrolling moves smoothly instead of a line at a time.
A slim mark on the right of the well showing where the transcript is.
Scroll counts backwards from the bottom, so a scroll of zero puts the thumb at the end of the track.
Keep a scrolled-back transcript over the same messages as new ones arrive.
Pinned to the bottom it stays pinned, which is what a chat should do; pushed back, it holds its place instead of being dragged along by every arrival.
Still downloading, or a picture that would not decode: keep its exact footprint so the transcript does not jump when it arrives.
modules drawn under top, centred in the well. Returns the bottom edge.
One draw-lattice* primitive: the whole code -- paper, quiet zone, and
modules together -- is a single analytic shape whose fragment integrates
the module grid exactly over each pixel's footprint, so no module edge can
composite against another and nothing shimmers as the phone moves. Whole
pixels per module stopped mattering when the filtering became exact, so
the code simply fills the well.
What the composer shows: the draft, or one dot per character of a secret.
The caret sits after the text rather than inside it, which is all a single-line composer with no selection needs.
The bezel is the body of the device; everything else is inside it.
The overlay on the wall
Repaint only when the console has published something new, or the player has typed. This runs every frame, so it has to be cheap to say no.
A film the console finished fetching is started here, on the thread that owns the device. The wall stays in :TELEGRAM mode: the film suppresses the panel while it runs and gives it back when it stops.
Say so on the panel rather than dropping it: a film that will not open is the one thing the player is waiting on.
Where in the panel's own pixels a pointer event landed, or NIL.
Enter, or the send button: an answer while logging in, else a message.
Move the transcript for one of the scrolling keys.
Clamping happens at paint time against the view that will actually be drawn, so this only has to say which way and how far.
The playable video line at texture Y, if the click landed on one.
Scroll the transcript under the pointer.
Only when the pointer is actually on the panel: a wheel turn aimed at the world should not quietly move a screen on a wall somewhere behind it.
Nothing else on the login screen is a control.
The back button, which only exists on the conversation screen.
A conversation opens at its newest message, not wherever the last one happened to be scrolled to.
A click inside a video's plate plays it on the wall.
TAB belongs to the session: it is how the player leaves the wall, and a composer that ate it would trap them at the screen.
One Escape leaves the conversation, the next leaves the wall.
Cmd-V or Ctrl-V pastes -- a two-factor password is not something anyone should have to type into a phone in a game -- taking the first line only, since the field is one line.
The wall mode
The panel shape that fits display's surface. A wall gets the wide
panel; the phone gets the tall one. A new kind of surface adds a method.
Mount a Telegram panel on display's surface -- a wall, or the phone.
A little relief, so the buttons and the bezel actually stand off the surface instead of being painted on it.
Stop the console thread and close its Telegram connection.
This is what makes a mode switch safe: the overlay is dropped by whoever is mounting the next one, and its thread has to go with it.
Loading this system is what makes the wall offer a third mode, and what makes the phone come out of the pocket as a messenger rather than a shell.
The phone's panel: the proportions of the slab in the hand, so the texture is not squeezed sideways onto it.
((geometry) &body body)Telegram gives every participant a colour and so does this. The choice is a hash of the name, so a person keeps their colour between sessions without anything having to be stored.
Addition over compatible quantities.
Multiplication and scalar scaling.
The non-negative remainder of integer division.
The maximum of compatible quantities.
The minimum of compatible quantities.
Division of two represented quantities.
Extract the unsigned bit field (BYTE SIZE POSITION) of one unsigned scalar.
(bytes type)Headings, paragraphs, figures and their IDs, mentions, marks.
Raise a dimensionless value to a dimensionless power.
(document)An extension FFmpeg will recognize from the name alone. The demuxer probes the content anyway, but a plausible suffix costs nothing and makes a leftover temporary file identifiable.
An open document and the context it was opened in.
The inner product of two vectors.
Logical conjunction of tests and raw truth values.
Test whether one compatible scalar is less than another.
(bytes width height)An open video file and its decoder.
(pathname &key (hardware :auto) hardware-configuration)Open PATHNAME, find its best video stream, and start a decoder for it. Returns a VIDEO. The caller owns it and must CLOSE-VIDEO it.
(video)Decode until VIDEO's frame holds the next picture. Return the frame or NIL. NIL means the file is exhausted; VIDEO's frame is left holding the last picture that was decoded.
(video width height &key array (alpha 255))Return VIDEO's current picture as a HEIGHT by WIDTH array of RGBA words. Each word is red in its low byte through ALPHA in its high byte, which is the packing luvcraft's block atlas uses. ARRAY is filled and returned when given, so a player can convert into the same array every frame. ALPHA is written rather than…
(video)(unix-seconds)(text columns)TEXT broken into lines of at most COLUMNS characters, on spaces when it can and mid-word when a word is longer than the whole line.
A run of prose; CHILDREN are inline objects.
Test whether one compatible scalar is greater than another.
Logical disjunction of tests and raw truth values.
One Telegram connection and roster, driven on its own thread, publishing a finished view for a McCLIM frame to paint.
(console &key status failure)(console name &optional argument)(console)(console peer)Subtraction or unary negation.
Test whether two compatible scalars are equal.
(console)Logical negation of one test or raw truth value.
(console)~/.telegram.env
(console)(console stage &key note failure)(api-id api-hash)(console)(console)(console)(console)One short wait for the phone to accept the code, then show whatever the login has now: the conversations, the password question, or a fresh code.
(console stage answer)Take ANSWER, the player's reply at login STAGE, on the console's thread. Each stage either moves to the next or, on failure, stays and says why.
(console name argument)Carry out one request on the console's own thread. Returns true when the published view should be rebuilt. Adding a command the panel can ask for is a method here.
(console)(console)Resume the stored session and load enough to show something -- or, when there is nothing to resume, start asking for what a login needs. The connection is this thread's: TELEGRAM.CLIENT:*CONNECTION* is rebound around the whole loop, so RESUME makes it current here and nowhere else.
(console)Leave the manual API-ID prompt when credentials appeared asynchronously. The next console pass opens normally, using a stored session or QR login.
(console &key (limit 2))Download and decode a few of the pictures the last view asked for. A handful per pass rather than all of them: a transcript full of photographs would otherwise stall the update poll behind a queue of downloads, and the pictures appearing over a second or two is the better failure.
Test whether one compatible scalar is at least another.
(console)(console)(&rest initargs)(console)(stream left top right bottom
&key (ink *communicator-bezel-ink*) recessed-p (relief 0.0) (radius 4))(sheet x1 y1 x2 y2 &rest options
&key (radius 7) (filled t) &allow-other-keys)(albedo height)(stream left top right bottom glyph)(stream name left top size)A little generated head. Telegram's own avatars are files this client cannot fetch yet; a hash of the name at least gives every speaker a stable face rather than a blank square.
Compare compatible quantities and produce dimensionless values.
(name-and-options lambda-list &body body)Define a function whose complete dynamic extent is an ambient trace zone. The inferred name is PACKAGE/FUNCTION. Use (NAME :ZONE ZONE-NAME) to provide a more semantic name and :VALUE FORM to attach a Tracy work count.
An explicitly owned AVFrame.
(view &optional (scroll 0))VIEW's transcript as (LINE TOP HEIGHT) triples, SCROLL pixels back. Bottom-anchored like every chat: at scroll zero the newest line sits against the composer, and scrolling moves the whole column down to uncover older ones. Drawing and hit-testing both read this, which is the only way a click can land on the picture…
(pane view scroll)(frame view)Keep a scrolled-back transcript over the same messages as new ones arrive. Pinned to the bottom it stays pinned, which is what a chat should do; pushed back, it holds its place instead of being dragged along by every arrival.
(pane modules top)MODULES drawn under TOP, centred in the well. Returns the bottom edge. One DRAW-LATTICE* primitive: the whole code -- paper, quiet zone, and modules together -- is a single analytic shape whose fragment integrates the module grid exactly over each pixel's footprint, so no module edge can composite against another…
(sheet modules x1 y1 x2 y2 &rest options)Draw a unit-cell bit grid as one exactly filtered backend primitive. INK paints the inked cells on white paper; the whole grid, paper edge included, is a single coverage computation, so nothing in it can seam.
(frame view)(name-and-options &rest method-tail)Define a method whose complete dynamic extent is an ambient trace zone. The inferred name includes the generic function, non-T required specializers, and qualifiers. :ZONE and :VALUE have the same meaning as in ZDEFUN.
((pane lobby-hud-pane) region)A direct GPU target for an ordered LUV-GPU-MEDIUM drawing stream. An embedded mirror retains an immutable prepared CPU revision for the game's final pass; a standalone mirror renders into its native canvas drawable.
A direct McCLIM surface mounted in the 3D scene.
(overlay session pass surface-texture)Encode OVERLAY into SESSION's open scene PASS for SURFACE-TEXTURE.
(overlay display session)Put OVERLAY where DISPLAY's surface is this frame. Asked at draw time, because the surface may be a phone in a moving hand; for a wall the answer is the same every frame and costs a few vector ops.
(context)Return CONTEXT's configured drawable extent as (WIDTH HEIGHT), or NIL.
(overlay session width height)(overlay session)Publish any complete pending render state for OVERLAY at a frame boundary.
(display pathname &key (hardware :required))Play PATHNAME on DISPLAY's authored wall using the session's video backend. HARDWARE is the decode policy MAKE-VIDEO-SCREEN takes. It defaults to :REQUIRED, which is right for authored films whose codec is known, and wrong for a film that arrived from somewhere: whatever a stranger's phone recorded is not…
(overlay event)(overlay x y)(frame key)Move the transcript for one of the scrolling keys. Clamping happens at paint time against the view that will actually be drawn, so this only has to say which way and how far.
(&optional (text *page*) (name "test"))(view y scroll)Test whether one compatible scalar is at most another.
(view y)(overlay session canvas event)Handle EVENT projected onto OVERLAY, returning true when consumed.
A native destination with a lifetime and frame clock.
A scroll, carrying where the pointer was and how far the wheel turned. The amounts are in wheel notches rather than pixels, positive up and right, already corrected for a natural-scrolling platform -- what the window system says the user asked for, not what the hardware reported.
(focus session canvas event)Handle EVENT while FOCUS owns SESSION's modal player interaction.
(session)(canvas)The text on the system clipboard, or NIL when there is none or the window system has no clipboard to ask.
(display)The panel shape that fits DISPLAY's surface. A wall gets the wide panel; the phone gets the tall one. A new kind of surface adds a method.
A wall terminal display drawn on a phone screen: the same runs, drawn against a per-frame uniform whose camera is expressed in the grip frame.
(display &key console)A McCLIM frame manager constrained by luv's current native host.
(overlay)Release resources owned by an object attached to luvcraft.
(overlay)The wall modes the hotbar offers, in slot order. A presentation layer which teaches CHANGE-TERMINAL-DISPLAY-MODE a new mode appends it here and gets a numbered slot; nothing else has to change.
The mode a phone's display starts in when it is first taken out. The phone is a terminal like a wall is, so :SHELL is what it knows by itself. The complete luvcraft system includes the Telegram presentation and sets this to :TELEGRAM when that part loads. DEFVAR deliberately preserves that application-owned choice…
(display session mode)Select DISPLAY's focused wall MODE. The built-in modes are the EQL-specialized symbols :SHELL and :FILM. LUVCRAFT/MCCLIM adds an :AFTER method which supplies the film browser, while the display continues to own focus and movie lifetime.
(display session)(display wanted-type)Return DISPLAY's mode child if it is already WANTED-TYPE, else drop it. Every mode installs a different child and only one can be mounted, so changing mode has to take the previous one down. It is released rather than merely forgotten: the child being replaced may own a thread or a socket, and a Telegram console…
A Telegram terminal bolted to a luvcraft wall.
The wall is the device. Build a rectangle of terminal blocks, look at it, press TAB to focus, and switch it to Telegram: the same surface that runs a shell or plays a film now shows a conversation, drawn by McCLIM into the texture the block world samples.
Two threads meet here and only one thing crosses between them. A console owns a Telegram connection and a roster on its own thread, and the roster is never touched from outside it; the game thread posts requests to a mailbox and reads one slot, which the console fills with a finished, immutable view. That is the whole concurrency story: a mailbox one way, a published snapshot the other, and no lock anywhere. It is also why the text is wrapped and the timestamps are formatted on the console's thread -- by the time a frame repaints, there is nothing left to compute.