├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── build └── .gitignore ├── conf.ex.json ├── css └── style.css ├── images ├── dolphin.jpg ├── plex.png ├── retroarch.png └── steam.jpg ├── js └── index.js ├── logs ├── .gitignore └── web │ ├── .gitignore │ └── apps │ └── .gitignore ├── preview ├── vid-thumb.jpeg └── web-preview01.png ├── src ├── desktop │ ├── app.cpp │ └── include │ │ ├── cmdline.hpp │ │ ├── config.hpp │ │ └── gui.hpp └── site │ ├── config │ ├── load.go │ ├── types.go │ └── utils.go │ ├── go.mod │ ├── main.go │ └── web │ ├── handler.go │ ├── routes.go │ └── server.go ├── systemd ├── wdal-app.service └── wdal-web.service └── templates └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | conf.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/desktop/modules/json"] 2 | path = src/desktop/modules/json 3 | url = https://github.com/nlohmann/json 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_DIR = build 2 | SRC_DIR = src 3 | SYSTEMD_DIR = systemd 4 | 5 | APP_BUILD = $(BUILD_DIR)/wdal-app 6 | WEB_BUILD = $(BUILD_DIR)/wdal-web 7 | 8 | ETC_DIR = /etc/wdal 9 | CONF_LOC = $(ETC_DIR)/conf.json 10 | 11 | MODULE_JSON_DIR = $(SRC_DIR)/desktop/modules/json/ 12 | 13 | all: wdal-app wdal-web 14 | 15 | json: 16 | mkdir $(MODULE_JSON_DIR)/build && cd $(MODULE_JSON_DIR)/build && cmake .. && cmake --build . --target install 17 | 18 | wdal-app: 19 | g++ `pkg-config --cflags gtk+-3.0 webkit2gtk-4.0` -o build/wdal-app $(SRC_DIR)/desktop/app.cpp `pkg-config --libs gtk+-3.0 webkit2gtk-4.0` 20 | 21 | wdal-web: 22 | cd $(SRC_DIR)/site/ && go build -o ../../$(BUILD_DIR)/wdal-web 23 | 24 | clean: 25 | rm -f $(BUILD_DIR)/* 26 | $(MAKE) -c $(MODULE_JSON_DIR) clean 27 | 28 | install: 29 | cp -f $(APP_BUILD) /usr/bin 30 | cp -f $(WEB_BUILD) /usr/bin 31 | mkdir -p $(ETC_DIR) 32 | cp -n ./conf.ex.json $(CONF_LOC) 33 | cp -n $(SYSTEMD_DIR)/wdal-app.service /etc/systemd/system 34 | cp -n $(SYSTEMD_DIR)/wdal-web.service /etc/systemd/system -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | After purchasing a [mini-PC](https://www.amazon.com/dp/B0D5CS3CDS) that I planned to use for movie and game streaming along with emulation ([Dolphin](https://dolphin-emu.org/); Wii/GameCube), I found opening specific applications from the terminal was inconvenient. I wanted an application that allowed me to launch specific applications such as [Steam Link](https://store.steampowered.com/app/353380/Steam_Link/), [RetroArch](https://www.retroarch.com/), [Dolphin](https://dolphin-emu.org/), and [Plex HTPC](https://support.plex.tv/articles/htpc-getting-started/) from the desktop itself and through a website. 2 | 3 | While I am sure there are applications that are already capable of achieving this, I wanted to make my own applications to gain more experience (specifically with C++ and GTK + WebKit2GTK). 4 | 5 | **NOTE** - The website intentionally doesn't contain any security measures and runs commands under the user running the website. Do **not** expose this website to unauthorized users! I only recommend running it under your LAN and on a server that doesn't contain any sensitive data. 6 | 7 | ## Preview 8 | ### Video Demonstration 9 | [![vid](./preview/vid-thumb.jpeg)](https://www.youtube.com/watch?v=0g1pquXcWic) 10 | ### Website 11 | ![Website Preview](./preview/web-preview01.png) 12 | 13 | **NOTE** - I may improve the front-end web design in the future, but wanted to create something simple first. 14 | 15 | ## Technologies Used 16 | ### Desktop Application 17 | I utilize [GTK 3](https://docs.gtk.org/gtk3/) and [WebKit2GTK](https://webkitgtk.org/) to create a desktop application through GNOME and display our web page. 18 | 19 | ### Web Application 20 | I simply utilize Golang for running a web server with its template system for serving HTML content along with basic JavaScript and CSS. 21 | 22 | ## Building 23 | I recommend using the [`Makefile`](./Makefile) via `make` to build this project. You will also need to build the [`nlohmann/json`](https://github.com/nlohmann/json) library which is used to parse our config file in the desktop application. 24 | 25 | ### Prerequisites 26 | * Golang 27 | * GTK 3 & WebKit2GTK (e.g. `libgtk-3-dev` and `libwebkit2gtk-4.0-dev` Ubuntu/Debian packages) 28 | * [`nlohmann/json`](https://github.com/nlohmann/json) 29 | 30 | For Ubuntu/Debian, I'd recommend the following command to install the required system packages. 31 | 32 | ```bash 33 | sudo apt install -y libgtk-3-dev libwebkit2gtk-4.0-dev pkgconf build-essential make cmake golang-go 34 | ``` 35 | 36 | ### Cloning 37 | Clone the repository recursively so that the JSON sub-module is also cloned. 38 | 39 | ```bash 40 | # Clone the repository 41 | git clone --recursive https://github.com/gamemann/web-desktop-app-launcher.git /var/web-desktop-app-launcher 42 | 43 | # Change directories. 44 | cd /var/web-desktop-app-launcher 45 | ``` 46 | 47 | **NOTE** - The reason I clone into `/var/` is because that is the directory the [`systemd`](./systemd/) services use as the working directory. You may change this if you'd like of course. 48 | 49 | ### Desktop Application 50 | #### Building The JSON Library 51 | You will need to build the [`nlohmann/json`](https://github.com/nlohmann/json) library before building the main desktop application. You can use `make json` or the following shell commands. 52 | 53 | ```bash 54 | # Change to JSON library. 55 | cd src/desktop/modules/json 56 | 57 | # Create build directory and change to it. 58 | mkdir build 59 | 60 | cd build/ 61 | 62 | # Use CMake. 63 | cmake .. 64 | 65 | # Install library to system. 66 | cmake --build . --target install 67 | ``` 68 | 69 | Afterwards, you may use `make` and `make install` (as root) to build and install the desktop and web applications. 70 | 71 | ## Command Line 72 | At this time, both the web and desktop applications use the same command-line options which are listed below. 73 | 74 | | Flag(s) | Default | Description | 75 | | ----- | ------- | ----------- | 76 | | `-c --cfgflag` | `/etc/wdal/conf.json` | The path to the config file. | 77 | | `-l --list` | N/A | Prints the config settings. | 78 | | `-v --version` | N/A | Prints the current version. | 79 | | `-h --help` | N/A | Prints the help menu. | 80 | 81 | ## Configuration 82 | Both the desktop and web applications parse a single JSON config file (the default path is `/etc/wdal/conf.json`). In both applications, you can also change the config path via the `-c` and `--cfgpath` command-line flags (e.g. `-c ./conf.json`). 83 | 84 | | Key | Type | Default | Description | 85 | | --- | ---- | ------- | ----------- | 86 | | desktop | Desktop Object | `{}` | The desktop application object (read below). | 87 | | web | Web Object | `{}` | The web object (read below). | 88 | | applications | Application Object Array | `[]` | The applications object array (read below). | 89 | 90 |
91 | Example(s) 92 | 93 | ```json 94 | { 95 | "desktop": { 96 | ... 97 | }, 98 | "web": { 99 | ... 100 | }, 101 | "apps": [ 102 | ... 103 | ] 104 | } 105 | ``` 106 |
107 | 108 | ### Desktop Object 109 | The desktop object contains information on the desktop application. 110 | 111 | | Key | Type | Default | Description | 112 | | --- | ---- | ------- | ----------- | 113 | | window_width | int | `1920` | The application window width (useless with full-screen mode). | 114 | | window_height | int | `1080` | The application window height (useless with full-screen mode). | 115 | | full_screen | bool | `true` | Whether to use full-screen mode for the desktop application. | 116 | 117 |
118 | Example(s) 119 | 120 | ```json 121 | { 122 | "window_width": 1200, 123 | "window_height": 720, 124 | "full_screen": false 125 | } 126 | ``` 127 |
128 | 129 | ### Web Object 130 | The web object contains information on the website and web server. 131 | 132 | | Key | Type | Default | Description | 133 | | --- | ---- | ------- | ----------- | 134 | | host | string | `127.0.0.1` | The web host/address to bind with. | 135 | | port | int | `2001` | The web port to bind with. | 136 | | log_to_file | bool | `true` | Logs `stdout` and `stderr` pipes from processes launched to `log_directory/apps`. | 137 | | log_directory | string | `logs/web` | The directory to log to. | 138 | | env | Object | `{}` | A string to string object that represents environmental variables that should be set before launching every application. | 139 | 140 |
141 | Example(s) 142 | 143 | ```json 144 | { 145 | "host": "192.168.1.5", 146 | "port": 80, 147 | "env": { 148 | "GLOBAL_ENV1": "VAL1", 149 | "GLOBAL_ENV2": "VAL2" 150 | } 151 | } 152 | ``` 153 |
154 | 155 | ### Application Object 156 | | Key | Type | Default | Description | 157 | | --- | ---- | ------- | ----------- | 158 | | name | string | `NULL` | The name of the application (for display). | 159 | | start | string | `NULL` | The command to execute when starting the application. | 160 | | stop | string | `NULL` | The command to execute when stopping the application. | 161 | | banner | string | `NULL` | The banner to use inside of the web-view card. | 162 | | env | Object | `{}` | A string to string object that represents environmental variables (key => value). | 163 | 164 |
165 | Example(s) 166 | 167 | ```json 168 | [ 169 | { 170 | "name": "Steam Link", 171 | "start": "flatpak run com.valvesoftware.SteamLink", 172 | "stop": "pkill steamlink", 173 | "banner": "/images/steam.jpg" 174 | }, 175 | { 176 | "name": "RetroArch", 177 | "start": "retroarch", 178 | "stop": "pkill retroarch", 179 | "banner": "/images/retroarch.png" 180 | }, 181 | { 182 | "name": "Dolphin", 183 | "start": "dolphin-emu", 184 | "stop": "pkill dolphin-emu", 185 | "banner": "/images/dolphin.jpg" 186 | }, 187 | { 188 | "name": "Plex HTPC", 189 | "start": "flatpak run tv.plex.PlexHTPC", 190 | "stop": "pkill plex-bin", 191 | "banner": "/images/plex.png" 192 | } 193 | ] 194 | ``` 195 |
196 | 197 | ## Notes 198 | ### Launching GUI Applications From WDAL On Debian 12 199 | There were a couple of things I needed to do in order to get applications to launch from WDAL. 200 | 201 | 1. The command `xhost +LOCAL:` (or `xhost +SI:localuser:$(whoami)`) needs to be executed. Executing this command doesn't save on reboot, but for most Linux distros you can put this command inside your `$HOME/.bashrc` file so it saves on reboot. 202 | 2. The `DISPLAY` (usually `:0`) and `XAUTHORITY` (usually `$HOME/.Xauthority`) environmental variables need to be set inside of the config for all apps. 203 | 204 | This is likely the case for other Linux distros also. There's a chance on other distros like Ubuntu that it has a different desktop manager (e.g. GDM) and the `.Xauthority` file is somewhere else. 205 | 206 | For my Debian 12 installation that has autologin enabled (and keyring disabled), placing the `xhost` command inside of the `$HOME/.bashrc`, `$HOME/.xinitrc`, `$HOME/.xprofile`, `/etc/profile`, and `/etc/X11/Xsession.d/60xhost` files did not work on reboot along with a `systemd` service. Therefore, I needed to create a desktop autostart file in `$HOME/.config/autostart/xhost.desktop` with the following contents (you may need to create the `$HOME/.config/autostart` directory). 207 | 208 | ```bash 209 | [Desktop Entry] 210 | Type=Application 211 | Exec=bash -c "sleep 5 && DISPLAY=:0 xhost +LOCAL:" 212 | X-GNOME-Autostart-enabled=true 213 | ``` 214 | 215 | ## Credits 216 | * [Christian Deacon](https://github.com/gamemann) -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /conf.ex.json: -------------------------------------------------------------------------------- 1 | { 2 | "desktop": { 3 | "window_width": 1920, 4 | "window_height": 1080, 5 | "full_screen": true 6 | }, 7 | "web": { 8 | "host": "127.0.0.1", 9 | "port": 2001, 10 | "env": { 11 | "DISPLAY": ":0", 12 | "XAUTHORITY": "$HOME/.Xauthority" 13 | } 14 | }, 15 | "applications": [ 16 | { 17 | "name": "Steam Link", 18 | "start": "flatpak run com.valvesoftware.SteamLink", 19 | "stop": "pkill steamlink", 20 | "banner": "/images/steam.jpg" 21 | }, 22 | { 23 | "name": "RetroArch", 24 | "start": "retroarch", 25 | "stop": "pkill retroarch", 26 | "banner": "/images/retroarch.png" 27 | }, 28 | { 29 | "name": "Dolphin", 30 | "start": "dolphin-emu", 31 | "stop": "pkill dolphin-emu", 32 | "banner": "/images/dolphin.jpg" 33 | }, 34 | { 35 | "name": "Plex HTPC", 36 | "start": "flatpak run tv.plex.PlexHTPC", 37 | "stop": "pkill plex-bin", 38 | "banner": "/images/plex.png" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #FFFFFF; 3 | background-color: #000000; 4 | } 5 | 6 | body > h1 { 7 | text-align: center; 8 | } 9 | 10 | button { 11 | color: #FFFFFF; 12 | cursor: pointer; 13 | font-size: 24px; 14 | font-weight: bold; 15 | border: 0px; 16 | } 17 | 18 | #app-list { 19 | display: grid; 20 | row-gap: 1.5em; 21 | column-gap: 1em; 22 | grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); 23 | } 24 | 25 | .app-item { 26 | width: 320px; 27 | height: 180px; 28 | padding: 5px; 29 | border-radius: 5px; 30 | background-color: #220058; 31 | position: relative; 32 | } 33 | 34 | .app-item > div:last-child { 35 | z-index: 20; 36 | height: 100%; 37 | display: flex; 38 | flex-direction: column; 39 | gap: 5px; 40 | position: relative; 41 | } 42 | 43 | .app-item > div > div:first-child { 44 | display: flex; 45 | justify-content: center; 46 | font-size: larger; 47 | font-weight: bold; 48 | } 49 | 50 | .app-item > img { 51 | position: absolute; 52 | top: 0; 53 | left: 0; 54 | width: 100%; 55 | height: 100%; 56 | z-index: 10; 57 | object-fit: cover; 58 | opacity: 0.7; 59 | border-radius: 5px; 60 | } 61 | 62 | .app-bg-overlay { 63 | position: absolute; 64 | background-color: #000000; 65 | top: 0; 66 | left: 0; 67 | width: 100%; 68 | height: 100%; 69 | z-index: 20; 70 | opacity: 0.5; 71 | } 72 | 73 | .app-item:hover img { 74 | opacity: 1; 75 | } 76 | 77 | .app-grow { 78 | flex-grow: 1; 79 | } 80 | 81 | .app-buttons { 82 | display: flex; 83 | justify-content: center; 84 | gap: 15px; 85 | } 86 | 87 | .app-start { 88 | padding: 5px; 89 | border-radius: 5px; 90 | background-color: #007736; 91 | } 92 | 93 | .app-start:hover { 94 | background-color: #009c46; 95 | } 96 | 97 | .app-stop { 98 | padding: 2px; 99 | border-radius: 5px; 100 | background-color: #FF0000; 101 | } 102 | 103 | .app-stop:hover { 104 | background-color: #ff2a2a; 105 | } 106 | -------------------------------------------------------------------------------- /images/dolphin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/web-desktop-app-launcher/cda0543c95119c208f52965b6f55e510be3c1d64/images/dolphin.jpg -------------------------------------------------------------------------------- /images/plex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/web-desktop-app-launcher/cda0543c95119c208f52965b6f55e510be3c1d64/images/plex.png -------------------------------------------------------------------------------- /images/retroarch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/web-desktop-app-launcher/cda0543c95119c208f52965b6f55e510be3c1d64/images/retroarch.png -------------------------------------------------------------------------------- /images/steam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/web-desktop-app-launcher/cda0543c95119c208f52965b6f55e510be3c1d64/images/steam.jpg -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | console.log("Window loaded!") 3 | 4 | function parseCmd() { 5 | // Get app info. 6 | var idx = parseInt(this.getAttribute("data-index"), 10) 7 | var type = parseInt(this.getAttribute("data-type"), 10) 8 | 9 | // Send POST request to back-end that submits command. 10 | fetch("/backend/submit", { 11 | method: "POST", 12 | headers: { 13 | "Content-Type": "application/json" 14 | }, 15 | body: JSON.stringify({ index: idx, type: type }) 16 | }) 17 | .then(data => { 18 | if (type == 0) 19 | console.log("Launched application successfully!") 20 | else 21 | console.log("Stopped application successfully!") 22 | }) 23 | .catch((err) => { 24 | console.error(err) 25 | }) 26 | } 27 | 28 | // Handle on clicks for app start buttons. 29 | var appStarts = document.getElementsByClassName("app-start") 30 | 31 | for (var i = 0; i < appStarts.length; i++) { 32 | appStarts[i].addEventListener("click", parseCmd) 33 | } 34 | 35 | // Handle on clicks for app stop buttons. 36 | var appStops = document.getElementsByClassName("app-stop") 37 | 38 | for (var i = 0; i < appStops.length; i++) { 39 | appStops[i].addEventListener("click", parseCmd) 40 | } 41 | } -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !web -------------------------------------------------------------------------------- /logs/web/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !apps -------------------------------------------------------------------------------- /logs/web/apps/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /preview/vid-thumb.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/web-desktop-app-launcher/cda0543c95119c208f52965b6f55e510be3c1d64/preview/vid-thumb.jpeg -------------------------------------------------------------------------------- /preview/web-preview01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/web-desktop-app-launcher/cda0543c95119c208f52965b6f55e510be3c1d64/preview/web-preview01.png -------------------------------------------------------------------------------- /src/desktop/app.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "include/cmdline.hpp" 6 | #include "include/config.hpp" 7 | #include "include/gui.hpp" 8 | 9 | #define VERSION "1.0.0" 10 | 11 | int main(int argc, char **argv) { 12 | int ret; 13 | 14 | // Parse command line. 15 | CmdLine cmd; 16 | 17 | if ((ret = ParseCmdLine(cmd, argc, argv)) != 0) { 18 | std::cerr << "Error parsing command line :: Return code => " << ret << std::endl; 19 | 20 | return ret; 21 | } 22 | 23 | // Check for version. 24 | if (cmd.Version) { 25 | printf(VERSION); 26 | 27 | return 0; 28 | } 29 | 30 | // Check for help. 31 | if (cmd.Help) { 32 | PrintHelp(); 33 | 34 | return 0; 35 | } 36 | 37 | // Parse config. 38 | Config cfg; 39 | 40 | if ((ret = ParseConfig(cfg, cmd.CfgPath)) != 0) 41 | return ret; 42 | 43 | // Check for list option. 44 | if (cmd.List) { 45 | ListConfig(cfg); 46 | 47 | return 0; 48 | } 49 | 50 | // Setup GUI application. 51 | SetupGui(cfg, argc, argv); 52 | 53 | return 0; 54 | } -------------------------------------------------------------------------------- /src/desktop/include/cmdline.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct cmdline { 7 | std::string CfgPath = "/etc/wdal/conf.json"; 8 | bool List = false; 9 | bool Version = false; 10 | bool Help = false; 11 | } typedef CmdLine; 12 | 13 | static struct option longOpts[] = { 14 | { "cfgpath", required_argument, nullptr, 'c' }, 15 | { "list", no_argument, nullptr, 'l' }, 16 | { "version", no_argument, nullptr, 'v' }, 17 | { "help", no_argument, nullptr, 'h' }, 18 | { nullptr, 0, nullptr, 0 } 19 | }; 20 | 21 | static void PrintHelp() { 22 | printf("Usage: wdal-app -c -l -v -h\n"); 23 | printf("-c --cfgpath => The path to the config file (default /etc/wdal/conf.json).\n"); 24 | printf("-l --list => Lists config options.\n"); 25 | printf("-v --version => Prints the current version.\n"); 26 | printf("-h --help => Prints the help menu.\n"); 27 | } 28 | 29 | static int ParseCmdLine(CmdLine& cmd, int argc, char** argv) { 30 | int opt; 31 | 32 | while ((opt = getopt_long(argc, argv, "c:lvh", longOpts, nullptr)) != -1) { 33 | switch (opt) { 34 | case 'c': 35 | cmd.CfgPath = optarg; 36 | 37 | break; 38 | 39 | case 'l': 40 | cmd.List = true; 41 | 42 | break; 43 | 44 | case 'v': 45 | cmd.Version = true; 46 | 47 | break; 48 | 49 | case 'h': 50 | cmd.Help = true; 51 | 52 | break; 53 | 54 | case '?': 55 | cmd.Help = true; 56 | 57 | break; 58 | 59 | default: 60 | std::cerr << "Unknown option: " << opt << std::endl; 61 | 62 | return 1; 63 | } 64 | } 65 | 66 | return 0; 67 | } -------------------------------------------------------------------------------- /src/desktop/include/config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | struct Config { 12 | int WindowWidth = 1920; 13 | int WindowHeight = 1080; 14 | bool FullScreen = true; 15 | std::string WebHost = "127.0.0.1"; 16 | int WebPort = 2001; 17 | } typedef Config; 18 | 19 | static void ListConfig(Config& cfg) { 20 | printf("General\n"); 21 | printf("\tWindow Width => %d.\n", cfg.WindowWidth); 22 | printf("\tWindow Height => %d.\n\n", cfg.WindowHeight); 23 | printf("Web Settings\n"); 24 | printf("\tHost => %s.\n", cfg.WebHost.c_str()); 25 | printf("\tPort => %d.\n", cfg.WebPort); 26 | } 27 | 28 | static int ParseConfig(Config& cfg, const std::string& path) { 29 | std::cout << "Parsing config file '" << path << "'..." << std::endl; 30 | 31 | std::ifstream f(path); 32 | 33 | if (!f.is_open()) { 34 | std::cerr << "Error opening config file :: " << std::strerror(errno) << std::endl; 35 | 36 | return 1; 37 | } 38 | 39 | // Parse JSON. 40 | nlohmann::json json_cfg; 41 | 42 | try { 43 | f >> json_cfg; 44 | } catch (nlohmann::json::parse_error& e) { 45 | std::cerr << "Error parsing JSON :: " << e.what() << std::endl; 46 | 47 | return 1; 48 | } 49 | 50 | // Assign values. 51 | try { 52 | cfg.WindowWidth = json_cfg.at("desktop").at("window_width").get(); 53 | cfg.WindowHeight = json_cfg.at("desktop").at("window_height").get(); 54 | cfg.FullScreen = json_cfg.at("desktop").at("full_screen").get(); 55 | cfg.WebHost = json_cfg.at("web").at("host").get(); 56 | cfg.WebPort = json_cfg.at("web").at("port").get(); 57 | } catch (const nlohmann::json::exception& e) { 58 | std::cerr << "Error accessing and assigning JSON fields :: " << e.what() << std::endl; 59 | 60 | return 1; 61 | } 62 | 63 | return 0; 64 | } -------------------------------------------------------------------------------- /src/desktop/include/gui.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "config.hpp" 9 | 10 | static void DestroyWindow(GtkWidget* widget, GtkWidget* window) 11 | { 12 | std::cout << "Closing application..." << std::endl; 13 | 14 | gtk_main_quit(); 15 | } 16 | 17 | static gboolean CloseWebView(WebKitWebView* webView, GtkWidget* window) 18 | { 19 | gtk_widget_destroy(window); 20 | 21 | return true; 22 | } 23 | 24 | static int SetupGui(Config& cfg, int argc, char **argv) { 25 | // Format full web URL. 26 | char url[256]; 27 | 28 | sprintf(url, "http://%s:%d", cfg.WebHost.c_str(), cfg.WebPort); 29 | 30 | std::cout << "Opening URL '" << url << "'!" << std::endl; 31 | 32 | // Init GTK. 33 | gtk_init(&argc, &argv); 34 | 35 | // Create main window with width/height from config. 36 | GtkWidget *main_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); 37 | gtk_window_set_default_size(GTK_WINDOW(main_window), cfg.WindowWidth, cfg.WindowHeight); 38 | 39 | // Check for full screen. 40 | if (cfg.FullScreen) 41 | gtk_window_fullscreen(GTK_WINDOW(main_window)); 42 | 43 | // Create web view window. 44 | WebKitWebView *webView = WEBKIT_WEB_VIEW(webkit_web_view_new()); 45 | 46 | gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(webView)); 47 | 48 | // Create signals for destroying windows. 49 | g_signal_connect(main_window, "destroy", G_CALLBACK(DestroyWindow), NULL); 50 | g_signal_connect(webView, "close", G_CALLBACK(CloseWebView), main_window); 51 | 52 | // Load URL inside of web window. 53 | webkit_web_view_load_uri(webView, url); 54 | 55 | // Gain focus. 56 | gtk_widget_grab_focus(GTK_WIDGET(webView)); 57 | 58 | // Show main window. 59 | gtk_widget_show_all(main_window); 60 | 61 | // Execute GTK. 62 | gtk_main(); 63 | 64 | return 0; 65 | } -------------------------------------------------------------------------------- /src/site/config/load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | func (cfg *Config) LoadFromFs(path string) error { 9 | var err error 10 | 11 | f, err := os.Open(path) 12 | 13 | if err != nil { 14 | return err 15 | } 16 | 17 | defer f.Close() 18 | 19 | stat, err := f.Stat() 20 | 21 | if err != nil { 22 | return err 23 | } 24 | 25 | data := make([]byte, stat.Size()) 26 | 27 | _, err = f.Read(data) 28 | 29 | if err != nil { 30 | return err 31 | } 32 | 33 | err = json.Unmarshal([]byte(data), cfg) 34 | 35 | return err 36 | } 37 | -------------------------------------------------------------------------------- /src/site/config/types.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Web struct { 4 | Host string `json:"host"` 5 | Port int `json:"port"` 6 | 7 | LogToFile bool `json:"log_to_file"` 8 | LogDirectory string `json:"log_directory"` 9 | 10 | Env map[string]string `json:"env"` 11 | } 12 | 13 | type App struct { 14 | Name string `json:"name"` 15 | Start string `json:"start"` 16 | Stop string `json:"stop"` 17 | Banner string `json:"banner"` 18 | Env map[string]string `json:"env"` 19 | } 20 | 21 | type Config struct { 22 | Web Web `json:"web"` 23 | Apps []App `json:"applications"` 24 | } 25 | -------------------------------------------------------------------------------- /src/site/config/utils.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "fmt" 4 | 5 | func (cfg *Config) Print() { 6 | fmt.Println("Web Settings") 7 | fmt.Printf("\tHost => %s\n", cfg.Web.Host) 8 | fmt.Printf("\tPort => %d\n", cfg.Web.Port) 9 | 10 | if len(cfg.Apps) > 0 { 11 | fmt.Println("Applications") 12 | for i, app := range cfg.Apps { 13 | fmt.Printf("\tApplication #%d\n", i+1) 14 | fmt.Printf("\t\tName => %s\n", app.Name) 15 | fmt.Printf("\t\tStart Command => %s\n", app.Start) 16 | fmt.Printf("\t\tStop Command => %s\n", app.Stop) 17 | fmt.Printf("\t\tBanner => %s\n", app.Banner) 18 | } 19 | } 20 | } 21 | 22 | func (cfg *Config) SetDefaults() { 23 | cfg.Web.LogToFile = true 24 | cfg.Web.LogDirectory = "logs/web" 25 | 26 | cfg.Web.Host = "127.0.0.1" 27 | cfg.Web.Port = 2001 28 | } 29 | -------------------------------------------------------------------------------- /src/site/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gamemann/web-desktop-app-launcher 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /src/site/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/gamemann/web-desktop-app-launcher/config" 9 | "github.com/gamemann/web-desktop-app-launcher/web" 10 | ) 11 | 12 | const HELPMENU = ` 13 | Usage: wdal-web --cfgpath --list --version --help\n\n 14 | \t--cfgpath => Path to config file (default /etc/wdal/conf.json).\n 15 | \t--list -l => Lists config options.\n 16 | \t--version -v => Prints current version.\n 17 | \t--help => Prints help menu.\n 18 | ` 19 | 20 | const VERSION = "1.0.0" 21 | 22 | func main() { 23 | // Parse command line arguments. 24 | var list bool 25 | var version bool 26 | var help bool 27 | 28 | flag.BoolVar(&list, "l", false, "List config option.") 29 | flag.BoolVar(&list, "list", false, "List config option.") 30 | 31 | flag.BoolVar(&version, "v", false, "Prints version.") 32 | flag.BoolVar(&version, "version", false, "Prints version.") 33 | 34 | flag.BoolVar(&help, "h", false, "Prints help menu.") 35 | flag.BoolVar(&help, "help", false, "Prints help menu.") 36 | 37 | // Check for version or help flags. 38 | if version { 39 | fmt.Println(VERSION) 40 | 41 | os.Exit(0) 42 | } 43 | 44 | if help { 45 | fmt.Print(HELPMENU) 46 | 47 | os.Exit(0) 48 | } 49 | 50 | var cfgPath string 51 | 52 | flag.StringVar(&cfgPath, "cfgpath", "/etc/wdal/conf.json", "The path to the config file.") 53 | flag.StringVar(&cfgPath, "c", "/etc/wdal/conf.json", "The path to the config file.") 54 | 55 | flag.Parse() 56 | 57 | // Load config. 58 | var cfg config.Config 59 | 60 | // Set defaults. 61 | cfg.SetDefaults() 62 | 63 | err := cfg.LoadFromFs(cfgPath) 64 | 65 | if err != nil { 66 | fmt.Println("Failed to load config file.") 67 | fmt.Println(err) 68 | 69 | os.Exit(1) 70 | } 71 | 72 | fmt.Printf("Starting web server on %s:%d...\n", cfg.Web.Host, cfg.Web.Port) 73 | 74 | // Setup and load web server. 75 | err = web.SetupServer(&cfg) 76 | 77 | if err != nil { 78 | fmt.Println("Failed to setup and load web server.") 79 | fmt.Println(err) 80 | 81 | os.Exit(1) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/site/web/handler.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "html/template" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | 13 | "github.com/gamemann/web-desktop-app-launcher/config" 14 | ) 15 | 16 | type AppData struct { 17 | Index int `json:"index"` 18 | Type int `json:"type"` 19 | } 20 | 21 | func RootHandler(w http.ResponseWriter, r *http.Request, cfg *config.Config) { 22 | tmpl, err := template.ParseFiles("templates/index.html") 23 | 24 | if err != nil { 25 | http.Error(w, "Error parsing template.", http.StatusInternalServerError) 26 | 27 | fmt.Println(err) 28 | 29 | return 30 | } 31 | 32 | err = tmpl.Execute(w, cfg.Apps) 33 | 34 | if err != nil { 35 | http.Error(w, "Error executing template.", http.StatusInternalServerError) 36 | 37 | fmt.Println(err) 38 | } 39 | } 40 | 41 | func BackendHandler(w http.ResponseWriter, r *http.Request, cfg *config.Config) { 42 | // Get type and application. 43 | if r.Method != "POST" { 44 | http.Error(w, "Wrong method.", http.StatusMethodNotAllowed) 45 | 46 | return 47 | } 48 | 49 | var appData AppData 50 | 51 | err := json.NewDecoder(r.Body).Decode(&appData) 52 | 53 | if err != nil { 54 | http.Error(w, "Error decoding JSON data.", http.StatusInternalServerError) 55 | 56 | return 57 | } 58 | 59 | // Get app. 60 | var app config.App 61 | found := false 62 | 63 | for k, v := range cfg.Apps { 64 | if k == appData.Index { 65 | app = v 66 | found = true 67 | 68 | break 69 | } 70 | } 71 | 72 | if !found { 73 | http.Error(w, "App not found at index.", http.StatusInternalServerError) 74 | 75 | return 76 | } 77 | 78 | toExec := app.Start 79 | 80 | if appData.Type == 1 { 81 | toExec = app.Stop 82 | } 83 | 84 | // We'll want to make sure we handle spaces properly. 85 | cmdSplit := strings.Fields(toExec) 86 | 87 | // Run command. 88 | cmd := exec.Command(cmdSplit[0], cmdSplit[1:]...) 89 | 90 | // Get current environment. 91 | env := os.Environ() 92 | 93 | // Add global environmental variables. 94 | for k, v := range cfg.Web.Env { 95 | env = append(env, fmt.Sprintf("%s=%s", k, v)) 96 | } 97 | 98 | // Add appplication-specific environmental variables. 99 | for k, v := range app.Env { 100 | env = append(env, fmt.Sprintf("%s=%s", k, v)) 101 | } 102 | 103 | cmd.Env = env 104 | 105 | // We need to get pipes now for logging. 106 | outPipe, _ := cmd.StdoutPipe() 107 | errPipe, _ := cmd.StderrPipe() 108 | 109 | err = cmd.Start() 110 | 111 | if err != nil { 112 | fmt.Println(err) 113 | 114 | return 115 | } 116 | 117 | fmt.Printf("Executed command: '%s'.\n", toExec) 118 | 119 | if cmd.Process == nil { 120 | fmt.Println("Process doesn't exist.") 121 | 122 | return 123 | } 124 | 125 | // Handle logging. 126 | if cfg.Web.LogToFile { 127 | go func() { 128 | fName := fmt.Sprintf("%s/apps/%d.log", cfg.Web.LogDirectory, cmd.Process.Pid) 129 | 130 | logFile, err := os.Create(fName) 131 | 132 | if err != nil { 133 | fmt.Printf("Failed to create log for process with ID '%d' (%s)", cmd.Process.Pid, fName) 134 | fmt.Println(err) 135 | 136 | return 137 | } 138 | 139 | outWriter := bufio.NewWriter(logFile) 140 | errWriter := bufio.NewWriter(logFile) 141 | 142 | // Handle stdout writes. 143 | go func() { 144 | scanner := bufio.NewScanner(outPipe) 145 | 146 | for scanner.Scan() { 147 | line := scanner.Text() 148 | outWriter.WriteString(line + "\n") 149 | outWriter.Flush() 150 | } 151 | }() 152 | 153 | // Handle stderr writes. 154 | go func() { 155 | scanner := bufio.NewScanner(errPipe) 156 | 157 | for scanner.Scan() { 158 | line := scanner.Text() 159 | errWriter.WriteString(line + "\n") 160 | errWriter.Flush() 161 | } 162 | }() 163 | }() 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/site/web/routes.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gamemann/web-desktop-app-launcher/config" 7 | ) 8 | 9 | func SetupRoutes(cfg *config.Config) { 10 | // Serve static mages. 11 | fs := http.FileServer(http.Dir("images")) 12 | http.Handle("/images/", http.StripPrefix("/images/", fs)) 13 | 14 | // Serve JavaScript. 15 | fs = http.FileServer(http.Dir("js")) 16 | http.Handle("/js/", http.StripPrefix("/js/", fs)) 17 | 18 | // Serve CSS. 19 | fs = http.FileServer(http.Dir("css")) 20 | http.Handle("/css/", http.StripPrefix("/css/", fs)) 21 | 22 | // Setup root handler. 23 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 24 | RootHandler(w, r, cfg) 25 | }) 26 | 27 | // Setup back-end handler. 28 | http.HandleFunc("/backend/submit", func(w http.ResponseWriter, r *http.Request) { 29 | BackendHandler(w, r, cfg) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/site/web/server.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gamemann/web-desktop-app-launcher/config" 8 | ) 9 | 10 | func SetupServer(cfg *config.Config) error { 11 | var err error 12 | 13 | // Parse host and port as address. 14 | addr := fmt.Sprintf("%s:%d", cfg.Web.Host, cfg.Web.Port) 15 | 16 | // Setup routes and handlers. 17 | SetupRoutes(cfg) 18 | 19 | err = http.ListenAndServe(addr, nil) 20 | 21 | return err 22 | } 23 | -------------------------------------------------------------------------------- /systemd/wdal-app.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WDAL Desktop 3 | After=network-online.target 4 | Requires=network-online.target 5 | 6 | [Service] 7 | User=myuser 8 | Environment="DISPLAY=:0" 9 | Environment="XAUTHORITY=$HOME/.Xauthority" 10 | WorkingDirectory=/var/web-desktop-app-launcher 11 | ExecStart=/usr/bin/wdal-app 12 | Restart=always 13 | 14 | [Install] 15 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /systemd/wdal-web.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WDAL Web 3 | After=network-online.target 4 | Requires=network-online.target 5 | 6 | [Service] 7 | User=myuser 8 | WorkingDirectory=/var/web-desktop-app-launcher 9 | ExecStart=/usr/bin/wdal-web 10 | Restart=always 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Web Desktop Application Launcher 8 | 9 | 10 | 11 | 12 | 13 |

Web Desktop Application Launcher

14 |
15 | {{range $index, $app := .}} 16 |
17 | {{if $app.Banner}} 18 | 19 |
20 | {{end}} 21 |
22 |
23 |

{{$app.Name}}

24 |
25 |
26 |
27 | {{if $app.Start}} 28 | 29 | {{end}} 30 | 31 | {{if $app.Stop}} 32 | 33 | {{end}} 34 |
35 |
36 |
37 | {{end}} 38 |
39 | 40 | --------------------------------------------------------------------------------