2020-09-06 11:32:08 +02:00
|
|
|
;; tinmop: an humble gemini and pleroma client
|
2020-05-08 15:45:43 +02:00
|
|
|
;; Copyright (C) 2020 cage
|
|
|
|
|
|
|
|
;; This program is free software: you can redistribute it and/or modify
|
|
|
|
;; it under the terms of the GNU General Public License as published by
|
|
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
|
|
;; (at your option) any later version.
|
|
|
|
|
|
|
|
;; This program is distributed in the hope that it will be useful,
|
|
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
;; GNU General Public License for more details.
|
|
|
|
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
|
|
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
(in-package :message-window)
|
|
|
|
|
|
|
|
(defclass message-window (wrapper-window
|
|
|
|
row-oriented-widget
|
|
|
|
focus-marked-window
|
|
|
|
title-window)
|
2021-04-05 17:33:16 +02:00
|
|
|
((support-text
|
2021-04-05 12:01:30 +02:00
|
|
|
:initform nil
|
2021-04-05 17:33:16 +02:00
|
|
|
:initarg :support-text
|
|
|
|
:reader support-text)
|
2020-05-08 15:45:43 +02:00
|
|
|
(line-position-mark
|
|
|
|
:initform (make-tui-string "0")
|
|
|
|
:initarg :line-position-mark
|
2020-06-22 13:58:04 +02:00
|
|
|
:accessor line-position-mark)
|
|
|
|
(metadata
|
|
|
|
:initform nil
|
|
|
|
:initarg :metadata
|
|
|
|
:accessor metadata)))
|
|
|
|
|
2020-12-29 12:36:10 +01:00
|
|
|
(defun gemini-window-p ()
|
|
|
|
(gemini-viewer:gemini-metadata-p (message-window:metadata specials:*message-window*)))
|
|
|
|
|
2020-06-22 13:58:04 +02:00
|
|
|
(defun display-gemini-text-p (window)
|
|
|
|
(eq (keybindings window)
|
|
|
|
keybindings:*gemini-message-keymap*))
|
2020-05-08 15:45:43 +02:00
|
|
|
|
2020-09-06 17:28:16 +02:00
|
|
|
(defun display-chat-p (window)
|
|
|
|
(eq (keybindings window)
|
|
|
|
keybindings:*chat-message-keymap*))
|
|
|
|
|
2020-06-28 12:59:23 +02:00
|
|
|
(defun prepare-for-display-status-mode (window)
|
2020-10-02 18:26:59 +02:00
|
|
|
(when (not (or (display-gemini-text-p window)
|
|
|
|
(display-chat-p window)))
|
|
|
|
(setf (keybindings window)
|
|
|
|
keybindings:*message-keymap*)))
|
2020-06-28 12:59:23 +02:00
|
|
|
|
2021-04-05 17:33:16 +02:00
|
|
|
(defmethod (setf support-text) (new-text (object message-window))
|
|
|
|
(setf (slot-value object 'support-text) new-text)
|
2020-09-10 17:50:22 +02:00
|
|
|
(handler-bind ((conditions:out-of-bounds
|
|
|
|
(lambda (e)
|
|
|
|
(invoke-restart 'ignore-selecting-action e))))
|
|
|
|
(prepare-for-rendering object)))
|
2020-05-08 15:45:43 +02:00
|
|
|
|
|
|
|
(defun refresh-line-mark-config (window)
|
|
|
|
(multiple-value-bind (mark-value mark-fg mark-bg)
|
|
|
|
(swconf:message-window-line-mark-values)
|
|
|
|
(setf (line-position-mark window)
|
|
|
|
(make-tui-string mark-value
|
|
|
|
:fgcolor mark-fg
|
|
|
|
:bgcolor mark-bg))))
|
|
|
|
|
|
|
|
(defmethod refresh-config :after ((object message-window))
|
|
|
|
(refresh-config-colors object swconf:+key-message-window+)
|
|
|
|
(refresh-line-mark-config object)
|
|
|
|
(let* ((thread-window-width (win-width *thread-window*))
|
|
|
|
(thread-window-height (win-height *thread-window*))
|
|
|
|
(command-window-height (win-height *command-window*))
|
|
|
|
(main-window-height (win-height *main-window*))
|
|
|
|
(height (- main-window-height
|
|
|
|
command-window-height
|
|
|
|
thread-window-height))
|
|
|
|
(width thread-window-width)
|
|
|
|
(x (win-x *thread-window*))
|
|
|
|
(y (+ (win-y *thread-window*)
|
|
|
|
thread-window-height)))
|
|
|
|
(win-resize object width height)
|
|
|
|
(win-move object x y)))
|
|
|
|
|
|
|
|
(defmethod calculate ((object message-window) dt)
|
|
|
|
(declare (ignore object dt)))
|
|
|
|
|
|
|
|
(defun draw-text (window)
|
2021-04-08 15:13:31 +02:00
|
|
|
(with-accessors ((row-selected-index row-selected-index)) window
|
|
|
|
(let ((actual-rows (line-oriented-window:rows-safe-subseq window row-selected-index)))
|
|
|
|
(loop for line in actual-rows
|
|
|
|
for y from 1 below (win-height-no-border window)
|
|
|
|
do
|
|
|
|
(cond
|
|
|
|
((invisible-row-p line)
|
|
|
|
(decf y))
|
|
|
|
((not (vspace-row-p line))
|
|
|
|
(let ((text-line (normal-text line)))
|
|
|
|
(print-text window text-line 1 y))))))))
|
2020-05-08 15:45:43 +02:00
|
|
|
|
|
|
|
(defun draw-buffer-line-mark (window)
|
|
|
|
(with-accessors ((rows rows)
|
|
|
|
(row-selected-index row-selected-index)
|
|
|
|
(line-position-mark line-position-mark)) window
|
|
|
|
(let* ((height (1- (win-height-no-border window)))
|
2021-04-08 15:13:31 +02:00
|
|
|
(rows-count (- (rows-length window) height))
|
2020-05-08 15:45:43 +02:00
|
|
|
(fraction (/ row-selected-index
|
2020-09-06 14:42:16 +02:00
|
|
|
(max 1 rows-count)))
|
2020-05-08 15:45:43 +02:00
|
|
|
(mark-y (1+ (truncate (* fraction height))))
|
|
|
|
(mark-x (1- (win-width window))))
|
|
|
|
(print-text window line-position-mark mark-x mark-y))))
|
|
|
|
|
|
|
|
(defmethod draw ((object message-window))
|
|
|
|
(when-window-shown (object)
|
|
|
|
(win-clear object :redraw nil)
|
|
|
|
(win-box object)
|
|
|
|
(draw-text object)
|
2021-04-08 15:13:31 +02:00
|
|
|
(when (not (line-oriented-window:rows-empty-p object))
|
2020-05-08 15:45:43 +02:00
|
|
|
(draw-buffer-line-mark object))
|
|
|
|
(call-next-method)))
|
|
|
|
|
2020-07-26 12:04:46 +02:00
|
|
|
(defgeneric prepare-for-rendering (object &key (jump-to-first-row)))
|
|
|
|
|
2021-04-05 17:33:16 +02:00
|
|
|
(defgeneric append-support-text (object text
|
2020-07-26 12:04:46 +02:00
|
|
|
&key prepare-for-rendering jump-to-first-row))
|
|
|
|
|
2020-05-08 15:45:43 +02:00
|
|
|
(defgeneric scroll-down (object &optional amount))
|
|
|
|
|
|
|
|
(defgeneric scroll-up (object &optional amount))
|
|
|
|
|
|
|
|
(defgeneric scroll-end (object))
|
|
|
|
|
|
|
|
(defgeneric scroll-begin (object))
|
|
|
|
|
|
|
|
(defgeneric scroll-next-page (object))
|
|
|
|
|
|
|
|
(defgeneric scroll-previous-page (object))
|
|
|
|
|
|
|
|
(defgeneric search-regex (object regex))
|
|
|
|
|
2021-04-05 12:01:30 +02:00
|
|
|
(defgeneric text->rendered-lines-rows (window text))
|
|
|
|
|
2021-04-08 15:13:31 +02:00
|
|
|
(defun make-render-vspace-row ()
|
2021-04-05 12:01:30 +02:00
|
|
|
(make-instance 'line
|
2021-04-05 18:10:14 +02:00
|
|
|
:normal-text (make-tui-string "")))
|
2021-04-05 12:01:30 +02:00
|
|
|
|
2021-04-08 15:13:31 +02:00
|
|
|
(defun vspace-row-p (row)
|
|
|
|
(string-empty-p (normal-text row)))
|
|
|
|
|
|
|
|
(defun make-invisible-row ()
|
2021-04-05 12:01:30 +02:00
|
|
|
(make-instance 'line
|
2021-04-08 15:13:31 +02:00
|
|
|
:fields (list :invisible t)
|
2021-04-05 18:10:14 +02:00
|
|
|
:normal-text (make-tui-string "")))
|
2021-04-05 12:01:30 +02:00
|
|
|
|
2021-04-08 15:13:31 +02:00
|
|
|
(defun invisible-row-p (row)
|
|
|
|
(getf (fields row) :invisible))
|
|
|
|
|
|
|
|
(defmethod text->rendered-lines-rows (window (text gemini-parser:pre-start))
|
|
|
|
(make-invisible-row))
|
|
|
|
|
|
|
|
(defmethod text->rendered-lines-rows (window (text gemini-parser:pre-end))
|
|
|
|
(make-invisible-row))
|
|
|
|
|
2021-04-08 16:32:34 +02:00
|
|
|
(defmethod text->rendered-lines-rows (window (text gemini-parser:pre-line))
|
|
|
|
(make-instance 'line
|
|
|
|
:normal-text
|
|
|
|
(reduce #'tui:cat-complex-string
|
|
|
|
(text->rendered-lines-rows window (gemini-parser:lines text)))
|
|
|
|
:fields (list :alt-text (gemini-parser:alt-text text)
|
|
|
|
:group-id (gemini-parser:group-id text)
|
|
|
|
:original-object text)))
|
|
|
|
|
2021-04-05 12:01:30 +02:00
|
|
|
(defmethod text->rendered-lines-rows (window (text list))
|
2021-04-05 14:47:57 +02:00
|
|
|
(flatten (loop for i in text
|
|
|
|
collect
|
|
|
|
(text->rendered-lines-rows window i))))
|
2021-04-05 12:01:30 +02:00
|
|
|
|
|
|
|
(defmethod text->rendered-lines-rows (window (text complex-string))
|
2021-04-08 16:32:34 +02:00
|
|
|
text)
|
2021-04-05 12:01:30 +02:00
|
|
|
|
2021-04-08 15:13:31 +02:00
|
|
|
(defmethod update-all-rows :around ((object message-window) (new-rows sequence))
|
|
|
|
(let ((new-rows (remove-if #'invisible-row-p new-rows)))
|
|
|
|
(call-next-method object new-rows)))
|
|
|
|
|
|
|
|
(defmethod append-new-rows :around ((object message-window) (new-rows sequence))
|
|
|
|
(let ((new-rows (loop for new-row in new-rows
|
|
|
|
when (not (invisible-row-p new-row))
|
|
|
|
collect
|
|
|
|
new-row)))
|
|
|
|
(call-next-method object new-rows)))
|
|
|
|
|
2021-04-05 14:47:57 +02:00
|
|
|
(defun colorize-lines (lines)
|
|
|
|
(let ((color-re (swconf:color-regexps)))
|
|
|
|
(loop for line in lines
|
|
|
|
collect
|
|
|
|
(let ((res line))
|
|
|
|
(loop for re in color-re do
|
|
|
|
(setf res (colorize-line res re)))
|
|
|
|
(colorized-line->tui-string res)))))
|
|
|
|
|
|
|
|
(defmethod text->rendered-lines-rows (window (text gemini-parser:quoted-lines))
|
|
|
|
(let ((colorized-lines (colorize-lines (gemini-parser:lines text))))
|
|
|
|
(loop for i in colorized-lines
|
|
|
|
collect
|
|
|
|
(make-instance 'line
|
|
|
|
:normal-text i))))
|
|
|
|
|
2021-04-05 12:01:30 +02:00
|
|
|
(defmethod text->rendered-lines-rows (window (text string))
|
2021-04-03 15:03:24 +02:00
|
|
|
(labels ((fit-lines (lines)
|
|
|
|
(let ((res ()))
|
|
|
|
(loop for line in lines do
|
|
|
|
(if (string-empty-p line)
|
|
|
|
(push nil res)
|
|
|
|
(loop
|
2021-04-05 12:01:30 +02:00
|
|
|
for fitted-line
|
|
|
|
in (flush-left-mono-text (split-words line)
|
|
|
|
(win-width-no-border window))
|
2021-04-03 15:03:24 +02:00
|
|
|
do
|
|
|
|
(push fitted-line res))))
|
|
|
|
(reverse res))))
|
2021-04-05 12:01:30 +02:00
|
|
|
(if (string= text (format nil "~%"))
|
2021-04-08 15:13:31 +02:00
|
|
|
(make-render-vspace-row)
|
2021-04-05 12:01:30 +02:00
|
|
|
(let* ((lines (split-lines text))
|
|
|
|
(fitted-lines (fit-lines lines))
|
2021-04-05 14:47:57 +02:00
|
|
|
(new-rows (colorize-lines fitted-lines)))
|
2021-04-05 12:01:30 +02:00
|
|
|
(mapcar (lambda (text-line)
|
|
|
|
(make-instance 'line
|
|
|
|
:normal-text text-line))
|
|
|
|
new-rows)))))
|
|
|
|
|
|
|
|
(defmethod text->rendered-lines-rows (window (text null))
|
2021-04-08 15:13:31 +02:00
|
|
|
(make-render-vspace-row))
|
2021-03-09 11:32:09 +01:00
|
|
|
|
|
|
|
(defmethod prepare-for-rendering ((object message-window) &key (jump-to-first-row t))
|
2021-04-05 17:33:16 +02:00
|
|
|
(with-accessors ((support-text support-text)) object
|
2021-03-09 11:32:09 +01:00
|
|
|
(when hooks:*before-prepare-for-rendering-message*
|
|
|
|
(hooks:run-hook 'hooks:*before-prepare-for-rendering-message* object))
|
2021-04-08 15:13:31 +02:00
|
|
|
(update-all-rows object
|
|
|
|
(text->rendered-lines-rows object support-text))
|
2021-03-09 11:32:09 +01:00
|
|
|
(when jump-to-first-row
|
|
|
|
(select-row object 0))
|
|
|
|
object))
|
2020-07-26 12:04:46 +02:00
|
|
|
|
2021-04-05 17:33:16 +02:00
|
|
|
(defmethod append-support-text ((object message-window) text
|
2020-07-26 12:04:46 +02:00
|
|
|
&key
|
|
|
|
(prepare-for-rendering nil)
|
|
|
|
(jump-to-first-row nil))
|
2021-04-05 17:33:16 +02:00
|
|
|
(with-slots (support-text) object
|
|
|
|
(setf support-text (strcat support-text text))
|
2020-07-26 12:04:46 +02:00
|
|
|
(when prepare-for-rendering
|
|
|
|
(prepare-for-rendering object :jump-to-first-row jump-to-first-row))))
|
|
|
|
|
2020-09-06 11:18:49 +02:00
|
|
|
(defun offset-to-move-end (win)
|
|
|
|
(with-accessors ((rows rows)
|
|
|
|
(row-selected-index row-selected-index)) win
|
|
|
|
(let ((win-height (win-height-no-border win)))
|
2021-04-08 15:13:31 +02:00
|
|
|
(- (- (rows-length win)
|
2020-09-06 11:18:49 +02:00
|
|
|
(- win-height 1))
|
|
|
|
row-selected-index))))
|
|
|
|
|
|
|
|
(defun scroll-end-reached-p (win)
|
|
|
|
(with-accessors ((rows rows)
|
|
|
|
(row-selected-index row-selected-index)) win
|
|
|
|
(let* ((win-height (win-height-no-border win))
|
2021-04-08 15:13:31 +02:00
|
|
|
(rows-left (- (rows-length win) row-selected-index)))
|
2020-09-06 11:18:49 +02:00
|
|
|
(< rows-left
|
|
|
|
win-height))))
|
|
|
|
|
2020-05-08 15:45:43 +02:00
|
|
|
(defmethod scroll-down ((object message-window) &optional (amount 1))
|
2020-09-06 11:18:49 +02:00
|
|
|
(when (not (or (scroll-end-reached-p object)
|
|
|
|
(= (row-move object amount)
|
|
|
|
0)))
|
2020-05-08 15:45:43 +02:00
|
|
|
(draw object)))
|
|
|
|
|
|
|
|
(defmethod scroll-up ((object message-window) &optional (amount 1))
|
|
|
|
(when (/= (row-move object (- amount))
|
|
|
|
0)
|
|
|
|
(draw object)))
|
|
|
|
|
|
|
|
(defmethod scroll-end ((object message-window))
|
|
|
|
(with-accessors ((rows rows)
|
|
|
|
(row-selected-index row-selected-index)) object
|
2020-09-06 11:18:49 +02:00
|
|
|
(let ((offset (offset-to-move-end object)))
|
|
|
|
(when (/= (row-move object offset)
|
|
|
|
0)
|
|
|
|
(draw object)))))
|
2020-05-08 15:45:43 +02:00
|
|
|
|
|
|
|
(defmethod scroll-begin ((object message-window))
|
|
|
|
(with-accessors ((rows rows)
|
|
|
|
(row-selected-index row-selected-index)) object
|
|
|
|
(when (/= (row-move object (- row-selected-index))
|
|
|
|
0)
|
|
|
|
(draw object))))
|
|
|
|
|
|
|
|
(defmethod scroll-next-page ((object message-window))
|
|
|
|
(with-accessors ((rows rows)
|
|
|
|
(row-selected-index row-selected-index)) object
|
|
|
|
(let ((actual-window-height (win-height-no-border object)))
|
2021-04-08 15:13:31 +02:00
|
|
|
(when (and (> (- (rows-length object)
|
2020-05-08 15:45:43 +02:00
|
|
|
row-selected-index)
|
|
|
|
actual-window-height)
|
|
|
|
(/= (row-move object actual-window-height)
|
|
|
|
0))
|
|
|
|
(draw object)))))
|
|
|
|
|
|
|
|
(defmethod scroll-previous-page ((object message-window))
|
|
|
|
(when (/= (row-move object (- (win-height-no-border object)))
|
|
|
|
0)
|
|
|
|
(draw object)))
|
|
|
|
|
|
|
|
(defun first-line->string (window)
|
2021-04-08 15:13:31 +02:00
|
|
|
(with-accessors ((row-selected-index row-selected-index)) window
|
|
|
|
(let ((complex (normal-text (rows-elt window row-selected-index))))
|
2021-04-05 18:10:14 +02:00
|
|
|
(values (tui-string->chars-string complex)
|
|
|
|
complex))))
|
2020-05-08 15:45:43 +02:00
|
|
|
|
|
|
|
(defmethod search-regex ((object message-window) regex)
|
2021-04-08 15:13:31 +02:00
|
|
|
(with-accessors ((row-selected-index row-selected-index)) object
|
|
|
|
(let ((line-found (rows-position-if object
|
|
|
|
(lambda (a)
|
|
|
|
(scan regex
|
|
|
|
(tui-string->chars-string (normal-text a))))
|
|
|
|
:start (min (1+ row-selected-index)
|
|
|
|
(rows-length object))))
|
2021-04-06 18:27:47 +02:00
|
|
|
(replacements-strings ()))
|
2021-04-06 19:59:58 +02:00
|
|
|
(if line-found
|
|
|
|
(progn
|
|
|
|
(row-move object (- line-found row-selected-index))
|
|
|
|
(draw object)
|
|
|
|
(multiple-value-bind (first-window-line-simple first-window-line-complex)
|
|
|
|
(first-line->string object)
|
|
|
|
(labels ((calc-highlight (&optional (start-scan 0))
|
|
|
|
(multiple-value-bind (start end)
|
|
|
|
(scan regex first-window-line-simple :start start-scan)
|
|
|
|
(when start
|
|
|
|
(let* ((mask (make-tui-string (subseq first-window-line-simple
|
|
|
|
start end)
|
|
|
|
:fgcolor (win-bgcolor object)
|
|
|
|
:bgcolor (win-fgcolor object)))
|
|
|
|
(prefix (tui-string-subseq first-window-line-complex
|
|
|
|
0
|
|
|
|
start))
|
|
|
|
(new-prefix (cat-tui-string prefix mask)))
|
|
|
|
(push new-prefix replacements-strings)
|
|
|
|
(calc-highlight end)))))
|
|
|
|
(highlight ()
|
|
|
|
(loop for replacement in replacements-strings do
|
|
|
|
(print-text object replacement 1 1))))
|
|
|
|
(calc-highlight)
|
|
|
|
(highlight))))
|
|
|
|
(line-oriented-window:cleanup-after-search object)))))
|
2020-05-08 15:45:43 +02:00
|
|
|
|
|
|
|
(defun init ()
|
|
|
|
(let* ((low-level-window (make-croatoan-window :enable-function-keys t)))
|
|
|
|
(setf *message-window*
|
|
|
|
(make-instance 'message-window
|
|
|
|
:title (_ "Messages")
|
|
|
|
:keybindings keybindings:*message-keymap*
|
|
|
|
:key-config swconf:+key-message-window+
|
|
|
|
:croatoan-window low-level-window))
|
|
|
|
(refresh-config *message-window*)
|
|
|
|
(draw *message-window*)
|
|
|
|
*message-window*))
|
2021-04-03 15:03:24 +02:00
|
|
|
|
|
|
|
(defgeneric viewport-width (object))
|
|
|
|
|
|
|
|
(defmethod viewport-width ((object message-window))
|
|
|
|
(windows:win-width-no-border object))
|