package main
import (
"github.com/rollicks-c/kgate/internal/cli"
"github.com/rollicks-c/term"
"os"
)
func main() {
if err := cli.CreateClient().Run(os.Args); err != nil {
term.Failf("%s\n", err.Error())
}
}
package cli
import (
"github.com/rollicks-c/kgate/internal/cli/commands/forwards"
"github.com/rollicks-c/kgate/internal/config"
"github.com/urfave/cli/v2"
)
func CreateClient() *cli.App {
app := cli.NewApp()
app.Name = config.AppName
app.Usage = config.Usage
app.Commands = createCommands()
app.Action = forwards.StartAll
app.Flags = []cli.Flag{forwards.FlagProfile}
return app
}
package forwards
import (
"fmt"
"github.com/rollicks-c/kgate/internal/cli/commands/profile"
"github.com/rollicks-c/kgate/internal/config"
"github.com/urfave/cli/v2"
)
var (
FlagProfile = &cli.StringFlag{
Name: "profile",
Aliases: []string{"p"},
}
FlagGroup = &cli.StringSliceFlag{
Name: "group",
Aliases: []string{"g"},
}
FlagAll = &cli.BoolFlag{
Name: "all",
Value: false,
Aliases: []string{"a"},
}
)
func Start(c *cli.Context) error {
selectedGroups := FlagGroup.Get(c)
allGroups := FlagAll.Get(c)
prof := config.Profiles().LoadCurrent()
return startGroups(prof, allGroups, selectedGroups...)
}
func StartAll(c *cli.Context) error {
profExp := FlagProfile.Get(c)
prof := config.Profiles().LoadCurrent()
if profExp != "" {
list, ok := profile.Find(profExp)
if !ok {
return fmt.Errorf("no profile found uniquely matching [%s]", profExp)
}
prof = config.Profiles().Load(list[0], false)
}
return startGroups(prof, true)
}
package forwards
import (
"fmt"
"github.com/rollicks-c/configcove/profiles"
"github.com/rollicks-c/kgate/internal/config"
"github.com/rollicks-c/kgate/internal/logic/gate"
)
func startGroups(profile profiles.Profile[config.Config], allGroups bool, selectedGroups ...string) error {
// sanity check
if allGroups && len(selectedGroups) > 0 {
return fmt.Errorf("cannot use -%s and -%s together", FlagAll.Name, FlagGroup.Name)
}
// load data
var groups []config.PortGroup
if allGroups {
groups = profile.Data.Groups
} else {
groups = filterGroups(profile.Data.Groups, selectedGroups)
}
// start forwards
if err := gate.RunGroups(groups...); err != nil {
return err
}
return nil
}
func filterGroups(pool []config.PortGroup, selectedNames []string) []config.PortGroup {
var res []config.PortGroup
for _, g := range pool {
if contains(selectedNames, g.Name) {
res = append(res, g)
}
}
return res
}
func contains(groups []string, name string) bool {
for _, g := range groups {
if g == name {
return true
}
}
return false
}
package groups
import (
"github.com/rollicks-c/kgate/internal/config"
"github.com/rollicks-c/term"
"github.com/urfave/cli/v2"
)
func List(c *cli.Context) error {
profile := config.Profiles().LoadCurrent()
term.Infof("profile [%s]\ngroups:\n", profile.Name)
for _, g := range profile.Data.Groups {
term.Infof(" - %s [%s]\n", g.Name, g.Target.K8sContext)
for _, pf := range g.PortForwards {
term.Infof(" - %s:%s/%s:%s\n", pf.LocalPort, pf.Namespace, pf.Service, pf.RemotePort)
}
}
return nil
}
package profile
import (
"fmt"
"github.com/rollicks-c/kgate/internal/config"
"github.com/rollicks-c/term"
"github.com/urfave/cli/v2"
"strings"
)
func Switch(c *cli.Context) error {
// no args: show current profile
if c.Args().Len() == 0 {
showProfile()
return nil
}
// switch
if err := switchProfile(c.Args().First()); err != nil {
return err
}
return nil
}
func List(c *cli.Context) error {
profileList := config.Profiles().List()
term.Infof("profiles:\n")
for _, p := range profileList {
active := ""
if p == config.Profiles().LoadCurrent().Name {
active = " (active)"
}
term.Infof("\t- %s%s\n", p, active)
}
return nil
}
func Create(c *cli.Context) error {
// gather profile name
profileName := c.Args().First()
if profileName == "" {
return fmt.Errorf("profile name is required")
}
// avoid name collision
profileList := config.Profiles().List()
term.Infof("profiles:\n")
for _, p := range profileList {
if p == profileName {
return fmt.Errorf("profile %s already exists\n", profileName)
}
}
// create profile
template := config.Profiles().LoadCurrent()
template.Name = profileName
template.Data.Groups = append(template.Data.Groups, config.PortGroup{
Target: config.Target{
K8sConfigFile: "${HOME}/.kube/config",
K8sContext: "context1",
},
PortForwards: []config.PortForward{
{
Namespace: "namespace1",
Service: "service1",
LocalPort: "8080",
RemotePort: "8080",
},
},
Name: "group1",
})
config.Profiles().Update(template)
term.Infof("profile %s created\n", profileName)
return nil
}
func Find(exp string) ([]string, bool) {
// fuzzy match
pList := config.Profiles().List()
sel := make([]string, 0)
for _, p := range pList {
if strings.HasPrefix(p, exp) {
sel = append(sel, p)
}
}
if len(sel) == 0 {
return sel, false
}
if len(sel) > 1 {
return sel, false
}
return sel, true
}
package profile
import (
"github.com/rollicks-c/kgate/internal/config"
"github.com/rollicks-c/term"
"strings"
)
func showProfile() {
profileName := config.Profiles().LoadCurrent().Name
term.Infof("profile: %s\n", profileName)
}
func switchProfile(exp string) error {
// fuzzy match
list, _ := Find(exp)
if len(list) == 0 {
term.Failf("no profile found matching [%s]\n", exp)
return nil
}
if len(list) > 1 {
term.Failf("multiple profiles found matching [%s]: %s\n", exp, strings.Join(list, ", "))
return nil
}
profileName := list[0]
// switch
err := config.Profiles().Switch(profileName)
if err != nil {
term.Failf("failed to switch profile: [%s]\n", err)
return err
}
term.Successf("switched to profile [%s]\n", profileName)
return nil
}
package cli
import (
"github.com/rollicks-c/kgate/internal/cli/commands/forwards"
"github.com/rollicks-c/kgate/internal/cli/commands/groups"
"github.com/rollicks-c/kgate/internal/cli/commands/profile"
"github.com/urfave/cli/v2"
)
func createCommands() []*cli.Command {
cmdList := []*cli.Command{
createForwardCommands(),
createGroupsCommands(),
createProfileCommands(),
}
return cmdList
}
func createForwardCommands() *cli.Command {
return &cli.Command{
Name: "forward",
Aliases: []string{"f"},
Action: forwards.Start,
Usage: "forwards [-a | -g <g1>, -g <g2>,...]",
Flags: []cli.Flag{
forwards.FlagGroup,
forwards.FlagAll,
},
}
}
func createGroupsCommands() *cli.Command {
return &cli.Command{
Name: "groups",
Aliases: []string{"g"},
Action: groups.List,
Subcommands: []*cli.Command{
{
Name: "list",
Aliases: []string{"ls"},
Usage: "list",
Action: groups.List,
},
},
}
}
func createProfileCommands() *cli.Command {
return &cli.Command{
Name: "profiles",
Aliases: []string{"p"},
Action: profile.Switch,
HideHelp: true,
Subcommands: []*cli.Command{
{
Name: "list",
Aliases: []string{"ls"},
Usage: "list",
Action: profile.List,
},
{
Name: "create",
Aliases: []string{"c"},
Usage: "create <name>",
Action: profile.Create,
},
{
Name: "switch",
Aliases: []string{"s"},
Usage: "switch",
Action: profile.Switch,
HideHelp: true,
},
},
}
}
package config
import (
"github.com/rollicks-c/configcove"
"github.com/rollicks-c/configcove/profiles"
"github.com/rollicks-c/secretblendproviders/envvar"
)
type PortGroup struct {
Target Target `yaml:"target"`
PortForwards []PortForward `yaml:"portForwards"`
Name string `yaml:"name"`
}
type Target struct {
K8sConfigFile string `yaml:"k8sConfigFile"`
K8sContext string `yaml:"k8sContext"`
}
type PortForward struct {
Namespace string `yaml:"namespace"`
Service string `yaml:"service"`
LocalPort string `yaml:"localPort"`
RemotePort string `yaml:"remotePort"`
}
type Config struct {
Groups []PortGroup `yaml:"groups"`
}
func init() {
_ = envvar.RegisterGlobally()
}
func Profiles() *profiles.Manager[Config] {
pm := configcove.Profiles[Config](AppName)
return pm
}
package forwarding
import (
"fmt"
"github.com/rollicks-c/kgate/internal/config"
"github.com/rollicks-c/kgate/internal/logic/model"
"time"
)
func CreateForwarder(group config.PortGroup, def config.PortForward) model.Process {
return &managedForwarder{
group: group,
serviceName: def.Service,
namespace: def.Namespace,
localPort: def.LocalPort,
remotePort: def.RemotePort,
readyCh: make(chan struct{}),
timeout: time.Second * 5,
}
}
func (m managedForwarder) ID() string {
return m.hash()
}
func (m managedForwarder) Group() string {
return m.group.Name
}
func (m managedForwarder) Describe() string {
return fmt.Sprintf("%s:%s/%s:%s", m.localPort, m.namespace, m.serviceName, m.remotePort)
}
func (m managedForwarder) Run(c model.Controller) {
// setup port-forwarder
c.UpdateProcess(m, model.Restart, "starting port-forwarder...")
pf, err := m.createPortForwarder(c.StopChannel())
if err != nil {
c.UpdateProcess(m, model.Failure, err.Error())
return
}
m.run(c, pf)
}
package forwarding
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/rollicks-c/kgate/internal/config"
"github.com/rollicks-c/kgate/internal/logic/model"
"io"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
"net/http"
"time"
)
type managedForwarder struct {
group config.PortGroup
serviceName, namespace string
localPort, remotePort string
readyCh chan struct{}
timeout time.Duration
}
type portForwarder interface {
ForwardPorts() error
}
func (m managedForwarder) run(c model.Controller, pf portForwarder) {
hasError := false
// start session
go func() {
// setup cleanup
defer c.StopWaitGroup().Done()
c.StopWaitGroup().Add(1)
// start port-forwarding
err := pf.ForwardPorts()
// handle errors
if err != nil {
c.UpdateProcess(
m,
model.Failure,
err.Error(),
)
hasError = true
} else {
c.UpdateProcess(
m,
model.Stopped,
"",
)
}
}()
// await readiness or timeout
select {
case <-m.readyCh:
c.UpdateProcess(
m,
model.Running,
"",
)
case <-time.After(m.timeout):
if !hasError {
c.UpdateProcess(
m,
model.Failure,
fmt.Sprintf("timeout occured (%s)", m.timeout.String()),
)
}
}
}
func (m managedForwarder) hash() string {
hash := sha256.New()
hash.Write([]byte(fmt.Sprintf("%v:%s:%s:%s:%s", m.group, m.serviceName, m.namespace, m.localPort, m.remotePort)))
return hex.EncodeToString(hash.Sum(nil))
}
func (m managedForwarder) createPortForwarder(stopChan chan struct{}) (*portforward.PortForwarder, error) {
// gather pod
client, conf, err := createClient(m.group.Target)
if err != nil {
return nil, err
}
podName, err := getPodForService(client, m.serviceName, m.namespace)
if err != nil {
return nil, fmt.Errorf("failed to get pod for service %s: %v", m.serviceName, err)
}
// prep port-forwarding request
url := client.CoreV1().RESTClient().Post().
Resource("pods").
Namespace(m.namespace).
Name(podName).
SubResource("portforward").
URL()
transport, upgrader, err := spdy.RoundTripperFor(conf)
if err != nil {
return nil, fmt.Errorf("failed to create round-tripper: %v", err)
}
// create port forward
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, http.MethodPost, url)
ports := []string{fmt.Sprintf("%s:%s", m.localPort, m.remotePort)}
pf, err := portforward.New(dialer, ports, stopChan, m.readyCh, io.Discard, io.Discard)
if err != nil {
return nil, fmt.Errorf("failed to create port-forward: %v", err)
}
// port-forwarding ready
return pf, nil
}
package forwarding
import (
"context"
"fmt"
"github.com/rollicks-c/kgate/internal/config"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"strings"
)
func createClient(target config.Target) (*kubernetes.Clientset, *rest.Config, error) {
// load kube config
loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: target.K8sConfigFile}
configOverrides := &clientcmd.ConfigOverrides{
CurrentContext: target.K8sContext,
}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
clientConfig, err := kubeConfig.ClientConfig()
if err != nil {
return nil, nil, err
}
// create client
clientSet, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
return nil, nil, fmt.Errorf("failed to create Kubernetes client: %v", err)
}
return clientSet, clientConfig, nil
}
func getPodForService(clientSet *kubernetes.Clientset, serviceName, namespace string) (string, error) {
// gather all pods in NS
pods, err := clientSet.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return "", fmt.Errorf("failed to list pods: %v", err)
}
// find pod for service
for _, pod := range pods.Items {
if strings.Contains(pod.Name, serviceName) {
return pod.Name, nil
}
}
return "", fmt.Errorf("no pod found for service %s", serviceName)
}
package gate
import (
"fmt"
"github.com/rollicks-c/kgate/internal/config"
)
func RunGroups(groups ...config.PortGroup) error {
// sanity check
if len(groups) == 0 {
return fmt.Errorf("no groups found")
}
// start session
newController(groups...).Run()
return nil
}
package gate
import (
"context"
"github.com/rollicks-c/kgate/internal/config"
"github.com/rollicks-c/kgate/internal/logic/forwarding"
"github.com/rollicks-c/kgate/internal/logic/model"
"github.com/rollicks-c/kgate/internal/logic/ui"
"github.com/rollicks-c/term"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
const (
defaultMessage = "press [yellow::b]q[-::-] to quit | [yellow::b]s[-::-] to stop/resume"
)
type controller struct {
// data
groups []config.PortGroup
session *session
// rendering
view model.Frontend
statusChan chan model.Update
// runtime
ctx model.Context
ctxStop context.CancelFunc
}
func (c controller) StopChannel() chan struct{} {
return c.session.stopChan
}
func (c controller) StopWaitGroup() *sync.WaitGroup {
return c.session.wg
}
func newController(groups ...config.PortGroup) *controller {
// create context
runContext, ctxStop := context.WithCancel(context.Background())
ctx := model.Context{
Context: runContext,
WG: &sync.WaitGroup{},
}
// create controller
c := &controller{
groups: groups,
ctx: ctx,
ctxStop: ctxStop,
statusChan: make(chan model.Update),
}
c.session = newSession()
c.view = ui.NewFancy()
//c.view = ui.NewSimple()
return c
}
func (c controller) Run() {
// start frontend
go c.view.Run(c)
go c.runUpdateLoop()
// start forwards
c.view.ShowMessage("[orange]starting port forwards...")
c.startSession()
// listen to terminate signals
c.view.ShowMessage(defaultMessage)
sigChan := make(chan os.Signal)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
select {
case <-sigChan:
case <-c.ctx.Done():
}
// controlled shutdown
c.shutdown()
}
func (c controller) TogglePause() {
go func() {
if c.session.isStopped {
c.session.reset()
c.startSession()
} else {
c.stopSession()
}
}()
}
func (c controller) Quit() {
go func() {
c.view.ShowMessage("[orange]stopping port forwards...")
c.stopSession()
<-time.After(500 * time.Millisecond)
c.ctxStop()
_ = syscall.Kill(os.Getpid(), syscall.SIGTERM)
}()
}
func (c controller) UpdateProcess(proc model.Process, state model.Status, msg string) {
update := model.Update{
ID: proc.ID(),
SortIndex: c.session.oridnalView[proc.ID()],
Group: proc.Group(),
PortForward: proc.Describe(),
Status: state,
Message: msg,
}
c.statusChan <- update
}
func (c controller) runUpdateLoop() {
for {
select {
case event := <-c.statusChan:
c.view.Update(event)
case <-c.ctx.Done():
return
}
}
}
func (c controller) shutdown() {
term.Warnf("shutting down app...\n")
// stop UI
close(c.statusChan)
c.view.Stop()
// stop all routines
c.ctxStop()
c.ctx.WG.Wait()
}
func (c controller) startSession() {
// iterate all port forwards
for _, g := range c.groups {
for _, pf := range g.PortForwards {
// create and run processes
proc := forwarding.CreateForwarder(g, pf)
c.session.addProcess(proc)
go proc.Run(c)
}
}
}
func (c controller) stopSession() {
// session already aborted
if c.session.isStopped {
return
}
// stop all forwards
close(c.session.stopChan)
c.session.wg.Wait()
// mark session as aborted
c.session.isStopped = true
}
package gate
import (
"github.com/rollicks-c/kgate/internal/logic/model"
"sort"
"sync"
)
type session struct {
// data
processes map[string]model.Process
oridnalView map[string]int
isStopped bool
// runtime
stopChan chan struct{}
wg *sync.WaitGroup
}
func newSession() *session {
s := &session{}
s.reset()
return s
}
func (s *session) reset() {
s.isStopped = false
s.stopChan = make(chan struct{})
s.processes = make(map[string]model.Process)
s.oridnalView = make(map[string]int)
s.wg = &sync.WaitGroup{}
}
func (s *session) addProcess(proc model.Process) {
s.processes[proc.ID()] = proc
s.buildOrdinalView()
}
func (s *session) buildOrdinalView() {
list := make([]model.Process, 0, len(s.processes))
for _, p := range s.processes {
list = append(list, p)
}
sort.Slice(list, func(i, j int) bool {
if list[i].Group() == list[j].Group() {
return list[i].Describe() < list[j].Describe()
}
return list[i].Group() < list[j].Group()
})
for i, p := range list {
s.oridnalView[p.ID()] = i
}
}
package ui
import (
"github.com/rollicks-c/kgate/internal/logic/model"
"github.com/rollicks-c/kgate/internal/logic/ui/fancy"
"github.com/rollicks-c/kgate/internal/logic/ui/simple"
)
func NewSimple() model.Frontend {
return simple.New()
}
func NewFancy() model.Frontend {
return fancy.New()
}
package fancy
import (
"github.com/rivo/tview"
"github.com/rollicks-c/kgate/internal/logic/model"
"github.com/rollicks-c/term"
)
type Frontend struct {
app *tview.Application
layout *appLayout
records map[string]int // id->row
}
func New() *Frontend {
// build app
layout := createLayout()
app := tview.NewApplication().
EnableMouse(false).
SetRoot(layout.root, true)
return &Frontend{
app: app,
layout: layout,
records: map[string]int{},
}
}
func (f Frontend) Run(controller model.Controller) {
// install event handlers
f.setupKeyHandler(controller)
// run
if err := f.app.Run(); err != nil {
panic(err)
}
}
func (f Frontend) ShowMessage(msg string) {
f.app.QueueUpdateDraw(func() {
f.layout.msgBox.SetText(msg + " ")
})
}
func (f Frontend) Stop() {
f.app.Stop()
term.Warnf("shutting down UI\n")
f.app.SetScreen(nil)
}
func (f Frontend) Update(update model.Update) {
f.app.QueueUpdateDraw(func() {
f.updateTableRow(update)
})
}
package fancy
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/rollicks-c/kgate/internal/logic/model"
"os"
"syscall"
)
func (f Frontend) setupKeyHandler(controller model.Controller) {
f.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Rune() == 's' {
controller.TogglePause()
}
if event.Rune() == 'q' {
controller.Quit()
}
if event.Key() == tcell.KeyCtrlC {
controller.Quit()
_ = syscall.Kill(os.Getpid(), syscall.SIGTERM)
}
return event
})
}
func (f Frontend) getStatusColor(value model.Status) tcell.Color {
switch value {
case model.Running:
return tcell.ColorGreen
case model.Stopped:
return tcell.ColorOrange
case model.Restart:
return tcell.ColorYellow
case model.Failure:
return tcell.ColorRed
default:
return tcell.ColorWhite
}
}
func (f Frontend) getStatusText(value model.Status) string {
switch value {
case model.Running:
return "🟢 Running"
case model.Stopped:
return "⛔ Stopped"
case model.Restart:
return "🔄 Restarting"
case model.Failure:
return "❌ Failure"
default:
return "⚪ Unknown"
}
}
func (f Frontend) updateTableRow(update model.Update) {
row := update.SortIndex + 1
f.layout.table.SetCell(row, 0,
tview.NewTableCell(padText(update.Group, 10)).
SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignLeft).
SetSelectable(true),
)
f.layout.table.SetCell(row, 1,
tview.NewTableCell(padText(update.PortForward, 45)).
SetTextColor(tcell.ColorLightBlue).
SetAlign(tview.AlignLeft).
SetSelectable(true),
)
f.layout.table.SetCell(row, 2,
tview.NewTableCell(padText(f.getStatusText(update.Status), 20)).
SetTextColor(f.getStatusColor(update.Status)).
SetAlign(tview.AlignLeft).
SetSelectable(true),
)
f.layout.table.SetCell(row, 3,
tview.NewTableCell(update.Message).
SetTextColor(f.getStatusColor(update.Status)).
SetTextColor(f.getStatusColor(update.Status)).
SetAlign(tview.AlignLeft).
SetSelectable(false),
)
}
package fancy
import (
"fmt"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/rollicks-c/kgate/internal/config"
)
type appLayout struct {
root *tview.Flex
msgBox *tview.TextView
table *tview.Table
}
func createLayout() *appLayout {
// create controls
header, msgBox := createHeader()
table := createTable()
// setup appLayout
flex := tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(header, 3, 1, false).
AddItem(table, 0, 1, true)
return &appLayout{
root: flex,
msgBox: msgBox,
table: table,
}
}
func createHeader() (*tview.Flex, *tview.TextView) {
// create controls
titleView := tview.NewTextView().
SetText(fmt.Sprintf("[yellow::b] %s", config.AppName)).
SetDynamicColors(true).
SetTextAlign(tview.AlignLeft)
msgView := tview.NewTextView().
SetText("").
SetDynamicColors(true).
SetTextAlign(tview.AlignRight)
// appLayout
headerFlex := tview.NewFlex().
SetDirection(tview.FlexColumn).
AddItem(titleView, 0, 1, false).
AddItem(nil, 0, 2, false).
AddItem(msgView, 50, 0, false)
headerFlex.SetBorder(true)
return headerFlex, msgView
}
func createTable() *tview.Table {
// create table
table := tview.NewTable().
SetBorders(false).
SetSelectable(false, false)
// add headers
headers := []string{"Group", "Port Forward", "Status", "Info"}
widths := []int{15, 15, 10, 25} // Column width for padding
for i, header := range headers {
table.SetCell(0, i,
tview.NewTableCell(padText(header, widths[i])).
SetTextColor(tcell.ColorBlack).
SetBackgroundColor(tcell.ColorWhite).
SetAlign(tview.AlignLeft).
SetSelectable(false).
SetStyle(tcell.StyleDefault.Bold(true)),
)
}
return table
}
func padText(text string, width int) string {
if len(text) >= width {
return text[:width]
}
return fmt.Sprintf("%-*s", width, text)
}
package simple
import (
"fmt"
"github.com/rollicks-c/kgate/internal/logic/model"
"os"
"os/exec"
"runtime"
)
type Frontend struct {
procList map[string]model.Update
}
func (f Frontend) Run(controller model.Controller) {
}
func New() *Frontend {
return &Frontend{
procList: make(map[string]model.Update),
}
}
func (f Frontend) ShowMessage(msg string) {
fmt.Println(msg)
}
func (f Frontend) Stop() {
f.clearTerminal()
}
func (f Frontend) Update(update model.Update) {
f.clearTerminal()
f.procList[update.ID] = update
for _, proc := range f.procList {
fmt.Println(proc)
}
}
func (f Frontend) clearTerminal() {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/c", "cls")
default:
cmd = exec.Command("clear")
}
cmd.Stdout = os.Stdout
cmd.Run()
}