Compare commits

..

3 Commits

Author SHA1 Message Date
Hayden Hargreaves
6b93fb2f6c feat: implemented a better way to draw the bar 2026-02-13 16:39:30 -07:00
Hayden Hargreaves
d5f0f2413a feat: settings abstraction supports numbers 2026-02-13 16:10:43 -07:00
Hayden Hargreaves
be46cae73d fix: settings abstracted a bit 2026-02-13 15:56:41 -07:00
8 changed files with 139 additions and 86 deletions

View File

@ -17,10 +17,8 @@ func generateLines(n int) []string {
} }
func main() { func main() {
// lines := []string{"Hello world testing main.go", "line 2", "line 3", "line 4", "line 5"}
tea.NewProgram( tea.NewProgram(
editor.NewModel(generateLines(32), action.Position{Line: 0, Col: 0}), editor.NewModel(generateLines(64), action.Position{Line: 0, Col: 0}),
tea.WithAltScreen(), tea.WithAltScreen(),
).Run() ).Run()
} }

View File

@ -1,6 +1,8 @@
package action package action
import tea "github.com/charmbracelet/bubbletea" import (
tea "github.com/charmbracelet/bubbletea"
)
// Mode constants for editor mode // Mode constants for editor mode
type Mode int type Mode int
@ -14,6 +16,31 @@ const (
VisualBlockMode VisualBlockMode
) )
func (m Mode) ToString() string {
switch m {
case NormalMode:
return "NORMAL"
case InsertMode:
return "INSERT"
case CommandMode:
return "COMMAND"
case VisualMode:
return "VISUAL"
case VisualLineMode:
return "V-LINE"
case VisualBlockMode:
return "V-BLOCK"
default:
return "-----"
}
}
func (m Mode) IsVisualMode() bool {
return m == VisualMode ||
m == VisualLineMode ||
m == VisualBlockMode
}
// 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 // Text buffer
@ -46,13 +73,11 @@ type Model interface {
SetInsertKeys(keys []string) SetInsertKeys(keys []string)
// Settings // Settings
TabSize() int Settings() Settings
ScrollOff() int
// Mode // Mode
Mode() Mode Mode() Mode
SetMode(mode Mode) SetMode(mode Mode)
IsVisualMode() bool
// Insert recording (for count replay) // Insert recording (for count replay)
SetInsertRecording(count int, action Action) SetInsertRecording(count int, action Action)

View File

@ -204,7 +204,7 @@ type InsertTab struct{}
func (a InsertTab) Execute(m Model) tea.Cmd { func (a InsertTab) Execute(m Model) tea.Cmd {
x, y := m.CursorX(), m.CursorY() x, y := m.CursorX(), m.CursorY()
l := m.Line(y) l := m.Line(y)
tabs := strings.Repeat(" ", m.TabSize()) tabs := strings.Repeat(" ", m.Settings().TabSize)
if x < len(l) { if x < len(l) {
m.SetLine(y, l[:x]+tabs+l[x:]) m.SetLine(y, l[:x]+tabs+l[x:])
} else { } else {

View File

@ -0,0 +1,20 @@
package action
type Settings struct {
Number bool
RelativeNumber bool
GutterSize int
TabSize int
ScrollOff int
// TODO: Colors
}
func NewDefaultSettings() Settings {
return Settings{
Number: true,
RelativeNumber: true,
GutterSize: 5,
TabSize: 2,
ScrollOff: 8,
}
}

View File

@ -30,9 +30,7 @@ type Model struct {
insertAction action.Action insertAction action.Action
// Settings // Settings
gutterSize int settings action.Settings
tabSize int
scrollOff int
} }
func NewModel(lines []string, pos action.Position) Model { func NewModel(lines []string, pos action.Position) Model {
@ -42,14 +40,11 @@ func NewModel(lines []string, pos action.Position) Model {
x: pos.Col, x: pos.Col,
y: pos.Line, y: pos.Line,
}, },
scrollY: 0, scrollY: 0,
mode: action.NormalMode, mode: action.NormalMode,
command: "", command: "",
input: input.NewHandler(), input: input.NewHandler(),
settings: action.NewDefaultSettings(),
gutterSize: 5,
tabSize: 2,
scrollOff: 8,
} }
} }
@ -139,12 +134,8 @@ func (m *Model) SetInsertKeys(keys []string) {
} }
// Settings // Settings
func (m *Model) TabSize() int { func (m *Model) Settings() action.Settings {
return m.tabSize return m.settings
}
func (m *Model) ScrollOff() int {
return m.scrollOff
} }
// Window // Window
@ -174,7 +165,7 @@ func (m *Model) AdjustScroll() {
} }
// Effective scrollOff (can't be more than half the viewport) // Effective scrollOff (can't be more than half the viewport)
off := min(m.ScrollOff(), viewportHeight/2) off := min(m.Settings().ScrollOff, viewportHeight/2)
// Cursor too close to top — scroll up // Cursor too close to top — scroll up
if m.CursorY() < m.ScrollY()+off { if m.CursorY() < m.ScrollY()+off {
@ -199,12 +190,6 @@ func (m *Model) SetMode(mode action.Mode) {
m.mode = mode m.mode = mode
} }
func (m *Model) IsVisualMode() bool {
return m.mode == action.VisualMode ||
m.mode == action.VisualLineMode ||
m.mode == action.VisualBlockMode
}
func (m *Model) SetInsertRecording(count int, act action.Action) { func (m *Model) SetInsertRecording(count int, act action.Action) {
m.insertCount = count m.insertCount = count
m.insertKeys = []string{} m.insertKeys = []string{}
@ -290,7 +275,7 @@ func (m *Model) processInsertKey(key string) {
} }
case "tab": case "tab":
tabs := strings.Repeat(" ", m.tabSize) tabs := strings.Repeat(" ", m.Settings().TabSize)
if x < len(l) { if x < len(l) {
m.SetLine(y, l[:x]+tabs+l[x:]) m.SetLine(y, l[:x]+tabs+l[x:])
} else { } else {

View File

@ -36,7 +36,7 @@ func (m Model) gutterStyle(currentLine bool) lipgloss.Style {
fg = lipgloss.Color("#d69d00") fg = lipgloss.Color("#d69d00")
} }
return lipgloss.NewStyle(). return lipgloss.NewStyle().
Width(m.gutterSize). Width(m.Settings().GutterSize).
Background(bg). Background(bg).
Foreground(fg) Foreground(fg)
} }

View File

@ -67,27 +67,39 @@ func (m Model) View() string {
if i < m.LineCount() { if i < m.LineCount() {
var ( if m.Settings().Number || m.Settings().RelativeNumber {
gutter string var (
currentLine bool = false gutter string
lineNumber int currentLine bool = false
) lineNumber int
if i > m.CursorY() { )
lineNumber = i - m.CursorY()
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber) if m.Settings().RelativeNumber {
} else if i < m.CursorY() { // Relative line numbers: show distance from cursor, current line shows absolute
lineNumber = m.CursorY() - i if i > m.CursorY() {
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber) lineNumber = i - m.CursorY()
} else { gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
lineNumber = i + 1 } else if i < m.CursorY() {
currentLine = true lineNumber = m.CursorY() - i
if lineNumber < 100 { gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
gutter = fmt.Sprintf("%*d ", m.gutterSize-2, lineNumber) } else {
} else { // Current line: show absolute number if Number is also set, otherwise show 0
gutter = fmt.Sprintf("%*d ", m.gutterSize-1, lineNumber) currentLine = true
if m.Settings().Number {
lineNumber = i + 1
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
} else {
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, 0)
}
}
} else if m.Settings().Number {
// Absolute line numbers only
lineNumber = i + 1
currentLine = (i == m.CursorY())
gutter = fmt.Sprintf("%*d ", m.Settings().GutterSize-1, lineNumber)
} }
view.WriteString(m.gutterStyle(currentLine).Render(gutter))
} }
view.WriteString(m.gutterStyle(currentLine).Render(gutter))
runes := []rune(m.Line(i)) runes := []rune(m.Line(i))
for x := 0; x <= len(runes); x++ { for x := 0; x <= len(runes); x++ {
@ -98,54 +110,67 @@ func (m Model) View() string {
view.WriteString(m.cursorStyle().Render(" ")) view.WriteString(m.cursorStyle().Render(" "))
} }
} else if x < len(runes) { } else if x < len(runes) {
if m.IsVisualMode() && posIsAnchor(m, x, i) { if m.Mode().IsVisualMode() && posIsAnchor(m, x, i) {
view.WriteString(m.visualAnchorStyle().Render(string(runes[x]))) view.WriteString(m.visualAnchorStyle().Render(string(runes[x])))
} else if m.IsVisualMode() && posInsideSelection(m, x, i) { } else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) {
view.WriteString(m.visualHighlightStyle().Render(string(runes[x]))) view.WriteString(m.visualHighlightStyle().Render(string(runes[x])))
} else { } else {
view.WriteRune(runes[x]) view.WriteRune(runes[x])
} }
// To highlight blank lines when in visual mode // To highlight blank lines when in visual mode
} else if m.IsVisualMode() && posInsideSelection(m, x, i) { } else if m.Mode().IsVisualMode() && posInsideSelection(m, x, i) {
view.WriteString(m.visualHighlightStyle().Render(" ")) view.WriteString(m.visualHighlightStyle().Render(" "))
} }
} }
} else { } else {
format := fmt.Sprintf("%%-%ds ", m.gutterSize-1) // Empty lines beyond file content
fmt.Fprintf(&view, format, "~") if m.Settings().Number || m.Settings().RelativeNumber {
format := fmt.Sprintf("%%-%ds ", m.Settings().GutterSize-1)
fmt.Fprintf(&view, format, "~")
} else {
view.WriteString("~")
}
} }
view.WriteString("\n") view.WriteString("\n")
} }
// Draw status bar bar := drawStatusBar(m)
var modeString string
switch m.Mode() {
case action.NormalMode:
modeString = "NORMAL"
case action.InsertMode:
modeString = "INSERT"
case action.CommandMode:
modeString = "COMMAND"
case action.VisualMode:
modeString = "VISUAL"
case action.VisualLineMode:
modeString = "V-LINE"
case action.VisualBlockMode:
modeString = "V-BLOCK"
}
// DEBUG BAR! Def not the final bar
var bar string
if m.Mode() == action.CommandMode {
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) :%s ", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.command)
} else if m.IsVisualMode() {
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) <%d, %d> ", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.AnchorX(), m.AnchorY())
} else {
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) | %s | %+v | %d", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.input.Pending(), m.insertKeys, m.insertCount)
}
view.WriteString(bar) view.WriteString(bar)
return view.String() return view.String()
} }
func drawStatusBar(m Model) string {
left := leftBar(m)
right := rightBar(m)
diff := m.win_w - (len(left) + len(right))
// This happens when the terminal spawns
if diff <= 0 {
return ""
}
middle := strings.Repeat(" ", diff)
return left + middle + right
}
func leftBar(m Model) (bar string) {
if m.Mode() == action.CommandMode {
bar = fmt.Sprintf(":%s", m.command)
} else {
bar = fmt.Sprintf(" %s", m.Mode().ToString())
}
return
}
func rightBar(m Model) (bar string) {
if m.Mode().IsVisualMode() {
lineCount := max(m.AnchorY(), m.CursorY()) - min(m.AnchorY(), m.CursorY()) + 1
bar = fmt.Sprintf("%d:%d <%d>", m.CursorY(), m.CursorX(), lineCount)
} else {
bar = fmt.Sprintf("%d:%d ", m.CursorY(), m.CursorX())
}
return
}

View File

@ -139,7 +139,7 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
case "operator": case "operator":
op := binding.(action.Operator) op := binding.(action.Operator)
// In visual mode, the selection is already defined — operate immediately // In visual mode, the selection is already defined — operate immediately
if m.IsVisualMode() { if m.Mode().IsVisualMode() {
start, end := normalizeVisualSelection(m) start, end := normalizeVisualSelection(m)
// Visual line mode is linewise, others are charwise // Visual line mode is linewise, others are charwise
mtype := action.Charwise mtype := action.Charwise