diff --git a/README.md b/README.md index 6c1151a..334d932 100644 --- a/README.md +++ b/README.md @@ -55,18 +55,21 @@ Others: | `}` | Increase cell width | | `/` | Search for cell matching regular expression | | `n` | Find next cell matching search | +| `y` | Copy cell value | + | `p` | Paste cell value | | `:` | 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. | \ No newline at end of file +| 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. | + diff --git a/commandmap.go b/commandmap.go index 4ad8582..18355c7 100644 --- a/commandmap.go +++ b/commandmap.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "regexp" + "strings" "github.com/lmika/shellwords" @@ -67,12 +68,16 @@ func (cm *CommandMapping) Eval(ctx *CommandContext, expr string) error { return nil } - cmd := cm.Commands[toks[0]] + return cm.Invoke(ctx, toks[0], toks[1:]) +} + +func (cm *CommandMapping) Invoke(ctx *CommandContext, name string, args []string) error { + cmd := cm.Commands[name] if cmd != nil { - return cmd.Do(ctx.WithArgs(toks[1:])) + return cmd.Do(ctx.WithArgs(args)) } - return fmt.Errorf("no such command: %v", expr) + return fmt.Errorf("no such command: %v", name) } // Registers the standard view navigation commands. These commands require the frame @@ -345,8 +350,65 @@ func (cm *CommandMapping) RegisterViewCommands() { 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 { - wSource, isWSource := ctx.Session().Source.(WritableModelSource) + 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) if !isWSource { return fmt.Errorf("model is not writable") } @@ -356,6 +418,7 @@ func (cm *CommandMapping) RegisterViewCommands() { } ctx.Frame().Message("Wrote " + wSource.String()) + ctx.Session().Source = wSource return nil }) diff --git a/main.go b/main.go index 5d7acf0..7459418 100644 --- a/main.go +++ b/main.go @@ -46,4 +46,7 @@ var codecModelSourceBuilders = map[string]codecModelSourceBuilder{ "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 cde8efa..08e6805 100644 --- a/modelsource.go +++ b/modelsource.go @@ -2,9 +2,12 @@ package main import ( "encoding/csv" + "errors" + "fmt" "io" "os" "path/filepath" + "strings" ) // ModelSource is a source of models. At a minimum, it must be able to read models. @@ -108,3 +111,55 @@ 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 +}