Merge pull request #31 from charmbracelet/drop-reflection-and-use-string-builder

Raphael Amorim created

perf: drop reflection usage and use string builder

Change summary

internal/llm/tools/diagnostics.go                   | 32 +++++++-------
internal/llm/tools/grep.go                          | 18 ++++----
internal/llm/tools/grep_test.go                     | 18 ++++----
internal/llm/tools/ls.go                            |  2 
internal/tui/components/chat/chat.go                |  2 
internal/tui/components/chat/editor/editor.go       |  9 ---
internal/tui/components/chat/editor/keys.go         | 15 +++++++
internal/tui/components/chat/sidebar/sidebar.go     |  2 
internal/tui/components/completions/item.go         |  2 
internal/tui/components/completions/keys.go         | 13 +++++
internal/tui/components/core/layout/layout.go       | 15 +-----
internal/tui/components/core/list/keys.go           | 16 +++++++
internal/tui/components/dialogs/commands/item.go    |  2 
internal/tui/components/dialogs/commands/keys.go    | 25 ++++++++++-
internal/tui/components/dialogs/compact/keys.go     | 14 +++++
internal/tui/components/dialogs/filepicker/keys.go  | 15 ++++++
internal/tui/components/dialogs/init/keys.go        | 14 +++++
internal/tui/components/dialogs/keys.go             | 10 +++
internal/tui/components/dialogs/models/keys.go      | 13 +++++
internal/tui/components/dialogs/permissions/keys.go | 16 ++++++
internal/tui/components/dialogs/quit/keys.go        | 15 ++++++
internal/tui/components/dialogs/sessions/keys.go    | 13 +++++
internal/tui/page/logs/keys.go                      | 10 +++
23 files changed, 209 insertions(+), 82 deletions(-)

Detailed changes

internal/llm/tools/diagnostics.go 🔗

@@ -245,28 +245,28 @@ func getDiagnostics(filePath string, lsps map[string]*lsp.Client) string {
 		return projectDiagnostics[i] < projectDiagnostics[j]
 	})
 
-	output := ""
+	var output strings.Builder
 
 	if len(fileDiagnostics) > 0 {
-		output += "\n<file_diagnostics>\n"
+		output.WriteString("\n<file_diagnostics>\n")
 		if len(fileDiagnostics) > 10 {
-			output += strings.Join(fileDiagnostics[:10], "\n")
-			output += fmt.Sprintf("\n... and %d more diagnostics", len(fileDiagnostics)-10)
+			output.WriteString(strings.Join(fileDiagnostics[:10], "\n"))
+			fmt.Fprintf(&output, "\n... and %d more diagnostics", len(fileDiagnostics)-10)
 		} else {
-			output += strings.Join(fileDiagnostics, "\n")
+			output.WriteString(strings.Join(fileDiagnostics, "\n"))
 		}
-		output += "\n</file_diagnostics>\n"
+		output.WriteString("\n</file_diagnostics>\n")
 	}
 
 	if len(projectDiagnostics) > 0 {
-		output += "\n<project_diagnostics>\n"
+		output.WriteString("\n<project_diagnostics>\n")
 		if len(projectDiagnostics) > 10 {
-			output += strings.Join(projectDiagnostics[:10], "\n")
-			output += fmt.Sprintf("\n... and %d more diagnostics", len(projectDiagnostics)-10)
+			output.WriteString(strings.Join(projectDiagnostics[:10], "\n"))
+			fmt.Fprintf(&output, "\n... and %d more diagnostics", len(projectDiagnostics)-10)
 		} else {
-			output += strings.Join(projectDiagnostics, "\n")
+			output.WriteString(strings.Join(projectDiagnostics, "\n"))
 		}
-		output += "\n</project_diagnostics>\n"
+		output.WriteString("\n</project_diagnostics>\n")
 	}
 
 	if len(fileDiagnostics) > 0 || len(projectDiagnostics) > 0 {
@@ -275,13 +275,13 @@ func getDiagnostics(filePath string, lsps map[string]*lsp.Client) string {
 		projectErrors := countSeverity(projectDiagnostics, "Error")
 		projectWarnings := countSeverity(projectDiagnostics, "Warn")
 
-		output += "\n<diagnostic_summary>\n"
-		output += fmt.Sprintf("Current file: %d errors, %d warnings\n", fileErrors, fileWarnings)
-		output += fmt.Sprintf("Project: %d errors, %d warnings\n", projectErrors, projectWarnings)
-		output += "</diagnostic_summary>\n"
+		output.WriteString("\n<diagnostic_summary>\n")
+		fmt.Fprintf(&output, "Current file: %d errors, %d warnings\n", fileErrors, fileWarnings)
+		fmt.Fprintf(&output, "Project: %d errors, %d warnings\n", projectErrors, projectWarnings)
+		output.WriteString("</diagnostic_summary>\n")
 	}
 
-	return output
+	return output.String()
 }
 
 func countSeverity(diagnostics []string, severity string) int {

internal/llm/tools/grep.go 🔗

@@ -198,35 +198,35 @@ func (g *grepTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
 		return ToolResponse{}, fmt.Errorf("error searching files: %w", err)
 	}
 
-	var output string
+	var output strings.Builder
 	if len(matches) == 0 {
-		output = "No files found"
+		output.WriteString("No files found")
 	} else {
-		output = fmt.Sprintf("Found %d matches\n", len(matches))
+		fmt.Fprintf(&output, "Found %d matches\n", len(matches))
 
 		currentFile := ""
 		for _, match := range matches {
 			if currentFile != match.path {
 				if currentFile != "" {
-					output += "\n"
+					output.WriteString("\n")
 				}
 				currentFile = match.path
-				output += fmt.Sprintf("%s:\n", match.path)
+				fmt.Fprintf(&output, "%s:\n", match.path)
 			}
 			if match.lineNum > 0 {
-				output += fmt.Sprintf("  Line %d: %s\n", match.lineNum, match.lineText)
+				fmt.Fprintf(&output, "  Line %d: %s\n", match.lineNum, match.lineText)
 			} else {
-				output += fmt.Sprintf("  %s\n", match.path)
+				fmt.Fprintf(&output, "  %s\n", match.path)
 			}
 		}
 
 		if truncated {
-			output += "\n(Results are truncated. Consider using a more specific path or pattern.)"
+			output.WriteString("\n(Results are truncated. Consider using a more specific path or pattern.)")
 		}
 	}
 
 	return WithResponseMetadata(
-		NewTextResponse(output),
+		NewTextResponse(output.String()),
 		GrepResponseMetadata{
 			NumberOfMatches: len(matches),
 			Truncated:       truncated,

internal/llm/tools/grep_test.go 🔗

@@ -7,24 +7,24 @@ import (
 
 func TestRegexCache(t *testing.T) {
 	cache := newRegexCache()
-	
+
 	// Test basic caching
 	pattern := "test.*pattern"
 	regex1, err := cache.get(pattern)
 	if err != nil {
 		t.Fatalf("Failed to compile regex: %v", err)
 	}
-	
+
 	regex2, err := cache.get(pattern)
 	if err != nil {
 		t.Fatalf("Failed to get cached regex: %v", err)
 	}
-	
+
 	// Should be the same instance (cached)
 	if regex1 != regex2 {
 		t.Error("Expected cached regex to be the same instance")
 	}
-	
+
 	// Test that it actually works
 	if !regex1.MatchString("test123pattern") {
 		t.Error("Regex should match test string")
@@ -34,13 +34,13 @@ func TestRegexCache(t *testing.T) {
 func TestGlobToRegexCaching(t *testing.T) {
 	// Test that globToRegex uses pre-compiled regex
 	pattern1 := globToRegex("*.{js,ts}")
-	
+
 	// Should not panic and should work correctly
 	regex1, err := regexp.Compile(pattern1)
 	if err != nil {
 		t.Fatalf("Failed to compile glob regex: %v", err)
 	}
-	
+
 	if !regex1.MatchString("test.js") {
 		t.Error("Glob regex should match .js files")
 	}
@@ -56,7 +56,7 @@ func TestGlobToRegexCaching(t *testing.T) {
 func BenchmarkRegexCacheVsCompile(b *testing.B) {
 	cache := newRegexCache()
 	pattern := "test.*pattern.*[0-9]+"
-	
+
 	b.Run("WithCache", func(b *testing.B) {
 		for b.Loop() {
 			_, err := cache.get(pattern)
@@ -65,7 +65,7 @@ func BenchmarkRegexCacheVsCompile(b *testing.B) {
 			}
 		}
 	})
-	
+
 	b.Run("WithoutCache", func(b *testing.B) {
 		for b.Loop() {
 			_, err := regexp.Compile(pattern)
@@ -74,4 +74,4 @@ func BenchmarkRegexCacheVsCompile(b *testing.B) {
 			}
 		}
 	})
-}
+}

internal/llm/tools/ls.go 🔗

@@ -209,7 +209,7 @@ func printNode(builder *strings.Builder, node *TreeNode, level int) {
 
 	nodeName := node.Name
 	if node.Type == "directory" {
-		nodeName += string(filepath.Separator)
+		nodeName = nodeName + string(filepath.Separator)
 	}
 
 	fmt.Fprintf(builder, "%s- %s\n", indent, nodeName)

internal/tui/components/chat/chat.go 🔗

@@ -500,6 +500,6 @@ func (m *messageListCmp) IsFocused() bool {
 }
 
 func (m *messageListCmp) Bindings() []key.Binding {
-	bindings := layout.KeyMapToSlice(m.defaultListKeyMap)
+	bindings := m.defaultListKeyMap.KeyBindings()
 	return bindings
 }

internal/tui/components/chat/editor/editor.go 🔗

@@ -18,7 +18,6 @@ import (
 	"github.com/charmbracelet/crush/internal/session"
 	"github.com/charmbracelet/crush/internal/tui/components/chat"
 	"github.com/charmbracelet/crush/internal/tui/components/completions"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs/quit"
@@ -49,12 +48,6 @@ type editorCmp struct {
 	isCompletionsOpen     bool
 }
 
-type DeleteAttachmentKeyMaps struct {
-	AttachmentDeleteMode key.Binding
-	Escape               key.Binding
-	DeleteAllAttachments key.Binding
-}
-
 var DeleteKeyMaps = DeleteAttachmentKeyMaps{
 	AttachmentDeleteMode: key.NewBinding(
 		key.WithKeys("ctrl+r"),
@@ -372,7 +365,7 @@ func (c *editorCmp) IsFocused() bool {
 }
 
 func (c *editorCmp) Bindings() []key.Binding {
-	return layout.KeyMapToSlice(c.keyMap)
+	return c.keyMap.KeyBindings()
 }
 
 func NewEditorCmp(app *app.App) util.Model {

internal/tui/components/chat/editor/keys.go 🔗

@@ -27,6 +27,21 @@ func DefaultEditorKeyMap() EditorKeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k EditorKeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.AddFile,
+		k.SendMessage,
+		k.OpenEditor,
+	}
+}
+
+type DeleteAttachmentKeyMaps struct {
+	AttachmentDeleteMode key.Binding
+	Escape               key.Binding
+	DeleteAllAttachments key.Binding
+}
+
 // TODO: update this to use the new keymap concepts
 var AttachmentsKeyMaps = DeleteAttachmentKeyMaps{
 	AttachmentDeleteMode: key.NewBinding(

internal/tui/components/chat/sidebar/sidebar.go 🔗

@@ -10,8 +10,8 @@ import (
 	"github.com/charmbracelet/crush/internal/session"
 	"github.com/charmbracelet/crush/internal/tui/components/chat"
 	"github.com/charmbracelet/crush/internal/tui/components/core"
-	"github.com/charmbracelet/crush/internal/tui/components/logo"
 	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
+	"github.com/charmbracelet/crush/internal/tui/components/logo"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
 	"github.com/charmbracelet/crush/internal/version"

internal/tui/components/completions/item.go 🔗

@@ -4,8 +4,8 @@ import (
 	"image/color"
 
 	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/crush/internal/tui/components/core/list"
 	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
+	"github.com/charmbracelet/crush/internal/tui/components/core/list"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
 	"github.com/charmbracelet/lipgloss/v2"

internal/tui/components/completions/keys.go 🔗

@@ -2,7 +2,6 @@ package completions
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 type KeyMap struct {
@@ -33,10 +32,20 @@ func DefaultKeyMap() KeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.Down,
+		k.Up,
+		k.Select,
+		k.Cancel,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k KeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])

internal/tui/components/core/layout/layout.go 🔗

@@ -1,8 +1,6 @@
 package layout
 
 import (
-	"reflect"
-
 	"github.com/charmbracelet/bubbles/v2/key"
 	tea "github.com/charmbracelet/bubbletea/v2"
 )
@@ -26,14 +24,7 @@ type Positionable interface {
 	SetPosition(x, y int) tea.Cmd
 }
 
-func KeyMapToSlice(t any) (bindings []key.Binding) {
-	typ := reflect.TypeOf(t)
-	if typ.Kind() != reflect.Struct {
-		return nil
-	}
-	for i := range typ.NumField() {
-		v := reflect.ValueOf(t).Field(i)
-		bindings = append(bindings, v.Interface().(key.Binding))
-	}
-	return
+// KeyMapProvider defines an interface for types that can provide their key bindings as a slice
+type KeyMapProvider interface {
+	KeyBindings() []key.Binding
 }

internal/tui/components/core/list/keys.go 🔗

@@ -51,3 +51,19 @@ func DefaultKeyMap() KeyMap {
 		),
 	}
 }
+
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.Down,
+		k.Up,
+		k.NDown,
+		k.NUp,
+		k.DownOneItem,
+		k.UpOneItem,
+		k.HalfPageDown,
+		k.HalfPageUp,
+		k.Home,
+		k.End,
+	}
+}

internal/tui/components/dialogs/commands/item.go 🔗

@@ -3,8 +3,8 @@ package commands
 import (
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/crush/internal/tui/components/core"
-	"github.com/charmbracelet/crush/internal/tui/components/core/list"
 	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
+	"github.com/charmbracelet/crush/internal/tui/components/core/list"
 	"github.com/charmbracelet/crush/internal/tui/styles"
 	"github.com/charmbracelet/crush/internal/tui/util"
 	"github.com/charmbracelet/x/ansi"

internal/tui/components/dialogs/commands/keys.go 🔗

@@ -2,7 +2,6 @@ package commands
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 type CommandsDialogKeyMap struct {
@@ -38,10 +37,21 @@ func DefaultCommandsDialogKeyMap() CommandsDialogKeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k CommandsDialogKeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.Select,
+		k.Next,
+		k.Previous,
+		k.Tab,
+		k.Close,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k CommandsDialogKeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])
@@ -86,10 +96,19 @@ func DefaultArgumentsDialogKeyMap() ArgumentsDialogKeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k ArgumentsDialogKeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.Confirm,
+		k.Next,
+		k.Previous,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k ArgumentsDialogKeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])

internal/tui/components/dialogs/compact/keys.go 🔗

@@ -2,7 +2,6 @@ package compact
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 // KeyMap defines the key bindings for the compact dialog.
@@ -40,10 +39,21 @@ func DefaultKeyMap() KeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.ChangeSelection,
+		k.Select,
+		k.Y,
+		k.N,
+		k.Close,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k KeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])

internal/tui/components/dialogs/filepicker/keys.go 🔗

@@ -2,7 +2,6 @@ package filepicker
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 // KeyMap defines keyboard bindings for dialog management.
@@ -45,10 +44,22 @@ func DefaultKeyMap() KeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.Select,
+		k.Down,
+		k.Up,
+		k.Forward,
+		k.Backward,
+		k.Close,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k KeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])

internal/tui/components/dialogs/init/keys.go 🔗

@@ -2,7 +2,6 @@ package init
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 type KeyMap struct {
@@ -38,10 +37,21 @@ func DefaultKeyMap() KeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.ChangeSelection,
+		k.Select,
+		k.Y,
+		k.N,
+		k.Close,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k KeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])

internal/tui/components/dialogs/keys.go 🔗

@@ -2,7 +2,6 @@ package dialogs
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 // KeyMap defines keyboard bindings for dialog management.
@@ -18,10 +17,17 @@ func DefaultKeyMap() KeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.Close,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k KeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])

internal/tui/components/dialogs/models/keys.go 🔗

@@ -2,7 +2,6 @@ package models
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 type KeyMap struct {
@@ -33,10 +32,20 @@ func DefaultKeyMap() KeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.Select,
+		k.Next,
+		k.Previous,
+		k.Close,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k KeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])

internal/tui/components/dialogs/permissions/keys.go 🔗

@@ -2,7 +2,6 @@ package permissions
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 type KeyMap struct {
@@ -48,10 +47,23 @@ func DefaultKeyMap() KeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.Left,
+		k.Right,
+		k.Tab,
+		k.Select,
+		k.Allow,
+		k.AllowSession,
+		k.Deny,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k KeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])

internal/tui/components/dialogs/quit/keys.go 🔗

@@ -2,7 +2,6 @@ package quit
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 // KeyMap defines the keyboard bindings for the quit dialog.
@@ -44,10 +43,22 @@ func DefaultKeymap() KeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.LeftRight,
+		k.EnterSpace,
+		k.Yes,
+		k.No,
+		k.Tab,
+		k.Close,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k KeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])

internal/tui/components/dialogs/sessions/keys.go 🔗

@@ -2,7 +2,6 @@ package sessions
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 type KeyMap struct {
@@ -33,10 +32,20 @@ func DefaultKeyMap() KeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.Select,
+		k.Next,
+		k.Previous,
+		k.Close,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k KeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])

internal/tui/page/logs/keys.go 🔗

@@ -2,7 +2,6 @@ package logs
 
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 )
 
 type KeyMap struct {
@@ -18,10 +17,17 @@ func DefaultKeyMap() KeyMap {
 	}
 }
 
+// KeyBindings implements layout.KeyMapProvider
+func (k KeyMap) KeyBindings() []key.Binding {
+	return []key.Binding{
+		k.Back,
+	}
+}
+
 // FullHelp implements help.KeyMap.
 func (k KeyMap) FullHelp() [][]key.Binding {
 	m := [][]key.Binding{}
-	slice := layout.KeyMapToSlice(k)
+	slice := k.KeyBindings()
 	for i := 0; i < len(slice); i += 4 {
 		end := min(i+4, len(slice))
 		m = append(m, slice[i:end])