Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Replace tcell with termbox #108

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c3f1b58
Replace tcell with termbox in Makefile
kimtore Sep 20, 2017
c7fcc44
Database: automatically replace queue and library in panels
kimtore Sep 20, 2017
2f831f2
Rip out tcell application loop, and compensate enough for compilation…
kimtore Sep 20, 2017
af03cf3
Remove direct access of options from UI
kimtore Sep 20, 2017
47fc259
Remove keyboard input handling from UI widget
kimtore Sep 20, 2017
1554933
Add skeleton and tests for a terminal keyboard event sampler.
kimtore Sep 21, 2017
ef438b0
Add logic for parsing termbox keypresses and generating key names.
kimtore Sep 21, 2017
3448c97
Refactor terminal input system and add more tests.
kimtore Sep 21, 2017
4badd69
Reinstate tcell in the Makefile
kimtore Sep 22, 2017
58fae71
Re-enable command queue.
kimtore Sep 22, 2017
ddb04a6
Remove UI object from global API.
kimtore Sep 22, 2017
e6d6e46
Draw topbar on screen.
kimtore Sep 22, 2017
7675795
Color support through termbox.
kimtore Sep 24, 2017
8a612b2
Ticker improvements
kimtore Sep 24, 2017
f5b4a28
Implement viewport functionality directly in SonglistWidget.
kimtore Sep 25, 2017
4ea1252
Remove obsolete EventPlayer channel.
kimtore Sep 26, 2017
86093e9
Draw column headers as part of SonglistWidget.
kimtore Sep 26, 2017
5b7a583
Code cleanup in terminal keypress module.
kimtore Sep 26, 2017
9893b3d
Documentation cleanup in terminal event sampler.
kimtore Sep 26, 2017
ddf4a81
Add history package, moved from MultibarWidget.
kimtore Sep 26, 2017
7d06822
Move buffering capabilities of MultibarWidget into a separate package.
kimtore Sep 26, 2017
37e7f0e
Move color and attribute styling out of term package.
kimtore Sep 27, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ test:
go test ./...

get-deps:
go get github.com/ambientsound/gompd/mpd
go get github.com/blevesearch/bleve
go get github.com/gdamore/tcell
go get github.com/jessevdk/go-flags
go get github.com/stretchr/testify
go get -u github.com/ambientsound/gompd/mpd
go get -u github.com/blevesearch/bleve
go get -u github.com/jessevdk/go-flags
go get -u github.com/gdamore/tcell
go get -u github.com/nsf/termbox-go
go get -u github.com/stretchr/testify
10 changes: 0 additions & 10 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ type API interface {

// Styles returns the current stylesheet.
Styles() style.Stylesheet

// UI returns the global UI object.
UI() UI
}

type baseAPI struct {
Expand All @@ -86,7 +83,6 @@ type baseAPI struct {
song func() *song.Song
songlistWidget func() SonglistWidget
styles style.Stylesheet
ui func() UI
}

func BaseAPI(
Expand All @@ -105,7 +101,6 @@ func BaseAPI(
song func() *song.Song,
songlistWidget func() SonglistWidget,
styles style.Stylesheet,
ui func() UI,

) API {
return &baseAPI{
Expand All @@ -124,7 +119,6 @@ func BaseAPI(
song: song,
songlistWidget: songlistWidget,
styles: styles,
ui: ui,
}
}

Expand Down Expand Up @@ -191,7 +185,3 @@ func (api *baseAPI) SonglistWidget() SonglistWidget {
func (api *baseAPI) Styles() style.Stylesheet {
return api.styles
}

func (api *baseAPI) UI() UI {
return api.ui()
}
10 changes: 8 additions & 2 deletions api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ type Collection interface {
}

type SonglistWidget interface {
GetVisibleBoundaries() (int, int)
ScrollViewport(int, bool)
Bottom() int
Scroll(int, bool)
Size() (int, int)
Top() int
}

type MultibarWidget interface {
Expand All @@ -31,3 +32,8 @@ type UI interface {
PostFunc(func())
Refresh()
}

type Buffer interface {
String() string
Cursor() int
}
320 changes: 320 additions & 0 deletions bufin/bufin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
// package bufin provides buffered text input with cursor, readline, tab completion, and history.
package bufin

import (
"strings"
"unicode"
"unicode/utf8"

"github.com/ambientsound/pms/api"
"github.com/ambientsound/pms/console"
"github.com/ambientsound/pms/history"
"github.com/ambientsound/pms/input/lexer"
"github.com/ambientsound/pms/tabcomplete"
"github.com/ambientsound/pms/term"
"github.com/ambientsound/pms/utils"
termbox "github.com/nsf/termbox-go"
)

// State signifies what state the buffer is in.
type State int

// State constants.
const (
StateBuffer State = iota // buffer is updated.
StateCursor // cursor is moved.
StateReturn // user presses <Return> during buffered mode.
StateCancel // user cancels buffered mode.
)

// Buffer buffers text input in a readline-like editing environment, and
// facilitates tab completion, history, and keeps a cursor.
type Buffer struct {
api api.API
cursor int // cursor position
history *history.History // backlog of text input
runes []rune // text input
state State
tabComplete *tabcomplete.TabComplete // tab completion list
}

// New returns Buffer.
func New(api api.API) *Buffer {
return &Buffer{
api: api,
history: history.New(),
runes: make([]rune, 0),
}
}

func (b *Buffer) String() string {
return string(b.runes)
}

func (b *Buffer) Len() int {
return len(b.runes)
}

// Cursor returns the cursor position.
func (b *Buffer) Cursor() int {
return b.cursor
}

func (b *Buffer) State() State {
return b.state
}

func (b *Buffer) setState(s State) {
b.state = s
}

func (b *Buffer) setRunes(r []rune) {
b.runes = r
b.validateCursor()
b.setState(StateBuffer)
}

// validateCursor makes sure the cursor stays within boundaries.
func (b *Buffer) validateCursor() {
if b.cursor > b.Len() {
b.cursor = b.Len()
}
if b.cursor < 0 {
b.cursor = 0
}
}

func (b *Buffer) handleTruncate() {
b.tabComplete = nil
b.setRunes(make([]rune, 0))
b.history.Reset(b.String())
}

// handleTextRune inserts a literal rune at the cursor position.
func (b *Buffer) handleTextRune(r rune) {
b.tabComplete = nil
runes := make([]rune, len(b.runes)+1)
copy(runes, b.runes[:b.cursor])
copy(runes[b.cursor+1:], b.runes[b.cursor:])
runes[b.cursor] = r
b.setRunes(runes)

b.cursor++
b.history.Reset(b.String())
}

// handleBackspace deletes a literal rune behind the cursor position.
func (b *Buffer) handleBackspace() {

b.tabComplete = nil

// Backspace on an empty string returns to normal mode.
if len(b.runes) == 0 {
b.handleAbort()
return
}

// Copy all runes except the deleted rune
runes := deleteBackwards(b.runes, b.cursor, 1)
b.cursor--
b.setRunes(runes)

b.history.Reset(b.String())
}

// handleDeleteWord deletes the previous word, along with all the backspace
// succeeding it.
func (b *Buffer) handleDeleteWord() {

b.tabComplete = nil

// We don't use the lexer here because it is too smart when it comes to
// quoted strings.
cursor := b.cursor - 1

// Scan backwards until a non-space character is found.
for ; cursor >= 0; cursor-- {
if !unicode.IsSpace(b.runes[cursor]) {
break
}
}

// Scan backwards until a space character is found.
for ; cursor >= 0; cursor-- {
if unicode.IsSpace(b.runes[cursor]) {
cursor++
break
}
}

// Delete backwards.
runes := deleteBackwards(b.runes, b.cursor, b.cursor-cursor)
b.cursor = cursor
b.setRunes(runes)

b.history.Reset(b.String())
}

func (b *Buffer) handleFinished() {
b.tabComplete = nil
b.history.Add(b.String())
}

func (b *Buffer) handleAbort() {
b.setRunes(make([]rune, 0))
b.handleFinished()
b.setState(StateCancel)
}

func (b *Buffer) handleComplete() {
b.handleFinished()
b.setState(StateReturn)
}

func (b *Buffer) handleHistory(offset int) {
b.tabComplete = nil
s := b.history.Navigate(offset)
b.setRunes([]rune(s))
b.cursor = len(b.runes)
}

func (b *Buffer) handleCursor(offset int) {
b.tabComplete = nil
b.cursor += offset
b.setState(StateCursor)
b.validateCursor()
}

// handleCursorWord moves the cursor forward to the start of the next word or
// backwards to the start of the previous word.
func (b *Buffer) handleCursorWord(offset int) {
b.tabComplete = nil
b.cursor += nextWord(b.runes, b.cursor, offset)
b.setState(StateCursor)
b.validateCursor()
}

// handleTab invokes tab completion.
func (b *Buffer) handleTab() {

// Ignore event if cursor is not at the end
if b.cursor != len(b.runes) {
return
}

// Initialize tabcomplete
if b.tabComplete == nil {
b.tabComplete = tabcomplete.New(b.String(), b.api)
}

// Get next sentence, and abort on any errors.
sentence, err := b.tabComplete.Scan()
if err != nil {
console.Log("Autocomplete: %s", err)
return
}

// Replace current text.
b.setRunes([]rune(sentence))
b.cursor = len(b.runes)
}

// handleTextInputEvent is called when an input event is received during any of the text input modes.
func (b *Buffer) handleTextInputEvent(ev term.KeyPress) bool {
switch ev.Key {

// Alt keys has to be handled a bit differently than Ctrl keys.
case 0:
if ev.Mod&term.ModAlt == 0 {
// Pass the rune on to the text handling function if the alt modifier was not used.
b.handleTextRune(ev.Ch)
} else {
switch ev.Ch {
case 'b':
b.handleCursorWord(-1)
case 'f':
b.handleCursorWord(1)
}
}

case termbox.KeyCtrlU:
b.handleTruncate()
case termbox.KeyEnter:
b.handleComplete()
case termbox.KeyTab:
b.handleTab()
case termbox.KeyArrowLeft, termbox.KeyCtrlB:
b.handleCursor(-1)
case termbox.KeyArrowRight, termbox.KeyCtrlF:
b.handleCursor(1)
case termbox.KeyArrowUp, termbox.KeyCtrlP:
b.handleHistory(-1)
case termbox.KeyArrowDown, termbox.KeyCtrlN:
b.handleHistory(1)
case termbox.KeyCtrlG, termbox.KeyCtrlC:
b.handleAbort()
case termbox.KeyCtrlA, termbox.KeyHome:
b.handleCursor(-len(b.runes))
case termbox.KeyCtrlE, termbox.KeyEnd:
b.handleCursor(len(b.runes))
case termbox.KeyBackspace, termbox.KeyDelete:
b.handleBackspace()
case termbox.KeyCtrlW:
b.handleDeleteWord()

default:
console.Log("Unhandled text input event: %+v", ev)
return false
}

return true
}

// deleteBackwards returns a new rune slice with a part cut out. If the deleted
// part is bigger than the string contains, deleteBackwards removes as much as
// possible.
func deleteBackwards(src []rune, cursor int, length int) []rune {
if cursor < length {
length = cursor
}
runes := make([]rune, len(src)-length)
index := copy(runes, src[:cursor-length])
copy(runes[index:], src[cursor:])
return runes
}

// nextWord returns the distance to the next word in a rune slice.
func nextWord(runes []rune, cursor, offset int) int {
var s string

switch {
// Move backwards
case offset < 0:
rev := utils.ReverseRunes(runes)
revIndex := len(runes) - cursor
runes := rev[revIndex:]
s = string(runes)

// Move forwards
case offset > 0:
runes := runes[cursor:]
s = string(runes)

default:
return 0
}

reader := strings.NewReader(s)
scanner := lexer.NewScanner(reader)

// Strip any whitespace, and count the total length of the whitespace and
// the next token.
tok, lit := scanner.Scan()
skip := utf8.RuneCountInString(lit)
if tok == lexer.TokenWhitespace {
_, lit = scanner.Scan()
skip += utf8.RuneCountInString(lit)
}

return offset * skip
}
Loading