Persist settings via configuration file

Resolves #11.
This commit is contained in:
Trevor Slocum 2020-02-13 17:27:42 -08:00
parent 16b7383f42
commit 23edcfcdee
8 changed files with 247 additions and 56 deletions

View file

@ -1,4 +1,5 @@
0.1.6:
- Persist settings via configuration file
- Draw playfield border as solid blocks
0.1.5:

65
cmd/netris/config.go Normal file
View file

@ -0,0 +1,65 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"path"
"gitlab.com/tslocum/netris/pkg/event"
"gopkg.in/yaml.v2"
)
type appConfig struct {
Input map[event.GameAction][]string // Keybinds
Name string
}
var config = &appConfig{}
func defaultConfigPath() string {
homedir, err := os.UserHomeDir()
if err == nil && homedir != "" {
return path.Join(homedir, ".config", "netris", "config.yaml")
}
return ""
}
func readConfig(configPath string) error {
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if configPath != defaultConfigPath() {
return fmt.Errorf("failed to read configuration: %s", err)
}
return nil
}
configData, err := ioutil.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read configuration: %s", err)
}
err = yaml.Unmarshal(configData, config)
if err != nil {
return fmt.Errorf("failed to parse configuration: %s", err)
}
return nil
}
func saveConfig(configPath string) error {
config.Name = nickname
out, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal configuration: %s", err)
}
os.MkdirAll(path.Dir(configPath), 0755) // Ignore error
err = ioutil.WriteFile(configPath, out, 0644)
if err != nil {
return fmt.Errorf("failed to write to %s: %s", configPath, err)
}
return nil
}

View file

@ -8,6 +8,7 @@ import (
"strings"
"github.com/gdamore/tcell"
"gitlab.com/tslocum/cbind"
"gitlab.com/tslocum/netris/pkg/event"
"gitlab.com/tslocum/netris/pkg/game"
)
@ -20,27 +21,59 @@ type Keybinding struct {
a event.GameAction
}
var keybindings = []*Keybinding{
{r: 'z', a: event.ActionRotateCCW},
{r: 'Z', a: event.ActionRotateCCW},
{r: 'x', a: event.ActionRotateCW},
{r: 'X', a: event.ActionRotateCW},
{k: tcell.KeyLeft, a: event.ActionMoveLeft},
{r: 'h', a: event.ActionMoveLeft},
{r: 'H', a: event.ActionMoveLeft},
{k: tcell.KeyDown, a: event.ActionSoftDrop},
{r: 'j', a: event.ActionSoftDrop},
{r: 'J', a: event.ActionSoftDrop},
{k: tcell.KeyUp, a: event.ActionHardDrop},
{r: 'k', a: event.ActionHardDrop},
{r: 'K', a: event.ActionHardDrop},
{k: tcell.KeyRight, a: event.ActionMoveRight},
{r: 'l', a: event.ActionMoveRight},
{r: 'L', a: event.ActionMoveRight},
var actionHandlers = map[event.GameAction]func(*tcell.EventKey) *tcell.EventKey{
event.ActionRotateCCW: rotateCCW,
event.ActionRotateCW: rotateCW,
event.ActionMoveLeft: moveLeft,
event.ActionMoveRight: moveRight,
event.ActionSoftDrop: softDrop,
event.ActionHardDrop: hardDrop,
}
var inputConfig = cbind.NewConfiguration()
var draftKeybindings []*Keybinding
func setKeyBinds() error {
if len(config.Input) == 0 {
setDefaultKeyBinds()
}
for a, keys := range config.Input {
a = event.GameAction(strings.ToLower(string(a)))
handler := actionHandlers[a]
if handler == nil {
return fmt.Errorf("failed to set keybind for %s: unknown action", a)
}
for _, k := range keys {
mod, key, ch, err := cbind.Decode(k)
if err != nil {
return fmt.Errorf("failed to set keybind %s for %s: %s", k, a, err)
}
if key == tcell.KeyRune {
inputConfig.SetRune(mod, ch, handler)
} else {
inputConfig.SetKey(mod, key, handler)
}
}
}
return nil
}
func setDefaultKeyBinds() {
config.Input = map[event.GameAction][]string{
event.ActionRotateCCW: {"z", "Z"},
event.ActionRotateCW: {"x", "X"},
event.ActionMoveLeft: {"Left", "h", "H"},
event.ActionMoveRight: {"Right", "l", "L"},
event.ActionSoftDrop: {"Down", "j", "J"},
event.ActionHardDrop: {"Up", "k", "K"},
}
}
func scrollMessages(direction int) {
var scroll int
if showLogLines > 3 {
@ -396,16 +429,59 @@ func handleKeypress(ev *tcell.EventKey) *tcell.EventKey {
return nil
}
for _, bind := range keybindings {
if (bind.k != 0 && bind.k != k) || (bind.r != 0 && bind.r != r) || (bind.m != 0 && bind.m != ev.Modifiers()) {
continue
} else if activeGame == nil {
break
}
return inputConfig.Capture(ev)
}
activeGame.ProcessAction(bind.a)
return nil
func rotateCCW(ev *tcell.EventKey) *tcell.EventKey {
if activeGame == nil {
return ev
}
return ev
activeGame.ProcessAction(event.ActionRotateCCW)
return nil
}
func rotateCW(ev *tcell.EventKey) *tcell.EventKey {
if activeGame == nil {
return ev
}
activeGame.ProcessAction(event.ActionRotateCW)
return nil
}
func moveLeft(ev *tcell.EventKey) *tcell.EventKey {
if activeGame == nil {
return ev
}
activeGame.ProcessAction(event.ActionMoveLeft)
return nil
}
func moveRight(ev *tcell.EventKey) *tcell.EventKey {
if activeGame == nil {
return ev
}
activeGame.ProcessAction(event.ActionMoveRight)
return nil
}
func softDrop(ev *tcell.EventKey) *tcell.EventKey {
if activeGame == nil {
return ev
}
activeGame.ProcessAction(event.ActionSoftDrop)
return nil
}
func hardDrop(ev *tcell.EventKey) *tcell.EventKey {
if activeGame == nil {
return ev
}
activeGame.ProcessAction(event.ActionHardDrop)
return nil
}

View file

@ -6,6 +6,8 @@ import (
"strconv"
"time"
"gitlab.com/tslocum/cbind"
"github.com/gdamore/tcell"
"gitlab.com/tslocum/cview"
"gitlab.com/tslocum/netris/pkg/event"
@ -121,9 +123,6 @@ func selectTitleButton() {
drawGhostPieceUnsaved = drawGhostPiece
draftKeybindings = make([]*Keybinding, len(keybindings))
copy(draftKeybindings, keybindings)
app.SetRoot(gameSettingsContainerGrid, true)
updateTitle()
case 2:
@ -160,8 +159,28 @@ func selectTitleButton() {
if currentSelection == 8 {
drawGhostPiece = drawGhostPieceUnsaved
keybindings = make([]*Keybinding, len(draftKeybindings))
copy(keybindings, draftKeybindings)
for _, bind := range draftKeybindings {
if bind.k == tcell.KeyRune {
inputConfig.SetRune(bind.m, bind.r, actionHandlers[bind.a])
} else {
inputConfig.SetKey(bind.m, bind.k, actionHandlers[bind.a])
}
encoded, err := cbind.Encode(bind.m, bind.k, bind.r)
if err == nil && encoded != "" {
// Remove existing keybinds
for existingBindAction, existingBinds := range config.Input {
for i, existingBind := range existingBinds {
if existingBind == encoded {
config.Input[existingBindAction] = append(config.Input[existingBindAction][:i], config.Input[existingBindAction][i+1:]...)
break
}
}
}
// Set keybind
config.Input[bind.a] = append(config.Input[bind.a], encoded)
}
}
}
draftKeybindings = nil

View file

@ -39,6 +39,8 @@ var (
nicknameFlag string
configPath string
blockSize = 0
fixedBlockSize bool
@ -76,6 +78,7 @@ func main() {
flag.StringVar(&connectAddress, "connect", "", "connect to server address or socket path")
flag.StringVar(&serverAddress, "server", game.DefaultServer, "server address or socket path")
flag.StringVar(&debugAddress, "debug-address", "", "address to serve debug info")
flag.StringVar(&configPath, "config", "", "path to configuration file")
flag.BoolVar(&logDebug, "debug", false, "enable debug logging")
flag.BoolVar(&logVerbose, "verbose", false, "enable verbose logging")
flag.Parse()
@ -100,8 +103,8 @@ func main() {
logLevel = game.LogDebug
}
if game.Nickname(nicknameFlag) != "" {
nickname = game.Nickname(nicknameFlag)
if configPath == "" {
configPath = defaultConfigPath()
}
if debugAddress != "" {
@ -110,6 +113,22 @@ func main() {
}()
}
err := readConfig(configPath)
if err != nil {
log.Fatalf("failed to read configuration file: %s", err)
}
err = setKeyBinds()
if err != nil {
log.Fatalf("failed to set keybinds: %s", err)
}
if nicknameFlag != "" && game.Nickname(nicknameFlag) != "" {
nickname = game.Nickname(nicknameFlag)
} else if config.Name != "" && game.Nickname(config.Name) != "" {
nickname = game.Nickname(config.Name)
}
app, err := initGUI(connectAddress != "")
if err != nil {
log.Fatalf("failed to initialize GUI: %s", err)
@ -168,6 +187,11 @@ func main() {
closeGUI()
err := saveConfig(configPath)
if err != nil {
log.Printf("warning: failed to save configuration: %s", err)
}
os.Exit(0)
}()

8
go.mod
View file

@ -9,8 +9,8 @@ require (
github.com/gdamore/tcell v1.3.0
github.com/gliderlabs/ssh v0.2.2
github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-runewidth v0.0.8 // indirect
gitlab.com/tslocum/cview v1.4.2-0.20200128151041-339db80f666d
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 // indirect
gitlab.com/tslocum/cbind v0.1.1
gitlab.com/tslocum/cview v1.4.4-0.20200213233906-2a8ba3160c01
golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678
gopkg.in/yaml.v2 v2.2.8
)

22
go.sum
View file

@ -17,25 +17,31 @@ github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
gitlab.com/tslocum/cview v1.4.2-0.20200128151041-339db80f666d h1:5rPwwmNYGLcOsyawvAw7m/Jtwp5rAuvLoqVW5k09AP0=
gitlab.com/tslocum/cview v1.4.2-0.20200128151041-339db80f666d/go.mod h1:QbxliYQa2I32UJH2boP54jq6tnWlgm6yViaFXKGDfuM=
gitlab.com/tslocum/cbind v0.1.1 h1:JXXtxMWHgWLvoF+QkrvcNvOQ59juy7OE1RhT7hZfdt0=
gitlab.com/tslocum/cbind v0.1.1/go.mod h1:rX7vkl0pUSg/yy427MmD1FZAf99S7WwpUlxF/qTpPqk=
gitlab.com/tslocum/cview v1.4.4-0.20200213233906-2a8ba3160c01 h1:YdNvWO1OoGnmtjdAx84+3MWW1DAANWV8WLN9ZXE7PZc=
gitlab.com/tslocum/cview v1.4.4-0.20200213233906-2a8ba3160c01/go.mod h1:+bEf1cg6IoWvL16YHJAKwGGpQf5s/nxXAA7YJr+WOHE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U=
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 h1:wCWoJcFExDgyYx2m2hpHgwz8W3+FPdfldvIgzqDIhyg=
golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -1,16 +1,16 @@
package event
type GameAction int
type GameAction string
const (
ActionUnknown GameAction = iota
ActionRotateCCW
ActionRotateCW
ActionMoveLeft
ActionMoveRight
ActionSoftDrop
ActionHardDrop
ActionPing
ActionStats
ActionNick
ActionUnknown = ""
ActionRotateCCW = "rotate-ccw"
ActionRotateCW = "rotate-cw"
ActionMoveLeft = "move-left"
ActionMoveRight = "move-right"
ActionSoftDrop = "soft-drop"
ActionHardDrop = "hard-drop"
ActionPing = "ping"
ActionStats = "stats"
ActionNick = "nick"
)