Have actually implemented useful commands for reading/writing CSV files
This commit is contained in:
parent
33847a78c1
commit
ae833d5db8
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bitbucket.org/lmika/ted-v2/ui"
|
"bitbucket.org/lmika/ted-v2/ui"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -53,6 +54,23 @@ func (cm *CommandMapping) KeyMapping(key rune) *Command {
|
||||||
return cm.KeyMappings[key]
|
return cm.KeyMappings[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Evaluate a command
|
||||||
|
func (cm *CommandMapping) Eval(ctx *CommandContext, expr string) error {
|
||||||
|
// TODO: Use propper expression language here
|
||||||
|
cmd := cm.Commands[expr]
|
||||||
|
if cmd != nil {
|
||||||
|
return cmd.Do(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no such command: %v", expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *CommandMapping) DoEval(ctx *CommandContext, expr string) {
|
||||||
|
if err := cm.Eval(ctx, expr); err != nil {
|
||||||
|
ctx.ShowError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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) }))
|
||||||
|
|
@ -87,8 +105,8 @@ func (cm *CommandMapping) RegisterViewCommands() {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error {
|
cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error {
|
||||||
ctx.Frame().Prompt(": ", func(res string) {
|
ctx.Frame().Prompt(":", func(res string) {
|
||||||
ctx.Frame().Message("Command = " + res)
|
cm.DoEval(ctx, res)
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
@ -104,10 +122,38 @@ func (cm *CommandMapping) RegisterViewCommands() {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
cm.Define("save", "Save current file", "", func(ctx *CommandContext) error {
|
||||||
|
wSource, isWSource := ctx.Session().Source.(WritableModelSource)
|
||||||
|
if !isWSource {
|
||||||
|
return fmt.Errorf("model is not writable")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wSource.Write(ctx.Session().Model); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Frame().Message("Wrote " + wSource.String())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
cm.Define("quit", "Quit TED", "", func(ctx *CommandContext) error {
|
cm.Define("quit", "Quit TED", "", func(ctx *CommandContext) error {
|
||||||
ctx.Session().UIManager.Shutdown()
|
ctx.Session().UIManager.Shutdown()
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
cm.Define("save-and-quit", "Save current file, then quit", "", func(ctx *CommandContext) error {
|
||||||
|
if err := cm.Eval(ctx, "save"); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cm.Eval(ctx, "quit")
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Aliases
|
||||||
|
cm.Commands["w"] = cm.Command("save")
|
||||||
|
cm.Commands["q"] = cm.Command("quit")
|
||||||
|
cm.Commands["wq"] = cm.Command("save-and-quit")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registers the standard view key bindings. These commands require the frame
|
// Registers the standard view key bindings. These commands require the frame
|
||||||
|
|
@ -133,8 +179,6 @@ func (cm *CommandMapping) RegisterViewKeyBindings() {
|
||||||
cm.MapKey('e', cm.Command("set-cell"))
|
cm.MapKey('e', cm.Command("set-cell"))
|
||||||
|
|
||||||
cm.MapKey(':', cm.Command("enter-command"))
|
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
|
||||||
|
|
|
||||||
10
frame.go
10
frame.go
|
|
@ -102,16 +102,20 @@ func (frame *Frame) Prompt(prompt string, callback func(res string)) {
|
||||||
frame.textEntry.Prompt = prompt
|
frame.textEntry.Prompt = prompt
|
||||||
frame.textEntry.SetValue("")
|
frame.textEntry.SetValue("")
|
||||||
|
|
||||||
|
frame.textEntry.OnCancel = frame.exitEntryMode
|
||||||
frame.textEntry.OnEntry = func(res string) {
|
frame.textEntry.OnEntry = func(res string) {
|
||||||
frame.textEntry.OnEntry = nil
|
frame.exitEntryMode()
|
||||||
frame.setMode(GridMode)
|
|
||||||
|
|
||||||
callback(res)
|
callback(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.setMode(EntryMode)
|
frame.setMode(EntryMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (frame *Frame) exitEntryMode() {
|
||||||
|
frame.textEntry.OnEntry = nil
|
||||||
|
frame.setMode(GridMode)
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
||||||
6
main.go
6
main.go
|
|
@ -11,11 +11,9 @@ func main() {
|
||||||
}
|
}
|
||||||
defer uiManager.Close()
|
defer uiManager.Close()
|
||||||
|
|
||||||
model := &StdModel{}
|
|
||||||
model.Resize(5, 5)
|
|
||||||
|
|
||||||
frame := NewFrame(uiManager)
|
frame := NewFrame(uiManager)
|
||||||
NewSession(uiManager, frame, model)
|
session := NewSession(uiManager, frame, CsvFileModelSource{"test.csv"})
|
||||||
|
session.LoadFromSource()
|
||||||
|
|
||||||
uiManager.SetRootComponent(frame.RootComponent())
|
uiManager.SetRootComponent(frame.RootComponent())
|
||||||
frame.enterMode(GridMode)
|
frame.enterMode(GridMode)
|
||||||
|
|
|
||||||
16
model.go
16
model.go
|
|
@ -3,20 +3,13 @@
|
||||||
*/
|
*/
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
// An abstract model interface. At a minimum, models must be read only.
|
||||||
/**
|
|
||||||
* An abstract model interface. At a minimum, models must be read only.
|
|
||||||
*/
|
|
||||||
type Model interface {
|
type Model interface {
|
||||||
|
|
||||||
/**
|
// The dimensions of the model (height, width).
|
||||||
* The dimensions of the model (height, width).
|
|
||||||
*/
|
|
||||||
Dimensions() (int, int)
|
Dimensions() (int, int)
|
||||||
|
|
||||||
/**
|
// Returns the value of a cell
|
||||||
* Returns the value of a cell.
|
|
||||||
*/
|
|
||||||
CellValue(r, c int) string
|
CellValue(r, c int) string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,4 +22,7 @@ type RWModel interface {
|
||||||
|
|
||||||
// Sets the cell value
|
// Sets the cell value
|
||||||
SetCellValue(r, c int, value string)
|
SetCellValue(r, c int, value string)
|
||||||
|
|
||||||
|
// Returns true if the model has been modified in some way
|
||||||
|
IsDirty() bool
|
||||||
}
|
}
|
||||||
|
|
|
||||||
91
modelsource.go
Normal file
91
modelsource.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"os"
|
||||||
|
"encoding/csv"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ModelSource is a source of models. At a minimum, it must be able to read models.
|
||||||
|
type ModelSource interface {
|
||||||
|
// Describes the source
|
||||||
|
String() string
|
||||||
|
|
||||||
|
// Read the model from the given source
|
||||||
|
Read() (Model, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writable models take a model and write it to the source
|
||||||
|
type WritableModelSource interface {
|
||||||
|
ModelSource
|
||||||
|
|
||||||
|
// Write writes a model to the source
|
||||||
|
Write(m Model) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// A model source backed by a CSV file
|
||||||
|
type CsvFileModelSource struct {
|
||||||
|
Filename string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Describes the source
|
||||||
|
func (s CsvFileModelSource) String() string {
|
||||||
|
return filepath.Base(s.Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the model from the given source
|
||||||
|
func (s CsvFileModelSource) Read() (Model, error) {
|
||||||
|
f, err := os.Open(s.Filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
model := new(StdModel)
|
||||||
|
r := csv.NewReader(f)
|
||||||
|
for {
|
||||||
|
record, err := r.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
model.appendStr(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
model.dirty = false
|
||||||
|
return model, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s CsvFileModelSource) Write(m Model) error {
|
||||||
|
f, err := os.Create(s.Filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := csv.NewWriter(f)
|
||||||
|
|
||||||
|
rows, cols := m.Dimensions()
|
||||||
|
|
||||||
|
for r := 0; r < rows; r++ {
|
||||||
|
record := make([]string, cols) // Reuse the record slice
|
||||||
|
for c := 0; c < cols; c++ {
|
||||||
|
record[c] = m.CellValue(r, c)
|
||||||
|
}
|
||||||
|
if err := w.Write(record); err != nil {
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Flush()
|
||||||
|
if err := w.Error(); err != nil {
|
||||||
|
f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
24
session.go
24
session.go
|
|
@ -6,14 +6,16 @@ import "bitbucket.org/lmika/ted-v2/ui"
|
||||||
// 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
|
||||||
|
Source ModelSource
|
||||||
Frame *Frame
|
Frame *Frame
|
||||||
Commands *CommandMapping
|
Commands *CommandMapping
|
||||||
UIManager *ui.Ui
|
UIManager *ui.Ui
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSession(uiManager *ui.Ui, frame *Frame, model Model) *Session {
|
func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session {
|
||||||
session := &Session{
|
session := &Session{
|
||||||
Model: model,
|
Model: nil,
|
||||||
|
Source: source,
|
||||||
Frame: frame,
|
Frame: frame,
|
||||||
Commands: NewCommandMapping(),
|
Commands: NewCommandMapping(),
|
||||||
UIManager: uiManager,
|
UIManager: uiManager,
|
||||||
|
|
@ -30,6 +32,17 @@ func NewSession(uiManager *ui.Ui, frame *Frame, model Model) *Session {
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadFromSource loads the model from the source, replacing the existing model
|
||||||
|
func (session *Session) LoadFromSource() error {
|
||||||
|
newModel, err := session.Source.Read()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Model = newModel
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
@ -59,6 +72,13 @@ func (scc *CommandContext) Frame() *Frame {
|
||||||
return scc.session.Frame
|
return scc.session.Frame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error displays an error if err is not nil
|
||||||
|
func (scc *CommandContext) ShowError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
scc.Frame().Message(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Session grid model
|
// Session grid model
|
||||||
type SessionGridModel struct {
|
type SessionGridModel struct {
|
||||||
Session *Session
|
Session *Session
|
||||||
|
|
|
||||||
88
stdmodel.go
88
stdmodel.go
|
|
@ -2,57 +2,91 @@ package main
|
||||||
|
|
||||||
// Cell
|
// Cell
|
||||||
type Cell struct {
|
type Cell struct {
|
||||||
Value string
|
Value string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard model
|
// Standard model
|
||||||
type StdModel struct {
|
type StdModel struct {
|
||||||
Cells [][]Cell
|
Cells [][]Cell
|
||||||
|
dirty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The dimensions of the model (height, width).
|
* The dimensions of the model (height, width).
|
||||||
*/
|
*/
|
||||||
func (sm *StdModel) Dimensions() (int, int) {
|
func (sm *StdModel) Dimensions() (int, int) {
|
||||||
if len(sm.Cells) == 0 {
|
if len(sm.Cells) == 0 {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
} else {
|
} else {
|
||||||
return len(sm.Cells), len(sm.Cells[0])
|
return len(sm.Cells), len(sm.Cells[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the value of a cell.
|
* Returns the value of a cell.
|
||||||
*/
|
*/
|
||||||
func (sm *StdModel) CellValue(r, c int) string {
|
func (sm *StdModel) CellValue(r, c int) string {
|
||||||
rs, cs := sm.Dimensions()
|
rs, cs := sm.Dimensions()
|
||||||
if (r >= 0) && (c >= 0) && (r < rs) && (c < cs) {
|
if (r >= 0) && (c >= 0) && (r < rs) && (c < cs) {
|
||||||
return sm.Cells[r][c].Value
|
return sm.Cells[r][c].Value
|
||||||
} else {
|
} else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize the model.
|
// Resize the model.
|
||||||
func (sm *StdModel) Resize(rs, cs int) {
|
func (sm *StdModel) Resize(rs, cs int) {
|
||||||
oldRowCount := len(sm.Cells)
|
oldRowCount := len(sm.Cells)
|
||||||
|
|
||||||
newRows := make([][]Cell, rs)
|
newRows := make([][]Cell, rs)
|
||||||
for r := range newRows {
|
for r := range newRows {
|
||||||
newCols := make([]Cell, cs)
|
newCols := make([]Cell, cs)
|
||||||
if r < oldRowCount {
|
if r < oldRowCount {
|
||||||
copy(newCols, sm.Cells[r])
|
copy(newCols, sm.Cells[r])
|
||||||
}
|
}
|
||||||
newRows[r] = newCols
|
newRows[r] = newCols
|
||||||
}
|
}
|
||||||
|
|
||||||
sm.Cells = newRows
|
sm.Cells = newRows
|
||||||
|
sm.dirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the cell value
|
// Sets the cell value
|
||||||
func (sm *StdModel) SetCellValue(r, c int, value string) {
|
func (sm *StdModel) SetCellValue(r, c int, value string) {
|
||||||
rs, cs := sm.Dimensions()
|
rs, cs := sm.Dimensions()
|
||||||
if (r >= 0) && (c >= 0) && (r < rs) && (c < cs) {
|
if (r >= 0) && (c >= 0) && (r < rs) && (c < cs) {
|
||||||
sm.Cells[r][c].Value = value
|
sm.Cells[r][c].Value = value
|
||||||
}
|
}
|
||||||
|
sm.dirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendStr appends the model with the given row
|
||||||
|
func (sm *StdModel) appendStr(row []string) {
|
||||||
|
if len(sm.Cells) == 0 {
|
||||||
|
cells := sm.strSliceToCell(row, len(row))
|
||||||
|
sm.Cells = [][]Cell{ cells }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cols := len(sm.Cells[0])
|
||||||
|
if len(row) > cols {
|
||||||
|
sm.Resize(len(sm.Cells), len(row))
|
||||||
|
cols = len(sm.Cells[0])
|
||||||
|
}
|
||||||
|
cells := sm.strSliceToCell(row, cols)
|
||||||
|
sm.Cells = append(sm.Cells, cells)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *StdModel) strSliceToCell(row []string, targetRowLen int) []Cell {
|
||||||
|
cs := make([]Cell, targetRowLen)
|
||||||
|
for i, c := range row {
|
||||||
|
if i < targetRowLen {
|
||||||
|
cs[i].Value = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *StdModel) IsDirty() bool {
|
||||||
|
return sm.dirty
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +87,7 @@ const (
|
||||||
KeyBackspace = KeyCtrlH
|
KeyBackspace = KeyCtrlH
|
||||||
KeyBackspace2 = KeyCtrl8
|
KeyBackspace2 = KeyCtrl8
|
||||||
KeyEnter = KeyCtrlM
|
KeyEnter = KeyCtrlM
|
||||||
|
KeyEsc = KeyCtrl3
|
||||||
)
|
)
|
||||||
|
|
||||||
// The type of events supported by the driver
|
// The type of events supported by the driver
|
||||||
|
|
@ -116,7 +117,6 @@ type Event struct {
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
package ui
|
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 {
|
||||||
|
|
@ -58,6 +60,9 @@ type TextEntry struct {
|
||||||
|
|
||||||
// Called when the user presses Enter
|
// Called when the user presses Enter
|
||||||
OnEntry func(val string)
|
OnEntry func(val string)
|
||||||
|
|
||||||
|
// Called when the user presses Esc or CtrlC
|
||||||
|
OnCancel func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (te *TextEntry) Remeasure(w, h int) (int, int) {
|
func (te *TextEntry) Remeasure(w, h int) (int, int) {
|
||||||
|
|
@ -127,11 +132,16 @@ func (te *TextEntry) KeyPressed(key rune, mod int) {
|
||||||
} 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 + "'")
|
|
||||||
if te.OnEntry != nil {
|
if te.OnEntry != nil {
|
||||||
te.OnEntry(te.value)
|
te.OnEntry(te.value)
|
||||||
}
|
}
|
||||||
|
} else if key == KeyCtrlC {
|
||||||
|
if te.OnCancel != nil {
|
||||||
|
te.OnCancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//panic(fmt.Sprintf("Entered key: '%x', mod: '%x'", key, mod))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backspace
|
// Backspace
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue