Hang a face off a message where the terminal can draw one
A cell holds a character, so the terminal has read a message as a nick with its words under it — the initial that stands in for a portrait in a window was a letter printed twice, indenting every nick past its own text. Kitty's graphics protocol is a picture over those cells, which jolt-tui already places for an `:image`, and frq has the portrait on disk either way. So the face comes back where the protocol is: `frq.tui` names the terminals that answer it, the same way the library does, and a message hangs its picture beside the whole of itself rather than off its heading. A portrait is two rows and a heading is one, so on the heading its lower half would sit beside a blank row; beside the message the words take that row, and the column the face makes is the indent the rest of the message was holding itself in by — so `indented` steps aside for it and nothing is spent. The column keeps its width with no picture in it, for a fetch still in flight and for the guests who have no profile at all: a backlog that steps sideways as portraits land is worse than one with a gap down its left. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9e3e031 parent: 7095bb6 modified
src/frq/app.jolt +172 -119 | @@ -141,6 +141,28 @@ | ||
| 141 | 141 | ;; text hangs off — the shape a terminal has read messages in for forty years. |
| 142 | 142 | (defonce terminal? (atom false)) |
| 143 | 143 | |
| 144 | +;; And whether that terminal draws pictures over its cells — Kitty's graphics | |
| 145 | +;; protocol, which kitty, Ghostty and WezTerm answer and an xterm does not. | |
| 146 | +;; `frq.tui` works it out from the environment and sets this beside the flag | |
| 147 | +;; above; a terminal that has not got it is the one described there, a column | |
| 148 | +;; of names with the words hanging under them. | |
| 149 | +(defonce terminal-graphics? (atom false)) | |
| 150 | + | |
| 151 | +(defn- terminal-face? | |
| 152 | + "Whether a message carries a picture of its sender in a terminal. | |
| 153 | + | |
| 154 | + Not the same question as `terminal?`: the face is drawn where the protocol | |
| 155 | + for one is, and where it is not an `:image` is `[ picture ]` printed beside | |
| 156 | + every nick — worse than the nothing that is there now." | |
| 157 | + [] | |
| 158 | + (and @terminal? @terminal-graphics?)) | |
| 159 | + | |
| 160 | +;; How big a face is on a message, in points — the size the window has always | |
| 161 | +;; drawn one at. A terminal's cell is eight points across and sixteen down, so | |
| 162 | +;; the same number is four columns by two rows there: a cached 128-pixel | |
| 163 | +;; portrait at a quarter size, and about the smallest a face is still a face at. | |
| 164 | +(def ^:private face-size 32) | |
| 165 | + | |
| 144 | 166 | ;; ---------------------------------------------------------------- chats |
| 145 | 167 | |
| 146 | 168 | (defn- open-button [name] |
| @@ -226,9 +248,13 @@ | ||
| 226 | 248 | The spacer asks for its width and no height. A `:size` is a length down the |
| 227 | 249 | page, and a spacer two cells tall beside an empty column — a message with no |
| 228 | 250 | picture and no reactions — is two blank rows in the middle of the backlog, |
| 229 | - which is a message and a half at this size." | |
| 251 | + which is a message and a half at this size. | |
| 252 | + | |
| 253 | + Nothing to do where a face is drawn: the column that holds the portrait is | |
| 254 | + already to the left of the whole message, and a second indent inside it would | |
| 255 | + hold the words in from a name that is itself held in." | |
| 230 | 256 | [k node] |
| 231 | - (if @terminal? | |
| 257 | + (if (and @terminal? (not (terminal-face?))) | |
| 232 | 258 | [:hbox {:key k :spacing 0} |
| 233 | 259 | [:spacer {:key :indent :size 0 :width-request 16}] |
| 234 | 260 | node] |
| @@ -676,6 +702,134 @@ | ||
| 676 | 702 | (let [w @s/window-width] |
| 677 | 703 | (if (pos? w) (min 900 (max 240 (long (* 0.95 w)))) 320))) |
| 678 | 704 | |
| 705 | +(defn- message-body | |
| 706 | + "A message without its face: the sender's line, the words, and what hangs | |
| 707 | + under them. | |
| 708 | + | |
| 709 | + Its own component because a terminal that draws pictures puts the face | |
| 710 | + beside all of this rather than on the sender's line — the shape the window | |
| 711 | + has always had, and the one that spends no row on the half of a portrait | |
| 712 | + that is taller than a line of text." | |
| 713 | + [m highlit?] | |
| 714 | + ;; A key each: the surface a jump leaves behind is a different node from the | |
| 715 | + ;; plain column, not the same node wearing another tag. | |
| 716 | + [(if highlit? :card :vbox) {:key (if highlit? :body-card :body-plain) | |
| 717 | + :spacing 2 :margin 0} | |
| 718 | + ;; Who, when, and what can be done about it, on one line above the | |
| 719 | + ;; words: the name and the time say where the message came from, and the | |
| 720 | + ;; chips at the far end are the two answers to it. | |
| 721 | + [:vbox {:key :who} | |
| 722 | + ;; An event has no sender to head it, but it still happened at a time, | |
| 723 | + ;; and a column of joins and quits with no clock on it says nothing | |
| 724 | + ;; about how long the room was quiet. The time alone is that heading. | |
| 725 | + (when (and (:system? m) (:at m)) | |
| 726 | + [:dim-label {:label (clock/clock-time (:at m))}]) | |
| 727 | + (when-not (:system? m) | |
| 728 | + ;; Everything that is about the person rather than the line: their | |
| 729 | + ;; picture, their name, and when they started saying this. | |
| 730 | + [:hbox {:spacing 6} | |
| 731 | + ;; Always an avatar, picture or not: the initial stands in until the | |
| 732 | + ;; fetch lands, and for the guests who have no profile at all, which | |
| 733 | + ;; is what keeps the column of faces straight down the left. | |
| 734 | + ;; Reading the tick subscribes this row to a fetch finishing. | |
| 735 | + ;; The face is also the way to the person behind it: Vidya's plain | |
| 736 | + ;; label does not answer the pointer, so the tap sleek puts on the | |
| 737 | + ;; nick lives here, on the one thing in the row that does. | |
| 738 | + ;; And, where there is a pointer to ask with, resting on the face | |
| 739 | + ;; answers before the click does: the card under it is the profile | |
| 740 | + ;; screen's first few lines, painted beside the pointer. | |
| 741 | + ;; And no face on this line in a terminal: the initial that stands in | |
| 742 | + ;; for a portrait in a window is a letter printed twice in a cell grid, | |
| 743 | + ;; which only pushed every nick in past the words it heads. Where the | |
| 744 | + ;; terminal can draw a picture there is a face after all, hung beside | |
| 745 | + ;; the whole message rather than off its heading — `message-row` has it. | |
| 746 | + (when-not @terminal? | |
| 747 | + (let [_ @s/media-tick] | |
| 748 | + [:avatar (cond-> {:label (:from m) | |
| 749 | + :src (or (avatars/path-when-ready (:actor m)) "") | |
| 750 | + :size face-size | |
| 751 | + :on-click #(profile/open! (:from m) (:actor m))} | |
| 752 | + (platform/desktop?) | |
| 753 | + (assoc :on-hover #(profile/hover! (:from m) (:actor m)) | |
| 754 | + :on-unhover #(profile/unhover! (:from m)))) | |
| 755 | + ;; Only the hovered face carries one: a card is painted when its | |
| 756 | + ;; node has children, and the pointer is on one face at a time. | |
| 757 | + (when (and (platform/desktop?) | |
| 758 | + (= (:from m) (:nick @profile/hovering))) | |
| 759 | + [hover-card (:from m) (:actor m)])])) | |
| 760 | + ;; The name carries the row, so it is set at body size in the plain | |
| 761 | + ;; text colour: dimmed caption made the one thing you scan a column | |
| 762 | + ;; for the faintest thing on it. | |
| 763 | + [:label {:label (:from m)}] | |
| 764 | + (when-let [at (:at m)] | |
| 765 | + [:dim-label {:label (clock/clock-time at)}]) | |
| 766 | + ;; Beside the clock, because it is the same kind of fact about the | |
| 767 | + ;; line: what is on screen is not what was first said. | |
| 768 | + (when (:edited? m) | |
| 769 | + [:dim-label {:label "(edited)"}]) | |
| 770 | + ;; And the two things you can do to the message, at the far end of | |
| 771 | + ;; its heading: a nested row laid out from the right takes what is | |
| 772 | + ;; left of the width and puts the chips against the edge of it, so | |
| 773 | + ;; the words below keep the whole column. | |
| 774 | + ;; | |
| 775 | + ;; A key of its own for each branch, rather than one key whose node | |
| 776 | + ;; changes tag under it: a line this client sent has no msgid until | |
| 777 | + ;; the server echoes it back, and swapping a `:spacer` for a chip in | |
| 778 | + ;; place is what left the row rebuilt in the wrong order. | |
| 779 | + (if (:id m) | |
| 780 | + [action-chips {:key :actions} @s/current m] | |
| 781 | + [:spacer {:key :actions-gap :size 0}])])] | |
| 782 | + ;; A little air under the sender's row: the name and face are a heading | |
| 783 | + ;; for what follows, and a heading that touches its text is not one. | |
| 784 | + ;; The text keeps a column of its own: a wrapping label directly in a | |
| 785 | + ;; row wraps against the row. | |
| 786 | + (indented | |
| 787 | + :text | |
| 788 | + [:vbox {:key :text :spacing 2 :margin-top 4} | |
| 789 | + ;; What this answers, in the column the answer itself is in: the chip | |
| 790 | + ;; is a quote of a line, so it wraps to the width a line has. Outside | |
| 791 | + ;; this column it wrapped to the whole row instead — wider than any | |
| 792 | + ;; message, and out to the window's edge on a narrow screen. | |
| 793 | + [:vbox {:key :reply-chip} | |
| 794 | + (when-let [reply-to (:reply-to m)] | |
| 795 | + [reply-chip @s/current reply-to])] | |
| 796 | + (map-indexed (fn [j run] (run-node j run (:system? m))) | |
| 797 | + (text-runs (:text m)))]) | |
| 798 | + ;; And the picker, when this is the message it was opened on: under the | |
| 799 | + ;; line it is about, where the reader is already looking. | |
| 800 | + ;; | |
| 801 | + ;; The id has to exist, not merely match: a line this client sent itself | |
| 802 | + ;; has no msgid, and neither does a closed picker — so `nil = nil` was | |
| 803 | + ;; every one of those messages opening a picker of its own at startup. | |
| 804 | + [:vbox {:key :picker :margin-bottom 4} | |
| 805 | + (when (and (:id m) (= (:id m) (:id @s/reacting))) | |
| 806 | + [emoji-picker])] | |
| 807 | + ;; Pictures under the line that linked them. The link stays: it is what a | |
| 808 | + ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. | |
| 809 | + (indented | |
| 810 | + :images | |
| 811 | + [:vbox {:key :images :spacing 4} | |
| 812 | + (when (seq (:images m)) | |
| 813 | + ;; Reading the tick is what subscribes this row to a fetch finishing. | |
| 814 | + (let [_ @s/media-tick] | |
| 815 | + (for [url (:images m)] | |
| 816 | + (when-let [path (media/path-when-ready url)] | |
| 817 | + [:image {:key url | |
| 818 | + :src path | |
| 819 | + :max-height (preview-height) | |
| 820 | + :max-width (preview-width) | |
| 821 | + :on-click #(reset! s/lightbox {:path path :url url})}]))))]) | |
| 822 | + ;; Reactions go last, under whatever the message turned out to be: a | |
| 823 | + ;; line with a picture on it is the picture, and pills between the words | |
| 824 | + ;; and the image they introduce read as reactions to the words alone. | |
| 825 | + ;; Nothing moves for a message with no picture — the box above it is | |
| 826 | + ;; empty, and the pills still sit under the last line of text. | |
| 827 | + (indented | |
| 828 | + :reactions-row | |
| 829 | + [:vbox {:key :reactions-row :margin-top 6} | |
| 830 | + (when (and (:id m) (not (:system? m)) (seq (:reactions m))) | |
| 831 | + [reaction-row @s/current m])])]) | |
| 832 | + | |
| 679 | 833 | (defn message-row |
| 680 | 834 | "One message: who said it, when, what you can do to it, and the words. |
| 681 | 835 | |
| @@ -710,123 +864,22 @@ | ||
| 710 | 864 | ;; without it every message in the backlog touches the one above it, and a |
| 711 | 865 | ;; conversation reads as one paragraph with names scattered through it. |
| 712 | 866 | (when @terminal? [:spacer {:key :gap :size 16}]) |
| 713 | - ;; Likewise a key each: the surface a jump leaves behind is a different | |
| 714 | - ;; node from the plain column, not the same node wearing another tag. | |
| 715 | - [(if highlit? :card :vbox) {:key (if highlit? :body-card :body-plain) | |
| 716 | - :spacing 2 :margin 0} | |
| 717 | - ;; Who, when, and what can be done about it, on one line above the | |
| 718 | - ;; words: the name and the time say where the message came from, and the | |
| 719 | - ;; chips at the far end are the two answers to it. | |
| 720 | - [:vbox {:key :who} | |
| 721 | - ;; An event has no sender to head it, but it still happened at a time, | |
| 722 | - ;; and a column of joins and quits with no clock on it says nothing | |
| 723 | - ;; about how long the room was quiet. The time alone is that heading. | |
| 724 | - (when (and (:system? m) (:at m)) | |
| 725 | - [:dim-label {:label (clock/clock-time (:at m))}]) | |
| 726 | - (when-not (:system? m) | |
| 727 | - ;; Everything that is about the person rather than the line: their | |
| 728 | - ;; picture, their name, and when they started saying this. | |
| 729 | - [:hbox {:spacing 6} | |
| 730 | - ;; Always an avatar, picture or not: the initial stands in until the | |
| 731 | - ;; fetch lands, and for the guests who have no profile at all, which | |
| 732 | - ;; is what keeps the column of faces straight down the left. | |
| 733 | - ;; Reading the tick subscribes this row to a fetch finishing. | |
| 734 | - ;; The face is also the way to the person behind it: Vidya's plain | |
| 735 | - ;; label does not answer the pointer, so the tap sleek puts on the | |
| 736 | - ;; nick lives here, on the one thing in the row that does. | |
| 737 | - ;; And, where there is a pointer to ask with, resting on the face | |
| 738 | - ;; answers before the click does: the card under it is the profile | |
| 739 | - ;; screen's first few lines, painted beside the pointer. | |
| 740 | - ;; And in a terminal there is no face at all: a cell grid has no | |
| 741 | - ;; picture to put in one, and the initial standing in for it is the | |
| 742 | - ;; first letter of the name printed twice, indenting every nick past | |
| 743 | - ;; the words it heads. | |
| 744 | - (when-not @terminal? | |
| 745 | - (let [_ @s/media-tick] | |
| 746 | - [:avatar (cond-> {:label (:from m) | |
| 747 | - :src (or (avatars/path-when-ready (:actor m)) "") | |
| 748 | - :size 32 | |
| 749 | - :on-click #(profile/open! (:from m) (:actor m))} | |
| 750 | - (platform/desktop?) | |
| 751 | - (assoc :on-hover #(profile/hover! (:from m) (:actor m)) | |
| 752 | - :on-unhover #(profile/unhover! (:from m)))) | |
| 753 | - ;; Only the hovered face carries one: a card is painted when its | |
| 754 | - ;; node has children, and the pointer is on one face at a time. | |
| 755 | - (when (and (platform/desktop?) | |
| 756 | - (= (:from m) (:nick @profile/hovering))) | |
| 757 | - [hover-card (:from m) (:actor m)])])) | |
| 758 | - ;; The name carries the row, so it is set at body size in the plain | |
| 759 | - ;; text colour: dimmed caption made the one thing you scan a column | |
| 760 | - ;; for the faintest thing on it. | |
| 761 | - [:label {:label (:from m)}] | |
| 762 | - (when-let [at (:at m)] | |
| 763 | - [:dim-label {:label (clock/clock-time at)}]) | |
| 764 | - ;; Beside the clock, because it is the same kind of fact about the | |
| 765 | - ;; line: what is on screen is not what was first said. | |
| 766 | - (when (:edited? m) | |
| 767 | - [:dim-label {:label "(edited)"}]) | |
| 768 | - ;; And the two things you can do to the message, at the far end of | |
| 769 | - ;; its heading: a nested row laid out from the right takes what is | |
| 770 | - ;; left of the width and puts the chips against the edge of it, so | |
| 771 | - ;; the words below keep the whole column. | |
| 772 | - ;; | |
| 773 | - ;; A key of its own for each branch, rather than one key whose node | |
| 774 | - ;; changes tag under it: a line this client sent has no msgid until | |
| 775 | - ;; the server echoes it back, and swapping a `:spacer` for a chip in | |
| 776 | - ;; place is what left the row rebuilt in the wrong order. | |
| 777 | - (if (:id m) | |
| 778 | - [action-chips {:key :actions} @s/current m] | |
| 779 | - [:spacer {:key :actions-gap :size 0}])])] | |
| 780 | - ;; A little air under the sender's row: the name and face are a heading | |
| 781 | - ;; for what follows, and a heading that touches its text is not one. | |
| 782 | - ;; The text keeps a column of its own: a wrapping label directly in a | |
| 783 | - ;; row wraps against the row. | |
| 784 | - (indented | |
| 785 | - :text | |
| 786 | - [:vbox {:key :text :spacing 2 :margin-top 4} | |
| 787 | - ;; What this answers, in the column the answer itself is in: the chip | |
| 788 | - ;; is a quote of a line, so it wraps to the width a line has. Outside | |
| 789 | - ;; this column it wrapped to the whole row instead — wider than any | |
| 790 | - ;; message, and out to the window's edge on a narrow screen. | |
| 791 | - [:vbox {:key :reply-chip} | |
| 792 | - (when-let [reply-to (:reply-to m)] | |
| 793 | - [reply-chip @s/current reply-to])] | |
| 794 | - (map-indexed (fn [j run] (run-node j run (:system? m))) | |
| 795 | - (text-runs (:text m)))]) | |
| 796 | - ;; And the picker, when this is the message it was opened on: under the | |
| 797 | - ;; line it is about, where the reader is already looking. | |
| 798 | - ;; | |
| 799 | - ;; The id has to exist, not merely match: a line this client sent itself | |
| 800 | - ;; has no msgid, and neither does a closed picker — so `nil = nil` was | |
| 801 | - ;; every one of those messages opening a picker of its own at startup. | |
| 802 | - [:vbox {:key :picker :margin-bottom 4} | |
| 803 | - (when (and (:id m) (= (:id m) (:id @s/reacting))) | |
| 804 | - [emoji-picker])] | |
| 805 | - ;; Pictures under the line that linked them. The link stays: it is what a | |
| 806 | - ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. | |
| 807 | - (indented | |
| 808 | - :images | |
| 809 | - [:vbox {:key :images :spacing 4} | |
| 810 | - (when (seq (:images m)) | |
| 811 | - ;; Reading the tick is what subscribes this row to a fetch finishing. | |
| 812 | - (let [_ @s/media-tick] | |
| 813 | - (for [url (:images m)] | |
| 814 | - (when-let [path (media/path-when-ready url)] | |
| 815 | - [:image {:key url | |
| 816 | - :src path | |
| 817 | - :max-height (preview-height) | |
| 818 | - :max-width (preview-width) | |
| 819 | - :on-click #(reset! s/lightbox {:path path :url url})}]))))]) | |
| 820 | - ;; Reactions go last, under whatever the message turned out to be: a | |
| 821 | - ;; line with a picture on it is the picture, and pills between the words | |
| 822 | - ;; and the image they introduce read as reactions to the words alone. | |
| 823 | - ;; Nothing moves for a message with no picture — the box above it is | |
| 824 | - ;; empty, and the pills still sit under the last line of text. | |
| 825 | - (indented | |
| 826 | - :reactions-row | |
| 827 | - [:vbox {:key :reactions-row :margin-top 6} | |
| 828 | - (when (and (:id m) (not (:system? m)) (seq (:reactions m))) | |
| 829 | - [reaction-row @s/current m])])]])) | |
| 867 | + ;; The face beside the message, where the terminal can draw one: a | |
| 868 | + ;; portrait is two rows tall and a heading is one, so hanging it off the | |
| 869 | + ;; sender's line would leave its lower half beside a blank row. Here the | |
| 870 | + ;; words take that row, and the column the face makes is the indent every | |
| 871 | + ;; other row of the message is held in by — `indented` steps aside for it. | |
| 872 | + (if (terminal-face?) | |
| 873 | + [:hbox {:key :faced :spacing 8} | |
| 874 | + [:vbox {:key :face :width-request face-size} | |
| 875 | + (let [_ @s/media-tick] | |
| 876 | + (when-let [path (avatars/path-when-ready (:actor m))] | |
| 877 | + [:image {:key :picture | |
| 878 | + :src path | |
| 879 | + :max-width face-size | |
| 880 | + :max-height face-size}]))] | |
| 881 | + [message-body m highlit?]] | |
| 882 | + [message-body m highlit?])])) | |
| 830 | 883 | |
| 831 | 884 | (defn- day-separator [day-key label] |
| 832 | 885 | [:vbox {:key day-key :spacing 4 :margin 0} |
| @@ -141,6 +141,28 @@ | |||
| 141 | ;; text hangs off — the shape a terminal has read messages in for forty years. | 141 | ;; text hangs off — the shape a terminal has read messages in for forty years. |
| 142 | (defonce terminal? (atom false)) | 142 | (defonce terminal? (atom false)) |
| 143 | 143 | ||
| 144 | +;; And whether that terminal draws pictures over its cells — Kitty's graphics | ||
| 145 | +;; protocol, which kitty, Ghostty and WezTerm answer and an xterm does not. | ||
| 146 | +;; `frq.tui` works it out from the environment and sets this beside the flag | ||
| 147 | +;; above; a terminal that has not got it is the one described there, a column | ||
| 148 | +;; of names with the words hanging under them. | ||
| 149 | +(defonce terminal-graphics? (atom false)) | ||
| 150 | + | ||
| 151 | +(defn- terminal-face? | ||
| 152 | + "Whether a message carries a picture of its sender in a terminal. | ||
| 153 | + | ||
| 154 | + Not the same question as `terminal?`: the face is drawn where the protocol | ||
| 155 | + for one is, and where it is not an `:image` is `[ picture ]` printed beside | ||
| 156 | + every nick — worse than the nothing that is there now." | ||
| 157 | + [] | ||
| 158 | + (and @terminal? @terminal-graphics?)) | ||
| 159 | + | ||
| 160 | +;; How big a face is on a message, in points — the size the window has always | ||
| 161 | +;; drawn one at. A terminal's cell is eight points across and sixteen down, so | ||
| 162 | +;; the same number is four columns by two rows there: a cached 128-pixel | ||
| 163 | +;; portrait at a quarter size, and about the smallest a face is still a face at. | ||
| 164 | +(def ^:private face-size 32) | ||
| 165 | + | ||
| 144 | ;; ---------------------------------------------------------------- chats | 166 | ;; ---------------------------------------------------------------- chats |
| 145 | 167 | ||
| 146 | (defn- open-button [name] | 168 | (defn- open-button [name] |
| @@ -226,9 +248,13 @@ | |||
| 226 | The spacer asks for its width and no height. A `:size` is a length down the | 248 | The spacer asks for its width and no height. A `:size` is a length down the |
| 227 | page, and a spacer two cells tall beside an empty column — a message with no | 249 | page, and a spacer two cells tall beside an empty column — a message with no |
| 228 | picture and no reactions — is two blank rows in the middle of the backlog, | 250 | picture and no reactions — is two blank rows in the middle of the backlog, |
| 229 | - which is a message and a half at this size." | 251 | + which is a message and a half at this size. |
| 252 | + | ||
| 253 | + Nothing to do where a face is drawn: the column that holds the portrait is | ||
| 254 | + already to the left of the whole message, and a second indent inside it would | ||
| 255 | + hold the words in from a name that is itself held in." | ||
| 230 | [k node] | 256 | [k node] |
| 231 | - (if @terminal? | 257 | + (if (and @terminal? (not (terminal-face?))) |
| 232 | [:hbox {:key k :spacing 0} | 258 | [:hbox {:key k :spacing 0} |
| 233 | [:spacer {:key :indent :size 0 :width-request 16}] | 259 | [:spacer {:key :indent :size 0 :width-request 16}] |
| 234 | node] | 260 | node] |
| @@ -676,6 +702,134 @@ | |||
| 676 | (let [w @s/window-width] | 702 | (let [w @s/window-width] |
| 677 | (if (pos? w) (min 900 (max 240 (long (* 0.95 w)))) 320))) | 703 | (if (pos? w) (min 900 (max 240 (long (* 0.95 w)))) 320))) |
| 678 | 704 | ||
| 705 | +(defn- message-body | ||
| 706 | + "A message without its face: the sender's line, the words, and what hangs | ||
| 707 | + under them. | ||
| 708 | + | ||
| 709 | + Its own component because a terminal that draws pictures puts the face | ||
| 710 | + beside all of this rather than on the sender's line — the shape the window | ||
| 711 | + has always had, and the one that spends no row on the half of a portrait | ||
| 712 | + that is taller than a line of text." | ||
| 713 | + [m highlit?] | ||
| 714 | + ;; A key each: the surface a jump leaves behind is a different node from the | ||
| 715 | + ;; plain column, not the same node wearing another tag. | ||
| 716 | + [(if highlit? :card :vbox) {:key (if highlit? :body-card :body-plain) | ||
| 717 | + :spacing 2 :margin 0} | ||
| 718 | + ;; Who, when, and what can be done about it, on one line above the | ||
| 719 | + ;; words: the name and the time say where the message came from, and the | ||
| 720 | + ;; chips at the far end are the two answers to it. | ||
| 721 | + [:vbox {:key :who} | ||
| 722 | + ;; An event has no sender to head it, but it still happened at a time, | ||
| 723 | + ;; and a column of joins and quits with no clock on it says nothing | ||
| 724 | + ;; about how long the room was quiet. The time alone is that heading. | ||
| 725 | + (when (and (:system? m) (:at m)) | ||
| 726 | + [:dim-label {:label (clock/clock-time (:at m))}]) | ||
| 727 | + (when-not (:system? m) | ||
| 728 | + ;; Everything that is about the person rather than the line: their | ||
| 729 | + ;; picture, their name, and when they started saying this. | ||
| 730 | + [:hbox {:spacing 6} | ||
| 731 | + ;; Always an avatar, picture or not: the initial stands in until the | ||
| 732 | + ;; fetch lands, and for the guests who have no profile at all, which | ||
| 733 | + ;; is what keeps the column of faces straight down the left. | ||
| 734 | + ;; Reading the tick subscribes this row to a fetch finishing. | ||
| 735 | + ;; The face is also the way to the person behind it: Vidya's plain | ||
| 736 | + ;; label does not answer the pointer, so the tap sleek puts on the | ||
| 737 | + ;; nick lives here, on the one thing in the row that does. | ||
| 738 | + ;; And, where there is a pointer to ask with, resting on the face | ||
| 739 | + ;; answers before the click does: the card under it is the profile | ||
| 740 | + ;; screen's first few lines, painted beside the pointer. | ||
| 741 | + ;; And no face on this line in a terminal: the initial that stands in | ||
| 742 | + ;; for a portrait in a window is a letter printed twice in a cell grid, | ||
| 743 | + ;; which only pushed every nick in past the words it heads. Where the | ||
| 744 | + ;; terminal can draw a picture there is a face after all, hung beside | ||
| 745 | + ;; the whole message rather than off its heading — `message-row` has it. | ||
| 746 | + (when-not @terminal? | ||
| 747 | + (let [_ @s/media-tick] | ||
| 748 | + [:avatar (cond-> {:label (:from m) | ||
| 749 | + :src (or (avatars/path-when-ready (:actor m)) "") | ||
| 750 | + :size face-size | ||
| 751 | + :on-click #(profile/open! (:from m) (:actor m))} | ||
| 752 | + (platform/desktop?) | ||
| 753 | + (assoc :on-hover #(profile/hover! (:from m) (:actor m)) | ||
| 754 | + :on-unhover #(profile/unhover! (:from m)))) | ||
| 755 | + ;; Only the hovered face carries one: a card is painted when its | ||
| 756 | + ;; node has children, and the pointer is on one face at a time. | ||
| 757 | + (when (and (platform/desktop?) | ||
| 758 | + (= (:from m) (:nick @profile/hovering))) | ||
| 759 | + [hover-card (:from m) (:actor m)])])) | ||
| 760 | + ;; The name carries the row, so it is set at body size in the plain | ||
| 761 | + ;; text colour: dimmed caption made the one thing you scan a column | ||
| 762 | + ;; for the faintest thing on it. | ||
| 763 | + [:label {:label (:from m)}] | ||
| 764 | + (when-let [at (:at m)] | ||
| 765 | + [:dim-label {:label (clock/clock-time at)}]) | ||
| 766 | + ;; Beside the clock, because it is the same kind of fact about the | ||
| 767 | + ;; line: what is on screen is not what was first said. | ||
| 768 | + (when (:edited? m) | ||
| 769 | + [:dim-label {:label "(edited)"}]) | ||
| 770 | + ;; And the two things you can do to the message, at the far end of | ||
| 771 | + ;; its heading: a nested row laid out from the right takes what is | ||
| 772 | + ;; left of the width and puts the chips against the edge of it, so | ||
| 773 | + ;; the words below keep the whole column. | ||
| 774 | + ;; | ||
| 775 | + ;; A key of its own for each branch, rather than one key whose node | ||
| 776 | + ;; changes tag under it: a line this client sent has no msgid until | ||
| 777 | + ;; the server echoes it back, and swapping a `:spacer` for a chip in | ||
| 778 | + ;; place is what left the row rebuilt in the wrong order. | ||
| 779 | + (if (:id m) | ||
| 780 | + [action-chips {:key :actions} @s/current m] | ||
| 781 | + [:spacer {:key :actions-gap :size 0}])])] | ||
| 782 | + ;; A little air under the sender's row: the name and face are a heading | ||
| 783 | + ;; for what follows, and a heading that touches its text is not one. | ||
| 784 | + ;; The text keeps a column of its own: a wrapping label directly in a | ||
| 785 | + ;; row wraps against the row. | ||
| 786 | + (indented | ||
| 787 | + :text | ||
| 788 | + [:vbox {:key :text :spacing 2 :margin-top 4} | ||
| 789 | + ;; What this answers, in the column the answer itself is in: the chip | ||
| 790 | + ;; is a quote of a line, so it wraps to the width a line has. Outside | ||
| 791 | + ;; this column it wrapped to the whole row instead — wider than any | ||
| 792 | + ;; message, and out to the window's edge on a narrow screen. | ||
| 793 | + [:vbox {:key :reply-chip} | ||
| 794 | + (when-let [reply-to (:reply-to m)] | ||
| 795 | + [reply-chip @s/current reply-to])] | ||
| 796 | + (map-indexed (fn [j run] (run-node j run (:system? m))) | ||
| 797 | + (text-runs (:text m)))]) | ||
| 798 | + ;; And the picker, when this is the message it was opened on: under the | ||
| 799 | + ;; line it is about, where the reader is already looking. | ||
| 800 | + ;; | ||
| 801 | + ;; The id has to exist, not merely match: a line this client sent itself | ||
| 802 | + ;; has no msgid, and neither does a closed picker — so `nil = nil` was | ||
| 803 | + ;; every one of those messages opening a picker of its own at startup. | ||
| 804 | + [:vbox {:key :picker :margin-bottom 4} | ||
| 805 | + (when (and (:id m) (= (:id m) (:id @s/reacting))) | ||
| 806 | + [emoji-picker])] | ||
| 807 | + ;; Pictures under the line that linked them. The link stays: it is what a | ||
| 808 | + ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. | ||
| 809 | + (indented | ||
| 810 | + :images | ||
| 811 | + [:vbox {:key :images :spacing 4} | ||
| 812 | + (when (seq (:images m)) | ||
| 813 | + ;; Reading the tick is what subscribes this row to a fetch finishing. | ||
| 814 | + (let [_ @s/media-tick] | ||
| 815 | + (for [url (:images m)] | ||
| 816 | + (when-let [path (media/path-when-ready url)] | ||
| 817 | + [:image {:key url | ||
| 818 | + :src path | ||
| 819 | + :max-height (preview-height) | ||
| 820 | + :max-width (preview-width) | ||
| 821 | + :on-click #(reset! s/lightbox {:path path :url url})}]))))]) | ||
| 822 | + ;; Reactions go last, under whatever the message turned out to be: a | ||
| 823 | + ;; line with a picture on it is the picture, and pills between the words | ||
| 824 | + ;; and the image they introduce read as reactions to the words alone. | ||
| 825 | + ;; Nothing moves for a message with no picture — the box above it is | ||
| 826 | + ;; empty, and the pills still sit under the last line of text. | ||
| 827 | + (indented | ||
| 828 | + :reactions-row | ||
| 829 | + [:vbox {:key :reactions-row :margin-top 6} | ||
| 830 | + (when (and (:id m) (not (:system? m)) (seq (:reactions m))) | ||
| 831 | + [reaction-row @s/current m])])]) | ||
| 832 | + | ||
| 679 | (defn message-row | 833 | (defn message-row |
| 680 | "One message: who said it, when, what you can do to it, and the words. | 834 | "One message: who said it, when, what you can do to it, and the words. |
| 681 | 835 | ||
| @@ -710,123 +864,22 @@ | |||
| 710 | ;; without it every message in the backlog touches the one above it, and a | 864 | ;; without it every message in the backlog touches the one above it, and a |
| 711 | ;; conversation reads as one paragraph with names scattered through it. | 865 | ;; conversation reads as one paragraph with names scattered through it. |
| 712 | (when @terminal? [:spacer {:key :gap :size 16}]) | 866 | (when @terminal? [:spacer {:key :gap :size 16}]) |
| 713 | - ;; Likewise a key each: the surface a jump leaves behind is a different | 867 | + ;; The face beside the message, where the terminal can draw one: a |
| 714 | - ;; node from the plain column, not the same node wearing another tag. | 868 | + ;; portrait is two rows tall and a heading is one, so hanging it off the |
| 715 | - [(if highlit? :card :vbox) {:key (if highlit? :body-card :body-plain) | 869 | + ;; sender's line would leave its lower half beside a blank row. Here the |
| 716 | - :spacing 2 :margin 0} | 870 | + ;; words take that row, and the column the face makes is the indent every |
| 717 | - ;; Who, when, and what can be done about it, on one line above the | 871 | + ;; other row of the message is held in by — `indented` steps aside for it. |
| 718 | - ;; words: the name and the time say where the message came from, and the | 872 | + (if (terminal-face?) |
| 719 | - ;; chips at the far end are the two answers to it. | 873 | + [:hbox {:key :faced :spacing 8} |
| 720 | - [:vbox {:key :who} | 874 | + [:vbox {:key :face :width-request face-size} |
| 721 | - ;; An event has no sender to head it, but it still happened at a time, | 875 | + (let [_ @s/media-tick] |
| 722 | - ;; and a column of joins and quits with no clock on it says nothing | 876 | + (when-let [path (avatars/path-when-ready (:actor m))] |
| 723 | - ;; about how long the room was quiet. The time alone is that heading. | 877 | + [:image {:key :picture |
| 724 | - (when (and (:system? m) (:at m)) | 878 | + :src path |
| 725 | - [:dim-label {:label (clock/clock-time (:at m))}]) | 879 | + :max-width face-size |
| 726 | - (when-not (:system? m) | 880 | + :max-height face-size}]))] |
| 727 | - ;; Everything that is about the person rather than the line: their | 881 | + [message-body m highlit?]] |
| 728 | - ;; picture, their name, and when they started saying this. | 882 | + [message-body m highlit?])])) |
| 729 | - [:hbox {:spacing 6} | ||
| 730 | - ;; Always an avatar, picture or not: the initial stands in until the | ||
| 731 | - ;; fetch lands, and for the guests who have no profile at all, which | ||
| 732 | - ;; is what keeps the column of faces straight down the left. | ||
| 733 | - ;; Reading the tick subscribes this row to a fetch finishing. | ||
| 734 | - ;; The face is also the way to the person behind it: Vidya's plain | ||
| 735 | - ;; label does not answer the pointer, so the tap sleek puts on the | ||
| 736 | - ;; nick lives here, on the one thing in the row that does. | ||
| 737 | - ;; And, where there is a pointer to ask with, resting on the face | ||
| 738 | - ;; answers before the click does: the card under it is the profile | ||
| 739 | - ;; screen's first few lines, painted beside the pointer. | ||
| 740 | - ;; And in a terminal there is no face at all: a cell grid has no | ||
| 741 | - ;; picture to put in one, and the initial standing in for it is the | ||
| 742 | - ;; first letter of the name printed twice, indenting every nick past | ||
| 743 | - ;; the words it heads. | ||
| 744 | - (when-not @terminal? | ||
| 745 | - (let [_ @s/media-tick] | ||
| 746 | - [:avatar (cond-> {:label (:from m) | ||
| 747 | - :src (or (avatars/path-when-ready (:actor m)) "") | ||
| 748 | - :size 32 | ||
| 749 | - :on-click #(profile/open! (:from m) (:actor m))} | ||
| 750 | - (platform/desktop?) | ||
| 751 | - (assoc :on-hover #(profile/hover! (:from m) (:actor m)) | ||
| 752 | - :on-unhover #(profile/unhover! (:from m)))) | ||
| 753 | - ;; Only the hovered face carries one: a card is painted when its | ||
| 754 | - ;; node has children, and the pointer is on one face at a time. | ||
| 755 | - (when (and (platform/desktop?) | ||
| 756 | - (= (:from m) (:nick @profile/hovering))) | ||
| 757 | - [hover-card (:from m) (:actor m)])])) | ||
| 758 | - ;; The name carries the row, so it is set at body size in the plain | ||
| 759 | - ;; text colour: dimmed caption made the one thing you scan a column | ||
| 760 | - ;; for the faintest thing on it. | ||
| 761 | - [:label {:label (:from m)}] | ||
| 762 | - (when-let [at (:at m)] | ||
| 763 | - [:dim-label {:label (clock/clock-time at)}]) | ||
| 764 | - ;; Beside the clock, because it is the same kind of fact about the | ||
| 765 | - ;; line: what is on screen is not what was first said. | ||
| 766 | - (when (:edited? m) | ||
| 767 | - [:dim-label {:label "(edited)"}]) | ||
| 768 | - ;; And the two things you can do to the message, at the far end of | ||
| 769 | - ;; its heading: a nested row laid out from the right takes what is | ||
| 770 | - ;; left of the width and puts the chips against the edge of it, so | ||
| 771 | - ;; the words below keep the whole column. | ||
| 772 | - ;; | ||
| 773 | - ;; A key of its own for each branch, rather than one key whose node | ||
| 774 | - ;; changes tag under it: a line this client sent has no msgid until | ||
| 775 | - ;; the server echoes it back, and swapping a `:spacer` for a chip in | ||
| 776 | - ;; place is what left the row rebuilt in the wrong order. | ||
| 777 | - (if (:id m) | ||
| 778 | - [action-chips {:key :actions} @s/current m] | ||
| 779 | - [:spacer {:key :actions-gap :size 0}])])] | ||
| 780 | - ;; A little air under the sender's row: the name and face are a heading | ||
| 781 | - ;; for what follows, and a heading that touches its text is not one. | ||
| 782 | - ;; The text keeps a column of its own: a wrapping label directly in a | ||
| 783 | - ;; row wraps against the row. | ||
| 784 | - (indented | ||
| 785 | - :text | ||
| 786 | - [:vbox {:key :text :spacing 2 :margin-top 4} | ||
| 787 | - ;; What this answers, in the column the answer itself is in: the chip | ||
| 788 | - ;; is a quote of a line, so it wraps to the width a line has. Outside | ||
| 789 | - ;; this column it wrapped to the whole row instead — wider than any | ||
| 790 | - ;; message, and out to the window's edge on a narrow screen. | ||
| 791 | - [:vbox {:key :reply-chip} | ||
| 792 | - (when-let [reply-to (:reply-to m)] | ||
| 793 | - [reply-chip @s/current reply-to])] | ||
| 794 | - (map-indexed (fn [j run] (run-node j run (:system? m))) | ||
| 795 | - (text-runs (:text m)))]) | ||
| 796 | - ;; And the picker, when this is the message it was opened on: under the | ||
| 797 | - ;; line it is about, where the reader is already looking. | ||
| 798 | - ;; | ||
| 799 | - ;; The id has to exist, not merely match: a line this client sent itself | ||
| 800 | - ;; has no msgid, and neither does a closed picker — so `nil = nil` was | ||
| 801 | - ;; every one of those messages opening a picker of its own at startup. | ||
| 802 | - [:vbox {:key :picker :margin-bottom 4} | ||
| 803 | - (when (and (:id m) (= (:id m) (:id @s/reacting))) | ||
| 804 | - [emoji-picker])] | ||
| 805 | - ;; Pictures under the line that linked them. The link stays: it is what a | ||
| 806 | - ;; failed fetch, an unsupported format, or a phone with no TLS leaves you. | ||
| 807 | - (indented | ||
| 808 | - :images | ||
| 809 | - [:vbox {:key :images :spacing 4} | ||
| 810 | - (when (seq (:images m)) | ||
| 811 | - ;; Reading the tick is what subscribes this row to a fetch finishing. | ||
| 812 | - (let [_ @s/media-tick] | ||
| 813 | - (for [url (:images m)] | ||
| 814 | - (when-let [path (media/path-when-ready url)] | ||
| 815 | - [:image {:key url | ||
| 816 | - :src path | ||
| 817 | - :max-height (preview-height) | ||
| 818 | - :max-width (preview-width) | ||
| 819 | - :on-click #(reset! s/lightbox {:path path :url url})}]))))]) | ||
| 820 | - ;; Reactions go last, under whatever the message turned out to be: a | ||
| 821 | - ;; line with a picture on it is the picture, and pills between the words | ||
| 822 | - ;; and the image they introduce read as reactions to the words alone. | ||
| 823 | - ;; Nothing moves for a message with no picture — the box above it is | ||
| 824 | - ;; empty, and the pills still sit under the last line of text. | ||
| 825 | - (indented | ||
| 826 | - :reactions-row | ||
| 827 | - [:vbox {:key :reactions-row :margin-top 6} | ||
| 828 | - (when (and (:id m) (not (:system? m)) (seq (:reactions m))) | ||
| 829 | - [reaction-row @s/current m])])]])) | ||
| 830 | 883 | ||
| 831 | (defn- day-separator [day-key label] | 884 | (defn- day-separator [day-key label] |
| 832 | [:vbox {:key day-key :spacing 4 :margin 0} | 885 | [:vbox {:key day-key :spacing 4 :margin 0} |
modified
src/frq/tui.jolt +35 -2 | @@ -33,6 +33,7 @@ | ||
| 33 | 33 | (:require [clojure.string :as str] |
| 34 | 34 | [frq.app :as app] |
| 35 | 35 | [frq.state :as s] |
| 36 | + [jolt.host :as host] | |
| 36 | 37 | [glimmer.core :as ui] |
| 37 | 38 | ;; last, so its install! is the one that stands |
| 38 | 39 | [glimmer-tui.core :as tui])) |
| @@ -79,6 +80,33 @@ | ||
| 79 | 80 | (reset! s/screen :chat) |
| 80 | 81 | nil) |
| 81 | 82 | |
| 83 | +(defn- graphics? | |
| 84 | + "Whether this terminal draws pictures over its cells. | |
| 85 | + | |
| 86 | + The same question `jolt-tui` asks itself before it reserves cells for an | |
| 87 | + `:image`, and asked the same way: the protocol's own query comes back as | |
| 88 | + input, long after the layout has had to decide how many rows a picture takes, | |
| 89 | + so a terminal is identified by name here as it is everywhere else — and | |
| 90 | + `JOLT_TUI_GRAPHICS` is the way in for one neither of us has learned yet. | |
| 91 | + | |
| 92 | + frq asks it too, rather than reading the answer back out of the library, | |
| 93 | + because what hangs on it is a tree: a face on a message is an `:image` node | |
| 94 | + or it is no node at all, and that is decided before the backend sees it." | |
| 95 | + [] | |
| 96 | + (let [env (fn [k] (or (host/getenv k) "")) | |
| 97 | + known? (fn [name] | |
| 98 | + (let [name (str/lower-case name)] | |
| 99 | + (boolean (some #(str/includes? name %) | |
| 100 | + ["kitty" "ghostty" "wezterm"]))))] | |
| 101 | + (case (str/lower-case (env "JOLT_TUI_GRAPHICS")) | |
| 102 | + ("1" "true") true | |
| 103 | + ("0" "false") false | |
| 104 | + (or (seq (env "KITTY_WINDOW_ID")) | |
| 105 | + (seq (env "GHOSTTY_RESOURCES_DIR")) | |
| 106 | + (known? (env "TERM")) | |
| 107 | + (known? (env "TERM_PROGRAM")) | |
| 108 | + false)))) | |
| 109 | + | |
| 82 | 110 | (defn- narrow! |
| 83 | 111 | "Tell the layout how much room it has, in the units it expects. |
| 84 | 112 | |
| @@ -108,9 +136,14 @@ | ||
| 108 | 136 | ;; nine the window's count works out to. |
| 109 | 137 | (reset! app/chrome-row points-per-row) |
| 110 | 138 | ;; And a message here is a name with its words hanging under it, rather than |
| 111 | - ;; a face with them beside it: there is no picture in a cell, and the initial | |
| 112 | - ;; that stood in for one only pushed every nick in past its own text. | |
| 139 | + ;; a face with them beside it: the initial that stands in for a portrait in a | |
| 140 | + ;; window is a letter printed twice here, which only pushed every nick in | |
| 141 | + ;; past its own text. | |
| 113 | 142 | (reset! app/terminal? true) |
| 143 | + ;; The face itself comes back where the terminal can draw one: this client | |
| 144 | + ;; fetches the picture either way, and Kitty's protocol puts it over the | |
| 145 | + ;; cells the layout reserved for it. | |
| 146 | + (reset! app/terminal-graphics? (boolean (graphics?))) | |
| 114 | 147 | (app/start! {:after! tui/after! |
| 115 | 148 | :every! tui/every! |
| 116 | 149 | :title! nil |
| @@ -33,6 +33,7 @@ | |||
| 33 | (:require [clojure.string :as str] | 33 | (:require [clojure.string :as str] |
| 34 | [frq.app :as app] | 34 | [frq.app :as app] |
| 35 | [frq.state :as s] | 35 | [frq.state :as s] |
| 36 | + [jolt.host :as host] | ||
| 36 | [glimmer.core :as ui] | 37 | [glimmer.core :as ui] |
| 37 | ;; last, so its install! is the one that stands | 38 | ;; last, so its install! is the one that stands |
| 38 | [glimmer-tui.core :as tui])) | 39 | [glimmer-tui.core :as tui])) |
| @@ -79,6 +80,33 @@ | |||
| 79 | (reset! s/screen :chat) | 80 | (reset! s/screen :chat) |
| 80 | nil) | 81 | nil) |
| 81 | 82 | ||
| 83 | +(defn- graphics? | ||
| 84 | + "Whether this terminal draws pictures over its cells. | ||
| 85 | + | ||
| 86 | + The same question `jolt-tui` asks itself before it reserves cells for an | ||
| 87 | + `:image`, and asked the same way: the protocol's own query comes back as | ||
| 88 | + input, long after the layout has had to decide how many rows a picture takes, | ||
| 89 | + so a terminal is identified by name here as it is everywhere else — and | ||
| 90 | + `JOLT_TUI_GRAPHICS` is the way in for one neither of us has learned yet. | ||
| 91 | + | ||
| 92 | + frq asks it too, rather than reading the answer back out of the library, | ||
| 93 | + because what hangs on it is a tree: a face on a message is an `:image` node | ||
| 94 | + or it is no node at all, and that is decided before the backend sees it." | ||
| 95 | + [] | ||
| 96 | + (let [env (fn [k] (or (host/getenv k) "")) | ||
| 97 | + known? (fn [name] | ||
| 98 | + (let [name (str/lower-case name)] | ||
| 99 | + (boolean (some #(str/includes? name %) | ||
| 100 | + ["kitty" "ghostty" "wezterm"]))))] | ||
| 101 | + (case (str/lower-case (env "JOLT_TUI_GRAPHICS")) | ||
| 102 | + ("1" "true") true | ||
| 103 | + ("0" "false") false | ||
| 104 | + (or (seq (env "KITTY_WINDOW_ID")) | ||
| 105 | + (seq (env "GHOSTTY_RESOURCES_DIR")) | ||
| 106 | + (known? (env "TERM")) | ||
| 107 | + (known? (env "TERM_PROGRAM")) | ||
| 108 | + false)))) | ||
| 109 | + | ||
| 82 | (defn- narrow! | 110 | (defn- narrow! |
| 83 | "Tell the layout how much room it has, in the units it expects. | 111 | "Tell the layout how much room it has, in the units it expects. |
| 84 | 112 | ||
| @@ -108,9 +136,14 @@ | |||
| 108 | ;; nine the window's count works out to. | 136 | ;; nine the window's count works out to. |
| 109 | (reset! app/chrome-row points-per-row) | 137 | (reset! app/chrome-row points-per-row) |
| 110 | ;; And a message here is a name with its words hanging under it, rather than | 138 | ;; And a message here is a name with its words hanging under it, rather than |
| 111 | - ;; a face with them beside it: there is no picture in a cell, and the initial | 139 | + ;; a face with them beside it: the initial that stands in for a portrait in a |
| 112 | - ;; that stood in for one only pushed every nick in past its own text. | 140 | + ;; window is a letter printed twice here, which only pushed every nick in |
| 141 | + ;; past its own text. | ||
| 113 | (reset! app/terminal? true) | 142 | (reset! app/terminal? true) |
| 143 | + ;; The face itself comes back where the terminal can draw one: this client | ||
| 144 | + ;; fetches the picture either way, and Kitty's protocol puts it over the | ||
| 145 | + ;; cells the layout reserved for it. | ||
| 146 | + (reset! app/terminal-graphics? (boolean (graphics?))) | ||
| 114 | (app/start! {:after! tui/after! | 147 | (app/start! {:after! tui/after! |
| 115 | :every! tui/every! | 148 | :every! tui/every! |
| 116 | :title! nil | 149 | :title! nil |