├── 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 |
--------------------------------------------------------------------------------