package editor import ( "fmt" "time" "timshome.page/gilo/key" "timshome.page/gilo/terminal" ) // ---------------------------------------------------------------------------- // !Editor // ---------------------------------------------------------------------------- type point struct { x int y int } type statusMsg struct { message string created time.Time } type editor struct { screen *terminal.Screen cursor *point offset *point document *document status *statusMsg quitTimes uint8 renderX int } func New() *editor { screen := terminal.Size() // Subtract rows for status bar and message bar/prompt screen.Rows -= 2 cursor := &point{0, 0} offset := &point{0, 0} document := newDocument() status := &statusMsg{ "", time.Now(), } return &editor{ screen, cursor, offset, document, status, KiloQuitTimes, 0, } } func (e *editor) Open(filename string) { e.document.open(filename) } func (e *editor) save() { size := e.document.save() if size > 0 { e.SetStatusMessage("%d bytes written to disk", size) } else { e.SetStatusMessage("Failed to save file") } } func (e *editor) SetStatusMessage(template string, a ...interface{}) { e.status = &statusMsg{ fmt.Sprintf(template, a...), time.Now(), } } func (e *editor) ProcessKeypress() bool { ch, _ := terminal.ReadKey() switch ch { case key.Ctrl('q'): if e.document.dirty && e.quitTimes > 0 { e.SetStatusMessage("WARNING!!! File has unsaved changes. Press Ctrl-Q %d more tiems to quite.", e.quitTimes) e.quitTimes -= 1 return true } // Clean up on exit terminal.Write(terminal.ClearScreen + terminal.ResetCursor) return false case key.Ctrl('s'): e.save() case key.Enter: case key.Backspace, key.Ctrl('h'): case key.Esc, key.Ctrl('l'): // Modifier keys that return ANSI escape sequences str := parseEscapeSequence() switch str { case keyUp, keyDown, keyLeft, keyRight, keyPageUp, keyPageDown, keyHome, keyEnd: e.moveCursor(str) } default: e.insertChar(ch) } if e.quitTimes != KiloQuitTimes { e.quitTimes = KiloQuitTimes } return true } func (e *editor) moveCursor(key string) { var row *row if e.cursor.y >= e.document.rowCount() { row = nil } else { row = e.document.rows[e.cursor.y] } switch key { case keyLeft: if e.cursor.x != 0 { e.cursor.x -= 1 } // Move from beginning of current row to end of previous row if e.cursor.y > 0 { e.cursor.y -= 1 e.cursor.x = e.document.rows[e.cursor.y].size() } case keyRight: if row != nil && e.cursor.x < row.size() { e.cursor.x += 1 } // 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 { e.cursor.y += 1 e.cursor.x = 0 } case keyUp: if e.cursor.y != 0 { e.cursor.y -= 1 } case keyDown: if e.cursor.y < e.document.rowCount() { e.cursor.y += 1 } case keyPageUp: if e.cursor.y > e.screen.Rows { e.cursor.y -= e.screen.Rows } else { e.cursor.y = 0 } case keyPageDown: if e.cursor.y+e.screen.Rows > e.document.rowCount() { e.cursor.y += e.screen.Rows } else { e.cursor.y = e.document.rowCount() - 1 } case keyHome: e.cursor.x = 0 case keyEnd: if row != nil { e.cursor.x = row.size() } } if e.cursor.y < e.document.rowCount() { row = e.document.rows[e.cursor.y] rowLen := row.size() // Snap to the end of a shorter line from a longer one if e.cursor.x > rowLen { e.cursor.x = rowLen } } } 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 e.document.dirty = true } // Convert the raw ANSI escape sequences to the type of key input func parseEscapeSequence() string { var runes []rune for i := 0; i < 3; i++ { ch, size := terminal.ReadKey() if size == 0 { return "\x1b" } runes = append(runes, ch) if i == 1 && runes[1] >= 'A' { // \eOH \eOF if runes[0] == 'O' { switch runes[1] { case 'H': return keyHome case 'F': return keyEnd } } // \e[A if runes[0] == '[' { switch runes[1] { case 'A': return keyUp case 'B': return keyDown case 'C': return keyRight case 'D': return keyLeft case 'H': return keyHome case 'F': return keyEnd } } } // \e[1~ if i == 2 && runes[0] == '[' && runes[2] == '~' { switch runes[1] { case '1': return keyHome case '3': return keyDelete case '4': return keyEnd case '5': return keyPageUp case '6': return keyPageDown case '7': return keyHome case '8': return keyEnd } } } return string('\x1b') }