/
main.go
264 lines (240 loc) · 6.44 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/user"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/jangler/edit"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/sdl_ttf"
)
const version = "0.3.0"
const (
insMark = iota // ID of the cursor/insertion mark
selMark // ID of the selection anchor mark
)
var (
darkFlag = false
expandtabFlag = false
fontFlag = ""
ptsizeFlag = 12
tabstopFlag = 8
versionFlag = false
)
var sectionFlags = make(map[string]map[string]string)
var shebangRegexp = regexp.MustCompile(`^#!(/usr/bin/env |/.+/)(.+)( |$)`)
// readIni reads option defaults from the .ini file, if one exists.
func readIni() {
if curUser, err := user.Current(); err == nil {
paths := []string{
filepath.Join(curUser.HomeDir, "fervor.ini"),
filepath.Join(curUser.HomeDir, ".config", "fervor.ini"),
}
for _, path := range paths {
contents, err := ioutil.ReadFile(path)
if err != nil {
continue
}
section := ""
sectionFlags[""] = make(map[string]string)
for _, line := range strings.Split(string(contents), "\n") {
// ignore comment lines
if strings.HasPrefix(line, ";") {
continue
}
if match, _ := regexp.MatchString(`^\[.+\]$`, line); match {
section = line
sectionFlags[section] = make(map[string]string)
} else {
tokens := strings.SplitN(line, "=", 2)
if len(tokens) == 2 {
if section == "" {
flag.Set(tokens[0], tokens[1]) // ignore errors
}
sectionFlags[section][tokens[0]] = tokens[1]
}
}
}
break // successfully read .ini file
}
} else {
log.Print(err)
}
}
// setFileFlags sets flags based on INI settings, the current file path, and
// the first line of the buffer, and returns syntax rules to be used for the
// file.
func setFileFlags(path, line string) []edit.Rule {
fn := filepath.Base(path)
var sb string
if subs := shebangRegexp.FindStringSubmatch(line); subs != nil {
sb = subs[2]
}
// first, reset flags to defaults
for k, v := range sectionFlags[""] {
flag.Set(k, v) // ignore errors
}
for section, flags := range sectionFlags {
match := false
if globs, ok := flags["filename"]; ok {
for _, glob := range strings.Split(globs, ";") {
if match, _ = filepath.Match(glob, fn); match {
break
}
}
}
if shebangs, ok := flags["shebang"]; ok && !match && sb != "" {
for _, shebang := range strings.Split(shebangs, ";") {
if shebang == sb {
match = true
break
}
}
}
if match {
for k, v := range flags {
flag.Set(k, v) // ignore errors
}
clampFlags()
if syntaxFunc, ok := syntaxMap[section]; ok {
return syntaxFunc()
}
break
}
}
return []edit.Rule{}
}
// clampFlags keeps flags within reasonable bounds.
func clampFlags() {
if ptsizeFlag < 8 {
ptsizeFlag = 8
}
if tabstopFlag < 1 {
tabstopFlag = 1
}
}
// initFlags initializes the flag package.
func initFlags() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [<option> ...] [<file> ...]\n",
os.Args[0])
fmt.Fprintln(os.Stderr, "\nOptions:")
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, `
Global and file-specific default options can be specified in either
~/fervor.ini or ~/.config/fervor.ini.`)
}
flag.BoolVar(&darkFlag, "dark", darkFlag, "use dark color scheme")
flag.BoolVar(&expandtabFlag, "expandtab", expandtabFlag,
"insert spaces using the Tab key")
flag.StringVar(&fontFlag, "font", fontFlag,
"use the font at the given path")
flag.IntVar(&ptsizeFlag, "ptsize", ptsizeFlag, "set point size of font")
flag.IntVar(&tabstopFlag, "tabstop", tabstopFlag,
"set width of tab stops, in columns")
flag.BoolVar(&versionFlag, "version", versionFlag,
"print version information and exit")
}
// parseFlags processes command-line flags.
func parseFlags() {
flag.Parse()
clampFlags()
if versionFlag {
fmt.Printf("%s version %s %s/%s\n", os.Args[0], version, runtime.GOOS,
runtime.GOARCH)
os.Exit(0)
}
sectionFlags[""] = map[string]string{
"dark": fmt.Sprintf("%v", darkFlag),
"expandtab": fmt.Sprintf("%v", expandtabFlag),
"font": fmt.Sprintf("%v", fontFlag),
"ptsize": fmt.Sprintf("%v", ptsizeFlag),
"tabstop": fmt.Sprintf("%v", tabstopFlag),
}
}
// openFile attempts to open the file given by path and return a new buffer
// containing the contents of that file. If an error is encountered, it returns
// a nil buffer and the error instead.
func openFile(path string) (*edit.Buffer, error) {
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
buf := edit.NewBuffer()
buf.Insert(buf.End(), string(contents))
if buf.Get(buf.ShiftIndex(buf.End(), -1), buf.End()) == "\n" {
buf.Delete(buf.ShiftIndex(buf.End(), -1), buf.End())
}
buf.ResetModified()
buf.ResetUndo()
syntaxRules := setFileFlags(path,
buf.Get(edit.Index{1, 0}, edit.Index{1, 1 << 30}))
buf.SetSyntax(syntaxRules)
return buf, nil
}
// lineEnding returns the line ending string used for a buffer, and converts
// the buffer to Unix line endings if it uses DOS line endings.
func lineEnding(b *edit.Buffer) string {
if strings.Contains(b.Get(edit.Index{1, 0}, edit.Index{2, 0}), "\r\n") {
text := b.Get(edit.Index{1, 0}, b.End())
b.Delete(edit.Index{1, 0}, b.End())
b.Insert(edit.Index{1, 0}, strings.Replace(text, "\r", "", -1))
return "\r\n"
}
return "\n"
}
func main() {
log.SetFlags(log.Lshortfile)
initFlags()
readIni()
parseFlags()
setColorScheme()
// init SDL
runtime.LockOSThread()
sdl.Init(sdl.INIT_VIDEO)
defer sdl.Quit()
if err := ttf.Init(); err != nil {
log.Fatal(err)
}
defer ttf.Quit()
// open new instances for other file args
if flag.NArg() > 1 {
for _, arg := range flag.Args()[1:] {
newInstance(arg, "")
}
}
// init buffer
var arg, status string
if flag.NArg() == 0 || flag.Arg(0) == "" {
arg = os.DevNull
} else {
arg = flag.Arg(0)
}
var buf *edit.Buffer
var err error
if buf, err = openFile(arg); err == nil {
status = fmt.Sprintf(`Opened "%s".`, minPath(arg))
} else {
status = fmt.Sprintf(`New file: "%s".`, minPath(arg))
buf = edit.NewBuffer()
buf.SetSyntax(setFileFlags(arg, ""))
}
pane := &Pane{buf, minPath(arg), tabstopFlag, 80, 25, lineEnding(buf)}
if pane.LineEnding == "\r\n" {
status += " [DOS]"
}
pane.SetTabWidth(tabstopFlag)
pane.Mark(edit.Index{1, 0}, selMark, insMark)
font := getFont()
win := createWindow(minPath(arg), font)
defer win.Destroy()
w, h := win.GetSize()
resize(pane, w, h)
eventLoop(pane, status, font, win)
}