wip: cleaned up the separation of concerns in the MWB model

This commit is contained in:
Hayden Hargreaves 2026-02-26 21:59:32 -07:00
parent ea4638d815
commit 770cbcceb7
4 changed files with 434 additions and 335 deletions

View File

@ -6,44 +6,29 @@ import (
// Model defines the interface for editor state that actions can modify // Model defines the interface for editor state that actions can modify
type Model interface { type Model interface {
// Text buffer // ==================================================
Lines() []string // Core Data Access
Line(idx int) string // ==================================================
SetLine(idx int, content string)
InsertLine(idx int, content string)
DeleteLine(idx int)
LineCount() int
// Cursor
CursorX() int
CursorY() int
SetCursorX(x int)
SetCursorY(y int)
ClampCursorX()
// Windows
Windows() []*Window Windows() []*Window
ActiveWindowId() int
ActiveWindow() *Window ActiveWindow() *Window
Buffers() []*Buffer
ActiveBuffer() *Buffer
// Window // ==================================================
ScrollY() int // Insert Mode State
SetScrollY(y int) // ==================================================
WinH() int
WinW() int
ViewPortH() int
// Anchor
AnchorX() int
AnchorY() int
SetAnchorX(x int)
SetAnchorY(y int)
// Insert
InsertKeys() []string InsertKeys() []string
SetInsertKeys(keys []string) SetInsertKeys(keys []string)
// Command mode // Insert recording (for count replay)
SetInsertRecording(count int, action Action)
// ExitInsertMode handles replay, cursor step-back, and mode transition on esc
ExitInsertMode()
// ==================================================
// Command Mode State
// ==================================================
Command() string Command() string
SetCommand(cmd string) SetCommand(cmd string)
CommandCursor() int CommandCursor() int
@ -53,25 +38,54 @@ type Model interface {
CommandOutput() string CommandOutput() string
SetCommandOutput(out string) SetCommandOutput(out string)
// Settings // ==================================================
// Editor-wide State
// ==================================================
Mode() Mode
SetMode(mode Mode)
Settings() Settings Settings() Settings
SetSettings(s Settings) SetSettings(s Settings)
// ==================================================
// Registers // Registers
// ==================================================
Registers() map[rune]Register Registers() map[rune]Register
GetRegister(name rune) (Register, bool) GetRegister(name rune) (Register, bool)
SetRegister(name rune, t RegisterType, cnt []string) error SetRegister(name rune, t RegisterType, cnt []string) error
UpdateDefaultRegister(t RegisterType, cnt []string) UpdateDefaultRegister(t RegisterType, cnt []string)
// Mode // ==================================================
Mode() Mode // Depreciated
SetMode(mode Mode) // ==================================================
// Text buffer
// Lines() []string
// Line(idx int) string
// SetLine(idx int, content string)
// InsertLine(idx int, content string)
// DeleteLine(idx int)
// LineCount() int
// Insert recording (for count replay) // Cursor
SetInsertRecording(count int, action Action) // CursorX() int
// CursorY() int
// SetCursorX(x int)
// SetCursorY(y int)
// ClampCursorX()
// Window
// ScrollY() int
// SetScrollY(y int)
// WinH() int
// WinW() int
// ViewPortH() int
//
// Anchor
// AnchorX() int
// AnchorY() int
// SetAnchorX(x int)
// SetAnchorY(y int)
// ExitInsertMode handles replay, cursor step-back, and mode transition on esc
ExitInsertMode()
} }
// Position represents a location in the buffer // Position represents a location in the buffer

View File

@ -5,6 +5,7 @@ type WinOptions struct {
// Number bool // Number bool
// Wrap bool // Wrap bool
// Relnumber bool // Relnumber bool
ScrollOff int
} }
type Window struct { type Window struct {
@ -12,33 +13,73 @@ type Window struct {
Number int // Ignored for now, will be used when splits come into play Number int // Ignored for now, will be used when splits come into play
Buffer *Buffer Buffer *Buffer
Cursor Position Cursor Position // DO NOT MODIFY DIRECTLY, USE SETTERS
Anchor Position Anchor Position
ScrollY int ScrollY int
Height int Height int
Width int Width int
// Folds // TODO // Folds TODO
// Options WinOptions Options WinOptions
} }
// ================================================== // ==================================================
// Helper methods // Helper methods
// ================================================== // ==================================================
// Window.ClampCursorX: Clamps the cursor in the X direction to ensure the cursor // Window.ClampCursor: Clamps the cursor in the all directions to ensure the cursor
// does not go into an invalid position. Such as negative values or past the end of // does not go into an invalid position. Such as negative values or past the end of
// the line. // the line. In the Y direction it validates that the cursor does not pass the end
func (w *Window) ClampCursorX() { // of the content or attempt to be "above" the content (negative value).
lineLen := len(w.Buffer.Lines[w.Cursor.Line]) func (w *Window) clampCursor() {
if lineLen == 0 { // Clamp line to valid range [0, lineCount-1]
maxLine := w.Buffer.LineCount() - 1
if maxLine < 0 {
maxLine = 0 // Empty buffer edge case
}
if w.Cursor.Line < 0 {
w.Cursor.Line = 0
} else if w.Cursor.Line > maxLine {
w.Cursor.Line = maxLine
}
// Clamp column to valid range [0, lineLen]
lineLen := len(w.Buffer.Lines[w.Cursor.Line]) // Safe now - Line is valid
if w.Cursor.Col < 0 {
w.Cursor.Col = 0
} else if lineLen == 0 {
w.Cursor.Col = 0 w.Cursor.Col = 0
} else if w.Cursor.Col >= lineLen { } else if w.Cursor.Col >= lineLen {
w.Cursor.Col = lineLen w.Cursor.Col = lineLen // Allow cursor after last char (insert mode)
} }
} }
// Window.AdjustScroll ensures the cursor stays within the height with scrollOff margins.
// Call this after any cursor movement.
func (w *Window) AdjustScroll() {
if w.Height <= 0 {
return
}
// Effective scrollOff (can't be more than half the viewport)
off := min(w.Options.ScrollOff, w.Height/2)
// Cursor too close to top — scroll up
if w.Cursor.Line < w.ScrollY+off {
w.ScrollY = w.Cursor.Line - off
}
// Cursor too close to bottom — scroll down
if w.Cursor.Line > w.ScrollY+w.Height-1-off {
w.ScrollY = w.Cursor.Line - w.Height + 1 + off
}
// Clamp scrollY to valid range
maxScroll := max(0, w.Buffer.LineCount()-w.Height)
w.ScrollY = max(0, min(w.ScrollY, maxScroll))
}
// ================================================== // ==================================================
// Setters // Setters
// ================================================== // ==================================================
@ -58,16 +99,19 @@ func (w *Window) SetBuffer(buffer *Buffer) {
// Window.SetCursor: Sets the cursor position in this window to the given position. // Window.SetCursor: Sets the cursor position in this window to the given position.
func (w *Window) SetCursor(cursor Position) { func (w *Window) SetCursor(cursor Position) {
w.Cursor = cursor w.Cursor = cursor
w.clampCursor()
} }
// Window.SetCursorLine: Sets the line number of the cursor position. // Window.SetCursorLine: Sets the line number of the cursor position.
func (w *Window) SetCursorLine(line int) { func (w *Window) SetCursorLine(line int) {
w.Cursor.Line = line w.Cursor.Line = line
w.clampCursor()
} }
// Window.SetCursorCol: Sets the column number of the cursor position. // Window.SetCursorCol: Sets the column number of the cursor position.
func (w *Window) SetCursorCol(col int) { func (w *Window) SetCursorCol(col int) {
w.Cursor.Col = col w.Cursor.Col = col
w.clampCursor()
} }
// Window.SetCursorPos: Sets both the line and column of the cursor position. This is // Window.SetCursorPos: Sets both the line and column of the cursor position. This is
@ -75,6 +119,7 @@ func (w *Window) SetCursorCol(col int) {
func (w *Window) SetCursorPos(line, col int) { func (w *Window) SetCursorPos(line, col int) {
w.Cursor.Line = line w.Cursor.Line = line
w.Cursor.Col = col w.Cursor.Col = col
w.clampCursor()
} }
// Window.SetAnchor: Sets the anchor position in this window. The anchor is used for // Window.SetAnchor: Sets the anchor position in this window. The anchor is used for

View File

@ -20,6 +20,9 @@ func NewWindowBuilder() *WindowBuilder {
ScrollY: 0, ScrollY: 0,
Height: 0, Height: 0,
Width: 0, Width: 0,
Options: WinOptions{
ScrollOff: 8, // 8 is default
},
}, },
} }
} }
@ -92,6 +95,13 @@ func (w *WindowBuilder) WithDimensions(width, height int) *WindowBuilder {
return w return w
} }
// WindowBuilder.WithOptions: Applies the options to the window that is being built.
// This is a convenience method for setting all options in one call.
func (w *WindowBuilder) WithOptions(options WinOptions) *WindowBuilder {
w.window.Options = options
return w
}
// WindowBuilder.Build: Build the final window and return it to the caller. Final // WindowBuilder.Build: Build the final window and return it to the caller. Final
// step in the process. This is where the ID is set, so many windows can be "in-progress" // step in the process. This is where the ID is set, so many windows can be "in-progress"
// but the ID will be set when they are built. Meaning, this is not thread safe. // but the ID will be set when they are built. Meaning, this is not thread safe.

View File

@ -77,84 +77,43 @@ func NewModel(lines []string, pos action.Position) *Model {
return &m return &m
} }
// Model.Init: Initialize the model and start any commands that may need to run. Required
// for the bubbletea architecture.
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
return nil return nil
} }
// Implement action.Model interface // Implement action.Model interface
func (m *Model) Lines() []string { // ==================================================
win := m.ActiveWindow() // Core Data Access
return win.Buffer.Lines // ==================================================
func (m *Model) Windows() []*action.Window {
return m.windows
} }
func (m *Model) Line(idx int) string { func (m *Model) ActiveWindow() *action.Window {
win := m.ActiveWindow() winId := m.activeWindowId
return win.Buffer.Line(idx) for i := range m.Windows() {
if m.windows[i].Id == winId {
return m.windows[i]
}
}
panic("Could not find window")
} }
func (m *Model) SetLine(idx int, content string) { func (m *Model) Buffers() []*action.Buffer {
win := m.ActiveWindow() return m.buffers
win.Buffer.SetLine(idx, content)
} }
func (m *Model) InsertLine(idx int, content string) { func (m *Model) ActiveBuffer() *action.Buffer {
win := m.ActiveWindow() win := m.ActiveWindow()
win.Buffer.InsertLine(idx, content) return win.Buffer
} }
func (m *Model) DeleteLine(idx int) { // ==================================================
win := m.ActiveWindow() // Insert Mode Methods
win.Buffer.DeleteLine(idx) // ==================================================
}
func (m *Model) LineCount() int {
win := m.ActiveWindow()
return win.Buffer.LineCount()
}
func (m *Model) CursorX() int {
win := m.ActiveWindow()
return win.Cursor.Col
}
func (m *Model) CursorY() int {
win := m.ActiveWindow()
return win.Cursor.Line
}
func (m *Model) SetCursorX(x int) {
win := m.ActiveWindow()
win.Cursor.Col = x
}
func (m *Model) SetCursorY(y int) {
win := m.ActiveWindow()
win.Cursor.Line = y
}
// Anchor methods
func (m *Model) AnchorX() int {
win := m.ActiveWindow()
return win.Anchor.Col
}
func (m *Model) AnchorY() int {
win := m.ActiveWindow()
return win.Anchor.Line
}
func (m *Model) SetAnchorX(x int) {
win := m.ActiveWindow()
win.Anchor.Col = x
}
func (m *Model) SetAnchorY(y int) {
win := m.ActiveWindow()
win.Anchor.Line = y
}
// Insert methods
func (m *Model) InsertKeys() []string { func (m *Model) InsertKeys() []string {
return m.insertKeys return m.insertKeys
} }
@ -163,7 +122,145 @@ func (m *Model) SetInsertKeys(keys []string) {
m.insertKeys = keys m.insertKeys = keys
} }
// Command mode func (m *Model) SetInsertRecording(count int, act action.Action) {
m.insertCount = count
m.insertKeys = []string{}
m.insertAction = act
}
func (m *Model) ExitInsertMode() {
win := m.ActiveWindow()
if m.insertCount > 1 {
m.replayInsert()
}
if win.Cursor.Col > 0 {
win.Cursor.Col--
}
m.mode = action.NormalMode
m.insertCount = 0
m.insertKeys = nil
}
func (m *Model) replayInsert() {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
// Replay (count - 1) more times
for i := 1; i < m.insertCount; i++ {
// For 'o' and 'O', we need to create a new line first
switch m.insertAction.(type) {
case action.OpenLineBelow:
pos := win.Cursor.Line
buf.InsertLine(pos+1, "")
win.SetCursorLine(pos + 1)
case action.OpenLineAbove:
pos := win.Cursor.Line
buf.InsertLine(pos, "")
// 'i' and 'a' don't need setup - just replay keys
}
// Replay each recorded keystroke
for _, key := range m.insertKeys {
m.processInsertKey(key)
}
}
}
// TODO: Fix this shitty shit shit shit
func (m *Model) processInsertKey(key string) {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
col := win.Cursor.Col
line := win.Cursor.Line
l := buf.Line(line)
switch key {
case "enter":
if col == len(l) {
buf.InsertLine(line+1, "")
} else {
buf.SetLine(line, l[:col])
buf.InsertLine(line+1, l[col:])
}
win.SetCursorLine(line + 1)
win.SetCursorCol(0)
case "backspace":
if col > 0 {
buf.SetLine(line, l[:col-1]+l[col:])
win.SetCursorCol(col - 1)
} else if line > 0 {
prevLine := buf.Line(line - 1)
newCol := len(prevLine)
buf.SetLine(line-1, prevLine+l)
buf.DeleteLine(line)
win.SetCursorLine(line - 1)
win.SetCursorCol(newCol)
}
case "delete":
if col == len(l) && line < buf.LineCount()-1 {
nextLine := buf.Line(line + 1)
buf.SetLine(line, l+nextLine)
buf.DeleteLine(line + 1)
} else if col < len(l) {
buf.SetLine(line, l[:col]+l[col+1:])
}
case "tab":
tabs := strings.Repeat(" ", m.Settings().TabSize)
if col < len(l) {
buf.SetLine(line, l[:col]+tabs+l[col:])
} else {
buf.SetLine(line, l+tabs)
}
win.SetCursorCol(col + len(tabs))
case "up":
if line > 0 {
win.SetCursorLine(line - 1)
win.ClampCursorX()
}
case "down":
if line+1 < buf.LineCount() {
win.SetCursorLine(line + 1)
win.ClampCursorX()
}
case "left":
if col > 0 {
win.SetCursorCol(col - 1)
} else if line > 0 {
prevLine := buf.Line(line - 1)
win.SetCursorCol(len(prevLine))
win.SetCursorLine(line - 1)
}
case "right":
if col < len(l) {
win.SetCursorCol(col + 1)
} else if line+1 < buf.LineCount() {
win.SetCursorCol(0)
win.SetCursorLine(line + 1)
}
default:
if col < len(l) {
buf.SetLine(line, l[:col]+key+l[col:])
} else {
buf.SetLine(line, l+key)
}
win.SetCursorCol(col + len(key))
}
}
// ==================================================
// Command Mode State
// ==================================================
func (m *Model) Command() string { func (m *Model) Command() string {
return m.command return m.command
} }
@ -202,7 +299,17 @@ func (m *Model) SetCommandOutput(out string) {
m.commandOutput = out m.commandOutput = out
} }
// Settings // ==================================================
// Editor-wide State
// ==================================================
func (m *Model) Mode() action.Mode {
return m.mode
}
func (m *Model) SetMode(mode action.Mode) {
m.mode = mode
}
func (m *Model) Settings() action.Settings { func (m *Model) Settings() action.Settings {
return m.settings return m.settings
} }
@ -211,7 +318,9 @@ func (m *Model) SetSettings(s action.Settings) {
m.settings = s m.settings = s
} }
// ==================================================
// Registers // Registers
// ==================================================
func (m *Model) Registers() map[rune]action.Register { func (m *Model) Registers() map[rune]action.Register {
return m.registers return m.registers
} }
@ -244,224 +353,145 @@ func (m *Model) UpdateDefaultRegister(t action.RegisterType, cnt []string) {
m.SetRegister('"', t, cnt) m.SetRegister('"', t, cnt)
} }
// Window // ==================================================
func (m *Model) ScrollY() int { // Depreciated
win := m.ActiveWindow() // ==================================================
return win.ScrollY // func (m *Model) Lines() []string {
} // win := m.ActiveWindow()
// return win.Buffer.Lines
func (m *Model) SetScrollY(y int) { // }
win := m.ActiveWindow() //
win.ScrollY = y // func (m *Model) Line(idx int) string {
} // win := m.ActiveWindow()
// return win.Buffer.Line(idx)
func (m *Model) WinH() int { // }
win := m.ActiveWindow() //
return win.Height // func (m *Model) SetLine(idx int, content string) {
} // win := m.ActiveWindow()
// win.Buffer.SetLine(idx, content)
func (m *Model) WinW() int { // }
win := m.ActiveWindow() //
return win.Width // func (m *Model) InsertLine(idx int, content string) {
} // win := m.ActiveWindow()
// win.Buffer.InsertLine(idx, content)
func (m *Model) ViewPortH() int { // }
win := m.ActiveWindow() //
return win.Height - 2 // func (m *Model) DeleteLine(idx int) {
} // win := m.ActiveWindow()
// win.Buffer.DeleteLine(idx)
func (m *Model) ClampCursorX() { // }
win := m.ActiveWindow() //
win.ClampCursorX() // func (m *Model) LineCount() int {
} // win := m.ActiveWindow()
// return win.Buffer.LineCount()
// AdjustScroll ensures the cursor stays within the viewport with scrollOff margins. // }
// Call this after any cursor movement. //
func (m *Model) AdjustScroll() { // func (m *Model) CursorX() int {
viewportHeight := m.ViewPortH() // win := m.ActiveWindow()
if viewportHeight <= 0 { // return win.Cursor.Col
return // }
} //
// func (m *Model) CursorY() int {
// Effective scrollOff (can't be more than half the viewport) // win := m.ActiveWindow()
off := min(m.Settings().ScrollOff, viewportHeight/2) // return win.Cursor.Line
// }
// Cursor too close to top — scroll up //
if m.CursorY() < m.ScrollY()+off { // func (m *Model) SetCursorX(x int) {
m.SetScrollY(m.CursorY() - off) // win := m.ActiveWindow()
} // win.Cursor.Col = x
// }
// Cursor too close to bottom — scroll down //
if m.CursorY() > m.ScrollY()+viewportHeight-1-off { // func (m *Model) SetCursorY(y int) {
m.SetScrollY(m.CursorY() - viewportHeight + 1 + off) // win := m.ActiveWindow()
} // win.Cursor.Line = y
// }
// Clamp scrollY to valid range //
maxScroll := max(0, m.LineCount()-viewportHeight) // // Anchor methods
m.SetScrollY(max(0, min(m.ScrollY(), maxScroll))) // func (m *Model) AnchorX() int {
} // win := m.ActiveWindow()
// return win.Anchor.Col
// Windows // }
func (m *Model) Windows() []*action.Window { //
return m.windows // func (m *Model) AnchorY() int {
} // win := m.ActiveWindow()
// return win.Anchor.Line
func (m *Model) ActiveWindowId() int { // }
return m.activeWindowId //
} // func (m *Model) SetAnchorX(x int) {
// win := m.ActiveWindow()
func (m *Model) ActiveWindow() *action.Window { // win.Anchor.Col = x
winId := m.ActiveWindowId() // }
for i := range m.Windows() { //
if m.windows[i].Id == winId { // func (m *Model) SetAnchorY(y int) {
return m.windows[i] // win := m.ActiveWindow()
} // win.Anchor.Line = y
} // }
panic("Could not find window") //
} // func (m *Model) GetCursorPosition() *action.Position {
// // Return a copy of the position
func (m *Model) Mode() action.Mode { // win := m.ActiveWindow()
return m.mode // pos := win.Cursor
} // return &pos
// }
func (m *Model) SetMode(mode action.Mode) { //
m.mode = mode // // Window
} // func (m *Model) ScrollY() int {
// win := m.ActiveWindow()
func (m *Model) SetInsertRecording(count int, act action.Action) { // return win.ScrollY
m.insertCount = count // }
m.insertKeys = []string{} //
m.insertAction = act // func (m *Model) SetScrollY(y int) {
} // win := m.ActiveWindow()
// win.ScrollY = y
func (m *Model) GetCursorPosition() *action.Position { // }
// Return a copy of the position //
win := m.ActiveWindow() // func (m *Model) WinH() int {
pos := win.Cursor // win := m.ActiveWindow()
return &pos // return win.Height
} // }
//
func (m *Model) replayInsert() { // func (m *Model) WinW() int {
win := m.ActiveWindow() // win := m.ActiveWindow()
// return win.Width
// Replay (count - 1) more times // }
for i := 1; i < m.insertCount; i++ { //
// For 'o' and 'O', we need to create a new line first // func (m *Model) ViewPortH() int {
switch m.insertAction.(type) { // win := m.ActiveWindow()
case action.OpenLineBelow: // return win.Height - 2
pos := win.Cursor.Line // }
win.Buffer.Lines = append(win.Buffer.Lines[:pos+1], append([]string{""}, win.Buffer.Lines[pos+1:]...)...) //
win.Cursor.Line++ // func (m *Model) ClampCursorX() {
win.Cursor.Col = 0 // win := m.ActiveWindow()
case action.OpenLineAbove: // win.ClampCursorX()
pos := win.Cursor.Line // }
win.Buffer.Lines = append(win.Buffer.Lines[:pos], append([]string{""}, win.Buffer.Lines[pos:]...)...) //
win.Cursor.Col = 0 // func (m *Model) ActiveWindowId() int {
// 'i' and 'a' don't need setup - just replay keys // return m.activeWindowId
} // }
//
// Replay each recorded keystroke // // TODO: MOVE THIS
for _, key := range m.insertKeys { // // AdjustScroll ensures the cursor stays within the viewport with scrollOff margins.
m.processInsertKey(key) // // Call this after any cursor movement.
} // func (m *Model) AdjustScroll() {
} // viewportHeight := m.ViewPortH()
} // if viewportHeight <= 0 {
// return
func (m *Model) ExitInsertMode() { // }
win := m.ActiveWindow() //
if m.insertCount > 1 { // // Effective scrollOff (can't be more than half the viewport)
m.replayInsert() // off := min(m.Settings().ScrollOff, viewportHeight/2)
} //
if win.Cursor.Col > 0 { // // Cursor too close to top — scroll up
win.Cursor.Col-- // if m.CursorY() < m.ScrollY()+off {
} // m.SetScrollY(m.CursorY() - off)
m.mode = action.NormalMode // }
m.insertCount = 0 //
m.insertKeys = nil // // Cursor too close to bottom — scroll down
} // if m.CursorY() > m.ScrollY()+viewportHeight-1-off {
// m.SetScrollY(m.CursorY() - viewportHeight + 1 + off)
func (m *Model) processInsertKey(key string) { // }
x := m.CursorX() //
y := m.CursorY() // // Clamp scrollY to valid range
l := m.Line(y) // maxScroll := max(0, m.LineCount()-viewportHeight)
// m.SetScrollY(max(0, min(m.ScrollY(), maxScroll)))
switch key { // }
case "enter":
if x == len(l) {
m.InsertLine(y+1, "")
} else {
m.SetLine(y, l[:x])
m.InsertLine(y+1, l[x:])
}
m.SetCursorY(y + 1)
m.SetCursorX(0)
case "backspace":
if x > 0 {
m.SetLine(y, l[:x-1]+l[x:])
m.SetCursorX(x - 1)
} else if y > 0 {
prevLine := m.Line(y - 1)
newX := len(prevLine)
m.SetLine(y-1, prevLine+l)
m.DeleteLine(y)
m.SetCursorY(y - 1)
m.SetCursorX(newX)
}
case "delete":
if x == len(l) && y < m.LineCount()-1 {
nextLine := m.Line(y + 1)
m.SetLine(y, l+nextLine)
m.DeleteLine(y + 1)
} else if x < len(l) {
m.SetLine(y, l[:x]+l[x+1:])
}
case "tab":
tabs := strings.Repeat(" ", m.Settings().TabSize)
if x < len(l) {
m.SetLine(y, l[:x]+tabs+l[x:])
} else {
m.SetLine(y, l+tabs)
}
m.SetCursorX(x + len(tabs))
case "up":
if y > 0 {
m.SetCursorY(y - 1)
m.ClampCursorX()
}
case "down":
if y+1 < m.LineCount() {
m.SetCursorY(y + 1)
m.ClampCursorX()
}
case "left":
if x > 0 {
m.SetCursorX(x - 1)
} else if y > 0 {
prevLine := m.Line(y - 1)
m.SetCursorX(len(prevLine))
m.SetCursorY(y - 1)
}
case "right":
if x < len(l) {
m.SetCursorX(x + 1)
} else if y+1 < m.LineCount() {
m.SetCursorX(0)
m.SetCursorY(y + 1)
}
default:
if x < len(l) {
m.SetLine(y, l[:x]+key+l[x:])
} else {
m.SetLine(y, l+key)
}
m.SetCursorX(x + len(key))
}
}