1package list
   2
   3import (
   4	"log/slog"
   5	"slices"
   6	"strings"
   7
   8	"github.com/charmbracelet/bubbles/v2/key"
   9	tea "github.com/charmbracelet/bubbletea/v2"
  10	"github.com/charmbracelet/crush/internal/csync"
  11	"github.com/charmbracelet/crush/internal/tui/components/anim"
  12	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
  13	"github.com/charmbracelet/crush/internal/tui/styles"
  14	"github.com/charmbracelet/crush/internal/tui/util"
  15	"github.com/charmbracelet/lipgloss/v2"
  16	uv "github.com/charmbracelet/ultraviolet"
  17)
  18
  19type Item interface {
  20	util.Model
  21	layout.Sizeable
  22	ID() string
  23}
  24
  25type HasAnim interface {
  26	Item
  27	Spinning() bool
  28}
  29
  30type List[T Item] interface {
  31	util.Model
  32	layout.Sizeable
  33	layout.Focusable
  34
  35	// Just change state
  36	MoveUp(int) tea.Cmd
  37	MoveDown(int) tea.Cmd
  38	GoToTop() tea.Cmd
  39	GoToBottom() tea.Cmd
  40	SelectItemAbove() tea.Cmd
  41	SelectItemBelow() tea.Cmd
  42	SetItems([]T) tea.Cmd
  43	SetSelected(string) tea.Cmd
  44	SelectedItem() *T
  45	Items() []T
  46	UpdateItem(string, T) tea.Cmd
  47	DeleteItem(string) tea.Cmd
  48	PrependItem(T) tea.Cmd
  49	AppendItem(T) tea.Cmd
  50	StartSelection(col, line int)
  51	EndSelection(col, line int)
  52}
  53
  54type direction int
  55
  56const (
  57	DirectionForward direction = iota
  58	DirectionBackward
  59)
  60
  61const (
  62	ItemNotFound              = -1
  63	ViewportDefaultScrollSize = 2
  64)
  65
  66type renderedItem struct {
  67	id     string
  68	view   string
  69	height int
  70	start  int
  71	end    int
  72}
  73
  74type confOptions struct {
  75	width, height int
  76	gap           int
  77	// if you are at the last item and go down it will wrap to the top
  78	wrap         bool
  79	keyMap       KeyMap
  80	direction    direction
  81	selectedItem string
  82	focused      bool
  83	resize       bool
  84	enableMouse  bool
  85}
  86
  87type list[T Item] struct {
  88	*confOptions
  89
  90	offset int
  91
  92	indexMap *csync.Map[string, int]
  93	items    *csync.Slice[T]
  94
  95	renderedItems *csync.Map[string, renderedItem]
  96
  97	rendered string
  98
  99	movingByItem       bool
 100	selectionStartCol  int
 101	selectionStartLine int
 102	selectionEndCol    int
 103	selectionEndLine   int
 104}
 105
 106type ListOption func(*confOptions)
 107
 108// WithSize sets the size of the list.
 109func WithSize(width, height int) ListOption {
 110	return func(l *confOptions) {
 111		l.width = width
 112		l.height = height
 113	}
 114}
 115
 116// WithGap sets the gap between items in the list.
 117func WithGap(gap int) ListOption {
 118	return func(l *confOptions) {
 119		l.gap = gap
 120	}
 121}
 122
 123// WithDirectionForward sets the direction to forward
 124func WithDirectionForward() ListOption {
 125	return func(l *confOptions) {
 126		l.direction = DirectionForward
 127	}
 128}
 129
 130// WithDirectionBackward sets the direction to forward
 131func WithDirectionBackward() ListOption {
 132	return func(l *confOptions) {
 133		l.direction = DirectionBackward
 134	}
 135}
 136
 137// WithSelectedItem sets the initially selected item in the list.
 138func WithSelectedItem(id string) ListOption {
 139	return func(l *confOptions) {
 140		l.selectedItem = id
 141	}
 142}
 143
 144func WithKeyMap(keyMap KeyMap) ListOption {
 145	return func(l *confOptions) {
 146		l.keyMap = keyMap
 147	}
 148}
 149
 150func WithWrapNavigation() ListOption {
 151	return func(l *confOptions) {
 152		l.wrap = true
 153	}
 154}
 155
 156func WithFocus(focus bool) ListOption {
 157	return func(l *confOptions) {
 158		l.focused = focus
 159	}
 160}
 161
 162func WithResizeByList() ListOption {
 163	return func(l *confOptions) {
 164		l.resize = true
 165	}
 166}
 167
 168func WithEnableMouse() ListOption {
 169	return func(l *confOptions) {
 170		l.enableMouse = true
 171	}
 172}
 173
 174func New[T Item](items []T, opts ...ListOption) List[T] {
 175	list := &list[T]{
 176		confOptions: &confOptions{
 177			direction: DirectionForward,
 178			keyMap:    DefaultKeyMap(),
 179			focused:   true,
 180		},
 181		items:              csync.NewSliceFrom(items),
 182		indexMap:           csync.NewMap[string, int](),
 183		renderedItems:      csync.NewMap[string, renderedItem](),
 184		selectionStartCol:  -1,
 185		selectionStartLine: -1,
 186		selectionEndLine:   -1,
 187		selectionEndCol:    -1,
 188	}
 189	for _, opt := range opts {
 190		opt(list.confOptions)
 191	}
 192
 193	for inx, item := range items {
 194		if i, ok := any(item).(Indexable); ok {
 195			i.SetIndex(inx)
 196		}
 197		list.indexMap.Set(item.ID(), inx)
 198	}
 199	return list
 200}
 201
 202// Init implements List.
 203func (l *list[T]) Init() tea.Cmd {
 204	return l.render()
 205}
 206
 207// Update implements List.
 208func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 209	switch msg := msg.(type) {
 210	case tea.MouseWheelMsg:
 211		if l.enableMouse {
 212			return l.handleMouseWheel(msg)
 213		}
 214		return l, nil
 215	case anim.StepMsg:
 216		var cmds []tea.Cmd
 217		for _, item := range slices.Collect(l.items.Seq()) {
 218			if i, ok := any(item).(HasAnim); ok && i.Spinning() {
 219				updated, cmd := i.Update(msg)
 220				cmds = append(cmds, cmd)
 221				if u, ok := updated.(T); ok {
 222					cmds = append(cmds, l.UpdateItem(u.ID(), u))
 223				}
 224			}
 225		}
 226		return l, tea.Batch(cmds...)
 227	case tea.KeyPressMsg:
 228		if l.focused {
 229			switch {
 230			case key.Matches(msg, l.keyMap.Down):
 231				return l, l.MoveDown(ViewportDefaultScrollSize)
 232			case key.Matches(msg, l.keyMap.Up):
 233				return l, l.MoveUp(ViewportDefaultScrollSize)
 234			case key.Matches(msg, l.keyMap.DownOneItem):
 235				return l, l.SelectItemBelow()
 236			case key.Matches(msg, l.keyMap.UpOneItem):
 237				return l, l.SelectItemAbove()
 238			case key.Matches(msg, l.keyMap.HalfPageDown):
 239				return l, l.MoveDown(l.height / 2)
 240			case key.Matches(msg, l.keyMap.HalfPageUp):
 241				return l, l.MoveUp(l.height / 2)
 242			case key.Matches(msg, l.keyMap.PageDown):
 243				return l, l.MoveDown(l.height)
 244			case key.Matches(msg, l.keyMap.PageUp):
 245				return l, l.MoveUp(l.height)
 246			case key.Matches(msg, l.keyMap.End):
 247				return l, l.GoToBottom()
 248			case key.Matches(msg, l.keyMap.Home):
 249				return l, l.GoToTop()
 250			}
 251			s := l.SelectedItem()
 252			if s == nil {
 253				return l, nil
 254			}
 255			item := *s
 256			var cmds []tea.Cmd
 257			updated, cmd := item.Update(msg)
 258			cmds = append(cmds, cmd)
 259			if u, ok := updated.(T); ok {
 260				cmds = append(cmds, l.UpdateItem(u.ID(), u))
 261			}
 262			return l, tea.Batch(cmds...)
 263		}
 264	}
 265	return l, nil
 266}
 267
 268func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (tea.Model, tea.Cmd) {
 269	var cmd tea.Cmd
 270	switch msg.Button {
 271	case tea.MouseWheelDown:
 272		cmd = l.MoveDown(ViewportDefaultScrollSize)
 273	case tea.MouseWheelUp:
 274		cmd = l.MoveUp(ViewportDefaultScrollSize)
 275	}
 276	return l, cmd
 277}
 278
 279// View implements List.
 280func (l *list[T]) View() string {
 281	if l.height <= 0 || l.width <= 0 {
 282		return ""
 283	}
 284	t := styles.CurrentTheme()
 285	view := l.rendered
 286	lines := strings.Split(view, "\n")
 287
 288	start, end := l.viewPosition()
 289	viewStart := max(0, start)
 290	viewEnd := min(len(lines), end+1)
 291	lines = lines[viewStart:viewEnd]
 292	if l.resize {
 293		return strings.Join(lines, "\n")
 294	}
 295	view = t.S().Base.
 296		Height(l.height).
 297		Width(l.width).
 298		Render(strings.Join(lines, "\n"))
 299	if l.selectionStartCol < 0 {
 300		return view
 301	}
 302	area := uv.Rect(0, 0, l.width, l.height)
 303	scr := uv.NewScreenBuffer(area.Dx(), area.Dy())
 304	uv.NewStyledString(view).Draw(scr, area)
 305
 306	selArea := uv.Rectangle{
 307		Min: uv.Pos(l.selectionStartCol, l.selectionStartLine),
 308		Max: uv.Pos(l.selectionEndCol, l.selectionEndLine),
 309	}
 310	selArea = selArea.Canon()
 311
 312	specialChars := make(map[string]bool, len(styles.AllIcons))
 313	for _, icon := range styles.AllIcons {
 314		specialChars[icon] = true
 315	}
 316
 317	isNonWhitespace := func(r rune) bool {
 318		return r != ' ' && r != '\t' && r != 0 && r != '\n' && r != '\r'
 319	}
 320
 321	type selectionBounds struct {
 322		startX, endX int
 323		inSelection  bool
 324	}
 325	lineSelections := make([]selectionBounds, scr.Height())
 326
 327	for y := range scr.Height() {
 328		bounds := selectionBounds{startX: -1, endX: -1, inSelection: false}
 329
 330		if y >= selArea.Min.Y && y <= selArea.Max.Y {
 331			bounds.inSelection = true
 332			if selArea.Min.Y == selArea.Max.Y {
 333				// Single line selection
 334				bounds.startX = selArea.Min.X
 335				bounds.endX = selArea.Max.X
 336			} else if y == selArea.Min.Y {
 337				// First line of multi-line selection
 338				bounds.startX = selArea.Min.X
 339				bounds.endX = scr.Width()
 340			} else if y == selArea.Max.Y {
 341				// Last line of multi-line selection
 342				bounds.startX = 0
 343				bounds.endX = selArea.Max.X
 344			} else {
 345				// Middle lines
 346				bounds.startX = 0
 347				bounds.endX = scr.Width()
 348			}
 349		}
 350		lineSelections[y] = bounds
 351	}
 352
 353	type lineBounds struct {
 354		start, end int
 355	}
 356	lineTextBounds := make([]lineBounds, scr.Height())
 357
 358	// First pass: find text bounds for lines that have selections
 359	for y := range scr.Height() {
 360		bounds := lineBounds{start: -1, end: -1}
 361
 362		// Only process lines that might have selections
 363		if lineSelections[y].inSelection {
 364			for x := range scr.Width() {
 365				cell := scr.CellAt(x, y)
 366				if cell == nil {
 367					continue
 368				}
 369
 370				cellStr := cell.String()
 371				if len(cellStr) == 0 {
 372					continue
 373				}
 374
 375				char := rune(cellStr[0])
 376				isSpecial := specialChars[cellStr]
 377
 378				if (isNonWhitespace(char) && !isSpecial) || cell.Style.Bg != nil {
 379					if bounds.start == -1 {
 380						bounds.start = x
 381					}
 382					bounds.end = x + 1 // Position after last character
 383				}
 384			}
 385		}
 386		lineTextBounds[y] = bounds
 387	}
 388
 389	// Second pass: apply selection highlighting
 390	for y := range scr.Height() {
 391		selBounds := lineSelections[y]
 392		if !selBounds.inSelection {
 393			continue
 394		}
 395
 396		textBounds := lineTextBounds[y]
 397		if textBounds.start < 0 {
 398			continue // No text on this line
 399		}
 400
 401		// Only scan within the intersection of text bounds and selection bounds
 402		scanStart := max(textBounds.start, selBounds.startX)
 403		scanEnd := min(textBounds.end, selBounds.endX)
 404
 405		for x := scanStart; x < scanEnd; x++ {
 406			cell := scr.CellAt(x, y)
 407			if cell == nil {
 408				continue
 409			}
 410
 411			cellStr := cell.String()
 412			if len(cellStr) > 0 && !specialChars[cellStr] {
 413				cell = cell.Clone()
 414				cell.Style = cell.Style.Background(t.BgOverlay).Foreground(t.White)
 415				scr.SetCell(x, y, cell)
 416			}
 417		}
 418	}
 419
 420	return scr.Render()
 421}
 422
 423func (l *list[T]) viewPosition() (int, int) {
 424	start, end := 0, 0
 425	renderedLines := lipgloss.Height(l.rendered) - 1
 426	if l.direction == DirectionForward {
 427		start = max(0, l.offset)
 428		end = min(l.offset+l.height-1, renderedLines)
 429	} else {
 430		start = max(0, renderedLines-l.offset-l.height+1)
 431		end = max(0, renderedLines-l.offset)
 432	}
 433	return start, end
 434}
 435
 436func (l *list[T]) recalculateItemPositions() {
 437	currentContentHeight := 0
 438	for _, item := range slices.Collect(l.items.Seq()) {
 439		rItem, ok := l.renderedItems.Get(item.ID())
 440		if !ok {
 441			continue
 442		}
 443		rItem.start = currentContentHeight
 444		rItem.end = currentContentHeight + rItem.height - 1
 445		l.renderedItems.Set(item.ID(), rItem)
 446		currentContentHeight = rItem.end + 1 + l.gap
 447	}
 448}
 449
 450func (l *list[T]) render() tea.Cmd {
 451	if l.width <= 0 || l.height <= 0 || l.items.Len() == 0 {
 452		return nil
 453	}
 454	l.setDefaultSelected()
 455
 456	var focusChangeCmd tea.Cmd
 457	if l.focused {
 458		focusChangeCmd = l.focusSelectedItem()
 459	} else {
 460		focusChangeCmd = l.blurSelectedItem()
 461	}
 462	// we are not rendering the first time
 463	if l.rendered != "" {
 464		// rerender everything will mostly hit cache
 465		l.rendered, _ = l.renderIterator(0, false, "")
 466		if l.direction == DirectionBackward {
 467			l.recalculateItemPositions()
 468		}
 469		// in the end scroll to the selected item
 470		if l.focused {
 471			l.scrollToSelection()
 472		}
 473		return focusChangeCmd
 474	}
 475	rendered, finishIndex := l.renderIterator(0, true, "")
 476	l.rendered = rendered
 477
 478	// recalculate for the initial items
 479	if l.direction == DirectionBackward {
 480		l.recalculateItemPositions()
 481	}
 482	renderCmd := func() tea.Msg {
 483		l.offset = 0
 484		// render the rest
 485		l.rendered, _ = l.renderIterator(finishIndex, false, l.rendered)
 486		// needed for backwards
 487		if l.direction == DirectionBackward {
 488			l.recalculateItemPositions()
 489		}
 490		// in the end scroll to the selected item
 491		if l.focused {
 492			l.scrollToSelection()
 493		}
 494
 495		return nil
 496	}
 497	return tea.Batch(focusChangeCmd, renderCmd)
 498}
 499
 500func (l *list[T]) setDefaultSelected() {
 501	if l.selectedItem == "" {
 502		if l.direction == DirectionForward {
 503			l.selectFirstItem()
 504		} else {
 505			l.selectLastItem()
 506		}
 507	}
 508}
 509
 510func (l *list[T]) scrollToSelection() {
 511	rItem, ok := l.renderedItems.Get(l.selectedItem)
 512	if !ok {
 513		l.selectedItem = ""
 514		l.setDefaultSelected()
 515		return
 516	}
 517
 518	start, end := l.viewPosition()
 519	// item bigger or equal to the viewport do nothing
 520	if rItem.start <= start && rItem.end >= end {
 521		return
 522	}
 523	// if we are moving by item we want to move the offset so that the
 524	// whole item is visible not just portions of it
 525	if l.movingByItem {
 526		if rItem.start >= start && rItem.end <= end {
 527			return
 528		}
 529		defer func() { l.movingByItem = false }()
 530	} else {
 531		// item already in view do nothing
 532		if rItem.start >= start && rItem.start <= end {
 533			return
 534		}
 535		if rItem.end >= start && rItem.end <= end {
 536			return
 537		}
 538	}
 539
 540	if rItem.height >= l.height {
 541		if l.direction == DirectionForward {
 542			l.offset = rItem.start
 543		} else {
 544			l.offset = max(0, lipgloss.Height(l.rendered)-(rItem.start+l.height))
 545		}
 546		return
 547	}
 548
 549	renderedLines := lipgloss.Height(l.rendered) - 1
 550
 551	// If item is above the viewport, make it the first item
 552	if rItem.start < start {
 553		if l.direction == DirectionForward {
 554			l.offset = rItem.start
 555		} else {
 556			l.offset = max(0, renderedLines-rItem.start-l.height+1)
 557		}
 558	} else if rItem.end > end {
 559		// If item is below the viewport, make it the last item
 560		if l.direction == DirectionForward {
 561			l.offset = max(0, rItem.end-l.height+1)
 562		} else {
 563			l.offset = max(0, renderedLines-rItem.end)
 564		}
 565	}
 566}
 567
 568func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
 569	rItem, ok := l.renderedItems.Get(l.selectedItem)
 570	if !ok {
 571		return nil
 572	}
 573	start, end := l.viewPosition()
 574	// item bigger than the viewport do nothing
 575	if rItem.start <= start && rItem.end >= end {
 576		return nil
 577	}
 578	// item already in view do nothing
 579	if rItem.start >= start && rItem.end <= end {
 580		return nil
 581	}
 582
 583	itemMiddle := rItem.start + rItem.height/2
 584
 585	if itemMiddle < start {
 586		// select the first item in the viewport
 587		// the item is most likely an item coming after this item
 588		inx, ok := l.indexMap.Get(rItem.id)
 589		if !ok {
 590			return nil
 591		}
 592		for {
 593			inx = l.firstSelectableItemBelow(inx)
 594			if inx == ItemNotFound {
 595				return nil
 596			}
 597			item, ok := l.items.Get(inx)
 598			if !ok {
 599				continue
 600			}
 601			renderedItem, ok := l.renderedItems.Get(item.ID())
 602			if !ok {
 603				continue
 604			}
 605
 606			// If the item is bigger than the viewport, select it
 607			if renderedItem.start <= start && renderedItem.end >= end {
 608				l.selectedItem = renderedItem.id
 609				return l.render()
 610			}
 611			// item is in the view
 612			if renderedItem.start >= start && renderedItem.start <= end {
 613				l.selectedItem = renderedItem.id
 614				return l.render()
 615			}
 616		}
 617	} else if itemMiddle > end {
 618		// select the first item in the viewport
 619		// the item is most likely an item coming after this item
 620		inx, ok := l.indexMap.Get(rItem.id)
 621		if !ok {
 622			return nil
 623		}
 624		for {
 625			inx = l.firstSelectableItemAbove(inx)
 626			if inx == ItemNotFound {
 627				return nil
 628			}
 629			item, ok := l.items.Get(inx)
 630			if !ok {
 631				continue
 632			}
 633			renderedItem, ok := l.renderedItems.Get(item.ID())
 634			if !ok {
 635				continue
 636			}
 637
 638			// If the item is bigger than the viewport, select it
 639			if renderedItem.start <= start && renderedItem.end >= end {
 640				l.selectedItem = renderedItem.id
 641				return l.render()
 642			}
 643			// item is in the view
 644			if renderedItem.end >= start && renderedItem.end <= end {
 645				l.selectedItem = renderedItem.id
 646				return l.render()
 647			}
 648		}
 649	}
 650	return nil
 651}
 652
 653func (l *list[T]) selectFirstItem() {
 654	inx := l.firstSelectableItemBelow(-1)
 655	if inx != ItemNotFound {
 656		item, ok := l.items.Get(inx)
 657		if ok {
 658			l.selectedItem = item.ID()
 659		}
 660	}
 661}
 662
 663func (l *list[T]) selectLastItem() {
 664	inx := l.firstSelectableItemAbove(l.items.Len())
 665	if inx != ItemNotFound {
 666		item, ok := l.items.Get(inx)
 667		if ok {
 668			l.selectedItem = item.ID()
 669		}
 670	}
 671}
 672
 673func (l *list[T]) firstSelectableItemAbove(inx int) int {
 674	for i := inx - 1; i >= 0; i-- {
 675		item, ok := l.items.Get(i)
 676		if !ok {
 677			continue
 678		}
 679		if _, ok := any(item).(layout.Focusable); ok {
 680			return i
 681		}
 682	}
 683	if inx == 0 && l.wrap {
 684		return l.firstSelectableItemAbove(l.items.Len())
 685	}
 686	return ItemNotFound
 687}
 688
 689func (l *list[T]) firstSelectableItemBelow(inx int) int {
 690	itemsLen := l.items.Len()
 691	for i := inx + 1; i < itemsLen; i++ {
 692		item, ok := l.items.Get(i)
 693		if !ok {
 694			continue
 695		}
 696		if _, ok := any(item).(layout.Focusable); ok {
 697			return i
 698		}
 699	}
 700	if inx == itemsLen-1 && l.wrap {
 701		return l.firstSelectableItemBelow(-1)
 702	}
 703	return ItemNotFound
 704}
 705
 706func (l *list[T]) focusSelectedItem() tea.Cmd {
 707	if l.selectedItem == "" || !l.focused {
 708		return nil
 709	}
 710	var cmds []tea.Cmd
 711	for _, item := range slices.Collect(l.items.Seq()) {
 712		if f, ok := any(item).(layout.Focusable); ok {
 713			if item.ID() == l.selectedItem && !f.IsFocused() {
 714				cmds = append(cmds, f.Focus())
 715				l.renderedItems.Del(item.ID())
 716			} else if item.ID() != l.selectedItem && f.IsFocused() {
 717				cmds = append(cmds, f.Blur())
 718				l.renderedItems.Del(item.ID())
 719			}
 720		}
 721	}
 722	return tea.Batch(cmds...)
 723}
 724
 725func (l *list[T]) blurSelectedItem() tea.Cmd {
 726	if l.selectedItem == "" || l.focused {
 727		return nil
 728	}
 729	var cmds []tea.Cmd
 730	for _, item := range slices.Collect(l.items.Seq()) {
 731		if f, ok := any(item).(layout.Focusable); ok {
 732			if item.ID() == l.selectedItem && f.IsFocused() {
 733				cmds = append(cmds, f.Blur())
 734				l.renderedItems.Del(item.ID())
 735			}
 736		}
 737	}
 738	return tea.Batch(cmds...)
 739}
 740
 741// render iterator renders items starting from the specific index and limits hight if limitHeight != -1
 742// returns the last index and the rendered content so far
 743// we pass the rendered content around and don't use l.rendered to prevent jumping of the content
 744func (l *list[T]) renderIterator(startInx int, limitHeight bool, rendered string) (string, int) {
 745	currentContentHeight := lipgloss.Height(rendered) - 1
 746	itemsLen := l.items.Len()
 747	for i := startInx; i < itemsLen; i++ {
 748		if currentContentHeight >= l.height && limitHeight {
 749			return rendered, i
 750		}
 751		// cool way to go through the list in both directions
 752		inx := i
 753
 754		if l.direction != DirectionForward {
 755			inx = (itemsLen - 1) - i
 756		}
 757
 758		item, ok := l.items.Get(inx)
 759		if !ok {
 760			continue
 761		}
 762		var rItem renderedItem
 763		if cache, ok := l.renderedItems.Get(item.ID()); ok {
 764			rItem = cache
 765		} else {
 766			rItem = l.renderItem(item)
 767			rItem.start = currentContentHeight
 768			rItem.end = currentContentHeight + rItem.height - 1
 769			l.renderedItems.Set(item.ID(), rItem)
 770		}
 771		gap := l.gap + 1
 772		if inx == itemsLen-1 {
 773			gap = 0
 774		}
 775
 776		if l.direction == DirectionForward {
 777			rendered += rItem.view + strings.Repeat("\n", gap)
 778		} else {
 779			rendered = rItem.view + strings.Repeat("\n", gap) + rendered
 780		}
 781		currentContentHeight = rItem.end + 1 + l.gap
 782	}
 783	return rendered, itemsLen
 784}
 785
 786func (l *list[T]) renderItem(item Item) renderedItem {
 787	view := item.View()
 788	return renderedItem{
 789		id:     item.ID(),
 790		view:   view,
 791		height: lipgloss.Height(view),
 792	}
 793}
 794
 795// AppendItem implements List.
 796func (l *list[T]) AppendItem(item T) tea.Cmd {
 797	var cmds []tea.Cmd
 798	cmd := item.Init()
 799	if cmd != nil {
 800		cmds = append(cmds, cmd)
 801	}
 802
 803	l.items.Append(item)
 804	l.indexMap = csync.NewMap[string, int]()
 805	for inx, item := range slices.Collect(l.items.Seq()) {
 806		l.indexMap.Set(item.ID(), inx)
 807	}
 808	if l.width > 0 && l.height > 0 {
 809		cmd = item.SetSize(l.width, l.height)
 810		if cmd != nil {
 811			cmds = append(cmds, cmd)
 812		}
 813	}
 814	cmd = l.render()
 815	if cmd != nil {
 816		cmds = append(cmds, cmd)
 817	}
 818	if l.direction == DirectionBackward {
 819		if l.offset == 0 {
 820			cmd = l.GoToBottom()
 821			if cmd != nil {
 822				cmds = append(cmds, cmd)
 823			}
 824		} else {
 825			newItem, ok := l.renderedItems.Get(item.ID())
 826			if ok {
 827				newLines := newItem.height
 828				if l.items.Len() > 1 {
 829					newLines += l.gap
 830				}
 831				l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
 832			}
 833		}
 834	}
 835	return tea.Sequence(cmds...)
 836}
 837
 838// Blur implements List.
 839func (l *list[T]) Blur() tea.Cmd {
 840	l.focused = false
 841	return l.render()
 842}
 843
 844// DeleteItem implements List.
 845func (l *list[T]) DeleteItem(id string) tea.Cmd {
 846	inx, ok := l.indexMap.Get(id)
 847	if !ok {
 848		return nil
 849	}
 850	l.items.Delete(inx)
 851	l.renderedItems.Del(id)
 852	for inx, item := range slices.Collect(l.items.Seq()) {
 853		l.indexMap.Set(item.ID(), inx)
 854	}
 855
 856	if l.selectedItem == id {
 857		if inx > 0 {
 858			item, ok := l.items.Get(inx - 1)
 859			if ok {
 860				l.selectedItem = item.ID()
 861			} else {
 862				l.selectedItem = ""
 863			}
 864		} else {
 865			l.selectedItem = ""
 866		}
 867	}
 868	cmd := l.render()
 869	if l.rendered != "" {
 870		renderedHeight := lipgloss.Height(l.rendered)
 871		if renderedHeight <= l.height {
 872			l.offset = 0
 873		} else {
 874			maxOffset := renderedHeight - l.height
 875			if l.offset > maxOffset {
 876				l.offset = maxOffset
 877			}
 878		}
 879	}
 880	return cmd
 881}
 882
 883// Focus implements List.
 884func (l *list[T]) Focus() tea.Cmd {
 885	l.focused = true
 886	return l.render()
 887}
 888
 889// GetSize implements List.
 890func (l *list[T]) GetSize() (int, int) {
 891	return l.width, l.height
 892}
 893
 894// GoToBottom implements List.
 895func (l *list[T]) GoToBottom() tea.Cmd {
 896	l.offset = 0
 897	l.selectedItem = ""
 898	l.direction = DirectionBackward
 899	return l.render()
 900}
 901
 902// GoToTop implements List.
 903func (l *list[T]) GoToTop() tea.Cmd {
 904	l.offset = 0
 905	l.selectedItem = ""
 906	l.direction = DirectionForward
 907	return l.render()
 908}
 909
 910// IsFocused implements List.
 911func (l *list[T]) IsFocused() bool {
 912	return l.focused
 913}
 914
 915// Items implements List.
 916func (l *list[T]) Items() []T {
 917	return slices.Collect(l.items.Seq())
 918}
 919
 920func (l *list[T]) incrementOffset(n int) {
 921	renderedHeight := lipgloss.Height(l.rendered)
 922	// no need for offset
 923	if renderedHeight <= l.height {
 924		return
 925	}
 926	maxOffset := renderedHeight - l.height
 927	n = min(n, maxOffset-l.offset)
 928	if n <= 0 {
 929		return
 930	}
 931	l.offset += n
 932}
 933
 934func (l *list[T]) decrementOffset(n int) {
 935	n = min(n, l.offset)
 936	if n <= 0 {
 937		return
 938	}
 939	l.offset -= n
 940	if l.offset < 0 {
 941		l.offset = 0
 942	}
 943}
 944
 945// MoveDown implements List.
 946func (l *list[T]) MoveDown(n int) tea.Cmd {
 947	if l.direction == DirectionForward {
 948		l.incrementOffset(n)
 949	} else {
 950		l.decrementOffset(n)
 951	}
 952	return l.changeSelectionWhenScrolling()
 953}
 954
 955// MoveUp implements List.
 956func (l *list[T]) MoveUp(n int) tea.Cmd {
 957	if l.direction == DirectionForward {
 958		l.decrementOffset(n)
 959	} else {
 960		l.incrementOffset(n)
 961	}
 962	return l.changeSelectionWhenScrolling()
 963}
 964
 965// PrependItem implements List.
 966func (l *list[T]) PrependItem(item T) tea.Cmd {
 967	cmds := []tea.Cmd{
 968		item.Init(),
 969	}
 970	l.items.Prepend(item)
 971	l.indexMap = csync.NewMap[string, int]()
 972	for inx, item := range slices.Collect(l.items.Seq()) {
 973		l.indexMap.Set(item.ID(), inx)
 974	}
 975	if l.width > 0 && l.height > 0 {
 976		cmds = append(cmds, item.SetSize(l.width, l.height))
 977	}
 978	cmds = append(cmds, l.render())
 979	if l.direction == DirectionForward {
 980		if l.offset == 0 {
 981			cmd := l.GoToTop()
 982			if cmd != nil {
 983				cmds = append(cmds, cmd)
 984			}
 985		} else {
 986			newItem, ok := l.renderedItems.Get(item.ID())
 987			if ok {
 988				newLines := newItem.height
 989				if l.items.Len() > 1 {
 990					newLines += l.gap
 991				}
 992				l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
 993			}
 994		}
 995	}
 996	return tea.Batch(cmds...)
 997}
 998
 999// SelectItemAbove implements List.
1000func (l *list[T]) SelectItemAbove() tea.Cmd {
1001	inx, ok := l.indexMap.Get(l.selectedItem)
1002	if !ok {
1003		return nil
1004	}
1005
1006	newIndex := l.firstSelectableItemAbove(inx)
1007	if newIndex == ItemNotFound {
1008		// no item above
1009		return nil
1010	}
1011	var cmds []tea.Cmd
1012	if newIndex == 1 {
1013		peakAboveIndex := l.firstSelectableItemAbove(newIndex)
1014		if peakAboveIndex == ItemNotFound {
1015			// this means there is a section above move to the top
1016			cmd := l.GoToTop()
1017			if cmd != nil {
1018				cmds = append(cmds, cmd)
1019			}
1020		}
1021	}
1022	item, ok := l.items.Get(newIndex)
1023	if !ok {
1024		return nil
1025	}
1026	l.selectedItem = item.ID()
1027	l.movingByItem = true
1028	renderCmd := l.render()
1029	if renderCmd != nil {
1030		cmds = append(cmds, renderCmd)
1031	}
1032	return tea.Sequence(cmds...)
1033}
1034
1035// SelectItemBelow implements List.
1036func (l *list[T]) SelectItemBelow() tea.Cmd {
1037	inx, ok := l.indexMap.Get(l.selectedItem)
1038	if !ok {
1039		return nil
1040	}
1041
1042	newIndex := l.firstSelectableItemBelow(inx)
1043	if newIndex == ItemNotFound {
1044		// no item above
1045		return nil
1046	}
1047	item, ok := l.items.Get(newIndex)
1048	if !ok {
1049		return nil
1050	}
1051	l.selectedItem = item.ID()
1052	l.movingByItem = true
1053	return l.render()
1054}
1055
1056// SelectedItem implements List.
1057func (l *list[T]) SelectedItem() *T {
1058	inx, ok := l.indexMap.Get(l.selectedItem)
1059	if !ok {
1060		return nil
1061	}
1062	if inx > l.items.Len()-1 {
1063		return nil
1064	}
1065	item, ok := l.items.Get(inx)
1066	if !ok {
1067		return nil
1068	}
1069	return &item
1070}
1071
1072// SetItems implements List.
1073func (l *list[T]) SetItems(items []T) tea.Cmd {
1074	l.items.SetSlice(items)
1075	var cmds []tea.Cmd
1076	for inx, item := range slices.Collect(l.items.Seq()) {
1077		if i, ok := any(item).(Indexable); ok {
1078			i.SetIndex(inx)
1079		}
1080		cmds = append(cmds, item.Init())
1081	}
1082	cmds = append(cmds, l.reset(""))
1083	return tea.Batch(cmds...)
1084}
1085
1086// SetSelected implements List.
1087func (l *list[T]) SetSelected(id string) tea.Cmd {
1088	l.selectedItem = id
1089	return l.render()
1090}
1091
1092func (l *list[T]) reset(selectedItem string) tea.Cmd {
1093	var cmds []tea.Cmd
1094	l.rendered = ""
1095	l.offset = 0
1096	l.selectedItem = selectedItem
1097	l.indexMap = csync.NewMap[string, int]()
1098	l.renderedItems = csync.NewMap[string, renderedItem]()
1099	for inx, item := range slices.Collect(l.items.Seq()) {
1100		l.indexMap.Set(item.ID(), inx)
1101		if l.width > 0 && l.height > 0 {
1102			cmds = append(cmds, item.SetSize(l.width, l.height))
1103		}
1104	}
1105	cmds = append(cmds, l.render())
1106	return tea.Batch(cmds...)
1107}
1108
1109// SetSize implements List.
1110func (l *list[T]) SetSize(width int, height int) tea.Cmd {
1111	oldWidth := l.width
1112	l.width = width
1113	l.height = height
1114	if oldWidth != width {
1115		cmd := l.reset(l.selectedItem)
1116		return cmd
1117	}
1118	return nil
1119}
1120
1121// UpdateItem implements List.
1122func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
1123	var cmds []tea.Cmd
1124	if inx, ok := l.indexMap.Get(id); ok {
1125		l.items.Set(inx, item)
1126		oldItem, hasOldItem := l.renderedItems.Get(id)
1127		oldPosition := l.offset
1128		if l.direction == DirectionBackward {
1129			oldPosition = (lipgloss.Height(l.rendered) - 1) - l.offset
1130		}
1131
1132		l.renderedItems.Del(id)
1133		cmd := l.render()
1134
1135		// need to check for nil because of sequence not handling nil
1136		if cmd != nil {
1137			cmds = append(cmds, cmd)
1138		}
1139		if hasOldItem && l.direction == DirectionBackward {
1140			// if we are the last item and there is no offset
1141			// make sure to go to the bottom
1142			if oldPosition < oldItem.end {
1143				newItem, ok := l.renderedItems.Get(item.ID())
1144				if ok {
1145					newLines := newItem.height - oldItem.height
1146					l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
1147				}
1148			}
1149		} else if hasOldItem && l.offset > oldItem.start {
1150			newItem, ok := l.renderedItems.Get(item.ID())
1151			if ok {
1152				newLines := newItem.height - oldItem.height
1153				l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
1154			}
1155		}
1156	}
1157	return tea.Sequence(cmds...)
1158}
1159
1160// StartSelection implements List.
1161func (l *list[T]) StartSelection(col, line int) {
1162	l.selectionStartCol = col
1163	l.selectionStartLine = line
1164	l.selectionEndCol = col
1165	l.selectionEndLine = line
1166	slog.Info("Position", "col", col, "line", line)
1167}
1168
1169// EndSelection implements List.
1170func (l *list[T]) EndSelection(col, line int) {
1171	l.selectionEndCol = col
1172	l.selectionEndLine = line
1173	slog.Info("Position", "col", col, "line", line)
1174}