Del: Update
This commit is contained in:
@@ -1,127 +0,0 @@
|
||||
// Package navigation implements the menu navigation stack for Nebara OS TUI.
|
||||
package navigation
|
||||
|
||||
import "github.com/nebara/nebara-os/internal/model"
|
||||
|
||||
// Level represents one level in the navigation stack.
|
||||
type Level struct {
|
||||
Items []model.MenuItem
|
||||
SelectedIdx int
|
||||
}
|
||||
|
||||
// Navigator maintains a stack of menu levels and tracks the current position.
|
||||
type Navigator struct {
|
||||
stack []Level
|
||||
}
|
||||
|
||||
// NewNavigator creates a Navigator rooted at the main menu.
|
||||
func NewNavigator(items []model.MenuItem, initialIdx int) *Navigator {
|
||||
if initialIdx < 0 || initialIdx >= len(items) {
|
||||
initialIdx = 0
|
||||
}
|
||||
return &Navigator{
|
||||
stack: []Level{
|
||||
{Items: items, SelectedIdx: initialIdx},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Current returns the active menu level.
|
||||
func (n *Navigator) Current() *Level {
|
||||
if len(n.stack) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &n.stack[len(n.stack)-1]
|
||||
}
|
||||
|
||||
// Depth returns the current nesting depth (0 = main menu).
|
||||
func (n *Navigator) Depth() int {
|
||||
return len(n.stack) - 1
|
||||
}
|
||||
|
||||
// IsAtRoot reports whether the navigator is at the main menu level.
|
||||
func (n *Navigator) IsAtRoot() bool {
|
||||
return len(n.stack) == 1
|
||||
}
|
||||
|
||||
// SelectedItem returns the currently highlighted menu item.
|
||||
func (n *Navigator) SelectedItem() *model.MenuItem {
|
||||
cur := n.Current()
|
||||
if cur == nil {
|
||||
return nil
|
||||
}
|
||||
return &cur.Items[cur.SelectedIdx]
|
||||
}
|
||||
|
||||
// MoveUp moves the selection one row up, clamping at the first item.
|
||||
func (n *Navigator) MoveUp() {
|
||||
cur := n.Current()
|
||||
if cur == nil {
|
||||
return
|
||||
}
|
||||
if cur.SelectedIdx > 0 {
|
||||
cur.SelectedIdx--
|
||||
}
|
||||
}
|
||||
|
||||
// MoveDown moves the selection one row down, clamping at the last item.
|
||||
func (n *Navigator) MoveDown() {
|
||||
cur := n.Current()
|
||||
if cur == nil {
|
||||
return
|
||||
}
|
||||
if cur.SelectedIdx < len(cur.Items)-1 {
|
||||
cur.SelectedIdx++
|
||||
}
|
||||
}
|
||||
|
||||
// MoveFirst jumps to the first item.
|
||||
func (n *Navigator) MoveFirst() {
|
||||
cur := n.Current()
|
||||
if cur != nil {
|
||||
cur.SelectedIdx = 0
|
||||
}
|
||||
}
|
||||
|
||||
// MoveLast jumps to the last item.
|
||||
func (n *Navigator) MoveLast() {
|
||||
cur := n.Current()
|
||||
if cur != nil {
|
||||
cur.SelectedIdx = len(cur.Items) - 1
|
||||
}
|
||||
}
|
||||
|
||||
// Enter descends into the submenu of the currently selected item.
|
||||
// Returns false when the item has no children (leaf node).
|
||||
func (n *Navigator) Enter() bool {
|
||||
item := n.SelectedItem()
|
||||
if item == nil || len(item.Children) == 0 {
|
||||
return false
|
||||
}
|
||||
n.stack = append(n.stack, Level{
|
||||
Items: item.Children,
|
||||
SelectedIdx: 0,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// Back pops the current level from the stack, returning to the parent.
|
||||
// Returns false when already at the root.
|
||||
func (n *Navigator) Back() bool {
|
||||
if len(n.stack) <= 1 {
|
||||
return false
|
||||
}
|
||||
n.stack = n.stack[:len(n.stack)-1]
|
||||
return true
|
||||
}
|
||||
|
||||
// BreadcrumbTitle builds a display title for the current navigation depth.
|
||||
// It returns empty string when at the root.
|
||||
func (n *Navigator) BreadcrumbTitle() string {
|
||||
if len(n.stack) <= 1 {
|
||||
return ""
|
||||
}
|
||||
// Return the label of the parent item whose children we are browsing.
|
||||
parent := n.stack[len(n.stack)-2]
|
||||
return parent.Items[parent.SelectedIdx].Label
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
package navigation_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nebara/nebara-os/internal/model"
|
||||
"github.com/nebara/nebara-os/internal/ui/navigation"
|
||||
)
|
||||
|
||||
func sampleItems() []model.MenuItem {
|
||||
return []model.MenuItem{
|
||||
{Label: "Item A", Description: "Desc A", Children: []model.MenuItem{
|
||||
{Label: "Child A1", Description: "Desc A1"},
|
||||
{Label: "Child A2", Description: "Desc A2"},
|
||||
}},
|
||||
{Label: "Item B", Description: "Desc B"},
|
||||
{Label: "Item C", Description: "Desc C"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNavigator_InitialIndex(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 1)
|
||||
if nav.Current().SelectedIdx != 1 {
|
||||
t.Errorf("expected initial index 1, got %d", nav.Current().SelectedIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNavigator_InvalidIndex(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 99)
|
||||
if nav.Current().SelectedIdx != 0 {
|
||||
t.Errorf("out-of-range index should clamp to 0, got %d", nav.Current().SelectedIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveDown(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 0)
|
||||
nav.MoveDown()
|
||||
if nav.Current().SelectedIdx != 1 {
|
||||
t.Errorf("expected index 1 after MoveDown, got %d", nav.Current().SelectedIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveDown_Clamps(t *testing.T) {
|
||||
items := sampleItems()
|
||||
nav := navigation.NewNavigator(items, len(items)-1)
|
||||
nav.MoveDown()
|
||||
if nav.Current().SelectedIdx != len(items)-1 {
|
||||
t.Errorf("MoveDown should clamp at last item")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveUp(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 2)
|
||||
nav.MoveUp()
|
||||
if nav.Current().SelectedIdx != 1 {
|
||||
t.Errorf("expected index 1 after MoveUp, got %d", nav.Current().SelectedIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveUp_Clamps(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 0)
|
||||
nav.MoveUp()
|
||||
if nav.Current().SelectedIdx != 0 {
|
||||
t.Errorf("MoveUp should clamp at 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveFirst(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 2)
|
||||
nav.MoveFirst()
|
||||
if nav.Current().SelectedIdx != 0 {
|
||||
t.Errorf("MoveFirst should jump to 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveLast(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 0)
|
||||
nav.MoveLast()
|
||||
last := len(sampleItems()) - 1
|
||||
if nav.Current().SelectedIdx != last {
|
||||
t.Errorf("MoveLast should jump to %d, got %d", last, nav.Current().SelectedIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnter_HasChildren(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 0) // Item A has children
|
||||
ok := nav.Enter()
|
||||
if !ok {
|
||||
t.Fatal("Enter on item with children should return true")
|
||||
}
|
||||
if nav.Depth() != 1 {
|
||||
t.Errorf("expected depth 1, got %d", nav.Depth())
|
||||
}
|
||||
if nav.Current().Items[0].Label != "Child A1" {
|
||||
t.Errorf("expected Child A1, got %s", nav.Current().Items[0].Label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnter_NoChildren(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 1) // Item B has no children
|
||||
ok := nav.Enter()
|
||||
if ok {
|
||||
t.Fatal("Enter on leaf item should return false")
|
||||
}
|
||||
if nav.Depth() != 0 {
|
||||
t.Errorf("depth should remain 0 for leaf enter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBack(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 0)
|
||||
nav.Enter()
|
||||
ok := nav.Back()
|
||||
if !ok {
|
||||
t.Fatal("Back from submenu should return true")
|
||||
}
|
||||
if nav.Depth() != 0 {
|
||||
t.Errorf("expected depth 0 after back, got %d", nav.Depth())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBack_AtRoot(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 0)
|
||||
ok := nav.Back()
|
||||
if ok {
|
||||
t.Fatal("Back at root should return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAtRoot(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 0)
|
||||
if !nav.IsAtRoot() {
|
||||
t.Error("should be at root initially")
|
||||
}
|
||||
nav.Enter()
|
||||
if nav.IsAtRoot() {
|
||||
t.Error("should not be at root after Enter")
|
||||
}
|
||||
nav.Back()
|
||||
if !nav.IsAtRoot() {
|
||||
t.Error("should be at root after Back")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectionPreservedAfterBack(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 0)
|
||||
nav.MoveDown() // idx=1
|
||||
nav.MoveDown() // idx=2
|
||||
nav.Back() // at root, nothing changes
|
||||
if nav.Current().SelectedIdx != 2 {
|
||||
t.Errorf("selection should be 2, got %d", nav.Current().SelectedIdx)
|
||||
}
|
||||
nav.MoveFirst() // idx=0
|
||||
nav.Enter() // into Item A's children
|
||||
nav.Back() // back to root, parent should restore idx=0
|
||||
if nav.Current().SelectedIdx != 0 {
|
||||
t.Errorf("after returning from submenu, root selection should be 0, got %d", nav.Current().SelectedIdx)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreadcrumbTitle(t *testing.T) {
|
||||
nav := navigation.NewNavigator(sampleItems(), 0)
|
||||
if nav.BreadcrumbTitle() != "" {
|
||||
t.Error("breadcrumb should be empty at root")
|
||||
}
|
||||
nav.Enter()
|
||||
if nav.BreadcrumbTitle() != "Item A" {
|
||||
t.Errorf("breadcrumb should be 'Item A', got %q", nav.BreadcrumbTitle())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user