diff --git a/lisp/channel-tui-main.lisp b/lisp/channel-tui-main.lisp index 94fdd6c..192487a 100644 --- a/lisp/channel-tui-main.lisp +++ b/lisp/channel-tui-main.lisp @@ -340,8 +340,17 @@ (add-msg :system "Usage: /resume ")))) ;; /help — search user manual ((and (>= (length text) 6) (string-equal (subseq text 0 6) "/help ")) - (let ((topic (string-trim '(#\Space) (subseq text 6)))) - (add-msg :system (format nil "Topic: ~a — use read-file to query USER_MANUAL.org" topic)))) + (let ((topic (string-trim '(#\Space) (subseq text 6))) + (sections (self-help-lookup (string-trim '(#\Space) (subseq text 6))))) + (if sections + (dolist (entry sections) + (let* ((title (car entry)) + (content (cdr entry)) + (preview (if (> (length content) 300) + (concatenate 'string (subseq content 0 297) "...") + content))) + (add-msg :system (format nil "~a: ~a" title preview)))) + (add-msg :system (format nil "No manual section found for '~a'" topic))))) ((string-equal text "/help") (add-msg :system "/eval Evaluate Lisp") (add-msg :system "/undo Undo last operation") @@ -558,6 +567,47 @@ (setf (st :dirty) (list nil t nil)) (loop-finish))) +;; v0.7.2 — self-help-lookup: read USER_MANUAL.org and find matching sections +(defun self-help-lookup (topic) + "Search USER_MANUAL.org for headlines matching TOPIC, return content previews." + (let* ((manual-path (merge-pathnames "projects/passepartout/docs/USER_MANUAL.org" + (merge-pathnames "memex/" (user-homedir-pathname)))) + (results nil)) + (handler-case + (let* ((text (uiop:read-file-string manual-path)) + (lines (uiop:split-string text :separator '(#\Newline))) + (in-section nil) + (section-content nil)) + (dolist (line lines) + (let ((trimmed (string-trim '(#\Space #\Tab) line))) + (cond + ;; New headline + ((and (>= (length trimmed) 2) (eql (char trimmed 0) #\*)) + ;; Flush previous section if in one + (when (and in-section section-content) + (push (cons in-section (string-trim '(#\Space #\Newline) + (format nil "~{~a~^ ~}" (reverse section-content)))) + results)) + ;; Check if this headline matches topic + (let ((title (string-trim '(#\Space #\*) trimmed))) + (if (search topic title :test #'char-equal) + (setf in-section title + section-content nil) + (setf in-section nil + section-content nil)))) + ;; Content line in matching section + (in-section + (when (and (> (length trimmed) 0) + (not (eql (char trimmed 0) #\#))) + (push trimmed section-content)))))) + ;; Flush last section + (when (and in-section section-content) + (push (cons in-section (string-trim '(#\Space #\Newline) + (format nil "~{~a~^ ~}" (reverse section-content)))) + results)) + (nreverse results)) + (error (c) (list (cons "Error" (format nil "Cannot read manual: ~a" c))))))) + (defun on-daemon-msg (msg) (let* ((payload (getf msg :payload)) (text (getf payload :text)) @@ -1220,3 +1270,12 @@ (fiveam:is (some (lambda (m) (search "IDENTITY" (getf m :content))) msgs)) (fiveam:is (some (lambda (m) (search "LOGS" (getf m :content))) msgs)) (fiveam:is (some (lambda (m) (search "TOOLS" (getf m :content))) msgs)))) + +(fiveam:test test-help-topic-lookup + "Contract v0.7.2: /help reads and searches USER_MANUAL.org." + (init-state) + (dolist (ch (coerce "/help configuration" 'list)) + (on-key (char-code ch))) + (on-key 13) + (let ((msgs (st :messages))) + (fiveam:is (some (lambda (m) (search ".env" (getf m :content))) msgs)))) diff --git a/org/channel-tui-main.org b/org/channel-tui-main.org index 6c58e82..590814c 100644 --- a/org/channel-tui-main.org +++ b/org/channel-tui-main.org @@ -374,8 +374,17 @@ Event handlers + daemon I/O + main loop. (add-msg :system "Usage: /resume ")))) ;; /help — search user manual ((and (>= (length text) 6) (string-equal (subseq text 0 6) "/help ")) - (let ((topic (string-trim '(#\Space) (subseq text 6)))) - (add-msg :system (format nil "Topic: ~a — use read-file to query USER_MANUAL.org" topic)))) + (let ((topic (string-trim '(#\Space) (subseq text 6))) + (sections (self-help-lookup (string-trim '(#\Space) (subseq text 6))))) + (if sections + (dolist (entry sections) + (let* ((title (car entry)) + (content (cdr entry)) + (preview (if (> (length content) 300) + (concatenate 'string (subseq content 0 297) "...") + content))) + (add-msg :system (format nil "~a: ~a" title preview)))) + (add-msg :system (format nil "No manual section found for '~a'" topic))))) ((string-equal text "/help") (add-msg :system "/eval Evaluate Lisp") (add-msg :system "/undo Undo last operation") @@ -592,6 +601,47 @@ Event handlers + daemon I/O + main loop. (setf (st :dirty) (list nil t nil)) (loop-finish))) +;; v0.7.2 — self-help-lookup: read USER_MANUAL.org and find matching sections +(defun self-help-lookup (topic) + "Search USER_MANUAL.org for headlines matching TOPIC, return content previews." + (let* ((manual-path (merge-pathnames "projects/passepartout/docs/USER_MANUAL.org" + (merge-pathnames "memex/" (user-homedir-pathname)))) + (results nil)) + (handler-case + (let* ((text (uiop:read-file-string manual-path)) + (lines (uiop:split-string text :separator '(#\Newline))) + (in-section nil) + (section-content nil)) + (dolist (line lines) + (let ((trimmed (string-trim '(#\Space #\Tab) line))) + (cond + ;; New headline + ((and (>= (length trimmed) 2) (eql (char trimmed 0) #\*)) + ;; Flush previous section if in one + (when (and in-section section-content) + (push (cons in-section (string-trim '(#\Space #\Newline) + (format nil "~{~a~^ ~}" (reverse section-content)))) + results)) + ;; Check if this headline matches topic + (let ((title (string-trim '(#\Space #\*) trimmed))) + (if (search topic title :test #'char-equal) + (setf in-section title + section-content nil) + (setf in-section nil + section-content nil)))) + ;; Content line in matching section + (in-section + (when (and (> (length trimmed) 0) + (not (eql (char trimmed 0) #\#))) + (push trimmed section-content)))))) + ;; Flush last section + (when (and in-section section-content) + (push (cons in-section (string-trim '(#\Space #\Newline) + (format nil "~{~a~^ ~}" (reverse section-content)))) + results)) + (nreverse results)) + (error (c) (list (cons "Error" (format nil "Cannot read manual: ~a" c))))))) + (defun on-daemon-msg (msg) (let* ((payload (getf msg :payload)) (text (getf payload :text)) @@ -1267,4 +1317,13 @@ Event handlers + daemon I/O + main loop. (fiveam:is (some (lambda (m) (search "IDENTITY" (getf m :content))) msgs)) (fiveam:is (some (lambda (m) (search "LOGS" (getf m :content))) msgs)) (fiveam:is (some (lambda (m) (search "TOOLS" (getf m :content))) msgs)))) + +(fiveam:test test-help-topic-lookup + "Contract v0.7.2: /help reads and searches USER_MANUAL.org." + (init-state) + (dolist (ch (coerce "/help configuration" 'list)) + (on-key (char-code ch))) + (on-key 13) + (let ((msgs (st :messages))) + (fiveam:is (some (lambda (m) (search ".env" (getf m :content))) msgs)))) #+end_src