From 5ea4cea03f7c9c25f0bcd0d9f78421aae63206c3 Mon Sep 17 00:00:00 2001
From: JordanViknar <74505993+JordanViknar@users.noreply.github.com>
Date: Sun, 1 Sep 2024 16:16:33 +0200
Subject: [PATCH 01/20] feat: Severely unfinished Lune port
---
.vscode/settings.json | 6 +
Makefile | 63 -
README.md | 184 ---
general/fs_utils.luau | 105 ++
general/init.luau | 5 +
.../logSystem.lua => general/log_system.luau | 56 +-
install/archlinux/PKGBUILD | 48 -
main.lua | 131 --
main.luau | 38 +
metadata/init.luau | 20 +
modules/config/configManager.lua | 125 --
modules/config/defaultConfigTemplate.lua | 90 --
modules/extra/programMetadata.lua | 17 -
modules/gameLauncher.lua | 103 --
modules/general/fsUtils.lua | 188 ---
modules/general/systemUtils.lua | 72 -
modules/objects/steamGameObject.lua | 62 -
modules/objects/toolObject.lua | 24 -
modules/steam/protonDBManager.lua | 41 -
modules/steam/steamConfigProvider.lua | 144 --
modules/steam/steamUtils.lua | 44 -
modules/tools/gamemode.lua | 10 -
modules/tools/gamescope.lua | 59 -
modules/tools/mangohud.lua | 10 -
modules/tools/obs-gamecapture.lua | 17 -
modules/tools/switcherooctl.lua | 15 -
modules/tools/zink.lua | 15 -
modules/ui/UItoSettingsList.lua | 52 -
modules/ui/aboutWindow.lua | 73 -
modules/ui/gameSettingsOverview.lua | 167 ---
modules/ui/lgiHelper.lua | 77 --
modules/ui/mainWindow.lua | 236 ----
modules/utilitiesList.lua | 15 -
sst | 27 -
steam/init.luau | 4 +
steam/steam_config_provider.luau | 167 +++
steam/steam_utils.luau | 39 +
.../vdfParser.lua => steam/vdf_parser.luau | 100 +-
ui/definitions/cleanerPage.blp | 10 -
ui/definitions/commandPage.blp | 10 -
ui/definitions/gamescopePage.blp | 215 ---
ui/definitions/mainWindow.blp | 147 --
ui/definitions/overviewPage.blp | 279 ----
ui/definitions/protonPage.blp | 225 ----
ui/definitions/settingsPage.blp | 59 -
ui/definitions/utilitiesPage.blp | 93 --
ui/main.ui | 1190 -----------------
ui/update-ui.lua | 81 --
48 files changed, 462 insertions(+), 4496 deletions(-)
create mode 100644 .vscode/settings.json
delete mode 100644 Makefile
delete mode 100644 README.md
create mode 100644 general/fs_utils.luau
create mode 100644 general/init.luau
rename modules/general/logSystem.lua => general/log_system.luau (54%)
delete mode 100644 install/archlinux/PKGBUILD
delete mode 100644 main.lua
create mode 100644 main.luau
create mode 100644 metadata/init.luau
delete mode 100644 modules/config/configManager.lua
delete mode 100644 modules/config/defaultConfigTemplate.lua
delete mode 100644 modules/extra/programMetadata.lua
delete mode 100644 modules/gameLauncher.lua
delete mode 100644 modules/general/fsUtils.lua
delete mode 100644 modules/general/systemUtils.lua
delete mode 100644 modules/objects/steamGameObject.lua
delete mode 100644 modules/objects/toolObject.lua
delete mode 100644 modules/steam/protonDBManager.lua
delete mode 100644 modules/steam/steamConfigProvider.lua
delete mode 100644 modules/steam/steamUtils.lua
delete mode 100644 modules/tools/gamemode.lua
delete mode 100644 modules/tools/gamescope.lua
delete mode 100644 modules/tools/mangohud.lua
delete mode 100644 modules/tools/obs-gamecapture.lua
delete mode 100644 modules/tools/switcherooctl.lua
delete mode 100644 modules/tools/zink.lua
delete mode 100644 modules/ui/UItoSettingsList.lua
delete mode 100644 modules/ui/aboutWindow.lua
delete mode 100644 modules/ui/gameSettingsOverview.lua
delete mode 100644 modules/ui/lgiHelper.lua
delete mode 100644 modules/ui/mainWindow.lua
delete mode 100644 modules/utilitiesList.lua
delete mode 100755 sst
create mode 100644 steam/init.luau
create mode 100644 steam/steam_config_provider.luau
create mode 100644 steam/steam_utils.luau
rename modules/steam/vdfParser.lua => steam/vdf_parser.luau (64%)
delete mode 100644 ui/definitions/cleanerPage.blp
delete mode 100644 ui/definitions/commandPage.blp
delete mode 100644 ui/definitions/gamescopePage.blp
delete mode 100644 ui/definitions/mainWindow.blp
delete mode 100644 ui/definitions/overviewPage.blp
delete mode 100644 ui/definitions/protonPage.blp
delete mode 100644 ui/definitions/settingsPage.blp
delete mode 100644 ui/definitions/utilitiesPage.blp
delete mode 100644 ui/main.ui
delete mode 100644 ui/update-ui.lua
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..351a6a5
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,6 @@
+{
+ "luau-lsp.require.mode": "relativeToFile",
+ "luau-lsp.require.directoryAliases": {
+ "@lune/": "~/.lune/.typedefs/0.8.8/"
+ }
+}
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 970abf2..0000000
--- a/Makefile
+++ /dev/null
@@ -1,63 +0,0 @@
-# Metadata
-PROJECT_NAME=simplesteamtinker
-INSTALL_FOLDER_NAME=SimpleSteamTinker
-VERSION=indev
-
-ifeq ($(PREFIX),)
- PREFIX := /usr
-endif
-BUILD_FOLDER := dist/
-
-.PHONY: system install uninstall clean local
-
-# -------------- System installation --------------
-# This will create a folder structure in dist that is compatible with most package managers.
-system:
- @echo "Packaging project for system installation..."
-# Executable
- mkdir -p $(BUILD_FOLDER)$(PREFIX)/bin
- install -Dm755 sst $(BUILD_FOLDER)$(PREFIX)/bin
-# Modules
- mkdir -p $(BUILD_FOLDER)$(PREFIX)/share/$(INSTALL_FOLDER_NAME)
- install -Dm644 main.lua $(BUILD_FOLDER)$(PREFIX)/share/$(INSTALL_FOLDER_NAME)/main.lua
- cp -r modules $(BUILD_FOLDER)$(PREFIX)/share/$(INSTALL_FOLDER_NAME)
-# UI
- mkdir -p $(BUILD_FOLDER)$(PREFIX)/share/$(INSTALL_FOLDER_NAME)/ui
- install -Dm644 ui/main.ui $(BUILD_FOLDER)$(PREFIX)/share/$(INSTALL_FOLDER_NAME)/ui/main.ui
-# Desktop file
- mkdir -p $(BUILD_FOLDER)$(PREFIX)/share/applications
- install -Dm644 assets/desktop/system.desktop $(BUILD_FOLDER)$(PREFIX)/share/applications/$(PROJECT_NAME).desktop
-# Icons
- mkdir -p $(BUILD_FOLDER)$(PREFIX)/share/icons/hicolor/256x256/apps
- mkdir -p $(BUILD_FOLDER)$(PREFIX)/share/icons/hicolor/scalable/apps
- install -Dm644 assets/icons/256x256.png $(BUILD_FOLDER)$(PREFIX)/share/icons/hicolor/256x256/apps/$(PROJECT_NAME).png
- install -Dm644 assets/icons/scalable.svg $(BUILD_FOLDER)$(PREFIX)/share/icons/hicolor/scalable/apps/$(PROJECT_NAME).svg
-
-# Should only be used as last resort, the end user should use a compatible package manager if possible instead.
-# This is only here for testing purposes, and also as a general "manual" on how to install the project for package maintainers.
-install: system
- @echo "Installing project to system..."
- cp -r $(BUILD_FOLDER)$(PREFIX)/bin/* $(PREFIX)/bin
- cp -r $(BUILD_FOLDER)$(PREFIX)/share/$(INSTALL_FOLDER_NAME) $(PREFIX)/share
- cp -r $(BUILD_FOLDER)$(PREFIX)/share/applications/* $(PREFIX)/share/applications
- cp -r $(BUILD_FOLDER)$(PREFIX)/share/icons/hicolor/256x256/apps/* $(PREFIX)/share/icons/hicolor/256x256/apps
- cp -r $(BUILD_FOLDER)$(PREFIX)/share/icons/hicolor/scalable/apps/* $(PREFIX)/share/icons/hicolor/scalable/apps
-# Update icon cache
- @echo "Updating icon cache..."
- gtk-update-icon-cache $(PREFIX)/share/icons/hicolor
-uninstall:
- @echo "Uninstalling project from system..."
- rm -ri $(PREFIX)/bin/sst
- rm -rI $(PREFIX)/share/$(INSTALL_FOLDER_NAME)
- rm -ri $(PREFIX)/share/applications/$(PROJECT_NAME).desktop
- rm -ri $(PREFIX)/share/icons/hicolor/256x256/apps/$(PROJECT_NAME).png
- rm -ri $(PREFIX)/share/icons/hicolor/scalable/apps/$(PROJECT_NAME).svg
-# Update icon cache
- @ echo "Updating icon cache..."
- gtk-update-icon-cache $(PREFIX)/share/icons/hicolor
-
-# -------------- Cleaning --------------
-
-clean:
- @echo "Cleaning up..."
- rm -rf $(BUILD_FOLDER)
diff --git a/README.md b/README.md
deleted file mode 100644
index c0c140e..0000000
--- a/README.md
+++ /dev/null
@@ -1,184 +0,0 @@
-> [!WARNING]
-> This project may be deprecated in the future, as I have been recently working on a **massive** rewrite, avoiding LGI and focusing more on portability and stability this time around.
-___
-
-> [!IMPORTANT]
-> SimpleSteamTinker is still in ***HEAVY*** development and very unfinished.
-> Expect an incomplete interface and lots of missing/unstable features.
->
-> If SteamTinkerLaunch is fast enough for you and fits your needs, you should not use this tool right now.
->
-> With that being said, I'd still very much appreciate people testing SimpleSteamTinker and reporting [issues](https://github.com/JordanViknar/SimpleSteamTinker/issues) or contributing.
-
-
-

-
SimpleSteamTinker
-
A work-in-progress simple, fast, and modern Adwaita alternative to SteamTinkerLaunch.
-
-

-
-

-

-

-
-
-

-
-
-## Description
-
-### Situation
-
-I like [SteamTinkerLaunch](https://github.com/sonic2kk/steamtinkerlaunch). It is for me one of the biggest tools to have ever been created for Linux gaming.
-
-However, it presents many flaws in my eyes : it is slow, impractical to use, and not user-friendly.
-
-I want Linux Gaming to be available to everyone, including any non-technical users. In my opinion, SteamTinkerLaunch's interface is not friendly to newcomers.
-
-Finally, my curiosity once led me to look inside its source code, only to be met with a single, extremely long (26k+ lines) Bash script. At that point, I decided there should be an alternative to it.
-
-*And thus came SimpleSteamTinker...*
-
-### Goal
-
-*TL;DR : Simple, Fast, Modern & Friendly*
-
-SimpleSteamTinker aims to be only an alternative solution to SteamTinkerLaunch, not to replace it : the more complex features it provides that aren't used by the average user won't be implemented.
-
-Using Lua, a fast and simple language, and Adwaita, a modern user-friendly interface system, the goal of this project is to have a clean and easy but *powerful* way of launching Steam games with custom options and tools.
-
-It takes inspiration from [Bottles](https://github.com/bottlesdevs/Bottles) and Adwaita applications in general.
-
-Additionally, it is considered SimpleSteamTinker should work *with* Steam rather than hack/replace its features. Anything that can be easily done through Steam will not be reimplemented (Proton version management for example. If you want to use GE-Proton, [ProtonPlus](https://github.com/Vysp3r/ProtonPlus) and [ProtonUp-Qt](https://github.com/DavidoTek/ProtonUp-Qt) are recommended).
-
-And finally, as a side goal : prove that Lua can *also* be used for developing modern applications, just like for example Python (which I often see in Adwaita apps).
-
-## Installation
-
-### Arch Linux-based distributions
-
-SimpleSteamTinker can be installed from Arch Linux, Manjaro, and variants through the provided [PKGBUILD](./install/archlinux/PKGBUILD).
-
-**Always** download the latest version of the PKGBUILD, then use **makepkg** in its directory.
-
-It will eventually be made available on the AUR once the program is complete enough.
-
-To enable a game in SimpleSteamTinker with this installation method, use this text as launch options in Steam :
-```bash
-sst %command%
-```
-
-### Packaging for other distributions
-
-Here are informations which might be useful to you :
-
-SimpleSteamTinker is meant to be installed in `/usr/share/SimpleSteamTinker` (except `sst` which goes into `/usr/bin`).
-The system desktop file goes in `/usr/share/applications` and the icons go in `/usr/share/icons` like many applications.
-
-Most of the file structure installation steps can be achieved with the Makefile.
-It is recommended to use it if possible in your packaging system.
-
-It depends on :
-- Lua 5.4
-- [LGI](https://github.com/lgi-devs/lgi) (development version **specifically**) + [GTK4](https://www.gtk.org/) + [libadwaita](https://gnome.pages.gitlab.gnome.org/libadwaita/)
-- Steam installation, with a user configured and games installed.
-- [LuaFileSystem](https://github.com/lunarmodules/luafilesystem)
-- [LuaSocket](https://github.com/lunarmodules/luasocket) + [LuaSec](https://github.com/lunarmodules/luasec)
-- [dkjson](https://dkolf.de/src/dkjson-lua.fsl/home)
-- [xclip](https://github.com/astrand/xclip) *(Might be replaced/made optional in the future.)*
-- [libnotify](https://gitlab.gnome.org/GNOME/libnotify)
-
-And optionally (if they're missing, their related features will simply be disabled) :
-
-[GameMode](https://github.com/FeralInteractive/gamemode), [MangoHud](https://github.com/flightlessmango/MangoHud), [Mesa](https://www.mesa3d.org/) *(for Zink support)*, [switcheroo-control](https://gitlab.freedesktop.org/hadess/switcheroo-control) and [Gamescope](https://github.com/ValveSoftware/gamescope).
-
-## Development
-
-Simply install the project's dependencies described earlier and clone it.
-
-To test the launch of Steam games, insert in their launch options :
-```bash
-/PATH/TO/CLONED/REPO/sst %command%
-```
-For example, in my case `/PATH/TO/CLONED/REPO/sst` is `/home/jordan/Programmation/SimpleSteamTinker/sst`.
-
-Additionally, if you want to perform UI-related changes, you'll need [Blueprint](https://gitlab.gnome.org/jwestman/blueprint-compiler).
-If, in the future, LGI becomes stable enough with GTK4 to use it directly to generate the UI, Blueprint might be dropped.
-
-*Note : A desktop file won't be provided with this method, and icons won't be installed.*
-
-## Comparison
-
-### Development
-
-| Development | SteamTinkerLaunch | SimpleSteamTinker |
-| --- | --- | --- |
-| Language | Bash | Lua |
-| Code | A single file with 26k lines | Organised in modules and commented |
-| UI | Directly generated by the code | (Mostly) done in separate GTK Blueprint files |
-| Interface system | yad + GTK3 | LGI + GTK4 + libadwaita |
-| License | GPL-3.0 | MPL-2.0 |
-
-### Interface
-
-| Features (interface) | SteamTinkerLaunch | SimpleSteamTinker |
-| --- | --- | --- |
-| Launch speed (on primary laptop) | ❌ (25 seconds) | ✅ (1 second) |
-| Game list | ❌ | ✅ |
-| Game status | ❌ | ✅ |
-| ProtonDB integration | ✅ | ✅ |
-| Launch Button | ✅ | ✅ |
-| Steam AppID | ✅ | ✅ |
-| Notifications | ✅ | ✅ |
-| Pre-game launch window | ✅ | ❌ *(Not implemented to speed up game startup and not get in the user's way. Might change in the future.)* |
-| Categories | ✅ | ❌ |
-| Custom default game config | ✅ | ❌ |
-| Game location | ~ *(impractical)* | ✅ |
-| Compatdata location | ❌ | ✅ |
-| Help pages *(ProtonDB, PCGamingWiki, etc.)* | ~ *(impractical)* | ✅ |
-
-### Tools, settings, etc.
-
-*(Note : Native Linux features/tools/settings are prioritized over alternatives designed for Windows.)*
-
-| Features (tools, settings, etc.) | SteamTinkerLaunch | SimpleSteamTinker |
-| --- | --- | --- |
-| Non-Steam game support | ✅ | ❌ *([Bottles](https://github.com/bottlesdevs/Bottles) recommended)* |
-| ~~Proton version management~~ | ✅ | ❌ *([ProtonPlus](https://github.com/Vysp3r/ProtonPlus) recommended)* |
-| dGPU management | ✅ | ✅ |
-| GameMode | ✅ | ✅ |
-| Custom launch system | ✅ | 🚧 *(Planned)* |
-| Winetricks | ✅ | ❌ *(Planned ?)* |
-| Protontricks | ❌ | ❌ *(Planned ?)* |
-| Proton settings | ✅ | ✅ |
-| DXVK settings | ✅ | 🚧 *(Planned)* |
-| VKD3D settings | ✅ | 🚧 *(Planned)* |
-| MangoHud | ✅ | ✅ |
-| Gamescope | ✅ | ✅ |
-| Shader support (ReShade/vkBasalt) | ✅ | 🚧 *(Planned)* |
-| SDL Wayland video driver | ✅ | ✅ |
-| Zink | ✅ | ✅ |
-| PulseAudio latency | ✅ | 🚧 *(Planned)* |
-| Vortex | ✅ | ❌ |
-| Mod Organizer 2 | ✅ | ❌ |
-| HedgeModManager | ✅ | ❌ |
-| geo-11 3D Driver | ✅ | ❌ |
-| SpecialK | ✅ | ❌ |
-| FlawlessWidescreen | ✅ | ❌ |
-| Stereo3D | ✅ | ❌ |
-| RADV Perftest Options | ✅ | ❌ *(Cannot test properly due to lack of an AMD GPU)* |
-| Steam Linux Runtime toggle | ✅ | ❌ |
-| steamwebhelper toggle | ✅ | ❌ |
-| obs-gamecapture | ✅ | ✅ |
-| ~~Nyrna~~ | ✅ | ❌ *(Waiting for Wayland support...)* |
-| ~~ReplaySorcery~~ | ✅ | ❌ *(Unmaintaned)* |
-| ~~Boxtron~~ | ✅ | ❌ *([ProtonPlus](https://github.com/Vysp3r/ProtonPlus) recommended)* |
-| ~~Roberta~~ | ✅ | ❌ *([ProtonPlus](https://github.com/Vysp3r/ProtonPlus) recommended)* |
-| ~~Luxtorpeda~~ | ✅ | ❌ *([ProtonPlus](https://github.com/Vysp3r/ProtonPlus) recommended)* |
-| Network monitoring and control | ✅ | 🚧 *(Planned)* |
-| Discord Rich Presence | ✅ | 🚧 *(Planned)* |
-
-## Bug Reports / Contributions / Suggestions
-You can report bugs or suggest features by making an issue, or you can contribute to this program directly by forking it and then sending a pull request.
-
-Any help will be very much appreciated. Thank you.
diff --git a/general/fs_utils.luau b/general/fs_utils.luau
new file mode 100644
index 0000000..3a341b6
--- /dev/null
+++ b/general/fs_utils.luau
@@ -0,0 +1,105 @@
+--!strict
+local fsUtils = {}
+
+-- Lune Modules
+local fs = require("@lune/fs")
+
+-- Internal Modules
+local logSystem = require("log_system")
+
+--[[
+ Name : function fsUtils.exists(path)
+ Description : Checks if a file or directory exists.
+ Arg 1 : path (string) : The path to check.
+ Return : true if the file or directory exists, false otherwise.
+]]
+function fsUtils.exists(path: string): boolean
+ return fs.metadata(path).exists
+end
+
+--[[
+ Name : function fsUtils.isDirectory(path)
+ Description : Checks if a path is a directory.
+ Arg 1 : path (string) : The path to check.
+ Return : true if the path is a directory, false otherwise.
+]]
+function fsUtils.isDirectory(path: string): boolean
+ return fs.isDir(path)
+end
+
+--[[
+ Name : function fsUtils.createOrUseDirectory(path)
+ Description : Creates a directory if it doesn't exist, or uses it if it does.
+ Arg 1 : path (string) : The path to create or use.
+ Return : true if the directory was created or used, false otherwise.
+]]
+function fsUtils.createOrUseDirectory(path: string): string
+ if not fs.isDir(path) then
+ logSystem.log("debug", "Directory "..path.." not found. Creating it...")
+ fs.writeDir(path)
+ end
+ return path
+end
+
+--[[
+ Name : function fsUtils.getSize(path)
+ Description : Gets the size of a file or directory.
+ Arg 1 : path (string) : The path to get the size of.
+ Return : The size of the file or directory.
+]]
+function fsUtils.getSize(path: string): ()
+ -- Can't be implemented for now.
+end
+
+--[[
+ Name : function fsUtils.sizeToUnit(size)
+ Description : Converts a size in bytes to a human-readable size.
+ Arg 1 : size (number) : The size to convert.
+ Return : The human-readable size.
+]]
+type SizeUnit = "B" | "KB" | "MB" | "GB" | "TB"
+function fsUtils.sizeToUnit(size: number): string
+ local unit: SizeUnit = "B"
+ if size > 1024 then
+ size = size / 1024
+ unit = "KB"
+ end
+ if size > 1024 then
+ size = size / 1024
+ unit = "MB"
+ end
+ if size > 1024 then
+ size = size / 1024
+ unit = "GB"
+ end
+ if size > 1024 then
+ size = size / 1024
+ unit = "TB"
+ end
+ return string.format("%.2f", size).." "..unit
+end
+
+-- Missing stuff here
+
+--[[
+ Name : function fsUtils.getFilenamePatternInDirectory(directory, pattern)
+ Description : Returns a table containing every filename matching a pattern in a directory.
+ Arg 1 : directory (string) : The directory to search in.
+ Arg 2 : pattern (string) : The pattern to search for.
+ Return : A table containing every filename matching the pattern.
+
+ Note : Used to for game detection to work around Steam not updating the libraryfolders.vdf file immediately.
+]]
+function fsUtils.getFilenamePatternInDirectory(directory: string, pattern: string): {string}
+ local result = {}
+
+ for _, file in fs.readDir(directory) do
+ if file:match(pattern) then
+ table.insert(result, file)
+ end
+ end
+
+ return result
+end
+
+return fsUtils
\ No newline at end of file
diff --git a/general/init.luau b/general/init.luau
new file mode 100644
index 0000000..d649aa4
--- /dev/null
+++ b/general/init.luau
@@ -0,0 +1,5 @@
+--!strict
+return {
+ logSystem = require("log_system"),
+ fsUtils = require("fs_utils"),
+}
\ No newline at end of file
diff --git a/modules/general/logSystem.lua b/general/log_system.luau
similarity index 54%
rename from modules/general/logSystem.lua
rename to general/log_system.luau
index 409a586..eec4731 100644
--- a/modules/general/logSystem.lua
+++ b/general/log_system.luau
@@ -1,3 +1,4 @@
+--!strict
local logSystem = {}
--[[
@@ -7,20 +8,20 @@ local logSystem = {}
Arg 2 : message (string) : The message to color.
Return : The colored message.
]]
-local function color(colorId, message)
+type LogColor = "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "grey" | "orange" | "reset"
+local function color(colorId: LogColor, message: string): string
local colorList = {
- ["red"] = "\27[31m",
- ["green"] = "\27[32m",
- ["yellow"] = "\27[33m",
- ["blue"] = "\27[34m",
- ["magenta"] = "\27[35m",
- ["cyan"] = "\27[36m",
- ["white"] = "\27[37m",
- ["grey"] = "\27[90m",
- ["orange"] = "\27[91m",
- ["reset"] = "\27[0m"
+ red = "\27[31m",
+ green = "\27[32m",
+ yellow = "\27[33m",
+ blue = "\27[34m",
+ magenta = "\27[35m",
+ cyan = "\27[36m",
+ white = "\27[37m",
+ grey = "\27[90m",
+ orange = "\27[91m",
+ reset = "\27[0m"
}
-
return colorList[colorId]..message..colorList["reset"]
end
@@ -31,43 +32,32 @@ end
Arg 2 : message (string) : The message to log.
Return : nil
]]
-function logSystem.log(type, message)
+type LogType = "info" | "warning" | "error" | "debug" | "download" | "data" | "speed"
+function logSystem.log(type: LogType, message: string | number)
local logReactions = {
- ["info"] = function(text)
+ info = function(text)
print("["..color("blue","Info").."] "..text)
end,
- ["warning"] = function(text)
+ warning = function(text)
print("["..color("yellow","Warning").."] "..color("yellow",text))
end,
- ["error"] = function(text)
+ error = function(text)
print(color("red","[Error]").." "..color("red",text))
end,
- ["debug"] = function(text)
+ debug = function(text)
print("["..color("grey","Debug").."] "..color("grey",text))
end,
- ["switch"] = function (text)
- print(color("green","[Output Switch] "..text))
- print("--------------------------")
- end,
- ["switchEnd"] = function (text)
- print("--------------------------")
- end,
- ["download"] = function (text)
+ download = function (text)
print("["..color("green","Download").."] "..text)
end,
- ["fileRead"] = function (text)
+ data = function (text)
print("["..color("magenta","Data").."] "..text)
end,
- ["speed"] = function (startTime)
+ speed = function (startTime)
print("["..color("grey","Speed").."] ".."Done in "..os.clock()-startTime.." seconds.")
end
}
-
- if (logReactions[type]) then
- logReactions[type](message)
- else
- error("Unknown log type : "..type)
- end
+ logReactions[type](message)
end
return logSystem
diff --git a/install/archlinux/PKGBUILD b/install/archlinux/PKGBUILD
deleted file mode 100644
index 658d7fb..0000000
--- a/install/archlinux/PKGBUILD
+++ /dev/null
@@ -1,48 +0,0 @@
-# Maintainer: JordanViknar
-
-pkgname=simplesteamtinker-git
-pkgver=r44.84be422
-pkgrel=1
-pkgdesc="A work-in-progress simple, fast and modern Adwaita alternative to SteamTinkerLaunch."
-arch=('x86_64' 'i686' 'armv7h' 'aarch64')
-url=https://github.com/JordanViknar/SimpleSteamTinker
-license=('MPL2')
-makedepends=(
- 'git'
- 'make'
-)
-depends=(
- 'lua'
- 'lua-lgi-git' # Used for GTK (git version because latest release neither supports Libadwaita nor GTK4 and is pretty old)
- 'libadwaita'
- 'gtk4'
- 'lua-filesystem' # Used for some filesystem operations
- 'lua-socket' # Used for network operations
- 'lua-sec' # Used for secure https interactions
- 'lua-dkjson' # Used for json parsing
- 'xclip' # Used for copying to clipboard
- 'libnotify' # Used for notifications
-)
-optdepends=(
- 'gamemode: Enable GameMode support'
- 'mangohud: Enable MangoHud support'
- 'mesa: Enable Zink support'
- 'switcheroo-control: Launch games with your dedicated GPU'
- 'gamescope: Enable GameScope support'
- 'protonplus: Recommended to install with SimpleSteamTinker'
-)
-provides=("${pkgname%-git}")
-conflicts=("${pkgname%-git}")
-source=('git+https://github.com/JordanViknar/SimpleSteamTinker')
-sha256sums=('SKIP')
-
-pkgver() {
- cd SimpleSteamTinker
- printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
-}
-
-package() {
- cd SimpleSteamTinker
- make system
- cp -r dist/* "${pkgdir}/"
-}
diff --git a/main.lua b/main.lua
deleted file mode 100644
index b9fe82f..0000000
--- a/main.lua
+++ /dev/null
@@ -1,131 +0,0 @@
--- Startup time
-local totalStartupTimeVar = os.clock()
-
--- Internal Modules
-local logSystem = require("modules.general.logSystem")
-local fsUtils = require("modules.general.fsUtils")
-local systemUtils = require("modules.general.systemUtils")
-local steamUtils = require("modules.steam.steamUtils")
-local programMetadata = require("modules.extra.programMetadata")
-local gameLauncher = require("modules.gameLauncher")
-
-if programMetadata.version:find("dev") then
- logSystem.log("warning", "DEVELOPMENT VERSION")
-end
-
---[[
- Chapter 0 : Preparing the environment
- We need to create the config folder if it doesn't exist.
-]]
--- Create the config folder if it doesn't exist
-fsUtils.createOrUseDirectory(programMetadata.folders.config)
--- And the games folder inside of it
-fsUtils.createOrUseDirectory(programMetadata.folders.gamesConfig)
-
--- Create the cache folder if it doesn't exist
-fsUtils.createOrUseDirectory(programMetadata.folders.cache)
-
--- Put the arguments inside the cache folder for testing purposes
-local cacheFile = io.open(programMetadata.folders.cache.."/lastArguments.txt", "w")
-if not cacheFile then
- logSystem.log("error", "Unable to open cache file.")
-else
- cacheFile:write(table.concat(arg, "\n"))
- cacheFile:close()
-end
-
---[[
- Chapter 1 : Game management
- What's the Steam config ? Is this being started through Steam or alone ? What to do ?!
-]]
-
--- We retrieve games registered in Steam.
-local steamGames = require("modules.steam.steamConfigProvider")
-collectgarbage("collect")
-
--- We check if we're starting a Steam game or not through the arguments.
-local gameStartStatus = steamUtils.isSteamArgs(arg)
-
-if gameStartStatus == "steamGame" then
- local game = steamGames[arg[3]:gsub("AppId=", "")]
-
- -- We notify the user that SimpleSteamTinker is running.
- logSystem.log("info", "Starting "..game.name.."...")
- systemUtils.sendNotification(programMetadata.name.." is starting...", "Detected "..game.name, "information")
-
- -- For every argument that is a path, we put \ next to the spaces
- for i, v in ipairs(arg) do
- if fsUtils.exists(v) then
- arg[i] = v:gsub(" ", "\\ ")
- end
- end
-
- -- We prepare the command
- local command = table.concat(arg, " ")
-
- logSystem.log("info", "Applying settings for "..game.name.."...")
- command = gameLauncher.prepareGameLaunch(game, command)
-
- if os.execute(command) then
- os.exit(0)
- else
- systemUtils.sendNotification(programMetadata.name.." crashed !", "Something bad happened while running "..game.name..".", "error")
- os.exit(1)
- end
-elseif gameStartStatus == "SteamTinkerLaunch" or (arg[3] and steamGames[arg[3]:gsub("AppId=", "")].status == "SteamTinkerLaunch") then
- -- We notify the user that SteamTinkerLaunch is running.
- logSystem.log("error", "Can't use "..programMetadata.name.." ! SteamTinkerLaunch detected.")
- systemUtils.sendNotification("Can't use "..programMetadata.name.." !", "SteamTinkerLaunch detected.", "warning")
-
- -- For every argument that is a path, we put \ next to the spaces
- for i, v in ipairs(arg) do
- if fsUtils.exists(v) then
- arg[i] = v:gsub(" ", "\\ ")
- end
- end
-
- -- We prepare the command
- local command = table.concat(arg, " ")
-
- os.execute(command)
- os.exit(0)
-end
-logSystem.log("info", "No game to be launched detected. Launching interface...")
-
-
-
---[[
- Chapter 2 : The main window
- No games started. Time to cook some Libadwaita goodness then.
-]]
-
--- External Modules
-local lgi = require("lgi")
-local Adw = lgi.Adw
-
--- Application
-local app = Adw.Application({
- application_id = programMetadata.developer.."."..programMetadata.name,
-})
-
--- Sort Steam games alphabetically
-local newSteamGamesTable = {}
-for _, v in pairs(steamGames) do
- table.insert(newSteamGamesTable, v)
-end
-table.sort(newSteamGamesTable, function(a, b) return a.name < b.name end)
-steamGames = newSteamGamesTable
-
--- Time to start the application
-local timeStart = os.clock()
-function app:on_startup()
- require("modules.ui.mainWindow")(app, steamGames)
-end
-
-function app:on_activate()
- logSystem.log("speed", timeStart)
- logSystem.log("info", "Startup time : "..os.clock()-totalStartupTimeVar.." seconds.")
- self.active_window:present()
-end
-
-return app:run(arg)
diff --git a/main.luau b/main.luau
new file mode 100644
index 0000000..cd423bb
--- /dev/null
+++ b/main.luau
@@ -0,0 +1,38 @@
+--!strict
+-- Startup time
+local totalStartupTimeVar = os.clock()
+
+-- Lune Modules
+local process = require("@lune/process")
+local fs = require("@lune/fs")
+
+-- Internal Modules
+local metadata = require("metadata")
+local logSystem = require("general").logSystem
+local fsUtils = require("general").fsUtils
+
+--[[
+ Chapter 0 : Preparing the environment
+ We need to create the config folder if it doesn't exist.
+]]
+-- Create folders
+fsUtils.createOrUseDirectory(metadata.folders.config)
+fsUtils.createOrUseDirectory(metadata.folders.gamesConfig)
+fsUtils.createOrUseDirectory(metadata.folders.cache)
+
+-- Grab arguments
+local arguments = process.args
+
+-- Development version
+if metadata.version:find("dev") then
+ logSystem.log("warning", "DEVELOPMENT VERSION")
+ -- Put the arguments inside the cache folder for testing purposes
+ fs.writeFile(metadata.folders.cache.."/arguments.txt", table.concat(arguments, "\n"))
+end
+
+--[[
+ Chapter 1 : Game management
+ What's the Steam config ? Is this being started through Steam or alone ? What to do ?!
+]]
+
+local steamConfig = require("steam").steamConfig
diff --git a/metadata/init.luau b/metadata/init.luau
new file mode 100644
index 0000000..1843401
--- /dev/null
+++ b/metadata/init.luau
@@ -0,0 +1,20 @@
+--!strict
+local process = require("@lune/process")
+
+local metadata = {
+ name ="SimpleSteamTinker",
+ description = "A work-in-progress fast, simple and modern Libadwaita alternative to SteamTinkerLaunch.",
+ executable="sst",
+ icon_name = "simplesteamtinker",
+ version="indev",
+ developer="JordanViknar",
+ url = "https://github.com/JordanViknar/SimpleSteamTinker",
+ folders = {
+ config = process.env.HOME.."/.config/SimpleSteamTinker",
+ gamesConfig = process.env.HOME.."/.config/SimpleSteamTinker/games",
+ storage = process.env.HOME.."/.local/share/SimpleSteamTinker",
+ cache = process.env.HOME.."/.cache/SimpleSteamTinker"
+ }
+}
+
+return metadata
diff --git a/modules/config/configManager.lua b/modules/config/configManager.lua
deleted file mode 100644
index 31e02da..0000000
--- a/modules/config/configManager.lua
+++ /dev/null
@@ -1,125 +0,0 @@
--- Internal Modules
-local gameConfigFolder = require("modules.extra.programMetadata").folders.gamesConfig
-local fsUtils = require("modules.general.fsUtils")
-local logSystem = require("modules.general.logSystem")
-
--- External Modules
-local json = require("dkjson")
-
-local configManager = {}
-
---[[
- Name : function configManager.createGameConfig(gameId)
- Description : Creates a config file for a game.
- Arg 1 : gameId (string) : The game's Steam ID.
- Return : The game's config.
-]]
-function configManager.createGameConfig(gameId)
- -- Grab the default template
- local gameConfig = require("modules.config.defaultConfigTemplate")
- gameConfig.steamGameId = gameId
-
- -- Write the new table to the file
- local path = string.format("%s/%s.json", gameConfigFolder, gameId)
- local file = assert(io.open(path, "w"), "Couldn't open " .. path .. " !")
- file:write(json.encode(gameConfig, { indent = false }))
- file:close()
-
- return gameConfig
-end
-
---[[
- Name : configManager.getGameConfig(gameId)
- Description : Retrieves a game's config.
- Arg 1 : gameId (string) : The game's Steam ID.
-]]
-function configManager.getGameConfig(gameId)
- -- Check if the config exists
- local path = string.format("%s/%s.json", gameConfigFolder, gameId)
- if not fsUtils.exists(path) then
- local warningMsg = "Config for game " .. gameId .. " at " .. path .. " doesn't exist. Creating it..."
- logSystem.log("warning", warningMsg)
- return configManager.createGameConfig(gameId)
- end
-
- -- Read the file
- local file = assert(io.open(path, "r"), "Couldn't open " .. path .. " !")
- local fileContent = file:read("*a")
- file:close()
-
- -- Parse the JSON
- local parsed = json.decode(fileContent)
-
- -- Update the config to be based on defaultConfigTemplate recursively
- local defaultConfig = require("modules.config.defaultConfigTemplate")
- local function updateConfig(config, template)
- for key, value in pairs(template) do
- if config[key] == nil then
- config[key] = value
- elseif type(value) == "table" then
- updateConfig(config[key], value)
- end
- end
- end
- updateConfig(parsed, defaultConfig)
-
- return parsed
-end
-
---[[
- Name : function configManager.modifyGameConfig(gameId, dataToChange)
- Description : Modifies a game's config.
- Arg 1 : gameId (string) : The game's Steam ID.
- Arg 2 : dataToChange (string) : The data to change. Example : "steamGameId"
- Arg 3 : value (any) : The value to set.
- Return : The game's config.
-]]
-function configManager.modifyGameConfig(gameId, dataToChange, value)
- -- Grab the original config data
- local gameConfig = configManager.getGameConfig(gameId)
-
- -- Parse dataToChange
- local keys = {}
- for substring in dataToChange:gmatch("[^.]+") do
- keys[#keys + 1] = substring
- end
-
- -- Modify the table
- local pointer = gameConfig
- for i = 1, #keys - 1 do
- pointer = pointer[keys[i]] or {}
- end
- pointer[keys[#keys]] = value
-
- -- Write the new table
- local path = string.format("%s/%s.json", gameConfigFolder, gameId)
- local file = assert(io.open(path, "w"), "Couldn't open " .. path .. " !")
-
- file:write(json.encode(gameConfig))
- file:close()
-
- return gameConfig
-end
-
---[[
- Name : function configManager.grabInTableFromString(table, string)
- Description : Grabs a value in a table from a string.
- Arg 1 : table (table) : The table to grab the value from.
- Arg 2 : string (string) : The string to parse. Example : "gamescope.general.resolution.internal.width"
- Return : The value.
-]]
-function configManager.grabInTableFromString(table, string)
- local keys = {}
- for substring in string:gmatch("[^.]+") do
- keys[#keys + 1] = substring
- end
-
- local pointer = table
- for i = 1, #keys do
- pointer = pointer[keys[i]] or {}
- end
-
- return pointer
-end
-
-return configManager
diff --git a/modules/config/defaultConfigTemplate.lua b/modules/config/defaultConfigTemplate.lua
deleted file mode 100644
index b605f27..0000000
--- a/modules/config/defaultConfigTemplate.lua
+++ /dev/null
@@ -1,90 +0,0 @@
-return
-
-{
- config_version = 1;
- steamGameId = nil;
-
- dgpu = {
- enabled = true;
- };
-
- -- Miscellaneous
- misc = {
- sdl_wayland = true;
- };
-
- -- Utilities
- utilities = {
- gamemode = {
- enabled = true;
- },
- mangohud = {
- enabled = false;
- },
- zink = {
- enabled = false;
- },
- obs_gamecapture = {
- enabled = false;
- },
- },
-
- -- GameScope
- gamescope = {
- enabled = false;
- general = {
- resolution = {
- enabled = false;
- internal = {
- width = 1280;
- height = 720;
- };
- external = {
- width = 1920;
- height = 1080;
- };
- };
- frame_limit = {
- enabled = false;
- normal = 60;
- unfocused = 5;
- };
- fullscreen = false;
- borderless = false;
- };
- filtering = {
- enabled = false;
- filter = "FSR"; -- "Linear", "Nearest", "FSR", "NIS", "Pixel"
- sharpness = 10; -- Ranges from 0 to 20
- }
- },
-
- -- Settings exclusive to Windows games
- proton = {
- direct3d = {
- enable_direct3d9 = true; -- Exclusive to Proton GE
- enable_direct3d10 = true;
- enable_direct3d11 = true;
- enable_direct3d12 = true; -- Exclusive to Proton GE
- use_wined3d = false;
- };
- sync = {
- enable_esync = true;
- enable_fsync = true;
- };
- nvidia = {
- enable_nvapi = false;
- hide_nvidia_gpu = false;
- };
- fsr = { -- Exclusive to Proton GE
- enabled = false;
- sharpness = 2; -- Between 0 and 5, 0 = maximum, 5 = minimum
- upscaling_mode = "none"; -- Can be "none", "performance", "balanced", "quality" or "ultra"
- resolution = {
- enabled = false;
- width = 1920;
- height = 1080;
- };
- };
- };
-}
diff --git a/modules/extra/programMetadata.lua b/modules/extra/programMetadata.lua
deleted file mode 100644
index 4df3476..0000000
--- a/modules/extra/programMetadata.lua
+++ /dev/null
@@ -1,17 +0,0 @@
-return {
- name="SimpleSteamTinker",
- description = "A work-in-progress fast, simple and modern Libadwaita alternative to SteamTinkerLaunch.",
- executable="sst",
- icon_name = "simplesteamtinker",
- version="indev",
- developer="JordanViknar",
- url = "https://github.com/JordanViknar/SimpleSteamTinker",
- installdir = os.getenv("SST_SCRIPT_PATH"),
- folders = {
- config = os.getenv("HOME").."/.config/SimpleSteamTinker",
- gamesConfig = os.getenv("HOME").."/.config/SimpleSteamTinker/games",
-
- storage = os.getenv("HOME").."/.local/share/SimpleSteamTinker",
- cache = os.getenv("HOME").."/.cache/SimpleSteamTinker"
- }
-}
diff --git a/modules/gameLauncher.lua b/modules/gameLauncher.lua
deleted file mode 100644
index 83f9fab..0000000
--- a/modules/gameLauncher.lua
+++ /dev/null
@@ -1,103 +0,0 @@
--- Internal Modules
-local logSystem = require("modules.general.logSystem")
-local systemUtils = require("modules.general.systemUtils")
-local configManager = require("modules.config.configManager")
-local programMetadata = require("modules.extra.programMetadata")
-
-local gameLauncher = {}
-
-function gameLauncher.prepareGameLaunch(game, command)
- -- First we retrive the configuration
- local gameConfig
- local status, result = pcall(function(value)
- gameConfig = configManager.getGameConfig(value)
- end, game.id)
-
- if not status then
- logSystem.log("error", "Couldn't retrieve configuration for "..game.name..". Error : "..result)
- systemUtils.sendNotification("Couldn't retrieve configuration for "..game.name.." !", result, "error")
- return command
- end
-
- -- We get the list of utilities
- local utilities = require("modules.utilitiesList")(gameConfig)
-
- -- Environment variables : Part 1
- local environmentVars = {}
-
- -- We iterate through the utilities and enable them if they are installed
- for _, utility in pairs(utilities) do
- if utility[2] --[[= Utility is enabled]] and utility[1].isInstalled then
- if utility[1].toolType == "executable" then
- command = utility[1].usage(command, gameConfig)
- elseif utility[1].toolType == "environmentVariable" then
- table.insert(environmentVars, utility[1].usage(command, gameConfig))
- end
- end
- end
-
- -- Special case for switcherooctl, some Steam launch desktop files force the dGPU on games even when disabled.
- if gameConfig.dgpu.enabled == false and utilities[3][1].isInstalled then -- Should use names instead of indexes later
- -- We add the environment variables to forcefully disable the dGPU
- table.insert(environmentVars, "DRI_PRIME=0 __NV_PRIME_RENDER_OFFLOAD=0 __VK_LAYER_NV_optimus=none __GLX_VENDOR_LIBRARY_NAME=none")
- end
-
- -- Environment variables not linked to a particular utility
- if game.os_platform == "Linux" then
- if gameConfig.misc.sdl_wayland == true then
- table.insert(environmentVars, "SDL_VIDEODRIVER=\"wayland,x11\"")
- else -- We force X11 to be used, just in case.
- table.insert(environmentVars, "SDL_VIDEODRIVER=\"x11\"")
- end
- elseif game.os_platform == "Windows" then
- -- Direct3D
- if gameConfig.proton.direct3d.enable_direct3d9 == false then table.insert(environmentVars, "PROTON_NO_D3D9=1") end
- if gameConfig.proton.direct3d.enable_direct3d10 == false then table.insert(environmentVars, "PROTON_NO_D3D10=1") end
- if gameConfig.proton.direct3d.enable_direct3d11 == false then table.insert(environmentVars, "PROTON_NO_D3D11=1") end
- if gameConfig.proton.direct3d.enable_direct3d12 == false then table.insert(environmentVars, "PROTON_NO_D3D12=1") end
- if gameConfig.proton.direct3d.use_wined3d == true then table.insert(environmentVars, "PROTON_USE_WINED3D=1") end
- -- Sync
- if gameConfig.proton.sync.enable_esync == false then table.insert(environmentVars, "PROTON_NO_ESYNC=1") end
- if gameConfig.proton.sync.enable_fsync == false then table.insert(environmentVars, "PROTON_NO_FSYNC=1") end
- -- NVIDIA
- if gameConfig.proton.nvidia.enable_nvapi == true then table.insert(environmentVars, "PROTON_ENABLE_NVAPI=1") end
- if gameConfig.proton.nvidia.hide_nvidia_gpu == true then table.insert(environmentVars, "PROTON_HIDE_NVIDIA_GPU=1") end
- -- FSR
- if gameConfig.proton.fsr.enabled == true then
- table.insert(environmentVars, string.format(
- "WINE_FULLSCREEN_FSR=1 WINE_FULLSCREEN_FSR_STRENGTH=%s WINE_FULLSCREEN_FSR_MODE=%s",
- gameConfig.proton.fsr.sharpness,
- string.lower(gameConfig.proton.fsr.upscaling_mode)
- ))
- if gameConfig.proton.fsr.resolution.enabled == true then
- table.insert(environmentVars, string.format(
- "WINE_FULLSCREEN_FSR_CUSTOM_MODE=%sx%s",
- gameConfig.proton.fsr.resolution.width,
- gameConfig.proton.fsr.resolution.height
- ))
- end
- else
- -- Explictly disable FSR if it's not enabled, recent Proton GE versions enable it by default.
- table.insert(environmentVars, "WINE_FULLSCREEN_FSR=0")
- end
- end
-
- -- We convert the environment variables table to a string
- if environmentVars ~= {} then
- local envVarString = table.concat(environmentVars, " ")
- command = "env "..envVarString.." "..command
- end
-
- -- Print the final command in cache for debugging purposes
- local cacheFile = io.open(programMetadata.folders.cache.."/lastCommand.txt", "w")
- if not cacheFile then
- logSystem.log("error", "Unable to open cache file.")
- else
- cacheFile:write(command)
- cacheFile:close()
- end
-
- return command
-end
-
-return gameLauncher
diff --git a/modules/general/fsUtils.lua b/modules/general/fsUtils.lua
deleted file mode 100644
index bbff7f5..0000000
--- a/modules/general/fsUtils.lua
+++ /dev/null
@@ -1,188 +0,0 @@
--- External Modules
-local lfs = require("lfs")
-
--- Internal Modules
-local logSystem = require("modules.general.logSystem")
-
--- Module
-local fsUtils = {}
-
---[[
- Name : function fsUtils.exists(path)
- Description : Checks if a file or directory exists.
- Arg 1 : path (string) : The path to check.
- Return : true if the file or directory exists, false otherwise.
-]]
-function fsUtils.exists(path)
- local attributes, err = lfs.attributes(path)
- if attributes then
- return true
- else
- return false, err
- end
-end
-
---[[
- Name : function fsUtils.isDirectory(path)
- Description : Checks if a path is a directory.
- Arg 1 : path (string) : The path to check.
- Return : true if the path is a directory, false otherwise.
-]]
-function fsUtils.isDirectory(path)
- local attributes, err = lfs.attributes(path)
- if attributes then
- if attributes.mode == "directory" then
- return true
- else
- return false
- end
- else
- return false, err
- end
-end
-
---[[
- Name : function fsUtils.createDirectory(path)
- Description : Creates a directory.
- Arg 1 : path (string) : The path to create.
- Return : true if the directory was created, false otherwise.
-]]
-function fsUtils.createDirectory(path)
- local success, err = lfs.mkdir(path)
- if success then
- return true
- else
- return false, err
- end
-end
-
---[[
- Name : function fsUtils.createOrUseDirectory(path)
- Description : Creates a directory if it doesn't exist, or uses it if it does.
- Arg 1 : path (string) : The path to create or use.
- Return : true if the directory was created or used, false otherwise.
-]]
-function fsUtils.createOrUseDirectory(path)
- if not fsUtils.exists(path) then
- logSystem.log("debug", "Directory "..path.." not found. Creating it...")
- return fsUtils.createDirectory(path)
- else
- return true
- end
-end
-
---[[
- Name : function fsUtils.getSize(path)
- Description : Gets the size of a file or directory.
- Arg 1 : path (string) : The path to get the size of.
- Return : The size of the file or directory.
-]]
-local function trim(s)
- return s:match'^()%s*$' and '' or s:match'^%s*(.*%S)'
-end
-function fsUtils.getSize(path)
- local handle = io.popen("du -sb '"..path:gsub("'", "'\\''").."' | cut -f1")
- if not handle then
- return 0
- end
-
- local result = handle:read("*a")
- handle:close()
-
- return tonumber(trim(result))
-end
-
---[[
- Name : function fsUtils.sizeToUnit(size)
- Description : Converts a size in bytes to a human-readable size.
- Arg 1 : size (number) : The size to convert.
- Return : The human-readable size.
-]]
-function fsUtils.sizeToUnit(size)
- local unit = "B"
- if size > 1024 then
- size = size / 1024
- unit = "KB"
- end
- if size > 1024 then
- size = size / 1024
- unit = "MB"
- end
- if size > 1024 then
- size = size / 1024
- unit = "GB"
- end
- if size > 1024 then
- size = size / 1024
- unit = "TB"
- end
- return string.format("%.2f", size).." "..unit
-end
-
---[[
- Name : function fsUtils.directoryContainsLinuxData(location)
- Description : Checks if a directory contains Linux data.
- Arg 1 : location (string) : The directory to check.
- Return : true if the directory contains Linux data, false otherwise.
-
- Note : Used to for game platform detection to work around Steam not cleaning up the compat.vdf file when going back from the Windows version of a game to the Linux version.
- Note 2 : The reason we don't use os.execute("file ...") is because Steam can get stuck while starting a game for whatever reason.
-]]
--- Alternative to using file command
-local function checkFileForKeywords(location, file)
- local filepath = location .. "/" .. file
-
- local fileHandle = io.open(filepath, "r")
- if not fileHandle then
- return false
- end
-
- local foundKeywords = false
-
- for line in fileHandle:lines() do
- if line:find("Linux") or line:find("shell") then
- foundKeywords = true
- break
- end
- end
-
- fileHandle:close()
-
- return foundKeywords
-end
-
-function fsUtils.directoryContainsLinuxData(location)
- for file in lfs.dir(location) do
- if (string.find(file,".sh") or not string.find(file,".")) and not fsUtils.isDirectory(location.."/"..file) then
- local containsKeywords = checkFileForKeywords(location, file)
- if containsKeywords then
- return true
- end
- end
- end
-
- return false
-end
-
---[[
- Name : function fsUtils.getFilenamePatternInDirectory(directory, pattern)
- Description : Returns a table containing every filename matching a pattern in a directory.
- Arg 1 : directory (string) : The directory to search in.
- Arg 2 : pattern (string) : The pattern to search for.
- Return : A table containing every filename matching the pattern.
-
- Note : Used to for game detection to work around Steam not updating the libraryfolders.vdf file immediately.
-]]
-function fsUtils.getFilenamePatternInDirectory(directory, pattern)
- local result = {}
-
- for file in lfs.dir(directory) do
- if file:match(pattern) then
- table.insert(result, file)
- end
- end
-
- return result
-end
-
-return fsUtils
diff --git a/modules/general/systemUtils.lua b/modules/general/systemUtils.lua
deleted file mode 100644
index c75b843..0000000
--- a/modules/general/systemUtils.lua
+++ /dev/null
@@ -1,72 +0,0 @@
--- Custom modules
-local logSystem = require("modules.general.logSystem")
-local fsUtils = require("modules.general.fsUtils")
-local programMetadata = require("modules.extra.programMetadata")
-
--- External modules
-local lgi = require("lgi")
-local Notify = lgi.Notify
-Notify.init(programMetadata.name)
-
--- Module
-local systemUtils = {}
-
---[[
- Name : function systemUtils.sendNotification(title, message, urgency, transient, time)
- Description : Sends a notification using notify-send.
- Arg 1 : title (string) : The notification's title.
- Arg 2 : message (string) : The notification's message.
- Arg 3 : type (string) : The notification's type. Used only for icons right now. Can be "warning", "error" or "information".
- Return : nil
-]]
-function systemUtils.sendNotification(title, message, type)
- -- Set icons
- local icon
- if type == "warning" then
- icon = "dialog-warning-symbolic"
- elseif type == "error" then
- icon = "dialog-error-symbolic"
- else
- icon = "dialog-information"
- end
-
- local notification = Notify.Notification.new(title, message, icon)
- notification:show()
-end
-
---[[
- Name : function systemUtils.copyToClipboard(text)
- Description : Copies a text to the clipboard using xclip.
- Arg 1 : text (string) : The text to copy.
- Return : true if successful, false otherwise.
-]]
-function systemUtils.copyToClipboard(text)
- local command = string.format("echo -n %q | xclip -selection clipboard", text)
-
- if not os.execute(command) then
- logSystem.log("error", "Unable to copy to clipboard using command : "..command)
- return false
- else
- return true
- end
-end
-
---[[
- Name : function systemUtils.isInstalled(command)
- Description : Checks if an command is installed.
- Arg 1 : command (string) : The command to check.
- Return : true if installed, false otherwise.
-]]
-function systemUtils.isInstalled(command)
- if command:sub(1, 1) == "/" then
- return fsUtils.exists(command)
- else
- if os.execute("which "..command.." &> /dev/null") then
- return true
- else
- return false
- end
- end
-end
-
-return systemUtils
diff --git a/modules/objects/steamGameObject.lua b/modules/objects/steamGameObject.lua
deleted file mode 100644
index 77e2b07..0000000
--- a/modules/objects/steamGameObject.lua
+++ /dev/null
@@ -1,62 +0,0 @@
--- Internal Modules
-local fsUtils = require("modules.general.fsUtils")
-
--- Constants
-local steamImagesLocation = os.getenv("HOME").."/.local/share/Steam/appcache/librarycache/"
-
--- Extra function
-local function ifExists(path)
- if fsUtils.exists(path) then
- return path
- else
- return nil
- end
-end
-
--- Object
-SteamGame = {
- metatable = {
- __index = SteamGame
- }
-}
-function SteamGame:new (id, name, library, location)
- -- Current way of detecting if an app is a game or a tool... not the greatest.
- local type = "game"
- local libraryIconPath = steamImagesLocation..id.."_library_600x900.jpg"
- if not fsUtils.exists(libraryIconPath) then
- libraryIconPath = nil
- type = "software"
- end
- if name:find("Proton") then
- type = "proton"
- end
-
- -- The actual OS detection is in steamConfigProvider.lua instead. This is here because Linux games can have compatdata left over from previously using Proton.
- local protonConfig = {}
- if fsUtils.exists(library.."/steamapps/compatdata/"..id) then
- protonConfig.compatdata = library.."/steamapps/compatdata/"..id
- end
-
- return setmetatable({
- id = id,
- name = name,
- library = library,
- location = location,
- size = nil, -- Slow, done in UI
- protondb_data = nil, -- VERY slow, done in UI
- type = type,
- os_platform = "Linux", -- Assume Linux first, normally gets reconfigured as Windows later
- proton_config = protonConfig,
- tool_status = false, -- Assume the game is not setup to use the tool, normally gets reconfigured true later
- images = {
- header = ifExists(steamImagesLocation..id.."_header.jpg"),
- icon = ifExists(steamImagesLocation..id.."_icon.jpg"),
- logo = ifExists(steamImagesLocation..id.."_logo.png"),
- library = libraryIconPath,
- library_hero = ifExists(steamImagesLocation..id.."_library_hero.jpg"),
- library_hero_blur = ifExists(steamImagesLocation..id.."_library_hero_blur.jpg"),
- }
- }, SteamGame.metatable)
-end
-
-return SteamGame
diff --git a/modules/objects/toolObject.lua b/modules/objects/toolObject.lua
deleted file mode 100644
index 0f81668..0000000
--- a/modules/objects/toolObject.lua
+++ /dev/null
@@ -1,24 +0,0 @@
--- Internal Modules
-local systemUtils = require("modules.general.systemUtils")
-local logSystem = require("modules.general.logSystem")
-
-Tool = {
- metatable = {
- __index = Tool
- }
-}
-
-function Tool:new (toolName, toolType, modification)
- assert(toolName)
- assert(toolType == "executable" or toolType == "environmentVariable", "Invalid tool type for tool "..toolName..".")
- assert(type(modification) == "function", "Invalid modification for tool "..toolName..".")
-
- return setmetatable({
- toolName = toolName,
- toolType = toolType, -- "executable" or "environmentVariable"
- isInstalled = systemUtils.isInstalled(toolName),
- usage = modification
- }, Tool.metatable)
-end
-
-return Tool
diff --git a/modules/steam/protonDBManager.lua b/modules/steam/protonDBManager.lua
deleted file mode 100644
index 1a5c448..0000000
--- a/modules/steam/protonDBManager.lua
+++ /dev/null
@@ -1,41 +0,0 @@
--- External Modules
-local http = require("socket.http")
-local ltn12 = require("ltn12")
-local json = require("dkjson")
-
--- Internal Modules
-local logSystem = require("modules.general.logSystem")
-
-local protonDBManager = {}
-
---[[
- Name : protonDBManager.getAppInfo(appId)
- Description : Retrieves the ProtonDB data for a given app ID.
- Arg 1 : appId (string) : The app ID to retrieve the data for.
- Return : The ProtonDB data for the given app ID.
-]]
-function protonDBManager.getAppInfo(appId)
- local url = "https://www.protondb.com/api/v1/reports/summaries/"..appId..".json"
- local response = {}
-
- local success, status, headers = http.request {
- url = url,
- method = "GET",
- sink = ltn12.sink.table(response)
- }
-
- if success then
- local data = json.decode(table.concat(response)) -- Parse the JSON response
- if data then
- return data
- else
- logSystem.log("error", "Failed to parse JSON response from ProtonDB for app "..appId..".")
- return "Not found"
- end
- else
- logSystem.log("error", "Failed to retrieve ProtonDB data for app "..appId..", with error : "..status..".")
- return "Unavailable"
- end
-end
-
-return protonDBManager
diff --git a/modules/steam/steamConfigProvider.lua b/modules/steam/steamConfigProvider.lua
deleted file mode 100644
index a922b79..0000000
--- a/modules/steam/steamConfigProvider.lua
+++ /dev/null
@@ -1,144 +0,0 @@
--- Internal Modules
-local logSystem = require("modules.general.logSystem")
-local vdfParser = require("modules.steam.vdfParser")
-local steamUtils = require("modules.steam.steamUtils")
-local programMetadata = require("modules.extra.programMetadata")
-local fsUtils = require("modules.general.fsUtils")
-
--- Timer
-local timeStart
-
---[[
- Chapter 1 : We recover the Steam user config so we can get the last active user.
- And also the SteamID3 to access their settings folder.
-]]
-logSystem.log("fileRead", "Detecting user config...")
-timeStart = os.clock()
-local userData = vdfParser.parseFile(os.getenv("HOME").."/.local/share/Steam/config/loginusers.vdf")
-
--- We add the SteamID3 to the user datas, and also grab the most recent user's ID while we're at it.
-local activeUserID
-for id,data in pairs(userData["users"]) do
- -- SteamID3
- data["steamID3"] = steamUtils.convertToSteamID3(id)
-
- -- Most recent
- if data["MostRecent"] == "1" then
- activeUserID = id
- end
-end
-logSystem.log("speed", timeStart)
-logSystem.log("info", "Active user : "..userData["users"][activeUserID]["AccountName"])
-
---[[
- Chapter 2 : We recover the user config to get which games have the tool enabled.
- This section would normally have quite an impact on performance.
- Thanks to some optimizations, however, the time is cut from 0.5 seconds to 0.04 seconds on my computer.
-]]
-logSystem.log("fileRead", "Detecting active user game configs...")
-timeStart = os.clock()
-
-local userAppSettings = vdfParser.parseFile(
- os.getenv("HOME").."/.local/share/Steam/userdata/"..userData["users"][activeUserID]["steamID3"].."/config/localconfig.vdf",
- {"UserLocalConfigStore","Software","Valve","Steam","apps"},
- {"CachedCommunityPreferences",
- "UIStoreLocalState",
- "CachedStorePreferences",
- "CachedNotificationPreferences",
- "SteamVoiceSettings_",
- "UIStoreLocalSteamUIState",
- "UIStoreLocalGamepadState",
- "GetEquippedProfileItemsForUser",
- "CTextFilterStore_strBannedPattern",
- "CTextFilterStore_strCleanPattern",
- "trendingstore_storage",
- "playnextstore_storage",
- "GetEquippedProfileItemsForUser"} -- Optimization : we remove big lines that we don't need.
-)["UserLocalConfigStore"]["Software"]["Valve"]["Steam"]["apps"]
-logSystem.log("speed", timeStart)
-
---[[
- Chapter 3 : We recover the Steam library config to get the list of games.
- We can't use just libraryfolders.vdf to get the game IDs, as Steam seems to not always update it immediately.
-]]
-logSystem.log("fileRead", "Detecting games...")
-timeStart = os.clock()
-
-local libraryFolders = vdfParser.parseFile(os.getenv("HOME").."/.local/share/Steam/config/libraryfolders.vdf")
-
--- Steam Game Object
-local SteamGame = require("modules.objects.steamGameObject")
--- And list
-local steamGames = {}
-
--- For each library
-for _,folderData in pairs(libraryFolders["libraryfolders"]) do
- -- We check if the folder exists
- if not fsUtils.exists(folderData["path"].."/steamapps") then
- logSystem.log("warning", "Steam library "..folderData["path"].." doesn't exist. Skipping...")
- goto continue
- end
-
- -- We get the list of every appmanifest_*.acf file in the folder
- local appManifestsFileNames = fsUtils.getFilenamePatternInDirectory(folderData["path"].."/steamapps", "appmanifest_")
- for _,appId in pairs(appManifestsFileNames) do
- -- We convert the filename to the app ID
- appId = appId:gsub("appmanifest_", ""):gsub(".acf", "")
-
- local appManifest = vdfParser.parseFile(folderData.path.."/steamapps/appmanifest_"..appId..".acf")
- -- We create a SteamGame object
- local steamGame = SteamGame:new(
- appId,
- appManifest["AppState"]["name"],
- folderData["path"],
- folderData.path.."/steamapps/common/"..appManifest["AppState"]["installdir"]
- )
- -- We add it to the list
- steamGames[appId] = steamGame
- end
-
- ::continue::
-end
-
--- And now we merge user game data and game data
-for gameID, data in pairs(userAppSettings) do
- -- We check if the game is actually installed before attempting a merge.
- if steamGames[gameID] ~= nil then
- -- Tool status
- if data["LaunchOptions"] and string.find(data["LaunchOptions"],programMetadata.executable) then
- steamGames[gameID].status = true
- end
- if data["LaunchOptions"] and string.find(data["LaunchOptions"],"steamtinkerlaunch") then
- steamGames[gameID].status = "SteamTinkerLaunch"
- end
- end
-end
-logSystem.log("speed", timeStart)
-
---[[
- Chapter 4 : We check if the game is in the compat list
-]]
-logSystem.log("fileRead","Detecting games' platforms...")
-timeStart = os.clock()
-
-local compatList = vdfParser.parseFile(
- os.getenv("HOME").."/.local/share/Steam/userdata/"..userData["users"][activeUserID]["steamID3"].."/config/compat.vdf"
-)["platform_overrides"]
-
-for gameID,_ in pairs(compatList) do
- -- Proton Status
- --[[
- WARNING : METHOD WRONGLY SETS WINDOWS STATUS FOR LINUX GAMES THAT WERE PREVIOUSLY USING PROTON
- Workaround : Check if Windows version contains Linux executable or shell script.
- ]]
- if steamGames[gameID] and steamGames.type ~= "proton" then
- if not fsUtils.directoryContainsLinuxData(steamGames[gameID].location) then
- steamGames[gameID].os_platform = "Windows"
- else
- logSystem.log("warning", "Game "..steamGames[gameID].name.." was listed as Windows game, but contains Linux data. Assuming it runs with Linux...")
- end
- end
-end
-logSystem.log("speed", timeStart)
-
-return steamGames
diff --git a/modules/steam/steamUtils.lua b/modules/steam/steamUtils.lua
deleted file mode 100644
index ed19bbb..0000000
--- a/modules/steam/steamUtils.lua
+++ /dev/null
@@ -1,44 +0,0 @@
-local steamUtils = {}
-
---[[
- This function checks if the arguments are from Steam or not.
- I suppose this is where non-Steam game support will be added later.
-]]
-function steamUtils.isSteamArgs(arguments)
- if arguments[7] ~= nil and arguments[7]:find("steamtinkerlaunch") then
- return "steamtinkerlaunch"
- elseif (
- --arguments[1] == os.getenv("HOME").."/.local/share/Steam/ubuntu12_32/reaper" and
- arguments[2] == "SteamLaunch" and
- arguments[3]:match("AppId=%d+") and
- arguments[4] == "--" and
- --arguments[5] == os.getenv("HOME").."/.local/share/Steam/ubuntu12_32/steam-launch-wrapper" and
- arguments[6] == "--"
- ) then
- return "steamGame"
- else
- return false
- end
-end
-
---[[
- Name : function steamUtils.convertToSteamID3(steamID)
- Description : Converts a SteamID64 to a SteamID3
- Arg 1 : string steamID
- Return : string steamID3
-]]
-function steamUtils.convertToSteamID3(steamID)
- local id_str = tostring(steamID)
-
- if tonumber(id_str) then
- local offset_id = tonumber(id_str) - 76561197960265728
- local account_type = offset_id % 2
- local account_id = math.floor((offset_id - account_type) / 2) + account_type
-
- return (account_id * 2) - account_type
- else
- error("Unable to decode SteamID : "..id_str)
- end
-end
-
-return steamUtils
diff --git a/modules/tools/gamemode.lua b/modules/tools/gamemode.lua
deleted file mode 100644
index de54ee9..0000000
--- a/modules/tools/gamemode.lua
+++ /dev/null
@@ -1,10 +0,0 @@
--- Internal Modules
-local toolObject = require("modules.objects.toolObject")
-
-local name = "gamemoderun"
-local type = "executable"
-local usage = function(command)
- return "gamemoderun "..command
-end
-
-return toolObject:new(name, type, usage)
diff --git a/modules/tools/gamescope.lua b/modules/tools/gamescope.lua
deleted file mode 100644
index bc34396..0000000
--- a/modules/tools/gamescope.lua
+++ /dev/null
@@ -1,59 +0,0 @@
--- Internal Modules
-local toolObject = require("modules.objects.toolObject")
-
-local name = "gamescope"
-local type = "executable"
-local usage = function(command, config)
- local options = ""
-
- -- Nested resolution
- if
- config.gamescope.general.resolution.enabled == true
- then
- options = string.format(
- "%s --nested-width %s --nested-height %s --output-width %s --output-height %s",
- options,
- config.gamescope.general.resolution.internal.width,
- config.gamescope.general.resolution.internal.height,
- config.gamescope.general.resolution.external.width,
- config.gamescope.general.resolution.external.height
- )
- end
-
- -- Frame limit
- if config.gamescope.general.frame_limit.enabled == true then
- options = string.format(
- "%s -r %s -o %s",
- options,
- config.gamescope.general.frame_limit.normal,
- config.gamescope.general.frame_limit.unfocused
- )
- end
-
- -- Fullscreen
- if config.gamescope.general.fullscreen == true then
- options = options.." -f"
- end
- if config.gamescope.general.borderless == true then
- options = options.." -b"
- end
-
- -- Filtering
- if config.gamescope.filtering.enabled == true then
- options = string.format(
- "%s --filter %s --sharpness %s",
- options,
- string.lower(config.gamescope.filtering.filter),
- config.gamescope.filtering.sharpness -- gamescope's sharpness is inverted
- )
- end
-
- -- The command
- return string.format(
- "gamescope %s %s",
- options,
- command
- )
-end
-
-return toolObject:new(name, type, usage)
diff --git a/modules/tools/mangohud.lua b/modules/tools/mangohud.lua
deleted file mode 100644
index 59bb082..0000000
--- a/modules/tools/mangohud.lua
+++ /dev/null
@@ -1,10 +0,0 @@
--- Internal Modules
-local toolObject = require("modules.objects.toolObject")
-
-local name = "mangohud"
-local type = "executable"
-local usage = function(command)
- return "mangohud "..command
-end
-
-return toolObject:new(name, type, usage)
diff --git a/modules/tools/obs-gamecapture.lua b/modules/tools/obs-gamecapture.lua
deleted file mode 100644
index b127f52..0000000
--- a/modules/tools/obs-gamecapture.lua
+++ /dev/null
@@ -1,17 +0,0 @@
--- Internal Modules
-local toolObject = require("modules.objects.toolObject")
-
---[[
- I was tempted to separate this tool in 2 options : one for the utility (supports Vulkan & OpenGL),
- and another for the environment variable which can be used with Vulkan games.
- However, I chose to stay simple and use what works for all : the utility.
- This is seemingly also the approach SteamTinkerLaunch goes with, judging from its wiki.
-]]
-
-local name = "obs-gamecapture"
-local type = "executable"
-local usage = function(command)
- return "obs-gamecapture "..command
-end
-
-return toolObject:new(name, type, usage)
diff --git a/modules/tools/switcherooctl.lua b/modules/tools/switcherooctl.lua
deleted file mode 100644
index 2aa25df..0000000
--- a/modules/tools/switcherooctl.lua
+++ /dev/null
@@ -1,15 +0,0 @@
--- Internal Modules
-local toolObject = require("modules.objects.toolObject")
-
-local name = "switcherooctl"
-local type = "executable"
-local usage = function(command, config)
- if config.utilities.zink.enabled == true then
- -- We let Zink handle the dGPU
- return command
- else
- return "switcherooctl launch "..command
- end
-end
-
-return toolObject:new(name, type, usage)
diff --git a/modules/tools/zink.lua b/modules/tools/zink.lua
deleted file mode 100644
index 3058c2a..0000000
--- a/modules/tools/zink.lua
+++ /dev/null
@@ -1,15 +0,0 @@
--- Internal Modules
-local toolObject = require("modules.objects.toolObject")
-
-local name = "/usr/lib/dri/zink_dri.so"
-local toolType = "environmentVariable"
-local usage = function(command, config)
- if config.dgpu.enabled and require("modules.tools.switcherooctl").isInstalled then
- -- We override switcherooctl (in its function) and let Zink handle the dGPU
- return "DRI_PRIME=1 __GLX_VENDOR_LIBRARY_NAME=mesa MESA_LOADER_DRIVER_OVERRIDE=zink GALLIUM_DRIVER=zink"
- else
- return "MESA_LOADER_DRIVER_OVERRIDE=zink"
- end
-end
-
-return toolObject:new(name, toolType, usage)
diff --git a/modules/ui/UItoSettingsList.lua b/modules/ui/UItoSettingsList.lua
deleted file mode 100644
index 29a6f8d..0000000
--- a/modules/ui/UItoSettingsList.lua
+++ /dev/null
@@ -1,52 +0,0 @@
-return {
- -- Settings
- ["dGPU_Switch"] = {type= "Switch", tool = "switcherooctl", setting = "dgpu.enabled"},
- ["SDL_Wayland_Switch"] = {type= "Switch", setting = "misc.sdl_wayland", os_platform = "Linux"},
-
- -- Utilities
- ["gamemode_Switch"] = {type= "Switch", tool = "gamemoderun", setting = "utilities.gamemode.enabled"},
- ["mangohud_Switch"] = {type= "Switch", tool = "mangohud", setting = "utilities.mangohud.enabled"},
- ["zink_Switch"] = {type= "Switch", tool = "/usr/lib/dri/zink_dri.so", setting = "utilities.zink.enabled"},
- ["obs_gamecapture_Switch"] = {type= "Switch", tool = "obs-gamecapture", setting = "utilities.obs_gamecapture.enabled"},
-
- -- Gamescope
- ["gamescope_Switch"] = {type= "Switch", tool = "gamescope", setting = "gamescope.enabled"},
- -- Resolution
- ["gamescope_Resolution_Switch"] = {type= "Switch", tool = "gamescope", setting = "gamescope.general.resolution.enabled"},
- ["gamescope_Resolution_Internal_Width_SpinRow"] = {type= "SpinRow", tool = "gamescope", setting = "gamescope.general.resolution.internal.width"},
- ["gamescope_Resolution_Internal_Height_SpinRow"] = {type= "SpinRow", tool = "gamescope", setting = "gamescope.general.resolution.internal.height"},
- ["gamescope_Resolution_External_Width_SpinRow"] = {type= "SpinRow", tool = "gamescope", setting = "gamescope.general.resolution.external.width"},
- ["gamescope_Resolution_External_Height_SpinRow"] = {type= "SpinRow", tool = "gamescope", setting = "gamescope.general.resolution.external.height"},
- -- Framerate
- ["gamescope_Framerate_Switch"] = {type= "Switch", tool = "gamescope", setting = "gamescope.general.frame_limit.enabled"},
- ["gamescope_Framerate_Normal_SpinRow"] = {type= "SpinRow", tool = "gamescope", setting = "gamescope.general.frame_limit.normal"},
- ["gamescope_Framerate_Unfocused_SpinRow"] = {type= "SpinRow", tool = "gamescope", setting = "gamescope.general.frame_limit.unfocused"},
- -- Filtering
- ["gamescope_Filtering_Switch"] = {type= "Switch", tool = "gamescope", setting = "gamescope.filtering.enabled"},
- ["gamescope_Filtering_Sharpness_SpinRow"] = {type= "SpinRow", tool = "gamescope", setting = "gamescope.filtering.sharpness"},
- ["gamescope_Filtering_Filter_ComboRow"] = {type= "ComboRow", tool = "gamescope", setting = "gamescope.filtering.filter", items = {"Linear","Nearest","FSR","NIS","Pixel"}},
- -- Extras
- ["gamescope_Borderless_Toggle"] = {type= "Toggle", tool = "gamescope", setting = "gamescope.general.borderless"},
- ["gamescope_Fullscreen_Toggle"] = {type= "Toggle", tool = "gamescope", setting = "gamescope.general.fullscreen"},
-
- -- Proton
- -- Direct3D
- ["Direct3D9_Switch"] = {type= "Switch", setting = "proton.direct3d.enable_direct3d9", os_platform = "Windows"},
- ["Direct3D10_Switch"] = {type= "Switch", setting = "proton.direct3d.enable_direct3d10", os_platform = "Windows"},
- ["Direct3D11_Switch"] = {type= "Switch", setting = "proton.direct3d.enable_direct3d11", os_platform = "Windows"},
- ["Direct3D12_Switch"] = {type= "Switch", setting = "proton.direct3d.enable_direct3d12", os_platform = "Windows"},
- ["WineD3D_Switch"] = {type= "Switch", setting = "proton.direct3d.use_wined3d", os_platform = "Windows"},
- -- Sync
- ["ESync_Toggle"] = {type= "Toggle", setting = "proton.sync.enable_esync", os_platform = "Windows"},
- ["FSync_Toggle"] = {type= "Toggle", setting = "proton.sync.enable_fsync", os_platform = "Windows"},
- -- NVIDIA
- ["Hide_NVIDIA_GPU_Switch"] = {type= "Switch", setting = "proton.nvidia.hide_nvidia_gpu", os_platform = "Windows"},
- ["Enable_NVAPI_Switch"] = {type= "Switch", setting = "proton.nvidia.enable_nvapi", os_platform = "Windows"},
- -- FSR
- ["Wine_FSR_Switch"] = {type= "Switch", setting = "proton.fsr.enabled", os_platform = "Windows"},
- ["Wine_FSR_Sharpness_SpinRow"] = {type= "SpinRow", setting = "proton.fsr.sharpness", os_platform = "Windows"},
- ["Wine_FSR_Upscaling_Resolution_Mode_ComboRow"] = {type = "ComboRow", setting = "proton.fsr.upscaling_mode", os_platform = "Windows", items = {"None", "Performance", "Balanced", "Quality", "Ultra"}},
- ["Wine_FSR_Resolution_Switch"] = {type = "Switch", setting = "proton.fsr.resolution.enabled", os_platform = "Windows"},
- ["Wine_FSR_Resolution_External_Width_SpinRow"] = {type= "SpinRow", setting = "proton.fsr.resolution.width", os_platform = "Windows"},
- ["Wine_FSR_Resolution_External_Height_SpinRow"] = {type= "SpinRow", setting = "proton.fsr.resolution.height", os_platform = "Windows"}
-}
diff --git a/modules/ui/aboutWindow.lua b/modules/ui/aboutWindow.lua
deleted file mode 100644
index 8529e8b..0000000
--- a/modules/ui/aboutWindow.lua
+++ /dev/null
@@ -1,73 +0,0 @@
--- External Modules
-local lgi = require("lgi")
-local Adw = lgi.Adw
-local Gtk = lgi.Gtk
-
--- Internal Modules
-local programMetadata = require("modules.extra.programMetadata")
-
--- Translation fake function
-local function _(text) return text end
-
-return function(application, interface, win)
- local creditLauncher = interface:get_object("aboutLauncher")
- creditLauncher.on_clicked = function()
- -- Create window with main metadata already set
- local creditWindow = Adw.AboutWindow{
- application = application,
- modal = true,
-
- application_icon = programMetadata.icon_name,
- application_name = programMetadata.name,
- version = programMetadata.version,
-
- title = _("About ")..programMetadata.name,
- developer_name = programMetadata.developer,
- website = programMetadata.url,
- issue_url = programMetadata.url.."/issues",
- license_type = Gtk.License.MPL_2_0,
-
- developers = {
- "JordanViknar https://github.com/JordanViknar",
- },
- artists = {
- "JordanViknar https://github.com/JordanViknar"
- },
- documenters = {
- "JordanViknar https://github.com/JordanViknar"
- }
- }
-
- -- Credits
- creditWindow:add_acknowledgement_section(_("Inspired by"),{
- "SteamTinkerLaunch https://github.com/sonic2kk/steamtinkerlaunch",
- "Bottles https://github.com/bottlesdevs/Bottles"
- })
- creditWindow:add_acknowledgement_section(_("Powered by"),{
- "Lua https://www.lua.org/",
- "LGI https://github.com/lgi-devs/lgi",
- "GTK https://www.gtk.org/",
- "libadwaita https://gnome.pages.gitlab.gnome.org/libadwaita/",
- "LuaFileSystem https://github.com/lunarmodules/luafilesystem",
- "LuaSocket https://github.com/lunarmodules/luasocket",
- "dkjson http://dkolf.de/src/dkjson-lua.fsl/home"
- })
- creditWindow:add_acknowledgement_section(_("Third-Party Tools and Special Thanks"),{
- "Steam https://store.steampowered.com/",
- "Proton https://github.com/ValveSoftware/proton",
- "GameMode https://github.com/FeralInteractive/gamemode",
- "MangoHud https://github.com/flightlessmango/MangoHud",
- "Zink https://docs.mesa3d.org/drivers/zink.html",
- "ProtonDB https://www.protondb.com/",
- "PCGamingWiki https://www.pcgamingwiki.com/",
- "SteamDB https://steamdb.info/",
- "switcheroo-control https://gitlab.freedesktop.org/hadess/switcheroo-control",
- "Gamescope https://github.com/ValveSoftware/gamescope"
- })
-
- creditWindow:set_comments(_("A work-in-progress fast, simple and modern Libadwaita alternative to SteamTinkerLaunch."))
-
- creditWindow:set_transient_for(win)
- creditWindow:present()
- end
-end
diff --git a/modules/ui/gameSettingsOverview.lua b/modules/ui/gameSettingsOverview.lua
deleted file mode 100644
index 82064dd..0000000
--- a/modules/ui/gameSettingsOverview.lua
+++ /dev/null
@@ -1,167 +0,0 @@
--- Internal Modules
-local lgiHelper = require("modules.ui.lgiHelper")
-local protonDBManager = require("modules.steam.protonDBManager")
-local fsUtils = require("modules.general.fsUtils")
-local logSystem = require("modules.general.logSystem")
-local systemUtils = require("modules.general.systemUtils")
-
--- External Modules
-local lgi = require("lgi")
-local Adw = lgi.Adw
-local Gio = lgi.Gio
-
-return function(application, interface, steamGameData)
- --[[
- SIDEBAR
- ]]
- -- Set the game banner in the sidebar
- interface:get_object("Sidebar_Banner"):set_filename(steamGameData.images.header)
-
- --[[
- OVERVIEW
- ]]
- -- Sets the game image in the overview area
- interface:get_object("Overview_Picture"):set_filename(steamGameData.images.library)
- -- Sets the game title in the overview area
- interface:get_object("gameTitle").label = steamGameData.name
-
- -- ProtonDB rating
- local protonRating_Label = interface:get_object("protonDBRating_Label")
- protonRating_Label.label = "Loading..."
- protonRating_Label.css_classes = {"dim-label"}
- if steamGameData.os_platform == "Windows" then
-
- local function loadProtonDBrating()
- -- If the game's rating hasn't been retrieved yet, we retrieve it.
- -- The reason why this is done HERE is because it would slow down startup otherwise.
- if steamGameData.protondb_data == nil then
- logSystem.log("download", "Loading ProtonDB rating for "..steamGameData.name.."...")
- steamGameData.protondb_data = protonDBManager.getAppInfo(steamGameData.id)
- end
-
- -- We set the rating label
- if steamGameData.protondb_data == "Not found" then
- protonRating_Label.css_classes = {"error"}
- elseif steamGameData.protondb_data == "Unavailable" then
- protonRating_Label.css_classes = {"warning"}
- elseif steamGameData.protondb_data["tier"] == "platinum" then
- protonRating_Label.css_classes = {"success"}
- elseif steamGameData.protondb_data["tier"] == "silver" or "gold" then
- protonRating_Label.css_classes = {"warning"}
- else
- protonRating_Label.css_classes = {"error"}
- end
- local rating = steamGameData.protondb_data["tier"] or steamGameData.protondb_data
- -- Set label and capitalize the first letter
- protonRating_Label.label = rating:sub(1, 1):upper()..rating:sub(2)
- end
- Gio.Async.start(loadProtonDBrating)()
-
- else
- protonRating_Label.label = "Native"
- protonRating_Label.css_classes = {"success"}
- end
-
- -- Sets up the game start button
- local gameLaunchButton = interface:get_object("gameLaunchButton")
- lgiHelper.replaceSignal(gameLaunchButton, "on_clicked", function()
- os.execute("xdg-open steam://rungameid/"..steamGameData.id.." &> /dev/null")
- end)
-
- -- Sets up the game's SimpleSteamTinker status
- local gameStatus, gameColor
- if steamGameData.status == true then
- gameStatus = "Enabled"
- gameColor = "success"
- elseif steamGameData.status == "SteamTinkerLaunch" then
- gameStatus = "SteamTinkerLaunch"
- gameColor = "warning"
- else
- gameStatus = "Disabled"
- gameColor = "error"
- end
- local gameStatus_Label = interface:get_object("gameStatus_Label")
- gameStatus_Label.label = gameStatus
- gameStatus_Label.css_classes = {gameColor}
-
- -- Sets the game ID in the overview area
- interface:get_object("gameID_Label").label = steamGameData.id
- -- Sets up the game ID copy button in the overview area
- local toastSystem = interface:get_object("toastSystem")
- local gameIDCopyButton = interface:get_object("gameID_copyButton")
- lgiHelper.replaceSignal(gameIDCopyButton, "on_clicked", function()
- if systemUtils.copyToClipboard(steamGameData.id) then
- toastSystem:add_toast(
- Adw.Toast.new("Game ID copied to clipboard !")
- )
- end
- end)
- -- Sets the game platform in the overview area
- interface:get_object("gamePlatform_Label").label = steamGameData.os_platform
-
- -- Sets the game size in the overview area
- local gameSize_Label = interface:get_object("gameSize_Label")
- gameSize_Label.label = "Loading..."
- if not steamGameData.size then
- logSystem.log("fileRead", "Detecting size for "..steamGameData.name.."...")
-
- local function insertGameSize()
- steamGameData.size = fsUtils.sizeToUnit(fsUtils.getSize(steamGameData.location))
- gameSize_Label.label = steamGameData.size
- end
- Gio.Async.start(insertGameSize)()
-
- else
- gameSize_Label.label = steamGameData.size
- end
-
- -- Sets the game folder in the overview area
- interface:get_object("gameLocation_ActionRow").subtitle = steamGameData.location
- -- Modifies the button to get to the game's folder
- local gameFolderButton = interface:get_object("gameLocationButton")
- lgiHelper.replaceSignal(gameFolderButton, "on_clicked", function()
- os.execute("xdg-open '"..steamGameData.location:gsub("'", "'\\''").."' &> /dev/null")
- end)
-
- -- Sets the compatdata folder in the overview area
- local gameCompatdata_ActionRow = interface:get_object("gameCompatdata_ActionRow")
- if steamGameData.os_platform == "Windows" then
- gameCompatdata_ActionRow.visible = true
- if steamGameData.proton_config.compatdata then
- gameCompatdata_ActionRow:set_sensitive(true)
- gameCompatdata_ActionRow.subtitle = steamGameData.proton_config.compatdata
- -- Modifies the button to get to the game's compatdata folder
- lgiHelper.replaceSignal(interface:get_object("gameCompatdataButton"), "on_clicked", function()
- os.execute("xdg-open '"..steamGameData.proton_config.compatdata:gsub("'", "'\\''").."' &> /dev/null")
- end)
- else
- gameCompatdata_ActionRow:set_sensitive(false)
- gameCompatdata_ActionRow.subtitle = "Not found. Try launching the game at least once ?"
- end
- else
- gameCompatdata_ActionRow.visible = false
- end
-
- --[[
- LINKS
- ]]
- local protonDBPage_Button = interface:get_object("protonDBPage_Button")
- lgiHelper.replaceSignal(protonDBPage_Button, "on_clicked", function()
- os.execute("xdg-open 'https://www.protondb.com/app/"..steamGameData.id.."' &> /dev/null")
- end)
-
- local steamDBPage_Button = interface:get_object("steamDBPage_Button")
- lgiHelper.replaceSignal(steamDBPage_Button, "on_clicked", function()
- os.execute("xdg-open 'https://steamdb.info/app/"..steamGameData.id.."' &> /dev/null")
- end)
-
- local PCGamingWikiPage_Button = interface:get_object("PCGamingWikiPage_Button")
- lgiHelper.replaceSignal(PCGamingWikiPage_Button, "on_clicked", function()
- os.execute("xdg-open 'https://www.pcgamingwiki.com/wiki/"..steamGameData.name:gsub(" ", "_").."' &> /dev/null")
- end)
-
- local SteambasePage_Button = interface:get_object("SteambasePage_Button")
- lgiHelper.replaceSignal(SteambasePage_Button, "on_clicked", function()
- os.execute("xdg-open 'https://steambase.io/apps/"..steamGameData.id.."' &> /dev/null")
- end)
-end
diff --git a/modules/ui/lgiHelper.lua b/modules/ui/lgiHelper.lua
deleted file mode 100644
index 75b1830..0000000
--- a/modules/ui/lgiHelper.lua
+++ /dev/null
@@ -1,77 +0,0 @@
--- External Modules
-local lgi = require("lgi")
-local GObject = lgi.GObject
-
--- Internal Modules
-local systemUtils = require("modules.general.systemUtils")
-local configManager = require("modules.config.configManager")
-
-local lgiHelper = {}
-
---[[
- Name : function lgiHelper.replaceSignal(object, signal, action)
- Description : This function helps replacing signals on widgets.
- For example, if you want to replace the "on_clicked" signal of a button.
-
- It avoids bugs like opening a game's directory also opening the previous ones.
- Arg 1 : The object to modify.
- Arg 2 : The signal to replace.
- Arg 3 : The function to run (when clicking for example).
-]]
-local signalList = {}
-function lgiHelper.replaceSignal(object, signal, action)
- lgiHelper.removeSignal(object, signal)
-
- if signalList[object] == nil then signalList[object] = {} end
- signalList[object][signal] = object[signal]:connect(action)
-end
-
---[[
- Name : function lgiHelper.removeSignal(object, signal)
- Description : Simply deactivate the function of a button.
- Arg 1 : The object to modify.
- Arg 2 : The signal to remove.
-]]
-function lgiHelper.removeSignal(object, signal)
- -- We create a table for the object if it doesn't exist.
- if signalList[object] == nil then signalList[object] = {} end
- -- If the object already has an event connected to the same signal, we disconnect it.
- if signalList[object][signal] then
- GObject.signal_handler_disconnect(object, signalList[object][signal])
- signalList[object][signal] = nil
- end
-end
-
---[[
- Name : function lgiHelper.connectUtilityToButton(id, button, utility, property, setting)
- Description : Connects a utility to a button.
- Arg 1 : string id : The game's ID.
- Arg 2 : The button to connect.
- Arg 3 : string utility : The utility's name.
- Arg 4 : string property : The property to connect.
- Arg 5 : string setting : The setting to modify.
-]]
-function lgiHelper.connectUtilityToButton(id, button, utility, property, setting)
- local signal = "on_state_set"
- lgiHelper.removeSignal(button, signal)
-
- -- If utility begins with /, then it's a file path.
- local isPresent = systemUtils.isInstalled(utility)
-
- if isPresent then
- button:set_sensitive(true)
- button:set_active(property)
-
- if signalList[button] == nil then signalList[button] = {} end
- signalList[button][signal] = button[signal]:connect(function()
- configManager.modifyGameConfig(id, setting, button:get_active())
- end)
- else
- button:set_sensitive(false)
- button:set_active(false)
- button.has_tooltip = true
- button.tooltip_text = "'"..utility.."' is not present on your system."
- end
-end
-
-return lgiHelper
diff --git a/modules/ui/mainWindow.lua b/modules/ui/mainWindow.lua
deleted file mode 100644
index da8014a..0000000
--- a/modules/ui/mainWindow.lua
+++ /dev/null
@@ -1,236 +0,0 @@
--- Internal Modules
-local programMetadata = require("modules.extra.programMetadata")
-local configManager = require("modules.config.configManager")
-local systemUtils = require("modules.general.systemUtils")
-local lgiHelper = require("modules.ui.lgiHelper")
-
--- External Modules
-local lgi = require("lgi")
-local Gtk = lgi.require("Gtk")
-local Adw = lgi.Adw
-
--- Install checks
-local installedList = {}
-
-return function(app, steamGames)
- -- We create the window
- local builder = Gtk.Builder.new_from_file(programMetadata.installdir.."ui/main.ui")
- local win = builder:get_object("mainWindow")
- win.application = app
- win.title = programMetadata.name
- win.startup_id = programMetadata.name
-
- -- Check for dev version and add relevant theme
- if programMetadata.version:find("dev") or programMetadata.version:find("a") or programMetadata.version:find("b") then
- win:add_css_class("devel")
- end
-
- --[[
- UI ELEMENTS
- ]]
- -- Page management
- local mainView = builder:get_object("mainView")
- local gameList = builder:get_object("gameList")
-
- local gameSettingsInterface = builder:get_object("gameSettings")
-
- -- Stack test
- local stack = builder:get_object("gameSettingsStack")
-
- -- For every game that has SimpleSteamMod enabled, we add a button in this list
- for _, game in pairs(steamGames) do
- if game.type == "game" then
- local statusIcon, statusLabel, statusColor
- -- Status label and icon
- if game.status == true then
- statusIcon = "emblem-default-symbolic"
- statusLabel = programMetadata.name.." is enabled for this game."
- statusColor = "success"
- elseif game.status == "SteamTinkerLaunch" then
- statusIcon = "emblem-important-symbolic"
- statusLabel = "SteamTinkerLaunch is enabled for this game instead of "..programMetadata.name.."."
- statusColor = "warning"
- else
- statusIcon = "dialog-error-symbolic"
- statusLabel = programMetadata.name.." is disabled for this game."
- statusColor = ""
- end
-
- -- OS Label
- local osLabel, osColor, osTooltip
- if game.os_platform == "Linux" then
- osLabel = "Native"
- osColor = "success"
- osTooltip = "This is a native Linux game."
- elseif game.os_platform == "Windows" then
- osLabel = "Proton"
- osColor = "accent"
- osTooltip = "This is a Windows game running through Proton."
- end
-
- -- The row
- local row = Adw.ActionRow {
- title = game.name,
- use_markup = false, -- Used to escape the ampersand
- activatable = true,
- subtitle_lines = 1
- }
-
- -- The game's image
- local image = Gtk.Image {
- file = game.images.icon,
- icon_size = Gtk.IconSize.LARGE
- }
- row:add_prefix(image)
-
- local box = Gtk.Box {
- orientation = Gtk.Orientation.HORIZONTAL,
- spacing = 12,
- Gtk.Label {
- label = osLabel,
- css_classes = { osColor, "dim-label" },
- has_tooltip = true,
- tooltip_text = osTooltip
- },
- Gtk.Image {
- icon_name = statusIcon,
- valign = Gtk.Align.CENTER,
- halign = Gtk.Align.CENTER,
- has_tooltip = true,
- tooltip_text = statusLabel,
- css_classes = { statusColor }
- }
- }
- local button = Gtk.Button {
- id = "button",
- icon_name = "go-next-symbolic",
- has_frame = false,
- valign = Gtk.Align.CENTER,
- halign = Gtk.Align.CENTER,
- css_classes = { "circular", "flat", "image-button" },
- on_clicked = function()
- -- We setup the gameSettings Overview UI for this game
- require("modules.ui.gameSettingsOverview")(app, builder, game)
-
- -- We connect the UI elements to the game settings
- local UIelements = require("modules.ui.UItoSettingsList")
- local gameSettings = configManager.getGameConfig(game.id)
-
- -- We iterate through the UI elements
- for widgetname, data in pairs(UIelements) do
- local widget = builder:get_object(widgetname)
-
- -- Split the setting into keys
- local keys = {}
- for substring in data.setting:gmatch("[^.]+") do
- keys[#keys + 1] = substring
- end
- -- Connect to the settings table
- local pointer = gameSettings
- for i = 1, #keys - 1 do
- pointer = pointer[keys[i]] or {}
- end
-
- -- We determine the right signal to use
- local signal
- if data.type == "Switch" then signal = "on_state_set"
- elseif data.type == "SpinRow" then signal = "on_changed"
- elseif data.type == "Toggle" then signal = "on_toggled"
- elseif data.type == "ComboRow" then
- signal = "on_notify" -- Not optimal, but it works. Isn't there a better way to do this ?
- else
- error("Unknown signal to use with type '"..data.type.."' for UI element '"..widgetname.."'")
- end
-
- lgiHelper.removeSignal(widget, signal)
-
- -- We hide the option if it doesn't fit the platform
- if data.os_platform and data.os_platform ~= game.os_platform then
- widget.visible = false
- goto skip
- elseif widget.visible ~= true then
- widget.visible = true
- end
-
- -- Should they be active ? Is the related tool installed ?
- if data.tool then
- if not installedList[data.tool] then
- installedList[data.tool] = systemUtils.isInstalled(data.tool)
- end
- if installedList[data.tool] == false then
- widget:set_sensitive(false)
- widget.has_tooltip = true
- widget.tooltip_text = data.tool.." is not present on your system."
- goto skip
- else
- widget:set_sensitive(true)
- end
- end
-
- -- We connect the UI element to the setting
- if data.type == "Switch" or data.type == "Toggle" then
- widget:set_active(pointer[keys[#keys]])
- lgiHelper.replaceSignal(widget, signal,function() configManager.modifyGameConfig(game.id, data.setting, widget:get_active()) end)
- elseif data.type == "SpinRow" then
- widget.value = pointer[keys[#keys]]
- lgiHelper.replaceSignal(widget, signal,function() configManager.modifyGameConfig(game.id, data.setting, math.floor(widget:get_value())) end)
- elseif data.type == "ComboRow" then
- -- We grab the model used
- local model = widget:get_model()
-
- -- We select the item that's already configured in the settings
- for index, filter in ipairs(data.items) do
- local currentFilter = configManager.grabInTableFromString(gameSettings, data.setting)
- if currentFilter == filter then
- widget:set_selected(index - 1)
- break -- No need to continue
- end
- end
-
- -- We connect to the setting
- lgiHelper.replaceSignal(widget, signal, function()
- local selected = model:get_string(widget:get_selected())
- configManager.modifyGameConfig(game.id, data.setting, selected)
- end)
- else
- error("Unknown type '"..data.type.."' for UI element '"..widgetname.."'")
- end
-
- ::skip::
- end
-
- -- Windows only pages
- local windowsPages = {"protonPage"}
- for _, page in ipairs(windowsPages) do
- page = builder:get_object(page)
- if game.os_platform == "Windows" then
- page.visible = true
- else
- page.visible = false
- end
- end
-
- -- We finally push the settings page to the user.
- mainView:push(gameSettingsInterface)
- end
- }
- box:append(button)
- row:add_suffix(box)
- row.activatable_widget = button
-
- gameList:add(row)
- end
- end
-
- --Topbar management
- builder:get_object("mainPageTopbar"):set_top_bar_style(Adw.ToolbarStyle.RAISED_BORDER)
- builder:get_object("gameSettingsTopbar"):set_top_bar_style(Adw.ToolbarStyle.RAISED_BORDER)
-
- -- Overview restoration when we exit out of the settings
- gameSettingsInterface.on_hidden = function()
- stack:set_visible_child_name("overviewPage")
- end
-
- -- Credits
- require("modules.ui.aboutWindow")(app, builder, win)
-end
diff --git a/modules/utilitiesList.lua b/modules/utilitiesList.lua
deleted file mode 100644
index e31d503..0000000
--- a/modules/utilitiesList.lua
+++ /dev/null
@@ -1,15 +0,0 @@
-return function (gameConfig)
- return
-
- -- Executables are ordered by priority (first only applies to the game, last applies to the whole sequence)
-
- {
- {require("modules.tools.zink"),gameConfig.utilities.zink.enabled},
- {require("modules.tools.gamescope"),gameConfig.gamescope.enabled},
- {require("modules.tools.switcherooctl"),gameConfig.dgpu.enabled},
- {require("modules.tools.mangohud"),gameConfig.utilities.mangohud.enabled},
- {require("modules.tools.obs-gamecapture"),gameConfig.utilities.obs_gamecapture.enabled},
- {require("modules.tools.gamemode"),gameConfig.utilities.gamemode.enabled}
- }
-
-end
diff --git a/sst b/sst
deleted file mode 100755
index 53f5969..0000000
--- a/sst
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/sh
-
-# Check if installed locally (main.lua is in the same directory as this script) or in the system (main.lua is in /usr/share/simplesteammod)
-if [ -f "$(dirname $0)/main.lua" ]; then
- # Installed locally
- SCRIPT_PATH="$(dirname $0)"
-elif [ -f "/usr/share/SimpleSteamTinker/main.lua" ]; then
- # Installed in the system
- SCRIPT_PATH="/usr/share/SimpleSteamTinker"
-else
- echo "main.lua not found. Please install SimpleSteamTinker and try again."
- exit 1
-fi
-
-# Set LUA_PATH to avoid missing require issues, using the detected directory
-export LUA_PATH="$SCRIPT_PATH/?.lua;$SCRIPT_PATH/?/init.lua;;"
-
-# Provide script directory
-export SST_SCRIPT_PATH="$SCRIPT_PATH/"
-
-# Check if Lua is installed and run main.lua, accounting for the script path
-if [ -x "$(command -v lua)" ]; then
- lua $SCRIPT_PATH/main.lua "$@"
-else
- echo "No Lua installation found. Please install Lua and try again."
- exit 1
-fi
diff --git a/steam/init.luau b/steam/init.luau
new file mode 100644
index 0000000..cfd0c9b
--- /dev/null
+++ b/steam/init.luau
@@ -0,0 +1,4 @@
+--!strict
+return {
+ steamConfig = require("steam_config_provider"),
+}
\ No newline at end of file
diff --git a/steam/steam_config_provider.luau b/steam/steam_config_provider.luau
new file mode 100644
index 0000000..0caa676
--- /dev/null
+++ b/steam/steam_config_provider.luau
@@ -0,0 +1,167 @@
+--!strict
+
+-- Modules
+local process = require("@lune/process")
+
+local logSystem = require("../general").logSystem
+-- local fsUtils = require("../general").fsUtils
+local steamUtils = require("steam_utils")
+local vdfParser = require("vdf_parser")
+
+-- Definitions
+type SteamID64_Dec = number
+type SteamID3 = number
+
+type loginusers_vdf = {
+ users : {
+ [SteamID64_Dec] : {
+ AccountName : string,
+ --PersonaName : string,
+ --RememberPassword : boolean,
+ --WantsOfflineMode : boolean,
+ --SkipOfflineModeWarning : boolean,
+ --AllowAutoLogin : boolean,
+ MostRecent : boolean,
+ --TimeStamp : number,
+ SteamID3 : SteamID3 -- Added manually later on
+ }
+ }
+}
+type localconfig_vdf = {
+ UserLocalConfigStore : {
+ Software : {
+ Valve : {
+ Steam : {
+ apps : {
+ [number] : {
+ --[[cloud : {
+ last_sync_state : string?,
+ quota_bytes : number?,
+ quota_files : number?,
+ used_bytes : number?,
+ used_files : number?,
+ }?,
+ LastPlayed : number?,
+ Playtime : (number | boolean)?,
+ BadgeData : string?,]]--
+ LaunchOptions : string?,
+ --[[Playtime2wks : boolean?,
+ autocloud : {
+ lastexit : number,
+ lastlaunch : number,
+ }?]]
+ }
+ }
+ }
+ }
+ }
+ }
+}
+type libraryfolders_vdf = {
+ libraryfolders : {
+ [number] : {
+ apps : {
+ [number] : number
+ },
+ --contentid : number,
+ --label : string,
+ path : string,
+ --time_last_update_corruption : number | boolean,
+ --totalsize : boolean,
+ --update_clean_bytes_tally : number | boolean
+ }
+ }
+}
+
+-- Timer
+local timeStart: number
+
+--[[
+ Chapter 1 : We recover the Steam user config so we can get the last active user.
+ And also the SteamID3 to access their settings folder.
+]]
+logSystem.log("data", "Detecting user config...")
+timeStart = os.clock()
+
+local result, userData: loginusers_vdf = pcall(vdfParser.parseFile, (process.env.HOME.."/.local/share/Steam/config/loginusers.vdf"))
+if not result then
+ logSystem.log("error", "Failed to parse loginusers.vdf.")
+ return nil
+end
+
+-- We add the SteamID3 to the user data, and also grab the most recent user's ID while we're at it.
+local activeUserID: SteamID64_Dec
+for id,data in pairs(userData.users) do
+ -- SteamID3
+ data.SteamID3 = steamUtils.convertToSteamID3(id)
+
+ -- Most recent
+ if data.MostRecent == true then
+ activeUserID = id
+ end
+end
+logSystem.log("speed", timeStart)
+
+print(userData)
+
+logSystem.log("info", "Active user : "..userData.users[activeUserID].AccountName)
+
+--[[
+ Chapter 2 : We recover the user config to get which games have the tool enabled.
+ This section would normally have quite an impact on performance.
+ Thanks to some optimizations, however, the time is cut from 0.5 seconds to 0.04 seconds on my computer.
+]]
+logSystem.log("data", "Detecting active user game configs...")
+timeStart = os.clock()
+
+local appParseSettings = {
+ file = process.env.HOME.."/.local/share/Steam/userdata/"..userData.users[activeUserID].SteamID3.."/config/localconfig.vdf",
+ stopKeyList = {
+ "UserLocalConfigStore",
+ "Software",
+ "Valve",
+ "Steam",
+ "apps"
+ },
+ wordsFromLinesToRemove = {
+ "CachedCommunityPreferences",
+ "UIStoreLocalState",
+ "CachedStorePreferences",
+ "CachedNotificationPreferences",
+ "SteamVoiceSettings_",
+ "UIStoreLocalSteamUIState",
+ "UIStoreLocalGamepadState",
+ "GetEquippedProfileItemsForUser",
+ "CTextFilterStore_strBannedPattern",
+ "CTextFilterStore_strCleanPattern",
+ "trendingstore_storage",
+ "playnextstore_storage",
+ "GetEquippedProfileItemsForUser"
+ }
+}
+
+local result, localconfig_vdf: localconfig_vdf = pcall(vdfParser.parseFile, appParseSettings.file, appParseSettings.stopKeyList, appParseSettings.wordsFromLinesToRemove)
+if not result then
+ logSystem.log("error", "Failed to parse active user localconfig.vdf with SteamID3 : "..userData.users[activeUserID].SteamID3)
+ return nil
+end
+logSystem.log("speed", timeStart)
+
+local userAppSettings = localconfig_vdf.UserLocalConfigStore.Software.Valve.Steam.apps
+print(userAppSettings)
+
+--[[
+ Chapter 3 : We recover the Steam library config to get the list of games.
+ We can't use just libraryfolders.vdf to get the game IDs, as Steam seems to not always update it immediately.
+]]
+logSystem.log("data", "Detecting games...")
+timeStart = os.clock()
+
+local result, libraryFolders: libraryfolders_vdf = pcall(vdfParser.parseFile, process.env.HOME.."/.local/share/Steam/config/libraryfolders.vdf")
+if not result then
+ logSystem.log("error", "Failed to parse libraryfolders.vdf.")
+ return nil
+end
+logSystem.log("speed", timeStart)
+
+print(libraryFolders)
diff --git a/steam/steam_utils.luau b/steam/steam_utils.luau
new file mode 100644
index 0000000..f50c8b0
--- /dev/null
+++ b/steam/steam_utils.luau
@@ -0,0 +1,39 @@
+--!strict
+local SteamUtils = {}
+
+--[[
+ This function checks if the arguments are from Steam or not.
+ I suppose this is where non-Steam game support will be added later.
+]]
+function SteamUtils.isSteamArgs(arguments: {string}): boolean
+ if (
+ --arguments[1] == process.env.HOME.."/.local/share/Steam/ubuntu12_32/reaper" and
+ arguments[2] == "SteamLaunch" and
+ arguments[3]:match("AppId=%d+") and
+ arguments[4] == "--" and
+ --arguments[5] == process.env.HOME.."/.local/share/Steam/ubuntu12_32/steam-launch-wrapper" and
+ arguments[6] == "--"
+ ) then
+ return true
+ else
+ return false
+ end
+end
+
+--[[
+ Name : function steamUtils.convertToSteamID3(steamID)
+ Description : Converts a SteamID64 to a SteamID3
+ Arg 1 : string steamID
+ Return : string steamID3
+]]
+type SteamID64_Dec = number
+type SteamID3 = number
+function SteamUtils.convertToSteamID3(steamID: SteamID64_Dec): SteamID3
+ local offset_id = steamID - 76561197960265728
+ local account_type = offset_id % 2
+ local account_id = math.floor((offset_id - account_type) / 2) + account_type
+ -- Check the -1 here in case of SteamID related bug.
+ return (account_id * 2) - account_type - 1
+end
+
+return SteamUtils
diff --git a/modules/steam/vdfParser.lua b/steam/vdf_parser.luau
similarity index 64%
rename from modules/steam/vdfParser.lua
rename to steam/vdf_parser.luau
index 30e67f6..98391c9 100644
--- a/modules/steam/vdfParser.lua
+++ b/steam/vdf_parser.luau
@@ -1,25 +1,10 @@
+--!nocheck
+--Note : Temporary
+
local vdfParser = {}
---[[
- Name : function vdfParser.printArray(data, indent)
- Description : Prints a table in a human-readable way. Used for testing.
- Arg 1 : data (table) : The table to print.
- Arg 2 : indent (number) : The indentation level. Default : 0
-]]
-function vdfParser.printArray(data, indent)
- indent = indent or 0
- local spaces = string.rep(" ", indent)
-
- for key, value in pairs(data) do
- if type(value) == "table" then
- print(spaces .. key .. ": {")
- vdfParser.printArray(value, indent + 1)
- print(spaces .. "}")
- else
- print(spaces .. key .. ": " .. tostring(value))
- end
- end
-end
+-- Lune Modules
+local fs = require("@lune/fs")
--[[
Name : function vdfParser.parseString(input, stopKeyList)
@@ -28,50 +13,74 @@ end
Arg 2 : stopKeyList (table) : A list of keys to stop the parsing at. Default : nil
Return : result (table) : The parsed table.
]]
-function vdfParser.parseString(input, stopKeyList)
+
+-- Status :
+-- keyWait : We wait for a key
+-- readingKey : We are reading a key
+-- valueWait : We wait for a value
+-- readingValue : We are reading a value
+-- readingTableValue : We are reading a table value
+type ReadStatus = "keyWait" | "readingKey" | "valueWait" | "readingTableValue" | "readingValue"
+
+function vdfParser.parseString(input, stopKeyList: {string}?): {[string]: any}
local result = {}
- local status = "keyWait"
+ local status: ReadStatus = "keyWait"
-- Temp Values
- local tempKey, tempValue, tempRecursiveInput = "", "", ""
+ local tempKey: string, tempValue: string, tempRecursiveInput: string = "", "", ""
- local indent = 0
- local previousChar = nil
+ local indent: number = 0
+ local previousChar: string = nil
-- Until we reached the end of the file
for i = 1, #input do
local char = input:sub(i, i)
- -- Status :
- -- keyWait : We wait for a key
- -- readingKey : We are reading a key
- -- valueWait : We wait for a value
- -- readingValue : We are reading a value
- -- readingTableValue : We are reading a table value
-
- -- String management
+ -- String key/value management
if char == '"' and previousChar ~= "\\" then
if status == "keyWait" then
+ -- We reached the key and start reading it.
status = "readingKey"
elseif status == "readingKey" then
+ -- We finished reading the key and are now waiting for the value.
status = "valueWait"
elseif status == "valueWait" then
+ -- We begin reading the value.
status = "readingValue"
elseif status == "readingValue" then
- result[tempKey] = tempValue
+ -- We finished reading the value.
+
+ -- Convert value into extra, more practical types
+ local actualValue: any
+ if tempValue == "0" then actualValue = false
+ elseif tempValue == "1" then actualValue = true
+ elseif tonumber(tempValue) then actualValue = tonumber(tempValue)
+ else actualValue = tempValue end
+
+ -- Same for key
+ local actualKey: any
+ if tonumber(tempKey) then actualKey = tonumber(tempKey)
+ else actualKey = tempKey end
+
+ result[actualKey] = actualValue
+
-- We stop the parsing if we reached the stopKeyList entry
if stopKeyList and tempKey == stopKeyList[1] then
return result
end
+
tempKey = ""
tempValue = ""
status = "keyWait"
elseif status == "readingTableValue" then
+ -- We are currently reading a table. Not stopping until we reached its end.
tempRecursiveInput = tempRecursiveInput..char
end
-- Subtable management
+
+ -- We reach the beginning of a table.
elseif char == '{' and previousChar ~= "\\" and (status == "valueWait" or status == "readingTableValue") then
if status == "valueWait" then
status = "readingTableValue"
@@ -79,7 +88,9 @@ function vdfParser.parseString(input, stopKeyList)
tempRecursiveInput = tempRecursiveInput..char
indent = indent + 1
end
- elseif char == '}' and previousChar ~= "\\" and status == "readingTableValue" then
+
+ -- We reach the end of a table.
+ elseif char == '}' and previousChar ~= "\\" and (status == "readingTableValue") then
if indent == 0 then
local newStopKeyList = nil
if stopKeyList then
@@ -90,7 +101,10 @@ function vdfParser.parseString(input, stopKeyList)
end
if stopKeyList == nil or stopKeyList[1] == nil or tempKey == stopKeyList[1] then
- result[tempKey] = vdfParser.parseString(tempRecursiveInput, newStopKeyList)
+ local actualKey: any
+ if tonumber(tempKey) then actualKey = tonumber(tempKey)
+ else actualKey = tempKey end
+ result[actualKey] = vdfParser.parseString(tempRecursiveInput, newStopKeyList)
end
if stopKeyList and tempKey == stopKeyList[1] then
@@ -109,9 +123,11 @@ function vdfParser.parseString(input, stopKeyList)
-- While reading a key
elseif status == "readingKey" then
tempKey = tempKey..char
+
-- While reading a value
elseif status == "readingValue" and (stopKeyList == nil or stopKeyList[1] == nil or tempKey == stopKeyList[1]) then
tempValue = tempValue..char
+
-- While reading a table value, before sending it through the function again to parse it
elseif status == "readingTableValue" and (stopKeyList == nil or stopKeyList[1] == nil or tempKey == stopKeyList[1]) then
tempRecursiveInput = tempRecursiveInput..char
@@ -134,14 +150,8 @@ end
Note : Arg 2 and Arg 3 are critical for optimization in big files. Do not hesitate to use them.
Results such as going from 0.5 seconds to 0.04 seconds are possible.
]]
-function vdfParser.parseFile(path, stopKeyList, wordsFromLinesToRemove)
- local file = io.open(path, "r")
- if not file then
- error("File not found : "..path)
- end
-
- local content = file:read("*all")
- file:close()
+function vdfParser.parseFile(path: string, stopKeyList: {string}?, wordsFromLinesToRemove: {string}?): {[string]: any}
+ local content = fs.readFile(path)
-- Remove every line that contains a word from wordsFromLinesToRemove
if wordsFromLinesToRemove then
@@ -164,4 +174,4 @@ function vdfParser.parseFile(path, stopKeyList, wordsFromLinesToRemove)
return vdfParser.parseString(content, stopKeyList)
end
-return vdfParser
+return vdfParser
\ No newline at end of file
diff --git a/ui/definitions/cleanerPage.blp b/ui/definitions/cleanerPage.blp
deleted file mode 100644
index 79fa5f4..0000000
--- a/ui/definitions/cleanerPage.blp
+++ /dev/null
@@ -1,10 +0,0 @@
-using Gtk 4.0;
-
-StackPage cleaner { //[MODIFIED IN LUA]: This page is only shown if the game is using Proton.
- title: _("Cleaner");
- child:
- Label {
- label: "In the future, this page will allow you to do some cleaning up.";
- wrap: true;
- };
-}
diff --git a/ui/definitions/commandPage.blp b/ui/definitions/commandPage.blp
deleted file mode 100644
index e9b1707..0000000
--- a/ui/definitions/commandPage.blp
+++ /dev/null
@@ -1,10 +0,0 @@
-using Gtk 4.0;
-
-StackPage commandPage {
- title: _("Launch options");
- child:
- Label {
- label: "In the future, this will be a page for configuring launch options.";
- wrap: true;
- };
-}
diff --git a/ui/definitions/gamescopePage.blp b/ui/definitions/gamescopePage.blp
deleted file mode 100644
index b5a11cb..0000000
--- a/ui/definitions/gamescopePage.blp
+++ /dev/null
@@ -1,215 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-
-StackPage gamescopePage { //[MODIFIED IN LUA]: This page is only shown if the game is using Proton.
- title: "Gamescope";
- child:
- Gtk.ScrolledWindow {
- Adw.Clamp {
- Box {
- margin-bottom: 16;
- margin-end: 12;
- margin-start: 12;
- margin-top: 16;
- orientation: vertical;
- spacing: 24;
- vexpand: true;
- vexpand-set: true;
-
- Adw.PreferencesGroup {
- title: "Gamescope";
- description: _("Gamescope is a tool from Valve that allows for games to run in an isolated XWayland instance.");
- Adw.ActionRow {
- title : _("Enable Gamescope");
- subtitle: _("May cause compatibility issues with some utilities and Wayland-native games.");
- activatable-widget: gamescope_Switch;
-
- sensitive: bind gamescope_Switch.sensitive bidirectional;
- has-tooltip: bind gamescope_Switch.has-tooltip bidirectional;
- tooltip-text: bind gamescope_Switch.tooltip-text bidirectional;
-
- Gtk.Switch gamescope_Switch {
- active: false;
- sensitive: false;
- valign: center;
- }
- }
- }
-
- Adw.PreferencesGroup {
- title: _("Resolution");
- description: _("Resolution settings for both the game and the Gamescope window.");
-
- sensitive: bind gamescope_Switch.active;
-
- Adw.ExpanderRow {
- title: _("Game resolution");
- subtitle: _("The resolution the game will run at.");
-
- sensitive: bind gamescope_Resolution_Switch.active;
-
- Adw.SpinRow gamescope_Resolution_Internal_Width_SpinRow {
- title: _("Width");
- subtitle: _("The width of the game's internal resolution.");
- numeric: true;
- adjustment :
- Gtk.Adjustment {
- lower: 0;
- upper: 7680;
- step-increment: 10;
- };
- }
- Adw.SpinRow gamescope_Resolution_Internal_Height_SpinRow {
- title: _("Height");
- subtitle: _("The height of the game's internal resolution.");
- numeric: true;
- adjustment :
- Gtk.Adjustment {
- lower: 0;
- upper: 7680;
- step-increment: 10;
- };
- }
- }
-
- Adw.ExpanderRow {
- title: _("Window resolution");
- subtitle: _("The resolution of the Gamescope window.");
-
- sensitive: bind gamescope_Resolution_Switch.active;
-
- Adw.SpinRow gamescope_Resolution_External_Width_SpinRow {
- title: _("Width");
- subtitle: _("The width of the Gamescope window.");
- numeric: true;
- adjustment :
- Gtk.Adjustment {
- lower: 0;
- upper: 7680;
- step-increment: 10;
- };
- }
- Adw.SpinRow gamescope_Resolution_External_Height_SpinRow {
- title: _("Height");
- subtitle: _("The height of the Gamescope window.");
- numeric: true;
- adjustment :
- Gtk.Adjustment {
- lower: 0;
- upper: 7680;
- step-increment: 10;
- };
- }
- }
-
- [header-suffix]
- Switch gamescope_Resolution_Switch {
- active: false;
- sensitive: bind gamescope_Switch.active;
- valign: center;
- }
- }
-
-
- Adw.PreferencesGroup {
- title: _("Framerate");
- description: _("The maximum framerate the game will run at.");
-
- sensitive: bind gamescope_Switch.active;
-
- Adw.SpinRow gamescope_Framerate_Normal_SpinRow {
- title: _("Normal");
- subtitle: _("The framerate when the game is focused.");
- sensitive: bind gamescope_Framerate_Switch.active;
- numeric: true;
- adjustment :
- Gtk.Adjustment {
- lower: 0;
- upper: 244;
- step-increment: 1;
- };
- }
- Adw.SpinRow gamescope_Framerate_Unfocused_SpinRow {
- title: _("Unfocused");
- subtitle: _("The framerate when the game is not focused.");
- sensitive: bind gamescope_Framerate_Switch.active;
- numeric: true;
- adjustment :
- Gtk.Adjustment {
- lower: 0;
- upper: 244;
- step-increment: 1;
- };
- }
-
- [header-suffix]
- Switch gamescope_Framerate_Switch {
- active: false;
- sensitive: bind gamescope_Switch.active;
- valign: center;
- }
- }
-
- Adw.PreferencesGroup {
- title: _("Filtering");
- description: _("Makes the game look smoother at lower resolutions.");
-
- sensitive: bind gamescope_Switch.active;
-
- Adw.ComboRow gamescope_Filtering_Filter_ComboRow {
- title: _("Filter");
- subtitle: _("The filter to use. (NIS = Nvidia Image Scaling, FSR = FidelityFX Super Resolution)");
- sensitive: bind gamescope_Filtering_Switch.active;
- model: Gtk.StringList {
- strings ["Linear", "Nearest", "FSR", "NIS", "Pixel"]
- };
- }
- Adw.SpinRow gamescope_Filtering_Sharpness_SpinRow {
- title: _("Sharpness");
- subtitle: _("Sets the sharpness of the filter. (0 = maximum sharpness, 20 = minimum)");
- sensitive: bind gamescope_Filtering_Switch.active;
- numeric: true;
- adjustment :
- Gtk.Adjustment {
- lower: 0;
- upper: 20;
- step-increment: 1;
- };
- }
-
- [header-suffix]
- Switch gamescope_Filtering_Switch {
- active: false;
- sensitive: bind gamescope_Switch.active;
- valign: center;
- }
- }
-
- Adw.PreferencesGroup {
- title: _("Extras");
- description: _("Additional settings for GameScope.");
-
- sensitive: bind gamescope_Switch.active;
-
- Adw.ActionRow {
- title: _("Window type");
- subtitle: _("The type of window to use for the Gamescope window.");
-
- Box {
- styles ["linked"]
-
- ToggleButton gamescope_Borderless_Toggle {
- label: _("Borderless");
- valign: center;
- }
- ToggleButton gamescope_Fullscreen_Toggle {
- label: _("Fullscreen");
- valign: center;
- }
- }
- }
- }
- }
- }
- };
-}
diff --git a/ui/definitions/mainWindow.blp b/ui/definitions/mainWindow.blp
deleted file mode 100644
index b7b88ae..0000000
--- a/ui/definitions/mainWindow.blp
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- Why not use LGI directly instead ?
-
- Because LGI doesn't support well in GTK4 many things that can be easily defined here.
- Still, both are by FAR a much better solution than using XML directly.
- Plus, I feel like using a universal standard would be a good idea to make it easier for people to contribute.
-*/
-
-using Gtk 4.0;
-using Adw 1;
-
-Adw.ApplicationWindow mainWindow {
- default-height: 500;
- default-width: 700;
- height-request: 400;
- show-menubar: true;
- width-request: 360;
-
- // AdwBreakpoint used to dynamically make the sidebar appear or disappear depending on the window's width.
- Adw.Breakpoint {
- condition ("max-width: 600sp")
- setters {
- sidebarToggleButton.visible: true;
- sidebarToggleButton.active: false;
- gameSettings_SplitView.collapsed: true;
- }
- }
-
- [content]
- Adw.ToastOverlay toastSystem { //[INTERACTS WITH LUA]
- Adw.NavigationView mainView {
- /*
- MAIN PAGE
- This is the page the program starts on when it's launched.
- */
- Adw.NavigationPage mainPage {
- title: _("SimpleSteamTinker");
- Adw.ToolbarView mainPageTopbar {
- [top]
- Adw.HeaderBar {
- styles ["titlebar"]
-
- [end]
- Button aboutLauncher {
- icon-name: "help-about-symbolic";
- styles ["image-button"]
- }
- }
-
- // This is where the games are listed.
- ScrolledWindow {
- hscrollbar-policy: never;
-
- Adw.Clamp {
- margin-bottom: 16;
- margin-end: 12;
- margin-start: 12;
- margin-top: 16;
-
- Adw.PreferencesGroup gameList { //[MODIFIED IN LUA]
- description: _("Insert \"sst %command%\" as a game\'s launch options to enable it here.\nCommand may change depending on installation method.");
- title: _("Detected Steam games");
- }
- }
- }
- }
- }
-
- // This is the page that shows up when you click on a game.
- Adw.NavigationPage gameSettings { //[INTERACTS THROUGH LUA]
- title: _("Game settings");
- Adw.ToolbarView gameSettingsTopbar {
- [top]
- Adw.HeaderBar {
- styles ["titlebar"]
-
- // Button that allows you to toggle the sidebar in the game settings page.
- ToggleButton sidebarToggleButton { //[MODIFIED IN LUA][INTERACTS THROUGH LUA]
- active: true;
- visible: false;
- has-frame: false;
- icon-name: "view-sidebar-start-symbolic";
- }
- }
-
- Adw.OverlaySplitView gameSettings_SplitView {
- /*
- SIDEBAR
- */
- min-sidebar-width: 200;
- show-sidebar: bind sidebarToggleButton.active bidirectional;
- sidebar:
- Adw.NavigationPage gameSettingsSidebar {
- title: _("Sidebar");
- child:
- Adw.ToolbarView {
- Box {
- orientation: vertical;
- vexpand: true;
- vexpand-set: true;
-
- // This is the header of the sidebar of the game settings page.
- Picture Sidebar_Banner {
- halign: fill;
- valign: fill;
- }
-
- // This is the list of pages in the sidebar of the game settings page.
- StackSidebar {
- vexpand: true;
- vexpand-set: true;
- stack: gameSettingsStack;
- }
- }
- };
- };
-
- /*
- CONTENT
- */
- content:
- Adw.NavigationPage {
- title: _("Game settings");
- Stack gameSettingsStack {
- transition-type: crossfade;
-
- //[overviewPage.blp]
-
- //[settingsPage.blp]
-
- //[utilitiesPage.blp]
-
- //[commandPage.blp]
-
- //[protonPage.blp]
-
- //[gamescopePage.blp]
-
- //[cleanerPage.blp]
- }
- };
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/ui/definitions/overviewPage.blp b/ui/definitions/overviewPage.blp
deleted file mode 100644
index be9264d..0000000
--- a/ui/definitions/overviewPage.blp
+++ /dev/null
@@ -1,279 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-
-StackPage overviewPage { //[INTERACTS IN LUA]
- name: "overviewPage";
- title: _("Overview");
-
- child:
- ScrolledWindow {
- hscrollbar-policy: never;
- styles ["background"]
-
- Box {
- orientation: vertical;
-
- Picture Overview_Picture { //[MODIFIED IN LUA]
- halign: fill;
- valign: fill;
- height-request: 256;
- }
-
- Adw.Clamp {
- Box {
- margin-bottom: 16;
- margin-end: 12;
- margin-start: 12;
- margin-top: 16;
- orientation: vertical;
- spacing: 24;
- vexpand: true;
- vexpand-set: true;
-
- Box {
- orientation: vertical;
- spacing: 8;
- halign: center;
-
- // Game title
- Label gameTitle { //[MODIFIED IN LUA]
- halign: center;
- valign: end;
- wrap: true;
-
- styles [
- "title-1"
- ]
- }
-
- // ProtonDB Rating
- Box protonDBRating_Box {
- orientation: horizontal;
- spacing: 8;
- halign: center;
- Label {
- label: _("ProtonDB Rating :");
- halign: center;
- valign: end;
-
- styles [
- "dim-label"
- ]
- }
- Label protonDBRating_Label { //[MODIFIED IN LUA]
- label: _("Unavailable");
- halign: center;
- valign: end;
-
- styles [
- "dim-label"
- ]
- }
- }
- }
-
- // Launch button
- Button gameLaunchButton { //[INTERACTS THROUGH LUA]
- halign: center;
- valign: end;
-
- Adw.ButtonContent {
- icon-name: "media-playback-start-symbolic";
- label: _("Launch");
- }
-
- styles [
- "suggested-action",
- "image-button",
- "pill"
- ]
- }
-
- // SimpleSteamTinker Status
- Adw.PreferencesGroup {
- Adw.ActionRow {
- title: _("SimpleSteamTinker Status");
- subtitle: _("Based on the game's launch options.");
- subtitle-lines: 1;
-
- [suffix]
- Label gameStatus_Label { //[MODIFIED IN LUA]
- label: _("Placeholder");
- halign: end;
- wrap: true;
- styles [
- "dim-label"
- ]
- }
- }
- }
-
- // Game details
- Adw.PreferencesGroup {
- title: _("Details");
- description: _("Information about the game.");
-
- Adw.ActionRow {
- title: _("Steam AppID");
- subtitle: _("The unique ID of the game that identifies it in Steam.");
- activatable-widget: gameID_copyButton;
- [suffix]
- Box {
- spacing: 8;
- Label gameID_Label { //[MODIFIED IN LUA]
- halign: end;
- wrap: true;
- selectable: true;
- styles [
- "dim-label"
- ]
- }
- Button gameID_copyButton { //[MODIFIED IN LUA][INTERACTS WITH LUA]
- halign: end;
- icon-name: "edit-copy-symbolic";
- styles [
- "image-button",
- "flat",
- "circular"
- ]
- }
- }
- }
- Adw.ActionRow {
- title: _("Platform");
- subtitle: _("The intended platform for the installed game version.");
-
- [suffix]
- Label gamePlatform_Label { //[MODIFIED IN LUA]
- halign: end;
- wrap: true;
- styles [
- "dim-label"
- ]
- }
- }
- Adw.ActionRow {
- title: _("Size");
- subtitle: _("The amount of space the game takes up on the system.");
-
- [suffix]
- Label gameSize_Label { //[MODIFIED IN LUA]
- halign: end;
- lines: 1;
- styles [
- "dim-label"
- ]
- }
- }
- Adw.ActionRow gameLocation_ActionRow { //[MODIFIED IN LUA]
- title: _("Install location");
- tooltip-text: _("The location of the game's files on the system.");
- has-tooltip: true;
- activatable-widget: gameLocationButton;
- subtitle-lines: 2;
- [suffix]
- Button gameLocationButton { //[MODIFIED IN LUA]
- Adw.ButtonContent {
- icon-name: "folder-open-symbolic";
- }
- styles [
- "image-button",
- "flat",
- "circular"
- ]
- }
- }
- Adw.ActionRow gameCompatdata_ActionRow { //[MODIFIED IN LUA]
- title: _("Virtual filesystem location");
- tooltip-text: _("The location of the game's virtual filesystem assigned by Steam. (Only applies to Windows games)");
- has-tooltip: true;
- activatable-widget: gameCompatdataButton;
- subtitle-lines: 2;
- [suffix]
- Button gameCompatdataButton { //[MODIFIED IN LUA]
- Adw.ButtonContent {
- icon-name: "folder-open-symbolic";
- }
- styles [
- "image-button",
- "flat",
- "circular"
- ]
- }
- }
- }
-
- // Links
- Adw.PreferencesGroup {
- title: _("Online help");
- description: _("Links to useful pages related to the game.");
-
- Adw.ActionRow {
- title: _("ProtonDB");
- activatable-widget: protonDBPage_Button;
- [suffix]
- Button protonDBPage_Button { //[MODIFIED IN LUA]
- Adw.ButtonContent {
- icon-name: "help-browser-symbolic";
- }
- styles [
- "image-button",
- "flat",
- "circular"
- ]
- }
- }
-
- Adw.ActionRow {
- title: _("PCGamingWiki");
- activatable-widget: PCGamingWikiPage_Button;
- [suffix]
- Button PCGamingWikiPage_Button { //[MODIFIED IN LUA]
- Adw.ButtonContent {
- icon-name: "help-browser-symbolic";
- }
- styles [
- "image-button",
- "flat",
- "circular"
- ]
- }
- }
-
- Adw.ActionRow {
- title: _("SteamDB");
- activatable-widget: steamDBPage_Button;
- [suffix]
- Button steamDBPage_Button { //[MODIFIED IN LUA]
- Adw.ButtonContent {
- icon-name: "help-browser-symbolic";
- }
- styles [
- "image-button",
- "flat",
- "circular"
- ]
- }
- }
-
- Adw.ActionRow {
- title: _("Steambase");
- activatable-widget: SteambasePage_Button;
- [suffix]
- Button SteambasePage_Button { //[MODIFIED IN LUA]
- Adw.ButtonContent {
- icon-name: "help-browser-symbolic";
- }
- styles [
- "image-button",
- "flat",
- "circular"
- ]
- }
- }
- }
- }
- }
- }
- };
-}
diff --git a/ui/definitions/protonPage.blp b/ui/definitions/protonPage.blp
deleted file mode 100644
index 551d6c9..0000000
--- a/ui/definitions/protonPage.blp
+++ /dev/null
@@ -1,225 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-
-StackPage protonPage { //[MODIFIED IN LUA]: This page is only shown if the game is using Proton.
- title: "Proton / Wine";
-
- child:
- Gtk.ScrolledWindow {
- Adw.Clamp {
- Box {
- margin-bottom: 16;
- margin-end: 12;
- margin-start: 12;
- margin-top: 16;
- orientation: vertical;
- spacing: 24;
- vexpand: true;
- vexpand-set: true;
-
- Adw.PreferencesGroup {
- title: "Direct3D";
- description: _("Configure Direct3D settings.");
-
- Adw.ExpanderRow {
- title: _("Versions");
- subtitle: _("Control which versions of Direct3D can be used by this game. If you are unsure, leave all versions enabled.");
-
- Adw.ActionRow {
- title: _("Direct3D 9");
- subtitle: _("Only applies to GE Proton.");
- activatable-widget: Direct3D9_Switch;
- sensitive: bind Direct3D9_Switch.sensitive bidirectional;
- has-tooltip: bind Direct3D9_Switch.has-tooltip bidirectional;
- tooltip-text: bind Direct3D9_Switch.tooltip-text bidirectional;
-
- Gtk.Switch Direct3D9_Switch {
- active: true;
- valign: center;
- }
- }
- Adw.ActionRow {
- title: _("Direct3D 10");
- activatable-widget: Direct3D10_Switch;
- sensitive: bind Direct3D10_Switch.sensitive bidirectional;
- has-tooltip: bind Direct3D10_Switch.has-tooltip bidirectional;
- tooltip-text: bind Direct3D10_Switch.tooltip-text bidirectional;
-
- Gtk.Switch Direct3D10_Switch {
- active: true;
- valign: center;
- }
- }
- Adw.ActionRow {
- title: _("Direct3D 11");
- activatable-widget: Direct3D11_Switch;
- sensitive: bind Direct3D11_Switch.sensitive bidirectional;
- has-tooltip: bind Direct3D11_Switch.has-tooltip bidirectional;
- tooltip-text: bind Direct3D11_Switch.tooltip-text bidirectional;
-
- Gtk.Switch Direct3D11_Switch {
- active: true;
- valign: center;
- }
- }
- Adw.ActionRow {
- title: _("Direct3D 12");
- subtitle: _("Only applies to GE Proton.");
- activatable-widget: Direct3D12_Switch;
- sensitive: bind Direct3D12_Switch.sensitive bidirectional;
- has-tooltip: bind Direct3D12_Switch.has-tooltip bidirectional;
- tooltip-text: bind Direct3D12_Switch.tooltip-text bidirectional;
-
- Gtk.Switch Direct3D12_Switch {
- active: true;
- valign: center;
- }
- }
- }
-
- Adw.ActionRow {
- title: _("Use WineD3D instead of DXVK");
- subtitle: _("Use OpenGL-based WineD3D instead of Vulkan-based DXVK for D3D11 and D3D10.");
- activatable-widget: WineD3D_Switch;
- sensitive: bind WineD3D_Switch.sensitive bidirectional;
- has-tooltip: bind WineD3D_Switch.has-tooltip bidirectional;
- tooltip-text: bind WineD3D_Switch.tooltip-text bidirectional;
-
- Gtk.Switch WineD3D_Switch {
- active: false;
- valign: center;
- }
- }
- }
-
- Adw.PreferencesGroup {
- title: "AMD FidelityFX Super Resolution 1.0";
- description: _("Note : this feature requires GE Proton, and is not restricted to AMD GPUs.");
-
- [header-suffix]
- Switch Wine_FSR_Switch {
- active: false;
- sensitive: true;
- valign: center;
- }
-
- Adw.SpinRow Wine_FSR_Sharpness_SpinRow {
- title: _("Sharpness");
- subtitle: _("The strength of the filter. (0 = strongest, 5 = weakest)");
- sensitive: bind Wine_FSR_Switch.active;
- numeric: true;
- adjustment :
- Gtk.Adjustment {
- lower: 0;
- upper: 5;
- step-increment: 1;
- };
- }
-
- Adw.ComboRow Wine_FSR_Upscaling_Resolution_Mode_ComboRow {
- title: _("Upscaling resolution mode");
- subtitle: _("Depending on what preset value you choose and your system, Proton will set an upscaling resolution automatically.");
- sensitive: bind Wine_FSR_Switch.active;
- model: Gtk.StringList {
- strings ["None", "Performance", "Balanced", "Quality", "Ultra"]
- };
- }
-
- Adw.ExpanderRow {
- title: _("Custom resolution");
- subtitle: _("Note : overrides the upscaling resolution mode. This shouldn't be needed in most cases.");
- sensitive: bind Wine_FSR_Switch.active;
-
- Adw.ActionRow {
- title: _("Enable the usage of a custom resolution");
- Switch Wine_FSR_Resolution_Switch {
- active: false;
- sensitive: bind Wine_FSR_Switch.active;
- valign: center;
- }
- }
- Adw.SpinRow Wine_FSR_Resolution_External_Width_SpinRow {
- title: _("Width");
- sensitive: bind Wine_FSR_Resolution_Switch.active;
- numeric: true;
- adjustment :
- Gtk.Adjustment {
- lower: 0;
- upper: 7680;
- step-increment: 10;
- };
- }
- Adw.SpinRow Wine_FSR_Resolution_External_Height_SpinRow {
- title: _("Height");
- sensitive: bind Wine_FSR_Resolution_Switch.active;
- numeric: true;
- adjustment :
- Gtk.Adjustment {
- lower: 0;
- upper: 7680;
- step-increment: 10;
- };
- }
- }
- }
-
- Adw.PreferencesGroup {
- title: _("NVIDIA features");
-
- Adw.ActionRow {
- title : _("Disguise NVIDIA GPUs as AMD");
- subtitle: _("Force NVIDIA GPUs to always be reported as AMD GPUs. Some games require this if they depend on Windows-only NVIDIA driver functionality.");
- subtitle-lines: 3;
-
- activatable-widget: Hide_NVIDIA_GPU_Switch;
- sensitive: bind Hide_NVIDIA_GPU_Switch.sensitive bidirectional;
- has-tooltip: bind Hide_NVIDIA_GPU_Switch.has-tooltip bidirectional;
- tooltip-text: bind Hide_NVIDIA_GPU_Switch.tooltip-text bidirectional;
-
- Gtk.Switch Hide_NVIDIA_GPU_Switch {
- active: false;
- valign: center;
- }
- }
- Adw.ActionRow {
- title : _("Enable NVAPI");
- subtitle: _("Enable NVIDIA's NVAPI GPU support library.");
- subtitle-lines: 3;
-
- activatable-widget: Enable_NVAPI_Switch;
- sensitive: bind Enable_NVAPI_Switch.sensitive bidirectional;
- has-tooltip: bind Enable_NVAPI_Switch.has-tooltip bidirectional;
- tooltip-text: bind Enable_NVAPI_Switch.tooltip-text bidirectional;
-
- Gtk.Switch Enable_NVAPI_Switch {
- active: false;
- valign: center;
- }
- }
- }
-
- Adw.PreferencesGroup {
- title: "Wine";
-
- Adw.ActionRow {
- title: _("Synchronization");
- Box {
- styles ["linked"]
-
- ToggleButton ESync_Toggle {
- label: "ESync";
- valign: center;
- active: true;
- }
- ToggleButton FSync_Toggle {
- label: "FSync";
- valign: center;
- active: true;
- }
- }
- }
- }
- }
- }
- };
-}
diff --git a/ui/definitions/settingsPage.blp b/ui/definitions/settingsPage.blp
deleted file mode 100644
index 6c5669b..0000000
--- a/ui/definitions/settingsPage.blp
+++ /dev/null
@@ -1,59 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-
-StackPage settingsPage { //[MODIFIED IN LUA]: This page is only shown if the game is using Proton.
- title: _("Settings");
- child:
- Gtk.ScrolledWindow {
- Adw.Clamp {
- Box {
- margin-bottom: 16;
- margin-end: 12;
- margin-start: 12;
- margin-top: 16;
- orientation: vertical;
- spacing: 24;
- vexpand: true;
- vexpand-set: true;
-
- Adw.PreferencesGroup {
- title: "Recommended";
- description: _("Settings recommended for the best experience.");
- Adw.ActionRow {
- title : _("Use dedicated graphics card");
- subtitle: _("Use your dedicated graphics card to boost performance by a lot, in exchange for higher power consumption.");
- subtitle-lines: 3;
-
- activatable-widget: dGPU_Switch;
- sensitive: bind dGPU_Switch.sensitive bidirectional;
- has-tooltip: bind dGPU_Switch.has-tooltip bidirectional;
- tooltip-text: bind dGPU_Switch.tooltip-text bidirectional;
-
- Gtk.Switch dGPU_Switch {
- active: false;
- sensitive: false;
- valign: center;
- }
- }
- Adw.ActionRow {
- title : _("Enable SDL's Wayland driver");
- subtitle: _("This option can give you smoother gameplay and performance in SDL-based games on Wayland. Useless on X11.");
- subtitle-lines: 3;
-
- activatable-widget: SDL_Wayland_Switch;
- sensitive: bind SDL_Wayland_Switch.sensitive bidirectional;
- visible: bind SDL_Wayland_Switch.visible bidirectional;
- has-tooltip: bind SDL_Wayland_Switch.has-tooltip bidirectional;
- tooltip-text: bind SDL_Wayland_Switch.tooltip-text bidirectional;
-
- Gtk.Switch SDL_Wayland_Switch {
- active: false;
- sensitive: true;
- valign: center;
- }
- }
- }
- }
- }
- };
-}
diff --git a/ui/definitions/utilitiesPage.blp b/ui/definitions/utilitiesPage.blp
deleted file mode 100644
index 5e87575..0000000
--- a/ui/definitions/utilitiesPage.blp
+++ /dev/null
@@ -1,93 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-
-StackPage utilitiesPage {
- title: _("In-game utilities");
-
- child:
- Adw.ClampScrollable {
- Box {
- margin-bottom: 16;
- margin-end: 12;
- margin-start: 12;
- margin-top: 16;
- orientation: vertical;
- spacing: 24;
-
- Adw.PreferencesGroup {
- title: "Recommended";
- description: _("Utilities recommended for the best experience.");
- Adw.ActionRow {
- subtitle: _("GameMode is a tool to optimize Linux system performance on demand.");
- title: "Feral GameMode";
- activatable-widget: gamemode_Switch;
- has-tooltip: bind gamemode_Switch.has-tooltip bidirectional;
- tooltip-text: bind gamemode_Switch.tooltip-text bidirectional;
- sensitive: bind gamemode_Switch.sensitive bidirectional;
-
- Switch gamemode_Switch { //[MODIFIED IN LUA][INTERACTS WITH LUA]
- active: false;
- sensitive: false;
- valign: center;
- }
- }
- }
-
- Adw.PreferencesGroup {
- title: _("Utilities");
- description: _("Utilities that can be used while the game is running, depending on your needs.");
-
- // Adw.SwitchRow seem to be buggy-ish. The value gets inverted, and clicking directly on the button
- // doesn't send the signal I need to run the functions.
-
- Adw.ActionRow {
- subtitle: _("A Vulkan and OpenGL overlay for monitoring FPS, temperatures, CPU/GPU load and more... May not work on some native OpenGL games.");
- subtitle-lines: 2;
- title: "MangoHud";
- activatable-widget: mangohud_Switch;
- has-tooltip: bind mangohud_Switch.has-tooltip bidirectional;
- tooltip-text: bind mangohud_Switch.tooltip-text bidirectional;
- sensitive: bind mangohud_Switch.sensitive bidirectional;
-
- Switch mangohud_Switch { //[MODIFIED IN LUA][INTERACTS WITH LUA]
- active: false;
- sensitive: false;
- valign: center;
- }
- }
-
- Adw.ActionRow {
- subtitle: _("Convert OpenGL games to Vulkan.");
- subtitle-lines: 2;
- title: "Zink";
- activatable-widget: zink_Switch;
- has-tooltip: bind zink_Switch.has-tooltip bidirectional;
- tooltip-text: bind zink_Switch.tooltip-text bidirectional;
- sensitive: bind zink_Switch.sensitive bidirectional;
-
- Switch zink_Switch { //[MODIFIED IN LUA][INTERACTS WITH LUA]
- active: false;
- sensitive: false;
- valign: center;
- }
- }
-
- Adw.ActionRow {
- subtitle: _("obs-gamecapture is a tool that captures a game window and plugs it into OBS Studio as a source with minimal overhead.");
- subtitle-lines: 2;
- title: _("OBS Game Capture");
- activatable-widget: obs_gamecapture_Switch;
- has-tooltip: bind obs_gamecapture_Switch.has-tooltip bidirectional;
- tooltip-text: bind obs_gamecapture_Switch.tooltip-text bidirectional;
- sensitive: bind obs_gamecapture_Switch.sensitive bidirectional;
-
- Switch obs_gamecapture_Switch { //[MODIFIED IN LUA][INTERACTS WITH LUA]
- active: false;
- sensitive: false;
- valign: center;
- }
- }
- }
- }
- };
-}
diff --git a/ui/main.ui b/ui/main.ui
deleted file mode 100644
index e9d7cb2..0000000
--- a/ui/main.ui
+++ /dev/null
@@ -1,1190 +0,0 @@
-
-
-
-
-
diff --git a/ui/update-ui.lua b/ui/update-ui.lua
deleted file mode 100644
index b61c29e..0000000
--- a/ui/update-ui.lua
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/bin/lua
-
---[[
- This script should be used after modifying any of the ui_definitions files.
- It will compile the ui_definitions files into ui files that GTK can use.
- If you do not, the modifications you've made to the UI outside of Lua code will not be applied.
-
- This is basically a workaround for LGI not working well with Gtk templates or to assemble widgets together in general right now.
-]]
-
--- Define the list of files to compile
-local baseFile = "mainWindow.blp"
-local files = {
- [baseFile] = false,
- ["overviewPage.blp"] = false,
- ["utilitiesPage.blp"] = false,
- ["settingsPage.blp"] = false,
- ["commandPage.blp"] = false,
- ["gamescopePage.blp"] = false,
- ["protonPage.blp"] = false,
- ["cleanerPage.blp"] = false,
-}
-
--- Add their contents to the list
-for file, _ in pairs(files) do
- local f = io.open("ui/definitions/"..file, "r")
- if not f then
- print("Error: Could not open file "..file)
- os.exit(1)
- end
- files[file] = f:read("*all")
- f:close()
-end
-
-local function removeUsageLines(content)
- local lines = {}
- for line in content:gmatch("[^\r\n]+") do
- if not line:find("using ") or not line:find(";") then
- table.insert(lines, line)
- end
- end
- return table.concat(lines, "\n")
-end
-
--- Inside the contents of each file, replace comments written like "//[FILENAME]" with the respective content
-for file, content in pairs(files) do
- local newContent = {}
- for line in content:gmatch("[^\r\n]+") do
-
- local fileName = line:match("//%[(.+)%]")
- if fileName then
- local fileContent = files[fileName]
- if fileContent then
- print("Inserting "..fileName.." into "..file..".")
- table.insert(newContent, removeUsageLines(fileContent))
- else
- table.insert(newContent, line)
- end
- else
- table.insert(newContent, line)
- end
-
- end
- files[file] = table.concat(newContent, "\n")
-end
-
--- Write it into a location
-local location = "/tmp/main.blp"
-local f = io.open(location, "w")
-if not f then
- print("Error: Could not open file "..location)
- os.exit(1)
-end
-f:write(files[baseFile])
-f:close()
-
--- Make the blueprint with blueprint-compiler
-local command = "blueprint-compiler compile "..location.." > ui/main.ui"
-print(command)
-os.execute(command)
-os.remove(location)
\ No newline at end of file
From 62c26328d3232caa53a9871749d0b1ec7a3b154e Mon Sep 17 00:00:00 2001
From: JordanViknar <74505993+JordanViknar@users.noreply.github.com>
Date: Sun, 1 Sep 2024 20:21:33 +0200
Subject: [PATCH 02/20] refactor: Organize more into modules
---
.luaurc | 8 +
Source/Metadata/init.luau | 13 ++
Source/Steam/Configuration.luau | 199 ++++++++++++++++++
Source/Steam/Utilities.luau | 43 ++++
.../Steam/VDFParser.luau | 127 +++++------
Source/Steam/init.luau | 3 +
Source/Utilities/Filesystem.luau | 71 +++++++
.../Utilities/Logging.luau | 86 ++++----
Source/Utilities/init.luau | 4 +
Source/init.luau | 40 ++++
assets/desktop/system.desktop | 8 -
assets/icons/256x256.png | Bin 21964 -> 0 bytes
assets/icons/scalable.svg | 144 -------------
assets/screenshots/ViewLight.ora | Bin 417586 -> 0 bytes
assets/screenshots/ViewLight.png | Bin 208520 -> 0 bytes
general/fs_utils.luau | 105 ---------
general/init.luau | 5 -
init.luau | 3 +
main.luau | 38 ----
metadata/init.luau | 20 --
steam/init.luau | 4 -
steam/steam_utils.luau | 39 ----
22 files changed, 500 insertions(+), 460 deletions(-)
create mode 100644 .luaurc
create mode 100644 Source/Metadata/init.luau
create mode 100644 Source/Steam/Configuration.luau
create mode 100644 Source/Steam/Utilities.luau
rename steam/vdf_parser.luau => Source/Steam/VDFParser.luau (65%)
create mode 100644 Source/Steam/init.luau
create mode 100644 Source/Utilities/Filesystem.luau
rename general/log_system.luau => Source/Utilities/Logging.luau (59%)
create mode 100644 Source/Utilities/init.luau
create mode 100644 Source/init.luau
delete mode 100755 assets/desktop/system.desktop
delete mode 100644 assets/icons/256x256.png
delete mode 100644 assets/icons/scalable.svg
delete mode 100644 assets/screenshots/ViewLight.ora
delete mode 100644 assets/screenshots/ViewLight.png
delete mode 100644 general/fs_utils.luau
delete mode 100644 general/init.luau
create mode 100644 init.luau
delete mode 100644 main.luau
delete mode 100644 metadata/init.luau
delete mode 100644 steam/init.luau
delete mode 100644 steam/steam_utils.luau
diff --git a/.luaurc b/.luaurc
new file mode 100644
index 0000000..632b85a
--- /dev/null
+++ b/.luaurc
@@ -0,0 +1,8 @@
+{
+ "languageMode": "strict",
+ "aliases": {
+ "Metadata": "./Source/Metadata",
+ "Steam" : "./Source/Steam",
+ "Utilities" : "./Source/Utilities"
+ }
+}
diff --git a/Source/Metadata/init.luau b/Source/Metadata/init.luau
new file mode 100644
index 0000000..265a25f
--- /dev/null
+++ b/Source/Metadata/init.luau
@@ -0,0 +1,13 @@
+local process = require("@lune/process")
+
+return {
+ name ="STweaks",
+ description = "A work-in-progress fast, simple and modern Libadwaita alternative to SteamTinkerLaunch.",
+ version="indev",
+ folders = {
+ config = process.env.HOME.."/.config/Stweaks",
+ gamesConfig = process.env.HOME.."/.config/Stweaks/games",
+ storage = process.env.HOME.."/.local/share/Stweaks",
+ cache = process.env.HOME.."/.cache/Stweaks"
+ }
+}
diff --git a/Source/Steam/Configuration.luau b/Source/Steam/Configuration.luau
new file mode 100644
index 0000000..f91b0d8
--- /dev/null
+++ b/Source/Steam/Configuration.luau
@@ -0,0 +1,199 @@
+local process = require("@lune/process")
+
+local Logging = require("../Utilities").Logging
+local SteamUtilities = require("Utilities")
+local VDFParser = require("VDFParser")
+
+type loginusers_vdf = {
+ users : {
+ [number] : {
+ AccountName : string,
+ --PersonaName : string,
+ --RememberPassword : boolean,
+ --WantsOfflineMode : boolean,
+ --SkipOfflineModeWarning : boolean,
+ --AllowAutoLogin : boolean,
+ MostRecent : boolean,
+ --TimeStamp : number,
+ SteamID3 : number -- Added manually later on
+ }
+ }
+}
+type localconfig_vdf = {
+ UserLocalConfigStore : {
+ Software : {
+ Valve : {
+ Steam : {
+ apps : {
+ [number] : {
+ --[[cloud : {
+ last_sync_state : string?,
+ quota_bytes : number?,
+ quota_files : number?,
+ used_bytes : number?,
+ used_files : number?,
+ }?,
+ LastPlayed : number?,
+ Playtime : (number | boolean)?,
+ BadgeData : string?,]]--
+ LaunchOptions : string?,
+ --[[Playtime2wks : boolean?,
+ autocloud : {
+ lastexit : number,
+ lastlaunch : number,
+ }?]]
+ }
+ }
+ }
+ }
+ }
+ }
+}
+type libraryfolders_vdf = {
+ libraryfolders : {
+ [number] : {
+ apps : {
+ [number] : number
+ },
+ --contentid : number,
+ --label : string,
+ path : string,
+ --time_last_update_corruption : number | boolean,
+ --totalsize : boolean,
+ --update_clean_bytes_tally : number | boolean
+ }
+ }
+}
+
+local Configuration = {}
+
+Configuration.Private = {}
+Configuration.Public = {}
+
+-- Public
+
+function Configuration.Public.getSteamConfiguration() : string?
+ --[[
+ Chapter 1 : We recover the Steam user config so we can get the last active user.
+ And also the SteamID3 to access their settings folder.
+ ]]
+ local userConfiguration = Configuration.Private.getUsersConfiguration()
+ if typeof(userConfiguration) == "string" then
+ return userConfiguration -- Error
+ end
+
+ local activeUserID = Configuration.Private.getMostRecentUserID(userConfiguration)
+ local userParameters = userConfiguration.users[activeUserID]
+
+ --[[
+ Chapter 2 : We recover the user config to get which games have the tool enabled.
+ This section would normally have quite an impact on performance.
+ Thanks to some optimizations, however, the time is cut from 0.5 seconds to 0.04 seconds on my computer.
+ ]]
+ local localconfig_vdf = Configuration.Private.getLocalConfigVDF(userParameters.SteamID3)
+ if typeof(localconfig_vdf) == "string" then
+ return localconfig_vdf -- Error
+ end
+ local userGameConfigurations = localconfig_vdf.UserLocalConfigStore.Software.Valve.Steam.apps
+
+ --[[
+ Chapter 3 : We recover the Steam library config to get the list of games.
+ We can't use just libraryfolders.vdf to get the game IDs, as Steam seems to not always update it immediately.
+ ]]
+ local libraries = Configuration.Private.getLibraries()
+ if typeof(libraries) == "string" then
+ return libraries -- Error
+ end
+
+ return
+end
+
+-- Private
+
+-- loginuser.vdf
+
+function Configuration.Private.getUsersConfiguration() : loginusers_vdf | string
+ Logging.write("data", "Detecting user config...")
+ local timeStart = os.clock()
+
+ local result, usersData: loginusers_vdf = pcall(VDFParser.parseFile, (process.env.HOME.."/.local/share/Steam/config/loginusers.vdf"))
+ if not result then
+ return "Failed to parse loginusers.vdf."
+ else
+ Logging.write("speed", timeStart)
+
+ for userID,userParameters in pairs(usersData.users) do
+ userParameters.SteamID3 = SteamUtilities.convertToSteamID3(userID)
+ end
+
+ return usersData
+ end
+end
+
+function Configuration.Private.getMostRecentUserID(userData : loginusers_vdf) : number
+ local activeUserID
+ for id,data in pairs(userData.users) do
+ if data.MostRecent == true then
+ activeUserID = id
+ end
+ end
+ return activeUserID
+end
+
+-- localconfig.vdf
+
+function Configuration.Private.getLocalConfigVDF(SteamID3 : number) : localconfig_vdf | string
+ Logging.write("data", "Detecting active user game configs...")
+ local timeStart = os.clock()
+
+ local appParseSettings = {
+ file = process.env.HOME.."/.local/share/Steam/userdata/"..SteamID3.."/config/localconfig.vdf",
+ stopKeyList = {
+ "UserLocalConfigStore",
+ "Software",
+ "Valve",
+ "Steam",
+ "apps"
+ },
+ wordsFromLinesToRemove = {
+ "CachedCommunityPreferences",
+ "UIStoreLocalState",
+ "CachedStorePreferences",
+ "CachedNotificationPreferences",
+ "SteamVoiceSettings_",
+ "UIStoreLocalSteamUIState",
+ "UIStoreLocalGamepadState",
+ "GetEquippedProfileItemsForUser",
+ "CTextFilterStore_strBannedPattern",
+ "CTextFilterStore_strCleanPattern",
+ "trendingstore_storage",
+ "playnextstore_storage",
+ "GetEquippedProfileItemsForUser"
+ }
+ }
+
+ local result, localconfig_vdf: localconfig_vdf = pcall(VDFParser.parseFile, appParseSettings.file, appParseSettings.stopKeyList, appParseSettings.wordsFromLinesToRemove)
+ if not result then
+ return "Failed to parse active user localconfig.vdf with SteamID3 : "..SteamID3
+ else
+ Logging.write("speed", timeStart)
+ return localconfig_vdf
+ end
+end
+
+-- libraryfolders.vdf
+
+function Configuration.Private.getLibraries() : libraryfolders_vdf | string
+ Logging.write("data", "Detecting games...")
+ local timeStart = os.clock()
+
+ local result, libraryFolders: libraryfolders_vdf = pcall(VDFParser.parseFile, process.env.HOME.."/.local/share/Steam/config/libraryfolders.vdf")
+ if not result then
+ return "Failed to parse libraryfolders.vdf."
+ else
+ Logging.write("speed", timeStart)
+ return libraryFolders
+ end
+end
+
+return Configuration.Public
diff --git a/Source/Steam/Utilities.luau b/Source/Steam/Utilities.luau
new file mode 100644
index 0000000..1977dde
--- /dev/null
+++ b/Source/Steam/Utilities.luau
@@ -0,0 +1,43 @@
+local process = require("@lune/process")
+
+--[=[
+ Minor Steam-related helper functions.
+]=]
+local Utilities = {}
+
+--[=[
+ Checks if the arguments are from Steam or not.
+ Non-Steam game support later on will probably necessitate changes.
+
+ @return If the program was launched from Steam.
+]=]
+function Utilities.isSteamLaunch(): boolean
+ local arguments = process.args
+
+ if (
+ --arguments[1] == process.env.HOME.."/.local/share/Steam/ubuntu12_32/reaper" and
+ arguments[2] == "SteamLaunch" and
+ arguments[3]:match("AppId=%d+") and
+ arguments[4] == "--" and
+ --arguments[5] == process.env.HOME.."/.local/share/Steam/ubuntu12_32/steam-launch-wrapper" and
+ arguments[6] == "--"
+ ) then
+ return true
+ else
+ return false
+ end
+end
+
+--[=[
+ Converts SteamID64 to SteamID3.
+]=]
+function Utilities.convertToSteamID3(steamID: number): number
+ local offset_id = steamID - 76561197960265728
+ local account_type = offset_id % 2
+ local account_id = math.floor((offset_id - account_type) / 2) + account_type
+
+ -- The -1 is meant to be a fix, but could also cause bugs by itself. Keep an eye out.
+ return (account_id * 2) - account_type - 1
+end
+
+return Utilities
diff --git a/steam/vdf_parser.luau b/Source/Steam/VDFParser.luau
similarity index 65%
rename from steam/vdf_parser.luau
rename to Source/Steam/VDFParser.luau
index 98391c9..bc57d3b 100644
--- a/steam/vdf_parser.luau
+++ b/Source/Steam/VDFParser.luau
@@ -1,37 +1,79 @@
--!nocheck
--Note : Temporary
+local fs = require("@lune/fs")
-local vdfParser = {}
+local VDFParser = {}
+VDFParser.Private = {}
+--[=[
+ Contains functions for parsing VDF files and strings.
+ Lune does not support VDF files natively at the moment.
+]=]
+VDFParser.Public = {}
+
+-- Public
+
+--[=[
+ Parses a VDF file into a table.
+
+ @param path string -- The path to the file.
+ @param stopKeyList {string}? -- Limits the parsing to the given key path.
+ @param wordsFromLinesToRemove {string}? -- A list of words that, if found in a line, will remove the line. Used for (bad) optimization.
+ @return The parsed table if successful.
+]=]
+function VDFParser.Public.parseFile(path: string, stopKeyList: {string}?, wordsFromLinesToRemove: {string}?): table
+ return VDFParser.Public.parseString(fs.readFile(path), stopKeyList, wordsFromLinesToRemove)
+end
--- Lune Modules
-local fs = require("@lune/fs")
+--[=[
+ Parses a VDF string into a table.
+
+ @param input string -- The VDF string to parse.
+ @param stopKeyList {string}? -- Limits the parsing to the given key path.
+ @param wordsFromLinesToRemove {string}? -- A list of words that, if found in a line, will remove the line. Used for (bad) optimization.
+ @return The parsed table if successful.
+]=]
+function VDFParser.Public.parseString(input: string, stopKeyList: {string}?, wordsFromLinesToRemove: {string}?): table
+ -- Remove every line that contains a word from wordsFromLinesToRemove
+ if wordsFromLinesToRemove then
+ local lines = {}
+ for line in input:gmatch("[^\r\n]+") do
+ local removeLine = false
+ for _, word in pairs(wordsFromLinesToRemove) do
+ if line:find(word) then
+ removeLine = true
+ break
+ end
+ end
+ if not removeLine then
+ table.insert(lines, line)
+ end
+ end
+ input = table.concat(lines, "\n")
+ end
+
+ return VDFParser.Private.parse(input, stopKeyList)
+end
+
+-- Private
---[[
- Name : function vdfParser.parseString(input, stopKeyList)
- Description : Parses a VDF string into a table.
- Arg 1 : input (string) : The VDF string to parse.
- Arg 2 : stopKeyList (table) : A list of keys to stop the parsing at. Default : nil
- Return : result (table) : The parsed table.
-]]
-
--- Status :
--- keyWait : We wait for a key
--- readingKey : We are reading a key
--- valueWait : We wait for a value
--- readingValue : We are reading a value
--- readingTableValue : We are reading a table value
-type ReadStatus = "keyWait" | "readingKey" | "valueWait" | "readingTableValue" | "readingValue"
-
-function vdfParser.parseString(input, stopKeyList: {string}?): {[string]: any}
+--[=[
+ Parses a VDF string into a table. Private side of the parser.
+
+ @param input string -- The VDF string to parse.
+ @param stopKeyList {string}? -- Limits the parsing to the given key path.
+ @return The parsed table if successful.
+]=]
+function VDFParser.Private.parse(input: string, stopKeyList: {string}?): table
local result = {}
+ type ReadStatus = "keyWait" | "readingKey" | "valueWait" | "readingTableValue" | "readingValue"
local status: ReadStatus = "keyWait"
-- Temp Values
- local tempKey: string, tempValue: string, tempRecursiveInput: string = "", "", ""
+ local tempKey, tempValue, tempRecursiveInput = "", "", ""
- local indent: number = 0
- local previousChar: string = nil
+ local indent = 0
+ local previousChar = nil
-- Until we reached the end of the file
for i = 1, #input do
@@ -104,7 +146,7 @@ function vdfParser.parseString(input, stopKeyList: {string}?): {[string]: any}
local actualKey: any
if tonumber(tempKey) then actualKey = tonumber(tempKey)
else actualKey = tempKey end
- result[actualKey] = vdfParser.parseString(tempRecursiveInput, newStopKeyList)
+ result[actualKey] = VDFParser.Private.parse(tempRecursiveInput, newStopKeyList)
end
if stopKeyList and tempKey == stopKeyList[1] then
@@ -139,39 +181,4 @@ function vdfParser.parseString(input, stopKeyList: {string}?): {[string]: any}
return result
end
---[[
- Name : function vdfParser.parseFile(path, stopKeyList, wordsFromLinesToRemove)
- Description : Parses a VDF file into a table.
- Arg 1 : path (string) : The path to the file to parse.
- Arg 2 : stopKeyList (table) : A list of keys to stop the parsing at. Default : nil
- Arg 3 : wordsFromLinesToRemove (table) : A list of words that, if found in a line, will remove the line. Default : nil
- Return : result (table) : The parsed table.
-
- Note : Arg 2 and Arg 3 are critical for optimization in big files. Do not hesitate to use them.
- Results such as going from 0.5 seconds to 0.04 seconds are possible.
-]]
-function vdfParser.parseFile(path: string, stopKeyList: {string}?, wordsFromLinesToRemove: {string}?): {[string]: any}
- local content = fs.readFile(path)
-
- -- Remove every line that contains a word from wordsFromLinesToRemove
- if wordsFromLinesToRemove then
- local lines = {}
- for line in content:gmatch("[^\r\n]+") do
- local removeLine = false
- for _, word in pairs(wordsFromLinesToRemove) do
- if line:find(word) then
- removeLine = true
- break
- end
- end
- if not removeLine then
- table.insert(lines, line)
- end
- end
- content = table.concat(lines, "\n")
- end
-
- return vdfParser.parseString(content, stopKeyList)
-end
-
-return vdfParser
\ No newline at end of file
+return VDFParser.Public
diff --git a/Source/Steam/init.luau b/Source/Steam/init.luau
new file mode 100644
index 0000000..dbe4676
--- /dev/null
+++ b/Source/Steam/init.luau
@@ -0,0 +1,3 @@
+return {
+ Configuration = require("Configuration")
+}
\ No newline at end of file
diff --git a/Source/Utilities/Filesystem.luau b/Source/Utilities/Filesystem.luau
new file mode 100644
index 0000000..3de9ef7
--- /dev/null
+++ b/Source/Utilities/Filesystem.luau
@@ -0,0 +1,71 @@
+local fs = require("@lune/fs")
+local Logging = require("Logging")
+
+--[=[
+ Utilities for filesystem operations. Mostly a remnant from SimpleSteamTinker.
+]=]
+local Filesystem = {}
+
+--[=[
+ Creates a directory if it doesn't exist, or uses it if it does.
+
+ @param path The path to create or use.
+ @return The path to the created or used directory.
+]=]
+function Filesystem.createOrUseDirectory(path: string): string
+ if not fs.isDir(path) then
+ Logging.write("debug", "Directory "..path.." not found. Creating it...")
+ fs.writeDir(path)
+ end
+ return path
+end
+
+--[=[
+ Gets the size of a file or directory. Unimplemented.
+
+ @param path The path to get the size of.
+]=]
+function Filesystem.getSize(path: string): ()
+ --TODO
+end
+
+--[=[
+ Converts a size in bytes to a human-readable size.
+
+ @param size The size to convert.
+ @return The human-readable size.
+]=]
+function Filesystem.sizeToUnit(size: number): string
+ local units = {"B", "KB", "MB", "GB", "TB"}
+
+ local unit
+ for i = 1, 5 do
+ if size > 1024 then
+ size = size / 1024
+ unit = units[i]
+ end
+ end
+
+ return string.format("%.2f", size).." "..unit
+end
+
+--[=[
+ Returns a table containing every filename matching a pattern in a directory.
+
+ @param directory The directory to search in.
+ @param pattern The pattern to search for.
+ @return A table containing every filename matching the pattern.
+]=]
+function Filesystem.getFilenamePatternInDirectory(directory: string, pattern: string): {string}
+ local result = {}
+
+ for _, file in fs.readDir(directory) do
+ if file:match(pattern) then
+ table.insert(result, file)
+ end
+ end
+
+ return result
+end
+
+return Filesystem
diff --git a/general/log_system.luau b/Source/Utilities/Logging.luau
similarity index 59%
rename from general/log_system.luau
rename to Source/Utilities/Logging.luau
index eec4731..5f5d7a1 100644
--- a/general/log_system.luau
+++ b/Source/Utilities/Logging.luau
@@ -1,40 +1,26 @@
---!strict
-local logSystem = {}
-
---[[
- Name : local function color(colorId, message)
- Description : Colors a message.
- Arg 1 : colorId (string) : The color's id.
- Arg 2 : message (string) : The message to color.
- Return : The colored message.
-]]
type LogColor = "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "grey" | "orange" | "reset"
-local function color(colorId: LogColor, message: string): string
- local colorList = {
- red = "\27[31m",
- green = "\27[32m",
- yellow = "\27[33m",
- blue = "\27[34m",
- magenta = "\27[35m",
- cyan = "\27[36m",
- white = "\27[37m",
- grey = "\27[90m",
- orange = "\27[91m",
- reset = "\27[0m"
- }
- return colorList[colorId]..message..colorList["reset"]
-end
-
---[[
- Name : function logSystem.log(type, message)
- Description : Logs a message.
- Arg 1 : type (string) : The log type.
- Arg 2 : message (string) : The message to log.
- Return : nil
-]]
type LogType = "info" | "warning" | "error" | "debug" | "download" | "data" | "speed"
-function logSystem.log(type: LogType, message: string | number)
- local logReactions = {
+
+local Logging = {}
+Logging.Private = {}
+--[=[
+ Contains functions for logging to the console.
+]=]
+Logging.Public = {}
+
+-- Public
+
+--[=[
+ Writes a message in the logs.
+
+ @param type string -- The log type.
+ @param message string -- The message to log.
+ @return nil
+]=]
+function Logging.Public.write(type: LogType, message: string | number)
+ local color = Logging.Private.colorText
+
+ local logFunctions = {
info = function(text)
print("["..color("blue","Info").."] "..text)
end,
@@ -57,7 +43,33 @@ function logSystem.log(type: LogType, message: string | number)
print("["..color("grey","Speed").."] ".."Done in "..os.clock()-startTime.." seconds.")
end
}
- logReactions[type](message)
+
+ logFunctions[type](message)
+end
+
+-- Private
+
+--[=[
+ Colors a text.
+
+ @param colorName string -- The color's name.
+ @param text string -- The text to color.
+ @return string -- The colored text.
+]=]
+function Logging.Private.colorText(colorName: string, text: string): string
+ local colorList = {
+ red = "\27[31m",
+ green = "\27[32m",
+ yellow = "\27[33m",
+ blue = "\27[34m",
+ magenta = "\27[35m",
+ cyan = "\27[36m",
+ white = "\27[37m",
+ grey = "\27[90m",
+ orange = "\27[91m",
+ reset = "\27[0m"
+ }
+ return colorList[colorName]..text..colorList["reset"]
end
-return logSystem
+return Logging.Public
diff --git a/Source/Utilities/init.luau b/Source/Utilities/init.luau
new file mode 100644
index 0000000..66ed92c
--- /dev/null
+++ b/Source/Utilities/init.luau
@@ -0,0 +1,4 @@
+return {
+ Logging = require("Logging"),
+ Filesystem = require("Filesystem"),
+}
\ No newline at end of file
diff --git a/Source/init.luau b/Source/init.luau
new file mode 100644
index 0000000..e1ecc1f
--- /dev/null
+++ b/Source/init.luau
@@ -0,0 +1,40 @@
+local Application = {}
+
+local process = require("@lune/process")
+local fs = require("@lune/fs")
+
+local metadata = require("Metadata")
+local Filesystem = require("Utilities").Filesystem
+local Logging = require("Utilities").Logging
+local Steam = require("Steam")
+
+function Application.start()
+ -- Startup time
+ local totalStartupTimeVar = os.clock()
+
+ --[[
+ Chapter 0 : Preparing the environment
+ We need to create the config folder if it doesn't exist.
+ ]]
+
+ -- Create folders
+ for _, folder in ipairs({metadata.folders.config, metadata.folders.gamesConfig, metadata.folders.cache}) do
+ Filesystem.createOrUseDirectory(folder)
+ end
+
+ -- Development version detection
+ if metadata.version:find("dev") then
+ Logging.write("warning", "DEVELOPMENT VERSION")
+ -- Put the arguments inside the cache folder for testing purposes
+ fs.writeFile(metadata.folders.cache.."/arguments.txt", table.concat(process.args, "\n"))
+ end
+
+ --[[
+ Chapter 1 : Game management
+ What's the Steam config ? Is this being started through Steam or alone ? What to do ?!
+ ]]
+
+ local steamConfig = require("Steam").Configuration.getSteamConfiguration()
+end
+
+return Application
diff --git a/assets/desktop/system.desktop b/assets/desktop/system.desktop
deleted file mode 100755
index c2e7d71..0000000
--- a/assets/desktop/system.desktop
+++ /dev/null
@@ -1,8 +0,0 @@
-[Desktop Entry]
-Name=SimpleSteamTinker
-Icon=simplesteamtinker
-Comment=A work-in-progress simple, fast and modern Adwaita alternative to SteamTinkerLaunch.
-Exec=/usr/bin/sst
-Terminal=false
-Type=Application
-Categories=Game;
diff --git a/assets/icons/256x256.png b/assets/icons/256x256.png
deleted file mode 100644
index 1796bc04ca771c2266682f192d0249ea67a92500..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 21964
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
diff --git a/assets/icons/scalable.svg b/assets/icons/scalable.svg
deleted file mode 100644
index 0cec0ac..0000000
--- a/assets/icons/scalable.svg
+++ /dev/null
@@ -1,144 +0,0 @@
-
-
\ No newline at end of file
diff --git a/assets/screenshots/ViewLight.ora b/assets/screenshots/ViewLight.ora
deleted file mode 100644
index 54dd31a0103b8f9ac56443fabd777957b3b483bd..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 417586
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