[Linux-only] basically bloxstap for sober

BRUH UPDATE

+185 -111
+13 -2
flake.nix
··· 1 1 { 2 2 description = "Alternative Roblox bootstrapper for Linux"; 3 + 3 4 inputs.nixpkgs.url = "github:NixOS/nixpkgs/master"; 4 5 inputs.flake-utils.url = "github:numtide/flake-utils"; 5 6 6 7 outputs = { self, nixpkgs, flake-utils }: 7 8 flake-utils.lib.eachDefaultSystem (system: let 8 9 pkgs = nixpkgs.legacyPackages.${system}; 10 + gtkDeps = with pkgs; [ 11 + gjs 12 + gtk3 13 + glib 14 + gobject-introspection 15 + ]; 9 16 in { 10 17 devShell = pkgs.mkShell { 11 18 nativeBuildInputs = [ pkgs.bashInteractive ]; 12 - buildInputs = with pkgs; [ gtk3 glib ]; 19 + buildInputs = gtkDeps; 20 + 13 21 shellHook = '' 14 - export PATH=${pkgs.bun}/bin:${pkgs.nodejs}/bin:${pkgs.nodePackages.npm}/bin:$PWD/dist:$PATH 22 + export PATH=${pkgs.bun}/bin:${pkgs.nodejs}/bin:${pkgs.nodePackages.npm}/bin:$PWD/dist:$PATH 23 + 24 + export GI_TYPELIB_PATH=${pkgs.gtk3}/lib/girepository-1.0:${pkgs.glib}/lib/girepository-1.0:${pkgs.gobject-introspection}/lib/girepository-1.0 25 + export LD_LIBRARY_PATH=${pkgs.glib.out}/lib:${pkgs.gtk3.out}/lib:$LD_LIBRARY_PATH 15 26 ''; 16 27 }; 17 28 });
+2 -1
package.json
··· 1 1 { 2 2 "name": "tuxstrap", 3 3 "version": "1.0.0", 4 - "description": "A commandline launcher for Roblox on Linux", 4 + "description": "A Bloxstrap \"alternative\" for Linux users.", 5 5 "main": "src/index.ts", 6 6 "scripts": { 7 7 "dev": "bun src/index.ts --use-features \"wayland-copy,regretevator-notifications,hyprland-ipc,regretevator-waybar\"", 8 + "ui": "bun --watch src/index.ts \"roblox://tuxstrap\"", 8 9 "dev-dev": "bun src/index.ts --verbose --all-features --reset-sober-config", 9 10 "build": "mkdir -p dist && bun build --minify --compile src/ --outfile dist/tuxstrap" 10 11 },
+2 -2
src/features/regretevator-waybar.ts
··· 8 8 import { homedir } from "os"; 9 9 10 10 function writeState(data: string) { 11 - writeFileSync(`${homedir()}/.regretevator_state`, data); 11 + writeFileSync(`/tmp/.regretevator_state`, data); 12 12 } 13 13 14 14 let isInitalLaunch = true; ··· 18 18 isInitalLaunch = true; 19 19 lastFloorNum = "0"; 20 20 try { 21 - rmSync(`${homedir()}/.regretevator_state`); 21 + rmSync(`/tmp/.regretevator_state`); 22 22 } catch { } 23 23 }); 24 24
+2 -2
src/features/roblox-logger.ts
··· 3 3 4 4 bloxstraprpc.aw?.BloxstrapRPCEvent.on("PlayerEvent", (action: "JOIN" | "LEAVE", name: string, id: string) => { 5 5 console.log(`[LOGGER] ${name} (${id}) ${action === "JOIN" ? "joined" : "left"} the server`) 6 - exec( 6 + /*exec( 7 7 `notify-send -u low "Roblox" "${name} (${id}) has ${action === "JOIN" ? "joined" : "left"} the game."` 8 - ); 8 + );*/ 9 9 }) 10 10 11 11 bloxstraprpc.aw?.BloxstrapRPCEvent.on("ChatMessage", (text: string) => {
+115 -85
src/index.ts
··· 17 17 18 18 import chalk from "chalk"; 19 19 import { updateSoberConfigWithFeatures } from "./lib/SoberConfigManager"; 20 + import { runSettingsManager } from "./lib/TuxstrapManager"; 20 21 21 22 // Env 22 23 ··· 38 39 ) 39 40 .option("-s, --silent", "Disable game join and plugin notifications") 40 41 .option("-v, --verbose", "Shows Roblox's stdout in the log") 41 - .option("--background", "Attempts to disown the Roblox process or run it with hyprctl") 42 - .option("--all-features", "Use all featurs of TuxStrap. Cannot be used with `--use-features`.") 42 + .option( 43 + "--background", 44 + "Attempts to disown the Roblox process or run it with hyprctl" 45 + ) 46 + .option( 47 + "--all-features", 48 + "Use all featurs of TuxStrap. Cannot be used with `--use-features`." 49 + ) 43 50 .option("--reset-sober-config", "Resets the Sober config.") 44 51 .argument( 45 52 "[url]", ··· 57 64 58 65 const options = program.opts(); 59 66 60 - console.log(readFileSync(join(__dirname, "lib", "tuxstrap_files", "tuxstrap-figlet.txt")).toString()); 61 - 62 - registerXdgOpen(); 67 + console.log( 68 + readFileSync( 69 + join(__dirname, "lib", "tuxstrap_files", "tuxstrap-figlet.txt") 70 + ).toString() 71 + ); 63 72 64 73 if (options.disown) { 65 74 if (process.setgid) { ··· 114 123 } 115 124 if (options.resetSoberConfig === true) { 116 125 try { 117 - rmSync(SOBER_CONFIG_PATH) 126 + rmSync(SOBER_CONFIG_PATH); 118 127 } catch {} 119 128 } 120 129 if (options.useFeatures) { ··· 128 137 ); 129 138 process.exit(1); 130 139 } 131 - (opts.useFeatures as string[]) = readdirSync(join(__dirname,"features")).map(a=>a.replace(".ts","")); 140 + (opts.useFeatures as string[]) = readdirSync( 141 + join(__dirname, "features") 142 + ).map((a) => a.replace(".ts", "")); 132 143 } 133 144 if (options.noFilemods === true) { 134 145 opts.useFilemods = false; ··· 143 154 opts.verbose = true; 144 155 } 145 156 146 - updateSoberConfigWithFeatures(opts.useFeatures) 157 + (() => { 158 + const URI = program.args[0] || "roblox://"; 147 159 148 - const URI = program.args[0] || "roblox://"; 149 - const sober_cmd = `${opts.useGamemode ? "gamemoderun " : ""}${LAUNCH_COMMAND}${program.args[0] ? ` "${URI}"` : "" 150 - }`; 151 - 152 - console.log("[TUXSTRAP]", "Using features: " + opts.useFeatures.join(" ").replace('"',"")); 153 - 154 - if (options.background) { 155 - // console.log("[TUXSTRAP]", "Process argv:", process.argv.join(" ")); 156 - const procargv = process.argv.join(" ").replace("--background", ""); 157 - if (process.env.HYPRLAND_INSTANCE_SIGNATURE) { 158 - console.log( 159 - "[TUXSTRAP]", 160 - "Detected HYPRLAND_INSTANCE_SIGNATURE - Executing in background" 161 - ); 162 - spawn("hyprctl", ["dispatch", "exec", procargv]); 163 - console.log(chalk.yellow("Launching Roblox, have fun!")); 164 - process.exit(0); 160 + if (URI === "roblox://tuxstrap") { 161 + runSettingsManager(); 162 + return; 165 163 } 166 - console.error( 167 - "[TUXSTRAP]", 168 - "Cannot execute in the background! Please use your WM/DE's preferred way to run commands in the background, or fork this process and disown it." 169 - ); 170 - process.exit(1); 171 - } 172 164 173 - console.log(chalk.yellow("Launching Roblox, have fun!")); 165 + updateSoberConfigWithFeatures(opts.useFeatures); 174 166 175 - if (options.opengl) 176 - exec(`notify-send -a "tuxstrap" -u low "Roblox" "Using OpenGL renderer"`); 167 + const sober_cmd = `${ 168 + opts.useGamemode ? "gamemoderun " : "" 169 + }${LAUNCH_COMMAND}${program.args[0] ? ` "${URI}"` : ""}`; 177 170 178 - const launch_time = Date.now(); 179 - const child = exec(sober_cmd); 180 - 181 - const watcher = new ActivityWatcher(child, opts); 182 - setActivityWatcherInstance(watcher); 171 + console.log( 172 + "[TUXSTRAP]", 173 + "Using features: " + opts.useFeatures.join(" ").replace('"', "") 174 + ); 183 175 184 - const rpc = new BloxstrapRPC(watcher); 185 - setBloxstrapRPCInstance(rpc); 186 - // Fix for TypeError: undefined is not an object (evaluating 'bloxstraprpc._stashedRPCMessage') 176 + registerXdgOpen(); 187 177 188 - child.on("exit", (code) => { 189 - if (code === 0) { 190 - process.exit(0); 191 - } 192 - console.error("[TUXSTRAP]", `Sober exited with code ${code}`); 193 - if (Date.now() - launch_time > 5000) { 178 + if (options.background) { 179 + // console.log("[TUXSTRAP]", "Process argv:", process.argv.join(" ")); 180 + const procargv = process.argv.join(" ").replace("--background", ""); 181 + if (process.env.HYPRLAND_INSTANCE_SIGNATURE) { 182 + console.log( 183 + "[TUXSTRAP]", 184 + "Detected HYPRLAND_INSTANCE_SIGNATURE - Executing in background" 185 + ); 186 + spawn("hyprctl", ["dispatch", "exec", procargv]); 187 + console.log(chalk.yellow("Launching Roblox, have fun!")); 188 + process.exit(0); 189 + } 194 190 console.error( 195 191 "[TUXSTRAP]", 196 - `Roblox has likely crashed, killed or been manually closed.` 197 - ); 198 - } else { 199 - console.error( 200 - "[TUXSTRAP]", 201 - `There might be another instance of Sober running.` 192 + "Cannot execute in the background! Please use your WM/DE's preferred way to run commands in the background, or fork this process and disown it." 202 193 ); 194 + process.exit(1); 203 195 } 204 - process.exit(1); 205 - }); 206 196 207 - (async () => { 208 - opts.useFeatures 209 - .map((v: string) => `features/${v}`) 210 - .forEach((m: string) => { 211 - try { 212 - require(`${__dirname}/${m}`); 213 - console.log("[TUXSTRAP]", `Successfully loaded ${m}`); 214 - } catch (e_) { 215 - if (`${e_}`.includes("find module")) { 216 - console.error("[TUXSTRAP]", `Feature ${m} doesn't exist`); 217 - if (opts.showNotifications) 218 - exec( 219 - `notify-send -a "tuxstrap" -u low "Roblox" "Cannot find ${m}"` 220 - ); 221 - } else { 222 - console.error("[INIT]", `Failed to load ${m}:`, e_); 223 - if (opts.showNotifications) 224 - exec( 225 - `notify-send -a "tuxstrap" -u low "Roblox" "Error loading ${m}"` 226 - ); 227 - } 228 - } 229 - }); 230 - })(); 197 + console.log(chalk.yellow("Launching Roblox, have fun!")); 231 198 232 - if (opts.showNotifications) { 233 - activityWatcher.BloxstrapRPCEvent.on("ObtainLog", () => { 199 + if (options.opengl) 234 200 exec( 235 - `notify-send -a tuxstrap -u low "TuxStrap" "Obtained Roblox's logfile"` 201 + `notify-send -a "tuxstrap" -u low "Roblox" "Using OpenGL renderer"` 236 202 ); 203 + 204 + const launch_time = Date.now(); 205 + const child = exec(sober_cmd); 206 + 207 + const watcher = new ActivityWatcher(child, opts); 208 + setActivityWatcherInstance(watcher); 209 + 210 + const rpc = new BloxstrapRPC(watcher); 211 + setBloxstrapRPCInstance(rpc); 212 + // Fix for TypeError: undefined is not an object (evaluating 'bloxstraprpc._stashedRPCMessage') 213 + 214 + child.on("exit", (code) => { 215 + if (code === 0) { 216 + process.exit(0); 217 + } 218 + console.error("[TUXSTRAP]", `Sober exited with code ${code}`); 219 + if (Date.now() - launch_time > 5000) { 220 + console.error( 221 + "[TUXSTRAP]", 222 + `Roblox has likely crashed, killed or been manually closed.` 223 + ); 224 + } else { 225 + console.error( 226 + "[TUXSTRAP]", 227 + `There might be another instance of Sober running.` 228 + ); 229 + } 230 + process.exit(1); 237 231 }); 238 - } 239 232 240 - watcher.stdoutWatcher(); 233 + (async () => { 234 + opts.useFeatures 235 + .map((v: string) => `features/${v}`) 236 + .forEach((m: string) => { 237 + try { 238 + require(`${__dirname}/${m}`); 239 + console.log("[TUXSTRAP]", `Successfully loaded ${m}`); 240 + } catch (e_) { 241 + if (`${e_}`.includes("find module")) { 242 + console.error( 243 + "[TUXSTRAP]", 244 + `Feature ${m} doesn't exist` 245 + ); 246 + if (opts.showNotifications) 247 + exec( 248 + `notify-send -a "tuxstrap" -u low "Roblox" "Cannot find ${m}"` 249 + ); 250 + } else { 251 + console.error("[INIT]", `Failed to load ${m}:`, e_); 252 + if (opts.showNotifications) 253 + exec( 254 + `notify-send -a "tuxstrap" -u low "Roblox" "Error loading ${m}"` 255 + ); 256 + } 257 + } 258 + }); 259 + })(); 260 + 261 + if (opts.showNotifications) { 262 + activityWatcher.BloxstrapRPCEvent.on("ObtainLog", () => { 263 + exec( 264 + `notify-send -a tuxstrap -u low "TuxStrap" "Obtained Roblox's logfile"` 265 + ); 266 + }); 267 + } 268 + 269 + watcher.stdoutWatcher(); 270 + })();
+7
src/lib/TuxstrapManager/index.ts
··· 1 + import { $ } from "bun"; 2 + 3 + 4 + export async function runSettingsManager() { 5 + console.log("Launching settings manager."); 6 + await $`gjs ${__dirname}/ManagerUI.js`.nothrow(); // TBD 7 + }
+25 -9
src/lib/XdgRegistration.ts
··· 7 7 8 8 const execAsync = promisify(exec); 9 9 10 - export async function registerXdgOpen() { 10 + export async function registerDesktopFile(filename: string, runPath: string): Promise<void> { 11 11 try { 12 - const commandToRun = process.argv[0]+' run '+join(__dirname,"..","index.ts") 12 + const commandToRun = process.argv[0] + ' run ' + runPath; 13 13 console.log("[TUXSTRAP] Possible bun run command:", commandToRun); 14 - 15 - const newDesktopSource = readFileSync(join(__dirname,"tuxstrap_files","tuxstrap.desktop")).toString().replace("%TUXSTRAP_BIN%",commandToRun) 16 - const desktopFilePath = join(process.env.HOME || "", ".local", "share", "applications", "tuxstrap.desktop"); 14 + 15 + const newDesktopSource = readFileSync(join(__dirname, "tuxstrap_files", filename)).toString().replace("%TUXSTRAP_BIN%", commandToRun); 16 + const desktopFilePath = join(process.env.HOME || "", ".local", "share", "applications", filename); 17 17 18 18 function hashContent(content: Buffer): string { 19 19 return createHash("sha256").update(content).digest("hex"); 20 20 } 21 21 22 - const newDesktopHash = hashContent(new Buffer(newDesktopSource)); 22 + const newDesktopHash = hashContent(Buffer.from(newDesktopSource)); 23 23 24 24 if (!existsSync(desktopFilePath) || hashContent(readFileSync(desktopFilePath)) !== newDesktopHash) { 25 25 writeFileSync(desktopFilePath, newDesktopSource); 26 - console.warn("[TUXSTRAP] Placed tuxstrap.desktop"); 26 + console.warn(`[TUXSTRAP] Placed ${filename}`); 27 27 } else { 28 - console.warn("[TUXSTRAP] XDG registration already exists"); 28 + console.warn(`[TUXSTRAP] Desktop file registration already exists (${filename})`); 29 29 } 30 30 } catch (error) { 31 - console.error("[TUXSTRAP] Failed to place tuxstrap.desktop", error); 31 + console.error(`[TUXSTRAP] Failed to place ${filename}`, error); 32 32 } 33 + } 34 + 35 + export async function registerXdgOpen(): Promise<void> { 36 + try { 37 + await registerDesktopFile( 38 + "tuxstrap.desktop", 39 + join(__dirname, "..", "index.ts") 40 + ); 41 + await registerDesktopFile( 42 + "tuxstrap-manager.desktop", 43 + join(__dirname, "..", "index.ts") 44 + ); 45 + } catch (error) { 46 + console.error("[TUXSTRAP] Failed to register desktop file", error); 47 + } 48 + 33 49 try { 34 50 await execAsync(`xdg-mime default tuxstrap.desktop x-scheme-handler/roblox`); 35 51 await execAsync(`xdg-mime default tuxstrap.desktop x-scheme-handler/roblox-player`);
+8
src/lib/tuxstrap_files/tuxstrap-manager.desktop
··· 1 + [Desktop Entry] 2 + Name=TuxStrap Settings 3 + Comment=Launches the TuxStrap Settings UI 4 + Keywords=Roblox;TuxStrap;Manager;Settings; 5 + Exec=%TUXSTRAP_BIN% --background --all-features "roblox://tuxstrap" 6 + Type=Application 7 + NoDisplay=false 8 + Categories=Game
+11 -10
tsconfig.json
··· 1 1 { 2 - "compilerOptions": { 3 - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 4 - "module": "commonjs", /* Specify what module code is generated. */ 5 - "rootDir": "./src/", /* Specify the root folder within your source files. */ 6 - "outDir": "./dist/", /* Specify an output folder for all emitted files. */ 7 - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 8 - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 9 - "strict": true, /* Enable all strict type-checking options. */ 10 - "skipLibCheck": true /* Skip type checking all .d.ts files. */ 11 - } 2 + "compilerOptions": { 3 + "jsx": "react", 4 + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 5 + "module": "commonjs" /* Specify what module code is generated. */, 6 + "rootDir": "./src/" /* Specify the root folder within your source files. */, 7 + "outDir": "./dist/" /* Specify an output folder for all emitted files. */, 8 + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 9 + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 10 + "strict": true /* Enable all strict type-checking options. */, 11 + "skipLibCheck": true /* Skip type checking all .d.ts files. */ 12 + } 12 13 }