package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"log"
|
|
"os"
|
|
|
|
"fmt"
|
|
"led/utils"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Editor is the initializing class
|
|
type Editor struct {
|
|
}
|
|
|
|
// Buffer is the temporary view presented to the user
|
|
type Buffer struct {
|
|
Lines []string
|
|
}
|
|
|
|
// Screen is the rendering of the entire viewport
|
|
type Screen struct {
|
|
}
|
|
|
|
// Modeline is the message line at the bottom of the viewport
|
|
type Modeline struct {
|
|
message string
|
|
}
|
|
|
|
// Statusline is the file path line at the bottom of the viewport
|
|
type Statusline struct {
|
|
message string
|
|
fileSize string
|
|
filePath string
|
|
location string
|
|
fileFormat string
|
|
format string
|
|
unformat string
|
|
}
|
|
|
|
// Cursor is the object containing cursor point position
|
|
type Cursor struct {
|
|
Row int
|
|
Col int
|
|
}
|
|
|
|
type messageColor struct {
|
|
foreground int
|
|
background int
|
|
}
|
|
|
|
var arrow = map[string][]byte{
|
|
"up": []byte{0x1b, 0x5b, 0x41},
|
|
"down": []byte{0x1b, 0x5b, 0x42},
|
|
"right": []byte{0x1b, 0x5b, 0x43},
|
|
"left": []byte{0x1b, 0x5b, 0x44},
|
|
}
|
|
var ctrl = map[string][]byte{
|
|
"a": []byte{0x1}, // C-a
|
|
"b": []byte{0x2},
|
|
"c": []byte{0x3},
|
|
"d": []byte{0x4},
|
|
"e": []byte{0x5},
|
|
"f": []byte{0x6},
|
|
"g": []byte{0x7},
|
|
"h": []byte{0x8},
|
|
"i": []byte{0x9},
|
|
"j": []byte{0xa},
|
|
"k": []byte{0xb},
|
|
"l": []byte{0xc},
|
|
"m": []byte{0xd},
|
|
"n": []byte{0xe},
|
|
"o": []byte{0xf},
|
|
"p": []byte{0x10},
|
|
"q": []byte{0x11},
|
|
"r": []byte{0x12},
|
|
"s": []byte{0x13},
|
|
"t": []byte{0x14},
|
|
"u": []byte{0x15},
|
|
"v": []byte{0x16},
|
|
"w": []byte{0x17},
|
|
"x": []byte{0x18},
|
|
"y": []byte{0x19},
|
|
"z": []byte{0x1a},
|
|
}
|
|
var meta = map[string][]byte{
|
|
"x": []byte{0x1b, 0x78},
|
|
}
|
|
|
|
var filePath string
|
|
var editor Editor
|
|
var buffer Buffer
|
|
var screen Screen
|
|
var modeline Modeline
|
|
var statusline Statusline
|
|
var cursor Cursor
|
|
var multiplier int
|
|
var prefix []byte
|
|
var fileBytes int
|
|
|
|
func main() {
|
|
if len(os.Args) == 1 {
|
|
fmt.Println("You MUST supply a file to edit")
|
|
return
|
|
}
|
|
filePath = os.Args[1]
|
|
editor.initialize()
|
|
editor.run()
|
|
}
|
|
|
|
func (e *Editor) initialize() {
|
|
// Load a file
|
|
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer file.Close()
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
multiplier = 1
|
|
statusline.setFormat()
|
|
buffer = buffer.new(lines)
|
|
cursor = cursor.new()
|
|
}
|
|
|
|
func (e *Editor) run() {
|
|
for {
|
|
e.render()
|
|
e.handleInput()
|
|
}
|
|
}
|
|
|
|
func (e *Editor) render() {
|
|
clearScreen()
|
|
screen.render()
|
|
moveCursor(cursor.Row, cursor.Col) // restore cursor
|
|
}
|
|
|
|
func (e *Editor) handleInput() {
|
|
c := utils.Getch()
|
|
// log.Printf("%#v\t%s\n", c, string(c))
|
|
modeline.setMessage("")
|
|
switch {
|
|
case bytes.Equal(c, ctrl["x"]):
|
|
reset()
|
|
prefix = ctrl["x"]
|
|
modeline.setMessage("C-x-")
|
|
case bytes.Equal(c, ctrl["c"]):
|
|
if bytes.Equal(prefix, ctrl["x"]) {
|
|
clearScreen()
|
|
moveCursor(1, 1)
|
|
os.Exit(0)
|
|
return
|
|
}
|
|
modeline.setMessage("Invalid command!")
|
|
reset()
|
|
case bytes.Equal(c, ctrl["s"]):
|
|
if bytes.Equal(prefix, ctrl["x"]) {
|
|
buffer.save()
|
|
statusline.setFormat()
|
|
modeline.setMessage(fmt.Sprintf("Saved \"%s\"", filePath))
|
|
return
|
|
}
|
|
modeline.setMessage("Invalid command, did you mean to save -> C-x C-s")
|
|
reset()
|
|
case bytes.Equal(c, ctrl["g"]):
|
|
modeline.setMessage("Quit")
|
|
reset()
|
|
case bytes.Equal(c, ctrl["e"]):
|
|
cursorEOL()
|
|
reset()
|
|
case bytes.Equal(c, ctrl["a"]):
|
|
cursorBOL()
|
|
reset()
|
|
case bytes.Equal(c, ctrl["u"]):
|
|
setMultiplier()
|
|
case bytes.Equal(c, arrow["up"]), bytes.Equal(c, ctrl["p"]): // UP, C-p
|
|
cursorUp(multiplier)
|
|
reset()
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x42}), bytes.Equal(c, []byte{0xe}): // DOWN, C-n
|
|
cursorDown(multiplier)
|
|
reset()
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x43}), bytes.Equal(c, []byte{0x6}): // RIGHT, C-f
|
|
cursorForward(multiplier)
|
|
reset()
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x44}), bytes.Equal(c, []byte{0x2}): // LEFT, C-b
|
|
cursorBackward(multiplier)
|
|
reset()
|
|
case bytes.Equal(c, []byte{0x1b, 0x62}): // M-b
|
|
cursorBackwardWord()
|
|
reset()
|
|
case bytes.Equal(c, []byte{0x1b, 0x66}): // M-f
|
|
cursorForwardWord()
|
|
reset()
|
|
case bytes.Equal(c, []byte{0x7f}): // backspace
|
|
buffer.deleteChar()
|
|
reset()
|
|
case bytes.Equal(c, []byte{0xb}): // C-k
|
|
buffer.deleteForward()
|
|
statusline.setFilePathColor(messageColor{1, 8})
|
|
reset()
|
|
default:
|
|
buffer.insertChar(string(c))
|
|
statusline.setFilePathColor(messageColor{1, 8})
|
|
reset()
|
|
}
|
|
}
|
|
|
|
func reset() {
|
|
prefix = nil
|
|
multiplier = 1
|
|
}
|
|
|
|
func setMultiplier() {
|
|
if multiplier == 1 {
|
|
multiplier = 4
|
|
modeline.setMessage("C-u-")
|
|
} else {
|
|
multiplier = multiplier + multiplier
|
|
msg := []string{}
|
|
count := 0
|
|
start := multiplier
|
|
for start > 2 {
|
|
start = start / 2
|
|
count++
|
|
}
|
|
for i := 0; i < count; i++ {
|
|
msg = append(msg, "C-u-")
|
|
}
|
|
|
|
modeline.setMessage(strings.Join(msg, ""))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* BUFFER
|
|
*/
|
|
func (b *Buffer) new(lines []string) Buffer {
|
|
b.Lines = lines
|
|
|
|
return *b
|
|
}
|
|
|
|
func (b *Buffer) fetch(line int) string {
|
|
// fix the index 1 issue
|
|
if line > len(b.Lines) {
|
|
return ""
|
|
}
|
|
return b.Lines[line-1]
|
|
}
|
|
|
|
func (b *Buffer) render() {
|
|
moveCursor(1, 1) // reset cursor for printing buffer
|
|
for _, str := range buffer.Lines {
|
|
// fmt.Printf("%d| %s\r\n", num, str) // with line nums
|
|
fmt.Printf("%s\r\n", str)
|
|
}
|
|
}
|
|
|
|
func (b *Buffer) save() {
|
|
// save file
|
|
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer file.Close()
|
|
out := strings.Join(b.Lines, "\n")
|
|
fileBytes, err = file.WriteString(out)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (b *Buffer) size() string {
|
|
total := 0
|
|
for _, v := range b.Lines {
|
|
total += len(v)
|
|
}
|
|
return humanReadable(total)
|
|
}
|
|
|
|
func (b *Buffer) insertChar(inp string) {
|
|
if len(b.Lines) < cursor.Row {
|
|
b.Lines = append(b.Lines, inp)
|
|
} else {
|
|
b.Lines[cursor.Row-1] = strings.Join(
|
|
[]string{
|
|
b.Lines[cursor.Row-1][:cursor.Col-1],
|
|
inp,
|
|
b.Lines[cursor.Row-1][cursor.Col-1:],
|
|
}, "")
|
|
}
|
|
cursor.Col++
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func (b *Buffer) deleteChar() {
|
|
if cursor.Col == 1 {
|
|
return
|
|
}
|
|
b.Lines[cursor.Row-1] = strings.Join(
|
|
[]string{
|
|
b.Lines[cursor.Row-1][:cursor.Col-2],
|
|
b.Lines[cursor.Row-1][cursor.Col-1:],
|
|
}, "")
|
|
cursor.Col--
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func (b *Buffer) deleteForward() {
|
|
b.Lines[cursor.Row-1] = b.Lines[cursor.Row-1][:cursor.Col-1]
|
|
}
|
|
|
|
/**
|
|
* SCREEN
|
|
*/
|
|
func getTermSize() []int {
|
|
cmd := exec.Command("stty", "size")
|
|
cmd.Stdin = os.Stdin
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
size := string(out)
|
|
size = strings.TrimSpace(size)
|
|
slice := strings.Split(size, " ")
|
|
asRowInt, _ := strconv.ParseInt(slice[0], 0, 32)
|
|
rows := int(asRowInt)
|
|
asColInt, _ := strconv.ParseInt(slice[1], 0, 32)
|
|
cols := int(asColInt)
|
|
// fmt.Printf("%#v, %#v", size, []int{rows, cols})
|
|
return []int{rows, cols}
|
|
}
|
|
|
|
func humanReadable(inp int) string {
|
|
switch {
|
|
case inp > 1000000:
|
|
return fmt.Sprintf("%dM", inp/1000000)
|
|
case inp > 1000:
|
|
return fmt.Sprintf("%dk", inp/1000)
|
|
default:
|
|
return fmt.Sprintf("%d", inp)
|
|
}
|
|
}
|
|
|
|
func (s *Screen) render() {
|
|
termSize := getTermSize()
|
|
|
|
buffer.render()
|
|
|
|
statusline.setSize(buffer.size())
|
|
statusline.setFilePath(filePath)
|
|
statusline.setLocation(cursor.Row, cursor.Col)
|
|
statusline.render(termSize)
|
|
|
|
modeline.render(termSize)
|
|
}
|
|
|
|
/**
|
|
* MODELINE
|
|
*/
|
|
func (m *Modeline) setMessage(msg string) {
|
|
m.message = msg
|
|
}
|
|
|
|
func (m *Modeline) render(size []int) {
|
|
if m.message != "" {
|
|
moveCursor(size[0], 0)
|
|
fmt.Printf("%s", m.message)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* STATUSLINE
|
|
*/
|
|
func (s *Statusline) setFormat() {
|
|
s.fileFormat = fmt.Sprintf("[48;5;8m[38;5;2m")
|
|
s.format = fmt.Sprintf("[48;5;8m[38;5;7m")
|
|
s.unformat = fmt.Sprintf("[48;5;0m[38;5;15m")
|
|
}
|
|
func (s *Statusline) setSize(fileSize string) {
|
|
s.fileSize = fileSize
|
|
}
|
|
func (s *Statusline) setFilePath(filePath string) {
|
|
array := []string{s.fileFormat, filePath, s.format}
|
|
s.filePath = strings.Join(array, " ")
|
|
}
|
|
func (s *Statusline) setFilePathColor(color messageColor) {
|
|
s.fileFormat = fmt.Sprintf("[48;5;%dm[38;5;%dm", color.background, color.foreground)
|
|
// statusline.setFilePath(s.filePath)
|
|
}
|
|
func (s *Statusline) setLocation(row, col int) {
|
|
s.location = fmt.Sprintf("%d:%d", row, col)
|
|
}
|
|
func (s *Statusline) render(size []int) {
|
|
moveCursor(size[0]-1, 0)
|
|
statuslineParts := []string{s.fileSize, s.fileFormat, s.filePath, s.format, s.location}
|
|
line := strings.Join(statuslineParts, " ")
|
|
addLength := len(s.fileFormat) + len(s.format) + len(s.unformat)
|
|
fmt.Printf("%s%-*s%s", s.format, size[1]+addLength-1, line, s.unformat)
|
|
}
|
|
|
|
/**
|
|
* CURSOR
|
|
*/
|
|
func (c *Cursor) new() Cursor {
|
|
return Cursor{
|
|
1,
|
|
1,
|
|
}
|
|
}
|
|
|
|
// func clamp(max, cur int) int {
|
|
// if cur <= max {
|
|
// return cur
|
|
// }
|
|
// return max
|
|
// }
|
|
|
|
func (c *Cursor) clampCol() {
|
|
line := buffer.fetch(c.Row)
|
|
if c.Col > len(line) {
|
|
c.Col = len(line) + 1
|
|
} else if c.Col < 1 {
|
|
c.Col = 1
|
|
}
|
|
}
|
|
|
|
func (c *Cursor) clampRow() {
|
|
rows := len(buffer.Lines)
|
|
if c.Row > rows+1 {
|
|
c.Row = rows + 1
|
|
} else if c.Row < 1 {
|
|
c.Row = 1
|
|
}
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
|
|
func clearScreen() {
|
|
fmt.Print("[2J")
|
|
}
|
|
func moveCursor(row, col int) {
|
|
fmt.Printf("[%d;%dH", row, col)
|
|
}
|
|
|
|
func cursorEOL() {
|
|
line := buffer.fetch(cursor.Row)
|
|
cursor.Col = len(line) + 1
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorBOL() {
|
|
cursor.Col = 1
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorUp(multiplier int) {
|
|
for k := 0; k < multiplier; k++ {
|
|
cursor.Row = cursor.Row - 1
|
|
}
|
|
cursor.clampRow()
|
|
cursor.clampCol()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorDown(multiplier int) {
|
|
for k := 0; k < multiplier; k++ {
|
|
cursor.Row = cursor.Row + 1
|
|
}
|
|
cursor.clampRow()
|
|
cursor.clampCol()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorForward(multiplier int) {
|
|
for k := 0; k < multiplier; k++ {
|
|
cursor.Col = cursor.Col + 1
|
|
}
|
|
cursor.clampCol()
|
|
cursor.clampRow()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorForwardWord() {
|
|
// split line into array
|
|
line := buffer.fetch(cursor.Row)
|
|
line = line[cursor.Col-1:]
|
|
|
|
content := []byte(line)
|
|
pattern := regexp.MustCompile(`\W\w`)
|
|
loc := pattern.FindIndex(content)
|
|
if loc != nil {
|
|
cursor.Col = cursor.Col + loc[0] + 1
|
|
|
|
cursor.clampCol()
|
|
cursor.clampRow()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
}
|
|
|
|
func cursorBackward(multiplier int) {
|
|
for k := 0; k < multiplier; k++ {
|
|
cursor.Col = cursor.Col - 1
|
|
}
|
|
cursor.clampCol()
|
|
cursor.clampRow()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
|
|
func cursorBackwardWord() {
|
|
line := buffer.fetch(cursor.Row)
|
|
line = strings.TrimRight(line[:cursor.Col-1], " ")
|
|
|
|
content := []byte(line)
|
|
pattern := regexp.MustCompile(`\w+.?$`)
|
|
loc := pattern.FindIndex(content)
|
|
if loc != nil {
|
|
cursor.Col = loc[0] + 1
|
|
|
|
cursor.clampCol()
|
|
cursor.clampRow()
|
|
moveCursor(cursor.Row, cursor.Col)
|
|
}
|
|
}
|