|
|
@ -8,8 +8,9 @@ import ( |
|
|
|
|
|
|
|
"editor/utils" |
|
|
|
"fmt" |
|
|
|
"golang.org/x/crypto/ssh/terminal" |
|
|
|
"os/exec" |
|
|
|
"regexp" |
|
|
|
"strconv" |
|
|
|
"strings" |
|
|
|
) |
|
|
|
|
|
|
@ -22,28 +23,89 @@ 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 |
|
|
|
} |
|
|
|
|
|
|
|
func clamp(max, cur int) int { |
|
|
|
if cur <= max { |
|
|
|
return cur |
|
|
|
} |
|
|
|
return max |
|
|
|
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 { |
|
|
|
log.Println("a file is required") |
|
|
|
fmt.Println("You MUST supply a file to edit") |
|
|
|
return |
|
|
|
} |
|
|
|
filePath = os.Args[1] |
|
|
@ -53,19 +115,12 @@ func main() { |
|
|
|
|
|
|
|
func (e *Editor) initialize() { |
|
|
|
// Load a file
|
|
|
|
file, err := os.Open(filePath) |
|
|
|
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755) |
|
|
|
if err != nil { |
|
|
|
log.Fatal(err) |
|
|
|
} |
|
|
|
defer file.Close() |
|
|
|
|
|
|
|
// RAW terminal
|
|
|
|
oldState, err := terminal.MakeRaw(0) |
|
|
|
if err != nil { |
|
|
|
log.Fatal(err) |
|
|
|
} |
|
|
|
defer terminal.Restore(0, oldState) |
|
|
|
|
|
|
|
var lines []string |
|
|
|
scanner := bufio.NewScanner(file) |
|
|
|
for scanner.Scan() { |
|
|
@ -76,6 +131,7 @@ func (e *Editor) initialize() { |
|
|
|
} |
|
|
|
|
|
|
|
multiplier = 1 |
|
|
|
statusline.setFormat() |
|
|
|
buffer = buffer.new(lines) |
|
|
|
cursor = cursor.new() |
|
|
|
} |
|
|
@ -89,66 +145,109 @@ func (e *Editor) run() { |
|
|
|
|
|
|
|
func (e *Editor) render() { |
|
|
|
clearScreen() |
|
|
|
buffer.render() |
|
|
|
|
|
|
|
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, []byte{0x3}), bytes.Equal(c, []byte{0x11}), bytes.Equal(c, []byte{0x18}): // C-c, C-q, C-x
|
|
|
|
// quit
|
|
|
|
clearScreen() |
|
|
|
moveCursor(1, 1) |
|
|
|
os.Exit(0) |
|
|
|
return |
|
|
|
case bytes.Equal(c, []byte{0x13}): // C-s
|
|
|
|
// save
|
|
|
|
buffer.save() |
|
|
|
case bytes.Equal(c, []byte{0x5}): // C-e
|
|
|
|
// end of line
|
|
|
|
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() |
|
|
|
case bytes.Equal(c, []byte{0x1}): // C-a
|
|
|
|
// beginning of line
|
|
|
|
reset() |
|
|
|
case bytes.Equal(c, ctrl["a"]): |
|
|
|
cursorBOL() |
|
|
|
case bytes.Equal(c, []byte{0x15}): // C-u
|
|
|
|
if multiplier == 1 { |
|
|
|
multiplier = 4 |
|
|
|
} else { |
|
|
|
multiplier = multiplier + multiplier |
|
|
|
} |
|
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x41}), bytes.Equal(c, []byte{0x10}): // UP, C-p
|
|
|
|
reset() |
|
|
|
case bytes.Equal(c, ctrl["u"]): |
|
|
|
setMultiplier() |
|
|
|
case bytes.Equal(c, arrow["up"]), bytes.Equal(c, ctrl["p"]): // UP, C-p
|
|
|
|
cursorUp(multiplier) |
|
|
|
multiplier = 1 |
|
|
|
reset() |
|
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x42}), bytes.Equal(c, []byte{0xe}): // DOWN, C-n
|
|
|
|
cursorDown(multiplier) |
|
|
|
multiplier = 1 |
|
|
|
reset() |
|
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x43}), bytes.Equal(c, []byte{0x6}): // RIGHT, C-f
|
|
|
|
cursorForward(multiplier) |
|
|
|
multiplier = 1 |
|
|
|
reset() |
|
|
|
case bytes.Equal(c, []byte{0x1b, 0x5b, 0x44}), bytes.Equal(c, []byte{0x2}): // LEFT, C-b
|
|
|
|
cursorBackward(multiplier) |
|
|
|
multiplier = 1 |
|
|
|
reset() |
|
|
|
case bytes.Equal(c, []byte{0x1b, 0x62}): // M-b
|
|
|
|
cursorBackwardWord() |
|
|
|
multiplier = 1 |
|
|
|
reset() |
|
|
|
case bytes.Equal(c, []byte{0x1b, 0x66}): // M-f
|
|
|
|
cursorForwardWord() |
|
|
|
multiplier = 1 |
|
|
|
reset() |
|
|
|
case bytes.Equal(c, []byte{0x7f}): // backspace
|
|
|
|
buffer.deleteChar() |
|
|
|
multiplier = 1 |
|
|
|
reset() |
|
|
|
case bytes.Equal(c, []byte{0xb}): // C-k
|
|
|
|
buffer.deleteForward() |
|
|
|
multiplier = 1 |
|
|
|
statusline.setFilePathColor(messageColor{1, 8}) |
|
|
|
reset() |
|
|
|
default: |
|
|
|
buffer.insertChar(string(c)) |
|
|
|
multiplier = 1 |
|
|
|
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 |
|
|
|
|
|
|
@ -179,12 +278,20 @@ func (b *Buffer) save() { |
|
|
|
} |
|
|
|
defer file.Close() |
|
|
|
out := strings.Join(b.Lines, "\n") |
|
|
|
_, err = file.WriteString(out) |
|
|
|
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) |
|
|
@ -217,6 +324,98 @@ 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, |
|
|
@ -224,6 +423,13 @@ func (c *Cursor) new() Cursor { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 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) { |
|
|
|