turbo-editors/turbo-corepublic Fork 0
v1.0.1
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on v1.0.1 · k33g · 16h ago
README.md · 75 lines · 4.5 KBmarkdown
Blame HistoryOpen raw

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
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

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# 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.