Automated release build for version: 1.0.0

This commit is contained in:
github-actions 2025-05-08 23:23:21 +00:00
parent 195f9dbafc
commit 6b14d2b9ce
127 changed files with 270 additions and 14040 deletions

View file

@ -1,17 +0,0 @@
# 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

20
.gitignore vendored
View file

@ -1,20 +0,0 @@
.env
config.js
devices/*
tmp
log.txt
Macrame.exe
public/*
macros/*
!macros/TEST-*
panels/*
!panels/test_panel
builds
node_modules
ToDo.md

BIN
Macrame.exe Normal file

Binary file not shown.

View file

@ -1,47 +0,0 @@
#!/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 <https://www.gnu.org/licenses/>."
# 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$GPL_HEADER\n-->\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!"

View file

@ -1,137 +0,0 @@
/*
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)
}
}

View file

@ -1,329 +0,0 @@
/*
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
}

View file

@ -1,123 +0,0 @@
/*
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

@ -1,44 +0,0 @@
/*
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

@ -1,66 +0,0 @@
/*
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

@ -1,135 +0,0 @@
/*
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
}

View file

@ -1,128 +0,0 @@
/*
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)
CheckUIDevDir()
}
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 CheckUIDevDir() {
log.Println("Checking FE dev directory...")
_, err := os.Stat("ui")
if err != nil {
log.Println("Error checking ui 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("ui/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
}

View file

@ -1,114 +0,0 @@
/*
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

@ -1,101 +0,0 @@
/*
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
}

View file

@ -1,45 +0,0 @@
/*
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
}

View file

@ -1,229 +0,0 @@
/*
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)
}

View file

@ -1,176 +0,0 @@
/*
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)
}

View file

@ -1,66 +0,0 @@
/*
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

@ -1,52 +0,0 @@
/*
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

@ -1,58 +0,0 @@
/*
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

@ -1,65 +0,0 @@
/*
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"`
}

View file

@ -1,64 +0,0 @@
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
}

View file

@ -1,34 +0,0 @@
@echo off
setlocal enabledelayedexpansion
REM Step 1: Build Macrame Go application for Windows
echo Building Macrame Go Application for Windows...
go build -ldflags "-H=windowsgui" -o Macrame.exe main.go
IF %ERRORLEVEL% NEQ 0 (
echo Go build failed!
exit /b %ERRORLEVEL%
) ELSE (
echo Go build was successful!
)
REM Step 2: Build Macrame Vue UI
echo Moving to ui directory and building Vue UI
cd ui
echo Running npm install...
call npm install
IF %ERRORLEVEL% NEQ 0 (
echo npm install failed!
exit /b %ERRORLEVEL%
)
echo Running npm run build...
call npm run build
IF %ERRORLEVEL% NEQ 0 (
echo npm run build failed!
exit /b %ERRORLEVEL%
)
echo Build complete.

46
go.mod
View file

@ -1,46 +0,0 @@
module macrame
go 1.24.1
require (
github.com/getlantern/systray v1.2.2
github.com/go-vgo/robotgo v0.110.7
)
require (
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/gen2brain/shm v0.1.1 // indirect
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/jezek/xgb v1.1.1 // indirect
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/otiai10/gosseract v2.2.1+incompatible // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/robotn/xgb v0.10.0 // indirect
github.com/robotn/xgbutil v0.10.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/vcaesar/gops v0.40.0 // indirect
github.com/vcaesar/imgo v0.40.2 // indirect
github.com/vcaesar/keycode v0.10.1 // indirect
github.com/vcaesar/tt v0.20.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
)

102
go.sum
View file

@ -1,102 +0,0 @@
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e h1:L+XrFvD0vBIBm+Wf9sFN6aU395t7JROoai0qXZraA4U=
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gen2brain/shm v0.1.1 h1:1cTVA5qcsUFixnDHl14TmRoxgfWEEZlTezpUj1vm5uQ=
github.com/gen2brain/shm v0.1.1/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-vgo/robotgo v0.110.7 h1:4scqQrJOBHoFCfcMROYEVFBxHvB3nF/UN6DWoRIFzBE=
github.com/go-vgo/robotgo v0.110.7/go.mod h1:eBUjTHY1HYjzdi1+UWJUbxB+b9gE+l4Ei7vQU/9SnLw=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c h1:1IlzDla/ZATV/FsRn1ETf7ir91PHS2mrd4VMunEtd9k=
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I=
github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
github.com/robotn/xgb v0.10.0 h1:O3kFbIwtwZ3pgLbp1h5slCQ4OpY8BdwugJLrUe6GPIM=
github.com/robotn/xgb v0.10.0/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
github.com/robotn/xgbutil v0.10.0 h1:gvf7mGQqCWQ68aHRtCxgdewRk+/KAJui6l3MJQQRCKw=
github.com/robotn/xgbutil v0.10.0/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35 h1:wAZbkTZkqDzWsqxPh2qkBd3KvFU7tcxV0BP0Rnhkxog=
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35/go.mod h1:aMd4yDHLjbOuYP6fMxj1d9ACDQlSWwYztcpybGHCQc8=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/vcaesar/gops v0.40.0 h1:I+1RCGiV+LkZJUYNzAd373xs0uM2UyeFdZBmow8HfCM=
github.com/vcaesar/gops v0.40.0/go.mod h1:3u/USW7JovqUK6i13VOD3qWfvXXd2TIIKE4PYIv4TOM=
github.com/vcaesar/imgo v0.40.2 h1:5GWScRLdBCMtO1v2I1bs+ZmDLZFINxYSMZ+mtUw5qPM=
github.com/vcaesar/imgo v0.40.2/go.mod h1:MVCl+FxHI2gTgmiHoi0n5xNCbYcfv9SVtdEOUC92+eo=
github.com/vcaesar/keycode v0.10.1 h1:0DesGmMAPWpYTCYddOFiCMKCDKgNnwiQa2QXindVUHw=
github.com/vcaesar/keycode v0.10.1/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ=
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg=
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

73
main.go
View file

@ -1,73 +0,0 @@
/*
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 main
import (
"log"
"net/http"
"os"
"macrame/app"
"macrame/app/helper"
)
func main() {
app.MCRMLogInit()
switchDir()
if helper.EnvGet("MCRM__PORT") == "" {
app.MCRMLog("Error: MCRM__PORT is not set")
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
apiInit(w, r)
})
helper.OpenBrowser("http://localhost:" + helper.EnvGet("MCRM__PORT"))
app.MCRMLog("Listening on http://localhost:" + helper.EnvGet("MCRM__PORT"))
app.InitSystray()
app.MCRMLog(http.ListenAndServe(":"+helper.EnvGet("MCRM__PORT"), nil))
}
func switchDir() {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
log.Println(cwd)
}
func apiInit(w http.ResponseWriter, r *http.Request) {
app.ApiCORS(w, r)
if r.Method == "GET" {
app.ApiGet(w, r)
} else if r.Method == "POST" {
app.ApiPost(w, r)
}
}

View file

@ -0,0 +1 @@
/*! tailwindcss v4.1.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){[data-v-0c582c8a],[data-v-0c582c8a]:before,[data-v-0c582c8a]:after,[data-v-0c582c8a]::backdrop{--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial}}}.accordion[data-v-0c582c8a]{display:grid}.accordion header[data-v-0c582c8a]{cursor:pointer;padding-inline:calc(var(--spacing,.25rem)*4);padding-block:calc(var(--spacing,.25rem)*2);grid-template-columns:1fr auto;display:grid}.accordion .accordion__wrapper[data-v-0c582c8a]{border-block-style:var(--tw-border-style);--tw-duration:.3s;--tw-ease:var(--ease-in-out,cubic-bezier(.4,0,.2,1));transition-duration:.3s;transition-timing-function:var(--ease-in-out,cubic-bezier(.4,0,.2,1));border-block-width:1px;border-top-color:#0000;border-bottom-color:#fff9;grid-template-rows:0fr;display:grid}@supports (color:color-mix(in lab,red,red)){.accordion .accordion__wrapper[data-v-0c582c8a]{border-bottom-color:color-mix(in oklab,var(--color-white,#fff)60%,transparent)}}.accordion .accordion__wrapper .accordion__content[data-v-0c582c8a]{opacity:0;transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4,0,.2,1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));grid-template-rows:0fr;transition-delay:0s;display:grid;overflow:hidden}.accordion .accordion__wrapper.open[data-v-0c582c8a]{border-top-color:#fff3;grid-template-rows:1fr}@supports (color:color-mix(in lab,red,red)){.accordion .accordion__wrapper.open[data-v-0c582c8a]{border-top-color:color-mix(in oklab,var(--color-white,#fff)20%,transparent)}}.accordion .accordion__wrapper.open .accordion__content[data-v-0c582c8a]{opacity:1;grid-template-rows:1fr;transition-delay:.2s}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}

View file

@ -0,0 +1,12 @@
import{c as d,_,a as f,o as h,b as m,d as g,e as c,g as a,k as C,t as k,j as w,h as i,u as p,l as x,L as y,E as B}from"./index-oAtpp-VZ.js";/**
* @license @tabler/icons-vue v3.31.0 - MIT
*
* This source code is licensed under the MIT license.
* See the LICENSE file in the root directory of this source tree.
*/var I=d("outline","chevron-down","IconChevronDown",[["path",{d:"M6 9l6 6l6 -6",key:"svg-0"}]]);/**
* @license @tabler/icons-vue v3.31.0 - MIT
*
* This source code is licensed under the MIT license.
* See the LICENSE file in the root directory of this source tree.
*/var A=d("outline","chevron-up","IconChevronUp",[["path",{d:"M6 15l6 -6l6 6",key:"svg-0"}]]);const $={class:"accordion"},D={class:"accordion__content"},M={__name:"AccordionComp",props:{title:String,open:Boolean},emits:["onOpen","onClose","onToggle"],setup(l,{expose:u,emit:v}){const s=v;u({toggleAccordion:n});const o=l,e=f(!1);h(()=>{o.open&&n(o.open)}),m(()=>{o.open&&n(o.open)});function n(t=!1){t?(e.value=!0,s("onOpen")):(e.value=!1,s("onClose")),s("onToggle")}return(t,r)=>(c(),g("div",$,[a("header",{onClick:r[0]||(r[0]=O=>n(!e.value))},[a("h4",null,k(l.title),1),C(x,{variant:"ghost",size:"sm",class:"!px-1"},{default:w(()=>[e.value?(c(),i(p(A),{key:1})):(c(),i(p(I),{key:0}))]),_:1})]),a("section",{class:B(`accordion__wrapper ${e.value?"open":""}`)},[a("div",D,[y(t.$slots,"default",{},void 0,!0)])],2)]))}},U=_(M,[["__scopeId","data-v-0c582c8a"]]);export{U as A};
//# sourceMappingURL=AccordionComp-D0eDAM6d.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"AccordionComp-D0eDAM6d.js","sources":["../../ui/node_modules/@tabler/icons-vue/dist/esm/icons/IconChevronDown.mjs","../../ui/node_modules/@tabler/icons-vue/dist/esm/icons/IconChevronUp.mjs","../../ui/src/components/base/AccordionComp.vue"],"sourcesContent":["/**\n * @license @tabler/icons-vue v3.31.0 - MIT\n *\n * This source code is licensed under the MIT license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createVueComponent from '../createVueComponent.mjs';\n\nvar IconChevronDown = createVueComponent(\"outline\", \"chevron-down\", \"IconChevronDown\", [[\"path\", { \"d\": \"M6 9l6 6l6 -6\", \"key\": \"svg-0\" }]]);\n\nexport { IconChevronDown as default };\n//# sourceMappingURL=IconChevronDown.mjs.map\n","/**\n * @license @tabler/icons-vue v3.31.0 - MIT\n *\n * This source code is licensed under the MIT license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createVueComponent from '../createVueComponent.mjs';\n\nvar IconChevronUp = createVueComponent(\"outline\", \"chevron-up\", \"IconChevronUp\", [[\"path\", { \"d\": \"M6 15l6 -6l6 6\", \"key\": \"svg-0\" }]]);\n\nexport { IconChevronUp as default };\n//# sourceMappingURL=IconChevronUp.mjs.map\n","<!--\nMacrame is a program that enables the user to create keyboard macros and button panels. \nThe macros are saved as simple JSON files and can be linked to the button panels. The panels can \nbe created with HTML and CSS.\n\nCopyright (C) 2025 Jesse Malotaux\n\nThis program is free software: you can redistribute it and/or modify \nit under the terms of the GNU General Public License as published by \nthe Free Software Foundation, either version 3 of the License, or \n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, \nbut WITHOUT ANY WARRANTY; without even the implied warranty of \nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the \nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License \nalong with this program. If not, see <https://www.gnu.org/licenses/>.\n-->\n\n<template>\n <div class=\"accordion\">\n <header @click=\"toggleAccordion(!accordionOpen)\">\n <h4>{{ title }}</h4>\n <ButtonComp variant=\"ghost\" size=\"sm\" class=\"!px-1\">\n <IconChevronDown v-if=\"!accordionOpen\" />\n <IconChevronUp v-else />\n </ButtonComp>\n </header>\n <section :class=\"`accordion__wrapper ${accordionOpen ? 'open' : ''}`\">\n <div class=\"accordion__content\">\n <slot />\n </div>\n </section>\n </div>\n</template>\n\n<script setup>\nimport { onMounted, onUpdated, ref } from 'vue'\nimport ButtonComp from './ButtonComp.vue'\nimport { IconChevronDown, IconChevronUp } from '@tabler/icons-vue'\n\nconst emit = defineEmits(['onOpen', 'onClose', 'onToggle'])\n\ndefineExpose({ toggleAccordion })\n\nconst props = defineProps({\n title: String,\n open: Boolean,\n})\n\nconst accordionOpen = ref(false)\n\nonMounted(() => {\n if (props.open) toggleAccordion(props.open)\n})\n\nonUpdated(() => {\n if (props.open) toggleAccordion(props.open)\n})\n\nfunction toggleAccordion(open = false) {\n if (open) {\n accordionOpen.value = true\n emit('onOpen')\n } else {\n accordionOpen.value = false\n emit('onClose')\n }\n\n emit('onToggle')\n}\n</script>\n\n<style scoped>\n@reference \"@/assets/main.css\";\n\n.accordion {\n @apply grid;\n\n header {\n @apply grid\n grid-cols-[1fr_auto]\n px-4 py-2\n cursor-pointer;\n }\n\n .accordion__wrapper {\n @apply grid\n grid-rows-[0fr]\n border-y\n border-b-white/60\n border-t-transparent\n duration-300\n ease-in-out;\n\n .accordion__content {\n @apply grid\n grid-rows-[0fr]\n overflow-hidden\n opacity-0\n transition-opacity\n delay-0;\n }\n\n &.open {\n @apply grid-rows-[1fr]\n border-t-white/20;\n\n .accordion__content {\n @apply grid-rows-[1fr] \n opacity-100\n delay-200;\n }\n }\n }\n}\n</style>\n"],"names":["IconChevronDown","createVueComponent","IconChevronUp","emit","__emit","__expose","toggleAccordion","props","__props","accordionOpen","ref","onMounted","onUpdated","open"],"mappings":"4IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GASA,IAAIA,EAAkBC,EAAmB,UAAW,eAAgB,kBAAmB,CAAC,CAAC,OAAQ,CAAE,EAAK,gBAAiB,IAAO,OAAO,CAAE,CAAC,CAAC,ECT3I;AAAA;AAAA;AAAA;AAAA;AAAA,GASA,IAAIC,EAAgBD,EAAmB,UAAW,aAAc,gBAAiB,CAAC,CAAC,OAAQ,CAAE,EAAK,iBAAkB,IAAO,OAAO,CAAE,CAAC,CAAC,0LCkCtI,MAAME,EAAOC,EAEbC,EAAa,CAAE,gBAAAC,CAAiB,CAAA,EAEhC,MAAMC,EAAQC,EAKRC,EAAgBC,EAAI,EAAK,EAE/BC,EAAU,IAAM,CACVJ,EAAM,MAAMD,EAAgBC,EAAM,IAAI,CAC5C,CAAC,EAEDK,EAAU,IAAM,CACVL,EAAM,MAAMD,EAAgBC,EAAM,IAAI,CAC5C,CAAC,EAED,SAASD,EAAgBO,EAAO,GAAO,CACjCA,GACFJ,EAAc,MAAQ,GACtBN,EAAK,QAAQ,IAEbM,EAAc,MAAQ,GACtBN,EAAK,SAAS,GAGhBA,EAAK,UAAU,CACjB","x_google_ignoreList":[0,1]}

View file

@ -0,0 +1 @@
.device-overview[data-v-0a3f2b41],.server-overview[data-v-6973a9f0]{align-content:flex-start;gap:calc(var(--spacing,.25rem)*4);display:grid}/*! tailwindcss v4.1.2 | MIT License | https://tailwindcss.com */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
import{a as i,o as r,b as v,d as _,e as k,g as c,L as d,k as u,j as C,u as b,U as $,l as B}from"./index-oAtpp-VZ.js";const x={class:"dialog-container"},L={__name:"DialogComp",props:{open:Boolean},emits:["onOpen","onClose","onToggle"],setup(p,{expose:g,emit:f}){const o=i(null),l=i(),t=f;g({toggleDialog:e});const s=p;r(()=>{s.open===!0&&e(s.open)}),v(()=>{s.open===!0&&e(s.open)});function e(n){n?(o.value.showModal(),t("onOpen")):(o.value.close(),t("onClose")),l.value=n,t("onToggle")}return r(()=>{l.value=s.open,o.value.innerHTML.includes("form")&&o.value.querySelector("form").addEventListener("submit",()=>{e()})}),(n,a)=>(k(),_("div",x,[c("div",{class:"trigger",onClick:a[0]||(a[0]=m=>e(!0))},[d(n.$slots,"trigger")]),c("dialog",{ref_key:"dialog",ref:o,class:"mcrm-block block__dark"},[u(B,{class:"dialog__close p-0",variant:"ghost",size:"sm",tabindex:"-1",onClick:a[1]||(a[1]=m=>e(!1))},{default:C(()=>[u(b($))]),_:1}),d(n.$slots,"content")],512)]))}};export{L as _};
//# sourceMappingURL=DialogComp-CyRyORDj.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"DialogComp-CyRyORDj.js","sources":["../../ui/src/components/base/DialogComp.vue"],"sourcesContent":["<!--\nMacrame is a program that enables the user to create keyboard macros and button panels. \nThe macros are saved as simple JSON files and can be linked to the button panels. The panels can \nbe created with HTML and CSS.\n\nCopyright (C) 2025 Jesse Malotaux\n\nThis program is free software: you can redistribute it and/or modify \nit under the terms of the GNU General Public License as published by \nthe Free Software Foundation, either version 3 of the License, or \n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, \nbut WITHOUT ANY WARRANTY; without even the implied warranty of \nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the \nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License \nalong with this program. If not, see <https://www.gnu.org/licenses/>.\n-->\n\n<template>\n <div class=\"dialog-container\">\n <div class=\"trigger\" @click=\"toggleDialog(true)\">\n <slot name=\"trigger\" />\n </div>\n <dialog ref=\"dialog\" class=\"mcrm-block block__dark\">\n <ButtonComp\n class=\"dialog__close p-0\"\n variant=\"ghost\"\n size=\"sm\"\n tabindex=\"-1\"\n @click=\"toggleDialog(false)\"\n >\n <IconX />\n </ButtonComp>\n <slot name=\"content\" />\n </dialog>\n </div>\n</template>\n\n<script setup>\nimport ButtonComp from './ButtonComp.vue'\nimport { IconX } from '@tabler/icons-vue'\nimport { onMounted, onUpdated, ref } from 'vue'\n\nconst dialog = ref(null)\nconst openDialog = ref()\n\nconst emit = defineEmits(['onOpen', 'onClose', 'onToggle'])\n\ndefineExpose({ toggleDialog })\n\nconst props = defineProps({\n open: Boolean,\n})\n\nonMounted(() => {\n if (props.open === true) toggleDialog(props.open)\n})\n\nonUpdated(() => {\n if (props.open === true) toggleDialog(props.open)\n})\n\nfunction toggleDialog(openToggle) {\n if (openToggle) {\n dialog.value.showModal()\n emit('onOpen')\n } else {\n dialog.value.close()\n emit('onClose')\n }\n\n openDialog.value = openToggle\n emit('onToggle')\n}\n\nonMounted(() => {\n openDialog.value = props.open\n\n if (dialog.value.innerHTML.includes('form')) {\n dialog.value.querySelector('form').addEventListener('submit', () => {\n toggleDialog()\n })\n }\n})\n</script>\n\n<style>\n@reference \"@/assets/main.css\";\n\n.dialog-container {\n @apply relative;\n\n dialog {\n @apply fixed\n top-1/2 left-1/2\n -translate-x-1/2 -translate-y-1/2\n max-w-[calc(100vw-2rem)]\n text-slate-200\n /* shadow-md */\n /* shadow-black */\n z-50\n pointer-events-none;\n\n &[open] {\n @apply pointer-events-auto;\n }\n\n &::backdrop {\n @apply bg-black/50 backdrop-blur-xs transition;\n }\n\n .dialog__close {\n @apply absolute\n top-4 right-4\n p-0\n text-white;\n\n svg {\n @apply size-5;\n }\n }\n }\n}\n.dialog__content {\n > *:first-child {\n @apply pr-8;\n }\n}\n</style>\n"],"names":["dialog","ref","openDialog","emit","__emit","__expose","toggleDialog","props","__props","onMounted","onUpdated","openToggle"],"mappings":"qQA8CA,MAAMA,EAASC,EAAI,IAAI,EACjBC,EAAaD,EAAG,EAEhBE,EAAOC,EAEbC,EAAa,CAAE,aAAAC,CAAc,CAAA,EAE7B,MAAMC,EAAQC,EAIdC,EAAU,IAAM,CACVF,EAAM,OAAS,IAAMD,EAAaC,EAAM,IAAI,CAClD,CAAC,EAEDG,EAAU,IAAM,CACVH,EAAM,OAAS,IAAMD,EAAaC,EAAM,IAAI,CAClD,CAAC,EAED,SAASD,EAAaK,EAAY,CAC5BA,GACFX,EAAO,MAAM,UAAS,EACtBG,EAAK,QAAQ,IAEbH,EAAO,MAAM,MAAK,EAClBG,EAAK,SAAS,GAGhBD,EAAW,MAAQS,EACnBR,EAAK,UAAU,CACjB,CAEA,OAAAM,EAAU,IAAM,CACdP,EAAW,MAAQK,EAAM,KAErBP,EAAO,MAAM,UAAU,SAAS,MAAM,GACxCA,EAAO,MAAM,cAAc,MAAM,EAAE,iBAAiB,SAAU,IAAM,CAClEM,EAAY,CACb,CAAA,CAEL,CAAC"}

View file

@ -0,0 +1 @@
/*! tailwindcss v4.1.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}.dialog-container{position:relative}.dialog-container dialog{pointer-events:none;z-index:50;--tw-translate-x: -50% ;max-width:calc(100vw - 2rem);--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y);color:var(--color-slate-200,oklch(92.9% .013 255.508));position:fixed;top:50%;left:50%}.dialog-container dialog[open]{pointer-events:auto}.dialog-container dialog::backdrop{--tw-backdrop-blur:blur(var(--blur-xs,4px));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4,0,.2,1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.dialog-container dialog::backdrop{background-color:color-mix(in oklab,var(--color-black,#000)50%,transparent)}}.dialog-container dialog .dialog__close{top:calc(var(--spacing,.25rem)*4);right:calc(var(--spacing,.25rem)*4);padding:calc(var(--spacing,.25rem)*0);color:var(--color-white,#fff);position:absolute}.dialog-container dialog .dialog__close svg{width:calc(var(--spacing,.25rem)*5);height:calc(var(--spacing,.25rem)*5)}.dialog__content>:first-child{padding-right:calc(var(--spacing,.25rem)*8)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}

View file

@ -0,0 +1,17 @@
import{c as a}from"./index-oAtpp-VZ.js";/**
* @license @tabler/icons-vue v3.31.0 - MIT
*
* This source code is licensed under the MIT license.
* See the LICENSE file in the root directory of this source tree.
*/var l=a("outline","device-floppy","IconDeviceFloppy",[["path",{d:"M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2",key:"svg-0"}],["path",{d:"M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0",key:"svg-1"}],["path",{d:"M14 4l0 4l-6 0l0 -4",key:"svg-2"}]]);/**
* @license @tabler/icons-vue v3.31.0 - MIT
*
* This source code is licensed under the MIT license.
* See the LICENSE file in the root directory of this source tree.
*/var v=a("outline","pencil","IconPencil",[["path",{d:"M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4",key:"svg-0"}],["path",{d:"M13.5 6.5l4 4",key:"svg-1"}]]);/**
* @license @tabler/icons-vue v3.31.0 - MIT
*
* This source code is licensed under the MIT license.
* See the LICENSE file in the root directory of this source tree.
*/var p=a("outline","trash","IconTrash",[["path",{d:"M4 7l16 0",key:"svg-0"}],["path",{d:"M10 11l0 6",key:"svg-1"}],["path",{d:"M14 11l0 6",key:"svg-2"}],["path",{d:"M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12",key:"svg-3"}],["path",{d:"M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3",key:"svg-4"}]]);export{p as I,l as a,v as b};
//# sourceMappingURL=IconTrash-DtIpQ-M_.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"IconTrash-DtIpQ-M_.js","sources":["../../ui/node_modules/@tabler/icons-vue/dist/esm/icons/IconDeviceFloppy.mjs","../../ui/node_modules/@tabler/icons-vue/dist/esm/icons/IconPencil.mjs","../../ui/node_modules/@tabler/icons-vue/dist/esm/icons/IconTrash.mjs"],"sourcesContent":["/**\n * @license @tabler/icons-vue v3.31.0 - MIT\n *\n * This source code is licensed under the MIT license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createVueComponent from '../createVueComponent.mjs';\n\nvar IconDeviceFloppy = createVueComponent(\"outline\", \"device-floppy\", \"IconDeviceFloppy\", [[\"path\", { \"d\": \"M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2\", \"key\": \"svg-0\" }], [\"path\", { \"d\": \"M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0\", \"key\": \"svg-1\" }], [\"path\", { \"d\": \"M14 4l0 4l-6 0l0 -4\", \"key\": \"svg-2\" }]]);\n\nexport { IconDeviceFloppy as default };\n//# sourceMappingURL=IconDeviceFloppy.mjs.map\n","/**\n * @license @tabler/icons-vue v3.31.0 - MIT\n *\n * This source code is licensed under the MIT license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createVueComponent from '../createVueComponent.mjs';\n\nvar IconPencil = createVueComponent(\"outline\", \"pencil\", \"IconPencil\", [[\"path\", { \"d\": \"M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4\", \"key\": \"svg-0\" }], [\"path\", { \"d\": \"M13.5 6.5l4 4\", \"key\": \"svg-1\" }]]);\n\nexport { IconPencil as default };\n//# sourceMappingURL=IconPencil.mjs.map\n","/**\n * @license @tabler/icons-vue v3.31.0 - MIT\n *\n * This source code is licensed under the MIT license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createVueComponent from '../createVueComponent.mjs';\n\nvar IconTrash = createVueComponent(\"outline\", \"trash\", \"IconTrash\", [[\"path\", { \"d\": \"M4 7l16 0\", \"key\": \"svg-0\" }], [\"path\", { \"d\": \"M10 11l0 6\", \"key\": \"svg-1\" }], [\"path\", { \"d\": \"M14 11l0 6\", \"key\": \"svg-2\" }], [\"path\", { \"d\": \"M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12\", \"key\": \"svg-3\" }], [\"path\", { \"d\": \"M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3\", \"key\": \"svg-4\" }]]);\n\nexport { IconTrash as default };\n//# sourceMappingURL=IconTrash.mjs.map\n"],"names":["IconDeviceFloppy","createVueComponent","IconPencil","IconTrash"],"mappings":"wCAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GASG,IAACA,EAAmBC,EAAmB,UAAW,gBAAiB,mBAAoB,CAAC,CAAC,OAAQ,CAAE,EAAK,uEAAwE,IAAO,OAAS,CAAA,EAAG,CAAC,OAAQ,CAAE,EAAK,2CAA4C,IAAO,OAAS,CAAA,EAAG,CAAC,OAAQ,CAAE,EAAK,sBAAuB,IAAO,OAAS,CAAA,CAAC,CAAC,ECT9U;AAAA;AAAA;AAAA;AAAA;AAAA,GASG,IAACC,EAAaD,EAAmB,UAAW,SAAU,aAAc,CAAC,CAAC,OAAQ,CAAE,EAAK,0DAA2D,IAAO,OAAS,CAAA,EAAG,CAAC,OAAQ,CAAE,EAAK,gBAAiB,IAAO,OAAO,CAAE,CAAC,CAAC,ECTzN;AAAA;AAAA;AAAA;AAAA;AAAA,GASG,IAACE,EAAYF,EAAmB,UAAW,QAAS,YAAa,CAAC,CAAC,OAAQ,CAAE,EAAK,YAAa,IAAO,OAAS,CAAA,EAAG,CAAC,OAAQ,CAAE,EAAK,aAAc,IAAO,OAAO,CAAE,EAAG,CAAC,OAAQ,CAAE,EAAK,aAAc,IAAO,OAAS,CAAA,EAAG,CAAC,OAAQ,CAAE,EAAK,iDAAkD,IAAO,OAAS,CAAA,EAAG,CAAC,OAAQ,CAAE,EAAK,2CAA4C,IAAO,OAAO,CAAE,CAAC,CAAC","x_google_ignoreList":[0,1,2]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before After
Before After

View file

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="mcrm-icon.svg" /> <link rel="icon" type="image/svg+xml" href="/assets/mcrm-icon-_gnb1h7S.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" /> <meta name="color-scheme" content="dark" />
<link rel="preconnect" href="https://fonts.bunny.net" /> <link rel="preconnect" href="https://fonts.bunny.net" />
@ -12,9 +12,10 @@
/> />
<title>Macrame</title> <title>Macrame</title>
<script src="config.js"></script> <script src="config.js"></script>
<script type="module" crossorigin src="/assets/index-oAtpp-VZ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BwE95Wut.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

View file

@ -1,9 +0,0 @@
[*.{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

1
ui/.gitattributes vendored
View file

@ -1 +0,0 @@
* text=auto eol=lf

30
ui/.gitignore vendored
View file

@ -1,30 +0,0 @@
# 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

View file

@ -1,7 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

View file

@ -1,8 +0,0 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode"
]
}

View file

@ -1,35 +0,0 @@
# fe
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View file

@ -1,19 +0,0 @@
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
export default [
{
name: 'app/files-to-lint',
files: ['**/*.{js,mjs,jsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
},
js.configs.recommended,
...pluginVue.configs['flat/essential'],
skipFormatting,
]

View file

@ -1,8 +0,0 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

5334
ui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,38 +0,0 @@
{
"name": "Macrame - UI",
"description": "Macrame UI - Macrame is a small application that can turn any device into a button panel for your pc.",
"version": "1.0.0",
"private": false,
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vite build --emptyOutDir",
"preview": "vite preview",
"lint": "eslint . --fix",
"format": "prettier --write src/"
},
"dependencies": {
"@tabler/icons-vue": "^3.30.0",
"@tailwindcss/vite": "^4.0.9",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@basitcodeenv/vue3-device-detect": "^1.0.3",
"@eslint/js": "^9.20.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-prettier": "^10.2.0",
"axios": "^1.8.3",
"crypto-js": "^4.2.0",
"eslint": "^9.20.1",
"eslint-plugin-vue": "^9.32.0",
"pinia": "^3.0.1",
"prettier": "^3.5.1",
"qrcode": "^1.5.4",
"sass-embedded": "^1.85.1",
"tailwindcss": "^4.0.9",
"uuid": "^11.1.0",
"vite": "^6.1.0",
"vite-plugin-vue-devtools": "^7.7.2"
}
}

View file

@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
<script type="module" crossorigin src="/assets/index-CNkZ911J.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-zqIqfzzx.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -1,98 +0,0 @@
<!--
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/>.
-->
<template>
<div class="app-background">
<img src="./assets/img/bg-gradient.svg" aria-hidden="true" />
<img src="@/assets/img/Macrame-Logo-white.svg" class="logo" aria-hidden="true" />
</div>
<MainMenu />
<RouterView />
<AlertComp
v-if="!isLocal && !handshake && route.fullPath !== '/devices'"
variant="warning"
:page-wide="true"
href="/devices"
>
<h4>Not authorized!</h4>
<p>Click here to start authorization and open the "Devices" page on your PC.</p>
</AlertComp>
</template>
<script setup>
import MainMenu from '@/components/base/MainMenu.vue'
import { onMounted, ref } from 'vue'
import { RouterView, useRoute } from 'vue-router'
import { useDeviceStore } from './stores/device'
import { isLocal } from './services/ApiService'
import AlertComp from './components/base/AlertComp.vue'
const device = useDeviceStore()
const route = useRoute()
const handshake = ref(false)
onMounted(() => {
// Setting device uuid from localstorage
// If not present in LocalStorage a new uuidV4 will be generated
device.uuid()
if (!isLocal) appHandshake()
device.$subscribe(() => {
if (device.key()) handshake.value = true
})
})
async function appHandshake() {
const hsReq = await device.remoteHandshake()
handshake.value = hsReq
}
</script>
<style scoped>
@reference "@/assets/main.css";
.app-background {
@apply fixed
inset-0
size-full
overflow-hidden
pointer-events-none
opacity-40
z-[-1];
img {
@apply absolute
size-full
object-cover;
}
.logo {
@apply absolute
top-[10%]
left-[10%]
scale-[1.8]
opacity-35
mix-blend-overlay;
}
}
</style>

View file

@ -1,34 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 140 80"
width="140"
height="80"
>
<g>
<path style="fill:#FFB900;" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12
L95.5,18.3z"/>
<path style="fill:#00BCFF;" d="M46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3c-0.2,0-0.4,0-0.5,0.1l-0.2,0.1l-7.8,7.8h0l12,12l8.5-8.5L46,18.3
z"/>
<path style="fill:#00BCFF;" d="M94.8,67.1L94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2
c2.8,0,5.6-0.7,8.1-2.2L94.8,67.1z"/>
<path style="fill:#00BCFF;" d="M127.4,28.9l-13.4-13.4l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8
c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5
l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8c0,1-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3v0c3.1-3.1,4.7-7.2,4.7-11.3
C132,36.1,130.5,32,127.4,28.9z"/>
<g>
<path style="fill:#FFB900;" d="M110.4,61.5l-5-5l0,0l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7
c-1-1-2.1-1.8-3.2-2.5C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9v0C9.7,32,8.1,36.1,8.1,40.2
c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8c0-1,0.4-2.1,1.1-2.8l13.4-13.4l8.5-8.5l0.1,0.1
c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l0,0l2.1,2.1l8.5,8.5l2.1,2.1l0,0l8.5,8.5l16.2,16.2l0,0L97,65l8.4,8.4
c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8L110.4,61.5z"/>
</g>
<g>
<path style="fill:#FFB900;" d="M70.1,42.3L70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2
c2.8,0,5.6-0.8,8.1-2.2c1.1-0.7,2.2-1.5,3.2-2.5l13.4-13.4l8.5-8.5L70.1,42.3z"/>
</g>
<g>
<path style="fill:#00BCFF;" d="M59.5,31.7L51,40.2L38.2,53l-3.5,3.5l0,0l-5,5L26.2,65l7.8,7.8c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5
l4.9-4.9l0.1-0.1l16.2-16.2l8.5-8.5L59.5,31.7z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,60 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px"
viewBox="0 0 140 80"
style="enable-background:new 0 0 140 80;"
xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);}
.st2{fill:url(#SVGID_3_);}
.st3{fill:url(#SVGID_4_);}
.st4{fill:url(#SVGID_5_);}
.st5{fill:url(#SVGID_6_);}
.st6{fill:url(#SVGID_7_);}
</style>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="28.05" x2="140" y2="28.05">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st0" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12L95.5,18.3z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="28" x2="140" y2="28">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st1" d="M57.3,29.5L46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3s-0.4,0-0.5,0.1l-0.2,0.1L36.8,26l12,12L57.3,29.5z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="65.25" x2="140" y2="65.25">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st2" d="M94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.7,8.1-2.2L94.7,67
L94.7,67z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="32.9162" x2="140" y2="32.9162">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st3" d="M114,15.5l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19
l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8
s-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3c3.1-3.1,4.7-7.2,4.7-11.3c0-4.1-1.5-8.2-4.6-11.3L114,15.5z"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="38.2163" x2="140" y2="38.2163">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st4" d="M105.4,56.5l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7c-1-1-2.1-1.8-3.2-2.5
C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9C9.7,32,8.1,36.1,8.1,40.2c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3
l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8s0.4-2.1,1.1-2.8L34.7,24l8.5-8.5l0.1,0.1c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l2.1,2.1
l8.5,8.5l2.1,2.1l8.5,8.5l16.2,16.2L97,65l8.4,8.4c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8l-3.5-3.4L105.4,56.5z"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="59.85" x2="140" y2="59.85">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st5" d="M70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.8,8.1-2.2
c1.1-0.7,2.2-1.5,3.2-2.5L70,59.3l8.5-8.5L70.1,42.3z"/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="52.6" x2="140" y2="52.6">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st6" d="M43.1,65.1l0.1-0.1l16.2-16.2l8.5-8.5l-8.4-8.6L51,40.2L38.2,53l-3.5,3.5l-5,5L26.2,65l7.8,7.8
c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5L43.1,65.1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -1,23 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px"
viewBox="0 0 140 80"
style="enable-background:new 0 0 140 80;"
xml:space="preserve">
<path style="fill:#fff" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12L95.5,18.3z"/>
<path style="fill:#fff" d="M57.3,29.5L46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3s-0.4,0-0.5,0.1l-0.2,0.1L36.8,26l12,12L57.3,29.5z"/>
<path style="fill:#fff" d="M94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.7,8.1-2.2L94.7,67
L94.7,67z"/>
<path style="fill:#fff" d="M114,15.5l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19
l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8
s-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3c3.1-3.1,4.7-7.2,4.7-11.3c0-4.1-1.5-8.2-4.6-11.3L114,15.5z"/>
<path style="fill:#fff" d="M105.4,56.5l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7c-1-1-2.1-1.8-3.2-2.5
C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9C9.7,32,8.1,36.1,8.1,40.2c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3
l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8s0.4-2.1,1.1-2.8L34.7,24l8.5-8.5l0.1,0.1c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l2.1,2.1
l8.5,8.5l2.1,2.1l8.5,8.5l16.2,16.2L97,65l8.4,8.4c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8l-3.5-3.4L105.4,56.5z"/>
<path style="fill:#fff" d="M70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.8,8.1-2.2
c1.1-0.7,2.2-1.5,3.2-2.5L70,59.3l8.5-8.5L70.1,42.3z"/>
<path style="fill:#fff" d="M43.1,65.1l0.1-0.1l16.2-16.2l8.5-8.5l-8.4-8.6L51,40.2L38.2,53l-3.5,3.5l-5,5L26.2,65l7.8,7.8
c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5L43.1,65.1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 2560 1440" style="enable-background:new 0 0 2560 1440;" xml:space="preserve">
<rect style="fill:#020618;" width="2560" height="1440"/>
<radialGradient id="SVGID_1_" cx="1280" cy="720" r="507.7116" fx="1274.7371" fy="1155.8185" gradientTransform="matrix(1 0 0 2.2985 0 -934.9553)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
</radialGradient>
<rect style="opacity:0.55;fill:url(#SVGID_1_);" width="2560" height="1440"/>
<radialGradient id="SVGID_2_" cx="1352.0476" cy="1354.1904" r="1334.0841" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
</radialGradient>
<rect style="opacity:0.55;fill:url(#SVGID_2_);" width="2560" height="1440"/>
<radialGradient id="SVGID_3_" cx="1292.0344" cy="1255.0016" r="2246.7517" gradientTransform="matrix(-0.7144 -0.6998 0.1899 -0.1939 1976.6873 2402.437)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
</radialGradient>
<polygon style="opacity:0.55;fill:url(#SVGID_3_);" points="2560,1440 0,1440 0,-7 2560,0 "/>
<radialGradient id="SVGID_4_" cx="1292.0344" cy="1255.8966" r="2246.5256" fx="334.4712" fy="1265.3895" gradientTransform="matrix(0.7144 -0.6998 -0.1899 -0.1939 583.4827 2403.5054)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
</radialGradient>
<polygon style="opacity:0.55;fill:url(#SVGID_4_);" points="0,1440 2560,1440 2560,0 0,0 "/>
<radialGradient id="SVGID_5_" cx="1239.8966" cy="1737.5518" r="877.3733" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#FFB900;stop-opacity:0"/>
</radialGradient>
<rect style="fill:url(#SVGID_5_);" width="2560" height="1440"/>
<radialGradient id="SVGID_6_" cx="1287.069" cy="950.5172" r="845.7465" fx="1276.8361" fy="325.8423" gradientTransform="matrix(-1 3.730347e-03 -1.479320e-03 -0.3966 2575.5352 1322.6541)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FFB900;stop-opacity:0.3"/>
<stop offset="1" style="stop-color:#FFB900;stop-opacity:0"/>
</radialGradient>
<rect style="fill:url(#SVGID_6_);" width="2560" height="1440"/>
<radialGradient id="SVGID_7_" cx="1316.8621" cy="1417.2759" r="1888.6272" gradientTransform="matrix(0.6652 -0.7467 0.1801 0.1604 185.7137 2173.2124)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FFB900;stop-opacity:0.38"/>
<stop offset="1" style="stop-color:#FFB900;stop-opacity:0"/>
</radialGradient>
<rect style="fill:url(#SVGID_7_);" width="2560" height="1440"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -1,38 +0,0 @@
/*
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/>.
*/
@import './style/_content.css';
@import './style/_form.css';
@import './style/_scrollbar.css';
@import './style/_macro.css';
@import './style/_mcrm-block.css';
@import './style/_panel.css';
@import 'tailwindcss';
@variant dark (&:where(.dark, .dark *));
html,
body,
:not(#panel-html__body) {
--font-sans: 'Roboto', sans-serif;
--font-mono: 'Fira Code', monospace;
}

View file

@ -1,68 +0,0 @@
/*
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/>.
*/
body {
@apply font-sans font-light tracking-wide bg-slate-900 text-slate-50;
}
h1,
h2 {
@apply font-mono font-bold;
}
h3,
h4,
h5,
h6 {
@apply font-semibold;
}
h1 {
@apply text-4xl;
}
h2 {
@apply text-3xl;
}
h3 {
@apply text-2xl;
}
h4 {
@apply text-xl;
}
h5 {
@apply text-lg;
}
ul {
@apply list-disc list-inside;
}
strong {
@apply font-bold;
}
a {
@apply underline text-amber-400 hover:text-amber-300;
}

View file

@ -1,49 +0,0 @@
/*
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/>.
*/
.input-group {
@apply grid gap-2;
}
input,
select {
@apply w-full px-2 py-1 text-white border rounded-md border-slate-400 bg-black/20;
}
:has(> input + span) {
@apply flex;
input {
@apply rounded-r-none;
}
span {
@apply flex items-center px-2 text-white rounded-r-md bg-slate-400;
}
}
select option {
@apply bg-slate-700;
&:not([disabled]) {
@apply cursor-pointer;
}
}

View file

@ -1,49 +0,0 @@
/*
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/>.
*/
/* @reference "main"; */
hr.spacer {
@apply relative
w-6
border
border-gray-300
opacity-80
overflow-visible;
&::before,
&::after {
@apply content-['']
absolute
top-1/2
-translate-y-1/2
size-2
bg-gray-300
rounded-full;
}
&::before {
@apply -left-1;
}
&::after {
@apply -right-1;
}
}

View file

@ -1,123 +0,0 @@
/*
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/>.
*/
.mcrm-block {
@apply relative p-6 overflow-hidden gap-x-6 gap-y-2 backdrop-blur-lg rounded-2xl;
&::before {
@apply content-['']
absolute
inset-0
p-px
rounded-2xl
size-full
bg-gradient-to-br
to-transparent
z-[10]
pointer-events-none;
mask:
linear-gradient(#000 0 0) exclude,
linear-gradient(#000 0 0) content-box;
}
&.block__light {
@apply bg-white/20;
&::before {
@apply from-white/20;
}
}
&.block__dark {
@apply bg-slate-900/70;
&::before {
@apply from-slate-400/40;
}
}
&.block__primary {
@apply bg-sky-300/20;
&::before {
@apply from-sky-100/20;
}
}
&.block__secondary {
@apply bg-amber-300/20;
&::before {
@apply from-amber-100/20;
}
}
&.block__success {
@apply bg-emerald-300/40;
&::before {
@apply from-emerald-100/40;
}
}
&.block__warning {
@apply bg-orange-300/40;
&::before {
@apply from-orange-100/40;
}
}
&.block__danger {
@apply bg-rose-300/40;
&::before {
@apply from-rose-100/40;
}
}
&.block-spacing__sm,
&.block-size__sm {
@apply p-4 gap-x-4 gap-y-2;
}
&.block-size__sm {
@apply rounded-lg;
&::before {
@apply rounded-lg;
}
}
&.block-spacing__lg,
&.block-size__lg {
@apply p-8 gap-x-8 gap-y-4;
}
&.block-size__lg {
@apply rounded-3xl;
&::before {
@apply rounded-3xl;
}
}
}

View file

@ -1,56 +0,0 @@
/*
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/>.
*/
.panel {
@apply grid
grid-rows-[auto_1fr]
fixed
top-2
left-4 sm:left-16
right-4 sm:right-16
bottom-2
overflow-hidden;
> .panel__header,
> .panel__title {
@apply px-4 py-2;
/* &:first-child {
@apply pt-4;
}
&:last-child {
@apply pb-4;
} */
}
.panel__title {
@apply pt-3 pl-16 text-transparent bg-gradient-to-r w-fit from-amber-300 to-white/50 sm:pl-4 bg-clip-text;
}
.panel__content {
@apply grid
h-[calc(100%-1rem)]
pt-4 sm:pt-0
pl-0 sm:pl-4
overflow-auto;
}
}

View file

@ -1,44 +0,0 @@
/*
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/>.
*/
::-webkit-scrollbar {
@apply w-2;
}
::-moz-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-thumb {
@apply rounded bg-slate-400/80;
}
::-moz-scrollbar-thumb {
@apply rounded bg-slate-400/80;
}
::-webkit-scrollbar-track {
@apply mr-1 rounded bg-slate-100/10;
}
::-moz-scrollbar-track {
@apply mr-1 rounded bg-slate-100/10;
}

View file

@ -1,119 +0,0 @@
<!--
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/>.
-->
<template>
<div class="accordion">
<header @click="toggleAccordion(!accordionOpen)">
<h4>{{ title }}</h4>
<ButtonComp variant="ghost" size="sm" class="!px-1">
<IconChevronDown v-if="!accordionOpen" />
<IconChevronUp v-else />
</ButtonComp>
</header>
<section :class="`accordion__wrapper ${accordionOpen ? 'open' : ''}`">
<div class="accordion__content">
<slot />
</div>
</section>
</div>
</template>
<script setup>
import { onMounted, onUpdated, ref } from 'vue'
import ButtonComp from './ButtonComp.vue'
import { IconChevronDown, IconChevronUp } from '@tabler/icons-vue'
const emit = defineEmits(['onOpen', 'onClose', 'onToggle'])
defineExpose({ toggleAccordion })
const props = defineProps({
title: String,
open: Boolean,
})
const accordionOpen = ref(false)
onMounted(() => {
if (props.open) toggleAccordion(props.open)
})
onUpdated(() => {
if (props.open) toggleAccordion(props.open)
})
function toggleAccordion(open = false) {
if (open) {
accordionOpen.value = true
emit('onOpen')
} else {
accordionOpen.value = false
emit('onClose')
}
emit('onToggle')
}
</script>
<style scoped>
@reference "@/assets/main.css";
.accordion {
@apply grid;
header {
@apply grid
grid-cols-[1fr_auto]
px-4 py-2
cursor-pointer;
}
.accordion__wrapper {
@apply grid
grid-rows-[0fr]
border-y
border-b-white/60
border-t-transparent
duration-300
ease-in-out;
.accordion__content {
@apply grid
grid-rows-[0fr]
overflow-hidden
opacity-0
transition-opacity
delay-0;
}
&.open {
@apply grid-rows-[1fr]
border-t-white/20;
.accordion__content {
@apply grid-rows-[1fr]
opacity-100
delay-200;
}
}
}
}
</style>

View file

@ -1,100 +0,0 @@
<!--
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/>.
-->
<template>
<div
:class="`alert alert__${variant} ${pageWide ? 'page-wide' : ''}`"
@click="href ? router.push(href) : null"
>
<IconInfoCircle v-if="variant === 'info'" />
<IconCheck v-if="variant === 'success'" />
<IconExclamationCircle v-if="variant === 'warning'" />
<IconAlertTriangle v-if="variant === 'error'" />
<div class="alert__content">
<slot />
</div>
</div>
</template>
<script setup>
import {
IconAlertTriangle,
IconCheck,
IconExclamationCircle,
IconInfoCircle,
} from '@tabler/icons-vue'
import { useRouter } from 'vue-router'
defineProps({
variant: String, // info, success, warning, error
pageWide: Boolean,
href: String,
})
const router = useRouter()
</script>
<style scoped>
@reference "@/assets/main.css";
.alert {
@apply grid
grid-cols-[1rem_1fr]
items-start
gap-4
p-4
border
border-white/10
bg-white/10
rounded-md
backdrop-blur-md;
&.alert__info {
@apply text-sky-100 bg-sky-400/40;
}
&.alert__success {
@apply text-lime-400 bg-lime-400/10;
}
&.alert__warning {
@apply text-amber-400 bg-amber-400/10;
}
&.alert__error {
@apply text-rose-400 bg-rose-400/10;
}
&.page-wide {
@apply fixed
bottom-0 left-0
w-full;
}
&[href] {
@apply cursor-pointer;
}
.alert__content {
@apply grid gap-2;
}
}
</style>

View file

@ -1,189 +0,0 @@
<!--
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/>.
-->
<template>
<template v-if="href">
<RouterLink :to="href" :class="classString">
<slot />
</RouterLink>
</template>
<template v-else>
<button :class="classString">
<slot />
</button>
</template>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
href: String,
variant: String,
size: String,
})
const classString = computed(() => {
let classes = 'btn'
if (props.variant) classes += ` btn__${props.variant}`
if (props.size) classes += ` btn__${props.size}`
return classes
})
</script>
<style>
@reference "@/assets/main.css";
button,
.btn {
@apply flex
items-center
gap-3
h-fit
px-4 py-2
border
border-solid
rounded-lg
tracking-wide
font-normal
transition-all
cursor-pointer
no-underline;
transition:
border-color 0.1s ease-in-out,
background-color 0.2s ease;
&:not(.button__subtle, .button__ghost):hover {
@apply shadow-black;
}
&[disabled],
&.disabled {
@apply opacity-50 pointer-events-none cursor-not-allowed;
}
svg {
@apply size-5 transition-[stroke] duration-400 ease-in-out;
}
&.btn__sm {
@apply px-3 py-1
text-sm;
svg {
@apply size-4;
}
}
&.btn__lg {
@apply px-6 py-3
text-lg;
svg {
@apply size-6;
}
}
&:hover {
@apply text-white;
svg {
@apply stroke-current;
}
}
&.btn__primary {
@apply bg-sky-100/10 border-sky-100 text-sky-100;
svg {
@apply stroke-sky-200;
}
&:hover {
@apply bg-sky-400/40 border-sky-300;
}
}
&.btn__secondary {
@apply bg-amber-100/10 border-amber-100 text-amber-100;
svg {
@apply stroke-amber-300;
}
&:hover {
@apply bg-amber-400/40 border-amber-400;
}
}
&.btn__danger {
@apply bg-rose-200/20 border-rose-100 text-rose-200;
svg {
@apply stroke-rose-400;
}
&:hover {
@apply bg-rose-400/40 border-rose-500 text-white;
}
}
&.btn__dark {
/* @apply bg-slate-700/80 hover:bg-slate-700 text-white border-slate-600; */
@apply bg-slate-200/10 border-slate-400 text-slate-100;
svg {
@apply stroke-slate-300;
}
&:hover {
@apply bg-slate-400/40 border-slate-200 text-white;
}
}
&.btn__success {
/* @apply bg-lime-500/80 hover:bg-lime-500 text-white border-lime-600; */
@apply bg-lime-200/10 border-lime-100 text-lime-100;
svg {
@apply stroke-lime-400;
}
&:hover {
@apply bg-lime-400/40 border-lime-500 text-white;
}
}
&.btn__subtle {
@apply bg-transparent hover:bg-white/10 text-white border-transparent;
&:hover {
@apply bg-white/20 to-white/30 border-white/40;
}
}
&.btn__ghost {
@apply bg-transparent text-white/80 border-transparent hover:text-white;
}
}
</style>

View file

@ -1,36 +0,0 @@
<!--
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/>.
-->
<template>
<div class="button-group">
<slot />
</div>
</template>
<script setup>
defineProps({
variant: String,
})
</script>
<style scoped>
@reference "@/assets/main.css";
</style>

View file

@ -1,103 +0,0 @@
<!--
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/>.
-->
<template>
<div class="context-menu">
<div class="context-menu__trigger" @click="toggle">
<slot name="trigger" />
</div>
<div :class="`context-menu__content ${menuOpen ? 'open' : ''}`">
<slot name="content" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUpdated } from 'vue'
defineExpose({ toggle })
const props = defineProps({
open: Boolean,
})
const menuOpen = ref(false)
onMounted(() => {
menuOpen.value = props.open
})
function toggle() {
menuOpen.value = !menuOpen.value
}
</script>
<style>
@reference "@/assets/main.css";
.context-menu {
@apply relative;
.context-menu__content {
@apply absolute
top-full
-translate-y-full
opacity-0
pointer-events-none
mt-2
min-w-full
grid
border
border-white/50
bg-slate-100/60
backdrop-blur-3xl
text-slate-800
rounded-md
z-50
transition-all;
&.open {
@apply translate-y-0
opacity-100
pointer-events-auto;
}
}
}
.context-menu ul {
@apply text-slate-800
divide-y
divide-slate-300;
li {
@apply flex
gap-2
items-center
p-2
hover:bg-black/10
cursor-pointer;
svg {
@apply size-5;
}
}
}
</style>

View file

@ -1,132 +0,0 @@
<!--
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/>.
-->
<template>
<div class="dialog-container">
<div class="trigger" @click="toggleDialog(true)">
<slot name="trigger" />
</div>
<dialog ref="dialog" class="mcrm-block block__dark">
<ButtonComp
class="dialog__close p-0"
variant="ghost"
size="sm"
tabindex="-1"
@click="toggleDialog(false)"
>
<IconX />
</ButtonComp>
<slot name="content" />
</dialog>
</div>
</template>
<script setup>
import ButtonComp from './ButtonComp.vue'
import { IconX } from '@tabler/icons-vue'
import { onMounted, onUpdated, ref } from 'vue'
const dialog = ref(null)
const openDialog = ref()
const emit = defineEmits(['onOpen', 'onClose', 'onToggle'])
defineExpose({ toggleDialog })
const props = defineProps({
open: Boolean,
})
onMounted(() => {
if (props.open === true) toggleDialog(props.open)
})
onUpdated(() => {
if (props.open === true) toggleDialog(props.open)
})
function toggleDialog(openToggle) {
if (openToggle) {
dialog.value.showModal()
emit('onOpen')
} else {
dialog.value.close()
emit('onClose')
}
openDialog.value = openToggle
emit('onToggle')
}
onMounted(() => {
openDialog.value = props.open
if (dialog.value.innerHTML.includes('form')) {
dialog.value.querySelector('form').addEventListener('submit', () => {
toggleDialog()
})
}
})
</script>
<style>
@reference "@/assets/main.css";
.dialog-container {
@apply relative;
dialog {
@apply fixed
top-1/2 left-1/2
-translate-x-1/2 -translate-y-1/2
max-w-[calc(100vw-2rem)]
text-slate-200
/* shadow-md */
/* shadow-black */
z-50
pointer-events-none;
&[open] {
@apply pointer-events-auto;
}
&::backdrop {
@apply bg-black/50 backdrop-blur-xs transition;
}
.dialog__close {
@apply absolute
top-4 right-4
p-0
text-white;
svg {
@apply size-5;
}
}
}
}
.dialog__content {
> *:first-child {
@apply pr-8;
}
}
</style>

View file

@ -1,59 +0,0 @@
<!--
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/>.
-->
<template>
<div v-if="loading" class="loading-component">
<span v-if="text">
{{ text }}
</span>
<IconLoader3 class="duration-1000 animate-spin" />
</div>
</template>
<script setup>
import { IconLoader3 } from '@tabler/icons-vue'
defineProps({
loading: Boolean,
text: String,
})
</script>
<style>
@reference "@/assets/main.css";
&:has(.loading-component) {
@apply relative;
}
.loading-component {
@apply absolute
inset-0
size-full
flex gap-2
flex-col
justify-center
items-center
text-sm
bg-black/50
backdrop-blur-md;
}
</style>

View file

@ -1,156 +0,0 @@
<!--
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/>.
-->
<template>
<nav id="main-menu">
<button id="menu-toggle" :class="menuOpen ? 'open' : ''" @click="menuOpen = !menuOpen">
<img
class="p-1 logo"
:class="{ 'opacity-0': menuOpen }"
src="@/assets/img/Macrame-Logo-gradient.svg"
aria-hidden="true"
/>
<IconX :class="{ 'opacity-0': !menuOpen }" />
</button>
<ul :class="menuOpen ? 'open' : ''">
<li>
<RouterLink @click="menuOpen = false" to="/"> <IconHome />Dashboard </RouterLink>
</li>
<li>
<RouterLink @click="menuOpen = false" to="/panels"> <IconLayoutGrid />Panels </RouterLink>
</li>
<li v-if="isLocal()">
<RouterLink @click="menuOpen = false" to="/macros"> <IconKeyboard />Macros </RouterLink>
</li>
<li>
<RouterLink @click="menuOpen = false" to="/devices">
<IconDevices />{{ isLocal() ? 'Devices' : 'Server' }}
</RouterLink>
</li>
<!-- <li>
<RouterLink @click="menuOpen = false" to="/settings">
<IconSettings />Settings
</RouterLink>
</li> -->
</ul>
</nav>
</template>
<script setup>
import { RouterLink } from 'vue-router'
import {
IconDevices,
IconHome,
IconKeyboard,
IconLayoutGrid,
IconSettings,
IconX,
} from '@tabler/icons-vue'
import { ref } from 'vue'
import { isLocal } from '@/services/ApiService'
const menuOpen = ref(false)
</script>
<style>
@reference "@/assets/main.css";
nav {
@apply relative flex z-50;
button {
@apply absolute
top-4 left-4
size-12
rounded-full
aspect-square
bg-white/20 hover:bg-white/40
border-0
cursor-pointer
transition-colors
backdrop-blur-md;
.logo,
svg {
@apply absolute
inset-1/2
-translate-1/2
transition-opacity
duration-400
ease-in-out;
}
.logo {
@apply w-full;
}
}
ul {
@apply absolute
top-20 left-0
-translate-x-full
grid
list-none
rounded-xl
overflow-hidden
bg-white/10
backdrop-blur-md
divide-y
divide-slate-600
transition-transform
duration-300
ease-in-out;
&.open {
@apply left-4 translate-x-0;
}
li {
a {
@apply flex
items-center
gap-2
px-4 py-2
text-white
no-underline
border-transparent
transition-colors;
svg {
@apply text-white/40 transition-colors;
}
&:hover {
@apply bg-white/20;
svg {
@apply text-white;
}
}
&.router-link-active {
@apply text-sky-300
bg-sky-200/20;
}
}
}
}
}
</style>

View file

@ -1,107 +0,0 @@
<!--
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/>.
-->
<template>
<div id="remote-dashboard">
<div id="panels" class="dashboard-block mcrm-block block__light" v-if="server.handshake">
<div class="icon__container">
<IconLayoutGrid />
</div>
<h4>{{ server.panelCount }} {{ server.panelCount != 1 ? 'Panels' : 'Panel' }}</h4>
<template v-if="server.panelCount == 0">
<p><em>No panels found. </em></p>
<p>Learn how to create a panel <a href="#" target="_blank">here</a>.</p>
</template>
<template v-else>
<p>Start using a panel!</p>
<ButtonComp variant="danger" href="/panels"> <IconLayoutGrid /> View panels </ButtonComp>
</template>
</div>
<div id="server" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconServer />
</div>
<h4>Server</h4>
<template v-if="server.handshake">
<p>
Linked with: <strong class="text-center">{{ server.ip }}</strong>
</p>
<ButtonComp variant="primary" href="/devices"> <IconServer /> View server</ButtonComp>
</template>
<template v-else>
<p>
<em>Not linked</em>
</p>
<ButtonComp variant="primary" href="/devices"> <IconLink /> Link with server</ButtonComp>
</template>
</div>
</div>
</template>
<script setup>
import { IconLayoutGrid, IconLink, IconServer } from '@tabler/icons-vue'
import { onMounted, reactive } from 'vue'
import ButtonComp from '../base/ButtonComp.vue'
import { useDeviceStore } from '@/stores/device'
import { usePanelStore } from '@/stores/panel'
const device = useDeviceStore()
const panel = usePanelStore()
const server = reactive({
ip: '',
handshake: '',
panelCount: 0,
})
onMounted(async () => {
const serverIp = await device.serverGetIP()
server.ip = serverIp
if (device.key()) server.handshake = true
device.$subscribe(() => {
if (device.key()) server.handshake = true
})
const panelCount = await panel.getList(true)
server.panelCount = panelCount
})
</script>
<style scoped>
@reference "@/assets/main.css";
#remote-dashboard {
@apply grid
pt-8
gap-4
md:w-fit
h-fit
content-start;
&.not__linked #server {
@apply row-start-1 md:col-start-1;
}
}
</style>

View file

@ -1,154 +0,0 @@
<!--
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/>.
-->
<template>
<div
id="server-dashboard"
:class="`${server.remoteCount == 0 ? 'no__devices' : 'devices__found'} ${server.macroCount == 0 ? 'no__macros' : 'macros__found'}`"
>
<div id="devices" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconDevices />
</div>
<h4>{{ server.remoteCount }} {{ server.remoteCount != 1 ? 'Devices' : 'Device' }}</h4>
<template v-if="server.remoteCount == 0">
<p><em>No devices found.</em></p>
<ButtonComp variant="primary" href="/devices"> <IconLink /> Link a device</ButtonComp>
</template>
<template v-else>
<p>Unlink a device or add new devices.</p>
<ButtonComp variant="primary" href="/devices"><IconDevices /> View devices</ButtonComp>
</template>
</div>
<div id="macros" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconKeyboard />
</div>
<h4>{{ server.macroCount }} {{ server.macroCount != 1 ? 'Macros' : 'Macro' }}</h4>
<template v-if="server.macroCount == 0">
<p><em>No macros found.</em></p>
<ButtonComp variant="secondary" href="/macros"> <IconLayoutGrid /> Create macro</ButtonComp>
</template>
<template v-else>
<p>Edit and view your macros.</p>
<ButtonComp variant="secondary" href="/macros"><IconKeyboard /> View macros</ButtonComp>
</template>
</div>
<div id="panels" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconLayoutGrid />
</div>
<h4>{{ server.panelCount }} {{ server.panelCount != 1 ? 'Panels' : 'Panel' }}</h4>
<template v-if="server.panelCount == 0">
<p><em>No panels found. </em></p>
<p>Learn how to create a panel <a href="#" target="_blank">here</a>.</p>
</template>
<template v-else>
<p>Link macros to panels or view a panel.</p>
<ButtonComp variant="danger" href="/panels"> <IconLayoutGrid /> View panels </ButtonComp>
</template>
</div>
</div>
</template>
<script setup>
import { useDeviceStore } from '@/stores/device'
import { usePanelStore } from '@/stores/panel'
import { IconDevices, IconKeyboard, IconLayoutGrid, IconLink } from '@tabler/icons-vue'
import { onMounted, reactive } from 'vue'
import ButtonComp from '../base/ButtonComp.vue'
import { GetMacroList } from '@/services/MacroService'
const device = useDeviceStore()
const panel = usePanelStore()
const server = reactive({
ip: '',
port: '',
fullPath: '',
remoteCount: 0,
macroCount: 0,
panelCount: 0,
})
onMounted(async () => {
const serverIP = await device.serverGetIP()
server.ip = serverIP
// server.port = window.__CONFIG__.MCRM__PORT
// server.fullPath = `http://${server.ip}:${server.port}`
const remoteCount = await device.serverGetRemotes(true)
server.remoteCount = remoteCount
const macroCount = await GetMacroList(true)
server.macroCount = macroCount
const panelCount = await panel.getList(true)
server.panelCount = panelCount
console.log(server)
})
</script>
<style scoped>
@reference "@/assets/main.css";
#server-dashboard {
@apply grid
grid-cols-1
grid-rows-3
md:grid-cols-3
md:grid-rows-1
gap-4
w-fit
h-fit
pt-8;
&.no__devices #devices {
@apply row-start-1 md:col-start-1;
}
&.no__macros.devices__found #devices {
@apply row-start-3 md:col-start-3;
}
&.devices__found #devices {
@apply row-start-3 md:col-start-3;
}
&.no__devices.no__macros #macros {
@apply row-start-2 md:col-start-2;
}
&.no__macros #macros {
@apply row-start-1 md:col-start-1;
}
&.macros__found #macros {
@apply row-start-2 md:col-start-2;
}
&.no__devices.macros__found #macros {
@apply row-start-3 md:col-start-3;
}
}
</style>

View file

@ -1,225 +0,0 @@
<!--
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/>.
-->
<template>
<div class="server-overview">
<AlertComp variant="info">
<strong>This is a remote device.</strong>
<em>UUID: {{ device.uuid() }} </em>
</AlertComp>
<div class="grid gap-4 mcrm-block block__light">
<h4 class="flex items-center justify-between gap-4 text-lg">
<span class="flex gap-4"><IconServer />Server</span>
<ButtonComp variant="primary" @click="checkServerStatus()"><IconReload /></ButtonComp>
</h4>
<p>
Connected to: <strong>{{ server.host }}</strong>
</p>
<!-- Alerts -->
<AlertComp v-if="server.status === 'authorized'" variant="success">Authorized</AlertComp>
<AlertComp v-if="server.status === 'unlinked'" variant="warning">Not linked</AlertComp>
<AlertComp v-if="server.status === 'unauthorized'" variant="info">
<div class="grid gap-2">
<strong>Access requested</strong>
<ul class="mb-4">
<li>
Navigate to <em class="font-semibold">http://localhost:{{ server.port }}/devices</em>.
</li>
<li>
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
Click on
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
<IconLink class="size-4" /> Link device
</span>
</div>
</li>
<li>Enter the the pin shown on the desktop in the dialog that will appear.</li>
</ul>
<template v-if="server.link == 'checking'">
<div class="grid grid-cols-[2rem_1fr] gap-2">
<IconReload class="animate-spin" />
Checking server for link...
</div>
</template>
<template v-if="server.link === false">
<ButtonComp variant="subtle" @click="pingLink()" class="w-fit">
<IconReload />Check for server link
</ButtonComp>
</template>
</div>
</AlertComp>
<ButtonComp
variant="danger"
v-if="server.status === 'authorized'"
@click="disonnectFromServer()"
>
<IconPlugConnectedX />
Disconnect
</ButtonComp>
</div>
<DialogComp ref="linkPinDialog">
<template #content>
<div class="grid w-64 gap-4">
<h3>Server link pin:</h3>
<form class="grid gap-4" @submit.prevent="decryptKey()">
<input
ref="linkPinInput"
class="input"
id="input-pin"
type="text"
pattern="[0-9]{4}"
v-model="server.inputPin"
autocomplete="off"
/>
<ButtonComp variant="primary">Enter</ButtonComp>
</form>
</div>
</template>
</DialogComp>
</div>
</template>
<script setup>
// TODO
// - [Delete local key button]
// - if not local key
// - - if !checkAccess -> requestAccess -> put settings.json (go)
// - - if checkAccess -> pingLink -> check for device.tmp (go)
// - - if [devicePin] -> handshake -> save key local, close dialog, update server status
import { IconKey, IconLink, IconPlugConnectedX, IconReload, IconServer } from '@tabler/icons-vue'
import AlertComp from '../base/AlertComp.vue'
import ButtonComp from '../base/ButtonComp.vue'
import { onMounted, onUpdated, reactive, ref } from 'vue'
import { useDeviceStore } from '@/stores/device'
import { deviceType, deviceModel, deviceVendor } from '@basitcodeenv/vue3-device-detect'
import DialogComp from '../base/DialogComp.vue'
import { AuthCall, decryptAES } from '@/services/EncryptService'
import axios from 'axios'
import { appUrl } from '@/services/ApiService'
const device = useDeviceStore()
const linkPinDialog = ref()
const linkPinInput = ref()
const server = reactive({
host: '',
port: window.__CONFIG__.MCRM__PORT,
status: false,
link: false,
inputPin: '',
encryptedKey: '',
key: '',
})
onMounted(async () => {
server.host = window.location.host
})
onUpdated(() => {
if (!server.status) checkServerStatus()
if (server.status === 'authorized' && server.inputPin) server.inputPin = ''
})
async function checkServerStatus(request = true) {
const status = await device.remoteCheckServerAccess()
server.status = status
if (status === 'unlinked' || status === 'unauthorized') {
if (request) requestAccess()
return true
}
if (!device.key()) {
server.status = 'unauthorized'
return true
}
const handshake = await device.remoteHandshake(device.key())
if (handshake) server.key = device.key()
else {
device.removeDeviceKey()
server.status = 'unlinked'
if (request) requestAccess()
}
return true
}
function requestAccess() {
let deviceName = `${deviceVendor() ? deviceVendor() : 'Unknown'} ${deviceVendor() ? deviceModel() : deviceType()}`
device.remoteRequestServerAccess(deviceName, deviceType()).then((data) => {
if (data.data) (server.status = data.data), pingLink()
})
}
function pingLink() {
server.link = 'checking'
device.remotePingLink((encryptedKey) => {
server.link = true
server.encryptedKey = encryptedKey
linkPinDialog.value.toggleDialog(true)
linkPinInput.value.focus()
})
}
async function decryptKey() {
const decryptedKey = decryptAES(server.inputPin, server.encryptedKey)
const handshake = await device.remoteHandshake(decryptedKey)
if (handshake) {
device.setDeviceKey(decryptedKey)
server.key = decryptedKey
linkPinDialog.value.toggleDialog(false)
server.status = 'authorized'
}
}
function disonnectFromServer() {
axios.post(appUrl() + '/device/link/remove', AuthCall({ uuid: device.uuid() })).then((data) => {
if (data.data) checkServerStatus(false)
})
}
</script>
<style scoped>
@reference "@/assets/main.css";
.server-overview {
@apply grid
gap-4
content-start;
}
#input-pin {
}
</style>

View file

@ -1,254 +0,0 @@
<!--
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/>.
-->
<template>
<div class="device-overview">
<AlertComp variant="info">
<strong>This is a server!</strong>
<em>UUID: {{ device.uuid() }} </em>
</AlertComp>
<div class="flex flex-wrap items-start gap-4 mcrm-block block__light">
<h4 class="flex items-center justify-between w-full gap-4 mb-4">
<span class="flex gap-4">
<IconDevices />{{ Object.keys(remote.devices).length }}
{{ Object.keys(remote.devices).length == 1 ? 'Device' : 'Devices' }}
</span>
<ButtonComp v-if="!remote.poll" variant="primary" @click="device.serverGetRemotes()"
><IconReload
/></ButtonComp>
</h4>
<template v-if="Object.keys(remote.devices).length > 0">
<template v-for="(remoteDevice, id) in remote.devices" :key="id">
<div class="mcrm-block block__dark block-size__sm w-64 grid !gap-4 content-start">
<div class="grid gap-2">
<h5 class="grid grid-cols-[auto_1fr] gap-2">
<IconDeviceUnknown v-if="remoteDevice.settings.type == 'unknown'" />
<IconDeviceMobile v-if="remoteDevice.settings.type == 'mobile'" />
<IconDeviceTablet v-if="remoteDevice.settings.type == 'tablet'" />
<IconDeviceDesktop v-if="remoteDevice.settings.type == 'desktop'" />
<span class="w-full truncate">
{{ remoteDevice.settings.name }}
</span>
</h5>
<em>{{ id }}</em>
</div>
<template v-if="remoteDevice.key">
<AlertComp variant="success">Authorized</AlertComp>
<ButtonComp variant="danger" @click="unlinkDevice(id)">
<IconLinkOff />Unlink device
</ButtonComp>
</template>
<template v-else>
<AlertComp variant="warning">Unauthorized</AlertComp>
<ButtonComp variant="primary" @click="startLink(id)">
<IconLink />Link device
</ButtonComp>
</template>
<template v-if="remote.pinlink.uuid == id">
<AlertComp variant="info">One time pin: {{ remote.pinlink.pin }}</AlertComp>
</template>
</div>
</template>
</template>
<!-- <template v-else>
<div class="grid w-full gap-4">
<em class="text-slate-300">No remote devices</em>
</div>
</template> -->
<AccordionComp
class="w-full mt-8 border-t border-t-white/50"
title="How to connect a device?"
:open="Object.keys(remote.devices).length == 0"
>
<div class="grid py-4">
<ul class="space-y-2">
<li>
Scan the QR code with the remote device.
<div class="grid gap-4 py-4 pl-6">
<canvas ref="serverQr"></canvas>
<p>
Or manually type the IP address: <br />
<strong>{{ server.ip }}/devices</strong>
</p>
</div>
</li>
<li>
The device will automatically request access, if you see "Access requested" on the
device.
</li>
<li v-if="!remote.poll">
<div class="inline-flex items-center gap-2">
Click the
<span class="p-1 border rounded-sm"><IconReload class="size-4" /></span> to reload
the devices.
</div>
</li>
<li>
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
Click on
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
<IconLink class="size-4" /> Link device
</span>
A one-time-pin will be shown in a dialog.
</div>
</li>
<li>Enter the pin on the remote device.</li>
<li>
Congratulations! You have linked a device! You can now start using panels on that
device.
</li>
</ul>
</div>
</AccordionComp>
<DialogComp ref="pinDialog">
<template #content>
<div class="grid gap-4">
<h3>Pin code</h3>
<span class="font-mono text-4xl tracking-wide">{{ remote.pinlink.pin }}</span>
</div>
</template>
</DialogComp>
</div>
</div>
</template>
<script setup>
import { onMounted, onUpdated, reactive, ref } from 'vue'
import AlertComp from '../base/AlertComp.vue'
import { useDeviceStore } from '@/stores/device'
import {
IconDevices,
IconDeviceDesktop,
IconDeviceMobile,
IconDeviceTablet,
IconDeviceUnknown,
IconLink,
IconLinkOff,
IconReload,
} from '@tabler/icons-vue'
import ButtonComp from '../base/ButtonComp.vue'
import DialogComp from '../base/DialogComp.vue'
import axios from 'axios'
import { appUrl } from '@/services/ApiService'
import AccordionComp from '../base/AccordionComp.vue'
import QRCode from 'qrcode'
const device = useDeviceStore()
const pinDialog = ref()
const serverQr = ref()
const server = reactive({
ip: '',
})
const remote = reactive({ devices: [], pinlink: false, poll: false })
onMounted(async () => {
device.serverGetRemotes()
device.$subscribe((mutation, state) => {
if (state.remote !== remote.devices) remote.devices = device.remote
})
getIp()
})
onUpdated(() => {
getIp()
if (Object.keys(remote.devices).length == 0 && !remote.poll) {
remote.poll = setInterval(() => {
device.serverGetRemotes()
}, 1000)
}
if (Object.keys(remote.devices).length > 0 && remote.poll) {
clearInterval(remote.poll)
remote.poll = false
}
})
async function getIp() {
const serverIP = await device.serverGetIP()
server.ip = serverIP
QRCode.toCanvas(serverQr.value, `${server.ip}/devices`, (error) => {
if (error) console.log('QRCode error: ', error)
})
}
async function startLink(deviceUuid) {
const pin = await device.serverStartLink(deviceUuid)
remote.pinlink = { uuid: deviceUuid, pin: pin }
pinDialog.value.toggleDialog(true)
pollLink()
setTimeout(() => {
resetPinLink()
}, 60000)
}
function pollLink() {
const pollInterval = setInterval(() => {
axios.post(appUrl() + '/device/link/poll', { uuid: remote.pinlink.uuid }).then((data) => {
if (!data.data) {
clearInterval(pollInterval)
resetPinLink()
device.serverGetRemotes()
}
})
}, 1000)
}
function resetPinLink() {
remote.pinlink = false
if (pinDialog.value) pinDialog.value.toggleDialog(false)
}
function unlinkDevice(id) {
axios.post(appUrl() + '/device/link/remove', { uuid: id }).then((data) => {
if (data.data) {
device.serverGetRemotes()
}
})
}
</script>
<style scoped>
@reference "@/assets/main.css";
.device-overview {
@apply grid
gap-4
content-start;
}
</style>

View file

@ -1,176 +0,0 @@
<!--
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/>.
-->
<template>
<div class="input-group form-select">
<label v-if="label">
{{ label }}
</label>
<div class="select__container">
<template v-if="search">
<div class="select__search-bar">
<input
type="search"
ref="selectSearch"
:list="`${name}-search__options`"
v-model="select.search"
@change="selectSearchValue($event)"
:disabled="!select.searchActive"
autocomplete="on"
/>
<datalist :id="`${name}-search__options`">
<option v-for="option in select.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</datalist>
<ButtonComp v-if="!select.searchActive" variant="ghost" size="sm" @click="initSearch">
<IconSearch />
</ButtonComp>
<ButtonComp v-else variant="ghost" size="sm" @click="resetSearch">
<IconSearchOff />
</ButtonComp>
</div>
</template>
<select :name="name" ref="selectEl" v-model="select.value" @change="changeSelect($event)">
<option value="" disabled>- Select {{ label.toLocaleLowerCase() }} -</option>
<option v-for="option in select.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
</template>
<script setup>
import { IconSearch, IconSearchOff } from '@tabler/icons-vue'
import { onMounted, onUpdated, reactive, ref } from 'vue'
import ButtonComp from '../base/ButtonComp.vue'
const emit = defineEmits(['change'])
const props = defineProps({
label: String,
name: String,
options: [Array, Object],
search: Boolean,
value: String,
})
const select = reactive({
options: [],
search: '',
searchActive: false,
changed: false,
value: '',
})
const selectEl = ref(null)
const selectSearch = ref(null)
onMounted(() => {
setValue()
if (typeof props.options == 'object') select.options = Object.values(props.options)
})
onUpdated(() => {
setValue()
})
const setValue = () => {
if ((select.value == '' && props.value) || (!select.changed && props.value != select.value)) {
select.value = props.value
}
select.changed = false
}
const initSearch = () => {
select.searchActive = true
select.search = ''
selectEl.value.classList = 'search__is-active'
setTimeout(() => {
selectSearch.value.focus()
}, 50)
}
const resetSearch = () => {
select.search = ''
select.searchActive = false
selectEl.value.classList = ''
}
const selectSearchValue = (event) => {
changeSelect(event)
resetSearch()
}
const changeSelect = (event) => {
select.changed = true
select.value = event.target.value
emit('change', select.value)
}
</script>
<style scoped>
@reference "@/assets/main.css";
.select__container {
@apply relative
h-8;
select,
.select__search-bar {
@apply absolute top-0 h-8;
}
}
.select__search-bar {
@apply right-0
grid
grid-cols-[1fr_auto]
items-center
w-full
pr-4
z-10
pointer-events-none;
button {
@apply pointer-events-auto;
}
input {
@apply border-0 bg-transparent pointer-events-auto px-2 py-0 focus:outline-0;
&[disabled] {
@apply pointer-events-none;
}
}
datalist {
@apply absolute
top-full left-0;
}
}
select.search__is-active {
@apply text-transparent;
}
</style>

View file

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View file

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View file

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View file

@ -1,7 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View file

@ -1,19 +0,0 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

View file

@ -1,164 +0,0 @@
<!--
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/>.
-->
<template>
<div class="macro-overview mcrm-block block__dark">
<h4 class="border-b-2 border-transparent">Saved Macros</h4>
<div class="macro-overview__list">
<LoadComp :loading="macros.loading" text="Loading macros..." />
<div class="macro-item" v-for="(macro, i) in macros.list" :key="i">
<ButtonComp
:variant="macroRecorder.macroName === macro.name ? 'secondary' : 'dark'"
class="overview__macro-open"
size="sm"
@click="macroRecorder.openMacro(macro.macroname, macro.name)"
>
<IconKeyboard /> <span>{{ macro.name }}</span>
</ButtonComp>
<div class="overview__macro-delete">
<ButtonComp
class="!text-red-500 hover:!text-red-300"
variant="ghost"
size="sm"
@click="startDelete(macro.name)"
>
<IconTrash />
</ButtonComp>
</div>
</div>
</div>
<DialogComp ref="deleteDialog">
<template #content>
<div class="grid gap-2">
<h4 class="pr-4">Are you sure you want to delete:</h4>
<h3 class="mb-2 text-center text-sky-500">{{ macroToBeDeleted }}</h3>
<div class="flex justify-between">
<ButtonComp size="sm" variant="subtle" @click="deleteDialog.toggleDialog(false)">
No
</ButtonComp>
<ButtonComp size="sm" variant="danger" @click="deleteMacro()">Yes</ButtonComp>
</div>
</div>
</template>
</DialogComp>
</div>
</template>
<script setup>
// TODO
// - delete macro
import { IconKeyboard, IconTrash } from '@tabler/icons-vue'
import ButtonComp from '../base/ButtonComp.vue'
import { onMounted, reactive, ref } from 'vue'
import { GetMacroList } from '@/services/MacroService'
import LoadComp from '../base/LoadComp.vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import DialogComp from '../base/DialogComp.vue'
const macros = reactive({
loading: true,
list: [],
})
const macroRecorder = useMacroRecorderStore()
const macroToBeDeleted = ref('')
const deleteDialog = ref()
onMounted(() => {
loadMacroList()
})
const loadMacroList = async () => {
const list = await GetMacroList()
macros.list = list
macros.loading = false
}
const startDelete = (macroFilename) => {
macroToBeDeleted.value = macroFilename
deleteDialog.value.toggleDialog(true)
}
const deleteMacro = async () => {
const resp = await macroRecorder.deleteMacro(macroToBeDeleted.value)
if (resp) {
deleteDialog.value.toggleDialog(false)
if (macroToBeDeleted.value === macroRecorder.macroName) macroRecorder.resetMacro()
macroToBeDeleted.value = ''
loadMacroList()
}
}
</script>
<style scoped>
@reference "@/assets/main.css";
.macro-overview {
@apply relative
grid
grid-rows-[auto_1fr];
&::after {
@apply content-['']
absolute
top-0
left-full
h-full
w-px
bg-slate-600;
}
.macro-overview__list {
@apply flex
flex-col
pr-1
-mr-1
gap-1
h-[calc(100vh-11.7rem)]
overflow-auto;
}
.macro-item {
@apply grid items-center grid-cols-[1fr_0fr] transition-[grid-template-columns] delay-0 duration-300;
&:hover {
@apply grid-cols-[1fr_auto] delay-500;
}
button.overview__macro-open {
@apply w-full grid grid-cols-[1rem_1fr] justify-items-start;
span {
@apply truncate w-full text-left;
}
}
div.overview__macro-delete {
@apply grid overflow-hidden transition;
}
}
}
</style>

View file

@ -1,106 +0,0 @@
<!--
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/>.
-->
<template>
<div class="macro-recorder mcrm-block block__light">
<div class="recorder-interface">
<!-- Recorder buttons -->
<RecorderHeader />
<!-- Recorder interface container -->
<div
:class="`recorder-interface__container ${macroRecorder.state.record && 'record'} ${macroRecorder.state.edit && 'edit'}`"
>
<!-- Shows the macro steps as kbd elements with delay and spacers-->
<RecorderOutput />
<!-- Input for recording macro steps -->
<RecorderInput />
</div>
<RecorderFooter />
</div>
</div>
</template>
<script setup>
import RecorderOutput from './parts/RecorderOutput.vue'
import RecorderInput from './parts/RecorderInput.vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import RecorderHeader from './parts/RecorderHeader.vue'
import RecorderFooter from './parts/RecorderFooter.vue'
const macroRecorder = useMacroRecorderStore()
</script>
<style>
@reference "@/assets/main.css";
.macro-recorder {
@apply h-full;
}
.recorder-interface {
@apply grid
grid-rows-[auto_1fr_auto]
gap-4
h-full
transition-[grid-template-rows];
}
.recorder-interface__container {
@apply relative
w-full
rounded-lg
bg-slate-950/50
border
border-slate-600
overflow-auto
transition-colors;
&.record {
@apply border-rose-300 bg-rose-400/10;
}
&.edit {
@apply border-sky-300 bg-sky-900/10;
}
}
#macro-name {
@apply w-full
bg-transparent
py-0
outline-0
border-transparent
border-b-slate-300
focus:border-transparent
focus:border-b-sky-400
focus:bg-sky-400/10
transition-colors
text-lg
rounded-none;
}
.disabled {
@apply opacity-50 pointer-events-none cursor-not-allowed;
}
</style>

View file

@ -1,78 +0,0 @@
<!--
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/>.
-->
<template>
<span :class="`delay ${active ? 'active' : ''} ${preset ? 'preset' : ''}`">
<template v-if="value < 10000"> {{ value }} <i>ms</i> </template>
<template v-else> >10 <i>s</i> </template>
</span>
</template>
<script setup>
import { IconTimeDuration10 } from '@tabler/icons-vue'
defineProps({
value: Number,
active: Boolean,
preset: Boolean,
})
</script>
<style scoped>
@reference "@/assets/main.css";
span.delay {
@apply flex
items-center
px-2 py-1
bg-slate-500
border
border-slate-400
text-slate-950
font-sans
font-semibold
rounded-sm
text-sm
cursor-default;
&.preset {
@apply text-amber-400
border-amber-300/80
bg-amber-100/60;
}
i {
@apply pl-1
font-normal
not-italic
opacity-80;
}
}
.edit span.delay {
@apply cursor-pointer;
&:hover,
&.active {
@apply bg-lime-700 border-lime-500 text-lime-200;
}
}
</style>

View file

@ -1,56 +0,0 @@
<!--
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/>.
-->
<template>
<div id="delete-key-dialog" class="dialog__content">
<h4 class="mb-4 text-slate-50">Delete key</h4>
<div class="flex justify-center w-full mb-4">
<MacroKey v-if="keyObj" :key-obj="keyObj" />
</div>
<p class="text-sm text-slate-300">Are you sure you want to delete this key?</p>
<div class="flex justify-end gap-2 mt-6">
<ButtonComp variant="danger" size="sm" @click="macroRecorder.deleteEditKey()">
Delete key
</ButtonComp>
</div>
</div>
</template>
<script setup>
import ButtonComp from '@/components/base/ButtonComp.vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import { onMounted, ref } from 'vue'
import MacroKey from './MacroKey.vue'
import { filterKey } from '@/services/MacroRecordService'
const macroRecorder = useMacroRecorderStore()
const keyObj = ref(null)
onMounted(() => {
keyObj.value = filterKey(macroRecorder.getEditKey())
})
</script>
<style scoped>
@reference "@/assets/main.css";
</style>
'

View file

@ -1,77 +0,0 @@
<!--
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/>.
-->
<template>
<div id="edit-delay-dialog" class="dialog__content">
<h4 class="mb-4 text-slate-50">Edit delay</h4>
<div v-if="editable.delay.value" class="flex justify-center">
<DelaySpan class="!text-lg" :value="editable.delay.value" />
</div>
<form class="grid gap-4 mt-6" submit.prevent>
<div v-if="editable.newDelay.value">
<input
type="number"
min="0"
max="3600000"
step="10"
v-model="editable.newDelay.value"
autofocus
/>
<span>ms</span>
</div>
<div class="flex justify-end">
<ButtonComp variant="primary" size="sm" @click.prevent="changeDelay()">
Change delay
</ButtonComp>
</div>
</form>
</div>
</template>
<script setup>
import ButtonComp from '@/components/base/ButtonComp.vue'
import { reactive, onMounted } from 'vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import DelaySpan from './DelaySpan.vue'
const macroRecorder = useMacroRecorderStore()
const editable = reactive({
delay: {},
newDelay: { value: 0 },
})
onMounted(() => {
editable.delay = macroRecorder.getEditDelay()
editable.newDelay.value = editable.delay.value
})
const changeDelay = () => {
if (!editable.newDelay.value) return
macroRecorder.recordStep(editable.newDelay.value, false, macroRecorder.state.editDelay)
macroRecorder.state.editDelay = false
}
</script>
<style scoped>
@reference "@/assets/main.css";
</style>

View file

@ -1,124 +0,0 @@
<!--
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/>.
-->
<template>
<div id="edit-key-dialog" class="dialog__content">
<h4 class="text-slate-50 mb-4">Press a key</h4>
<div class="flex justify-center" @click="$refs.newKeyInput.focus()">
<MacroKey
v-if="editable.key.keyObj"
:key-obj="editable.key.keyObj"
:direction="editable.key.direction"
/>
<template v-if="typeof editable.newKey.keyObj === 'object'">
<span class="px-4 flex items-center text-white"> >>> </span>
<MacroKey :key-obj="editable.newKey.keyObj" :direction="editable.newKey.direction" />
</template>
</div>
<form class="grid gap-4" submit.prevent>
<input
class="size-0 opacity-0"
type="text"
min="0"
max="1"
ref="newKeyInput"
placeholder="New key"
autofocus
@keydown.prevent="handleNewKey($event)"
/>
<div class="flex gap-2 justify-center">
<ButtonComp
variant="secondary"
:class="editable.newKey.direction === 'down' ? 'selected' : ''"
size="sm"
@click.prevent="handleNewDirection('down')"
>
&darr; Down
</ButtonComp>
<ButtonComp
variant="secondary"
:class="editable.newKey.direction === 'up' ? 'selected' : ''"
size="sm"
@click.prevent="handleNewDirection('up')"
>
&uarr; Up
</ButtonComp>
</div>
<div class="flex justify-end">
<ButtonComp variant="primary" size="sm" @click.prevent="changeKey()">
Change key
</ButtonComp>
</div>
</form>
</div>
</template>
<script setup>
import MacroKey from './MacroKey.vue'
import ButtonComp from '@/components/base/ButtonComp.vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import { filterKey } from '@/services/MacroRecordService'
import { reactive, ref, onMounted } from 'vue'
const editable = reactive({
key: {},
newKey: {},
})
const macroRecorder = useMacroRecorderStore()
const newKeyInput = ref(null)
onMounted(() => {
editable.key = macroRecorder.getEditKey()
editable.newKey.direction = editable.key.direction
})
const handleNewKey = (e) => {
editable.newKey.e = e
editable.newKey.keyObj = filterKey(e)
}
const handleNewDirection = (direction) => {
editable.newKey.direction = direction
editable.newKey.keyObj = filterKey(editable.key)
}
const changeKey = () => {
macroRecorder.recordStep(
editable.newKey.e,
editable.newKey.direction,
macroRecorder.state.editKey,
)
macroRecorder.state.editKey = false
}
</script>
<style scoped>
@reference "@/assets/main.css";
button.selected {
@apply ring-2 ring-offset-1 ring-sky-500 bg-sky-500;
}
</style>

View file

@ -1,90 +0,0 @@
<!--
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/>.
-->
<template>
<ContextMenu ref="ctxtMenu">
<template #trigger>
<ButtonComp variant="secondary" size="sm"> <IconTimeDuration15 />Fixed delay </ButtonComp>
</template>
<template #content>
<ul>
<li @click="changeDelay(0)">0ms</li>
<li @click="changeDelay(15)">15ms</li>
<li @click="changeDelay(50)">50ms</li>
<li @click="changeDelay(100)">100ms</li>
<li>
<DialogComp>
<template #trigger>
<span>Custom delay</span>
</template>
<template #content>
<h4 class="text-slate-50 mb-4">Custom delay</h4>
<form
class="grid gap-4 w-44"
@submit.prevent="changeDelay(parseInt($refs.customDelayInput.value))"
>
<div>
<input
type="number"
step="10"
min="0"
max="3600000"
ref="customDelayInput"
placeholder="100"
/>
<span>ms</span>
</div>
<div class="flex justify-end">
<ButtonComp variant="primary" size="sm">Set custom delay</ButtonComp>
</div>
</form>
</template>
</DialogComp>
</li>
</ul>
</template>
</ContextMenu>
</template>
<script setup>
import ContextMenu from '@/components/base/ContextMenu.vue'
import { IconTimeDuration15 } from '@tabler/icons-vue'
import ButtonComp from '@/components/base/ButtonComp.vue'
import DialogComp from '@/components/base/DialogComp.vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import { ref } from 'vue'
const macroRecorder = useMacroRecorderStore()
const ctxtMenu = ref()
function changeDelay(num) {
macroRecorder.changeDelay(num)
ctxtMenu.value.toggle()
}
</script>
<style scoped>
@reference "@/assets/main.css";
</style>

View file

@ -1,155 +0,0 @@
<!--
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/>.
-->
<template>
<div id="insert-key-dialog" class="dialog__content w-96">
<h4 class="text-slate-50 mb-4">Insert key {{ position }}</h4>
<p v-if="inputFocus" class="text-center">[Press a key]</p>
<input
class="size-0 opacity-0"
type="text"
min="0"
max="1"
ref="insertKeyInput"
placeholder="New key"
@focusin="inputFocus = true"
@focusout="inputFocus = false"
@keydown.prevent="handleInsertKey($event)"
autofocus
/>
<div class="insert-output" :class="position == 'before' ? 'flex-row-reverse' : ''">
<MacroKey v-if="keyObjs.selected" :key-obj="keyObjs.selected" />
<hr class="spacer" />
<DelaySpan :preset="true" :value="10" />
<hr class="spacer" />
<MacroKey
v-if="keyObjs.insert"
class="insert"
:key-obj="keyObjs.insert"
:direction="keyObjs.insertDirection"
@click="insertKeyInput.focus()"
/>
<MacroKey v-if="!keyObjs.insert" :empty="true" @click="insertKeyInput.focus()" />
<template v-if="keyObjs.adjacentDelay">
<hr class="spacer" />
<DelaySpan :value="keyObjs.adjacentDelay.value" />
</template>
<template v-if="keyObjs.adjacent">
<hr class="spacer" />
<MacroKey :key-obj="keyObjs.adjacent" />
</template>
</div>
<div class="insert-key__direction">
<ButtonComp
variant="secondary"
:class="keyObjs.insertDirection === 'down' ? 'selected' : ''"
size="sm"
@click.prevent="keyObjs.insertDirection = 'down'"
>
&darr; Down
</ButtonComp>
<ButtonComp
variant="secondary"
:class="keyObjs.insertDirection === 'up' ? 'selected' : ''"
size="sm"
@click.prevent="keyObjs.insertDirection = 'up'"
>
&uarr; Up
</ButtonComp>
</div>
<div class="flex justify-end">
<ButtonComp variant="primary" size="sm" @click="insertKey()">Insert key</ButtonComp>
</div>
</div>
</template>
<script setup>
import MacroKey from './MacroKey.vue'
import DelaySpan from './DelaySpan.vue'
import ButtonComp from '@/components/base/ButtonComp.vue'
import { onMounted, reactive, ref } from 'vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import { filterKey } from '@/services/MacroRecordService'
const props = defineProps({
position: String,
})
const macroRecorder = useMacroRecorderStore()
const keyObjs = reactive({
selected: null,
insert: null,
insertEvent: null,
insertDirection: 'down',
adjacent: null,
adjacentDelay: null,
adjacentDelayIndex: null,
})
const insertKeyInput = ref(null)
const inputFocus = ref(false)
onMounted(() => {
keyObjs.selected = filterKey(macroRecorder.getEditKey())
const adjacentKey = macroRecorder.getAdjacentKey(props.position, true)
if (adjacentKey) keyObjs.adjacent = filterKey(adjacentKey.key)
if (adjacentKey.delay) {
keyObjs.adjacentDelay = adjacentKey.delay
keyObjs.adjacentDelayIndex = adjacentKey.delayIndex
}
})
const handleInsertKey = (e) => {
keyObjs.insert = filterKey(e)
keyObjs.insertEvent = e
}
const insertKey = () => {
macroRecorder.insertKey(keyObjs.insertEvent, keyObjs.insertDirection, keyObjs.adjacentDelayIndex)
}
</script>
<style scoped>
@reference "@/assets/main.css";
.insert-output {
@apply flex
justify-center
items-center
w-full
mb-4;
}
.insert-key__direction {
@apply flex
justify-center
gap-2
mt-6;
}
button.selected {
@apply bg-sky-500
ring-2
ring-offset-1
ring-sky-500;
}
</style>

View file

@ -1,130 +0,0 @@
<!--
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/>.
-->
<template>
<kbd :class="`${active ? 'active' : ''} ${empty ? 'empty' : ''}`">
<template v-if="keyObj">
<sup v-if="keyObj.loc">
{{ keyObj.loc }}
</sup>
<span :innerHTML="keyObj.str" />
<span class="dir">{{ dir.value === 'down' ? '&darr;' : '&uarr;' }}</span>
</template>
<template v-else-if="empty">
<span>[ ]</span>
</template>
</kbd>
</template>
<script setup>
import { onMounted, onUpdated, reactive } from 'vue'
const props = defineProps({
keyObj: Object,
direction: String,
active: Boolean,
empty: Boolean,
})
const dir = reactive({
value: false,
})
onMounted(() => {
if (props.empty) return
setDirection()
})
onUpdated(() => {
setDirection()
})
const setDirection = () => {
if (props.direction) dir.value = props.direction
else dir.value = props.keyObj.direction
}
</script>
<style>
@reference "@/assets/main.css";
kbd {
@apply flex
items-center
gap-2
pl-4 pr-2 py-1
h-9
bg-slate-700
font-mono
font-bold
text-lg
text-white
whitespace-nowrap
uppercase
rounded-md
border
border-slate-500
transition-all
shadow-slate-500;
box-shadow: 0 0.2rem 0 0.2rem var(--tw-shadow-color);
&:has(sup) {
@apply pl-2;
}
sup {
@apply text-slate-200 text-xs font-light mt-1;
}
span.dir {
@apply text-slate-200 pl-1;
}
&.empty {
@apply pl-3 pr-3
bg-sky-400/50
border-sky-300
shadow-sky-600
tracking-widest
cursor-pointer;
}
&.insert {
@apply bg-yellow-500/50
border-yellow-300
shadow-yellow-600
cursor-pointer;
}
}
:has(kdb):not(.edit) kbd {
@apply pointer-events-none cursor-default;
}
.edit kbd {
@apply cursor-pointer pointer-events-auto;
&:hover,
&.active {
@apply bg-sky-900 border-sky-400 shadow-sky-700;
}
}
</style>

View file

@ -1,80 +0,0 @@
<!--
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/>.
-->
<template>
<div id="validation-error__dialog" class="dialog__content">
<h4 class="mb-4 text-slate-50">There's an error in your macro</h4>
<div class="grid gap-4" v-if="(errors && errors.up.length > 0) || errors.down.length > 0">
<div v-if="errors.down.length > 0">
<p>
The following keys have been <strong>pressed</strong> down, but
<strong>not released</strong>.
</p>
<ul>
<li v-for="key in errors.down" :key="key">{{ key.toUpperCase() }}</li>
</ul>
</div>
<div v-if="errors.up.length > 0">
<p>
The following keys have been <strong>released</strong>, but
<strong>not pressed</strong> down.
</p>
<ul>
<li v-for="key in errors.up" :key="key">{{ key.toUpperCase() }}</li>
</ul>
</div>
</div>
<div class="flex justify-end mt-4">
<ButtonComp size="sm" variant="danger" @click="macroRecorder.state.validationErrors = false">
Close
</ButtonComp>
</div>
</div>
</template>
<script setup>
import ButtonComp from '@/components/base/ButtonComp.vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import { onMounted, reactive } from 'vue'
const macroRecorder = useMacroRecorderStore()
const errors = reactive({
up: [],
down: [],
})
onMounted(() => {
macroRecorder.$subscribe((mutation) => {
if (mutation.events && mutation.events.key == 'validationErrors') {
errors.up = mutation.events.newValue !== false ? macroRecorder.state.validationErrors.up : []
errors.down =
mutation.events.newValue !== false ? macroRecorder.state.validationErrors.down : []
}
})
})
</script>
<style scoped>
@reference "@/assets/main.css";
</style>

Some files were not shown because too many files have changed in this diff Show more