mirror of
https://github.com/wagoodman/dive
synced 2024-05-30 10:42:12 +02:00
exp with vert layout
This commit is contained in:
parent
6dd7f7af6e
commit
6c2aac7340
|
@ -16,7 +16,7 @@ const debug = false
|
|||
type app struct {
|
||||
gui *gocui.Gui
|
||||
controllers *Controller
|
||||
layout *layoutManager
|
||||
layout *LayoutManager
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -35,11 +35,11 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC
|
|||
return
|
||||
}
|
||||
|
||||
lm := newLayoutManager(theControls)
|
||||
lm := NewLayoutManager(theControls)
|
||||
|
||||
gui.Cursor = false
|
||||
//g.Mouse = true
|
||||
gui.SetManagerFunc(lm.layout)
|
||||
gui.SetManagerFunc(lm.Layout)
|
||||
|
||||
// var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook)
|
||||
//
|
||||
|
@ -77,7 +77,7 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC
|
|||
return
|
||||
}
|
||||
|
||||
theControls.Status.AddHelpKeys(globalHelpKeys...)
|
||||
theControls.Help.AddHelpKeys(globalHelpKeys...)
|
||||
|
||||
// perform the first update and render now that all resources have been loaded
|
||||
err = theControls.UpdateAndRender()
|
||||
|
@ -106,7 +106,6 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC
|
|||
// }
|
||||
// }
|
||||
|
||||
var lastX, lastY int
|
||||
|
||||
// quit is the gocui callback invoked when the user hits Ctrl+C
|
||||
func (a *app) quit() error {
|
||||
|
|
|
@ -14,10 +14,11 @@ type Controller struct {
|
|||
gui *gocui.Gui
|
||||
Tree *view.FileTree
|
||||
Layer *view.Layer
|
||||
Status *view.Status
|
||||
Help *view.Help
|
||||
Filter *view.Filter
|
||||
Details *view.Details
|
||||
lookup map[string]view.Renderer
|
||||
|
||||
lookup map[string]view.View
|
||||
}
|
||||
|
||||
func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*Controller, error) {
|
||||
|
@ -26,7 +27,7 @@ func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.
|
|||
controller := &Controller{
|
||||
gui: g,
|
||||
}
|
||||
controller.lookup = make(map[string]view.Renderer)
|
||||
controller.lookup = make(map[string]view.View)
|
||||
|
||||
controller.Layer, err = view.NewLayerView("layers", g, analysis.Layers)
|
||||
if err != nil {
|
||||
|
@ -47,10 +48,10 @@ func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.
|
|||
// layer view cursor down event should trigger an update in the file tree
|
||||
controller.Layer.AddLayerChangeListener(controller.onLayerChange)
|
||||
|
||||
controller.Status = view.NewStatusView("status", g)
|
||||
controller.lookup[controller.Status.Name()] = controller.Status
|
||||
controller.Help = view.NewHelpView("status", g)
|
||||
controller.lookup[controller.Help.Name()] = controller.Help
|
||||
// set the layer view as the first selected view
|
||||
controller.Status.SetCurrentView(controller.Layer)
|
||||
controller.Help.SetCurrentView(controller.Layer)
|
||||
|
||||
// update the status pane when a filetree option is changed by the user
|
||||
controller.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange)
|
||||
|
@ -79,11 +80,11 @@ func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.
|
|||
}
|
||||
|
||||
func (c *Controller) onFileTreeViewOptionChange() error {
|
||||
err := c.Status.Update()
|
||||
err := c.Help.Update()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status.Render()
|
||||
return c.Help.Render()
|
||||
}
|
||||
|
||||
func (c *Controller) onFilterEdit(filter string) error {
|
||||
|
@ -173,10 +174,10 @@ func (c *Controller) ToggleView() (err error) {
|
|||
v := c.gui.CurrentView()
|
||||
if v == nil || v.Name() == c.Layer.Name() {
|
||||
_, err = c.gui.SetCurrentView(c.Tree.Name())
|
||||
c.Status.SetCurrentView(c.Tree)
|
||||
c.Help.SetCurrentView(c.Tree)
|
||||
} else {
|
||||
_, err = c.gui.SetCurrentView(c.Layer.Name())
|
||||
c.Status.SetCurrentView(c.Layer)
|
||||
c.Help.SetCurrentView(c.Layer)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
58
runtime/ui/layout/vertical.go
Normal file
58
runtime/ui/layout/vertical.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package layout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wagoodman/dive/runtime/ui/view"
|
||||
)
|
||||
|
||||
type Vertical struct {
|
||||
visible bool
|
||||
width int
|
||||
elements []View
|
||||
}
|
||||
|
||||
// how does overrun work? which view gets precidence? how does max possible height work?
|
||||
|
||||
func NewVerticalLayout() *Vertical {
|
||||
return &Vertical{
|
||||
visible: true,
|
||||
width: view.WidthFull,
|
||||
elements: make([]View, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (v Vertical) SetWidth(w int) {
|
||||
v.width = w
|
||||
}
|
||||
|
||||
func (v *Vertical) AddView(sub View) error {
|
||||
for _, element := range v.elements {
|
||||
if element.Name() == sub.Name() {
|
||||
return fmt.Errorf("view already added")
|
||||
}
|
||||
}
|
||||
v.elements = append(v.elements, sub)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Vertical) Name() string {
|
||||
return view.IdentityNone
|
||||
}
|
||||
|
||||
func (v *Vertical) IsVisible() bool {
|
||||
return v.visible
|
||||
}
|
||||
|
||||
func (v *Vertical) Height() (height int) {
|
||||
for _, element := range v.elements {
|
||||
height += element.Height()
|
||||
if height == view.HeightFull {
|
||||
return view.HeightFull
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (v *Vertical) Width() int {
|
||||
return v.width
|
||||
}
|
10
runtime/ui/layout/view.go
Normal file
10
runtime/ui/layout/view.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package layout
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/dive/runtime/ui/view"
|
||||
)
|
||||
|
||||
type View interface {
|
||||
view.Identifiable
|
||||
view.Dimensional
|
||||
}
|
|
@ -6,13 +6,15 @@ import (
|
|||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type layoutManager struct {
|
||||
var lastY, lastX int
|
||||
|
||||
type LayoutManager struct {
|
||||
fileTreeSplitRatio float64
|
||||
controllers *Controller
|
||||
controller *Controller
|
||||
}
|
||||
|
||||
// todo: this needs a major refactor (derive layout from view obj info, which should not live here)
|
||||
func newLayoutManager(c *Controller) *layoutManager {
|
||||
func NewLayoutManager(c *Controller) *LayoutManager {
|
||||
|
||||
fileTreeSplitRatio := viper.GetFloat64("filetree.pane-width")
|
||||
if fileTreeSplitRatio >= 1 || fileTreeSplitRatio <= 0 {
|
||||
|
@ -20,14 +22,14 @@ func newLayoutManager(c *Controller) *layoutManager {
|
|||
fileTreeSplitRatio = 0.5
|
||||
}
|
||||
|
||||
return &layoutManager{
|
||||
return &LayoutManager{
|
||||
fileTreeSplitRatio: fileTreeSplitRatio,
|
||||
controllers: c,
|
||||
controller: c,
|
||||
}
|
||||
}
|
||||
|
||||
// IsNewView determines if a view has already been created based on the set of errors given (a bit hokie)
|
||||
func IsNewView(errs ...error) bool {
|
||||
// isNewView determines if a view has already been created based on the set of errors given (a bit hokie)
|
||||
func isNewView(errs ...error) bool {
|
||||
for _, err := range errs {
|
||||
if err == nil {
|
||||
return false
|
||||
|
@ -41,7 +43,7 @@ func IsNewView(errs ...error) bool {
|
|||
|
||||
// layout defines the definition of the window pane size and placement relations to one another. This
|
||||
// is invoked at application start and whenever the screen dimensions change.
|
||||
func (lm *layoutManager) layout(g *gocui.Gui) error {
|
||||
func (lm *LayoutManager) Layout(g *gocui.Gui) error {
|
||||
// TODO: this logic should be refactored into an abstraction that takes care of the math for us
|
||||
|
||||
maxX, maxY := g.Size()
|
||||
|
@ -64,12 +66,12 @@ func (lm *layoutManager) layout(g *gocui.Gui) error {
|
|||
headerRows := 2
|
||||
|
||||
filterBarHeight := 1
|
||||
statusBarHeight := 1
|
||||
helpBarHeight := 1
|
||||
|
||||
statusBarIndex := 1
|
||||
helpBarIndex := 1
|
||||
filterBarIndex := 2
|
||||
|
||||
layersHeight := len(lm.controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
|
||||
layersHeight := len(lm.controller.Layer.Layers) + headerRows + 1 // layers + header + base image layer row
|
||||
maxLayerHeight := int(0.75 * float64(maxY))
|
||||
if layersHeight > maxLayerHeight {
|
||||
layersHeight = maxLayerHeight
|
||||
|
@ -78,7 +80,7 @@ func (lm *layoutManager) layout(g *gocui.Gui) error {
|
|||
var view, header *gocui.View
|
||||
var viewErr, headerErr, err error
|
||||
|
||||
if !lm.controllers.Filter.IsVisible() {
|
||||
if !lm.controller.Filter.IsVisible() {
|
||||
bottomRows--
|
||||
filterBarHeight = 0
|
||||
}
|
||||
|
@ -93,21 +95,21 @@ func (lm *layoutManager) layout(g *gocui.Gui) error {
|
|||
}
|
||||
|
||||
// Layers
|
||||
view, viewErr = g.SetView(lm.controllers.Layer.Name(), -1, -1+headerRows, splitCols, layersHeight)
|
||||
header, headerErr = g.SetView(lm.controllers.Layer.Name()+"header", -1, -1, splitCols, headerRows)
|
||||
if IsNewView(viewErr, headerErr) {
|
||||
err = lm.controllers.Layer.Setup(view, header)
|
||||
view, viewErr = g.SetView(lm.controller.Layer.Name(), -1, -1+headerRows, splitCols, layersHeight)
|
||||
header, headerErr = g.SetView(lm.controller.Layer.Name()+"header", -1, -1, splitCols, headerRows)
|
||||
if isNewView(viewErr, headerErr) {
|
||||
err = lm.controller.Layer.Setup(view, header)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup layer controller", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = g.SetCurrentView(lm.controllers.Layer.Name()); err != nil {
|
||||
if _, err = g.SetCurrentView(lm.controller.Layer.Name()); err != nil {
|
||||
logrus.Error("unable to set view to layer", err)
|
||||
return err
|
||||
}
|
||||
// since we are selecting the view, we should rerender to indicate it is selected
|
||||
err = lm.controllers.Layer.Render()
|
||||
err = lm.controller.Layer.Render()
|
||||
if err != nil {
|
||||
logrus.Error("unable to render layer view", err)
|
||||
return err
|
||||
|
@ -115,10 +117,10 @@ func (lm *layoutManager) layout(g *gocui.Gui) error {
|
|||
}
|
||||
|
||||
// Details
|
||||
view, viewErr = g.SetView(lm.controllers.Details.Name(), -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows)
|
||||
header, headerErr = g.SetView(lm.controllers.Details.Name()+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows)
|
||||
if IsNewView(viewErr, headerErr) {
|
||||
err = lm.controllers.Details.Setup(view, header)
|
||||
view, viewErr = g.SetView(lm.controller.Details.Name(), -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows)
|
||||
header, headerErr = g.SetView(lm.controller.Details.Name()+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows)
|
||||
if isNewView(viewErr, headerErr) {
|
||||
err = lm.controller.Details.Setup(view, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -126,39 +128,39 @@ func (lm *layoutManager) layout(g *gocui.Gui) error {
|
|||
|
||||
// Filetree
|
||||
offset := 0
|
||||
if !lm.controllers.Tree.AreAttributesVisible() {
|
||||
if !lm.controller.Tree.AreAttributesVisible() {
|
||||
offset = 1
|
||||
}
|
||||
view, viewErr = g.SetView(lm.controllers.Tree.Name(), splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows)
|
||||
header, headerErr = g.SetView(lm.controllers.Tree.Name()+"header", splitCols, -1, debugCols, headerRows-offset)
|
||||
if IsNewView(viewErr, headerErr) {
|
||||
err = lm.controllers.Tree.Setup(view, header)
|
||||
view, viewErr = g.SetView(lm.controller.Tree.Name(), splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows)
|
||||
header, headerErr = g.SetView(lm.controller.Tree.Name()+"header", splitCols, -1, debugCols, headerRows-offset)
|
||||
if isNewView(viewErr, headerErr) {
|
||||
err = lm.controller.Tree.Setup(view, header)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup tree controller", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = lm.controllers.Tree.OnLayoutChange(resized)
|
||||
err = lm.controller.Tree.OnLayoutChange(resized)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup layer controller onLayoutChange", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Status Bar
|
||||
view, viewErr = g.SetView(lm.controllers.Status.Name(), -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1))
|
||||
if IsNewView(viewErr, headerErr) {
|
||||
err = lm.controllers.Status.Setup(view, nil)
|
||||
// Help Bar
|
||||
view, viewErr = g.SetView(lm.controller.Help.Name(), -1, maxY-helpBarHeight-helpBarIndex, maxX, maxY-(helpBarIndex-1))
|
||||
if isNewView(viewErr, headerErr) {
|
||||
err = lm.controller.Help.Setup(view, nil)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup status controller", err)
|
||||
logrus.Error("unable to setup help controller", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Filter Bar
|
||||
view, viewErr = g.SetView(lm.controllers.Filter.Name(), len(lm.controllers.Filter.HeaderStr())-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1))
|
||||
header, headerErr = g.SetView(lm.controllers.Filter.Name()+"header", -1, maxY-filterBarHeight-filterBarIndex, len(lm.controllers.Filter.HeaderStr()), maxY-(filterBarIndex-1))
|
||||
if IsNewView(viewErr, headerErr) {
|
||||
err = lm.controllers.Filter.Setup(view, header)
|
||||
view, viewErr = g.SetView(lm.controller.Filter.Name(), len(lm.controller.Filter.HeaderStr())-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1))
|
||||
header, headerErr = g.SetView(lm.controller.Filter.Name()+"header", -1, maxY-filterBarHeight-filterBarIndex, len(lm.controller.Filter.HeaderStr()), maxY-(filterBarIndex-1))
|
||||
if isNewView(viewErr, headerErr) {
|
||||
err = lm.controller.Filter.Setup(view, header)
|
||||
if err != nil {
|
||||
logrus.Error("unable to setup filter controller", err)
|
||||
return err
|
||||
|
|
|
@ -43,6 +43,14 @@ func NewDetailsView(name string, gui *gocui.Gui, efficiency float64, inefficienc
|
|||
return controller
|
||||
}
|
||||
|
||||
func (c *Details) Height() int {
|
||||
return HeightFull
|
||||
}
|
||||
|
||||
func (c *Details) Width() int {
|
||||
return WidthFull
|
||||
}
|
||||
|
||||
func (c *Details) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
|
|
@ -175,6 +175,14 @@ func (c *FileTree) Setup(v *gocui.View, header *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *FileTree) Height() int {
|
||||
return HeightFull
|
||||
}
|
||||
|
||||
func (c *FileTree) Width() int {
|
||||
return WidthFull
|
||||
}
|
||||
|
||||
// IsVisible indicates if the file tree view pane is currently initialized
|
||||
func (c *FileTree) IsVisible() bool {
|
||||
return c != nil
|
||||
|
|
|
@ -43,6 +43,15 @@ func (c *Filter) AddFilterEditListener(listener ...FilterEditListener) {
|
|||
c.filterEditListeners = append(c.filterEditListeners, listener...)
|
||||
}
|
||||
|
||||
func (c *Filter) Height() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (c *Filter) Width() int {
|
||||
return WidthFull
|
||||
}
|
||||
|
||||
|
||||
func (c *Filter) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
|
|
@ -10,21 +10,21 @@ import (
|
|||
"github.com/jroimartin/gocui"
|
||||
)
|
||||
|
||||
// Status holds the UI objects and data models for populating the bottom-most pane. Specifically the panel
|
||||
// Help holds the UI objects and data models for populating the bottom-most pane. Specifically the panel
|
||||
// shows the user a set of possible actions to take in the window and currently selected pane.
|
||||
type Status struct {
|
||||
type Help struct {
|
||||
name string
|
||||
gui *gocui.Gui
|
||||
view *gocui.View
|
||||
|
||||
selectedView Renderer
|
||||
selectedView View
|
||||
|
||||
helpKeys []*key.Binding
|
||||
}
|
||||
|
||||
// NewStatusView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewStatusView(name string, gui *gocui.Gui) (controller *Status) {
|
||||
controller = new(Status)
|
||||
// NewHelpView creates a new view object attached the the global [gocui] screen object.
|
||||
func NewHelpView(name string, gui *gocui.Gui) (controller *Help) {
|
||||
controller = new(Help)
|
||||
|
||||
// populate main fields
|
||||
controller.name = name
|
||||
|
@ -34,20 +34,28 @@ func NewStatusView(name string, gui *gocui.Gui) (controller *Status) {
|
|||
return controller
|
||||
}
|
||||
|
||||
func (c *Status) SetCurrentView(r Renderer) {
|
||||
func (c *Help) SetCurrentView(r View) {
|
||||
c.selectedView = r
|
||||
}
|
||||
|
||||
func (c *Status) Name() string {
|
||||
func (c *Help) Height() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (c *Help) Width() int {
|
||||
return WidthFull
|
||||
}
|
||||
|
||||
func (c *Help) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c *Status) AddHelpKeys(keys ...*key.Binding) {
|
||||
func (c *Help) AddHelpKeys(keys ...*key.Binding) {
|
||||
c.helpKeys = append(c.helpKeys, keys...)
|
||||
}
|
||||
|
||||
// Setup initializes the UI concerns within the context of a global [gocui] view object.
|
||||
func (c *Status) Setup(v *gocui.View, header *gocui.View) error {
|
||||
func (c *Help) Setup(v *gocui.View, header *gocui.View) error {
|
||||
|
||||
// set controller options
|
||||
c.view = v
|
||||
|
@ -57,27 +65,27 @@ func (c *Status) Setup(v *gocui.View, header *gocui.View) error {
|
|||
}
|
||||
|
||||
// IsVisible indicates if the status view pane is currently initialized.
|
||||
func (c *Status) IsVisible() bool {
|
||||
func (c *Help) IsVisible() bool {
|
||||
return c != nil
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down in the details pane (currently indicates nothing).
|
||||
func (c *Status) CursorDown() error {
|
||||
func (c *Help) CursorDown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursorUp moves the cursor up in the details pane (currently indicates nothing).
|
||||
func (c *Status) CursorUp() error {
|
||||
func (c *Help) CursorUp() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update refreshes the state objects for future rendering (currently does nothing).
|
||||
func (c *Status) Update() error {
|
||||
func (c *Help) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render flushes the state objects to the screen.
|
||||
func (c *Status) Render() error {
|
||||
func (c *Help) Render() error {
|
||||
c.gui.Update(func(g *gocui.Gui) error {
|
||||
c.view.Clear()
|
||||
|
||||
|
@ -97,7 +105,7 @@ func (c *Status) Render() error {
|
|||
}
|
||||
|
||||
// KeyHelp indicates all the possible global actions a user can take when any pane is selected.
|
||||
func (c *Status) KeyHelp() string {
|
||||
func (c *Help) KeyHelp() string {
|
||||
var help string
|
||||
for _, binding := range c.helpKeys {
|
||||
help += binding.RenderKeyHelp()
|
|
@ -79,6 +79,15 @@ func (c *Layer) notifyLayerChangeListeners() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Layer) Height() int {
|
||||
return HeightFull
|
||||
}
|
||||
|
||||
func (c *Layer) Width() int {
|
||||
return WidthFull
|
||||
}
|
||||
|
||||
|
||||
func (c *Layer) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
package view
|
||||
|
||||
// Controller defines the a renderable terminal screen pane.
|
||||
type Renderer interface {
|
||||
Update() error
|
||||
Render() error
|
||||
IsVisible() bool
|
||||
KeyHelp() string
|
||||
}
|
28
runtime/ui/view/view.go
Normal file
28
runtime/ui/view/view.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package view
|
||||
|
||||
const (
|
||||
HeightFull = -1
|
||||
WidthFull = -1
|
||||
IdentityNone = ""
|
||||
)
|
||||
|
||||
|
||||
type Identifiable interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
type Dimensional interface {
|
||||
IsVisible() bool
|
||||
Height() int
|
||||
Width() int
|
||||
}
|
||||
|
||||
|
||||
// View defines the an element with state that can be updated, queried if visible, and render elements to the screen
|
||||
type View interface {
|
||||
Identifiable
|
||||
Dimensional
|
||||
Update() error
|
||||
Render() error
|
||||
KeyHelp() string
|
||||
}
|
Loading…
Reference in a new issue