Have actually implemented useful commands for reading/writing CSV files

This commit is contained in:
Leon Mika 2018-09-01 11:27:34 +10:00
parent 33847a78c1
commit ae833d5db8
9 changed files with 252 additions and 55 deletions

View file

@ -2,6 +2,7 @@ package main
import (
"bitbucket.org/lmika/ted-v2/ui"
"fmt"
)
const (
@ -53,6 +54,23 @@ func (cm *CommandMapping) KeyMapping(key rune) *Command {
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
func (cm *CommandMapping) RegisterViewCommands() {
cm.Define("move-down", "Moves the cursor down one row", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, 1) }))
@ -88,7 +106,7 @@ func (cm *CommandMapping) RegisterViewCommands() {
cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error {
ctx.Frame().Prompt(":", func(res string) {
ctx.Frame().Message("Command = " + res)
cm.DoEval(ctx, res)
})
return nil
})
@ -104,10 +122,38 @@ func (cm *CommandMapping) RegisterViewCommands() {
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 {
ctx.Session().UIManager.Shutdown()
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
@ -133,8 +179,6 @@ func (cm *CommandMapping) RegisterViewKeyBindings() {
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

View file

@ -102,16 +102,20 @@ func (frame *Frame) Prompt(prompt string, callback func(res string)) {
frame.textEntry.Prompt = prompt
frame.textEntry.SetValue("")
frame.textEntry.OnCancel = frame.exitEntryMode
frame.textEntry.OnEntry = func(res string) {
frame.textEntry.OnEntry = nil
frame.setMode(GridMode)
frame.exitEntryMode()
callback(res)
}
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
func (frame *Frame) ShowMessage(msg string) {
frame.messageView.Text = msg

View file

@ -11,11 +11,9 @@ func main() {
}
defer uiManager.Close()
model := &StdModel{}
model.Resize(5, 5)
frame := NewFrame(uiManager)
NewSession(uiManager, frame, model)
session := NewSession(uiManager, frame, CsvFileModelSource{"test.csv"})
session.LoadFromSource()
uiManager.SetRootComponent(frame.RootComponent())
frame.enterMode(GridMode)

View file

@ -3,20 +3,13 @@
*/
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 {
/**
* The dimensions of the model (height, width).
*/
// The dimensions of the model (height, width).
Dimensions() (int, int)
/**
* Returns the value of a cell.
*/
// Returns the value of a cell
CellValue(r, c int) string
}
@ -29,4 +22,7 @@ type RWModel interface {
// Sets the cell value
SetCellValue(r, c int, value string)
// Returns true if the model has been modified in some way
IsDirty() bool
}

91
modelsource.go Normal file
View 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()
}

View file

@ -6,14 +6,16 @@ import "bitbucket.org/lmika/ted-v2/ui"
// the interaction between the two and the user.
type Session struct {
Model Model
Source ModelSource
Frame *Frame
Commands *CommandMapping
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{
Model: model,
Model: nil,
Source: source,
Frame: frame,
Commands: NewCommandMapping(),
UIManager: uiManager,
@ -30,6 +32,17 @@ func NewSession(uiManager *ui.Ui, frame *Frame, model Model) *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
func (session *Session) KeyPressed(key rune, mod int) {
// Add the mod key modifier
@ -59,6 +72,13 @@ func (scc *CommandContext) Frame() *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
type SessionGridModel struct {
Session *Session

View file

@ -8,6 +8,7 @@ type Cell struct {
// Standard model
type StdModel struct {
Cells [][]Cell
dirty bool
}
/**
@ -47,6 +48,7 @@ func (sm *StdModel) Resize(rs, cs int) {
}
sm.Cells = newRows
sm.dirty = true
}
// Sets the cell value
@ -55,4 +57,36 @@ func (sm *StdModel) SetCellValue(r, c int, value string) {
if (r >= 0) && (c >= 0) && (r < rs) && (c < cs) {
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
}

View file

@ -87,6 +87,7 @@ const (
KeyBackspace = KeyCtrlH
KeyBackspace2 = KeyCtrl8
KeyEnter = KeyCtrlM
KeyEsc = KeyCtrl3
)
// The type of events supported by the driver
@ -116,7 +117,6 @@ type Event struct {
// The terminal driver interface.
type Driver interface {
// Initializes the driver. Returns an error if there was an error
Init() error

View file

@ -2,7 +2,9 @@
package ui
import "unicode"
import (
"unicode"
)
// A text component. This simply renders a text string.
type TextView struct {
@ -58,6 +60,9 @@ type TextEntry struct {
// Called when the user presses Enter
OnEntry func(val string)
// Called when the user presses Esc or CtrlC
OnCancel func()
}
func (te *TextEntry) Remeasure(w, h int) (int, int) {
@ -127,13 +132,18 @@ func (te *TextEntry) KeyPressed(key rune, mod int) {
} else if key == KeyDelete {
te.removeCharAtPos(te.cursorOffset)
} else if key == KeyEnter {
//panic("Entered text: '" + te.value + "'")
if te.OnEntry != nil {
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
func (te *TextEntry) backspace() {
te.removeCharAtPos(te.cursorOffset - 1)