Compare commits
No commits in common. "1f3c11f26524fb77e7b0eb702093b0412bc37be4" and "6e6e586f1d5216492c9b5ef12ba5b13bf3df768d" have entirely different histories.
1f3c11f265
...
6e6e586f1d
45
README.md
45
README.md
|
|
@ -15,14 +15,10 @@ go get github.com/lmika/ted
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
ted [FLAGS] FILE
|
ted <csvfile>
|
||||||
```
|
```
|
||||||
|
|
||||||
Flags:
|
Can either be a new CSV file, or an existing CSV file.
|
||||||
|
|
||||||
- `-c <codec>` 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.
|
|
||||||
|
|
||||||
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.
|
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 |
|
| `e` | Edit cell value |
|
||||||
| `r` | Replace cell value |
|
| `r` | Replace cell value |
|
||||||
| `a` | Insert row below cursor and edit value |
|
| `a` | Insert row below cursor and edit value |
|
||||||
| `O` | Insert column to the right of cursor |
|
|
||||||
| `D` | Delete current row |
|
| `D` | Delete current row |
|
||||||
|
|
||||||
Others:
|
Others:
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
|:----|:--------------------------------------------|
|
|:-----------|:--------------------|
|
||||||
| `{` | Reduce cell width |
|
| `{` | Reduce cell width |
|
||||||
| `}` | Increase cell width |
|
| `}` | Increase cell width |
|
||||||
| `/` | Search for cell matching regular expression |
|
| `/` | Search for cell matching regular expression |
|
||||||
| `n` | Find next cell matching search |
|
| `n` | Find next cell matching search |
|
||||||
| `N` | Find previous cell matching search |
|
| `:` | Enter command |
|
||||||
| `y` | Copy cell value |
|
|
||||||
| `p` | Paste cell value |
|
|
||||||
| `:` | Enter command |
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
Commands can be entered by pressing `:` and typing in the command or alias.
|
Commands can be entered by pressing `:` and typing in the command or alias.
|
||||||
|
|
||||||
| Command | Alias | Description |
|
| Command | Alias | Description |
|
||||||
|:----------------------|:-----------|:------------------------|
|
|:----------------|:-----------|:------------------------|
|
||||||
| `save` | `w` | Save the current file. |
|
| `save` | `w` | Save the current file. |
|
||||||
| `quit` | `q` | Quit the application without saving changes. |
|
| `quit` | `q` | Quit the application without saving changes. |
|
||||||
| `save-and-quit` | `wq` | Save the current file and quit the application. |
|
| `save-and-quit` | `wq` | Save the current file and quit the application. |
|
||||||
| `open-down` | | Insert a new row below the currently selected row. |
|
| `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. |
|
| `open-right` | | Insert a new column to the right of the currently selected column. |
|
||||||
| `delete-row` | | Delete the currently selected row. |
|
| `delete-row` | | Delete the currently selected row. |
|
||||||
| `delete-column` | | Delete the currently selected column. |
|
| `delete-column` | | Delete the currently selected column. |
|
||||||
|
|
||||||
201
commandmap.go
201
commandmap.go
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/lmika/shellwords"
|
"github.com/lmika/shellwords"
|
||||||
|
|
||||||
|
|
@ -68,16 +67,12 @@ func (cm *CommandMapping) Eval(ctx *CommandContext, expr string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return cm.Invoke(ctx, toks[0], toks[1:])
|
cmd := cm.Commands[toks[0]]
|
||||||
}
|
|
||||||
|
|
||||||
func (cm *CommandMapping) Invoke(ctx *CommandContext, name string, args []string) error {
|
|
||||||
cmd := cm.Commands[name]
|
|
||||||
if cmd != nil {
|
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
|
// 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 {
|
cm.Define("search", "Search for a cell", "", func(ctx *CommandContext) error {
|
||||||
ctx.Frame().Prompt(PromptOptions{Prompt: "/"}, func(res string) error {
|
ctx.Frame().Prompt(PromptOptions{Prompt: "/"}, func(res string) error {
|
||||||
if res == "" {
|
|
||||||
ctx.Session().SetLastSearch(nil)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
re, err := regexp.Compile(res)
|
re, err := regexp.Compile(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid regexp: %v", err)
|
return fmt.Errorf("invalid regexp: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Session().SetLastSearch(re)
|
ctx.session.LastSearch = re
|
||||||
return ctx.Session().Commands.Eval(ctx, "search-next")
|
return ctx.Session().Commands.Eval(ctx, "search-next")
|
||||||
})
|
})
|
||||||
return nil
|
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 {
|
cm.Define("x-replace", "Performs a search and replace", "", func(ctx *CommandContext) error {
|
||||||
if len(ctx.Args()) != 2 {
|
if len(ctx.Args()) != 2 {
|
||||||
return errors.New("Usage: x-replace MATCH REPLACEMENT")
|
return errors.New("Usage: x-replace MATCH REPLACEMENT")
|
||||||
|
|
@ -223,7 +187,11 @@ func (cm *CommandMapping) RegisterViewCommands() {
|
||||||
grid := ctx.Frame().Grid()
|
grid := ctx.Frame().Grid()
|
||||||
cellX, _ := grid.CellPosition()
|
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 {
|
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 {
|
cm.Define("mark-row-red", "Set row marker to red", "", func(ctx *CommandContext) error {
|
||||||
_, cellY := ctx.Frame().Grid().CellPosition()
|
_, 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 := ctx.ModelVC().RowAttrs(cellY)
|
||||||
attrs.Marker = MarkerRed
|
attrs.Marker = MarkerRed
|
||||||
ctx.ModelVC().SetRowAttrs(cellY, attrs)
|
ctx.ModelVC().SetRowAttrs(cellY, attrs)
|
||||||
|
|
@ -330,10 +276,7 @@ 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(PromptOptions{
|
ctx.Frame().Prompt(PromptOptions{Prompt: ":"}, func(res string) error {
|
||||||
Prompt: ":",
|
|
||||||
CancelOnEmptyBackspace: true,
|
|
||||||
}, func(res string) error {
|
|
||||||
return cm.Eval(ctx, res)
|
return cm.Eval(ctx, res)
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -357,15 +300,7 @@ func (cm *CommandMapping) RegisterViewCommands() {
|
||||||
grid := ctx.Frame().Grid()
|
grid := ctx.Frame().Grid()
|
||||||
cellX, cellY := grid.CellPosition()
|
cellX, cellY := grid.CellPosition()
|
||||||
|
|
||||||
if _, isRwModel := ctx.ModelVC().Model().(RWModel); !isRwModel {
|
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 {
|
|
||||||
ctx.Frame().Prompt(PromptOptions{
|
ctx.Frame().Prompt(PromptOptions{
|
||||||
Prompt: "> ",
|
Prompt: "> ",
|
||||||
InitialValue: grid.Model().CellValue(cellX, cellY),
|
InitialValue: grid.Model().CellValue(cellX, cellY),
|
||||||
|
|
@ -379,89 +314,9 @@ func (cm *CommandMapping) RegisterViewCommands() {
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
cm.Define("save", "Save current file", "", func(ctx *CommandContext) error {
|
||||||
var source ModelSource
|
wSource, isWSource := ctx.Session().Source.(WritableModelSource)
|
||||||
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)
|
|
||||||
if !isWSource {
|
if !isWSource {
|
||||||
return fmt.Errorf("model is not writable")
|
return fmt.Errorf("model is not writable")
|
||||||
}
|
}
|
||||||
|
|
@ -471,7 +326,6 @@ func (cm *CommandMapping) RegisterViewCommands() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Frame().Message("Wrote " + wSource.String())
|
ctx.Frame().Message("Wrote " + wSource.String())
|
||||||
ctx.Session().Source = wSource
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -508,7 +362,6 @@ func (cm *CommandMapping) RegisterViewKeyBindings() {
|
||||||
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.KeyCtrlC, cm.Command("quit"))
|
|
||||||
|
|
||||||
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"))
|
||||||
|
|
@ -520,15 +373,10 @@ func (cm *CommandMapping) RegisterViewKeyBindings() {
|
||||||
|
|
||||||
cm.MapKey('a', cm.Command("append"))
|
cm.MapKey('a', cm.Command("append"))
|
||||||
|
|
||||||
cm.MapKey('O', cm.Command("open-right"))
|
|
||||||
cm.MapKey('D', cm.Command("delete-row"))
|
cm.MapKey('D', cm.Command("delete-row"))
|
||||||
|
|
||||||
cm.MapKey('/', cm.Command("search"))
|
cm.MapKey('/', cm.Command("search"))
|
||||||
cm.MapKey('n', cm.Command("search-next"))
|
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('0', cm.Command("clear-row-marker"))
|
||||||
cm.MapKey('1', cm.Command("mark-row-red"))
|
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
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
7
frame.go
7
frame.go
|
|
@ -106,16 +106,13 @@ func (frame *Frame) Error(err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PromptOptions struct {
|
type PromptOptions struct {
|
||||||
Prompt string
|
Prompt string
|
||||||
InitialValue string
|
InitialValue string
|
||||||
CancelOnEmptyBackspace bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt the user for input. This switches the mode to entry mode.
|
// Prompt the user for input. This switches the mode to entry mode.
|
||||||
func (frame *Frame) Prompt(options PromptOptions, callback func(res string) error) {
|
func (frame *Frame) Prompt(options PromptOptions, callback func(res string) error) {
|
||||||
frame.textEntry.Reset()
|
|
||||||
frame.textEntry.Prompt = options.Prompt
|
frame.textEntry.Prompt = options.Prompt
|
||||||
frame.textEntry.CancelOnEmptyBackspace = options.CancelOnEmptyBackspace
|
|
||||||
frame.textEntry.SetValue(options.InitialValue)
|
frame.textEntry.SetValue(options.InitialValue)
|
||||||
|
|
||||||
frame.textEntry.OnCancel = frame.exitEntryMode
|
frame.textEntry.OnCancel = frame.exitEntryMode
|
||||||
|
|
|
||||||
18
go.mod
18
go.mod
|
|
@ -1,21 +1,9 @@
|
||||||
module github.com/lmika/ted
|
module github.com/lmika/ted
|
||||||
|
|
||||||
go 1.22
|
go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gdamore/tcell v1.4.0
|
|
||||||
github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe
|
github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe
|
||||||
github.com/stretchr/testify v1.7.5
|
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||||
)
|
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1
|
||||||
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
35
go.sum
35
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 h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18=
|
||||||
github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe/go.mod h1:qpdOkLougV5Yry4Px9f1w1pNMavcr6Z67VW5Ro+vW5I=
|
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/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 h1:lh3PyZvY+B9nFliSGTn5uFuqQQJGuNrD0MLCokv09ag=
|
||||||
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||||
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=
|
|
||||||
|
|
|
||||||
26
main.go
26
main.go
|
|
@ -1,14 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/lmika/ted/ui"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/lmika/ted/ui"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var flagCodec = flag.String("c", "csv", "file codec to use")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if flag.NArg() == 0 {
|
if flag.NArg() == 0 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: ted FILENAME")
|
fmt.Fprintln(os.Stderr, "usage: ted FILENAME")
|
||||||
|
|
@ -21,33 +20,12 @@ func main() {
|
||||||
}
|
}
|
||||||
defer uiManager.Close()
|
defer uiManager.Close()
|
||||||
|
|
||||||
codecBuilder, hasCodec := codecModelSourceBuilders[*flagCodec]
|
|
||||||
if !hasCodec {
|
|
||||||
fmt.Fprintf(os.Stderr, "unrecognised codec: %v", *flagCodec)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
frame := NewFrame(uiManager)
|
frame := NewFrame(uiManager)
|
||||||
session := NewSession(uiManager, frame, codecBuilder(flag.Arg(0)))
|
session := NewSession(uiManager, frame, CsvFileModelSource{flag.Arg(0)})
|
||||||
session.LoadFromSource()
|
session.LoadFromSource()
|
||||||
session.ResizeToBestFit()
|
|
||||||
|
|
||||||
uiManager.SetRootComponent(frame.RootComponent())
|
uiManager.SetRootComponent(frame.RootComponent())
|
||||||
frame.enterMode(GridMode)
|
frame.enterMode(GridMode)
|
||||||
|
|
||||||
uiManager.Loop()
|
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}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/csv"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"os"
|
||||||
|
"encoding/csv"
|
||||||
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ModelSource is a source of models. At a minimum, it must be able to read models.
|
// 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
|
// A model source backed by a CSV file
|
||||||
type CsvFileModelSource struct {
|
type CsvFileModelSource struct {
|
||||||
filename string
|
Filename string
|
||||||
options CsvFileModelSourceOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
type CsvFileModelSourceOptions struct {
|
|
||||||
Comma rune
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCsvFileModelSource(filename string, options CsvFileModelSourceOptions) CsvFileModelSource {
|
|
||||||
return CsvFileModelSource{
|
|
||||||
filename: filename,
|
|
||||||
options: options,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Describes the source
|
// Describes the source
|
||||||
func (s CsvFileModelSource) String() string {
|
func (s CsvFileModelSource) String() string {
|
||||||
return filepath.Base(s.filename)
|
return filepath.Base(s.Filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the model from the given source
|
// Read the model from the given source
|
||||||
func (s CsvFileModelSource) Read() (Model, error) {
|
func (s CsvFileModelSource) Read() (Model, error) {
|
||||||
// Check if the file exists. If not, return an empty model
|
// 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
|
return NewSingleCellStdModel(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Open(s.filename)
|
f, err := os.Open(s.Filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -64,13 +49,12 @@ func (s CsvFileModelSource) Read() (Model, error) {
|
||||||
|
|
||||||
model := new(StdModel)
|
model := new(StdModel)
|
||||||
r := csv.NewReader(f)
|
r := csv.NewReader(f)
|
||||||
r.Comma = s.options.Comma
|
|
||||||
r.FieldsPerRecord = -1
|
|
||||||
for {
|
for {
|
||||||
record, err := r.Read()
|
record, err := r.Read()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
} else if err != nil {
|
}
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,20 +66,19 @@ func (s CsvFileModelSource) Read() (Model, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s CsvFileModelSource) Write(m Model) error {
|
func (s CsvFileModelSource) Write(m Model) error {
|
||||||
f, err := os.Create(s.filename)
|
f, err := os.Create(s.Filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w := csv.NewWriter(f)
|
w := csv.NewWriter(f)
|
||||||
w.Comma = s.options.Comma
|
|
||||||
|
|
||||||
rows, cols := m.Dimensions()
|
rows, cols := m.Dimensions()
|
||||||
|
|
||||||
for r := 0; r < rows; r++ {
|
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++ {
|
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 {
|
if err := w.Write(record); err != nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
@ -111,55 +94,3 @@ func (s CsvFileModelSource) Write(m Model) error {
|
||||||
|
|
||||||
return f.Close()
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
36
session.go
36
session.go
|
|
@ -6,12 +6,6 @@ import (
|
||||||
"github.com/lmika/ted/ui"
|
"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 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 {
|
||||||
|
|
@ -21,7 +15,6 @@ type Session struct {
|
||||||
Commands *CommandMapping
|
Commands *CommandMapping
|
||||||
UIManager *ui.Ui
|
UIManager *ui.Ui
|
||||||
modelController *ModelViewCtrl
|
modelController *ModelViewCtrl
|
||||||
pasteBoard RWModel
|
|
||||||
|
|
||||||
LastSearch *regexp.Regexp
|
LastSearch *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
@ -35,7 +28,6 @@ func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session {
|
||||||
Commands: NewCommandMapping(),
|
Commands: NewCommandMapping(),
|
||||||
UIManager: uiManager,
|
UIManager: uiManager,
|
||||||
modelController: NewGridViewModel(model),
|
modelController: NewGridViewModel(model),
|
||||||
pasteBoard: NewSingleCellStdModel(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.SetModel(&SessionGridModel{session.modelController})
|
frame.SetModel(&SessionGridModel{session.modelController})
|
||||||
|
|
@ -61,23 +53,6 @@ func (session *Session) LoadFromSource() {
|
||||||
session.modelController.SetModel(newModel)
|
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
|
// 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
|
||||||
|
|
@ -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
|
// The command context used by the session
|
||||||
type CommandContext struct {
|
type CommandContext struct {
|
||||||
session *Session
|
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) {
|
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)
|
rowAttrs := sgm.GridViewModel.RowAttrs(y)
|
||||||
colAttrs := sgm.GridViewModel.ColAttrs(y)
|
colAttrs := sgm.GridViewModel.ColAttrs(y)
|
||||||
|
|
||||||
|
|
|
||||||
12
stdmodel.go
12
stdmodel.go
|
|
@ -14,20 +14,12 @@ type StdModel struct {
|
||||||
//
|
//
|
||||||
func NewSingleCellStdModel() *StdModel {
|
func NewSingleCellStdModel() *StdModel {
|
||||||
sm := new(StdModel)
|
sm := new(StdModel)
|
||||||
sm.appendStr([]string{""})
|
sm.appendStr([]string { "" })
|
||||||
sm.dirty = false
|
sm.dirty = false
|
||||||
|
|
||||||
return sm
|
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).
|
* 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) {
|
func (sm *StdModel) appendStr(row []string) {
|
||||||
if len(sm.Cells) == 0 {
|
if len(sm.Cells) == 0 {
|
||||||
cells := sm.strSliceToCell(row, len(row))
|
cells := sm.strSliceToCell(row, len(row))
|
||||||
sm.Cells = [][]Cell{cells}
|
sm.Cells = [][]Cell{ cells }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,10 @@ const (
|
||||||
KeyArrowLeft
|
KeyArrowLeft
|
||||||
KeyArrowRight
|
KeyArrowRight
|
||||||
|
|
||||||
KeyBackspace = KeyCtrlH
|
KeyBackspace = KeyCtrlH
|
||||||
KeyBackspace2
|
KeyBackspace2 = KeyCtrl8
|
||||||
KeyEnter = KeyCtrlM
|
KeyEnter = KeyCtrlM
|
||||||
KeyEsc = KeyCtrl3
|
KeyEsc = KeyCtrl3
|
||||||
)
|
)
|
||||||
|
|
||||||
// The type of events supported by the driver
|
// The type of events supported by the driver
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,6 @@ type TextEntry struct {
|
||||||
value string
|
value string
|
||||||
cursorOffset int
|
cursorOffset int
|
||||||
displayOffset 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
|
// Called when the user presses Enter
|
||||||
OnEntry func(val string)
|
OnEntry func(val string)
|
||||||
|
|
@ -70,10 +65,6 @@ type TextEntry struct {
|
||||||
OnCancel func()
|
OnCancel func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (te *TextEntry) Reset() {
|
|
||||||
te.isDirty = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (te *TextEntry) Remeasure(w, h int) (int, int) {
|
func (te *TextEntry) Remeasure(w, h int) (int, int) {
|
||||||
return w, 1
|
return w, 1
|
||||||
}
|
}
|
||||||
|
|
@ -125,18 +116,16 @@ func (te *TextEntry) KeyPressed(key rune, mod int) {
|
||||||
te.moveCursorBy(-1)
|
te.moveCursorBy(-1)
|
||||||
} else if key == KeyArrowRight {
|
} else if key == KeyArrowRight {
|
||||||
te.moveCursorBy(1)
|
te.moveCursorBy(1)
|
||||||
} else if (key == KeyHome) || (key == KeyCtrlA) {
|
} else if key == KeyHome {
|
||||||
te.moveCursorTo(0)
|
te.moveCursorTo(0)
|
||||||
} else if (key == KeyEnd) || (key == KeyCtrlE) {
|
} 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 if te.cursorOffset == 0 {
|
} else if te.cursorOffset == 0 {
|
||||||
if te.CancelOnEmptyBackspace && !te.isDirty {
|
te.cancelAndExit()
|
||||||
te.cancelAndExit()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
te.backspace()
|
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.
|
// 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() {
|
||||||
te.isDirty = true
|
|
||||||
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 {
|
||||||
|
|
@ -193,7 +181,6 @@ func (te *TextEntry) killLine() {
|
||||||
|
|
||||||
// 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) {
|
||||||
te.isDirty = true
|
|
||||||
if te.cursorOffset >= len(te.value) {
|
if te.cursorOffset >= len(te.value) {
|
||||||
te.value += string(key)
|
te.value += string(key)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -204,7 +191,6 @@ func (te *TextEntry) insertRune(key rune) {
|
||||||
|
|
||||||
// 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) {
|
||||||
te.isDirty = true
|
|
||||||
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:]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gdamore/tcell/termbox"
|
"github.com/nsf/termbox-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TermboxDriver struct {
|
type TermboxDriver struct {
|
||||||
|
|
@ -100,36 +100,38 @@ var termboxKeysToSpecialKeys = map[termbox.Key]rune{
|
||||||
termbox.KeyArrowDown: KeyArrowDown,
|
termbox.KeyArrowDown: KeyArrowDown,
|
||||||
termbox.KeyArrowLeft: KeyArrowLeft,
|
termbox.KeyArrowLeft: KeyArrowLeft,
|
||||||
termbox.KeyArrowRight: KeyArrowRight,
|
termbox.KeyArrowRight: KeyArrowRight,
|
||||||
termbox.KeyBackspace2: KeyBackspace2,
|
|
||||||
termbox.KeyCtrlA: KeyCtrlA,
|
termbox.KeyCtrlSpace: KeyCtrlSpace,
|
||||||
termbox.KeyCtrlB: KeyCtrlB,
|
termbox.KeyCtrlA: KeyCtrlA,
|
||||||
termbox.KeyCtrlC: KeyCtrlC,
|
termbox.KeyCtrlB: KeyCtrlB,
|
||||||
termbox.KeyCtrlD: KeyCtrlD,
|
termbox.KeyCtrlC: KeyCtrlC,
|
||||||
termbox.KeyCtrlE: KeyCtrlE,
|
termbox.KeyCtrlD: KeyCtrlD,
|
||||||
termbox.KeyCtrlF: KeyCtrlF,
|
termbox.KeyCtrlE: KeyCtrlE,
|
||||||
termbox.KeyCtrlG: KeyCtrlG,
|
termbox.KeyCtrlF: KeyCtrlF,
|
||||||
termbox.KeyCtrlH: KeyCtrlH,
|
termbox.KeyCtrlG: KeyCtrlG,
|
||||||
termbox.KeyCtrlI: KeyCtrlI,
|
termbox.KeyCtrlH: KeyCtrlH,
|
||||||
termbox.KeyCtrlJ: KeyCtrlJ,
|
termbox.KeyCtrlI: KeyCtrlI,
|
||||||
termbox.KeyCtrlK: KeyCtrlK,
|
termbox.KeyCtrlJ: KeyCtrlJ,
|
||||||
termbox.KeyCtrlL: KeyCtrlL,
|
termbox.KeyCtrlK: KeyCtrlK,
|
||||||
termbox.KeyCtrlM: KeyCtrlM,
|
termbox.KeyCtrlL: KeyCtrlL,
|
||||||
termbox.KeyCtrlN: KeyCtrlN,
|
termbox.KeyCtrlM: KeyCtrlM,
|
||||||
termbox.KeyCtrlO: KeyCtrlO,
|
termbox.KeyCtrlN: KeyCtrlN,
|
||||||
termbox.KeyCtrlP: KeyCtrlP,
|
termbox.KeyCtrlO: KeyCtrlO,
|
||||||
termbox.KeyCtrlQ: KeyCtrlQ,
|
termbox.KeyCtrlP: KeyCtrlP,
|
||||||
termbox.KeyCtrlR: KeyCtrlR,
|
termbox.KeyCtrlQ: KeyCtrlQ,
|
||||||
termbox.KeyCtrlS: KeyCtrlS,
|
termbox.KeyCtrlR: KeyCtrlR,
|
||||||
termbox.KeyCtrlT: KeyCtrlT,
|
termbox.KeyCtrlS: KeyCtrlS,
|
||||||
termbox.KeyCtrlU: KeyCtrlU,
|
termbox.KeyCtrlT: KeyCtrlT,
|
||||||
termbox.KeyCtrlV: KeyCtrlV,
|
termbox.KeyCtrlU: KeyCtrlU,
|
||||||
termbox.KeyCtrlW: KeyCtrlW,
|
termbox.KeyCtrlV: KeyCtrlV,
|
||||||
termbox.KeyCtrlX: KeyCtrlX,
|
termbox.KeyCtrlW: KeyCtrlW,
|
||||||
termbox.KeyCtrlY: KeyCtrlY,
|
termbox.KeyCtrlX: KeyCtrlX,
|
||||||
termbox.KeyCtrlZ: KeyCtrlZ,
|
termbox.KeyCtrlY: KeyCtrlY,
|
||||||
termbox.KeyCtrl3: KeyCtrl3,
|
termbox.KeyCtrlZ: KeyCtrlZ,
|
||||||
termbox.KeyCtrl4: KeyCtrl4,
|
termbox.KeyCtrl3: KeyCtrl3,
|
||||||
termbox.KeyCtrl5: KeyCtrl5,
|
termbox.KeyCtrl4: KeyCtrl4,
|
||||||
termbox.KeyCtrl6: KeyCtrl6,
|
termbox.KeyCtrl5: KeyCtrl5,
|
||||||
termbox.KeyCtrl7: KeyCtrl7,
|
termbox.KeyCtrl6: KeyCtrl6,
|
||||||
|
termbox.KeyCtrl7: KeyCtrl7,
|
||||||
|
termbox.KeyCtrl8: KeyCtrl8,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
viewmodel.go
50
viewmodel.go
|
|
@ -2,14 +2,12 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"regexp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelViewCtrl struct {
|
type ModelViewCtrl struct {
|
||||||
model Model
|
model Model
|
||||||
rowAttrs []SliceAttr
|
rowAttrs []SliceAttr
|
||||||
colAttrs []SliceAttr
|
colAttrs []SliceAttr
|
||||||
lastSearchRegexp *regexp.Regexp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGridViewModel(model Model) *ModelViewCtrl {
|
func NewGridViewModel(model Model) *ModelViewCtrl {
|
||||||
|
|
@ -18,10 +16,6 @@ func NewGridViewModel(model Model) *ModelViewCtrl {
|
||||||
return gvm
|
return gvm
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gvm *ModelViewCtrl) SetLastSearchRegexp(r *regexp.Regexp) {
|
|
||||||
gvm.lastSearchRegexp = r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gvm *ModelViewCtrl) Model() Model {
|
func (gvm *ModelViewCtrl) Model() Model {
|
||||||
return gvm.model
|
return gvm.model
|
||||||
}
|
}
|
||||||
|
|
@ -50,9 +44,7 @@ func (gvm *ModelViewCtrl) SetRowAttrs(row int, newAttrs SliceAttr) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gvm *ModelViewCtrl) SetColAttrs(col 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 {
|
func (gvm *ModelViewCtrl) SetCellValue(r, c int, newValue string) error {
|
||||||
|
|
@ -77,40 +69,6 @@ func (gvm *ModelViewCtrl) Resize(newRow, newCol int) error {
|
||||||
return nil
|
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
|
// Deletes a row of a model
|
||||||
func (gvm *ModelViewCtrl) DeleteRow(row int) error {
|
func (gvm *ModelViewCtrl) DeleteRow(row int) error {
|
||||||
rwModel, isRWModel := gvm.model.(RWModel)
|
rwModel, isRWModel := gvm.model.(RWModel)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue