fix: add SIGWINCH resize handling for /dev/tty input path
Main loop now checks cl-tty.input::*terminal-resized-p* on every iteration. When set (by SIGWINCH handler), re-queries backend-size and marks all regions dirty for re-render.
This commit is contained in:
@@ -866,43 +866,49 @@
|
||||
(cl-tty.backend:with-terminal (be w h)
|
||||
(let ((tty (open "/dev/tty" :direction :input)))
|
||||
;; Initial render
|
||||
(cl-tty.backend:backend-clear be)
|
||||
(view-status be w h)
|
||||
(view-chat be w h)
|
||||
(view-input be w h)
|
||||
(cl-tty.backend:draw-text be 0 (- h 4) (make-string w :initial-element #\─)
|
||||
(theme-color :separator) nil)
|
||||
(loop while (st :running) do
|
||||
(dolist (ev (drain-queue))
|
||||
(cond
|
||||
((eq (getf ev :type) :daemon)
|
||||
(on-daemon-msg (getf ev :payload)))
|
||||
((eq (getf ev :type) :disconnected)
|
||||
(setf (st :connected) nil
|
||||
(st :busy) nil)
|
||||
(add-msg :system "* Connection lost — type /reconnect to retry *"))))
|
||||
;; Read key input from /dev/tty (non-blocking)
|
||||
(let ((raw-ch (read-char-no-hang tty nil nil)))
|
||||
(when raw-ch
|
||||
(let ((code (char-code raw-ch)))
|
||||
(let ((ch (cond
|
||||
((= code 13) :enter)
|
||||
((= code 10) :enter)
|
||||
((= code 27) :escape)
|
||||
((= code 9) :tab)
|
||||
((or (= code 127) (= code 8)) :backspace)
|
||||
((and (>= code 1) (<= code 26))
|
||||
(intern (string-upcase (format nil "CTRL-~a"
|
||||
(code-char (+ #x60 code))))
|
||||
:keyword))
|
||||
(t raw-ch))))
|
||||
(case ch
|
||||
(:CTRL-Q (setf (st :running) nil))
|
||||
(:CTRL-P (command-palette-show-commands))
|
||||
(:CTRL-B (setf (st :sidebar-visible) (not (st :sidebar-visible)))
|
||||
(setf (st :dirty) (list t t nil)))
|
||||
(:CTRL-L (setf (st :dirty) (list t t t)))
|
||||
(t (on-key ch)))))))
|
||||
(cl-tty.backend:backend-clear be)
|
||||
(view-status be w h)
|
||||
(view-chat be w h)
|
||||
(view-input be w h)
|
||||
(cl-tty.backend:draw-text be 0 (- h 4) (make-string w :initial-element #\─)
|
||||
(theme-color :separator) nil)
|
||||
(loop while (st :running) do
|
||||
(dolist (ev (drain-queue))
|
||||
(cond
|
||||
((eq (getf ev :type) :daemon)
|
||||
(on-daemon-msg (getf ev :payload)))
|
||||
((eq (getf ev :type) :disconnected)
|
||||
(setf (st :connected) nil
|
||||
(st :busy) nil)
|
||||
(add-msg :system "* Connection lost — type /reconnect to retry *"))))
|
||||
;; Check for terminal resize (SIGWINCH sets this flag)
|
||||
(when (boundp 'cl-tty.input::*terminal-resized-p*)
|
||||
(when cl-tty.input::*terminal-resized-p*
|
||||
(setf cl-tty.input::*terminal-resized-p* nil)
|
||||
(multiple-value-setq (w h) (cl-tty.backend:backend-size be))
|
||||
(setf (st :dirty) (list t t t))))
|
||||
;; Read key input from /dev/tty (non-blocking)
|
||||
(let ((raw-ch (read-char-no-hang tty nil nil)))
|
||||
(when raw-ch
|
||||
(let ((code (char-code raw-ch)))
|
||||
(let ((ch (cond
|
||||
((= code 13) :enter)
|
||||
((= code 10) :enter)
|
||||
((= code 27) :escape)
|
||||
((= code 9) :tab)
|
||||
((or (= code 127) (= code 8)) :backspace)
|
||||
((and (>= code 1) (<= code 26))
|
||||
(intern (string-upcase (format nil "CTRL-~a"
|
||||
(code-char (+ #x60 code))))
|
||||
:keyword))
|
||||
(t raw-ch))))
|
||||
(case ch
|
||||
(:CTRL-Q (setf (st :running) nil))
|
||||
(:CTRL-P (command-palette-show-commands))
|
||||
(:CTRL-B (setf (st :sidebar-visible) (not (st :sidebar-visible)))
|
||||
(setf (st :dirty) (list t t nil)))
|
||||
(:CTRL-L (setf (st :dirty) (list t t t)))
|
||||
(t (on-key ch)))))))
|
||||
(when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
|
||||
(cl-tty.backend:backend-clear be)
|
||||
(view-status be w h)
|
||||
@@ -944,29 +950,23 @@
|
||||
(sel-p (theme-color :accent))
|
||||
(t (theme-color :agent-fg)))
|
||||
nil :bold sel-p)
|
||||
(incf y-off)))))))
|
||||
(sleep 0.1)))
|
||||
(incf y-off)))))))
|
||||
(sleep 0.1))))
|
||||
(close tty))
|
||||
(disconnect-daemon))))
|
||||
(disconnect-daemon)))
|
||||
|
||||
#+passepartout-tests
|
||||
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||
(ql:quickload :fiveam :silent t))
|
||||
|
||||
#+passepartout-tests
|
||||
(defpackage :passepartout-tui-tests
|
||||
(:use :cl :passepartout :passepartout.channel-tui)
|
||||
(:export #:tui-suite))
|
||||
|
||||
#+passepartout-tests
|
||||
(in-package :passepartout-tui-tests)
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:def-suite tui-suite :description "Verification of the TUI model and event handling")
|
||||
#+passepartout-tests
|
||||
(fiveam:in-suite tui-suite)
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-init-state
|
||||
"Contract model.1: init-state returns fresh state plist with required keys."
|
||||
(init-state)
|
||||
@@ -978,7 +978,6 @@
|
||||
(fiveam:is (eq 0 (st :scroll-offset)))
|
||||
(fiveam:is (eq nil (st :busy))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-add-msg
|
||||
"Contract model.2: add-msg appends a message with role, content, and time."
|
||||
(init-state)
|
||||
@@ -990,7 +989,6 @@
|
||||
(fiveam:is (stringp (getf msg :time)))
|
||||
(fiveam:is (= 5 (length (getf msg :time))))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-add-msg-dirty-flag
|
||||
"Contract model.2: add-msg sets dirty flags for status and chat."
|
||||
(init-state)
|
||||
@@ -1001,7 +999,6 @@
|
||||
(fiveam:is (eq t (second dirty)))
|
||||
(fiveam:is (eq nil (third dirty)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-queue-event-roundtrip
|
||||
"Contract model.3: queue-event + drain-queue preserves events in order."
|
||||
(init-state)
|
||||
@@ -1013,7 +1010,6 @@
|
||||
(fiveam:is (equal '(:type :daemon :payload (:text "hi")) (second evs)))
|
||||
(fiveam:is (null (drain-queue)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-enter-sends-user-message
|
||||
"Contract 1: on-key with Enter extracts input, adds user message, clears buffer."
|
||||
(init-state)
|
||||
@@ -1032,7 +1028,6 @@
|
||||
(fiveam:is (eq :user (getf last :role)))
|
||||
(fiveam:is (string= "test" (getf last :content))))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-eval-command
|
||||
"Contract 1: on-key handles /eval command and displays result."
|
||||
(init-state)
|
||||
@@ -1046,7 +1041,6 @@
|
||||
(fiveam:is (eq :system (getf last-msg :role)))
|
||||
(fiveam:is (search "=> 3" (getf last-msg :content))))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-backspace
|
||||
"Contract 1: on-key with Backspace removes last character from buffer."
|
||||
(init-state)
|
||||
@@ -1057,7 +1051,6 @@
|
||||
(on-key 263)
|
||||
(fiveam:is (string= "ab" (input-string))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-focus-command
|
||||
"Contract 1: /focus command parses project name."
|
||||
(init-state)
|
||||
@@ -1067,7 +1060,6 @@
|
||||
(let ((msg (aref (st :messages) 0)))
|
||||
(fiveam:is (eq :system (getf msg :role)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-scope-command
|
||||
"Contract 1: /scope command with valid argument."
|
||||
(init-state)
|
||||
@@ -1077,7 +1069,6 @@
|
||||
(let ((msg (aref (st :messages) 0)))
|
||||
(fiveam:is (eq :system (getf msg :role)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-unfocus-command
|
||||
"Contract 1: /unfocus command dispatches correctly."
|
||||
(init-state)
|
||||
@@ -1087,7 +1078,6 @@
|
||||
(let ((msg (aref (st :messages) 0)))
|
||||
(fiveam:is (eq :system (getf msg :role)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-tab-completion
|
||||
"Contract 1: Tab completes / commands when input starts with /."
|
||||
(init-state)
|
||||
@@ -1096,7 +1086,6 @@
|
||||
(on-key 9)
|
||||
(fiveam:is (string= "/eval " (input-string))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-tab-no-slash
|
||||
"Contract 1: Tab does nothing when input doesn't start with /."
|
||||
(init-state)
|
||||
@@ -1105,7 +1094,6 @@
|
||||
(on-key 9)
|
||||
(fiveam:is (string= "hello" (input-string))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-multiline
|
||||
"Contract 1: \\ + Enter inserts newline instead of sending."
|
||||
(init-state)
|
||||
@@ -1116,7 +1104,6 @@
|
||||
(fiveam:is (search "line1" (input-string)))
|
||||
(fiveam:is (search (string #\Newline) (input-string))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-help
|
||||
"Contract 1: /help displays command list."
|
||||
(init-state)
|
||||
@@ -1127,7 +1114,6 @@
|
||||
(fiveam:is (>= (length msgs) 3))
|
||||
(fiveam:is (some (lambda (m) (search "/eval" (getf m :content))) msgs))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-activity-indicator
|
||||
"Contract model: :busy flag is set on send and cleared on agent response."
|
||||
(init-state)
|
||||
@@ -1141,7 +1127,6 @@
|
||||
(on-daemon-msg '(:type :event :payload (:text "hi back")))
|
||||
(fiveam:is (eq nil (st :busy))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-theme
|
||||
"Contract view: *tui-theme* provides warm color mappings."
|
||||
(fiveam:is (string= "#FFB347" (getf *tui-theme* :user-fg)))
|
||||
@@ -1150,7 +1135,6 @@
|
||||
(fiveam:is (string= "#E8D5B7" (getf *tui-theme* :input-fg)))
|
||||
(fiveam:is (string= "#FFFFFF" (theme-color :unknown-role))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-ctrl-u-clears
|
||||
"Contract v0.9.0: Ctrl+U (via dispatch-key-event) clears the input buffer."
|
||||
(init-state)
|
||||
@@ -1159,7 +1143,6 @@
|
||||
(cl-tty.input:make-key-event :key :u :ctrl t :code 21))
|
||||
(fiveam:is (string= "" (input-string))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-on-key-ctrl-l-redraws
|
||||
"Contract v0.9.0: Ctrl+L (via dispatch-key-event) sets all dirty flags."
|
||||
(init-state)
|
||||
@@ -1170,7 +1153,6 @@
|
||||
(fiveam:is (eq t (first d)))
|
||||
(fiveam:is (eq t (second d)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-scroll-notify
|
||||
"Contract/v0.7.0: add-msg sets scroll-notify when scrolled up."
|
||||
(init-state)
|
||||
@@ -1181,7 +1163,6 @@
|
||||
(add-msg :agent "hi2")
|
||||
(fiveam:is (eq nil (st :scroll-notify))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-tab-subcommand
|
||||
"Contract/v0.7.0: Tab completes subcommand for /theme."
|
||||
(init-state)
|
||||
@@ -1191,7 +1172,6 @@
|
||||
|
||||
;; ── v0.7.1 Streaming ──
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-stream-chunk-appends
|
||||
"Contract/v0.7.1: stream-chunk frame appends to last message."
|
||||
(init-state)
|
||||
@@ -1204,7 +1184,6 @@
|
||||
(fiveam:is (string= "Hello world" (getf msg :content)))
|
||||
(fiveam:is (eq t (getf msg :streaming))))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-stream-chunk-final
|
||||
"Contract/v0.7.1: final empty chunk stamps timestamp and clears streaming flag."
|
||||
(init-state)
|
||||
@@ -1215,7 +1194,6 @@
|
||||
(fiveam:is (string= "Hi" (getf msg :content)))
|
||||
(fiveam:is (null (st :streaming-text)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-stream-interrupt
|
||||
"Contract/v0.7.1: Esc during streaming appends [interrupted] and finalizes."
|
||||
(init-state)
|
||||
@@ -1227,7 +1205,6 @@
|
||||
(fiveam:is (null (st :streaming-text)))
|
||||
(fiveam:is (null (st :busy)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-stream-check-skip
|
||||
"Contract/v0.7.1: Esc without active streaming does nothing."
|
||||
(init-state)
|
||||
@@ -1235,7 +1212,6 @@
|
||||
(fiveam:is (null (st :streaming-text)))
|
||||
(fiveam:is (= 0 (length (st :messages)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-tab-open-url
|
||||
"Contract/v0.7.1: Tab on empty input with URL message extracts URL."
|
||||
(init-state)
|
||||
@@ -1245,7 +1221,6 @@
|
||||
|
||||
;; ── v0.7.2 HITL Panels ──
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-hitl-panel-in-on-daemon-msg
|
||||
"Contract v0.7.2: approval-required messages render as HITL panels."
|
||||
(init-state)
|
||||
@@ -1258,7 +1233,6 @@
|
||||
(fiveam:is (getf m :panel))
|
||||
(fiveam:is (search "rm -rf" (getf m :content)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-hitl-panel-after-approve
|
||||
"Contract v0.7.2: /approve adds confirmation and marks panel resolved."
|
||||
(init-state)
|
||||
@@ -1275,7 +1249,6 @@
|
||||
(let ((m (aref (st :messages) (1- (length (st :messages))))))
|
||||
(fiveam:is (search "Approved" (getf m :content)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-hitl-panel-after-deny
|
||||
"Contract v0.7.2: /deny marks panel as denied."
|
||||
(init-state)
|
||||
@@ -1288,7 +1261,6 @@
|
||||
(fiveam:is (getf m :panel))
|
||||
(fiveam:is (eq :denied (getf m :panel-resolved)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-hitl-approve-parsed
|
||||
"Contract v0.7.2: /approve HITL-xxxx sends structured event, not raw text."
|
||||
(init-state)
|
||||
@@ -1302,7 +1274,6 @@
|
||||
(fiveam:is (eq :system (getf m :role)))
|
||||
(fiveam:is (search "Approved" (getf m :content))))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-hitl-deny-parsed
|
||||
"Contract v0.7.2: /deny HITL-xxxx sends structured denial."
|
||||
(init-state)
|
||||
@@ -1315,7 +1286,6 @@
|
||||
|
||||
;; ── v0.7.2 Undo/Redo ──
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-undo-command
|
||||
"Contract v0.7.2: /undo sends undo event."
|
||||
(init-state)
|
||||
@@ -1326,7 +1296,6 @@
|
||||
(fiveam:is (eq :system (getf m :role)))
|
||||
(fiveam:is (search "Undo" (getf m :content)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-redo-command
|
||||
"Contract v0.7.2: /redo sends redo event."
|
||||
(init-state)
|
||||
@@ -1339,7 +1308,6 @@
|
||||
|
||||
;; ── v0.7.2 Self-help ──
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-why-command
|
||||
"Contract v0.7.2: /why shows gate trace from last message."
|
||||
(init-state)
|
||||
@@ -1353,7 +1321,6 @@
|
||||
(fiveam:is (search "[BLOCKED]" (getf m :content)))
|
||||
(fiveam:is (search "shell" (getf m :content)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-why-no-trace
|
||||
"Contract v0.7.2: /why with no gate trace shows fallback message."
|
||||
(init-state)
|
||||
@@ -1366,7 +1333,6 @@
|
||||
|
||||
;; ── v0.7.2 Gate Trace Toggle (Ctrl+G) ──
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-ctrlg-toggle-gate-trace
|
||||
"Contract v0.9.0: Ctrl+G (via dispatch-key-event) toggles gate-trace collapse state."
|
||||
(init-state)
|
||||
@@ -1382,7 +1348,6 @@
|
||||
(m (aref msgs (1- (length msgs)))))
|
||||
(fiveam:is (search "shown" (getf m :content)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-ctrlg-no-gate-trace
|
||||
"Contract v0.9.0: Ctrl+G (via dispatch-key-event) with no gate trace shows fallback."
|
||||
(init-state)
|
||||
@@ -1393,7 +1358,6 @@
|
||||
|
||||
;; ── v0.7.2 Message Search Mode ──
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-search-mode-activate
|
||||
"Contract v0.7.2: /search enters search mode."
|
||||
(init-state)
|
||||
@@ -1406,7 +1370,6 @@
|
||||
(fiveam:is (string= "hello" (st :search-query)))
|
||||
(fiveam:is (= 1 (length (st :search-matches)))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-search-mode-escape-exits
|
||||
"Contract v0.7.2: Escape exits search mode."
|
||||
(init-state)
|
||||
@@ -1418,7 +1381,6 @@
|
||||
(on-key 27) ;; Escape
|
||||
(fiveam:is (null (st :search-mode))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-search-mode-up-down-nav
|
||||
"Contract v0.7.2: Up/Down navigates between search matches."
|
||||
(init-state)
|
||||
@@ -1436,7 +1398,6 @@
|
||||
(on-key 259) ;; Up (clamped)
|
||||
(fiveam:is (= 0 (st :search-match-idx))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-context-sections
|
||||
"Contract v0.7.2: /context shows section breakdown with IDENTITY, TOOLS, LOGS."
|
||||
(init-state)
|
||||
@@ -1449,7 +1410,6 @@
|
||||
(fiveam:is (some (lambda (m) (search "LOGS" (getf m :content))) msgs))
|
||||
(fiveam:is (some (lambda (m) (search "TOOLS" (getf m :content))) msgs))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-help-topic-lookup
|
||||
"Contract v0.7.2: /help <topic> reads and searches USER_MANUAL.org."
|
||||
(init-state)
|
||||
@@ -1459,7 +1419,6 @@
|
||||
(let ((msgs (st :messages)))
|
||||
(fiveam:is (some (lambda (m) (search ".env" (getf m :content))) msgs))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-pads-page-up
|
||||
"Contract v0.7.2: PageUp scrolls by page size (> 5 lines)."
|
||||
(init-state)
|
||||
@@ -1468,7 +1427,6 @@
|
||||
(on-key :ppage)
|
||||
(fiveam:is (> (st :scroll-offset) 5) "Should scroll by more than 5 lines"))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-pads-page-down-clamp
|
||||
"Contract v0.7.2: PageDown clamps to 0."
|
||||
(init-state)
|
||||
@@ -1479,7 +1437,6 @@
|
||||
|
||||
;; ── v0.8.0 Minibuffer ──
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-slash-commands-defined
|
||||
"Contract v0.8.0: *slash-commands* is non-nil list of option plists."
|
||||
(fiveam:is (listp passepartout.channel-tui::*slash-commands*))
|
||||
@@ -1488,14 +1445,12 @@
|
||||
(and (getf opt :title) (getf opt :value) (getf opt :category)))
|
||||
passepartout.channel-tui::*slash-commands*)))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-minibuffer-state
|
||||
"Contract v0.8.0: init-state has :dialog-stack and :minibuffer-active fields."
|
||||
(init-state)
|
||||
(fiveam:is (null (st :dialog-stack)))
|
||||
(fiveam:is (null (st :minibuffer-active))))
|
||||
|
||||
#+passepartout-tests
|
||||
(fiveam:test test-command-palette-state
|
||||
"Contract v0.8.0: init-state has :command-palette-active and :command-palette-dialog as nil."
|
||||
(init-state)
|
||||
|
||||
@@ -925,6 +925,12 @@ Event handlers + daemon I/O + main loop.
|
||||
(setf (st :connected) nil
|
||||
(st :busy) nil)
|
||||
(add-msg :system "* Connection lost — type /reconnect to retry *"))))
|
||||
;; Check for terminal resize (SIGWINCH sets this flag)
|
||||
(when (boundp 'cl-tty.input::*terminal-resized-p*)
|
||||
(when cl-tty.input::*terminal-resized-p*
|
||||
(setf cl-tty.input::*terminal-resized-p* nil)
|
||||
(multiple-value-setq (w h) (cl-tty.backend:backend-size be))
|
||||
(setf (st :dirty) (list t t t))))
|
||||
;; Read key input from /dev/tty (non-blocking)
|
||||
(let ((raw-ch (read-char-no-hang tty nil nil)))
|
||||
(when raw-ch
|
||||
|
||||
Reference in New Issue
Block a user