<commit_before>// Package reactionsmenu provides a reactions menu component.
package reactionsmenu

import (
	"context"
	"fmt"
	"log"
	"strings"

	"github.com/gopherjs/gopherjs/js"
	"github.com/shurcooL/component"
	"github.com/shurcooL/go/gopherjs_http/jsutil"
	homecomponent "github.com/shurcooL/home/component"
	"github.com/shurcooL/htmlg"
	"github.com/shurcooL/reactions"
	reactionscomponent "github.com/shurcooL/reactions/component"
	"github.com/shurcooL/users"
	"honnef.co/go/js/dom"
)

var document = dom.GetWindow().Document().(dom.HTMLDocument)

var rm reactionsMenu

type reactionsMenu struct {
	reactableURI      string
	reactionsService  reactions.Service
	authenticatedUser users.User

	menu    *dom.HTMLDivElement
	filter  *dom.HTMLInputElement
	results *dom.HTMLDivElement

	filtered []string

	// From last Show, needed to rerender reactableContainer after toggling a reaction.
	reactableID        string
	reactableContainer dom.Element
}

// Setup sets up the reaction menu on the current page.
// It must be called exactly once when document.Body() already exists.
func Setup(reactableURI string, reactionsService reactions.Service, authenticatedUser users.User) {
	rm = reactionsMenu{
		reactableURI:      reactableURI,
		reactionsService:  reactionsService,
		authenticatedUser: authenticatedUser,
	}

	js.Global.Set("ShowReactionMenu", jsutil.Wrap(rm.Show))
	js.Global.Set("ToggleReaction", jsutil.Wrap(rm.ToggleReaction))

	rm.menu = document.CreateElement("div").(*dom.HTMLDivElement)
	rm.menu.SetID("rm-reactions-menu")

	container := document.CreateElement("div").(*dom.HTMLDivElement)
	container.SetClass("rm-reactions-menu-container")
	rm.menu.AppendChild(container)

	// Disable for unauthenticated user.
	if rm.authenticatedUser.ID == 0 {
		disabled := document.CreateElement("div").(*dom.HTMLDivElement)
		disabled.SetClass("rm-reactions-menu-disabled")
		signIn := document.CreateElement("div").(*dom.HTMLDivElement)
		signIn.SetClass("rm-reactions-menu-signin")
		returnURL := dom.GetWindow().Location().Pathname + dom.GetWindow().Location().Search
		signIn.SetInnerHTML(htmlg.RenderComponentsString(homecomponent.PostButton{Action: "/login/github", Text: "Sign in via GitHub", ReturnURL: returnURL}, component.Text(" to react.")))
		disabled.AppendChild(signIn)
		container.AppendChild(disabled)
	}

	rm.filter = document.CreateElement("input").(*dom.HTMLInputElement)
	rm.filter.SetClass("rm-reactions-filter")
	rm.filter.Placeholder = "Search"
	rm.menu.AddEventListener("click", false, func(event dom.Event) {
		if rm.authenticatedUser.ID != 0 {
			rm.filter.Focus()
		}
	})
	container.AppendChild(rm.filter)
	rm.results = document.CreateElement("div").(*dom.HTMLDivElement)
	rm.results.SetClass("rm-reactions-results")
	rm.results.AddEventListener("click", false, func(event dom.Event) {
		me := event.(*dom.MouseEvent)
		x := (me.ClientX - int(rm.results.GetBoundingClientRect().Left) + rm.results.Underlying().Get("scrollLeft").Int()) / 30
		if x >= 9 {
			return // Out of bounds to the right, likely because of scrollbar.
		}
		y := (me.ClientY - int(rm.results.GetBoundingClientRect().Top) + rm.results.Underlying().Get("scrollTop").Int()) / 30
		i := y*9 + x
		if i < 0 || i >= len(rm.filtered) {
			return
		}
		emojiID := rm.filtered[i]
		go func() {
			reactions, err := rm.reactionsService.Toggle(context.TODO(), rm.reactableURI, rm.reactableID, reactions.ToggleRequest{Reaction: reactions.EmojiID(strings.Trim(emojiID, ":"))})
			if err != nil {
				log.Println(err)
				return
			}
			inner := reactionscomponent.ReactionsBarInner{
				Reactions:   reactions,
				CurrentUser: rm.authenticatedUser,
				ReactableID: rm.reactableID,
			}
			rm.reactableContainer.SetInnerHTML(htmlg.Render(inner.Render()...))
		}()
		rm.hide()
	})
	container.AppendChild(rm.results)
	preview := document.CreateElement("div").(*dom.HTMLDivElement)
	preview.SetClass("rm-reactions-preview")
	preview.SetInnerHTML(`<span id="rm-reactions-preview-emoji"><span class="rm-emoji rm-large"></span></span><span id="rm-reactions-preview-label"></span>`)
	container.AppendChild(preview)

	rm.updateFilteredResults()
	rm.filter.AddEventListener("input", false, func(dom.Event) {
		rm.updateFilteredResults()
	})

	rm.results.AddEventListener("mousemove", false, func(event dom.Event) {
		me := event.(*dom.MouseEvent)
		x := (me.ClientX - int(rm.results.GetBoundingClientRect().Left) + rm.results.Underlying().Get("scrollLeft").Int()) / 30
		if x >= 9 {
			return // Out of bounds to the right, likely because of scrollbar.
		}
		y := (me.ClientY - int(rm.results.GetBoundingClientRect().Top) + rm.results.Underlying().Get("scrollTop").Int()) / 30
		i := y*9 + x
		rm.updateSelected(i)
	})

	document.AddEventListener("keydown", false, func(event dom.Event) {
		if event.DefaultPrevented() {
			return
		}
		// Ignore when some element other than body has focus (it means the user is typing elsewhere).
		/*if !event.Target().IsEqualNode(document.Body()) {
			return
		}*/

		switch ke := event.(*dom.KeyboardEvent); {
		// Escape.
		case ke.KeyCode == 27 && !ke.Repeat && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey:
			if rm.menu.Style().GetPropertyValue("display") == "none" {
				return
			}

			rm.menu.Style().SetProperty("display", "none", "")

			ke.PreventDefault()
		}
	})

	document.Body().AppendChild(rm.menu)

	document.AddEventListener("click", false, func(event dom.Event) {
		if event.DefaultPrevented() {
			return
		}

		if !rm.menu.Contains(event.Target()) {
			rm.hide()
		}
	})
}

func (rm *reactionsMenu) Show(this dom.HTMLElement, event dom.Event, reactableID string) {
	rm.reactableID = reactableID
	rm.reactableContainer = document.GetElementByID("reactable-container-" + reactableID)

	rm.filter.Value = ""
	rm.filter.Underlying().Call("dispatchEvent", js.Global.Get("CustomEvent").New("input")) // Trigger "input" event listeners.
	rm.updateSelected(0)

	rm.menu.Style().SetProperty("display", "initial", "")

	// rm.menu aims to have 270px client width. Due to optional scrollbars
	// taking up some of that space, we may need to compensate and increase width.
	if scrollbarWidth := rm.results.OffsetWidth() - rm.results.Get("clientWidth").Float(); scrollbarWidth > 0 {
		rm.menu.Style().SetProperty("width", fmt.Sprintf("%fpx", 270+scrollbarWidth+1), "")
	}

	rm.results.Set("scrollTop", 0)
	top := float64(dom.GetWindow().ScrollY()) + this.GetBoundingClientRect().Top - rm.menu.GetBoundingClientRect().Height - 10
	if minTop := float64(dom.GetWindow().ScrollY()) + 12; top < minTop {
		top = minTop
	}
	rm.menu.Style().SetProperty("top", fmt.Sprintf("%vpx", top), "")
	left := float64(dom.GetWindow().ScrollX()) + this.GetBoundingClientRect().Left
	if maxLeft := float64(dom.GetWindow().InnerWidth()+dom.GetWindow().ScrollX()) - rm.menu.GetBoundingClientRect().Width - 12; left > maxLeft {
		left = maxLeft
	}
	if minLeft := float64(dom.GetWindow().ScrollX()) + 12; left < minLeft {
		left = minLeft
	}
	rm.menu.Style().SetProperty("left", fmt.Sprintf("%vpx", left), "")
	if rm.authenticatedUser.ID != 0 {
		rm.filter.Focus()
	}

	event.PreventDefault()
}

func (rm *reactionsMenu) hide() {
	rm.menu.Style().SetProperty("display", "none", "")
}

func (rm *reactionsMenu) ToggleReaction(this dom.HTMLElement, event dom.Event, emojiID string) {
	container := getAncestorByClassName(this, "reactable-container")
	reactableID := container.GetAttribute("data-reactableID")

	if rm.authenticatedUser.ID == 0 {
		rm.Show(this, event, reactableID)
		return
	}

	go func() {
		reactions, err := rm.reactionsService.Toggle(context.TODO(), rm.reactableURI, reactableID, reactions.ToggleRequest{Reaction: reactions.EmojiID(emojiID)})
		if err != nil {
			log.Println(err)
			return
		}
		inner := reactionscomponent.ReactionsBarInner{
			Reactions:   reactions,
			CurrentUser: rm.authenticatedUser,
			ReactableID: reactableID,
		}
		container.SetInnerHTML(htmlg.Render(inner.Render()...))
	}()
}

func (rm *reactionsMenu) updateFilteredResults() {
	lower := strings.ToLower(strings.TrimSpace(rm.filter.Value))
	rm.results.SetInnerHTML("")
	rm.filtered = nil
	for _, emojiID := range reactions.Sorted {
		if lower != "" && !strings.Contains(emojiID, lower) {
			continue
		}
		element := document.CreateElement("div")
		rm.results.AppendChild(element)
		element.SetOuterHTML(`<div class="rm-reaction"><span class="rm-emoji" style="background-position: ` + reactions.Position(emojiID) + `;"></span></div>`)
		rm.filtered = append(rm.filtered, emojiID)
	}
}

// updateSelected updates selected reaction to rm.filtered[index].
func (rm *reactionsMenu) updateSelected(index int) {
	if index < 0 || index >= len(rm.filtered) {
		return
	}
	emojiID := rm.filtered[index]

	label := document.GetElementByID("rm-reactions-preview-label").(*dom.HTMLSpanElement)
	label.SetTextContent(strings.Trim(emojiID, ":"))
	emoji := document.GetElementByID("rm-reactions-preview-emoji").(*dom.HTMLSpanElement)
	emoji.FirstChild().(dom.HTMLElement).Style().SetProperty("background-position", reactions.Position(emojiID), "")
}

func getAncestorByClassName(el dom.Element, class string) dom.Element {
	for ; el != nil && !el.Class().Contains(class); el = el.ParentElement() {
	}
	return el
}
<commit_msg>reactionsmenu: Clean up and document when colons are present in emojiID.<commit_after>// Package reactionsmenu provides a reactions menu component.
package reactionsmenu

import (
	"context"
	"fmt"
	"log"
	"strings"

	"github.com/gopherjs/gopherjs/js"
	"github.com/shurcooL/component"
	"github.com/shurcooL/go/gopherjs_http/jsutil"
	homecomponent "github.com/shurcooL/home/component"
	"github.com/shurcooL/htmlg"
	"github.com/shurcooL/reactions"
	reactionscomponent "github.com/shurcooL/reactions/component"
	"github.com/shurcooL/users"
	"honnef.co/go/js/dom"
)

var document = dom.GetWindow().Document().(dom.HTMLDocument)

var rm reactionsMenu

type reactionsMenu struct {
	reactableURI      string
	reactionsService  reactions.Service
	authenticatedUser users.User

	menu    *dom.HTMLDivElement
	filter  *dom.HTMLInputElement
	results *dom.HTMLDivElement

	filtered []string // Filtered emojiIDs from reactions.Sorted list. With colons, e.g., ":+1:".

	// From last Show, needed to rerender reactableContainer after toggling a reaction.
	reactableID        string
	reactableContainer dom.Element
}

// Setup sets up the reaction menu on the current page.
// It must be called exactly once when document.Body() already exists.
func Setup(reactableURI string, reactionsService reactions.Service, authenticatedUser users.User) {
	rm = reactionsMenu{
		reactableURI:      reactableURI,
		reactionsService:  reactionsService,
		authenticatedUser: authenticatedUser,
	}

	js.Global.Set("ShowReactionMenu", jsutil.Wrap(rm.Show))
	js.Global.Set("ToggleReaction", jsutil.Wrap(rm.ToggleReaction))

	rm.menu = document.CreateElement("div").(*dom.HTMLDivElement)
	rm.menu.SetID("rm-reactions-menu")

	container := document.CreateElement("div").(*dom.HTMLDivElement)
	container.SetClass("rm-reactions-menu-container")
	rm.menu.AppendChild(container)

	// Disable for unauthenticated user.
	if rm.authenticatedUser.ID == 0 {
		disabled := document.CreateElement("div").(*dom.HTMLDivElement)
		disabled.SetClass("rm-reactions-menu-disabled")
		signIn := document.CreateElement("div").(*dom.HTMLDivElement)
		signIn.SetClass("rm-reactions-menu-signin")
		returnURL := dom.GetWindow().Location().Pathname + dom.GetWindow().Location().Search
		signIn.SetInnerHTML(htmlg.RenderComponentsString(homecomponent.PostButton{Action: "/login/github", Text: "Sign in via GitHub", ReturnURL: returnURL}, component.Text(" to react.")))
		disabled.AppendChild(signIn)
		container.AppendChild(disabled)
	}

	rm.filter = document.CreateElement("input").(*dom.HTMLInputElement)
	rm.filter.SetClass("rm-reactions-filter")
	rm.filter.Placeholder = "Search"
	rm.menu.AddEventListener("click", false, func(event dom.Event) {
		if rm.authenticatedUser.ID != 0 {
			rm.filter.Focus()
		}
	})
	container.AppendChild(rm.filter)
	rm.results = document.CreateElement("div").(*dom.HTMLDivElement)
	rm.results.SetClass("rm-reactions-results")
	rm.results.AddEventListener("click", false, func(event dom.Event) {
		me := event.(*dom.MouseEvent)
		x := (me.ClientX - int(rm.results.GetBoundingClientRect().Left) + rm.results.Underlying().Get("scrollLeft").Int()) / 30
		if x >= 9 {
			return // Out of bounds to the right, likely because of scrollbar.
		}
		y := (me.ClientY - int(rm.results.GetBoundingClientRect().Top) + rm.results.Underlying().Get("scrollTop").Int()) / 30
		i := y*9 + x
		if i < 0 || i >= len(rm.filtered) {
			return
		}
		reaction := reactions.EmojiID(strings.Trim(rm.filtered[i], ":")) // Trim colons because reactions.Sorted has them, but reactions.EmojiID shouldn't.
		go func() {
			reactions, err := rm.reactionsService.Toggle(context.TODO(), rm.reactableURI, rm.reactableID, reactions.ToggleRequest{Reaction: reaction})
			if err != nil {
				log.Println(err)
				return
			}
			inner := reactionscomponent.ReactionsBarInner{
				Reactions:   reactions,
				CurrentUser: rm.authenticatedUser,
				ReactableID: rm.reactableID,
			}
			rm.reactableContainer.SetInnerHTML(htmlg.Render(inner.Render()...))
		}()
		rm.hide()
	})
	container.AppendChild(rm.results)
	preview := document.CreateElement("div").(*dom.HTMLDivElement)
	preview.SetClass("rm-reactions-preview")
	preview.SetInnerHTML(`<span id="rm-reactions-preview-emoji"><span class="rm-emoji rm-large"></span></span><span id="rm-reactions-preview-label"></span>`)
	container.AppendChild(preview)

	rm.updateFilteredResults()
	rm.filter.AddEventListener("input", false, func(dom.Event) {
		rm.updateFilteredResults()
	})

	rm.results.AddEventListener("mousemove", false, func(event dom.Event) {
		me := event.(*dom.MouseEvent)
		x := (me.ClientX - int(rm.results.GetBoundingClientRect().Left) + rm.results.Underlying().Get("scrollLeft").Int()) / 30
		if x >= 9 {
			return // Out of bounds to the right, likely because of scrollbar.
		}
		y := (me.ClientY - int(rm.results.GetBoundingClientRect().Top) + rm.results.Underlying().Get("scrollTop").Int()) / 30
		i := y*9 + x
		rm.updateSelected(i)
	})

	document.AddEventListener("keydown", false, func(event dom.Event) {
		if event.DefaultPrevented() {
			return
		}
		// Ignore when some element other than body has focus (it means the user is typing elsewhere).
		/*if !event.Target().IsEqualNode(document.Body()) {
			return
		}*/

		switch ke := event.(*dom.KeyboardEvent); {
		// Escape.
		case ke.KeyCode == 27 && !ke.Repeat && !ke.CtrlKey && !ke.AltKey && !ke.MetaKey && !ke.ShiftKey:
			if rm.menu.Style().GetPropertyValue("display") == "none" {
				return
			}

			rm.menu.Style().SetProperty("display", "none", "")

			ke.PreventDefault()
		}
	})

	document.Body().AppendChild(rm.menu)

	document.AddEventListener("click", false, func(event dom.Event) {
		if event.DefaultPrevented() {
			return
		}

		if !rm.menu.Contains(event.Target()) {
			rm.hide()
		}
	})
}

// Show shows the reactions menu.
func (rm *reactionsMenu) Show(this dom.HTMLElement, event dom.Event, reactableID string) {
	rm.reactableID = reactableID
	rm.reactableContainer = document.GetElementByID("reactable-container-" + reactableID)

	rm.filter.Value = ""
	rm.filter.Underlying().Call("dispatchEvent", js.Global.Get("CustomEvent").New("input")) // Trigger "input" event listeners.
	rm.updateSelected(0)

	rm.menu.Style().SetProperty("display", "initial", "")

	// rm.menu aims to have 270px client width. Due to optional scrollbars
	// taking up some of that space, we may need to compensate and increase width.
	if scrollbarWidth := rm.results.OffsetWidth() - rm.results.Get("clientWidth").Float(); scrollbarWidth > 0 {
		rm.menu.Style().SetProperty("width", fmt.Sprintf("%fpx", 270+scrollbarWidth+1), "")
	}

	rm.results.Set("scrollTop", 0)
	top := float64(dom.GetWindow().ScrollY()) + this.GetBoundingClientRect().Top - rm.menu.GetBoundingClientRect().Height - 10
	if minTop := float64(dom.GetWindow().ScrollY()) + 12; top < minTop {
		top = minTop
	}
	rm.menu.Style().SetProperty("top", fmt.Sprintf("%vpx", top), "")
	left := float64(dom.GetWindow().ScrollX()) + this.GetBoundingClientRect().Left
	if maxLeft := float64(dom.GetWindow().InnerWidth()+dom.GetWindow().ScrollX()) - rm.menu.GetBoundingClientRect().Width - 12; left > maxLeft {
		left = maxLeft
	}
	if minLeft := float64(dom.GetWindow().ScrollX()) + 12; left < minLeft {
		left = minLeft
	}
	rm.menu.Style().SetProperty("left", fmt.Sprintf("%vpx", left), "")
	if rm.authenticatedUser.ID != 0 {
		rm.filter.Focus()
	}

	event.PreventDefault()
}

func (rm *reactionsMenu) hide() {
	rm.menu.Style().SetProperty("display", "none", "")
}

// ToggleReaction toggles reaction. reaction is withot colons, e.g., "+1".
func (rm *reactionsMenu) ToggleReaction(this dom.HTMLElement, event dom.Event, reaction reactions.EmojiID) {
	container := getAncestorByClassName(this, "reactable-container")
	reactableID := container.GetAttribute("data-reactableID")

	if rm.authenticatedUser.ID == 0 {
		rm.Show(this, event, reactableID)
		return
	}

	go func() {
		reactions, err := rm.reactionsService.Toggle(context.TODO(), rm.reactableURI, reactableID, reactions.ToggleRequest{Reaction: reaction})
		if err != nil {
			log.Println(err)
			return
		}
		inner := reactionscomponent.ReactionsBarInner{
			Reactions:   reactions,
			CurrentUser: rm.authenticatedUser,
			ReactableID: reactableID,
		}
		container.SetInnerHTML(htmlg.Render(inner.Render()...))
	}()
}

func (rm *reactionsMenu) updateFilteredResults() {
	lower := strings.ToLower(strings.TrimSpace(rm.filter.Value))
	rm.results.SetInnerHTML("")
	rm.filtered = nil
	for _, emojiID := range reactions.Sorted {
		if lower != "" && !strings.Contains(emojiID, lower) {
			continue
		}
		element := document.CreateElement("div")
		rm.results.AppendChild(element)
		element.SetOuterHTML(`<div class="rm-reaction"><span class="rm-emoji" style="background-position: ` + reactions.Position(emojiID) + `;"></span></div>`)
		rm.filtered = append(rm.filtered, emojiID)
	}
}

// updateSelected updates selected reaction to rm.filtered[index].
func (rm *reactionsMenu) updateSelected(index int) {
	if index < 0 || index >= len(rm.filtered) {
		return
	}
	emojiID := rm.filtered[index]

	label := document.GetElementByID("rm-reactions-preview-label").(*dom.HTMLSpanElement)
	label.SetTextContent(strings.Trim(emojiID, ":"))
	emoji := document.GetElementByID("rm-reactions-preview-emoji").(*dom.HTMLSpanElement)
	emoji.FirstChild().(dom.HTMLElement).Style().SetProperty("background-position", reactions.Position(emojiID), "")
}

func getAncestorByClassName(el dom.Element, class string) dom.Element {
	for ; el != nil && !el.Class().Contains(class); el = el.ParentElement() {
	}
	return el
}
<|endoftext|>


<commit_before>package clients

import (
	"context"

	mClient "github.com/manifoldco/manifold-cli/generated/marketplace/client"
	"github.com/manifoldco/manifold-cli/generated/marketplace/client/resource"
	mModels "github.com/manifoldco/manifold-cli/generated/marketplace/models"
	pClient "github.com/manifoldco/manifold-cli/generated/provisioning/client"
	"github.com/manifoldco/manifold-cli/generated/provisioning/client/operation"
	pModels "github.com/manifoldco/manifold-cli/generated/provisioning/models"
)

// FetchOperations returns the resources for the authenticated user
func FetchOperations(ctx context.Context, c *pClient.Provisioning) ([]*pModels.Operation, error) {
	res, err := c.Operation.GetOperations(
		operation.NewGetOperationsParamsWithContext(ctx), nil)
	if err != nil {
		return nil, err
	}

	var results []*pModels.Operation
	for _, o := range res.Payload {
		// TODO: remove this once CLI has first-class Teams support
		if !o.Body.TeamID().IsEmpty() {
			continue
		}
		results = append(results, o)
	}
	return results, nil
}

// FetchResources returns the resources for the authenticated user
func FetchResources(ctx context.Context, c *mClient.Marketplace) ([]*mModels.Resource, error) {
	res, err := c.Resource.GetResources(
		resource.NewGetResourcesParamsWithContext(ctx), nil)
	if err != nil {
		return nil, err
	}

	var results []*mModels.Resource
	for _, r := range res.Payload {
		// TODO: remove this once CLI has first-class Teams support
		if r.Body.TeamID != nil {
			continue
		}
		results = append(results, r)
	}
	return results, nil
}
<commit_msg>Fix bug with list cmd regarding nil team ID<commit_after>package clients

import (
	"context"

	mClient "github.com/manifoldco/manifold-cli/generated/marketplace/client"
	"github.com/manifoldco/manifold-cli/generated/marketplace/client/resource"
	mModels "github.com/manifoldco/manifold-cli/generated/marketplace/models"
	pClient "github.com/manifoldco/manifold-cli/generated/provisioning/client"
	"github.com/manifoldco/manifold-cli/generated/provisioning/client/operation"
	pModels "github.com/manifoldco/manifold-cli/generated/provisioning/models"
)

// FetchOperations returns the resources for the authenticated user
func FetchOperations(ctx context.Context, c *pClient.Provisioning) ([]*pModels.Operation, error) {
	res, err := c.Operation.GetOperations(
		operation.NewGetOperationsParamsWithContext(ctx), nil)
	if err != nil {
		return nil, err
	}

	var results []*pModels.Operation
	for _, o := range res.Payload {
		// TODO: remove this once CLI has first-class Teams support
		if o.Body.TeamID() != nil {
			continue
		}
		results = append(results, o)
	}
	return results, nil
}

// FetchResources returns the resources for the authenticated user
func FetchResources(ctx context.Context, c *mClient.Marketplace) ([]*mModels.Resource, error) {
	res, err := c.Resource.GetResources(
		resource.NewGetResourcesParamsWithContext(ctx), nil)
	if err != nil {
		return nil, err
	}

	var results []*mModels.Resource
	for _, r := range res.Payload {
		// TODO: remove this once CLI has first-class Teams support
		if r.Body.TeamID != nil {
			continue
		}
		results = append(results, r)
	}
	return results, nil
}
<|endoftext|>


<commit_before>/*
Copyright 2019 The Kubernetes Authors.

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.
*/

package magnum

import (
	"io"
	"os"
	"sync"

	apiv1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/autoscaler/cluster-autoscaler/cloudprovider"
	"k8s.io/autoscaler/cluster-autoscaler/config"
	"k8s.io/autoscaler/cluster-autoscaler/config/dynamic"
	"k8s.io/autoscaler/cluster-autoscaler/utils/errors"
	klog "k8s.io/klog/v2"
)

const (
	// GPULabel is the label added to nodes with GPU resource.
	GPULabel = "cloud.google.com/gke-accelerator"
)

var (
	availableGPUTypes = map[string]struct{}{
		"nvidia-tesla-k80":  {},
		"nvidia-tesla-p100": {},
		"nvidia-tesla-v100": {},
	}
)

// magnumCloudProvider implements CloudProvider interface from cluster-autoscaler/cloudprovider module.
type magnumCloudProvider struct {
	magnumManager   *magnumManager
	resourceLimiter *cloudprovider.ResourceLimiter
	nodeGroups      []magnumNodeGroup
}

func buildMagnumCloudProvider(magnumManager magnumManager, resourceLimiter *cloudprovider.ResourceLimiter) (cloudprovider.CloudProvider, error) {
	mcp := &magnumCloudProvider{
		magnumManager:   &magnumManager,
		resourceLimiter: resourceLimiter,
		nodeGroups:      []magnumNodeGroup{},
	}
	return mcp, nil
}

// Name returns the name of the cloud provider.
func (mcp *magnumCloudProvider) Name() string {
	return cloudprovider.MagnumProviderName
}

// GPULabel returns the label added to nodes with GPU resource.
func (mcp *magnumCloudProvider) GPULabel() string {
	return GPULabel
}

// GetAvailableGPUTypes return all available GPU types cloud provider supports
func (mcp *magnumCloudProvider) GetAvailableGPUTypes() map[string]struct{} {
	return availableGPUTypes
}

// NodeGroups returns all node groups managed by this cloud provider.
func (mcp *magnumCloudProvider) NodeGroups() []cloudprovider.NodeGroup {
	groups := make([]cloudprovider.NodeGroup, len(mcp.nodeGroups))
	for i, group := range mcp.nodeGroups {
		groups[i] = &group
	}
	return groups
}

// AddNodeGroup appends a node group to the list of node groups managed by this cloud provider.
func (mcp *magnumCloudProvider) AddNodeGroup(group magnumNodeGroup) {
	mcp.nodeGroups = append(mcp.nodeGroups, group)
}

// NodeGroupForNode returns the node group that a given node belongs to.
//
// Since only a single node group is currently supported, the first node group is always returned.
func (mcp *magnumCloudProvider) NodeGroupForNode(node *apiv1.Node) (cloudprovider.NodeGroup, error) {
	// TODO: wait for magnum nodegroup support
	if _, found := node.ObjectMeta.Labels["node-role.kubernetes.io/master"]; found {
		return nil, nil
	}
	return &(mcp.nodeGroups[0]), nil
}

// Pricing is not implemented.
func (mcp *magnumCloudProvider) Pricing() (cloudprovider.PricingModel, errors.AutoscalerError) {
	return nil, cloudprovider.ErrNotImplemented
}

// GetAvailableMachineTypes is not implemented.
func (mcp *magnumCloudProvider) GetAvailableMachineTypes() ([]string, error) {
	return []string{}, nil
}

// NewNodeGroup is not implemented.
func (mcp *magnumCloudProvider) NewNodeGroup(machineType string, labels map[string]string, systemLabels map[string]string,
	taints []apiv1.Taint, extraResources map[string]resource.Quantity) (cloudprovider.NodeGroup, error) {
	return nil, cloudprovider.ErrNotImplemented
}

// GetResourceLimiter returns resource constraints for the cloud provider
func (mcp *magnumCloudProvider) GetResourceLimiter() (*cloudprovider.ResourceLimiter, error) {
	return mcp.resourceLimiter, nil
}

// GetInstanceID gets the instance ID for the specified node.
func (mcp *magnumCloudProvider) GetInstanceID(node *apiv1.Node) string {
	return node.Spec.ProviderID
}

// Refresh is called before every autoscaler main loop.
//
// Currently only prints debug information.
func (mcp *magnumCloudProvider) Refresh() error {
	for _, nodegroup := range mcp.nodeGroups {
		klog.V(3).Info(nodegroup.Debug())
	}
	return nil
}

// Cleanup currently does nothing.
func (mcp *magnumCloudProvider) Cleanup() error {
	return nil
}

// BuildMagnum is called by the autoscaler to build a magnum cloud provider.
//
// The magnumManager is created here, and the node groups are created
// based on the specs provided via the command line parameters.
func BuildMagnum(opts config.AutoscalingOptions, do cloudprovider.NodeGroupDiscoveryOptions, rl *cloudprovider.ResourceLimiter) cloudprovider.CloudProvider {
	var config io.ReadCloser

	// Should be loaded with --cloud-config /etc/kubernetes/kube_openstack_config from master node
	if opts.CloudConfig != "" {
		var err error
		config, err = os.Open(opts.CloudConfig)
		if err != nil {
			klog.Fatalf("Couldn't open cloud provider configuration %s: %#v", opts.CloudConfig, err)
		}
		defer config.Close()
	}

	manager, err := createMagnumManager(config, do, opts)
	if err != nil {
		klog.Fatalf("Failed to create magnum manager: %v", err)
	}

	provider, err := buildMagnumCloudProvider(manager, rl)
	if err != nil {
		klog.Fatalf("Failed to create magnum cloud provider: %v", err)
	}

	// TODO: When magnum node group support is available then 0 NodeGroupSpecs will be a valid input.
	if len(do.NodeGroupSpecs) == 0 {
		klog.Fatalf("Must specify at least one node group with --nodes=<min>:<max>:<name>,...")
	}

	// TODO: Temporary, only makes sense to have one nodegroup until magnum nodegroups are implemented
	// Node groups will be available in k8s_fedora_atomic_v2 driver https://review.openstack.org/#/c/629274/
	if len(do.NodeGroupSpecs) > 1 {
		klog.Fatalf("Magnum autoscaler only supports a single nodegroup for now")
	}

	clusterUpdateLock := sync.Mutex{}

	for _, nodegroupSpec := range do.NodeGroupSpecs {
		spec, err := dynamic.SpecFromString(nodegroupSpec, scaleToZeroSupported)
		if err != nil {
			klog.Fatalf("Could not parse node group spec %s: %v", nodegroupSpec, err)
		}

		ng := magnumNodeGroup{
			magnumManager:       manager,
			id:                  spec.Name,
			clusterUpdateMutex:  &clusterUpdateLock,
			minSize:             spec.MinSize,
			maxSize:             spec.MaxSize,
			targetSize:          new(int),
			waitTimeStep:        waitForStatusTimeStep,
			deleteBatchingDelay: deleteNodesBatchingDelay,
		}
		*ng.targetSize, err = ng.magnumManager.nodeGroupSize(ng.id)
		if err != nil {
			klog.Fatalf("Could not set current nodes in node group: %v", err)
		}
		provider.(*magnumCloudProvider).AddNodeGroup(ng)
	}

	return provider
}
<commit_msg>Update magnum cloud provider<commit_after>/*
Copyright 2019 The Kubernetes Authors.

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.
*/

package magnum

import (
	"fmt"
	"io"
	"os"
	"strings"
	"sync"
	"time"

	apiv1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	"k8s.io/autoscaler/cluster-autoscaler/cloudprovider"
	"k8s.io/autoscaler/cluster-autoscaler/config"
	"k8s.io/autoscaler/cluster-autoscaler/config/dynamic"
	"k8s.io/autoscaler/cluster-autoscaler/utils/errors"
	klog "k8s.io/klog/v2"
)

const (
	// GPULabel is the label added to nodes with GPU resource.
	GPULabel = "magnum.openstack.org/gpu"

	// Refresh interval for node group auto discovery
	discoveryRefreshInterval = 1 * time.Minute
)

var (
	availableGPUTypes = map[string]struct{}{}
)

// magnumCloudProvider implements CloudProvider interface from cluster-autoscaler/cloudprovider module.
type magnumCloudProvider struct {
	magnumManager   magnumManager
	resourceLimiter *cloudprovider.ResourceLimiter

	nodeGroups []*magnumNodeGroup

	// To be locked when modifying or reading the node groups slice.
	nodeGroupsLock *sync.Mutex

	// To be locked when modifying or reading the cluster state from Magnum.
	clusterUpdateLock *sync.Mutex

	usingAutoDiscovery   bool
	autoDiscoveryConfigs []magnumAutoDiscoveryConfig
	lastDiscoveryRefresh time.Time
}

func buildMagnumCloudProvider(magnumManager magnumManager, resourceLimiter *cloudprovider.ResourceLimiter) (*magnumCloudProvider, error) {
	mcp := &magnumCloudProvider{
		magnumManager:   magnumManager,
		resourceLimiter: resourceLimiter,
		nodeGroups:      []*magnumNodeGroup{},
		nodeGroupsLock:  &sync.Mutex{},
	}
	return mcp, nil
}

// Name returns the name of the cloud provider.
func (mcp *magnumCloudProvider) Name() string {
	return cloudprovider.MagnumProviderName
}

// GPULabel returns the label added to nodes with GPU resource.
func (mcp *magnumCloudProvider) GPULabel() string {
	return GPULabel
}

// GetAvailableGPUTypes return all available GPU types cloud provider supports
func (mcp *magnumCloudProvider) GetAvailableGPUTypes() map[string]struct{} {
	return availableGPUTypes
}

// NodeGroups returns all node groups managed by this cloud provider.
func (mcp *magnumCloudProvider) NodeGroups() []cloudprovider.NodeGroup {
	mcp.nodeGroupsLock.Lock()
	defer mcp.nodeGroupsLock.Unlock()

	// Have to convert to a slice of the NodeGroup interface type.
	groups := make([]cloudprovider.NodeGroup, len(mcp.nodeGroups))
	for i, group := range mcp.nodeGroups {
		groups[i] = group
	}
	return groups
}

// AddNodeGroup appends a node group to the list of node groups managed by this cloud provider.
func (mcp *magnumCloudProvider) AddNodeGroup(group *magnumNodeGroup) {
	mcp.nodeGroupsLock.Lock()
	defer mcp.nodeGroupsLock.Unlock()
	mcp.nodeGroups = append(mcp.nodeGroups, group)
}

// NodeGroupForNode returns the node group that a given node belongs to.
func (mcp *magnumCloudProvider) NodeGroupForNode(node *apiv1.Node) (cloudprovider.NodeGroup, error) {
	mcp.nodeGroupsLock.Lock()
	defer mcp.nodeGroupsLock.Unlock()

	// Ignore master node
	if _, found := node.ObjectMeta.Labels["node-role.kubernetes.io/master"]; found {
		return nil, nil
	}

	ngUUID, err := mcp.magnumManager.nodeGroupForNode(node)
	if err != nil {
		return nil, fmt.Errorf("error finding node group UUID for node %s: %v", node.Spec.ProviderID, err)
	}

	for _, group := range mcp.nodeGroups {
		if group.UUID == ngUUID {
			klog.V(4).Infof("Node %s belongs to node group %s", node.Spec.ProviderID, group.Id())
			return group, nil
		}
	}

	klog.V(4).Infof("Node %s is not part of an autoscaled node group", node.Spec.ProviderID)

	return nil, nil
}

// Pricing is not implemented.
func (mcp *magnumCloudProvider) Pricing() (cloudprovider.PricingModel, errors.AutoscalerError) {
	return nil, cloudprovider.ErrNotImplemented
}

// GetAvailableMachineTypes is not implemented.
func (mcp *magnumCloudProvider) GetAvailableMachineTypes() ([]string, error) {
	return []string{}, nil
}

// NewNodeGroup is not implemented.
func (mcp *magnumCloudProvider) NewNodeGroup(machineType string, labels map[string]string, systemLabels map[string]string,
	taints []apiv1.Taint, extraResources map[string]resource.Quantity) (cloudprovider.NodeGroup, error) {
	return nil, cloudprovider.ErrNotImplemented
}

// GetResourceLimiter returns resource constraints for the cloud provider
func (mcp *magnumCloudProvider) GetResourceLimiter() (*cloudprovider.ResourceLimiter, error) {
	return mcp.resourceLimiter, nil
}

// Refresh is called before every autoscaler main loop.
//
// Debug information for each node group is printed with logging level >= 5.
// Every 60 seconds the node group state on the Magnum side is checked,
// to see if there are any node groups that need to be added/removed/updated.
func (mcp *magnumCloudProvider) Refresh() error {
	mcp.nodeGroupsLock.Lock()
	for _, nodegroup := range mcp.nodeGroups {
		klog.V(5).Info(nodegroup.Debug())
	}
	mcp.nodeGroupsLock.Unlock()

	if mcp.usingAutoDiscovery {
		if time.Since(mcp.lastDiscoveryRefresh) > discoveryRefreshInterval {
			mcp.lastDiscoveryRefresh = time.Now()
			err := mcp.refreshNodeGroups()
			if err != nil {
				return fmt.Errorf("error refreshing node groups: %v", err)
			}
		}
	}

	return nil
}

// Cleanup currently does nothing.
func (mcp *magnumCloudProvider) Cleanup() error {
	return nil
}

// refreshNodeGroups gets the list of node groups which meet the requirements for autoscaling,
// creates magnumNodeGroups for any that do not exist in the cloud provider,
// and drops any node groups which are present in the cloud provider but not in the
// list of node groups that should be autoscaled.
//
// Any node groups which have had their min/max node count updated in Magnum
// are updated with the new limits.
func (mcp *magnumCloudProvider) refreshNodeGroups() error {
	mcp.clusterUpdateLock.Lock()
	defer mcp.clusterUpdateLock.Unlock()

	// Get the list of node groups that match the auto discovery configuration and
	// meet the requirements for autoscaling.
	nodeGroups, err := mcp.magnumManager.autoDiscoverNodeGroups(mcp.autoDiscoveryConfigs)
	if err != nil {
		return fmt.Errorf("could not discover node groups: %v", err)
	}

	// Track names of node groups which are added or removed (for logging).
	var newNodeGroupNames []string
	var droppedNodeGroupNames []string

	// Use maps for easier lookups of node group names.

	// Node group names as registered in the autoscaler.
	registeredNGs := make(map[string]*magnumNodeGroup)
	mcp.nodeGroupsLock.Lock()
	for _, ng := range mcp.nodeGroups {
		registeredNGs[ng.UUID] = ng
	}
	mcp.nodeGroupsLock.Unlock()

	// Node group names that exist on the cloud side and should be autoscaled.
	autoscalingNGs := make(map[string]string)

	for _, nodeGroup := range nodeGroups {
		name := uniqueName(nodeGroup)

		// Just need the name in the key.
		autoscalingNGs[nodeGroup.UUID] = ""

		if ng, alreadyRegistered := registeredNGs[nodeGroup.UUID]; alreadyRegistered {
			// Node group exists in autoscaler and in cloud, only need to check if min/max node count have changed.
			if ng.minSize != nodeGroup.MinNodeCount {
				ng.minSize = nodeGroup.MinNodeCount
				klog.V(2).Infof("Node group %s min node count changed to %d", nodeGroup.Name, ng.minSize)
			}
			// Node groups with unset max node count are not eligible for autoscaling, so this dereference is safe.
			if ng.maxSize != *nodeGroup.MaxNodeCount {
				ng.maxSize = *nodeGroup.MaxNodeCount
				klog.V(2).Infof("Node group %s max node count changed to %d", nodeGroup.Name, ng.maxSize)
			}
			continue
		}

		// The node group is not known to the autoscaler, so create it.
		ng := &magnumNodeGroup{
			magnumManager:     mcp.magnumManager,
			id:                name,
			UUID:              nodeGroup.UUID,
			clusterUpdateLock: mcp.clusterUpdateLock,
			minSize:           nodeGroup.MinNodeCount,
			maxSize:           *nodeGroup.MaxNodeCount,
			targetSize:        nodeGroup.NodeCount,
			deletedNodes:      make(map[string]time.Time),
		}
		mcp.AddNodeGroup(ng)
		mcp.magnumManager.fetchNodeGroupStackIDs(ng.UUID)
		newNodeGroupNames = append(newNodeGroupNames, name)
	}

	// Drop any node groups that should not be autoscaled either
	// because they were deleted or had their maximum node count unset.
	// Done by copying all node groups to a buffer, clearing the original
	// node groups and copying back only the ones that should still exist.
	mcp.nodeGroupsLock.Lock()
	buffer := make([]*magnumNodeGroup, len(mcp.nodeGroups))
	copy(buffer, mcp.nodeGroups)

	mcp.nodeGroups = nil

	for _, ng := range buffer {
		if _, ok := autoscalingNGs[ng.UUID]; ok {
			mcp.nodeGroups = append(mcp.nodeGroups, ng)
		} else {
			droppedNodeGroupNames = append(droppedNodeGroupNames, ng.id)
		}
	}

	mcp.nodeGroupsLock.Unlock()

	// Log whatever actions were taken
	if len(newNodeGroupNames) == 0 && len(droppedNodeGroupNames) == 0 {
		klog.V(3).Info("No nodegroups added or removed")
		return nil
	}

	if len(newNodeGroupNames) > 0 {
		klog.V(2).Infof("Discovered %d new node groups for autoscaling: %s", len(newNodeGroupNames),
			strings.Join(newNodeGroupNames, ", "))
	}
	if len(droppedNodeGroupNames) > 0 {
		klog.V(2).Infof("Dropped %d node groups which should no longer be autoscaled: %s",
			len(droppedNodeGroupNames), strings.Join(droppedNodeGroupNames, ", "))
	}

	return nil
}

// BuildMagnum is called by the autoscaler to build a magnum cloud provider.
//
// The magnumManager is created here, and the initial node groups are created
// based on the static or auto discovery specs provided via the command line parameters.
func BuildMagnum(opts config.AutoscalingOptions, do cloudprovider.NodeGroupDiscoveryOptions, rl *cloudprovider.ResourceLimiter) cloudprovider.CloudProvider {
	var config io.ReadCloser

	// Should be loaded with --cloud-config /etc/kubernetes/kube_openstack_config from master node.
	if opts.CloudConfig != "" {
		var err error
		config, err = os.Open(opts.CloudConfig)
		if err != nil {
			klog.Fatalf("Couldn't open cloud provider configuration from %s: %#v", opts.CloudConfig, err)
		}
		defer config.Close()
	}

	// Check that one of static node group discovery or auto discovery are specified.
	if !do.DiscoverySpecified() {
		klog.Fatal("no node group discovery options specified")
	}
	if do.StaticDiscoverySpecified() && do.AutoDiscoverySpecified() {
		klog.Fatal("can not use both static node group discovery and node group auto discovery")
	}

	manager, err := createMagnumManager(config, do, opts)
	if err != nil {
		klog.Fatalf("Failed to create magnum manager: %v", err)
	}

	provider, err := buildMagnumCloudProvider(manager, rl)
	if err != nil {
		klog.Fatalf("Failed to create magnum cloud provider: %v", err)
	}

	clusterUpdateLock := sync.Mutex{}
	provider.clusterUpdateLock = &clusterUpdateLock

	// Handle initial node group discovery.
	if do.StaticDiscoverySpecified() {
		for _, nodegroupSpec := range do.NodeGroupSpecs {
			// Parse a node group spec in the form min:max:name
			spec, err := dynamic.SpecFromString(nodegroupSpec, scaleToZeroSupported)
			if err != nil {
				klog.Fatalf("Could not parse node group spec %s: %v", nodegroupSpec, err)
			}

			ng := &magnumNodeGroup{
				magnumManager:     manager,
				id:                spec.Name,
				clusterUpdateLock: &clusterUpdateLock,
				minSize:           spec.MinSize,
				maxSize:           spec.MaxSize,
				targetSize:        1,
				deletedNodes:      make(map[string]time.Time),
			}

			// Lookup the nodegroup with this name and create a unique name for it based on the UUID.
			name, uuid, err := ng.magnumManager.uniqueNameAndIDForNodeGroup(ng.id)
			if err != nil {
				klog.Fatalf("could not get unique name and UUID for node group %s: %v", spec.Name, err)
			}
			ng.id = name
			ng.UUID = uuid

			// Fetch the current size of this node group.
			ng.targetSize, err = ng.magnumManager.nodeGroupSize(ng.UUID)
			if err != nil {
				klog.Fatalf("Could not get current number of nodes in node group %s: %v", spec.Name, err)
			}

			provider.AddNodeGroup(ng)
			manager.(*magnumManagerImpl).fetchNodeGroupStackIDs(ng.UUID)
		}
	} else if do.AutoDiscoverySpecified() {
		provider.usingAutoDiscovery = true
		cfgs, err := parseMagnumAutoDiscoverySpecs(do)
		if err != nil {
			klog.Fatalf("Could not parse auto discovery specs: %v", err)
		}
		provider.autoDiscoveryConfigs = cfgs

		err = provider.refreshNodeGroups()
		if err != nil {
			klog.Fatalf("Initial node group discovery failed: %v", err)
		}
		provider.lastDiscoveryRefresh = time.Now()
	}

	return provider
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/csv"
	"errors"
	"fmt"
	"io"
	"strconv"
)

type parsedInput struct {
	isRegression bool
	X            [][]float64
	YClf         []string  // will be nil when isRegression = true
	YReg         []float64 // will be nil when isRegression = false
	VarNames     []string
}

// parse csv file, detect if first row is header/has var names,
// returns X, Y, varNames, error
func parseCSV(r io.Reader) (*parsedInput, error) {
	reader := csv.NewReader(r)

	// isRegression=true, parse as regression until we hit
	// errors parsing floats, then set flag
	p := &parsedInput{isRegression: true}

	// grab first fow
	row, err := reader.Read()
	if err != nil {
		return p, err
	}

	// check if it's a header row
	varNames, err := parseHeader(row)
	if err == nil {
		p.VarNames = varNames
	} else {
		// use X1, X2,...Xn for var names
		for i := range row[1:] {
			p.VarNames = append(p.VarNames, fmt.Sprintf("X%d", i+1))
		}

		// parse row
		err = p.ParseRow(row)
		if err != nil {
			return p, err
		}
	}

	// keep reading rows until EOF
	for {
		row, err := reader.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			return p, err
		}

		p.ParseRow(row)
	}

	// drop the y vals we aren't using
	if p.isRegression {
		p.YClf = nil
	} else {
		p.YReg = nil
	}

	return p, err
}

func (p *parsedInput) ParseRow(row []string) error {
	xi, err := parseFeatureVals(row)
	if err != nil {
		return err
	}
	p.X = append(p.X, xi)

	// parse as regression and classification until we encounter errors
	// parsing floats
	if p.isRegression {
		yi, err := strconv.ParseFloat(row[0], 64)
		if err != nil {
			p.isRegression = false
		}
		p.YReg = append(p.YReg, yi)

	}
	p.YClf = append(p.YClf, row[0])

	return nil
}

func parseFeatureVals(row []string) ([]float64, error) {
	var xi []float64
	if len(row) < 1 {
		return xi, errors.New("row only has one column")
	}
	for _, val := range row[1:] {
		fv, err := strconv.ParseFloat(val, 64)
		if err != nil {
			return xi, err
		}
		xi = append(xi, fv)
	}
	return xi, nil
}

func parseHeader(row []string) ([]string, error) {
	colNames := []string{}

	// we only accept numeric input values, so we can consider the first row
	// as a header row if one or more of the values isn't a number
	if len(row) > 1 {
		for _, val := range row[1:] {
			_, err := strconv.ParseFloat(val, 64)
			if err == nil {
				return colNames, errors.New("not a header row")
			}

			colNames = append(colNames, val)
		}
	}

	return colNames, nil
}
<commit_msg>check error when parsing csv row<commit_after>package main

import (
	"encoding/csv"
	"errors"
	"fmt"
	"io"
	"strconv"
)

type parsedInput struct {
	isRegression bool
	X            [][]float64
	YClf         []string  // will be nil when isRegression = true
	YReg         []float64 // will be nil when isRegression = false
	VarNames     []string
}

// parse csv file, detect if first row is header/has var names,
// returns X, Y, varNames, error
func parseCSV(r io.Reader) (*parsedInput, error) {
	reader := csv.NewReader(r)

	// isRegression=true, parse as regression until we hit
	// errors parsing floats, then set flag
	p := &parsedInput{isRegression: true}

	// grab first fow
	row, err := reader.Read()
	if err != nil {
		return p, err
	}

	// check if it's a header row
	varNames, err := parseHeader(row)
	if err == nil {
		p.VarNames = varNames
	} else {
		// use X1, X2,...Xn for var names
		for i := range row[1:] {
			p.VarNames = append(p.VarNames, fmt.Sprintf("X%d", i+1))
		}

		// parse row
		err = p.ParseRow(row)
		if err != nil {
			return p, err
		}
	}

	// keep reading rows until EOF
	for {
		row, err := reader.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			return p, err
		}

		err = p.ParseRow(row)
		if err != nil {
			return p, err
		}
	}

	// drop the y vals we aren't using
	if p.isRegression {
		p.YClf = nil
	} else {
		p.YReg = nil
	}

	return p, err
}

func (p *parsedInput) ParseRow(row []string) error {
	xi, err := parseFeatureVals(row)
	if err != nil {
		return err
	}
	p.X = append(p.X, xi)

	// parse as regression and classification until we encounter errors
	// parsing floats
	if p.isRegression {
		yi, err := strconv.ParseFloat(row[0], 64)
		if err != nil {
			p.isRegression = false
		}
		p.YReg = append(p.YReg, yi)

	}
	p.YClf = append(p.YClf, row[0])

	return nil
}

func parseFeatureVals(row []string) ([]float64, error) {
	var xi []float64
	if len(row) < 1 {
		return xi, errors.New("row only has one column")
	}
	for _, val := range row[1:] {
		fv, err := strconv.ParseFloat(val, 64)
		if err != nil {
			return xi, err
		}
		xi = append(xi, fv)
	}
	return xi, nil
}

func parseHeader(row []string) ([]string, error) {
	colNames := []string{}

	// we only accept numeric input values, so we can consider the first row
	// as a header row if one or more of the values isn't a number
	if len(row) > 1 {
		for _, val := range row[1:] {
			_, err := strconv.ParseFloat(val, 64)
			if err == nil {
				return colNames, errors.New("not a header row")
			}

			colNames = append(colNames, val)
		}
	}

	return colNames, nil
}
<|endoftext|>


<commit_before>package gowd

import (
	"io"
	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
	"strings"
	"errors"
)

//ParseElements parse an html fragment and return a list of elements
func ParseElements(r io.Reader) ([]*Element, error) {
	nodes, err := html.ParseFragment(r, &html.Node{
		Type:     html.ElementNode,
		Data:     "body",
		DataAtom: atom.Body,
	})
	if err != nil {
		return nil, err
	}
	elems := make([]*Element, 0)
	for _, node := range nodes {
		elems = append(elems, NewElementFromNode(node))
	}
	return elems, nil
}

//NewElementFromNode creates an element from existing node
func NewElementFromNode(node*html.Node) *Element {
	elem := &Element{
		data:          strings.Trim(node.Data,"\n\r\t"),
		Attributes:    node.Attr,
		nodeType:      node.Type,
		Kids:          make([]*Element, 0),
		eventHandlers: make(map[string]EventHandler),
	}
	for c := node.FirstChild; c != nil; c = c.NextSibling {
		elem.AddElement(NewElementFromNode(c));
	}
	return elem
}

func ParseElement(innerHtml string) (*Element, error) {
	elems, err := ParseElements(strings.NewReader(innerHtml))
	if err != nil {
		return nil, err
	}
	if len(elems) != 1 {
		return nil, errors.New("The provided html must yield only one html element")
	}
	return elems[0], nil
}<commit_msg>improving the error for when there are too many items in the parsed html<commit_after>package gowd

import (
	"io"
	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
	"strings"
	"errors"
	"fmt"
)

//ParseElements parse an html fragment and return a list of elements
func ParseElements(r io.Reader) ([]*Element, error) {
	nodes, err := html.ParseFragment(r, &html.Node{
		Type:     html.ElementNode,
		Data:     "body",
		DataAtom: atom.Body,
	})
	if err != nil {
		return nil, err
	}
	elems := make([]*Element, 0)
	for _, node := range nodes {
		elems = append(elems, NewElementFromNode(node))
	}
	return elems, nil
}

//NewElementFromNode creates an element from existing node
func NewElementFromNode(node*html.Node) *Element {
	elem := &Element{
		data:          strings.Trim(node.Data, "\n\r\t "),
		Attributes:    node.Attr,
		nodeType:      node.Type,
		Kids:          make([]*Element, 0),
		eventHandlers: make(map[string]EventHandler),
	}
	for c := node.FirstChild; c != nil; c = c.NextSibling {
		elem.AddElement(NewElementFromNode(c));
	}
	return elem
}

func ParseElement(innerHtml string) (*Element, error) {
	elems, err := ParseElements(strings.NewReader(innerHtml))
	if err != nil {
		return nil, err
	}
	if len(elems) != 1 {
		tags := "["
		for _, e := range elems {
			tags += fmt.Sprintf("%v:'%s', ",e.nodeType,strings.Trim(e.data,"\t\n\r "))

		}
		tags += ")"
		return nil, errors.New("The provided html must yield only one html element, I have: " + tags)
	}
	return elems[0], nil
}<|endoftext|>


<commit_before>package main

import (
  "os"
  "fmt"
  "io"
  "bufio"
  "strings"
  "path/filepath"
  "errors"
  "bytes"
  "go/token"
  "go/parser"
  "go/printer"
  "go/ast"
)

const verbose bool = false

var output bytes.Buffer

//--- Linear pattern matcher

type Pattern struct {
  Text []rune
  Length, Pos int
}

func newPattern(s string) Pattern {
  runes := []rune(s)
  return Pattern{ Text: runes, Length: len(runes) }
}

// Next returns true if Pos advances past the last character of Text.
func (pattern *Pattern) Next(ch rune) bool {
  // If Pos is past the end of Text, reset it to the beginning.
  if pattern.Pos == pattern.Length {
    pattern.Pos = 0
  }
  // Try to match the current rune in Text.
  if ch == pattern.Text[pattern.Pos] {
    pattern.Pos++
  }
  // Check for a complete match.
  return pattern.Pos == pattern.Length
}

//--- Template parsing and output generation

type TemplateEntry struct {
  SitePath, HardPath string
  FileInfo os.FileInfo
  InsertionLine int
}

func (entry TemplateEntry) String() string {
  if entry.InsertionLine == 0 {
    return entry.SitePath
  }
  return fmt.Sprintf("-> line %d: %s", entry.InsertionLine, entry.SitePath)
}

func makeTemplateEntry(siteRoot, startDir, sitePath string,
    insertionLine int) (*TemplateEntry, error) {
  hardPath := makeHardPath(siteRoot, startDir, sitePath)
  fileInfo, error := os.Stat(hardPath)
  if error != nil {
    return nil, error
  }
  entry := TemplateEntry{
      SitePath: sitePath,
      HardPath: hardPath,
      FileInfo: fileInfo,
      InsertionLine: insertionLine,
    }
  return &entry, nil
}

func makeHardPath(siteRoot, startDir, sitePath string) string {
  // A hard path names a location in the physical file system rather than
  //  in the website's directory structure. It is either an absolute path
  //  or a relative path with respect to the directory containing the
  //  top-level template that is being parsed.
  var dir string
  if filepath.IsAbs(sitePath) {
    dir = siteRoot
  } else {
    dir = startDir
  }
  // Note that filepath.Join automatically performs filepath.Clean, thus
  //  returning a lexically unique form of the path. However, the path
  //  does not uniquely identify a file if it includes a symbolic link.
  //  Therefore, we cannot rely on string comparison to prevent cycles.
  hardPath := filepath.Join(dir, sitePath)
  return hardPath
}

func parse(templatePath string) error {

  // We resolve absolute paths with the website root.
  siteRoot := "/var/www/dd1"  // Stub. We'll get the real value from Apache.
  // We resolve relative paths using the starting directory.
  startDir := filepath.Dir(templatePath)

  // Make an insertion stack with a top-level entry.
  entry, error := makeTemplateEntry(siteRoot, startDir, templatePath, 0)
  if error != nil {
    return error
  }
  stack := []*TemplateEntry{ entry }
  return doParse(siteRoot, startDir, stack)
}

func doParse(siteRoot, startDir string, stack []*TemplateEntry) error {
  current := stack[len(stack)-1]
  if verbose {
    fmt.Fprintf(&output, "// start \"%s\"\n", current.SitePath)
  }
  var topLevel bool
  if len(stack) == 1 {
    topLevel = true
  }

  // Check for an insertion cycle.
  for i := len(stack)-2; i >= 0; i-- {
    ancestor := stack[i]
    if os.SameFile(ancestor.FileInfo, current.FileInfo) {
      lines := []string{ "doParse: insertion cycle" }
      for j := i; j < len(stack); j++ {
        lines = append(lines, stack[j].String())
      }
      message := fmt.Sprintf(strings.Join(lines, "\n  "))
      return errors.New(message)
    }
  }

  var error error
  var file *os.File

  file, error = os.Open(current.HardPath)
  if error != nil {
    return error
  }

  reader := bufio.NewReader(file)
  writer := bufio.NewWriter(os.Stdout)
  defer writer.Flush()

  codePattern := newPattern("<?code")
  insertPattern := newPattern("<?insert")
  openPatterns := []*Pattern{ &codePattern, &insertPattern }
  var open *Pattern
  close := newPattern("?>")

  var buffer []rune
  var ch rune
  var size int
  countBytes, countRunes := 0, 0
  lineIndex := 1
  prefix := true

  for {
    ch, size, error = reader.ReadRune()
    if error == nil {
      buffer = append(buffer, ch)
      countBytes += size
      countRunes += 1
      if ch == '\n' {
        lineIndex += 1
      }
    } else {
      content := string(buffer)
      if topLevel {
        content = strings.TrimSpace(content)
      }
      emitStatic(content)
      break
    }

    if open == nil {
      for _, pattern := range openPatterns {
        if pattern.Next(ch) {
          open = pattern
          content := string(buffer[0:len(buffer)-open.Length])
          if prefix {
            if topLevel {
              content = strings.TrimSpace(content)
            }
            prefix = false
          }
          emitStatic(content)
          buffer = []rune{}
        }
      }
    } else {
      if close.Next(ch) {
        content := buffer[0:len(buffer)-close.Length]
        if open == &codePattern {
          emitCode(string(content))
        } else if open == &insertPattern {
          childPath := strings.TrimSpace(string(content))
          entry, error := makeTemplateEntry(siteRoot, startDir, childPath,
              lineIndex)
          if error != nil {
            return error
          }
          stack = append(stack, entry)
          error = doParse(siteRoot, startDir, stack)
          if error != nil {
            return error
          }
          stack = stack[0:len(stack)-1]
        }
        open = nil
        buffer = []rune{}
      }
    }
  }
  if verbose {
    fmt.Fprintf(&output, "// finish \"%s\"\n", current.SitePath)
    fmt.Fprintf(&output, "// read %d bytes, %d runes\n", countBytes, countRunes)
    fmt.Fprintf(&output, "// finished on line %d\n", lineIndex)
  }
  if error == io.EOF {
    return nil
  }
  return error
}

func emitCode(content string) {
  fmt.Fprint(&output, content)
}

func emitStatic(content string) {
  if len(content) == 0 {
    return
  }
  from := 0
  for pos, ch := range content {
    if ch == '`' {
      if pos != from {
        raw := fmt.Sprintf("`%s`", content[from:pos])
        emitRaw(raw)
      }
      emitRaw("'`'")
      from = pos+1
    }
  }
  if from != len(content) {
    raw := fmt.Sprintf("`%s`", content[from:len(content)])
    emitRaw(raw)
  }
}
func emitRaw(s string) {
  fmt.Fprintf(&output, "fmt.Print(%s)\n", s)
}

func main() {
  writer := bufio.NewWriter(os.Stdout)
  defer writer.Flush()
  log := os.Stderr

  numFiles := len(os.Args)-1
  if numFiles == 0 {
    writer.WriteString("No files specified.\n")
    return
  }
  for argIx := 1; argIx <= numFiles; argIx++ {
    // Parse the top-level template. The resulting source code goes into
    //  a global variable called output, the type of which is bytes.Buffer.
    path := os.Args[argIx]
    error := parse(path)
    if error == nil {
      // Use Go's parser on the code that came out of the template parser.
      fileSet := token.NewFileSet()
      fileNode, error := parser.ParseFile(fileSet, "output", output.Bytes(),
          parser.ParseComments)
      if error == nil {
        // If the Go parsing process was successful, it results in an
        //  ast.File node. We're going to use this node for code injection
        //  and eventually for printing.

        // Scan the imported packages and check for fmt.
        importSpecs := fileNode.Imports
        fmtFound := false
        for _, importSpec := range importSpecs {
          path := importSpec.Path.Value
          if path == "fmt" {
            fmtFound = true
            break
          }
        }
        if !fmtFound {
          fmt.Fprintf(log, "fmt not imported\n")
          // If the fmt package was not imported, make a new ast.importSpec
          //  node. We won't try to set accurate token positions because
          //  we're only using the AST to print out new source code.
          path := ast.BasicLit{ Kind: token.STRING, Value: "\"fmt\"" }
          importSpec := ast.ImportSpec{ Path: &path }
          importDecl := ast.GenDecl{
            Tok: token.IMPORT,
            Specs: []ast.Spec{ &importSpec },
          }
          fileNode.Decls = append(fileNode.Decls, nil)
          copy(fileNode.Decls[1:], fileNode.Decls)
          fileNode.Decls[0] = &importDecl
        }

        // Print with a custom configuration: soft tabs of two spaces each.
        config := printer.Config{ Mode: printer.UseSpaces, Tabwidth: 2 }
        (&config).Fprint(writer, fileSet, fileNode)
      } else {
        // If there was a Go parsing error, print our template-generated
        //  source code, followed by the Go parser's error message.
        writer.Write(output.Bytes())
        writer.WriteString("\n----------\n")
        writer.WriteString(fmt.Sprintf("Go parsing error: %s\n", error))
      }
    } else {
      writer.WriteString(fmt.Sprintf("template parsing error: %s\n", error))
    }
  }
}
<commit_msg>Use astutil.AddImport instead of custom code<commit_after>package main

import (
  "os"
  foo "fmt"
  "io"
  "bufio"
  "strings"
  "path/filepath"
  "errors"
  "bytes"
  "go/token"
  "go/parser"
  "go/printer"
  "golang.org/x/tools/go/ast/astutil"
)

const verbose bool = false

var output bytes.Buffer

//--- Linear pattern matcher

type Pattern struct {
  Text []rune
  Length, Pos int
}

func newPattern(s string) Pattern {
  runes := []rune(s)
  return Pattern{ Text: runes, Length: len(runes) }
}

// Next returns true if Pos advances past the last character of Text.
func (pattern *Pattern) Next(ch rune) bool {
  // If Pos is past the end of Text, reset it to the beginning.
  if pattern.Pos == pattern.Length {
    pattern.Pos = 0
  }
  // Try to match the current rune in Text.
  if ch == pattern.Text[pattern.Pos] {
    pattern.Pos++
  }
  // Check for a complete match.
  return pattern.Pos == pattern.Length
}

//--- Template parsing and output generation

type TemplateEntry struct {
  SitePath, HardPath string
  FileInfo os.FileInfo
  InsertionLine int
}

func (entry TemplateEntry) String() string {
  if entry.InsertionLine == 0 {
    return entry.SitePath
  }
  return fmt.Sprintf("-> line %d: %s", entry.InsertionLine, entry.SitePath)
}

func makeTemplateEntry(siteRoot, startDir, sitePath string,
    insertionLine int) (*TemplateEntry, error) {
  hardPath := makeHardPath(siteRoot, startDir, sitePath)
  fileInfo, error := os.Stat(hardPath)
  if error != nil {
    return nil, error
  }
  entry := TemplateEntry{
      SitePath: sitePath,
      HardPath: hardPath,
      FileInfo: fileInfo,
      InsertionLine: insertionLine,
    }
  return &entry, nil
}

func makeHardPath(siteRoot, startDir, sitePath string) string {
  // A hard path names a location in the physical file system rather than
  //  in the website's directory structure. It is either an absolute path
  //  or a relative path with respect to the directory containing the
  //  top-level template that is being parsed.
  var dir string
  if filepath.IsAbs(sitePath) {
    dir = siteRoot
  } else {
    dir = startDir
  }
  // Note that filepath.Join automatically performs filepath.Clean, thus
  //  returning a lexically unique form of the path. However, the path
  //  does not uniquely identify a file if it includes a symbolic link.
  //  Therefore, we cannot rely on string comparison to prevent cycles.
  hardPath := filepath.Join(dir, sitePath)
  return hardPath
}

func parse(templatePath string) error {

  // We resolve absolute paths with the website root.
  siteRoot := "/var/www/dd1"  // Stub. We'll get the real value from Apache.
  // We resolve relative paths using the starting directory.
  startDir := filepath.Dir(templatePath)

  // Make an insertion stack with a top-level entry.
  entry, error := makeTemplateEntry(siteRoot, startDir, templatePath, 0)
  if error != nil {
    return error
  }
  stack := []*TemplateEntry{ entry }
  return doParse(siteRoot, startDir, stack)
}

func doParse(siteRoot, startDir string, stack []*TemplateEntry) error {
  current := stack[len(stack)-1]
  if verbose {
    fmt.Fprintf(&output, "// start \"%s\"\n", current.SitePath)
  }
  var topLevel bool
  if len(stack) == 1 {
    topLevel = true
  }

  // Check for an insertion cycle.
  for i := len(stack)-2; i >= 0; i-- {
    ancestor := stack[i]
    if os.SameFile(ancestor.FileInfo, current.FileInfo) {
      lines := []string{ "doParse: insertion cycle" }
      for j := i; j < len(stack); j++ {
        lines = append(lines, stack[j].String())
      }
      message := fmt.Sprintf(strings.Join(lines, "\n  "))
      return errors.New(message)
    }
  }

  var error error
  var file *os.File

  file, error = os.Open(current.HardPath)
  if error != nil {
    return error
  }

  reader := bufio.NewReader(file)
  writer := bufio.NewWriter(os.Stdout)
  defer writer.Flush()

  codePattern := newPattern("<?code")
  insertPattern := newPattern("<?insert")
  openPatterns := []*Pattern{ &codePattern, &insertPattern }
  var open *Pattern
  close := newPattern("?>")

  var buffer []rune
  var ch rune
  var size int
  countBytes, countRunes := 0, 0
  lineIndex := 1
  prefix := true

  for {
    ch, size, error = reader.ReadRune()
    if error == nil {
      buffer = append(buffer, ch)
      countBytes += size
      countRunes += 1
      if ch == '\n' {
        lineIndex += 1
      }
    } else {
      content := string(buffer)
      if topLevel {
        content = strings.TrimSpace(content)
      }
      emitStatic(content)
      break
    }

    if open == nil {
      for _, pattern := range openPatterns {
        if pattern.Next(ch) {
          open = pattern
          content := string(buffer[0:len(buffer)-open.Length])
          if prefix {
            if topLevel {
              content = strings.TrimSpace(content)
            }
            prefix = false
          }
          emitStatic(content)
          buffer = []rune{}
        }
      }
    } else {
      if close.Next(ch) {
        content := buffer[0:len(buffer)-close.Length]
        if open == &codePattern {
          emitCode(string(content))
        } else if open == &insertPattern {
          childPath := strings.TrimSpace(string(content))
          entry, error := makeTemplateEntry(siteRoot, startDir, childPath,
              lineIndex)
          if error != nil {
            return error
          }
          stack = append(stack, entry)
          error = doParse(siteRoot, startDir, stack)
          if error != nil {
            return error
          }
          stack = stack[0:len(stack)-1]
        }
        open = nil
        buffer = []rune{}
      }
    }
  }
  if verbose {
    fmt.Fprintf(&output, "// finish \"%s\"\n", current.SitePath)
    fmt.Fprintf(&output, "// read %d bytes, %d runes\n", countBytes, countRunes)
    fmt.Fprintf(&output, "// finished on line %d\n", lineIndex)
  }
  if error == io.EOF {
    return nil
  }
  return error
}

func emitCode(content string) {
  fmt.Fprint(&output, content)
}

func emitStatic(content string) {
  if len(content) == 0 {
    return
  }
  from := 0
  for pos, ch := range content {
    if ch == '`' {
      if pos != from {
        raw := fmt.Sprintf("`%s`", content[from:pos])
        emitRaw(raw)
      }
      emitRaw("'`'")
      from = pos+1
    }
  }
  if from != len(content) {
    raw := fmt.Sprintf("`%s`", content[from:len(content)])
    emitRaw(raw)
  }
}
func emitRaw(s string) {
  fmt.Fprintf(&output, "fmt.Print(%s)\n", s)
}

func main() {
  writer := bufio.NewWriter(os.Stdout)
  defer writer.Flush()
  log := os.Stderr

  numFiles := len(os.Args)-1
  if numFiles == 0 {
    writer.WriteString("No files specified.\n")
    return
  }
  for argIx := 1; argIx <= numFiles; argIx++ {
    // Parse the top-level template. The resulting source code goes into
    //  a global variable called output, the type of which is bytes.Buffer.
    path := os.Args[argIx]
    error := parse(path)
    if error == nil {
      // Use Go's parser on the code that came out of the template parser.
      fileSet := token.NewFileSet()
      fileNode, error := parser.ParseFile(fileSet, "output", output.Bytes(),
          parser.ParseComments)
      if error == nil {
        // Before injecting an import statement, we have to check for
        //  various collisions. The astutil.AddNameImport function only
        //  checks for an import with the same path. There are more cases.
        //  0. no import has the name or path "fmt":
        //    import "fmt"
        //  1. an import has the path "fmt" with a different name:
        //    import "fmt"
        //  2. an import has the name "fmt" with a different path:
        //    import _fmt0 "fmt" (or _fmt1, ...) and write _fmt0.Printf
        //  3. an import has the name and path "fmt":
        //    do nothing
        //  to inject an import statement. That function includes a
        //  import of fmt because astutil.AddImport includes such a check.
        //  However, it does not check for aliased package names. We have
        //  to do this ourselves. If there is a conflict, we correct it by
        //  aliasing fmt to _fmt0 (or _fmt1, _fmt2, ...).
        //  Hmm... what if fmt is imported but aliased to something else?
        added := astutil.AddImport(fileSet, fileNode, "fmt")
        fmt.Fprintf(log, "added: %t\n", added)

        // Print with a custom configuration: soft tabs of two spaces each.
        config := printer.Config{ Mode: printer.UseSpaces, Tabwidth: 2 }
        (&config).Fprint(writer, fileSet, fileNode)
      } else {
        // If there was a Go parsing error, print our template-generated
        //  source code, followed by the Go parser's error message.
        writer.Write(output.Bytes())
        writer.WriteString("\n----------\n")
        writer.WriteString(fmt.Sprintf("Go parsing error: %s\n", error))
      }
    } else {
      writer.WriteString(fmt.Sprintf("template parsing error: %s\n", error))
    }
  }
}
<|endoftext|>


<commit_before>package main

import (
	"github.com/influxdata/influxdb/v2/cmd/influx/config"
	"github.com/spf13/cobra"
)

func cmdConfig(f *globalFlags, opt genericCLIOpts) *cobra.Command {
	path, dir, err := defaultConfigPath()
	if err != nil {
		panic(err)
	}
	builder := cmdConfigBuilder{
		genericCLIOpts: opt,
		globalFlags:    f,
		svc:            config.NewLocalConfigSVC(path, dir),
	}
	builder.globalFlags = f
	return builder.cmd()
}

type cmdConfigBuilder struct {
	genericCLIOpts
	*globalFlags

	name   string
	url    string
	token  string
	active bool
	org    string

	json        bool
	hideHeaders bool

	svc config.ConfigsService
}

func (b *cmdConfigBuilder) cmd() *cobra.Command {
	cmd := b.newCmd("config [config name]", b.cmdSwitchActiveRunEFn, false)
	cmd.Short = "Config management commands"
	cmd.Args = cobra.ArbitraryArgs

	cmd.AddCommand(
		b.cmdCreate(),
		b.cmdDelete(),
		b.cmdUpdate(),
		b.cmdList(),
	)
	return cmd
}

func (b *cmdConfigBuilder) cmdSwitchActiveRunEFn(cmd *cobra.Command, args []string) error {
	if len(args) > 0 {
		cfg, err := b.svc.SwitchActive(args[0])
		if err != nil {
			return err
		}

		return b.printConfigs(configPrintOpts{
			config: cfg,
		})
	}

	configs, err := b.svc.ListConfigs()
	if err != nil {
		return err
	}

	var active config.Config
	for _, cfg := range configs {
		if cfg.Active {
			active = cfg
			break
		}
	}
	if !active.Active {
		return nil
	}

	return b.printConfigs(configPrintOpts{
		config: active,
	})
}

func (b *cmdConfigBuilder) cmdCreate() *cobra.Command {
	cmd := b.newCmd("create", b.cmdCreateRunEFn, false)
	cmd.Short = "Create config"

	b.registerPrintFlags(cmd)
	cmd.Flags().StringVarP(&b.name, "name", "n", "", "The config name (required)")
	cmd.MarkFlagRequired("name")
	cmd.Flags().StringVarP(&b.token, "token", "t", "", "The config token (required)")
	cmd.MarkFlagRequired("token")
	cmd.Flags().StringVarP(&b.url, "url", "u", "", "The config url (required)")
	cmd.MarkFlagRequired("url")

	cmd.Flags().BoolVarP(&b.active, "active", "a", false, "Set it to be the active config")
	cmd.Flags().StringVarP(&b.org, "org", "o", "", "The optional organization name")
	return cmd
}

func (b *cmdConfigBuilder) cmdCreateRunEFn(*cobra.Command, []string) error {
	cfg, err := b.svc.CreateConfig(config.Config{
		Name:   b.name,
		Host:   b.url,
		Token:  b.token,
		Org:    b.org,
		Active: b.active,
	})
	if err != nil {
		return err
	}

	return b.printConfigs(configPrintOpts{
		config: cfg,
	})
}

func (b *cmdConfigBuilder) cmdDelete() *cobra.Command {
	cmd := b.newCmd("delete", b.cmdDeleteRunEFn, false)
	cmd.Short = "Delete config"

	b.registerPrintFlags(cmd)
	cmd.Flags().StringVarP(&b.name, "name", "n", "", "The config name (required)")
	cmd.MarkFlagRequired("name")

	return cmd
}

func (b *cmdConfigBuilder) cmdDeleteRunEFn(cmd *cobra.Command, args []string) error {
	cfg, err := b.svc.DeleteConfig(b.name)
	if err != nil {
		return err
	}

	return b.printConfigs(configPrintOpts{
		delete: true,
		config: cfg,
	})
}

func (b *cmdConfigBuilder) cmdUpdate() *cobra.Command {
	cmd := b.newCmd("set", b.cmdUpdateRunEFn, false)
	cmd.Aliases = []string{"update"}
	cmd.Short = "Update config"

	b.registerPrintFlags(cmd)
	cmd.Flags().StringVarP(&b.name, "name", "n", "", "The config name (required)")
	cmd.MarkFlagRequired("name")

	cmd.Flags().StringVarP(&b.token, "token", "t", "", "The config token (required)")
	cmd.Flags().StringVarP(&b.url, "url", "u", "", "The config url (required)")
	cmd.Flags().BoolVarP(&b.active, "active", "a", false, "Set it to be the active config")
	cmd.Flags().StringVarP(&b.org, "org", "o", "", "The optional organization name")
	return cmd
}

func (b *cmdConfigBuilder) cmdUpdateRunEFn(*cobra.Command, []string) error {
	cfg, err := b.svc.UpdateConfig(config.Config{
		Name:   b.name,
		Host:   b.url,
		Token:  b.token,
		Org:    b.org,
		Active: b.active,
	})
	if err != nil {
		return err
	}

	return b.printConfigs(configPrintOpts{
		config: cfg,
	})
}

func (b *cmdConfigBuilder) cmdList() *cobra.Command {
	cmd := b.newCmd("list", b.cmdListRunEFn, false)
	cmd.Aliases = []string{"ls"}
	cmd.Short = "List configs"
	b.registerPrintFlags(cmd)
	return cmd
}

func (b *cmdConfigBuilder) cmdListRunEFn(*cobra.Command, []string) error {
	cfgs, err := b.svc.ListConfigs()
	if err != nil {
		return err
	}

	return b.printConfigs(configPrintOpts{configs: cfgs})
}

func (b *cmdConfigBuilder) registerPrintFlags(cmd *cobra.Command) {
	registerPrintOptions(cmd, &b.hideHeaders, &b.json)
}

func (b *cmdConfigBuilder) printConfigs(opts configPrintOpts) error {
	if b.json {
		var v interface{} = opts.configs
		if opts.configs == nil {
			v = opts.config
		}
		return b.writeJSON(v)
	}

	w := b.newTabWriter()
	defer w.Flush()

	w.HideHeaders(b.hideHeaders)

	headers := []string{"Active", "Name", "URL", "Org"}
	if opts.delete {
		headers = append(headers, "Deleted")
	}
	w.WriteHeaders(headers...)

	if opts.configs == nil {
		opts.configs = config.Configs{
			opts.config.Name: opts.config,
		}
	}
	for _, c := range opts.configs {
		var active string
		if c.Active {
			active = "*"
		}
		m := map[string]interface{}{
			"Active": active,
			"Name":   c.Name,
			"URL":    c.Host,
			"Org":    c.Org,
		}
		if opts.delete {
			m["Deleted"] = true
		}

		w.Write(m)
	}

	return nil
}

type configPrintOpts struct {
	delete  bool
	config  config.Config
	configs config.Configs
}
<commit_msg>chore(influx): refactor influx config delete cmd to for consistency and update usage<commit_after>package main

import (
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/cmd/influx/config"
	"github.com/spf13/cobra"
)

func cmdConfig(f *globalFlags, opt genericCLIOpts) *cobra.Command {
	path, dir, err := defaultConfigPath()
	if err != nil {
		panic(err)
	}
	builder := cmdConfigBuilder{
		genericCLIOpts: opt,
		globalFlags:    f,
		svc:            config.NewLocalConfigSVC(path, dir),
	}
	builder.globalFlags = f
	return builder.cmd()
}

type cmdConfigBuilder struct {
	genericCLIOpts
	*globalFlags

	name   string
	url    string
	token  string
	active bool
	org    string

	json        bool
	hideHeaders bool

	svc config.ConfigsService
}

func (b *cmdConfigBuilder) cmd() *cobra.Command {
	cmd := b.newCmd("config [config name]", b.cmdSwitchActiveRunEFn, false)
	cmd.Args = cobra.ArbitraryArgs
	cmd.Short = "Config management commands"
	cmd.Long = `
	Providing no argument to the config command will print the active configuration. When
	an argument is provided, the active config will be switched to the config with a name
	matching that of the argument provided.

	Examples:
		# show active config
		influx config

		# set active config to previously active config
		influx config -

		# set active config
		influx config $CONFIG_NAME

	The influx config command displays the active InfluxDB connection configuration and
	manages multiple connection configurations stored, by default, in ~/.influxdbv2/configs.
	Each connection includes a URL, token, associated organization, and active setting.
	InfluxDB reads the token from the active connection configuration, so you don't have
	to manually enter a token to log into InfluxDB.

	For information about the config command, see
	https://v2.docs.influxdata.com/v2.0/reference/cli/influx/config
`

	cmd.AddCommand(
		b.cmdCreate(),
		b.cmdDelete(),
		b.cmdUpdate(),
		b.cmdList(),
	)
	return cmd
}

func (b *cmdConfigBuilder) cmdSwitchActiveRunEFn(cmd *cobra.Command, args []string) error {
	if len(args) > 0 {
		cfg, err := b.svc.SwitchActive(args[0])
		if err != nil {
			return err
		}

		return b.printConfigs(configPrintOpts{
			config: cfg,
		})
	}

	configs, err := b.svc.ListConfigs()
	if err != nil {
		return err
	}

	var active config.Config
	for _, cfg := range configs {
		if cfg.Active {
			active = cfg
			break
		}
	}
	if !active.Active {
		return nil
	}

	return b.printConfigs(configPrintOpts{
		config: active,
	})
}

func (b *cmdConfigBuilder) cmdCreate() *cobra.Command {
	cmd := b.newCmd("create", b.cmdCreateRunEFn, false)
	cmd.Short = "Create config"
	cmd.Long = `
	The influx config create command creates a new InfluxDB connection configuration
	and stores it in the configs file (by default, stored at ~/.influxdbv2/configs).

	Examples:
		# create a config and set it active
		influx config create -a -n $CFG_NAME -u $HOST_URL -t $TOKEN -o $ORG_NAME

		# create a config and without setting it active
		influx config create -n $CFG_NAME -u $HOST_URL -t $TOKEN -o $ORG_NAME

	For information about the config command, see
	https://v2.docs.influxdata.com/v2.0/reference/cli/influx/config
	and
	https://v2.docs.influxdata.com/v2.0/reference/cli/influx/config/create`

	b.registerPrintFlags(cmd)
	cmd.Flags().StringVarP(&b.name, "name", "n", "", "The config name (required)")
	cmd.MarkFlagRequired("name")
	cmd.Flags().StringVarP(&b.token, "token", "t", "", "The config token (required)")
	cmd.MarkFlagRequired("token")
	cmd.Flags().StringVarP(&b.url, "url", "u", "", "The config url (required)")
	cmd.MarkFlagRequired("url")

	cmd.Flags().BoolVarP(&b.active, "active", "a", false, "Set it to be the active config")
	cmd.Flags().StringVarP(&b.org, "org", "o", "", "The optional organization name")
	return cmd
}

func (b *cmdConfigBuilder) cmdCreateRunEFn(*cobra.Command, []string) error {
	cfg, err := b.svc.CreateConfig(config.Config{
		Name:   b.name,
		Host:   b.url,
		Token:  b.token,
		Org:    b.org,
		Active: b.active,
	})
	if err != nil {
		return err
	}

	return b.printConfigs(configPrintOpts{
		config: cfg,
	})
}

func (b *cmdConfigBuilder) cmdDelete() *cobra.Command {
	cmd := b.newCmd("rm [cfg_name]", b.cmdDeleteRunEFn, false)
	cmd.Aliases = []string{"delete", "remove"}
	cmd.Args = cobra.ArbitraryArgs
	cmd.Short = "Delete config"
	cmd.Long = `
	The influx config delete command deletes an InfluxDB connection configuration from
	the configs file (by default, stored at ~/.influxdbv2/configs).

	Examples:
		# delete a config
		influx config rm $CFG_NAME

		# delete multiple configs
		influx config rm $CFG_NAME_1 $CFG_NAME_2

	For information about the config command, see
	https://v2.docs.influxdata.com/v2.0/reference/cli/influx/config
	and
	https://v2.docs.influxdata.com/v2.0/reference/cli/influx/config/remove`

	b.registerPrintFlags(cmd)
	cmd.Flags().StringVarP(&b.name, "name", "n", "", "The config name (required)")
	cmd.Flags().MarkHidden("name")

	return cmd
}

func (b *cmdConfigBuilder) cmdDeleteRunEFn(cmd *cobra.Command, args []string) error {
	deletedConfigs := make(config.Configs)
	for _, name := range append(args, b.name) {
		if name == "" {
			continue
		}

		cfg, err := b.svc.DeleteConfig(name)
		if influxdb.ErrorCode(err) == influxdb.ENotFound {
			continue
		}
		if err != nil {
			return err
		}
		deletedConfigs[name] = cfg
	}

	return b.printConfigs(configPrintOpts{
		delete:  true,
		configs: deletedConfigs,
	})
}

func (b *cmdConfigBuilder) cmdUpdate() *cobra.Command {
	cmd := b.newCmd("set", b.cmdUpdateRunEFn, false)
	cmd.Aliases = []string{"update"}
	cmd.Short = "Update config"
	cmd.Long = `
	The influx config set command updates information in an InfluxDB connection
	configuration in the configs file (by default, stored at ~/.influxdbv2/configs).

	Examples:
		# update a config and set active
		influx config set -a -n $CFG_NAME -u $HOST_URL -t $TOKEN -o $ORG_NAME

		# update a config and do not set to active
		influx config set -n $CFG_NAME -u $HOST_URL -t $TOKEN -o $ORG_NAME

	For information about the config command, see
	https://v2.docs.influxdata.com/v2.0/reference/cli/influx/config
	and
	https://v2.docs.influxdata.com/v2.0/reference/cli/influx/config/set`

	b.registerPrintFlags(cmd)
	cmd.Flags().StringVarP(&b.name, "name", "n", "", "The config name (required)")
	cmd.MarkFlagRequired("name")

	cmd.Flags().StringVarP(&b.token, "token", "t", "", "The config token (required)")
	cmd.Flags().StringVarP(&b.url, "url", "u", "", "The config url (required)")
	cmd.Flags().BoolVarP(&b.active, "active", "a", false, "Set it to be the active config")
	cmd.Flags().StringVarP(&b.org, "org", "o", "", "The optional organization name")
	return cmd
}

func (b *cmdConfigBuilder) cmdUpdateRunEFn(*cobra.Command, []string) error {
	cfg, err := b.svc.UpdateConfig(config.Config{
		Name:   b.name,
		Host:   b.url,
		Token:  b.token,
		Org:    b.org,
		Active: b.active,
	})
	if err != nil {
		return err
	}

	return b.printConfigs(configPrintOpts{
		config: cfg,
	})
}

func (b *cmdConfigBuilder) cmdList() *cobra.Command {
	cmd := b.newCmd("ls", b.cmdListRunEFn, false)
	cmd.Aliases = []string{"list"}
	cmd.Short = "List configs"
	cmd.Long = `
	The influx config ls command lists all InfluxDB connection configurations
	in the configs file (by default, stored at ~/.influxdbv2/configs). Each
	connection configuration includes a URL, authentication token, and active
	setting. An asterisk (*) indicates the active configuration.

	Examples:
		# list configs
		influx config ls

		# list configs with long alias
		influx config list

	For information about the config command, see
	https://v2.docs.influxdata.com/v2.0/reference/cli/influx/config
	and
	https://v2.docs.influxdata.com/v2.0/reference/cli/influx/config/list`
	b.registerPrintFlags(cmd)
	return cmd
}

func (b *cmdConfigBuilder) cmdListRunEFn(*cobra.Command, []string) error {
	cfgs, err := b.svc.ListConfigs()
	if err != nil {
		return err
	}

	return b.printConfigs(configPrintOpts{configs: cfgs})
}

func (b *cmdConfigBuilder) registerPrintFlags(cmd *cobra.Command) {
	registerPrintOptions(cmd, &b.hideHeaders, &b.json)
}

func (b *cmdConfigBuilder) printConfigs(opts configPrintOpts) error {
	if b.json {
		var v interface{} = opts.configs
		if opts.configs == nil {
			v = opts.config
		}
		return b.writeJSON(v)
	}

	w := b.newTabWriter()
	defer w.Flush()

	w.HideHeaders(b.hideHeaders)

	headers := []string{"Active", "Name", "URL", "Org"}
	if opts.delete {
		headers = append(headers, "Deleted")
	}
	w.WriteHeaders(headers...)

	if opts.configs == nil {
		opts.configs = config.Configs{
			opts.config.Name: opts.config,
		}
	}
	for _, c := range opts.configs {
		var active string
		if c.Active {
			active = "*"
		}
		m := map[string]interface{}{
			"Active": active,
			"Name":   c.Name,
			"URL":    c.Host,
			"Org":    c.Org,
		}
		if opts.delete {
			m["Deleted"] = true
		}

		w.Write(m)
	}

	return nil
}

type configPrintOpts struct {
	delete  bool
	config  config.Config
	configs config.Configs
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"github.com/carbontwelve/go-irc-stats/ircstats"
	"log"
	"os"
	"path/filepath"
)

var (
	Version string
	Build string

	logReader ircstats.IrcLogReader

	version = flag.Bool("version", false, "Display executable version and build.")
	cleardb = flag.Bool("del", false, "Delete database and re-parse from the beginning")
	verbose = flag.Bool("v", false, "Display actual output")
	configPath = flag.String("c", "config.yaml", "Path to config.yaml")
	cwd = flag.String("d", "", "change to this directory before doing anything")
)

func main() {
	flag.Usage = func() {
		fmt.Fprintln(os.Stderr, `ircstats is a golang port from ruby of 0x263b/Stats
Usage:
        ircstats [options] ...
The options are:
  -h    Display this help text`)
		flag.PrintDefaults()
		os.Exit(1)
	}

	// Parse command line flags
	flag.Parse()

	if *version {
		fmt.Printf("Version: %s Build %s\n", Version, Build)
		os.Exit(0)
	}

	// Should we be noisy?
	// If false, all stdout messages will be muted
	if *verbose {
		fmt.Println("Being Loud!")
	}

	// Should we change the current working directory? (This is super useful when testing)
	if *cwd != "" {
		logError(os.Chdir(*cwd))
	}

	// Load Configuration
	config := ircstats.Config{}
	logError(config.Load(*configPath))

	// Load Data Store (I call it Database, but its actually a binary file)
	db := ircstats.Database{}
	if *cleardb {
		logError(os.Remove(config.DatabaseLocation))
	}
	db.Load(config.DatabaseLocation)

	fmt.Println("Last Parsed: ", db.LastGenerated)
	logReader = *ircstats.NewIrcLogReader(config)

	// Load log file and parse any new lines
	// If config.Location is a directory, identify all files within and load one after the other
	var logReaderErr error
	if isDirectory(config.Location) {
		logReaderErr = filepath.Walk(config.Location, func(path string, f os.FileInfo, err error) error {
			if isDirectory(path) == true {
				return nil;
			}
			return logReader.Load(path, &db)
		});
	} else {
		logReaderErr = logReader.Load(config.Location, &db)
	}

	logError(logReaderErr)

	//
	// Save database to disk
	//
	logError(db.Save(config.DatabaseLocation))

	vd := *ircstats.NewViewData(config)
	vd.Calculate(db)
	vd.JsonData.Debug()

	//
	// Generate the template
	//
	v := *ircstats.NewView()
	logError(v.Parse("template.html", vd))
}

// Returns true if path is a directory
func isDirectory(path string) bool {
	if info, err := os.Stat(path); err == nil && info.IsDir() {
		return true;
	}
	return false;
}

// Logs an error if err is not nil
func logError(err error) {
	if err != nil {
		if *verbose {
			panic(err)
		} else {
			log.Fatal(err)
		}
	}
}
<commit_msg>Added todo, do we even need to panic on flag?<commit_after>package main

import (
	"flag"
	"fmt"
	"github.com/carbontwelve/go-irc-stats/ircstats"
	"log"
	"os"
	"path/filepath"
)

var (
	Version string
	Build string

	logReader ircstats.IrcLogReader

	version = flag.Bool("version", false, "Display executable version and build.")
	cleardb = flag.Bool("del", false, "Delete database and re-parse from the beginning")
	verbose = flag.Bool("v", false, "Display actual output")
	configPath = flag.String("c", "config.yaml", "Path to config.yaml")
	cwd = flag.String("d", "", "change to this directory before doing anything")
)

func main() {
	flag.Usage = func() {
		fmt.Fprintln(os.Stderr, `ircstats is a golang port from ruby of 0x263b/Stats
Usage:
        ircstats [options] ...
The options are:
  -h    Display this help text`)
		flag.PrintDefaults()
		os.Exit(1)
	}

	// Parse command line flags
	flag.Parse()

	if *version {
		fmt.Printf("Version: %s Build %s\n", Version, Build)
		os.Exit(0)
	}

	// Should we be noisy?
	// If false, all stdout messages will be muted
	if *verbose {
		fmt.Println("Being Loud!")
	}

	// Should we change the current working directory? (This is super useful when testing)
	if *cwd != "" {
		logError(os.Chdir(*cwd))
	}

	// Load Configuration
	config := ircstats.Config{}
	logError(config.Load(*configPath))

	// Load Data Store (I call it Database, but its actually a binary file)
	db := ircstats.Database{}
	if *cleardb {
		logError(os.Remove(config.DatabaseLocation))
	}
	db.Load(config.DatabaseLocation)

	fmt.Println("Last Parsed: ", db.LastGenerated)
	logReader = *ircstats.NewIrcLogReader(config)

	// Load log file and parse any new lines
	// If config.Location is a directory, identify all files within and load one after the other
	var logReaderErr error
	if isDirectory(config.Location) {
		logReaderErr = filepath.Walk(config.Location, func(path string, f os.FileInfo, err error) error {
			if isDirectory(path) == true {
				return nil;
			}
			return logReader.Load(path, &db)
		});
	} else {
		logReaderErr = logReader.Load(config.Location, &db)
	}

	logError(logReaderErr)

	//
	// Save database to disk
	//
	logError(db.Save(config.DatabaseLocation))

	vd := *ircstats.NewViewData(config)
	vd.Calculate(db)
	vd.JsonData.Debug()

	//
	// Generate the template
	//
	v := *ircstats.NewView()
	logError(v.Parse("template.html", vd))
}

// Returns true if path is a directory
func isDirectory(path string) bool {
	if info, err := os.Stat(path); err == nil && info.IsDir() {
		return true;
	}
	return false;
}

// Logs an error if err is not nil
func logError(err error) {
	if err != nil {
		if *verbose {
			panic(err) //@todo should this look for a debug flag instead?
		} else {
			log.Fatal(err)
		}
	}
}
<|endoftext|>


<commit_before>package main

import (
	"os"
	"os/exec"
	"regexp"
	"strings"

	"github.com/gdamore/tcell"
)

func HandleShellCommand(input string, view *View) {
	inputCmd := strings.Split(input, " ")[0]
	args := strings.Split(input, " ")[1:]

	// Execute Command
	cmdout := exec.Command(inputCmd, args...)
	output, _ := cmdout.CombinedOutput()
	outstring := string(output)
	totalLines := strings.Split(outstring, "\n")
	if len(totalLines) == 2 {
		messenger.Message(outstring)
		return
	}
	if outstring != "" {
		// Display nonblank output
		DisplayBlock(outstring)
	}
}

// DisplayBlock displays txt
// It blocks the main loop
func DisplayBlock(text string) {
	topline := 0
	_, height := screen.Size()
	screen.HideCursor()
	totalLines := strings.Split(text, "\n")
	for {
		screen.Clear()

		lineEnd := topline + height
		if lineEnd > len(totalLines) {
			lineEnd = len(totalLines)
		}
		lines := totalLines[topline:lineEnd]
		for y, line := range lines {
			for x, ch := range line {
				st := defStyle
				screen.SetContent(x, y, ch, nil, st)
			}
		}

		screen.Show()

		event := screen.PollEvent()
		switch e := event.(type) {
		case *tcell.EventResize:
			_, height = e.Size()
		case *tcell.EventKey:
			switch e.Key() {
			case tcell.KeyUp:
				if topline > 0 {
					topline--
				}
			case tcell.KeyDown:
				if topline < len(totalLines)-height {
					topline++
				}
			case tcell.KeyCtrlQ, tcell.KeyCtrlW, tcell.KeyEscape, tcell.KeyCtrlC:
				return
			}
		}
	}
}

// HandleCommand handles input from the user
func HandleCommand(input string, view *View) {
	inputCmd := strings.Split(input, " ")[0]
	args := strings.Split(input, " ")[1:]

	commands := []string{"set", "quit", "save", "replace"}

	i := 0
	cmd := inputCmd

	for _, c := range commands {
		if strings.HasPrefix(c, inputCmd) {
			i++
			cmd = c
		}
	}
	if i == 1 {
		inputCmd = cmd
	}

	switch inputCmd {
	case "set":
		SetOption(view, args)
	case "quit":
		if view.CanClose("Quit anyway? ") {
			screen.Fini()
			os.Exit(0)
		}
	case "save":
		view.Save()
	case "replace":
		r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`)
		replaceCmd := r.FindAllString(strings.Join(args, " "), -1)
		if len(replaceCmd) < 2 {
			messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
			return
		}

		var flags string
		if len(replaceCmd) == 3 {
			// The user included some flags
			flags = replaceCmd[2]
		}

		search := string(replaceCmd[0])
		replace := string(replaceCmd[1])

		if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) {
			search = search[1 : len(search)-1]
		}
		if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) {
			replace = replace[1 : len(replace)-1]
		}

		search = strings.Replace(search, `\"`, `"`, -1)
		replace = strings.Replace(replace, `\"`, `"`, -1)

		// messenger.Error(search + " -> " + replace)

		regex, err := regexp.Compile(search)
		if err != nil {
			messenger.Error(err.Error())
			return
		}

		found := false
		for {
			match := regex.FindStringIndex(view.buf.text)
			if match == nil {
				break
			}
			found = true
			if strings.Contains(flags, "c") {
				// 	// The 'check' flag was used
				// 	if messenger.YesNoPrompt("Perform replacement?") {
				// 		view.eh.Replace(match[0], match[1], replace)
				// 	} else {
				// 		continue
				// 	}
			}
			view.eh.Replace(match[0], match[1], replace)
		}
		if !found {
			messenger.Message("Nothing matched " + search)
		}
	default:
		messenger.Error("Unknown command: " + inputCmd)
	}
}
<commit_msg>shell command output is held in buffer until completion<commit_after>package main

import (
	"bytes"
	"os"
	"os/exec"
	"regexp"
	"strings"

	"github.com/gdamore/tcell"
)

// HandleShellCommand runs the shell command and outputs to DisplayBlock
func HandleShellCommand(input string, view *View) {
	inputCmd := strings.Split(input, " ")[0]
	args := strings.Split(input, " ")[1:]

	// Execute Command
	cmd := exec.Command(inputCmd, args...)
	outputBytes := &bytes.Buffer{}

	cmd.Stdout = outputBytes // send output to buffer
	cmd.Start()
	cmd.Wait() // wait for command to finish
	outstring := outputBytes.String()
	totalLines := strings.Split(outstring, "\n")

	if len(totalLines) < 3 {
		messenger.Message(outstring)
		return
	}

	if outstring != "" {
		// Display nonblank output
		DisplayBlock(outstring)
	}
}

// DisplayBlock displays txt
// It blocks the main loop
func DisplayBlock(text string) {
	topline := 0
	_, height := screen.Size()
	screen.HideCursor()
	totalLines := strings.Split(text, "\n")
	for {
		screen.Clear()

		lineEnd := topline + height
		if lineEnd > len(totalLines) {
			lineEnd = len(totalLines)
		}
		lines := totalLines[topline:lineEnd]
		for y, line := range lines {
			for x, ch := range line {
				st := defStyle
				screen.SetContent(x, y, ch, nil, st)
			}
		}

		screen.Show()

		event := screen.PollEvent()
		switch e := event.(type) {
		case *tcell.EventResize:
			_, height = e.Size()
		case *tcell.EventKey:
			switch e.Key() {
			case tcell.KeyPgUp:
				if topline > height {
					topline = topline - height
				} else {
					topline = 0
				}
			case tcell.KeyPgDn:
				if topline < len(totalLines)-height {
					topline = topline + height
				}
			case tcell.KeyUp:
				if topline > 0 {
					topline--
				}
			case tcell.KeyDown:
				if topline < len(totalLines)-height {
					topline++
				}
			case tcell.KeyCtrlQ, tcell.KeyCtrlW, tcell.KeyEscape, tcell.KeyCtrlC:
				return
			default:
				return
			}
		}
	}
}

// HandleCommand handles input from the user
func HandleCommand(input string, view *View) {
	inputCmd := strings.Split(input, " ")[0]
	args := strings.Split(input, " ")[1:]

	commands := []string{"set", "quit", "save", "replace"}

	i := 0
	cmd := inputCmd

	for _, c := range commands {
		if strings.HasPrefix(c, inputCmd) {
			i++
			cmd = c
		}
	}
	if i == 1 {
		inputCmd = cmd
	}

	switch inputCmd {
	case "set":
		SetOption(view, args)
	case "quit":
		if view.CanClose("Quit anyway? ") {
			screen.Fini()
			os.Exit(0)
		}
	case "save":
		view.Save()
	case "replace":
		r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`)
		replaceCmd := r.FindAllString(strings.Join(args, " "), -1)
		if len(replaceCmd) < 2 {
			messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
			return
		}

		var flags string
		if len(replaceCmd) == 3 {
			// The user included some flags
			flags = replaceCmd[2]
		}

		search := string(replaceCmd[0])
		replace := string(replaceCmd[1])

		if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) {
			search = search[1 : len(search)-1]
		}
		if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) {
			replace = replace[1 : len(replace)-1]
		}

		search = strings.Replace(search, `\"`, `"`, -1)
		replace = strings.Replace(replace, `\"`, `"`, -1)

		// messenger.Error(search + " -> " + replace)

		regex, err := regexp.Compile(search)
		if err != nil {
			messenger.Error(err.Error())
			return
		}

		found := false
		for {
			match := regex.FindStringIndex(view.buf.text)
			if match == nil {
				break
			}
			found = true
			if strings.Contains(flags, "c") {
				// 	// The 'check' flag was used
				// 	if messenger.YesNoPrompt("Perform replacement?") {
				// 		view.eh.Replace(match[0], match[1], replace)
				// 	} else {
				// 		continue
				// 	}
			}
			view.eh.Replace(match[0], match[1], replace)
		}
		if !found {
			messenger.Message("Nothing matched " + search)
		}
	default:
		messenger.Error("Unknown command: " + inputCmd)
	}
}
<|endoftext|>