diff --git a/README.md b/README.md index b89d339..b527ab2 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,10 @@ go get github.com/lmika/ted ## Usage ``` -ted [FLAGS] FILE +ted ``` -Flags: - -- `-c ` the format that the file is in. Either `csv` or `tsv` files are supported. Default is `csv` - -File can either be a new file, or an existing file. +Can either be a new CSV file, or an existing CSV file. TED is similar to Vim in that it is modal. After opening a file, the editor starts off in view mode, which permits navigating around. @@ -45,33 +41,28 @@ Editing: | `e` | Edit cell value | | `r` | Replace cell value | | `a` | Insert row below cursor and edit value | -| `O` | Insert column to the right of cursor | | `D` | Delete current row | Others: -| Key | Action | -|:----|:--------------------------------------------| -| `{` | Reduce cell width | -| `}` | Increase cell width | -| `/` | Search for cell matching regular expression | -| `n` | Find next cell matching search | -| `N` | Find previous cell matching search | -| `y` | Copy cell value | -| `p` | Paste cell value | -| `:` | Enter command | +| Key | Action | +|:-----------|:--------------------| +| `{` | Reduce cell width | +| `}` | Increase cell width | +| `/` | Search for cell matching regular expression | +| `n` | Find next cell matching search | +| `:` | Enter command | ## Commands Commands can be entered by pressing `:` and typing in the command or alias. -| Command | Alias | Description | -|:----------------------|:-----------|:------------------------| -| `save` | `w` | Save the current file. | -| `quit` | `q` | Quit the application without saving changes. | -| `save-and-quit` | `wq` | Save the current file and quit the application. | -| `open-down` | | Insert a new row below the currently selected row. | -| `open-right` | | Insert a new column to the right of the currently selected column. | -| `delete-row` | | Delete the currently selected row. | -| `delete-column` | | Delete the currently selected column. | - +| Command | Alias | Description | +|:----------------|:-----------|:------------------------| +| `save` | `w` | Save the current file. | +| `quit` | `q` | Quit the application without saving changes. | +| `save-and-quit` | `wq` | Save the current file and quit the application. | +| `open-down` | | Insert a new row below the currently selected row. | +| `open-right` | | Insert a new column to the right of the currently selected column. | +| `delete-row` | | Delete the currently selected row. | +| `delete-column` | | Delete the currently selected column. | \ No newline at end of file diff --git a/commandmap.go b/commandmap.go index 09f237c..e997b9a 100644 --- a/commandmap.go +++ b/commandmap.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "regexp" - "strings" "github.com/lmika/shellwords" @@ -68,16 +67,12 @@ func (cm *CommandMapping) Eval(ctx *CommandContext, expr string) error { return nil } - return cm.Invoke(ctx, toks[0], toks[1:]) -} - -func (cm *CommandMapping) Invoke(ctx *CommandContext, name string, args []string) error { - cmd := cm.Commands[name] + cmd := cm.Commands[toks[0]] if cmd != nil { - return cmd.Do(ctx.WithArgs(args)) + return cmd.Do(ctx.WithArgs(toks[1:])) } - return fmt.Errorf("no such command: %v", name) + return fmt.Errorf("no such command: %v", expr) } // Registers the standard view navigation commands. These commands require the frame @@ -126,17 +121,12 @@ func (cm *CommandMapping) RegisterViewCommands() { }) cm.Define("search", "Search for a cell", "", func(ctx *CommandContext) error { ctx.Frame().Prompt(PromptOptions{Prompt: "/"}, func(res string) error { - if res == "" { - ctx.Session().SetLastSearch(nil) - return nil - } - re, err := regexp.Compile(res) if err != nil { return fmt.Errorf("invalid regexp: %v", err) } - ctx.Session().SetLastSearch(re) + ctx.session.LastSearch = re return ctx.Session().Commands.Eval(ctx, "search-next") }) return nil @@ -164,32 +154,6 @@ func (cm *CommandMapping) RegisterViewCommands() { } } }) - cm.Define("search-prev", "Goto the previous cell", "", func(ctx *CommandContext) error { - if ctx.session.LastSearch == nil { - ctx.Session().Commands.Eval(ctx, "search") - } - - height, width := ctx.ModelVC().Model().Dimensions() - startX, startY := ctx.Frame().Grid().CellPosition() - cellX, cellY := startX, startY - - for { - cellX-- - if cellX < 0 { - cellX = width - 1 - cellY-- - if cellY < 0 { - cellY = height - 1 - } - } - if ctx.session.LastSearch.MatchString(ctx.ModelVC().Model().CellValue(cellY, cellX)) { - ctx.Frame().Grid().MoveTo(cellX, cellY) - return nil - } else if (cellX == startX) && (cellY == startY) { - return errors.New("No match found") - } - } - }) cm.Define("x-replace", "Performs a search and replace", "", func(ctx *CommandContext) error { if len(ctx.Args()) != 2 { return errors.New("Usage: x-replace MATCH REPLACEMENT") @@ -223,7 +187,11 @@ func (cm *CommandMapping) RegisterViewCommands() { grid := ctx.Frame().Grid() cellX, _ := grid.CellPosition() - return ctx.ModelVC().OpenRight(cellX) + height, width := ctx.ModelVC().Model().Dimensions() + if cellX == width-1 { + return ctx.ModelVC().Resize(height, width+1) + } + return nil }) cm.Define("open-down", "Inserts a row below the curser", "", func(ctx *CommandContext) error { @@ -283,28 +251,6 @@ func (cm *CommandMapping) RegisterViewCommands() { cm.Define("mark-row-red", "Set row marker to red", "", func(ctx *CommandContext) error { _, cellY := ctx.Frame().Grid().CellPosition() - namedArgs, _ := extractNamedArgs(ctx.args) - if rowMatch, hasRowMatch := namedArgs["row-match"]; hasRowMatch { - rows, cols := ctx.ModelVC().Model().Dimensions() - - for r := 0; r < rows; r++ { - rowMatches := false - for c := 0; c < cols; c++ { - cellVal := ctx.ModelVC().Model().CellValue(r, c) - if cellVal == rowMatch { - rowMatches = true - break - } - } - if rowMatches { - attrs := ctx.ModelVC().RowAttrs(r) - attrs.Marker = MarkerRed - ctx.ModelVC().SetRowAttrs(r, attrs) - } - } - return nil - } - attrs := ctx.ModelVC().RowAttrs(cellY) attrs.Marker = MarkerRed ctx.ModelVC().SetRowAttrs(cellY, attrs) @@ -330,10 +276,7 @@ func (cm *CommandMapping) RegisterViewCommands() { }) cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error { - ctx.Frame().Prompt(PromptOptions{ - Prompt: ":", - CancelOnEmptyBackspace: true, - }, func(res string) error { + ctx.Frame().Prompt(PromptOptions{Prompt: ":"}, func(res string) error { return cm.Eval(ctx, res) }) return nil @@ -357,15 +300,7 @@ func (cm *CommandMapping) RegisterViewCommands() { grid := ctx.Frame().Grid() cellX, cellY := grid.CellPosition() - if _, isRwModel := ctx.ModelVC().Model().(RWModel); !isRwModel { - return errors.New("Model is read-only") - } - - if len(ctx.Args()) == 1 { - if err := ctx.ModelVC().SetCellValue(cellY, cellX, ctx.Args()[0]); err != nil { - return err - } - } else { + if _, isRwModel := ctx.ModelVC().Model().(RWModel); isRwModel { ctx.Frame().Prompt(PromptOptions{ Prompt: "> ", InitialValue: grid.Model().CellValue(cellX, cellY), @@ -379,89 +314,9 @@ func (cm *CommandMapping) RegisterViewCommands() { } return nil }) - cm.Define("yank", "Yank cell value", "", func(ctx *CommandContext) error { - grid := ctx.Frame().Grid() - cellX, cellY := grid.CellPosition() - - // TODO: allow ranges - ctx.Session().pasteBoard.SetCellValue(0, 0, grid.Model().CellValue(cellX, cellY)) - - return nil - }) - cm.Define("paste", "Paste cell value", "", func(ctx *CommandContext) error { - grid := ctx.Frame().Grid() - cellX, cellY := grid.CellPosition() - - // TODO: allow ranges - if _, isRwModel := ctx.ModelVC().Model().(RWModel); !isRwModel { - return errors.New("Model is read-only") - } - if err := ctx.ModelVC().SetCellValue(cellY, cellX, ctx.Session().pasteBoard.CellValue(0, 0)); err != nil { - return err - } - - return nil - }) - - cm.Define("to-upper", "Convert cell value to uppercase", "", func(ctx *CommandContext) error { - grid := ctx.Frame().Grid() - cellX, cellY := grid.CellPosition() - - // TODO: allow ranges - - if _, isRwModel := ctx.ModelVC().Model().(RWModel); !isRwModel { - return errors.New("Model is read-only") - } - - currentValue := ctx.ModelVC().Model().CellValue(cellY, cellX) - newValue := strings.ToUpper(currentValue) - if err := ctx.ModelVC().SetCellValue(cellY, cellX, newValue); err != nil { - return err - } - - return nil - }) - - cm.Define("each-row", "Executes the command for each row in the column", "", func(ctx *CommandContext) error { - if len(ctx.args) != 1 { - return errors.New("Sub-command required") - } - - grid := ctx.Frame().Grid() - rows, _ := ctx.ModelVC().Model().Dimensions() - - cellX, cellY := grid.CellPosition() - defer grid.MoveTo(cellX, cellY) - - subCommand := ctx.args - - for r := 0; r < rows; r++ { - grid.MoveTo(cellX, r) - - if err := ctx.Session().Commands.Invoke(ctx, subCommand[0], subCommand[1:]); err != nil { - return fmt.Errorf("at [%d, %d]: %v", cellX, r, err) - } - } - - return nil - }) cm.Define("save", "Save current file", "", func(ctx *CommandContext) error { - var source ModelSource - if len(ctx.args) >= 2 { - targetCodecName := ctx.args[0] - codecBuilder, hasCodec := codecModelSourceBuilders[targetCodecName] - if !hasCodec { - return fmt.Errorf("unrecognsed codec: %v", targetCodecName) - } - - targetFilename := ctx.args[1] - source = codecBuilder(targetFilename) - } else { - source = ctx.Session().Source - } - - wSource, isWSource := source.(WritableModelSource) + wSource, isWSource := ctx.Session().Source.(WritableModelSource) if !isWSource { return fmt.Errorf("model is not writable") } @@ -471,7 +326,6 @@ func (cm *CommandMapping) RegisterViewCommands() { } ctx.Frame().Message("Wrote " + wSource.String()) - ctx.Session().Source = wSource return nil }) @@ -508,7 +362,6 @@ func (cm *CommandMapping) RegisterViewKeyBindings() { cm.MapKey(ui.KeyCtrlK, cm.Command("row-bottom")) cm.MapKey(ui.KeyCtrlJ, cm.Command("col-left")) cm.MapKey(ui.KeyCtrlL, cm.Command("col-right")) - cm.MapKey(ui.KeyCtrlC, cm.Command("quit")) cm.MapKey(ui.KeyArrowUp, cm.Command("move-up")) cm.MapKey(ui.KeyArrowDown, cm.Command("move-down")) @@ -520,15 +373,10 @@ func (cm *CommandMapping) RegisterViewKeyBindings() { cm.MapKey('a', cm.Command("append")) - cm.MapKey('O', cm.Command("open-right")) cm.MapKey('D', cm.Command("delete-row")) cm.MapKey('/', cm.Command("search")) cm.MapKey('n', cm.Command("search-next")) - cm.MapKey('N', cm.Command("search-prev")) - - cm.MapKey('y', cm.Command("yank")) - cm.MapKey('p', cm.Command("paste")) cm.MapKey('0', cm.Command("clear-row-marker")) cm.MapKey('1', cm.Command("mark-row-red")) @@ -550,28 +398,3 @@ func gridNavOperation(op func(grid *ui.Grid)) func(ctx *CommandContext) error { return nil } } - -// extractNamedArgs will extract out any arguments of the form '-name value' and stick it in a map. -// Any non-named arguments will be extracted out as positional. If an argument only has a name, it -// will be added to the map with the empty string as the value. -func extractNamedArgs(args []string) (namedArgs map[string]string, positionalArgs []string) { - for len(args) > 0 { - if args[0][0] == '-' { - if namedArgs == nil { - namedArgs = make(map[string]string) - } - - if len(args) >= 2 && args[1][0] != '-' { - namedArgs[args[0][1:]] = args[1] - args = args[2:] - } else { - namedArgs[args[0][1:]] = "" - args = args[1:] - } - } else { - positionalArgs = append(positionalArgs, args[0]) - args = args[1:] - } - } - return namedArgs, positionalArgs -} diff --git a/frame.go b/frame.go index a656d54..6d72caf 100644 --- a/frame.go +++ b/frame.go @@ -106,16 +106,13 @@ func (frame *Frame) Error(err error) { } type PromptOptions struct { - Prompt string - InitialValue string - CancelOnEmptyBackspace bool + Prompt string + InitialValue string } // Prompt the user for input. This switches the mode to entry mode. func (frame *Frame) Prompt(options PromptOptions, callback func(res string) error) { - frame.textEntry.Reset() frame.textEntry.Prompt = options.Prompt - frame.textEntry.CancelOnEmptyBackspace = options.CancelOnEmptyBackspace frame.textEntry.SetValue(options.InitialValue) frame.textEntry.OnCancel = frame.exitEntryMode diff --git a/go.mod b/go.mod index d31faa9..1aaa82d 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,9 @@ module github.com/lmika/ted -go 1.22 +go 1.15 require ( - github.com/gdamore/tcell v1.4.0 github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe - github.com/stretchr/testify v1.7.5 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gdamore/encoding v1.0.0 // indirect - github.com/lucasb-eyer/go-colorful v1.0.3 // indirect - github.com/mattn/go-runewidth v0.0.10 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect - golang.org/x/text v0.3.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 ) diff --git a/go.sum b/go.sum index a863b4b..87c946c 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,6 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= -github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= -github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18= github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe/go.mod h1:qpdOkLougV5Yry4Px9f1w1pNMavcr6Z67VW5Ro+vW5I= -github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= -github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= -github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= -golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 h1:lh3PyZvY+B9nFliSGTn5uFuqQQJGuNrD0MLCokv09ag= +github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= diff --git a/main.go b/main.go index 36cead3..6aca219 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,13 @@ package main import ( + "github.com/lmika/ted/ui" "flag" "fmt" - "github.com/lmika/ted/ui" "os" ) func main() { - var flagCodec = flag.String("c", "csv", "file codec to use") flag.Parse() if flag.NArg() == 0 { fmt.Fprintln(os.Stderr, "usage: ted FILENAME") @@ -21,33 +20,12 @@ func main() { } defer uiManager.Close() - codecBuilder, hasCodec := codecModelSourceBuilders[*flagCodec] - if !hasCodec { - fmt.Fprintf(os.Stderr, "unrecognised codec: %v", *flagCodec) - os.Exit(1) - } - frame := NewFrame(uiManager) - session := NewSession(uiManager, frame, codecBuilder(flag.Arg(0))) + session := NewSession(uiManager, frame, CsvFileModelSource{flag.Arg(0)}) session.LoadFromSource() - session.ResizeToBestFit() uiManager.SetRootComponent(frame.RootComponent()) frame.enterMode(GridMode) uiManager.Loop() } - -type codecModelSourceBuilder func(filename string) ModelSource - -var codecModelSourceBuilders = map[string]codecModelSourceBuilder{ - "csv": func(filename string) ModelSource { - return NewCsvFileModelSource(filename, CsvFileModelSourceOptions{Comma: ','}) - }, - "tsv": func(filename string) ModelSource { - return NewCsvFileModelSource(filename, CsvFileModelSourceOptions{Comma: '\t'}) - }, - "jira": func(filename string) ModelSource { - return JiraTableModelSource{Filename: filename, Header: true} - }, -} diff --git a/modelsource.go b/modelsource.go index 08e6805..9b5c4fe 100644 --- a/modelsource.go +++ b/modelsource.go @@ -1,13 +1,10 @@ package main import ( - "encoding/csv" - "errors" - "fmt" - "io" - "os" "path/filepath" - "strings" + "os" + "encoding/csv" + "io" ) // ModelSource is a source of models. At a minimum, it must be able to read models. @@ -29,34 +26,22 @@ type WritableModelSource interface { // A model source backed by a CSV file type CsvFileModelSource struct { - filename string - options CsvFileModelSourceOptions -} - -type CsvFileModelSourceOptions struct { - Comma rune -} - -func NewCsvFileModelSource(filename string, options CsvFileModelSourceOptions) CsvFileModelSource { - return CsvFileModelSource{ - filename: filename, - options: options, - } + Filename string } // Describes the source func (s CsvFileModelSource) String() string { - return filepath.Base(s.filename) + return filepath.Base(s.Filename) } // Read the model from the given source func (s CsvFileModelSource) Read() (Model, error) { // Check if the file exists. If not, return an empty model - if _, err := os.Stat(s.filename); os.IsNotExist(err) { + if _, err := os.Stat(s.Filename); os.IsNotExist(err) { return NewSingleCellStdModel(), nil } - f, err := os.Open(s.filename) + f, err := os.Open(s.Filename) if err != nil { return nil, err } @@ -64,13 +49,12 @@ func (s CsvFileModelSource) Read() (Model, error) { model := new(StdModel) r := csv.NewReader(f) - r.Comma = s.options.Comma - r.FieldsPerRecord = -1 for { record, err := r.Read() if err == io.EOF { break - } else if err != nil { + } + if err != nil { return nil, err } @@ -82,20 +66,19 @@ func (s CsvFileModelSource) Read() (Model, error) { } func (s CsvFileModelSource) Write(m Model) error { - f, err := os.Create(s.filename) + f, err := os.Create(s.Filename) if err != nil { return err } w := csv.NewWriter(f) - w.Comma = s.options.Comma rows, cols := m.Dimensions() for r := 0; r < rows; r++ { - record := make([]string, cols) // Reuse the record slice + record := make([]string, cols) // Reuse the record slice for c := 0; c < cols; c++ { - record[c] = m.CellValue(r, c) + record[c] = m.CellValue(r, c) } if err := w.Write(record); err != nil { f.Close() @@ -110,56 +93,4 @@ func (s CsvFileModelSource) Write(m Model) error { } return f.Close() -} - -type JiraTableModelSource struct { - Filename string - Header bool -} - -func (s JiraTableModelSource) String() string { - return filepath.Base(s.Filename) -} - -func (JiraTableModelSource) Read() (Model, error) { - return nil, errors.New("read not supported yet") -} - -func (s JiraTableModelSource) Write(m Model) error { - f, err := os.Create(s.Filename) - if err != nil { - return err - } - defer f.Close() - - rows, cols := m.Dimensions() - line := new(strings.Builder) - - for r := 0; r < rows; r++ { - sep := "|" - if r == 0 && s.Header { - sep = "||" - } - - line.Reset() - line.WriteString(sep) - line.WriteRune(' ') - - for c := 0; c < cols; c++ { - if c >= 1 { - line.WriteRune(' ') - line.WriteString(sep) - line.WriteRune(' ') - } - line.WriteString(m.CellValue(r, c)) - } - - line.WriteRune(' ') - line.WriteString(sep) - if _, err := fmt.Fprintln(f, line.String()); err != nil { - return err - } - } - - return nil -} +} \ No newline at end of file diff --git a/session.go b/session.go index 4776971..a8154b7 100644 --- a/session.go +++ b/session.go @@ -6,12 +6,6 @@ import ( "github.com/lmika/ted/ui" ) -const ( - bestFixRightMargin = 2 - bestFixMinColumnSize = 8 - bestFixMaxColumnSize = 32 -) - // The session is responsible for managing the UI and the model and handling // the interaction between the two and the user. type Session struct { @@ -21,7 +15,6 @@ type Session struct { Commands *CommandMapping UIManager *ui.Ui modelController *ModelViewCtrl - pasteBoard RWModel LastSearch *regexp.Regexp } @@ -35,7 +28,6 @@ func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session { Commands: NewCommandMapping(), UIManager: uiManager, modelController: NewGridViewModel(model), - pasteBoard: NewSingleCellStdModel(), } frame.SetModel(&SessionGridModel{session.modelController}) @@ -61,23 +53,6 @@ func (session *Session) LoadFromSource() { session.modelController.SetModel(newModel) } -func (sesion *Session) ResizeToBestFit() { - mr, mc := sesion.model.Dimensions() - colMaxSize := make([]int, mc) - for c := 0; c < mc; c++ { - colMaxSize[c] = bestFixMinColumnSize - for r := 0; r < mr; r++ { - colMaxSize[c] = max(colMaxSize[c], len(sesion.model.CellValue(r, c))+bestFixRightMargin) - } - } - - for c, ml := range colMaxSize { - sesion.modelController.SetColAttrs(c, SliceAttr{ - Size: min(ml, bestFixMaxColumnSize), - }) - } -} - // Input from the frame func (session *Session) KeyPressed(key rune, mod int) { // Add the mod key modifier @@ -94,11 +69,6 @@ func (session *Session) KeyPressed(key rune, mod int) { } } -func (s *Session) SetLastSearch(re *regexp.Regexp) { - s.LastSearch = re - s.modelController.lastSearchRegexp = re -} - // The command context used by the session type CommandContext struct { session *Session @@ -160,12 +130,6 @@ func (sgm *SessionGridModel) CellValue(x int, y int) string { } func (sgm *SessionGridModel) CellAttributes(x int, y int) (fg, bg ui.Attribute) { - if sgm.GridViewModel.lastSearchRegexp != nil { - if sgm.GridViewModel.lastSearchRegexp.MatchString(sgm.GridViewModel.Model().CellValue(y, x)) { - return ui.ColorMagenta, 0 - } - } - rowAttrs := sgm.GridViewModel.RowAttrs(y) colAttrs := sgm.GridViewModel.ColAttrs(y) diff --git a/stdmodel.go b/stdmodel.go index 975279d..306e224 100644 --- a/stdmodel.go +++ b/stdmodel.go @@ -14,20 +14,12 @@ type StdModel struct { // func NewSingleCellStdModel() *StdModel { sm := new(StdModel) - sm.appendStr([]string{""}) + sm.appendStr([]string { "" }) sm.dirty = false return sm } -func NewStdModelFromSlice(str [][]string) *StdModel { - sm := new(StdModel) - for _, r := range str { - sm.appendStr(r) - } - return sm -} - /** * The dimensions of the model (height, width). */ @@ -81,7 +73,7 @@ func (sm *StdModel) SetCellValue(r, c int, value string) { func (sm *StdModel) appendStr(row []string) { if len(sm.Cells) == 0 { cells := sm.strSliceToCell(row, len(row)) - sm.Cells = [][]Cell{cells} + sm.Cells = [][]Cell{ cells } return } diff --git a/ui/driver.go b/ui/driver.go index bef7321..9b642f7 100644 --- a/ui/driver.go +++ b/ui/driver.go @@ -84,10 +84,10 @@ const ( KeyArrowLeft KeyArrowRight - KeyBackspace = KeyCtrlH - KeyBackspace2 - KeyEnter = KeyCtrlM - KeyEsc = KeyCtrl3 + KeyBackspace = KeyCtrlH + KeyBackspace2 = KeyCtrl8 + KeyEnter = KeyCtrlM + KeyEsc = KeyCtrl3 ) // The type of events supported by the driver diff --git a/ui/stdcomps.go b/ui/stdcomps.go index 5594523..7c7090b 100644 --- a/ui/stdcomps.go +++ b/ui/stdcomps.go @@ -57,11 +57,6 @@ type TextEntry struct { value string cursorOffset int displayOffset int - isDirty bool - - // CancelOnEmptyBackspace will cancel the text entry prompt if no other - // key was pressed and the prompt was empty. - CancelOnEmptyBackspace bool // Called when the user presses Enter OnEntry func(val string) @@ -70,10 +65,6 @@ type TextEntry struct { OnCancel func() } -func (te *TextEntry) Reset() { - te.isDirty = false -} - func (te *TextEntry) Remeasure(w, h int) (int, int) { return w, 1 } @@ -125,18 +116,16 @@ func (te *TextEntry) KeyPressed(key rune, mod int) { te.moveCursorBy(-1) } else if key == KeyArrowRight { te.moveCursorBy(1) - } else if (key == KeyHome) || (key == KeyCtrlA) { + } else if key == KeyHome { te.moveCursorTo(0) - } else if (key == KeyEnd) || (key == KeyCtrlE) { + } else if key == KeyEnd { te.moveCursorTo(len(te.value)) } else if (key == KeyBackspace) || (key == KeyBackspace2) { if mod&ModKeyAlt != 0 { te.backspaceWhile(unicode.IsSpace) te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) }) } else if te.cursorOffset == 0 { - if te.CancelOnEmptyBackspace && !te.isDirty { - te.cancelAndExit() - } + te.cancelAndExit() } else { te.backspace() } @@ -182,7 +171,6 @@ func (te *TextEntry) backspaceWhile(guard func(r rune) bool) { // Kill the line. If the cursor is at the end of the line, kill to the start. // Otherwise, trim the line. func (te *TextEntry) killLine() { - te.isDirty = true if te.cursorOffset < len(te.value) { te.value = te.value[:te.cursorOffset] } else { @@ -193,7 +181,6 @@ func (te *TextEntry) killLine() { // Inserts a rune at the cursor position func (te *TextEntry) insertRune(key rune) { - te.isDirty = true if te.cursorOffset >= len(te.value) { te.value += string(key) } else { @@ -204,7 +191,6 @@ func (te *TextEntry) insertRune(key rune) { // Remove the character at a specific position func (te *TextEntry) removeCharAtPos(pos int) { - te.isDirty = true if (pos >= 0) && (pos < len(te.value)) { te.value = te.value[:pos] + te.value[pos+1:] } diff --git a/ui/termboxdriver.go b/ui/termboxdriver.go index 6f724a5..a7a9d26 100644 --- a/ui/termboxdriver.go +++ b/ui/termboxdriver.go @@ -3,7 +3,7 @@ package ui import ( - "github.com/gdamore/tcell/termbox" + "github.com/nsf/termbox-go" ) type TermboxDriver struct { @@ -100,36 +100,38 @@ var termboxKeysToSpecialKeys = map[termbox.Key]rune{ termbox.KeyArrowDown: KeyArrowDown, termbox.KeyArrowLeft: KeyArrowLeft, termbox.KeyArrowRight: KeyArrowRight, - termbox.KeyBackspace2: KeyBackspace2, - termbox.KeyCtrlA: KeyCtrlA, - termbox.KeyCtrlB: KeyCtrlB, - termbox.KeyCtrlC: KeyCtrlC, - termbox.KeyCtrlD: KeyCtrlD, - termbox.KeyCtrlE: KeyCtrlE, - termbox.KeyCtrlF: KeyCtrlF, - termbox.KeyCtrlG: KeyCtrlG, - termbox.KeyCtrlH: KeyCtrlH, - termbox.KeyCtrlI: KeyCtrlI, - termbox.KeyCtrlJ: KeyCtrlJ, - termbox.KeyCtrlK: KeyCtrlK, - termbox.KeyCtrlL: KeyCtrlL, - termbox.KeyCtrlM: KeyCtrlM, - termbox.KeyCtrlN: KeyCtrlN, - termbox.KeyCtrlO: KeyCtrlO, - termbox.KeyCtrlP: KeyCtrlP, - termbox.KeyCtrlQ: KeyCtrlQ, - termbox.KeyCtrlR: KeyCtrlR, - termbox.KeyCtrlS: KeyCtrlS, - termbox.KeyCtrlT: KeyCtrlT, - termbox.KeyCtrlU: KeyCtrlU, - termbox.KeyCtrlV: KeyCtrlV, - termbox.KeyCtrlW: KeyCtrlW, - termbox.KeyCtrlX: KeyCtrlX, - termbox.KeyCtrlY: KeyCtrlY, - termbox.KeyCtrlZ: KeyCtrlZ, - termbox.KeyCtrl3: KeyCtrl3, - termbox.KeyCtrl4: KeyCtrl4, - termbox.KeyCtrl5: KeyCtrl5, - termbox.KeyCtrl6: KeyCtrl6, - termbox.KeyCtrl7: KeyCtrl7, + + termbox.KeyCtrlSpace: KeyCtrlSpace, + termbox.KeyCtrlA: KeyCtrlA, + termbox.KeyCtrlB: KeyCtrlB, + termbox.KeyCtrlC: KeyCtrlC, + termbox.KeyCtrlD: KeyCtrlD, + termbox.KeyCtrlE: KeyCtrlE, + termbox.KeyCtrlF: KeyCtrlF, + termbox.KeyCtrlG: KeyCtrlG, + termbox.KeyCtrlH: KeyCtrlH, + termbox.KeyCtrlI: KeyCtrlI, + termbox.KeyCtrlJ: KeyCtrlJ, + termbox.KeyCtrlK: KeyCtrlK, + termbox.KeyCtrlL: KeyCtrlL, + termbox.KeyCtrlM: KeyCtrlM, + termbox.KeyCtrlN: KeyCtrlN, + termbox.KeyCtrlO: KeyCtrlO, + termbox.KeyCtrlP: KeyCtrlP, + termbox.KeyCtrlQ: KeyCtrlQ, + termbox.KeyCtrlR: KeyCtrlR, + termbox.KeyCtrlS: KeyCtrlS, + termbox.KeyCtrlT: KeyCtrlT, + termbox.KeyCtrlU: KeyCtrlU, + termbox.KeyCtrlV: KeyCtrlV, + termbox.KeyCtrlW: KeyCtrlW, + termbox.KeyCtrlX: KeyCtrlX, + termbox.KeyCtrlY: KeyCtrlY, + termbox.KeyCtrlZ: KeyCtrlZ, + termbox.KeyCtrl3: KeyCtrl3, + termbox.KeyCtrl4: KeyCtrl4, + termbox.KeyCtrl5: KeyCtrl5, + termbox.KeyCtrl6: KeyCtrl6, + termbox.KeyCtrl7: KeyCtrl7, + termbox.KeyCtrl8: KeyCtrl8, } diff --git a/viewmodel.go b/viewmodel.go index 68aa95b..1d0b3cd 100644 --- a/viewmodel.go +++ b/viewmodel.go @@ -2,14 +2,12 @@ package main import ( "errors" - "regexp" ) type ModelViewCtrl struct { - model Model - rowAttrs []SliceAttr - colAttrs []SliceAttr - lastSearchRegexp *regexp.Regexp + model Model + rowAttrs []SliceAttr + colAttrs []SliceAttr } func NewGridViewModel(model Model) *ModelViewCtrl { @@ -18,10 +16,6 @@ func NewGridViewModel(model Model) *ModelViewCtrl { return gvm } -func (gvm *ModelViewCtrl) SetLastSearchRegexp(r *regexp.Regexp) { - gvm.lastSearchRegexp = r -} - func (gvm *ModelViewCtrl) Model() Model { return gvm.model } @@ -50,9 +44,7 @@ func (gvm *ModelViewCtrl) SetRowAttrs(row int, newAttrs SliceAttr) { } func (gvm *ModelViewCtrl) SetColAttrs(col int, newAttrs SliceAttr) { - if col >= 0 && col < len(gvm.colAttrs) { - gvm.colAttrs[col] = newAttrs - } + gvm.colAttrs[col] = newAttrs } func (gvm *ModelViewCtrl) SetCellValue(r, c int, newValue string) error { @@ -77,40 +69,6 @@ func (gvm *ModelViewCtrl) Resize(newRow, newCol int) error { return nil } -func (gvm *ModelViewCtrl) OpenRight(col int) error { - if col < 0 { - return errors.New("col out of bound") - } - return gvm.insertColumn(col + 1) -} - -func (gvm *ModelViewCtrl) insertColumn(col int) error { - rwModel, isRWModel := gvm.model.(RWModel) - if !isRWModel { - return ErrModelReadOnly - } - - dr, dc := rwModel.Dimensions() - if col < 0 || col > dc { - return errors.New("col out of bound") - } - - rwModel.Resize(dr, dc+1) - - for c := dc; c >= col; c-- { - for r := 0; r < dr; r++ { - if c == col { - rwModel.SetCellValue(r, c, "") - } else { - rwModel.SetCellValue(r, c, rwModel.CellValue(r, c-1)) - } - } - } - gvm.modelWasResized() - - return nil -} - // Deletes a row of a model func (gvm *ModelViewCtrl) DeleteRow(row int) error { rwModel, isRWModel := gvm.model.(RWModel) diff --git a/viewmodel_test.go b/viewmodel_test.go deleted file mode 100644 index 56ff8e6..0000000 --- a/viewmodel_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "fmt" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestModelViewCtrl_OpenRight(t *testing.T) { - t.Run("should move cols to the right within the model", func(t *testing.T) { - rwModel := NewStdModelFromSlice([][]string{ - {"letters", "numbers", "greek"}, - {"a", "1", "alpha"}, - {"b", "2", "bravo"}, - {"c", "3", "charlie"}, - }) - - mvc := NewGridViewModel(rwModel) - err := mvc.OpenRight(1) - assert.NoError(t, err) - - assertModel(t, rwModel, [][]string{ - {"letters", "numbers", "", "greek"}, - {"a", "1", "", "alpha"}, - {"b", "2", "", "bravo"}, - {"c", "3", "", "charlie"}, - }) - }) - - t.Run("should move cols to the right at the left of the model", func(t *testing.T) { - rwModel := NewStdModelFromSlice([][]string{ - {"letters", "numbers", "greek"}, - {"a", "1", "alpha"}, - {"b", "2", "bravo"}, - {"c", "3", "charlie"}, - }) - - mvc := NewGridViewModel(rwModel) - err := mvc.OpenRight(0) - assert.NoError(t, err) - - assertModel(t, rwModel, [][]string{ - {"letters", "", "numbers", "greek"}, - {"a", "", "1", "alpha"}, - {"b", "", "2", "bravo"}, - {"c", "", "3", "charlie"}, - }) - }) - - t.Run("should move cols to the right at the right of the model", func(t *testing.T) { - rwModel := NewStdModelFromSlice([][]string{ - {"letters", "numbers", "greek"}, - {"a", "1", "alpha"}, - {"b", "2", "bravo"}, - {"c", "3", "charlie"}, - }) - - mvc := NewGridViewModel(rwModel) - err := mvc.OpenRight(2) - assert.NoError(t, err) - - assertModel(t, rwModel, [][]string{ - {"letters", "numbers", "greek", ""}, - {"a", "1", "alpha", ""}, - {"b", "2", "bravo", ""}, - {"c", "3", "charlie", ""}, - }) - }) - - t.Run("should return error if row out of bounds", func(t *testing.T) { - scenario := []int{-1, 3, 12} - for _, scenario := range scenario { - t.Run(fmt.Sprint(scenario), func(t *testing.T) { - rwModel := NewStdModelFromSlice([][]string{ - {"letters", "numbers", "greek"}, - {"a", "1", "alpha"}, - {"b", "2", "bravo"}, - {"c", "3", "charlie"}, - }) - mvc := NewGridViewModel(rwModel) - err := mvc.OpenRight(scenario) - assert.Error(t, err) - }) - } - }) -} - -func assertModel(t *testing.T, actual Model, expected [][]string) { - dr, dc := actual.Dimensions() - assert.Equalf(t, len(expected), dr, "number of rows in model") - - for r, row := range expected { - assert.Equalf(t, len(row), dc, "number of cols in row %v", r) - for c, cell := range row { - assert.Equalf(t, cell, actual.CellValue(r, c), "cell value at row %v, col %v", r, c) - } - } -}