commit ec02dc3c4c38549541731a8cac69d0266868c160 Author: Levi Olson Date: Thu Feb 7 09:56:24 2019 -0600 Initial commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..20f08be --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +SHELL := /bin/bash + +init: + @echo -n "Building Imports..."; \ + cd ~/go/src/editor/utils/; go build .; cd ..; \ + echo -e "\nDone"; \ + echo "Running Editor"; \ + go run main.go file.txt; diff --git a/editor b/editor new file mode 100755 index 0000000..06abb6c Binary files /dev/null and b/editor differ diff --git a/file.txt b/file.txt new file mode 100644 index 0000000..fd8db73 --- /dev/null +++ b/file.txt @@ -0,0 +1,16 @@ +This is the very first line and its awesome +This is line two. + add stff + + +This is farther down the page. +End of file + + + + + + + + + \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..8da5dd8 --- /dev/null +++ b/main.go @@ -0,0 +1,334 @@ +package main + +import ( + "bufio" + "bytes" + "log" + "os" + + "editor/utils" + "fmt" + "golang.org/x/crypto/ssh/terminal" + "regexp" + "strings" +) + +// Editor is the initializing class +type Editor struct { +} + +// Buffer is the temporary view presented to the user +type Buffer struct { + Lines []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 +} + +var filePath string +var editor Editor +var buffer Buffer +var cursor Cursor +var multiplier int + +func main() { + if len(os.Args) == 1 { + log.Println("a file is required") + return + } + filePath = os.Args[1] + editor.initialize() + editor.run() +} + +func (e *Editor) initialize() { + // Load a file + file, err := os.Open(filePath) + 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() { + lines = append(lines, scanner.Text()) + } + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + + multiplier = 1 + buffer = buffer.new(lines) + cursor = cursor.new() +} + +func (e *Editor) run() { + for { + e.render() + e.handleInput() + } +} + +func (e *Editor) render() { + clearScreen() + buffer.render() + + moveCursor(cursor.Row, cursor.Col) // restore cursor +} + +func (e *Editor) handleInput() { + c := utils.Getch() + // log.Printf("%#v\t%s\n", c, string(c)) + 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 + cursorEOL() + case bytes.Equal(c, []byte{0x1}): // C-a + // beginning of line + 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 + cursorUp(multiplier) + multiplier = 1 + case bytes.Equal(c, []byte{0x1b, 0x5b, 0x42}), bytes.Equal(c, []byte{0xe}): // DOWN, C-n + cursorDown(multiplier) + multiplier = 1 + case bytes.Equal(c, []byte{0x1b, 0x5b, 0x43}), bytes.Equal(c, []byte{0x6}): // RIGHT, C-f + cursorForward(multiplier) + multiplier = 1 + case bytes.Equal(c, []byte{0x1b, 0x5b, 0x44}), bytes.Equal(c, []byte{0x2}): // LEFT, C-b + cursorBackward(multiplier) + multiplier = 1 + case bytes.Equal(c, []byte{0x1b, 0x62}): // M-b + cursorBackwardWord() + multiplier = 1 + case bytes.Equal(c, []byte{0x1b, 0x66}): // M-f + cursorForwardWord() + multiplier = 1 + case bytes.Equal(c, []byte{0x7f}): // backspace + buffer.deleteChar() + multiplier = 1 + case bytes.Equal(c, []byte{0xb}): // C-k + buffer.deleteForward() + multiplier = 1 + default: + buffer.insertChar(string(c)) + multiplier = 1 + } +} + +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") + _, err = file.WriteString(out) + if err != nil { + log.Fatal(err) + } +} + +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] +} + +func (c *Cursor) new() Cursor { + return Cursor{ + 1, + 1, + } +} + +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("") +} +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) + } +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..8cc44bd --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,19 @@ +package utils + +import ( + "github.com/pkg/term" +) + +// Getch simply listens for input from stdin +func Getch() []byte { + t, _ := term.Open("/dev/tty") + term.RawMode(t) + bytes := make([]byte, 3) + numRead, err := t.Read(bytes) + t.Restore() + t.Close() + if err != nil { + return nil + } + return bytes[0:numRead] +}