Merge pull request #392 from jesseduffield/list-panel-filtering
commit
04cf34383d
@ -0,0 +1,5 @@
|
||||
package commands
|
||||
|
||||
type Project struct {
|
||||
Name string
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func sampleContainers(userConfig *config.AppConfig) []*Container {
|
||||
return []*Container{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: types.Container{
|
||||
State: "exited",
|
||||
},
|
||||
Config: userConfig,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: userConfig,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: userConfig,
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: types.Container{
|
||||
State: "created",
|
||||
},
|
||||
Config: userConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func expectedPerStatusContainers(appConfig *config.AppConfig) []*Container {
|
||||
return []*Container{
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: types.Container{
|
||||
State: "exited",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: types.Container{
|
||||
State: "created",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func expectedLegacySortedContainers(appConfig *config.AppConfig) []*Container {
|
||||
return []*Container{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: types.Container{
|
||||
State: "exited",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: types.Container{
|
||||
State: "running",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: types.Container{
|
||||
State: "created",
|
||||
},
|
||||
Config: appConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqualContainers(t *testing.T, left *Container, right *Container) {
|
||||
t.Helper()
|
||||
assert.Equal(t, left.Container.State, right.Container.State)
|
||||
assert.Equal(t, left.Container.ID, right.Container.ID)
|
||||
assert.Equal(t, left.Name, right.Name)
|
||||
}
|
||||
|
||||
func TestSortContainers(t *testing.T) {
|
||||
appConfig := NewDummyAppConfig()
|
||||
appConfig.UserConfig = &config.UserConfig{
|
||||
Gui: config.GuiConfig{
|
||||
LegacySortContainers: false,
|
||||
},
|
||||
}
|
||||
command := &DockerCommand{
|
||||
Config: appConfig,
|
||||
}
|
||||
|
||||
containers := sampleContainers(appConfig)
|
||||
|
||||
sorted := expectedPerStatusContainers(appConfig)
|
||||
|
||||
ct := command.sortedContainers(containers)
|
||||
|
||||
assert.Equal(t, len(ct), len(sorted))
|
||||
|
||||
for i := 0; i < len(ct); i++ {
|
||||
assertEqualContainers(t, sorted[i], ct[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacySortedContainers(t *testing.T) {
|
||||
appConfig := NewDummyAppConfig()
|
||||
appConfig.UserConfig = &config.UserConfig{
|
||||
Gui: config.GuiConfig{
|
||||
LegacySortContainers: true,
|
||||
},
|
||||
}
|
||||
command := &DockerCommand{
|
||||
Config: appConfig,
|
||||
}
|
||||
|
||||
containers := sampleContainers(appConfig)
|
||||
|
||||
sorted := expectedLegacySortedContainers(appConfig)
|
||||
|
||||
ct := command.sortedContainers(containers)
|
||||
|
||||
for i := 0; i < len(ct); i++ {
|
||||
assertEqualContainers(t, sorted[i], ct[i])
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleOpenFilter() error {
|
||||
panel, ok := gui.currentListPanel()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if panel.IsFilterDisabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.Filter.active = true
|
||||
gui.State.Filter.panel = panel
|
||||
|
||||
return gui.switchFocus(gui.Views.Filter)
|
||||
}
|
||||
|
||||
func (gui *Gui) onNewFilterNeedle(value string) error {
|
||||
gui.State.Filter.needle = value
|
||||
gui.ResetOrigin(gui.State.Filter.panel.GetView())
|
||||
return gui.State.Filter.panel.RerenderList()
|
||||
}
|
||||
|
||||
func (gui *Gui) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
return func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
matched := f(v, key, ch, mod)
|
||||
if matched {
|
||||
if err := gui.onNewFilterNeedle(v.TextArea.GetContent()); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) escapeFilterPrompt() error {
|
||||
if err := gui.clearFilter(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.returnFocus()
|
||||
}
|
||||
|
||||
func (gui *Gui) clearFilter() error {
|
||||
gui.State.Filter.needle = ""
|
||||
gui.State.Filter.active = false
|
||||
panel := gui.State.Filter.panel
|
||||
gui.State.Filter.panel = nil
|
||||
gui.Views.Filter.ClearTextArea()
|
||||
|
||||
if panel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.ResetOrigin(panel.GetView())
|
||||
|
||||
return panel.RerenderList()
|
||||
}
|
||||
|
||||
// returns to the list view with the filter still applied
|
||||
func (gui *Gui) commitFilter() error {
|
||||
if gui.State.Filter.needle == "" {
|
||||
if err := gui.clearFilter(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.returnFocus()
|
||||
}
|
||||
|
||||
func (gui *Gui) filterPrompt() string {
|
||||
return fmt.Sprintf("%s: ", gui.Tr.FilterPrompt)
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func (gui *Gui) newLineFocused(v *gocui.View) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentListPanel, ok := gui.currentListPanel()
|
||||
if ok {
|
||||
return currentListPanel.HandleSelect()
|
||||
}
|
||||
|
||||
switch v.Name() {
|
||||
case "confirmation":
|
||||
return nil
|
||||
case "main":
|
||||
v.Highlight = false
|
||||
return nil
|
||||
case "filter":
|
||||
return nil
|
||||
default:
|
||||
panic(gui.Tr.NoViewMachingNewLineFocusedSwitchStatement)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move some of this logic into our onFocusLost and onFocus hooks
|
||||
func (gui *Gui) switchFocus(newView *gocui.View) error {
|
||||
gui.Mutexes.ViewStackMutex.Lock()
|
||||
defer gui.Mutexes.ViewStackMutex.Unlock()
|
||||
|
||||
return gui.switchFocusAux(newView)
|
||||
}
|
||||
|
||||
func (gui *Gui) switchFocusAux(newView *gocui.View) error {
|
||||
gui.pushView(newView.Name())
|
||||
gui.Log.Info("setting highlight to true for view " + newView.Name())
|
||||
gui.Log.Info("new focused view is " + newView.Name())
|
||||
if _, err := gui.g.SetCurrentView(newView.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.g.Cursor = newView.Editable
|
||||
|
||||
if err := gui.renderPanelOptions(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newViewStack := gui.State.ViewStack
|
||||
|
||||
if gui.State.Filter.panel != nil && !lo.Contains(newViewStack, gui.State.Filter.panel.GetView().Name()) {
|
||||
if err := gui.clearFilter(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add 'onFocusLost' hook
|
||||
if !lo.Contains(newViewStack, "menu") {
|
||||
gui.Views.Menu.Visible = false
|
||||
}
|
||||
|
||||
return gui.newLineFocused(newView)
|
||||
}
|
||||
|
||||
func (gui *Gui) returnFocus() error {
|
||||
gui.Mutexes.ViewStackMutex.Lock()
|
||||
defer gui.Mutexes.ViewStackMutex.Unlock()
|
||||
|
||||
if len(gui.State.ViewStack) <= 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
previousViewName := gui.State.ViewStack[len(gui.State.ViewStack)-2]
|
||||
previousView, err := gui.g.View(previousViewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.switchFocusAux(previousView)
|
||||
}
|
||||
|
||||
func (gui *Gui) removeViewFromStack(view *gocui.View) {
|
||||
gui.Mutexes.ViewStackMutex.Lock()
|
||||
defer gui.Mutexes.ViewStackMutex.Unlock()
|
||||
|
||||
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
|
||||
return viewName != view.Name()
|
||||
})
|
||||
}
|
||||
|
||||
// Not to be called directly. Use `switchFocus` instead
|
||||
func (gui *Gui) pushView(name string) {
|
||||
// No matter what view we're pushing, we first remove all popup panels from the stack
|
||||
// (unless it's the search view because we may be searching the menu panel)
|
||||
if name != "filter" {
|
||||
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
|
||||
return !gui.isPopupPanel(viewName)
|
||||
})
|
||||
}
|
||||
|
||||
// If we're pushing a side panel, we remove all other panels
|
||||
if lo.Contains(gui.sideViewNames(), name) {
|
||||
gui.State.ViewStack = []string{}
|
||||
}
|
||||
|
||||
// If we're pushing a panel that's already in the stack, we remove it
|
||||
gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool {
|
||||
return viewName != name
|
||||
})
|
||||
|
||||
gui.State.ViewStack = append(gui.State.ViewStack, name)
|
||||
}
|
||||
|
||||
// excludes popups
|
||||
func (gui *Gui) currentStaticViewName() string {
|
||||
gui.Mutexes.ViewStackMutex.Lock()
|
||||
defer gui.Mutexes.ViewStackMutex.Unlock()
|
||||
|
||||
for i := len(gui.State.ViewStack) - 1; i >= 0; i-- {
|
||||
if !lo.Contains(gui.popupViewNames(), gui.State.ViewStack[i]) {
|
||||
return gui.State.ViewStack[i]
|
||||
}
|
||||
}
|
||||
|
||||
return gui.initiallyFocusedViewName()
|
||||
}
|
||||
|
||||
func (gui *Gui) currentSideViewName() string {
|
||||
gui.Mutexes.ViewStackMutex.Lock()
|
||||
defer gui.Mutexes.ViewStackMutex.Unlock()
|
||||
|
||||
// we expect that there is a side window somewhere in the view stack, so we will search from top to bottom
|
||||
for idx := range gui.State.ViewStack {
|
||||
reversedIdx := len(gui.State.ViewStack) - 1 - idx
|
||||
viewName := gui.State.ViewStack[reversedIdx]
|
||||
if lo.Contains(gui.sideViewNames(), viewName) {
|
||||
return viewName
|
||||
}
|
||||
}
|
||||
|
||||
return gui.initiallyFocusedViewName()
|
||||
}
|
@ -1,112 +1,133 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
type CreateMenuOptions struct {
|
||||
Title string
|
||||
Items []*types.MenuItem
|
||||
HideCancel bool
|
||||
}
|
||||
|
||||
func (gui *Gui) getMenuPanel() *panels.SideListPanel[*types.MenuItem] {
|
||||
return &panels.SideListPanel[*types.MenuItem]{
|
||||
ListPanel: panels.ListPanel[*types.MenuItem]{
|
||||
List: panels.NewFilteredList[*types.MenuItem](),
|
||||
View: gui.Views.Menu,
|
||||
},
|
||||
NoItemsMessage: "",
|
||||
Gui: gui.intoInterface(),
|
||||
OnClick: gui.onMenuPress,
|
||||
Sort: nil,
|
||||
GetTableCells: presentation.GetMenuItemDisplayStrings,
|
||||
OnRerender: func() error {
|
||||
return gui.resizePopupPanel(gui.Views.Menu)
|
||||
},
|
||||
// so that we can avoid some UI trickiness, the menu will not have filtering
|
||||
// abillity yet. To support it, we would need to have filter state against
|
||||
// each panel (e.g. for when you filter the images panel, then bring up
|
||||
// the options menu, then try to filter that too.
|
||||
DisableFilter: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) onMenuPress(menuItem *types.MenuItem) error {
|
||||
if err := gui.handleMenuClose(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if menuItem.OnPress != nil {
|
||||
return menuItem.OnPress()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.focusY(gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Menu
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), false)
|
||||
func (gui *Gui) handleMenuPress() error {
|
||||
selectedMenuItem, err := gui.Panels.Menu.GetSelectedItem()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.handleMenuSelect(g, v)
|
||||
return gui.onMenuPress(selectedMenuItem)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuClick(g *gocui.Gui, v *gocui.View) error {
|
||||
itemCount := gui.State.MenuItemCount
|
||||
handleSelect := gui.handleMenuSelect
|
||||
selectedLine := &gui.State.Panels.Menu.SelectedLine
|
||||
func (gui *Gui) Menu(opts CreateMenuOptions) error {
|
||||
if !opts.HideCancel {
|
||||
// this is mutative but I'm okay with that for now
|
||||
opts.Items = append(opts.Items, &types.MenuItem{
|
||||
LabelColumns: []string{gui.Tr.Cancel},
|
||||
OnPress: func() error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err := gui.handleClick(v, itemCount, selectedLine, handleSelect); err != nil {
|
||||
return err
|
||||
maxColumnSize := 1
|
||||
|
||||
for _, item := range opts.Items {
|
||||
if item.LabelColumns == nil {
|
||||
item.LabelColumns = []string{item.Label}
|
||||
}
|
||||
|
||||
if item.OpensMenu {
|
||||
item.LabelColumns[0] = utils.OpensMenuStyle(item.LabelColumns[0])
|
||||
}
|
||||
|
||||
maxColumnSize = utils.Max(maxColumnSize, len(item.LabelColumns))
|
||||
}
|
||||
|
||||
return gui.State.Panels.Menu.OnPress(g, v)
|
||||
}
|
||||
for _, item := range opts.Items {
|
||||
if len(item.LabelColumns) < maxColumnSize {
|
||||
// we require that each item has the same number of columns so we're padding out with blank strings
|
||||
// if this item has too few
|
||||
item.LabelColumns = append(item.LabelColumns, make([]string, maxColumnSize-len(item.LabelColumns))...)
|
||||
}
|
||||
}
|
||||
|
||||
gui.Panels.Menu.SetItems(opts.Items)
|
||||
gui.Panels.Menu.SetSelectedLineIdx(0)
|
||||
|
||||
func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Menu
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true)
|
||||
if err := gui.Panels.Menu.RerenderList(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.Views.Menu.Title = opts.Title
|
||||
gui.Views.Menu.Visible = true
|
||||
|
||||
return gui.handleMenuSelect(g, v)
|
||||
return gui.switchFocus(gui.Views.Menu)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) renderMenuOptions() error {
|
||||
optionsMap := map[string]string{
|
||||
"esc/q": gui.Tr.Close,
|
||||
"esc": gui.Tr.Close,
|
||||
"↑ ↓": gui.Tr.Navigate,
|
||||
"enter": gui.Tr.Execute,
|
||||
}
|
||||
return gui.renderOptionsMap(optionsMap)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
|
||||
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
|
||||
if err := g.DeleteKeybinding("menu", key, gocui.ModNone); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
func (gui *Gui) handleMenuClose() error {
|
||||
gui.Views.Menu.Visible = false
|
||||
return gui.returnFocus()
|
||||
}
|
||||
|
||||
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
|
||||
isFocused := gui.g.CurrentView().Name() == "menu"
|
||||
gui.State.MenuItemCount = itemCount
|
||||
list, err := utils.RenderList(items, utils.IsFocused(isFocused))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
|
||||
_, _ = gui.g.SetView("menu", x0, y0, x1, y1, 0)
|
||||
menuView := gui.Views.Menu
|
||||
menuView.Title = title
|
||||
menuView.FgColor = gocui.ColorDefault
|
||||
menuView.Clear()
|
||||
fmt.Fprint(menuView, list)
|
||||
gui.State.Panels.Menu.SelectedLine = 0
|
||||
|
||||
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
|
||||
selectedLine := gui.State.Panels.Menu.SelectedLine
|
||||
|
||||
menuView.Visible = false
|
||||
err := gui.returnFocus()
|
||||
if err != nil {
|
||||
// this code is here for when we do add filter ability to the menu panel,
|
||||
// though it's currently disabled
|
||||
if gui.State.Filter.panel == gui.Panels.Menu {
|
||||
if err := gui.clearFilter(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := handlePress(selectedLine); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.Panels.Menu.OnPress = wrappedHandlePress
|
||||
|
||||
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
|
||||
_ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone)
|
||||
|
||||
if err := gui.g.SetKeybinding("menu", nil, key, gocui.ModNone, wrappedHandlePress); err != nil {
|
||||
return err
|
||||
}
|
||||
// we need to remove the view from the view stack because we're about to
|
||||
// return focus and don't want to land in the search view when it was searching
|
||||
// the menu in the first place
|
||||
gui.removeViewFromStack(gui.Views.Filter)
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
menuView.Visible = true
|
||||
return gui.switchFocus(menuView)
|
||||
})
|
||||
return nil
|
||||
return gui.returnFocus()
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package gui
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
|
||||
func (gui *Gui) intoInterface() panels.IGui {
|
||||
return gui
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package panels
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// A 'context' generally corresponds to an item and the tab in the main panel that we're
|
||||
// displaying. So if we switch to a new item, or change the tab in the panel panel
|
||||
// for the current item, we end up with a new context. When we have a new context,
|
||||
// we render new content to the main panel.
|
||||
type ContextState[T any] struct {
|
||||
// index of the currently selected tab in the main view.
|
||||
mainTabIdx int
|
||||
// this function returns the tabs that we can display for an item (the tabs
|
||||
// are shown on the main view)
|
||||
GetMainTabs func() []MainTab[T]
|
||||
// This tells us whether we need to re-render to the main panel for a given item.
|
||||
// This should include the item's ID and if you want to invalidate the cache for
|
||||
// some other reason, you can add that to the key as well (e.g. the container's state).
|
||||
GetItemContextCacheKey func(item T) string
|
||||
}
|
||||
|
||||
type MainTab[T any] struct {
|
||||
// key used as part of the context cache key
|
||||
Key string
|
||||
// title of the tab, rendered in the main view
|
||||
Title string
|
||||
// function to render the content of the tab
|
||||
Render func(item T) tasks.TaskFunc
|
||||
}
|
||||
|
||||
func (self *ContextState[T]) GetMainTabTitles() []string {
|
||||
return lo.Map(self.GetMainTabs(), func(tab MainTab[T], _ int) string {
|
||||
return tab.Title
|
||||
})
|
||||
}
|
||||
|
||||
func (self *ContextState[T]) GetCurrentContextKey(item T) string {
|
||||
return self.GetItemContextCacheKey(item) + "-" + self.GetCurrentMainTab().Key
|
||||
}
|
||||
|
||||
func (self *ContextState[T]) GetCurrentMainTab() MainTab[T] {
|
||||
return self.GetMainTabs()[self.mainTabIdx]
|
||||
}
|
||||
|
||||
func (self *ContextState[T]) HandleNextMainTab() {
|
||||
tabs := self.GetMainTabs()
|
||||
|
||||
if len(tabs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
self.mainTabIdx = (self.mainTabIdx + 1) % len(tabs)
|
||||
}
|
||||
|
||||
func (self *ContextState[T]) HandlePrevMainTab() {
|
||||
tabs := self.GetMainTabs()
|
||||
|
||||
if len(tabs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
self.mainTabIdx = (self.mainTabIdx - 1 + len(tabs)) % len(tabs)
|
||||
}
|
||||
|
||||
func (self *ContextState[T]) SetMainTabIndex(index int) {
|
||||
self.mainTabIdx = index
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package panels
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type FilteredList[T comparable] struct {
|
||||
allItems []T
|
||||
// indices of items in the allItems slice that are included in the filtered list
|
||||
indices []int
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewFilteredList[T comparable]() *FilteredList[T] {
|
||||
return &FilteredList[T]{}
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) SetItems(items []T) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
self.allItems = items
|
||||
self.indices = make([]int, len(items))
|
||||
for i := range self.indices {
|
||||
self.indices[i] = i
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) Filter(filter func(T, int) bool) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
self.indices = self.indices[:0]
|
||||
for i, item := range self.allItems {
|
||||
if filter(item, i) {
|
||||
self.indices = append(self.indices, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) Sort(less func(T, T) bool) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
if less == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sort.Slice(self.indices, func(i, j int) bool {
|
||||
return less(self.allItems[self.indices[i]], self.allItems[self.indices[j]])
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) Get(index int) T {
|
||||
self.mutex.RLock()
|
||||
defer self.mutex.RUnlock()
|
||||
|
||||
return self.allItems[self.indices[index]]
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) TryGet(index int) (T, bool) {
|
||||
self.mutex.RLock()
|
||||
defer self.mutex.RUnlock()
|
||||
|
||||
if index < 0 || index >= len(self.indices) {
|
||||
var zero T
|
||||
return zero, false
|
||||
}
|
||||
|
||||
return self.allItems[self.indices[index]], true
|
||||
}
|
||||
|
||||
// returns the length of the filtered list
|
||||
func (self *FilteredList[T]) Len() int {
|
||||
self.mutex.RLock()
|
||||
defer self.mutex.RUnlock()
|
||||
|
||||
return len(self.indices)
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) GetIndex(item T) int {
|
||||
self.mutex.RLock()
|
||||
defer self.mutex.RUnlock()
|
||||
|
||||
for i, index := range self.indices {
|
||||
if self.allItems[index] == item {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) GetItems() []T {
|
||||
self.mutex.RLock()
|
||||
defer self.mutex.RUnlock()
|
||||
|
||||
result := make([]T, len(self.indices))
|
||||
for i, index := range self.indices {
|
||||
result[i] = self.allItems[index]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) GetAllItems() []T {
|
||||
self.mutex.RLock()
|
||||
defer self.mutex.RUnlock()
|
||||
|
||||
return self.allItems
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
package panels
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilteredListGet(t *testing.T) {
|
||||
tests := []struct {
|
||||
f *FilteredList[int]
|
||||
args int
|
||||
want int
|
||||
}{
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
args: 1,
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
args: 2,
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||
args: 0,
|
||||
want: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := tt.f.Get(tt.args); got != tt.want {
|
||||
t.Errorf("FilteredList.Get() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteredListLen(t *testing.T) {
|
||||
tests := []struct {
|
||||
f *FilteredList[int]
|
||||
want int
|
||||
}{
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||
want: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := tt.f.Len(); got != tt.want {
|
||||
t.Errorf("FilteredList.Len() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteredListFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
f *FilteredList[int]
|
||||
args func(int, int) bool
|
||||
want *FilteredList[int]
|
||||
}{
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
args: func(i int, _ int) bool { return i%2 == 0 },
|
||||
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||
},
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
args: func(i int, _ int) bool { return i%2 == 1 },
|
||||
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 2}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt.f.Filter(tt.args)
|
||||
assert.EqualValues(t, tt.f.indices, tt.want.indices)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteredListSort(t *testing.T) {
|
||||
tests := []struct {
|
||||
f *FilteredList[int]
|
||||
args func(int, int) bool
|
||||
want *FilteredList[int]
|
||||
}{
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
args: func(i int, j int) bool { return i < j },
|
||||
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
},
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
args: func(i int, j int) bool { return i > j },
|
||||
want: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{2, 1, 0}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt.f.Sort(tt.args)
|
||||
assert.EqualValues(t, tt.f.indices, tt.want.indices)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteredListGetIndex(t *testing.T) {
|
||||
tests := []struct {
|
||||
f *FilteredList[int]
|
||||
args int
|
||||
want int
|
||||
}{
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
args: 1,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
args: 2,
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||
args: 0,
|
||||
want: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := tt.f.GetIndex(tt.args); got != tt.want {
|
||||
t.Errorf("FilteredList.GetIndex() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteredListGetItems(t *testing.T) {
|
||||
tests := []struct {
|
||||
f *FilteredList[int]
|
||||
want []int
|
||||
}{
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
want: []int{1, 2, 3},
|
||||
},
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||
want: []int{2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := tt.f.GetItems()
|
||||
assert.EqualValues(t, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteredListSetItems(t *testing.T) {
|
||||
tests := []struct {
|
||||
f *FilteredList[int]
|
||||
args []int
|
||||
want *FilteredList[int]
|
||||
}{
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{0, 1, 2}},
|
||||
args: []int{4, 5, 6},
|
||||
want: &FilteredList[int]{allItems: []int{4, 5, 6}, indices: []int{0, 1, 2}},
|
||||
},
|
||||
{
|
||||
f: &FilteredList[int]{allItems: []int{1, 2, 3}, indices: []int{1}},
|
||||
args: []int{4},
|
||||
want: &FilteredList[int]{allItems: []int{4}, indices: []int{0}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt.f.SetItems(tt.args)
|
||||
assert.EqualValues(t, tt.f.indices, tt.want.indices)
|
||||
assert.EqualValues(t, tt.f.allItems, tt.want.allItems)
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package panels
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
lcUtils "github.com/jesseduffield/lazycore/pkg/utils"
|
||||
)
|
||||
|
||||
type ListPanel[T comparable] struct {
|
||||
SelectedIdx int
|
||||
List *FilteredList[T]
|
||||
View *gocui.View
|
||||
}
|
||||
|
||||
func (self *ListPanel[T]) SetSelectedLineIdx(value int) {
|
||||
clampedValue := 0
|
||||
if self.List.Len() > 0 {
|
||||
clampedValue = lcUtils.Clamp(value, 0, self.List.Len()-1)
|
||||
}
|
||||
|
||||
self.SelectedIdx = clampedValue
|
||||
}
|
||||
|
||||
func (self *ListPanel[T]) clampSelectedLineIdx() {
|
||||
clamped := lcUtils.Clamp(self.SelectedIdx, 0, self.List.Len()-1)
|
||||
|
||||
if clamped != self.SelectedIdx {
|
||||
self.SelectedIdx = clamped
|
||||
}
|
||||
}
|
||||
|
||||
// moves the cursor up or down by the given amount (up for negative values)
|
||||
func (self *ListPanel[T]) moveSelectedLine(delta int) {
|
||||
self.SetSelectedLineIdx(self.SelectedIdx + delta)
|
||||
}
|
||||
|
||||
func (self *ListPanel[T]) SelectNextLine() {
|
||||
self.moveSelectedLine(1)
|
||||
}
|
||||
|
||||
func (self *ListPanel[T]) SelectPrevLine() {
|
||||
self.moveSelectedLine(-1)
|
||||
}
|
@ -0,0 +1,271 @@
|
||||
package panels
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type ISideListPanel interface {
|
||||
SetMainTabIndex(int)
|
||||
HandleSelect() error
|
||||
GetView() *gocui.View
|
||||
Refocus()
|
||||
RerenderList() error
|
||||
IsFilterDisabled() bool
|
||||
IsHidden() bool
|
||||
HandleNextLine() error
|
||||
HandlePrevLine() error
|
||||
HandleClick() error
|
||||
HandlePrevMainTab() error
|
||||
HandleNextMainTab() error
|
||||
}
|
||||
|
||||
// list panel at the side of the screen that renders content to the main panel
|
||||
type SideListPanel[T comparable] struct {
|
||||
ContextState *ContextState[T]
|
||||
|
||||
ListPanel[T]
|
||||
|
||||
// message to render in the main view if there are no items in the panel
|
||||
// and it has focus. Leave empty if you don't want to render anything
|
||||
NoItemsMessage string
|
||||
|
||||
// a representation of the gui
|
||||
Gui IGui
|
||||
|
||||
// this Filter is applied on top of additional default filters
|
||||
Filter func(T) bool
|
||||
Sort func(a, b T) bool
|
||||
|
||||
// a callback to invoke when the item is clicked
|
||||
OnClick func(T) error
|
||||
|
||||
// returns the cells that we render to the view in a table format. The cells will
|
||||
// be rendered with padding.
|
||||
GetTableCells func(T) []string
|
||||
|
||||
// function to be called after re-rendering list. Can be nil
|
||||
OnRerender func() error
|
||||
|
||||
// set this to true if you don't want to allow manual filtering via '/'
|
||||
DisableFilter bool
|
||||
|
||||
// This can be nil if you want to always show the panel
|
||||
Hide func() bool
|
||||
}
|
||||
|
||||
var _ ISideListPanel = &SideListPanel[int]{}
|
||||
|
||||
type IGui interface {
|
||||
HandleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func() error) error
|
||||
NewSimpleRenderStringTask(getContent func() string) tasks.TaskFunc
|
||||
FocusY(selectedLine int, itemCount int, view *gocui.View)
|
||||
ShouldRefresh(contextKey string) bool
|
||||
GetMainView() *gocui.View
|
||||
IsCurrentView(*gocui.View) bool
|
||||
FilterString(view *gocui.View) string
|
||||
IgnoreStrings() []string
|
||||
Update(func() error)
|
||||
|
||||
QueueTask(f func(ctx context.Context)) error
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) HandleClick() error {
|
||||
itemCount := self.List.Len()
|
||||
handleSelect := self.HandleSelect
|
||||
selectedLine := &self.SelectedIdx
|
||||
|
||||
if err := self.Gui.HandleClick(self.View, itemCount, selectedLine, handleSelect); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if self.OnClick != nil {
|
||||
selectedItem, err := self.GetSelectedItem()
|
||||
if err == nil {
|
||||
return self.OnClick(selectedItem)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) GetView() *gocui.View {
|
||||
return self.View
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) HandleSelect() error {
|
||||
item, err := self.GetSelectedItem()
|
||||
if err != nil {
|
||||
if err.Error() != self.NoItemsMessage {
|
||||
return err
|
||||
}
|
||||
|
||||
if self.NoItemsMessage != "" {
|
||||
self.Gui.NewSimpleRenderStringTask(func() string { return self.NoItemsMessage })
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
self.Refocus()
|
||||
|
||||
return self.renderContext(item)
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) renderContext(item T) error {
|
||||
if self.ContextState == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
key := self.ContextState.GetCurrentContextKey(item)
|
||||
if !self.Gui.ShouldRefresh(key) {
|
||||
return nil
|
||||
}
|
||||
|
||||
mainView := self.Gui.GetMainView()
|
||||
mainView.Tabs = self.ContextState.GetMainTabTitles()
|
||||
mainView.TabIndex = self.ContextState.mainTabIdx
|
||||
|
||||
task := self.ContextState.GetCurrentMainTab().Render(item)
|
||||
|
||||
return self.Gui.QueueTask(task)
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) GetSelectedItem() (T, error) {
|
||||
var zero T
|
||||
|
||||
item, ok := self.List.TryGet(self.SelectedIdx)
|
||||
if !ok {
|
||||
// could probably have a better error here
|
||||
return zero, errors.New(self.NoItemsMessage)
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) HandleNextLine() error {
|
||||
self.SelectNextLine()
|
||||
|
||||
return self.HandleSelect()
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) HandlePrevLine() error {
|
||||
self.SelectPrevLine()
|
||||
|
||||
return self.HandleSelect()
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) HandleNextMainTab() error {
|
||||
if self.ContextState == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.ContextState.HandleNextMainTab()
|
||||
|
||||
return self.HandleSelect()
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) HandlePrevMainTab() error {
|
||||
if self.ContextState == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.ContextState.HandlePrevMainTab()
|
||||
|
||||
return self.HandleSelect()
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) Refocus() {
|
||||
self.Gui.FocusY(self.SelectedIdx, self.List.Len(), self.View)
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) SetItems(items []T) {
|
||||
self.List.SetItems(items)
|
||||
self.FilterAndSort()
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) FilterAndSort() {
|
||||
filterString := self.Gui.FilterString(self.View)
|
||||
|
||||
self.List.Filter(func(item T, index int) bool {
|
||||
if self.Filter != nil && !self.Filter(item) {
|
||||
return false
|
||||
}
|
||||
|
||||
if lo.SomeBy(self.Gui.IgnoreStrings(), func(ignore string) bool {
|
||||
return lo.SomeBy(self.GetTableCells(item), func(searchString string) bool {
|
||||
return strings.Contains(searchString, ignore)
|
||||
})
|
||||
}) {
|
||||
return false
|
||||
}
|
||||
|
||||
if filterString != "" {
|
||||
return lo.SomeBy(self.GetTableCells(item), func(searchString string) bool {
|
||||
return strings.Contains(searchString, filterString)
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
self.List.Sort(self.Sort)
|
||||
|
||||
self.clampSelectedLineIdx()
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) RerenderList() error {
|
||||
self.FilterAndSort()
|
||||
|
||||
self.Gui.Update(func() error {
|
||||
self.View.Clear()
|
||||
table := lo.Map(self.List.GetItems(), func(item T, index int) []string {
|
||||
return self.GetTableCells(item)
|
||||
})
|
||||
renderedTable, err := utils.RenderTable(table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(self.View, renderedTable)
|
||||
|
||||
if self.OnRerender != nil {
|
||||
if err := self.OnRerender(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if self.Gui.IsCurrentView(self.View) {
|
||||
return self.HandleSelect()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) SetMainTabIndex(index int) {
|
||||
if self.ContextState == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.ContextState.SetMainTabIndex(index)
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) IsFilterDisabled() bool {
|
||||
return self.DisableFilter
|
||||
}
|
||||
|
||||
func (self *SideListPanel[T]) IsHidden() bool {
|
||||
if self.Hide == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return self.Hide()
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/asciigraph"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/mcuadros/go-lookup"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func RenderStats(userConfig *config.UserConfig, container *commands.Container, viewWidth int) (string, error) {
|
||||
stats, ok := container.GetLastStats()
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
graphSpecs := userConfig.Stats.Graphs
|
||||
graphs := make([]string, len(graphSpecs))
|
||||
for i, spec := range graphSpecs {
|
||||
graph, err := plotGraph(container, spec, viewWidth-10)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
graphs[i] = utils.ColoredString(graph, utils.GetColorAttribute(spec.Color))
|
||||
}
|
||||
|
||||
pidsCount := fmt.Sprintf("PIDs: %d", stats.ClientStats.PidsStats.Current)
|
||||
dataReceived := fmt.Sprintf("Traffic received: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.RxBytes))
|
||||
dataSent := fmt.Sprintf("Traffic sent: %s", utils.FormatDecimalBytes(stats.ClientStats.Networks.Eth0.TxBytes))
|
||||
|
||||
originalJSON, err := json.MarshalIndent(stats, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
contents := fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n%s\n\n%s",
|
||||
utils.ColoredString(strings.Join(graphs, "\n\n"), color.FgGreen),
|
||||
pidsCount,
|
||||
dataReceived,
|
||||
dataSent,
|
||||
string(originalJSON),
|
||||
)
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
// plotGraph returns the plotted graph based on the graph spec and the stat history
|
||||
func plotGraph(container *commands.Container, spec config.GraphConfig, width int) (string, error) {
|
||||
container.StatsMutex.Lock()
|
||||
defer container.StatsMutex.Unlock()
|
||||
|
||||
data := make([]float64, len(container.StatHistory))
|
||||
|
||||
for i, stats := range container.StatHistory {
|
||||
value, err := lookup.LookupString(stats, spec.StatPath)
|
||||
if err != nil {
|
||||
return "Could not find key: " + spec.StatPath, nil
|
||||
}
|
||||
floatValue, err := getFloat(value.Interface())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data[i] = floatValue
|
||||
}
|
||||
|
||||
max := spec.Max
|
||||
if spec.MaxType == "" {
|
||||
max = lo.Max(data)
|
||||
}
|
||||
|
||||
min := spec.Min
|
||||
if spec.MinType == "" {
|
||||
min = lo.Min(data)
|
||||
}
|
||||
|
||||
height := 10
|
||||
if spec.Height > 0 {
|
||||
height = spec.Height
|
||||
}
|
||||
|
||||
caption := fmt.Sprintf(
|
||||
"%s: %0.2f (%v)",
|
||||
spec.Caption,
|
||||
data[len(data)-1],
|
||||
time.Since(container.StatHistory[0].RecordedAt).Round(time.Second),
|
||||
)
|
||||
|
||||
return asciigraph.Plot(
|
||||
data,
|
||||
asciigraph.Height(height),
|
||||
asciigraph.Width(width),
|
||||
asciigraph.Min(min),
|
||||
asciigraph.Max(max),
|
||||
asciigraph.Caption(caption),
|
||||
), nil
|
||||
}
|
||||
|
||||
// from Dave C's answer at https://stackoverflow.com/questions/20767724/converting-unknown-interface-to-float64-in-golang
|
||||
func getFloat(unk interface{}) (float64, error) {
|
||||
floatType := reflect.TypeOf(float64(0))
|
||||
stringType := reflect.TypeOf("")
|
||||
|
||||
switch i := unk.(type) {
|
||||
case float64:
|
||||
return i, nil
|
||||
case float32:
|
||||
return float64(i), nil
|
||||
case int64:
|
||||
return float64(i), nil
|
||||
case int32:
|
||||
return float64(i), nil
|
||||
case int:
|
||||
return float64(i), nil
|
||||
case uint64:
|
||||
return float64(i), nil
|
||||
case uint32:
|
||||
return float64(i), nil
|
||||
case uint:
|
||||
return float64(i), nil
|
||||
case string:
|
||||
return strconv.ParseFloat(i, 64)
|
||||
default:
|
||||
v := reflect.ValueOf(unk)
|
||||
v = reflect.Indirect(v)
|
||||
if v.Type().ConvertibleTo(floatType) {
|
||||
fv := v.Convert(floatType)
|
||||
return fv.Float(), nil
|
||||
} else if v.Type().ConvertibleTo(stringType) {
|
||||
sv := v.Convert(stringType)
|
||||
s := sv.String()
|
||||
return strconv.ParseFloat(s, 64)
|
||||
} else {
|
||||
return math.NaN(), fmt.Errorf("Can't convert %v to float64", v.Type())
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func GetContainerDisplayStrings(container *commands.Container) []string {
|
||||
return []string{
|
||||
getContainerDisplayStatus(container),
|
||||
getContainerDisplaySubstatus(container),
|
||||
container.Name,
|
||||
getDisplayCPUPerc(container),
|
||||
utils.ColoredString(displayPorts(container), color.FgYellow),
|
||||
utils.ColoredString(displayContainerImage(container), color.FgMagenta),
|
||||
}
|
||||
}
|
||||
|
||||
func displayContainerImage(container *commands.Container) string {
|
||||
return strings.TrimPrefix(container.Container.Image, "sha256:")
|
||||
}
|
||||
|
||||
func displayPorts(c *commands.Container) string {
|
||||
portStrings := lo.Map(c.Container.Ports, func(port dockerTypes.Port, _ int) string {
|
||||
if port.PublicPort == 0 {
|
||||
return fmt.Sprintf("%d/%s", port.PrivatePort, port.Type)
|
||||
}
|
||||
|
||||
// docker ps will show '0.0.0.0:80->80/tcp' but we'll show
|
||||
// '80->80/tcp' instead to save space (unless the IP is something other than
|
||||
// 0.0.0.0)
|
||||
ipString := ""
|
||||
if port.IP != "0.0.0.0" {
|
||||
ipString = port.IP + ":"
|
||||
}
|
||||
return fmt.Sprintf("%s%d->%d/%s", ipString, port.PublicPort, port.PrivatePort, port.Type)
|
||||
})
|
||||
|
||||
// sorting because the order of the ports is not deterministic
|
||||
// and we don't want to have them constantly swapping
|
||||
sort.Strings(portStrings)
|
||||
|
||||
return strings.Join(portStrings, ", ")
|
||||
}
|
||||
|
||||
// getContainerDisplayStatus returns the colored status of the container
|
||||
func getContainerDisplayStatus(c *commands.Container) string {
|
||||
return utils.ColoredString(c.Container.State, getContainerColor(c))
|
||||
}
|
||||
|
||||
// GetDisplayStatus returns the exit code if the container has exited, and the health status if the container is running (and has a health check)
|
||||
func getContainerDisplaySubstatus(c *commands.Container) string {
|
||||
if !c.DetailsLoaded() {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch c.Container.State {
|
||||
case "exited":
|
||||
return utils.ColoredString(
|
||||
fmt.Sprintf("(%s)", strconv.Itoa(c.Details.State.ExitCode)), getContainerColor(c),
|
||||
)
|
||||
case "running":
|
||||
return getHealthStatus(c)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getHealthStatus(c *commands.Container) string {
|
||||
if !c.DetailsLoaded() {
|
||||
return ""
|
||||
}
|
||||
|
||||
healthStatusColorMap := map[string]color.Attribute{
|
||||
"healthy": color.FgGreen,
|
||||
"unhealthy": color.FgRed,
|
||||
"starting": color.FgYellow,
|
||||
}
|
||||
|
||||
if c.Details.State.Health == nil {
|
||||
return ""
|
||||
}
|
||||
healthStatus := c.Details.State.Health.Status
|
||||
if healthStatusColor, ok := healthStatusColorMap[healthStatus]; ok {
|
||||
return utils.ColoredString(fmt.Sprintf("(%s)", healthStatus), healthStatusColor)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getDisplayCPUPerc colors the cpu percentage based on how extreme it is
|
||||
func getDisplayCPUPerc(c *commands.Container) string {
|
||||
stats, ok := c.GetLastStats()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
percentage := stats.DerivedStats.CPUPercentage
|
||||
formattedPercentage := fmt.Sprintf("%.2f%%", stats.DerivedStats.CPUPercentage)
|
||||
|
||||
var clr color.Attribute
|
||||
if percentage > 90 {
|
||||
clr = color.FgRed
|
||||
} else if percentage > 50 {
|
||||
clr = color.FgYellow
|
||||
} else {
|
||||
clr = color.FgWhite
|
||||
}
|
||||
|
||||
return utils.ColoredString(formattedPercentage, clr)
|
||||
}
|
||||
|
||||
// getContainerColor Container color
|
||||
func getContainerColor(c *commands.Container) color.Attribute {
|
||||
switch c.Container.State {
|
||||
case "exited":
|
||||
// This means the colour may be briefly yellow and then switch to red upon starting
|
||||
// Not sure what a better alternative is.
|
||||
if !c.DetailsLoaded() || c.Details.State.ExitCode == 0 {
|
||||
return color.FgYellow
|
||||
}
|
||||
return color.FgRed
|
||||
case "created":
|
||||
return color.FgCyan
|
||||
case "running":
|
||||
return color.FgGreen
|
||||
case "paused":
|
||||
return color.FgYellow
|
||||
case "dead":
|
||||
return color.FgRed
|
||||
case "restarting":
|
||||
return color.FgBlue
|
||||
case "removing":
|
||||
return color.FgMagenta
|
||||
default:
|
||||
return color.FgWhite
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
)
|
||||
|
||||
func GetImageDisplayStrings(image *commands.Image) []string {
|
||||
return []string{
|
||||
image.Name,
|
||||
image.Tag,
|
||||
utils.FormatDecimalBytes(int(image.Image.Size)),
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package presentation
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
|
||||
func GetMenuItemDisplayStrings(menuItem *types.MenuItem) []string {
|
||||
return menuItem.LabelColumns
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package presentation
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
|
||||
func GetProjectDisplayStrings(project *commands.Project) []string {
|
||||
return []string{project.Name}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
)
|
||||
|
||||
func GetServiceDisplayStrings(service *commands.Service) []string {
|
||||
if service.Container == nil {
|
||||
return []string{
|
||||
utils.ColoredString("none", color.FgBlue),
|
||||
"",
|
||||
service.Name,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
}
|
||||
}
|
||||
|
||||
container := service.Container
|
||||
return []string{
|
||||
getContainerDisplayStatus(container),
|
||||
getContainerDisplaySubstatus(container),
|
||||
service.Name,
|
||||
getDisplayCPUPerc(container),
|
||||
utils.ColoredString(displayPorts(container), color.FgYellow),
|
||||
utils.ColoredString(displayContainerImage(container), color.FgMagenta),
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package presentation
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
|
||||
func GetVolumeDisplayStrings(volume *commands.Volume) []string {
|
||||
return []string{volume.Volume.Driver, volume.Name}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func sampleContainers() []*commands.Container {
|
||||
return []*commands.Container{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: dockerTypes.Container{
|
||||
State: "exited",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: dockerTypes.Container{
|
||||
State: "created",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func expectedPerStatusContainers() []*commands.Container {
|
||||
return []*commands.Container{
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: dockerTypes.Container{
|
||||
State: "exited",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: dockerTypes.Container{
|
||||
State: "created",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func expectedLegacySortedContainers() []*commands.Container {
|
||||
return []*commands.Container{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: dockerTypes.Container{
|
||||
State: "exited",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: dockerTypes.Container{
|
||||
State: "created",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqualContainers(t *testing.T, left *commands.Container, right *commands.Container) {
|
||||
t.Helper()
|
||||
assert.Equal(t, left.Container.State, right.Container.State)
|
||||
assert.Equal(t, left.Container.ID, right.Container.ID)
|
||||
assert.Equal(t, left.Name, right.Name)
|
||||
}
|
||||
|
||||
func TestSortContainers(t *testing.T) {
|
||||
actual := sampleContainers()
|
||||
|
||||
expected := expectedPerStatusContainers()
|
||||
|
||||
sort.Slice(actual, func(i, j int) bool {
|
||||
return sortContainers(actual[i], actual[j], false)
|
||||
})
|
||||
|
||||
assert.Equal(t, len(actual), len(expected))
|
||||
|
||||
for i := 0; i < len(actual); i++ {
|
||||
assertEqualContainers(t, expected[i], actual[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacySortedContainers(t *testing.T) {
|
||||
actual := sampleContainers()
|
||||
|
||||
expected := expectedLegacySortedContainers()
|
||||
|
||||
sort.Slice(actual, func(i, j int) bool {
|
||||
return sortContainers(actual[i], actual[j], true)
|
||||
})
|
||||
|
||||
assert.Equal(t, len(actual), len(expected))
|
||||
|
||||
for i := 0; i < len(actual); i++ {
|
||||
assertEqualContainers(t, expected[i], actual[i])
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
)
|
||||
|
||||
func (gui *Gui) QueueTask(f func(ctx context.Context)) error {
|
||||
return gui.taskManager.NewTask(f)
|
||||
}
|
||||
|
||||
type RenderStringTaskOpts struct {
|
||||
Autoscroll bool
|
||||
Wrap bool
|
||||
GetStrContent func() string
|
||||
}
|
||||
|
||||
type TaskOpts struct {
|
||||
Autoscroll bool
|
||||
Wrap bool
|
||||
Func func(ctx context.Context)
|
||||
}
|
||||
|
||||
type TickerTaskOpts struct {
|
||||
Duration time.Duration
|
||||
Before func(ctx context.Context)
|
||||
Func func(ctx context.Context, notifyStopped chan struct{})
|
||||
Autoscroll bool
|
||||
Wrap bool
|
||||
}
|
||||
|
||||
func (gui *Gui) NewRenderStringTask(opts RenderStringTaskOpts) tasks.TaskFunc {
|
||||
taskOpts := TaskOpts{
|
||||
Autoscroll: opts.Autoscroll,
|
||||
Wrap: opts.Wrap,
|
||||
Func: func(ctx context.Context) {
|
||||
gui.RenderStringMain(opts.GetStrContent())
|
||||
},
|
||||
}
|
||||
|
||||
return gui.NewTask(taskOpts)
|
||||
}
|
||||
|
||||
// assumes it's cheap to obtain the content (otherwise we would pass a function that returns the content)
|
||||
func (gui *Gui) NewSimpleRenderStringTask(getContent func() string) tasks.TaskFunc {
|
||||
return gui.NewRenderStringTask(RenderStringTaskOpts{
|
||||
GetStrContent: getContent,
|
||||
Autoscroll: false,
|
||||
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) NewTask(opts TaskOpts) tasks.TaskFunc {
|
||||
return func(ctx context.Context) {
|
||||
mainView := gui.Views.Main
|
||||
mainView.Autoscroll = opts.Autoscroll
|
||||
mainView.Wrap = opts.Wrap
|
||||
|
||||
opts.Func(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTickerTask is a convenience function for making a new task that repeats some action once per e.g. second
|
||||
// the before function gets called after the lock is obtained, but before the ticker starts.
|
||||
// if you handle a message on the stop channel in f() you need to send a message on the notifyStopped channel because returning is not sufficient. Here, unlike in a regular task, simply returning means we're now going to wait till the next tick to run again.
|
||||
func (gui *Gui) NewTickerTask(opts TickerTaskOpts) tasks.TaskFunc {
|
||||
notifyStopped := make(chan struct{}, 10)
|
||||
|
||||
task := func(ctx context.Context) {
|
||||
if opts.Before != nil {
|
||||
opts.Before(ctx)
|
||||
}
|
||||
tickChan := time.NewTicker(opts.Duration)
|
||||
defer tickChan.Stop()
|
||||
// calling f first so that we're not waiting for the first tick
|
||||
opts.Func(ctx, notifyStopped)
|
||||
for {
|
||||
select {
|
||||
case <-notifyStopped:
|
||||
gui.Log.Info("exiting ticker task due to notifyStopped channel")
|
||||
return
|
||||
case <-ctx.Done():
|
||||
gui.Log.Info("exiting ticker task due to stopped channel")
|
||||
return
|
||||
case <-tickChan.C:
|
||||
gui.Log.Info("running ticker task again")
|
||||
opts.Func(ctx, notifyStopped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
taskOpts := TaskOpts{
|
||||
Autoscroll: opts.Autoscroll,
|
||||
Wrap: opts.Wrap,
|
||||
Func: task,
|
||||
}
|
||||
|
||||
return gui.NewTask(taskOpts)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package types
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
|
||||
// alternative to Label. Allows specifying columns which will be auto-aligned
|
||||
LabelColumns []string
|
||||
|
||||
OnPress func() error
|
||||
|
||||
// Only applies when Label is used
|
||||
OpensMenu bool
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// This wraps a writer and ensures that before we actually write anything we call a given function first
|
||||
|
||||
type OnceWriter struct {
|
||||
writer io.Writer
|
||||
once sync.Once
|
||||
f func()
|
||||
}
|
||||
|
||||
var _ io.Writer = &OnceWriter{}
|
||||
|
||||
func NewOnceWriter(writer io.Writer, f func()) *OnceWriter {
|
||||
return &OnceWriter{
|
||||
writer: writer,
|
||||
f: f,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *OnceWriter) Write(p []byte) (n int, err error) {
|
||||
self.once.Do(func() {
|
||||
self.f()
|
||||
})
|
||||
|
||||
return self.writer.Write(p)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.13.x
|
||||
- tip
|
||||
|
||||
before_install:
|
||||
- go get -t -v ./...
|
||||
|
||||
script:
|
||||
- go generate
|
||||
- git diff --cached --exit-code
|
||||
- ./go.test.sh
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
@ -0,0 +1,4 @@
|
||||
*~
|
||||
*.test
|
||||
.*.swp
|
||||
.DS_Store
|
@ -0,0 +1,11 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.3.x
|
||||
- 1.4.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- master
|
@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
@ -0,0 +1,5 @@
|
||||
# goid [![Build Status](https://travis-ci.org/petermattis/goid.svg?branch=master)](https://travis-ci.org/petermattis/goid)
|
||||
|
||||
Programatically retrieve the current goroutine's ID. See [the CI
|
||||
configuration](.travis.yml) for supported Go versions. In addition,
|
||||
gccgo 7.2.1 (Go 1.8.3) is supported.
|
@ -0,0 +1,35 @@
|
||||
// Copyright 2016 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
package goid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func ExtractGID(s []byte) int64 {
|
||||
s = s[len("goroutine "):]
|
||||
s = s[:bytes.IndexByte(s, ' ')]
|
||||
gid, _ := strconv.ParseInt(string(s), 10, 64)
|
||||
return gid
|
||||
}
|
||||
|
||||
// Parse the goid from runtime.Stack() output. Slow, but it works.
|
||||
func getSlow() int64 {
|
||||
var buf [64]byte
|
||||
return ExtractGID(buf[:runtime.Stack(buf[:], false)])
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
// Copyright 2018 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
// +build gccgo
|
||||
|
||||
package goid
|
||||
|
||||
//extern runtime.getg
|
||||
func getg() *g
|
||||
|
||||
func Get() int64 {
|
||||
return getg().goid
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// Copyright 2015 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
// +build !go1.4
|
||||
|
||||
#include <runtime.h>
|
||||
|
||||
void ·Get(int64 ret) {
|
||||
ret = g->goid;
|
||||
USED(&ret);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
// Copyright 2015 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
// +build !go1.4
|
||||
|
||||
package goid
|
||||
|
||||
// Get returns the id of the current goroutine.
|
||||
func Get() int64
|
@ -0,0 +1,34 @@
|
||||
// Copyright 2015 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
// +build go1.4,!go1.5
|
||||
|
||||
package goid
|
||||
|
||||
import "unsafe"
|
||||
|
||||
var pointerSize = unsafe.Sizeof(uintptr(0))
|
||||
|
||||
// Backdoor access to runtime·getg().
|
||||
func getg() uintptr // in goid_go1.4.s
|
||||
|
||||
// Get returns the id of the current goroutine.
|
||||
func Get() int64 {
|
||||
// The goid is the 16th field in the G struct where each field is a
|
||||
// pointer, uintptr or padded to that size. See runtime.h from the
|
||||
// Go sources. I'm not aware of a cleaner way to determine the
|
||||
// offset.
|
||||
return *(*int64)(unsafe.Pointer(getg() + 16*pointerSize))
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Assembly to get into package runtime without using exported symbols.
|
||||
// See https://github.com/golang/go/blob/release-branch.go1.4/misc/cgo/test/backdoor/thunk.s
|
||||
|
||||
// +build amd64 amd64p32 arm 386
|
||||
// +build go1.4,!go1.5
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
#ifdef GOARCH_arm
|
||||
#define JMP B
|
||||
#endif
|
||||
|
||||
TEXT ·getg(SB),NOSPLIT,$0-0
|
||||
JMP runtime·getg(SB)
|
@ -0,0 +1,21 @@
|
||||
// Copyright 2016 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
// +build amd64 amd64p32
|
||||
// +build gc,go1.5
|
||||
|
||||
package goid
|
||||
|
||||
func Get() int64
|
@ -0,0 +1,29 @@
|
||||
// Copyright 2016 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
// Assembly to mimic runtime.getg.
|
||||
|
||||
// +build amd64 amd64p32
|
||||
// +build gc,go1.5
|
||||
|
||||
#include "go_asm.h"
|
||||
#include "textflag.h"
|
||||
|
||||
// func Get() int64
|
||||
TEXT ·Get(SB),NOSPLIT,$0-8
|
||||
MOVQ (TLS), R14
|
||||
MOVQ g_goid(R14), R13
|
||||
MOVQ R13, ret+0(FP)
|
||||
RET
|
@ -0,0 +1,26 @@
|
||||
// Copyright 2016 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
// +build arm
|
||||
// +build gc,go1.5
|
||||
|
||||
package goid
|
||||
|
||||
// Backdoor access to runtime·getg().
|
||||
func getg() *g // in goid_go1.5plus.s
|
||||
|
||||
func Get() int64 {
|
||||
return getg().goid
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
// Copyright 2016 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
// Assembly to mimic runtime.getg.
|
||||
// This should work on arm64 as well, but it hasn't been tested.
|
||||
|
||||
// +build arm
|
||||
// +build gc,go1.5
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func getg() *g
|
||||
TEXT ·getg(SB),NOSPLIT,$0-8
|
||||
MOVW g, ret+0(FP)
|
||||
RET
|
@ -0,0 +1,23 @@
|
||||
// Copyright 2016 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
// +build go1.4,!go1.5,!amd64,!amd64p32,!arm,!386 go1.5,!go1.6,!amd64,!amd64p32,!arm go1.6,!amd64,!amd64p32,!arm go1.9,!amd64,!amd64p32,!arm
|
||||
|
||||
package goid
|
||||
|
||||
// Get returns the id of the current goroutine.
|
||||
func Get() int64 {
|
||||
return getSlow()
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// +build gccgo,go1.8
|
||||
|
||||
package goid
|
||||
|
||||
// https://github.com/gcc-mirror/gcc/blob/gcc-7-branch/libgo/go/runtime/runtime2.go#L329-L422
|
||||
|
||||
type g struct {
|
||||
_panic uintptr
|
||||
_defer uintptr
|
||||
m uintptr
|
||||
syscallsp uintptr
|
||||
syscallpc uintptr
|
||||
param uintptr
|
||||
atomicstatus uint32
|
||||
goid int64 // Here it is!
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
// Copyright 2016 Peter Mattis.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License. See the AUTHORS file
|
||||
// for names of contributors.
|
||||
|
||||
// +build go1.5,!go1.6
|
||||
|
||||
package goid
|
||||
|
||||
// Just enough of the structs from runtime/runtime2.go to get the offset to goid.
|
||||
// See https://github.com/golang/go/blob/release-branch.go1.5/src/runtime/runtime2.go
|
||||
|
||||
type stack struct {
|
||||
lo uintptr
|
||||
hi uintptr
|
||||
}
|
||||
|
||||
type gobuf struct {
|
||||
sp uintptr
|
||||
pc uintptr
|
||||
g uintptr
|
||||
ctxt uintptr
|
||||
ret uintptr
|
||||
lr uintptr
|
||||
bp uintptr
|
||||
}
|
||||
|
||||
type g struct {
|
||||
stack stack
|
||||
stackguard0 uintptr
|
||||
stackguard1 uintptr
|
||||
|
||||
_panic uintptr
|
||||
_defer uintptr
|
||||
m uintptr
|
||||
stackAlloc uintptr
|
||||
sched gobuf
|
||||
syscallsp uintptr
|
||||
syscallpc uintptr
|
||||
stkbar []uintptr
|
||||
stkbarPos uintptr
|
||||
param uintptr
|
||||
atomicstatus uint32
|
||||
stackLock uint32
|
||||
goid int64 // Here it is!
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
// +build gc,go1.6,!go1.9
|
||||
|
||||
package goid
|
||||
|
||||
// Just enough of the structs from runtime/runtime2.go to get the offset to goid.
|
||||
// See https://github.com/golang/go/blob/release-branch.go1.6/src/runtime/runtime2.go
|
||||
|
||||
type stack struct {
|
||||
lo uintptr
|
||||
hi uintptr
|
||||
}
|
||||
|
||||
type gobuf struct {
|
||||
sp uintptr
|
||||
pc uintptr
|
||||
g uintptr
|
||||
ctxt uintptr
|
||||
ret uintptr
|
||||
lr uintptr
|
||||
bp uintptr
|
||||
}
|
||||
|
||||
type g struct {
|
||||
stack stack
|
||||
stackguard0 uintptr
|
||||
stackguard1 uintptr
|
||||
|
||||
_panic uintptr
|
||||
_defer uintptr
|
||||
m uintptr
|
||||
stackAlloc uintptr
|
||||
sched gobuf
|
||||
syscallsp uintptr
|
||||
syscallpc uintptr
|
||||
stkbar []uintptr
|
||||
stkbarPos uintptr
|
||||
stktopsp uintptr
|
||||
param uintptr
|
||||
atomicstatus uint32
|
||||
stackLock uint32
|
||||
goid int64 // Here it is!
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
// +build gc,go1.9
|
||||
|
||||
package goid
|
||||
|
||||
type stack struct {
|
||||
lo uintptr
|
||||
hi uintptr
|
||||
}
|
||||
|
||||
type gobuf struct {
|
||||
sp uintptr
|
||||
pc uintptr
|
||||
g uintptr
|
||||
ctxt uintptr
|
||||
ret uintptr
|
||||
lr uintptr
|
||||
bp uintptr
|
||||
}
|
||||
|
||||
type g struct {
|
||||
stack stack
|
||||
stackguard0 uintptr
|
||||
stackguard1 uintptr
|
||||
|
||||
_panic uintptr
|
||||
_defer uintptr
|
||||
m uintptr
|
||||
sched gobuf
|
||||
syscallsp uintptr
|
||||
syscallpc uintptr
|
||||
stktopsp uintptr
|
||||
param uintptr
|
||||
atomicstatus uint32
|
||||
stackLock uint32
|
||||
goid int64 // Here it is!
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue