initial commit

master
Adam Veldhousen 4 years ago
commit 3ed0418f72
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

2
.gitignore vendored

@ -0,0 +1,2 @@
.bin
pound

@ -0,0 +1,3 @@
# Pound
a text editor - following along with this: https://viewsourcecode.org/snaptoken/kilo/03.rawInputAndOutput.html

@ -0,0 +1,24 @@
package main
import (
"bufio"
"log"
"os"
)
func fileOpen(fname string) {
f, err := os.Open(fname)
if err != nil {
log.Fatal(err)
}
defer f.Close()
fread := bufio.NewScanner(f)
fread.Split(bufio.ScanLines)
state.rows = nil
for fread.Scan() {
state.rows = append(state.rows, fread.Text())
}
state.file = fname
}

@ -0,0 +1,8 @@
module git.vdhsn.com/adam/pound
go 1.14
require (
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942
golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b // indirect
)

@ -0,0 +1,4 @@
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0=
github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b h1:h03Ur1RlPrGTjua4koYdpGl8W0eYo8p1uI9w7RPlkdk=
golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

@ -0,0 +1,231 @@
package main
import (
"bufio"
"fmt"
"io"
"log"
"os"
"syscall"
"github.com/pkg/term/termios"
)
var (
stdin = bufio.NewReader(os.Stdin)
stdout = bufio.NewWriter(os.Stdout)
)
const (
KEY_ESCAPE = rune('\x1b')
KEY_UP_ARROW = (iota + 1000)
KEY_DOWN_ARROW = (iota + 1000)
KEY_LEFT_ARROW = (iota + 1000)
KEY_RIGHT_ARROW = (iota + 1000)
KEY_PAGEUP = (iota + 1000)
KEY_PAGEDOWN = (iota + 1000)
KEY_HOME = (iota + 1000)
KEY_END = (iota + 1000)
KEY_DELETE = (iota + 1000)
)
func editorKeyPresses() bool {
char := editorReadKey()
editorMoveCaret(char)
if char == ctrl_key('r') {
uiRefresh()
}
if char == ctrl_key('w') {
for i := state.rowOffset + state.Cy; i > 0; i-- {
editorMoveCaret(KEY_UP_ARROW)
}
}
if char == ctrl_key('s') {
for i := state.rowOffset + state.Cy; i < len(state.rows)-1; i++ {
editorMoveCaret(KEY_DOWN_ARROW)
}
}
if char == ctrl_key('q') || char == 'q' {
return false
}
return true
}
func editorMoveCaret(char rune) {
x, y := state.Cx, state.Cy
gutterWidth, _ := calcMaxGutterWidth(len(state.rows))
switch char {
case KEY_UP_ARROW:
if y != 0 {
y--
}
break
case KEY_DOWN_ARROW:
if state.rows != nil && y < len(state.rows)-1 {
y++
}
break
case KEY_LEFT_ARROW:
if x > gutterWidth {
x--
} else if y != 0 {
y--
x = len(state.rows[y]) + gutterWidth
}
break
case KEY_RIGHT_ARROW:
if state.rows != nil && (x-gutterWidth) < len(state.rows[y]) {
x++
} else if y < len(state.rows)-1 {
y++
x = gutterWidth
}
break
case KEY_HOME:
x = gutterWidth
break
case KEY_END:
x = state.Cols - 1
break
}
if (x - gutterWidth) > len(state.rows[y]) {
x = len(state.rows[y]) + gutterWidth
}
if x >= state.colOffset+(state.Cols-gutterWidth) {
state.colOffset = x - (state.Cols - gutterWidth)
}
if x < (state.colOffset + gutterWidth) {
state.colOffset = x
}
if y >= state.rowOffset+(state.Rows-2) {
state.rowOffset = y - (state.Rows - 2)
}
if y < state.rowOffset {
state.rowOffset = y
}
state.Cx = x
state.Cy = y
state.StatusLine = fmt.Sprintf("offset: %d:%d, loc %d:%d", state.colOffset, state.rowOffset, x, y)
}
func editorReadKey() rune {
in := '\000'
var err error
var read int
for read != 1 {
in, read, err = stdin.ReadRune()
// read, err = os.Stdin.Read(in)
if err != nil && err != io.EOF {
log.Fatal("FAILED TO READ:", err)
}
}
// fmt.Printf("%v == %v ? %v", rune(in[0]), KEY_ESCAPE, rune(in[0]) == KEY_ESCAPE)
if in == KEY_ESCAPE {
seq := make([]rune, 3)
if seq[0], read, err = stdin.ReadRune(); err != nil || read != 1 {
return KEY_ESCAPE
}
if seq[1], read, err = stdin.ReadRune(); err != nil || read != 1 {
return KEY_ESCAPE
}
if seq[0] == '[' {
// fmt.Printf("%c %c %c", seq[0], seq[1], seq[2])
if seq[1] >= '0' && seq[1] <= '9' {
if seq[2], read, err = stdin.ReadRune(); err != nil || read != 1 {
return KEY_ESCAPE
}
state.StatusLine = fmt.Sprintf("Last Read = '%c''%c''%c' ", seq[0], seq[1], seq[2])
// handle page up/down and home keys
if seq[2] == '~' {
switch seq[1] {
case '1':
case '7':
return KEY_HOME
case 3:
return KEY_DELETE
case '4':
case '8':
return KEY_END
case '5':
return KEY_PAGEUP
case '6':
return KEY_PAGEDOWN
}
}
} else {
// handle arrow keys and home keys
switch seq[1] {
case 'A':
return KEY_UP_ARROW
case 'B':
return KEY_DOWN_ARROW
case 'C':
return KEY_RIGHT_ARROW
case 'D':
return KEY_LEFT_ARROW
case 'H':
return KEY_HOME
case 'F':
return KEY_END
}
}
} else if seq[0] == 'O' {
switch seq[1] {
case 'H':
return KEY_HOME
case 'F':
return KEY_END
}
}
return KEY_ESCAPE
}
return in
}
func enableRawMode() {
if err := termios.Tcgetattr(uintptr(0), &state.orig); err != nil {
log.Fatal("Tcgetattr, ", err)
}
raw := state.orig
raw.Iflag &^= syscall.IXON | syscall.ICRNL | syscall.BRKINT | syscall.INPCK | syscall.ISTRIP
raw.Oflag &^= syscall.OPOST
raw.Cflag &^= syscall.CS8
raw.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
raw.Cc[syscall.VMIN] = 0
raw.Cc[syscall.VTIME] = 1
if err := termios.Tcsetattr(uintptr(0), termios.TCSAFLUSH, &raw); err != nil {
log.Fatal("tcsetattr, ", err)
}
}
func disableRawMode() {
if err := termios.Tcsetattr(uintptr(0), termios.TCSAFLUSH, &state.orig); err != nil {
log.Fatal(err)
}
}
func ctrl_key(key rune) rune {
return rune(int(key) & 0x1f)
}

@ -0,0 +1,51 @@
package main
import (
"bytes"
"log"
"os"
"syscall"
)
var state editorState
type editorState struct {
orig syscall.Termios
Buffer *bytes.Buffer
Cx int
Cy int
Rows int
Cols int
StatusLine string
rows []string
rowOffset int
colOffset int
file string
}
func main() {
enableRawMode()
initEditor()
defer disableRawMode()
defer uiClearScreen()
if len(os.Args) > 1 {
fileOpen(os.Args[1])
}
uiRefresh()
for editorKeyPresses() {
uiRefresh()
}
}
func initEditor() {
var err error
if state.Rows, state.Cols, err = getWindowSize(); err != nil {
log.Fatal(err)
}
state.Cx = 5
state.Buffer = &bytes.Buffer{}
state.rows = []string{"Pound Ed -- version 0.0.0"}
state.file = ""
}

@ -0,0 +1,35 @@
app := pound
PKGS := $(shell go list ./... | grep -v vendor)
OUTDIR := .bin
BINARY := $(OUTDIR)/$(app)
GOBIN := $(GOPATH)/bin
LINTBIN := $(GOBIN)/golangci-lint
clean_list = $(OUTDIR)
.PHONY: build clean dev test publish lint
default: lint test build dev
$(OUTDIR):
@mkdir .bin
$(BINARY): . $(OUTDIR)
go build -o $@ $<
dev: clean $(BINARY)
LB_DEBUG=true $(BINARY) $(FPATH)
build: $(BINARY)
clean: $(clean_list)
rm -rf $<
$(LINTBIN):
@GO111MODULE=off go get github.com/golangci/golangci-lint/cmd/golangci-lint
lint: $(LINTBIN)
go mod tidy -v
$(LINTBIN) run -p bugs -p format -p performance -p unused
test: lint
go test -v -cover $(PKGS)

162
ui.go

@ -0,0 +1,162 @@
package main
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"os"
"runtime"
"syscall"
"unsafe"
)
func uiDrawRows() {
buf := state.Buffer
rowLen := 0
gutterWidth, gutterFmtStr := calcMaxGutterWidth(len(state.rows))
statusLine := fmt.Sprintf(
"'%s' | L: %v/%v/%v | C: %v/%v/%v | %s",
state.file,
state.Cy+1,
len(state.rows),
state.Rows,
state.Cx-gutterWidth,
rowLen,
state.Cols,
state.StatusLine)
for y := 0; y < state.Rows; y++ {
if y == state.Rows-1 {
statusLen := len(statusLine)
if statusLen > state.Cols {
statusLen = state.Cols
}
buf.WriteString(statusLine[:statusLen])
} else if y < state.Rows && state.Cy < len(state.rows) {
rowIdx := y + state.rowOffset
// wrie the gutter w/ line numbers
fmt.Fprintf(buf, gutterFmtStr, rowIdx+1)
// calc how much room left for text
editorCols := state.Cols - gutterWidth
// get the row which is the y on screen + offset
row := state.rows[rowIdx]
rowLen := len(row)
if rowLen > editorCols {
rowLen = rowLen - editorCols
}
if state.colOffset < rowLen {
buf.WriteString(row[state.colOffset:rowLen])
}
} else {
buf.WriteString(" ~")
}
buf.WriteString("\x1b[K")
if y < state.Rows-1 {
buf.WriteString("\r\n")
}
}
}
func uiRefresh() {
buf := state.Buffer
buf.WriteString("\x1b[?25l")
buf.WriteString("\x1b[H")
var err error
if state.Rows, state.Cols, err = getWindowSize(); err != nil {
log.Fatal(err)
}
uiDrawRows()
setCursorPosition(state.Cx-state.colOffset, state.Cy-state.rowOffset)
buf.WriteString("\x1b[?25h")
os.Stdout.Write(buf.Bytes())
buf.Reset()
}
type window struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}
func uiClearScreen() {
os.Stdout.Write([]byte("\x1b[2J"))
os.Stdout.Write([]byte("\x1b[H"))
}
func setCursorPosition(x, y int) {
fmt.Fprintf(state.Buffer, "\x1b[%d;%dH", y+1, x+1)
}
func getCursorPosition() (int, int, error) {
if _, err := os.Stdout.Write([]byte("\x1b[6n")); err != nil && err != io.EOF {
return -1, -1, err
}
buf := make([]byte, 32)
temp := make([]byte, 1)
for i := 0; i < len(buf)-1; i++ {
if _, err := os.Stdin.Read(temp); err != nil && err != io.EOF {
return -1, -1, err
}
buf[i] = temp[0]
if bytes.ContainsRune(temp, 'R') {
break
}
}
if buf[0] != '\x1b' || buf[1] != '[' {
return 0, 0, errors.New("can't read pos")
}
var rows, cols int
if _, err := fmt.Fscanf(bytes.NewBuffer(buf[2:]), "%d;%d", &rows, &cols); err != nil && err != io.EOF {
return 0, 0, errors.New("can't read pos")
}
return rows, cols, nil
}
func getWindowSize() (int, int, error) {
w := new(window)
_, _, err := syscall.Syscall(syscall.SYS_IOCTL,
os.Stdout.Fd(),
syscall.TIOCGWINSZ,
uintptr(unsafe.Pointer(w)),
)
runtime.GC()
if int(err) == -1 || w.Col == 0 {
if _, err := os.Stdout.Write([]byte("\x1b[999C\x1b[999B")); err != nil {
return -1, -1, errors.New("ioctl FALLBACK")
}
return getCursorPosition()
}
return int(w.Row), int(w.Col), nil
}
func iscntrl(c int) bool {
return c > 0x00 && c < 0x1f || c == 0x7f
}
func calcMaxGutterWidth(lineCount int) (int, string) {
maxDigits := len(fmt.Sprintf("%d", lineCount))
// 3 accounts for ' ~ '
return 3 + maxDigits, fmt.Sprintf("%%%dd ~ ", maxDigits)
}
Loading…
Cancel
Save