Skip to content

Commit a6657a8

Browse files
committed
docs: improved file browser
1 parent be52b0b commit a6657a8

File tree

4 files changed

+249
-61
lines changed

4 files changed

+249
-61
lines changed

AGENTS.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ Always use `:reload` when requiring namespaces to pick up changes.
1919

2020
# Code Style
2121

22+
## Keep it simple
23+
24+
Always pick the simple solution and don't overthink. Later requirements are to be solved later. Don't optimize early.
25+
26+
## Avoid duplication
27+
28+
Prefer a solution that has logic just once and references this solution before you duplicate logic.
29+
2230
## Namespace Requires and Imports
2331

2432
Always use proper `:require` and `:import` declarations in the `ns` form instead of fully qualified names in code.
@@ -49,6 +57,13 @@ Types: `feat`, `fix`, `refactor`, `perf`, `test`, `docs`, `chore`
4957

5058
# Testing
5159

60+
## Running examples
61+
62+
You can run examples like the file-browser with this command
63+
```bash
64+
clojure -M -m examples.file-browser
65+
```
66+
5267
## Running Tests via REPL
5368

5469
See [ADR 003: Testing Strategy](docs/adr/003-testing-strategy.md) for the full decision record.

docs/adr/adr-005-draft.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# ADR 005: Layout Primitives
2+
3+
## Status
4+
5+
Proposed
6+
7+
## Context
8+
9+
Building panel-based layouts (e.g., a file browser with a list on the left and details box on the right) exposed three problems in the current layout system:
10+
11+
### 1. `split-lines` is duplicated and drops trailing empty lines
12+
13+
Three private `split-lines` functions exist in `layout.clj`, `border.clj`, and `overlay.clj`. All use `clojure.string/split-lines`, which drops trailing empty strings. This breaks the rendering pipeline when `:height` is used with borders:
14+
15+
```
16+
align-vertical "a\nb" height=5 → "a\nb\n\n\n" (correct, 5 lines)
17+
align-horizontal processes it → "a\nb" (3 trailing lines lost)
18+
apply-border wraps it → only 4 lines instead of 7
19+
```
20+
21+
The `:height` style option is effectively broken when combined with `:border`.
22+
23+
### 2. No column layout primitive
24+
25+
`join-horizontal` concatenates text blocks side-by-side but doesn't guarantee the total width equals the terminal width. There's no way to say "left column gets the remaining space, right column is 36 chars wide." Building a two-panel layout requires manual line-by-line concatenation with `pad-right`:
26+
27+
```clojure
28+
;; What you have to write today
29+
(defn- two-columns [left right left-width height]
30+
(let [left-lines (str/split-lines left)
31+
right-lines (str/split-lines right)]
32+
(str/join "\n" (map (fn [i]
33+
(str (w/pad-right (nth left-lines i "") left-width)
34+
(nth right-lines i "")))
35+
(range height)))))
36+
```
37+
38+
### 3. No word-wrap utility
39+
40+
Text that exceeds a column width can only be truncated (`charm.ansi.width/truncate`). There's no way to wrap text to fit within a width, which is needed for content panes and description text.
41+
42+
## Decision
43+
44+
Add three focused primitives rather than a full layout engine.
45+
46+
### 1. Shared `split-lines` in `charm.ansi.width`
47+
48+
Move `split-lines` to `charm.ansi.width` as a public function. Use `(str/split text #"\n" -1)` to preserve trailing empty lines. Update `layout.clj`, `border.clj`, and `overlay.clj` to use it.
49+
50+
```clojure
51+
(defn split-lines
52+
"Split text into lines, preserving trailing empty lines.
53+
Unlike clojure.string/split-lines, this is a true inverse of
54+
(clojure.string/join \"\\n\" lines)."
55+
[s]
56+
(if (or (nil? s) (empty? s))
57+
[""]
58+
(str/split s #"\n" -1)))
59+
```
60+
61+
### 2. `columns` function in `charm.style.layout`
62+
63+
A function that joins pre-rendered text blocks into a fixed-width, fixed-height grid. Each column has a fixed width. Rows are padded/truncated to the specified height.
64+
65+
```clojure
66+
(defn columns
67+
"Join text blocks into a fixed-width row layout.
68+
69+
Each column is a map with :content (string) and :width (int).
70+
The last column's width is optional — it takes whatever space it has.
71+
72+
Options:
73+
:height - Total height in lines (default: tallest column)"
74+
[cols & {:keys [height]}]
75+
...)
76+
```
77+
78+
Usage:
79+
80+
```clojure
81+
(columns [{:content file-list-view :width 44}
82+
{:content details-view}]
83+
:height 20)
84+
```
85+
86+
This operates at the line level: split each column's content into lines, pad each line to the column's width with `pad-right`, concatenate row by row.
87+
88+
### 3. `word-wrap` in `charm.ansi.width`
89+
90+
Wrap text to fit within a display width, breaking at word boundaries.
91+
92+
```clojure
93+
(defn word-wrap
94+
"Wrap text to fit within a display width, breaking at spaces.
95+
Preserves existing line breaks."
96+
[s width]
97+
...)
98+
```
99+
100+
## Consequences
101+
102+
### Pros
103+
104+
- `split-lines` duplication eliminated — single source of truth in `charm.ansi.width`
105+
- `:height` + `:border` works correctly in the style pipeline
106+
- Panel layouts (file browser, split views) become trivial with `columns`
107+
- `word-wrap` enables content-aware text display in fixed-width panes
108+
109+
### Cons
110+
111+
- Changing `split-lines` to preserve trailing empty lines changes behavior for all layout functions — needs careful testing
112+
- `columns` is intentionally simple (fixed widths only) — proportional/flex sizing is left for later if needed
113+
114+
## Notes
115+
116+
- `columns` is not a component — it's a pure layout function that works on pre-rendered strings
117+
- The existing `join-horizontal`/`join-vertical` remain useful for simpler cases where exact width control isn't needed
118+
- A future ADR could propose flex-based sizing if fixed-width columns prove insufficient

docs/examples/src/examples/file_browser.clj

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
(ns examples.file-browser
22
"File browser demonstrating list component with a details pane."
3-
(:require [charm.core :as charm]
4-
[clojure.java.io :as io])
5-
(:import [java.io File]
6-
[java.text SimpleDateFormat]
7-
[java.util Date]))
3+
(:require
4+
[charm.ansi.width :as w]
5+
[charm.components.help :as help]
6+
[charm.core :as charm]
7+
[charm.style.border :as border]
8+
[charm.style.core :as style]
9+
[clojure.java.io :as io]
10+
[clojure.string :as str])
11+
(:import
12+
[java.io File]
13+
[java.text SimpleDateFormat]
14+
[java.util Date]))
815

916
(def title-style
10-
(charm/style :fg charm/magenta :bold true))
17+
(style/style :fg charm/magenta :bold true))
1118

1219
(def path-style
13-
(charm/style :fg 240))
20+
(style/style :fg 240))
1421

1522
(def detail-label-style
16-
(charm/style :fg 240))
23+
(style/style :fg 240))
1724

1825
(def detail-value-style
19-
(charm/style :fg charm/cyan))
26+
(style/style :fg charm/cyan))
2027

2128
(def help-bindings
2229
(charm/help-from-pairs
@@ -25,7 +32,10 @@
2532
"Backspace/h" "back"
2633
"q" "quit"))
2734

28-
(def ^:private details-width 34)
35+
(def ^:private details-width 36)
36+
37+
;; Header (title + path + blank line) + blank line before help + help line
38+
(def ^:private chrome-height 5)
2939

3040
(defn format-size
3141
"Format file size in human-readable format."
@@ -74,16 +84,21 @@
7484
(format-size (:size info)))
7585
:data info}))
7686

77-
(defn- make-file-list [items width]
87+
(defn- list-width [term-width]
88+
(max 20 (- term-width details-width)))
89+
90+
(defn- list-height
91+
"Number of list items visible. Each item takes 2 lines (title + description)."
92+
[term-height]
93+
(max 3 (quot (- term-height chrome-height) 2)))
94+
95+
(defn- make-file-list [items term-width term-height]
7896
(charm/item-list items
79-
:height 15
80-
:width width
97+
:height (list-height term-height)
98+
:width (list-width term-width)
8199
:show-descriptions true
82100
:cursor-style (charm/style :fg charm/cyan :bold true)))
83101

84-
(defn- list-width [term-width]
85-
(max 20 (- term-width details-width 2)))
86-
87102
(defn init []
88103
(let [start-path (System/getProperty "user.dir")
89104
files (list-directory start-path)
@@ -92,7 +107,8 @@
92107
:files files
93108
:items items
94109
:term-width 80
95-
:file-list (make-file-list items (list-width 80))
110+
:term-height 24
111+
:file-list (make-file-list items 80 24)
96112
:help (charm/help help-bindings :width 60)}
97113
nil]))
98114

@@ -106,7 +122,7 @@
106122
:current-path path
107123
:files files
108124
:items items
109-
:file-list (make-file-list items (list-width (:term-width state)))))
125+
:file-list (make-file-list items (:term-width state) (:term-height state))))
110126
state)))
111127

112128
(defn go-up
@@ -136,10 +152,12 @@
136152

137153
;; Window resize
138154
(charm/window-size? msg)
139-
(let [w (:width msg)]
155+
(let [w (:width msg)
156+
h (:height msg)]
140157
[(assoc state
141158
:term-width w
142-
:file-list (make-file-list (:items state) (list-width w)))
159+
:term-height h
160+
:file-list (make-file-list (:items state) w h))
143161
nil])
144162

145163
;; Go up directory
@@ -164,36 +182,49 @@
164182
[state]
165183
(if-let [selected (charm/list-selected-item (:file-list state))]
166184
(let [info (:data selected)]
167-
(str (charm/render detail-label-style "Name ")
168-
(charm/render detail-value-style (:name info)) "\n"
169-
(charm/render detail-label-style "Type ")
170-
(charm/render detail-value-style (if (:directory? info) "Directory" "File")) "\n"
171-
(charm/render detail-label-style "Size ")
172-
(charm/render detail-value-style (format-size (:size info))) "\n"
173-
(charm/render detail-label-style "Modified ")
174-
(charm/render detail-value-style (format-date (:modified info))) "\n"
175-
(charm/render detail-label-style "Access ")
176-
(charm/render detail-value-style
185+
(str (style/render detail-label-style "Name ")
186+
(style/render detail-value-style (:name info)) "\n"
187+
(style/render detail-label-style "Type ")
188+
(style/render detail-value-style (if (:directory? info) "Directory" "File")) "\n"
189+
(style/render detail-label-style "Size ")
190+
(style/render detail-value-style (format-size (:size info))) "\n"
191+
(style/render detail-label-style "Modified ")
192+
(style/render detail-value-style (format-date (:modified info))) "\n"
193+
(style/render detail-label-style "Access ")
194+
(style/render detail-value-style
177195
(str (when (:readable? info) "r")
178196
(when (:writable? info) "w")
179197
(when (:hidden? info) " (hidden)")))))
180-
(charm/render detail-label-style "No file selected")))
198+
(style/render detail-label-style "No file selected")))
199+
200+
(defn- two-columns
201+
"Join left and right text blocks into a fixed-width, fixed-height grid.
202+
Each row is: left padded to left-width, then right. Rows are filled to height."
203+
[left right left-width height]
204+
(let [left-lines (str/split-lines left)
205+
right-lines (str/split-lines right)
206+
render-row (fn [i]
207+
(str (w/pad-right (nth left-lines i "") left-width)
208+
(nth right-lines i "")))]
209+
(str/join "\n" (map render-row (range height)))))
181210

182211
(defn view [state]
183212
(let [file-list-view (charm/list-view (:file-list state))
184213
details-view (render-details state)
185-
details-style (charm/style :border charm/rounded-border
214+
details-style (style/style :border border/rounded
186215
:border-fg 240
187216
:padding [0 1]
188-
:width (- details-width 4))]
189-
(str (charm/render title-style "File Browser") "\n"
190-
(charm/render path-style (:current-path state)) "\n\n"
191-
(charm/join-horizontal :top
192-
file-list-view
193-
" "
194-
(charm/render details-style details-view))
195-
"\n\n"
196-
(charm/help-view (:help state)))))
217+
:width (- details-width 2))
218+
content-height (* (list-height (:term-height state)) 2)
219+
left-w (list-width (:term-width state))]
220+
(str (style/render title-style "File Browser") "\n"
221+
(style/render path-style (:current-path state)) "\n\n"
222+
(two-columns file-list-view
223+
(style/render details-style details-view)
224+
left-w
225+
content-height)
226+
"\n"
227+
(help/short-help-view (:help state)))))
197228

198229
(defn -main [& _args]
199230
(charm/run {:init init

0 commit comments

Comments
 (0)