diff --git a/build.gradle.kts b/build.gradle.kts index 55efe295fb..e5d11fbcac 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,7 @@ import org.gradle.api.tasks.wrapper.Wrapper.DistributionType.ALL */ plugins { - kotlin("jvm") version("1.9.20") apply(false) + kotlin("jvm") version("1.9.21") apply(false) id("idea") id("eclipse") @@ -27,7 +27,7 @@ plugins { id("com.github.jk1.dependency-license-report") version("2.5") id("org.jetbrains.kotlinx.binary-compatibility-validator") version("0.13.2") id("org.graalvm.buildtools.native") version("0.9.28") apply(false) - id("io.gitlab.arturbosch.detekt") version("1.23.3") apply(false) + id("io.gitlab.arturbosch.detekt") version("1.23.4") apply(false) id("me.champeau.jmh") version("0.7.2") apply(false) } @@ -140,7 +140,7 @@ gradle.taskGraph.whenReady(closureOf { }) tasks.wrapper { - gradleVersion = "8.4" + gradleVersion = "8.5" distributionType = ALL } diff --git a/core/api/core.api b/core/api/core.api index ac9c733e59..f2b2c5f79f 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -447,7 +447,9 @@ public final class com/hexagonkt/core/security/KeyStoresKt { public final class com/hexagonkt/core/text/Ansi { public static final field CSI Ljava/lang/String; public static final field INSTANCE Lcom/hexagonkt/core/text/Ansi; + public static final field OSC Ljava/lang/String; public static final field RESET Ljava/lang/String; + public static final field ST Ljava/lang/String; public final fun getREGEX ()Lkotlin/text/Regex; } @@ -487,6 +489,10 @@ public final class com/hexagonkt/core/text/AnsiColor { public static final field WHITE_BG Ljava/lang/String; public static final field YELLOW Ljava/lang/String; public static final field YELLOW_BG Ljava/lang/String; + public final fun bg (III)Ljava/lang/String; + public static synthetic fun bg$default (Lcom/hexagonkt/core/text/AnsiColor;IIIILjava/lang/Object;)Ljava/lang/String; + public final fun fg (III)Ljava/lang/String; + public static synthetic fun fg$default (Lcom/hexagonkt/core/text/AnsiColor;IIIILjava/lang/Object;)Ljava/lang/String; } public final class com/hexagonkt/core/text/AnsiEffect { @@ -494,9 +500,16 @@ public final class com/hexagonkt/core/text/AnsiEffect { public static final field BLINK_OFF Ljava/lang/String; public static final field BOLD Ljava/lang/String; public static final field BOLD_OFF Ljava/lang/String; + public static final field DIM Ljava/lang/String; + public static final field DIM_OFF Ljava/lang/String; + public static final field FAST_BLINK Ljava/lang/String; public static final field INSTANCE Lcom/hexagonkt/core/text/AnsiEffect; public static final field INVERSE Ljava/lang/String; public static final field INVERSE_OFF Ljava/lang/String; + public static final field ITALIC Ljava/lang/String; + public static final field ITALIC_OFF Ljava/lang/String; + public static final field STRIKE Ljava/lang/String; + public static final field STRIKE_OFF Ljava/lang/String; public static final field UNDERLINE Ljava/lang/String; public static final field UNDERLINE_OFF Ljava/lang/String; } diff --git a/core/src/main/kotlin/com/hexagonkt/core/Jvm.kt b/core/src/main/kotlin/com/hexagonkt/core/Jvm.kt index ad71d12757..a269af5da9 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/Jvm.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/Jvm.kt @@ -83,6 +83,12 @@ object Jvm { fun usedMemory(): String = (runtime.totalMemory() - runtime.freeMemory()).let { "%,d".format(it / 1024) } + /** + * Add a map to system properties, optionally overriding them. + * + * @param settings Data to be added to system properties. + * @param overwrite If true, overwrite existing entries with supplied data. + */ fun loadSystemSettings(settings: Map, overwrite: Boolean = false) { settings.keys.forEach { check(it.matches(systemSettingPattern)) { diff --git a/core/src/main/kotlin/com/hexagonkt/core/text/Ansi.kt b/core/src/main/kotlin/com/hexagonkt/core/text/Ansi.kt index 2e8a0d636f..41d2f052ce 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/text/Ansi.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/text/Ansi.kt @@ -3,8 +3,6 @@ package com.hexagonkt.core.text /** * Constants for console formatting with [ANSI](https://en.wikipedia.org/wiki/ANSI_escape_code) * codes. They can be used in strings to enable or disable a display option. - * - * TODO Add other sequences (like OSC) */ object Ansi { /** Regex that matches ANSI escape sequences. */ @@ -12,6 +10,10 @@ object Ansi { /** Control Sequence Introducer. */ const val CSI = "\u001B[" + /** Operating System Command. */ + const val OSC = "\u001B]" + /** String Terminator. */ + const val ST = "\u001B\\" /** Disable all options applied before. */ const val RESET = "${CSI}0m" diff --git a/core/src/main/kotlin/com/hexagonkt/core/text/AnsiColor.kt b/core/src/main/kotlin/com/hexagonkt/core/text/AnsiColor.kt index 7d304a2dd1..6278bccd7c 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/text/AnsiColor.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/text/AnsiColor.kt @@ -1,76 +1,113 @@ package com.hexagonkt.core.text -// TODO Add RGB colors +import com.hexagonkt.core.text.Ansi.CSI + object AnsiColor { /** Set black as the foreground color. */ - const val BLACK = "${Ansi.CSI}30m" + const val BLACK = "${CSI}30m" /** Set red as the foreground color. */ - const val RED = "${Ansi.CSI}31m" + const val RED = "${CSI}31m" /** Set green as the foreground color. */ - const val GREEN = "${Ansi.CSI}32m" + const val GREEN = "${CSI}32m" /** Set yellow as the foreground color. */ - const val YELLOW = "${Ansi.CSI}33m" + const val YELLOW = "${CSI}33m" /** Set blue as the foreground color. */ - const val BLUE = "${Ansi.CSI}34m" + const val BLUE = "${CSI}34m" /** Set magenta as the foreground color. */ - const val MAGENTA = "${Ansi.CSI}35m" + const val MAGENTA = "${CSI}35m" /** Set cyan as the foreground color. */ - const val CYAN = "${Ansi.CSI}36m" + const val CYAN = "${CSI}36m" /** Set white as the foreground color. */ - const val WHITE = "${Ansi.CSI}37m" + const val WHITE = "${CSI}37m" /** Set back the default foreground color. */ - const val DEFAULT = "${Ansi.CSI}39m" + const val DEFAULT = "${CSI}39m" /** Set black as the background color. */ - const val BLACK_BG = "${Ansi.CSI}40m" + const val BLACK_BG = "${CSI}40m" /** Set red as the background color. */ - const val RED_BG = "${Ansi.CSI}41m" + const val RED_BG = "${CSI}41m" /** Set green as the background color. */ - const val GREEN_BG = "${Ansi.CSI}42m" + const val GREEN_BG = "${CSI}42m" /** Set yellow as the background color. */ - const val YELLOW_BG = "${Ansi.CSI}43m" + const val YELLOW_BG = "${CSI}43m" /** Set blue as the background color. */ - const val BLUE_BG = "${Ansi.CSI}44m" + const val BLUE_BG = "${CSI}44m" /** Set magenta as the background color. */ - const val MAGENTA_BG = "${Ansi.CSI}45m" + const val MAGENTA_BG = "${CSI}45m" /** Set cyan as the background color. */ - const val CYAN_BG = "${Ansi.CSI}46m" + const val CYAN_BG = "${CSI}46m" /** Set white as the background color. */ - const val WHITE_BG = "${Ansi.CSI}47m" + const val WHITE_BG = "${CSI}47m" /** Set back the default background color. */ - const val DEFAULT_BG = "${Ansi.CSI}49m" + const val DEFAULT_BG = "${CSI}49m" /** Set bright black as the foreground color. */ - const val BRIGHT_BLACK = "${Ansi.CSI}90m" + const val BRIGHT_BLACK = "${CSI}90m" /** Set bright red as the foreground color. */ - const val BRIGHT_RED = "${Ansi.CSI}91m" + const val BRIGHT_RED = "${CSI}91m" /** Set bright green as the foreground color. */ - const val BRIGHT_GREEN = "${Ansi.CSI}92m" + const val BRIGHT_GREEN = "${CSI}92m" /** Set bright yellow as the foreground color. */ - const val BRIGHT_YELLOW = "${Ansi.CSI}93m" + const val BRIGHT_YELLOW = "${CSI}93m" /** Set bright blue as the foreground color. */ - const val BRIGHT_BLUE = "${Ansi.CSI}94m" + const val BRIGHT_BLUE = "${CSI}94m" /** Set bright magenta as the foreground color. */ - const val BRIGHT_MAGENTA = "${Ansi.CSI}95m" + const val BRIGHT_MAGENTA = "${CSI}95m" /** Set bright cyan as the foreground color. */ - const val BRIGHT_CYAN = "${Ansi.CSI}96m" + const val BRIGHT_CYAN = "${CSI}96m" /** Set bright white as the foreground color. */ - const val BRIGHT_WHITE = "${Ansi.CSI}97m" + const val BRIGHT_WHITE = "${CSI}97m" /** Set bright black as the background color. */ - const val BRIGHT_BLACK_BG = "${Ansi.CSI}100m" + const val BRIGHT_BLACK_BG = "${CSI}100m" /** Set bright red as the background color. */ - const val BRIGHT_RED_BG = "${Ansi.CSI}101m" + const val BRIGHT_RED_BG = "${CSI}101m" /** Set bright green as the background color. */ - const val BRIGHT_GREEN_BG = "${Ansi.CSI}102m" + const val BRIGHT_GREEN_BG = "${CSI}102m" /** Set bright yellow as the background color. */ - const val BRIGHT_YELLOW_BG = "${Ansi.CSI}103m" + const val BRIGHT_YELLOW_BG = "${CSI}103m" /** Set bright blue as the background color. */ - const val BRIGHT_BLUE_BG = "${Ansi.CSI}104m" + const val BRIGHT_BLUE_BG = "${CSI}104m" /** Set bright magenta as the background color. */ - const val BRIGHT_MAGENTA_BG = "${Ansi.CSI}105m" + const val BRIGHT_MAGENTA_BG = "${CSI}105m" /** Set bright cyan as the background color. */ - const val BRIGHT_CYAN_BG = "${Ansi.CSI}106m" + const val BRIGHT_CYAN_BG = "${CSI}106m" /** Set bright white as the background color. */ - const val BRIGHT_WHITE_BG = "${Ansi.CSI}107m" + const val BRIGHT_WHITE_BG = "${CSI}107m" + + /** + * Set true color (24 bit) foreground. + * + * @param r Red intensity. Must be in the 0..255 range. + * @param g Green intensity. Must be in the 0..255 range. + * @param b Blue intensity. Must be in the 0..255 range. + * + * @return Escape code to set the foreground color. + */ + fun fg(r: Int = 0, g: Int = 0, b: Int = 0): String { + requireRange(r, 0..255, "Red") + requireRange(g, 0..255, "Green") + requireRange(b, 0..255, "Blue") + return "${CSI}38;2;$r;$g;${b}m" + } + + /** + * Set true color (24 bit) background. + * + * @param r Red intensity. Must be in the 0..255 range. + * @param g Green intensity. Must be in the 0..255 range. + * @param b Blue intensity. Must be in the 0..255 range. + * + * @return Escape code to set the background color. + */ + fun bg(r: Int = 0, g: Int = 0, b: Int = 0): String { + requireRange(r, 0..255, "Red") + requireRange(g, 0..255, "Green") + requireRange(b, 0..255, "Blue") + return "${CSI}48;2;$r;$g;${b}m" + } + + private fun requireRange(value: Int, range: IntRange, name: String) { + require(value in range) { "$name value must be in the $range range: $value" } + } } diff --git a/core/src/main/kotlin/com/hexagonkt/core/text/AnsiEffect.kt b/core/src/main/kotlin/com/hexagonkt/core/text/AnsiEffect.kt index a8c98f1584..b97fc17d94 100644 --- a/core/src/main/kotlin/com/hexagonkt/core/text/AnsiEffect.kt +++ b/core/src/main/kotlin/com/hexagonkt/core/text/AnsiEffect.kt @@ -1,22 +1,35 @@ package com.hexagonkt.core.text -// TODO Add '2', '3', '6' and '9' codes object AnsiEffect { /** Enable bold text. */ const val BOLD = "${Ansi.CSI}1m" + /** Enable dim text. */ + const val DIM = "${Ansi.CSI}2m" + /** Enable italic text. */ + const val ITALIC = "${Ansi.CSI}3m" /** Enable underline text. */ const val UNDERLINE = "${Ansi.CSI}4m" /** Enable blinking text. */ const val BLINK = "${Ansi.CSI}5m" + /** Enable fast blinking text. */ + const val FAST_BLINK = "${Ansi.CSI}6m" /** Enable inverse color text. */ const val INVERSE = "${Ansi.CSI}7m" + /** Enable strike text. */ + const val STRIKE = "${Ansi.CSI}9m" /** Disable bold text. */ const val BOLD_OFF = "${Ansi.CSI}21m" + /** Disable dim text. */ + const val DIM_OFF = "${Ansi.CSI}22m" + /** Disable italic text. */ + const val ITALIC_OFF = "${Ansi.CSI}23m" /** Disable underline text. */ const val UNDERLINE_OFF = "${Ansi.CSI}24m" /** Disable blinking text. */ const val BLINK_OFF = "${Ansi.CSI}25m" /** Disable inverse color text. */ const val INVERSE_OFF = "${Ansi.CSI}27m" + /** Disable strike text. */ + const val STRIKE_OFF = "${Ansi.CSI}29m" } diff --git a/core/src/main/resources/META-INF/native-image/com.hexagonkt/core/native-image.properties b/core/src/main/resources/META-INF/native-image/com.hexagonkt/core/native-image.properties index 1a61283f0a..026e9923af 100644 --- a/core/src/main/resources/META-INF/native-image/com.hexagonkt/core/native-image.properties +++ b/core/src/main/resources/META-INF/native-image/com.hexagonkt/core/native-image.properties @@ -1,5 +1,10 @@ Args= \ + -H:+UnlockExperimentalVMOptions \ -H:IncludeResources=hexagon\\.properties \ + --strict-image-heap \ --enable-url-protocols=http,https,classpath \ --initialize-at-run-time=com.hexagonkt.core.NetworkKt \ - --initialize-at-build-time=com.hexagonkt.core.ClasspathHandler + --initialize-at-build-time=kotlin.SynchronizedLazyImpl \ + --initialize-at-build-time=kotlin.UNINITIALIZED_VALUE \ + --initialize-at-build-time=com.hexagonkt.core.ClasspathHandler \ + --initialize-at-build-time=com.hexagonkt.core.ClasspathHandler$classLoader$2 diff --git a/core/src/test/kotlin/com/hexagonkt/core/text/AnsiTest.kt b/core/src/test/kotlin/com/hexagonkt/core/text/AnsiTest.kt index f13c6d771a..26e83f8bc6 100644 --- a/core/src/test/kotlin/com/hexagonkt/core/text/AnsiTest.kt +++ b/core/src/test/kotlin/com/hexagonkt/core/text/AnsiTest.kt @@ -1,19 +1,93 @@ package com.hexagonkt.core.text import com.hexagonkt.core.logging.Logger +import com.hexagonkt.core.text.Ansi.OSC +import com.hexagonkt.core.text.Ansi.RESET +import com.hexagonkt.core.text.Ansi.ST import com.hexagonkt.core.text.AnsiColor.BLACK import com.hexagonkt.core.text.AnsiColor.BLUE_BG import com.hexagonkt.core.text.AnsiEffect.UNDERLINE import org.junit.jupiter.api.Test +import java.lang.IllegalArgumentException +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith internal class AnsiTest { private val logger: Logger by lazy { Logger(this::class) } + @Test fun `Color edge values`() { + println("${AnsiColor.bg(0, 0, 0)}${AnsiColor.fg(0, 0, 0)}TEST$RESET") + println("${AnsiColor.bg(255, 255, 255)}${AnsiColor.fg(255, 255, 255)}TEST$RESET") + assertEquals( + "Red value must be in the 0..255 range: -1", + assertFailsWith { AnsiColor.bg(-1, 0, 0) }.message + ) + assertEquals( + "Red value must be in the 0..255 range: 256", + assertFailsWith { AnsiColor.bg(256, 0, 0) }.message + ) + assertEquals( + "Green value must be in the 0..255 range: -1", + assertFailsWith { AnsiColor.bg(0, -1, 0) }.message + ) + assertEquals( + "Green value must be in the 0..255 range: 256", + assertFailsWith { AnsiColor.bg(0, 256, 0) }.message + ) + assertEquals( + "Blue value must be in the 0..255 range: -1", + assertFailsWith { AnsiColor.bg(0, 0, -1) }.message + ) + assertEquals( + "Blue value must be in the 0..255 range: 256", + assertFailsWith { AnsiColor.bg(0, 0, 256) }.message + ) + } + + @Test fun `True colors`() { + fun background(r: Int, g: Int, b: Int) { + val bg = AnsiColor.bg(r, g, b) + print("${bg}X$RESET") + } + + fun foreground(r: Int, g: Int, b: Int) { + val fg = AnsiColor.fg(r, g, b) + print("${fg}X$RESET") + } + + for (r in 0..255 step 4) + background(r, 0, 0) + + println() + for (r in 0..255 step 4) + background(0, r, 0) + + println() + for (r in 0..255 step 4) + background(0, 0, r) + + println() + for (r in 0..255 step 4) + foreground(r, 0, 0) + + println() + for (r in 0..255 step 4) + foreground(0, r, 0) + + println() + for (r in 0..255 step 4) + foreground(0, 0, r) + } + + @Test fun `Switch terminal emulator title`() { + print("${OSC}2;TEST$ST") + } + @Test fun `ANSI codes are printed properly`() { fun test(message: String) { - logger.info { "${message}${Ansi.RESET} | normal text" } + logger.info { "${message}$RESET | normal text" } } test("${Ansi.CSI}30m black") @@ -65,6 +139,15 @@ internal class AnsiTest { test("${AnsiEffect.BLINK_OFF}blink off") test("${AnsiEffect.INVERSE_OFF}inverse off") + test("${AnsiEffect.DIM}dim") + test("${AnsiEffect.ITALIC}italic") + test("${AnsiEffect.FAST_BLINK}fast blink") + test("${AnsiEffect.STRIKE}strike") + + test("${AnsiEffect.DIM_OFF}dim off") + test("${AnsiEffect.ITALIC_OFF}italic off") + test("${AnsiEffect.STRIKE_OFF}strike off") + test("$BLACK$BLUE_BG${UNDERLINE}black fg blue bg underline") } } diff --git a/gradle.properties b/gradle.properties index b4a30c02d4..acd240dad5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.warning.mode=all org.gradle.console=plain # Gradle -version=3.4.3 +version=3.4.4 group=com.hexagonkt description=The atoms of your platform @@ -31,35 +31,37 @@ logoSmall=assets/img/logo.svg iconsDirectory=content # VERSIONS -kotlinVersion=1.9.20 +kotlinVersion=1.9.21 dokkaVersion=1.9.10 mockkVersion=1.13.8 -junitVersion=5.10.0 +junitVersion=5.10.1 gatlingVersion=3.9.5 jmhVersion=1.37 -mkdocsMaterialVersion=9.4.7 +mkdocsMaterialVersion=9.4.14 mermaidDokkaVersion=0.4.4 nativeToolsVersion=0.9.28 # http_server_netty -nettyVersion=4.1.100.Final +nettyVersion=4.1.101.Final nettyTcNativeVersion=2.0.62.Final # http_server_helidon -helidonVersion=4.0.0 +helidonVersion=4.0.1 # http_server_servlet servletVersion=6.0.0 jettyVersion=12.0.3 # rest_tools -swaggerValidatorVersion=2.38.0 +swaggerRequestValidatorVersion=2.39.0 # logging slf4jVersion=2.0.9 -logbackVersion=1.4.11 +logbackVersion=1.4.12 # serialization +# TODO +#jacksonVersion=2.16.0 jacksonVersion=2.15.3 dslJsonVersion=2.0.2 @@ -67,10 +69,10 @@ dslJsonVersion=2.0.2 freemarkerVersion=2.3.32 # templates_jte -jteVersion=3.1.4 +jteVersion=3.1.5 # templates_pebble -pebbleVersion=3.2.1 +pebbleVersion=3.2.2 # templates_rocker rockerVersion=1.4.0 diff --git a/gradle/kotlin.gradle b/gradle/kotlin.gradle index bd800bcfdb..114166a79d 100644 --- a/gradle/kotlin.gradle +++ b/gradle/kotlin.gradle @@ -35,7 +35,7 @@ repositories { } dependencies { - final String scriptJunitVersion = findProperty("junitVersion") ?: "5.10.0" + final String scriptJunitVersion = findProperty("junitVersion") ?: "5.10.1" implementation("org.jetbrains.kotlin:kotlin-stdlib") diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49..d64cd49177 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8838ba97ba..e6aba2515d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/http/http_server_jetty/src/main/resources/META-INF/native-image/com.hexagonkt/http_server_jetty/native-image.properties b/http/http_server_jetty/src/main/resources/META-INF/native-image/com.hexagonkt/http_server_jetty/native-image.properties index 2f30ad20d0..dbafe7a21f 100644 --- a/http/http_server_jetty/src/main/resources/META-INF/native-image/com.hexagonkt/http_server_jetty/native-image.properties +++ b/http/http_server_jetty/src/main/resources/META-INF/native-image/com.hexagonkt/http_server_jetty/native-image.properties @@ -1 +1,4 @@ -Args=-H:IncludeResources=.*\\.css +Args= \ + -H:IncludeResources=.*\\.css \ + --initialize-at-build-time=org.slf4j.helpers.NOPLogger \ + --initialize-at-build-time=org.slf4j.helpers.NOPLoggerFactory diff --git a/http/rest_tools/build.gradle.kts b/http/rest_tools/build.gradle.kts index f6dfb2fa17..eecbd14b3a 100644 --- a/http/rest_tools/build.gradle.kts +++ b/http/rest_tools/build.gradle.kts @@ -12,12 +12,12 @@ apply(from = "$rootDir/gradle/detekt.gradle") description = "Tools to test and document REST services." dependencies { - val swaggerValidatorVersion = properties["swaggerValidatorVersion"] + val swaggerRequestValidatorVersion = properties["swaggerRequestValidatorVersion"] "api"(project(":http:rest")) "api"(project(":http:http_server")) "api"(project(":http:http_client")) - "api"("com.atlassian.oai:swagger-request-validator-core:$swaggerValidatorVersion") + "api"("com.atlassian.oai:swagger-request-validator-core:$swaggerRequestValidatorVersion") "testImplementation"(project(":http:http_client_jetty")) "testImplementation"(project(":http:http_server_jetty")) diff --git a/site/pages/index.md b/site/pages/index.md index f237ef48a1..e94c33f531 100644 --- a/site/pages/index.md +++ b/site/pages/index.md @@ -121,10 +121,12 @@ develop applications, but not strictly required. Some of these modules are: * [Schedulers]: Provides repeated tasks execution based on [Cron] expressions. * [Models]: Contain classes that model common data objects. +* [Args]: Command line arguments definition and parsing. [Web]: /web [Schedulers]: /scheduler [Models]: /models +[Args]: /args [Cron]: https://en.wikipedia.org/wiki/Cron # Architecture