turbo-editors/turbo-corepublic Fork 0
main
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 main · k33g · 4h ago
view.go · 127 lines · 3.4 KBGo Blame HistoryRaw
  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
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package filetree

import (
	"codeberg.org/turbo-editors/turbo-core/ui"
)

// The markers drawn before a directory, and the indentation of one level.
//
// A file gets a marker's worth of blank so that names line up whatever their
// neighbours are, which is what makes a column of names readable at a glance.
const (
	openMarker   = "▼ "
	closedMarker = "▶ "
	fileMarker   = "  "
	indent       = "  "
)

// wheelStep is how many rows one notch of the mouse wheel moves.
const wheelStep = 3

// View shows a project's files in a window and opens the one that is chosen.
//
// It is a ui.Widget, so it goes into an ordinary window and gets the desktop's
// moving, resizing and maximising for free.
//
//	view, err := filetree.NewView(".")
//	if err != nil {
//		return err
//	}
//	view.OnOpen = func(path string) { editor.Open(path) }
//	window := ui.NewWindow(view.Title(), view)
type View struct {
	ui.FocusBox

	tree     *Tree
	selected int
	top      int

	// OnOpen is called with the absolute path of a file that was chosen. A
	// directory never reaches it: choosing one opens or closes the branch.
	OnOpen func(path string)
}

// NewView reads a directory and returns the widget showing it.
//
// The error is the one New gives: a root that is not a directory, or cannot be
// read at all, leaves the caller with nothing to show.
func NewView(root string) (*View, error) {
	tree, err := New(root)
	if err != nil {
		return nil, err
	}
	return &View{tree: tree}, nil
}

// Title returns what the window holding this view should be called: the name
// of the directory the tree is rooted at.
func (v *View) Title() string { return v.tree.Root().Name() }

// Root returns the absolute path the tree is rooted at, which is how a caller
// can tell whether it already has a tree on this project.
func (v *View) Root() string { return v.tree.Root().Path() }

// Refresh re-reads the project and keeps the highlight on the same entry when
// it is still there.
//
//	view.Refresh() // after a build, or when a file is saved
func (v *View) Refresh() {
	wanted := v.selectedPath()
	v.tree.Refresh()
	v.selectPath(wanted)
}

// selectedPath returns the path of the highlighted row, or "" when there is
// no row to be on.
func (v *View) selectedPath() string {
	rows := v.tree.Rows()
	if v.selected < 0 || v.selected >= len(rows) {
		return ""
	}
	return rows[v.selected].Node.Path()
}

// selectPath puts the highlight back on a path, or on the nearest row that
// still exists when it has gone.
func (v *View) selectPath(path string) {
	rows := v.tree.Rows()
	for i, row := range rows {
		if row.Node.Path() == path {
			v.selected = i
			return
		}
	}
	v.selected = min(max(v.selected, 0), max(len(rows)-1, 0))
}

// Selected returns the highlighted node, or nil when the project is empty.
func (v *View) Selected() *Node {
	rows := v.tree.Rows()
	if v.selected < 0 || v.selected >= len(rows) {
		return nil
	}
	return rows[v.selected].Node
}

// Choose acts on the highlighted row: a directory opens or closes, and a file
// is handed to OnOpen.
func (v *View) Choose() {
	node := v.Selected()
	if node == nil {
		return
	}
	if node.IsDir() {
		node.Toggle()
		v.keepSelectionInRange()
		return
	}
	if v.OnOpen != nil {
		v.OnOpen(node.Path())
	}
}

// keepSelectionInRange holds the highlight inside the rows there are, which
// collapsing a branch above it can otherwise leave past the end.
func (v *View) keepSelectionInRange() {
	v.selected = min(max(v.selected, 0), max(len(v.tree.Rows())-1, 0))
}