feat: lots of IO fixes! Writing and forcing seems to be working.

This commit is contained in:
Hayden Hargreaves 2026-03-10 12:32:32 -07:00
parent c963d66e3b
commit 8364d8b880
14 changed files with 3439 additions and 56 deletions

View File

@ -11,7 +11,12 @@ import (
// main: Entry point for the Gim text editor. Creates a buffer and window,
// initializes the editor model, and runs the BubbleTea TUI program.
func main() {
// TODO: Read OS args and open file
// TODO: Need to implement the force bang handling!!! Opencode has this
buf := core.NewBufferBuilder().
ReadOnly().
Build()
win := core.NewWindowBuilder().
@ -34,6 +39,10 @@ func main() {
for _, win := range final.Windows() {
fmt.Printf("\t%+v\n", *win.Buffer)
}
fmt.Printf("PRINTING BUFFERS: %+v\n", final.Buffers())
fmt.Printf("PRINTING ACTIVE BUFFER: %+v\n", final.ActiveBuffer())
} else {
fmt.Printf("PRINTING ALL: %+v\n", m)
}

View File

@ -13,6 +13,7 @@ type Model interface {
Windows() []*core.Window
ActiveWindow() *core.Window
Buffers() []*core.Buffer
SetBuffers(bufs []*core.Buffer)
ActiveBuffer() *core.Buffer
// ==================================================

View File

@ -1,11 +1,16 @@
package command
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
@ -17,44 +22,199 @@ type ErrorMsg struct {
Err error
}
// --------------------------------------------------
// Quit Commands
// --------------------------------------------------
// cmdQuit: Handles :quit / :q command.
func cmdQuit(m action.Model, args []string) tea.Cmd {
return func() tea.Msg {
return tea.Quit()
func cmdQuit(m action.Model, args []string, force bool) tea.Cmd {
// :q! forces quit, ignoring unsaved changes
if force {
return tea.Quit
}
bufs := m.Buffers()
// Cannot exit if any buffer has unsaved changes
for _, buf := range bufs {
if buf.Modified {
m.SetCommandError(fmt.Errorf("unsaved changes to '%s'", buf.Filename))
m.ActiveWindow().SetBuffer(buf)
return nil
}
}
return tea.Quit
}
// cmdQuitAll: Handles :qall / :qa command.
func cmdQuitAll(m action.Model, args []string) tea.Cmd {
return func() tea.Msg {
return tea.Quit()
func cmdQuitAll(m action.Model, args []string, force bool) tea.Cmd {
// TODO: Until splits are implemented, this is the same as cmdQuit
return cmdQuit(m, args, force)
}
// --------------------------------------------------
// File Commands (write & edit)
// --------------------------------------------------
// cmdWrite: Handles :write / :w command
func cmdWrite(m action.Model, args []string, force bool) tea.Cmd {
buf := m.ActiveBuffer()
cmd, err := writeBuffer(m, buf, args, force)
if err != nil {
m.SetCommandError(err)
}
return cmd
}
// cmdWriteAll: Handles :wall / :wa command
func cmdWriteAll(m action.Model, args []string, force bool) tea.Cmd {
var cmds []tea.Cmd
bufs := m.Buffers()
for _, buf := range bufs {
if buf.Modified {
cmd, err := writeBuffer(m, buf, args, force)
if err != nil {
m.SetCommandError(err)
return nil
}
cmds = append(cmds, cmd)
}
}
// cmdWrite: Handles :write / :w command (TODO: implement file saving).
func cmdWrite(m action.Model, args []string) tea.Cmd {
// TODO: Implement file saving
// If args provided, save to that filename
// Otherwise save to current file
return tea.Batch(cmds...)
}
// cmdWriteQuit: Handles :wq command
func cmdWriteQuit(m action.Model, args []string, force bool) tea.Cmd {
buf := m.ActiveBuffer()
cmd, err := writeBuffer(m, buf, args, force)
if err != nil {
m.SetCommandError(err)
return cmd
}
return tea.Batch(cmd, tea.Quit)
}
// cmdWriteQuitAll: Handles :wqall / :wqa / :xa command.
// Writes all modified buffers then quits.
func cmdWriteQuitAll(m action.Model, args []string, force bool) tea.Cmd {
var cmds []tea.Cmd
bufs := m.Buffers()
for _, buf := range bufs {
if buf.Modified {
cmd, err := writeBuffer(m, buf, args, force)
if err != nil {
m.SetCommandError(err)
return nil
}
cmds = append(cmds, cmd)
}
}
cmds = append(cmds, tea.Quit)
return tea.Batch(cmds...)
}
// cmdEdit: Handles :edit / :e
func cmdEdit(m action.Model, args []string, force bool) tea.Cmd {
// must have arguments, cant edit nothing
if len(args) < 1 {
m.SetCommandError(fmt.Errorf(":edit requires an argument"))
return nil
}
// cmdWriteAll: Handles :wall / :wa command (TODO: implement saving all buffers).
func cmdWriteAll(m action.Model, args []string) tea.Cmd {
// TODO: Implement saving all buffers
// Vim's Approach:
// " When you do :edit filename.txt
// 1. Check if file exists and is readable (if not, open new buffer)
// 2. Detect file encoding (UTF-8, etc.)
// 3. Read entire file into memory
// 4. Split by line endings (respecting fileformat)
// 5. Create new buffer with these lines
// 6. Set buffer metadata:
// - buftype = "" (normal file)
// - modified = 0 (not modified)
// - fileformat = "unix" | "dos" | "mac"
// - fileencoding = "utf-8" (etc.)
filename := args[0]
ext := filepath.Ext(filename)
// If the buffer already exists, just switch to it.
bufs := m.Buffers()
for _, buf := range bufs {
if buf.Filename == filename {
m.ActiveWindow().SetBuffer(buf)
return nil
}
}
file, err := os.Open(filename)
notFound := errors.Is(err, os.ErrNotExist)
if err != nil && !notFound {
m.SetCommandError(err)
return nil
}
if file != nil {
defer file.Close()
}
// Create a buffer with the new file name, writing the file will
// handle the saving logic
if notFound {
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithFiletype(ext).
Listed().
Loaded().
Build()
m.SetBuffers(append(m.Buffers(), &buf))
m.ActiveWindow().SetBuffer(&buf)
// Need to adjust the cursor when we make a new file
m.ActiveWindow().ClampCursor()
return nil
}
// cmdWriteQuit: Handles :wq command (TODO: save then quit).
func cmdWriteQuit(m action.Model, args []string) tea.Cmd {
// TODO: Save then quit
return func() tea.Msg {
return tea.Quit()
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSuffix(line, "\r")
// BUG: This is bad, we don't want to this, but we have to
cleaned := strings.ReplaceAll(line, "\t", strings.Repeat(" ", m.Settings().TabStop))
lines = append(lines, cleaned)
}
buf := core.NewBufferBuilder().
WithType(core.FileBuffer).
WithFilename(filename).
WithFiletype(ext).
WithLines(lines).
Listed().
Loaded().
Build()
m.SetBuffers(append(m.Buffers(), &buf))
m.ActiveWindow().SetBuffer(&buf)
return nil
}
// --------------------------------------------------
// Register Commands
// --------------------------------------------------
// cmdRegisters: Handles :register command (debug - displays register content).
func cmdRegisters(m action.Model, args []string) tea.Cmd {
func cmdRegisters(m action.Model, args []string, force bool) tea.Cmd {
// TODO: This is temporary, for debugging
if len(args) < 1 {
m.SetCommandError(fmt.Errorf("Please provide a name. Register dump not yet implemented."))
@ -79,6 +239,10 @@ func cmdRegisters(m action.Model, args []string) tea.Cmd {
return nil
}
// --------------------------------------------------
// Settings Commands
// --------------------------------------------------
// cmdSet: Handles :set option[=value] command for configuring editor settings.
// Examples:
//
@ -87,7 +251,7 @@ func cmdRegisters(m action.Model, args []string) tea.Cmd {
// :set number! - toggle number
// :set tabstop=4 - set tabstop to 4
// :set ts=4 - set tabstop to 4 (abbreviation)
func cmdSet(m action.Model, args []string) tea.Cmd {
func cmdSet(m action.Model, args []string, force bool) tea.Cmd {
if len(args) == 0 {
out := fmt.Sprintf("%+v", m.Settings())
m.SetCommandOutput(out)

File diff suppressed because it is too large Load Diff

83
internal/command/io.go Normal file
View File

@ -0,0 +1,83 @@
package command
import (
"bufio"
"fmt"
"os"
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
tea "github.com/charmbracelet/bubbletea"
)
func writeBuffer(m action.Model, buf *core.Buffer, args []string, force bool) (tea.Cmd, error) {
// Key Steps:
// - Backup Creation (if backup is set): Copy original to .bak file
// - Line Ending Application: Apply fileformat setting (unix=\n, dos=\r\n, mac=\r)
// - Character Encoding: Convert from internal representation to fileencoding
// - Atomic Write: Write to temporary file first (.swp or similar)
// - Atomic Rename: Rename temp file to target filename (atomic operation)
// - Metadata Update: Clear modified flag, update timestamp
// Safety Mechanisms:
// " Vim's write safety
// 1. Check file permissions
// 2. If file exists and 'writebackup' set:
// - Create backup (.bak)
// 3. Write to temporary file (.swp~)
// 4. Verify write succeeded
// 5. Rename temp to target (atomic)
// 6. Remove backup if 'backup' not set
// 7. Update buffer metadata
// TODO: Implement atomic and safe writes
// Check readonly flag ONLY if not forced with !
if !force && buf.ReadOnly {
return nil, fmt.Errorf("cannot write to 'readonly' buffer")
}
if buf.Type == core.ScatchBuffer {
return nil, fmt.Errorf("cannot write to buffer of type 'ScratchBuffer'")
}
// Get the filename; differs by the type
var filename string
if len(args) > 0 {
filename = args[0]
} else {
if buf.Filename == "" {
return nil, fmt.Errorf("cannot write: no file name provided")
}
filename = buf.Filename
}
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
if err != nil {
return nil, err
}
defer file.Close()
// Same write operation regardless of where the file came from
var bytes int
// Using a bufio.Writer because its more efficient
writer := bufio.NewWriter(file)
for _, line := range buf.Lines {
n, err := writer.WriteString(line + "\n")
if err != nil {
return nil, err
}
bytes += n
}
if err := writer.Flush(); err != nil {
return nil, err
}
output := fmt.Sprintf("'%s', %dL %db written", filename, buf.LineCount(), bytes)
m.SetCommandOutput(output)
buf.SetModified(false)
return nil, nil
}

View File

@ -12,7 +12,7 @@ import (
type Command struct {
Name string // Full name: "quit"
ShortForm string // Minimum abbreviation: "q"
Handler func(m action.Model, args []string) tea.Cmd // Handler function
Handler func(m action.Model, args []string, force bool) tea.Cmd // Handler function
}
// Registry: Holds all registered commands.
@ -75,24 +75,34 @@ func (r *Registry) Lookup(input string) (*Command, error) {
}
// Parse: Splits a command line into command name and arguments.
func Parse(cmdLine string) (name string, args []string) {
func Parse(cmdLine string) (name string, args []string, force bool) {
parts := strings.Fields(cmdLine)
if len(parts) == 0 {
return "", nil
return "", nil, false
}
return parts[0], parts[1:]
name = parts[0]
args = parts[1:]
// Check if command ends with ! (force flag)
if strings.HasSuffix(name, "!") {
name = strings.TrimSuffix(name, "!")
force = true
}
return name, args, force
}
// Registry.Execute: Parses and executes a command line.
func (r *Registry) Execute(m action.Model, cmdLine string) (tea.Cmd, error) {
name, args := Parse(cmdLine)
name, args, force := Parse(cmdLine)
cmd, err := r.Lookup(name)
if err != nil {
return nil, err
}
return cmd.Handler(m, args), nil
return cmd.Handler(m, args, force), nil
}
// DefaultRegistry is the global command registry
@ -132,6 +142,12 @@ func (r *Registry) registerDefaults() {
Handler: cmdWriteQuit,
})
r.Register(Command{
Name: "wqall",
ShortForm: "wqa",
Handler: cmdWriteQuitAll,
})
// Set command
r.Register(Command{
Name: "set",
@ -145,4 +161,11 @@ func (r *Registry) registerDefaults() {
ShortForm: "reg",
Handler: cmdRegisters,
})
// File commands
r.Register(Command{
Name: "edit",
ShortForm: "e",
Handler: cmdEdit,
})
}

View File

@ -89,6 +89,26 @@ func TestRegistryLookup(t *testing.T) {
}
})
t.Run("e matches edit", func(t *testing.T) {
cmd, err := r.Lookup("e")
if err != nil {
t.Fatalf("Lookup error: %v", err)
}
if cmd.Name != "edit" {
t.Errorf("cmd.Name = %q, want \"edit\"", cmd.Name)
}
})
t.Run("ed matches edit", func(t *testing.T) {
cmd, err := r.Lookup("ed")
if err != nil {
t.Fatalf("Lookup error: %v", err)
}
if cmd.Name != "edit" {
t.Errorf("cmd.Name = %q, want \"edit\"", cmd.Name)
}
})
t.Run("unknown command returns error", func(t *testing.T) {
_, err := r.Lookup("xyz")
if err == nil {
@ -106,17 +126,20 @@ func TestRegistryLookup(t *testing.T) {
func TestParse(t *testing.T) {
t.Run("command only", func(t *testing.T) {
name, args := Parse("quit")
name, args, force := Parse("quit")
if name != "quit" {
t.Errorf("name = %q, want \"quit\"", name)
}
if len(args) != 0 {
t.Errorf("len(args) = %d, want 0", len(args))
}
if force {
t.Error("force should be false")
}
})
t.Run("command with one arg", func(t *testing.T) {
name, args := Parse("set number")
name, args, force := Parse("set number")
if name != "set" {
t.Errorf("name = %q, want \"set\"", name)
}
@ -126,10 +149,13 @@ func TestParse(t *testing.T) {
if args[0] != "number" {
t.Errorf("args[0] = %q, want \"number\"", args[0])
}
if force {
t.Error("force should be false")
}
})
t.Run("command with multiple args", func(t *testing.T) {
name, args := Parse("set number tabstop=4")
name, args, force := Parse("set number tabstop=4")
if name != "set" {
t.Errorf("name = %q, want \"set\"", name)
}
@ -142,26 +168,74 @@ func TestParse(t *testing.T) {
if args[1] != "tabstop=4" {
t.Errorf("args[1] = %q, want \"tabstop=4\"", args[1])
}
if force {
t.Error("force should be false")
}
})
t.Run("empty string", func(t *testing.T) {
name, args := Parse("")
name, args, force := Parse("")
if name != "" {
t.Errorf("name = %q, want \"\"", name)
}
if args != nil {
t.Errorf("args = %v, want nil", args)
}
if force {
t.Error("force should be false")
}
})
t.Run("whitespace only", func(t *testing.T) {
name, args := Parse(" ")
name, args, force := Parse(" ")
if name != "" {
t.Errorf("name = %q, want \"\"", name)
}
if args != nil {
t.Errorf("args = %v, want nil", args)
}
if force {
t.Error("force should be false")
}
})
t.Run("command with force flag", func(t *testing.T) {
name, args, force := Parse("quit!")
if name != "quit" {
t.Errorf("name = %q, want \"quit\"", name)
}
if len(args) != 0 {
t.Errorf("len(args) = %d, want 0", len(args))
}
if !force {
t.Error("force should be true")
}
})
t.Run("write command with force", func(t *testing.T) {
name, _, force := Parse("w!")
if name != "w" {
t.Errorf("name = %q, want \"w\"", name)
}
if !force {
t.Error("force should be true")
}
})
t.Run("command with force and args", func(t *testing.T) {
name, args, force := Parse("w! file.txt")
if name != "w" {
t.Errorf("name = %q, want \"w\"", name)
}
if len(args) != 1 {
t.Errorf("len(args) = %d, want 1", len(args))
}
if args[0] != "file.txt" {
t.Errorf("args[0] = %q, want \"file.txt\"", args[0])
}
if !force {
t.Error("force should be true")
}
})
}

View File

@ -4,9 +4,18 @@ type BufferOptions struct {
// tabstop expandtab
}
type BufferType int
const (
ScatchBuffer BufferType = iota
FileBuffer
DirectoryBuffer
)
type Buffer struct {
// Buffer data
Id int
Type BufferType
// File data
Filename string
@ -17,6 +26,7 @@ type Buffer struct {
Modified bool
Loaded bool
Listed bool
ReadOnly bool
// Options BufferOptions
// UndoTree TODO: This will be big
@ -36,16 +46,17 @@ func (b *Buffer) Line(idx int) string {
}
// Buffer.SetLine: Set the content of the line at an index. Does nothing if the
// index is out of bounds.
// index is out of bounds. This function sets the modified flag.
func (b *Buffer) SetLine(idx int, content string) {
if idx >= 0 && idx < len(b.Lines) {
b.Lines[idx] = content
}
b.Modified = true
}
// Buffer.InsertLine: Insert a line with content at an index. The index is clamped
// to valid bounds (0 to len(Lines)). The new line is inserted before the line at
// the given index.
// the given index. This function sets the modified flag.
func (b *Buffer) InsertLine(idx int, content string) {
if idx < 0 {
idx = 0
@ -54,14 +65,16 @@ func (b *Buffer) InsertLine(idx int, content string) {
idx = len(b.Lines)
}
b.Lines = append(b.Lines[:idx], append([]string{content}, b.Lines[idx:]...)...)
b.Modified = true
}
// Buffer.DeleteLine: Delete a line at an index. Does nothing if the index is out
// of bounds.
// of bounds. This function sets the modified flag.
func (b *Buffer) DeleteLine(idx int) {
if idx >= 0 && idx < len(b.Lines) {
b.Lines = append(b.Lines[:idx], b.Lines[idx+1:]...)
}
b.Modified = true
}
// Buffer.LineCount: Get the number of lines in the buffer.
@ -109,3 +122,9 @@ func (b *Buffer) SetLoaded(loaded bool) {
func (b *Buffer) SetListed(listed bool) {
b.Listed = listed
}
// Buffer.SetType: Set the buffers type. This type is used to determine handling
// of I/O functions.
func (b *Buffer) SetType(t BufferType) {
b.Type = t
}

View File

@ -13,12 +13,14 @@ func NewBufferBuilder() *BufferBuilder {
return &BufferBuilder{
buffer: Buffer{
Id: 0, // This is set when built
Type: ScatchBuffer, // Default buffer type
Filename: "",
Filetype: "",
Lines: []string{""},
Modified: false,
Loaded: false,
Listed: false,
ReadOnly: false,
},
}
}
@ -62,6 +64,20 @@ func (b *BufferBuilder) Listed() *BufferBuilder {
return b
}
// BufferBuilder.ReadOnly: Sets the readonly flag of the buffer being built. By default,
// buffers are built with the readonly flag being false.
func (b *BufferBuilder) ReadOnly() *BufferBuilder {
b.buffer.ReadOnly = true
return b
}
// BufferBuilder.Listed: Sets the type of the buffer being built. By default, buffers
// are build with the ScatchBuffer type.
func (b *BufferBuilder) WithType(t BufferType) *BufferBuilder {
b.buffer.Type = t
return b
}
// BufferBuilder.Build: Build the final buffer and return it to the caller. Final
// step in the process. This is where the ID is set, so many buffers can be "in-progress"
// but the ID will be set when they are built. Meaning, this is not thread safe.

View File

@ -40,8 +40,10 @@ type Window struct {
// 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
// the line. In the Y direction it validates that the cursor does not pass the end
// of the content or attempt to be "above" the content (negative value).
func (w *Window) clampCursor() {
// of the content or attempt to be "above" the content (negative value). This function
// is automatically called in any time the cursor changes. It only needs to be called
// when a force clamp is needed.
func (w *Window) ClampCursor() {
// Clamp line to valid range [0, lineCount-1]
maxLine := max(w.Buffer.LineCount()-1, 0)
if w.Cursor.Line < 0 {
@ -50,8 +52,15 @@ func (w *Window) clampCursor() {
w.Cursor.Line = maxLine
}
// Handle empty buffer - no lines to clamp column against
if w.Buffer.LineCount() == 0 {
w.Cursor.Line = 0
w.Cursor.Col = 0
return
}
// Clamp column to valid range [0, lineLen]
lineLen := len(w.Buffer.Lines[w.Cursor.Line]) // Safe now - Line is valid
lineLen := len(w.Buffer.Lines[w.Cursor.Line])
if w.Cursor.Col < 0 {
w.Cursor.Col = 0
} else if lineLen == 0 {
@ -111,27 +120,29 @@ func (w *Window) SetNumber(number int) {
}
// Window.SetBuffer: Sets the buffer that this window should display. This is used when
// switching between buffers or opening a new file in the current window.
// switching between buffers or opening a new file in the current window. This function
// does clamp the cursor to the current buffer
func (w *Window) SetBuffer(buffer *Buffer) {
w.Buffer = buffer
w.ClampCursor()
}
// Window.SetCursor: Sets the cursor position in this window to the given position.
func (w *Window) SetCursor(cursor Position) {
w.Cursor = cursor
w.clampCursor()
w.ClampCursor()
}
// Window.SetCursorLine: Sets the line number of the cursor position.
func (w *Window) SetCursorLine(line int) {
w.Cursor.Line = line
w.clampCursor()
w.ClampCursor()
}
// Window.SetCursorCol: Sets the column number of the cursor position.
func (w *Window) SetCursorCol(col int) {
w.Cursor.Col = col
w.clampCursor()
w.ClampCursor()
}
// Window.SetCursorPos: Sets both the line and column of the cursor position. This is
@ -139,7 +150,7 @@ func (w *Window) SetCursorCol(col int) {
func (w *Window) SetCursorPos(line, col int) {
w.Cursor.Line = line
w.Cursor.Col = col
w.clampCursor()
w.ClampCursor()
}
// Window.SetAnchor: Sets the anchor position in this window. The anchor is used for

View File

@ -38,6 +38,13 @@ func sendKeys(tm *teatest.TestModel, keys ...string) {
}
}
// sendKeyString is a convenience function for sending many keys.
func sendKeyString(tm *teatest.TestModel, keyString string) {
for _, key := range keyString {
sendKeys(tm, string(key))
}
}
// TestModelOption is a functional option for configuring test models
type TestModelOption func(*testModelConfig)

View File

@ -8,8 +8,6 @@ import (
// NOTE: AI Generated tests
// Default settings are: Number=true, RelativeNumber=true, TabSize=2, ScrollOff=8
func TestCommandSetBoolean(t *testing.T) {
t.Run("':set nonumber' disables line numbers", func(t *testing.T) {
// Default has Number=true
@ -339,3 +337,16 @@ func TestCommandModeErrors(t *testing.T) {
}
})
}
func TestCommandEdit(t *testing.T) {
t.Run(":edit with no args fails", func(t *testing.T) {
tm := newTestModel(t)
sendKeyString(tm, ":edit")
sendKeys(tm, "enter")
m := getFinalModel(t, tm)
if m.commandError == nil {
t.Error("expected commandError to be set for edit without args")
}
})
}

View File

@ -80,6 +80,10 @@ func (m *Model) Buffers() []*core.Buffer {
return m.buffers
}
func (m *Model) SetBuffers(bufs []*core.Buffer) {
m.buffers = bufs
}
func (m *Model) ActiveBuffer() *core.Buffer {
win := m.ActiveWindow()
return win.Buffer

View File

@ -199,7 +199,21 @@ func drawStatusBar(w *core.Window, mode core.Mode) string {
// leftBar: Returns the left side of the status bar showing the current mode.
func leftBar(w *core.Window, mode core.Mode) string {
buf := w.Buffer
return fmt.Sprintf(" %s %s", mode.ToString(), buf.Filename)
var flags []string
if buf.Modified {
flags = append(flags, "!")
}
if buf.ReadOnly {
flags = append(flags, "x")
}
var flagStr string
if len(flags) > 0 {
flagStr = "(" + strings.Join(flags, "") + ")"
}
return fmt.Sprintf(" %s %s %s", mode.ToString(), buf.Filename, flagStr)
}
// rightBar: Returns the right side of the status bar showing cursor position