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 (
|
||||
"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) }))
|
||||
|
|
@ -87,8 +105,8 @@ 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)
|
||||
ctx.Frame().Prompt(":", func(res string) {
|
||||
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
|
||||
|
|
|
|||
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.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
|
||||
|
|
|
|||
6
main.go
6
main.go
|
|
@ -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)
|
||||
|
|
|
|||
16
model.go
16
model.go
|
|
@ -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
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.
|
||||
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
|
||||
|
|
|
|||
88
stdmodel.go
88
stdmodel.go
|
|
@ -2,57 +2,91 @@ package main
|
|||
|
||||
// Cell
|
||||
type Cell struct {
|
||||
Value string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Standard model
|
||||
type StdModel struct {
|
||||
Cells [][]Cell
|
||||
Cells [][]Cell
|
||||
dirty bool
|
||||
}
|
||||
|
||||
/**
|
||||
* The dimensions of the model (height, width).
|
||||
*/
|
||||
func (sm *StdModel) Dimensions() (int, int) {
|
||||
if len(sm.Cells) == 0 {
|
||||
return 0, 0
|
||||
} else {
|
||||
return len(sm.Cells), len(sm.Cells[0])
|
||||
}
|
||||
if len(sm.Cells) == 0 {
|
||||
return 0, 0
|
||||
} else {
|
||||
return len(sm.Cells), len(sm.Cells[0])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of a cell.
|
||||
*/
|
||||
func (sm *StdModel) CellValue(r, c int) string {
|
||||
rs, cs := sm.Dimensions()
|
||||
if (r >= 0) && (c >= 0) && (r < rs) && (c < cs) {
|
||||
return sm.Cells[r][c].Value
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
rs, cs := sm.Dimensions()
|
||||
if (r >= 0) && (c >= 0) && (r < rs) && (c < cs) {
|
||||
return sm.Cells[r][c].Value
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Resize the model.
|
||||
func (sm *StdModel) Resize(rs, cs int) {
|
||||
oldRowCount := len(sm.Cells)
|
||||
oldRowCount := len(sm.Cells)
|
||||
|
||||
newRows := make([][]Cell, rs)
|
||||
for r := range newRows {
|
||||
newCols := make([]Cell, cs)
|
||||
if r < oldRowCount {
|
||||
copy(newCols, sm.Cells[r])
|
||||
}
|
||||
newRows[r] = newCols
|
||||
}
|
||||
newRows := make([][]Cell, rs)
|
||||
for r := range newRows {
|
||||
newCols := make([]Cell, cs)
|
||||
if r < oldRowCount {
|
||||
copy(newCols, sm.Cells[r])
|
||||
}
|
||||
newRows[r] = newCols
|
||||
}
|
||||
|
||||
sm.Cells = newRows
|
||||
sm.Cells = newRows
|
||||
sm.dirty = true
|
||||
}
|
||||
|
||||
// Sets the cell value
|
||||
func (sm *StdModel) SetCellValue(r, c int, value string) {
|
||||
rs, cs := sm.Dimensions()
|
||||
if (r >= 0) && (c >= 0) && (r < rs) && (c < cs) {
|
||||
sm.Cells[r][c].Value = value
|
||||
}
|
||||
rs, cs := sm.Dimensions()
|
||||
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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,11 +132,16 @@ 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue