Initial commit
This commit is contained in:
commit
8e194e278c
|
@ -0,0 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
menu "github.com/localhots/themenu"
|
||||
)
|
||||
|
||||
func main() {
|
||||
menu.Main()
|
||||
log.Println("Shut down")
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
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,49 @@
|
|||
package fonts
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Find looks up font file location by the name.
|
||||
func Find(name string) (string, error) {
|
||||
// Normalize font file name
|
||||
fileName := strings.Replace(name, " ", "-", -1) + ".ttf"
|
||||
|
||||
for _, dir := range fontDirs() {
|
||||
dir, err := expandHome(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, fileName)
|
||||
if fileExist(path) {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func expandHome(path string) (string, error) {
|
||||
if !strings.HasPrefix(path, "~/") {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(u.HomeDir, path[2:]), nil
|
||||
}
|
||||
|
||||
func fileExist(path string) bool {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// +build darwin
|
||||
|
||||
package fonts
|
||||
|
||||
// Docs: https://support.apple.com/en-us/HT201722
|
||||
func fontDirs() []string {
|
||||
return []string{
|
||||
".", // Current directory
|
||||
"~/Library/Fonts", // User
|
||||
"/Library/Fonts", // Local
|
||||
"/System/Library/Fonts", // System
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// +build !linux,!darwin
|
||||
|
||||
package fonts
|
||||
|
||||
func fontDirs() []string {
|
||||
return []string{
|
||||
".", // Current directory
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// +build linux
|
||||
|
||||
package fonts
|
||||
|
||||
func fontDirs() []string {
|
||||
return []string{
|
||||
".", // Current directory
|
||||
"~/.fonts", // User
|
||||
"~/.fonts/truetype", // User
|
||||
"~/.local/share/fonts", // User
|
||||
"~/.local/share/fonts/truetype", // User
|
||||
"/usr/share/fonts", // System
|
||||
"/usr/share/fonts/truetype", // System
|
||||
}
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
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"`
|
||||
|
||||
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")
|
||||
}
|
||||
if err := drawItemLabel(r, offsetY, *li); err != nil {
|
||||
return errors.Annotate(err, "draw item label")
|
||||
}
|
||||
if err := drawActionKeyLabel(r, offsetY, *li); 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
|
||||
}
|
||||
}
|
||||
|
||||
func (li *listItem) label() string {
|
||||
if li.subLabel != "" {
|
||||
return li.Label + " " + li.subLabel
|
||||
}
|
||||
return li.Label
|
||||
}
|
||||
|
||||
func drawItemBackground(r *sdl.Renderer, offsetY int32, active bool) error {
|
||||
if err := setDrawColor(r, theme.ItemBackground); err != nil {
|
||||
return errors.Annotate(err, "set item background color")
|
||||
}
|
||||
|
||||
actionKeyFrame := &sdl.Rect{
|
||||
X: int32(paddingX + borderWidth),
|
||||
Y: offsetY,
|
||||
W: int32(itemHeight()),
|
||||
H: int32(itemHeight()),
|
||||
}
|
||||
var err error
|
||||
if active {
|
||||
err = r.DrawRect(actionKeyFrame)
|
||||
} else {
|
||||
err = r.FillRect(actionKeyFrame)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Annotate(err, "draw item")
|
||||
}
|
||||
|
||||
itemBackground := &sdl.Rect{
|
||||
X: int32(borderWidth + paddingX + itemHeight() + paddingX),
|
||||
Y: offsetY,
|
||||
W: int32(windowWidth - 3*paddingX - itemHeight() - 2*borderWidth),
|
||||
H: int32(itemHeight()),
|
||||
}
|
||||
|
||||
if active {
|
||||
err = r.DrawRect(itemBackground)
|
||||
} else {
|
||||
err = r.FillRect(itemBackground)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Annotate(err, "draw item")
|
||||
}
|
||||
|
||||
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]"
|
||||
}
|
||||
}
|
||||
color := theme.ItemText
|
||||
if li.Condition != nil && li.Condition.busy {
|
||||
color = theme.TextBusy
|
||||
}
|
||||
|
||||
labelTexture, err := renderText(r, label, color)
|
||||
if err != nil {
|
||||
return errors.Annotate(err, "draw item label")
|
||||
}
|
||||
defer labelTexture.Destroy()
|
||||
lw, lh, err := font.SizeUTF8(label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const magicNumber = 3
|
||||
itemLabel := &sdl.Rect{
|
||||
X: int32(paddingX*2+itemHeight()+(itemHeight()-lh)/2) + magicNumber,
|
||||
Y: offsetY + int32(itemHeight()-lh)/2,
|
||||
W: int32(lw),
|
||||
H: int32(lh),
|
||||
}
|
||||
if err := r.Copy(labelTexture, nil, itemLabel); err != nil {
|
||||
return errors.Annotate(err, "render item label")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func drawActionKeyLabel(r *sdl.Renderer, offsetY int32, li listItem) error {
|
||||
label := strings.ToUpper(li.ActionKey)
|
||||
color := theme.ItemText
|
||||
if li.Condition != nil && li.Condition.busy {
|
||||
color = theme.TextBusy
|
||||
}
|
||||
|
||||
labelTexture, err := renderText(r, label, color)
|
||||
if err != nil {
|
||||
return errors.Annotate(err, "draw item label")
|
||||
}
|
||||
defer labelTexture.Destroy()
|
||||
lw, lh, err := font.SizeUTF8(label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
itemLabel := &sdl.Rect{
|
||||
X: int32(paddingX + (itemHeight()-lw)/2),
|
||||
Y: offsetY + int32(itemHeight()-lh)/2,
|
||||
W: int32(lw),
|
||||
H: int32(lh),
|
||||
}
|
||||
if err := r.Copy(labelTexture, nil, itemLabel); err != nil {
|
||||
return errors.Annotate(err, "render item label")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderText(r *sdl.Renderer, text string, c sdl.Color) (t *sdl.Texture, err error) {
|
||||
fs, err := font.RenderUTF8Blended(text, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fs.Free()
|
||||
|
||||
t, err = r.CreateTextureFromSurface(fs)
|
||||
if err != nil {
|
||||
return nil, 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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package menu
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
"github.com/veandco/go-sdl2/ttf"
|
||||
)
|
||||
|
||||
var frameLimit = 60
|
||||
|
||||
// Main is the main function.
|
||||
func Main() {
|
||||
conf := flag.String("config", "config.json", "Path to config file")
|
||||
flag.IntVar(&windowWidth, "width", windowWidth, "Window width")
|
||||
flag.IntVar(&windowHeight, "height", windowHeight, "Window height")
|
||||
flag.IntVar(&borderWidth, "border", borderWidth, "Border width")
|
||||
flag.IntVar(&frameLimit, "fps", frameLimit, "FPS limit")
|
||||
flag.StringVar(&fontName, "fontname", fontName, "Font name (must be TrueType)")
|
||||
flag.IntVar(&fontSize, "fontsize", fontSize, "Font size")
|
||||
flag.Parse()
|
||||
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile)
|
||||
log.Println("Loading config")
|
||||
items, err := getItems(*conf)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read config file: %v", err)
|
||||
}
|
||||
keepConditionsEvaluated(items)
|
||||
|
||||
log.Println("Loading font")
|
||||
if err := ttf.Init(); err != nil {
|
||||
log.Fatalf("Failed to initialize TrueType package: %v", err)
|
||||
}
|
||||
defer ttf.Quit()
|
||||
if err := useFont(fontName); err != nil {
|
||||
log.Fatalf("Failed to open font: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Initializing SDL")
|
||||
if err := sdl.Init(sdl.INIT_EVENTS); err != nil {
|
||||
log.Fatalf("Failed to initialize sdl: %v", err)
|
||||
}
|
||||
defer sdl.Quit()
|
||||
sdl.SetHint(sdl.HINT_RENDER_SCALE_QUALITY, "1")
|
||||
|
||||
log.Println("Creating window")
|
||||
sdlWindow, err := sdl.CreateWindow("menu",
|
||||
sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED,
|
||||
int32(windowWidth), int32(windowHeight),
|
||||
sdl.WINDOW_SHOWN|sdl.WINDOW_BORDERLESS|sdl.WINDOW_ALLOW_HIGHDPI|sdl.WINDOW_OPENGL,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create window: %v", err)
|
||||
}
|
||||
defer sdlWindow.Destroy()
|
||||
|
||||
log.Println("Creating renderer")
|
||||
renderer, err := sdl.CreateRenderer(sdlWindow, -1, sdl.RENDERER_ACCELERATED|sdl.RENDERER_PRESENTVSYNC)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create renderer: %v", err)
|
||||
}
|
||||
defer renderer.Destroy()
|
||||
|
||||
w := window{
|
||||
window: sdlWindow,
|
||||
allItems: items,
|
||||
curItems: items,
|
||||
}
|
||||
w.eventLoop(renderer)
|
||||
}
|
||||
|
||||
func (w *window) eventLoop(r *sdl.Renderer) {
|
||||
// Throttle rendering
|
||||
ticker := time.NewTicker(time.Second / time.Duration(frameLimit))
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
event := sdl.PollEvent()
|
||||
switch tevt := event.(type) {
|
||||
case *sdl.QuitEvent:
|
||||
log.Println("Shutting down")
|
||||
return
|
||||
case *sdl.KeyboardEvent:
|
||||
if err := w.handleKeyEvent(tevt); err != nil {
|
||||
log.Printf("Failed to process key event: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.render(r); err != nil {
|
||||
log.Printf("Failed to render window: %v", err)
|
||||
}
|
||||
// Ta-dah!
|
||||
r.Present()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) handleKeyEvent(e *sdl.KeyboardEvent) error {
|
||||
if e.Keysym.Sym == sdl.K_ESCAPE {
|
||||
w.curItems = w.allItems
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try and transform a key code into a string containing a letter
|
||||
key := string(rune(e.Keysym.Sym))
|
||||
for i, itm := range w.curItems {
|
||||
if itm.ActionKey == key {
|
||||
if len(itm.Items) > 0 {
|
||||
w.curItems = itm.Items
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getItems(path string) ([]listItem, error) {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items []listItem
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package menu
|
||||
|
||||
import (
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
type colorScheme struct {
|
||||
Background sdl.Color
|
||||
ItemBackground sdl.Color
|
||||
ItemText sdl.Color
|
||||
TextBusy sdl.Color
|
||||
}
|
||||
|
||||
var theme = themeDracula
|
||||
|
||||
var themeGrayScale = colorScheme{
|
||||
Background: sdl.Color{R: 20, G: 20, B: 20, A: 255},
|
||||
ItemBackground: sdl.Color{R: 60, G: 60, B: 60, A: 255},
|
||||
ItemText: sdl.Color{R: 240, G: 240, B: 240, A: 255},
|
||||
}
|
||||
|
||||
var themeBlueOnBlack = colorScheme{
|
||||
Background: sdl.Color{R: 20, G: 20, B: 20, A: 255},
|
||||
ItemBackground: sdl.Color{R: 0, G: 100, B: 200, A: 255},
|
||||
ItemText: sdl.Color{R: 255, G: 255, B: 255, A: 255},
|
||||
}
|
||||
|
||||
var themeDracula = colorScheme{
|
||||
Background: rgb(40, 42, 54),
|
||||
ItemBackground: rgb(68, 71, 90),
|
||||
ItemText: rgb(248, 248, 242),
|
||||
TextBusy: rgb(255, 184, 108),
|
||||
}
|
||||
|
||||
func rgb(r, g, b uint8) sdl.Color {
|
||||
return sdl.Color{R: r, G: g, B: b, A: 255}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package menu
|
||||
|
||||
import (
|
||||
"github.com/juju/errors"
|
||||
"github.com/localhots/themenu/fonts"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
"github.com/veandco/go-sdl2/ttf"
|
||||
)
|
||||
|
||||
const (
|
||||
paddingX = 8
|
||||
paddingY = 8
|
||||
)
|
||||
|
||||
var (
|
||||
windowWidth = 600
|
||||
windowHeight = 800
|
||||
borderWidth = 1
|
||||
|
||||
fontName = "InconsolataGo Regular"
|
||||
fontSize = 24
|
||||
font *ttf.Font
|
||||
)
|
||||
|
||||
type window struct {
|
||||
window *sdl.Window
|
||||
allItems, curItems []listItem
|
||||
}
|
||||
|
||||
func (w window) render(r *sdl.Renderer) error {
|
||||
// Resize window
|
||||
windowHeight = borderWidth*2 + // Border
|
||||
len(w.curItems)*itemHeight() + // Backgrounds
|
||||
(len(w.curItems)+1)*paddingY // Paddings
|
||||
w.window.SetSize(int32(windowWidth), int32(windowHeight))
|
||||
|
||||
// Reset background
|
||||
if err := drawWindowBackground(r); err != nil {
|
||||
return errors.Annotate(err, "draw window background")
|
||||
}
|
||||
if err := drawWindowBorder(r, int32(borderWidth)); err != nil {
|
||||
return errors.Annotate(err, "draw window border")
|
||||
}
|
||||
|
||||
// Draw items
|
||||
for i, itm := range w.curItems {
|
||||
offsetY := int32(i*(itemHeight()+paddingY) + paddingY + borderWidth)
|
||||
if err := itm.render(r, offsetY); err != nil {
|
||||
return errors.Annotate(err, "render item")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func drawWindowBackground(r *sdl.Renderer) error {
|
||||
if err := setDrawColor(r, theme.Background); err != nil {
|
||||
return errors.Annotate(err, "set background color")
|
||||
}
|
||||
backgroundFrame := &sdl.Rect{
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: int32(windowWidth),
|
||||
H: int32(windowHeight),
|
||||
}
|
||||
if err := r.FillRect(backgroundFrame); err != nil {
|
||||
return errors.Annotate(err, "draw background")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func drawWindowBorder(r *sdl.Renderer, width int32) error {
|
||||
if err := setDrawColor(r, theme.ItemBackground); err != nil {
|
||||
return errors.Annotate(err, "set border color")
|
||||
}
|
||||
for i := int32(1); i <= width; i++ {
|
||||
borderFrame := &sdl.Rect{
|
||||
X: i,
|
||||
Y: i,
|
||||
W: int32(windowWidth) - i*2,
|
||||
H: int32(windowHeight) - i*2,
|
||||
}
|
||||
if err := r.DrawRect(borderFrame); err != nil {
|
||||
return errors.Annotate(err, "draw border")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func useFont(name string) error {
|
||||
fontPath, err := fonts.Find(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fontPath == "" {
|
||||
return errors.New("font not found")
|
||||
}
|
||||
|
||||
font, err = ttf.OpenFont(fontPath, fontSize)
|
||||
if err != nil {
|
||||
return errors.Annotatef(err, "load font %s", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func itemHeight() int {
|
||||
return fontSize + 2*paddingY
|
||||
}
|
||||
|
||||
func setDrawColor(r *sdl.Renderer, c sdl.Color) error {
|
||||
return r.SetDrawColor(c.R, c.G, c.B, c.A)
|
||||
}
|
Loading…
Reference in New Issue