Refactor: Moved Go app to the root and fixed links.

This commit is contained in:
Jesse Malotaux 2025-05-03 20:56:15 +02:00
parent d1de67910d
commit 7157d43168
28 changed files with 100 additions and 164 deletions

137
app/api.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}

329
app/device.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

123
app/helper/api-helper.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

128
app/helper/env-helper.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

114
app/helper/macro-helper.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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 ""
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

45
app/log.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

229
app/macro.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

176
app/panel.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

66
app/structs/api-struct.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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",
},
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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"`
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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"`
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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"`
}

64
app/systray.go Normal file
View file

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