1
0
Fork 0
This commit is contained in:
Gregory Eremin 2019-06-17 00:40:41 +02:00
parent 8e194e278c
commit 5fbc39f4a2
9 changed files with 278 additions and 135 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
vendor
config.json

138
command.go Normal file
View File

@ -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
View File

@ -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
}

6
go.mod Normal file
View File

@ -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
)

4
go.sum Normal file
View File

@ -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=

205
item.go
View File

@ -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()
}
}

28
main.go
View File

@ -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)
}
}

View File

@ -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 {