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() return e.processKeypressChar(ch) } /** * Determine what to do with an individual character of input */ func (e *editor) processKeypressChar(ch rune) bool { 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'): e.delChar() case key.Esc, key.Ctrl('l'): // Modifier keys that return ANSI escape sequences str := parseEscapeSequence() // Don't swallow a character after ESC if it doesn't // start an ANSI escape sequence if len(str) == 1 { return e.processKeypressChar(rune(str[0])) } e.processKeypressStr(str) default: e.insertChar(ch) } // Clear the quit message and restart the // confirmation count if confirmation is not // completed if e.quitTimes != KiloQuitTimes { e.quitTimes = KiloQuitTimes e.SetStatusMessage("") } return true } /** * Determine what do do with a parsed ANSI escape sequence */ func (e *editor) processKeypressStr(key string) { switch key { case keyUp, keyDown, keyLeft, keyRight, keyPageUp, keyPageDown, keyHome, keyEnd: e.moveCursor(key) case keyDelete: e.moveCursor(keyRight) e.delChar() } } 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 } func (e *editor) delChar() { if e.cursor.y == e.document.rowCount() { return } if e.cursor.x > 0 { e.document.rows[e.cursor.y].deleteRune(e.cursor.x - 1) e.cursor.x -= 1 } e.document.dirty = true } // Convert the raw ANSI escape sequences to the type of key input func parseEscapeSequence() string { // If we aren't starting an escape sequence, // return the character startChar, _ := terminal.ReadKey() if startChar != '[' && startChar != 'O' { return string(startChar) } // Read one or two characters after // \e[ or \eO, which is the end of the // handled escape sequences runes := [2]rune{'\000', '\000'} for i := 0; i < 2; i++ { ch, size := terminal.ReadKey() if size == 0 { return string(rune(key.Esc)) } runes[i] = ch if i == 0 && runes[0] >= 'A' && runes[0] <= 'Z' { return escSeqToKey([]rune{startChar, runes[0]}) } // \e[*~ if i == 1 && startChar == '[' && runes[1] == '~' { return escSeqToKey([]rune{startChar, runes[0], runes[1]}) } } return string(rune(key.Esc)) } func escSeqToKey (seq []rune) string { // \eO* // \e[* if len(seq) == 2 { startChar, cmd := seq[0], seq[1] if startChar == 'O' { switch cmd { case 'H': return keyHome case 'F': return keyEnd } } else if startChar == '[' { switch cmd { case 'A': return keyUp case 'B': return keyDown case 'C': return keyRight case 'D': return keyLeft case 'H': return keyHome case 'F': return keyEnd } } } else if len(seq) == 3 { // \e[*~ cmd := seq[1] switch cmd { 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(rune(key.Esc)) }