1
0
Fork 0

Initial commit

This commit is contained in:
Gregory Eremin 2019-06-15 15:47:49 +02:00
commit 8e194e278c
10 changed files with 649 additions and 0 deletions

12
cmd/main.go Normal file
View File

@ -0,0 +1,12 @@
package main
import (
"log"
menu "github.com/localhots/themenu"
)
func main() {
menu.Main()
log.Println("Shut down")
}

26
exec.go Normal file
View File

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

49
fonts/fonts.go Normal file
View File

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

13
fonts/fonts_darwin.go Normal file
View File

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

9
fonts/fonts_fallback.go Normal file
View File

@ -0,0 +1,9 @@
// +build !linux,!darwin
package fonts
func fontDirs() []string {
return []string{
".", // Current directory
}
}

15
fonts/fonts_linux.go Normal file
View File

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

227
item.go Normal file
View File

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

147
main.go Normal file
View File

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

37
theme.go Normal file
View File

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

114
window.go Normal file
View File

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