Have added prompting.

This commit is contained in:
Leon Mika 2017-08-26 09:48:22 +10:00
parent 665bc8cdae
commit 33847a78c1
10 changed files with 847 additions and 774 deletions

View file

@ -1,128 +1,148 @@
package main package main
import ( import (
"./ui" "bitbucket.org/lmika/ted-v2/ui"
) )
const ( const (
ModAlt rune = 1 << 31 - 1 ModAlt rune = 1<<31 - 1
) )
// A command // A command
type Command struct { type Command struct {
Name string Name string
Doc string Doc string
Action func(ctx CommandContext) error
// TODO: Add argument mapping which will fetch properties from the environment
Action func(ctx *CommandContext) error
} }
// Execute the command // Execute the command
func (cmd *Command) Do(ctx CommandContext) error { func (cmd *Command) Do(ctx *CommandContext) error {
return cmd.Action(ctx) return cmd.Action(ctx)
}
// The command context
type CommandContext interface {
// Returns the current frame. If no frame is defined, returns nil.
Frame() *Frame
} }
// A command mapping // A command mapping
type CommandMapping struct { type CommandMapping struct {
Commands map[string]*Command Commands map[string]*Command
KeyMappings map[rune]*Command KeyMappings map[rune]*Command
} }
// Creates a new, empty command mapping // Creates a new, empty command mapping
func NewCommandMapping() *CommandMapping { func NewCommandMapping() *CommandMapping {
return &CommandMapping{make(map[string]*Command), make(map[rune]*Command)} return &CommandMapping{make(map[string]*Command), make(map[rune]*Command)}
} }
// Adds a new command // Adds a new command
func (cm *CommandMapping) Define(name string, doc string, opts string, fn func(ctx CommandContext) error) { func (cm *CommandMapping) Define(name string, doc string, opts string, fn func(ctx *CommandContext) error) {
cm.Commands[name] = &Command{name, doc, fn} cm.Commands[name] = &Command{name, doc, fn}
} }
// Adds a key mapping // Adds a key mapping
func (cm *CommandMapping) MapKey(key rune, cmd *Command) { func (cm *CommandMapping) MapKey(key rune, cmd *Command) {
cm.KeyMappings[key] = cmd cm.KeyMappings[key] = cmd
} }
// Searches for a command by name. Returns the command or null // Searches for a command by name. Returns the command or null
func (cm *CommandMapping) Command(name string) *Command { func (cm *CommandMapping) Command(name string) *Command {
return cm.Commands[name] return cm.Commands[name]
} }
// Searches for a command by key mapping // Searches for a command by key mapping
func (cm *CommandMapping) KeyMapping(key rune) *Command { func (cm *CommandMapping) KeyMapping(key rune) *Command {
return cm.KeyMappings[key] return cm.KeyMappings[key]
} }
// Registers the standard view navigation commands. These commands require the frame // Registers the standard view navigation commands. These commands require the frame
func (cm *CommandMapping) RegisterViewCommands() { func (cm *CommandMapping) RegisterViewCommands() {
cm.Define("move-down", "Moves the cursor down one row", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, 1) })) cm.Define("move-down", "Moves the cursor down one row", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, 1) }))
cm.Define("move-up", "Moves the cursor up one row", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, -1) })) cm.Define("move-up", "Moves the cursor up one row", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, -1) }))
cm.Define("move-left", "Moves the cursor left one column", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(-1, 0) })) cm.Define("move-left", "Moves the cursor left one column", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(-1, 0) }))
cm.Define("move-right", "Moves the cursor right one column", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(1, 0) })) cm.Define("move-right", "Moves the cursor right one column", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(1, 0) }))
// TODO: Pages are just 25 rows and 15 columns at the moment // TODO: Pages are just 25 rows and 15 columns at the moment
cm.Define("page-down", "Moves the cursor down one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, 25) })) cm.Define("page-down", "Moves the cursor down one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, 25) }))
cm.Define("page-up", "Moves the cursor up one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, -25) })) cm.Define("page-up", "Moves the cursor up one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, -25) }))
cm.Define("page-left", "Moves the cursor left one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(-15, 0) })) cm.Define("page-left", "Moves the cursor left one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(-15, 0) }))
cm.Define("page-right", "Moves the cursor right one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(15, 0) })) cm.Define("page-right", "Moves the cursor right one page", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(15, 0) }))
cm.Define("row-top", "Moves the cursor to the top of the row", "", gridNavOperation(func(grid *ui.Grid) { cm.Define("row-top", "Moves the cursor to the top of the row", "", gridNavOperation(func(grid *ui.Grid) {
cellX, _ := grid.CellPosition() cellX, _ := grid.CellPosition()
grid.MoveTo(cellX, 0) grid.MoveTo(cellX, 0)
})) }))
cm.Define("row-bottom", "Moves the cursor to the bottom of the row", "", gridNavOperation(func(grid *ui.Grid) { cm.Define("row-bottom", "Moves the cursor to the bottom of the row", "", gridNavOperation(func(grid *ui.Grid) {
cellX, _ := grid.CellPosition() cellX, _ := grid.CellPosition()
_, dimY := grid.Model().Dimensions() _, dimY := grid.Model().Dimensions()
grid.MoveTo(cellX, dimY - 1) grid.MoveTo(cellX, dimY-1)
})) }))
cm.Define("col-left", "Moves the cursor to the left-most column", "", gridNavOperation(func(grid *ui.Grid) { cm.Define("col-left", "Moves the cursor to the left-most column", "", gridNavOperation(func(grid *ui.Grid) {
_, cellY := grid.CellPosition() _, cellY := grid.CellPosition()
grid.MoveTo(0, cellY) grid.MoveTo(0, cellY)
})) }))
cm.Define("col-right", "Moves the cursor to the right-most column", "", gridNavOperation(func(grid *ui.Grid) { cm.Define("col-right", "Moves the cursor to the right-most column", "", gridNavOperation(func(grid *ui.Grid) {
_, cellY := grid.CellPosition() _, cellY := grid.CellPosition()
dimX, _ := grid.Model().Dimensions() dimX, _ := grid.Model().Dimensions()
grid.MoveTo(dimX - 1, cellY) grid.MoveTo(dimX-1, cellY)
})) }))
cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error {
ctx.Frame().Prompt(": ", func(res string) {
ctx.Frame().Message("Command = " + res)
})
return nil
})
cm.Define("set-cell", "Change the value of the selected cell", "", func(ctx *CommandContext) error {
grid := ctx.Frame().Grid()
cellX, cellY := grid.CellPosition()
if rwModel, isRwModel := ctx.Session().Model.(RWModel); isRwModel {
ctx.Frame().Prompt("> ", func(res string) {
rwModel.SetCellValue(cellY, cellX, res)
})
}
return nil
})
cm.Define("quit", "Quit TED", "", func(ctx *CommandContext) error {
ctx.Session().UIManager.Shutdown()
return nil
})
} }
// Registers the standard view key bindings. These commands require the frame // Registers the standard view key bindings. These commands require the frame
func (cm *CommandMapping) RegisterViewKeyBindings() { func (cm *CommandMapping) RegisterViewKeyBindings() {
cm.MapKey('i', cm.Command("move-up")) cm.MapKey('i', cm.Command("move-up"))
cm.MapKey('k', cm.Command("move-down")) cm.MapKey('k', cm.Command("move-down"))
cm.MapKey('j', cm.Command("move-left")) cm.MapKey('j', cm.Command("move-left"))
cm.MapKey('l', cm.Command("move-right")) cm.MapKey('l', cm.Command("move-right"))
cm.MapKey('I', cm.Command("page-up")) cm.MapKey('I', cm.Command("page-up"))
cm.MapKey('K', cm.Command("page-down")) cm.MapKey('K', cm.Command("page-down"))
cm.MapKey('J', cm.Command("page-left")) cm.MapKey('J', cm.Command("page-left"))
cm.MapKey('L', cm.Command("page-right")) cm.MapKey('L', cm.Command("page-right"))
cm.MapKey(ui.KeyCtrlI, cm.Command("row-top")) cm.MapKey(ui.KeyCtrlI, cm.Command("row-top"))
cm.MapKey(ui.KeyCtrlK, cm.Command("row-bottom")) cm.MapKey(ui.KeyCtrlK, cm.Command("row-bottom"))
cm.MapKey(ui.KeyCtrlJ, cm.Command("col-left")) cm.MapKey(ui.KeyCtrlJ, cm.Command("col-left"))
cm.MapKey(ui.KeyCtrlL, cm.Command("col-right")) cm.MapKey(ui.KeyCtrlL, cm.Command("col-right"))
cm.MapKey(ui.KeyArrowUp, cm.Command("move-up")) cm.MapKey(ui.KeyArrowUp, cm.Command("move-up"))
cm.MapKey(ui.KeyArrowDown, cm.Command("move-down")) cm.MapKey(ui.KeyArrowDown, cm.Command("move-down"))
cm.MapKey(ui.KeyArrowLeft, cm.Command("move-left")) cm.MapKey(ui.KeyArrowLeft, cm.Command("move-left"))
cm.MapKey(ui.KeyArrowRight, cm.Command("move-right")) cm.MapKey(ui.KeyArrowRight, cm.Command("move-right"))
cm.MapKey(':', cm.Command("enter-command")) cm.MapKey('e', cm.Command("set-cell"))
cm.MapKey(':', cm.Command("enter-command"))
cm.MapKey('q', cm.Command("quit"))
} }
// A nativation command factory. This will perform the passed in operation with the current grid and // A nativation command factory. This will perform the passed in operation with the current grid and
// will display the cell value in the message box. // will display the cell value in the message box.
func gridNavOperation(op func(grid *ui.Grid)) func(ctx CommandContext) error { func gridNavOperation(op func(grid *ui.Grid)) func(ctx *CommandContext) error {
return func(ctx CommandContext) error { return func(ctx *CommandContext) error {
op(ctx.Frame().Grid()) op(ctx.Frame().Grid())
ctx.Frame().ShowCellValue() ctx.Frame().ShowCellValue()
return nil return nil
} }
} }

130
frame.go
View file

@ -1,89 +1,133 @@
package main package main
import ( import (
"./ui" "bitbucket.org/lmika/ted-v2/ui"
) )
type Mode int type Mode int
const ( const (
// The grid is selectable NilMode Mode = iota
GridMode Mode = iota
// The grid is selectable
GridMode
// EntryMode is when the text entry is selected
EntryMode
) )
// A frame is a UI instance. // A frame is a UI instance.
type Frame struct { type Frame struct {
Session *Session Session *Session
uiManager *ui.Ui mode Mode
clientArea *ui.RelativeLayout
grid *ui.Grid uiManager *ui.Ui
messageView *ui.TextView clientArea *ui.RelativeLayout
textEntry *ui.TextEntry grid *ui.Grid
statusBar *ui.StatusBar messageView *ui.TextView
textEntrySwitch *ui.ProxyLayout textEntry *ui.TextEntry
statusBar *ui.StatusBar
textEntrySwitch *ui.ProxyLayout
} }
// Creates the UI and returns a new frame // Creates the UI and returns a new frame
func NewFrame(uiManager *ui.Ui) *Frame { func NewFrame(uiManager *ui.Ui) *Frame {
frame := &Frame{ frame := &Frame{
uiManager: uiManager, uiManager: uiManager,
} }
frame.grid = ui.NewGrid(nil) frame.grid = ui.NewGrid(nil)
frame.messageView = &ui.TextView{"Hello"} frame.messageView = &ui.TextView{"Hello"}
frame.statusBar = &ui.StatusBar{"Test", "Status"} frame.statusBar = &ui.StatusBar{"Test", "Status"}
frame.textEntrySwitch = &ui.ProxyLayout{frame.messageView} frame.textEntrySwitch = &ui.ProxyLayout{frame.messageView}
frame.textEntry = &ui.TextEntry{} frame.textEntry = &ui.TextEntry{}
// Build the UI frame // Build the UI frame
statusLayout := &ui.VertLinearLayout{} statusLayout := &ui.VertLinearLayout{}
statusLayout.Append(frame.statusBar) statusLayout.Append(frame.statusBar)
statusLayout.Append(frame.textEntrySwitch) statusLayout.Append(frame.textEntrySwitch)
frame.clientArea = &ui.RelativeLayout{ Client: frame.grid, South: statusLayout } frame.clientArea = &ui.RelativeLayout{Client: frame.grid, South: statusLayout}
return frame return frame
} }
// Returns the root component of the frame // Returns the root component of the frame
func (frame *Frame) RootComponent() ui.UiComponent { func (frame *Frame) RootComponent() ui.UiComponent {
return frame.clientArea return frame.clientArea
} }
// Sets the current model of the frame // Sets the current model of the frame
func (frame *Frame) SetModel(model ui.GridModel) { func (frame *Frame) SetModel(model ui.GridModel) {
frame.grid.SetModel(model) frame.grid.SetModel(model)
} }
// Returns the grid component // Returns the grid component
func (frame *Frame) Grid() *ui.Grid { func (frame *Frame) Grid() *ui.Grid {
return frame.grid return frame.grid
} }
// Sets the specific mode. // Enter the specific mode.
func (frame *Frame) EnterMode(mode Mode) { func (frame *Frame) enterMode(mode Mode) {
switch mode { switch mode {
case GridMode: case GridMode:
frame.uiManager.SetFocusedComponent(frame) frame.uiManager.SetFocusedComponent(frame)
} case EntryMode:
frame.textEntrySwitch.Component = frame.textEntry
frame.uiManager.SetFocusedComponent(frame.textEntry)
}
}
// Exit the specific mode.
func (frame *Frame) exitMode(mode Mode) {
switch mode {
case EntryMode:
frame.textEntrySwitch.Component = frame.messageView
}
}
func (frame *Frame) setMode(mode Mode) {
frame.exitMode(frame.mode)
frame.mode = mode
frame.enterMode(frame.mode)
}
// Message sets the message view's message
func (frame *Frame) Message(s string) {
frame.messageView.Text = s
}
// Prompt the user for input. This switches the mode to entry mode.
func (frame *Frame) Prompt(prompt string, callback func(res string)) {
frame.textEntry.Prompt = prompt
frame.textEntry.SetValue("")
frame.textEntry.OnEntry = func(res string) {
frame.textEntry.OnEntry = nil
frame.setMode(GridMode)
callback(res)
}
frame.setMode(EntryMode)
} }
// Show a message. This will switch the bottom to the messageView and select the frame // Show a message. This will switch the bottom to the messageView and select the frame
func (frame *Frame) ShowMessage(msg string) { func (frame *Frame) ShowMessage(msg string) {
frame.messageView.Text = msg frame.messageView.Text = msg
frame.textEntrySwitch.Component = frame.messageView frame.textEntrySwitch.Component = frame.messageView
//frame.EnterMode(GridMode) //frame.EnterMode(GridMode)
} }
// Shows the value of the currently select grid cell // Shows the value of the currently select grid cell
func (frame *Frame) ShowCellValue() { func (frame *Frame) ShowCellValue() {
displayValue := frame.grid.CurrentCellDisplayValue() displayValue := frame.grid.CurrentCellDisplayValue()
frame.ShowMessage(displayValue) frame.ShowMessage(displayValue)
} }
// Handle the main grid input as this is the "component" that handles command input. // Handle the main grid input as this is the "component" that handles command input.
func (frame *Frame) KeyPressed(key rune, mod int) { func (frame *Frame) KeyPressed(key rune, mod int) {
if frame.Session != nil { if frame.Session != nil {
frame.Session.KeyPressed(key, mod) frame.Session.KeyPressed(key, mod)
} }
} }

25
main.go
View file

@ -1,23 +1,24 @@
package main package main
import ( import (
"./ui" "bitbucket.org/lmika/ted-v2/ui"
) )
func main() { func main() {
uiManager, err := ui.NewUI() uiManager, err := ui.NewUI()
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer uiManager.Close() defer uiManager.Close()
model := &StdModel{} model := &StdModel{}
model.Resize(5, 5)
frame := NewFrame(uiManager) frame := NewFrame(uiManager)
NewSession(frame, model) NewSession(uiManager, frame, model)
uiManager.SetRootComponent(frame.RootComponent()) uiManager.SetRootComponent(frame.RootComponent())
frame.EnterMode(GridMode) frame.enterMode(GridMode)
uiManager.Loop() uiManager.Loop()
} }

View file

@ -1,83 +1,86 @@
package main package main
import "./ui" import "bitbucket.org/lmika/ted-v2/ui"
// The session is responsible for managing the UI and the model and handling // The session is responsible for managing the UI and the model and handling
// the interaction between the two and the user. // the interaction between the two and the user.
type Session struct { type Session struct {
Model Model Model Model
Frame *Frame Frame *Frame
Commands *CommandMapping Commands *CommandMapping
UIManager *ui.Ui
} }
func NewSession(frame *Frame, model Model) *Session { func NewSession(uiManager *ui.Ui, frame *Frame, model Model) *Session {
session := &Session{ session := &Session{
Model: model, Model: model,
Frame: frame, Frame: frame,
Commands: NewCommandMapping(), Commands: NewCommandMapping(),
} UIManager: uiManager,
}
frame.SetModel(&SessionGridModel{session}) frame.SetModel(&SessionGridModel{session})
session.Commands.RegisterViewCommands() session.Commands.RegisterViewCommands()
session.Commands.RegisterViewKeyBindings() session.Commands.RegisterViewKeyBindings()
// Also assign this session with the frame // Also assign this session with the frame
frame.Session = session frame.Session = session
return session return session
} }
// Input from the frame // Input from the frame
func (session *Session) KeyPressed(key rune, mod int) { func (session *Session) KeyPressed(key rune, mod int) {
// Add the mod key modifier // Add the mod key modifier
if (mod & ui.ModKeyAlt != 0) { if mod&ui.ModKeyAlt != 0 {
key |= ModAlt key |= ModAlt
} }
cmd := session.Commands.KeyMapping(key) cmd := session.Commands.KeyMapping(key)
if cmd != nil { if cmd != nil {
err := cmd.Do(SessionCommandContext{session}) err := cmd.Do(&CommandContext{session})
if err != nil { if err != nil {
session.Frame.ShowMessage(err.Error()) session.Frame.ShowMessage(err.Error())
} }
} }
} }
// The command context used by the session // The command context used by the session
type SessionCommandContext struct { type CommandContext struct {
Session *Session session *Session
} }
func (scc SessionCommandContext) Frame() *Frame { func (scc *CommandContext) Session() *Session {
return scc.Session.Frame return scc.session
} }
func (scc *CommandContext) Frame() *Frame {
return scc.session.Frame
}
// Session grid model // Session grid model
type SessionGridModel struct { type SessionGridModel struct {
Session *Session Session *Session
} }
// Returns the size of the grid model (width x height) // Returns the size of the grid model (width x height)
func (sgm *SessionGridModel) Dimensions() (int, int) { func (sgm *SessionGridModel) Dimensions() (int, int) {
rs, cs := sgm.Session.Model.Dimensions() rs, cs := sgm.Session.Model.Dimensions()
return cs, rs return cs, rs
} }
// Returns the size of the particular column. If the size is 0, this indicates that the column is hidden. // Returns the size of the particular column. If the size is 0, this indicates that the column is hidden.
func (sgm *SessionGridModel) ColWidth(int) int { func (sgm *SessionGridModel) ColWidth(int) int {
return 24 return 24
} }
// Returns the size of the particular row. If the size is 0, this indicates that the row is hidden. // Returns the size of the particular row. If the size is 0, this indicates that the row is hidden.
func (sgm *SessionGridModel) RowHeight(int) int { func (sgm *SessionGridModel) RowHeight(int) int {
return 1 return 1
} }
// Returns the value of the cell a position X, Y // Returns the value of the cell a position X, Y
func (sgm *SessionGridModel) CellValue(x int, y int) string { func (sgm *SessionGridModel) CellValue(x int, y int) string {
return sgm.Session.Model.CellValue(y, x) return sgm.Session.Model.CellValue(y, x)
} }

View file

@ -6,87 +6,90 @@ package ui
// drawing within it's context. // drawing within it's context.
type DrawContext struct { type DrawContext struct {
// The left and top position of the context. // The left and top position of the context.
X, Y int X, Y int
// The width and height position of the context. // The width and height position of the context.
W, H int W, H int
// The current driver // The current driver
driver Driver driver Driver
// The current foregound and background attributes // The current foregound and background attributes
fa, ba Attribute fa, ba Attribute
} }
// Returns a new subcontext. The sub-context must be an area within the current context. // Returns a new subcontext. The sub-context must be an area within the current context.
func (dc *DrawContext) NewSubContext(offsetX, offsetY, width, height int) *DrawContext { func (dc *DrawContext) NewSubContext(offsetX, offsetY, width, height int) *DrawContext {
return &DrawContext{ return &DrawContext{
X: intMax(dc.X + offsetX, dc.X), X: intMax(dc.X+offsetX, dc.X),
Y: intMax(dc.Y + offsetY, dc.Y), Y: intMax(dc.Y+offsetY, dc.Y),
W: intMax(width, 0), W: intMax(width, 0),
H: intMax(height, 0), H: intMax(height, 0),
driver: dc.driver, driver: dc.driver,
} }
} }
// Sets the foreground attribute // Sets the foreground attribute
func (dc *DrawContext) SetFgAttr(attr Attribute) { func (dc *DrawContext) SetFgAttr(attr Attribute) {
dc.fa = attr dc.fa = attr
} }
// Sets the background attribute // Sets the background attribute
func (dc *DrawContext) SetBgAttr(attr Attribute) { func (dc *DrawContext) SetBgAttr(attr Attribute) {
dc.ba = attr dc.ba = attr
} }
// Draws a horizontal rule with a specific rune. // Draws a horizontal rule with a specific rune.
func (dc *DrawContext) HorizRule(y int, ch rune) { func (dc *DrawContext) HorizRule(y int, ch rune) {
for x := 0; x < dc.W; x++ { for x := 0; x < dc.W; x++ {
dc.DrawRune(x, y, ch) dc.DrawRune(x, y, ch)
} }
} }
// Prints a string at a specific offset. This will be bounded by the size of the drawing context. // Prints a string at a specific offset. This will be bounded by the size of the drawing context.
func (dc *DrawContext) Print(x, y int, str string) { func (dc *DrawContext) Print(x, y int, str string) {
for _, ch := range str { for _, ch := range str {
dc.DrawRune(x, y, ch) dc.DrawRune(x, y, ch)
x++ x++
} }
} }
// Prints a right-justified string at a specific offset. This will be bounded by the size of the drawing context. // Prints a right-justified string at a specific offset. This will be bounded by the size of the drawing context.
func (dc *DrawContext) PrintRight(x, y int, str string) { func (dc *DrawContext) PrintRight(x, y int, str string) {
l := len(str) l := len(str)
dc.Print(x - l, y, str) dc.Print(x-l, y, str)
} }
// Draws a rune at a local point X, Y with the current foreground and background attributes // Draws a rune at a local point X, Y with the current foreground and background attributes
func (dc *DrawContext) DrawRune(x, y int, ch rune) { func (dc *DrawContext) DrawRune(x, y int, ch rune) {
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext { if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
dc.driver.SetCell(rx, ry, ch, dc.fa, dc.ba) dc.driver.SetCell(rx, ry, ch, dc.fa, dc.ba)
} }
} }
// Draws a rune at a local point X, Y with specific foreground and background attributes // Draws a rune at a local point X, Y with specific foreground and background attributes
func (dc *DrawContext) DrawRuneWithAttrs(x, y int, ch rune, fa, ba Attribute) { func (dc *DrawContext) DrawRuneWithAttrs(x, y int, ch rune, fa, ba Attribute) {
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext { if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
dc.driver.SetCell(rx, ry, ch, fa, ba) dc.driver.SetCell(rx, ry, ch, fa, ba)
} }
} }
// Set the position of the cursor // Set the position of the cursor
func (dc *DrawContext) SetCursorPosition(x, y int) { func (dc *DrawContext) SetCursorPosition(x, y int) {
if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext { if rx, ry, isWithinContext := dc.localPointToRealPoint(x, y); isWithinContext {
dc.driver.SetCursor(rx, ry) dc.driver.SetCursor(rx, ry)
} }
}
// HideCursor hides the cursor
func (dc *DrawContext) HideCursor() {
dc.driver.HideCursor()
} }
// Converts a local point to a real point. If the point is within the context, also returns true // Converts a local point to a real point. If the point is within the context, also returns true
func (dc *DrawContext) localPointToRealPoint(x, y int) (int, int, bool) { func (dc *DrawContext) localPointToRealPoint(x, y int) (int, int, bool) {
rx, ry := x + dc.X, y + dc.Y rx, ry := x+dc.X, y+dc.Y
return rx, ry, (rx >= dc.X) && (ry >= dc.Y) && (rx < dc.X + dc.W) && (ry < dc.Y + dc.H) return rx, ry, (rx >= dc.X) && (ry >= dc.Y) && (rx < dc.X+dc.W) && (ry < dc.Y+dc.H)
} }

View file

@ -3,140 +3,141 @@
package ui package ui
// The set of attributes a specific cell can have // The set of attributes a specific cell can have
type Attribute uint16 type Attribute uint16
const ( const (
// Can have only one of these // Can have only one of these
ColorDefault Attribute = iota ColorDefault Attribute = iota
ColorBlack ColorBlack
ColorRed ColorRed
ColorGreen ColorGreen
ColorYellow ColorYellow
ColorBlue ColorBlue
ColorMagenta ColorMagenta
ColorCyan ColorCyan
ColorWhite ColorWhite
) )
// and zero or more of these (combined using OR '|') // and zero or more of these (combined using OR '|')
const ( const (
AttrBold Attribute = 1 << (iota + 9) AttrBold Attribute = 1 << (iota + 9)
AttrUnderline AttrUnderline
AttrReverse AttrReverse
) )
// Special keys // Special keys
const ( const (
KeyCtrlSpace rune = 0x8000 + iota KeyCtrlSpace rune = 0x8000 + iota
KeyCtrlA KeyCtrlA
KeyCtrlB KeyCtrlB
KeyCtrlC KeyCtrlC
KeyCtrlD KeyCtrlD
KeyCtrlE KeyCtrlE
KeyCtrlF KeyCtrlF
KeyCtrlG KeyCtrlG
KeyCtrlH KeyCtrlH
KeyCtrlI KeyCtrlI
KeyCtrlJ KeyCtrlJ
KeyCtrlK KeyCtrlK
KeyCtrlL KeyCtrlL
KeyCtrlM KeyCtrlM
KeyCtrlN KeyCtrlN
KeyCtrlO KeyCtrlO
KeyCtrlP KeyCtrlP
KeyCtrlQ KeyCtrlQ
KeyCtrlR KeyCtrlR
KeyCtrlS KeyCtrlS
KeyCtrlT KeyCtrlT
KeyCtrlU KeyCtrlU
KeyCtrlV KeyCtrlV
KeyCtrlW KeyCtrlW
KeyCtrlX KeyCtrlX
KeyCtrlY KeyCtrlY
KeyCtrlZ KeyCtrlZ
KeyCtrl3 KeyCtrl3
KeyCtrl4 KeyCtrl4
KeyCtrl5 KeyCtrl5
KeyCtrl6 KeyCtrl6
KeyCtrl7 KeyCtrl7
KeyCtrl8 KeyCtrl8
KeyF1 KeyF1
KeyF2 KeyF2
KeyF3 KeyF3
KeyF4 KeyF4
KeyF5 KeyF5
KeyF6 KeyF6
KeyF7 KeyF7
KeyF8 KeyF8
KeyF9 KeyF9
KeyF10 KeyF10
KeyF11 KeyF11
KeyF12 KeyF12
KeyInsert KeyInsert
KeyDelete KeyDelete
KeyHome KeyHome
KeyEnd KeyEnd
KeyPgup KeyPgup
KeyPgdn KeyPgdn
KeyArrowUp KeyArrowUp
KeyArrowDown KeyArrowDown
KeyArrowLeft KeyArrowLeft
KeyArrowRight KeyArrowRight
KeyBackspace = KeyCtrlH KeyBackspace = KeyCtrlH
KeyBackspace2 = KeyCtrl8 KeyBackspace2 = KeyCtrl8
KeyEnter = KeyCtrlM KeyEnter = KeyCtrlM
) )
// The type of events supported by the driver // The type of events supported by the driver
type EventType int type EventType int
const ( const (
EventNone EventType = iota EventNone EventType = iota
// Event when the window is resized // Event when the window is resized
EventResize EventResize
// Event indicating a key press. The key is set in Ch and modifications // Event indicating a key press. The key is set in Ch and modifications
// are set in Or // are set in Or
EventKeyPress EventKeyPress
) )
const ( const (
ModKeyAlt int = (1 << iota) ModKeyAlt int = (1 << iota)
) )
// Data from an event callback. // Data from an event callback.
type Event struct { type Event struct {
Type EventType Type EventType
Par int Par int
Ch rune Ch rune
} }
// The terminal driver interface. // The terminal driver interface.
type Driver interface { type Driver interface {
// Initializes the driver. Returns an error if there was an error // Initializes the driver. Returns an error if there was an error
Init() error Init() error
// Closes the driver // Closes the driver
Close() Close()
// Returns the size of the window. // Returns the size of the window.
Size() (int, int) Size() (int, int)
// Sets the value of a specific cell // Sets the value of a specific cell
SetCell(x, y int, ch rune, fg, bg Attribute) SetCell(x, y int, ch rune, fg, bg Attribute)
// Synchronizes the internal buffer with the real buffer // Synchronizes the internal buffer with the real buffer
Sync() Sync()
// Wait for an event // Wait for an event
WaitForEvent() Event WaitForEvent() Event
// Move the position of the cursor // Move the position of the cursor
SetCursor(x, y int) SetCursor(x, y int)
// Hide the cursor
HideCursor()
} }

View file

@ -5,407 +5,396 @@ package ui
import "strconv" import "strconv"
/** /**
* An abstract display model. * An abstract display model.
*/ */
type GridModel interface { type GridModel interface {
/** /**
* Returns the size of the grid model (width x height) * Returns the size of the grid model (width x height)
*/ */
Dimensions() (int, int) Dimensions() (int, int)
/** /**
* Returns the size of the particular column. If the size is 0, this indicates that the column is hidden. * Returns the size of the particular column. If the size is 0, this indicates that the column is hidden.
*/ */
ColWidth(int) int ColWidth(int) int
/** /**
* Returns the size of the particular row. If the size is 0, this indicates that the row is hidden. * Returns the size of the particular row. If the size is 0, this indicates that the row is hidden.
*/ */
RowHeight(int) int RowHeight(int) int
/** /**
* Returns the value of the cell a position X, Y * Returns the value of the cell a position X, Y
*/ */
CellValue(int, int) string CellValue(int, int) string
} }
type gridPoint int
type gridPoint int
/** /**
* The grid component. * The grid component.
*/ */
type Grid struct { type Grid struct {
model GridModel // The grid model model GridModel // The grid model
viewCellX int // Left most cell viewCellX int // Left most cell
viewCellY int // Top most cell viewCellY int // Top most cell
selCellX int // The currently selected cell selCellX int // The currently selected cell
selCellY int selCellY int
cellsWide int // Measured number of cells. Recalculated on redraw. cellsWide int // Measured number of cells. Recalculated on redraw.
cellsHigh int cellsHigh int
} }
/** /**
* Clipping rectangle * Clipping rectangle
*/ */
type gridRect struct { type gridRect struct {
x1 gridPoint x1 gridPoint
y1 gridPoint y1 gridPoint
x2 gridPoint x2 gridPoint
y2 gridPoint y2 gridPoint
} }
/** /**
* Creates a new gridRect from integers. * Creates a new gridRect from integers.
*/ */
func newGridRect(x1, y1, x2, y2 int) gridRect { func newGridRect(x1, y1, x2, y2 int) gridRect {
return gridRect{gridPoint(x1), gridPoint(y1), gridPoint(x2), gridPoint(y2)} return gridRect{gridPoint(x1), gridPoint(y1), gridPoint(x2), gridPoint(y2)}
} }
/** /**
* Creates a new grid. * Creates a new grid.
*/ */
func NewGrid(model GridModel) *Grid { func NewGrid(model GridModel) *Grid {
return &Grid{model, 0, 0, 0, 0, -1, -1} return &Grid{model, 0, 0, 0, 0, -1, -1}
} }
// Returns the model // Returns the model
func (grid *Grid) Model() GridModel { func (grid *Grid) Model() GridModel {
return grid.model return grid.model
} }
// Sets the model // Sets the model
func (grid *Grid) SetModel(model GridModel) { func (grid *Grid) SetModel(model GridModel) {
grid.model = model grid.model = model
} }
/** /**
* Shifts the viewport of the grid. * Shifts the viewport of the grid.
*/ */
func (grid *Grid) ShiftBy(x int, y int) { func (grid *Grid) ShiftBy(x int, y int) {
grid.viewCellX += x grid.viewCellX += x
grid.viewCellY += y grid.viewCellY += y
} }
// Returns the display value of the currently selected cell. // Returns the display value of the currently selected cell.
func (grid *Grid) CurrentCellDisplayValue() string { func (grid *Grid) CurrentCellDisplayValue() string {
if grid.isCellValid(grid.selCellX, grid.selCellY) { if grid.isCellValid(grid.selCellX, grid.selCellY) {
return grid.model.CellValue(grid.selCellX, grid.selCellY) return grid.model.CellValue(grid.selCellX, grid.selCellY)
} else { } else {
return "" return ""
} }
} }
// Moves the currently selected cell by a delta. This will be implemented as single stepped // Moves the currently selected cell by a delta. This will be implemented as single stepped
// moveTo calls to handle invalid cells. // moveTo calls to handle invalid cells.
func (grid *Grid) MoveBy(x int, y int) { func (grid *Grid) MoveBy(x int, y int) {
grid.MoveTo(grid.selCellX + x, grid.selCellY + y) grid.MoveTo(grid.selCellX+x, grid.selCellY+y)
} }
// Moves the currently selected cell to a specific row. The row must be valid, otherwise the // Moves the currently selected cell to a specific row. The row must be valid, otherwise the
// currently selected cell will not be changed. Returns true if the move was successful // currently selected cell will not be changed. Returns true if the move was successful
func (grid *Grid) MoveTo(newX, newY int) { func (grid *Grid) MoveTo(newX, newY int) {
maxX, maxY := grid.model.Dimensions() maxX, maxY := grid.model.Dimensions()
newX = intMinMax(newX, 0, maxX - 1) newX = intMinMax(newX, 0, maxX-1)
newY = intMinMax(newY, 0, maxY - 1) newY = intMinMax(newY, 0, maxY-1)
if grid.isCellValid(newX, newY) { if grid.isCellValid(newX, newY) {
grid.selCellX = newX grid.selCellX = newX
grid.selCellY = newY grid.selCellY = newY
grid.reposition() grid.reposition()
} }
} }
// Returns the currently selected cell position. // Returns the currently selected cell position.
func (grid *Grid) CellPosition() (int, int) { func (grid *Grid) CellPosition() (int, int) {
return grid.selCellX, grid.selCellY return grid.selCellX, grid.selCellY
} }
// Returns true if the user can enter the specific cell // Returns true if the user can enter the specific cell
func (grid *Grid) isCellValid(x int, y int) bool { func (grid *Grid) isCellValid(x int, y int) bool {
maxX, maxY := grid.model.Dimensions() maxX, maxY := grid.model.Dimensions()
return (x >= 0) && (y >= 0) && (x < maxX) && (y < maxY) return (x >= 0) && (y >= 0) && (x < maxX) && (y < maxY)
} }
// Determine the topmost cell based on the location of the currently selected cell // Determine the topmost cell based on the location of the currently selected cell
func (grid *Grid) reposition() { func (grid *Grid) reposition() {
// If we have no measurement information, forget it. // If we have no measurement information, forget it.
if (grid.cellsWide == -1) || (grid.cellsHigh == -1) { if (grid.cellsWide == -1) || (grid.cellsHigh == -1) {
return return
} }
if grid.selCellX < grid.viewCellX { if grid.selCellX < grid.viewCellX {
grid.viewCellX = grid.selCellX grid.viewCellX = grid.selCellX
} else if grid.selCellX >= (grid.viewCellX + grid.cellsWide - 3) { } else if grid.selCellX >= (grid.viewCellX + grid.cellsWide - 3) {
grid.viewCellX = grid.selCellX - (grid.cellsWide - 3) grid.viewCellX = grid.selCellX - (grid.cellsWide - 3)
} }
if grid.selCellY < grid.viewCellY { if grid.selCellY < grid.viewCellY {
grid.viewCellY = grid.selCellY grid.viewCellY = grid.selCellY
} else if grid.selCellY >= (grid.viewCellY + grid.cellsHigh - 3) { } else if grid.selCellY >= (grid.viewCellY + grid.cellsHigh - 3) {
grid.viewCellY = grid.selCellY - (grid.cellsHigh - 3) grid.viewCellY = grid.selCellY - (grid.cellsHigh - 3)
} }
} }
// Gets the cell value and attributes of a particular cell // Gets the cell value and attributes of a particular cell
func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) { func (grid *Grid) getCellData(cellX, cellY int) (text string, fg, bg Attribute) {
// The fixed cells // The fixed cells
modelCellX := cellX - 1 + grid.viewCellX modelCellX := cellX - 1 + grid.viewCellX
modelCellY := cellY - 1 + grid.viewCellY modelCellY := cellY - 1 + grid.viewCellY
modelMaxX, modelMaxY := grid.model.Dimensions() modelMaxX, modelMaxY := grid.model.Dimensions()
if (cellX == 0) && (cellY == 0) { if (cellX == 0) && (cellY == 0) {
return "", AttrBold, AttrBold return "", AttrBold, AttrBold
} else if (cellX == 0) { } else if cellX == 0 {
if (modelCellY == grid.selCellY) { if modelCellY == grid.selCellY {
return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrBold | AttrReverse return strconv.Itoa(modelCellY), AttrBold | AttrReverse, AttrReverse
} else { } else {
return strconv.Itoa(modelCellY), AttrBold, AttrBold return strconv.Itoa(modelCellY), AttrBold, 0
} }
} else if (cellY == 0) { } else if cellY == 0 {
if (modelCellX == grid.selCellX) { if modelCellX == grid.selCellX {
return strconv.Itoa(modelCellX), AttrBold | AttrReverse, AttrBold | AttrReverse return strconv.Itoa(modelCellX), AttrBold | AttrReverse, AttrReverse
} else { } else {
return strconv.Itoa(modelCellX), AttrBold, AttrBold return strconv.Itoa(modelCellX), AttrBold, 0
} }
} else { } else {
// The data from the model // The data from the model
if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) { if (modelCellX >= 0) && (modelCellY >= 0) && (modelCellX < modelMaxX) && (modelCellY < modelMaxY) {
if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) { if (modelCellX == grid.selCellX) && (modelCellY == grid.selCellY) {
return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse return grid.model.CellValue(modelCellX, modelCellY), AttrReverse, AttrReverse
} else { } else {
return grid.model.CellValue(modelCellX, modelCellY), 0, 0 return grid.model.CellValue(modelCellX, modelCellY), 0, 0
} }
} else { } else {
return "~", ColorBlue | AttrBold, 0 return "~", ColorBlue | AttrBold, 0
} }
} }
} }
// Gets the cell dimensions // Gets the cell dimensions
func (grid *Grid) getCellDimensions(cellX, cellY int) (width, height int) { func (grid *Grid) getCellDimensions(cellX, cellY int) (width, height int) {
var cellWidth, cellHeight int var cellWidth, cellHeight int
modelCellX := cellX - 1 + grid.viewCellX modelCellX := cellX - 1 + grid.viewCellX
modelCellY := cellY - 1 + grid.viewCellY modelCellY := cellY - 1 + grid.viewCellY
modelMaxX, modelMaxY := grid.model.Dimensions() modelMaxX, modelMaxY := grid.model.Dimensions()
// Get the cell width & height from model (if within range) // Get the cell width & height from model (if within range)
if (modelCellX >= 0) && (modelCellX < modelMaxX) { if (modelCellX >= 0) && (modelCellX < modelMaxX) {
cellWidth = grid.model.ColWidth(modelCellX) cellWidth = grid.model.ColWidth(modelCellX)
} else { } else {
cellWidth = 8 cellWidth = 8
} }
if (modelCellY >= 0) && (modelCellY < modelMaxY) { if (modelCellY >= 0) && (modelCellY < modelMaxY) {
cellHeight = grid.model.RowHeight(modelCellY) cellHeight = grid.model.RowHeight(modelCellY)
} else { } else {
cellHeight = 2 cellHeight = 2
} }
if (cellX == 0) && (cellY == 0) { if (cellX == 0) && (cellY == 0) {
return 8, 1 return 8, 1
} else if (cellX == 0) { } else if cellX == 0 {
return 8, cellHeight return 8, cellHeight
} else if (cellY == 0) { } else if cellY == 0 {
return cellWidth, 1 return cellWidth, 1
} else { } else {
return cellWidth, cellHeight return cellWidth, cellHeight
} }
// XXX: Workaround for bug in compiler // XXX: Workaround for bug in compiler
panic("Unreachable code") panic("Unreachable code")
return 0, 0 return 0, 0
} }
/** /**
* Renders a cell which contains text. The clip rectangle defines the size of the cell, as well as the left offset * Renders a cell which contains text. The clip rectangle defines the size of the cell, as well as the left offset
* of the cell. The sx and sy determine the screen position of the cell top-left. * of the cell. The sx and sy determine the screen position of the cell top-left.
*/ */
func (grid *Grid) renderCell(ctx *DrawContext, cellClipRect gridRect, sx int, sy int, text string, fg, bg Attribute) { func (grid *Grid) renderCell(ctx *DrawContext, cellClipRect gridRect, sx int, sy int, text string, fg, bg Attribute) {
for x := cellClipRect.x1; x <= cellClipRect.x2; x++ { for x := cellClipRect.x1; x <= cellClipRect.x2; x++ {
for y := cellClipRect.y1; y <= cellClipRect.y2; y++ { for y := cellClipRect.y1; y <= cellClipRect.y2; y++ {
currRune := ' ' currRune := ' '
if (y == 0) { if y == 0 {
textPos := int(x) textPos := int(x)
if textPos < len(text) { if textPos < len(text) {
currRune = rune(text[textPos]) currRune = rune(text[textPos])
} }
} }
// TODO: This might be better if this wasn't so low-level // TODO: This might be better if this wasn't so low-level
ctx.DrawRuneWithAttrs(int(x - cellClipRect.x1) + sx, int(y - cellClipRect.y1) + sy, currRune, fg, bg) ctx.DrawRuneWithAttrs(int(x-cellClipRect.x1)+sx, int(y-cellClipRect.y1)+sy, currRune, fg, bg)
} }
} }
} }
// Renders a column. The viewport determines the maximum position of the rendered cell. CellX and CellY are the // Renders a column. The viewport determines the maximum position of the rendered cell. CellX and CellY are the
// cell indicies to render, cellOffset are the LOCAL offset of the cell. // cell indicies to render, cellOffset are the LOCAL offset of the cell.
// This function will return the new X position (gridRect.x1 + colWidth) // This function will return the new X position (gridRect.x1 + colWidth)
func (grid *Grid) renderColumn(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (gridPoint, int) { func (grid *Grid) renderColumn(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (gridPoint, int) {
// The top-left position of the column // The top-left position of the column
screenX := int(screenViewPort.x1) screenX := int(screenViewPort.x1)
screenY := int(screenViewPort.y1) screenY := int(screenViewPort.y1)
screenWidth := int(screenViewPort.x2 - screenViewPort.x1) screenWidth := int(screenViewPort.x2 - screenViewPort.x1)
screenHeight := int(screenViewPort.y2 - screenViewPort.y1) screenHeight := int(screenViewPort.y2 - screenViewPort.y1)
// Work out the column width and cap it if it will spill over the edge of the viewport // Work out the column width and cap it if it will spill over the edge of the viewport
colWidth, _ := grid.getCellDimensions(cellX, cellY) colWidth, _ := grid.getCellDimensions(cellX, cellY)
colWidth -= cellOffsetX colWidth -= cellOffsetX
if colWidth > screenWidth { if colWidth > screenWidth {
colWidth = screenHeight colWidth = screenHeight
} }
// The maximum // The maximum
maxScreenY := screenY + screenHeight maxScreenY := screenY + screenHeight
cellsHigh := 0 cellsHigh := 0
for screenY < maxScreenY { for screenY < maxScreenY {
// Cap the row height if it will go beyond the edge of the viewport. // Cap the row height if it will go beyond the edge of the viewport.
_, rowHeight := grid.getCellDimensions(cellX, cellY) _, rowHeight := grid.getCellDimensions(cellX, cellY)
if screenY + rowHeight > maxScreenY { if screenY+rowHeight > maxScreenY {
rowHeight = maxScreenY - screenY rowHeight = maxScreenY - screenY
} }
cellText, cellFg, cellBg := grid.getCellData(cellX, cellY) cellText, cellFg, cellBg := grid.getCellData(cellX, cellY)
grid.renderCell(ctx, newGridRect(cellOffsetX, cellOffsetY, colWidth - cellOffsetX, rowHeight), grid.renderCell(ctx, newGridRect(cellOffsetX, cellOffsetY, colWidth-cellOffsetX, rowHeight),
screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse screenX, screenY, cellText, cellFg, cellBg) // termbox.AttrReverse, termbox.AttrReverse
cellY++ cellY++
cellsHigh++ cellsHigh++
screenY = screenY + rowHeight - cellOffsetY screenY = screenY + rowHeight - cellOffsetY
cellOffsetY = 0 cellOffsetY = 0
} }
return gridPoint(screenX + colWidth), cellsHigh return gridPoint(screenX + colWidth), cellsHigh
} }
// Renders the grid. Returns the number of cells in the X and Y direction were rendered. // Renders the grid. Returns the number of cells in the X and Y direction were rendered.
// //
func (grid *Grid) renderGrid(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (int, int) { func (grid *Grid) renderGrid(ctx *DrawContext, screenViewPort gridRect, cellX int, cellY int, cellOffsetX int, cellOffsetY int) (int, int) {
var cellsHigh = 0 var cellsHigh = 0
var cellsWide = 0 var cellsWide = 0
for screenViewPort.x1 < screenViewPort.x2 { for screenViewPort.x1 < screenViewPort.x2 {
screenViewPort.x1, cellsHigh = grid.renderColumn(ctx, screenViewPort, cellX, cellY, cellOffsetX, cellOffsetY) screenViewPort.x1, cellsHigh = grid.renderColumn(ctx, screenViewPort, cellX, cellY, cellOffsetX, cellOffsetY)
cellX = cellX + 1 cellX = cellX + 1
cellsWide++ cellsWide++
cellOffsetX = 0 cellOffsetX = 0
} }
return cellsWide, cellsHigh return cellsWide, cellsHigh
} }
/** /**
* Returns the cell of the particular point, along with the top-left position of the cell. * Returns the cell of the particular point, along with the top-left position of the cell.
*/ */
func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, posY int) { func (grid *Grid) pointToCell(x int, y int) (cellX int, cellY int, posX int, posY int) {
var wid, hei int = grid.model.Dimensions() var wid, hei int = grid.model.Dimensions()
posX = 0 posX = 0
posY = 0 posY = 0
cellX = -1 cellX = -1
cellY = -1 cellY = -1
// Go through columns to locate the particular cellX // Go through columns to locate the particular cellX
for cx := 0; cx < wid; cx++ { for cx := 0; cx < wid; cx++ {
if (x >= posX) && (x < posX + grid.model.ColWidth(cx)) { if (x >= posX) && (x < posX+grid.model.ColWidth(cx)) {
// We found the X position // We found the X position
cellX = int(cx) cellX = int(cx)
break break
} }
} }
for cy := 0; cy < hei; cy++ { for cy := 0; cy < hei; cy++ {
if (y >= posY) && (y < posY + grid.model.RowHeight(cy)) { if (y >= posY) && (y < posY+grid.model.RowHeight(cy)) {
// And the Y position // And the Y position
cellY = int(cy) cellY = int(cy)
break break
} }
} }
return return
} }
/** /**
* Returns the requested dimensions of a grid (as required by UiComponent) * Returns the requested dimensions of a grid (as required by UiComponent)
*/ */
func (grid *Grid) Remeasure(w, h int) (int, int) { func (grid *Grid) Remeasure(w, h int) (int, int) {
return w, h return w, h
} }
/** /**
* Redraws the grid. * Redraws the grid.
*/ */
func (grid *Grid) Redraw(ctx *DrawContext) { func (grid *Grid) Redraw(ctx *DrawContext) {
viewportRect := newGridRect(0, 0, ctx.W, ctx.H) viewportRect := newGridRect(0, 0, ctx.W, ctx.H)
grid.cellsWide, grid.cellsHigh = grid.renderGrid(ctx, viewportRect, 0, 0, 0, 0) grid.cellsWide, grid.cellsHigh = grid.renderGrid(ctx, viewportRect, 0, 0, 0, 0)
} }
// Called when the component has focus and a key has been pressed. // Called when the component has focus and a key has been pressed.
// This is the default behaviour of the grid, but it is not used by the main grid. // This is the default behaviour of the grid, but it is not used by the main grid.
func (grid *Grid) KeyPressed(key rune, mod int) { func (grid *Grid) KeyPressed(key rune, mod int) {
// TODO: Not sure if this would be better handled using commands // TODO: Not sure if this would be better handled using commands
if (key == 'i') || (key == KeyArrowUp) { if (key == 'i') || (key == KeyArrowUp) {
grid.MoveBy(0, -1) grid.MoveBy(0, -1)
} else if (key == 'k') || (key == KeyArrowDown) { } else if (key == 'k') || (key == KeyArrowDown) {
grid.MoveBy(0, 1) grid.MoveBy(0, 1)
} else if (key == 'j') || (key == KeyArrowLeft) { } else if (key == 'j') || (key == KeyArrowLeft) {
grid.MoveBy(-1, 0) grid.MoveBy(-1, 0)
} else if (key == 'l') || (key == KeyArrowRight) { } else if (key == 'l') || (key == KeyArrowRight) {
grid.MoveBy(1, 0) grid.MoveBy(1, 0)
} }
} }
// -------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------
// Test Model // Test Model
type TestModel struct { type TestModel struct {
thing int thing int
} }
/** /**
* Returns the size of the grid model (width x height) * Returns the size of the grid model (width x height)
*/ */
func (model *TestModel) Dimensions() (int, int) { func (model *TestModel) Dimensions() (int, int) {
return 100, 100 return 100, 100
} }
/** /**
* Returns the size of the particular column. If the size is 0, this indicates that the column is hidden. * Returns the size of the particular column. If the size is 0, this indicates that the column is hidden.
*/ */
func (model *TestModel) ColWidth(int) int { func (model *TestModel) ColWidth(int) int {
return 16 return 16
} }
/** /**
* Returns the size of the particular row. If the size is 0, this indicates that the row is hidden. * Returns the size of the particular row. If the size is 0, this indicates that the row is hidden.
*/ */
func (model *TestModel) RowHeight(int) int { func (model *TestModel) RowHeight(int) int {
return 1 return 1
} }
/** /**
* Returns the value of the cell a position X, Y * Returns the value of the cell a position X, Y
*/ */
func (model *TestModel) CellValue(x int, y int) string { func (model *TestModel) CellValue(x int, y int) string {
return strconv.Itoa(x) + "," + strconv.Itoa(y) return strconv.Itoa(x) + "," + strconv.Itoa(y)
} }

View file

@ -3,84 +3,86 @@
package ui package ui
// The UI manager // The UI manager
type Ui struct { type Ui struct {
// The root component // The root component
rootComponent UiComponent rootComponent UiComponent
focusedComponent FocusableComponent focusedComponent FocusableComponent
drawContext *DrawContext drawContext *DrawContext
driver Driver driver Driver
shutdown bool
} }
// Creates a new UI context. This also initializes the UI state. // Creates a new UI context. This also initializes the UI state.
// Returns the context and an error. // Returns the context and an error.
func NewUI() (*Ui, error) { func NewUI() (*Ui, error) {
driver := &TermboxDriver{} driver := &TermboxDriver{}
err := driver.Init() err := driver.Init()
if err != nil { if err != nil {
return nil, err return nil, err
} }
drawContext := &DrawContext{ driver: driver } drawContext := &DrawContext{driver: driver}
ui := &Ui{ drawContext: drawContext, driver: driver } ui := &Ui{drawContext: drawContext, driver: driver}
return ui, nil return ui, nil
} }
// Closes the UI context. // Closes the UI context.
func (ui *Ui) Close() { func (ui *Ui) Close() {
ui.driver.Close() ui.driver.Close()
} }
// Sets the root component // Sets the root component
func (ui *Ui) SetRootComponent(comp UiComponent) { func (ui *Ui) SetRootComponent(comp UiComponent) {
ui.rootComponent = comp ui.rootComponent = comp
ui.Remeasure() ui.Remeasure()
} }
// Sets the focused component // Sets the focused component
func (ui *Ui) SetFocusedComponent(newFocused FocusableComponent) { func (ui *Ui) SetFocusedComponent(newFocused FocusableComponent) {
ui.focusedComponent = newFocused ui.focusedComponent = newFocused
} }
// Remeasures the UI // Remeasures the UI
func (ui *Ui) Remeasure() { func (ui *Ui) Remeasure() {
ui.drawContext.X = 0 ui.drawContext.X = 0
ui.drawContext.Y = 0 ui.drawContext.Y = 0
ui.drawContext.W, ui.drawContext.H = ui.driver.Size() ui.drawContext.W, ui.drawContext.H = ui.driver.Size()
ui.rootComponent.Remeasure(ui.drawContext.W, ui.drawContext.H) ui.rootComponent.Remeasure(ui.drawContext.W, ui.drawContext.H)
} }
// Redraws the UI. // Redraws the UI.
func (ui *Ui) Redraw() { func (ui *Ui) Redraw() {
ui.Remeasure() ui.Remeasure()
ui.rootComponent.Redraw(ui.drawContext) ui.rootComponent.Redraw(ui.drawContext)
ui.driver.Sync() ui.driver.Sync()
} }
// Quit indicates to the UI that it should shutdown
func (ui *Ui) Shutdown() {
ui.shutdown = true
}
// Enter the UI loop // Enter the UI loop
func (ui *Ui) Loop() { func (ui *Ui) Loop() {
for { for !ui.shutdown {
ui.Redraw() ui.Redraw()
event := ui.driver.WaitForEvent() event := ui.driver.WaitForEvent()
// TODO: If the event is a key-press, do something. // TODO: If the event is a key-press, do something.
if event.Type == EventKeyPress { if event.Type == EventKeyPress {
if ui.focusedComponent != nil { if ui.focusedComponent != nil {
ui.focusedComponent.KeyPressed(event.Ch, event.Par) ui.focusedComponent.KeyPressed(event.Ch, event.Par)
} }
} else if event.Type == EventResize { } else if event.Type == EventResize {
// HACK: Find another way to refresh the size of the screen to prevent a full redraw. // HACK: Find another way to refresh the size of the screen to prevent a full redraw.
ui.driver.Sync() ui.driver.Sync()
} }
} }
} }

View file

@ -4,178 +4,187 @@ package ui
import "unicode" import "unicode"
// A text component. This simply renders a text string. // A text component. This simply renders a text string.
type TextView struct { type TextView struct {
// The string to render // The string to render
Text string Text string
} }
// Minimum dimensions // Minimum dimensions
func (tv *TextView) Remeasure(w, h int) (int, int) { func (tv *TextView) Remeasure(w, h int) (int, int) {
return w, 1 return w, 1
} }
// Status bar redraw // Status bar redraw
func (tv *TextView) Redraw(context *DrawContext) { func (tv *TextView) Redraw(context *DrawContext) {
context.SetFgAttr(0) context.SetFgAttr(0)
context.SetBgAttr(0) context.SetBgAttr(0)
context.HorizRule(0, ' ') context.HorizRule(0, ' ')
context.Print(0, 0, tv.Text) context.Print(0, 0, tv.Text)
context.HideCursor()
} }
// Status bar component. This component displays text on the left and right of it's // Status bar component. This component displays text on the left and right of it's
// allocated space. // allocated space.
type StatusBar struct { type StatusBar struct {
Left string // Left aligned string Left string // Left aligned string
Right string // Right aligned string Right string // Right aligned string
} }
// Minimum dimensions // Minimum dimensions
func (sbar *StatusBar) Remeasure(w, h int) (int, int) { func (sbar *StatusBar) Remeasure(w, h int) (int, int) {
return w, 1 return w, 1
} }
// Status bar redraw // Status bar redraw
func (sbar *StatusBar) Redraw(context *DrawContext) { func (sbar *StatusBar) Redraw(context *DrawContext) {
context.SetFgAttr(AttrReverse) context.SetFgAttr(AttrReverse)
context.SetBgAttr(AttrReverse) context.SetBgAttr(AttrReverse)
context.HorizRule(0, ' ') context.HorizRule(0, ' ')
context.Print(0, 0, sbar.Left) context.Print(0, 0, sbar.Left)
context.PrintRight(context.W, 0, sbar.Right) context.PrintRight(context.W, 0, sbar.Right)
} }
// A single-text entry component. // A single-text entry component.
type TextEntry struct { type TextEntry struct {
Prompt string Prompt string
value string value string
cursorOffset int cursorOffset int
displayOffset int displayOffset int
// Called when the user presses Enter
OnEntry func(val string)
} }
func (te *TextEntry) Remeasure(w, h int) (int, int) { func (te *TextEntry) Remeasure(w, h int) (int, int) {
return w, 1 return w, 1
} }
func (te *TextEntry) Redraw(context *DrawContext) { func (te *TextEntry) Redraw(context *DrawContext) {
context.HorizRule(0, ' ') context.HorizRule(0, ' ')
valueOffsetX := 0 valueOffsetX := 0
displayOffsetX := te.calculateDisplayOffset(context.W) displayOffsetX := te.calculateDisplayOffset(context.W)
if te.Prompt != "" { if te.Prompt != "" {
context.SetFgAttr(ColorDefault | AttrBold) context.SetFgAttr(ColorDefault | AttrBold)
context.Print(0, 0, te.Prompt) context.Print(0, 0, te.Prompt)
context.SetFgAttr(ColorDefault) context.SetFgAttr(ColorDefault)
valueOffsetX = len(te.Prompt) valueOffsetX = len(te.Prompt)
} }
context.Print(valueOffsetX, 0, te.value[displayOffsetX:intMin(displayOffsetX + context.W,len(te.value))]) context.Print(valueOffsetX, 0, te.value[displayOffsetX:intMin(displayOffsetX+context.W, len(te.value))])
context.SetCursorPosition(te.cursorOffset + valueOffsetX - displayOffsetX, 0) context.SetCursorPosition(te.cursorOffset+valueOffsetX-displayOffsetX, 0)
//context.Print(0, 0, fmt.Sprintf("%d,%d", te.cursorOffset, displayOffsetX)) //context.Print(0, 0, fmt.Sprintf("%d,%d", te.cursorOffset, displayOffsetX))
} }
func (te *TextEntry) calculateDisplayOffset(displayWidth int) int { func (te *TextEntry) calculateDisplayOffset(displayWidth int) int {
if te.Prompt != "" { if te.Prompt != "" {
displayWidth -= len(te.Prompt) displayWidth -= len(te.Prompt)
} }
virtualCursorOffset := te.cursorOffset - te.displayOffset virtualCursorOffset := te.cursorOffset - te.displayOffset
if virtualCursorOffset >= displayWidth { if virtualCursorOffset >= displayWidth {
te.displayOffset = te.cursorOffset - displayWidth + 10 te.displayOffset = te.cursorOffset - displayWidth + 10
} else if (virtualCursorOffset < 0) { } else if virtualCursorOffset < 0 {
te.displayOffset = intMax(te.cursorOffset - displayWidth + 1, 0) te.displayOffset = intMax(te.cursorOffset-displayWidth+1, 0)
} }
return te.displayOffset return te.displayOffset
}
// SetValue sets the value of the text entry
func (te *TextEntry) SetValue(val string) {
te.value = val
te.cursorOffset = len(val)
} }
func (te *TextEntry) KeyPressed(key rune, mod int) { func (te *TextEntry) KeyPressed(key rune, mod int) {
if (key >= ' ') && (key <= '~') { if (key >= ' ') && (key <= '~') {
te.insertRune(key) te.insertRune(key)
} else if (key == KeyArrowLeft) { } else if key == KeyArrowLeft {
te.moveCursorBy(-1) te.moveCursorBy(-1)
} else if (key == KeyArrowRight) { } else if key == KeyArrowRight {
te.moveCursorBy(1) te.moveCursorBy(1)
} else if (key == KeyHome) { } else if key == KeyHome {
te.moveCursorTo(0) te.moveCursorTo(0)
} else if (key == KeyEnd) { } else if key == KeyEnd {
te.moveCursorTo(len(te.value)) te.moveCursorTo(len(te.value))
} else if (key == KeyBackspace) || (key == KeyBackspace2) { } else if (key == KeyBackspace) || (key == KeyBackspace2) {
if (mod & ModKeyAlt != 0) { if mod&ModKeyAlt != 0 {
te.backspaceWhile(unicode.IsSpace) te.backspaceWhile(unicode.IsSpace)
te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) }) te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) })
} else { } else {
te.backspace() te.backspace()
} }
} else if (key == KeyCtrlK) { } else if key == KeyCtrlK {
te.killLine() te.killLine()
} else if (key == KeyDelete) { } else if key == KeyDelete {
te.removeCharAtPos(te.cursorOffset) te.removeCharAtPos(te.cursorOffset)
} else if (key == KeyEnter) { } else if key == KeyEnter {
panic("Entered text: '" + te.value + "'") //panic("Entered text: '" + te.value + "'")
} if te.OnEntry != nil {
te.OnEntry(te.value)
}
}
} }
// Backspace // Backspace
func (te *TextEntry) backspace() { func (te *TextEntry) backspace() {
te.removeCharAtPos(te.cursorOffset - 1) te.removeCharAtPos(te.cursorOffset - 1)
te.moveCursorBy(-1) te.moveCursorBy(-1)
} }
// Backspace while the character underneith the cursor matches the guard // Backspace while the character underneith the cursor matches the guard
func (te *TextEntry) backspaceWhile(guard func(r rune) bool) { func (te *TextEntry) backspaceWhile(guard func(r rune) bool) {
for (te.cursorOffset > 0) { for te.cursorOffset > 0 {
ch := rune(te.value[te.cursorOffset - 1]) ch := rune(te.value[te.cursorOffset-1])
if guard(ch) { if guard(ch) {
te.backspace() te.backspace()
} else { } else {
break break
} }
} }
} }
// Kill the line. If the cursor is at the end of the line, kill to the start. // Kill the line. If the cursor is at the end of the line, kill to the start.
// Otherwise, trim the line. // Otherwise, trim the line.
func (te *TextEntry) killLine() { func (te *TextEntry) killLine() {
if (te.cursorOffset < len(te.value)) { if te.cursorOffset < len(te.value) {
te.value = te.value[:te.cursorOffset] te.value = te.value[:te.cursorOffset]
} else { } else {
te.value = "" te.value = ""
te.cursorOffset = 0 te.cursorOffset = 0
} }
} }
// Inserts a rune at the cursor position // Inserts a rune at the cursor position
func (te *TextEntry) insertRune(key rune) { func (te *TextEntry) insertRune(key rune) {
if (te.cursorOffset >= len(te.value)) { if te.cursorOffset >= len(te.value) {
te.value += string(key) te.value += string(key)
} else { } else {
te.value = te.value[:te.cursorOffset] + string(key) + te.value[te.cursorOffset:] te.value = te.value[:te.cursorOffset] + string(key) + te.value[te.cursorOffset:]
} }
te.moveCursorBy(1) te.moveCursorBy(1)
} }
// Remove the character at a specific position // Remove the character at a specific position
func (te *TextEntry) removeCharAtPos(pos int) { func (te *TextEntry) removeCharAtPos(pos int) {
if (pos >= 0) && (pos < len(te.value)) { if (pos >= 0) && (pos < len(te.value)) {
te.value = te.value[:pos] + te.value[pos+1:] te.value = te.value[:pos] + te.value[pos+1:]
} }
} }
// Move the cursor // Move the cursor
func (te *TextEntry) moveCursorBy(byX int) { func (te *TextEntry) moveCursorBy(byX int) {
te.moveCursorTo(te.cursorOffset + byX) te.moveCursorTo(te.cursorOffset + byX)
} }
func (te *TextEntry) moveCursorTo(toX int) { func (te *TextEntry) moveCursorTo(toX int) {
te.cursorOffset = intMinMax(toX, 0, len(te.value)) te.cursorOffset = intMinMax(toX, 0, len(te.value))
} }

View file

@ -3,134 +3,135 @@
package ui package ui
import ( import (
"github.com/nsf/termbox-go" "github.com/nsf/termbox-go"
) )
type TermboxDriver struct { type TermboxDriver struct {
} }
// Initializes the driver. Returns an error if there was an error // Initializes the driver. Returns an error if there was an error
func (td *TermboxDriver) Init() error { func (td *TermboxDriver) Init() error {
err := termbox.Init() err := termbox.Init()
if err != nil { if err != nil {
return err return err
} }
termbox.SetInputMode(termbox.InputAlt) termbox.SetInputMode(termbox.InputAlt)
return nil return nil
} }
// Closes the driver // Closes the driver
func (td *TermboxDriver) Close() { func (td *TermboxDriver) Close() {
termbox.Close() termbox.Close()
} }
// Returns the size of the window. // Returns the size of the window.
func (td *TermboxDriver) Size() (int, int) { func (td *TermboxDriver) Size() (int, int) {
return termbox.Size() return termbox.Size()
} }
// Sets the value of a specific cell // Sets the value of a specific cell
func (td *TermboxDriver) SetCell(x, y int, ch rune, fg, bg Attribute) { func (td *TermboxDriver) SetCell(x, y int, ch rune, fg, bg Attribute) {
termbox.SetCell(x, y, ch, termbox.Attribute(fg), termbox.Attribute(bg)) termbox.SetCell(x, y, ch, termbox.Attribute(fg), termbox.Attribute(bg))
}
// Hide the cursor
func (td *TermboxDriver) HideCursor() {
termbox.HideCursor()
} }
// Synchronizes the internal buffer with the real buffer // Synchronizes the internal buffer with the real buffer
func (td *TermboxDriver) Sync() { func (td *TermboxDriver) Sync() {
termbox.Flush() termbox.Flush()
} }
// Wait for an event // Wait for an event
func (td *TermboxDriver) WaitForEvent() Event { func (td *TermboxDriver) WaitForEvent() Event {
tev := termbox.PollEvent() tev := termbox.PollEvent()
switch tev.Type { switch tev.Type {
case termbox.EventResize: case termbox.EventResize:
return Event{EventResize, 0, 0} return Event{EventResize, 0, 0}
case termbox.EventKey: case termbox.EventKey:
mod := 0 mod := 0
if tev.Mod & termbox.ModAlt != 0 { if tev.Mod&termbox.ModAlt != 0 {
mod = ModKeyAlt mod = ModKeyAlt
} }
if tev.Ch != 0 { if tev.Ch != 0 {
return Event{EventKeyPress, mod, tev.Ch} return Event{EventKeyPress, mod, tev.Ch}
} else if spec, hasSpec := termboxKeysToSpecialKeys[tev.Key] ; hasSpec { } else if spec, hasSpec := termboxKeysToSpecialKeys[tev.Key]; hasSpec {
return Event{EventKeyPress, mod, spec} return Event{EventKeyPress, mod, spec}
} else { } else {
return Event{EventNone, mod, 0} return Event{EventNone, mod, 0}
} }
default: default:
return Event{EventNone, 0, 0} return Event{EventNone, 0, 0}
} }
} }
// Move the position of the cursor // Move the position of the cursor
func (td *TermboxDriver) SetCursor(x, y int) { func (td *TermboxDriver) SetCursor(x, y int) {
termbox.SetCursor(x, y) termbox.SetCursor(x, y)
} }
// Map from termbox Keys to driver key runes // Map from termbox Keys to driver key runes
var termboxKeysToSpecialKeys = map[termbox.Key]rune { var termboxKeysToSpecialKeys = map[termbox.Key]rune{
termbox.KeySpace: ' ', termbox.KeySpace: ' ',
termbox.KeyF1: KeyF1, termbox.KeyF1: KeyF1,
termbox.KeyF2: KeyF2, termbox.KeyF2: KeyF2,
termbox.KeyF3: KeyF3, termbox.KeyF3: KeyF3,
termbox.KeyF4: KeyF4, termbox.KeyF4: KeyF4,
termbox.KeyF5: KeyF5, termbox.KeyF5: KeyF5,
termbox.KeyF6: KeyF6, termbox.KeyF6: KeyF6,
termbox.KeyF7: KeyF7, termbox.KeyF7: KeyF7,
termbox.KeyF8: KeyF8, termbox.KeyF8: KeyF8,
termbox.KeyF9: KeyF9, termbox.KeyF9: KeyF9,
termbox.KeyF10: KeyF10, termbox.KeyF10: KeyF10,
termbox.KeyF11: KeyF11, termbox.KeyF11: KeyF11,
termbox.KeyF12: KeyF12, termbox.KeyF12: KeyF12,
termbox.KeyInsert: KeyInsert, termbox.KeyInsert: KeyInsert,
termbox.KeyDelete: KeyDelete, termbox.KeyDelete: KeyDelete,
termbox.KeyHome: KeyHome, termbox.KeyHome: KeyHome,
termbox.KeyEnd: KeyEnd, termbox.KeyEnd: KeyEnd,
termbox.KeyPgup: KeyPgup, termbox.KeyPgup: KeyPgup,
termbox.KeyPgdn: KeyPgdn, termbox.KeyPgdn: KeyPgdn,
termbox.KeyArrowUp: KeyArrowUp, termbox.KeyArrowUp: KeyArrowUp,
termbox.KeyArrowDown: KeyArrowDown, termbox.KeyArrowDown: KeyArrowDown,
termbox.KeyArrowLeft: KeyArrowLeft, termbox.KeyArrowLeft: KeyArrowLeft,
termbox.KeyArrowRight: KeyArrowRight, termbox.KeyArrowRight: KeyArrowRight,
termbox.KeyCtrlSpace: KeyCtrlSpace, termbox.KeyCtrlSpace: KeyCtrlSpace,
termbox.KeyCtrlA: KeyCtrlA, termbox.KeyCtrlA: KeyCtrlA,
termbox.KeyCtrlB: KeyCtrlB, termbox.KeyCtrlB: KeyCtrlB,
termbox.KeyCtrlC: KeyCtrlC, termbox.KeyCtrlC: KeyCtrlC,
termbox.KeyCtrlD: KeyCtrlD, termbox.KeyCtrlD: KeyCtrlD,
termbox.KeyCtrlE: KeyCtrlE, termbox.KeyCtrlE: KeyCtrlE,
termbox.KeyCtrlF: KeyCtrlF, termbox.KeyCtrlF: KeyCtrlF,
termbox.KeyCtrlG: KeyCtrlG, termbox.KeyCtrlG: KeyCtrlG,
termbox.KeyCtrlH: KeyCtrlH, termbox.KeyCtrlH: KeyCtrlH,
termbox.KeyCtrlI: KeyCtrlI, termbox.KeyCtrlI: KeyCtrlI,
termbox.KeyCtrlJ: KeyCtrlJ, termbox.KeyCtrlJ: KeyCtrlJ,
termbox.KeyCtrlK: KeyCtrlK, termbox.KeyCtrlK: KeyCtrlK,
termbox.KeyCtrlL: KeyCtrlL, termbox.KeyCtrlL: KeyCtrlL,
termbox.KeyCtrlM: KeyCtrlM, termbox.KeyCtrlM: KeyCtrlM,
termbox.KeyCtrlN: KeyCtrlN, termbox.KeyCtrlN: KeyCtrlN,
termbox.KeyCtrlO: KeyCtrlO, termbox.KeyCtrlO: KeyCtrlO,
termbox.KeyCtrlP: KeyCtrlP, termbox.KeyCtrlP: KeyCtrlP,
termbox.KeyCtrlQ: KeyCtrlQ, termbox.KeyCtrlQ: KeyCtrlQ,
termbox.KeyCtrlR: KeyCtrlR, termbox.KeyCtrlR: KeyCtrlR,
termbox.KeyCtrlS: KeyCtrlS, termbox.KeyCtrlS: KeyCtrlS,
termbox.KeyCtrlT: KeyCtrlT, termbox.KeyCtrlT: KeyCtrlT,
termbox.KeyCtrlU: KeyCtrlU, termbox.KeyCtrlU: KeyCtrlU,
termbox.KeyCtrlV: KeyCtrlV, termbox.KeyCtrlV: KeyCtrlV,
termbox.KeyCtrlW: KeyCtrlW, termbox.KeyCtrlW: KeyCtrlW,
termbox.KeyCtrlX: KeyCtrlX, termbox.KeyCtrlX: KeyCtrlX,
termbox.KeyCtrlY: KeyCtrlY, termbox.KeyCtrlY: KeyCtrlY,
termbox.KeyCtrlZ: KeyCtrlZ, termbox.KeyCtrlZ: KeyCtrlZ,
termbox.KeyCtrl3: KeyCtrl3, termbox.KeyCtrl3: KeyCtrl3,
termbox.KeyCtrl4: KeyCtrl4, termbox.KeyCtrl4: KeyCtrl4,
termbox.KeyCtrl5: KeyCtrl5, termbox.KeyCtrl5: KeyCtrl5,
termbox.KeyCtrl6: KeyCtrl6, termbox.KeyCtrl6: KeyCtrl6,
termbox.KeyCtrl7: KeyCtrl7, termbox.KeyCtrl7: KeyCtrl7,
termbox.KeyCtrl8: KeyCtrl8, termbox.KeyCtrl8: KeyCtrl8,
} }