├── Makefile ├── README.md ├── constants.go ├── debugger.go ├── go.mod ├── go.sum ├── main.go └── session.go /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | go run . 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # novavim 2 | This is my attempt at making Vim from scratch in Go, meaning this project will utilize no libs outside of the Go standard library: no ncurses, no terminal UI libraries, nada - the only exceptions are libraries that I make myself, such as this one. 3 | 4 | I love Vim, so by no means am I trying to make something better - in fact, this is going to be pretty hot garbage - but nevertheless I'm going to pursue this hopeless endeavor simply because it's fun and I have no life. 5 | 6 | 7 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | VI_BACKSPACE = 0x7f 5 | VI_ENTER = 0xa 6 | VI_ESC = 0x1b 7 | 8 | VI_a = 0x61 9 | VI_b = 0x62 10 | VI_c = 0x63 11 | VI_d = 0x64 12 | VI_e = 0x65 13 | VI_f = 0x66 14 | VI_g = 0x67 15 | VI_h = 0x68 16 | VI_i = 0x69 17 | VI_j = 0x6a 18 | VI_k = 0x6b 19 | VI_l = 0x6c 20 | VI_m = 0x6d 21 | VI_n = 0x6e 22 | VI_o = 0x6f 23 | VI_p = 0x70 24 | VI_q = 0x71 25 | VI_r = 0x72 26 | VI_s = 0x73 27 | VI_t = 0x74 28 | VI_u = 0x75 29 | VI_v = 0x76 30 | VI_w = 0x77 31 | VI_x = 0x78 32 | VI_y = 0x79 33 | VI_z = 0x7a 34 | 35 | VI_A = 0x41 36 | VI_B = 0x42 37 | VI_C = 0x43 38 | VI_D = 0x44 39 | VI_E = 0x45 40 | VI_F = 0x46 41 | VI_G = 0x47 42 | VI_H = 0x48 43 | VI_I = 0x49 44 | VI_J = 0x4a 45 | VI_K = 0x4b 46 | VI_L = 0x4c 47 | VI_M = 0x4d 48 | VI_N = 0x4e 49 | VI_O = 0x4f 50 | VI_P = 0x50 51 | VI_Q = 0x51 52 | VI_R = 0x52 53 | VI_S = 0x53 54 | VI_T = 0x54 55 | VI_U = 0x55 56 | VI_V = 0x56 57 | VI_W = 0x57 58 | VI_X = 0x58 59 | VI_Y = 0x59 60 | VI_Z = 0x5a 61 | 62 | MD_NORMAL = "NORMAL" 63 | MD_INSERT = "INSERT" 64 | 65 | CURSOR_ROW_START = 1 66 | CURSOR_COL_START = 5 67 | ) 68 | -------------------------------------------------------------------------------- /debugger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | // Write logs to a separate terminal 10 | // Ex: 11 | // wr := debug("ttys012") 12 | // wr("I am writing into another terminal") 13 | func Debug(charDevice string) func([]byte) { 14 | dev := fmt.Sprintf("/dev/%s", charDevice) 15 | f, err := os.OpenFile(dev, os.O_WRONLY, 0755) 16 | if err != nil { 17 | log.Fatalln(err) 18 | } 19 | f.Write([]byte("\n")) 20 | 21 | return func(b []byte) { 22 | output := fmt.Sprintf("%v -> %s\n", b, b) 23 | f.Write([]byte(output)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/solidiquis/novavim 2 | 3 | go 1.16 4 | 5 | require github.com/solidiquis/ansigo v0.1.0 // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/solidiquis/ansigo v0.0.0-20210327065907-7ea9efbbc96b h1:eyn3e3COn9QOq4VN6tDrLn5Qj8EXDtt8fkkMep4HPds= 2 | github.com/solidiquis/ansigo v0.0.0-20210327065907-7ea9efbbc96b/go.mod h1:xJM8EzTEAJxbmZEDfCQTsawR3to2O1VPIepr5GLZE88= 3 | github.com/solidiquis/ansigo v0.1.0 h1:viPSB32iVG9g5/TO5R6/atNEpBaogYc+qUsKnKdUtHE= 4 | github.com/solidiquis/ansigo v0.1.0/go.mod h1:RGzuVSjR1qo9QnaINg9qsGeQxY1Cf027gvPpOR50LBg= 5 | golang.org/x/sys v0.0.0-20210326220804-49726bf1d181 h1:64ChN/hjER/taL4YJuA+gpLfIMT+/NFherRZixbxOhg= 6 | golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ansi "github.com/solidiquis/ansigo" 6 | ) 7 | 8 | func main() { 9 | sn := InitSession() 10 | stdin := make(chan string, 1) 11 | 12 | sn.InitWindow() 13 | 14 | go sn.WinResizeListener() 15 | go ansi.GetChar(stdin) 16 | 17 | wr := Debug("ttys014") 18 | 19 | for { 20 | wr([]byte(fmt.Sprint(sn.CursorCol))) 21 | wr([]byte(fmt.Sprint(sn.CursorRow))) 22 | select { 23 | case ch := <-stdin: 24 | if ch[0] == VI_ESC { 25 | sn.SetMode(MD_NORMAL) 26 | continue 27 | } 28 | 29 | // NORMAL MODE 30 | if sn.Mode == MD_NORMAL { 31 | switch ch[0] { 32 | // movement 33 | case VI_h: 34 | sn.CursorLeft(1) 35 | case VI_j: 36 | sn.CursorDown(1) 37 | case VI_k: 38 | sn.CursorUp(1) 39 | case VI_l: 40 | sn.CursorRight(1) 41 | 42 | // delete 43 | case VI_d: 44 | subCh := <-stdin 45 | switch subCh[0] { 46 | case VI_d: 47 | ansi.EraseLine() 48 | ansi.CursorBackward(1) // Delete to beginning of column offset 49 | ansi.CursorUp(1) 50 | default: 51 | continue 52 | } 53 | 54 | // insert 55 | case VI_O, VI_o: 56 | sn.AddLine(ch[0]) 57 | sn.SetMode(MD_INSERT) 58 | case VI_i: 59 | sn.SetMode(MD_INSERT) 60 | } 61 | continue 62 | } 63 | 64 | // INSERT MODE 65 | if sn.Mode == MD_INSERT { 66 | switch ch[0] { 67 | case VI_BACKSPACE: 68 | sn.Backspace() 69 | case VI_ENTER: 70 | sn.AddLine(ch[0]) 71 | default: 72 | fmt.Print(string(ch)) 73 | sn.Lines[sn.CursorRow] += string(ch) 74 | sn.CursorCol++ 75 | } 76 | } 77 | } 78 | } // for 79 | } 80 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ansi "github.com/solidiquis/ansigo" 6 | "log" 7 | "math" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | ) 12 | 13 | type session struct { 14 | // Window dimensions 15 | Width int 16 | Height int 17 | 18 | // Current mode 19 | Mode string 20 | 21 | // Text and associated line number 22 | Lines map[int]string 23 | 24 | // Last line number 25 | LastLine int 26 | 27 | // Cursor Position 28 | CursorRow int 29 | CursorCol int 30 | 31 | // Offsets for cursor boundaries 32 | ColOffset int 33 | RowOffset int 34 | 35 | // Whitespace from left of window to line num 36 | OffsetChars string 37 | } 38 | 39 | func InitSession() *session { 40 | cols, rows, err := ansi.TerminalDimensions() 41 | if err != nil { 42 | log.Fatalln(err) 43 | } 44 | 45 | return &session{ 46 | Width: cols, 47 | Height: rows, 48 | Mode: MD_NORMAL, 49 | Lines: make(map[int]string), 50 | LastLine: 1, 51 | CursorRow: CURSOR_ROW_START, 52 | CursorCol: CURSOR_COL_START, 53 | ColOffset: CURSOR_COL_START, 54 | RowOffset: rows - 1, 55 | 56 | // TODO: Should be determined by highest 57 | // line number in a given file. 58 | OffsetChars: " ", 59 | } 60 | } 61 | 62 | func (sn *session) InitWindow() { 63 | ansi.EraseScreen() 64 | ansi.CursorSetPos(sn.Height, 0) 65 | fmt.Print(ansi.Bright(MD_NORMAL)) 66 | ansi.CursorSetPos(0, 0) 67 | fmt.Print(ansi.FgYellow(" 1 ")) 68 | } 69 | 70 | func (sn *session) WinResizeListener() { 71 | for { 72 | sig := make(chan os.Signal, 1) 73 | signal.Notify(sig, syscall.SIGWINCH) 74 | <-sig 75 | 76 | c, r, err := ansi.TerminalDimensions() 77 | if err != nil { 78 | log.Fatalln(err) 79 | } 80 | 81 | sn.Height = c 82 | sn.Width = r 83 | } 84 | } 85 | 86 | func (sn *session) AddLine(key byte) { 87 | sn.LastLine++ 88 | 89 | // How much initial whitespace before line number 90 | offset := int(math.Floor(math.Log10(float64(sn.LastLine))+1)) - 1 91 | 92 | ws := sn.OffsetChars 93 | if sn.ColOffset > 0 { 94 | trim := len(ws) - offset 95 | ws = sn.OffsetChars[:trim] 96 | } 97 | 98 | // print the new line number 99 | ansi.CursorSetPos(sn.LastLine, 0) 100 | ln := ansi.FgYellow(fmt.Sprintf("%s%d ", ws, sn.LastLine)) 101 | fmt.Print(ln) 102 | 103 | // place cursor in correct position 104 | switch key { 105 | case VI_ENTER, VI_o: 106 | sn.CursorRow++ 107 | } 108 | 109 | sn.CursorCol = sn.ColOffset 110 | ansi.CursorSetPos(sn.CursorRow, sn.ColOffset) 111 | } 112 | 113 | func (sn *session) SetMode(mode string) { 114 | ansi.CursorSavePos() 115 | sn.Mode = mode 116 | ansi.CursorSetPos(sn.Height, 0) 117 | ansi.EraseLine() 118 | fmt.Print(ansi.Bright(mode)) 119 | ansi.CursorRestorePos() 120 | } 121 | 122 | func (sn *session) Backspace() { 123 | if sn.CursorCol-1 < sn.ColOffset { 124 | return 125 | } 126 | 127 | sn.Lines[sn.CursorRow] = sn.Lines[sn.CursorRow][:len(sn.Lines[sn.CursorRow])-1] 128 | ansi.Backspace() 129 | sn.CursorCol-- 130 | } 131 | 132 | func (sn *session) CursorRight(n int) { 133 | if sn.CursorCol+n > sn.ColOffset+len(sn.Lines[sn.CursorRow]) { 134 | return 135 | } 136 | 137 | sn.CursorCol += n 138 | ansi.CursorForward(n) 139 | } 140 | 141 | func (sn *session) CursorLeft(n int) { 142 | if sn.CursorCol-n < sn.ColOffset { 143 | return 144 | } 145 | 146 | sn.CursorCol -= n 147 | ansi.CursorBackward(n) 148 | } 149 | 150 | func (sn *session) CursorUp(n int) { 151 | if sn.CursorRow-1 < 1 { 152 | return 153 | } 154 | 155 | sn.CursorRow -= n 156 | ansi.CursorUp(n) 157 | } 158 | 159 | // TODO: Set bottom-most boundaries 160 | func (sn *session) CursorDown(n int) { 161 | if sn.CursorRow+n > sn.LastLine { 162 | return 163 | } 164 | 165 | sn.CursorRow += n 166 | 167 | // If next line is blank, jump to col-offset 168 | ln, ok := sn.Lines[sn.CursorRow] 169 | if !ok { 170 | sn.CursorColHome() 171 | return 172 | } 173 | 174 | if sn.CursorCol < len(ln)+sn.ColOffset { 175 | ansi.CursorDown(n) 176 | return 177 | } 178 | 179 | col := sn.ColOffset + len(ln) 180 | sn.CursorCol = col 181 | ansi.CursorSetPos(sn.CursorRow, col) 182 | } 183 | 184 | func (sn *session) CursorColHome() { 185 | sn.CursorCol = sn.ColOffset 186 | ansi.CursorSetPos(sn.CursorRow, sn.CursorCol) 187 | } 188 | --------------------------------------------------------------------------------