WIP
This commit is contained in:
parent
8e194e278c
commit
5fbc39f4a2
|
@ -0,0 +1,2 @@
|
|||
vendor
|
||||
config.json
|
|
@ -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
|
||||
}
|
||||
}
|
26
exec.go
26
exec.go
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
197
item.go
197
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"`
|
||||
type menuItem struct {
|
||||
ID string `json:"id"`
|
||||
Label *string `json:"label"`
|
||||
LabelCommand *command `json:"label_cmd"`
|
||||
ActionKey string `json:"key"`
|
||||
Keep bool `json:"keep"`
|
||||
Command string `json:"command"`
|
||||
Condition *ifCondition `json:"if"`
|
||||
Items []listItem `json:"items"`
|
||||
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 (mi *menuItem) prepare() {
|
||||
if mi.LabelCommand != nil {
|
||||
mi.LabelCommand.keepUpdated()
|
||||
}
|
||||
if mi.Toggle != nil {
|
||||
mi.Toggle.StateCommand.keepUpdated()
|
||||
}
|
||||
for i := range mi.Items {
|
||||
mi.Items[i].prepare()
|
||||
}
|
||||
}
|
||||
|
||||
func (li *listItem) render(r *sdl.Renderer, offsetY int32) error {
|
||||
if err := drawItemBackground(r, offsetY, li.active); err != nil {
|
||||
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, *li); err != nil {
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
28
main.go
28
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue