This repository has been archived on 2024-09-01. You can view files and clone it, but cannot push or open issues or pull requests.
Files
al-skel/etc/skel/go/src/gorice/gorice.go
2017-10-23 16:35:35 -07:00

752 lines
20 KiB
Go

package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"github.com/ghodss/yaml"
"github.com/lucasb-eyer/go-colorful"
"io"
"io/ioutil"
"math/rand"
"os"
"os/exec"
"reflect"
"strconv"
"strings"
"text/template"
"time"
)
// ConfigData is used to to load the json data.
type ConfigData struct {
Files []string `json:"files"` // A list of files that the config uses.
Current string `json:"current"` // The current config being used.
}
// Arguments is a struct I use to pass into each command.
type Arguments struct {
Param string // The second argument passed to the script.
Files []string // The rest of the arguments passed. This is always a list of files.
Force bool // Used to force the loading of a config
}
// Template is data that will be passed to the template function.
type Template struct {
Data map[string]interface{} // A mapping of key, value pairs for config info.
Name string // The name of the config you're loading.
}
// FillDefaults will compare a second Template struct and fill in any that are missing in the original Template.
func (t *Template) FillDefaults(other Template) {
for name, value := range other.Data {
_, ok := t.Data[name]
if !ok {
t.Data[name] = value
}
}
}
// RGB is just a simple struct that contains RGB information.
type RGB struct {
R float64
G float64
B float64
}
// Command is information and a reference to the commands usable at the command line.
type Command struct {
Reference func(Arguments)
Description string
Usage string
}
// confDir is the config directory that stores all of the data.
var confDir = os.ExpandEnv("$HOME/.gorice/")
// EDITOR is the bash variable $EDITOR.
var EDITOR = os.Getenv("EDITOR")
// commands is a mapping of usable command line commands.
var commands = map[string]Command{
"create": Command{
Reference: createGroup,
Description: "Creates a group to store configs.",
Usage: "group_name"},
"load": Command{
Reference: loadConfig,
Description: "Loads a config.",
Usage: "group_name/config_name"},
"track": Command{
Reference: addFiles,
Description: "Tracks files to be put through the config parser for a group.",
Usage: "group_name /path/to/file.txt /path/to/other.whatever"},
"tracked": Command{
Reference: listFiles,
Description: "Lists the files the program is tracking for a group.",
Usage: "group_name"},
"untrack": Command{
Reference: removeFiles,
Description: "Removes files from the tracked list for a group.",
Usage: "group_name /path/to/file.txt /path.to/other.whatever"},
"edit": Command{
Reference: editConfig,
Description: "Edits a config file.",
Usage: "group_name/config_name"},
"list": Command{
Reference: listConfigs,
Description: "Lists the configs you have for a group.",
Usage: "group_name"},
"reload": Command{
Reference: reloadConfig,
Description: "Reloads the current config for a group.",
Usage: "group_name"},
"delete": Command{
Reference: deleteConfig,
Description: "Removes a config from the group.",
Usage: "group_name/config_name",
},
"dump": Command{
Reference: dumpGroup,
Description: "Loads every config and dumps the data into a file.",
Usage: "group_name /path/to/output.template",
},
"check": Command{
Reference: checkGroup,
Description: "Checks all configs in the group, if any cannot be loaded it outputs them.",
Usage: "group_name",
},
}
// funcmap is a simple mapping of functions that will get passed to the template files.
var funcmap = template.FuncMap{
"rgb": hex2rgb,
"sumInts": sumInts,
"increment": increment,
"rgbmax": hex2rgbMax,
}
// Increment increments a color by a certain amount, can be negative.
func increment(hex string, amount float64) string {
color, _ := colorful.Hex(hex)
r, g, b := (color.R*255)+amount, (color.G*255)+amount, (color.B*255)+amount
// Not exactly sure if I need pointers for this but whatever.
colorz := []*float64{&r, &g, &b}
for c := range colorz {
if c < 0 {
c = 0
}
if c > 255 {
c = 255
}
}
new := colorful.Color{R: r / 255, G: g / 255, B: b / 255}
return new.Hex()
}
// sumInts sums some ints. Simple huh?
func sumInts(ints ...int) int {
var total int
for i := range ints {
total += i
}
return total
}
// hex2rgb congers a hex string to an RGB structure. Used in the templates.
func hex2rgb(hex string) RGB {
color, _ := colorful.Hex(hex)
return RGB{color.R * 255, color.G * 255, color.B * 255}
}
func hex2rgbMax(hex string, max float64) RGB {
color, _ := colorful.Hex(hex)
return RGB{color.R * max, color.G * max, color.B * max}
}
// addFiles adds files to the configuration group. Nothing more, nothing less.
func addFiles(arg Arguments) {
backupFiles(arg.Files)
data := loadData(arg.Param)
data.Files = append(data.Files, arg.Files...)
dumpData(arg.Param, data)
}
// removeFiles removes files from the configuration group.
func removeFiles(arg Arguments) {
data := loadData(arg.Param)
files := data.Files
new := []string{}
for _, name := range files {
if !isTracking(arg.Files, name) {
new = append(new, name)
} else {
os.Remove(fmt.Sprintf("%s.template", name))
}
}
data.Files = new
dumpData(arg.Param, data)
}
func isTracking(array []string, data string) bool {
for _, item := range array {
if item == data {
return true
}
}
return false
}
// listFiles lists files tracked in the configuration group.
func listFiles(arg Arguments) {
data := loadData(arg.Param)
files := data.Files
for _, name := range files {
fmt.Println(name)
}
}
// dumpData takes a huge data dump in a json file.
// Trust me, it will feel a lot better once it's done.
func dumpData(groupName string, data ConfigData) {
jsonData, err := json.Marshal(data)
fullpath := fmt.Sprintf("%stemplates/%s/data.json", confDir, groupName)
if checkError(err, false) {
return
}
fileWrite(fullpath, string(jsonData))
}
// dumpGroup outputs all information from a group into a template.
func dumpGroup(arg Arguments) {
groupName := arg.Param
files := arg.Files
output := []Template{}
dir := configList(groupName)
groupName = strings.TrimPrefix(groupName, ".")
for _, name := range dir {
data, err := loadTemplateData(groupName, name+".yaml")
checkError(err, true)
output = append(output, data)
}
for _, file := range files {
if !strings.HasSuffix(file, ".template") {
fmt.Printf("File %s is not a template file, skipping.\n", file)
}
err := dumpGroupFile(file, output)
checkError(err, false)
}
}
// dumpGroupFile outputs information to a single file.
func dumpGroupFile(templateFile string, info []Template) error {
var output bytes.Buffer
originalFile := strings.TrimSuffix(templateFile, ".template")
data, err := fileRead(templateFile)
if err != nil {
return err
}
template := template.New(originalFile)
template.Funcs(funcmap)
tmp, err := template.Parse(data)
if err != nil {
return err
}
err = tmp.Execute(&output, info)
if err != nil {
return err
}
result := output.String()
os.Remove(originalFile)
fileWrite(originalFile, result)
return nil
}
func checkGroup(arg Arguments) {
groupName := arg.Param
files := configList(groupName)
groupName = strings.TrimPrefix(groupName, ".")
for _, name := range files {
_, err := loadTemplateData(groupName, name+".yaml")
if err != nil {
fmt.Println(name)
}
}
}
// listConfigs outputs a list of the configs you have in a certain group.
func listConfigs(arg Arguments) {
var groupName = string(arg.Param)
items := configList(groupName)
for _, item := range items {
fmt.Println(item)
}
}
func configList(groupName string) []string {
showHidden := false
output := []string{}
if strings.HasPrefix(groupName, ".") {
groupName = strings.TrimPrefix(groupName, ".")
showHidden = true
}
folderPath := fmt.Sprintf("%stemplates/%s", confDir, groupName)
dir, err := ioutil.ReadDir(folderPath)
checkError(err, true)
for _, file := range dir {
name := file.Name()
if (strings.HasPrefix(name, ".") && !showHidden) || !strings.HasSuffix(name, ".yaml") {
continue
}
output = append(output, strings.TrimSuffix(name, ".yaml"))
}
return output
}
// loadData loads one of the config groups data.json file
func loadData(groupName string) ConfigData {
var config ConfigData
fullPath := fmt.Sprintf("%stemplates/%s/data.json", confDir, groupName)
file, err := os.Open(fullPath)
if !checkError(err, false) {
data, _ := ioutil.ReadAll(file)
json.Unmarshal(data, &config)
return config
}
return ConfigData{}
}
// watchConfig watches a config file for changes while you are editing it.
// This is used to reload it on the fly without quitting the editor.
func watchConfig(group string, name string, channel chan bool) {
fullpath := fmt.Sprintf("%stemplates/%s/%s.yaml", confDir, group, name)
originalStat, _ := os.Stat(fullpath)
watch := true
for watch {
newStat, _ := os.Stat(fullpath)
if newStat.ModTime() != originalStat.ModTime() {
originalStat = newStat
configName := fmt.Sprintf("%s/%s", group, name)
loadConfig(Arguments{Param: configName, Files: nil, Force: true})
}
select {
case check := <-channel:
watch = check
default:
watch = true
}
time.Sleep(1 * time.Second)
}
}
// editConfig loads a specified configuration in your favorite editor! If you have $EDITOR set.
func editConfig(arg Arguments) {
var name string
var group string
var fullpath string
split := strings.Split(arg.Param, "/")
group = split[0]
name = split[1]
jsonData := loadData(group)
if name == "reload" {
fullpath = fmt.Sprintf("%stemplates/%s/reload.sh.template", confDir, group)
} else if name == "current" {
fullpath = fmt.Sprintf("%stemplates/%s", confDir, jsonData.Current)
name = strings.Split(jsonData.Current, "/")[1]
name = strings.TrimSuffix(name, ".yaml")
arg.Param = fmt.Sprintf("%s/%s", group, name)
} else {
fullpath = fmt.Sprintf("%stemplates/%s/%s.yaml", confDir, group, name)
}
defaultConfig := fmt.Sprintf("%stemplates/%s/default.yaml", confDir, group)
_, err := os.Stat(fullpath)
if err != nil {
fileCopy(defaultConfig, fullpath)
}
watchChan := make(chan bool)
wait := false
if jsonData.Current == fmt.Sprintf("%s/%s.yaml", group, name) {
go watchConfig(group, name, watchChan)
wait = true
}
cmd := exec.Command(EDITOR, fullpath)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Run()
if wait {
watchChan <- false
}
defaultData, _ := fileRead(defaultConfig)
currentData, _ := fileRead(fullpath)
if defaultData == currentData && name != "default" {
os.Remove(fullpath)
}
}
// createGroup creates a config group and sets up all of the default files.
func createGroup(arg Arguments) {
var group string
var files []string
var groupPath string
files = arg.Files
group = arg.Param
groupPath = fmt.Sprintf("%stemplates/%s/", confDir, group)
ok := os.Mkdir(groupPath, 0777)
if ok == nil {
fmt.Printf("Creating %s\n", groupPath)
os.Create(groupPath + "default.yaml")
os.Create(groupPath + "reload.sh.template")
os.Chmod(groupPath+"reload.sh", 0755)
confFile, _ := os.OpenFile(groupPath+"data.json", os.O_RDWR|os.O_CREATE, 0666)
data := ConfigData{Files: files}
bytes, err := json.Marshal(data)
if !checkError(err, false) {
confFile.Write(bytes)
backupFiles(files)
}
}
}
func deleteConfig(arg Arguments) {
split := strings.Split(arg.Param, "/")
group := split[0]
config := split[1]
fullpath := fmt.Sprintf("%stemplates/%s/%s.yaml", confDir, group, config)
err := os.Remove(fullpath)
checkError(err, true)
}
// reloadConfig reloads the active config.
// You only need to specify the group name for this to work.
func reloadConfig(arg Arguments) {
name := arg.Param
data := loadData(name)
config := strings.Replace(data.Current, ".yaml", "", 1)
args := Arguments{Param: config, Force: true, Files: nil}
loadConfig(args)
}
// loadConfig loads a config from a group. If the group/name matches
// the current config it will not load anything.
// If it loads the config it will then attempt to reload the config by
// running its respective reload.sh
func loadConfig(arg Arguments) {
split := strings.Split(arg.Param, "/")
group := split[0]
name := split[1]
// Check if the config you're loading is the current one.
// Unless the arg.Force is true execution will stop here.
configData := loadData(group)
if configData.Current == fmt.Sprintf("%s/%s.yaml", group, name) && arg.Force == false {
return
}
currentSplit := strings.Split(configData.Current, "/")
ignoreName := ""
if len(currentSplit) > 1 {
ignoreName = currentSplit[1]
}
// Find the name, possibly from a random list.
switch name {
case "random":
folderPath := fmt.Sprintf("%stemplates/%s", confDir, group)
dir, _ := ioutil.ReadDir(folderPath)
files := shuffleFolder(dir, ignoreName, false)
name = files[0].Name()
case ".random":
folderPath := fmt.Sprintf("%stemplates/%s", confDir, group)
dir, _ := ioutil.ReadDir(folderPath)
files := shuffleFolder(dir, ignoreName, true)
name = files[0].Name()
default:
name = name + ".yaml"
}
tstruct, err := loadTemplateData(group, name)
if err != nil {
return
}
// Iterate through the files and load the template data then copy them.
for _, filepath := range configData.Files {
templatePath := filepath + ".template"
err := templateFile(filepath, templatePath, tstruct)
checkError(err, false)
}
// Attempt to reload the config.
reloadPath := fmt.Sprintf("%stemplates/%s/reload.sh", confDir, group)
err = templateFile(reloadPath, reloadPath+".template", tstruct)
if !checkError(err, false) {
cmd := exec.Command("/bin/sh", reloadPath)
cmd.Run()
}
// Update the 'Current' config.
jsonData := loadData(group)
jsonData.Current = fmt.Sprintf("%s/%s", group, name)
dumpData(group, jsonData)
}
// LoadTemplateData loads the template information for a group / name combo.
func loadTemplateData(group string, name string) (Template, error) {
var config map[string]interface{}
var defaultConfig map[string]interface{}
// Concat the fullpath of the file
fullpath := fmt.Sprintf("%stemplates/%s/%s", confDir, group, name)
data, err := fileRead(fullpath)
if err != nil {
return Template{}, err
}
// Load the YAML data
err = yaml.Unmarshal([]byte(data), &config)
if err != nil {
return Template{}, err
}
items := parseYAML(config)
tstruct := Template{Data: items, Name: fmt.Sprintf("%s/%s", group, name)}
// Load the default YAML
defaultPath := fmt.Sprintf("%stemplates/%s/default.yaml", confDir, group)
defaultData, err := fileRead(defaultPath)
// If there arent any errors, implement the default data.
if !checkError(err, false) {
err = yaml.Unmarshal([]byte(defaultData), &defaultConfig)
if err != nil {
return Template{}, err
}
ditems := parseYAML(defaultConfig)
dstruct := Template{Data: ditems}
tstruct.FillDefaults(dstruct)
}
return tstruct, nil
}
// templateFile runs a certain file through the templating engine.
func templateFile(filepath string, templatePath string, templateStruct Template) error {
var replaced bytes.Buffer
fileData, err := fileRead(templatePath)
if err != nil {
return err
}
newTemplate := template.New(filepath)
newTemplate.Funcs(funcmap)
tmp, _ := newTemplate.Parse(fileData)
tmp.Execute(&replaced, templateStruct)
result := replaced.String()
os.Remove(filepath)
err = fileWrite(filepath, result)
if err != nil {
return err
}
return nil
}
// backupFiles iterates through a slice of files and copies them to a backup.
func backupFiles(files []string) {
for _, directory := range files {
newpath := directory + ".template"
fileCopy(directory, newpath)
}
}
// A simple filecopy. Not that much to see here.
func fileCopy(source string, dest string) {
sfile, err := os.Open(source)
checkError(err, true)
defer sfile.Close()
dfile, err := os.Create(dest)
checkError(err, true)
defer dfile.Close()
_, err = io.Copy(dfile, sfile)
checkError(err, true)
dfile.Sync()
checkError(err, true)
}
// checkError is a simple function that checks an error, returns true if
// there was an error and false otherwise.
// The user can specify a fatal param, if this is true the code will stop.
func checkError(err error, fatal bool) bool {
if err != nil {
fmt.Println("error:", err)
if fatal {
os.Exit(0)
}
return true
}
return false
}
// shuffleFolder randomly shuffles a list of files in a folder.
// Used when the user loads a random config.
// TODO: Replace this with a random choice rather than random shuffle.
func shuffleFolder(data []os.FileInfo, ignore string, hidden bool) []os.FileInfo {
var output []os.FileInfo
for _, file := range data {
name := file.Name()
if (strings.HasPrefix(name, ".") && !hidden) || !strings.HasSuffix(name, ".yaml") || name == "default.yaml" || name == ignore {
continue
}
output = append(output, file)
}
rand.Seed(time.Now().UnixNano())
for i := range output {
j := rand.Intn(i + 1)
output[i], output[j] = output[j], output[i]
}
return output
}
// fileWrite writes to a file. Pretty simple.
func fileWrite(filepath string, data string) error {
os.Remove(filepath)
file, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0777)
if err != nil {
return err
}
defer file.Close()
_, err = file.Write([]byte(data))
if err != nil {
return err
}
return nil
}
// fileRead reads a file. Pretty simple.
func fileRead(filepath string) (string, error) {
file, err := os.Open(filepath)
if err != nil {
return "", err
}
defer file.Close()
data, err := ioutil.ReadAll(file)
if err != nil {
return "", err
}
return string(data), nil
}
// parseYAML parses the YAML configs via the flatten function,
// this will flatten all the elements into simple map[string]interface{}
// structure.
func parseYAML(config map[string]interface{}) map[string]interface{} {
items := make(map[string]interface{})
flatten(config, []string{}, items)
return items
}
// Do the heavy lifting of parsing and flattening of the YAML config files.
func flatten(level map[string]interface{}, path []string, items map[string]interface{}) {
for name, value := range level {
path = append(path, name)
fullpath := strings.Join(path, "_")
switch value.(type) {
case string:
items[fullpath] = value.(string)
case float64:
items[fullpath] = strconv.Itoa(int(value.(float64)))
case map[string]interface{}:
flatten(value.(map[string]interface{}), path, items)
case []interface{}:
values := value.([]interface{})
items[fullpath] = values
default:
fmt.Println(reflect.TypeOf(value))
}
path = path[:len(path)-1]
}
}
// Usage prints out the usage for this program.
func Usage() {
fmt.Println("Usage: gorice [-f] command [args]")
fmt.Println()
flag.PrintDefaults()
padding := 0
for command := range commands {
if len(command) > padding {
padding = len(command) + 1
}
}
fmt.Println("\nList of commands: ")
for command := range commands {
info := commands[command]
length := len(command)
strPadding := strings.Repeat(" ", (padding - length))
fmt.Printf(" info: %s%s- %s\n usage: %s %s\n\n", command, strPadding, info.Description, command, info.Usage)
}
fmt.Println()
os.Exit(0)
}
func main() {
flag.Usage = Usage
var command string
var param string
var files []string
var force bool
flag.BoolVar(&force, "f", false, "Forces a config to be reloaded.")
flag.Parse()
// Create the folder structure
os.MkdirAll(fmt.Sprintf("%stemplates/", confDir), 0777)
args := flag.Args()
if len(args) < 2 {
fmt.Println("Not enough arguments.")
return
}
command = args[0]
param = args[1]
files = args[2:]
// Call the function if it exists
fn, ok := commands[command]
if ok {
fn.Reference(Arguments{Param: param, Files: files, Force: force})
} else {
fmt.Println("Unknown command", command)
}
}