summaryrefslogtreecommitdiffstats
path: root/runtime
diff options
context:
space:
mode:
Diffstat (limited to 'runtime')
-rw-r--r--runtime/ci/evaluator.go (renamed from runtime/ci_evaluator.go)11
-rw-r--r--runtime/ci/evaluator_test.go (renamed from runtime/ci_evaluator_test.go)6
-rw-r--r--runtime/ci/reference_file.go (renamed from runtime/reference_file.go)2
-rw-r--r--runtime/ci/rule.go (renamed from runtime/ci_rule.go)6
-rw-r--r--runtime/export/export.go (renamed from runtime/export.go)27
-rw-r--r--runtime/export/export_test.go (renamed from runtime/export_test.go)11
-rw-r--r--runtime/run.go32
-rw-r--r--runtime/ui/details_controller.go149
-rw-r--r--runtime/ui/filetree_controller.go403
-rw-r--r--runtime/ui/filetree_viewmodel.go425
-rw-r--r--runtime/ui/filetree_viewmodel_test.go385
-rw-r--r--runtime/ui/filter_controller.go113
-rw-r--r--runtime/ui/layer_controller.go313
-rw-r--r--runtime/ui/status_controller.go77
-rw-r--r--runtime/ui/testdata/TestFileShowAggregateChanges.txt36
-rw-r--r--runtime/ui/testdata/TestFileTreeDirCollapse.txt13
-rw-r--r--runtime/ui/testdata/TestFileTreeDirCollapseAll.txt9
-rw-r--r--runtime/ui/testdata/TestFileTreeDirCursorRight.txt22
-rw-r--r--runtime/ui/testdata/TestFileTreeFilterTree.txt7
-rw-r--r--runtime/ui/testdata/TestFileTreeGoCase.txt416
-rw-r--r--runtime/ui/testdata/TestFileTreeHideAddedRemovedModified.txt21
-rw-r--r--runtime/ui/testdata/TestFileTreeHideTypeWithFilter.txt1
-rw-r--r--runtime/ui/testdata/TestFileTreeHideUnmodified.txt10
-rw-r--r--runtime/ui/testdata/TestFileTreeNoAttributes.txt416
-rw-r--r--runtime/ui/testdata/TestFileTreePageDown.txt11
-rw-r--r--runtime/ui/testdata/TestFileTreePageUp.txt11
-rw-r--r--runtime/ui/testdata/TestFileTreeRestrictedHeight.txt22
-rw-r--r--runtime/ui/testdata/TestFileTreeSelectLayer.txt23
-rw-r--r--runtime/ui/ui.go395
29 files changed, 3327 insertions, 46 deletions
diff --git a/runtime/ci_evaluator.go b/runtime/ci/evaluator.go
index d1b159d..2c3cb93 100644
--- a/runtime/ci_evaluator.go
+++ b/runtime/ci/evaluator.go
@@ -1,8 +1,10 @@
-package runtime
+package ci
import (
"fmt"
"github.com/dustin/go-humanize"
+ "github.com/wagoodman/dive/dive/image"
+ "github.com/wagoodman/dive/utils"
"sort"
"strconv"
"strings"
@@ -10,7 +12,6 @@ import (
"github.com/spf13/viper"
"github.com/logrusorgru/aurora"
- "github.com/wagoodman/dive/image"
)
type CiEvaluator struct {
@@ -133,7 +134,7 @@ func (ci *CiEvaluator) Evaluate(analysis *image.AnalysisResult) bool {
}
func (ci *CiEvaluator) Report() {
- fmt.Println(title("Inefficient Files:"))
+ fmt.Println(utils.TitleFormat("Inefficient Files:"))
template := "%5s %12s %-s\n"
fmt.Printf(template, "Count", "Wasted Space", "File Path")
@@ -142,11 +143,11 @@ func (ci *CiEvaluator) Report() {
fmt.Println("None")
} else {
for _, file := range ci.InefficientFiles {
- fmt.Printf(template, strconv.Itoa(file.References), humanize.Bytes(uint64(file.SizeBytes)), file.Path)
+ fmt.Printf(template, strconv.Itoa(file.References), humanize.Bytes(file.SizeBytes), file.Path)
}
}
- fmt.Println(title("Results:"))
+ fmt.Println(utils.TitleFormat("Results:"))
status := "PASS"
diff --git a/runtime/ci_evaluator_test.go b/runtime/ci/evaluator_test.go
index c7124ad..acd6b9b 100644
--- a/runtime/ci_evaluator_test.go
+++ b/runtime/ci/evaluator_test.go
@@ -1,16 +1,16 @@
-package runtime
+package ci
import (
+ "github.com/wagoodman/dive/dive/image/docker"
"strings"
"testing"
"github.com/spf13/viper"
- "github.com/wagoodman/dive/image"
)
func Test_Evaluator(t *testing.T) {
- result, err := image.TestLoadDockerImageTar("../.data/test-docker-image.tar")
+ result, err := docker.TestLoadDockerImageTar("../../.data/test-docker-image.tar")
if err != nil {
t.Fatalf("Test_Export: unable to fetch analysis: %v", err)
}
diff --git a/runtime/reference_file.go b/runtime/ci/reference_file.go
index bef54eb..c5891c7 100644
--- a/runtime/reference_file.go
+++ b/runtime/ci/reference_file.go
@@ -1,4 +1,4 @@
-package runtime
+package ci
type ReferenceFile struct {
References int `json:"count"`
diff --git a/runtime/ci_rule.go b/runtime/ci/rule.go
index d14295a..60b350d 100644
--- a/runtime/ci_rule.go
+++ b/runtime/ci/rule.go
@@ -1,14 +1,14 @@
-package runtime
+package ci
import (
"fmt"
+ "github.com/wagoodman/dive/dive/image"
"strconv"
"github.com/spf13/viper"
"github.com/dustin/go-humanize"
"github.com/logrusorgru/aurora"
- "github.com/wagoodman/dive/image"
)
const (
@@ -25,7 +25,7 @@ type CiRule interface {
Key() string
Configuration() string
Validate() error
- Evaluate(*image.AnalysisResult) (RuleStatus, string)
+ Evaluate(result *image.AnalysisResult) (RuleStatus, string)
}
type GenericCiRule struct {
diff --git a/runtime/export.go b/runtime/export/export.go
index e769015..58aa095 100644
--- a/runtime/export.go
+++ b/runtime/export/export.go
@@ -1,10 +1,9 @@
-package runtime
+package export
import (
"encoding/json"
+ "github.com/wagoodman/dive/dive/image"
"io/ioutil"
-
- "github.com/wagoodman/dive/image"
)
type export struct {
@@ -20,16 +19,22 @@ type exportLayer struct {
}
type exportImage struct {
- SizeBytes uint64 `json:"sizeBytes"`
- InefficientBytes uint64 `json:"inefficientBytes"`
- EfficiencyScore float64 `json:"efficiencyScore"`
- InefficientFiles []ReferenceFile `json:"ReferenceFile"`
+ SizeBytes uint64 `json:"sizeBytes"`
+ InefficientBytes uint64 `json:"inefficientBytes"`
+ EfficiencyScore float64 `json:"efficiencyScore"`
+ InefficientFiles []exportReferenceFile `json:"exportReferenceFile"`
+}
+
+type exportReferenceFile struct {
+ References int `json:"count"`
+ SizeBytes uint64 `json:"sizeBytes"`
+ Path string `json:"file"`
}
-func newExport(analysis *image.AnalysisResult) *export {
+func NewExport(analysis *image.AnalysisResult) *export {
data := export{}
data.Layer = make([]exportLayer, len(analysis.Layers))
- data.Image.InefficientFiles = make([]ReferenceFile, len(analysis.Inefficiencies))
+ data.Image.InefficientFiles = make([]exportReferenceFile, len(analysis.Inefficiencies))
// export layers in order
for revIdx := len(analysis.Layers) - 1; revIdx >= 0; revIdx-- {
@@ -51,7 +56,7 @@ func newExport(analysis *image.AnalysisResult) *export {
for idx := 0; idx < len(analysis.Inefficiencies); idx++ {
fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx]
- data.Image.InefficientFiles[idx] = ReferenceFile{
+ data.Image.InefficientFiles[idx] = exportReferenceFile{
References: len(fileData.Nodes),
SizeBytes: uint64(fileData.CumulativeSize),
Path: fileData.Path,
@@ -65,7 +70,7 @@ func (exp *export) marshal() ([]byte, error) {
return json.MarshalIndent(&exp, "", " ")
}
-func (exp *export) toFile(exportFilePath string) error {
+func (exp *export) ToFile(exportFilePath string) error {
payload, err := exp.marshal()
if err != nil {
return err
diff --git a/runtime/export_test.go b/runtime/export/export_test.go
index 709225c..e3750ed 100644
--- a/runtime/export_test.go
+++ b/runtime/export/export_test.go
@@ -1,18 +1,17 @@
-package runtime
+package export
import (
+ "github.com/wagoodman/dive/dive/image/docker"
"testing"
-
- "github.com/wagoodman/dive/image"
)
func Test_Export(t *testing.T) {
- result, err := image.TestLoadDockerImageTar("../.data/test-docker-image.tar")
+ result, err := docker.TestLoadDockerImageTar("../../.data/test-docker-image.tar")
if err != nil {
t.Fatalf("Test_Export: unable to fetch analysis: %v", err)
}
- export := newExport(result)
+ export := NewExport(result)
payload, err := export.marshal()
if err != nil {
t.Errorf("Test_Export: unable to export analysis: %v", err)
@@ -109,7 +108,7 @@ func Test_Export(t *testing.T) {
"sizeBytes": 1220598,
"inefficientBytes": 32025,
"efficiencyScore": 0.9844212134184309,
- "ReferenceFile": [
+ "exportReferenceFile": [
{
"count": 2,
"sizeBytes": 12810,
diff --git a/runtime/run.go b/runtime/run.go
index a427bfa..99c6b7e 100644
--- a/runtime/run.go
+++ b/runtime/run.go
@@ -2,28 +2,26 @@ package runtime
import (
"fmt"
+ "github.com/wagoodman/dive/dive"
+ "github.com/wagoodman/dive/runtime/ci"
+ "github.com/wagoodman/dive/runtime/export"
"io/ioutil"
"log"
"os"
"github.com/dustin/go-humanize"
- "github.com/logrusorgru/aurora"
- "github.com/wagoodman/dive/filetree"
- "github.com/wagoodman/dive/image"
- "github.com/wagoodman/dive/ui"
+ "github.com/wagoodman/dive/dive/filetree"
+ "github.com/wagoodman/dive/dive/image"
+ "github.com/wagoodman/dive/runtime/ui"
"github.com/wagoodman/dive/utils"
)
-func title(s string) string {
- return aurora.Bold(s).String()
-}
-
func runCi(analysis *image.AnalysisResult, options Options) {
fmt.Printf(" efficiency: %2.4f %%\n", analysis.Efficiency*100)
fmt.Printf(" wastedBytes: %d bytes (%s)\n", analysis.WastedBytes, humanize.Bytes(analysis.WastedBytes))
fmt.Printf(" userWastedPercent: %2.4f %%\n", analysis.WastedUserPercent*100)
- evaluator := NewCiEvaluator(options.CiConfig)
+ evaluator := ci.NewCiEvaluator(options.CiConfig)
pass := evaluator.Evaluate(analysis)
evaluator.Report()
@@ -63,13 +61,13 @@ func Run(options Options) {
doBuild := len(options.BuildArgs) > 0
if doBuild {
- fmt.Println(title("Building image..."))
+ fmt.Println(utils.TitleFormat("Building image..."))
options.ImageId = runBuild(options.BuildArgs)
}
- analyzer := image.GetAnalyzer(options.ImageId)
+ analyzer := dive.GetAnalyzer(options.ImageId)
- fmt.Println(title("Fetching image...") + " (this can take a while with large images)")
+ fmt.Println(utils.TitleFormat("Fetching image...") + " (this can take a while with large images)")
reader, err := analyzer.Fetch()
if err != nil {
fmt.Printf("cannot fetch image: %v\n", err)
@@ -77,7 +75,7 @@ func Run(options Options) {
}
defer reader.Close()
- fmt.Println(title("Parsing image..."))
+ fmt.Println(utils.TitleFormat("Parsing image..."))
err = analyzer.Parse(reader)
if err != nil {
fmt.Printf("cannot parse image: %v\n", err)
@@ -85,9 +83,9 @@ func Run(options Options) {
}
if doExport {
- fmt.Println(title(fmt.Sprintf("Analyzing image... (export to '%s')", options.ExportFile)))
+ fmt.Println(utils.TitleFormat(fmt.Sprintf("Analyzing image... (export to '%s')", options.ExportFile)))
} else {
- fmt.Println(title("Analyzing image..."))
+ fmt.Println(utils.TitleFormat("Analyzing image..."))
}
result, err := analyzer.Analyze()
@@ -97,7 +95,7 @@ func Run(options Options) {
}
if doExport {
- err = newExport(result).toFile(options.ExportFile)
+ err = export.NewExport(result).ToFile(options.ExportFile)
if err != nil {
fmt.Printf("cannot write export file: %v\n", err)
utils.Exit(1)
@@ -111,7 +109,7 @@ func Run(options Options) {
utils.Exit(0)
}
- fmt.Println(title("Building cache..."))
+ fmt.Println(utils.TitleFormat("Building cache..."))
cache := filetree.NewFileTreeCache(result.RefTrees)
cache.Build()
diff --git a/runtime/ui/details_controller.go b/runtime/ui/details_controller.go
new file mode 100644
index 0000000..ab9d7ec
--- /dev/null
+++ b/runtime/ui/details_controller.go
@@ -0,0 +1,149 @@
+package ui
+
+import (
+ "fmt"
+ "github.com/wagoodman/dive/dive/filetree"
+ "strconv"
+ "strings"
+
+ "github.com/dustin/go-humanize"
+ "github.com/jroimartin/gocui"
+ "github.com/lunixbochs/vtclean"
+)
+
+// DetailsController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
+// shows the layer details and image statistics.
+type DetailsController struct {
+ Name string
+ gui *gocui.Gui
+ view *gocui.View
+ header *gocui.View
+ efficiency float64
+ inefficiencies filetree.EfficiencySlice
+}
+
+// NewDetailsController creates a new view object attached the the global [gocui] screen object.
+func NewDetailsController(name string, gui *gocui.Gui, efficiency float64, inefficiencies filetree.EfficiencySlice) (controller *DetailsController) {
+ controller = new(DetailsController)
+
+ // populate main fields
+ controller.Name = name
+ controller.gui = gui
+ controller.efficiency = efficiency
+ controller.inefficiencies = inefficiencies
+
+ return controller
+}
+
+// Setup initializes the UI concerns within the context of a global [gocui] view object.
+func (controller *DetailsController) Setup(v *gocui.View, header *gocui.View) error {
+
+ // set controller options
+ controller.view = v
+ controller.view.Editable = false
+ controller.view.Wrap = true
+ controller.view.Highlight = false
+ controller.view.Frame = false
+
+ controller.header = header
+ controller.header.Editable = false
+ controller.header.Wrap = false
+ controller.header.Frame = false
+
+ // set keybindings
+ if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
+ return err
+ }
+ if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
+ return err
+ }
+
+ return controller.Render()
+}
+
+// IsVisible indicates if the details view pane is currently initialized.
+func (controller *DetailsController) IsVisible() bool {
+ return controller != nil
+}
+
+// CursorDown moves the cursor down in the details pane (currently indicates nothing).
+func (controller *DetailsController) CursorDown() error {
+ return CursorDown(controller.gui, controller.view)
+}
+
+// CursorUp moves the cursor up in the details pane (currently indicates nothing).
+func (controller *DetailsController) CursorUp() error {
+ return CursorUp(controller.gui, controller.view)
+}
+
+// Update refreshes the state objects for future rendering.
+func (controller *DetailsController) Update() error {
+ return nil
+}
+
+// Render flushes the state objects to the screen. The details pane reports:
+// 1. the current selected layer's command string
+// 2. the image efficiency score
+// 3. the estimated wasted image space
+// 4. a list of inefficient file allocations
+func (controller *DetailsController) Render() error {
+ currentLayer := Controllers.Layer.currentLayer()
+
+ var wastedSpace int64
+
+ template := "%5s %12s %-s\n"
+ inefficiencyReport := fmt.Sprintf(Formatting.Header(template), "Count", "Total Space", "Path")
+
+ height := 100
+ if controller.view != nil {
+ _, height = controller.view.Size()
+ }
+
+ for idx := 0; idx < len(controller.inefficiencies); idx++ {
+ data := controller.inefficiencies[len(controller.inefficiencies)-1-idx]
+ wastedSpace += data.CumulativeSize
+
+ // todo: make this report scrollable
+ if idx < height {
+ inefficiencyReport += fmt.Sprintf(template, strconv.Itoa(len(data.Nodes)), humanize.Bytes(uint64(data.CumulativeSize)), data.Path)
+ }
+ }
+
+ imageSizeStr := fmt.Sprintf("%s %s", Formatting.Header("Total Image size:"), humanize.Bytes(Controllers.Layer.ImageSize))
+ effStr := fmt.Sprintf("%s %d %%", Formatting.Header("Image efficiency score:"), int(100.0*controller.efficiency))
+ wastedSpaceStr := fmt.Sprintf("%s %s", Formatting.Header("Potential wasted space:"), humanize.Bytes(uint64(wastedSpace)))
+
+ controller.gui.Update(func(g *gocui.Gui) error {
+ // update header
+ controller.header.Clear()
+ width, _ := controller.view.Size()
+
+ layerHeaderStr := fmt.Sprintf("[Layer Details]%s", strings.Repeat("─", width-15))
+ imageHeaderStr := fmt.Sprintf("[Image Details]%s", strings.Repeat("─", width-15))
+
+ _, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(layerHeaderStr, false)))
+
+ // update contents
+ controller.view.Clear()
+ _, _ = fmt.Fprintln(controller.view, Formatting.Header("Digest: ")+currentLayer.Id())
+ // TODO: add back in with controller model
+ // fmt.Fprintln(view.view, Formatting.Header("Tar ID: ")+currentLayer.TarId())
+ _, _ = fmt.Fprintln(controller.view, Formatting.Header("Command:"))
+ _, _ = fmt.Fprintln(controller.view, currentLayer.Command())
+
+ _, _ = fmt.Fprintln(controller.view, "\n"+Formatting.Header(vtclean.Clean(imageHeaderStr, false)))
+
+ _, _ = fmt.Fprintln(controller.view, imageSizeStr)
+ _, _ = fmt.Fprintln(controller.view, wastedSpaceStr)
+ _, _ = fmt.Fprintln(controller.view, effStr+"\n")
+
+ _, _ = fmt.Fprintln(controller.view, inefficiencyReport)
+ return nil
+ })
+ return nil
+}
+
+// KeyHelp indicates all the possible actions a user can take while the current pane is selected (currently does nothing).
+func (controller *DetailsController) KeyHelp() string {
+ return "TBD"
+}
diff --git a/runtime/ui/filetree_controller.go b/runtime/ui/filetree_controller.go
new file mode 100644
index 0000000..90bcba6
--- /dev/null
+++ b/runtime/ui/filetree_controller.go
@@ -0,0 +1,403 @@
+package ui
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/lunixbochs/vtclean"
+ "github.com/sirupsen/logrus"
+ "github.com/spf13/viper"
+ "github.com/wagoodman/keybinding"
+
+ "github.com/jroimartin/gocui"
+ "github.com/wagoodman/dive/dive/filetree"
+)
+
+const (
+ CompareLayer CompareType = iota
+ CompareAll
+)
+
+type CompareType int
+
+// FileTreeController holds the UI objects and data models for populating the right pane. Specifically the pane that
+// shows selected layer or aggregate file ASCII tree.
+type FileTreeController struct {
+ Name string
+ gui *gocui.Gui
+ view *gocui.View
+ header *gocui.View
+ vm *FileTreeViewModel
+
+ keybindingToggleCollapse []keybinding.Key
+ keybindingToggleCollapseAll []keybinding.Key
+ keybindingToggleAttributes []keybinding.Key
+ keybindingToggleAdded []keybinding.Key
+ keybindingToggleRemoved []keybinding.Key
+ keybindingToggleModified []keybinding.Key
+ keybindingToggleUnmodified []keybinding.Key
+ keybindingPageDown []keybinding.Key
+ keybindingPageUp []keybinding.Key
+}
+
+// NewFileTreeController creates a new view object attached the the global [gocui] screen object.
+func NewFileTreeController(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree, cache filetree.TreeCache) (controller *FileTreeController) {
+ controller = new(FileTreeController)
+
+ // populate main fields
+ controller.Name = name
+ controller.gui = gui
+ controller.vm = NewFileTreeViewModel(tree, refTrees, cache)
+
+ var err error
+ controller.keybindingToggleCollapse, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-dir"))
+ if err != nil {
+ logrus.Error(err)
+ }
+
+ controller.keybindingToggleCollapseAll, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-collapse-all-dir"))
+ if err != nil {
+ logrus.Error(err)
+ }
+
+ controller.keybindingToggleAttributes, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-filetree-attributes"))
+ if err != nil {
+ logrus.Error(err)
+ }
+
+ controller.keybindingToggleAdded, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-added-files"))
+ if err != nil {
+ logrus.Error(err)
+ }
+
+ controller.keybindingToggleRemoved, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-removed-files"))
+ if err != nil {
+ logrus.Error(err)
+ }
+
+ controller.keybindingToggleModified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-modified-files"))
+ if err != nil {
+ logrus.Error(err)
+ }
+
+ // support legacy behavior first, then use default behavior
+ controller.keybindingToggleUnmodified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unchanged-files"))
+ if err != nil {
+ controller.keybindingToggleUnmodified, err = keybinding.ParseAll(viper.GetString("keybinding.toggle-unmodified-files"))
+ if err != nil {
+ logrus.Error(err)
+ }
+ }
+
+ controller.keybindingPageUp, err = keybinding.ParseAll(viper.GetString("keybinding.page-up"))
+ if err != nil {
+ logrus.Error(err)
+ }
+
+ controller.keybindingPageDown, err = keybinding.ParseAll(viper.GetString("keybinding.page-down"))
+ if err != nil {
+ logrus.Error(err)
+ }
+
+ return controller
+}
+
+// Setup initializes the UI concerns within the context of a global [gocui] view object.
+func (controller *FileTreeController) Setup(v *gocui.View, header *gocui.View) error {
+
+ // set controller options
+ controller.view = v
+ controller.view.Editable = false
+ controller.view.Wrap = false
+ controller.view.Frame = false
+
+ controller.header = header
+ controller.header.Editable = false
+ controller.header.Wrap = false
+ controller.header.Frame = false
+
+ // set keybindings
+ if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorDown() }); err != nil {
+ return err
+ }
+ if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorUp() }); err != nil {
+ return err
+ }
+ if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowLeft, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorLeft() }); err != nil {
+ return err
+ }
+ if err := controller.gui.SetKeybinding(controller.Name, gocui.KeyArrowRight, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return controller.CursorRight() }); err != nil {
+ return err
+ }
+
+ for _, key := range controller.keybindingPageUp {
+ if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageUp() }); err != nil {
+ return err
+ }
+ }
+ for _, key := range controller.keybindingPageDown {
+ if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.PageDown() }); err != nil {
+ return err
+ }
+ }
+ for _, key := range controller.keybindingToggleCollapse {
+ if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleCollapse() }); err != nil {
+ return err
+ }
+ }
+ for _, key := range controller.keybindingToggleCollapseAll {
+ if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleCollapseAll() }); err != nil {
+ return err
+ }
+ }
+ for _, key := range controller.keybindingToggleAttributes {
+ if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleAttributes() }); err != nil {
+ return err
+ }
+ }
+ for _, key := range controller.keybindingToggleAdded {
+ if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Added) }); err != nil {
+ return err
+ }
+ }
+ for _, key := range controller.keybindingToggleRemoved {
+ if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Removed) }); err != nil {
+ return err
+ }
+ }
+ for _, key := range controller.keybindingToggleModified {
+ if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Modified) }); err != nil {
+ return err
+ }
+ }
+ for _, key := range controller.keybindingToggleUnmodified {
+ if err := controller.gui.SetKeybinding(controller.Name, key.Value, key.Modifier, func(*gocui.Gui, *gocui.View) error { return controller.toggleShowDiffType(filetree.Unmodified) }); err != nil {
+ return err
+ }
+ }
+
+ _, height := controller.view.Size()
+ controller.vm.Setup(0, height)
+ _ = controller.Update()
+ _ = controller.Render()
+
+ return nil
+}
+
+// IsVisible indicates if the file tree view pane is currently initialized
+func (controller *FileTreeController) IsVisible() bool {
+ return controller != nil
+}
+
+// resetCursor moves the cursor back to the top of the buffer and translates to the top of the buffer.
+func (controller *FileTreeController) resetCursor() {
+ _ = controller.view.SetCursor(0, 0)
+ controller.vm.resetCursor()
+}
+
+// setTreeByLayer populates the view model by stacking the indicated image layer file trees.
+func (controller *FileTreeController) setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) error {
+ err := controller.vm.setTreeByLayer(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
+ if err != nil {
+ return err
+ }
+ // controller.resetCursor()
+
+ _ = controller.Update()
+ return controller.Render()
+}
+
+// CursorDown moves the cursor down and renders the view.
+// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
+// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
+// this range into the view buffer. This is much faster when tree sizes are large.
+func (controller *FileTreeController) CursorDown() error {
+ if controller.vm.CursorDown() {
+ return controller.Render()
+ }
+ return nil
+}
+
+// CursorUp moves the cursor up and renders the view.
+// Note: we cannot use the gocui buffer since any state change requires writing the entire tree to the buffer.
+// Instead we are keeping an upper and lower bounds of the tree string to render and only flushing
+// this range into the view buffer. This is much faster when tree sizes are large.
+func (controller *FileTreeController) CursorUp() error {
+ if controller.vm.CursorUp() {
+ return controller.Render()
+ }
+ return nil
+}
+
+// CursorLeft moves the cursor up until we reach the Parent Node or top of the tree
+func (controller *FileTreeController) CursorLeft() error {
+ err := controller.vm.CursorLeft(filterRegex())
+ if err != nil {
+ return err
+ }
+ _ = controller.Update()
+ return controller.Render()
+}
+
+// CursorRight descends into directory expanding it if needed
+func (controller *FileTreeController) CursorRight() error {
+ err := controller.vm.CursorRight(filterRegex())
+ if err != nil {
+ return err
+ }
+ _ = controller.Update()
+ return controller.Render()
+}
+
+// PageDown moves to next page putting the cursor on top
+func (controller *FileTreeController) PageDown() error {
+ err := controller.vm.PageDown()
+ if err != nil {
+ return err
+ }
+ return controller.Render()
+}
+
+// PageUp moves to previous page putting the cursor on top
+func (controller *FileTreeController) PageUp() error {
+ err := controller.vm.PageUp()
+ if err != nil {
+ return err
+ }
+ return controller.Render()
+}
+
+// getAbsPositionNode determines the selected screen cursor's location in the file tree, returning the selected FileNode.
+// func (controller *FileTreeController) getAbsPositionNode() (node *filetree.FileNode) {
+// return controller.vm.getAbsPositionNode(filterRegex())
+// }
+
+// toggleCollapse will collapse/expand the selected FileNode.
+func (controller *FileTreeController) toggleCollapse() error {
+ err := controller.vm.toggleCollapse(filterRegex())
+ if err != nil {
+ return err
+ }
+ _ = controller.Update()
+ return controller.Render()
+}
+
+// toggleCollapseAll will collapse/expand the all directories.
+func (controller *FileTreeController) toggleCollapseAll() error {
+ err := controller.vm.toggleCollapseAll()
+ if err != nil {