Search in documents #37
Merged@k33g wants to merge
feature/search-in-doc into mainMerged as
303fa046a9df.59 files changed
modified
.tickets/issues/0106-search-doc-bm25-search-in-text-files.yaml +97 -12 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 106 |
| 3 | -title: search_doc — BM25 search in the text files of a folder (built-in tool) | |
| 4 | -state: open | |
| 3 | +title: search_docs — BM25 search in the text files of folders (/docs and a built-in tool) | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-10-20T20:00:00.000Z |
| 9 | -updatedAt: 2026-10-20T20:00:00.000Z | |
| 9 | +updatedAt: 2026-10-20T23:30:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - kind::feature |
| 12 | 12 | milestone: v0.8.0 |
| @@ -67,7 +67,7 @@ body: | | ||
| 67 | 67 | tasks: |
| 68 | 68 | - id: 1 |
| 69 | 69 | title: "Agree on the config key, defaults and the open questions below" |
| 70 | - state: open | |
| 70 | + state: closed | |
| 71 | 71 | priority: none |
| 72 | 72 | author: |
| 73 | 73 | name: k33g |
| @@ -76,7 +76,7 @@ tasks: | ||
| 76 | 76 | updatedAt: 2026-10-20T20:00:00.000Z |
| 77 | 77 | - id: 2 |
| 78 | 78 | title: "docsearch: chunker — Markdown (# headings, not inside code fences), AsciiDoc (= headings), plain text (blank-line paragraphs); heading path and line kept; long sections split by paragraphs — with tests" |
| 79 | - state: open | |
| 79 | + state: closed | |
| 80 | 80 | priority: none |
| 81 | 81 | author: |
| 82 | 82 | name: k33g |
| @@ -85,7 +85,7 @@ tasks: | ||
| 85 | 85 | updatedAt: 2026-10-20T20:00:00.000Z |
| 86 | 86 | - id: 3 |
| 87 | 87 | title: "docsearch: BM25 (k1 1.2, b 0.75) over the chunks, words folded like store.fold (case, accents), heading boost — with tests" |
| 88 | - state: open | |
| 88 | + state: closed | |
| 89 | 89 | priority: none |
| 90 | 90 | author: |
| 91 | 91 | name: k33g |
| @@ -94,7 +94,7 @@ tasks: | ||
| 94 | 94 | updatedAt: 2026-10-20T20:00:00.000Z |
| 95 | 95 | - id: 4 |
| 96 | 96 | title: "docsearch: walk the folders (extensions, size cap, skip hidden dirs, .git, node_modules), scanned at each call, no index — with tests" |
| 97 | - state: open | |
| 97 | + state: closed | |
| 98 | 98 | priority: none |
| 99 | 99 | author: |
| 100 | 100 | name: k33g |
| @@ -102,8 +102,8 @@ tasks: | ||
| 102 | 102 | createdAt: 2026-10-20T20:00:00.000Z |
| 103 | 103 | updatedAt: 2026-10-20T20:00:00.000Z |
| 104 | 104 | - id: 5 |
| 105 | - title: "built-in tool search_doc {query, limit?}: hits = path:line, heading path, snippet; declared only when the config enables it; never asks; ACP kind search — with tests" | |
| 106 | - state: open | |
| 105 | + title: "built-in tool search_docs {query, limit?} and the /docs <words> command: hits = path:line, heading path, snippet; declared only when the config enables it; never asks; ACP kind search — with tests" | |
| 106 | + state: closed | |
| 107 | 107 | priority: none |
| 108 | 108 | author: |
| 109 | 109 | name: k33g |
| @@ -112,7 +112,7 @@ tasks: | ||
| 112 | 112 | updatedAt: 2026-10-20T20:00:00.000Z |
| 113 | 113 | - id: 6 |
| 114 | 114 | title: "Measure on a small model (llmman) over mm's own docs/ (EN+FR): found when the words are there, not called otherwise; time per call on docs/" |
| 115 | - state: open | |
| 115 | + state: closed | |
| 116 | 116 | priority: none |
| 117 | 117 | author: |
| 118 | 118 | name: k33g |
| @@ -121,11 +121,96 @@ tasks: | ||
| 121 | 121 | updatedAt: 2026-10-20T20:00:00.000Z |
| 122 | 122 | - id: 7 |
| 123 | 123 | title: "Docs EN+FR (reference configuration and tools, how-to, explanation: why BM25 and no index), demo if useful, quality gate, .memory" |
| 124 | - state: open | |
| 124 | + state: closed | |
| 125 | 125 | priority: none |
| 126 | 126 | author: |
| 127 | 127 | name: k33g |
| 128 | 128 | email: ph.charriere@gmail.com |
| 129 | 129 | createdAt: 2026-10-20T20:00:00.000Z |
| 130 | 130 | updatedAt: 2026-10-20T20:00:00.000Z |
| 131 | -comments: [] | |
| 131 | +comments: | |
| 132 | + - id: 1 | |
| 133 | + author: | |
| 134 | + name: k33g | |
| 135 | + email: ph.charriere@gmail.com | |
| 136 | + createdAt: 2026-10-20T21:00:00.000Z | |
| 137 | + body: | | |
| 138 | + Decided by the user (2026-10-20) — the proposal is validated, with: | |
| 139 | + - tool name: search_docs (plural); | |
| 140 | + - config block `docSearch`, with TWO switches instead of `enabled`: | |
| 141 | + `searchDocs: true` → the /docs <words> command (for the user), | |
| 142 | + `autoSearchDocs: true` → the search_docs tool (for the model); | |
| 143 | + both default false, independent (as /memory and search_memory); | |
| 144 | + - /docs: yes; | |
| 145 | + - prefix matching for words of 4+ letters: yes; | |
| 146 | + - extensions: only what the configuration lists (no hidden extra); | |
| 147 | + the default list is the validated one (.md .markdown .txt .adoc | |
| 148 | + .asciidoc .rst); symlinks not followed; | |
| 149 | + - key name: docSearch. | |
| 150 | + Then: demos/docs-search, whose documents are a set of simple | |
| 151 | + tutorials on using ratatui in Rust (Markdown, generated for the demo). | |
| 152 | + Branch feature/search-in-doc (created by the user). | |
| 153 | + - id: 2 | |
| 154 | + author: | |
| 155 | + name: k33g | |
| 156 | + email: ph.charriere@gmail.com | |
| 157 | + createdAt: 2026-10-20T23:00:00.000Z | |
| 158 | + body: | | |
| 159 | + Tasks 2-6 done on feature/search-in-doc (go test ./... green, gofmt/vet | |
| 160 | + clean): | |
| 161 | + - internal/docsearch (new): passages.go (sections at # / = headings, | |
| 162 | + fences and listing blocks ignored, heading path, cut at blank lines | |
| 163 | + past 1,500 runes, never inside a fence), bm25.go (k1 1.2, b 0.75, | |
| 164 | + heading-path words +2, prefix match for 4+ runes, plural/-ed within | |
| 165 | + 2 runes, query words < 3 runes dropped beside longer ones), walk.go | |
| 166 | + (listed extensions only, maxFileSize, hidden dirs + node_modules | |
| 167 | + skipped, symlinks not followed, NUL = binary, missing dirs | |
| 168 | + reported), docsearch.go (Search, Result.Format), configured.go | |
| 169 | + (Configured/Answer from config). | |
| 170 | + - internal/textfold (new): Fold/Rune moved out of store (shared by | |
| 171 | + store and docsearch, no duplicate). | |
| 172 | + - config/docsearch.go: docSearch (searchDocs, autoSearchDocs, dirs | |
| 173 | + [docs], extensions [.md .markdown .txt .adoc .asciidoc .rst], | |
| 174 | + maxFileSize 1MB — ByteSize reads 1MB / 512KB / bytes); validated | |
| 175 | + only when a switch is on; search_docs reserved. | |
| 176 | + - command/docs.go SetDocs; /docs in the REPL, the window (notice) and | |
| 177 | + ACP (agent text, input hint); tools/searchdocs.go search_docs | |
| 178 | + (query, limit? — 5, cap 20), never asks, ACP kind search, dir = the | |
| 179 | + sink's WorkDir else the process cwd; main.go wiring. | |
| 180 | + - Speed (best of 10): mm's docs/ 137 files / 1,320 passages ≈ 45 ms; | |
| 181 | + the demo's 10 files / 44 passages ≈ 1 ms. | |
| 182 | + demos/docs-search: Rata, Qwen3.5 4B on llmman, 9 Ratatui 0.30 | |
| 183 | + tutorials + index; check-tutorials.sh compiles every complete program | |
| 184 | + + snippets.rs in a rust container (-Dwarnings): OK, 10 programs, | |
| 185 | + ratatui 0.30.2. Measured (temp 0, 3 runs each, identical): List → | |
| 186 | + ListState, 04-lists.md:5 3/3; popup → Clear + centered, NO link 3/3; | |
| 187 | + Windows double key → right answer (KeyEventKind::Press) 3/3 but cited | |
| 188 | + code examples instead of 08:60 (ranked first by the search); progress | |
| 189 | + bar → Gauge + event::poll, 06:9 3/3; two French questions → English | |
| 190 | + searches, French answers, right links 6/6; "Write src/main.rs …" → | |
| 191 | + file written 3/3, compiles against ratatui 0.30.2 3/3; greeting and | |
| 192 | + an off-topic question → no search 6/6. Prompt lesson: the Windows | |
| 193 | + question was answered without a search (3/3) until the prompt said any | |
| 194 | + terminal-app question is a Ratatui question — after which the popup | |
| 195 | + answer stopped citing its link. | |
| 196 | + Task 7: docs EN+FR done (reference configuration + validation errors, | |
| 197 | + slash-commands, tools, acp; how-to search-the-docs; explanation | |
| 198 | + document-search; indexes; root README; packages.drawio). Quality gate | |
| 199 | + NOT run (qlty absent here) — task 7 stays open for it. | |
| 200 | + - id: 3 | |
| 201 | + author: | |
| 202 | + name: k33g | |
| 203 | + email: ph.charriere@gmail.com | |
| 204 | + createdAt: 2026-10-20T23:30:00.000Z | |
| 205 | + body: | | |
| 206 | + Closed at the user's request. Delivered: internal/docsearch + | |
| 207 | + internal/textfold, docSearch config (searchDocs / autoSearchDocs), | |
| 208 | + /docs in the three front ends, the search_docs tool, tests, docs EN+FR | |
| 209 | + (reference, how-to search-the-docs, explanation document-search), | |
| 210 | + demos/docs-search (Ratatui tutorials, compile-checked, measured). | |
| 211 | + Task 7 closed with the qlty quality gate still NOT run (qlty is not | |
| 212 | + installed in this sandbox); gofmt and go vet clean, go test ./... | |
| 213 | + green. Known limits left as documented: words, not meanings; in the | |
| 214 | + demo the popup answer gives no link and the Windows answer cites code | |
| 215 | + examples rather than the explaining passage. | |
| 216 | + | |
| @@ -1,12 +1,12 @@ | |||
| 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | id: 106 | 2 | id: 106 |
| 3 | -title: search_doc — BM25 search in the text files of a folder (built-in tool) | 3 | +title: search_docs — BM25 search in the text files of folders (/docs and a built-in tool) |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-10-20T20:00:00.000Z | 8 | createdAt: 2026-10-20T20:00:00.000Z |
| 9 | -updatedAt: 2026-10-20T20:00:00.000Z | 9 | +updatedAt: 2026-10-20T23:30:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - kind::feature | 11 | - kind::feature |
| 12 | milestone: v0.8.0 | 12 | milestone: v0.8.0 |
| @@ -67,7 +67,7 @@ body: | | |||
| 67 | tasks: | 67 | tasks: |
| 68 | - id: 1 | 68 | - id: 1 |
| 69 | title: "Agree on the config key, defaults and the open questions below" | 69 | title: "Agree on the config key, defaults and the open questions below" |
| 70 | - state: open | 70 | + state: closed |
| 71 | priority: none | 71 | priority: none |
| 72 | author: | 72 | author: |
| 73 | name: k33g | 73 | name: k33g |
| @@ -76,7 +76,7 @@ tasks: | |||
| 76 | updatedAt: 2026-10-20T20:00:00.000Z | 76 | updatedAt: 2026-10-20T20:00:00.000Z |
| 77 | - id: 2 | 77 | - id: 2 |
| 78 | title: "docsearch: chunker — Markdown (# headings, not inside code fences), AsciiDoc (= headings), plain text (blank-line paragraphs); heading path and line kept; long sections split by paragraphs — with tests" | 78 | title: "docsearch: chunker — Markdown (# headings, not inside code fences), AsciiDoc (= headings), plain text (blank-line paragraphs); heading path and line kept; long sections split by paragraphs — with tests" |
| 79 | - state: open | 79 | + state: closed |
| 80 | priority: none | 80 | priority: none |
| 81 | author: | 81 | author: |
| 82 | name: k33g | 82 | name: k33g |
| @@ -85,7 +85,7 @@ tasks: | |||
| 85 | updatedAt: 2026-10-20T20:00:00.000Z | 85 | updatedAt: 2026-10-20T20:00:00.000Z |
| 86 | - id: 3 | 86 | - id: 3 |
| 87 | title: "docsearch: BM25 (k1 1.2, b 0.75) over the chunks, words folded like store.fold (case, accents), heading boost — with tests" | 87 | title: "docsearch: BM25 (k1 1.2, b 0.75) over the chunks, words folded like store.fold (case, accents), heading boost — with tests" |
| 88 | - state: open | 88 | + state: closed |
| 89 | priority: none | 89 | priority: none |
| 90 | author: | 90 | author: |
| 91 | name: k33g | 91 | name: k33g |
| @@ -94,7 +94,7 @@ tasks: | |||
| 94 | updatedAt: 2026-10-20T20:00:00.000Z | 94 | updatedAt: 2026-10-20T20:00:00.000Z |
| 95 | - id: 4 | 95 | - id: 4 |
| 96 | title: "docsearch: walk the folders (extensions, size cap, skip hidden dirs, .git, node_modules), scanned at each call, no index — with tests" | 96 | title: "docsearch: walk the folders (extensions, size cap, skip hidden dirs, .git, node_modules), scanned at each call, no index — with tests" |
| 97 | - state: open | 97 | + state: closed |
| 98 | priority: none | 98 | priority: none |
| 99 | author: | 99 | author: |
| 100 | name: k33g | 100 | name: k33g |
| @@ -102,8 +102,8 @@ tasks: | |||
| 102 | createdAt: 2026-10-20T20:00:00.000Z | 102 | createdAt: 2026-10-20T20:00:00.000Z |
| 103 | updatedAt: 2026-10-20T20:00:00.000Z | 103 | updatedAt: 2026-10-20T20:00:00.000Z |
| 104 | - id: 5 | 104 | - id: 5 |
| 105 | - title: "built-in tool search_doc {query, limit?}: hits = path:line, heading path, snippet; declared only when the config enables it; never asks; ACP kind search — with tests" | 105 | + title: "built-in tool search_docs {query, limit?} and the /docs <words> command: hits = path:line, heading path, snippet; declared only when the config enables it; never asks; ACP kind search — with tests" |
| 106 | - state: open | 106 | + state: closed |
| 107 | priority: none | 107 | priority: none |
| 108 | author: | 108 | author: |
| 109 | name: k33g | 109 | name: k33g |
| @@ -112,7 +112,7 @@ tasks: | |||
| 112 | updatedAt: 2026-10-20T20:00:00.000Z | 112 | updatedAt: 2026-10-20T20:00:00.000Z |
| 113 | - id: 6 | 113 | - id: 6 |
| 114 | title: "Measure on a small model (llmman) over mm's own docs/ (EN+FR): found when the words are there, not called otherwise; time per call on docs/" | 114 | title: "Measure on a small model (llmman) over mm's own docs/ (EN+FR): found when the words are there, not called otherwise; time per call on docs/" |
| 115 | - state: open | 115 | + state: closed |
| 116 | priority: none | 116 | priority: none |
| 117 | author: | 117 | author: |
| 118 | name: k33g | 118 | name: k33g |
| @@ -121,11 +121,96 @@ tasks: | |||
| 121 | updatedAt: 2026-10-20T20:00:00.000Z | 121 | updatedAt: 2026-10-20T20:00:00.000Z |
| 122 | - id: 7 | 122 | - id: 7 |
| 123 | title: "Docs EN+FR (reference configuration and tools, how-to, explanation: why BM25 and no index), demo if useful, quality gate, .memory" | 123 | title: "Docs EN+FR (reference configuration and tools, how-to, explanation: why BM25 and no index), demo if useful, quality gate, .memory" |
| 124 | - state: open | 124 | + state: closed |
| 125 | priority: none | 125 | priority: none |
| 126 | author: | 126 | author: |
| 127 | name: k33g | 127 | name: k33g |
| 128 | email: ph.charriere@gmail.com | 128 | email: ph.charriere@gmail.com |
| 129 | createdAt: 2026-10-20T20:00:00.000Z | 129 | createdAt: 2026-10-20T20:00:00.000Z |
| 130 | updatedAt: 2026-10-20T20:00:00.000Z | 130 | updatedAt: 2026-10-20T20:00:00.000Z |
| 131 | -comments: [] | 131 | +comments: |
| 132 | + - id: 1 | ||
| 133 | + author: | ||
| 134 | + name: k33g | ||
| 135 | + email: ph.charriere@gmail.com | ||
| 136 | + createdAt: 2026-10-20T21:00:00.000Z | ||
| 137 | + body: | | ||
| 138 | + Decided by the user (2026-10-20) — the proposal is validated, with: | ||
| 139 | + - tool name: search_docs (plural); | ||
| 140 | + - config block `docSearch`, with TWO switches instead of `enabled`: | ||
| 141 | + `searchDocs: true` → the /docs <words> command (for the user), | ||
| 142 | + `autoSearchDocs: true` → the search_docs tool (for the model); | ||
| 143 | + both default false, independent (as /memory and search_memory); | ||
| 144 | + - /docs: yes; | ||
| 145 | + - prefix matching for words of 4+ letters: yes; | ||
| 146 | + - extensions: only what the configuration lists (no hidden extra); | ||
| 147 | + the default list is the validated one (.md .markdown .txt .adoc | ||
| 148 | + .asciidoc .rst); symlinks not followed; | ||
| 149 | + - key name: docSearch. | ||
| 150 | + Then: demos/docs-search, whose documents are a set of simple | ||
| 151 | + tutorials on using ratatui in Rust (Markdown, generated for the demo). | ||
| 152 | + Branch feature/search-in-doc (created by the user). | ||
| 153 | + - id: 2 | ||
| 154 | + author: | ||
| 155 | + name: k33g | ||
| 156 | + email: ph.charriere@gmail.com | ||
| 157 | + createdAt: 2026-10-20T23:00:00.000Z | ||
| 158 | + body: | | ||
| 159 | + Tasks 2-6 done on feature/search-in-doc (go test ./... green, gofmt/vet | ||
| 160 | + clean): | ||
| 161 | + - internal/docsearch (new): passages.go (sections at # / = headings, | ||
| 162 | + fences and listing blocks ignored, heading path, cut at blank lines | ||
| 163 | + past 1,500 runes, never inside a fence), bm25.go (k1 1.2, b 0.75, | ||
| 164 | + heading-path words +2, prefix match for 4+ runes, plural/-ed within | ||
| 165 | + 2 runes, query words < 3 runes dropped beside longer ones), walk.go | ||
| 166 | + (listed extensions only, maxFileSize, hidden dirs + node_modules | ||
| 167 | + skipped, symlinks not followed, NUL = binary, missing dirs | ||
| 168 | + reported), docsearch.go (Search, Result.Format), configured.go | ||
| 169 | + (Configured/Answer from config). | ||
| 170 | + - internal/textfold (new): Fold/Rune moved out of store (shared by | ||
| 171 | + store and docsearch, no duplicate). | ||
| 172 | + - config/docsearch.go: docSearch (searchDocs, autoSearchDocs, dirs | ||
| 173 | + [docs], extensions [.md .markdown .txt .adoc .asciidoc .rst], | ||
| 174 | + maxFileSize 1MB — ByteSize reads 1MB / 512KB / bytes); validated | ||
| 175 | + only when a switch is on; search_docs reserved. | ||
| 176 | + - command/docs.go SetDocs; /docs in the REPL, the window (notice) and | ||
| 177 | + ACP (agent text, input hint); tools/searchdocs.go search_docs | ||
| 178 | + (query, limit? — 5, cap 20), never asks, ACP kind search, dir = the | ||
| 179 | + sink's WorkDir else the process cwd; main.go wiring. | ||
| 180 | + - Speed (best of 10): mm's docs/ 137 files / 1,320 passages ≈ 45 ms; | ||
| 181 | + the demo's 10 files / 44 passages ≈ 1 ms. | ||
| 182 | + demos/docs-search: Rata, Qwen3.5 4B on llmman, 9 Ratatui 0.30 | ||
| 183 | + tutorials + index; check-tutorials.sh compiles every complete program | ||
| 184 | + + snippets.rs in a rust container (-Dwarnings): OK, 10 programs, | ||
| 185 | + ratatui 0.30.2. Measured (temp 0, 3 runs each, identical): List → | ||
| 186 | + ListState, 04-lists.md:5 3/3; popup → Clear + centered, NO link 3/3; | ||
| 187 | + Windows double key → right answer (KeyEventKind::Press) 3/3 but cited | ||
| 188 | + code examples instead of 08:60 (ranked first by the search); progress | ||
| 189 | + bar → Gauge + event::poll, 06:9 3/3; two French questions → English | ||
| 190 | + searches, French answers, right links 6/6; "Write src/main.rs …" → | ||
| 191 | + file written 3/3, compiles against ratatui 0.30.2 3/3; greeting and | ||
| 192 | + an off-topic question → no search 6/6. Prompt lesson: the Windows | ||
| 193 | + question was answered without a search (3/3) until the prompt said any | ||
| 194 | + terminal-app question is a Ratatui question — after which the popup | ||
| 195 | + answer stopped citing its link. | ||
| 196 | + Task 7: docs EN+FR done (reference configuration + validation errors, | ||
| 197 | + slash-commands, tools, acp; how-to search-the-docs; explanation | ||
| 198 | + document-search; indexes; root README; packages.drawio). Quality gate | ||
| 199 | + NOT run (qlty absent here) — task 7 stays open for it. | ||
| 200 | + - id: 3 | ||
| 201 | + author: | ||
| 202 | + name: k33g | ||
| 203 | + email: ph.charriere@gmail.com | ||
| 204 | + createdAt: 2026-10-20T23:30:00.000Z | ||
| 205 | + body: | | ||
| 206 | + Closed at the user's request. Delivered: internal/docsearch + | ||
| 207 | + internal/textfold, docSearch config (searchDocs / autoSearchDocs), | ||
| 208 | + /docs in the three front ends, the search_docs tool, tests, docs EN+FR | ||
| 209 | + (reference, how-to search-the-docs, explanation document-search), | ||
| 210 | + demos/docs-search (Ratatui tutorials, compile-checked, measured). | ||
| 211 | + Task 7 closed with the qlty quality gate still NOT run (qlty is not | ||
| 212 | + installed in this sandbox); gofmt and go vet clean, go test ./... | ||
| 213 | + green. Known limits left as documented: words, not meanings; in the | ||
| 214 | + demo the popup answer gives no link and the Windows answer cites code | ||
| 215 | + examples rather than the explaining passage. | ||
| 216 | + | ||
modified
README.md +2 -0 | @@ -121,6 +121,7 @@ Anything that speaks ACP can host `mm -acp` the same way: **JetBrains IDEs** (In | ||
| 121 | 121 | - **Plans**: the built-in `plan` tool lets the agent turn a list of tasks into a tracked plan and show its progress after each answer. See [Manage plans and tasks](docs/en/how-to/manage-plans-and-tasks.md). |
| 122 | 122 | - **Save an answer**: `/save` writes the last answer to a Markdown file; with `saveAnswerTool: true`, the model does it itself when you ask in your own words (`save_answer`). Never overwrites a file. See [Save an answer to a file](docs/en/how-to/save-an-answer.md). |
| 123 | 123 | - **Unload a model**: `/unload [model]` frees a model from the server's memory — in the terminal, the window and an editor —, and the `unload_model` tool lets the agent do it itself, e.g. on another agent's model once it has answered; llmman, Ollama and llama-server in router mode. `unloadModels: false` turns both off. See [Free a model's memory](docs/en/how-to/unload-a-model.md) and `demos/unload-models/`. |
| 124 | +- **Search a documentation folder**: `/docs <words>` searches the text files of the folders named in `docSearch.dirs` (`.md`, `.adoc`, `.txt`… — only the extensions listed) and returns the best passages with their `path:line` link, ranked with BM25 — no index, no model (`docSearch.searchDocs`); the `search_docs` tool lets the model do it before it answers (`docSearch.autoSearchDocs`); both off by default. See [Search a documentation folder](docs/en/how-to/search-the-docs.md) and `demos/docs-search/`. | |
| 124 | 125 | - **Search earlier sessions**: with sessions on, `/memory <words>` searches the project's stored conversations (`sessions.searchMemory`), and the `search_memory` tool lets the model do it when a question refers to something decided before (`sessions.autoSearchMemory`); both off by default. The search matches words, not meanings. See [Search earlier sessions](docs/en/how-to/search-earlier-sessions.md) and `demos/memory-search/`. |
| 125 | 126 | - **Docker Sandboxes**: the sandbox kit lives in its own repository, [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit); `sbx/` explains how to use it (`sbx run …`), with two mixins (`go-toolchain`, `cli-tools`) and ready-made configurations — see `sbx/README.md` and [Run mini-me in a Docker Sandbox](docs/en/how-to/run-in-a-docker-sandbox.md). |
| 126 | 127 | |
| @@ -155,6 +156,7 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h | ||
| 155 | 156 | | `demos/guard/` | one guard model (Granite Guardian 3.2 3B-A800M on llmman) answering `Yes`/`No` to "is this question harmful?"; why its `system:` line is a risk name, not a prompt; custom risks | |
| 156 | 157 | | `demos/guards/` | an assistant (Gemma 4 E4B) protected by seven guard agents, one per risk: a Node.js `preGeneration` hook asks them in parallel without the history, the assistant refuses or answers according to their verdicts; `displayHooks: true`; measured results | |
| 157 | 158 | | `demos/unload-models/` | a coordinator (Qwen3.5 9B) hands programming questions to an expert (Qwen3.5 4B) with `ask_agent`, a `preAsk` hook unloads the coordinator's own model before the expert starts and a `postAsk` hook unloads the expert's once it has answered — the two models are never loaded together; `/unload` and `unload_model` on request; llmman, measured results | |
| 159 | +| `demos/docs-search/` | an assistant answering from a documentation folder: nine Ratatui tutorials (Rust terminal UIs, each program checked against ratatui 0.30 by `check-tutorials.sh`), `/docs <words>` for you and the `search_docs` tool for the model; Qwen3.5 4B on llmman, measured results | | |
| 158 | 160 | | `demos/memory-search/` | an assistant that remembers a project's decisions: four earlier sessions (port, storage, logs in French, deployment as a `/compact` summary), `/memory <words>` for you and the `search_memory` tool for the model; Qwen3.5 4B on llmman, measured results — including what a word search cannot find | |
| 159 | 161 | | `demos/declared-tools/` | an ops desk with no `bash` and no skills, acting only through four tools declared under `tools:` — release, version bump, service logs, slow requests — covering every parameter type (string, enum, boolean, integer, number), a safe default (`dryRun: true`) and an optional argument; Mellum2 12B on DMR, measured results | |
| 160 | 162 | |
| @@ -121,6 +121,7 @@ Anything that speaks ACP can host `mm -acp` the same way: **JetBrains IDEs** (In | |||
| 121 | - **Plans**: the built-in `plan` tool lets the agent turn a list of tasks into a tracked plan and show its progress after each answer. See [Manage plans and tasks](docs/en/how-to/manage-plans-and-tasks.md). | 121 | - **Plans**: the built-in `plan` tool lets the agent turn a list of tasks into a tracked plan and show its progress after each answer. See [Manage plans and tasks](docs/en/how-to/manage-plans-and-tasks.md). |
| 122 | - **Save an answer**: `/save` writes the last answer to a Markdown file; with `saveAnswerTool: true`, the model does it itself when you ask in your own words (`save_answer`). Never overwrites a file. See [Save an answer to a file](docs/en/how-to/save-an-answer.md). | 122 | - **Save an answer**: `/save` writes the last answer to a Markdown file; with `saveAnswerTool: true`, the model does it itself when you ask in your own words (`save_answer`). Never overwrites a file. See [Save an answer to a file](docs/en/how-to/save-an-answer.md). |
| 123 | - **Unload a model**: `/unload [model]` frees a model from the server's memory — in the terminal, the window and an editor —, and the `unload_model` tool lets the agent do it itself, e.g. on another agent's model once it has answered; llmman, Ollama and llama-server in router mode. `unloadModels: false` turns both off. See [Free a model's memory](docs/en/how-to/unload-a-model.md) and `demos/unload-models/`. | 123 | - **Unload a model**: `/unload [model]` frees a model from the server's memory — in the terminal, the window and an editor —, and the `unload_model` tool lets the agent do it itself, e.g. on another agent's model once it has answered; llmman, Ollama and llama-server in router mode. `unloadModels: false` turns both off. See [Free a model's memory](docs/en/how-to/unload-a-model.md) and `demos/unload-models/`. |
| 124 | +- **Search a documentation folder**: `/docs <words>` searches the text files of the folders named in `docSearch.dirs` (`.md`, `.adoc`, `.txt`… — only the extensions listed) and returns the best passages with their `path:line` link, ranked with BM25 — no index, no model (`docSearch.searchDocs`); the `search_docs` tool lets the model do it before it answers (`docSearch.autoSearchDocs`); both off by default. See [Search a documentation folder](docs/en/how-to/search-the-docs.md) and `demos/docs-search/`. | ||
| 124 | - **Search earlier sessions**: with sessions on, `/memory <words>` searches the project's stored conversations (`sessions.searchMemory`), and the `search_memory` tool lets the model do it when a question refers to something decided before (`sessions.autoSearchMemory`); both off by default. The search matches words, not meanings. See [Search earlier sessions](docs/en/how-to/search-earlier-sessions.md) and `demos/memory-search/`. | 125 | - **Search earlier sessions**: with sessions on, `/memory <words>` searches the project's stored conversations (`sessions.searchMemory`), and the `search_memory` tool lets the model do it when a question refers to something decided before (`sessions.autoSearchMemory`); both off by default. The search matches words, not meanings. See [Search earlier sessions](docs/en/how-to/search-earlier-sessions.md) and `demos/memory-search/`. |
| 125 | - **Docker Sandboxes**: the sandbox kit lives in its own repository, [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit); `sbx/` explains how to use it (`sbx run …`), with two mixins (`go-toolchain`, `cli-tools`) and ready-made configurations — see `sbx/README.md` and [Run mini-me in a Docker Sandbox](docs/en/how-to/run-in-a-docker-sandbox.md). | 126 | - **Docker Sandboxes**: the sandbox kit lives in its own repository, [mini-me-kit](https://rickub.com/bots-garden/mini-me-kit); `sbx/` explains how to use it (`sbx run …`), with two mixins (`go-toolchain`, `cli-tools`) and ready-made configurations — see `sbx/README.md` and [Run mini-me in a Docker Sandbox](docs/en/how-to/run-in-a-docker-sandbox.md). |
| 126 | 127 | ||
| @@ -155,6 +156,7 @@ The `demos/` directory holds ready-made set-ups (most with a README explaining h | |||
| 155 | | `demos/guard/` | one guard model (Granite Guardian 3.2 3B-A800M on llmman) answering `Yes`/`No` to "is this question harmful?"; why its `system:` line is a risk name, not a prompt; custom risks | | 156 | | `demos/guard/` | one guard model (Granite Guardian 3.2 3B-A800M on llmman) answering `Yes`/`No` to "is this question harmful?"; why its `system:` line is a risk name, not a prompt; custom risks | |
| 156 | | `demos/guards/` | an assistant (Gemma 4 E4B) protected by seven guard agents, one per risk: a Node.js `preGeneration` hook asks them in parallel without the history, the assistant refuses or answers according to their verdicts; `displayHooks: true`; measured results | | 157 | | `demos/guards/` | an assistant (Gemma 4 E4B) protected by seven guard agents, one per risk: a Node.js `preGeneration` hook asks them in parallel without the history, the assistant refuses or answers according to their verdicts; `displayHooks: true`; measured results | |
| 157 | | `demos/unload-models/` | a coordinator (Qwen3.5 9B) hands programming questions to an expert (Qwen3.5 4B) with `ask_agent`, a `preAsk` hook unloads the coordinator's own model before the expert starts and a `postAsk` hook unloads the expert's once it has answered — the two models are never loaded together; `/unload` and `unload_model` on request; llmman, measured results | | 158 | | `demos/unload-models/` | a coordinator (Qwen3.5 9B) hands programming questions to an expert (Qwen3.5 4B) with `ask_agent`, a `preAsk` hook unloads the coordinator's own model before the expert starts and a `postAsk` hook unloads the expert's once it has answered — the two models are never loaded together; `/unload` and `unload_model` on request; llmman, measured results | |
| 159 | +| `demos/docs-search/` | an assistant answering from a documentation folder: nine Ratatui tutorials (Rust terminal UIs, each program checked against ratatui 0.30 by `check-tutorials.sh`), `/docs <words>` for you and the `search_docs` tool for the model; Qwen3.5 4B on llmman, measured results | | ||
| 158 | | `demos/memory-search/` | an assistant that remembers a project's decisions: four earlier sessions (port, storage, logs in French, deployment as a `/compact` summary), `/memory <words>` for you and the `search_memory` tool for the model; Qwen3.5 4B on llmman, measured results — including what a word search cannot find | | 160 | | `demos/memory-search/` | an assistant that remembers a project's decisions: four earlier sessions (port, storage, logs in French, deployment as a `/compact` summary), `/memory <words>` for you and the `search_memory` tool for the model; Qwen3.5 4B on llmman, measured results — including what a word search cannot find | |
| 159 | | `demos/declared-tools/` | an ops desk with no `bash` and no skills, acting only through four tools declared under `tools:` — release, version bump, service logs, slow requests — covering every parameter type (string, enum, boolean, integer, number), a safe default (`dryRun: true`) and an optional argument; Mellum2 12B on DMR, measured results | | 161 | | `demos/declared-tools/` | an ops desk with no `bash` and no skills, acting only through four tools declared under `tools:` — release, version bump, service logs, slow requests — covering every parameter type (string, enum, boolean, integer, number), a safe default (`dryRun: true`) and an optional argument; Mellum2 12B on DMR, measured results | |
| 160 | 162 | ||
added
demos/docs-search/README.en.md +75 -0 | new file mode 100644 | ||
| @@ -0,0 +1,75 @@ | ||
| 1 | +# mini-me demo: answering from a documentation folder | |
| 2 | + | |
| 3 | +*[Version française](README.fr.md)* | |
| 4 | + | |
| 5 | +Rata helps people build terminal user interfaces in Rust with [Ratatui](https://ratatui.rs). It knows nothing about Ratatui by itself that you should trust: before it answers, it searches `docs/` — nine short tutorials — with the `search_docs` tool, answers from the passages it gets back, and gives their `path:line` link. You can search the same folder yourself with `/docs <words>`. The search is BM25 over the Markdown files: no index, no model. The chat model is Qwen3.5 4B, served by [llmman](https://github.com/llmmanorg/llmman). | |
| 6 | + | |
| 7 | +## What is here | |
| 8 | + | |
| 9 | +| File | Role | | |
| 10 | +|---|---| | |
| 11 | +| `agent.yaml` | Rata: `docSearch` with `searchDocs: true` (the `/docs` command) and `autoSearchDocs: true` (the `search_docs` tool) over `docs/`, `.md` only; `read_file` to open a whole tutorial, `write_file` to write a program; no `bash`. | | |
| 12 | +| `docs/` | Nine Ratatui 0.30 tutorials — getting started, layout, blocks and paragraphs, lists, tables, gauges and ticks, tabs, app state and events, popups — and their index. Each holds a complete program. | | |
| 13 | +| `check-tutorials.sh` | Compiles every complete program of `docs/` (and `snippets.rs`, the expressions quoted in the prose) against the current ratatui 0.30, in a throwaway `rust` container, warnings as errors. Needs Docker only. | | |
| 14 | +| `snippets.rs` | The short expressions the tutorials quote, so that the text cannot drift from the API. | | |
| 15 | + | |
| 16 | +## Run the demo | |
| 17 | + | |
| 18 | +```bash | |
| 19 | +llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | |
| 20 | +cd demos/docs-search | |
| 21 | +mm -tui agent.yaml | |
| 22 | +``` | |
| 23 | + | |
| 24 | +Search by hand: | |
| 25 | + | |
| 26 | +```text | |
| 27 | +/docs liststate | |
| 28 | +``` | |
| 29 | + | |
| 30 | +```text | |
| 31 | +🔎 5 passage(s) for "liststate" — 10 file(s) in docs | |
| 32 | + | |
| 33 | +── docs/04-lists.md:5 · Lists and selection › Keep the selection with ListState | |
| 34 | +## Keep the selection with ListState | |
| 35 | + | |
| 36 | +`ListState` remembers which item is selected and how far the list is scrolled. … | |
| 37 | +``` | |
| 38 | + | |
| 39 | +Then ask Rata: | |
| 40 | + | |
| 41 | +```text | |
| 42 | +How do I keep the selected item of a List in ratatui? | |
| 43 | +Every key I press acts twice on Windows. Why? | |
| 44 | +Comment afficher un tableau avec une ligne sélectionnée ? | |
| 45 | +Write src/main.rs: a list of three fruits I can move through with the arrow keys, q to quit. | |
| 46 | +``` | |
| 47 | + | |
| 48 | +Each search shows as `📚 search_docs: "<words>"` followed by the passages. Check the tutorials still compile with `./check-tutorials.sh` (`OK: 10 programs compile against ratatui 0.30.2` on 2026-10-20). | |
| 49 | + | |
| 50 | +## Measured results (2026-10-20, `mm -p`, llmman) | |
| 51 | + | |
| 52 | +Qwen3.5 4B Q4_K_M, `temperature: 0`, thinking off, this `agent.yaml`, a fresh copy of the demo for every run. Three runs per question; the same search and the same answer each time. | |
| 53 | + | |
| 54 | +| Question | Searched | Answer | Link given | | | |
| 55 | +|---|---|---|---|---| | |
| 56 | +| How do I keep the selected item of a List in ratatui? | `List selected item index` | `ListState`, `render_stateful_widget`, `state.selected()` | `docs/04-lists.md:5` | 3/3 | | |
| 57 | +| How do I show a popup in the middle of the screen? | `popup center middle screen` | `Clear` first, then the dialog in a centered area | none | 3/3 | | |
| 58 | +| Every key I press acts twice on Windows. Why? | `Windows key repeat double press` | Press and Release events; check `KeyEventKind::Press` | `01-getting-started.md:15`, `03-blocks-and-paragraphs.md:11` (code using the check), not the passage that explains it, `08-app-state-and-events.md:60` — which the search ranked first | 3/3 | | |
| 59 | +| How can I animate a progress bar without waiting for a key? | `animate progress bar key event` | `Gauge` + `event::poll` with a timeout, a tick when no event | `docs/06-gauges-and-ticks.md:9` | 3/3 | | |
| 60 | +| Comment découper l'écran en en-tête, corps et pied de page ? | `header footer body layout` | `Layout::vertical` with `Length` / `Min`, in French | `docs/02-layout.md:16` | 3/3 | | |
| 61 | +| Comment afficher un tableau avec une ligne sélectionnée ? | `table selection line` | `Table` + `TableState` + `row_highlight_style`, in French | `docs/05-tables.md:1` | 3/3 | | |
| 62 | +| Write src/main.rs: a list of three fruits I can move through with the arrow keys, q to quit. | `list widget` | `src/main.rs` written — it **compiles** against ratatui 0.30.2 (checked in the `rust` container) | — | 3/3 | | |
| 63 | +| Bonjour, qui es-tu ? | — | one line, in French, no search | — | 3/3 | | |
| 64 | +| What is the capital of Japan? One word. | — | `Tokyo`, no search | — | 3/3 | | |
| 65 | + | |
| 66 | +14 to 38 s per question with a search, under 2 s without; two runs took 164 s and 316 s with the same answer as the others (cause not established — the model server is shared). One search over `docs/` takes about 1 ms. | |
| 67 | + | |
| 68 | +## Good to know | |
| 69 | + | |
| 70 | +- **The model translates, not the search.** The tutorials are in English and the search matches words: French words would find nothing. Told to search "with English words — widget and type names", the model turned both French questions into English searches every time, and answered in French. | |
| 71 | +- **What the prompt had to say.** Without "any question about a terminal app … is a Ratatui question, even when it does not name Ratatui", the Windows question got a generic answer without a search (3/3). With it, the other answers stayed right, but the popup answer stopped giving its link (it did give `02-layout.md:68` and `09-popups.md:9` before) — a small model's citations move with the prompt. | |
| 72 | +- **Links are `path:line`**: an editor opens them, and `read_file` reads the whole tutorial when a passage is not enough. | |
| 73 | +- **Each switch on its own.** `searchDocs` gives you `/docs`; `autoSearchDocs` gives the model `search_docs`. Both are off by default. | |
| 74 | + | |
| 75 | +See also: [How to search a documentation folder](../../docs/en/how-to/search-the-docs.md), [`search_docs`](../../docs/en/reference/tools.md#search_docs), [Why it works this way](../../docs/en/explanation/document-search.md). | |
| new file mode 100644 | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | +# mini-me demo: answering from a documentation folder | ||
| 2 | + | ||
| 3 | +*[Version française](README.fr.md)* | ||
| 4 | + | ||
| 5 | +Rata helps people build terminal user interfaces in Rust with [Ratatui](https://ratatui.rs). It knows nothing about Ratatui by itself that you should trust: before it answers, it searches `docs/` — nine short tutorials — with the `search_docs` tool, answers from the passages it gets back, and gives their `path:line` link. You can search the same folder yourself with `/docs <words>`. The search is BM25 over the Markdown files: no index, no model. The chat model is Qwen3.5 4B, served by [llmman](https://github.com/llmmanorg/llmman). | ||
| 6 | + | ||
| 7 | +## What is here | ||
| 8 | + | ||
| 9 | +| File | Role | | ||
| 10 | +|---|---| | ||
| 11 | +| `agent.yaml` | Rata: `docSearch` with `searchDocs: true` (the `/docs` command) and `autoSearchDocs: true` (the `search_docs` tool) over `docs/`, `.md` only; `read_file` to open a whole tutorial, `write_file` to write a program; no `bash`. | | ||
| 12 | +| `docs/` | Nine Ratatui 0.30 tutorials — getting started, layout, blocks and paragraphs, lists, tables, gauges and ticks, tabs, app state and events, popups — and their index. Each holds a complete program. | | ||
| 13 | +| `check-tutorials.sh` | Compiles every complete program of `docs/` (and `snippets.rs`, the expressions quoted in the prose) against the current ratatui 0.30, in a throwaway `rust` container, warnings as errors. Needs Docker only. | | ||
| 14 | +| `snippets.rs` | The short expressions the tutorials quote, so that the text cannot drift from the API. | | ||
| 15 | + | ||
| 16 | +## Run the demo | ||
| 17 | + | ||
| 18 | +```bash | ||
| 19 | +llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | ||
| 20 | +cd demos/docs-search | ||
| 21 | +mm -tui agent.yaml | ||
| 22 | +``` | ||
| 23 | + | ||
| 24 | +Search by hand: | ||
| 25 | + | ||
| 26 | +```text | ||
| 27 | +/docs liststate | ||
| 28 | +``` | ||
| 29 | + | ||
| 30 | +```text | ||
| 31 | +🔎 5 passage(s) for "liststate" — 10 file(s) in docs | ||
| 32 | + | ||
| 33 | +── docs/04-lists.md:5 · Lists and selection › Keep the selection with ListState | ||
| 34 | +## Keep the selection with ListState | ||
| 35 | + | ||
| 36 | +`ListState` remembers which item is selected and how far the list is scrolled. … | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +Then ask Rata: | ||
| 40 | + | ||
| 41 | +```text | ||
| 42 | +How do I keep the selected item of a List in ratatui? | ||
| 43 | +Every key I press acts twice on Windows. Why? | ||
| 44 | +Comment afficher un tableau avec une ligne sélectionnée ? | ||
| 45 | +Write src/main.rs: a list of three fruits I can move through with the arrow keys, q to quit. | ||
| 46 | +``` | ||
| 47 | + | ||
| 48 | +Each search shows as `📚 search_docs: "<words>"` followed by the passages. Check the tutorials still compile with `./check-tutorials.sh` (`OK: 10 programs compile against ratatui 0.30.2` on 2026-10-20). | ||
| 49 | + | ||
| 50 | +## Measured results (2026-10-20, `mm -p`, llmman) | ||
| 51 | + | ||
| 52 | +Qwen3.5 4B Q4_K_M, `temperature: 0`, thinking off, this `agent.yaml`, a fresh copy of the demo for every run. Three runs per question; the same search and the same answer each time. | ||
| 53 | + | ||
| 54 | +| Question | Searched | Answer | Link given | | | ||
| 55 | +|---|---|---|---|---| | ||
| 56 | +| How do I keep the selected item of a List in ratatui? | `List selected item index` | `ListState`, `render_stateful_widget`, `state.selected()` | `docs/04-lists.md:5` | 3/3 | | ||
| 57 | +| How do I show a popup in the middle of the screen? | `popup center middle screen` | `Clear` first, then the dialog in a centered area | none | 3/3 | | ||
| 58 | +| Every key I press acts twice on Windows. Why? | `Windows key repeat double press` | Press and Release events; check `KeyEventKind::Press` | `01-getting-started.md:15`, `03-blocks-and-paragraphs.md:11` (code using the check), not the passage that explains it, `08-app-state-and-events.md:60` — which the search ranked first | 3/3 | | ||
| 59 | +| How can I animate a progress bar without waiting for a key? | `animate progress bar key event` | `Gauge` + `event::poll` with a timeout, a tick when no event | `docs/06-gauges-and-ticks.md:9` | 3/3 | | ||
| 60 | +| Comment découper l'écran en en-tête, corps et pied de page ? | `header footer body layout` | `Layout::vertical` with `Length` / `Min`, in French | `docs/02-layout.md:16` | 3/3 | | ||
| 61 | +| Comment afficher un tableau avec une ligne sélectionnée ? | `table selection line` | `Table` + `TableState` + `row_highlight_style`, in French | `docs/05-tables.md:1` | 3/3 | | ||
| 62 | +| Write src/main.rs: a list of three fruits I can move through with the arrow keys, q to quit. | `list widget` | `src/main.rs` written — it **compiles** against ratatui 0.30.2 (checked in the `rust` container) | — | 3/3 | | ||
| 63 | +| Bonjour, qui es-tu ? | — | one line, in French, no search | — | 3/3 | | ||
| 64 | +| What is the capital of Japan? One word. | — | `Tokyo`, no search | — | 3/3 | | ||
| 65 | + | ||
| 66 | +14 to 38 s per question with a search, under 2 s without; two runs took 164 s and 316 s with the same answer as the others (cause not established — the model server is shared). One search over `docs/` takes about 1 ms. | ||
| 67 | + | ||
| 68 | +## Good to know | ||
| 69 | + | ||
| 70 | +- **The model translates, not the search.** The tutorials are in English and the search matches words: French words would find nothing. Told to search "with English words — widget and type names", the model turned both French questions into English searches every time, and answered in French. | ||
| 71 | +- **What the prompt had to say.** Without "any question about a terminal app … is a Ratatui question, even when it does not name Ratatui", the Windows question got a generic answer without a search (3/3). With it, the other answers stayed right, but the popup answer stopped giving its link (it did give `02-layout.md:68` and `09-popups.md:9` before) — a small model's citations move with the prompt. | ||
| 72 | +- **Links are `path:line`**: an editor opens them, and `read_file` reads the whole tutorial when a passage is not enough. | ||
| 73 | +- **Each switch on its own.** `searchDocs` gives you `/docs`; `autoSearchDocs` gives the model `search_docs`. Both are off by default. | ||
| 74 | + | ||
| 75 | +See also: [How to search a documentation folder](../../docs/en/how-to/search-the-docs.md), [`search_docs`](../../docs/en/reference/tools.md#search_docs), [Why it works this way](../../docs/en/explanation/document-search.md). | ||
added
demos/docs-search/README.fr.md +75 -0 | new file mode 100644 | ||
| @@ -0,0 +1,75 @@ | ||
| 1 | +# Démo mini-me : répondre à partir d'un dossier de documentation | |
| 2 | + | |
| 3 | +*[English version](README.en.md)* | |
| 4 | + | |
| 5 | +Rata aide à construire des interfaces en mode terminal en Rust avec [Ratatui](https://ratatui.rs). Il ne sait rien de Ratatui par lui-même qui mérite confiance : avant de répondre, il cherche dans `docs/` — neuf tutoriels courts — avec l'outil `search_docs`, répond à partir des passages qu'il reçoit et donne leur lien `chemin:ligne`. Vous pouvez chercher vous-même dans le même dossier avec `/docs <mots>`. La recherche est un BM25 sur les fichiers Markdown : ni index, ni modèle. Le modèle de chat est Qwen3.5 4B, servi par [llmman](https://github.com/llmmanorg/llmman). | |
| 6 | + | |
| 7 | +## Contenu | |
| 8 | + | |
| 9 | +| Fichier | Rôle | | |
| 10 | +|---|---| | |
| 11 | +| `agent.yaml` | Rata : `docSearch` avec `searchDocs: true` (la commande `/docs`) et `autoSearchDocs: true` (l'outil `search_docs`) sur `docs/`, `.md` seulement ; `read_file` pour ouvrir un tutoriel entier, `write_file` pour écrire un programme ; pas de `bash`. | | |
| 12 | +| `docs/` | Neuf tutoriels Ratatui 0.30 — premiers pas, layout, blocs et paragraphes, listes, tableaux, jauges et ticks, onglets, état de l'application et événements, popups — et leur index. Chacun contient un programme complet. | | |
| 13 | +| `check-tutorials.sh` | Compile chaque programme complet de `docs/` (et `snippets.rs`, les expressions citées dans le texte) avec la ratatui 0.30 actuelle, dans un conteneur `rust` jetable, avertissements traités comme des erreurs. Ne demande que Docker. | | |
| 14 | +| `snippets.rs` | Les petites expressions que citent les tutoriels, pour que le texte ne puisse pas s'écarter de l'API. | | |
| 15 | + | |
| 16 | +## Lancer la démo | |
| 17 | + | |
| 18 | +```bash | |
| 19 | +llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | |
| 20 | +cd demos/docs-search | |
| 21 | +mm -tui agent.yaml | |
| 22 | +``` | |
| 23 | + | |
| 24 | +Chercher à la main : | |
| 25 | + | |
| 26 | +```text | |
| 27 | +/docs liststate | |
| 28 | +``` | |
| 29 | + | |
| 30 | +```text | |
| 31 | +🔎 5 passage(s) for "liststate" — 10 file(s) in docs | |
| 32 | + | |
| 33 | +── docs/04-lists.md:5 · Lists and selection › Keep the selection with ListState | |
| 34 | +## Keep the selection with ListState | |
| 35 | + | |
| 36 | +`ListState` remembers which item is selected and how far the list is scrolled. … | |
| 37 | +``` | |
| 38 | + | |
| 39 | +Puis demander à Rata : | |
| 40 | + | |
| 41 | +```text | |
| 42 | +How do I keep the selected item of a List in ratatui? | |
| 43 | +Every key I press acts twice on Windows. Why? | |
| 44 | +Comment afficher un tableau avec une ligne sélectionnée ? | |
| 45 | +Write src/main.rs: a list of three fruits I can move through with the arrow keys, q to quit. | |
| 46 | +``` | |
| 47 | + | |
| 48 | +Chaque recherche s'affiche sous la forme `📚 search_docs: "<mots>"` suivie des passages. Vérifiez que les tutoriels compilent toujours avec `./check-tutorials.sh` (`OK: 10 programs compile against ratatui 0.30.2` le 2026-10-20). | |
| 49 | + | |
| 50 | +## Résultats mesurés (2026-10-20, `mm -p`, llmman) | |
| 51 | + | |
| 52 | +Qwen3.5 4B Q4_K_M, `temperature: 0`, réflexion désactivée, cet `agent.yaml`, une copie neuve de la démo à chaque exécution. Trois exécutions par question ; la même recherche et la même réponse à chaque fois. | |
| 53 | + | |
| 54 | +| Question | Recherché | Réponse | Lien donné | | | |
| 55 | +|---|---|---|---|---| | |
| 56 | +| How do I keep the selected item of a List in ratatui? | `List selected item index` | `ListState`, `render_stateful_widget`, `state.selected()` | `docs/04-lists.md:5` | 3/3 | | |
| 57 | +| How do I show a popup in the middle of the screen? | `popup center middle screen` | `Clear` d'abord, puis le dialogue dans une zone centrée | aucun | 3/3 | | |
| 58 | +| Every key I press acts twice on Windows. Why? | `Windows key repeat double press` | événements Press et Release ; tester `KeyEventKind::Press` | `01-getting-started.md:15`, `03-blocks-and-paragraphs.md:11` (du code qui fait ce test), pas le passage qui l'explique, `08-app-state-and-events.md:60` — que la recherche avait classé premier | 3/3 | | |
| 59 | +| How can I animate a progress bar without waiting for a key? | `animate progress bar key event` | `Gauge` + `event::poll` avec délai, un tick quand rien n'arrive | `docs/06-gauges-and-ticks.md:9` | 3/3 | | |
| 60 | +| Comment découper l'écran en en-tête, corps et pied de page ? | `header footer body layout` | `Layout::vertical` avec `Length` / `Min`, en français | `docs/02-layout.md:16` | 3/3 | | |
| 61 | +| Comment afficher un tableau avec une ligne sélectionnée ? | `table selection line` | `Table` + `TableState` + `row_highlight_style`, en français | `docs/05-tables.md:1` | 3/3 | | |
| 62 | +| Write src/main.rs: a list of three fruits I can move through with the arrow keys, q to quit. | `list widget` | `src/main.rs` écrit — il **compile** avec ratatui 0.30.2 (vérifié dans le conteneur `rust`) | — | 3/3 | | |
| 63 | +| Bonjour, qui es-tu ? | — | une ligne, en français, sans recherche | — | 3/3 | | |
| 64 | +| What is the capital of Japan? One word. | — | `Tokyo`, sans recherche | — | 3/3 | | |
| 65 | + | |
| 66 | +De 14 à 38 s par question avec recherche, moins de 2 s sans ; deux exécutions ont pris 164 s et 316 s avec la même réponse que les autres (cause non établie — le serveur de modèles est partagé). Une recherche dans `docs/` prend environ 1 ms. | |
| 67 | + | |
| 68 | +## Bon à savoir | |
| 69 | + | |
| 70 | +- **C'est le modèle qui traduit, pas la recherche.** Les tutoriels sont en anglais et la recherche porte sur les mots : des mots français ne trouveraient rien. Invité à chercher « avec des mots anglais — noms de widgets et de types », le modèle a transformé les deux questions en français en recherches en anglais à chaque fois, et a répondu en français. | |
| 71 | +- **Ce que le prompt a dû dire.** Sans « toute question sur une application terminal … est une question Ratatui, même quand elle ne nomme pas Ratatui », la question sur Windows recevait une réponse générique, sans recherche (3/3). Avec cette phrase, les autres réponses sont restées justes, mais la réponse sur la popup a cessé de donner son lien (elle donnait `02-layout.md:68` et `09-popups.md:9` avant) — les citations d'un petit modèle bougent avec le prompt. | |
| 72 | +- **Les liens sont des `chemin:ligne`** : un éditeur les ouvre, et `read_file` lit le tutoriel entier quand un passage ne suffit pas. | |
| 73 | +- **Chaque interrupteur séparément.** `searchDocs` vous donne `/docs` ; `autoSearchDocs` donne `search_docs` au modèle. Les deux sont désactivés par défaut. | |
| 74 | + | |
| 75 | +Voir aussi : [Chercher dans un dossier de documentation](../../docs/fr/how-to/search-the-docs.md), [`search_docs`](../../docs/fr/reference/tools.md#search_docs), [Pourquoi elle fonctionne ainsi](../../docs/fr/explanation/document-search.md). | |
| new file mode 100644 | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | +# Démo mini-me : répondre à partir d'un dossier de documentation | ||
| 2 | + | ||
| 3 | +*[English version](README.en.md)* | ||
| 4 | + | ||
| 5 | +Rata aide à construire des interfaces en mode terminal en Rust avec [Ratatui](https://ratatui.rs). Il ne sait rien de Ratatui par lui-même qui mérite confiance : avant de répondre, il cherche dans `docs/` — neuf tutoriels courts — avec l'outil `search_docs`, répond à partir des passages qu'il reçoit et donne leur lien `chemin:ligne`. Vous pouvez chercher vous-même dans le même dossier avec `/docs <mots>`. La recherche est un BM25 sur les fichiers Markdown : ni index, ni modèle. Le modèle de chat est Qwen3.5 4B, servi par [llmman](https://github.com/llmmanorg/llmman). | ||
| 6 | + | ||
| 7 | +## Contenu | ||
| 8 | + | ||
| 9 | +| Fichier | Rôle | | ||
| 10 | +|---|---| | ||
| 11 | +| `agent.yaml` | Rata : `docSearch` avec `searchDocs: true` (la commande `/docs`) et `autoSearchDocs: true` (l'outil `search_docs`) sur `docs/`, `.md` seulement ; `read_file` pour ouvrir un tutoriel entier, `write_file` pour écrire un programme ; pas de `bash`. | | ||
| 12 | +| `docs/` | Neuf tutoriels Ratatui 0.30 — premiers pas, layout, blocs et paragraphes, listes, tableaux, jauges et ticks, onglets, état de l'application et événements, popups — et leur index. Chacun contient un programme complet. | | ||
| 13 | +| `check-tutorials.sh` | Compile chaque programme complet de `docs/` (et `snippets.rs`, les expressions citées dans le texte) avec la ratatui 0.30 actuelle, dans un conteneur `rust` jetable, avertissements traités comme des erreurs. Ne demande que Docker. | | ||
| 14 | +| `snippets.rs` | Les petites expressions que citent les tutoriels, pour que le texte ne puisse pas s'écarter de l'API. | | ||
| 15 | + | ||
| 16 | +## Lancer la démo | ||
| 17 | + | ||
| 18 | +```bash | ||
| 19 | +llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | ||
| 20 | +cd demos/docs-search | ||
| 21 | +mm -tui agent.yaml | ||
| 22 | +``` | ||
| 23 | + | ||
| 24 | +Chercher à la main : | ||
| 25 | + | ||
| 26 | +```text | ||
| 27 | +/docs liststate | ||
| 28 | +``` | ||
| 29 | + | ||
| 30 | +```text | ||
| 31 | +🔎 5 passage(s) for "liststate" — 10 file(s) in docs | ||
| 32 | + | ||
| 33 | +── docs/04-lists.md:5 · Lists and selection › Keep the selection with ListState | ||
| 34 | +## Keep the selection with ListState | ||
| 35 | + | ||
| 36 | +`ListState` remembers which item is selected and how far the list is scrolled. … | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +Puis demander à Rata : | ||
| 40 | + | ||
| 41 | +```text | ||
| 42 | +How do I keep the selected item of a List in ratatui? | ||
| 43 | +Every key I press acts twice on Windows. Why? | ||
| 44 | +Comment afficher un tableau avec une ligne sélectionnée ? | ||
| 45 | +Write src/main.rs: a list of three fruits I can move through with the arrow keys, q to quit. | ||
| 46 | +``` | ||
| 47 | + | ||
| 48 | +Chaque recherche s'affiche sous la forme `📚 search_docs: "<mots>"` suivie des passages. Vérifiez que les tutoriels compilent toujours avec `./check-tutorials.sh` (`OK: 10 programs compile against ratatui 0.30.2` le 2026-10-20). | ||
| 49 | + | ||
| 50 | +## Résultats mesurés (2026-10-20, `mm -p`, llmman) | ||
| 51 | + | ||
| 52 | +Qwen3.5 4B Q4_K_M, `temperature: 0`, réflexion désactivée, cet `agent.yaml`, une copie neuve de la démo à chaque exécution. Trois exécutions par question ; la même recherche et la même réponse à chaque fois. | ||
| 53 | + | ||
| 54 | +| Question | Recherché | Réponse | Lien donné | | | ||
| 55 | +|---|---|---|---|---| | ||
| 56 | +| How do I keep the selected item of a List in ratatui? | `List selected item index` | `ListState`, `render_stateful_widget`, `state.selected()` | `docs/04-lists.md:5` | 3/3 | | ||
| 57 | +| How do I show a popup in the middle of the screen? | `popup center middle screen` | `Clear` d'abord, puis le dialogue dans une zone centrée | aucun | 3/3 | | ||
| 58 | +| Every key I press acts twice on Windows. Why? | `Windows key repeat double press` | événements Press et Release ; tester `KeyEventKind::Press` | `01-getting-started.md:15`, `03-blocks-and-paragraphs.md:11` (du code qui fait ce test), pas le passage qui l'explique, `08-app-state-and-events.md:60` — que la recherche avait classé premier | 3/3 | | ||
| 59 | +| How can I animate a progress bar without waiting for a key? | `animate progress bar key event` | `Gauge` + `event::poll` avec délai, un tick quand rien n'arrive | `docs/06-gauges-and-ticks.md:9` | 3/3 | | ||
| 60 | +| Comment découper l'écran en en-tête, corps et pied de page ? | `header footer body layout` | `Layout::vertical` avec `Length` / `Min`, en français | `docs/02-layout.md:16` | 3/3 | | ||
| 61 | +| Comment afficher un tableau avec une ligne sélectionnée ? | `table selection line` | `Table` + `TableState` + `row_highlight_style`, en français | `docs/05-tables.md:1` | 3/3 | | ||
| 62 | +| Write src/main.rs: a list of three fruits I can move through with the arrow keys, q to quit. | `list widget` | `src/main.rs` écrit — il **compile** avec ratatui 0.30.2 (vérifié dans le conteneur `rust`) | — | 3/3 | | ||
| 63 | +| Bonjour, qui es-tu ? | — | une ligne, en français, sans recherche | — | 3/3 | | ||
| 64 | +| What is the capital of Japan? One word. | — | `Tokyo`, sans recherche | — | 3/3 | | ||
| 65 | + | ||
| 66 | +De 14 à 38 s par question avec recherche, moins de 2 s sans ; deux exécutions ont pris 164 s et 316 s avec la même réponse que les autres (cause non établie — le serveur de modèles est partagé). Une recherche dans `docs/` prend environ 1 ms. | ||
| 67 | + | ||
| 68 | +## Bon à savoir | ||
| 69 | + | ||
| 70 | +- **C'est le modèle qui traduit, pas la recherche.** Les tutoriels sont en anglais et la recherche porte sur les mots : des mots français ne trouveraient rien. Invité à chercher « avec des mots anglais — noms de widgets et de types », le modèle a transformé les deux questions en français en recherches en anglais à chaque fois, et a répondu en français. | ||
| 71 | +- **Ce que le prompt a dû dire.** Sans « toute question sur une application terminal … est une question Ratatui, même quand elle ne nomme pas Ratatui », la question sur Windows recevait une réponse générique, sans recherche (3/3). Avec cette phrase, les autres réponses sont restées justes, mais la réponse sur la popup a cessé de donner son lien (elle donnait `02-layout.md:68` et `09-popups.md:9` avant) — les citations d'un petit modèle bougent avec le prompt. | ||
| 72 | +- **Les liens sont des `chemin:ligne`** : un éditeur les ouvre, et `read_file` lit le tutoriel entier quand un passage ne suffit pas. | ||
| 73 | +- **Chaque interrupteur séparément.** `searchDocs` vous donne `/docs` ; `autoSearchDocs` donne `search_docs` au modèle. Les deux sont désactivés par défaut. | ||
| 74 | + | ||
| 75 | +Voir aussi : [Chercher dans un dossier de documentation](../../docs/fr/how-to/search-the-docs.md), [`search_docs`](../../docs/fr/reference/tools.md#search_docs), [Pourquoi elle fonctionne ainsi](../../docs/fr/explanation/document-search.md). | ||
added
demos/docs-search/agent.yaml +79 -0 | new file mode 100644 | ||
| @@ -0,0 +1,79 @@ | ||
| 1 | +# Docs search — an assistant that answers from a documentation folder: | |
| 2 | +# | |
| 3 | +# /docs <words> the user's search through docs/ (docSearch.searchDocs) | |
| 4 | +# search_docs the same search, called by the model before it answers | |
| 5 | +# (docSearch.autoSearchDocs) | |
| 6 | +# | |
| 7 | +# docs/ holds nine short tutorials on Ratatui (Rust terminal UIs), each a | |
| 8 | +# complete program checked by ./check-tutorials.sh against ratatui 0.30. | |
| 9 | +# | |
| 10 | +# llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | |
| 11 | +# llmman serve | |
| 12 | +# | |
| 13 | +# cd demos/docs-search | |
| 14 | +# mm -tui agent.yaml or mm -p "…" agent.yaml | |
| 15 | + | |
| 16 | +provider: llmman | |
| 17 | +model: hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | |
| 18 | +baseUrl: http://127.0.0.1:17434/v1 | |
| 19 | +fallback: http://host.docker.internal:17434/v1 | |
| 20 | +contextWindow: 16384 | |
| 21 | + | |
| 22 | +docSearch: | |
| 23 | + # /docs <words>, for you. | |
| 24 | + searchDocs: true | |
| 25 | + # search_docs, for the model. | |
| 26 | + autoSearchDocs: true | |
| 27 | + dirs: [docs] | |
| 28 | + extensions: [.md] | |
| 29 | + maxFileSize: 1MB | |
| 30 | + | |
| 31 | +# read_file to open a whole tutorial from a link, write_file to write a | |
| 32 | +# program the user asks for; no bash. | |
| 33 | +yolo: true | |
| 34 | +bashTool: false | |
| 35 | +editTools: true | |
| 36 | +planTool: false | |
| 37 | +askUserTool: false | |
| 38 | +skillsDir: no-skills | |
| 39 | + | |
| 40 | +agentsMd: | |
| 41 | + enabled: false | |
| 42 | + | |
| 43 | +sampling: | |
| 44 | + temperature: 0.0 | |
| 45 | + parallel_tool_calls: false | |
| 46 | + max_tokens: 3072 | |
| 47 | + chat_template_kwargs: | |
| 48 | + enable_thinking: false | |
| 49 | + | |
| 50 | +maxTurns: 8 | |
| 51 | +watchdogTimeout: 2m | |
| 52 | +firstTokenTimeout: 3m | |
| 53 | +displayCommands: true | |
| 54 | + | |
| 55 | +acp: | |
| 56 | + name: rata | |
| 57 | + title: 🐀 Rata (Ratatui docs) | |
| 58 | + | |
| 59 | +system: | | |
| 60 | + Your name is Rata. You help people build terminal user interfaces in Rust | |
| 61 | + with Ratatui. The folder docs/ holds Ratatui tutorials, written in English. | |
| 62 | + | |
| 63 | + Any question about a terminal app — widgets, layout, keys, events, | |
| 64 | + colors, the terminal itself — is a Ratatui question, even when it does | |
| 65 | + not name Ratatui. For each one, call search_docs first, with English | |
| 66 | + words — widget and type names work best (List, ListState, Layout, Gauge, | |
| 67 | + Tabs, Clear, event::poll…). Answer from the passages it returns, and end | |
| 68 | + with the link of the passage you used (path:line). If a passage is not | |
| 69 | + enough, read_file the tutorial it comes from. Never invent an API: if | |
| 70 | + search_docs finds nothing, try other English words once, then say the | |
| 71 | + tutorials do not cover it. | |
| 72 | + | |
| 73 | + When the user asks you to write or create a file, call write_file with | |
| 74 | + the whole content; do not only show the code. | |
| 75 | + | |
| 76 | + For anything else (a greeting, a question that is not about Ratatui), | |
| 77 | + answer directly, without search_docs. | |
| 78 | + | |
| 79 | + Always answer in the language of the user's question. | |
| new file mode 100644 | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | +# Docs search — an assistant that answers from a documentation folder: | ||
| 2 | +# | ||
| 3 | +# /docs <words> the user's search through docs/ (docSearch.searchDocs) | ||
| 4 | +# search_docs the same search, called by the model before it answers | ||
| 5 | +# (docSearch.autoSearchDocs) | ||
| 6 | +# | ||
| 7 | +# docs/ holds nine short tutorials on Ratatui (Rust terminal UIs), each a | ||
| 8 | +# complete program checked by ./check-tutorials.sh against ratatui 0.30. | ||
| 9 | +# | ||
| 10 | +# llmman pull hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | ||
| 11 | +# llmman serve | ||
| 12 | +# | ||
| 13 | +# cd demos/docs-search | ||
| 14 | +# mm -tui agent.yaml or mm -p "…" agent.yaml | ||
| 15 | + | ||
| 16 | +provider: llmman | ||
| 17 | +model: hf.co/unsloth/Qwen3.5-4B-GGUF:Q4_K_M | ||
| 18 | +baseUrl: http://127.0.0.1:17434/v1 | ||
| 19 | +fallback: http://host.docker.internal:17434/v1 | ||
| 20 | +contextWindow: 16384 | ||
| 21 | + | ||
| 22 | +docSearch: | ||
| 23 | + # /docs <words>, for you. | ||
| 24 | + searchDocs: true | ||
| 25 | + # search_docs, for the model. | ||
| 26 | + autoSearchDocs: true | ||
| 27 | + dirs: [docs] | ||
| 28 | + extensions: [.md] | ||
| 29 | + maxFileSize: 1MB | ||
| 30 | + | ||
| 31 | +# read_file to open a whole tutorial from a link, write_file to write a | ||
| 32 | +# program the user asks for; no bash. | ||
| 33 | +yolo: true | ||
| 34 | +bashTool: false | ||
| 35 | +editTools: true | ||
| 36 | +planTool: false | ||
| 37 | +askUserTool: false | ||
| 38 | +skillsDir: no-skills | ||
| 39 | + | ||
| 40 | +agentsMd: | ||
| 41 | + enabled: false | ||
| 42 | + | ||
| 43 | +sampling: | ||
| 44 | + temperature: 0.0 | ||
| 45 | + parallel_tool_calls: false | ||
| 46 | + max_tokens: 3072 | ||
| 47 | + chat_template_kwargs: | ||
| 48 | + enable_thinking: false | ||
| 49 | + | ||
| 50 | +maxTurns: 8 | ||
| 51 | +watchdogTimeout: 2m | ||
| 52 | +firstTokenTimeout: 3m | ||
| 53 | +displayCommands: true | ||
| 54 | + | ||
| 55 | +acp: | ||
| 56 | + name: rata | ||
| 57 | + title: 🐀 Rata (Ratatui docs) | ||
| 58 | + | ||
| 59 | +system: | | ||
| 60 | + Your name is Rata. You help people build terminal user interfaces in Rust | ||
| 61 | + with Ratatui. The folder docs/ holds Ratatui tutorials, written in English. | ||
| 62 | + | ||
| 63 | + Any question about a terminal app — widgets, layout, keys, events, | ||
| 64 | + colors, the terminal itself — is a Ratatui question, even when it does | ||
| 65 | + not name Ratatui. For each one, call search_docs first, with English | ||
| 66 | + words — widget and type names work best (List, ListState, Layout, Gauge, | ||
| 67 | + Tabs, Clear, event::poll…). Answer from the passages it returns, and end | ||
| 68 | + with the link of the passage you used (path:line). If a passage is not | ||
| 69 | + enough, read_file the tutorial it comes from. Never invent an API: if | ||
| 70 | + search_docs finds nothing, try other English words once, then say the | ||
| 71 | + tutorials do not cover it. | ||
| 72 | + | ||
| 73 | + When the user asks you to write or create a file, call write_file with | ||
| 74 | + the whole content; do not only show the code. | ||
| 75 | + | ||
| 76 | + For anything else (a greeting, a question that is not about Ratatui), | ||
| 77 | + answer directly, without search_docs. | ||
| 78 | + | ||
| 79 | + Always answer in the language of the user's question. | ||
added
demos/docs-search/check-tutorials.sh +37 -0 | new file mode 100755 | ||
| @@ -0,0 +1,37 @@ | ||
| 1 | +#!/bin/sh | |
| 2 | +# check-tutorials.sh — compiles every complete program of docs/*.md (the | |
| 3 | +# ```rust blocks that hold a `fn main`) against the current ratatui 0.30, in | |
| 4 | +# a throwaway Rust container: nothing to install but Docker. Also compiles | |
| 5 | +# snippets.rs, the short expressions the tutorials quote in their prose. | |
| 6 | +# | |
| 7 | +# ./check-tutorials.sh → "OK: N programs compile" or cargo's errors | |
| 8 | +set -eu | |
| 9 | +cd "$(dirname "$0")" | |
| 10 | +work=$(mktemp -d) | |
| 11 | +trap 'rm -rf "$work"' EXIT | |
| 12 | +mkdir -p "$work/src/bin" | |
| 13 | +cat > "$work/Cargo.toml" <<'TOML' | |
| 14 | +[package] | |
| 15 | +name = "tutorials" | |
| 16 | +version = "0.1.0" | |
| 17 | +edition = "2021" | |
| 18 | + | |
| 19 | +[dependencies] | |
| 20 | +ratatui = "0.30" | |
| 21 | +TOML | |
| 22 | +n=0 | |
| 23 | +for doc in docs/*.md; do | |
| 24 | + # one file per ```rust block holding fn main (```rust,ignore blocks are skipped) | |
| 25 | + awk -v out="$work/src/bin/$(basename "$doc" .md)" ' | |
| 26 | + /^```rust$/ { inblock = 1; buf = ""; next } | |
| 27 | + /^```/ && inblock { inblock = 0; if (buf ~ /fn main/) { k++; f = out "-" k ".rs"; printf "%s", buf > f; close(f) } next } | |
| 28 | + inblock { buf = buf $0 "\n" } | |
| 29 | + ' "$doc" | |
| 30 | +done | |
| 31 | +cp snippets.rs "$work/src/bin/snippets.rs" | |
| 32 | +n=$(ls "$work/src/bin" | wc -l | tr -d ' ') | |
| 33 | +# As the calling user (files in $work stay removable), with a cargo home | |
| 34 | +# inside $work since the image's own one belongs to root. | |
| 35 | +docker run --rm --user "$(id -u):$(id -g)" -e CARGO_HOME=/work/.cargo -e RUSTFLAGS=-Dwarnings \ | |
| 36 | + -v "$work":/work -w /work rust:1-slim cargo build --quiet --bins | |
| 37 | +echo "OK: $n programs compile against ratatui $(grep -A1 'name = "ratatui"' "$work/Cargo.lock" | sed -n 's/version = "\(.*\)"/\1/p')" | |
| new file mode 100755 | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | +#!/bin/sh | ||
| 2 | +# check-tutorials.sh — compiles every complete program of docs/*.md (the | ||
| 3 | +# ```rust blocks that hold a `fn main`) against the current ratatui 0.30, in | ||
| 4 | +# a throwaway Rust container: nothing to install but Docker. Also compiles | ||
| 5 | +# snippets.rs, the short expressions the tutorials quote in their prose. | ||
| 6 | +# | ||
| 7 | +# ./check-tutorials.sh → "OK: N programs compile" or cargo's errors | ||
| 8 | +set -eu | ||
| 9 | +cd "$(dirname "$0")" | ||
| 10 | +work=$(mktemp -d) | ||
| 11 | +trap 'rm -rf "$work"' EXIT | ||
| 12 | +mkdir -p "$work/src/bin" | ||
| 13 | +cat > "$work/Cargo.toml" <<'TOML' | ||
| 14 | +[package] | ||
| 15 | +name = "tutorials" | ||
| 16 | +version = "0.1.0" | ||
| 17 | +edition = "2021" | ||
| 18 | + | ||
| 19 | +[dependencies] | ||
| 20 | +ratatui = "0.30" | ||
| 21 | +TOML | ||
| 22 | +n=0 | ||
| 23 | +for doc in docs/*.md; do | ||
| 24 | + # one file per ```rust block holding fn main (```rust,ignore blocks are skipped) | ||
| 25 | + awk -v out="$work/src/bin/$(basename "$doc" .md)" ' | ||
| 26 | + /^```rust$/ { inblock = 1; buf = ""; next } | ||
| 27 | + /^```/ && inblock { inblock = 0; if (buf ~ /fn main/) { k++; f = out "-" k ".rs"; printf "%s", buf > f; close(f) } next } | ||
| 28 | + inblock { buf = buf $0 "\n" } | ||
| 29 | + ' "$doc" | ||
| 30 | +done | ||
| 31 | +cp snippets.rs "$work/src/bin/snippets.rs" | ||
| 32 | +n=$(ls "$work/src/bin" | wc -l | tr -d ' ') | ||
| 33 | +# As the calling user (files in $work stay removable), with a cargo home | ||
| 34 | +# inside $work since the image's own one belongs to root. | ||
| 35 | +docker run --rm --user "$(id -u):$(id -g)" -e CARGO_HOME=/work/.cargo -e RUSTFLAGS=-Dwarnings \ | ||
| 36 | + -v "$work":/work -w /work rust:1-slim cargo build --quiet --bins | ||
| 37 | +echo "OK: $n programs compile against ratatui $(grep -A1 'name = "ratatui"' "$work/Cargo.lock" | sed -n 's/version = "\(.*\)"/\1/p')" | ||
added
demos/docs-search/docs/01-getting-started.md +58 -0 | new file mode 100644 | ||
| @@ -0,0 +1,58 @@ | ||
| 1 | +# Getting started with Ratatui | |
| 2 | + | |
| 3 | +Ratatui is a Rust library for building terminal user interfaces (TUIs). It draws widgets — blocks, paragraphs, lists, tables, gauges — into a buffer, and a backend (Crossterm by default) writes that buffer to the terminal. Ratatui does not read the keyboard itself: events come from the backend, here Crossterm, which Ratatui re-exports as `ratatui::crossterm`. | |
| 4 | + | |
| 5 | +## Create the project | |
| 6 | + | |
| 7 | +```sh | |
| 8 | +cargo new hello-tui | |
| 9 | +cd hello-tui | |
| 10 | +cargo add ratatui | |
| 11 | +``` | |
| 12 | + | |
| 13 | +`cargo add ratatui` adds the current version (0.30 at the time of writing). The Crossterm backend is enabled by default, so there is nothing else to add: use `ratatui::crossterm` rather than a separate `crossterm` dependency, and the two versions can never disagree. | |
| 14 | + | |
| 15 | +## The smallest app | |
| 16 | + | |
| 17 | +Replace `src/main.rs` with: | |
| 18 | + | |
| 19 | +```rust | |
| 20 | +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; | |
| 21 | +use ratatui::DefaultTerminal; | |
| 22 | + | |
| 23 | +fn main() -> std::io::Result<()> { | |
| 24 | + let mut terminal = ratatui::init(); | |
| 25 | + let result = run(&mut terminal); | |
| 26 | + ratatui::restore(); | |
| 27 | + result | |
| 28 | +} | |
| 29 | + | |
| 30 | +fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> { | |
| 31 | + loop { | |
| 32 | + terminal.draw(|frame| { | |
| 33 | + frame.render_widget("Hello, Ratatui! Press q to quit.", frame.area()); | |
| 34 | + })?; | |
| 35 | + if let Event::Key(key) = event::read()? { | |
| 36 | + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { | |
| 37 | + return Ok(()); | |
| 38 | + } | |
| 39 | + } | |
| 40 | + } | |
| 41 | +} | |
| 42 | +``` | |
| 43 | + | |
| 44 | +Run it with `cargo run`. The text fills the top-left of the terminal; `q` quits. | |
| 45 | + | |
| 46 | +## What init and restore do | |
| 47 | + | |
| 48 | +`ratatui::init()` switches the terminal to raw mode (keys arrive one by one, unechoed) and to the alternate screen (your shell's content comes back when the app ends), and installs a panic hook that restores the terminal if the app panics. `ratatui::restore()` undoes both. Always call `restore` before `main` returns, even on error — that is why the loop lives in its own `run` function whose result is returned after `restore`. | |
| 49 | + | |
| 50 | +## The draw loop | |
| 51 | + | |
| 52 | +`terminal.draw` takes a closure that receives a `Frame`. `frame.area()` is the whole terminal as a `Rect`; `frame.render_widget(widget, area)` draws a widget into a part of it. Ratatui redraws the whole UI every time: it keeps the previous buffer and only sends the cells that changed, so drawing everything each loop is cheap. A `&str` is itself a widget, which is why the smallest app can render a plain string. | |
| 53 | + | |
| 54 | +## Next steps | |
| 55 | + | |
| 56 | +- Split the screen into areas: see the layout tutorial. | |
| 57 | +- Draw borders, titles and wrapped text: see the blocks and paragraphs tutorial. | |
| 58 | +- Keep state and react to keys: see the app state and events tutorial. | |
| new file mode 100644 | |||
| @@ -0,0 +1,58 @@ | |||
| 1 | +# Getting started with Ratatui | ||
| 2 | + | ||
| 3 | +Ratatui is a Rust library for building terminal user interfaces (TUIs). It draws widgets — blocks, paragraphs, lists, tables, gauges — into a buffer, and a backend (Crossterm by default) writes that buffer to the terminal. Ratatui does not read the keyboard itself: events come from the backend, here Crossterm, which Ratatui re-exports as `ratatui::crossterm`. | ||
| 4 | + | ||
| 5 | +## Create the project | ||
| 6 | + | ||
| 7 | +```sh | ||
| 8 | +cargo new hello-tui | ||
| 9 | +cd hello-tui | ||
| 10 | +cargo add ratatui | ||
| 11 | +``` | ||
| 12 | + | ||
| 13 | +`cargo add ratatui` adds the current version (0.30 at the time of writing). The Crossterm backend is enabled by default, so there is nothing else to add: use `ratatui::crossterm` rather than a separate `crossterm` dependency, and the two versions can never disagree. | ||
| 14 | + | ||
| 15 | +## The smallest app | ||
| 16 | + | ||
| 17 | +Replace `src/main.rs` with: | ||
| 18 | + | ||
| 19 | +```rust | ||
| 20 | +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; | ||
| 21 | +use ratatui::DefaultTerminal; | ||
| 22 | + | ||
| 23 | +fn main() -> std::io::Result<()> { | ||
| 24 | + let mut terminal = ratatui::init(); | ||
| 25 | + let result = run(&mut terminal); | ||
| 26 | + ratatui::restore(); | ||
| 27 | + result | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> { | ||
| 31 | + loop { | ||
| 32 | + terminal.draw(|frame| { | ||
| 33 | + frame.render_widget("Hello, Ratatui! Press q to quit.", frame.area()); | ||
| 34 | + })?; | ||
| 35 | + if let Event::Key(key) = event::read()? { | ||
| 36 | + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { | ||
| 37 | + return Ok(()); | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | +} | ||
| 42 | +``` | ||
| 43 | + | ||
| 44 | +Run it with `cargo run`. The text fills the top-left of the terminal; `q` quits. | ||
| 45 | + | ||
| 46 | +## What init and restore do | ||
| 47 | + | ||
| 48 | +`ratatui::init()` switches the terminal to raw mode (keys arrive one by one, unechoed) and to the alternate screen (your shell's content comes back when the app ends), and installs a panic hook that restores the terminal if the app panics. `ratatui::restore()` undoes both. Always call `restore` before `main` returns, even on error — that is why the loop lives in its own `run` function whose result is returned after `restore`. | ||
| 49 | + | ||
| 50 | +## The draw loop | ||
| 51 | + | ||
| 52 | +`terminal.draw` takes a closure that receives a `Frame`. `frame.area()` is the whole terminal as a `Rect`; `frame.render_widget(widget, area)` draws a widget into a part of it. Ratatui redraws the whole UI every time: it keeps the previous buffer and only sends the cells that changed, so drawing everything each loop is cheap. A `&str` is itself a widget, which is why the smallest app can render a plain string. | ||
| 53 | + | ||
| 54 | +## Next steps | ||
| 55 | + | ||
| 56 | +- Split the screen into areas: see the layout tutorial. | ||
| 57 | +- Draw borders, titles and wrapped text: see the blocks and paragraphs tutorial. | ||
| 58 | +- Keep state and react to keys: see the app state and events tutorial. | ||
added
demos/docs-search/docs/02-layout.md +82 -0 | new file mode 100644 | ||
| @@ -0,0 +1,82 @@ | ||
| 1 | +# Splitting the screen with Layout | |
| 2 | + | |
| 3 | +A `Layout` cuts a `Rect` into smaller ones, vertically or horizontally, following a list of constraints. Layouts nest: split the screen into rows, then split a row into columns. | |
| 4 | + | |
| 5 | +## Constraints | |
| 6 | + | |
| 7 | +| Constraint | Meaning | | |
| 8 | +|---|---| | |
| 9 | +| `Constraint::Length(n)` | exactly `n` rows (or columns) | | |
| 10 | +| `Constraint::Min(n)` | at least `n`, takes what is left | | |
| 11 | +| `Constraint::Max(n)` | at most `n` | | |
| 12 | +| `Constraint::Percentage(p)` | `p` % of the space | | |
| 13 | +| `Constraint::Ratio(a, b)` | `a / b` of the space | | |
| 14 | +| `Constraint::Fill(w)` | shares the remaining space with the other `Fill`s, by weight `w` | | |
| 15 | + | |
| 16 | +## Header, body, footer | |
| 17 | + | |
| 18 | +`Layout::vertical` stacks areas from top to bottom; `areas` returns them as an array you can destructure, so the number of areas is checked at compile time. | |
| 19 | + | |
| 20 | +```rust | |
| 21 | +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; | |
| 22 | +use ratatui::layout::{Constraint, Layout}; | |
| 23 | +use ratatui::widgets::{Block, Paragraph}; | |
| 24 | +use ratatui::{DefaultTerminal, Frame}; | |
| 25 | + | |
| 26 | +fn main() -> std::io::Result<()> { | |
| 27 | + let mut terminal = ratatui::init(); | |
| 28 | + let result = run(&mut terminal); | |
| 29 | + ratatui::restore(); | |
| 30 | + result | |
| 31 | +} | |
| 32 | + | |
| 33 | +fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> { | |
| 34 | + loop { | |
| 35 | + terminal.draw(draw)?; | |
| 36 | + if let Event::Key(key) = event::read()? { | |
| 37 | + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { | |
| 38 | + return Ok(()); | |
| 39 | + } | |
| 40 | + } | |
| 41 | + } | |
| 42 | +} | |
| 43 | + | |
| 44 | +fn draw(frame: &mut Frame) { | |
| 45 | + let [header, body, footer] = Layout::vertical([ | |
| 46 | + Constraint::Length(3), | |
| 47 | + Constraint::Min(0), | |
| 48 | + Constraint::Length(1), | |
| 49 | + ]) | |
| 50 | + .areas(frame.area()); | |
| 51 | + | |
| 52 | + let [left, right] = | |
| 53 | + Layout::horizontal([Constraint::Percentage(30), Constraint::Fill(1)]).areas(body); | |
| 54 | + | |
| 55 | + frame.render_widget(Paragraph::new("My app").block(Block::bordered()), header); | |
| 56 | + frame.render_widget(Block::bordered().title("Menu"), left); | |
| 57 | + frame.render_widget(Block::bordered().title("Content"), right); | |
| 58 | + frame.render_widget("q quit", footer); | |
| 59 | +} | |
| 60 | +``` | |
| 61 | + | |
| 62 | +The header is always 3 rows high, the footer 1, and the body takes the rest. The body is then split into a menu (30 % of the width) and a content panel that fills the remaining space. | |
| 63 | + | |
| 64 | +## Spacing and margins | |
| 65 | + | |
| 66 | +`Layout::vertical([...]).spacing(1)` leaves one empty row between areas. `.margin(1)` keeps one cell free all around the area being split. `Block::bordered()` already takes one cell on each side for its border; draw the content in `block.inner(area)` to stay inside it. | |
| 67 | + | |
| 68 | +## Centering an area | |
| 69 | + | |
| 70 | +To center something, give the layout a single constraint and `Flex::Center`: | |
| 71 | + | |
| 72 | +```rust,ignore | |
| 73 | +use ratatui::layout::{Constraint, Flex, Layout, Rect}; | |
| 74 | + | |
| 75 | +fn centered(area: Rect, width: u16, height: u16) -> Rect { | |
| 76 | + let [area] = Layout::horizontal([Constraint::Length(width)]).flex(Flex::Center).areas(area); | |
| 77 | + let [area] = Layout::vertical([Constraint::Length(height)]).flex(Flex::Center).areas(area); | |
| 78 | + area | |
| 79 | +} | |
| 80 | +``` | |
| 81 | + | |
| 82 | +The popups tutorial uses this to draw a dialog in the middle of the screen. | |
| new file mode 100644 | |||
| @@ -0,0 +1,82 @@ | |||
| 1 | +# Splitting the screen with Layout | ||
| 2 | + | ||
| 3 | +A `Layout` cuts a `Rect` into smaller ones, vertically or horizontally, following a list of constraints. Layouts nest: split the screen into rows, then split a row into columns. | ||
| 4 | + | ||
| 5 | +## Constraints | ||
| 6 | + | ||
| 7 | +| Constraint | Meaning | | ||
| 8 | +|---|---| | ||
| 9 | +| `Constraint::Length(n)` | exactly `n` rows (or columns) | | ||
| 10 | +| `Constraint::Min(n)` | at least `n`, takes what is left | | ||
| 11 | +| `Constraint::Max(n)` | at most `n` | | ||
| 12 | +| `Constraint::Percentage(p)` | `p` % of the space | | ||
| 13 | +| `Constraint::Ratio(a, b)` | `a / b` of the space | | ||
| 14 | +| `Constraint::Fill(w)` | shares the remaining space with the other `Fill`s, by weight `w` | | ||
| 15 | + | ||
| 16 | +## Header, body, footer | ||
| 17 | + | ||
| 18 | +`Layout::vertical` stacks areas from top to bottom; `areas` returns them as an array you can destructure, so the number of areas is checked at compile time. | ||
| 19 | + | ||
| 20 | +```rust | ||
| 21 | +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; | ||
| 22 | +use ratatui::layout::{Constraint, Layout}; | ||
| 23 | +use ratatui::widgets::{Block, Paragraph}; | ||
| 24 | +use ratatui::{DefaultTerminal, Frame}; | ||
| 25 | + | ||
| 26 | +fn main() -> std::io::Result<()> { | ||
| 27 | + let mut terminal = ratatui::init(); | ||
| 28 | + let result = run(&mut terminal); | ||
| 29 | + ratatui::restore(); | ||
| 30 | + result | ||
| 31 | +} | ||
| 32 | + | ||
| 33 | +fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> { | ||
| 34 | + loop { | ||
| 35 | + terminal.draw(draw)?; | ||
| 36 | + if let Event::Key(key) = event::read()? { | ||
| 37 | + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { | ||
| 38 | + return Ok(()); | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + } | ||
| 42 | +} | ||
| 43 | + | ||
| 44 | +fn draw(frame: &mut Frame) { | ||
| 45 | + let [header, body, footer] = Layout::vertical([ | ||
| 46 | + Constraint::Length(3), | ||
| 47 | + Constraint::Min(0), | ||
| 48 | + Constraint::Length(1), | ||
| 49 | + ]) | ||
| 50 | + .areas(frame.area()); | ||
| 51 | + | ||
| 52 | + let [left, right] = | ||
| 53 | + Layout::horizontal([Constraint::Percentage(30), Constraint::Fill(1)]).areas(body); | ||
| 54 | + | ||
| 55 | + frame.render_widget(Paragraph::new("My app").block(Block::bordered()), header); | ||
| 56 | + frame.render_widget(Block::bordered().title("Menu"), left); | ||
| 57 | + frame.render_widget(Block::bordered().title("Content"), right); | ||
| 58 | + frame.render_widget("q quit", footer); | ||
| 59 | +} | ||
| 60 | +``` | ||
| 61 | + | ||
| 62 | +The header is always 3 rows high, the footer 1, and the body takes the rest. The body is then split into a menu (30 % of the width) and a content panel that fills the remaining space. | ||
| 63 | + | ||
| 64 | +## Spacing and margins | ||
| 65 | + | ||
| 66 | +`Layout::vertical([...]).spacing(1)` leaves one empty row between areas. `.margin(1)` keeps one cell free all around the area being split. `Block::bordered()` already takes one cell on each side for its border; draw the content in `block.inner(area)` to stay inside it. | ||
| 67 | + | ||
| 68 | +## Centering an area | ||
| 69 | + | ||
| 70 | +To center something, give the layout a single constraint and `Flex::Center`: | ||
| 71 | + | ||
| 72 | +```rust,ignore | ||
| 73 | +use ratatui::layout::{Constraint, Flex, Layout, Rect}; | ||
| 74 | + | ||
| 75 | +fn centered(area: Rect, width: u16, height: u16) -> Rect { | ||
| 76 | + let [area] = Layout::horizontal([Constraint::Length(width)]).flex(Flex::Center).areas(area); | ||
| 77 | + let [area] = Layout::vertical([Constraint::Length(height)]).flex(Flex::Center).areas(area); | ||
| 78 | + area | ||
| 79 | +} | ||
| 80 | +``` | ||
| 81 | + | ||
| 82 | +The popups tutorial uses this to draw a dialog in the middle of the screen. | ||
added
demos/docs-search/docs/03-blocks-and-paragraphs.md +63 -0 | new file mode 100644 | ||
| @@ -0,0 +1,63 @@ | ||
| 1 | +# Blocks, paragraphs and styled text | |
| 2 | + | |
| 3 | +## Block: borders and titles | |
| 4 | + | |
| 5 | +A `Block` draws a border and a title around an area. `Block::bordered()` is a block with all four borders; `.title("…")` sets the title; `.border_type(BorderType::Rounded)` rounds the corners. Most widgets take a block with `.block(...)` and draw inside it. | |
| 6 | + | |
| 7 | +## Paragraph: text that wraps | |
| 8 | + | |
| 9 | +`Paragraph::new(text)` draws text. Without wrapping, a long line is cut at the edge; `.wrap(Wrap { trim: true })` wraps it at word boundaries and trims the leading spaces of wrapped lines. `.scroll((y, 0))` scrolls it down by `y` lines, `.alignment(Alignment::Center)` centers it. | |
| 10 | + | |
| 11 | +## Styled text: Span, Line, Text | |
| 12 | + | |
| 13 | +Text is built from three types: | |
| 14 | + | |
| 15 | +- a `Span` is a piece of text with one style; | |
| 16 | +- a `Line` is a row of spans; | |
| 17 | +- a `Text` is several lines. | |
| 18 | + | |
| 19 | +The `Stylize` trait adds short methods to strings, spans and widgets: `"warning".red().bold()`, `"ok".green()`, `Paragraph::new("x").on_blue()`. | |
| 20 | + | |
| 21 | +```rust | |
| 22 | +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; | |
| 23 | +use ratatui::style::Stylize; | |
| 24 | +use ratatui::text::{Line, Text}; | |
| 25 | +use ratatui::widgets::{Block, BorderType, Paragraph, Wrap}; | |
| 26 | +use ratatui::{DefaultTerminal, Frame}; | |
| 27 | + | |
| 28 | +fn main() -> std::io::Result<()> { | |
| 29 | + let mut terminal = ratatui::init(); | |
| 30 | + let result = run(&mut terminal); | |
| 31 | + ratatui::restore(); | |
| 32 | + result | |
| 33 | +} | |
| 34 | + | |
| 35 | +fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> { | |
| 36 | + loop { | |
| 37 | + terminal.draw(draw)?; | |
| 38 | + if let Event::Key(key) = event::read()? { | |
| 39 | + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { | |
| 40 | + return Ok(()); | |
| 41 | + } | |
| 42 | + } | |
| 43 | + } | |
| 44 | +} | |
| 45 | + | |
| 46 | +fn draw(frame: &mut Frame) { | |
| 47 | + let text = Text::from(vec![ | |
| 48 | + Line::from(vec!["Status: ".into(), "running".green().bold()]), | |
| 49 | + Line::from(vec!["Errors: ".into(), "2".red()]), | |
| 50 | + Line::from(""), | |
| 51 | + Line::from("This long line is wrapped at word boundaries when the window is narrower than the text."), | |
| 52 | + ]); | |
| 53 | + let block = Block::bordered() | |
| 54 | + .border_type(BorderType::Rounded) | |
| 55 | + .title(" Report ".bold()); | |
| 56 | + let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); | |
| 57 | + frame.render_widget(paragraph, frame.area()); | |
| 58 | +} | |
| 59 | +``` | |
| 60 | + | |
| 61 | +## Colors | |
| 62 | + | |
| 63 | +`Stylize` covers the named colors (`.red()`, `.on_blue()` for the background, `.light_green()`…) and the modifiers (`.bold()`, `.italic()`, `.underlined()`, `.reversed()`, `.dim()`). For anything else, build a `Style`: `Style::new().fg(Color::Rgb(255, 128, 0)).add_modifier(Modifier::BOLD)`, then `.style(style)` on a widget or `Span::styled("text", style)`. | |
| new file mode 100644 | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | +# Blocks, paragraphs and styled text | ||
| 2 | + | ||
| 3 | +## Block: borders and titles | ||
| 4 | + | ||
| 5 | +A `Block` draws a border and a title around an area. `Block::bordered()` is a block with all four borders; `.title("…")` sets the title; `.border_type(BorderType::Rounded)` rounds the corners. Most widgets take a block with `.block(...)` and draw inside it. | ||
| 6 | + | ||
| 7 | +## Paragraph: text that wraps | ||
| 8 | + | ||
| 9 | +`Paragraph::new(text)` draws text. Without wrapping, a long line is cut at the edge; `.wrap(Wrap { trim: true })` wraps it at word boundaries and trims the leading spaces of wrapped lines. `.scroll((y, 0))` scrolls it down by `y` lines, `.alignment(Alignment::Center)` centers it. | ||
| 10 | + | ||
| 11 | +## Styled text: Span, Line, Text | ||
| 12 | + | ||
| 13 | +Text is built from three types: | ||
| 14 | + | ||
| 15 | +- a `Span` is a piece of text with one style; | ||
| 16 | +- a `Line` is a row of spans; | ||
| 17 | +- a `Text` is several lines. | ||
| 18 | + | ||
| 19 | +The `Stylize` trait adds short methods to strings, spans and widgets: `"warning".red().bold()`, `"ok".green()`, `Paragraph::new("x").on_blue()`. | ||
| 20 | + | ||
| 21 | +```rust | ||
| 22 | +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; | ||
| 23 | +use ratatui::style::Stylize; | ||
| 24 | +use ratatui::text::{Line, Text}; | ||
| 25 | +use ratatui::widgets::{Block, BorderType, Paragraph, Wrap}; | ||
| 26 | +use ratatui::{DefaultTerminal, Frame}; | ||
| 27 | + | ||
| 28 | +fn main() -> std::io::Result<()> { | ||
| 29 | + let mut terminal = ratatui::init(); | ||
| 30 | + let result = run(&mut terminal); | ||
| 31 | + ratatui::restore(); | ||
| 32 | + result | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> { | ||
| 36 | + loop { | ||
| 37 | + terminal.draw(draw)?; | ||
| 38 | + if let Event::Key(key) = event::read()? { | ||
| 39 | + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { | ||
| 40 | + return Ok(()); | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +fn draw(frame: &mut Frame) { | ||
| 47 | + let text = Text::from(vec![ | ||
| 48 | + Line::from(vec!["Status: ".into(), "running".green().bold()]), | ||
| 49 | + Line::from(vec!["Errors: ".into(), "2".red()]), | ||
| 50 | + Line::from(""), | ||
| 51 | + Line::from("This long line is wrapped at word boundaries when the window is narrower than the text."), | ||
| 52 | + ]); | ||
| 53 | + let block = Block::bordered() | ||
| 54 | + .border_type(BorderType::Rounded) | ||
| 55 | + .title(" Report ".bold()); | ||
| 56 | + let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); | ||
| 57 | + frame.render_widget(paragraph, frame.area()); | ||
| 58 | +} | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +## Colors | ||
| 62 | + | ||
| 63 | +`Stylize` covers the named colors (`.red()`, `.on_blue()` for the background, `.light_green()`…) and the modifiers (`.bold()`, `.italic()`, `.underlined()`, `.reversed()`, `.dim()`). For anything else, build a `Style`: `Style::new().fg(Color::Rgb(255, 128, 0)).add_modifier(Modifier::BOLD)`, then `.style(style)` on a widget or `Span::styled("text", style)`. | ||
added
demos/docs-search/docs/04-lists.md +60 -0 | new file mode 100644 | ||
| @@ -0,0 +1,60 @@ | ||
| 1 | +# Lists and selection | |
| 2 | + | |
| 3 | +A `List` shows items, one per row. To let the user move a selection, keep a `ListState` next to the list and render it with `render_stateful_widget`. | |
| 4 | + | |
| 5 | +## Keep the selection with ListState | |
| 6 | + | |
| 7 | +`ListState` remembers which item is selected and how far the list is scrolled. It lives in your app, not in the widget: the widget is rebuilt at each draw, the state survives between draws. `state.select_next()` and `state.select_previous()` move the selection; `state.select(Some(0))` selects the first item; `state.selected()` tells which one is selected. | |
| 8 | + | |
| 9 | +## Highlight the selected item | |
| 10 | + | |
| 11 | +`.highlight_style(...)` styles the selected row and `.highlight_symbol(">> ")` puts a marker before it. `HighlightSpacing::Always` keeps the room for the symbol even when nothing is selected, so the text does not jump. | |
| 12 | + | |
| 13 | +## A list you move through with the arrows | |
| 14 | + | |
| 15 | +```rust | |
| 16 | +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; | |
| 17 | +use ratatui::style::Style; | |
| 18 | +use ratatui::widgets::{Block, HighlightSpacing, List, ListState}; | |
| 19 | +use ratatui::DefaultTerminal; | |
| 20 | + | |
| 21 | +fn main() -> std::io::Result<()> { | |
| 22 | + let mut terminal = ratatui::init(); | |
| 23 | + let result = run(&mut terminal); | |
| 24 | + ratatui::restore(); | |
| 25 | + result | |
| 26 | +} | |
| 27 | + | |
| 28 | +fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> { | |
| 29 | + let books = ["Dune", "Neuromancer", "Hyperion", "Foundation", "Solaris"]; | |
| 30 | + let mut state = ListState::default(); | |
| 31 | + state.select(Some(0)); | |
| 32 | + loop { | |
| 33 | + terminal.draw(|frame| { | |
| 34 | + let list = List::new(books) | |
| 35 | + .block(Block::bordered().title(" Books — ↑/↓ to move, q to quit ")) | |
| 36 | + .highlight_style(Style::new().black().on_yellow()) | |
| 37 | + .highlight_symbol(">> ") | |
| 38 | + .highlight_spacing(HighlightSpacing::Always); | |
| 39 | + frame.render_stateful_widget(list, frame.area(), &mut state); | |
| 40 | + })?; | |
| 41 | + if let Event::Key(key) = event::read()? { | |
| 42 | + if key.kind != KeyEventKind::Press { | |
| 43 | + continue; | |
| 44 | + } | |
| 45 | + match key.code { | |
| 46 | + KeyCode::Char('q') => return Ok(()), | |
| 47 | + KeyCode::Down | KeyCode::Char('j') => state.select_next(), | |
| 48 | + KeyCode::Up | KeyCode::Char('k') => state.select_previous(), | |
| 49 | + _ => {} | |
| 50 | + } | |
| 51 | + } | |
| 52 | + } | |
| 53 | +} | |
| 54 | +``` | |
| 55 | + | |
| 56 | +`select_next` stops at the last item; when the list is taller than the area, `ListState` scrolls it so the selected item stays visible. | |
| 57 | + | |
| 58 | +## Items with several styles | |
| 59 | + | |
| 60 | +An item can be any `Line` or `Text`: `ListItem::new(Line::from(vec!["Dune".bold(), " — Herbert".dim()]))`. `List::new` takes anything that iterates over items, strings included. | |
| new file mode 100644 | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | +# Lists and selection | ||
| 2 | + | ||
| 3 | +A `List` shows items, one per row. To let the user move a selection, keep a `ListState` next to the list and render it with `render_stateful_widget`. | ||
| 4 | + | ||
| 5 | +## Keep the selection with ListState | ||
| 6 | + | ||
| 7 | +`ListState` remembers which item is selected and how far the list is scrolled. It lives in your app, not in the widget: the widget is rebuilt at each draw, the state survives between draws. `state.select_next()` and `state.select_previous()` move the selection; `state.select(Some(0))` selects the first item; `state.selected()` tells which one is selected. | ||
| 8 | + | ||
| 9 | +## Highlight the selected item | ||
| 10 | + | ||
| 11 | +`.highlight_style(...)` styles the selected row and `.highlight_symbol(">> ")` puts a marker before it. `HighlightSpacing::Always` keeps the room for the symbol even when nothing is selected, so the text does not jump. | ||
| 12 | + | ||
| 13 | +## A list you move through with the arrows | ||
| 14 | + | ||
| 15 | +```rust | ||
| 16 | +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; | ||
| 17 | +use ratatui::style::Style; | ||
| 18 | +use ratatui::widgets::{Block, HighlightSpacing, List, ListState}; | ||
| 19 | +use ratatui::DefaultTerminal; | ||
| 20 | + | ||
| 21 | +fn main() -> std::io::Result<()> { | ||
| 22 | + let mut terminal = ratatui::init(); | ||
| 23 | + let result = run(&mut terminal); | ||
| 24 | + ratatui::restore(); | ||
| 25 | + result | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> { | ||
| 29 | + let books = ["Dune", "Neuromancer", "Hyperion", "Foundation", "Solaris"]; | ||
| 30 | + let mut state = ListState::default(); | ||
| 31 | + state.select(Some(0)); | ||
| 32 | + loop { | ||
| 33 | + terminal.draw(|frame| { | ||
| 34 | + let list = List::new(books) | ||
| 35 | + .block(Block::bordered().title(" Books — ↑/↓ to move, q to quit ")) | ||
| 36 | + .highlight_style(Style::new().black().on_yellow()) | ||
| 37 | + .highlight_symbol(">> ") | ||
| 38 | + .highlight_spacing(HighlightSpacing::Always); | ||
| 39 | + frame.render_stateful_widget(list, frame.area(), &mut state); | ||
| 40 | + })?; | ||
| 41 | + if let Event::Key(key) = event::read()? { | ||
| 42 | + if key.kind != KeyEventKind::Press { | ||
| 43 | + continue; | ||
| 44 | + } | ||
| 45 | + match key.code { | ||
| 46 | + KeyCode::Char('q') => return Ok(()), | ||
| 47 | + KeyCode::Down | KeyCode::Char('j') => state.select_next(), | ||
| 48 | + KeyCode::Up | KeyCode::Char('k') => state.select_previous(), | ||
| 49 | + _ => {} | ||
| 50 | + } | ||
| 51 | + } | ||
| 52 | + } | ||
| 53 | +} | ||
| 54 | +``` | ||
| 55 | + | ||
| 56 | +`select_next` stops at the last item; when the list is taller than the area, `ListState` scrolls it so the selected item stays visible. | ||
| 57 | + | ||
| 58 | +## Items with several styles | ||
| 59 | + | ||
| 60 | +An item can be any `Line` or `Text`: `ListItem::new(Line::from(vec!["Dune".bold(), " — Herbert".dim()]))`. `List::new` takes anything that iterates over items, strings included. | ||
added
demos/docs-search/docs/05-tables.md +63 -0 | new file mode 100644 | ||
| @@ -0,0 +1,63 @@ | ||
| 1 | +# Tables | |
| 2 | + | |
| 3 | +A `Table` shows rows and columns, with an optional header. Like a list, it can keep a selected row in a `TableState`. | |
| 4 | + | |
| 5 | +## Rows, cells and column widths | |
| 6 | + | |
| 7 | +`Row::new(vec!["Dune", "Herbert", "1965"])` is a row of cells. `Table::new(rows, widths)` needs the column widths as constraints, the same ones as `Layout`: `[Constraint::Fill(2), Constraint::Fill(1), Constraint::Length(4)]`. `.header(Row::new(...))` adds a header row, `.column_spacing(2)` the space between columns. | |
| 8 | + | |
| 9 | +## Select a row with TableState | |
| 10 | + | |
| 11 | +`TableState` works like `ListState`: `select_next`, `select_previous`, `selected`. Style the selected row with `.row_highlight_style(...)` and mark it with `.highlight_symbol("> ")`. | |
| 12 | + | |
| 13 | +```rust | |
| 14 | +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; | |
| 15 | +use ratatui::layout::Constraint; | |
| 16 | +use ratatui::style::{Style, Stylize}; | |
| 17 | +use ratatui::widgets::{Block, Row, Table, TableState}; | |
| 18 | +use ratatui::DefaultTerminal; | |
| 19 | + | |
| 20 | +fn main() -> std::io::Result<()> { | |
| 21 | + let mut terminal = ratatui::init(); | |
| 22 | + let result = run(&mut terminal); | |
| 23 | + ratatui::restore(); | |
| 24 | + result | |
| 25 | +} | |
| 26 | + | |
| 27 | +fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> { | |
| 28 | + let books = [ | |
| 29 | + ["Dune", "Frank Herbert", "1965"], | |
| 30 | + ["Neuromancer", "William Gibson", "1984"], | |
| 31 | + ["Hyperion", "Dan Simmons", "1989"], | |
| 32 | + ]; | |
| 33 | + let mut state = TableState::default().with_selected(Some(0)); | |
| 34 | + loop { | |
| 35 | + terminal.draw(|frame| { | |
| 36 | + let rows = books.iter().map(|b| Row::new(b.to_vec())); | |
| 37 | + let widths = [Constraint::Fill(2), Constraint::Fill(2), Constraint::Length(4)]; | |
| 38 | + let table = Table::new(rows, widths) | |
| 39 | + .header(Row::new(vec!["Title", "Author", "Year"]).bold().underlined()) | |
| 40 | + .block(Block::bordered().title(" Library ")) | |
| 41 | + .row_highlight_style(Style::new().reversed()) | |
| 42 | + .highlight_symbol("> ") | |
| 43 | + .column_spacing(2); | |
| 44 | + frame.render_stateful_widget(table, frame.area(), &mut state); | |
| 45 | + })?; | |
| 46 | + if let Event::Key(key) = event::read()? { | |
| 47 | + if key.kind != KeyEventKind::Press { | |
| 48 | + continue; | |
| 49 | + } | |
| 50 | + match key.code { | |
| 51 | + KeyCode::Char('q') => return Ok(()), | |
| 52 | + KeyCode::Down => state.select_next(), | |
| 53 | + KeyCode::Up => state.select_previous(), | |
| 54 | + _ => {} | |
| 55 | + } | |
| 56 | + } | |
| 57 | + } | |
| 58 | +} | |
| 59 | +``` | |
| 60 | + | |
| 61 | +## Cells with their own style | |
| 62 | + | |
| 63 | +A cell can be styled on its own: `Cell::from("overdue").red()`, then `Row::new(vec![Cell::from("Dune"), Cell::from("overdue").red()])`. A whole row too: `Row::new(...).on_dark_gray()`. `.height(2)` gives a row two lines. | |
| new file mode 100644 | |||
| @@ -0,0 +1,63 @@ | |||
| 1 | +# Tables | ||
| 2 | + | ||
| 3 | +A `Table` shows rows and columns, with an optional header. Like a list, it can keep a selected row in a `TableState`. | ||
| 4 | + | ||
| 5 | +## Rows, cells and column widths | ||
| 6 | + | ||
| 7 | +`Row::new(vec!["Dune", "Herbert", "1965"])` is a row of cells. `Table::new(rows, widths)` needs the column widths as constraints, the same ones as `Layout`: `[Constraint::Fill(2), Constraint::Fill(1), Constraint::Length(4)]`. `.header(Row::new(...))` adds a header row, `.column_spacing(2)` the space between columns. | ||
| 8 | + | ||
| 9 | +## Select a row with TableState | ||
| 10 | + | ||
| 11 | +`TableState` works like `ListState`: `select_next`, `select_previous`, `selected`. Style the selected row with `.row_highlight_style(...)` and mark it with `.highlight_symbol("> ")`. | ||
| 12 | + | ||
| 13 | +```rust | ||
| 14 | +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; | ||
| 15 | +use ratatui::layout::Constraint; | ||
| 16 | +use ratatui::style::{Style, Stylize}; | ||
| 17 | +use ratatui::widgets::{Block, Row, Table, TableState}; | ||
| 18 | +use ratatui::DefaultTerminal; | ||
| 19 | + | ||
| 20 | +fn main() -> std::io::Result<()> { | ||
| 21 | + let mut terminal = ratatui::init(); | ||
| 22 | + let result = run(&mut terminal); | ||
| 23 | + ratatui::restore(); | ||
| 24 | + result | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +fn run(terminal: &mut DefaultTerminal) -> std::io::Result<()> { | ||
| 28 | + let books = [ | ||
| 29 | + ["Dune", "Frank Herbert", "1965"], | ||
| 30 | + ["Neuromancer", "William Gibson", "1984"], | ||
| 31 | + ["Hyperion", "Dan Simmons", "1989"], | ||
| 32 | + ]; | ||
| 33 | + let mut state = TableState::default().with_selected(Some(0)); | ||
| 34 | + loop { | ||
| 35 | + terminal.draw(|frame| { | ||
| 36 | + let rows = books.iter().map(|b| Row::new(b.to_vec())); | ||
| 37 | + let widths = [Constraint::Fill(2), Constraint::Fill(2), Constraint::Length(4)]; | ||
| 38 | + let table = Table::new(rows, widths) | ||
| 39 | + .header(Row::new(vec!["Title", "Author", "Year"]).bold().underlined()) | ||
| 40 | + .block(Block::bordered().title(" Library ")) | ||
| 41 | + .row_highlight_style(Style::new().reversed()) | ||
| 42 | + .highlight_symbol("> ") | ||
| 43 | + .column_spacing(2); | ||
| 44 | + frame.render_stateful_widget(table, frame.area(), &mut state); | ||
| 45 | + })?; | ||
| 46 | + if let Event::Key(key) = event::read()? { | ||
| 47 | + if key.kind != KeyEventKind::Press { | ||
| 48 | + continue; | ||
| 49 | + } | ||
| 50 | + match key.code { | ||
| 51 | + KeyCode::Char('q') => return Ok(()), | ||
| 52 | + KeyCode::Down => state.select_next(), | ||
| 53 | + KeyCode::Up => state.select_previous(), | ||
| 54 | + _ => {} | ||
| 55 | + } | ||
| 56 | + } | ||
| 57 | + } | ||
| 58 | +} | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +## Cells with their own style | ||
| 62 | + | ||
| 63 | +A cell can be styled on its own: `Cell::from("overdue").red()`, then `Row::new(vec![Cell::from("Dune"), Cell::from("overdue").red()])`. A whole row too: `Row::new(...).on_dark_gray()`. `.height(2)` gives a row two lines. | ||
added
demos/docs-search/docs/06-gauges-and-ticks.md +56 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
demos/docs-search/docs/07-tabs.md +64 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
demos/docs-search/docs/08-app-state-and-events.md +70 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
demos/docs-search/docs/09-popups.md +67 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
demos/docs-search/docs/README.md +13 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
demos/docs-search/snippets.rs +27 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/diagrams/packages.drawio +1 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/en/README.md +2 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
docs/en/explanation/document-search.md +31 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
docs/en/how-to/search-the-docs.md +68 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/en/reference/acp.md +1 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/en/reference/configuration.md +20 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/en/reference/slash-commands.md +3 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/en/reference/tools.md +20 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/README.md +2 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
docs/fr/explanation/document-search.md +31 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
docs/fr/how-to/search-the-docs.md +68 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/reference/acp.md +1 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/reference/configuration.md +20 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/reference/slash-commands.md +3 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
docs/fr/reference/tools.md +20 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
docs_assemble_test.go +36 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/acp/acp.go +13 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/acp/docs_test.go +53 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/agent/agent.go +3 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/agent/memory_test.go +23 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/command/command.go +2 -1 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/command/docs.go +23 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/command/docs_test.go +27 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/config/config.go +13 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/config/docsearch.go +110 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/config/docsearch_test.go +64 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/config/tools.go +1 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/docsearch/bm25.go +146 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/docsearch/configured.go +27 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/docsearch/docsearch.go +133 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/docsearch/docsearch_test.go +196 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/docsearch/passages.go +157 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/docsearch/walk.go +85 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/store/search.go +4 -31 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/store/search_test.go +0 -17 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/textfold/textfold.go +43 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/textfold/textfold_test.go +20 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/tools/searchdocs.go +67 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
internal/tools/searchdocs_test.go +49 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/commands.go +3 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/turn_test.go +18 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
main.go +6 -0 Large diff collapsed to keep the page fast — load it when you need it.