This commit is contained in:
parent
bd9a07feed
commit
41d1aa6553
@ -24,7 +24,7 @@ func (e *editor) RefreshScreen() {
|
||||
e.drawStatusBar(ab)
|
||||
e.drawMessageBar(ab)
|
||||
|
||||
ab.append(terminal.MoveCursor(e.renderX - e.offset.x, e.cursor.y - e.offset.y))
|
||||
ab.append(terminal.MoveCursor(e.renderX-e.offset.x, e.cursor.y-e.offset.y))
|
||||
|
||||
ab.append(terminal.ShowCursor)
|
||||
|
||||
@ -42,7 +42,7 @@ func (e *editor) scroll() {
|
||||
e.offset.y = e.cursor.y
|
||||
}
|
||||
|
||||
if e.cursor.y >= e.offset.y + e.screen.Rows {
|
||||
if e.cursor.y >= e.offset.y+e.screen.Rows {
|
||||
e.offset.y = e.cursor.y - e.screen.Rows + 1
|
||||
}
|
||||
|
||||
@ -50,13 +50,13 @@ func (e *editor) scroll() {
|
||||
e.offset.x = e.renderX
|
||||
}
|
||||
|
||||
if e.renderX >= e.offset.x + e.screen.Cols {
|
||||
if e.renderX >= e.offset.x+e.screen.Cols {
|
||||
e.offset.x = e.renderX - e.screen.Cols
|
||||
}
|
||||
}
|
||||
|
||||
func (e *editor) drawRows(ab *buffer) {
|
||||
for y :=0; y < e.screen.Rows; y++ {
|
||||
for y := 0; y < e.screen.Rows; y++ {
|
||||
fileRow := y + e.offset.y
|
||||
|
||||
if fileRow >= e.document.rowCount() {
|
||||
@ -73,20 +73,19 @@ func (e *editor) drawRows(ab *buffer) {
|
||||
}
|
||||
|
||||
rowLen := e.document.rows[fileRow].rSize() - e.offset.x
|
||||
outputRow := truncateString(string(e.document.rows[fileRow].render[e.offset.x:]), rowLen)
|
||||
outputRow := truncate(string(e.document.rows[fileRow].render[e.offset.x:]), rowLen)
|
||||
ab.append(outputRow)
|
||||
}
|
||||
|
||||
ab.append(terminal.ClearLine)
|
||||
ab.append("\r\n")
|
||||
ab.appendLn(terminal.ClearLine)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *editor) drawPlaceholderRow(y int, ab *buffer) {
|
||||
if e.document.rowCount() == 0 && y == e.screen.Rows / 3 {
|
||||
if e.document.rowCount() == 0 && y == e.screen.Rows/3 {
|
||||
welcome := fmt.Sprintf("Gilo editor -- version %s", KiloVersion)
|
||||
if len(welcome) > e.screen.Cols {
|
||||
welcome = truncateString(welcome, e.screen.Cols)
|
||||
welcome = truncate(welcome, e.screen.Cols)
|
||||
}
|
||||
|
||||
padding := (e.screen.Cols - len(welcome)) / 2
|
||||
@ -119,7 +118,7 @@ func (e *editor) drawStatusBar(ab *buffer) {
|
||||
leftStatus := fmt.Sprintf("%.20s - %d lines", fileName, e.document.rowCount())
|
||||
length := len(leftStatus)
|
||||
if length > cols {
|
||||
leftStatus = truncateString(leftStatus, cols)
|
||||
leftStatus = truncate(leftStatus, cols)
|
||||
|
||||
ab.append(leftStatus)
|
||||
ab.append(terminal.ResetColor)
|
||||
@ -147,10 +146,9 @@ func (e *editor) drawStatusBar(ab *buffer) {
|
||||
ab.append(leftStatus)
|
||||
|
||||
// Pad the rest of the status line
|
||||
padding := strings.Repeat(" ", cols - length)
|
||||
padding := strings.Repeat(" ", cols-length)
|
||||
ab.append(padding)
|
||||
|
||||
|
||||
ab.append(terminal.ResetColor)
|
||||
}
|
||||
|
||||
@ -158,7 +156,7 @@ func (e *editor) drawMessageBar(ab *buffer) {
|
||||
ab.append("\r\n")
|
||||
ab.append(terminal.ClearLine)
|
||||
|
||||
msg := truncateString(e.status.message, e.screen.Cols)
|
||||
msg := truncate(e.status.message, e.screen.Cols)
|
||||
if len(msg) > 0 && time.Since(e.status.created).Seconds() < 5.0 {
|
||||
ab.append(msg)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package editor
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"timshome.page/gilo/key"
|
||||
"timshome.page/gilo/terminal"
|
||||
)
|
||||
|
||||
@ -35,8 +36,8 @@ func New() *editor {
|
||||
// Subtract rows for status bar and message bar/prompt
|
||||
screen.Rows -= 2
|
||||
|
||||
cursor := &point { 0, 0 }
|
||||
offset := &point { 0, 0 }
|
||||
cursor := &point{0, 0}
|
||||
offset := &point{0, 0}
|
||||
document := newDocument()
|
||||
status := &statusMsg{
|
||||
"",
|
||||
@ -58,45 +59,47 @@ func (e *editor) Open(filename string) {
|
||||
}
|
||||
|
||||
func (e *editor) SetStatusMessage(template string, a ...interface{}) {
|
||||
e.status = &statusMsg {
|
||||
e.status = &statusMsg{
|
||||
fmt.Sprintf(template, a...),
|
||||
time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *editor) ProcessKeypress() bool {
|
||||
var str string
|
||||
|
||||
ch, _ := terminal.ReadKey()
|
||||
if isCtrl(ch) {
|
||||
|
||||
switch ch {
|
||||
case ctrl('q'):
|
||||
case key.Ctrl('q'):
|
||||
// Clean up on exit
|
||||
terminal.Write(terminal.ClearScreen + terminal.ResetCursor)
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '\x1b' {
|
||||
str = parseEscapeSequence()
|
||||
} else {
|
||||
str = string(ch)
|
||||
}
|
||||
case key.Esc:
|
||||
// Modifier keys that return ANSI escape sequences
|
||||
str := parseEscapeSequence()
|
||||
|
||||
switch str {
|
||||
case keyUp, keyDown, keyLeft, keyRight, keyPageUp, keyPageDown, keyHome, keyEnd:
|
||||
case keyUp,
|
||||
keyDown,
|
||||
keyLeft,
|
||||
keyRight,
|
||||
keyPageUp,
|
||||
keyPageDown,
|
||||
keyHome,
|
||||
keyEnd:
|
||||
|
||||
e.moveCursor(str)
|
||||
return true
|
||||
}
|
||||
|
||||
default:
|
||||
// Do something later
|
||||
terminal.Write("Code: %v", str)
|
||||
e.insertChar(ch)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *editor) moveCursor (key string) {
|
||||
func (e *editor) moveCursor(key string) {
|
||||
var row *row
|
||||
if e.cursor.y >= e.document.rowCount() {
|
||||
row = nil
|
||||
@ -121,7 +124,7 @@ func (e *editor) moveCursor (key string) {
|
||||
}
|
||||
|
||||
// Move from end of current line to beginning of next line
|
||||
if row != nil && e.cursor.x == row.size() && e.cursor.y < e.document.rowCount() - 1 {
|
||||
if row != nil && e.cursor.x == row.size() && e.cursor.y < e.document.rowCount()-1 {
|
||||
e.cursor.y += 1
|
||||
e.cursor.x = 0
|
||||
}
|
||||
@ -141,7 +144,7 @@ func (e *editor) moveCursor (key string) {
|
||||
}
|
||||
|
||||
case keyPageDown:
|
||||
if e.cursor.y + e.screen.Rows > e.document.rowCount() {
|
||||
if e.cursor.y+e.screen.Rows > e.document.rowCount() {
|
||||
e.cursor.y += e.screen.Rows
|
||||
} else {
|
||||
e.cursor.y = e.document.rowCount() - 1
|
||||
@ -166,8 +169,17 @@ func (e *editor) moveCursor (key string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *editor) insertChar(ch rune) {
|
||||
if e.cursor.y == e.document.rowCount() {
|
||||
e.document.appendRow("")
|
||||
}
|
||||
|
||||
e.document.rows[e.cursor.y].insertRune(ch, e.cursor.x)
|
||||
e.cursor.x += 1
|
||||
}
|
||||
|
||||
// Convert the raw ANSI escape sequences to the type of key input
|
||||
func parseEscapeSequence () string {
|
||||
func parseEscapeSequence() string {
|
||||
var runes []rune
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
|
27
editor/fn.go
27
editor/fn.go
@ -4,7 +4,7 @@ package editor
|
||||
import "strings"
|
||||
|
||||
// Truncate a string to a length
|
||||
func truncateString(s string, length int) string {
|
||||
func truncate(s string, length int) string {
|
||||
if length < 1 {
|
||||
return ""
|
||||
}
|
||||
@ -27,28 +27,3 @@ func truncateString(s string, length int) string {
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Is this an ASCII character?
|
||||
func isAscii(char rune) bool {
|
||||
return char < 0x80
|
||||
}
|
||||
|
||||
// Is this an ASCII ctrl character?
|
||||
func isCtrl(char rune) bool {
|
||||
if !isAscii(char) {
|
||||
return false
|
||||
}
|
||||
|
||||
return char == 0x7f || char < 0x20
|
||||
}
|
||||
|
||||
// Return the input code of a Ctrl-key chord.
|
||||
func ctrl(char rune) rune {
|
||||
if !isAscii(char) {
|
||||
return 0
|
||||
}
|
||||
|
||||
ch := char & 0x1f
|
||||
|
||||
return ch
|
||||
}
|
@ -4,67 +4,9 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type isRune struct {
|
||||
arg1 rune
|
||||
expected bool
|
||||
}
|
||||
|
||||
// (╯°□°)╯︵ ┻━┻
|
||||
var isAsciiTest = []isRune {
|
||||
{'┻', false},
|
||||
{'$', true},
|
||||
{'︵', false},
|
||||
{0x7f, true},
|
||||
}
|
||||
|
||||
func TestIsAscii(t *testing.T) {
|
||||
for _, test := range isAsciiTest {
|
||||
if output := isAscii(test.arg1); output != test.expected {
|
||||
t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isCtrlTest = []isRune {
|
||||
{0x78, false},
|
||||
{0x7f, true},
|
||||
{0x02, true},
|
||||
{0x98, false},
|
||||
{'a', false},
|
||||
}
|
||||
|
||||
func TestIsCtrl(t *testing.T) {
|
||||
for _, test := range isCtrlTest {
|
||||
if output := isCtrl(test.arg1); output != test.expected {
|
||||
t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ctrlTest struct {
|
||||
arg1, expected rune
|
||||
}
|
||||
|
||||
var ctrlTests = []ctrlTest {
|
||||
{'A', '\x01'},
|
||||
{'B', '\x02'},
|
||||
{'Z', '\x1a'},
|
||||
{'#', 3},
|
||||
{'┻', 0},
|
||||
{'😿', 0},
|
||||
}
|
||||
|
||||
func TestCtrl(t *testing.T) {
|
||||
for _, test := range ctrlTests {
|
||||
if output := ctrl(test.arg1); output != test.expected {
|
||||
t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateString(t *testing.T) {
|
||||
firstString := "abcdefghijklmnopqrstuvwxyz"
|
||||
truncated := truncateString(firstString, 13)
|
||||
truncated := truncate(firstString, 13)
|
||||
|
||||
got := len(truncated)
|
||||
want := 13
|
||||
@ -75,7 +17,7 @@ func TestTruncateString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTruncateStringNegative(t *testing.T) {
|
||||
got := truncateString("fdlkjf", -5)
|
||||
got := truncate("fdlkjf", -5)
|
||||
want := ""
|
||||
|
||||
if got != want {
|
||||
@ -86,11 +28,10 @@ func TestTruncateStringNegative(t *testing.T) {
|
||||
func TestTruncateShorterString(t *testing.T) {
|
||||
str := "abcdefg"
|
||||
|
||||
got := truncateString(str, 13)
|
||||
got := truncate(str, 13)
|
||||
want := str
|
||||
|
||||
if got != want {
|
||||
t.Errorf("Truncated value: %q, expected value: %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,5 @@
|
||||
package editor
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// !Terminal Input Escape Code Sequences
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const(
|
||||
KeyArrowUp = "A"
|
||||
KeyArrowDown = "B"
|
||||
KeyArrowRight = "C"
|
||||
KeyArrowLeft = "D"
|
||||
KeyPageUp = "5~"
|
||||
KeyPageDown = "6~"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// !Constants representing input keys
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -1,6 +1,8 @@
|
||||
package editor
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type row struct {
|
||||
chars []rune
|
||||
@ -16,7 +18,7 @@ func newRow(s string) *row {
|
||||
render = append(render, ch)
|
||||
}
|
||||
|
||||
return &row {chars, render}
|
||||
return &row{chars, render}
|
||||
}
|
||||
|
||||
func (r *row) size() int {
|
||||
@ -27,6 +29,31 @@ func (r *row) rSize() int {
|
||||
return len(r.render)
|
||||
}
|
||||
|
||||
func (r *row) insertRune(ch rune, at int) {
|
||||
// If insertion index is invalid, just
|
||||
// append the rune to the end of the array
|
||||
if at < 0 || at >= r.size() {
|
||||
r.chars = append(r.chars, ch)
|
||||
r.update()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var newSlice []rune
|
||||
|
||||
// Split the character array at the insertion point
|
||||
start := r.chars[0:at]
|
||||
end := r.chars[at:r.size()]
|
||||
|
||||
// Splice it back together
|
||||
newSlice = append(newSlice, start...)
|
||||
newSlice = append(newSlice, ch)
|
||||
newSlice = append(newSlice, end...)
|
||||
|
||||
r.chars = newSlice
|
||||
r.update()
|
||||
}
|
||||
|
||||
func (r *row) update() {
|
||||
r.render = r.render[:0]
|
||||
replacement := strings.Repeat(" ", KiloTabStop)
|
||||
@ -37,7 +64,11 @@ func (r *row) update() {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *row) cursorXToRenderX (cursorX int) int {
|
||||
func (r *row) toString() string {
|
||||
return string(r.chars)
|
||||
}
|
||||
|
||||
func (r *row) cursorXToRenderX(cursorX int) int {
|
||||
renderX := 0
|
||||
i := 0
|
||||
|
||||
|
@ -38,3 +38,35 @@ func TestRenderSize(t *testing.T) {
|
||||
t.Errorf("Row rsize not equal to number of runes in original string")
|
||||
}
|
||||
}
|
||||
|
||||
type insertRune struct {
|
||||
initial string
|
||||
ch rune
|
||||
at int
|
||||
}
|
||||
|
||||
var insertRuneTests = []insertRune{
|
||||
{"abde", 'c', 2},
|
||||
{"bcde", 'a', 0},
|
||||
{"abcd", 'e', 4},
|
||||
{"abcd", 'e', 17},
|
||||
{"abcd", 'e', -2},
|
||||
}
|
||||
|
||||
func TestInsertRune(t *testing.T) {
|
||||
for _, test := range insertRuneTests {
|
||||
row := newRow(test.initial)
|
||||
row.insertRune(test.ch, test.at)
|
||||
|
||||
if row.size() != 5 {
|
||||
t.Errorf("Row size after inserting rune at index [%d] is %d, should be %d", test.at, row.size(), 5)
|
||||
}
|
||||
|
||||
got := row.toString()
|
||||
want := "abcde"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("Row after update is '%s', should be '%s'", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
gilo.go
2
gilo.go
@ -29,7 +29,7 @@ func main() {
|
||||
e.Open(os.Args[1])
|
||||
}
|
||||
|
||||
e.SetStatusMessage("HELP: Ctrl-Q = quit");
|
||||
e.SetStatusMessage("HELP: Ctrl-Q = quit")
|
||||
|
||||
// The input loop
|
||||
for {
|
||||
|
34
key/key.go
Normal file
34
key/key.go
Normal file
@ -0,0 +1,34 @@
|
||||
package key
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// !Terminal Input Escape Code Sequences
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
Esc = '\x1b'
|
||||
)
|
||||
|
||||
// Is this an ASCII character?
|
||||
func isAscii(char rune) bool {
|
||||
return char < 0x80
|
||||
}
|
||||
|
||||
// Is this an ASCII ctrl character?
|
||||
func IsCtrl(char rune) bool {
|
||||
if !isAscii(char) {
|
||||
return false
|
||||
}
|
||||
|
||||
return char == 0x7f || char < 0x20
|
||||
}
|
||||
|
||||
// Return the input code of a Ctrl-key chord.
|
||||
func Ctrl(char rune) rune {
|
||||
if !isAscii(char) {
|
||||
return 0
|
||||
}
|
||||
|
||||
ch := char & 0x1f
|
||||
|
||||
return ch
|
||||
}
|
61
key/key_test.go
Normal file
61
key/key_test.go
Normal file
@ -0,0 +1,61 @@
|
||||
package key
|
||||
|
||||
import "testing"
|
||||
|
||||
type isRune struct {
|
||||
arg1 rune
|
||||
expected bool
|
||||
}
|
||||
|
||||
// (╯°□°)╯︵ ┻━┻
|
||||
var isAsciiTest = []isRune{
|
||||
{'┻', false},
|
||||
{'$', true},
|
||||
{'︵', false},
|
||||
{0x7f, true},
|
||||
}
|
||||
|
||||
func TestIsAscii(t *testing.T) {
|
||||
for _, test := range isAsciiTest {
|
||||
if output := isAscii(test.arg1); output != test.expected {
|
||||
t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isCtrlTest = []isRune{
|
||||
{0x78, false},
|
||||
{0x7f, true},
|
||||
{0x02, true},
|
||||
{0x98, false},
|
||||
{'a', false},
|
||||
}
|
||||
|
||||
func TestIsCtrl(t *testing.T) {
|
||||
for _, test := range isCtrlTest {
|
||||
if output := IsCtrl(test.arg1); output != test.expected {
|
||||
t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ctrlTest struct {
|
||||
arg1, expected rune
|
||||
}
|
||||
|
||||
var ctrlTests = []ctrlTest{
|
||||
{'A', '\x01'},
|
||||
{'B', '\x02'},
|
||||
{'Z', '\x1a'},
|
||||
{'#', 3},
|
||||
{'┻', 0},
|
||||
{'😿', 0},
|
||||
}
|
||||
|
||||
func TestCtrl(t *testing.T) {
|
||||
for _, test := range ctrlTests {
|
||||
if output := Ctrl(test.arg1); output != test.expected {
|
||||
t.Errorf("Output %v not equal to expected %v for input %q", output, test.expected, test.arg1)
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,6 @@ import "fmt"
|
||||
|
||||
const EscPrefix = "\x1b["
|
||||
|
||||
|
||||
const (
|
||||
// Clears the line after the escape sequence
|
||||
ClearLine = EscPrefix + "K"
|
||||
@ -41,13 +40,12 @@ const (
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Add the ANSI escape code prefix to the relevant escape code
|
||||
func Code (s string, a ...interface{}) string {
|
||||
func Code(s string, a ...interface{}) string {
|
||||
str := fmt.Sprintf(s, a...)
|
||||
|
||||
return EscPrefix + str
|
||||
}
|
||||
|
||||
|
||||
// Generate the escape sequence to move the terminal cursor to the 0-based coordinate
|
||||
func MoveCursor(x int, y int) string {
|
||||
// Allow 0-based indexing, the terminal code is 1-based
|
||||
|
@ -12,14 +12,14 @@ type Screen struct {
|
||||
}
|
||||
|
||||
// Get the size of the terminal in rows and columns
|
||||
func Size () *Screen {
|
||||
func Size() *Screen {
|
||||
cols := 80
|
||||
rows := 24
|
||||
|
||||
// Try the syscall first
|
||||
cols, rows, err := term.GetSize(int(os.Stdin.Fd()))
|
||||
if err == nil {
|
||||
return &Screen {rows, cols }
|
||||
return &Screen{rows, cols}
|
||||
}
|
||||
|
||||
// Figure out the size the hard way
|
||||
@ -27,10 +27,10 @@ func Size () *Screen {
|
||||
rows, cols = sizeTrick()
|
||||
}
|
||||
|
||||
return &Screen{ rows, cols }
|
||||
return &Screen{rows, cols}
|
||||
}
|
||||
|
||||
func sizeTrick () (rows int, cols int) {
|
||||
func sizeTrick() (rows int, cols int) {
|
||||
// Move cursor to location further than likely screen size
|
||||
// The cursor will move to maximum available position
|
||||
fmt.Print(Code("999C") + Code("999B"))
|
||||
@ -46,7 +46,7 @@ func sizeTrick () (rows int, cols int) {
|
||||
continue
|
||||
}
|
||||
|
||||
if char == 'R' || char == '\x00'{
|
||||
if char == 'R' || char == '\x00' {
|
||||
break
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user