# filetree A project's files as an expandable tree, and the widget that shows one. Depends on `ui` for its place on screen and `theme` for its colours, and on nothing above it. `app` opens and owns the window; this package knows nothing about menus, buffers or language servers. ## Two halves **`Tree` and `Node`** — the model. It reads directories, remembers what is open, and flattens itself into the rows a widget would paint. No tcell, no `ui`, no screen: it is tested by making files and comparing values, which is where most of this package's tests are. **`View`** — a `ui.Widget`. It draws the rows, walks them with the keyboard and the mouse, and hands a chosen file's path to whoever set `OnOpen`. ## What it shows Directories first, then files, each group sorted by name. **`.git` is the only thing hidden.** That is deliberately not the Open dialog's rule, which hides every dot-entry. `.turbo-go/settings.toml` is a file the editor asks people to edit, and `.gitignore` and `.qlty/` belong to the project too — hiding them would make the editor's own configuration unreachable from the editor's own file browser. `.git` differs in kind rather than spelling: nothing in it is meant to be opened by hand, and it holds enough objects to bury everything else. ## Reading is lazy, refreshing is not a walk A directory is read the first time it is expanded. Opening a tree on a large project costs one listing. `Refresh` re-reads **only the directories that have already been read**, so refreshing costs what is on screen rather than a walk of the project. It keeps the shape: a directory that was open stays open, one that has gone takes its branch with it, and the widget puts the highlight back on the same entry — or on the nearest remaining row when that entry is gone. There is no filesystem watching, and no dependency for one. The caller picks the moments: `app` refreshes after a save, and the view refreshes itself on `F5` or `Ctrl-R`. ## Theme keys `tree.text`, `tree.directory`, `tree.selected`, `tree.unfocused`. These could not be borrowed from the `list.*` group, and the reason is worth keeping: `list.selected` is coloured to stand out against a **dialog**. In `turbo-classic` it is white on navy while `window.body` is navy — a tree in a window would have highlighted its selected row in the colour it sits on. `TestTheHighlightIsVisibleInEveryShippedTheme` holds every shipped theme to 64 channel values between `tree.text` and `tree.selected`. ## Public API | Name | What it does | | --- | --- | | `New(root string) (*Tree, error)` | Reads a directory; errors on a file or a path that cannot be read | | `(*Tree) Root() *Node` | The directory the tree is rooted at | | `(*Tree) Rows() []Row` | The visible lines, top to bottom; the root is not one of them | | `(*Tree) Refresh()` | Re-reads what has been read, keeping the shape | | `Row{Node, Depth}` | One line and how deep it sits | | `(*Node) Name/Path/IsDir/Expanded/Children` | What one entry is | | `(*Node) Expand/Collapse/Toggle` | Opening and closing a directory | | `NewView(root string) (*View, error)` | Starts a tree and returns the widget showing it | | `(*View) Title() string` | The project's directory name, for the window | | `(*View) Root() string` | The absolute path it is rooted at | | `(*View) Selected() *Node` | The highlighted entry, or nil | | `(*View) Choose()` | Act on the highlight: open a file, toggle a directory | | `(*View) Refresh()` | Re-read, keeping the highlight where it was | | `(*View) OnOpen func(path string)` | Called with a chosen **file's** path; a directory never reaches it | | `(*View) Draw/SetBounds/HandleKey/HandleMouse` | The `ui` widget contract | ```go view, err := filetree.NewView(".") if err != nil { return err } view.OnOpen = func(path string) { editor.Open(path) } window := ui.NewWindow(view.Title(), view) ``` ## Keys `↑` `↓` `PgUp` `PgDn` `Home` `End` move. `→` expands a closed directory and otherwise steps to the next row; `←` collapses an open one and otherwise steps out to its parent — so the two together walk the shape and neither is ever a no-op. `Enter` chooses. `F5` and `Ctrl-R` refresh. The parent is found as *the nearest row above with a smaller depth*, which is what the eye reads as "the folder this is in" and needs no parent pointer on the nodes. ## Tests ```sh make test go test ./filetree/ ``` The model tests build real directories under `t.TempDir()`; the view tests draw onto a `tcell.SimulationScreen` and assert on the picture a terminal would show.