feat(ui): simplify editor and embed into main UI model

Ayman Bagabas created

Change summary

internal/ui/model/editor.go | 235 ----------------------------------
internal/ui/model/keys.go   |  64 +++++++++
internal/ui/model/ui.go     | 267 ++++++++++++++++++++++++++++++++------
3 files changed, 285 insertions(+), 281 deletions(-)

Detailed changes

internal/ui/model/editor.go 🔗

@@ -1,235 +0,0 @@
-package model
-
-import (
-	"math/rand"
-
-	"charm.land/bubbles/v2/key"
-	"charm.land/bubbles/v2/textarea"
-	tea "charm.land/bubbletea/v2"
-	"github.com/charmbracelet/crush/internal/ui/common"
-)
-
-type EditorKeyMap struct {
-	AddFile     key.Binding
-	SendMessage key.Binding
-	OpenEditor  key.Binding
-	Newline     key.Binding
-
-	// Attachments key maps
-	AttachmentDeleteMode key.Binding
-	Escape               key.Binding
-	DeleteAllAttachments key.Binding
-}
-
-func DefaultEditorKeyMap() EditorKeyMap {
-	return EditorKeyMap{
-		AddFile: key.NewBinding(
-			key.WithKeys("/"),
-			key.WithHelp("/", "add file"),
-		),
-		SendMessage: key.NewBinding(
-			key.WithKeys("enter"),
-			key.WithHelp("enter", "send"),
-		),
-		OpenEditor: key.NewBinding(
-			key.WithKeys("ctrl+o"),
-			key.WithHelp("ctrl+o", "open editor"),
-		),
-		Newline: key.NewBinding(
-			key.WithKeys("shift+enter", "ctrl+j"),
-			// "ctrl+j" is a common keybinding for newline in many editors. If
-			// the terminal supports "shift+enter", we substitute the help text
-			// to reflect that.
-			key.WithHelp("ctrl+j", "newline"),
-		),
-		AttachmentDeleteMode: key.NewBinding(
-			key.WithKeys("ctrl+r"),
-			key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
-		),
-		Escape: key.NewBinding(
-			key.WithKeys("esc", "alt+esc"),
-			key.WithHelp("esc", "cancel delete mode"),
-		),
-		DeleteAllAttachments: key.NewBinding(
-			key.WithKeys("r"),
-			key.WithHelp("ctrl+r+r", "delete all attachments"),
-		),
-	}
-}
-
-// EditorModel represents the editor UI model.
-type EditorModel struct {
-	com *common.Common
-
-	keyMap   EditorKeyMap
-	textarea *textarea.Model
-
-	attachments []any // TODO: Implement attachments
-
-	readyPlaceholder   string
-	workingPlaceholder string
-}
-
-// NewEditorModel creates a new instance of EditorModel.
-func NewEditorModel(com *common.Common) *EditorModel {
-	ta := textarea.New()
-	ta.SetStyles(com.Styles.TextArea)
-	ta.ShowLineNumbers = false
-	ta.CharLimit = -1
-	ta.SetVirtualCursor(false)
-	ta.Focus()
-	e := &EditorModel{
-		com:      com,
-		keyMap:   DefaultEditorKeyMap(),
-		textarea: ta,
-	}
-
-	e.setEditorPrompt()
-	e.randomizePlaceholders()
-	e.textarea.Placeholder = e.readyPlaceholder
-
-	return e
-}
-
-// Init initializes the editor model.
-func (m *EditorModel) Init() tea.Cmd {
-	return nil
-}
-
-// Update handles updates to the editor model.
-func (m *EditorModel) Update(msg tea.Msg) (*EditorModel, tea.Cmd) {
-	var cmds []tea.Cmd
-	var cmd tea.Cmd
-
-	m.textarea, cmd = m.textarea.Update(msg)
-	cmds = append(cmds, cmd)
-
-	// Textarea placeholder logic
-	if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
-		m.textarea.Placeholder = m.workingPlaceholder
-	} else {
-		m.textarea.Placeholder = m.readyPlaceholder
-	}
-	if m.com.App.Permissions.SkipRequests() {
-		m.textarea.Placeholder = "Yolo mode!"
-	}
-
-	// TODO: Add attachments
-
-	return m, tea.Batch(cmds...)
-}
-
-// View renders the editor model.
-func (m *EditorModel) View() string {
-	return m.textarea.View()
-}
-
-// ShortHelp returns the short help view for the editor model.
-func (m *EditorModel) ShortHelp() []key.Binding {
-	k := m.keyMap
-	binds := []key.Binding{
-		k.AddFile,
-		k.SendMessage,
-		k.OpenEditor,
-		k.Newline,
-	}
-
-	if len(m.attachments) > 0 {
-		binds = append(binds,
-			k.AttachmentDeleteMode,
-			k.DeleteAllAttachments,
-			k.Escape,
-		)
-	}
-
-	return binds
-}
-
-// FullHelp returns the full help view for the editor model.
-func (m *EditorModel) FullHelp() [][]key.Binding {
-	return [][]key.Binding{
-		m.ShortHelp(),
-	}
-}
-
-// Cursor returns the relative cursor position of the editor.
-func (m *EditorModel) Cursor() *tea.Cursor {
-	return m.textarea.Cursor()
-}
-
-// Blur implements Container.
-func (m *EditorModel) Blur() tea.Cmd {
-	m.textarea.Blur()
-	return nil
-}
-
-// Focus implements Container.
-func (m *EditorModel) Focus() tea.Cmd {
-	return m.textarea.Focus()
-}
-
-// Focused returns whether the editor is focused.
-func (m *EditorModel) Focused() bool {
-	return m.textarea.Focused()
-}
-
-// SetSize sets the size of the editor.
-func (m *EditorModel) SetSize(width, height int) {
-	m.textarea.SetWidth(width)
-	m.textarea.SetHeight(height)
-}
-
-func (m *EditorModel) setEditorPrompt() {
-	if m.com.App.Permissions.SkipRequests() {
-		m.textarea.SetPromptFunc(4, m.yoloPromptFunc)
-		return
-	}
-	m.textarea.SetPromptFunc(4, m.normalPromptFunc)
-}
-
-func (m *EditorModel) normalPromptFunc(info textarea.PromptInfo) string {
-	t := m.com.Styles
-	if info.LineNumber == 0 {
-		return "  > "
-	}
-	if info.Focused {
-		return t.EditorPromptNormalFocused.Render()
-	}
-	return t.EditorPromptNormalBlurred.Render()
-}
-
-func (m *EditorModel) yoloPromptFunc(info textarea.PromptInfo) string {
-	t := m.com.Styles
-	if info.LineNumber == 0 {
-		if info.Focused {
-			return t.EditorPromptYoloIconFocused.Render()
-		} else {
-			return t.EditorPromptYoloIconBlurred.Render()
-		}
-	}
-	if info.Focused {
-		return t.EditorPromptYoloDotsFocused.Render()
-	}
-	return t.EditorPromptYoloDotsBlurred.Render()
-}
-
-var readyPlaceholders = [...]string{
-	"Ready!",
-	"Ready...",
-	"Ready?",
-	"Ready for instructions",
-}
-
-var workingPlaceholders = [...]string{
-	"Working!",
-	"Working...",
-	"Brrrrr...",
-	"Prrrrrrrr...",
-	"Processing...",
-	"Thinking...",
-}
-
-func (m *EditorModel) randomizePlaceholders() {
-	m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))]
-	m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))]
-}

internal/ui/model/keys.go 🔗

@@ -3,16 +3,32 @@ package model
 import "charm.land/bubbles/v2/key"
 
 type KeyMap struct {
+	Editor struct {
+		AddFile     key.Binding
+		SendMessage key.Binding
+		OpenEditor  key.Binding
+		Newline     key.Binding
+		AddImage    key.Binding
+		MentionFile key.Binding
+
+		// Attachments key maps
+		AttachmentDeleteMode key.Binding
+		Escape               key.Binding
+		DeleteAllAttachments key.Binding
+	}
+
+	// Global key maps
 	Quit     key.Binding
 	Help     key.Binding
 	Commands key.Binding
+	Models   key.Binding
 	Suspend  key.Binding
 	Sessions key.Binding
 	Tab      key.Binding
 }
 
 func DefaultKeyMap() KeyMap {
-	return KeyMap{
+	km := KeyMap{
 		Quit: key.NewBinding(
 			key.WithKeys("ctrl+c"),
 			key.WithHelp("ctrl+c", "quit"),
@@ -25,6 +41,10 @@ func DefaultKeyMap() KeyMap {
 			key.WithKeys("ctrl+p"),
 			key.WithHelp("ctrl+p", "commands"),
 		),
+		Models: key.NewBinding(
+			key.WithKeys("ctrl+m", "ctrl+l"),
+			key.WithHelp("ctrl+l", "models"),
+		),
 		Suspend: key.NewBinding(
 			key.WithKeys("ctrl+z"),
 			key.WithHelp("ctrl+z", "suspend"),
@@ -38,4 +58,46 @@ func DefaultKeyMap() KeyMap {
 			key.WithHelp("tab", "change focus"),
 		),
 	}
+
+	km.Editor.AddFile = key.NewBinding(
+		key.WithKeys("/"),
+		key.WithHelp("/", "add file"),
+	)
+	km.Editor.SendMessage = key.NewBinding(
+		key.WithKeys("enter"),
+		key.WithHelp("enter", "send"),
+	)
+	km.Editor.OpenEditor = key.NewBinding(
+		key.WithKeys("ctrl+o"),
+		key.WithHelp("ctrl+o", "open editor"),
+	)
+	km.Editor.Newline = key.NewBinding(
+		key.WithKeys("shift+enter", "ctrl+j"),
+		// "ctrl+j" is a common keybinding for newline in many editors. If
+		// the terminal supports "shift+enter", we substitute the help tex
+		// to reflect that.
+		key.WithHelp("ctrl+j", "newline"),
+	)
+	km.Editor.AddImage = key.NewBinding(
+		key.WithKeys("ctrl+f"),
+		key.WithHelp("ctrl+f", "add image"),
+	)
+	km.Editor.MentionFile = key.NewBinding(
+		key.WithKeys("@"),
+		key.WithHelp("@", "mention file"),
+	)
+	km.Editor.AttachmentDeleteMode = key.NewBinding(
+		key.WithKeys("ctrl+r"),
+		key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
+	)
+	km.Editor.Escape = key.NewBinding(
+		key.WithKeys("esc", "alt+esc"),
+		key.WithHelp("esc", "cancel delete mode"),
+	)
+	km.Editor.DeleteAllAttachments = key.NewBinding(
+		key.WithKeys("r"),
+		key.WithHelp("ctrl+r+r", "delete all attachments"),
+	)
+
+	return km
 }

internal/ui/model/ui.go 🔗

@@ -8,8 +8,10 @@ import (
 
 	"charm.land/bubbles/v2/help"
 	"charm.land/bubbles/v2/key"
+	"charm.land/bubbles/v2/textarea"
 	tea "charm.land/bubbletea/v2"
 	"charm.land/lipgloss/v2"
+	"github.com/charmbracelet/crush/internal/session"
 	"github.com/charmbracelet/crush/internal/ui/common"
 	"github.com/charmbracelet/crush/internal/ui/dialog"
 	uv "github.com/charmbracelet/ultraviolet"
@@ -20,20 +22,21 @@ type uiState uint8
 
 // Possible uiState values.
 const (
-	uiChat uiState = iota
-	uiEdit
+	uiEdit uiState = iota
+	uiChat
 )
 
 // UI represents the main user interface model.
 type UI struct {
-	com *common.Common
+	com  *common.Common
+	sess *session.Session
 
 	state uiState
 
 	keyMap KeyMap
+	keyenh tea.KeyboardEnhancementsMsg
 
 	chat   *ChatModel
-	editor *EditorModel
 	side   *SidebarModel
 	dialog *dialog.Overlay
 	help   help.Model
@@ -47,18 +50,40 @@ type UI struct {
 	// QueryVersion instructs the TUI to query for the terminal version when it
 	// starts.
 	QueryVersion bool
+
+	// Editor components
+	textarea textarea.Model
+
+	attachments []any // TODO: Implement attachments
+
+	readyPlaceholder   string
+	workingPlaceholder string
 }
 
 // New creates a new instance of the [UI] model.
 func New(com *common.Common) *UI {
-	return &UI{
-		com:    com,
-		dialog: dialog.NewOverlay(),
-		keyMap: DefaultKeyMap(),
-		editor: NewEditorModel(com),
-		side:   NewSidebarModel(com),
-		help:   help.New(),
+	// Editor components
+	ta := textarea.New()
+	ta.SetStyles(com.Styles.TextArea)
+	ta.ShowLineNumbers = false
+	ta.CharLimit = -1
+	ta.SetVirtualCursor(false)
+	ta.Focus()
+
+	ui := &UI{
+		com:      com,
+		dialog:   dialog.NewOverlay(),
+		keyMap:   DefaultKeyMap(),
+		side:     NewSidebarModel(com),
+		help:     help.New(),
+		textarea: ta,
 	}
+
+	ui.setEditorPrompt()
+	ui.randomizePlaceholders()
+	ui.textarea.Placeholder = ui.readyPlaceholder
+
+	return ui
 }
 
 // Init initializes the UI model.
@@ -73,6 +98,7 @@ func (m *UI) Init() tea.Cmd {
 // Update handles updates to the UI model.
 func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
+	hasDialogs := m.dialog.HasDialogs()
 	switch msg := msg.(type) {
 	case tea.EnvMsg:
 		// Is this Windows Terminal?
@@ -88,18 +114,30 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		return m, nil
 	case tea.WindowSizeMsg:
 		m.updateLayoutAndSize(msg.Width, msg.Height)
+	case tea.KeyboardEnhancementsMsg:
+		m.keyenh = msg
+		if msg.SupportsKeyDisambiguation() {
+			m.keyMap.Models.SetHelp("ctrl+m", "models")
+			m.keyMap.Editor.Newline.SetHelp("shift+enter", "newline")
+		}
 	case tea.KeyPressMsg:
-		if m.dialog.HasDialogs() {
+		if hasDialogs {
 			m.updateDialogs(msg, &cmds)
-		} else {
+		}
+	}
+
+	if !hasDialogs {
+		// This branch only handles UI elements when there's no dialog shown.
+		switch msg := msg.(type) {
+		case tea.KeyPressMsg:
 			switch {
 			case key.Matches(msg, m.keyMap.Tab):
 				if m.state == uiChat {
 					m.state = uiEdit
-					cmds = append(cmds, m.editor.Focus())
+					cmds = append(cmds, m.textarea.Focus())
 				} else {
 					m.state = uiChat
-					cmds = append(cmds, m.editor.Blur())
+					m.textarea.Blur()
 				}
 			case key.Matches(msg, m.keyMap.Help):
 				m.help.ShowAll = !m.help.ShowAll
@@ -109,10 +147,31 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					m.dialog.AddDialog(dialog.NewQuit(m.com))
 					return m, nil
 				}
+			case key.Matches(msg, m.keyMap.Commands):
+				// TODO: Implement me
+			case key.Matches(msg, m.keyMap.Models):
+				// TODO: Implement me
+			case key.Matches(msg, m.keyMap.Sessions):
+				// TODO: Implement me
 			default:
 				m.updateFocused(msg, &cmds)
 			}
 		}
+
+		// This logic gets triggered on any message type, but should it?
+		switch m.state {
+		case uiChat:
+		case uiEdit:
+			// Textarea placeholder logic
+			if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
+				m.textarea.Placeholder = m.workingPlaceholder
+			} else {
+				m.textarea.Placeholder = m.readyPlaceholder
+			}
+			if m.com.App.Permissions.SkipRequests() {
+				m.textarea.Placeholder = "Yolo mode!"
+			}
+		}
 	}
 
 	return m, tea.Batch(cmds...)
@@ -126,7 +185,7 @@ func (m *UI) View() tea.View {
 	layers := []*lipgloss.Layer{}
 
 	// Determine the help key map based on focus
-	helpKeyMap := m.focusedKeyMap()
+	var helpKeyMap help.KeyMap = m
 
 	// The screen areas we're working with
 	area := m.layout.area
@@ -137,11 +196,6 @@ func (m *UI) View() tea.View {
 
 	if m.dialog.HasDialogs() {
 		if dialogView := m.dialog.View(); dialogView != "" {
-			// If the dialog has its own help, use that instead
-			if len(m.dialog.FullHelp()) > 0 || len(m.dialog.ShortHelp()) > 0 {
-				helpKeyMap = m.dialog
-			}
-
 			dialogWidth, dialogHeight := lipgloss.Width(dialogView), lipgloss.Height(dialogView)
 			dialogArea := common.CenterRect(area, dialogWidth, dialogHeight)
 			layers = append(layers,
@@ -153,8 +207,8 @@ func (m *UI) View() tea.View {
 		}
 	}
 
-	if m.state == uiEdit && m.editor.Focused() {
-		cur := m.editor.Cursor()
+	if m.state == uiEdit && m.textarea.Focused() {
+		cur := m.textarea.Cursor()
 		cur.X++ // Adjust for app margins
 		cur.Y += editRect.Min.Y
 		v.Cursor = cur
@@ -171,7 +225,7 @@ func (m *UI) View() tea.View {
 			).X(chatRect.Min.X).Y(chatRect.Min.Y),
 			lipgloss.NewLayer(m.side.View()).
 				X(sideRect.Min.X).Y(sideRect.Min.Y),
-			lipgloss.NewLayer(m.editor.View()).
+			lipgloss.NewLayer(m.textarea.View()).
 				X(editRect.Min.X).Y(editRect.Min.Y),
 			lipgloss.NewLayer(m.help.View(helpKeyMap)).
 				X(helpRect.Min.X).Y(helpRect.Min.Y),
@@ -189,11 +243,82 @@ func (m *UI) View() tea.View {
 	return v
 }
 
-func (m *UI) focusedKeyMap() help.KeyMap {
-	if m.state == uiChat {
-		return m.chat
+// ShortHelp implements [help.KeyMap].
+func (m *UI) ShortHelp() []key.Binding {
+	var binds []key.Binding
+	k := &m.keyMap
+
+	if m.sess == nil {
+		// no session selected
+		binds = append(binds,
+			k.Commands,
+			k.Models,
+			k.Editor.Newline,
+			k.Quit,
+			k.Help,
+		)
+	} else {
+		// we have a session
 	}
-	return m.editor
+
+	// switch m.state {
+	// case uiChat:
+	// case uiEdit:
+	// 	binds = append(binds,
+	// 		k.Editor.AddFile,
+	// 		k.Editor.SendMessage,
+	// 		k.Editor.OpenEditor,
+	// 		k.Editor.Newline,
+	// 	)
+	//
+	// 	if len(m.attachments) > 0 {
+	// 		binds = append(binds,
+	// 			k.Editor.AttachmentDeleteMode,
+	// 			k.Editor.DeleteAllAttachments,
+	// 			k.Editor.Escape,
+	// 		)
+	// 	}
+	// }
+
+	return binds
+}
+
+// FullHelp implements [help.KeyMap].
+func (m *UI) FullHelp() [][]key.Binding {
+	var binds [][]key.Binding
+	k := &m.keyMap
+	help := k.Help
+	help.SetHelp("ctrl+g", "less")
+
+	if m.sess == nil {
+		// no session selected
+		binds = append(binds,
+			[]key.Binding{
+				k.Commands,
+				k.Models,
+				k.Sessions,
+			},
+			[]key.Binding{
+				k.Editor.Newline,
+				k.Editor.AddImage,
+				k.Editor.MentionFile,
+				k.Editor.OpenEditor,
+			},
+			[]key.Binding{
+				help,
+			},
+		)
+	} else {
+		// we have a session
+	}
+
+	// switch m.state {
+	// case uiChat:
+	// case uiEdit:
+	// 	binds = append(binds, m.ShortHelp())
+	// }
+
+	return binds
 }
 
 // updateDialogs updates the dialog overlay with the given message and appends
@@ -213,7 +338,16 @@ func (m *UI) updateFocused(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
 	case uiChat:
 		m.updateChat(msg, cmds)
 	case uiEdit:
-		m.updateEditor(msg, cmds)
+		switch {
+		case key.Matches(msg, m.keyMap.Editor.Newline):
+			m.textarea.InsertRune('\n')
+		}
+
+		ta, cmd := m.textarea.Update(msg)
+		m.textarea = ta
+		if cmd != nil {
+			*cmds = append(*cmds, cmd)
+		}
 	}
 }
 
@@ -227,26 +361,13 @@ func (m *UI) updateChat(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
 	}
 }
 
-// updateEditor updates the editor model with the given message and appends any
-// resulting commands to the cmds slice.
-func (m *UI) updateEditor(msg tea.KeyPressMsg, cmds *[]tea.Cmd) {
-	updatedEditor, cmd := m.editor.Update(msg)
-	m.editor = updatedEditor
-	if cmd != nil {
-		*cmds = append(*cmds, cmd)
-	}
-}
-
 // updateLayoutAndSize updates the layout and sub-models sizes based on the
 // given terminal width and height given in cells.
 func (m *UI) updateLayoutAndSize(w, h int) {
 	// The screen area we're working with
 	area := image.Rect(0, 0, w, h)
-	helpKeyMap := m.focusedKeyMap()
+	var helpKeyMap help.KeyMap = m
 	helpHeight := 1
-	if m.dialog.HasDialogs() && len(m.dialog.FullHelp()) > 0 && len(m.dialog.ShortHelp()) > 0 {
-		helpKeyMap = m.dialog
-	}
 	if m.help.ShowAll {
 		for _, row := range helpKeyMap.FullHelp() {
 			helpHeight = max(helpHeight, len(row))
@@ -280,8 +401,9 @@ func (m *UI) updateLayoutAndSize(w, h int) {
 
 	// Update sub-model sizes
 	m.side.SetWidth(m.layout.sidebar.Dx())
-	m.editor.SetSize(m.layout.editor.Dx(), m.layout.editor.Dy())
-	m.help.Width = m.layout.help.Dx()
+	m.textarea.SetWidth(m.layout.editor.Dx())
+	m.textarea.SetHeight(m.layout.editor.Dy())
+	m.help.SetWidth(m.layout.help.Dx())
 }
 
 // layout defines the positioning of UI elements.
@@ -304,3 +426,58 @@ type layout struct {
 	// help is the area for the help view.
 	help uv.Rectangle
 }
+
+func (m *UI) setEditorPrompt() {
+	if m.com.App.Permissions.SkipRequests() {
+		m.textarea.SetPromptFunc(4, m.yoloPromptFunc)
+		return
+	}
+	m.textarea.SetPromptFunc(4, m.normalPromptFunc)
+}
+
+func (m *UI) normalPromptFunc(info textarea.PromptInfo) string {
+	t := m.com.Styles
+	if info.LineNumber == 0 {
+		return "  > "
+	}
+	if info.Focused {
+		return t.EditorPromptNormalFocused.Render()
+	}
+	return t.EditorPromptNormalBlurred.Render()
+}
+
+func (m *UI) yoloPromptFunc(info textarea.PromptInfo) string {
+	t := m.com.Styles
+	if info.LineNumber == 0 {
+		if info.Focused {
+			return t.EditorPromptYoloIconFocused.Render()
+		} else {
+			return t.EditorPromptYoloIconBlurred.Render()
+		}
+	}
+	if info.Focused {
+		return t.EditorPromptYoloDotsFocused.Render()
+	}
+	return t.EditorPromptYoloDotsBlurred.Render()
+}
+
+var readyPlaceholders = [...]string{
+	"Ready!",
+	"Ready...",
+	"Ready?",
+	"Ready for instructions",
+}
+
+var workingPlaceholders = [...]string{
+	"Working!",
+	"Working...",
+	"Brrrrr...",
+	"Prrrrrrrr...",
+	"Processing...",
+	"Thinking...",
+}
+
+func (m *UI) randomizePlaceholders() {
+	m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))]
+	m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))]
+}