diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85042ac --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +editor diff --git a/Makefile b/Makefile index 20f08be..7571f02 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,31 @@ SHELL := /bin/bash +FILE := tmp.txt +CAT := bat -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; +ifeq (, $(shell which bat)) + $(error "No bat in $(PATH), consider installing ti") + CAT = cat +endif + +build_and_run: clean build run + +.PHONY : build_and_run build run clean test + +build : + @echo "-> Building" + @cd ~/go/src/editor/utils/; go build .; cd .. + @go build . + @echo "-> Done" + +run : + @echo "-> Running" + @./editor $(FILE) + +clean : + @echo "-> Cleaning up" + @-rm editor + +test : + @echo -e "-> Generating test file" + @echo -e "This is a line.\nThis is another line.\n\n\nThis is the end." > tmp.txt + @$(CAT) tmp.txt diff --git a/editor b/editor deleted file mode 100755 index 06abb6c..0000000 Binary files a/editor and /dev/null differ diff --git a/file.txt b/file.txt deleted file mode 100644 index fd8db73..0000000 --- a/file.txt +++ /dev/null @@ -1,16 +0,0 @@ -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 index 8da5dd8..8273ded 100644 --- a/main.go +++ b/main.go @@ -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("") + s.format = fmt.Sprintf("") + s.unformat = fmt.Sprintf("") +} +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) {