package filetree import "github.com/gdamore/tcell/v2" // HandleKey walks the tree, opens branches and chooses files. // // Left and right do what they do in every tree: right opens a closed branch // and steps into an open one, left closes an open branch and steps out of a // closed one, so the two together walk the shape without ever being a no-op. func (v *View) HandleKey(ev *tcell.EventKey) bool { if !v.Focused() { return false } if v.handleMovementKey(ev) { return true } switch ev.Key() { case tcell.KeyRight: v.expandOrDescend() case tcell.KeyLeft: v.collapseOrAscend() case tcell.KeyEnter: v.Choose() case tcell.KeyF5, tcell.KeyCtrlR: v.Refresh() default: return false } return true } // handleMovementKey deals with the keys that only move the highlight. func (v *View) handleMovementKey(ev *tcell.EventKey) bool { page := max(v.Bounds().H, 1) switch ev.Key() { case tcell.KeyUp: v.move(-1) case tcell.KeyDown: v.move(1) case tcell.KeyPgUp: v.move(-page) case tcell.KeyPgDn: v.move(page) case tcell.KeyHome: v.moveTo(0) case tcell.KeyEnd: v.moveTo(len(v.tree.Rows()) - 1) default: return false } return true } // move shifts the highlight by a number of rows, stopping at either end. func (v *View) move(by int) { v.moveTo(v.selected + by) } // moveTo puts the highlight on a row, clamped to the rows there are. func (v *View) moveTo(index int) { v.selected = min(max(index, 0), max(len(v.tree.Rows())-1, 0)) } // expandOrDescend opens a closed directory, or steps into one already open. func (v *View) expandOrDescend() { node := v.Selected() if node == nil { return } if node.IsDir() && !node.Expanded() { node.Expand() return } v.move(1) } // collapseOrAscend closes an open directory, or steps out to the parent of // anything else. func (v *View) collapseOrAscend() { node := v.Selected() if node == nil { return } if node.IsDir() && node.Expanded() { node.Collapse() v.keepSelectionInRange() return } v.selectParent() } // selectParent moves the highlight to the branch the current row sits in. // // The parent is 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 to find. func (v *View) selectParent() { rows := v.tree.Rows() if v.selected <= 0 || v.selected >= len(rows) { return } depth := rows[v.selected].Depth for i := v.selected - 1; i >= 0; i-- { if rows[i].Depth < depth { v.selected = i return } } } // HandleMouse highlights the clicked row, chooses it on a second click, and // scrolls with the wheel. func (v *View) HandleMouse(ev *tcell.EventMouse) bool { if !v.Bounds().Contains(ev.Position()) { return false } switch ev.Buttons() { case tcell.WheelUp: v.move(-wheelStep) case tcell.WheelDown: v.move(wheelStep) case tcell.Button1: v.clickRow(ev) } return true } // clickRow selects the row under the pointer, or acts on it when it was // already the selected one — the same second-click rule the list boxes use. func (v *View) clickRow(ev *tcell.EventMouse) { _, y := ev.Position() index := v.top + y - v.Bounds().Y if index == v.selected { v.Choose() return } v.moveTo(index) }