@gesslar/muddy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Type.js ADDED
@@ -0,0 +1,113 @@
1
+ import {URL} from "node:url"
2
+ import {Collection, Util} from "@gesslar/toolkit"
3
+
4
+ // Aliases
5
+ const {capitalize} = Util
6
+ const {allocateObject} = Collection
7
+ const {freeze} = Object
8
+
9
+ /**
10
+ * Type constants and mappings for Mudlet module types.
11
+ *
12
+ * Provides mappings between singular/plural forms, class constructors,
13
+ * file names, URLs, and XML package tag names for all module types.
14
+ */
15
+ const Type = {}
16
+
17
+ // Base
18
+ const single = () => freeze(new Array("alias", "key", "script", "timer", "trigger"))
19
+ const plural = () => freeze(new Array("aliases", "keys", "scripts", "timers", "triggers"))
20
+
21
+ /**
22
+ * Array of singular module type names.
23
+ *
24
+ * @type {Array<string>}
25
+ */
26
+ Type.SINGLE = single()
27
+
28
+ /**
29
+ * Array of plural module type names.
30
+ *
31
+ * @type {Array<string>}
32
+ */
33
+ Type.PLURAL = plural()
34
+
35
+ /**
36
+ * Mapping from singular to plural module type names.
37
+ *
38
+ * @type {Readonly<Record<string, string>>}
39
+ */
40
+ Type.TO_PLURAL = freeze(await allocateObject(single(), plural()))
41
+
42
+ /**
43
+ * Mapping from plural to singular module type names.
44
+ *
45
+ * @type {Readonly<Record<string, string>>}
46
+ */
47
+ Type.TO_SINGLE = freeze(await allocateObject(plural(), single()))
48
+
49
+ const classify = async e => (await import(`./modules/${capitalize(e)}.js`)).default
50
+
51
+ /**
52
+ * Mapping from plural module type names to their class constructors.
53
+ *
54
+ * @type {Readonly<Record<string, Function>>}
55
+ */
56
+ Type.CLASS = Object.freeze(await allocateObject(
57
+ plural(),
58
+ await Promise.all(single().map(classify))
59
+ ))
60
+
61
+ const upper = e => e.toUpperCase()
62
+
63
+ /**
64
+ * Mapping from uppercase type names to plural module type names.
65
+ *
66
+ * @type {Readonly<Record<string, string>>}
67
+ */
68
+ Type.TYPES = Object.freeze(await allocateObject(
69
+ single().map(upper),
70
+ plural()
71
+ ))
72
+
73
+ const file = e => `${e}.json`
74
+
75
+ /**
76
+ * Mapping from plural module type names to their JSON file names.
77
+ *
78
+ * @type {Readonly<Record<string, string>>}
79
+ */
80
+ Type.FILES = Object.freeze(await allocateObject(
81
+ plural(),
82
+ plural().map(file)
83
+ ))
84
+
85
+ const baseUrl = "https://schema.gesslar.dev/muddler/v1/"
86
+ const url = e => new URL(`${e}.json`, baseUrl)
87
+
88
+ /**
89
+ * Mapping from type names to their schema URLs.
90
+ *
91
+ * @type {Readonly<Record<string, URL>>}
92
+ */
93
+ Type.URL = Object.freeze(await allocateObject(
94
+ ["mfile", "common", ...plural()],
95
+ ["mfile", "common", ...plural()].map(url)
96
+ ))
97
+
98
+ const pack = e => `${capitalize(e)}Package`
99
+
100
+ /**
101
+ * Mapping from plural module type names to their XML package tag names.
102
+ *
103
+ * @type {Readonly<Record<string, string>>}
104
+ */
105
+ Type.PACKAGES = Object.freeze(await allocateObject(
106
+ plural(),
107
+ single().map(pack)
108
+ ))
109
+
110
+ /**
111
+ * Sealed Type object containing all module type constants and mappings.
112
+ */
113
+ export default Object.seal(Type)
package/src/Watch.js ADDED
@@ -0,0 +1,139 @@
1
+ import {Disposer, Notify, Sass, Term, Time} from "@gesslar/toolkit"
2
+ import {watch} from "node:fs/promises"
3
+ import process from "node:process"
4
+
5
+ import Muddy from "./Muddy.js"
6
+
7
+ /**
8
+ * @import {DirectoryObject} from "@gesslar/toolkit"
9
+ * @import {DisposerClass} from "@gesslar/toolkit"
10
+ * @import {FileObject} from "@gesslar/toolkit"
11
+ * @import {Glog} from "@gesslar/toolkit"
12
+ * @import {NotifyClass} from "@gesslar/toolkit"
13
+ */
14
+
15
+ export default class Watch {
16
+ /** @type {DirectoryObject} */
17
+ #projectDirectory
18
+ /** @type {DirectoryObject} */
19
+ #srcDirectory
20
+ /** @type {FileObject} */
21
+ #mfile
22
+ /** @type {Glog} */
23
+ #glog
24
+ /** @type {NotifyClass} */
25
+ #notify
26
+ /** @type {DisposerClass} */
27
+ #disposer
28
+ // eslint-disable-next-line jsdoc/no-undefined-types
29
+ /** @type {Array<AsyncIterator>} */
30
+ #watchers = new Array()
31
+ /** @type {AbortController} */
32
+ #ac
33
+ /** @type {boolean} */
34
+ #pending = false
35
+ /** @type {boolean} */
36
+ #busy = false
37
+
38
+ /**
39
+ * Main execution point.
40
+ *
41
+ * @param {DirectoryObject} projectDirectory - The directory of project.
42
+ * @param {Glog} log - The Glog instance object.
43
+ */
44
+ async run(projectDirectory, log) {
45
+ this.#projectDirectory = projectDirectory
46
+ this.#srcDirectory = projectDirectory.getDirectory("src")
47
+ this.#mfile = this.#projectDirectory.getFile("mfile")
48
+ this.#glog = log
49
+ this.#notify = Notify
50
+ this.#disposer = Disposer
51
+
52
+ await new Muddy().run(this.#projectDirectory, this.#glog)
53
+
54
+ this.#initialiseInputHandler()
55
+ this.#startWatch()
56
+ }
57
+
58
+ async #startWatch() {
59
+ this.#ac = new AbortController()
60
+
61
+ const toWatch = [this.#srcDirectory, this.#mfile]
62
+
63
+ try {
64
+ for(const w of toWatch) {
65
+ const watcher = watch(w.url, {
66
+ recursive: w.isDirectory ?? false,
67
+ persistent: true,
68
+ signal: this.#ac.signal,
69
+ overflow: "error"
70
+ })
71
+
72
+ this.#watchers.push(watcher)
73
+
74
+ ;(async() => {
75
+ try {
76
+ for await(const _ of watcher) {
77
+ if(this.#busy) {
78
+ this.#pending = true
79
+ continue
80
+ }
81
+
82
+ this.#pending = false
83
+ this.#busy = true
84
+
85
+ while(true) {
86
+ await Time.after(50)
87
+ await new Muddy().run(this.#projectDirectory, this.#glog)
88
+
89
+ if(this.#pending) {
90
+ this.#pending = false
91
+ continue
92
+ }
93
+
94
+ break
95
+ }
96
+
97
+ this.#busy = false
98
+ }
99
+ } catch(err) {
100
+ if(err.name === "AbortError")
101
+ return
102
+
103
+ throw err
104
+ }
105
+ })()
106
+ }
107
+ } catch {}
108
+ }
109
+
110
+ /**
111
+ * Initialises the input handler for watch mode.
112
+ * Sets up raw mode input handling for interactive commands.
113
+ *
114
+ * @returns {void}
115
+ */
116
+ async #initialiseInputHandler() {
117
+ Term
118
+ .setCharMode()
119
+ .resume()
120
+ .utf8()
121
+ .hideCursor()
122
+
123
+ process.stdin.on("data", async key => {
124
+ try {
125
+ if(key === "q" || key === "\u0003" || key === "\u0004") { // Ctrl+C
126
+ Term
127
+ .setLineMode()
128
+ .showCursor()
129
+ .pause()
130
+ .write("\nExiting.\n")
131
+
132
+ process.exit(0)
133
+ }
134
+ } catch(error) {
135
+ Sass.new("Processing input.", error).report(true)
136
+ }
137
+ })
138
+ }
139
+ }
package/src/cli.js ADDED
@@ -0,0 +1,88 @@
1
+ #! /usr/bin/env node
2
+
3
+ import c from "@gesslar/colours"
4
+ import {Collection, DirectoryObject, Glog, Sass, Term} from "@gesslar/toolkit"
5
+ import {Command} from "commander"
6
+ import process from "node:process"
7
+
8
+ import Muddy from "./Muddy.js"
9
+ import Watch from "./Watch.js"
10
+
11
+ const aliasNames = ["OK", "INFO", "WARN", "ERR"]
12
+ const aliasCodes = ["{<I}{F064}", "{<I}{F023}", "{<I}{F178}", "{<I}{F166}"]
13
+ const kindNames = ["aliases", "keys", "scripts", "timers", "triggers", "other"]
14
+ const kindCodes = ["{<I}{F167}", "{<I}{F135}", "{<I}{F026}", "{<I}{F223}", "{<I}{F204}", "{<I}{F136}"]
15
+
16
+ const glogColourNames = ["success", "info", "warn", "error"]
17
+ const glogColourCodes = ["{F035}", "{F033}", "{F208}", "{F032}"]
18
+
19
+ void (async() => {
20
+ const opts = {}, args = []
21
+
22
+ try {
23
+ // Initialize colours aliases
24
+ aliasNames.forEach((e, i) => c.alias.set(e, aliasCodes[i]))
25
+ kindNames.forEach((e, i) => c.alias.set(e, kindCodes[i]))
26
+
27
+ // Initialize logging
28
+ const glog = new Glog()
29
+ .withName("MUDDY")
30
+ .withStackTrace()
31
+ .noDisplayName()
32
+ .withColours(await Collection.allocateObject(
33
+ glogColourNames,
34
+ glogColourCodes
35
+ ))
36
+ .withSymbols(await Collection.allocateObject(
37
+ glogColourNames,
38
+ [`•`, `•`, `•`, `•`]
39
+ ))
40
+
41
+ const program = new Command("muddy")
42
+ .argument("[directory]", "The project directory containing an 'mfile' file and 'src/' directory.")
43
+ .option("-w, --watch", "Enable watch mode.", false)
44
+ .option("-n, --nerd", "Nerd mode. Advanced error reporting.", false)
45
+ .parse()
46
+
47
+ Object.assign(opts, program.opts())
48
+ args.push(...program.args)
49
+
50
+ const dirArg = args.join(" ").trim()
51
+ const cwd = new DirectoryObject(dirArg || ".")
52
+
53
+ if(!await cwd.exists) {
54
+ glog.error(`No such directory '${dirArg}'.`)
55
+ process.exit(1)
56
+ }
57
+
58
+ if(!(await cwd.hasDirectory("src") && await cwd.hasFile("mfile"))) {
59
+ glog.error(`'${cwd.path}' is not a valid muddy project directory.`)
60
+ process.exit(1)
61
+ }
62
+
63
+ if(opts.watch) {
64
+ setupAbortHandlers()
65
+ await new Watch().run(cwd, glog)
66
+ } else {
67
+ await new Muddy().run(cwd, glog)
68
+ }
69
+ } catch(error) {
70
+ Sass.from(error, "Starting muddy.").report(opts.nerd ?? false)
71
+ }
72
+
73
+ /**
74
+ * Creates handlers for various reasons that the application may crash.
75
+ */
76
+ function setupAbortHandlers() {
77
+ void["SIGINT", "SIGTERM", "SIGHUP"].forEach(signal => {
78
+ process.on(signal, () => {
79
+ Term
80
+ .setLineMode()
81
+ .showCursor()
82
+ .pause()
83
+
84
+ process.exit(0)
85
+ })
86
+ })
87
+ }
88
+ })()
@@ -0,0 +1,158 @@
1
+ import {Valid} from "@gesslar/toolkit"
2
+
3
+ import Module from "./MudletModule.js"
4
+
5
+ export default class Action extends Module {
6
+ #meta = new Map()
7
+
8
+ constructor(object={}) {
9
+ super(object)
10
+
11
+ const {
12
+ css="",
13
+ commandButtonUp="",
14
+ commandButtonDown="",
15
+ icon="",
16
+ orientation=0,
17
+ location=0,
18
+ posX=0,
19
+ posY=0,
20
+ mButtonState=1,
21
+ sizeX=0,
22
+ sizeY=0,
23
+ buttonColumn=0,
24
+ buttonRotation=0
25
+ } = object
26
+
27
+ // Validate string fields
28
+ Valid.type(css, "String")
29
+ Valid.type(commandButtonUp, "String")
30
+ Valid.type(commandButtonDown, "String")
31
+ Valid.type(icon, "String")
32
+
33
+ // Validate orientation (0=horizontal, 1=vertical)
34
+ Valid.type(orientation, "Number")
35
+ Valid.assert(orientation === 0 || orientation === 1, "orientation must be 0 or 1")
36
+
37
+ // Validate location (0, 2, 3, or 4)
38
+ Valid.type(location, "Number")
39
+ Valid.assert([0, 2, 3, 4].includes(location), "location must be 0, 2, 3, or 4")
40
+
41
+ // Validate non-negative integers
42
+ Valid.type(posX, "Number")
43
+ Valid.assert(posX >= 0, "posX must be non-negative")
44
+ Valid.type(posY, "Number")
45
+ Valid.assert(posY >= 0, "posY must be non-negative")
46
+ Valid.type(sizeX, "Number")
47
+ Valid.assert(sizeX >= 0, "sizeX must be non-negative")
48
+ Valid.type(sizeY, "Number")
49
+ Valid.assert(sizeY >= 0, "sizeY must be non-negative")
50
+ Valid.type(buttonColumn, "Number")
51
+ Valid.assert(buttonColumn >= 0, "buttonColumn must be non-negative")
52
+
53
+ // Validate button state (1=unchecked/up, 2=checked/down)
54
+ Valid.type(mButtonState, "Number")
55
+ Valid.assert(mButtonState === 1 || mButtonState === 2, "mButtonState must be 1 or 2")
56
+
57
+ // Validate button rotation (0, 90, 180, or 270)
58
+ Valid.type(buttonRotation, "Number")
59
+ Valid.assert([0, 90, 180, 270].includes(buttonRotation), "buttonRotation must be 0, 90, 180, or 270")
60
+
61
+ // Store metadata
62
+ this.#meta.set("css", css)
63
+ this.#meta.set("commandButtonUp", commandButtonUp)
64
+ this.#meta.set("commandButtonDown", commandButtonDown)
65
+ this.#meta.set("icon", icon)
66
+ this.#meta.set("orientation", orientation)
67
+ this.#meta.set("location", location)
68
+ this.#meta.set("posX", posX)
69
+ this.#meta.set("posY", posY)
70
+ this.#meta.set("mButtonState", mButtonState)
71
+ this.#meta.set("sizeX", sizeX)
72
+ this.#meta.set("sizeY", sizeY)
73
+ this.#meta.set("buttonColumn", buttonColumn)
74
+ this.#meta.set("buttonRotation", buttonRotation)
75
+ }
76
+
77
+ get css() {
78
+ return this.#meta.get("css")
79
+ }
80
+
81
+ get commandButtonUp() {
82
+ return this.#meta.get("commandButtonUp")
83
+ }
84
+
85
+ get commandButtonDown() {
86
+ return this.#meta.get("commandButtonDown")
87
+ }
88
+
89
+ get icon() {
90
+ return this.#meta.get("icon")
91
+ }
92
+
93
+ get orientation() {
94
+ return this.#meta.get("orientation")
95
+ }
96
+
97
+ get location() {
98
+ return this.#meta.get("location")
99
+ }
100
+
101
+ get posX() {
102
+ return this.#meta.get("posX")
103
+ }
104
+
105
+ get posY() {
106
+ return this.#meta.get("posY")
107
+ }
108
+
109
+ get mButtonState() {
110
+ return this.#meta.get("mButtonState")
111
+ }
112
+
113
+ get sizeX() {
114
+ return this.#meta.get("sizeX")
115
+ }
116
+
117
+ get sizeY() {
118
+ return this.#meta.get("sizeY")
119
+ }
120
+
121
+ get buttonColumn() {
122
+ return this.#meta.get("buttonColumn")
123
+ }
124
+
125
+ get buttonRotation() {
126
+ return this.#meta.get("buttonRotation")
127
+ }
128
+
129
+ toJSON() {
130
+ return Object.assign(
131
+ super.toJSON(),
132
+ {...Object.fromEntries(this.#meta)}
133
+ )
134
+ }
135
+
136
+ toXMLFragment() {
137
+ const frag = super.toXMLFragment()
138
+
139
+ // Add elements in schema order (after name, packageName, script from Module)
140
+ frag
141
+ .last()
142
+ .ele({css: this.css}).up()
143
+ .ele({commandButtonUp: this.commandButtonUp}).up()
144
+ .ele({commandButtonDown: this.commandButtonDown}).up()
145
+ .ele({icon: this.icon}).up()
146
+ .ele({orientation: this.orientation}).up()
147
+ .ele({location: this.location}).up()
148
+ .ele({posX: this.posX}).up()
149
+ .ele({posY: this.posY}).up()
150
+ .ele({mButtonState: this.mButtonState}).up()
151
+ .ele({sizeX: this.sizeX}).up()
152
+ .ele({sizeY: this.sizeY}).up()
153
+ .ele({buttonColumn: this.buttonColumn}).up()
154
+ .ele({buttonRotation: this.buttonRotation}).up()
155
+
156
+ return frag
157
+ }
158
+ }
@@ -0,0 +1,45 @@
1
+ import {Valid} from "@gesslar/toolkit"
2
+
3
+ import MudletModule from "./MudletModule.js"
4
+
5
+ export default class Alias extends MudletModule {
6
+ #meta = new Map()
7
+
8
+ constructor(object={regex: "", command: ""}) {
9
+ super(object)
10
+
11
+ const {regex="", command=""} = object
12
+ Valid.type(regex, "String")
13
+ Valid.type(command, "String")
14
+
15
+ this.#meta.set("regex", regex)
16
+ this.#meta.set("command", command)
17
+ }
18
+
19
+ get regex() {
20
+ return this.#meta.get("regex")
21
+ }
22
+
23
+ get command() {
24
+ return this.#meta.get("command")
25
+ }
26
+
27
+ toJSON() {
28
+ return Object.assign(
29
+ super.toJSON(),
30
+ {...Object.fromEntries(this.#meta)}
31
+ )
32
+ }
33
+
34
+ toXMLFragment() {
35
+ const frag = super.toXMLFragment()
36
+
37
+ // Add elements in schema order (after name, script, packageName from Module)
38
+ frag
39
+ .last()
40
+ .ele({command: this.command}).up()
41
+ .ele({regex: this.regex}).up()
42
+
43
+ return frag
44
+ }
45
+ }
@@ -0,0 +1,61 @@
1
+ import {Valid} from "@gesslar/toolkit"
2
+
3
+ import MudletModule from "./MudletModule.js"
4
+
5
+ export default class Key extends MudletModule {
6
+ #meta = new Map()
7
+
8
+ constructor(object={}) {
9
+ super(object)
10
+
11
+ const {
12
+ command="",
13
+ keyCode=0,
14
+ keyModifier=0
15
+ } = object
16
+
17
+ // Validate string fields
18
+ Valid.type(command, "String")
19
+
20
+ // Validate integer fields
21
+ Valid.type(keyCode, "Number")
22
+ Valid.type(keyModifier, "Number")
23
+
24
+ // Store metadata
25
+ this.#meta.set("command", command)
26
+ this.#meta.set("keyCode", keyCode)
27
+ this.#meta.set("keyModifier", keyModifier)
28
+ }
29
+
30
+ get command() {
31
+ return this.#meta.get("command")
32
+ }
33
+
34
+ get keyCode() {
35
+ return this.#meta.get("keyCode")
36
+ }
37
+
38
+ get keyModifier() {
39
+ return this.#meta.get("keyModifier")
40
+ }
41
+
42
+ toJSON() {
43
+ return Object.assign(
44
+ super.toJSON(),
45
+ {...Object.fromEntries(this.#meta)}
46
+ )
47
+ }
48
+
49
+ toXMLFragment() {
50
+ const frag = super.toXMLFragment()
51
+
52
+ // Add elements in schema order (after name, packageName, script from Module)
53
+ frag
54
+ .last()
55
+ .ele({command: this.command}).up()
56
+ .ele({keyCode: this.keyCode}).up()
57
+ .ele({keyModifier: this.keyModifier}).up()
58
+
59
+ return frag
60
+ }
61
+ }
@@ -0,0 +1,16 @@
1
+ const Mfile = new Object()
2
+
3
+ Mfile.FIELDS = Object.freeze([
4
+ "package", "title", "description", "version", "author", "icon",
5
+ "dependencies", "outputFile",
6
+ ])
7
+
8
+ Mfile.MFILE_TO_CONFIG = Object.freeze(new Map([
9
+ ["package", "mpackage"],
10
+ ["author", "author"],
11
+ ["icon", "icon"],
12
+ ["description", "description"],
13
+ ["version", "version"],
14
+ ]))
15
+
16
+ export default Mfile