commit
3ed0418f72
@ -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)
|
@ -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…
Reference in new issue