diff --git a/.cargo/config.toml b/.cargo/config.toml
index 5340764..f3bb56d 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,5 +1,8 @@
-[target.'cfg(not(target_os = "windows"))']
-runner = "sudo"
+# [target.'cfg(not(target_os = "windows"))']
+# runner = "sudo"
-[target.'cfg(target_os = "windows")']
-# todo: windows
+# [target.'cfg(target_os = "windows")']
+# # todo: windows
+
+[build]
+target-dir = ".build/rust"
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..097f9f9
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,9 @@
+#
+# https://help.github.com/articles/dealing-with-line-endings/
+#
+# Linux start script should use lf
+/gradlew text eol=lf
+
+# These are Windows script files and should use crlf
+*.bat text eol=crlf
+
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 1a227b9..7972b34 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -19,6 +19,7 @@ Lowest:
Web:
+JVM:
-->
Changes
-
\ No newline at end of file
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 3472a1b..5a3d77b 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -8,29 +8,39 @@ on:
workflow_dispatch:
jobs:
- clippy:
- name: Cargo Clippy
+ linux:
+ name: Linux
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read
steps:
-
- # START Common Build
- name: Checkout Repository
uses: actions/checkout@v3
+ - name: Setup Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
- name: Install Toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
- name: Install Dependencies
- run: sudo apt-get install libgtk-3-dev libsdl-pango-dev libx11-dev libxtst-dev libudev-dev libinput-dev -y
+ run: python x.py installDeps
- name: Build
- run: cargo build --workspace --all-features
- # END Common Build
+ run: python x.py build full
+ clippy:
+ name: Cargo Clippy
+ runs-on: ubuntu-latest
+ needs: linux
+ permissions:
+ contents: read
+ security-events: write
+ actions: read
+ steps:
- name: Install required cargo
run: cargo install clippy-sarif sarif-fmt
- name: Run rust-clippy
@@ -43,4 +53,52 @@ jobs:
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: rust-clippy-results.sarif
- wait-for-processing: true
\ No newline at end of file
+ wait-for-processing: true
+
+ windows:
+ name: Windows
+ runs-on: windows-latest
+ permissions:
+ contents: read
+ security-events: write
+ actions: read
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v3
+ - name: Setup Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+ - name: Install Toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: nightly
+ - name: Install Dependencies
+ run: python x.py installDeps
+ - name: Build
+ run: python x.py build full
+
+ macos:
+ name: MacOS
+ runs-on: macos-latest
+ permissions:
+ contents: read
+ security-events: write
+ actions: read
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v3
+ - name: Setup Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+ - name: Install Toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: nightly
+ - name: Install Dependencies
+ run: python x.py installDeps
+ - name: Build
+ run: python x.py build full
\ No newline at end of file
diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml
index a68c581..615fb59 100644
--- a/.github/workflows/doc.yml
+++ b/.github/workflows/doc.yml
@@ -16,35 +16,45 @@ concurrency:
cancel-in-progress: true
jobs:
- deploy:
- name: Deploy to GitHub Pages
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
+ setup:
+ name: Setup
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ security-events: write
+ actions: read
steps:
-
- # START Common Build
- name: Checkout Repository
uses: actions/checkout@v3
+ - name: Setup Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
- name: Install Toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
- name: Install Dependencies
- run: sudo apt-get install libgtk-3-dev libsdl-pango-dev libx11-dev libxtst-dev libudev-dev libinput-dev -y
+ run: python x.py installDeps
- name: Build
- run: cargo build --workspace --all-features
- # END Common Build
+ run: python x.py build full
+ deploy:
+ name: Deploy to GitHub Pages
+ runs-on: ubuntu-latest
+ needs: setup
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
- run: cargo doc --workspace --no-deps
- name: Setup Pages
uses: actions/configure-pages@v2
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
- path: './target/doc/'
+ path: './.build/rust/doc/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d8ab922..dc25cfc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,31 +1,33 @@
#========================================#
# Large amounts of code / logs
-/node_modules
-/corelogs
+node_modules
+corelogs
#========================================#
#========================================#
# IDE specific files
-/.vscode
-/.idea
-/.vs
+.vscode
+.idea
+.vs
#========================================#
#========================================#
# Private data
-/private
+private
#========================================#
#========================================#
# Build output
-/.turbobuild
-/.turbo
-/.build
-/target
-/build
-/dist
-/out
-/bin
+__pycache__
+.turbobuild
+.gradle
+.turbo
+.build
+target
+build
+dist
+out
+bin
#========================================#
#========================================#
diff --git a/Cargo.toml b/Cargo.toml
index 6445295..c02cf4a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,78 +1,91 @@
-[package]
-name = "titanium"
-version = "1.0.0-alpha.4"
-edition = "2021"
-
-[lib]
-path = "titanium/lib.rs"
-
-[workspace]
-members = [
- "titanium/desktop",
- "titanium/web",
-
- # Examples
- "examples/desktop_discord",
- "examples/desktop_memory",
- "examples/desktop_gui",
- "examples/web",
-]
-
-[dependencies.titaniummacros]
-path = "./titanium/macros"
-package = "macros"
-optional = true
-
-[dependencies.async-std]
-features = ["attributes"]
-version = "1.12"
-optional = true
-
-[dependencies.titaniumutils]
-path = "./titanium/utils"
-package = "titanium_utils"
-default-features = false
-features = ["macros"]
-
-[dependencies.titaniumassets]
-path = "./titanium/assets"
-package = "titanium_assets"
-optional = true
-
-[dependencies.titaniumcommon]
-path = "./titanium/common"
-package = "titanium_common"
-optional = true
-
-[dependencies.titaniumdesktop]
-path = "./titanium/desktop"
-package = "titanium_desktop"
-optional = true
-
-[dependencies.titaniumweb]
-path = "./titanium/web"
-package = "titanium_web"
-optional = true
-
-[features]
-default = ["macros", "core"]
-
-core = ["titaniumcommon", "titaniumassets"]
-macros = ["titaniummacros", "async-std"]
-
-desktop = ["titaniumdesktop"]
-desktop-gui = ["desktop", "titaniumdesktop/gui"]
-desktop-memory = ["desktop", "titaniumdesktop/memory"]
-desktop-discord = ["desktop", "titaniumdesktop/discord"]
-desktop-keybinds = ["desktop", "titaniumdesktop/keybinds"]
-desktop-full = [
- "desktop-gui",
- "desktop-memory",
- "desktop-discord",
- "desktop-keybinds",
- "macros"
-]
-
-web = ["titaniumweb"]
-web-socket = ["web", "titaniumweb/socket"]
-web-full = ["web", "web-socket", "titaniumweb/full", "macros"]
+[package]
+name = "titanium"
+version = "1.0.0-alpha.5"
+edition = "2021"
+
+[lib]
+path = "titanium/src/lib.rs"
+
+#?RESERVED_FOR_TITANIUM_FRAMEWORK_START
+
+#?RESERVED_FOR_TITANIUM_FRAMEWORK_END
+
+[workspace]
+members = [
+ "titanium/assets",
+ "titanium/common",
+ "titanium/desktop",
+ "titanium/macros",
+ "titanium/utils",
+ "titanium/web",
+
+ #Examples
+ "examples/desktop_discord",
+ "examples/desktop_memory",
+ "examples/desktop_gui",
+ "examples/web",
+]
+
+[dependencies.titaniummacros]
+path = "./titanium/macros"
+package = "macros"
+optional = true
+
+[dependencies.async-std]
+features = ["attributes"]
+version = "1.12"
+optional = true
+
+[dependencies.titaniumutils]
+path = "./titanium/utils"
+package = "titanium_utils"
+default-features = false
+features = ["macros"]
+
+[dependencies.titaniumassets]
+path = "./titanium/assets"
+package = "titanium_assets"
+optional = true
+
+[dependencies.titaniumcommon]
+path = "./titanium/common"
+package = "titanium_common"
+optional = true
+
+[dependencies.titaniumdesktop]
+path = "./titanium/desktop"
+package = "titanium_desktop"
+optional = true
+
+[dependencies.titaniumweb]
+path = "./titanium/web"
+package = "titanium_web"
+optional = true
+
+[features]
+default = ["basic"]
+
+basic = ["macros", "core"]
+core = ["titaniumcommon", "titaniumassets"]
+macros = ["titaniummacros", "async-std"]
+
+# Desktop Target
+desktop = ["titaniumdesktop"]
+desktop-gui = ["desktop", "titaniumdesktop/gui"]
+desktop-memory = ["desktop", "titaniumdesktop/memory"]
+desktop-discord = ["desktop", "titaniumdesktop/discord"]
+desktop-keybinds = ["desktop", "titaniumdesktop/keybinds"]
+desktop-full = [
+ "desktop-gui",
+ "desktop-memory",
+ "desktop-discord",
+ "desktop-keybinds",
+ "macros"
+]
+
+# Web Target
+web = ["titaniumweb"]
+web-socket = ["web", "titaniumweb/socket"]
+web-full = ["web", "web-socket", "titaniumweb/full", "macros"]
+
+#!TITANIUM_OK
\ No newline at end of file
diff --git a/epearl b/epearl
new file mode 100755
index 0000000..0593974
--- /dev/null
+++ b/epearl
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+#
+# MIT License
+#
+# Copyright (c) 2022 AtomicGamer9523
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
+
+
+# ##########################################################################
+#
+# Enderpearl script for Most OSes
+#
+# ##########################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+exec "python" "$APP_HOME/x.py" "$@"
\ No newline at end of file
diff --git a/epearl.bat b/epearl.bat
new file mode 100644
index 0000000..0987336
--- /dev/null
+++ b/epearl.bat
@@ -0,0 +1,71 @@
+@REM !/bin/bat
+
+@REM
+@REM MIT License
+@REM
+@REM Copyright (c) 2022 AtomicGamer9523
+@REM
+@REM Permission is hereby granted, free of charge, to any person obtaining a copy
+@REM of this software and associated documentation files (the "Software"), to deal
+@REM in the Software without restriction, including without limitation the rights
+@REM to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+@REM copies of the Software, and to permit persons to whom the Software is
+@REM furnished to do so, subject to the following conditions:
+@REM
+@REM The above copyright notice and this permission notice shall be included in all
+@REM copies or substantial portions of the Software.
+@REM
+@REM THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+@REM IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+@REM FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+@REM AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+@REM LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+@REM OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+@REM SOFTWARE.
+@REM
+
+
+@echo off
+@REM ##########################################################################
+@REM
+@REM Enderpearl script for Windows
+@REM
+@REM ##########################################################################
+
+@REM Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@REM Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@REM Build
+set PYTHON_EXE=python.exe
+%PYTHON_EXE% --version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: no 'python' command could be found in your PATH.
+echo.
+echo Please install python
+
+:execute
+
+@REM Build Enderpearl
+"%PYTHON_EXE%" "%APP_HOME%\x.py" "%*%"
+
+:end
+@REM End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
\ No newline at end of file
diff --git a/examples/desktop_discord/main.rs b/examples/desktop_discord/main.rs
index 8fe4b65..db2494d 100644
--- a/examples/desktop_discord/main.rs
+++ b/examples/desktop_discord/main.rs
@@ -1,45 +1,19 @@
-use titanium::desktop::{
- discord::*,
- keybinds
-};
-
-fn main() {
- let mut drpc = DiscordClient::new(1090677709350391879);
-
- drpc.on_ready(|_ctx| {
- println!("ready?");
- });
-
- drpc.on_activity_join_request(|ctx| {
- println!("Join request: {:?}", ctx.event);
- });
-
- drpc.on_activity_join(|ctx| {
- println!("Joined: {:?}", ctx.event);
- });
-
- drpc.on_activity_spectate(|ctx| {
- println!("Spectate: {:?}", ctx.event);
- });
-
- let drpc_thread = drpc.start();
-
- println!("blocking!");
-
- drpc.block_until_event(DiscordEvent::Ready).unwrap();
-
- println!("ready!");
-
- // Set the activity
- drpc.set_activity(|act| act.state("rusting"))
- .expect("Failed to set activity");
-
+use titanium::desktop::{discord,keybinds};
+use discord::DiscordIpc;
+
+fn main() -> Result<(), Box> {
+ let mut client = discord::titanium()?;
+ loop {
+ if let Ok(_) = client.connect() { break };
+ println!("Failed to connect to Discord, retrying in 10 seconds...");
+ std::thread::sleep(std::time::Duration::from_secs(10));
+ }
+ client.set_activity(discord::titanium_activity())?;
keybinds::exit::set_handler(move || {
- println!("Exiting...");
- drpc.clear_activity().unwrap();
+ println!();println!("Exiting...");
+ client.close().expect("Failed to close client");
std::process::exit(0);
- })
- .unwrap();
+ })?;
- drpc_thread.join().unwrap();
-}
\ No newline at end of file
+ loop {}
+}
diff --git a/examples/jvm/Example.java b/examples/jvm/Example.java
new file mode 100644
index 0000000..73de1af
--- /dev/null
+++ b/examples/jvm/Example.java
@@ -0,0 +1,16 @@
+import net.titanium.composer.*;
+import net.titanium.events.*;
+import net.titanium.*;
+
+public class Example {
+ public static void main(String[] args) { new Main(args); }
+ Main(String[] args) {
+ Composer.subscribe($.EVENT_BUS,this);
+ $.init();
+ }
+
+ @EventHandler
+ private static void onInit(InitEvent event) {
+ $.LOG.info("Info");
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..3bbba31
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,7 @@
+# Allows 4 GigaBytes of memory for Gradle to use. Generally, the more the better.
+org.gradle.jvmargs=-Xmx4G
+
+# SLF4J is used for logging in the project.
+slf4j_version = 1.7+
+# Logback is used for logging in the project.
+logback_version = 1.4+
\ No newline at end of file
diff --git a/gradle/gradlew b/gradle/gradlew
new file mode 100755
index 0000000..c724835
--- /dev/null
+++ b/gradle/gradlew
@@ -0,0 +1,246 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && cd .. && pwd -P ) || exit
+
+echo "APP_HOME is $APP_HOME"
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradle/gradlew.bat b/gradle/gradlew.bat
new file mode 100644
index 0000000..0f6520f
--- /dev/null
+++ b/gradle/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..ccebba7
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..cfa5105
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
+networkTimeout=10000
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..9cf221f
--- /dev/null
+++ b/index.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..54776ff
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'Titanium'
+include('titanium')
diff --git a/titanium/__init__.py b/titanium/__init__.py
new file mode 100644
index 0000000..8c50fa4
--- /dev/null
+++ b/titanium/__init__.py
@@ -0,0 +1,66 @@
+if __name__ != "titanium": raise Exception("This file is not meant to be run directly")
+from titanium.commands import *
+from titanium.utils import *
+
+VERSION = "1.0.0-alpha.5"
+
+COMMANDS.add(HELP)
+COMMANDS.add(RUN)
+COMMANDS.add(BUILD)
+COMMANDS.add(CLEAN)
+COMMANDS.add(START)
+
+def command_parser(cmd: str, args: list[str]) -> bool:
+ METADATA = Metadata.get(args)
+
+ if check_version(cmd, args): return HANDLED
+
+ if cmd.startswith("help") or cmd.startswith("h"):
+ if HELP.run(args): return HANDLED
+
+ elif cmd.startswith("build") or cmd.startswith("b"):
+ if BUILD.run(args, METADATA): return HANDLED
+
+ elif cmd.startswith("clean") or cmd.startswith("c"):
+ if CLEAN.run(args): return HANDLED
+
+ elif cmd.startswith("start") or cmd.startswith("s"):
+ if START.run(args): return HANDLED
+
+ elif cmd.startswith("run") or cmd.startswith("r"):
+ if RUN.run(args): return HANDLED
+
+ elif cmd.startswith("version") or cmd.startswith("v"):
+ print(f"Titanium version {VERSION}")
+ return HANDLED
+
+ elif cmd.startswith("installDeps"):
+ return INSTALL_DEPS.run(args)
+
+ return NOT_HANDLED
+
+def helper() -> str: return COMMANDS.as_list_help()
+
+#
+# MIT License
+#
+# Copyright (c) 2022 AtomicGamer9523
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
diff --git a/titanium/assets/Cargo.toml b/titanium/assets/Cargo.toml
index bb83428..961c89a 100644
--- a/titanium/assets/Cargo.toml
+++ b/titanium/assets/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "titanium_assets"
-version = "1.0.0-alpha.4"
+version = "1.0.0-alpha.5"
edition = "2021"
[lib]
diff --git a/titanium/build.gradle b/titanium/build.gradle
new file mode 100644
index 0000000..149df60
--- /dev/null
+++ b/titanium/build.gradle
@@ -0,0 +1,25 @@
+plugins {
+ id 'java'
+ id 'application'
+ id 'java-library'
+}
+
+project.buildDir = '../.build/java'
+
+application {
+ mainClass = 'main.Main'
+}
+
+compileJava {
+ options.encoding = 'UTF-8'
+ options.compilerArgs += ["-Xlint:none"]
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation "org.slf4j:slf4j-api:${project.slf4j_version}"
+ implementation "ch.qos.logback:logback-classic:${project.logback_version}"
+}
diff --git a/titanium/commands.py b/titanium/commands.py
new file mode 100644
index 0000000..f726728
--- /dev/null
+++ b/titanium/commands.py
@@ -0,0 +1,256 @@
+if __name__ != "titanium.commands": raise Exception("This file is not meant to be run directly")
+
+from titanium.utils import *
+import platform, os
+
+class BuildCommand(Command):
+ def __init__(self, target_triple: str = "", release: bool = False, target: BuildTarget = BuildTarget.NONE_OR_INVALID) -> None:
+ opsy: str = ""
+ try: opsy = target_triple.split("-")[2].capitalize()
+ except Exception: opsy = "Windows" if platform.system() == "Windows" else "Linux"
+ PREFIX = (".\\" if opsy == "Windows" else "./") + "epearl"
+ super().__init__("build",
+ simple = "builds the project",
+ description = f"""Builds the project. Allows you to specify the {COLORS.OKCYAN}platform{COLORS.ENDC}, {COLORS.OKCYAN}target{COLORS.ENDC}, and {COLORS.OKCYAN}build type{COLORS.ENDC}.
+ The default {COLORS.OKCYAN}platform{COLORS.ENDC} is {COLORS.WARNING}the current platform{COLORS.ENDC} ({COLORS.HEADER}{opsy}{COLORS.ENDC}).
+ The default {COLORS.OKCYAN}build type{COLORS.ENDC} is {COLORS.WARNING}debug{COLORS.ENDC} ({COLORS.HEADER}{("release" if release else "debug")}{COLORS.ENDC}).
+ The default {COLORS.OKCYAN}target{COLORS.ENDC} is {COLORS.WARNING}desktop{COLORS.ENDC} ({COLORS.HEADER}{target.n()}{COLORS.ENDC}).
+
+{COLORS.GRAY}Examples:{COLORS.ENDC}
+{PREFIX} build --status {COLORS.GRAY} Prints the status and the default output of the build{COLORS.ENDC}
+{PREFIX} build desktop {COLORS.GRAY} Builds for the desktop target in debug mode{COLORS.ENDC}
+{PREFIX} build web --debug {COLORS.GRAY} Builds for the web target in debug mode{COLORS.ENDC}
+{PREFIX} build JVM --release {COLORS.GRAY} Builds for the JVM target in release mode{COLORS.ENDC}
+""",
+ color = COLORS.OKBLUE
+ )
+ def run(self, cliargs: list[str], metadata: Metadata = Metadata.empty()) -> bool:
+ if h(self.name, cliargs): return HANDLED
+ if cliargs[0] == "--status":
+ LOG.log(COLORS.OKCYAN + "Build Status:" + COLORS.ENDC + f""" Building for {COLORS.HEADER}{metadata.target.n()}{COLORS.ENDC} on {COLORS.HEADER}{metadata.triple}{COLORS.ENDC} in {COLORS.HEADER}{'release' if metadata.release else 'debug'}{COLORS.ENDC} mode.""")
+ return HANDLED
+ full_build = False
+ if "full" in cliargs:
+ full_build = True
+ LOG.log("Full build requested! THIS WILL TAKE A WHILE!")
+
+ def build_desktop(full_build: bool = False):
+ config: list[str] = (
+ ["--config \"target.'cfg(not(target_os=\\\"windows\\\"))'.runner='sudo'\""]
+ if metadata.triple.split("-")[2] == "linux" else
+ []
+ )
+ cmd = f"""cargo +nightly {(" ".join(config))} build -Z unstable-options --target-dir .build/rust --out-dir .build/bin --target {metadata.triple} {"" if not full_build else "--all-features"}"""
+ failableRun(cmd)
+ return HANDLED
+
+ def build_web():
+ LOG.log("Building for the web target is not yet supported!")
+ return HANDLED
+
+ def build_jvm():
+ LOG.log("Building for the JVM target is not yet supported!")
+ return HANDLED
+
+ if full_build:
+ LOG.log("Full build requested! THIS WILL TAKE A WHILE!")
+ LOG.log("Cleaning...")
+ CLEAN.run([])
+ LOG.log("Building...")
+ build_desktop(True)
+ build_web()
+ build_jvm()
+ LOG.log("Done!")
+ return HANDLED
+
+ buildtarget = BuildTarget.parse(cliargs[0])
+ if buildtarget == BuildTarget.NONE_OR_INVALID:
+ LOG.log(f"""{COLORS.FAIL}'{COLORS.OKBLUE}{cliargs[0]}{COLORS.FAIL}' doesn't seem to be a valid target! Valid Targets include: '{COLORS.OKCYAN}desktop{COLORS.FAIL}', '{COLORS.OKCYAN}web{COLORS.FAIL}', and '{COLORS.OKCYAN}jvm{COLORS.FAIL}'{COLORS.ENDC}""")
+ return HANDLED
+
+ if buildtarget == BuildTarget.Desktop: return build_desktop()
+ if buildtarget == BuildTarget.Web: return build_web()
+ if buildtarget == BuildTarget.JVM: return build_jvm()
+
+ return HANDLED
+class CleanCommand(Command):
+ def __init__(self) -> None: super().__init__("clean",
+ simple = "cleans the project",
+ description = f"""Cleans the project. Deletes the {COLORS.OKCYAN}.build{COLORS.ENDC} folder.""",
+ color = COLORS.OKBLUE
+ )
+ def run(self, cliargs: list[str]) -> bool:
+ import shutil, os
+ if h(self.name, cliargs, False): return HANDLED
+ LOG.log("Cleaning...")
+ CMD = "cargo +nightly -Z unstable-options clean --quiet"
+ failableRun(CMD)
+ try:
+ shutil.rmtree(".build")
+ shutil.rmtree("./titanium/utils/__pycache__")
+ shutil.rmtree("./titanium/__pycache__")
+ except Exception: pass
+ try: os.remove("Cargo.lock")
+ except Exception: pass
+ LOG.log("Done!")
+ return HANDLED
+class HelpCommand(Command):
+ def __init__(self) -> None: super().__init__("help",
+ simple = "shows this help message",
+ description = f"""Shows this help message.
+ If you need help with a specific command, add a {COLORS.HEADER}-h{COLORS.ENDC} to the end.""",
+ color = COLORS.OKGREEN
+ )
+ def run(self, cliargs: list[str]) -> bool:
+ if h(self.name, cliargs, False): return HANDLED
+ LOG.log(COMMANDS.as_list_help())
+ return HANDLED
+class StartCommand(Command):
+ def __init__(self) -> None: super().__init__("start",
+ simple = "sets up the project",
+ description = f"""Sets up the project. Can be either {COLORS.OKCYAN}lib{COLORS.ENDC} or {COLORS.OKCYAN}framework{COLORS.ENDC}.
+ Use {COLORS.OKCYAN}lib{COLORS.ENDC} if you are going to import titanium into another project as a dependency.
+ Use {COLORS.OKCYAN}framework{COLORS.ENDC} if you are going to use titanium and its dependencies as a framework."""
+ ,
+ color = COLORS.OKCYAN
+ )
+ def run(self, cliargs: list[str]) -> bool:
+ if h(self.name, cliargs): return HANDLED
+
+ def as_framework() -> bool:
+ if os.path.exists("main"):
+ LOG.log("Project already set up as a framework!")
+ return HANDLED
+ LOG.log("Setting up project as a framework...")
+ try:
+ try: os.mkdir("main")
+ except Exception: pass
+ mainFile = open("main/main.rs","wt")
+ mainFile.write("use titanium::*;\n\nfn main() {\n hello_world!();\n}")
+ mainFile.close()
+ except Exception: pass
+ f = open("Cargo.toml", "r")
+ OLD_FILE = f.read()
+ f.close()
+ f = open("Cargo.toml", "r")
+ newData = f.readlines()
+ f.close()
+ shouldWriteNextLine = False
+ for line in range(len(newData)):
+ if shouldWriteNextLine:
+ newData[line] = "[[bin]]\npath = \"main/main.rs\"\nname = \"main\"\n"
+ shouldWriteNextLine = False
+ if "#"+"?RESERVED_FOR_TITANIUM_FRAMEWORK_START" in newData[line]: shouldWriteNextLine = True
+ if "#"+"?RESERVED_FOR_TITANIUM_FRAMEWORK_END" in newData[line]: shouldWriteNextLine = False
+ FILE = open("Cargo.toml", "wt")
+ FILE.write("".join(newData))
+ FILE.close()
+ backup = open("Cargo.toml", "rt")
+ backupData = backup.read()
+ if ("#" + "!TITANIUM_OK") not in backupData:
+ LOG.log("File not updated successfully, rolling back...")
+ f = open("Cargo.toml", "wt")
+ f.write(OLD_FILE)
+ f.close()
+ backup.close()
+ failableRun("cargo +nightly -Z unstable-options check")
+ LOG.log("Done!")
+ return HANDLED
+
+ def as_lib() -> bool:
+ LOG.log("Setting up project as a library...")
+ if os.path.exists("main"):
+ import shutil
+ try: shutil.rmtree("main")
+ except Exception: pass
+ f = open("Cargo.toml", "r")
+ OLD_FILE = f.read()
+ f.close()
+ f = open("Cargo.toml", "r")
+ newData = f.readlines()
+ f.close()
+ shouldWriteNextLine = False
+ for line in range(len(newData)):
+ if shouldWriteNextLine:
+ newData[line] = ""
+ newData[line+1] = ""
+ newData[line+2] = "\n"
+ shouldWriteNextLine = False
+ if "#"+"?RESERVED_FOR_TITANIUM_FRAMEWORK_START" in newData[line]: shouldWriteNextLine = True
+ if "#"+"?RESERVED_FOR_TITANIUM_FRAMEWORK_END" in newData[line]: shouldWriteNextLine = False
+ FILE = open("Cargo.toml", "wt")
+ FILE.write("".join(newData))
+ FILE.close()
+ backup = open("Cargo.toml", "rt")
+ backupData = backup.read()
+ if ("#" + "!TITANIUM_OK") not in backupData:
+ LOG.log("File not updated successfully, rolling back...")
+ LOG.log();LOG.log();LOG.log(f"{COLORS.FAIL}THIS IS A BUG, PLEASE REPORT IT TO THE TITANIUM DEVELOPERS{COLORS.ENDC}")
+ f = open("Cargo.toml", "wt")
+ f.write(OLD_FILE)
+ f.close()
+ backup.close()
+ LOG.log("Done!")
+ return HANDLED
+
+ if "lib" in cliargs[0]: return as_lib()
+ if "framework" in cliargs[0]: return as_framework()
+ return HANDLED
+class RunCommand(Command):
+ def __init__(self) -> None: super().__init__("run",
+ simple = "runs the project",
+ description = f"""Runs the project.""",
+ color = COLORS.OKBLUE
+ )
+ def run(self, cliargs: list[str]) -> bool:
+ if h(self.name, cliargs, False): return HANDLED
+ if os.path.exists("main"):
+ CMD = "RUSTFLAGS=\"-Z macro-backtrace\" cargo +nightly -Z unstable-options run"
+ failableRun(CMD)
+ else:
+ LOG.log("Project is not set up as a framework!")
+ LOG.log("Use the start command to set it up as a framework.")
+ return HANDLED
+class InstallDepsCommand(Command):
+ def __init__(self) -> None: super().__init__("install-deps",
+ simple = "installs dependencies",
+ description = f"""Installs dependencies. This command is the same as {COLORS.OKCYAN}cargo +nightly -Z unstable-options check{COLORS.ENDC}""",
+ color = COLORS.OKGREEN
+ )
+ def run(self, cliargs: list[str]) -> bool:
+ meta = Metadata.get([])
+ if "linux" in meta.triple:
+ failableRun("sudo apt-get install libgtk-3-dev libsdl-pango-dev libx11-dev libxtst-dev libudev-dev libinput-dev -y")
+ return HANDLED
+
+BUILD = BuildCommand()
+CLEAN = CleanCommand()
+HELP = HelpCommand()
+START = StartCommand()
+RUN = RunCommand()
+INSTALL_DEPS = InstallDepsCommand()
+
+
+#
+# MIT License
+#
+# Copyright (c) 2022 AtomicGamer9523
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
diff --git a/titanium/common/Cargo.toml b/titanium/common/Cargo.toml
index 298f92d..805a755 100644
--- a/titanium/common/Cargo.toml
+++ b/titanium/common/Cargo.toml
@@ -1,4 +1,4 @@
[package]
name = "titanium_common"
-version = "1.0.0-alpha.4"
+version = "1.0.0-alpha.5"
edition = "2021"
diff --git a/titanium/config/windows.manifest.rc b/titanium/config/windows.manifest.rc
new file mode 100644
index 0000000..c7d5097
--- /dev/null
+++ b/titanium/config/windows.manifest.rc
@@ -0,0 +1,2 @@
+#define RT_MANIFEST 24
+1 RT_MANIFEST "./titanium/config/windows.manifest.xml"
\ No newline at end of file
diff --git a/titanium/config/windows.manifest.xml b/titanium/config/windows.manifest.xml
new file mode 100644
index 0000000..b30241b
--- /dev/null
+++ b/titanium/config/windows.manifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/titanium/desktop/Cargo.toml b/titanium/desktop/Cargo.toml
index d95d88c..5f776d1 100644
--- a/titanium/desktop/Cargo.toml
+++ b/titanium/desktop/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "titanium_desktop"
-version = "1.0.0-alpha.4"
+version = "1.0.0-alpha.5"
edition = "2021"
[lib]
diff --git a/titanium/desktop/discord/Cargo.toml b/titanium/desktop/discord/Cargo.toml
index 96b076e..1869703 100644
--- a/titanium/desktop/discord/Cargo.toml
+++ b/titanium/desktop/discord/Cargo.toml
@@ -1,7 +1,10 @@
[package]
name = "titanium_desktop_discord"
-version = "1.0.0-alpha.4"
+version = "1.0.0-alpha.5"
edition = "2021"
[dependencies]
-discord-presence = "0.5"
\ No newline at end of file
+serde_json = "1.0"
+serde = "1.0"
+serde_derive = "1.0"
+uuid = { version = "0.8", features = ["v4"] }
\ No newline at end of file
diff --git a/titanium/desktop/discord/src/discord_ipc.rs b/titanium/desktop/discord/src/discord_ipc.rs
new file mode 100644
index 0000000..e2800b6
--- /dev/null
+++ b/titanium/desktop/discord/src/discord_ipc.rs
@@ -0,0 +1,204 @@
+use crate::{
+ models::Activity,
+ pack_unpack::{pack, unpack},
+};
+use serde_json::{json, Value};
+use std::error::Error;
+use uuid::Uuid;
+
+type Result = std::result::Result>;
+
+/// A client that connects to and communicates with the Discord IPC.
+///
+/// Implemented via the [`DiscordRPCClient`](struct@crate::DiscordRPCClient) struct.
+pub trait DiscordIpc {
+ /// Connects the client to the Discord IPC.
+ ///
+ /// This method attempts to first establish a connection,
+ /// and then sends a handshake.
+ ///
+ /// # Errors
+ ///
+ /// Returns an `Err` variant if the client
+ /// fails to connect to the socket, or if it fails to
+ /// send a handshake.
+ ///
+ /// # Examples
+ /// ```
+ /// let mut client = discord_rich_presence::new_client("")?;
+ /// client.connect()?;
+ /// ```
+ fn connect(&mut self) -> Result<()> {
+ self.connect_ipc()?;
+ self.send_handshake()?;
+
+ Ok(())
+ }
+
+ /// Reconnects to the Discord IPC.
+ ///
+ /// This method closes the client's active connection,
+ /// then re-connects it and re-sends a handshake.
+ ///
+ /// # Errors
+ ///
+ /// Returns an `Err` variant if the client
+ /// failed to connect to the socket, or if it failed to
+ /// send a handshake.
+ ///
+ /// # Examples
+ /// ```
+ /// let mut client = discord_rich_presence::new_client("")?;
+ /// client.connect()?;
+ ///
+ /// client.close()?;
+ /// client.reconnect()?;
+ /// ```
+ fn reconnect(&mut self) -> Result<()> {
+ self.close()?;
+ self.connect_ipc()?;
+ self.send_handshake()?;
+
+ Ok(())
+ }
+
+ #[doc(hidden)]
+ fn get_client_id(&self) -> &String;
+
+ #[doc(hidden)]
+ fn connect_ipc(&mut self) -> Result<()>;
+
+ /// Handshakes the Discord IPC.
+ ///
+ /// This method sends the handshake signal to the IPC.
+ /// It is usually not called manually, as it is automatically
+ /// called by [`connect`] and/or [`reconnect`].
+ ///
+ /// [`connect`]: #method.connect
+ /// [`reconnect`]: #method.reconnect
+ ///
+ /// # Errors
+ ///
+ /// Returns an `Err` variant if sending the handshake failed.
+ fn send_handshake(&mut self) -> Result<()> {
+ self.send(
+ json!({
+ "v": 1,
+ "client_id": self.get_client_id()
+ }),
+ 0,
+ )?;
+ // TODO: Return an Err if the handshake is rejected
+ self.recv()?;
+
+ Ok(())
+ }
+
+ /// Sends JSON data to the Discord IPC.
+ ///
+ /// This method takes data (`serde_json::Value`) and
+ /// an opcode as its parameters.
+ ///
+ /// # Errors
+ /// Returns an `Err` variant if writing to the socket failed
+ ///
+ /// # Examples
+ /// ```
+ /// let payload = serde_json::json!({ "field": "value" });
+ /// client.send(payload, 0)?;
+ /// ```
+ fn send(&mut self, data: Value, opcode: u8) -> Result<()> {
+ let data_string = data.to_string();
+ let header = pack(opcode.into(), data_string.len() as u32)?;
+
+ self.write(&header)?;
+ self.write(data_string.as_bytes())?;
+
+ Ok(())
+ }
+
+ #[doc(hidden)]
+ fn write(&mut self, data: &[u8]) -> Result<()>;
+
+ /// Receives an opcode and JSON data from the Discord IPC.
+ ///
+ /// This method returns any data received from the IPC.
+ /// It returns a tuple containing the opcode, and the JSON data.
+ ///
+ /// # Errors
+ /// Returns an `Err` variant if reading the socket was
+ /// unsuccessful.
+ ///
+ /// # Examples
+ /// ```
+ /// client.connect_ipc()?;
+ /// client.send_handshake()?;
+ ///
+ /// println!("{:?}", client.recv()?);
+ /// ```
+ fn recv(&mut self) -> Result<(u32, Value)> {
+ let mut header = [0; 8];
+
+ self.read(&mut header)?;
+ let (op, length) = unpack(header.to_vec())?;
+
+ let mut data = vec![0u8; length as usize];
+ self.read(&mut data)?;
+
+ let response = String::from_utf8(data.to_vec())?;
+ let json_data = serde_json::from_str::(&response)?;
+
+ Ok((op, json_data))
+ }
+
+ #[doc(hidden)]
+ fn read(&mut self, buffer: &mut [u8]) -> Result<()>;
+
+ /// Sets a Discord activity.
+ ///
+ /// This method is an abstraction of [`send`],
+ /// wrapping it such that only an activity payload
+ /// is required.
+ ///
+ /// [`send`]: #method.send
+ ///
+ /// # Errors
+ /// Returns an `Err` variant if sending the payload failed.
+ fn set_activity(&mut self, activity_payload: Activity) -> Result<()> {
+ let data = json!({
+ "cmd": "SET_ACTIVITY",
+ "args": {
+ "pid": std::process::id(),
+ "activity": activity_payload
+ },
+ "nonce": Uuid::new_v4().to_string()
+ });
+ self.send(data, 1)?;
+
+ Ok(())
+ }
+
+ /// Works the same as as [`set_activity`] but clears activity instead.
+ ///
+ /// [`set_activity`]: #method.set_activity
+ ///
+ /// # Errors
+ /// Returns an `Err` variant if sending the payload failed.
+ fn clear_activity(&mut self) -> Result<()> {
+ let data = json!({
+ "cmd": "SET_ACTIVITY",
+ "args": {
+ "pid": std::process::id(),
+ "activity": None::<()>
+ },
+ "nonce": Uuid::new_v4().to_string()
+ });
+
+ self.send(data, 1)?;
+
+ Ok(())
+ }
+
+ /// Closes the Discord IPC connection. Implementation is dependent on platform.
+ fn close(&mut self) -> Result<()>;
+}
\ No newline at end of file
diff --git a/titanium/desktop/discord/src/ipc_unix.rs b/titanium/desktop/discord/src/ipc_unix.rs
new file mode 100644
index 0000000..1e2f097
--- /dev/null
+++ b/titanium/desktop/discord/src/ipc_unix.rs
@@ -0,0 +1,112 @@
+use crate::discord_ipc::DiscordIpc;
+use serde_json::json;
+use std::os::unix::net::UnixStream;
+use std::{
+ env::var,
+ error::Error,
+ io::{Read, Write},
+ net::Shutdown,
+ path::PathBuf,
+};
+
+// Environment keys to search for the Discord pipe
+const ENV_KEYS: [&str; 4] = ["XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"];
+
+type Result = std::result::Result>;
+
+/// A wrapper struct for the functionality contained in the
+/// underlying [`DiscordIpc`](trait@DiscordIpc) trait.
+#[allow(dead_code)]
+#[derive(Debug)]
+pub struct DiscordRPCClient {
+ /// Client ID of the IPC client.
+ pub client_id: String,
+ connected: bool,
+ socket: Option,
+}
+
+impl DiscordRPCClient {
+ /// Creates a new `DiscordRPCClient`.
+ ///
+ /// # Examples
+ /// ```
+ /// let ipc_client = DiscordRPCClient::new("")?;
+ /// ```
+ pub fn new(client_id: &str) -> Result {
+ let client = Self {
+ client_id: client_id.to_string(),
+ connected: false,
+ socket: None,
+ };
+
+ Ok(client)
+ }
+
+ fn get_pipe_pattern() -> PathBuf {
+ let mut path = String::new();
+
+ for key in &ENV_KEYS {
+ match var(key) {
+ Ok(val) => {
+ path = val;
+ break;
+ }
+ Err(_e) => continue,
+ }
+ }
+ PathBuf::from(path)
+ }
+}
+
+impl DiscordIpc for DiscordRPCClient {
+ fn connect_ipc(&mut self) -> Result<()> {
+ for i in 0..10 {
+ let path = DiscordRPCClient::get_pipe_pattern().join(format!("discord-ipc-{}", i));
+
+ match UnixStream::connect(&path) {
+ Ok(socket) => {
+ self.socket = Some(socket);
+ return Ok(());
+ }
+ Err(_) => continue,
+ }
+ }
+
+ Err("Couldn't connect to the Discord IPC socket".into())
+ }
+
+ fn write(&mut self, data: &[u8]) -> Result<()> {
+ let socket = self.socket.as_mut().expect("Client not connected");
+
+ socket.write_all(data)?;
+
+ Ok(())
+ }
+
+ fn read(&mut self, buffer: &mut [u8]) -> Result<()> {
+ let socket = self.socket.as_mut().unwrap();
+
+ socket.read_exact(buffer)?;
+
+ Ok(())
+ }
+
+ fn close(&mut self) -> Result<()> {
+ let data = json!({});
+ if self.send(data, 2).is_ok() {}
+
+ let socket = self.socket.as_mut().unwrap();
+
+ socket.flush()?;
+ match socket.shutdown(Shutdown::Both) {
+ Ok(()) => (),
+ Err(_err) => (),
+ };
+
+ Ok(())
+ }
+
+ fn get_client_id(&self) -> &String {
+ &self.client_id
+ }
+}
\ No newline at end of file
diff --git a/titanium/desktop/discord/src/ipc_windows.rs b/titanium/desktop/discord/src/ipc_windows.rs
new file mode 100644
index 0000000..5781cd3
--- /dev/null
+++ b/titanium/desktop/discord/src/ipc_windows.rs
@@ -0,0 +1,88 @@
+use crate::discord_ipc::DiscordIpc;
+use serde_json::json;
+use std::{
+ error::Error,
+ fs::{File, OpenOptions},
+ io::{Read, Write},
+ os::windows::fs::OpenOptionsExt,
+ path::PathBuf,
+};
+
+type Result = std::result::Result>;
+
+/// A wrapper struct for the functionality contained in the
+/// underlying [`DiscordIpc`](trait@DiscordIpc) trait.
+#[allow(dead_code)]
+#[derive(Debug)]
+pub struct DiscordRPCClient {
+ /// Client ID of the IPC client.
+ pub client_id: String,
+ connected: bool,
+ socket: Option,
+}
+
+impl DiscordRPCClient {
+ /// Creates a new `DiscordRPCClient`.
+ ///
+ /// # Examples
+ /// ```
+ /// let ipc_client = DiscordRPCClient::new("")?;
+ /// ```
+ pub fn new(client_id: &str) -> Result {
+ let client = Self {
+ client_id: client_id.to_string(),
+ connected: false,
+ socket: None,
+ };
+
+ Ok(client)
+ }
+}
+
+impl DiscordIpc for DiscordRPCClient {
+ fn connect_ipc(&mut self) -> Result<()> {
+ for i in 0..10 {
+ let path = PathBuf::from(format!(r"\\?\pipe\discord-ipc-{}", i));
+
+ match OpenOptions::new().access_mode(0x3).open(&path) {
+ Ok(handle) => {
+ self.socket = Some(handle);
+ return Ok(());
+ }
+ Err(_) => continue,
+ }
+ }
+
+ Err("Couldn't connect to the Discord IPC socket".into())
+ }
+
+ fn write(&mut self, data: &[u8]) -> Result<()> {
+ let socket = self.socket.as_mut().expect("Client not connected");
+
+ socket.write_all(data)?;
+
+ Ok(())
+ }
+
+ fn read(&mut self, buffer: &mut [u8]) -> Result<()> {
+ let socket = self.socket.as_mut().unwrap();
+
+ socket.read_exact(buffer)?;
+
+ Ok(())
+ }
+
+ fn close(&mut self) -> Result<()> {
+ let data = json!({});
+ if self.send(data, 2).is_ok() {}
+
+ let socket = self.socket.as_mut().unwrap();
+ socket.flush()?;
+
+ Ok(())
+ }
+
+ fn get_client_id(&self) -> &String {
+ &self.client_id
+ }
+}
\ No newline at end of file
diff --git a/titanium/desktop/discord/src/lib.rs b/titanium/desktop/discord/src/lib.rs
index 823e5af..421ce23 100644
--- a/titanium/desktop/discord/src/lib.rs
+++ b/titanium/desktop/discord/src/lib.rs
@@ -2,14 +2,59 @@
#![deny(missing_docs,missing_debug_implementations,unused,clippy::all)]
-pub use discord_presence::{
- Client as DiscordClient,
- Result as DiscordResult,
- Event as DiscordEvent,
- DiscordError
-};
-
-/// Discord Rich Presence models.
-pub mod models {
- pub use discord_presence::models::*;
-}
\ No newline at end of file
+/// The client ID of the Titanium Discord application.
+pub const CLIENT_ID: &str = "1090677709350391879";
+
+mod discord_ipc;
+mod pack_unpack;
+pub use discord_ipc::*;
+pub mod models;
+
+#[cfg(unix)]
+mod ipc_unix;
+#[cfg(unix)]
+use ipc_unix as ipc;
+
+#[cfg(windows)]
+mod ipc_windows;
+#[cfg(windows)]
+use ipc_windows as ipc;
+
+pub use ipc::DiscordRPCClient;
+
+/// Creates a new that is automatically connected to the Titanium Discord application.
+#[inline]
+#[must_use = "This function returns a new client"]
+pub fn titanium() -> Result> {
+ DiscordRPCClient::new(CLIENT_ID)
+}
+
+/// Creates a new activity for the Titanium Discord application.
+#[inline]
+#[must_use = "This function returns a new activity"]
+pub fn titanium_activity<'a>() -> models::Activity<'a> {
+ models::Activity::new()
+ .timestamps(models::Timestamps::new()
+ .start(time())
+ )
+ .state("Universal Game Client")
+ .assets(models::Assets::new()
+ .large_image("titanium")
+ .large_text("Titanium Client")
+ .small_image("dbug")
+ .small_text("Developing")
+ )
+ .buttons(vec![
+ models::Button::new("GitHub","https://github.com/TBBOFBD/Titanium"),
+ models::Button::new("Discord","https://discord.gg/mh9VYsCrPT")
+ ])
+}
+
+fn time() -> i64 {
+ use std::time::{SystemTime, UNIX_EPOCH};
+ let start = SystemTime::now();
+ let since_the_epoch = start
+ .duration_since(UNIX_EPOCH)
+ .expect("Time went backwards");
+ since_the_epoch.as_secs() as i64
+}
diff --git a/titanium/desktop/discord/src/models.rs b/titanium/desktop/discord/src/models.rs
new file mode 100644
index 0000000..80eb111
--- /dev/null
+++ b/titanium/desktop/discord/src/models.rs
@@ -0,0 +1,337 @@
+//! Provides an interface for building activities to send
+//! to Discord via [`DiscordIpc::set_activity`](crate::DiscordIpc::set_activity).
+use serde_derive::Serialize;
+
+/// A struct representing a Discord rich presence activity
+///
+/// Note that all methods return `Self`, and can be chained
+/// for fluency
+#[derive(Debug, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Activity<'a> {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ state: Option<&'a str>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ details: Option<&'a str>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ timestamps: Option,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ party: Option>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ assets: Option>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ secrets: Option>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ buttons: Option>>,
+}
+
+/// A struct representing an `Activity`'s timestamps
+///
+/// Note that all methods return `Self`, and can be chained
+/// for fluency
+#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Timestamps {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ start: Option,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ end: Option,
+}
+
+/// A struct representing an `Activity`'s game party
+///
+/// Note that all methods return `Self`, and can be chained
+/// for fluency
+#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Party<'a> {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ id: Option<&'a str>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ size: Option<[i32; 2]>,
+}
+
+/// A struct representing the art assets and hover text
+/// used by an `Activity`
+///
+/// Note that all methods return `Self`, and can be chained
+/// for fluency
+#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Assets<'a> {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ large_image: Option<&'a str>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ large_text: Option<&'a str>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ small_image: Option<&'a str>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ small_text: Option<&'a str>,
+}
+
+/// A struct representing the secrets used by an
+/// `Activity`
+///
+/// Note that all methods return `Self`, and can be chained
+/// for fluency
+#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Secrets<'a> {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ join: Option<&'a str>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ spectate: Option<&'a str>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ r#match: Option<&'a str>,
+}
+
+/// A struct representing the buttons that are
+/// attached to an `Activity`
+///
+/// An activity may have a maximum of 2 buttons
+#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Button<'a> {
+ label: &'a str,
+ url: &'a str,
+}
+
+impl<'a> Activity<'a> {
+ /// Creates a new `Activity`
+ pub fn new() -> Self {
+ Activity {
+ state: None,
+ details: None,
+ assets: None,
+ buttons: None,
+ party: None,
+ secrets: None,
+ timestamps: None,
+ }
+ }
+
+ /// Sets the state of the activity
+ pub fn state(mut self, state: &'a str) -> Self {
+ self.state = Some(state);
+ self
+ }
+
+ /// Sets the details of the activity
+ pub fn details(mut self, details: &'a str) -> Self {
+ self.details = Some(details);
+ self
+ }
+
+ /// Add a `Timestamps` to this activity
+ pub fn timestamps(mut self, timestamps: Timestamps) -> Self {
+ self.timestamps = Some(timestamps);
+ self
+ }
+
+ /// Add a `Party` to this activity
+ pub fn party(mut self, party: Party<'a>) -> Self {
+ self.party = Some(party);
+ self
+ }
+
+ /// Add an `Assets` to this activity
+ pub fn assets(mut self, assets: Assets<'a>) -> Self {
+ self.assets = Some(assets);
+ self
+ }
+
+ /// Add a `Secrets` to this activity
+ pub fn secrets(mut self, secrets: Secrets<'a>) -> Self {
+ self.secrets = Some(secrets);
+ self
+ }
+
+ /// Add a `Vec` of `Button`s to this activity
+ ///
+ /// An activity may contain no more than 2 buttons
+ pub fn buttons(mut self, buttons: Vec