From 5fbc39f4a2dfe65e005d0bc06a2f2e5684e628d0 Mon Sep 17 00:00:00 2001 From: Gregory Eremin Date: Mon, 17 Jun 2019 00:40:41 +0200 Subject: [PATCH] WIP --- .gitignore | 2 + command.go | 138 ++++++++++++++++++++++++++++ exec.go | 26 ------ go.mod | 6 ++ go.sum | 4 + item.go | 205 +++++++++++++++++++++++------------------- main.go | 28 +++--- {cmd => menu}/main.go | 0 window.go | 4 +- 9 files changed, 278 insertions(+), 135 deletions(-) create mode 100644 .gitignore create mode 100644 command.go delete mode 100644 exec.go create mode 100644 go.mod create mode 100644 go.sum rename {cmd => menu}/main.go (100%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b506de --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor +config.json \ No newline at end of file diff --git a/command.go b/command.go new file mode 100644 index 0000000..8030b13 --- /dev/null +++ b/command.go @@ -0,0 +1,138 @@ +package menu + +import ( + "bytes" + "encoding/json" + "log" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/juju/errors" +) + +var errTimeout = errors.New("command timed out") + +type command struct { + commandDetails + + busy bool + updateInterval time.Duration + timeout time.Duration + ticker *time.Ticker + + out *string + error error +} + +type commandDetails struct { + ShellCommand string `json:"cmd"` + UpdateInterval string `json:"update_interval"` + Timeout string `json:"timeout"` +} + +// Can be both a structure or a command string. +func (c *command) UnmarshalJSON(b []byte) error { + if len(b) > 0 && b[0] == '{' { + err := json.Unmarshal(b, &c.commandDetails) + if err != nil { + return err + } + if c.UpdateInterval != "" { + c.updateInterval, err = time.ParseDuration(c.UpdateInterval) + if err != nil { + return err + } + } + if c.Timeout != "" { + c.timeout, err = time.ParseDuration(c.Timeout) + if err != nil { + return err + } + } + return nil + } + + str, err := strconv.Unquote(string(b)) + if err != nil { + return err + } + c.ShellCommand = str + + return nil +} + +func (c *command) exec() { + c.busy = true + defer func() { c.busy = false }() + + switch { + case c.ShellCommand != "": + c.out, c.error = execShellCommand(c.ShellCommand, c.timeout) + } +} + +func (c *command) keepUpdated() { + c.exec() + if c.updateInterval == 0 { + return + // c.UpdateInterval = 3 * time.Second + } + + c.ticker = time.NewTicker(c.updateInterval) + go func() { + for range c.ticker.C { + c.exec() + if c.error != nil { + log.Printf("Command failed: %v", c.error) + } + } + }() +} + +func (c *command) resetTimer() { + if c.ticker != nil { + c.ticker.Stop() + } + c.keepUpdated() +} + +func execShellCommand(shellCommand string, timeout time.Duration) (*string, error) { + log.Println("Command:", shellCommand) + var out bytes.Buffer + cmd := exec.Command("bash", "-c", shellCommand) + cmd.Stdout = &out + cmd.Stderr = &out + if err := cmd.Start(); err != nil { + return nil, err + } + if err := waitWithTimeout(cmd, timeout); err != nil { + return nil, err + } + + strOut := strings.TrimSpace(out.String()) + // log.Println("Output:", strOut) + return &strOut, nil +} + +func waitWithTimeout(cmd *exec.Cmd, timeout time.Duration) error { + if timeout == 0 { + return cmd.Wait() + } + + errCh := make(chan error, 1) + go func() { + errCh <- cmd.Wait() + }() + + select { + case <-time.After(timeout): + if err := cmd.Process.Kill(); err != nil { + log.Printf("Failed to kill command after timeout: %v", err) + } + return errTimeout + case err := <-errCh: + return err + } +} diff --git a/exec.go b/exec.go deleted file mode 100644 index 977eaf5..0000000 --- a/exec.go +++ /dev/null @@ -1,26 +0,0 @@ -package menu - -import ( - "log" - "os/exec" - "strings" -) - -type output struct { - code int - body string -} - -func execCommand(command string) (string, error) { - if command == "todo" { - return "", nil - } - - log.Println("Command", command) - cmd := exec.Command("bash", "-c", command) - out, err := cmd.CombinedOutput() - if err != nil { - return "", err - } - return strings.TrimSpace(string(out)), nil -} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..087cce5 --- /dev/null +++ b/go.mod @@ -0,0 +1,6 @@ +module github.com/localhots/themenu + +require ( + github.com/juju/errors v0.0.0-20190207033735-e65537c515d7 + github.com/veandco/go-sdl2 v0.3.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..101d198 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/juju/errors v0.0.0-20190207033735-e65537c515d7 h1:dMIPRDg6gi7CUp0Kj2+HxqJ5kTr1iAdzsXYIrLCNSmU= +github.com/juju/errors v0.0.0-20190207033735-e65537c515d7/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/veandco/go-sdl2 v0.3.0 h1:IWYkHMp8V3v37NsKjszln8FFnX2+ab0538J371t+rss= +github.com/veandco/go-sdl2 v0.3.0/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg= diff --git a/item.go b/item.go index 37d0e12..c4056e0 100644 --- a/item.go +++ b/item.go @@ -1,83 +1,127 @@ package menu import ( - "log" "strings" - "time" "github.com/juju/errors" "github.com/veandco/go-sdl2/sdl" ) -type listItem struct { - Label string `json:"label"` - ActionKey string `json:"key"` - Keep bool `json:"keep"` - Command string `json:"command"` - Condition *ifCondition `json:"if"` - Items []listItem `json:"items"` +type menuItem struct { + ID string `json:"id"` + Label *string `json:"label"` + LabelCommand *command `json:"label_cmd"` + ActionKey string `json:"key"` + ActionCommand *command `json:"action_cmd"` + Toggle *toggle `json:"switch"` + Items []menuItem `json:"items"` + Invalidates []string `json:"invalidates"` active bool - - subLabel string } -type ifCondition struct { - Command string `json:"command"` - Output map[string]listItem `json:"output"` - - cachedOutput *string - busy bool -} - -func (li *listItem) render(r *sdl.Renderer, offsetY int32) error { - if err := drawItemBackground(r, offsetY, li.active); err != nil { - return errors.Annotate(err, "draw item background") +func (mi *menuItem) prepare() { + if mi.LabelCommand != nil { + mi.LabelCommand.keepUpdated() } - if err := drawItemLabel(r, offsetY, *li); err != nil { + if mi.Toggle != nil { + mi.Toggle.StateCommand.keepUpdated() + } + for i := range mi.Items { + mi.Items[i].prepare() + } +} + +func (mi *menuItem) trigger() { + if mi.ActionCommand != nil && !mi.ActionCommand.busy { + mi.ActionCommand.exec() + } + if mi.Toggle != nil { + mi.Toggle.trigger() + } +} + +func (mi *menuItem) label() string { + if cmd := mi.LabelCommand; cmd != nil { + // pretty.Println(cmd) + if cmd.error != nil { + return cmd.error.Error() + } + if cmd.out != nil { + return *cmd.out + } + } + if cmd := mi.ActionCommand; cmd != nil { + if cmd.error != nil { + return cmd.error.Error() + } + } + if mi.Toggle != nil { + return mi.Toggle.label() + } + if mi.Label != nil { + return *mi.Label + } + return "No name" +} + +func (mi *menuItem) busy() bool { + if mi.LabelCommand != nil && mi.LabelCommand.busy { + return true + } + if mi.ActionCommand != nil && mi.ActionCommand.busy { + return true + } + if mi.Toggle != nil && mi.Toggle.StateCommand.busy { + return true + } + return false +} + +func (mi *menuItem) render(r *sdl.Renderer, offsetY int32) error { + if mi.ActionKey != "" { + if err := drawItemBackground(r, offsetY, mi.active); err != nil { + return errors.Annotate(err, "draw item background") + } + } + if err := drawItemLabel(r, offsetY, *mi); err != nil { return errors.Annotate(err, "draw item label") } - if err := drawActionKeyLabel(r, offsetY, *li); err != nil { + if err := drawActionKeyLabel(r, offsetY, *mi); err != nil { return errors.Annotate(err, "draw item label") } return nil } -func (li *listItem) call() error { - defer func() { - if err := recover(); err != nil { - log.Printf("Action failed: %v", err) - } - }() - - switch { - case li.Condition != nil: - if li.Condition.cachedOutput == nil { - return nil - } - - out := *li.Condition.cachedOutput - if match, ok := li.Condition.Output[out]; ok { - _, err := execCommand(match.Command) - return err - } - - log.Println("no match", out) - return nil - case li.Command != "": - _, err := execCommand(li.Command) - return err - default: - return nil - } +type toggle struct { + StateCommand command `json:"state_cmd"` + States map[string]menuItem `json:"states"` } -func (li *listItem) label() string { - if li.subLabel != "" { - return li.Label + " " + li.subLabel +func (t *toggle) label() string { + if t.StateCommand.error != nil { + return t.StateCommand.error.Error() + } + if t.StateCommand.out != nil { + if opt, ok := t.States[*t.StateCommand.out]; ok { + return opt.label() + } + } + return "No name" +} + +func (t *toggle) trigger() { + if t.StateCommand.busy { + return + } + + if t.StateCommand.out != nil { + if opt, ok := t.States[*t.StateCommand.out]; ok { + opt.trigger() + t.StateCommand.resetTimer() + } } - return li.Label } func drawItemBackground(r *sdl.Renderer, offsetY int32, active bool) error { @@ -120,23 +164,16 @@ func drawItemBackground(r *sdl.Renderer, offsetY int32, active bool) error { return nil } -func drawItemLabel(r *sdl.Renderer, offsetY int32, li listItem) error { - label := li.Label - if li.Condition != nil { - if li.Condition.cachedOutput != nil { - out := *li.Condition.cachedOutput - if match, ok := li.Condition.Output[out]; ok { - label = match.Label - } - } - if li.Condition.busy { - label += " [busy]" - } - } +func drawItemLabel(r *sdl.Renderer, offsetY int32, mi menuItem) error { + label := mi.label() color := theme.ItemText - if li.Condition != nil && li.Condition.busy { + if mi.busy() && mi.ActionKey != "" { + label += " [busy]" color = theme.TextBusy } + if label == "" { + return nil + } labelTexture, err := renderText(r, label, color) if err != nil { @@ -161,12 +198,15 @@ func drawItemLabel(r *sdl.Renderer, offsetY int32, li listItem) error { return nil } -func drawActionKeyLabel(r *sdl.Renderer, offsetY int32, li listItem) error { - label := strings.ToUpper(li.ActionKey) +func drawActionKeyLabel(r *sdl.Renderer, offsetY int32, mi menuItem) error { + label := strings.ToUpper(mi.ActionKey) color := theme.ItemText - if li.Condition != nil && li.Condition.busy { + if mi.busy() && mi.ActionKey != "" { color = theme.TextBusy } + if label == "" { + return nil + } labelTexture, err := renderText(r, label, color) if err != nil { @@ -204,24 +244,3 @@ func renderText(r *sdl.Renderer, text string, c sdl.Color) (t *sdl.Texture, err return } - -func (c *ifCondition) evaluate() { - fn := func() { - c.busy = true - out, err := execCommand(c.Command) - if err != nil { - log.Printf("Error evaluating condition: %v", err) - } else { - out = strings.TrimSpace(out) - c.cachedOutput = &out - } - c.busy = false - } - fn() - - t := time.NewTicker(2 * time.Second) - defer t.Stop() - for range t.C { - fn() - } -} diff --git a/main.go b/main.go index 93c8114..02a0f21 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,9 @@ func Main() { if err != nil { log.Fatalf("Failed to read config file: %v", err) } - keepConditionsEvaluated(items) + for i := range items { + items[i].prepare() + } log.Println("Loading font") if err := ttf.Init(); err != nil { @@ -96,6 +98,7 @@ func (w *window) eventLoop(r *sdl.Renderer) { } // Ta-dah! r.Present() + // pretty.Println(w) } } @@ -116,8 +119,14 @@ func (w *window) handleKeyEvent(e *sdl.KeyboardEvent) error { w.curItems[i].active = (e.State == sdl.PRESSED) if e.State == sdl.RELEASED { - if itm.Condition == nil || !itm.Condition.busy { - go w.curItems[i].call() + // pretty.Println("triggered", itm) + itm.trigger() + for _, inv := range itm.Invalidates { + for _, itm := range w.curItems { + if itm.ID == inv { + itm.prepare() + } + } } } } @@ -126,22 +135,13 @@ func (w *window) handleKeyEvent(e *sdl.KeyboardEvent) error { return nil } -func getItems(path string) ([]listItem, error) { +func getItems(path string) ([]menuItem, error) { b, err := ioutil.ReadFile(path) if err != nil { return nil, err } - var items []listItem + var items []menuItem err = json.Unmarshal(b, &items) return items, err } - -func keepConditionsEvaluated(items []listItem) { - for _, item := range items { - if item.Condition != nil { - go item.Condition.evaluate() - } - keepConditionsEvaluated(item.Items) - } -} diff --git a/cmd/main.go b/menu/main.go similarity index 100% rename from cmd/main.go rename to menu/main.go diff --git a/window.go b/window.go index d521e0e..741e106 100644 --- a/window.go +++ b/window.go @@ -15,7 +15,7 @@ const ( var ( windowWidth = 600 windowHeight = 800 - borderWidth = 1 + borderWidth = 0 fontName = "InconsolataGo Regular" fontSize = 24 @@ -24,7 +24,7 @@ var ( type window struct { window *sdl.Window - allItems, curItems []listItem + allItems, curItems []menuItem } func (w window) render(r *sdl.Renderer) error {