branch: externals-release/ement commit b88e303b348a30af23d63f4d820780df55570913 Author: Adam Porter <a...@alphapapa.net> Commit: Adam Porter <a...@alphapapa.net>
Tidy: Indentation of cl-labels forms Developing on Emacs 29.1 now, so now makem.sh's lint-indent rule will run clean. --- ement-directory.el | 40 ++--- ement-lib.el | 450 ++++++++++++++++++++++++++--------------------------- ement-notify.el | 14 +- ement-room-list.el | 136 ++++++++-------- ement.el | 290 +++++++++++++++++----------------- 5 files changed, 462 insertions(+), 468 deletions(-) diff --git a/ement-directory.el b/ement-directory.el index 4f61d2c684..5a7b5968be 100644 --- a/ement-directory.el +++ b/ement-directory.el @@ -296,31 +296,31 @@ APPEND-P, add ROOMS to buffer rather than replacing existing contents. To be called by `ement-directory-search'." (declare (indent defun)) (let (column-sizes window-start) - (cl-labels ((format-item - ;; NOTE: We use the buffer-local variable `ement-directory-etc' rather - ;; than a closure variable because the taxy-magit-section struct's format - ;; table is not stored in it, and we can't reuse closures' variables. - ;; (It would be good to store the format table in the taxy-magit-section - ;; in the future, to make this cleaner.) - (item) (gethash item (alist-get 'format-table ement-directory-etc))) + (cl-labels ((format-item (item) + ;; NOTE: We use the buffer-local variable `ement-directory-etc' rather + ;; than a closure variable because the taxy-magit-section struct's format + ;; table is not stored in it, and we can't reuse closures' variables. + ;; (It would be good to store the format table in the taxy-magit-section + ;; in the future, to make this cleaner.) + (gethash item (alist-get 'format-table ement-directory-etc))) ;; NOTE: Since these functions take an "item" (which is a [room session] ;; vector), they're prefixed "item-" rather than "room-". - (size - (item) (pcase-let (((map ('num_joined_members size)) item)) - size)) + (size (item) + (pcase-let (((map ('num_joined_members size)) item)) + size)) (t<nil (a b) (and a (not b))) (t>nil (a b) (and (not a) b)) (make-fn (&rest args) - (apply #'make-taxy-magit-section - :make #'make-fn - :format-fn #'format-item - ;; FIXME: Should we reuse `ement-room-list-level-indent' here? - :level-indent ement-room-list-level-indent - ;; :visibility-fn #'visible-p - ;; :heading-indent 2 - :item-indent 2 - ;; :heading-face-fn #'heading-face - args))) + (apply #'make-taxy-magit-section + :make #'make-fn + :format-fn #'format-item + ;; FIXME: Should we reuse `ement-room-list-level-indent' here? + :level-indent ement-room-list-level-indent + ;; :visibility-fn #'visible-p + ;; :heading-indent 2 + :item-indent 2 + ;; :heading-face-fn #'heading-face + args))) (with-current-buffer (get-buffer-create buffer-name) (unless (eq 'ement-directory-mode major-mode) ;; Don't obliterate buffer-local variables. diff --git a/ement-lib.el b/ement-lib.el index 9b040fcf12..bf9d7b733b 100644 --- a/ement-lib.el +++ b/ement-lib.el @@ -172,8 +172,7 @@ include with the request (see Matrix spec)." :alias (read-string "New room alias (e.g. \"foo\" for \"#foo:matrix.org\"): ") :topic (read-string "New room topic: ") :visibility (completing-read "New room visibility: " '(private public)))) - (cl-labels ((given-p - (var) (and var (not (string-empty-p var))))) + (cl-labels ((given-p (var) (and var (not (string-empty-p var))))) (pcase-let* ((endpoint "createRoom") (data (ement-aprog1 (ement-alist "visibility" visibility) @@ -419,14 +418,14 @@ new one automatically if necessary." (ement-with-room-and-session (let* ((prompt (format "Toggle tag (%s): " (ement--format-room ement-room))) (default-tags - (ement-alist (propertize "Favourite" - 'face (when (ement--room-tagged-p "m.favourite" ement-room) - 'transient-value)) - "m.favourite" - (propertize "Low-priority" - 'face (when (ement--room-tagged-p "m.lowpriority" ement-room) - 'transient-value)) - "m.lowpriority")) + (ement-alist (propertize "Favourite" + 'face (when (ement--room-tagged-p "m.favourite" ement-room) + 'transient-value)) + "m.favourite" + (propertize "Low-priority" + 'face (when (ement--room-tagged-p "m.lowpriority" ement-room) + 'transient-value)) + "m.lowpriority")) (input (completing-read prompt default-tags)) (tag (alist-get input default-tags (concat "u." input) nil #'string=))) (list tag ement-room ement-session)))) @@ -510,11 +509,11 @@ Interactively, with prefix, prompt for room and session, otherwise use current room." (interactive (ement-with-room-and-session (list ement-room ement-session))) (cl-labels ((heading (string) - (propertize (or string "") 'face 'font-lock-builtin-face)) + (propertize (or string "") 'face 'font-lock-builtin-face)) (id (string) - (propertize (or string "") 'face 'font-lock-constant-face)) + (propertize (or string "") 'face 'font-lock-constant-face)) (member< - (a b) (string-collate-lessp (car a) (car b) nil t))) + (a b) (string-collate-lessp (car a) (car b) nil t))) (pcase-let* (((cl-struct ement-room (id room-id) avatar display-name canonical-alias members timeline status topic (local (map fetched-members-p))) room) @@ -601,31 +600,31 @@ Returns one of nil (meaning default rules are used), `all-loud', (let ((push-rules (cl-find-if (lambda (alist) (equal "m.push_rules" (alist-get 'type alist))) (ement-session-account-data session)))) - (cl-labels ((override-mute-rule-for-room-p - ;; Following findOverrideMuteRule() in RoomNotifs.ts. - (room) (when-let ((overrides (map-nested-elt push-rules '(content global override)))) - (cl-loop for rule in overrides - when (and (alist-get 'enabled rule) - (rule-for-room-p rule room)) - return rule))) - (rule-for-room-p - ;; Following isRuleForRoom() in RoomNotifs.ts. - (rule room) (and (/= 1 (length (alist-get 'conditions rule))) - (pcase-let* ((condition (elt (alist-get 'conditions rule) 0)) - ((map kind key pattern) condition)) - (and (equal "event_match" kind) - (equal "room_id" key) - (equal (ement-room-id room) pattern))))) - (mute-rule-p - (rule) (when-let ((actions (alist-get 'actions rule))) - (seq-contains-p actions "dont_notify"))) - ;; NOTE: Although v1.7 of the spec says that "dont_notify" is - ;; obsolete, the latest revision of matrix-react-sdk (released last week - ;; as v3.77.1) still works as modeled here. - (tweak-rule-p - (type rule) (when-let ((actions (alist-get 'actions rule))) - (and (seq-contains-p actions "notify") - (seq-contains-p actions `(set_tweak . ,type) 'seq-contains-p))))) + (cl-labels ((override-mute-rule-for-room-p (room) + ;; Following findOverrideMuteRule() in RoomNotifs.ts. + (when-let ((overrides (map-nested-elt push-rules '(content global override)))) + (cl-loop for rule in overrides + when (and (alist-get 'enabled rule) + (rule-for-room-p rule room)) + return rule))) + (rule-for-room-p (rule room) + ;; Following isRuleForRoom() in RoomNotifs.ts. + (and (/= 1 (length (alist-get 'conditions rule))) + (pcase-let* ((condition (elt (alist-get 'conditions rule) 0)) + ((map kind key pattern) condition)) + (and (equal "event_match" kind) + (equal "room_id" key) + (equal (ement-room-id room) pattern))))) + (mute-rule-p (rule) + (when-let ((actions (alist-get 'actions rule))) + (seq-contains-p actions "dont_notify"))) + ;; NOTE: Although v1.7 of the spec says that "dont_notify" is + ;; obsolete, the latest revision of matrix-react-sdk (released last week + ;; as v3.77.1) still works as modeled here. + (tweak-rule-p (type rule) + (when-let ((actions (alist-get 'actions rule))) + (and (seq-contains-p actions "notify") + (seq-contains-p actions `(set_tweak . ,type) 'seq-contains-p))))) ;; If none of these match, nil is returned, meaning that the default rule is used ;; for the room. (if (override-mute-rule-for-room-p room) @@ -675,34 +674,34 @@ default, `all', `mentions-and-keywords', or `none'." (state (alist-get selected-rule available-states nil nil #'equal))) (list state ement-room ement-session)))) (cl-labels ((set-rule (kind rule queue message-fn) - (pcase-let* (((cl-struct ement-room (id room-id)) room) - (rule-id (url-hexify-string room-id)) - (endpoint (format "pushrules/global/%s/%s" kind rule-id)) - (method (if rule 'put 'delete)) - (then (if rule - ;; Setting rules requires PUTting the rules, then making a second - ;; request to enable them. - (lambda (_data) - (ement-api session (concat endpoint "/enabled") :queue queue :version "r0" - :method 'put :data (json-encode (ement-alist 'enabled t)) - :then message-fn)) - message-fn))) - (ement-api session endpoint :queue queue :method method :version "r0" - :data (json-encode rule) - :then then - :else (lambda (plz-error) - (pcase-let* (((cl-struct plz-error response) plz-error) - ((cl-struct plz-response status) response)) - (pcase status - (404 (pcase rule - (`nil - ;; Room already had no rules, so none being found is not an - ;; error. - nil) - (_ ;; Unexpected error: re-signal. - (ement-api-error plz-error)))) - (_ ;; Unexpected error: re-signal. - (ement-api-error plz-error))))))))) + (pcase-let* (((cl-struct ement-room (id room-id)) room) + (rule-id (url-hexify-string room-id)) + (endpoint (format "pushrules/global/%s/%s" kind rule-id)) + (method (if rule 'put 'delete)) + (then (if rule + ;; Setting rules requires PUTting the rules, then making a second + ;; request to enable them. + (lambda (_data) + (ement-api session (concat endpoint "/enabled") :queue queue :version "r0" + :method 'put :data (json-encode (ement-alist 'enabled t)) + :then message-fn)) + message-fn))) + (ement-api session endpoint :queue queue :method method :version "r0" + :data (json-encode rule) + :then then + :else (lambda (plz-error) + (pcase-let* (((cl-struct plz-error response) plz-error) + ((cl-struct plz-response status) response)) + (pcase status + (404 (pcase rule + (`nil + ;; Room already had no rules, so none being found is not an + ;; error. + nil) + (_ ;; Unexpected error: re-signal. + (ement-api-error plz-error)))) + (_ ;; Unexpected error: re-signal. + (ement-api-error plz-error))))))))) (pcase-let* ((available-states (ement-alist nil (ement-alist @@ -789,13 +788,13 @@ Selects from seen users on all sessions. If point is on an event, suggests the event's sender as initial input. Allows unseen user IDs to be input as well." (cl-labels ((format-user (user) - ;; FIXME: Per-room displaynames are now stored in room structs - ;; rather than user structs, so to be complete, this needs to - ;; iterate over all known rooms, looking for the user's - ;; displayname in that room. - (format "%s <%s>" - (ement-user-displayname user) - (ement-user-id user)))) + ;; FIXME: Per-room displaynames are now stored in room structs + ;; rather than user structs, so to be complete, this needs to + ;; iterate over all known rooms, looking for the user's + ;; displayname in that room. + (format "%s <%s>" + (ement-user-displayname user) + (ement-user-id user)))) (let* ((display-to-id (cl-loop for key being the hash-keys of ement-users using (hash-values value) @@ -921,31 +920,30 @@ avatars, etc." ;; string as argument.) ;; TODO: Try using HSV somehow so we could avoid having so many strings return a ;; nearly-black color. - (cl-labels ((relative-luminance - ;; Copy of `modus-themes-wcag-formula', an elegant - ;; implementation by Protesilaos Stavrou. Also see - ;; <https://en.wikipedia.org/wiki/Relative_luminance> and - ;; <https://www.w3.org/TR/WCAG20/#relativeluminancedef>. - (rgb) (cl-loop for k in '(0.2126 0.7152 0.0722) - for x in rgb - sum (* k (if (<= x 0.03928) - (/ x 12.92) - (expt (/ (+ x 0.055) 1.055) 2.4))))) - (contrast-ratio - ;; Copy of `modus-themes-contrast'; see above. - (a b) (let ((ct (/ (+ (relative-luminance a) 0.05) - (+ (relative-luminance b) 0.05)))) - (max ct (/ ct)))) - (increase-contrast - (color against target toward) - (let ((gradient (cdr (color-gradient color toward 20))) - new-color) - (cl-loop do (setf new-color (pop gradient)) - while new-color - until (>= (contrast-ratio new-color against) target) - ;; Avoid infinite loop in case of weirdness - ;; by returning color as a fallback. - finally return (or new-color color))))) + (cl-labels ((relative-luminance (rgb) + ;; Copy of `modus-themes-wcag-formula', an elegant + ;; implementation by Protesilaos Stavrou. Also see + ;; <https://en.wikipedia.org/wiki/Relative_luminance> and + ;; <https://www.w3.org/TR/WCAG20/#relativeluminancedef>. + (cl-loop for k in '(0.2126 0.7152 0.0722) + for x in rgb + sum (* k (if (<= x 0.03928) + (/ x 12.92) + (expt (/ (+ x 0.055) 1.055) 2.4))))) + (contrast-ratio (a b) + ;; Copy of `modus-themes-contrast'; see above. + (let ((ct (/ (+ (relative-luminance a) 0.05) + (+ (relative-luminance b) 0.05)))) + (max ct (/ ct)))) + (increase-contrast (color against target toward) + (let ((gradient (cdr (color-gradient color toward 20))) + new-color) + (cl-loop do (setf new-color (pop gradient)) + while new-color + until (>= (contrast-ratio new-color against) target) + ;; Avoid infinite loop in case of weirdness + ;; by returning color as a fallback. + finally return (or new-color color))))) (let* ((id string) (id-hash (float (+ (abs (sxhash id)) ement-room-prism-color-adjustment))) ;; TODO: Wrap-around the value to get the color I want. @@ -1024,12 +1022,12 @@ period, anywhere in the body." ;; "@foo and @bar:matrix.org: hi" ;; "foo: how about you and @bar ..." (declare (indent defun)) - (cl-labels ((members-having-displayname - ;; Iterating over the hash table values isn't as efficient as a hash - ;; lookup, but in most rooms it shouldn't be a problem. - (name members) (cl-loop for user being the hash-values of members - when (equal name (ement--user-displayname-in room user)) - collect user))) + (cl-labels ((members-having-displayname (name members) + ;; Iterating over the hash table values isn't as efficient as a hash + ;; lookup, but in most rooms it shouldn't be a problem. + (cl-loop for user being the hash-values of members + when (equal name (ement--user-displayname-in room user)) + collect user))) (pcase-let* (((cl-struct ement-room members) room) (regexp (rx (or bos bow (1+ blank)) (or (seq (group @@ -1216,35 +1214,34 @@ DATA is an unsent message event's data alist." (defun ement--direct-room-for-user (user session) "Return last-modified direct room with USER on SESSION, if one exists." ;; Loosely modeled on the Element function findDMForUser in createRoom.ts. - (cl-labels ((membership-event-for-p - (event user) (and (equal "m.room.member" (ement-event-type event)) - (equal (ement-user-id user) (ement-event-state-key event)))) - (latest-membership-for - (user room) - (when-let ((latest-membership-event - (car - (cl-sort - ;; I guess we need to check both state and timeline events. - (append (cl-remove-if-not (lambda (event) - (membership-event-for-p event user)) - (ement-room-state room)) - (cl-remove-if-not (lambda (event) - (membership-event-for-p event user)) - (ement-room-timeline room))) - (lambda (a b) - ;; Sort latest first so we can use the car. - (> (ement-event-origin-server-ts a) - (ement-event-origin-server-ts b))))))) - (alist-get 'membership (ement-event-content latest-membership-event)))) - (latest-event-in - (room) (car - (cl-sort - (append (ement-room-state room) - (ement-room-timeline room)) - (lambda (a b) - ;; Sort latest first so we can use the car. - (> (ement-event-origin-server-ts a) - (ement-event-origin-server-ts b))))))) + (cl-labels ((membership-event-for-p (event user) + (and (equal "m.room.member" (ement-event-type event)) + (equal (ement-user-id user) (ement-event-state-key event)))) + (latest-membership-for (user room) + (when-let ((latest-membership-event + (car + (cl-sort + ;; I guess we need to check both state and timeline events. + (append (cl-remove-if-not (lambda (event) + (membership-event-for-p event user)) + (ement-room-state room)) + (cl-remove-if-not (lambda (event) + (membership-event-for-p event user)) + (ement-room-timeline room))) + (lambda (a b) + ;; Sort latest first so we can use the car. + (> (ement-event-origin-server-ts a) + (ement-event-origin-server-ts b))))))) + (alist-get 'membership (ement-event-content latest-membership-event)))) + (latest-event-in (room) + (car + (cl-sort + (append (ement-room-state room) + (ement-room-timeline room)) + (lambda (a b) + ;; Sort latest first so we can use the car. + (> (ement-event-origin-server-ts a) + (ement-event-origin-server-ts b))))))) (let* ((direct-rooms (cl-remove-if-not (lambda (room) (ement--room-direct-p room session)) @@ -1430,10 +1427,10 @@ Works in major-modes `ement-room-mode', (defun ement--room-direct-p (room session) "Return non-nil if ROOM on SESSION is a direct chat." - (cl-labels ((content-contains-room-id - (content room-id) (cl-loop for (_user-id . room-ids) in content - ;; NOTE: room-ids is a vector. - thereis (seq-contains-p room-ids room-id)))) + (cl-labels ((content-contains-room-id (content room-id) + (cl-loop for (_user-id . room-ids) in content + ;; NOTE: room-ids is a vector. + thereis (seq-contains-p room-ids room-id)))) (pcase-let* (((cl-struct ement-session account-data) session) ((cl-struct ement-room id) room)) (or (cl-loop for event in account-data @@ -1452,63 +1449,62 @@ Works in major-modes `ement-room-mode', ;; or when to use "m.room.member" events for rooms without heroes (e.g. invited rooms). ;; TODO: Add SESSION argument and use it to remove local user from names. (cl-labels ((latest-event (type content-field) - (or (cl-loop for event in (ement-room-timeline room) - when (and (equal type (ement-event-type event)) - (not (string-empty-p (alist-get content-field (ement-event-content event))))) - return (alist-get content-field (ement-event-content event))) - (cl-loop for event in (ement-room-state room) - when (and (equal type (ement-event-type event)) - (not (string-empty-p (alist-get content-field (ement-event-content event))))) - return (alist-get content-field (ement-event-content event))))) - (member-events-name - () (when-let ((member-events (cl-loop for accessor in '(ement-room-timeline ement-room-state ement-room-invite-state) - append (cl-remove-if-not (apply-partially #'equal "m.room.member") - (funcall accessor room) - :key #'ement-event-type)))) - (string-join (delete-dups - (mapcar (lambda (event) - (ement--user-displayname-in room (ement-event-sender event))) - member-events)) - ", "))) - (heroes-name - () (pcase-let* (((cl-struct ement-room summary) room) - ((map ('m.heroes hero-ids) ('m.joined_member_count joined-count) - ('m.invited_member_count invited-count)) - summary)) - ;; TODO: Disambiguate hero display names. - (when hero-ids - (cond ((<= (+ joined-count invited-count) 1) - ;; Empty room. - (empty-room hero-ids joined-count)) - ((>= (length hero-ids) (1- (+ joined-count invited-count))) - ;; Members == heroes. - (hero-names hero-ids)) - ((and (< (length hero-ids) (1- (+ joined-count invited-count))) - (> (+ joined-count invited-count) 1)) - ;; More members than heroes. - (heroes-and-others hero-ids joined-count)))))) - (hero-names - (heroes) (string-join (mapcar #'hero-name heroes) ", ")) - (hero-name - (id) (if-let ((user (gethash id ement-users))) - (ement--user-displayname-in room user) - id)) - (heroes-and-others - (heroes joined) - (format "%s, and %s others" (hero-names heroes) - (- joined (length heroes)))) - (name-override - () (when-let ((event (alist-get "org.matrix.msc3015.m.room.name.override" - (ement-room-account-data room) - nil nil #'equal))) - (map-nested-elt event '(content name)))) - (empty-room - (heroes joined) (cl-etypecase (length heroes) - ((satisfies zerop) "Empty room") - ((number 1 5) (format "Empty room (was %s)" - (hero-names heroes))) - (t (format "Empty room (was %s)" - (heroes-and-others heroes joined)))))) + (or (cl-loop for event in (ement-room-timeline room) + when (and (equal type (ement-event-type event)) + (not (string-empty-p (alist-get content-field (ement-event-content event))))) + return (alist-get content-field (ement-event-content event))) + (cl-loop for event in (ement-room-state room) + when (and (equal type (ement-event-type event)) + (not (string-empty-p (alist-get content-field (ement-event-content event))))) + return (alist-get content-field (ement-event-content event))))) + (member-events-name () + (when-let ((member-events (cl-loop for accessor in '(ement-room-timeline ement-room-state ement-room-invite-state) + append (cl-remove-if-not (apply-partially #'equal "m.room.member") + (funcall accessor room) + :key #'ement-event-type)))) + (string-join (delete-dups + (mapcar (lambda (event) + (ement--user-displayname-in room (ement-event-sender event))) + member-events)) + ", "))) + (heroes-name () + (pcase-let* (((cl-struct ement-room summary) room) + ((map ('m.heroes hero-ids) ('m.joined_member_count joined-count) + ('m.invited_member_count invited-count)) + summary)) + ;; TODO: Disambiguate hero display names. + (when hero-ids + (cond ((<= (+ joined-count invited-count) 1) + ;; Empty room. + (empty-room hero-ids joined-count)) + ((>= (length hero-ids) (1- (+ joined-count invited-count))) + ;; Members == heroes. + (hero-names hero-ids)) + ((and (< (length hero-ids) (1- (+ joined-count invited-count))) + (> (+ joined-count invited-count) 1)) + ;; More members than heroes. + (heroes-and-others hero-ids joined-count)))))) + (hero-names (heroes) + (string-join (mapcar #'hero-name heroes) ", ")) + (hero-name (id) + (if-let ((user (gethash id ement-users))) + (ement--user-displayname-in room user) + id)) + (heroes-and-others (heroes joined) + (format "%s, and %s others" (hero-names heroes) + (- joined (length heroes)))) + (name-override () + (when-let ((event (alist-get "org.matrix.msc3015.m.room.name.override" + (ement-room-account-data room) + nil nil #'equal))) + (map-nested-elt event '(content name)))) + (empty-room (heroes joined) + (cl-etypecase (length heroes) + ((satisfies zerop) "Empty room") + ((number 1 5) (format "Empty room (was %s)" + (hero-names heroes))) + (t (format "Empty room (was %s)" + (heroes-and-others heroes joined)))))) (or (name-override) (latest-event "m.room.name" 'name) (latest-event "m.room.canonical_alias" 'alias) @@ -1564,19 +1560,19 @@ is not at the latest known message event." ;; A room should rarely, if ever, have a nil timeline, but in case it does ;; (which apparently can happen, given user reports), it should not be ;; considered unread. - (cl-labels ((event-counts-toward-unread-p - ;; NOTE: We only consider message events, so membership, reaction, - ;; etc. events will not mark a room as unread. Ideally, I think - ;; that join/leave events should, at least optionally, mark a room - ;; as unread (e.g. in a 1:1 room with a friend, if the other user - ;; left, one would probably want to know, and marking the room - ;; unread would help the user notice), but since membership events - ;; have to be processed to understand their meaning, it's not - ;; straightforward to know whether one should mark a room unread. - - ;; FIXME: Use code from `ement-room--format-member-event' to - ;; distinguish ones that should count. - (event) (equal "m.room.message" (ement-event-type event)))) + (cl-labels ((event-counts-toward-unread-p (event) + ;; NOTE: We only consider message events, so membership, reaction, + ;; etc. events will not mark a room as unread. Ideally, I think + ;; that join/leave events should, at least optionally, mark a room + ;; as unread (e.g. in a 1:1 room with a friend, if the other user + ;; left, one would probably want to know, and marking the room + ;; unread would help the user notice), but since membership events + ;; have to be processed to understand their meaning, it's not + ;; straightforward to know whether one should mark a room unread. + + ;; FIXME: Use code from `ement-room--format-member-event' to + ;; distinguish ones that should count. + (equal "m.room.message" (ement-event-type event)))) (let ((our-read-receipt-event-id (car (gethash our-id receipts))) (first-counting-event (cl-find-if #'event-counts-toward-unread-p timeline))) (cond ((equal fully-read-event-id (ement-event-id (car timeline))) @@ -1630,11 +1626,11 @@ problems." (if-let ((cached-name (gethash user (ement-room-displaynames room)))) cached-name ;; Put timeline events before state events, because IIUC they should be more recent. - (cl-labels ((join-displayname-event-p - (event) (and (eq user (ement-event-sender event)) - (equal "m.room.member" (ement-event-type event)) - (equal "join" (alist-get 'membership (ement-event-content event))) - (alist-get 'displayname (ement-event-content event))))) + (cl-labels ((join-displayname-event-p (event) + (and (eq user (ement-event-sender event)) + (equal "m.room.member" (ement-event-type event)) + (equal "join" (alist-get 'membership (ement-event-content event))) + (alist-get 'displayname (ement-event-content event))))) ;; FIXME: Should probably sort the relevant events to get the latest one. (if-let* ((displayname (or (cl-loop for event in (ement-room-timeline room) when (join-displayname-event-p event) @@ -1733,19 +1729,19 @@ seconds, etc." (if (< seconds 1) (if abbreviate "0s" "0 seconds") (cl-macrolet ((format> (place) - ;; When PLACE is greater than 0, return formatted string using its symbol name. - `(when (> ,place 0) - (format "%d%s%s" ,place - (if abbreviate "" " ") - (if abbreviate - ,(substring (symbol-name place) 0 1) - ,(symbol-name place))))) + ;; When PLACE is greater than 0, return formatted string using its symbol name. + `(when (> ,place 0) + (format "%d%s%s" ,place + (if abbreviate "" " ") + (if abbreviate + ,(substring (symbol-name place) 0 1) + ,(symbol-name place))))) (join-places (&rest places) - ;; Return string joining the names and values of PLACES. - `(string-join (delq nil - (list ,@(cl-loop for place in places - collect `(format> ,place)))) - (if abbreviate "" ", ")))) + ;; Return string joining the names and values of PLACES. + `(string-join (delq nil + (list ,@(cl-loop for place in places + collect `(format> ,place)))) + (if abbreviate "" ", ")))) (pcase-let ((`(,years ,days ,hours ,minutes ,seconds) (ement--human-duration seconds))) (join-places years days hours minutes seconds))))) @@ -1756,9 +1752,9 @@ a simple calculation that does not account for leap years, leap seconds, etc." ;; Copied from `ts-human-format-duration' (same author). (cl-macrolet ((dividef (place divisor) - ;; Divide PLACE by DIVISOR, set PLACE to the remainder, and return the quotient. - `(prog1 (/ ,place ,divisor) - (setf ,place (% ,place ,divisor))))) + ;; Divide PLACE by DIVISOR, set PLACE to the remainder, and return the quotient. + `(prog1 (/ ,place ,divisor) + (setf ,place (% ,place ,divisor))))) (let* ((seconds (floor seconds)) (years (dividef seconds 31536000)) (days (dividef seconds 86400)) diff --git a/ement-notify.el b/ement-notify.el index 173a173a82..c341678b40 100644 --- a/ement-notify.el +++ b/ement-notify.el @@ -214,13 +214,13 @@ If ROOM has no existing buffer, do nothing." (function dbus-get-unique-name "dbusbind.c") (function x-change-window-property "xfns.c") (function x-window-property "xfns.c")) - (cl-labels ((mark-frame-urgent - (frame) (let* ((prop "WM_HINTS") - (hints (cl-coerce - (x-window-property prop frame prop nil nil t) - 'list))) - (setf (car hints) (logior (car hints) 256)) - (x-change-window-property prop hints nil prop 32 t)))) + (cl-labels ((mark-frame-urgent (frame) + (let* ((prop "WM_HINTS") + (hints (cl-coerce + (x-window-property prop frame prop nil nil t) + 'list))) + (setf (car hints) (logior (car hints) 256)) + (x-change-window-property prop hints nil prop 32 t)))) (when-let* ((buffer (alist-get 'buffer (ement-room-local room))) (frames (cl-loop for frame in (frame-list) when (eq 'x (framep frame)) diff --git a/ement-room-list.el b/ement-room-list.el index 224b375073..232ef6d2c8 100644 --- a/ement-room-list.el +++ b/ement-room-list.el @@ -165,10 +165,10 @@ from recent to non-recent for rooms updated in the past hour.") (ement-room-list-define-key membership (&key name status) ;; FIXME: Docstring: status should be a symbol of either `invite', `join', `leave'. (cl-labels ((format-membership (membership) - (pcase membership - ('join "Joined") - ('invite "Invited") - ('leave "[Left]")))) + (pcase membership + ('join "Joined") + ('invite "Invited") + ('leave "[Left]")))) (pcase-let ((`[,(cl-struct ement-room (status membership)) ,_session] item)) (if status (when (equal status membership) @@ -200,12 +200,12 @@ from recent to non-recent for rooms updated in the past hour.") (pcase-let* ((`[,room ,session] item) ((cl-struct ement-session rooms) session) ((cl-struct ement-room type (local (map parents))) room)) - (cl-labels ((format-space - (id) (let* ((parent-room (cl-find id rooms :key #'ement-room-id :test #'equal)) - (space-name (if parent-room - (ement-room-display-name parent-room) - id))) - (concat "Space: " space-name)))) + (cl-labels ((format-space (id) + (let* ((parent-room (cl-find id rooms :key #'ement-room-id :test #'equal)) + (space-name (if parent-room + (ement-room-display-name parent-room) + id))) + (concat "Space: " space-name)))) (when-let ((key (if id ;; ID specified. (cond ((or (member id parents) @@ -553,64 +553,64 @@ DISPLAY-BUFFER-ACTION is nil, the buffer is not displayed." (format-item (item) (gethash item format-table)) ;; NOTE: Since these functions take an "item" (which is a [room session] ;; vector), they're prefixed "item-" rather than "room-". - (item-latest-ts - (item) (or (ement-room-latest-ts (elt item 0)) - ;; Room has no latest timestamp. FIXME: This shouldn't - ;; happen, but it can, maybe due to oversights elsewhere. - 0)) - (item-unread-p - (item) (pcase-let ((`[,room ,session] item)) - (ement--room-unread-p room session))) - (item-left-p - (item) (pcase-let ((`[,(cl-struct ement-room status) ,_session] item)) - (equal 'leave status))) - (item-buffer-p - (item) (pcase-let ((`[,(cl-struct ement-room (local (map buffer))) ,_session] item)) - (buffer-live-p buffer))) - (taxy-unread-p - (taxy) (or (cl-some #'item-unread-p (taxy-items taxy)) - (cl-some #'taxy-unread-p (taxy-taxys taxy)))) - (item-space-p - (item) (pcase-let ((`[,(cl-struct ement-room type) ,_session] item)) - (equal "m.space" type))) - (item-favourite-p - (item) (pcase-let ((`[,room ,_session] item)) - (ement--room-favourite-p room))) - (item-low-priority-p - (item) (pcase-let ((`[,room ,_session] item)) - (ement--room-low-priority-p room))) - (visible-p - ;; This is very confusing and doesn't currently work. - (section) (let ((value (oref section value))) - (if (cl-typecase value - (taxy-magit-section (item-unread-p value)) - (ement-room nil)) - 'show - 'hide))) - (item-invited-p - (item) (pcase-let ((`[,(cl-struct ement-room status) ,_session] item)) - (equal 'invite status))) - (taxy-latest-ts - (taxy) (apply #'max most-negative-fixnum - (delq nil - (list - (when (taxy-items taxy) - (item-latest-ts (car (taxy-items taxy)))) - (when (taxy-taxys taxy) - (cl-loop for sub-taxy in (taxy-taxys taxy) - maximizing (taxy-latest-ts sub-taxy))))))) + (item-latest-ts (item) + (or (ement-room-latest-ts (elt item 0)) + ;; Room has no latest timestamp. FIXME: This shouldn't + ;; happen, but it can, maybe due to oversights elsewhere. + 0)) + (item-unread-p (item) + (pcase-let ((`[,room ,session] item)) + (ement--room-unread-p room session))) + (item-left-p (item) + (pcase-let ((`[,(cl-struct ement-room status) ,_session] item)) + (equal 'leave status))) + (item-buffer-p (item) + (pcase-let ((`[,(cl-struct ement-room (local (map buffer))) ,_session] item)) + (buffer-live-p buffer))) + (taxy-unread-p (taxy) + (or (cl-some #'item-unread-p (taxy-items taxy)) + (cl-some #'taxy-unread-p (taxy-taxys taxy)))) + (item-space-p (item) + (pcase-let ((`[,(cl-struct ement-room type) ,_session] item)) + (equal "m.space" type))) + (item-favourite-p (item) + (pcase-let ((`[,room ,_session] item)) + (ement--room-favourite-p room))) + (item-low-priority-p (item) + (pcase-let ((`[,room ,_session] item)) + (ement--room-low-priority-p room))) + (visible-p (section) + ;; This is very confusing and doesn't currently work. + (let ((value (oref section value))) + (if (cl-typecase value + (taxy-magit-section (item-unread-p value)) + (ement-room nil)) + 'show + 'hide))) + (item-invited-p (item) + (pcase-let ((`[,(cl-struct ement-room status) ,_session] item)) + (equal 'invite status))) + (taxy-latest-ts (taxy) + (apply #'max most-negative-fixnum + (delq nil + (list + (when (taxy-items taxy) + (item-latest-ts (car (taxy-items taxy)))) + (when (taxy-taxys taxy) + (cl-loop for sub-taxy in (taxy-taxys taxy) + maximizing (taxy-latest-ts sub-taxy))))))) (t<nil (a b) (and a (not b))) (t>nil (a b) (and (not a) b)) (make-fn (&rest args) - (apply #'make-taxy-magit-section - :make #'make-fn - :format-fn #'format-item - :level-indent ement-room-list-level-indent - ;; :visibility-fn #'visible-p - ;; :heading-indent 2 - :item-indent 2 - ;; :heading-face-fn #'heading-face - args))) + (apply #'make-taxy-magit-section + :make #'make-fn + :format-fn #'format-item + :level-indent ement-room-list-level-indent + ;; :visibility-fn #'visible-p + ;; :heading-indent 2 + :item-indent 2 + ;; :heading-face-fn #'heading-face + args))) ;; (when (get-buffer buffer-name) ;; (kill-buffer buffer-name)) (unless ement-sessions @@ -626,9 +626,9 @@ DISPLAY-BUFFER-ACTION is nil, the buffer is not displayed." append (cl-loop for room in (ement-session-rooms session) collect (vector room session)))) (taxy (cl-macrolet ((first-item - (pred) `(lambda (taxy) - (when (taxy-items taxy) - (,pred (car (taxy-items taxy)))))) + (pred) `(lambda (taxy) + (when (taxy-items taxy) + (,pred (car (taxy-items taxy)))))) (name= (name) `(lambda (taxy) (equal ,name (taxy-name taxy))))) (thread-last diff --git a/ement.el b/ement.el index 1baa9984e6..a4913919cd 100644 --- a/ement.el +++ b/ement.el @@ -227,106 +227,105 @@ the port, e.g. (1 (list :session (cdar ement-sessions))) (otherwise (list :session (ement-complete-session)))))) (let (sso-server-process) - (cl-labels ((new-session - () (unless (string-match (rx bos "@" (group (1+ (not (any ":")))) ; Username - ":" (group (optional (1+ (not (any blank)))))) ; Server name - user-id) - (user-error "Invalid user ID format: use @USERNAME:SERVER")) - (let* ((username (match-string 1 user-id)) - (server-name (match-string 2 user-id)) - (uri-prefix (or uri-prefix (ement--hostname-uri server-name))) - (user (make-ement-user :id user-id :username username)) - (server (make-ement-server :name server-name :uri-prefix uri-prefix)) - (transaction-id (ement--initial-transaction-id)) - (initial-device-display-name (format "Ement.el: %s@%s" - ;; Just to be extra careful: - (or user-login-name "[unknown user-login-name]") - (or (system-name) "[unknown system-name]"))) - (device-id (secure-hash 'sha256 initial-device-display-name))) - (make-ement-session :user user :server server :transaction-id transaction-id - :device-id device-id :initial-device-display-name initial-device-display-name - :events (make-hash-table :test #'equal)))) - (password-login - () (pcase-let* (((cl-struct ement-session user device-id initial-device-display-name) session) - ((cl-struct ement-user id) user) - (data (ement-alist "type" "m.login.password" - "identifier" - (ement-alist "type" "m.id.user" - "user" id) - "password" (or password - (read-passwd (format "Password for %s: " id))) - "device_id" device-id - "initial_device_display_name" initial-device-display-name))) - ;; TODO: Clear password in callback (if we decide to hold on to it for retrying login timeouts). - (ement-api session "login" :method 'post :data (json-encode data) - :then (apply-partially #'ement--login-callback session)) - (ement-message "Logging in with password..."))) - (sso-filter - (process string) - ;; NOTE: This is technically wrong, because it's not guaranteed that the - ;; string will be a complete request--it could just be a chunk. But in - ;; practice, if this works, it's much simpler than setting up process log - ;; functions and per-client buffers for this throwaway, pretend HTTP server. - (when (string-match (rx "GET /?loginToken=" (group (0+ nonl)) " " (0+ nonl)) string) - (unwind-protect - (pcase-let* ((token (match-string 1 string)) - ((cl-struct ement-session user device-id initial-device-display-name) - session) - ((cl-struct ement-user id) user) - (data (ement-alist - "type" "m.login.token" - "identifier" (ement-alist "type" "m.id.user" - "user" id) - "token" token - "device_id" device-id - "initial_device_display_name" initial-device-display-name))) - (ement-api session "login" :method 'post - :data (json-encode data) - :then (apply-partially #'ement--login-callback session)) - (process-send-string process "HTTP/1.0 202 Accepted + (cl-labels ((new-session () + (unless (string-match (rx bos "@" (group (1+ (not (any ":")))) ; Username + ":" (group (optional (1+ (not (any blank)))))) ; Server name + user-id) + (user-error "Invalid user ID format: use @USERNAME:SERVER")) + (let* ((username (match-string 1 user-id)) + (server-name (match-string 2 user-id)) + (uri-prefix (or uri-prefix (ement--hostname-uri server-name))) + (user (make-ement-user :id user-id :username username)) + (server (make-ement-server :name server-name :uri-prefix uri-prefix)) + (transaction-id (ement--initial-transaction-id)) + (initial-device-display-name (format "Ement.el: %s@%s" + ;; Just to be extra careful: + (or user-login-name "[unknown user-login-name]") + (or (system-name) "[unknown system-name]"))) + (device-id (secure-hash 'sha256 initial-device-display-name))) + (make-ement-session :user user :server server :transaction-id transaction-id + :device-id device-id :initial-device-display-name initial-device-display-name + :events (make-hash-table :test #'equal)))) + (password-login () + (pcase-let* (((cl-struct ement-session user device-id initial-device-display-name) session) + ((cl-struct ement-user id) user) + (data (ement-alist "type" "m.login.password" + "identifier" + (ement-alist "type" "m.id.user" + "user" id) + "password" (or password + (read-passwd (format "Password for %s: " id))) + "device_id" device-id + "initial_device_display_name" initial-device-display-name))) + ;; TODO: Clear password in callback (if we decide to hold on to it for retrying login timeouts). + (ement-api session "login" :method 'post :data (json-encode data) + :then (apply-partially #'ement--login-callback session)) + (ement-message "Logging in with password..."))) + (sso-filter (process string) + ;; NOTE: This is technically wrong, because it's not guaranteed that the + ;; string will be a complete request--it could just be a chunk. But in + ;; practice, if this works, it's much simpler than setting up process log + ;; functions and per-client buffers for this throwaway, pretend HTTP server. + (when (string-match (rx "GET /?loginToken=" (group (0+ nonl)) " " (0+ nonl)) string) + (unwind-protect + (pcase-let* ((token (match-string 1 string)) + ((cl-struct ement-session user device-id initial-device-display-name) + session) + ((cl-struct ement-user id) user) + (data (ement-alist + "type" "m.login.token" + "identifier" (ement-alist "type" "m.id.user" + "user" id) + "token" token + "device_id" device-id + "initial_device_display_name" initial-device-display-name))) + (ement-api session "login" :method 'post + :data (json-encode data) + :then (apply-partially #'ement--login-callback session)) + (process-send-string process "HTTP/1.0 202 Accepted Content-Type: text/plain; charset=utf-8 Ement: SSO login accepted; session token received. Connecting to Matrix server. (You may close this page.)") - (process-send-eof process)) - (delete-process sso-server-process) - (delete-process process)))) + (process-send-eof process)) + (delete-process sso-server-process) + (delete-process process)))) (sso-login () - (setf sso-server-process - (make-network-process - :name "ement-sso" :family 'ipv4 :host 'local :service ement-sso-server-port - :filter #'sso-filter :server t :noquery t)) - ;; Kill server after 2 minutes in case of problems. - (run-at-time 120 nil (lambda () - (when (process-live-p sso-server-process) - (delete-process sso-server-process)))) - (let ((url (concat (ement-server-uri-prefix (ement-session-server session)) - "/_matrix/client/r0/login/sso/redirect?redirectUrl=http://localhost:" - (number-to-string ement-sso-server-port)))) - (funcall browse-url-secondary-browser-function url) - (message "Browsing to single sign-on page <%s>..." url))) - (flows-callback - (data) (let ((flows (cl-loop for flow across (map-elt data 'flows) - for type = (map-elt flow 'type) - when (member type '("m.login.password" "m.login.sso")) - collect type))) - (pcase (length flows) - (0 (error "Ement: No supported login flows: Server:%S Supported flows:%S" - (ement-server-uri-prefix (ement-session-server session)) - (map-elt data 'flows))) - (1 (pcase (car flows) - ("m.login.password" (password-login)) - ("m.login.sso" (sso-login)) - (_ (error "Ement: Unsupported login flow: %s Server:%S Supported flows:%S" - (car flows) (ement-server-uri-prefix (ement-session-server session)) - (map-elt data 'flows))))) - (_ (pcase (completing-read "Select authentication method: " - (cl-loop for flow in flows - collect (string-trim-left flow (rx "m.login.")))) - ("password" (password-login)) - ("sso" (sso-login)) - (else (error "Ement: Unsupported login flow:%S Server:%S Supported flows:%S" - else (ement-server-uri-prefix (ement-session-server session)) - (map-elt data 'flows))))))))) + (setf sso-server-process + (make-network-process + :name "ement-sso" :family 'ipv4 :host 'local :service ement-sso-server-port + :filter #'sso-filter :server t :noquery t)) + ;; Kill server after 2 minutes in case of problems. + (run-at-time 120 nil (lambda () + (when (process-live-p sso-server-process) + (delete-process sso-server-process)))) + (let ((url (concat (ement-server-uri-prefix (ement-session-server session)) + "/_matrix/client/r0/login/sso/redirect?redirectUrl=http://localhost:" + (number-to-string ement-sso-server-port)))) + (funcall browse-url-secondary-browser-function url) + (message "Browsing to single sign-on page <%s>..." url))) + (flows-callback (data) + (let ((flows (cl-loop for flow across (map-elt data 'flows) + for type = (map-elt flow 'type) + when (member type '("m.login.password" "m.login.sso")) + collect type))) + (pcase (length flows) + (0 (error "Ement: No supported login flows: Server:%S Supported flows:%S" + (ement-server-uri-prefix (ement-session-server session)) + (map-elt data 'flows))) + (1 (pcase (car flows) + ("m.login.password" (password-login)) + ("m.login.sso" (sso-login)) + (_ (error "Ement: Unsupported login flow: %s Server:%S Supported flows:%S" + (car flows) (ement-server-uri-prefix (ement-session-server session)) + (map-elt data 'flows))))) + (_ (pcase (completing-read "Select authentication method: " + (cl-loop for flow in flows + collect (string-trim-left flow (rx "m.login.")))) + ("password" (password-login)) + ("sso" (sso-login)) + (else (error "Ement: Unsupported login flow:%S Server:%S Supported flows:%S" + else (ement-server-uri-prefix (ement-session-server session)) + (map-elt data 'flows))))))))) (if session ;; Start syncing given session. (let ((user-id (ement-user-id (ement-session-user session)))) @@ -456,20 +455,20 @@ To be called from `ement-disconnect-hook'." If no URI is found, prompt the user for the hostname." ;; FIXME: When fail-prompting, a URI should be returned, not just a hostname. ;; SPEC: <https://matrix.org/docs/spec/client_server/r0.6.1#id178> ("4.1 Well-known URI") - (cl-labels ((fail-prompt - () (let ((input (read-string "Auto-discovery of server's well-known URI failed. Input server hostname, or leave blank to use server name: "))) - (pcase input - ("" hostname) - (_ input)))) + (cl-labels ((fail-prompt () + (let ((input (read-string "Auto-discovery of server's well-known URI failed. Input server hostname, or leave blank to use server name: "))) + (pcase input + ("" hostname) + (_ input)))) (parse (string) - (if-let* ((object (ignore-errors (json-read-from-string string))) - (url (map-nested-elt object '(m.homeserver base_url))) - ((string-match-p - (rx bos "http" (optional "s") "://" (1+ nonl)) - url))) - url - ;; Parsing error: FAIL_PROMPT. - (fail-prompt)))) + (if-let* ((object (ignore-errors (json-read-from-string string))) + (url (map-nested-elt object '(m.homeserver base_url))) + ((string-match-p + (rx bos "http" (optional "s") "://" (1+ nonl)) + url))) + url + ;; Parsing error: FAIL_PROMPT. + (fail-prompt)))) (condition-case err (let ((response (plz 'get (concat "https://" hostname "/.well-known/matrix/client") :as 'response :then 'sync))) @@ -724,23 +723,22 @@ Also used for left rooms, in which case STATUS should be set to (alist-get 'new-account-data-events (ement-room-local room))) ;; Save state and timeline events. - (cl-macrolet ((push-events - (type accessor) - ;; Push new events of TYPE to room's slot of ACCESSOR, and return the latest timestamp pushed. - `(let ((ts 0)) - ;; NOTE: We replace each event in the vector with the - ;; struct, which is used when calling hooks later. - (cl-loop for event across-ref (alist-get 'events ,type) - do (setf event (ement--make-event event)) - do (push event (,accessor room)) - (when (ement--sync-messages-p session) - (ement-progress-update)) - (when (> (ement-event-origin-server-ts event) ts) - (setf ts (ement-event-origin-server-ts event)))) - ;; One would think that one should use `maximizing' here, but, completely - ;; inexplicably, it sometimes returns nil, even when every single value it's comparing - ;; is a number. It's absolutely bizarre, but I have to do the equivalent manually. - ts))) + (cl-macrolet ((push-events (type accessor) + ;; Push new events of TYPE to room's slot of ACCESSOR, and return the latest timestamp pushed. + `(let ((ts 0)) + ;; NOTE: We replace each event in the vector with the + ;; struct, which is used when calling hooks later. + (cl-loop for event across-ref (alist-get 'events ,type) + do (setf event (ement--make-event event)) + do (push event (,accessor room)) + (when (ement--sync-messages-p session) + (ement-progress-update)) + (when (> (ement-event-origin-server-ts event) ts) + (setf ts (ement-event-origin-server-ts event)))) + ;; One would think that one should use `maximizing' here, but, completely + ;; inexplicably, it sometimes returns nil, even when every single value it's comparing + ;; is a number. It's absolutely bizarre, but I have to do the equivalent manually. + ts))) ;; FIXME: This is a bit convoluted and hacky now. Refactor it. (setf latest-timestamp (max (push-events state ement-room-state) @@ -827,16 +825,16 @@ Adds sender to `ement-users' when necessary." (defun ement--read-sessions () "Return saved sessions alist read from disk. Returns nil if unable to read `ement-sessions-file'." - (cl-labels ((plist-to-session - (plist) (pcase-let* (((map (:user user-data) (:server server-data) - (:token token) (:transaction-id transaction-id)) - plist) - (user (apply #'make-ement-user user-data)) - (server (apply #'make-ement-server server-data)) - (session (make-ement-session :user user :server server - :token token :transaction-id transaction-id))) - (setf (ement-session-events session) (make-hash-table :test #'equal)) - session))) + (cl-labels ((plist-to-session (plist) + (pcase-let* (((map (:user user-data) (:server server-data) + (:token token) (:transaction-id transaction-id)) + plist) + (user (apply #'make-ement-user user-data)) + (server (apply #'make-ement-server server-data)) + (session (make-ement-session :user user :server server + :token token :transaction-id transaction-id))) + (setf (ement-session-events session) (make-hash-table :test #'equal)) + session))) (when (file-exists-p ement-sessions-file) (pcase-let* ((read-circle t) (sessions (with-temp-buffer @@ -858,16 +856,16 @@ Returns nil if unable to read `ement-sessions-file'." ;; NOTE: This writes all current sessions, even if there are multiple active ones and only one ;; is being disconnected. That's probably okay, but it might be something to keep in mind. - (cl-labels ((session-plist - (session) (pcase-let* (((cl-struct ement-session user server token transaction-id) session) - ((cl-struct ement-user (id user-id) username) user) - ((cl-struct ement-server (name server-name) uri-prefix) server)) - (list :user (list :id user-id - :username username) - :server (list :name server-name - :uri-prefix uri-prefix) - :token token - :transaction-id transaction-id)))) + (cl-labels ((session-plist (session) + (pcase-let* (((cl-struct ement-session user server token transaction-id) session) + ((cl-struct ement-user (id user-id) username) user) + ((cl-struct ement-server (name server-name) uri-prefix) server)) + (list :user (list :id user-id + :username username) + :server (list :name server-name + :uri-prefix uri-prefix) + :token token + :transaction-id transaction-id)))) (message "Ement: Writing sessions...") (with-temp-file ement-sessions-file (pcase-let* ((print-level nil)