diff --git a/.air.toml b/.air.toml
new file mode 100644
index 0000000..435a885
--- /dev/null
+++ b/.air.toml
@@ -0,0 +1,17 @@
+# Working directory
+root = "."
+
+# The main Go file
+main = "main.go"
+
+bin = "tmp/main.exe"
+
+# Watching all Go files, excluding certain directories
+[build]
+ cmd = "go build -o ./tmp/main.exe main.go"
+ include_ext = ["go"]
+ exclude_dir = ["fe", "panels", "builds"]
+
+# Restart on file changes
+[log]
+ time = true
diff --git a/.gitignore b/.gitignore
index 3c3629e..345ff6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,14 @@
+.env
+config.js
+
+devices/*
+tmp/
+log.txt
+
+Macrame.exe
+
+public
+macros/*
+builds
node_modules
+ToDo.md
\ No newline at end of file
diff --git a/add-gpl-header.sh b/add-gpl-header.sh
new file mode 100644
index 0000000..fe20142
--- /dev/null
+++ b/add-gpl-header.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+# Directory to start searching from (default to current directory)
+DIR="${1:-.}"
+
+# Define the GPLv3 header content
+GPL_HEADER="Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see ."
+
+# Loop through all files in the directory (and subdirectories), excluding node_modules
+find "$DIR" \( -iname \*.go -o \( -path "$DIR/fe/src/*.js" -o -path "$DIR/fe/src/*.vue" -o -path "$DIR/fe/src/*.css" \) \) ! -path "*/node_modules/*" | while read file
+do
+ # Check if the file already has a GPL header
+ if ! grep -q "Copyright (C) 2025 Jesse Malotaux" "$file"; then
+ # Check if it's a Vue file and handle it carefully
+ if [[ "$file" == *.vue ]]; then
+ echo "Adding GPL header to: $file"
+ # Prepend the GPL header to Vue files as raw text
+ # Make sure we don't add comment marks that break Vue syntax
+ echo -e "\n\n$(cat "$file")" > "$file"
+ else
+ echo "Adding GPL header to: $file"
+ # Prepend the GPL header to other files (go, js, ts, etc.) as comments
+ echo -e "/*\n$GPL_HEADER\n*/\n\n$(cat "$file")" > "$file"
+ fi
+ else
+ echo "GPL header already present in: $file"
+ fi
+done
+
+echo "GPL header addition complete!"
diff --git a/app/api.go b/app/api.go
new file mode 100644
index 0000000..e9c2f84
--- /dev/null
+++ b/app/api.go
@@ -0,0 +1,137 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package app
+
+import (
+ "macrame/app/helper"
+ "mime"
+ "net/http"
+ "path/filepath"
+ "strings"
+)
+
+func ApiCORS(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, *http.Request) {
+ origin := r.Header.Get("Origin")
+
+ w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
+
+ if strings.HasPrefix(r.Host, "192.168.") {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ }
+
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Language, Accept-Encoding")
+
+ return w, r
+}
+
+func ApiGet(w http.ResponseWriter, r *http.Request) {
+ root, err := filepath.Abs("public")
+ if err != nil {
+ MCRMLog("ApiGet Abs Error: ", err)
+ return
+ }
+
+ var file string
+
+ if strings.Contains(r.URL.Path, "/config.js") {
+ file = filepath.Join(root, "config.js")
+ w.Header().Set("Content-Type", "text/javascript") // set content type header
+ } else if r.URL.Path != "/" {
+ file = filepath.Join(root, r.URL.Path)
+ }
+
+ contentType := mime.TypeByExtension(filepath.Ext(file)) // get content type
+
+ if contentType == "" {
+ file = filepath.Join(root, "index.html")
+ }
+
+ http.ServeFile(w, r, file)
+}
+
+func ApiPost(w http.ResponseWriter, r *http.Request) {
+ access, data, err := helper.EndpointAccess(w, r)
+
+ if !access || err != nil {
+ MCRMLog("ApiPost EndPointAccess Error: ", err)
+ return
+ }
+
+ if data != "" {
+ ApiAuth(data, w, r)
+ return
+ }
+
+ switch r.URL.Path {
+ case "/macro/check":
+ CheckMacro(w, r)
+ case "/macro/record":
+ SaveMacro(w, r)
+ case "/macro/list":
+ ListMacros(w, r)
+ case "/macro/open":
+ OpenMacro(w, r)
+ case "/macro/delete":
+ DeleteMacro(w, r)
+ case "/macro/play":
+ PlayMacro("", w, r)
+ case "/device/server/ip":
+ ListServerIP(w)
+ case "/device/list":
+ DeviceList(w, r)
+ case "/device/access/check":
+ DeviceAccessCheck(w, r)
+ case "/device/access/request":
+ DeviceAccessRequest(w, r)
+ case "/device/link/ping":
+ PingLink(w, r)
+ case "/device/link/start":
+ StartLink(w, r)
+ case "/device/link/poll":
+ PollLink(w, r)
+ case "/device/link/remove":
+ RemoveLink("", w, r)
+ case "/device/handshake":
+ Handshake(w, r)
+ case "/panel/list":
+ PanelList(w, r)
+ case "/panel/get":
+ GetPanel("", w, r)
+ case "/panel/save/json":
+ SavePanelJSON(w, r)
+ }
+}
+
+func ApiAuth(data string, w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/macro/play":
+ PlayMacro(data, w, r)
+ case "/device/link/remove":
+ RemoveLink(data, w, r)
+ case "/panel/list":
+ MCRMLog("Authenticated Panellist")
+ PanelList(w, r)
+ case "/panel/get":
+ GetPanel(data, w, r)
+ }
+}
diff --git a/app/device.go b/app/device.go
new file mode 100644
index 0000000..6352e5f
--- /dev/null
+++ b/app/device.go
@@ -0,0 +1,329 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package app
+
+import (
+ "encoding/json"
+ "fmt"
+ "macrame/app/helper"
+ "macrame/app/structs"
+ "math/rand"
+ "net"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func ListServerIP(w http.ResponseWriter) {
+ ip, err := GetServerIp()
+
+ if err != nil {
+ MCRMLog("GetServerIP err: ", err)
+ return
+ }
+
+ json.NewEncoder(w).Encode(ip)
+}
+
+func GetServerIp() (string, error) {
+ ifs, err := net.Interfaces()
+ if err != nil {
+ MCRMLog(err)
+ return "", err
+ }
+
+ for _, ifi := range ifs {
+ // Skip interfaces that are down
+ if ifi.Flags&net.FlagUp == 0 {
+ continue
+ }
+ // Skip loopback interfaces
+ if ifi.Flags&net.FlagLoopback != 0 {
+ continue
+ }
+
+ addrs, err := ifi.Addrs()
+ if err != nil {
+ MCRMLog(err)
+ continue
+ }
+
+ for _, addr := range addrs {
+ var ip net.IP
+
+ switch v := addr.(type) {
+ case *net.IPNet:
+ ip = v.IP
+ case *net.IPAddr:
+ ip = v.IP
+ }
+
+ if ip == nil || ip.To4() == nil {
+ continue
+ }
+
+ // Skip APIPA (169.254.x.x) addresses
+ if ip.IsLinkLocalUnicast() {
+ continue
+ }
+
+ // Found a good IP, return it
+ return ip.String(), nil
+ }
+ }
+
+ return "", fmt.Errorf("No IP found")
+}
+
+func DeviceList(w http.ResponseWriter, r *http.Request) {
+ dir := "devices"
+
+ files, err := os.ReadDir(dir)
+ if err != nil {
+ os.MkdirAll(dir, 0600)
+ files = nil
+ MCRMLog("DeviceList Error: ", err)
+ }
+
+ devices := make(map[string]map[string]interface{})
+
+ for _, file := range files {
+ filePath := dir + "/" + file.Name()
+ ext := filepath.Ext(filePath)
+ device := strings.TrimSuffix(file.Name(), ext)
+
+ if _, ok := devices[device]; !ok {
+ devices[device] = make(map[string]interface{})
+ }
+
+ if ext == ".json" {
+ devices[device]["settings"] = readDeviceSettings(filePath)
+ }
+ if ext == ".key" {
+ devices[device]["key"] = true
+ }
+ }
+
+ result := map[string]interface{}{
+ "devices": devices,
+ }
+
+ json.NewEncoder(w).Encode(result)
+}
+
+func readDeviceSettings(filepath string) (settings structs.Settings) {
+ data, err := os.ReadFile(filepath)
+ if err != nil {
+ MCRMLog("readDeviceSettings Error: ", err)
+ }
+
+ err = json.Unmarshal(data, &settings)
+ if err != nil {
+ MCRMLog("readDeviceSettings JSON Error: ", err)
+ }
+
+ return settings
+}
+
+func DeviceAccessCheck(w http.ResponseWriter, r *http.Request) {
+ var req structs.Check
+
+ err := json.NewDecoder(r.Body).Decode(&req)
+
+ if err != nil {
+ MCRMLog("DeviceAccessCheck Error: ", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ _, errSett := os.Stat("devices/" + req.Uuid + ".json")
+ _, errKey := os.Stat("devices/" + req.Uuid + ".key")
+
+ if (errSett == nil) && (errKey == nil) {
+ json.NewEncoder(w).Encode("authorized")
+ } else if (errSett == nil) && (errKey != nil) {
+ MCRMLog("DeviceAccessCheck: UUID: ", req.Uuid, "; Access: Unauthorized")
+ json.NewEncoder(w).Encode("unauthorized")
+ } else {
+ MCRMLog("DeviceAccessCheck: UUID: ", req.Uuid, "; Access: Unlinked")
+ json.NewEncoder(w).Encode("unlinked")
+ }
+}
+
+func DeviceAccessRequest(w http.ResponseWriter, r *http.Request) {
+ var req structs.Request
+
+ err := json.NewDecoder(r.Body).Decode(&req)
+
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ deviceSettings := structs.Settings{Name: req.Name, Type: req.Type}
+
+ settingsJSON, err := json.Marshal(deviceSettings)
+ if err != nil {
+ MCRMLog("DeviceAccessRequest JSON Error: ", err)
+ return
+ }
+
+ err = os.WriteFile("devices/"+req.Uuid+".json", settingsJSON, 0644)
+
+ if err != nil {
+ MCRMLog("DeviceAccessRequest Error: ", err)
+ return
+ }
+
+ json.NewEncoder(w).Encode("unauthorized")
+}
+
+func PingLink(w http.ResponseWriter, r *http.Request) {
+ var req structs.Check
+ err := json.NewDecoder(r.Body).Decode(&req)
+
+ if err != nil {
+ MCRMLog("PingLink Error: ", err)
+ json.NewEncoder(w).Encode(false)
+ return
+ }
+
+ key, keyErr := os.ReadFile("devices/" + req.Uuid + ".key")
+ pin, pinErr := os.ReadFile("devices/" + req.Uuid + ".tmp")
+
+ encryptedKey, encErr := helper.EncryptAES(string(pin), string(key))
+
+ if keyErr == nil && pinErr == nil && encErr == nil {
+ MCRMLog("PINGLINK FIXED")
+ w.Header().Set("Content-Type", "application/json")
+ w.Write([]byte(encryptedKey))
+ return
+ } else {
+ MCRMLog("PingLink Error: keyErr:", keyErr, "; pinErr:", pinErr, "; encErr:", encErr)
+ }
+
+ json.NewEncoder(w).Encode(false)
+}
+
+func StartLink(w http.ResponseWriter, r *http.Request) {
+ var req structs.Check
+
+ err := json.NewDecoder(r.Body).Decode(&req)
+
+ if err != nil {
+ MCRMLog("StartLink Error: ", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ pin := fmt.Sprintf("%04d", rand.Intn(10000))
+
+ deviceKey := helper.GenerateKey()
+
+ errKey := helper.SaveDeviceKey(req.Uuid, deviceKey)
+ savedPin, errPin := helper.TempPinFile(req.Uuid, pin)
+
+ if errKey == nil && errPin == nil && savedPin {
+ json.NewEncoder(w).Encode(pin)
+ } else {
+ MCRMLog("StartLink Error: errKey:", errKey, "; errPin:", errPin)
+ }
+}
+
+func PollLink(w http.ResponseWriter, r *http.Request) {
+ var req structs.Check
+
+ err := json.NewDecoder(r.Body).Decode(&req)
+
+ if err != nil {
+ json.NewEncoder(w).Encode(false)
+ return
+ }
+
+ if helper.CheckPinFile(req.Uuid) {
+ json.NewEncoder(w).Encode(true)
+ return
+ }
+
+ json.NewEncoder(w).Encode(false)
+}
+
+func RemoveLink(data string, w http.ResponseWriter, r *http.Request) {
+ req := &structs.Check{}
+ _, err := helper.ParseRequest(req, data, r)
+
+ if err != nil {
+ MCRMLog("RemoveLink ParseRequest Error: ", err)
+ json.NewEncoder(w).Encode(false)
+ return
+ }
+
+ err = os.Remove("devices/" + req.Uuid + ".key")
+
+ if err != nil {
+ MCRMLog("RemoveLink Remove Error: ", err)
+ json.NewEncoder(w).Encode(false)
+ return
+ }
+
+ json.NewEncoder(w).Encode(true)
+}
+
+func Handshake(w http.ResponseWriter, r *http.Request) {
+ var req structs.Handshake
+
+ err := json.NewDecoder(r.Body).Decode(&req)
+
+ if err != nil {
+ return
+ }
+
+ deviceKey, err := helper.GetKeyByUuid(req.Uuid)
+
+ if err != nil {
+ MCRMLog("Handshake GetKeyByUuid Error: ", err)
+ json.NewEncoder(w).Encode(false)
+ return
+ }
+
+ decryptShake, _ := helper.DecryptAES(deviceKey, req.Shake)
+
+ helper.RemovePinFile(req.Uuid)
+
+ if decryptShake == getDateStr() {
+ json.NewEncoder(w).Encode(true)
+ return
+ } else {
+ os.Remove("devices/" + req.Uuid + ".key")
+ }
+
+ json.NewEncoder(w).Encode(false)
+}
+
+func getDateStr() string {
+ date := time.Now()
+ year, month, day := date.Date()
+ formattedDate := fmt.Sprintf("%04d%02d%02d", year, month, day)
+ return formattedDate
+}
diff --git a/app/helper/api-helper.go b/app/helper/api-helper.go
new file mode 100644
index 0000000..f88106b
--- /dev/null
+++ b/app/helper/api-helper.go
@@ -0,0 +1,123 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package helper
+
+import (
+ "encoding/json"
+ "errors"
+ "net"
+ "net/http"
+ "strings"
+
+ "macrame/app/structs"
+ . "macrame/app/structs"
+)
+
+func EndpointAccess(w http.ResponseWriter, r *http.Request) (bool, string, error) {
+ ip, _, err := net.SplitHostPort(r.RemoteAddr)
+
+ if err != nil {
+ return false, "", errors.New(r.URL.Path + ": SplitHostPort error: " + err.Error())
+ }
+
+ if (isLocal(ip) && isEndpointAllowed("Local", r.URL.Path)) ||
+ (isLanRemote(ip) && isEndpointAllowed("Remote", r.URL.Path)) {
+ return true, "", nil
+ } else if isLanRemote(ip) && isEndpointAllowed("Auth", r.URL.Path) {
+ data, err := decryptAuth(r)
+
+ if err != nil {
+ return false, "", err
+ }
+
+ return data != "", data, nil
+ }
+
+ return false, "", errors.New(r.URL.Path + ": not authorized or accessible")
+}
+
+func isLocal(ip string) bool {
+ return ip == "127.0.0.1" || ip == "::1"
+}
+
+func isLanRemote(ip string) bool {
+ return strings.HasPrefix(ip, "192.168.")
+}
+
+func isEndpointAllowed(source string, endpoint string) bool {
+ var endpoints, err = getAllowedEndpoints(source)
+ if err != "" {
+ return false
+ }
+
+ if (endpoints != nil) && (len(endpoints) > 0) {
+ for _, e := range endpoints {
+ if e == endpoint {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func getAllowedEndpoints(source string) (endpoints []string, err string) {
+ if source == "Local" {
+ return Endpoints.Local, ""
+ }
+ if source == "Remote" {
+ return Endpoints.Remote, ""
+ }
+ if source == "Auth" {
+ return Endpoints.Auth, ""
+ }
+
+ return []string{}, "No allowed endpoints"
+}
+
+func decryptAuth(r *http.Request) (string, error) {
+ var req structs.Authcall
+
+ err := json.NewDecoder(r.Body).Decode(&req)
+
+ if err != nil || req.Uuid == "" || req.Data == "" {
+ return "", errors.New("DecryptAuth Error: " + err.Error() + "; UUID: " + req.Uuid + "; Data: " + req.Data)
+ }
+
+ deviceKey, errKey := GetKeyByUuid(req.Uuid)
+ decryptData, errDec := DecryptAES(deviceKey, req.Data)
+
+ if errKey != nil && errDec != nil || decryptData == "" {
+ return "", errors.New("DecryptAuth Error: " + errKey.Error() + "; " + errDec.Error() + "; UUID: " + req.Uuid + "; Data: " + req.Data)
+ }
+
+ return decryptData, nil
+}
+
+func ParseRequest(req interface{}, data string, r *http.Request) (d interface{}, err error) {
+ if data != "" {
+ dataBytes := []byte(data)
+ return req, json.Unmarshal(dataBytes, &req)
+ } else {
+ return req, json.NewDecoder(r.Body).Decode(&req)
+ }
+}
diff --git a/app/helper/browser-helper.go b/app/helper/browser-helper.go
new file mode 100644
index 0000000..5b2e3d0
--- /dev/null
+++ b/app/helper/browser-helper.go
@@ -0,0 +1,44 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package helper
+
+import (
+ "os/exec"
+ "runtime"
+)
+
+func OpenBrowser(url string) bool {
+ var args []string
+
+ switch runtime.GOOS {
+ case "darwin":
+ args = []string{"open"}
+ case "windows":
+ args = []string{"cmd", "/c", "start"}
+ default:
+ args = []string{"xdg-open"}
+ }
+
+ cmd := exec.Command(args[0], append(args[1:], url)...)
+
+ return cmd.Start() == nil
+}
diff --git a/app/helper/device-helper.go b/app/helper/device-helper.go
new file mode 100644
index 0000000..30d7c56
--- /dev/null
+++ b/app/helper/device-helper.go
@@ -0,0 +1,66 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package helper
+
+import (
+ "errors"
+ "os"
+ "time"
+)
+
+func TempPinFile(Uuid string, pin string) (bool, error) {
+ err := os.WriteFile("devices/"+Uuid+".tmp", []byte(pin), 0644)
+ if err != nil {
+ return false, errors.New("TempPinFile Error: " + err.Error())
+ }
+
+ time.AfterFunc(1*time.Minute, func() {
+ os.Remove("devices/" + Uuid + ".tmp")
+ })
+
+ return true, nil
+}
+
+func RemovePinFile(Uuid string) error { return os.Remove("devices/" + Uuid + ".tmp") }
+
+func CheckPinFile(Uuid string) bool {
+ _, err := os.Stat("devices/" + Uuid + ".tmp")
+ return err == nil
+}
+
+func SaveDeviceKey(Uuid string, key string) error {
+ err := os.WriteFile("devices/"+Uuid+".key", []byte(key), 0644)
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func GetKeyByUuid(Uuid string) (string, error) {
+ data, err := os.ReadFile("devices/" + Uuid + ".key")
+ if err != nil {
+ return "", err
+ }
+ return string(data), nil
+}
diff --git a/app/helper/encrypt-helper.go b/app/helper/encrypt-helper.go
new file mode 100644
index 0000000..ad883c5
--- /dev/null
+++ b/app/helper/encrypt-helper.go
@@ -0,0 +1,135 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package helper
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ mathRand "math/rand"
+ "strings"
+)
+
+func EncryptAES(key string, plaintext string) (string, error) {
+ origData := []byte(plaintext)
+
+ // Create AES cipher
+ block, err := aes.NewCipher(keyToBytes(key))
+ if err != nil {
+ return "", err
+ }
+ blockSize := block.BlockSize()
+
+ origData = PKCS5Padding(origData, blockSize)
+
+ iv := []byte(EnvGet("MCRM__IV"))
+ blockMode := cipher.NewCBCEncrypter(block, iv)
+
+ crypted := make([]byte, len(origData))
+ blockMode.CryptBlocks(crypted, origData)
+
+ cryptedString := base64.StdEncoding.EncodeToString(crypted)
+
+ return cryptedString, nil
+}
+
+func DecryptAES(key string, cryptedText string) (string, error) {
+ crypted, err := base64.StdEncoding.DecodeString(cryptedText)
+
+ if err != nil {
+ return "", err
+ }
+
+ block, err := aes.NewCipher(keyToBytes(key))
+ if err != nil {
+ return "", err
+ }
+
+ iv := []byte(EnvGet("MCRM__IV"))
+ blockMode := cipher.NewCBCDecrypter(block, iv)
+
+ origData := make([]byte, len(crypted))
+
+ blockMode.CryptBlocks(origData, crypted)
+ origData, err = PKCS5UnPadding(origData)
+
+ if err != nil || len(origData) <= 3 {
+ return "", errors.New("invalid key")
+ }
+
+ origDataString := string(origData)
+
+ return origDataString, nil
+}
+
+func GenerateRandomString(length int) string {
+ b := make([]byte, length)
+ _, err := rand.Read(b)
+ if err != nil {
+ panic(err)
+ }
+ return base64.StdEncoding.EncodeToString(b)
+}
+
+func GenerateRandomIntegerString(length int) string {
+ var sb strings.Builder
+ for i := 0; i < length; i++ {
+ sb.WriteByte('0' + byte(mathRand.Intn(10)))
+ }
+ return sb.String()
+}
+
+func GenerateKey() string {
+ return strings.Replace(GenerateRandomString(24), "=", "", -1)
+}
+
+func keyToBytes(key string) []byte {
+ // Convert key to bytes
+ keyBytes := []byte(key)
+
+ // If key is 4 characters, append salt
+ if len(key) == 4 {
+ keyBytes = []byte(key + EnvGet("MCRM__SALT"))
+ }
+
+ return keyBytes
+}
+
+func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
+ padding := blockSize - len(ciphertext)%blockSize
+ padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+ return append(ciphertext, padtext...)
+}
+
+func PKCS5UnPadding(origData []byte) ([]byte, error) {
+ length := len(origData)
+ unpadding := int(origData[length-1])
+
+ if (unpadding >= length) || (unpadding == 0) {
+ return nil, errors.New("unpadding error")
+ }
+
+ return origData[:(length - unpadding)], nil
+}
diff --git a/app/helper/env-helper.go b/app/helper/env-helper.go
new file mode 100644
index 0000000..28af4d4
--- /dev/null
+++ b/app/helper/env-helper.go
@@ -0,0 +1,128 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package helper
+
+import (
+ "encoding/json"
+ "log"
+ "net"
+ "os"
+ "strconv"
+ "strings"
+)
+
+var configPath = "public/config.js"
+
+func EnvGet(key string) string {
+
+ if !configFileExists() {
+ CreateConfigFile(configPath)
+ CheckFeDevDir()
+ }
+
+ data, err := os.ReadFile(configPath)
+ if err != nil {
+ log.Println("Error reading config.js:", err)
+ return ""
+ }
+
+ raw := strings.TrimSpace(string(data))
+ raw = strings.TrimPrefix(raw, "window.__CONFIG__ = ")
+ raw = strings.TrimSuffix(raw, ";")
+
+ var config map[string]string
+ if err := json.Unmarshal([]byte(raw), &config); err != nil {
+ log.Println("Error parsing config.js:", err)
+ return ""
+ }
+
+ return config[key]
+}
+
+func configFileExists() bool {
+ _, err := os.Stat(configPath)
+ return err == nil
+}
+
+func CheckFeDevDir() {
+ log.Println("Checking FE dev directory...")
+ _, err := os.Stat("fe")
+
+ if err != nil {
+ log.Println("Error checking FE dev directory:", err)
+ return
+ }
+
+ copyConfigToFe()
+}
+
+func copyConfigToFe() {
+ data, err := os.ReadFile(configPath)
+
+ if err != nil {
+ log.Println("Error reading config.js:", err)
+ return
+ }
+
+ if err := os.WriteFile("fe/config.js", data, 0644); err != nil {
+ log.Println("Error writing config.js:", err)
+ }
+}
+
+func CreateConfigFile(filename string) {
+ port, _ := findOpenPort()
+ saltKey := GenerateKey()
+ salt := saltKey[:28]
+ iv := GenerateRandomIntegerString(16)
+
+ config := map[string]string{
+ "MCRM__PORT": port,
+ "MCRM__SALT": salt,
+ "MCRM__IV": iv,
+ }
+
+ jsonData, err := json.MarshalIndent(config, "", " ")
+ if err != nil {
+ log.Println("Error creating config JSON:", err)
+ return
+ }
+ jsData := "window.__CONFIG__ = " + string(jsonData) + ";"
+
+ log.Println("Created JS config:", jsData)
+
+ if err := os.WriteFile(filename, []byte(jsData), 0644); err != nil {
+ log.Println("Error writing config.js:", err)
+ }
+}
+
+func findOpenPort() (string, error) {
+ addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
+ if err != nil {
+ return "", err
+ }
+ l, err := net.ListenTCP("tcp", addr)
+ if err != nil {
+ return "", err
+ }
+ defer l.Close()
+ return strconv.Itoa(l.Addr().(*net.TCPAddr).Port), nil
+}
diff --git a/app/helper/macro-helper.go b/app/helper/macro-helper.go
new file mode 100644
index 0000000..a49dc57
--- /dev/null
+++ b/app/helper/macro-helper.go
@@ -0,0 +1,114 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package helper
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/go-vgo/robotgo"
+)
+
+func FormatMacroFileName(s string) string {
+ // Remove invalid characters
+ re := regexp.MustCompile(`[\/\?\*\>\<\:\"\|
+]`)
+ s = re.ReplaceAllString(s, "")
+
+ // Replace spaces with underscores
+ s = strings.ReplaceAll(s, " ", "_")
+
+ // Remove special characters
+ re = regexp.MustCompile(`[!@#$%^&\(\)\[\]\{\}\~]`)
+ s = re.ReplaceAllString(s, "")
+
+ // Truncate the string
+ if len(s) > 255 {
+ s = s[:255]
+ }
+
+ return s
+}
+
+func ReadMacroFile(filename string) (steps []map[string]interface{}, err error) {
+ content, err := os.ReadFile(filename)
+
+ if err != nil {
+ return nil, err
+ }
+
+ err = json.Unmarshal(content, &steps)
+
+ return steps, err
+}
+
+func RunMacroSteps(steps []map[string]interface{}) error {
+ for _, step := range steps {
+ switch step["type"] {
+ case "key":
+ keyCode := step["code"].(string)
+
+ if strings.Contains(keyCode, "|") {
+ keyCode = handleToggleCode(keyCode, step["direction"].(string))
+ }
+
+ robotgo.KeyToggle(keyCode, step["direction"].(string))
+ case "delay":
+
+ time.Sleep(time.Duration(step["value"].(float64)) * time.Millisecond)
+
+ default:
+ return errors.New("RunMacroSteps Unknown step type: %v" + fmt.Sprint(step["type"]))
+ }
+ }
+ return nil
+}
+
+var toggleCodes = map[string]bool{}
+
+func handleToggleCode(keyCode string, direction string) string {
+ splitCodes := strings.Split(keyCode, "|")
+
+ if direction == "down" {
+ if _, ok := toggleCodes[splitCodes[0]]; !ok {
+ toggleCodes[splitCodes[0]] = true
+ return splitCodes[0]
+ }
+ return splitCodes[1]
+ }
+
+ if direction == "up" {
+ if toggleCodes[splitCodes[0]] {
+ toggleCodes[splitCodes[0]] = false
+ return splitCodes[0]
+ }
+ delete(toggleCodes, splitCodes[0])
+ return splitCodes[1]
+ }
+
+ return ""
+}
diff --git a/app/helper/translation-helper.go b/app/helper/translation-helper.go
new file mode 100644
index 0000000..fdcffff
--- /dev/null
+++ b/app/helper/translation-helper.go
@@ -0,0 +1,101 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package helper
+
+import (
+ "strings"
+)
+
+var translations = map[string]string{
+ "ArrowUp": "up",
+ "ArrowDown": "down",
+ "ArrowRight": "right",
+ "ArrowLeft": "left",
+ "Meta": "cmd",
+ "MetaLeft": "lcmd",
+ "MetaRight": "rcmd",
+ "Alt": "alt",
+ "AltLeft": "lalt",
+ "AltRight": "ralt",
+ "Control": "ctrl",
+ "ControlLeft": "lctrl",
+ "ControlRight": "rctrl",
+ "Shift": "shift",
+ "ShiftLeft": "lshift",
+ "ShiftRight": "rshift",
+ "AudioVolumeMute": "audio_mute",
+ "AudioVolumeDown": "audio_vol_down",
+ "AudioVolumeUp": "audio_vol_up",
+ "MediaTrackPrevious": "audio_prev",
+ "MediaTrackNext": "audio_next",
+ "MediaPlayPause": "audio_play|audio_pause",
+ "Numpad0": "num0",
+ "Numpad1": "num1",
+ "Numpad2": "num2",
+ "Numpad3": "num3",
+ "Numpad4": "num4",
+ "Numpad5": "num5",
+ "Numpad6": "num6",
+ "Numpad7": "num7",
+ "Numpad8": "num8",
+ "Numpad9": "num9",
+ "NumLock": "num_lock",
+ "NumpadDecimal": "num.",
+ "NumpadAdd": "num+",
+ "NumpadSubtract": "num-",
+ "NumpadMultiply": "num*",
+ "NumpadDivide": "num/",
+ "NumpadEnter": "num_enter",
+ "Clear": "num_clear",
+ "BracketLeft": "[",
+ "BracketRight": "]",
+ "Quote": "'",
+ "Semicolon": ";",
+ "Backquote": "`",
+ "Backslash": "\\",
+ "IntlBackslash": "\\",
+ "Slash": "/",
+ "Comma": ",",
+ "Period": ".",
+ "Equal": "=",
+ "Minus": "-",
+}
+
+func Translate(code string) string {
+ if val, ok := translations[code]; ok {
+ return val
+ }
+ return strings.ToLower(code)
+}
+
+func ReverseTranslate(name string) string {
+ if name == "\\" {
+ return "Backslash"
+ }
+
+ for key, value := range translations {
+ if value == name {
+ return key
+ }
+ }
+ return name
+}
diff --git a/app/log.go b/app/log.go
new file mode 100644
index 0000000..db662ed
--- /dev/null
+++ b/app/log.go
@@ -0,0 +1,45 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package app
+
+import (
+ "log"
+ "os"
+)
+
+var logFile *os.File
+
+func MCRMLogInit() {
+ var err error
+ // Open or create the log file with append permissions
+ logFile, err = os.OpenFile("log.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
+ if err != nil {
+ log.Fatal(err) // Exit if we can't open the log file
+ }
+
+ // Optionally set log to write to file in addition to standard log output
+ log.SetOutput(logFile)
+}
+
+func MCRMLog(v ...interface{}) {
+ log.Println(v...) // Logs to terminal as well
+}
diff --git a/app/macro.go b/app/macro.go
new file mode 100644
index 0000000..5dff9e0
--- /dev/null
+++ b/app/macro.go
@@ -0,0 +1,229 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package app
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "macrame/app/helper"
+ "macrame/app/structs"
+)
+
+func CheckMacro(w http.ResponseWriter, r *http.Request) {
+ var req structs.MacroRequest
+ err := json.NewDecoder(r.Body).Decode(&req)
+
+ if err != nil {
+ MCRMLog("OpenMacro Decode Error: ", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ var filename = helper.FormatMacroFileName(req.Macro)
+
+ macroFile, err := helper.ReadMacroFile(fmt.Sprintf("macros/%s.json", filename))
+
+ if macroFile != nil && err == nil {
+ json.NewEncoder(w).Encode(true)
+ return
+ } else {
+ MCRMLog("OpenMacro ReadMacroFile Error: ", err)
+ json.NewEncoder(w).Encode(false)
+ return
+ }
+}
+
+func SaveMacro(w http.ResponseWriter, r *http.Request) {
+ var newMacro structs.NewMacro
+
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ MCRMLog("SaveMacro ReadAll Error: ", err)
+ return
+ }
+
+ err = json.Unmarshal(body, &newMacro)
+ if err != nil {
+ MCRMLog("SaveMacro Unmarshal Error: ", err)
+ return
+ }
+
+ simplifiedSteps := make([]map[string]interface{}, 0)
+ for _, step := range newMacro.Steps {
+ simplifiedSteps = append(simplifiedSteps, simplifyMacro(step))
+ }
+
+ stepsJSON, err := json.Marshal(simplifiedSteps)
+ if err != nil {
+ MCRMLog("SaveMacro Marshal Error: ", err)
+ return
+ }
+
+ err = os.WriteFile("macros/"+helper.FormatMacroFileName(newMacro.Name)+".json", stepsJSON, 0644)
+ if err != nil {
+ MCRMLog("SaveMacro WriteFile Error: ", err)
+ return
+ }
+}
+
+func simplifyMacro(step structs.Step) map[string]interface{} {
+ simplified := make(map[string]interface{})
+
+ simplified["type"] = step.Type
+
+ switch step.Type {
+ case "delay":
+ simplified["value"] = step.Value
+ case "key":
+ keyCode := step.Code
+
+ if keyCode == "" || (strings.Contains(keyCode, "Digit")) {
+ keyCode = step.Key
+ } else if strings.Contains(keyCode, "Key") {
+ keyCode = strings.Replace(keyCode, "Key", "", 1)
+ }
+
+ simplified["code"] = helper.Translate(keyCode)
+ simplified["direction"] = step.Direction
+ }
+
+ return simplified
+}
+
+func ListMacros(w http.ResponseWriter, r *http.Request) {
+ dir := "macros"
+ files, err := os.ReadDir(dir)
+ if err != nil {
+ MCRMLog("ListMacros ReadDir Error: ", err)
+ json.NewEncoder(w).Encode(false)
+ return
+ }
+
+ var macroList []structs.MacroInfo
+
+ for _, file := range files {
+ filename := filepath.Base(file.Name())
+ macroname := strings.TrimSuffix(filename, filepath.Ext(filename))
+ nicename := strings.Replace(macroname, "_", " ", -1)
+
+ macroList = append(macroList, structs.MacroInfo{
+ Name: nicename,
+ Macroname: macroname,
+ })
+ }
+
+ json.NewEncoder(w).Encode(macroList)
+}
+
+func DeleteMacro(w http.ResponseWriter, r *http.Request) {
+ var req structs.MacroRequest
+
+ err := json.NewDecoder(r.Body).Decode(&req)
+
+ if err != nil {
+ MCRMLog("DeleteMacro Decode Error: ", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ var filename = helper.FormatMacroFileName(req.Macro)
+
+ err = os.Remove("macros/" + filename + ".json")
+
+ if err != nil {
+ MCRMLog("DeleteMacro Remove Error: ", err)
+ json.NewEncoder(w).Encode(false)
+ return
+ }
+ log.Println("Deleted Macro:", req.Macro)
+ json.NewEncoder(w).Encode(true)
+}
+
+func PlayMacro(data string, w http.ResponseWriter, r *http.Request) {
+ req := &structs.MacroRequest{}
+ _, err := helper.ParseRequest(req, data, r)
+
+ if err != nil {
+ MCRMLog("PlayMacro ParseRequest Error: ", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ macro := req.Macro
+
+ MCRMLog("Playing Macro: ", macro)
+
+ var filename = helper.FormatMacroFileName(macro)
+ var filepath = fmt.Sprintf("macros/%s.json", filename)
+
+ macroFile, err := helper.ReadMacroFile(filepath)
+ if err != nil {
+ MCRMLog("PlayMacro ReadMacroFile Error: ", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ err = helper.RunMacroSteps(macroFile)
+
+ if err != nil {
+ MCRMLog("PlayMacro RunMacroSteps Error: ", err)
+ return
+ }
+}
+
+func OpenMacro(w http.ResponseWriter, r *http.Request) {
+ var req structs.MacroRequest
+
+ err := json.NewDecoder(r.Body).Decode(&req)
+
+ if err != nil {
+ MCRMLog("OpenMacro Decode Error: ", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ macroFile, err := helper.ReadMacroFile(fmt.Sprintf("macros/%s.json", req.Macro))
+
+ if err != nil {
+ MCRMLog("OpenMacro ReadMacroFile Error: ", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ // Walk through the macro file and reverse translate codes
+ for i, action := range macroFile {
+ if actionType, ok := action["type"].(string); ok && actionType == "key" {
+ if code, ok := action["code"].(string); ok {
+ macroFile[i]["code"] = helper.ReverseTranslate(code)
+ }
+ }
+ }
+
+ json.NewEncoder(w).Encode(macroFile)
+}
diff --git a/app/panel.go b/app/panel.go
new file mode 100644
index 0000000..9c4a15f
--- /dev/null
+++ b/app/panel.go
@@ -0,0 +1,176 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package app
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "macrame/app/helper"
+ "macrame/app/structs"
+ "net/http"
+ "os"
+ "strings"
+)
+
+func PanelList(w http.ResponseWriter, r *http.Request) {
+ panelDirs, err := os.ReadDir("panels")
+ if err != nil {
+ MCRMLog("PanelList ReadDir Error: ", err)
+ json.NewEncoder(w).Encode(false)
+ return
+ }
+
+ var panelList []interface{}
+
+ for _, panelDir := range panelDirs {
+ if panelDir.IsDir() {
+ panelList = append(panelList, getPanelInfo(panelDir.Name()))
+ }
+ }
+
+ if len(panelList) == 0 {
+ MCRMLog("PanelList: No panels found")
+ json.NewEncoder(w).Encode(false)
+ return
+ }
+
+ json.NewEncoder(w).Encode(panelList)
+}
+
+func getPanelInfo(dirname string) structs.PanelInfo {
+ var panelInfo structs.PanelInfo
+
+ jsonFile, err := os.ReadFile("panels/" + dirname + "/panel.json")
+
+ if err != nil {
+ panelInfo.Name = strings.Replace(dirname, "_", " ", -1)
+ panelInfo.Description = "null"
+ panelInfo.AspectRatio = "null"
+ panelInfo.Macros = make(map[string]string)
+ } else {
+ err = json.Unmarshal(jsonFile, &panelInfo)
+ if err != nil {
+ MCRMLog("getPanelInfo Unmarshal Error: ", err)
+ }
+ }
+
+ panelInfo.Dir = dirname
+
+ thumb := getPanelThumb(dirname)
+ panelInfo.Thumb = thumb
+
+ return panelInfo
+}
+
+func getPanelThumb(dirname string) string {
+ extensions := []string{".jpg", ".jpeg", ".png", ".webp"}
+
+ for _, ext := range extensions {
+ filename := "thumbnail" + ext
+ file, err := os.Open("panels/" + dirname + "/" + filename)
+ if err != nil {
+ MCRMLog("getPanelThumb Open Error: ", err)
+ continue
+ }
+ defer file.Close()
+
+ return encodeImg(file)
+ }
+
+ return ""
+}
+
+func getPanelCode(dirname string) (html string, css string) {
+ htmlBytes, _ := os.ReadFile("panels/" + dirname + "/index.html")
+ cssBytes, _ := os.ReadFile("panels/" + dirname + "/output.css")
+
+ return string(htmlBytes), string(cssBytes)
+}
+
+func encodeImg(file *os.File) string {
+ contents, err := os.ReadFile(file.Name())
+ if err != nil {
+ MCRMLog("encodeImg ReadFile Error: ", err)
+ return ""
+ }
+
+ encoded := base64.StdEncoding.EncodeToString(contents)
+ return encoded
+}
+
+func GetPanel(data string, w http.ResponseWriter, r *http.Request) {
+ req := &structs.PanelRequest{}
+
+ _, err := helper.ParseRequest(req, data, r)
+ if err != nil {
+ MCRMLog("GetPanel ParseRequest Error: ", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ var response = structs.PanelResponse{}
+
+ panelInfo := getPanelInfo(req.Dir)
+ panelHtml, panelCss := getPanelCode(req.Dir)
+
+ response.Dir = panelInfo.Dir
+ response.Name = panelInfo.Name
+ response.Description = panelInfo.Description
+ response.AspectRatio = panelInfo.AspectRatio
+ response.Macros = panelInfo.Macros
+ response.Thumb = panelInfo.Thumb
+ response.HTML = panelHtml
+ response.Style = panelCss
+
+ json.NewEncoder(w).Encode(response)
+}
+
+func SavePanelJSON(w http.ResponseWriter, r *http.Request) {
+ var req structs.PanelSaveJSON
+ err := json.NewDecoder(r.Body).Decode(&req)
+ if err != nil {
+ MCRMLog("SavePanelJSON Decode Error: ", err)
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ filePath := "panels/" + req.Dir + "/panel.json"
+
+ req.Dir = ""
+
+ // Marshal the data to JSON without the dir field
+ jsonData, err := json.Marshal(req)
+ if err != nil {
+ MCRMLog("SavePanelJSON Marshal Error: ", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ err = os.WriteFile(filePath, jsonData, 0644)
+ if err != nil {
+ MCRMLog("SavePanelJSON WriteFile Error: ", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/app/structs/api-struct.go b/app/structs/api-struct.go
new file mode 100644
index 0000000..7fc7889
--- /dev/null
+++ b/app/structs/api-struct.go
@@ -0,0 +1,66 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package structs
+
+type Allowed struct {
+ Local []string
+ Remote []string
+ Auth []string
+}
+
+var Endpoints = Allowed{
+ Local: []string{
+ "/macro/check",
+ "/macro/record",
+ "/macro/list",
+ "/macro/open",
+ "/macro/delete",
+ "/macro/play",
+ "/device/server/ip",
+ "/device/list",
+ "/device/access/check",
+ "/device/access/request",
+ "/device/link/ping",
+ "/device/link/start",
+ "/device/link/poll",
+ "/device/link/remove",
+ "/device/handshake",
+ "/panel/get",
+ "/panel/list",
+ "/panel/save/json",
+ },
+ Remote: []string{
+ "/device/access/check",
+ "/device/access/request",
+ "/device/server/ip",
+ "/device/link/ping",
+ "/device/link/end",
+ "/device/handshake",
+ "/device/auth",
+ },
+ Auth: []string{
+ "/macro/play",
+ "/device/link/remove",
+ "/panel/get",
+ "/panel/list",
+ },
+}
diff --git a/app/structs/device-struct.go b/app/structs/device-struct.go
new file mode 100644
index 0000000..dcad84b
--- /dev/null
+++ b/app/structs/device-struct.go
@@ -0,0 +1,52 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package structs
+
+type Settings struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+}
+
+type RemoteWebhook struct {
+ Event string `json:"event"`
+ Data string `json:"data"`
+}
+
+type Check struct {
+ Uuid string `json:"uuid"`
+}
+
+type Request struct {
+ Uuid string `json:"uuid"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+}
+
+type Handshake struct {
+ Uuid string `json:"uuid"`
+ Shake string `json:"shake"`
+}
+
+type Authcall struct {
+ Uuid string `json:"uuid"`
+ Data string `json:"d"`
+}
diff --git a/app/structs/macro-struct.go b/app/structs/macro-struct.go
new file mode 100644
index 0000000..94e1fef
--- /dev/null
+++ b/app/structs/macro-struct.go
@@ -0,0 +1,58 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package structs
+
+type MacroRequest struct {
+ Macro string `json:"macro"`
+}
+
+type Step struct {
+ Type string `json:"type"`
+ Key string `json:"key"`
+ Code string `json:"code"`
+ Location int `json:"location"`
+ Direction string `json:"direction"`
+ Value int `json:"value"`
+}
+
+type NewMacro struct {
+ Name string `json:"name"`
+ Steps []Step `json:"steps"`
+}
+
+type MacroInfo struct {
+ Name string `json:"name"`
+ Macroname string `json:"macroname"`
+}
+
+type MacroKey struct {
+ Type string `json:"type"`
+ Key string `json:"key"`
+ Code string `json:"code"`
+ Location int `json:"location"`
+ Direction string `json:"direction"`
+}
+
+type MacroDelay struct {
+ Type string `json:"type"`
+ Value int `json:"value"`
+}
diff --git a/app/structs/panel-struct.go b/app/structs/panel-struct.go
new file mode 100644
index 0000000..7d1bb97
--- /dev/null
+++ b/app/structs/panel-struct.go
@@ -0,0 +1,65 @@
+/*
+Macrame is a program that enables the user to create keyboard macros and button panels.
+The macros are saved as simple JSON files and can be linked to the button panels. The panels can
+be created with HTML and CSS.
+
+Copyright (C) 2025 Jesse Malotaux
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+package structs
+
+type PanelJSON struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ AspectRatio string `json:"aspectRatio"`
+ Macros map[string]string `json:"macros"`
+}
+
+type PanelSaveJSON struct {
+ // Name string `json:"name"`
+ // Description string `json:"description"`
+ // AspectRatio string `json:"aspectRatio"`
+ // Macros map[string]string `json:"macros"`
+ Dir string `json:"dir"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ AspectRatio string `json:"aspectRatio"`
+ Macros interface{} `json:"macros"`
+}
+
+type PanelInfo struct {
+ Dir string `json:"dir"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ AspectRatio string `json:"aspectRatio"`
+ Macros map[string]string `json:"macros"`
+ Thumb string `json:"thumb"`
+}
+
+type PanelRequest struct {
+ Dir string `json:"dir"`
+}
+
+type PanelResponse struct {
+ Dir string `json:"dir"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ AspectRatio string `json:"aspectRatio"`
+ Macros map[string]string `json:"macros"`
+ Thumb string `json:"thumb"`
+ HTML string `json:"html"`
+ Style string `json:"style"`
+}
diff --git a/app/systray.go b/app/systray.go
new file mode 100644
index 0000000..2c4d819
--- /dev/null
+++ b/app/systray.go
@@ -0,0 +1,64 @@
+package app
+
+import (
+ "macrame/app/helper"
+ "os"
+
+ "github.com/getlantern/systray"
+)
+
+func InitSystray() {
+ go func() {
+ systray.Run(OnReady, OnExit)
+ }()
+}
+
+func OnReady() {
+ systray.SetIcon(getIcon("favicon.ico"))
+ systray.SetTitle("Macrame")
+ systray.SetTooltip("Macrame - Server")
+
+ ip, err := GetServerIp()
+
+ if err == nil {
+ systray.AddMenuItem("IP: "+ip+":"+helper.EnvGet("MCRM__PORT"), "Server IP")
+ }
+
+ systray.AddSeparator()
+
+ addMCRMItem("Dashboard", "/")
+ addMCRMItem("Panels", "/panels")
+ addMCRMItem("Macros", "/macros")
+ addMCRMItem("Devices", "/devices")
+
+ systray.AddSeparator()
+
+ mQuit := systray.AddMenuItem("Quit Macrame", "Quit Macrame")
+ go func() {
+ <-mQuit.ClickedCh
+ os.Exit(0)
+ }()
+}
+
+func addMCRMItem(name, urlPath string) {
+ m := systray.AddMenuItem(name, name)
+
+ go func() {
+ <-m.ClickedCh
+ helper.OpenBrowser("http://localhost:" + helper.EnvGet("MCRM__PORT") + urlPath)
+ }()
+}
+
+func OnExit() {
+ systray.Quit()
+}
+
+func getIcon(path string) []byte {
+ icon, err := os.ReadFile(path)
+
+ if err != nil {
+ MCRMLog("getIcon Error: ", err)
+ }
+
+ return icon
+}
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..65eb876
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+# Set the name of the build directory
+BUILD_DIR="Macrame_$(date +'%m%d%H%M%S')"
+
+# Build the Go application
+go build -ldflags "-H=windowsgui" -o Macrame.exe main.go
+
+# Build the frontend
+cd fe
+npm run build
+
+cd ../builds
+
+# Create the new build directory
+mkdir $BUILD_DIR
+mkdir $BUILD_DIR/macros
+mkdir $BUILD_DIR/panels
+mkdir $BUILD_DIR/panels/test_panel
+mkdir $BUILD_DIR/public
+mkdir $BUILD_DIR/public/assets
+
+# Move the generated files to the new build directory
+cp ../Macrame.exe $BUILD_DIR/Macrame.exe
+cp ../favicon.ico $BUILD_DIR/favicon.ico
+find ../macros -type f ! -name 'ED-*' -exec cp --parents {} "$BUILD_DIR/macros/" \;
+cp -r ../panels/test_panel/* $BUILD_DIR/panels/test_panel/
+mv ../public/* $BUILD_DIR/public/
+
+# cp ../install.bat $BUILD_DIR/install.bat
+
+powershell -Command "Compress-Archive -Path $BUILD_DIR/* -DestinationPath $BUILD_DIR.zip -Force"
+
+# Print the path to the new build directory
+echo "Build directory: ../$BUILD_DIR"
\ No newline at end of file
diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 0000000..a9a8c24
Binary files /dev/null and b/favicon.ico differ
diff --git a/fe/.editorconfig b/fe/.editorconfig
new file mode 100644
index 0000000..7f5b23f
--- /dev/null
+++ b/fe/.editorconfig
@@ -0,0 +1,9 @@
+[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
+charset = utf-8
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+end_of_line = lf
+max_line_length = 100
diff --git a/fe/.gitattributes b/fe/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/fe/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/fe/.gitignore b/fe/.gitignore
new file mode 100644
index 0000000..8ee54e8
--- /dev/null
+++ b/fe/.gitignore
@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo
diff --git a/fe/.prettierrc.json b/fe/.prettierrc.json
new file mode 100644
index 0000000..17a23d0
--- /dev/null
+++ b/fe/.prettierrc.json
@@ -0,0 +1,7 @@
+
+{
+ "$schema": "https://json.schemastore.org/prettierrc",
+ "semi": false,
+ "singleQuote": true,
+ "printWidth": 100
+}
diff --git a/fe/.vscode/extensions.json b/fe/.vscode/extensions.json
new file mode 100644
index 0000000..c92168f
--- /dev/null
+++ b/fe/.vscode/extensions.json
@@ -0,0 +1,8 @@
+{
+ "recommendations": [
+ "Vue.volar",
+ "dbaeumer.vscode-eslint",
+ "EditorConfig.EditorConfig",
+ "esbenp.prettier-vscode"
+ ]
+}
diff --git a/fe/index.html b/fe/index.html
index 70978ff..3dbdfe8 100644
--- a/fe/index.html
+++ b/fe/index.html
@@ -2,14 +2,16 @@