From e790b508a15e65aaf29cd1fd761128f946110cbf Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 21 Oct 2020 14:35:52 +1100 Subject: [PATCH 01/10] Some small quality of life improvements - Added Ctrl+A and Ctrl+E for moving around the edit bar a.la. Emacs - Added the "yank" and "paste" command to copy and paste a single cell value. These are mapped to "y" and "p". - Modified the edit bar so that entering commands is the only case where typing backspace on an empty edit bar will cancel out of edit mode --- commandmap.go | 41 +++++++++++++++++++++++++++++++++++++++-- frame.go | 7 +++++-- session.go | 2 ++ ui/stdcomps.go | 20 +++++++++++++++++--- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/commandmap.go b/commandmap.go index e997b9a..157bfb0 100644 --- a/commandmap.go +++ b/commandmap.go @@ -276,7 +276,10 @@ func (cm *CommandMapping) RegisterViewCommands() { }) cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error { - ctx.Frame().Prompt(PromptOptions{Prompt: ":"}, func(res string) error { + ctx.Frame().Prompt(PromptOptions{ + Prompt: ":", + CancelOnEmptyBackspace: true, + }, func(res string) error { return cm.Eval(ctx, res) }) return nil @@ -300,7 +303,15 @@ func (cm *CommandMapping) RegisterViewCommands() { grid := ctx.Frame().Grid() 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{ Prompt: "> ", InitialValue: grid.Model().CellValue(cellX, cellY), @@ -314,6 +325,29 @@ 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("save", "Save current file", "", func(ctx *CommandContext) error { wSource, isWSource := ctx.Session().Source.(WritableModelSource) @@ -378,6 +412,9 @@ func (cm *CommandMapping) RegisterViewKeyBindings() { cm.MapKey('/', cm.Command("search")) cm.MapKey('n', cm.Command("search-next")) + 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")) cm.MapKey('2', cm.Command("mark-row-green")) diff --git a/frame.go b/frame.go index 6d72caf..a656d54 100644 --- a/frame.go +++ b/frame.go @@ -106,13 +106,16 @@ func (frame *Frame) Error(err error) { } type PromptOptions struct { - Prompt string - InitialValue string + Prompt string + InitialValue string + CancelOnEmptyBackspace bool } // 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/session.go b/session.go index a8154b7..7418549 100644 --- a/session.go +++ b/session.go @@ -15,6 +15,7 @@ type Session struct { Commands *CommandMapping UIManager *ui.Ui modelController *ModelViewCtrl + pasteBoard RWModel LastSearch *regexp.Regexp } @@ -28,6 +29,7 @@ func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session { Commands: NewCommandMapping(), UIManager: uiManager, modelController: NewGridViewModel(model), + pasteBoard: NewSingleCellStdModel(), } frame.SetModel(&SessionGridModel{session.modelController}) diff --git a/ui/stdcomps.go b/ui/stdcomps.go index 7c7090b..5594523 100644 --- a/ui/stdcomps.go +++ b/ui/stdcomps.go @@ -57,6 +57,11 @@ 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) @@ -65,6 +70,10 @@ type TextEntry struct { OnCancel func() } +func (te *TextEntry) Reset() { + te.isDirty = false +} + func (te *TextEntry) Remeasure(w, h int) (int, int) { return w, 1 } @@ -116,16 +125,18 @@ func (te *TextEntry) KeyPressed(key rune, mod int) { te.moveCursorBy(-1) } else if key == KeyArrowRight { te.moveCursorBy(1) - } else if key == KeyHome { + } else if (key == KeyHome) || (key == KeyCtrlA) { te.moveCursorTo(0) - } else if key == KeyEnd { + } else if (key == KeyEnd) || (key == KeyCtrlE) { 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 { - te.cancelAndExit() + if te.CancelOnEmptyBackspace && !te.isDirty { + te.cancelAndExit() + } } else { te.backspace() } @@ -171,6 +182,7 @@ 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 { @@ -181,6 +193,7 @@ 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 { @@ -191,6 +204,7 @@ 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:] } From 185900d626955896544d6fd21f8588282bbc8a1a Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 24 Mar 2021 08:23:23 +1100 Subject: [PATCH 02/10] Replaced termbox with tcell --- go.mod | 5 +++-- go.sum | 21 +++++++++++++++++---- ui/termboxdriver.go | 4 +--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 1aaa82d..5c67045 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/lmika/ted go 1.15 require ( + github.com/gdamore/tcell v1.4.0 github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe - github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 + github.com/mattn/go-runewidth v0.0.10 // indirect + github.com/rivo/uniseg v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 87c946c..101b134 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,19 @@ +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/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= +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/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +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= +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= diff --git a/ui/termboxdriver.go b/ui/termboxdriver.go index a7a9d26..e8746af 100644 --- a/ui/termboxdriver.go +++ b/ui/termboxdriver.go @@ -3,7 +3,7 @@ package ui import ( - "github.com/nsf/termbox-go" + "github.com/gdamore/tcell/termbox" ) type TermboxDriver struct { @@ -101,7 +101,6 @@ var termboxKeysToSpecialKeys = map[termbox.Key]rune{ termbox.KeyArrowLeft: KeyArrowLeft, termbox.KeyArrowRight: KeyArrowRight, - termbox.KeyCtrlSpace: KeyCtrlSpace, termbox.KeyCtrlA: KeyCtrlA, termbox.KeyCtrlB: KeyCtrlB, termbox.KeyCtrlC: KeyCtrlC, @@ -133,5 +132,4 @@ var termboxKeysToSpecialKeys = map[termbox.Key]rune{ termbox.KeyCtrl5: KeyCtrl5, termbox.KeyCtrl6: KeyCtrl6, termbox.KeyCtrl7: KeyCtrl7, - termbox.KeyCtrl8: KeyCtrl8, } From 82face0010842482b96ff15db003b9f4c07b2b95 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 24 Mar 2021 08:28:40 +1100 Subject: [PATCH 03/10] Fixed some bugs with the backspace key mappings --- ui/driver.go | 8 +++--- ui/termboxdriver.go | 64 ++++++++++++++++++++++----------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/ui/driver.go b/ui/driver.go index 9b642f7..bef7321 100644 --- a/ui/driver.go +++ b/ui/driver.go @@ -84,10 +84,10 @@ const ( KeyArrowLeft KeyArrowRight - KeyBackspace = KeyCtrlH - KeyBackspace2 = KeyCtrl8 - KeyEnter = KeyCtrlM - KeyEsc = KeyCtrl3 + KeyBackspace = KeyCtrlH + KeyBackspace2 + KeyEnter = KeyCtrlM + KeyEsc = KeyCtrl3 ) // The type of events supported by the driver diff --git a/ui/termboxdriver.go b/ui/termboxdriver.go index e8746af..6f724a5 100644 --- a/ui/termboxdriver.go +++ b/ui/termboxdriver.go @@ -100,36 +100,36 @@ var termboxKeysToSpecialKeys = map[termbox.Key]rune{ termbox.KeyArrowDown: KeyArrowDown, termbox.KeyArrowLeft: KeyArrowLeft, termbox.KeyArrowRight: KeyArrowRight, - - 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.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, } From 975955236fa700dda7823148c9aa80bd1b5d871c Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Fri, 3 Jun 2022 10:29:06 +1000 Subject: [PATCH 04/10] Added support for TSV files This uses a similar parser as CSV files, except configured for tabs. Have also fixed the configured parser to ignore column counts. --- README.md | 8 ++++++-- main.go | 22 ++++++++++++++++++++-- modelsource.go | 38 ++++++++++++++++++++++++++------------ 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b527ab2..6c1151a 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,14 @@ go get github.com/lmika/ted ## Usage ``` -ted +ted [FLAGS] FILE ``` -Can either be a new CSV file, or an existing CSV file. +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. 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. diff --git a/main.go b/main.go index 6aca219..5d7acf0 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,14 @@ 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") @@ -20,8 +21,14 @@ 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, CsvFileModelSource{flag.Arg(0)}) + session := NewSession(uiManager, frame, codecBuilder(flag.Arg(0))) session.LoadFromSource() uiManager.SetRootComponent(frame.RootComponent()) @@ -29,3 +36,14 @@ func main() { 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'}) + }, +} diff --git a/modelsource.go b/modelsource.go index 9b5c4fe..cde8efa 100644 --- a/modelsource.go +++ b/modelsource.go @@ -1,10 +1,10 @@ package main import ( - "path/filepath" - "os" "encoding/csv" "io" + "os" + "path/filepath" ) // ModelSource is a source of models. At a minimum, it must be able to read models. @@ -26,22 +26,34 @@ type WritableModelSource interface { // A model source backed by a CSV file 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 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 } @@ -49,12 +61,13 @@ 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 - } - if err != nil { + } else if err != nil { return nil, err } @@ -66,19 +79,20 @@ 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() @@ -93,4 +107,4 @@ func (s CsvFileModelSource) Write(m Model) error { } return f.Close() -} \ No newline at end of file +} From 5424a6b92738f4973cd480106b3df44bc9103c2a Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Fri, 1 Jul 2022 16:15:54 +1000 Subject: [PATCH 05/10] Allowed open-right to work on all columns Previously it only worked on the right-most column. Also bounded this command to "O" (this might change). --- commandmap.go | 9 ++--- go.mod | 1 + go.sum | 15 ++++++++ stdmodel.go | 12 +++++- viewmodel.go | 37 +++++++++++++++++- viewmodel_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 viewmodel_test.go diff --git a/commandmap.go b/commandmap.go index 157bfb0..4ad8582 100644 --- a/commandmap.go +++ b/commandmap.go @@ -187,11 +187,7 @@ func (cm *CommandMapping) RegisterViewCommands() { grid := ctx.Frame().Grid() cellX, _ := grid.CellPosition() - height, width := ctx.ModelVC().Model().Dimensions() - if cellX == width-1 { - return ctx.ModelVC().Resize(height, width+1) - } - return nil + return ctx.ModelVC().OpenRight(cellX) }) cm.Define("open-down", "Inserts a row below the curser", "", func(ctx *CommandContext) error { @@ -277,7 +273,7 @@ func (cm *CommandMapping) RegisterViewCommands() { cm.Define("enter-command", "Enter command", "", func(ctx *CommandContext) error { ctx.Frame().Prompt(PromptOptions{ - Prompt: ":", + Prompt: ":", CancelOnEmptyBackspace: true, }, func(res string) error { return cm.Eval(ctx, res) @@ -407,6 +403,7 @@ 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")) diff --git a/go.mod b/go.mod index 5c67045..6bb538b 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,5 @@ require ( github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe github.com/mattn/go-runewidth v0.0.10 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/stretchr/testify v1.7.5 // indirect ) diff --git a/go.sum b/go.sum index 101b134..b1e8d4a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +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= @@ -9,11 +12,23 @@ github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i 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 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= 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 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +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/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= diff --git a/stdmodel.go b/stdmodel.go index 306e224..975279d 100644 --- a/stdmodel.go +++ b/stdmodel.go @@ -14,12 +14,20 @@ 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). */ @@ -73,7 +81,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/viewmodel.go b/viewmodel.go index 1d0b3cd..720552f 100644 --- a/viewmodel.go +++ b/viewmodel.go @@ -44,7 +44,9 @@ func (gvm *ModelViewCtrl) SetRowAttrs(row int, newAttrs SliceAttr) { } func (gvm *ModelViewCtrl) SetColAttrs(col int, newAttrs SliceAttr) { - gvm.colAttrs[col] = newAttrs + if col >= 0 && col < len(gvm.colAttrs) { + gvm.colAttrs[col] = newAttrs + } } func (gvm *ModelViewCtrl) SetCellValue(r, c int, newValue string) error { @@ -69,6 +71,39 @@ 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)) + } + } + } + + 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 new file mode 100644 index 0000000..56ff8e6 --- /dev/null +++ b/viewmodel_test.go @@ -0,0 +1,98 @@ +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) + } + } +} From 3ee6cbf5147a663a8ad818f204da6c1be81a063c Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 5 Oct 2022 11:49:37 +1100 Subject: [PATCH 06/10] Added 'each-row' and 'to-upper' commands --- README.md | 21 ++++++++------- commandmap.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++--- main.go | 3 +++ modelsource.go | 55 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 13 deletions(-) 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 +} From 9267fc92a7bb0b163fd1289bb0c3373f90c6c3d9 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 24 Sep 2024 12:45:22 +1000 Subject: [PATCH 07/10] A few minor feature Added automatic resizing of the columns when loading a model Added colouring of the currently search for cell --- commandmap.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++- go.mod | 18 +++++++++++++---- go.sum | 3 +-- main.go | 1 + session.go | 38 ++++++++++++++++++++++++++++++++++-- viewmodel.go | 12 +++++++++--- 6 files changed, 114 insertions(+), 12 deletions(-) diff --git a/commandmap.go b/commandmap.go index 18355c7..6da306f 100644 --- a/commandmap.go +++ b/commandmap.go @@ -126,12 +126,17 @@ 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.LastSearch = re + ctx.Session().SetLastSearch(re) return ctx.Session().Commands.Eval(ctx, "search-next") }) return nil @@ -252,6 +257,28 @@ 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) @@ -495,3 +522,28 @@ 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/go.mod b/go.mod index 6bb538b..d31faa9 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,21 @@ module github.com/lmika/ted -go 1.15 +go 1.22 require ( github.com/gdamore/tcell v1.4.0 github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe - github.com/mattn/go-runewidth v0.0.10 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/stretchr/testify v1.7.5 // indirect + 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 ) diff --git a/go.sum b/go.sum index b1e8d4a..a863b4b 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,10 @@ github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRR 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 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= 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 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= 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= @@ -28,6 +26,7 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeo 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= diff --git a/main.go b/main.go index 7459418..36cead3 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ func main() { frame := NewFrame(uiManager) session := NewSession(uiManager, frame, codecBuilder(flag.Arg(0))) session.LoadFromSource() + session.ResizeToBestFit() uiManager.SetRootComponent(frame.RootComponent()) frame.enterMode(GridMode) diff --git a/session.go b/session.go index 7418549..4776971 100644 --- a/session.go +++ b/session.go @@ -6,6 +6,12 @@ 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 { @@ -15,7 +21,7 @@ type Session struct { Commands *CommandMapping UIManager *ui.Ui modelController *ModelViewCtrl - pasteBoard RWModel + pasteBoard RWModel LastSearch *regexp.Regexp } @@ -29,7 +35,7 @@ func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session { Commands: NewCommandMapping(), UIManager: uiManager, modelController: NewGridViewModel(model), - pasteBoard: NewSingleCellStdModel(), + pasteBoard: NewSingleCellStdModel(), } frame.SetModel(&SessionGridModel{session.modelController}) @@ -55,6 +61,23 @@ 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 @@ -71,6 +94,11 @@ 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 @@ -132,6 +160,12 @@ 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/viewmodel.go b/viewmodel.go index 720552f..ba06ff5 100644 --- a/viewmodel.go +++ b/viewmodel.go @@ -2,12 +2,14 @@ package main import ( "errors" + "regexp" ) type ModelViewCtrl struct { - model Model - rowAttrs []SliceAttr - colAttrs []SliceAttr + model Model + rowAttrs []SliceAttr + colAttrs []SliceAttr + lastSearchRegexp *regexp.Regexp } func NewGridViewModel(model Model) *ModelViewCtrl { @@ -16,6 +18,10 @@ 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 } From a1df99860e3c90ee5a3303aee9e6bc40ab3b3290 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 24 Sep 2024 13:07:30 +1000 Subject: [PATCH 08/10] Added Ctrl+C and search previous command --- commandmap.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/commandmap.go b/commandmap.go index 6da306f..ddbd9fe 100644 --- a/commandmap.go +++ b/commandmap.go @@ -164,6 +164,32 @@ 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 cellY < 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") @@ -482,6 +508,7 @@ 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")) @@ -498,6 +525,7 @@ func (cm *CommandMapping) RegisterViewKeyBindings() { 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")) From f271865808b5ae09ed42dd1e24080c6795a4832e Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 8 Oct 2024 14:11:27 +1100 Subject: [PATCH 09/10] Updated readme --- README.md | 20 +++++++++++--------- commandmap.go | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 334d932..b89d339 100644 --- a/README.md +++ b/README.md @@ -45,19 +45,21 @@ 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 | -| `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 | +| `N` | Find previous cell matching search | +| `y` | Copy cell value | +| `p` | Paste cell value | +| `:` | Enter command | ## Commands diff --git a/commandmap.go b/commandmap.go index ddbd9fe..09f237c 100644 --- a/commandmap.go +++ b/commandmap.go @@ -175,7 +175,7 @@ func (cm *CommandMapping) RegisterViewCommands() { for { cellX-- - if cellY < 0 { + if cellX < 0 { cellX = width - 1 cellY-- if cellY < 0 { From 1f3c11f26524fb77e7b0eb702093b0412bc37be4 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 9 Oct 2024 07:47:34 +1100 Subject: [PATCH 10/10] Fixed bug which made new columns non-resizable --- viewmodel.go | 1 + 1 file changed, 1 insertion(+) diff --git a/viewmodel.go b/viewmodel.go index ba06ff5..68aa95b 100644 --- a/viewmodel.go +++ b/viewmodel.go @@ -106,6 +106,7 @@ func (gvm *ModelViewCtrl) insertColumn(col int) error { } } } + gvm.modelWasResized() return nil }