allow changing window size
parent
df1a0b7c07
commit
badc43b225
@ -0,0 +1,237 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazycore/pkg/boxlayout"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// In this file we use the boxlayout package, along with knowledge about the app's state,
|
||||
// to arrange the windows (i.e. panels) on the screen.
|
||||
|
||||
const INFO_SECTION_PADDING = " "
|
||||
|
||||
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
|
||||
minimumHeight := 9
|
||||
minimumWidth := 10
|
||||
width, height := gui.g.Size()
|
||||
if width < minimumWidth || height < minimumHeight {
|
||||
return boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height)
|
||||
}
|
||||
|
||||
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
|
||||
|
||||
sidePanelsDirection := boxlayout.COLUMN
|
||||
portraitMode := width <= 84 && height > 45
|
||||
if portraitMode {
|
||||
sidePanelsDirection = boxlayout.ROW
|
||||
}
|
||||
|
||||
showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine
|
||||
infoSectionSize := 0
|
||||
if showInfoSection {
|
||||
infoSectionSize = 1
|
||||
}
|
||||
|
||||
root := &boxlayout.Box{
|
||||
Direction: boxlayout.ROW,
|
||||
Children: []*boxlayout.Box{
|
||||
{
|
||||
Direction: sidePanelsDirection,
|
||||
Weight: 1,
|
||||
Children: []*boxlayout.Box{
|
||||
{
|
||||
Direction: boxlayout.ROW,
|
||||
Weight: sideSectionWeight,
|
||||
ConditionalChildren: gui.sidePanelChildren,
|
||||
},
|
||||
{
|
||||
Window: "main",
|
||||
Weight: mainSectionWeight,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Direction: boxlayout.COLUMN,
|
||||
Size: infoSectionSize,
|
||||
Children: gui.infoSectionChildren(informationStr, appStatus),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return boxlayout.ArrangeWindows(root, 0, 0, width, height)
|
||||
}
|
||||
|
||||
func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
|
||||
result := map[K]V{}
|
||||
for _, currMap := range maps {
|
||||
for key, value := range currMap {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) getMidSectionWeights() (int, int) {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
|
||||
sidePanelWidthRatio := gui.Config.UserConfig.Gui.SidePanelWidth
|
||||
// we could make this better by creating ratios like 2:3 rather than always 1:something
|
||||
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
|
||||
sideSectionWeight := 1
|
||||
|
||||
if currentWindow == "main" && gui.State.ScreenMode == SCREEN_FULL {
|
||||
mainSectionWeight = 1
|
||||
sideSectionWeight = 0
|
||||
} else {
|
||||
if gui.State.ScreenMode == SCREEN_HALF {
|
||||
mainSectionWeight = 1
|
||||
} else if gui.State.ScreenMode == SCREEN_FULL {
|
||||
mainSectionWeight = 0
|
||||
}
|
||||
}
|
||||
|
||||
return sideSectionWeight, mainSectionWeight
|
||||
}
|
||||
|
||||
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
|
||||
result := []*boxlayout.Box{}
|
||||
|
||||
if len(appStatus) > 0 {
|
||||
result = append(result,
|
||||
&boxlayout.Box{
|
||||
Window: "appStatus",
|
||||
Size: runewidth.StringWidth(appStatus) + runewidth.StringWidth(INFO_SECTION_PADDING),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
result = append(result,
|
||||
[]*boxlayout.Box{
|
||||
{
|
||||
Window: "options",
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: "information",
|
||||
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
|
||||
Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)),
|
||||
},
|
||||
}...,
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) sideViewNames() []string {
|
||||
if gui.DockerCommand.InDockerComposeProject {
|
||||
return []string{"project", "services", "containers", "images", "volumes"}
|
||||
} else {
|
||||
return []string{"project", "containers", "images", "volumes"}
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||
currentWindow := gui.currentSideWindowName()
|
||||
sideWindowNames := gui.sideViewNames()
|
||||
|
||||
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
|
||||
fullHeightBox := func(window string) *boxlayout.Box {
|
||||
if window == currentWindow {
|
||||
return &boxlayout.Box{
|
||||
Window: window,
|
||||
Weight: 1,
|
||||
}
|
||||
} else {
|
||||
return &boxlayout.Box{
|
||||
Window: window,
|
||||
Size: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
|
||||
return fullHeightBox(window)
|
||||
})
|
||||
|
||||
} else if height >= 28 {
|
||||
accordionMode := gui.Config.UserConfig.Gui.ExpandFocusedSidePanel
|
||||
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||
if accordionMode && defaultBox.Window == currentWindow {
|
||||
return &boxlayout.Box{
|
||||
Window: defaultBox.Window,
|
||||
Weight: 2,
|
||||
}
|
||||
}
|
||||
|
||||
return defaultBox
|
||||
}
|
||||
|
||||
return append([]*boxlayout.Box{
|
||||
{
|
||||
Window: sideWindowNames[0],
|
||||
Size: 3,
|
||||
},
|
||||
}, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box {
|
||||
return accordionBox(&boxlayout.Box{Window: window, Weight: 1})
|
||||
})...)
|
||||
} else {
|
||||
squashedHeight := 1
|
||||
if height >= 21 {
|
||||
squashedHeight = 3
|
||||
}
|
||||
|
||||
squashedSidePanelBox := func(window string) *boxlayout.Box {
|
||||
if window == currentWindow {
|
||||
return &boxlayout.Box{
|
||||
Window: window,
|
||||
Weight: 1,
|
||||
}
|
||||
} else {
|
||||
return &boxlayout.Box{
|
||||
Window: window,
|
||||
Size: squashedHeight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box {
|
||||
return squashedSidePanelBox(window)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: reintroduce
|
||||
// func (gui *Gui) currentSideWindowName() string {
|
||||
// // there is always one and only one cyclable context in the context stack. We'll look from top to bottom
|
||||
// gui.State.ContextManager.RLock()
|
||||
// defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
// for idx := range gui.State.ContextManager.ContextStack {
|
||||
// reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx
|
||||
// context := gui.State.ContextManager.ContextStack[reversedIdx]
|
||||
|
||||
// if context.GetKind() == types.SIDE_CONTEXT {
|
||||
// return context.GetWindowName()
|
||||
// }
|
||||
// }
|
||||
|
||||
// return "files" // default
|
||||
// }
|
||||
|
||||
func (gui *Gui) currentNonPopupWindowName() string {
|
||||
return gui.peekPreviousView()
|
||||
}
|
||||
|
||||
// TODO: do this better.
|
||||
func (gui *Gui) currentSideWindowName() string {
|
||||
windowName := gui.currentWindow()
|
||||
if !lo.Contains(gui.sideViewNames(), windowName) {
|
||||
return gui.peekPreviousView()
|
||||
}
|
||||
|
||||
return windowName
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Views struct {
|
||||
Project *gocui.View
|
||||
Services *gocui.View
|
||||
Containers *gocui.View
|
||||
Images *gocui.View
|
||||
Volumes *gocui.View
|
||||
|
||||
Main *gocui.View
|
||||
|
||||
Options *gocui.View
|
||||
Confirmation *gocui.View
|
||||
Menu *gocui.View
|
||||
Information *gocui.View
|
||||
AppStatus *gocui.View
|
||||
Limit *gocui.View
|
||||
}
|
||||
|
||||
type viewNameMapping struct {
|
||||
viewPtr **gocui.View
|
||||
name string
|
||||
}
|
||||
|
||||
func (gui *Gui) orderedViews() []*gocui.View {
|
||||
return lo.Map(gui.orderedViewNameMappings(), func(v viewNameMapping, _ int) *gocui.View {
|
||||
return *v.viewPtr
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
|
||||
return []viewNameMapping{
|
||||
// first layer. Ordering within this layer does not matter because there are
|
||||
// no overlapping views
|
||||
{viewPtr: &gui.Views.Project, name: "project"},
|
||||
{viewPtr: &gui.Views.Services, name: "services"},
|
||||
{viewPtr: &gui.Views.Containers, name: "containers"},
|
||||
{viewPtr: &gui.Views.Images, name: "images"},
|
||||
{viewPtr: &gui.Views.Volumes, name: "volumes"},
|
||||
|
||||
{viewPtr: &gui.Views.Main, name: "main"},
|
||||
|
||||
// bottom line
|
||||
{viewPtr: &gui.Views.Options, name: "options"},
|
||||
{viewPtr: &gui.Views.AppStatus, name: "appStatus"},
|
||||
{viewPtr: &gui.Views.Information, name: "information"},
|
||||
|
||||
// popups.
|
||||
{viewPtr: &gui.Views.Menu, name: "menu"},
|
||||
{viewPtr: &gui.Views.Confirmation, name: "confirmation"},
|
||||
|
||||
// this guy will cover everything else when it appears
|
||||
{viewPtr: &gui.Views.Limit, name: "limit"},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) createAllViews() error {
|
||||
var err error
|
||||
for _, mapping := range gui.orderedViewNameMappings() {
|
||||
*mapping.viewPtr, err = gui.prepareView(mapping.name)
|
||||
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
(*mapping.viewPtr).FgColor = gocui.ColorDefault
|
||||
}
|
||||
|
||||
gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
|
||||
// when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default.
|
||||
gui.Views.Main.IgnoreCarriageReturns = true
|
||||
|
||||
gui.Views.Project.Title = gui.Tr.ProjectTitle
|
||||
|
||||
gui.Views.Services.Highlight = true
|
||||
gui.Views.Services.Title = gui.Tr.ServicesTitle
|
||||
|
||||
gui.Views.Containers.Highlight = true
|
||||
if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject {
|
||||
gui.Views.Containers.Title = gui.Tr.ContainersTitle
|
||||
} else {
|
||||
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
|
||||
}
|
||||
|
||||
gui.Views.Images.Highlight = true
|
||||
gui.Views.Images.Title = gui.Tr.ImagesTitle
|
||||
|
||||
gui.Views.Volumes.Highlight = true
|
||||
gui.Views.Volumes.Title = gui.Tr.VolumesTitle
|
||||
|
||||
gui.Views.Options.Frame = false
|
||||
gui.Views.Options.FgColor = gui.GetOptionsPanelTextColor()
|
||||
|
||||
gui.Views.AppStatus.FgColor = gocui.ColorCyan
|
||||
gui.Views.AppStatus.Frame = false
|
||||
|
||||
gui.Views.Information.Frame = false
|
||||
gui.Views.Information.FgColor = gocui.ColorGreen
|
||||
|
||||
if err := gui.renderString(gui.g, "information", gui.getInformationContent()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.Views.Confirmation.Visible = false
|
||||
gui.Views.Menu.Visible = false
|
||||
|
||||
gui.Views.Limit.Visible = false
|
||||
gui.Views.Limit.Title = gui.Tr.NotEnoughSpace
|
||||
gui.Views.Limit.Wrap = true
|
||||
|
||||
gui.waitForIntro.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getInformationContent() string {
|
||||
informationStr := gui.Config.Version
|
||||
if !gui.g.Mouse {
|
||||
return informationStr
|
||||
}
|
||||
|
||||
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
|
||||
return donate + " " + informationStr
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package gui
|
||||
|
||||
func (gui *Gui) currentWindow() string {
|
||||
// at the moment, we only have one view per window in lazydocker, so we
|
||||
// are using the view name as the window name
|
||||
return gui.currentViewName()
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct`
|
||||
# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag.
|
||||
GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor && go mod tidy
|
||||
|
||||
# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master)
|
@ -0,0 +1,5 @@
|
||||
# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct`
|
||||
# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag.
|
||||
GOPROXY=direct go get -u github.com/jesseduffield/lazycore@master && go mod vendor && go mod tidy
|
||||
|
||||
# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master)
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Jesse Duffield
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,211 @@
|
||||
package boxlayout
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazycore/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type Dimensions struct {
|
||||
X0 int
|
||||
X1 int
|
||||
Y0 int
|
||||
Y1 int
|
||||
}
|
||||
|
||||
type Direction int
|
||||
|
||||
const (
|
||||
ROW Direction = iota
|
||||
COLUMN
|
||||
)
|
||||
|
||||
// to give a high-level explanation of what's going on here. We layout our windows by arranging a bunch of boxes in the available space.
|
||||
// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN.
|
||||
// If a box represents a window, you can put the window name in the Window field.
|
||||
// When determining how to divvy-up the available height (for row children) or width (for column children), we first
|
||||
// give the boxes with a static `size` the space that they want. Then we apportion
|
||||
// the remaining space based on the weights of the dynamic boxes (you can't define
|
||||
// both size and weight at the same time: you gotta pick one). If there are two
|
||||
// boxes, one with weight 1 and the other with weight 2, the first one gets 33%
|
||||
// of the available space and the second one gets the remaining 66%
|
||||
|
||||
type Box struct {
|
||||
// Direction decides how the children boxes are laid out. ROW means the children will each form a row i.e. that they will be stacked on top of eachother.
|
||||
Direction Direction
|
||||
|
||||
// function which takes the width and height assigned to the box and decides which orientation it will have
|
||||
ConditionalDirection func(width int, height int) Direction
|
||||
|
||||
Children []*Box
|
||||
|
||||
// function which takes the width and height assigned to the box and decides the layout of the children.
|
||||
ConditionalChildren func(width int, height int) []*Box
|
||||
|
||||
// Window refers to the name of the window this box represents, if there is one
|
||||
Window string
|
||||
|
||||
// static Size. If parent box's direction is ROW this refers to height, otherwise width
|
||||
Size int
|
||||
|
||||
// dynamic size. Once all statically sized children have been considered, Weight decides how much of the remaining space will be taken up by the box
|
||||
// TODO: consider making there be one int and a type enum so we can't have size and Weight simultaneously defined
|
||||
Weight int
|
||||
}
|
||||
|
||||
func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions {
|
||||
children := root.getChildren(width, height)
|
||||
if len(children) == 0 {
|
||||
// leaf node
|
||||
if root.Window != "" {
|
||||
dimensionsForWindow := Dimensions{X0: x0, Y0: y0, X1: x0 + width - 1, Y1: y0 + height - 1}
|
||||
return map[string]Dimensions{root.Window: dimensionsForWindow}
|
||||
}
|
||||
return map[string]Dimensions{}
|
||||
}
|
||||
|
||||
direction := root.getDirection(width, height)
|
||||
|
||||
var availableSize int
|
||||
if direction == COLUMN {
|
||||
availableSize = width
|
||||
} else {
|
||||
availableSize = height
|
||||
}
|
||||
|
||||
sizes := calcSizes(children, availableSize)
|
||||
|
||||
result := map[string]Dimensions{}
|
||||
offset := 0
|
||||
for i, child := range children {
|
||||
boxSize := sizes[i]
|
||||
|
||||
var resultForChild map[string]Dimensions
|
||||
if direction == COLUMN {
|
||||
resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height)
|
||||
} else {
|
||||
resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize)
|
||||
}
|
||||
|
||||
result = mergeDimensionMaps(result, resultForChild)
|
||||
offset += boxSize
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func calcSizes(boxes []*Box, availableSpace int) []int {
|
||||
normalizedWeights := normalizeWeights(lo.Map(boxes, func(box *Box, _ int) int { return box.Weight }))
|
||||
|
||||
totalWeight := 0
|
||||
reservedSpace := 0
|
||||
for i, box := range boxes {
|
||||
if box.isStatic() {
|
||||
reservedSpace += box.Size
|
||||
} else {
|
||||
totalWeight += normalizedWeights[i]
|
||||
}
|
||||
}
|
||||
|
||||
dynamicSpace := utils.Max(0, availableSpace-reservedSpace)
|
||||
|
||||
unitSize := 0
|
||||
extraSpace := 0
|
||||
if totalWeight > 0 {
|
||||
unitSize = dynamicSpace / totalWeight
|
||||
extraSpace = dynamicSpace % totalWeight
|
||||
}
|
||||
|
||||
result := make([]int, len(boxes))
|
||||
for i, box := range boxes {
|
||||
if box.isStatic() {
|
||||
// assuming that only one static child can have a size greater than the
|
||||
// available space. In that case we just crop the size to what's available
|
||||
result[i] = utils.Min(availableSpace, box.Size)
|
||||
} else {
|
||||
result[i] = unitSize * normalizedWeights[i]
|
||||
}
|
||||
}
|
||||
|
||||
// distribute the remainder across dynamic boxes.
|
||||
for extraSpace > 0 {
|
||||
for i, weight := range normalizedWeights {
|
||||
if weight > 0 {
|
||||
result[i]++
|
||||
extraSpace--
|
||||
normalizedWeights[i]--
|
||||
|
||||
if extraSpace == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// removes common multiple from weights e.g. if we get 2, 4, 4 we return 1, 2, 2.
|
||||
func normalizeWeights(weights []int) []int {
|
||||
if len(weights) == 0 {
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// to spare us some computation we'll exit early if any of our weights is 1
|
||||
if lo.SomeBy(weights, func(weight int) bool { return weight == 1 }) {
|
||||
return weights
|
||||
}
|
||||
|
||||
// map weights to factorSlices and find the lowest common factor
|
||||
positiveWeights := lo.Filter(weights, func(weight int, _ int) bool { return weight > 0 })
|
||||
factorSlices := lo.Map(positiveWeights, func(weight int, _ int) []int { return calcFactors(weight) })
|
||||
commonFactors := factorSlices[0]
|
||||
for _, factors := range factorSlices {
|
||||
commonFactors = lo.Intersect(commonFactors, factors)
|
||||
}
|
||||
|
||||
if len(commonFactors) == 0 {
|
||||
return weights
|
||||
}
|
||||
|
||||
newWeights := lo.Map(weights, func(weight int, _ int) int { return weight / commonFactors[0] })
|
||||
|
||||
return normalizeWeights(newWeights)
|
||||
}
|
||||
|
||||
func calcFactors(n int) []int {
|
||||
factors := []int{}
|
||||
for i := 2; i <= n; i++ {
|
||||
if n%i == 0 {
|
||||
factors = append(factors, i)
|
||||
}
|
||||
}
|
||||
return factors
|
||||
}
|
||||
|
||||
func (b *Box) isStatic() bool {
|
||||
return b.Size > 0
|
||||
}
|
||||
|
||||
func (b *Box) getDirection(width int, height int) Direction {
|
||||
if b.ConditionalDirection != nil {
|
||||
return b.ConditionalDirection(width, height)
|
||||
}
|
||||
return b.Direction
|
||||
}
|
||||
|
||||
func (b *Box) getChildren(width int, height int) []*Box {
|
||||
if b.ConditionalChildren != nil {
|
||||
return b.ConditionalChildren(width, height)
|
||||
}
|
||||
return b.Children
|
||||
}
|
||||
|
||||
func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions {
|
||||
result := map[string]Dimensions{}
|
||||
for _, dimensionMap := range []map[string]Dimensions{a, b} {
|
||||
for k, v := range dimensionMap {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
// Min returns the minimum of two integers
|
||||
func Min(x, y int) int {
|
||||
if x < y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
func Max(x, y int) int {
|
||||
if x > y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,228 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DispatchingStrategy[T any] func(msg T, index uint64, channels []<-chan T) int
|
||||
|
||||
// ChannelDispatcher distributes messages from input channels into N child channels.
|
||||
// Close events are propagated to children.
|
||||
// Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0.
|
||||
func ChannelDispatcher[T any](stream <-chan T, count int, channelBufferCap int, strategy DispatchingStrategy[T]) []<-chan T {
|
||||
children := createChannels[T](count, channelBufferCap)
|
||||
|
||||
roChildren := channelsToReadOnly(children)
|
||||
|
||||
go func() {
|
||||
// propagate channel closing to children
|
||||
defer closeChannels(children)
|
||||
|
||||
var i uint64 = 0
|
||||
|
||||
for {
|
||||
msg, ok := <-stream
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
destination := strategy(msg, i, roChildren) % count
|
||||
children[destination] <- msg
|
||||
|
||||
i++
|
||||
}
|
||||
}()
|
||||
|
||||
return roChildren
|
||||
}
|
||||
|
||||
func createChannels[T any](count int, channelBufferCap int) []chan T {
|
||||
children := make([]chan T, 0, count)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
children = append(children, make(chan T, channelBufferCap))
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
func channelsToReadOnly[T any](children []chan T) []<-chan T {
|
||||
roChildren := make([]<-chan T, 0, len(children))
|
||||
|
||||
for i := range children {
|
||||
roChildren = append(roChildren, children[i])
|
||||
}
|
||||
|
||||
return roChildren
|
||||
}
|
||||
|
||||
func closeChannels[T any](children []chan T) {
|
||||
for i := 0; i < len(children); i++ {
|
||||
close(children[i])
|
||||
}
|
||||
}
|
||||
|
||||
func channelIsNotFull[T any](ch <-chan T) bool {
|
||||
return cap(ch) == 0 || len(ch) < cap(ch)
|
||||
}
|
||||
|
||||
// DispatchingStrategyRoundRobin distributes messages in a rotating sequential manner.
|
||||
// If the channel capacity is exceeded, the next channel will be selected and so on.
|
||||
func DispatchingStrategyRoundRobin[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
for {
|
||||
i := int(index % uint64(len(channels)))
|
||||
if channelIsNotFull(channels[i]) {
|
||||
return i
|
||||
}
|
||||
|
||||
index++
|
||||
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
|
||||
}
|
||||
}
|
||||
|
||||
// DispatchingStrategyRandom distributes messages in a random manner.
|
||||
// If the channel capacity is exceeded, another random channel will be selected and so on.
|
||||
func DispatchingStrategyRandom[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
for {
|
||||
i := rand.Intn(len(channels))
|
||||
if channelIsNotFull(channels[i]) {
|
||||
return i
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
|
||||
}
|
||||
}
|
||||
|
||||
// DispatchingStrategyRandom distributes messages in a weighted manner.
|
||||
// If the channel capacity is exceeded, another random channel will be selected and so on.
|
||||
func DispatchingStrategyWeightedRandom[T any](weights []int) DispatchingStrategy[T] {
|
||||
seq := []int{}
|
||||
|
||||
for i := 0; i < len(weights); i++ {
|
||||
for j := 0; j < weights[i]; j++ {
|
||||
seq = append(seq, i)
|
||||
}
|
||||
}
|
||||
|
||||
return func(msg T, index uint64, channels []<-chan T) int {
|
||||
for {
|
||||
i := seq[rand.Intn(len(seq))]
|
||||
if channelIsNotFull(channels[i]) {
|
||||
return i
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DispatchingStrategyFirst distributes messages in the first non-full channel.
|
||||
// If the capacity of the first channel is exceeded, the second channel will be selected and so on.
|
||||
func DispatchingStrategyFirst[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
for {
|
||||
for i := range channels {
|
||||
if channelIsNotFull(channels[i]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
|
||||
}
|
||||
}
|
||||
|
||||
// DispatchingStrategyLeast distributes messages in the emptiest channel.
|
||||
func DispatchingStrategyLeast[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
seq := Range(len(channels))
|
||||
|
||||
return MinBy(seq, func(item int, min int) bool {
|
||||
return len(channels[item]) < len(channels[min])
|
||||
})
|
||||
}
|
||||
|
||||
// DispatchingStrategyMost distributes messages in the fulliest channel.
|
||||
// If the channel capacity is exceeded, the next channel will be selected and so on.
|
||||
func DispatchingStrategyMost[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
seq := Range(len(channels))
|
||||
|
||||
return MaxBy(seq, func(item int, max int) bool {
|
||||
return len(channels[item]) > len(channels[max]) && channelIsNotFull(channels[item])
|
||||
})
|
||||
}
|
||||
|
||||
// SliceToChannel returns a read-only channels of collection elements.
|
||||
func SliceToChannel[T any](bufferSize int, collection []T) <-chan T {
|
||||
ch := make(chan T, bufferSize)
|
||||
|
||||
go func() {
|
||||
for _, item := range collection {
|
||||
ch <- item
|
||||
}
|
||||
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// Generator implements the generator design pattern.
|
||||
func Generator[T any](bufferSize int, generator func(yield func(T))) <-chan T {
|
||||
ch := make(chan T, bufferSize)
|
||||
|
||||
go func() {
|
||||
// WARNING: infinite loop
|
||||
generator(func(t T) {
|
||||
ch <- t
|
||||
})
|
||||
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// Batch creates a slice of n elements from a channel. Returns the slice and the slice length.
|
||||
// @TODO: we should probaby provide an helper that reuse the same buffer.
|
||||
func Batch[T any](ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) {
|
||||
buffer := make([]T, 0, size)
|
||||
index := 0
|
||||
now := time.Now()
|
||||
|
||||
for ; index < size; index++ {
|
||||
item, ok := <-ch
|
||||
if !ok {
|
||||
return buffer, index, time.Since(now), false
|
||||
}
|
||||
|
||||
buffer = append(buffer, item)
|
||||
}
|
||||
|
||||
return buffer, index, time.Since(now), true
|
||||
}
|
||||
|
||||
// BatchWithTimeout creates a slice of n elements from a channel, with timeout. Returns the slice and the slice length.
|
||||
// @TODO: we should probaby provide an helper that reuse the same buffer.
|
||||
func BatchWithTimeout[T any](ch <-chan T, size int, timeout time.Duration) (collection []T, length int, readTime time.Duration, ok bool) {
|
||||
expire := time.NewTimer(timeout)
|
||||
defer expire.Stop()
|
||||
|
||||
buffer := make([]T, 0, size)
|
||||
index := 0
|
||||
now := time.Now()
|
||||
|
||||
for ; index < size; index++ {
|
||||
select {
|
||||
case item, ok := <-ch:
|
||||
if !ok {
|
||||
return buffer, index, time.Since(now), false
|
||||
}
|
||||
|
||||
buffer = append(buffer, item)
|
||||
|
||||
case <-expire.C:
|
||||
return buffer, index, time.Since(now), true
|
||||
}
|
||||
}
|
||||
|
||||
return buffer, index, time.Since(now), true
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package lo
|
||||
|
||||
// Partial returns new function that, when called, has its first argument set to the provided value.
|
||||
func Partial[T1, T2, R any](f func(T1, T2) R, arg1 T1) func(T2) R {
|
||||
return func(t2 T2) R {
|
||||
return f(arg1, t2)
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package lo
|
||||
|
||||
// ToPtr returns a pointer copy of value.
|
||||
func ToPtr[T any](x T) *T {
|
||||
return &x
|
||||
}
|
||||
|
||||
// ToSlicePtr returns a slice of pointer copy of value.
|
||||
func ToSlicePtr[T any](collection []T) []*T {
|
||||
return Map(collection, func(x T, _ int) *T {
|
||||
return &x
|
||||
})
|
||||
}
|
||||
|
||||
// Empty returns an empty value.
|
||||
func Empty[T any]() T {
|
||||
var t T
|
||||
return t
|
||||
}
|
||||
|
||||
// Coalesce returns the first non-empty arguments. Arguments must be comparable.
|
||||
func Coalesce[T comparable](v ...T) (result T, ok bool) {
|
||||
for _, e := range v {
|
||||
if e != result {
|
||||
result = e
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// https://github.com/stretchr/testify/issues/1101
|
||||
func testWithTimeout(t *testing.T, timeout time.Duration) {
|
||||
t.Helper()
|
||||
|
||||
testFinished := make(chan struct{})
|
||||
t.Cleanup(func() { close(testFinished) })
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-testFinished:
|
||||
case <-time.After(timeout):
|
||||
t.Errorf("test timed out after %s", timeout)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type foo struct {
|
||||
bar string
|
||||
}
|
||||
|
||||
func (f foo) Clone() foo {
|
||||
return foo{f.bar}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package lo
|
||||
|
||||
// ToPtr returns a pointer copy of value.
|
||||
func ToPtr[T any](x T) *T {
|
||||
return &x
|
||||
}
|
||||
|
||||
// FromPtr returns the pointer value or empty.
|
||||
func FromPtr[T any](x *T) T {
|
||||
if x == nil {
|
||||
return Empty[T]()
|
||||
}
|
||||
|
||||
return *x
|
||||
}
|
||||
|
||||
// FromPtrOr returns the pointer value or the fallback value.
|
||||
func FromPtrOr[T any](x *T, fallback T) T {
|
||||
if x == nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return *x
|
||||
}
|
||||
|
||||
// ToSlicePtr returns a slice of pointer copy of value.
|
||||
func ToSlicePtr[T any](collection []T) []*T {
|
||||
return Map(collection, func(x T, _ int) *T {
|
||||
return &x
|
||||
})
|
||||
}
|
||||
|
||||
// ToAnySlice returns a slice with all elements mapped to `any` type
|
||||
func ToAnySlice[T any](collection []T) []any {
|
||||
result := make([]any, len(collection))
|
||||
for i, item := range collection {
|
||||
result[i] = item
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FromAnySlice returns an `any` slice with all elements mapped to a type.
|
||||
// Returns false in case of type conversion failure.
|
||||
func FromAnySlice[T any](in []any) (out []T, ok bool) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
out = []T{}
|
||||
ok = false
|
||||
}
|
||||
}()
|
||||
|
||||
result := make([]T, len(in))
|
||||
for i, item := range in {
|
||||
result[i] = item.(T)
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
// Empty returns an empty value.
|
||||
func Empty[T any]() T {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
|
||||
// IsEmpty returns true if argument is a zero value.
|
||||
func IsEmpty[T comparable](v T) bool {
|
||||
var zero T
|
||||
return zero == v
|
||||
}
|
||||
|
||||
// IsNotEmpty returns true if argument is not a zero value.
|
||||
func IsNotEmpty[T comparable](v T) bool {
|
||||
var zero T
|
||||
return zero != v
|
||||
}
|
||||
|
||||
// Coalesce returns the first non-empty arguments. Arguments must be comparable.
|
||||
func Coalesce[T comparable](v ...T) (result T, ok bool) {
|
||||
for _, e := range v {
|
||||
if e != result {
|
||||
result = e
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
//go:build go1.17
|
||||
// +build go1.17
|
||||
|
||||
// TODO: once support for Go 1.16 is dropped, this file can be
|
||||
// merged/removed with assertion_compare_go1.17_test.go and
|
||||
// assertion_compare_legacy.go
|
||||
|
||||
package assert
|
||||
|
||||
import "reflect"
|
||||
|
||||
// Wrapper around reflect.Value.CanConvert, for compatibility
|
||||
// reasons.
|
||||
func canConvert(value reflect.Value, to reflect.Type) bool {
|
||||
return value.CanConvert(to)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
//go:build !go1.17
|
||||
// +build !go1.17
|
||||
|
||||
// TODO: once support for Go 1.16 is dropped, this file can be
|
||||
// merged/removed with assertion_compare_go1.17_test.go and
|
||||
// assertion_compare_can_convert.go
|
||||
|
||||
package assert
|
||||
|
||||
import "reflect"
|
||||
|
||||
// Older versions of Go does not have the reflect.Value.CanConvert
|
||||
// method.
|
||||
func canConvert(value reflect.Value, to reflect.Type) bool {
|
||||
return false
|
||||
}
|
Loading…
Reference in New Issue