├── config ├── flex-launcher.rc ├── flex-launcher.ico ├── launcher.desktop.in ├── flex-launcher.manifest.in ├── PKGBUILD.in ├── config.ini.in ├── config_settings.cmake └── launcher_config.h.in ├── .gitignore ├── assets ├── icons │ ├── kodi.png │ ├── plex.png │ ├── sleep.png │ ├── steam.png │ ├── system.png │ ├── restart.png │ └── retroarch.png └── fonts │ ├── FreeSans.ttf │ ├── DejaVuSans.ttf │ ├── Inter-Regular.ttf │ ├── NotoSans-Regular.ttf │ ├── OpenSans-Regular.ttf │ ├── Roboto-Regular.ttf │ └── SourceSansPro-Regular.ttf ├── docs ├── flex-launcher.png ├── assets │ ├── icons │ │ ├── favicon.png │ │ ├── windows.svg │ │ ├── source.svg │ │ └── raspberry_pi.svg │ └── screenshots │ │ ├── screenshot1.png │ │ └── screenshot2.png ├── _data │ └── menu.yml ├── _includes │ └── head-custom-google-analytics.html ├── _config.yml ├── Gemfile ├── _layouts │ └── default.html ├── download.md ├── compilation.md ├── index.md ├── setup_windows.md ├── flex-launcher.svg ├── setup.md ├── _sass │ └── jekyll-theme-slate.scss └── setup_linux.md ├── src ├── platform │ ├── slideshow.h │ ├── CMakeLists.txt │ ├── unix.h │ ├── keycode_convert.h │ ├── platform.h │ ├── unix.c │ └── win32.c ├── clock.h ├── CMakeLists.txt ├── debug.h ├── util.h ├── image.h ├── launcher.h ├── clock.c ├── debug.c └── image.c ├── vcpkg.json ├── UNLICENSE ├── CHANGELOG ├── .github └── workflows │ └── build.yml ├── README.md └── CMakeLists.txt /config/flex-launcher.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON "flex-launcher.ico" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/Gemfile.lock 2 | docs/_site 3 | docs/.jekyll-cache 4 | -------------------------------------------------------------------------------- /assets/icons/kodi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/icons/kodi.png -------------------------------------------------------------------------------- /assets/icons/plex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/icons/plex.png -------------------------------------------------------------------------------- /assets/icons/sleep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/icons/sleep.png -------------------------------------------------------------------------------- /assets/icons/steam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/icons/steam.png -------------------------------------------------------------------------------- /assets/icons/system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/icons/system.png -------------------------------------------------------------------------------- /docs/flex-launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/docs/flex-launcher.png -------------------------------------------------------------------------------- /assets/fonts/FreeSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/fonts/FreeSans.ttf -------------------------------------------------------------------------------- /assets/icons/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/icons/restart.png -------------------------------------------------------------------------------- /config/flex-launcher.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/config/flex-launcher.ico -------------------------------------------------------------------------------- /assets/fonts/DejaVuSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/fonts/DejaVuSans.ttf -------------------------------------------------------------------------------- /assets/icons/retroarch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/icons/retroarch.png -------------------------------------------------------------------------------- /assets/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /docs/assets/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/docs/assets/icons/favicon.png -------------------------------------------------------------------------------- /assets/fonts/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/fonts/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/assets/fonts/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /docs/assets/screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/docs/assets/screenshots/screenshot1.png -------------------------------------------------------------------------------- /docs/assets/screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/flex-launcher/HEAD/docs/assets/screenshots/screenshot2.png -------------------------------------------------------------------------------- /src/platform/slideshow.h: -------------------------------------------------------------------------------- 1 | static const char *extensions[] = { 2 | ".jpg", 3 | ".jpeg", 4 | ".png", 5 | ".webp" 6 | }; 7 | #define NUM_IMAGE_EXTENSIONS sizeof(extensions) / sizeof(extensions[0]) -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | "sdl2", 4 | { 5 | "name": "sdl2-image", 6 | "features": ["libjpeg-turbo", "libwebp"] 7 | }, 8 | "sdl2-ttf", 9 | "inih", 10 | "getopt" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /docs/_data/menu.yml: -------------------------------------------------------------------------------- 1 | - title: Home 2 | link: /flex-launcher 3 | - title: Download 4 | link: /flex-launcher/download 5 | - title: Configuration 6 | link: /flex-launcher/configuration 7 | - title: Setup Guide 8 | link: /flex-launcher/setup 9 | -------------------------------------------------------------------------------- /config/launcher.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.5 3 | Type=Application 4 | Name=@CMAKE_PROJECT_NAME@ 5 | GenericName=Application Launcher 6 | Comment=@CMAKE_PROJECT_DESCRIPTION@ 7 | TryExec=@EXECUTABLE_TITLE@ 8 | Exec=@EXECUTABLE_TITLE@ 9 | Icon=@EXECUTABLE_TITLE@ 10 | Categories=Video;AudioVideo; 11 | -------------------------------------------------------------------------------- /docs/_includes/head-custom-google-analytics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /src/platform/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Build platform-specific library 2 | if (UNIX) 3 | add_library(platform unix.c unix.h platform.h) 4 | target_link_libraries(platform PkgConfig::SDL2) 5 | endif () 6 | if (WIN32) 7 | add_library(platform win32.c platform.h) 8 | target_link_libraries(platform 9 | $ 10 | $,SDL2::SDL2,SDL2::SDL2-static> 11 | ) 12 | endif () 13 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Flex Launcher 2 | description: A customizable HTPC application launcher and front-end interface 3 | url: "" # the base hostname & protocol for your site, e.g. http://example.com 4 | github_username: complexlogic 5 | repository: complexlogic/flex-launcher 6 | launcher_version: 2.2 7 | 8 | # Build settings 9 | remote_theme: pages-themes/slate@v0.2.0 10 | plugins: 11 | - jekyll-feed 12 | - jekyll-remote-theme 13 | - jekyll-seo-tag 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/assets/icons/windows.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/platform/unix.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define CMD_SHUTDOWN "systemctl poweroff" 4 | #define CMD_RESTART "systemctl reboot" 5 | #define CMD_SLEEP "systemctl suspend" 6 | #define EXT_DESKTOP ".desktop" 7 | #define DELIMITER_ACTION ";" 8 | #define DESKTOP_SECTION_HEADER "Desktop Entry" 9 | #define DESKTOP_SECTION_HEADER_ACTION "Desktop Action %s" 10 | #define KEY_EXEC "Exec" 11 | #define MAX_INI_SECTION 100 12 | 13 | typedef struct { 14 | char section[MAX_INI_SECTION + 1]; 15 | char *exec; 16 | } Desktop; 17 | -------------------------------------------------------------------------------- /config/flex-launcher.manifest.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UTF-8 7 | permonitor 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/assets/icons/source.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Svg Vector Icons : http://www.onlinewebfonts.com/icon 6 | 7 | -------------------------------------------------------------------------------- /src/platform/keycode_convert.h: -------------------------------------------------------------------------------- 1 | typedef struct { 2 | SDL_Keycode sdl; 3 | UINT win; 4 | } keycode_conversion; 5 | 6 | static const keycode_conversion table[] = { 7 | {SDLK_F1, VK_F1}, 8 | {SDLK_F2, VK_F2}, 9 | {SDLK_F3, VK_F3}, 10 | {SDLK_F4, VK_F4}, 11 | {SDLK_F5, VK_F5}, 12 | {SDLK_F6, VK_F6}, 13 | {SDLK_F7, VK_F7}, 14 | {SDLK_F8, VK_F8}, 15 | {SDLK_F9, VK_F9}, 16 | {SDLK_F10, VK_F10}, 17 | {SDLK_F11, VK_F11}, 18 | {SDLK_F13, VK_F13}, 19 | {SDLK_F14, VK_F14}, 20 | {SDLK_F15, VK_F15}, 21 | {SDLK_F16, VK_F16}, 22 | {SDLK_F17, VK_F17}, 23 | {SDLK_F18, VK_F18}, 24 | {SDLK_F19, VK_F19}, 25 | {SDLK_F20, VK_F20}, 26 | {SDLK_F21, VK_F21}, 27 | {SDLK_F22, VK_F22}, 28 | {SDLK_F23, VK_F23}, 29 | {SDLK_F24, VK_F24} 30 | }; 31 | -------------------------------------------------------------------------------- /config/PKGBUILD.in: -------------------------------------------------------------------------------- 1 | pkgname=@EXECUTABLE_TITLE@ 2 | pkgver=@CMAKE_PROJECT_VERSION@ 3 | pkgrel=1 4 | #epoch= 5 | pkgdesc="@CMAKE_PROJECT_DESCRIPTION@" 6 | arch=('x86_64') 7 | url="@CMAKE_PROJECT_HOMEPAGE_URL@" 8 | license=('Unlicense') 9 | #groups=() 10 | depends=('sdl2' 'sdl2_image' 'sdl2_ttf' 'libinih') 11 | makedepends=('cmake') 12 | #checkdepends=() 13 | #optdepends=() 14 | #provides=() 15 | conflicts=() 16 | #replaces=() 17 | #backup=() 18 | #options=() 19 | #install= 20 | #changelog= 21 | source=('file://@EXECUTABLE_TITLE@.tar.gz') 22 | #noextract=() 23 | md5sums=('SKIP') 24 | #validpgpkeys=() 25 | 26 | build() { 27 | builddir=$srcdir/@EXECUTABLE_TITLE@/build 28 | mkdir $builddir && cd $builddir 29 | cmake -DCMAKE_INSTALL_PREFIX=/usr .. 30 | make 31 | } 32 | 33 | package() { 34 | cd $srcdir/@EXECUTABLE_TITLE@/build 35 | make DESTDIR="$pkgdir/" install 36 | } 37 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | #gem "jekyll", "~> 4.2.2" 4 | gem 'jekyll-seo-tag' 5 | gem "jekyll-remote-theme" 6 | gem "github-pages", group: :jekyll_plugins 7 | 8 | # If you have any plugins, put them here! 9 | group :jekyll_plugins do 10 | gem "jekyll-feed", "~> 0.12" 11 | end 12 | 13 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 14 | # and associated library. 15 | platforms :mingw, :x64_mingw, :mswin, :jruby do 16 | gem "tzinfo", "~> 1.2" 17 | gem "tzinfo-data" 18 | end 19 | 20 | # Performance-booster for watching directories on Windows 21 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 22 | 23 | # Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem 24 | # do not have a Java counterpart. 25 | gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] 26 | 27 | gem "webrick", "~> 1.7" 28 | -------------------------------------------------------------------------------- /src/clock.h: -------------------------------------------------------------------------------- 1 | #define MAX_CLOCK_CHARS 20 2 | #define CLOCK_SPACING_FACTOR 0.5F 3 | 4 | // Clock 5 | typedef struct { 6 | SDL_Surface *time_surface; 7 | SDL_Surface *date_surface; 8 | SDL_Texture *time_texture; 9 | SDL_Texture *date_texture; 10 | SDL_Rect time_rect; 11 | SDL_Rect date_rect; 12 | TextInfo text_info; 13 | time_t current_time; 14 | struct tm *time_info; 15 | int x_offset_time; 16 | int x_offset_date; 17 | int y_offset; 18 | int y_advance; 19 | char time_string[MAX_CLOCK_CHARS + 1]; 20 | char date_string[MAX_CLOCK_CHARS + 1]; 21 | TimeFormat time_format; 22 | DateFormat date_format; 23 | bool render_time; 24 | bool render_date; 25 | } Clock; 26 | 27 | void init_clock(Clock *clk); 28 | void get_time(Clock *clk); 29 | void render_clock(Clock *clk); 30 | int render_clock_async(void *data); 31 | TimeFormat get_time_format(const char *region); 32 | DateFormat get_date_format(const char *region); 33 | -------------------------------------------------------------------------------- /src/platform/platform.h: -------------------------------------------------------------------------------- 1 | #ifdef _WIN32 2 | #define FILE_MODE_WRITE "wt" 3 | #else 4 | #define FILE_MODE_WRITE "w" 5 | #endif 6 | 7 | // Abstracted platform function prototypes 8 | bool file_exists(const char *path); 9 | bool directory_exists(const char *path); 10 | void get_region(char *buffer); 11 | void scan_slideshow_directory(Slideshow *slideshow, const char *directory); 12 | bool start_process(char *cmd, bool application); 13 | void scmd_shutdown(void); 14 | void scmd_restart(void); 15 | void scmd_sleep(void); 16 | 17 | // Linux-specific function prototypes 18 | #ifdef __unix__ 19 | void make_directory(const char *directory); 20 | void print_usage(void); 21 | #endif 22 | 23 | // Windows-specific function prototypes 24 | #ifdef _WIN32 25 | bool has_exit_hotkey(void); 26 | void set_exit_hotkey(SDL_Keycode keycode); 27 | void register_exit_hotkey(void); 28 | void check_exit_hotkey(SDL_SysWMmsg *msg); 29 | void set_foreground_window(void); 30 | void set_window_transparent(void); 31 | void hide_cursor(Entry* entry); 32 | #endif -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #Build main launcher executable file 2 | set(SOURCES 3 | launcher.c 4 | launcher.h 5 | util.c 6 | util.h 7 | image.c 8 | image.h 9 | debug.c 10 | debug.h 11 | clock.c 12 | clock.h 13 | ) 14 | if (UNIX) 15 | add_executable(${EXECUTABLE_TITLE} ${SOURCES}) 16 | endif () 17 | if (WIN32) 18 | set(APP_ICON_RESOURCE_WINDOWS "${PROJECT_SOURCE_DIR}/config/${EXECUTABLE_TITLE}.rc") 19 | set(MANIFEST_FILE "${PROJECT_BINARY_DIR}/${EXECUTABLE_TITLE}.manifest") 20 | add_executable(${EXECUTABLE_TITLE} WIN32 ${SOURCES} ${MANIFEST_FILE} ${APP_ICON_RESOURCE_WINDOWS}) 21 | set_property(TARGET ${EXECUTABLE_TITLE} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${PROJECT_BINARY_DIR}") 22 | endif() 23 | 24 | # Build libraries 25 | add_subdirectory("platform") 26 | 27 | # Link libraries together 28 | target_link_libraries(${EXECUTABLE_TITLE} platform) 29 | if (UNIX) 30 | target_link_libraries(${EXECUTABLE_TITLE} PkgConfig::SDL2 PkgConfig::SDL2_IMAGE PkgConfig::SDL2_TTF PkgConfig::INIH m) 31 | else () 32 | target_link_libraries(${EXECUTABLE_TITLE} 33 | $ 34 | $,SDL2::SDL2,SDL2::SDL2-static> 35 | $,SDL2_image::SDL2_image,SDL2_image::SDL2_image-static> 36 | $,SDL2_ttf::SDL2_ttf,SDL2_ttf::SDL2_ttf-static> 37 | ${INIH} 38 | ${GETOPT} 39 | PowrProf 40 | ) 41 | target_include_directories(${EXECUTABLE_TITLE} PUBLIC ${INIH_INCLUDE_DIR} ${GETOPT_INCLUDE_DIR}) 42 | endif () 43 | target_include_directories(${EXECUTABLE_TITLE} SYSTEM PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/external") 44 | -------------------------------------------------------------------------------- /src/debug.h: -------------------------------------------------------------------------------- 1 | typedef enum { 2 | LOGLEVEL_DEBUG = 0, 3 | LOGLEVEL_ERROR, 4 | LOGLEVEL_FATAL 5 | } LogLevel; 6 | 7 | void output_log(LogLevel log_level, const char *format, ...); 8 | void print_compiler_info(FILE *stream); 9 | void debug_video(SDL_Renderer *renderer, SDL_DisplayMode *display_mode); 10 | void debug_settings(void); 11 | void debug_gamepad(GamepadControl *gamepad_controls); 12 | void debug_hotkeys(Hotkey *hotkeys); 13 | void debug_menu_entries(Menu *first_menu, size_t num_menus); 14 | void debug_slideshow(Slideshow *slideshow); 15 | void debug_button_positions(Entry *entry, Menu *current_menu, Geometry *geo); 16 | 17 | #ifdef _WIN32 18 | #define endline "\r\n" 19 | #else 20 | #define endline "\n" 21 | #endif 22 | 23 | #define log_debug(msg, ...) output_log(LOGLEVEL_DEBUG, msg endline, ##__VA_ARGS__) 24 | #define log_error(msg, ...) output_log(LOGLEVEL_ERROR, "" msg endline, ##__VA_ARGS__) 25 | #define log_fatal(msg, ...) output_log(LOGLEVEL_FATAL, "" msg endline, ##__VA_ARGS__) 26 | 27 | #define DEBUG_COLOR(setting_name, color) log_debug("%-25s #%.2X%.2X%.2X%.2X", setting_name ":", color.r, color.g, color.b, color.a) 28 | #define DEBUG_MODE(setting_name, type, value) log_debug("%-25s %s", setting_name ":", get_mode_setting(type, value)) 29 | #define DEBUG_BOOL(setting_name, value) log_debug("%-25s %s", setting_name ":", value ? "true" : "false"); 30 | #define DEBUG_STR(setting_name, value) log_debug("%-25s %s", setting_name ":", value != NULL ? value : "(null)") 31 | #define DEBUG_INT(setting_name, value) log_debug("%-25s %i", setting_name ":", value) 32 | #define DEBUG_FLOAT(setting_name, value) log_debug("%-25s %.2f", setting_name ":", value) 33 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #define MAX_LOG_LINE_BYTES 501 2 | #define MAX_PATH_CHARS 1001 //250 wide characters 3 | #define INVALID_PERCENT_VALUE -1 4 | 5 | #ifdef _WIN32 6 | #define PATH_SEPARATOR "\\" 7 | #else 8 | #define PATH_SEPARATOR "/" 9 | #endif 10 | 11 | #define UNUSED(x) (void)(x) 12 | #define SELECTED_SUFFIX "_selected" 13 | #define LEN(x) ((sizeof(x)/sizeof(x[0])) - sizeof(x[0])) 14 | #define MATCH(x, y) !strcmp(x, y) 15 | 16 | #define DIV_ROUND_UP(a, b) ((a + (b - 1)) / b) 17 | #define MIN(a, b) (((a) < (b)) ? (a) : (b)) 18 | 19 | struct gamepad_info { 20 | const char *label; 21 | int type; 22 | int index; 23 | }; 24 | 25 | int config_handler(void *user, const char *section, const char *name, const char *value); 26 | int convert_percent(const char *string, int max_value); 27 | const char *get_mode_setting(int type, int value); 28 | int utf8_length(const char *string); 29 | unsigned int calculate_width(int buttons, int icon_spacing, int icon_size, int highlight_padding); 30 | bool hex_to_color(const char *string, SDL_Color *color); 31 | bool convert_bool(const char *string, bool *setting); 32 | bool is_percent(const char *string); 33 | char *selected_path(const char *path); 34 | char *join_paths(char *buffer, size_t bytes, int num_paths, ...); 35 | char *find_file(const char *file, int num_prefixes, const char **prefixes); 36 | void handle_arguments(int argc, char *argv[], char **config_file_path); 37 | void copy_string(char* dest, const char* string, size_t size); 38 | void utf8_truncate(char *string, int width, int max_width); 39 | void convert_percent_to_int(char *string, int *result, int max_value); 40 | void add_hotkey(const char *keycode, const char *cmd); 41 | void random_array(int *array, int array_size); 42 | void clean_path(char *path); 43 | void validate_settings(Geometry *geo); 44 | void parse_config_file(const char *config_file_path); 45 | void read_file(const char *path, char **buffer); 46 | void sprintf_alloc(char **buffer, const char *format, ...); 47 | Uint16 get_unicode_code_point(const char *p, int *bytes); 48 | Menu *get_menu(const char *menu_name); 49 | Entry *advance_entries(Entry *entry, int spaces, Direction direction); -------------------------------------------------------------------------------- /src/image.h: -------------------------------------------------------------------------------- 1 | // Dynamic SVG generation for highlight 2 | #define HIGHLIGHT_OUTLINE_FORMAT " stroke-width=\"%i\" stroke=\"#%02X%02X%02X\" stroke-opacity=\"%.2f\"" 3 | #define HIGHLIGHT_FORMAT "" 4 | #define SCROLL_INDICATOR_FORMAT " " 5 | #define SHADOW_OPACITY_MULTIPLIER 0.75F 6 | 7 | // Macro functions 8 | #define format_highlight_outline(buffer, outline_size, outline_color, outline_opacity) sprintf_alloc(buffer, HIGHLIGHT_OUTLINE_FORMAT, outline_size, outline_color.r, outline_color.g, outline_color.b, outline_opacity) 9 | #define format_highlight(buffer, width, height, corner_radius, fill_color, fill_opacity, outline_buffer) sprintf_alloc(buffer, HIGHLIGHT_FORMAT, width, height, width, height, corner_radius, fill_color.r, fill_color.g, fill_color.b, fill_opacity, outline_buffer) 10 | #define format_scroll_indicator(buffer, fill_color, outline_size, outline_color, opacity) sprintf_alloc(buffer, SCROLL_INDICATOR_FORMAT, fill_color.r, fill_color.g, fill_color.b, opacity, outline_color.r, outline_color.g, outline_color.b, outline_size, opacity) 11 | #define calculate_shadow_alpha(x) x.shadow_color->a = (Uint8) (SHADOW_OPACITY_MULTIPLIER * (float) x.color->a) 12 | 13 | typedef struct { 14 | TTF_Font *font; 15 | int font_size; 16 | char **font_path; 17 | SDL_Color *color; 18 | bool shadow; 19 | SDL_Color *shadow_color; 20 | int max_width; 21 | ModeOversize oversize_mode; 22 | } TextInfo; 23 | 24 | int init_svg(void); 25 | int load_font(TextInfo *info, const char *default_font); 26 | void quit_svg(void); 27 | void render_scroll_indicators(Scroll *scroll, int height, Geometry *geo); 28 | SDL_Surface *load_next_slideshow_background(Slideshow *slideshow, bool transition); 29 | int load_next_slideshow_background_async(void *data); 30 | SDL_Texture *load_texture(SDL_Surface *surface); 31 | SDL_Texture *load_texture_from_file(const char *path); 32 | SDL_Texture *rasterize_svg(char *buffer, int w, int h, SDL_Rect *rect); 33 | SDL_Texture *rasterize_svg_from_file(const char *path, int w, int h, SDL_Rect *rect); 34 | SDL_Texture *render_highlight(int width, int height, SDL_Rect *rect); 35 | SDL_Surface *render_text(const char *text, TextInfo *info, SDL_Rect *rect, int *text_height); 36 | SDL_Texture *render_text_texture(const char *text, TextInfo *info, SDL_Rect *rect, int *text_height); 37 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% seo %} 12 | {% include head-custom.html %} 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | {% if site.github.is_project_page %} 21 | View on GitHub 22 | {% endif %} 23 | 24 |

{{ site.title | default: site.github.repository_name }}

25 |

{{ site.description | default: site.github.project_tagline }}

26 | 40 | {% if site.show_downloads %} 41 |
42 | Download this project as a .zip file 43 | Download this project as a tar.gz file 44 |
45 | {% endif %} 46 |
47 |
48 | 49 | 50 |
51 |
52 | {{ content }} 53 |
54 |
55 | 56 | 57 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v2.2 (2024-11-24) 2 | - Make application timeout configurable 3 | - Add WrapEntries feature 4 | - Various fixes, optimizations, and code quality improvements 5 | - Windows: Improve logic relating to handling of launched applications (focus issues, etc.) 6 | - Linux: Don't set SDL_VIDEODRIVER environment variable 7 | 8 | v2.1 (2023-1-7) 9 | - Added OnLaunch 'Quit' mode 10 | 11 | v2.0 (2022-12-19) 12 | - Change application tracking to window focus-based instead of process based 13 | - Remove OnLaunch "Hide" mode 14 | - Reorganize configuration file into smaller sections 15 | - Make VSync optional 16 | - Add support for transparent backgrounds 17 | - Allow control from multiple gamepads 18 | - Allow unlimited number of slideshow backgrounds 19 | - Make highlight optional 20 | - Make application titles optional 21 | - Specify color and key HEX codes with string beginning with # character 22 | - Rename ButtonCenterline setting to VCenter 23 | - Create dialog box on fatal errors 24 | - Linux: Prefer native Wayland over XWayland 25 | 26 | v1.8 (2022-10-7) 27 | - Added inhibit OS screensaver feature 28 | - Minor bugfixes 29 | - Windows: Added "chrome_proxy.exe" to the list of supported browser processes 30 | 31 | v1.7 (2022-5-30) 32 | - Added selected icons feature 33 | - Added StartupCmd and QuitCmd features 34 | - New getopt-based command line parsing 35 | 36 | v1.6.1 (2022-4-19) 37 | - Windows: Disable system DPI scaling 38 | 39 | v1.6 (2022-4-2) 40 | - Added background overlay feature 41 | - Added text shadows feature 42 | - Added outline configuration option for highlight 43 | - Added scroll indicator outline configuration option 44 | - Fixed bug where an unmapped controller won't automatically connect upon startup 45 | 46 | v1.5 (2022-2-28) 47 | - Added :fork special command 48 | - Enabled VSync 49 | - Added MouseSelect setting for gyroscopic mouse support 50 | - Windows: Added exit hotkey feature 51 | - Windows: The :sleep special command now properly sleeps the PC instead of hibernating it 52 | 53 | v1.4 (2022-2-5) 54 | - Added clock widget to show the current time and date 55 | - Windows: Full Unicode support implemented 56 | - Linux: Changed .desktop file action delimiting character from pipe | to semicolon ; 57 | 58 | v1.3 (2022-1-13) 59 | - Added hotkeys feature 60 | - Removed EscQuit setting. Esc to quit functionality is now implemented as a configurable hotkey 61 | - Changed slideshow time units from milliseconds to seconds 62 | - Windows: Applications now launch via WIN32 API instead of the standard C runtime 63 | - Windows: Implemented OnLaunch setting that was previously Linux exclusive 64 | - Windows: Fixed web browser non-blocking issue 65 | - Linux: Improved Wayland support 66 | 67 | v1.2 (2022-1-2) 68 | - Added slideshow background mode 69 | - Added screensaver feature 70 | - Windows: Improved Unicode support 71 | 72 | v1.1 (2021-12-24) 73 | - Added logging interface 74 | - Linux: Added OnLaunch setting 75 | - Changed all RGBA color settings to RGB for simplicity. Transparency is now set solely in the separate opacity setting 76 | - Refactored automatic file searching logic 77 | 78 | v1.0.1 (2021-12-4) 79 | - Added support for WebP images 80 | - Improved handling of paths enclosed in quotes 81 | - Windows: Added application icon to executable 82 | 83 | v1.0 (2021-11-28) 84 | - Initial release 85 | -------------------------------------------------------------------------------- /docs/download.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Download 4 | --- 5 | 6 | ## License 7 | Flex Launcher is released under the [Unlicense](https://unlicense.org/), i.e. public domain. 8 | 9 | ## Download 10 | Binary packages are available for Windows 64 bit, Linux x86-64, and Raspberry Pi. The latest stable release is version {{site.launcher_version}}. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 33 | 34 | 35 | 36 | 41 | 42 | 43 |
18 |

Windows 10/11:

19 | 21 |
26 |

APT-based (Debian, Ubuntu):

27 | 29 |

Pacman-based (Arch, Manjaro):

30 | 32 |
37 |

Raspberry Pi (64 bit only)

38 | 40 |
44 | 45 | 46 | ## Source Code 47 | If your platform is not listed above, you're interested in development, or you simply prefer compiling your own software, you can download source packages below. See the [compilation guide](compilation) for build instructions. 48 | 49 | 50 | 51 | 52 | 53 | 60 | 61 | 62 |
54 |

Source Packages:

55 | 59 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/compilation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Compilation Guide 4 | --- 5 | # Compilation Guide 6 | ## Table of Contents 7 | 1. [Overview](#overview) 8 | 2. [Linux](#linux) 9 | 3. [Windows](#windows) 10 | 11 | ## Overview 12 | Flex Launcher builds natively on Linux and Windows, and features a cross-platform CMake build system. The following external dependencies are required: 13 | - SDL ≥ 2.0.14 14 | - SDL_image ≥ 2.0.5 15 | - SDL_ttf ≥ 2.0.15 16 | 17 | ## Linux 18 | Flex Launcher on Linux builds with GCC. This guide assumes you already have the development tools Git, CMake, pkg-config, and GCC installed on your system. If not, consult your distro's documentation. 19 | 20 | First, install the dependencies. The steps to do so are dependent on your distro: 21 | 22 | #### APT-based Distributions (Debian, Ubuntu, Mint, Raspberry Pi OS etc.) 23 | ```bash 24 | sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libinih-dev 25 | ``` 26 | 27 | #### Pacman-based Distributions (Arch, Manjaro, etc.) 28 | ```bash 29 | sudo pacman -S sdl2 sdl2_image sdl2_ttf libinih 30 | ``` 31 | 32 | #### DNF-based Distributions (Fedora) 33 | ```bash 34 | sudo dnf install SDL2-devel SDL2_image-devel SDL2_ttf-devel inih-devel 35 | ``` 36 | 37 | ### Building 38 | Clone the master repo and create a build directory: 39 | ```bash 40 | git clone https://github.com/complexlogic/flex-launcher.git 41 | cd flex-launcher 42 | mkdir build && cd build 43 | ``` 44 | Generate the Makefile: 45 | ```bash 46 | cmake .. -DCMAKE_BUILD_TYPE=Release 47 | ``` 48 | If you're building on Raspberry Pi, it's recommended to pass `-DRPI=1` to cmake, which tweaks the default configuration to be more Pi-centric. 49 | 50 | Build and test the program: 51 | ```bash 52 | make 53 | ./flex-launcher 54 | ``` 55 | Optionally, install it into your system directories: 56 | ```bash 57 | sudo make install 58 | ``` 59 | By default, this will install the program and assets with a prefix of `/usr/local`. If you wish to use a different prefix, re-run the cmake generation step with `-DCMAKE_INSTALL_PREFIX=prefix`. 60 | 61 | ## Windows 62 | Flex Launcher on Windows builds with Visual Studio, and uses [vcpkg](https://vcpkg.io/en/index.html) to manage the dependencies. Before starting, make sure the following steps are completed: 63 | - Visual Studio is installed. The free Community Edition is available for download from Microsoft's website. The following tools and features for Visual Studio are required: 64 | - C++ core desktop features 65 | - Latest MSVC 66 | - Latest Windows SDK 67 | - C++ CMake tools 68 | - Git is installed and in your `Path` environment variable 69 | - CMake is installed and in your `Path` environment variable 70 | 71 | ### Building 72 | Clone the master repo and create a build directory: 73 | ``` 74 | git clone https://github.com/complexlogic/flex-launcher.git 75 | cd flex-launcher 76 | mkdir build 77 | cd build 78 | ``` 79 | Build the dependencies and generate the Visual Studio project files: 80 | ``` 81 | git clone https://github.com/microsoft/vcpkg 82 | cmake .. -DCMAKE_TOOLCHAIN_FILE=".\vcpkg\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET="x64-windows-static" 83 | ``` 84 | Build and test the program: 85 | ``` 86 | cmake --build . 87 | .\Debug\flex-launcher.exe 88 | ``` 89 | Optionally, generate a clean zipped install package which may then be extracted to a directory of your choosing: 90 | ``` 91 | cmake --build . --config Release --target package 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | Flex Launcher is a customizable application launcher and front end designed with a TV-friendly [10 foot user interface](https://en.wikipedia.org/wiki/10-foot_user_interface), intending to mimic the look and feel of a streaming box or game console. Flex Launcher allows you to launch applications on your HTPC or couch gaming PC entirely by use of a TV remote or a gamepad. No keyboard or mouse required! 6 | 7 | Flex Launcher is compatible with both Windows and Linux (including Raspberry Pi devices). 8 | 9 | ## Screenshots 10 |

11 |

12 | 13 | ## About 14 | Flex Launcher was initially released in late 2021. Its creation was motivated by the puzzling lack of good HTPC control options at the time. Flex Launcher is designed with the following principles in mind: 15 | - **Flexibility:** Indicated in the name *Flex* Launcher, the configuration should be highly flexible, with user having a great deal of control over the appearance and behavior of the program. 16 | - **Simplicity:** The interface should be simple, yet beautiful. 17 | - **Portability:** Many of the other existing solutions only support a single platform. Flex Launcher aims to be portable and cross-platform, running on ARM-based SBCs such as Raspberry Pi, to high-end Windows PCs, and everything in between. 18 | - **Ease of Navigation:** Flex Launcher should be completely navigable using only directional keys, enter, and back. The launcher should include built-in support for gamepad devices. 19 | 20 | Flex Launcher is completely free and open source. Accordingly, you are entitled to modify or redsitribute it as you wish. The source code is available on [GitHub](https://github.com/complexlogic/flex-launcher). 21 | 22 | ## Features 23 | The following is a list of features supported by Flex Launcher: 24 | 25 | #### Custom Backgrounds 26 | The background of the Flex Launcher is customizable. There are three supported modes for the background: a solid color, an image, or a slideshow of images. In the case of the slideshow, the user is able to specify how long to show the images, and how quickly to fade between them. 27 | 28 | #### Application Icons 29 | The user is able to pick the application icons, specify how large they should be, and how far apart they should be. The user is also able to control the vertical centering of the application icons. 30 | 31 | #### Submenus 32 | Flex Launcher supports submenus, which allows you to group your various applications in an organized fashion. For example, you can have separate menus for media and gaming applications. 33 | 34 | #### Selection Highlighting 35 | The user is able to change the size, color, opacity, and corner radius of the default highlight rectangle, as well as the border. 36 | 37 | Additionally, Flex Launcher also supports an icon-based selection mode where the user can specify a different icon that will display when a menu entry is selected. This allows the user to implement custom highlighting effects. 38 | 39 | #### Fonts 40 | The font, size, color, and opacity of all texts are customizable. Flex Launcher ships with a few royalty free fonts, but the user is able to substitute their own font instead. 41 | 42 | #### Hotkeys 43 | With the hotkeys feature, you can map custom commands to a button on your remote. This can be used to “speed dial” your favorite applications, add new controls, or exit a currently running application. 44 | 45 | #### System Controls 46 | There are built-in menu options to shut down, restart, or put your PC to sleep. 47 | 48 | #### Gamepad Controls 49 | Flex Launcher has full support for gamepad controls, allowing easy menu navigation. Enable the controls via the configuration file and connect your gamepad. The user can also add a custom mapping for a given gamepad. 50 | 51 | #### Screensaver 52 | Flex Launcher includes a screensaver mode that will dim the screen after it has been idle. The user is able to adjust how long the idle time should be, and how much to dim the screen. 53 | 54 | #### Clock 55 | There is an available clock widget that displays the current time and, optionally, the current date. The user is able to adjust the size, font, placement, and time/date format. 56 | 57 | ## Support 58 | If you believe you have found a bug in Flex Launcher, open an issue on the [GitHub issue tracker](https://github.com/complexlogic/flex-launcher/issues). For general technical support or feature requests, please use the [Discussions page](https://github.com/complexlogic/flex-launcher/discussions) instead. 59 | -------------------------------------------------------------------------------- /docs/setup_windows.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Windows Setup Guide 4 | --- 5 | # Windows Setup Guide 6 | ## Table of Contents 7 | 1. [Overview](#overview) 8 | 2. [Editing Your Config File](#editing-your-config-file) 9 | 3. [Autostarting](#autostarting) 10 | 4. [Autologin](#autologin) 11 | 5. [Disable Lock Screen](#disable-lock-screen) 12 | 6. [Kiosk Mode Setup](#kiosk-mode-setup) 13 | 14 | ## Overview 15 | This page contains tips for setting up Flex Launcher on Windows-based systems. These instructions are tailored for Windows 10, but should apply to Windows 11 as well. 16 | 17 | ## Editing Your Config File 18 | I recommend editing your configuration file with a development-oriented text editor such as [Notepad++](https://notepad-plus-plus.org/) instead of the default Windows Notepad program. These text editors have color syntax highlighting for the INI format which makes sections, keys, values, and comments visually distinct. 19 | 20 | Several settings require a path as a value. In Windows Explorer, hold shift on your keyboard, then right click on a file or folder and select "Copy as path" from the context menu to copy the file path. You can then paste the path into your configuration file so you don't have to type the path manually. 21 | 22 | ## Autostarting 23 | In a typical HTPC setup, Flex Launcher should be configured to autostart after login. Right click `flex-launcher.exe` and selct "Create shortcut" in the context menu. Use Windows key + R to bring up the run box, then type `shell:startup` and press enter. This will bring up a Windows Explorer window of your autostart folder. Move the shortcut created in the previous step into this folder, which will cause Flex Launcher to autostart. 24 | 25 | Make sure that no other graphical programs are set to autostart. The window creation order of autostarted programs is not easily predictable, so if there is another program that creates a window *after* Flex Launcher, it could cause Flex Launcher to lose the window focus. 26 | 27 | ## Autologin 28 | Autologin should be configured to prevent users from having to enter a PIN/password with a keyboard after boot. Use Windows key + R to bring up the run box, then type `netplwiz` and press enter. In the resulting window, uncheck the box that says "Users must enter a user name and password to use this computer", then click apply. In the dialog box that pops up, enter the user name that you want to sign in automatically, and the password for it. 29 | 30 | ## Disable Lock Screen 31 | If you plan to use the `:sleep` special command to put your HTPC to sleep, you will be greeted by the Windows lock screen by default after your HTPC wakes up. This requires you to enter your PIN/password with a keyboard, even if autologin was configured. The lock screen should be disabled so that you return back to Flex Launcher immediately after waking from sleep. 32 | 33 | Open the Settings app, then select Accounts, then select Sign-in options. Under the "Require sign-in" section, change the drop down menu value to "Never". 34 | 35 | ## Kiosk Mode Setup 36 | "Kiosk Mode" typically refers to a user interface which resembles an embedded-style device that performs only a single function (as opposed to a multitasking PC with a desktop interface). This section contains instructions to set up my interpretation of a "Kiosk Mode" interface for a Windows HTPC. Before starting, make sure you have [set up autologin](#autologin) and [disabled your lock screen](#disable-lock-screen) per the previous sections. 37 | 38 | In Windows Explorer, navigate to the folder that contains Flex Launcher. Hold shift on your keyboard, then right click on `flex-launcher.exe` and select "Copy as path" from the context menu. Use Windows key + R to bring up the run box, then type `gpedit.msc` and press enter. In the left pane under "User Configuration", select "Administrative Templates", then "System". In the right pane, double click "Custom User Interface". Change the radio button to "Enabled". Then, under "Interface file name", paste the path to `flex-launcher.exe` that was copied in the previous step. Press OK to confirm the changes. This will cause Flex Launcher to replace the Windows desktop as your default graphical user interface. 39 | 40 | Running Flex Launcher in this manner changes the working directory. The default config file that ships with Flex Launcher uses relative paths from the directory containing `flex-launcher.exe`, which will no longer be valid. Therefore, you should convert all relative paths in your config file to absolute paths, e.g. `.\assets\icons\kodi.png` should instead be `C:\path\to\assets\icons\kodi.png`, otherwise images will not load properly. 41 | 42 | Reboot your HTPC for the change to take effect. 43 | 44 | ### Restoring the Desktop 45 | It may be desirable to use the desktop interface occasionally, e.g. during maintenance or installation of new software. You can get your desktop back by running `explorer.exe`. This can be accomplished in a few ways. 46 | 47 | Using the `QuitCmd` setting in your config file, you can automatically restore your desktop after you quit Flex Launcher: 48 | ```ini 49 | [General] 50 | ... 51 | QuitCmd=:fork explorer.exe 52 | ``` 53 | 54 | To manually restore the desktop, hold Ctrl + Shift + Esc to bring up Task Manager. Then, select File->Run new task from the toolbar. Type `explorer.exe` and press OK. 55 | 56 | You can permanently switch back to the desktop as your default interface by navigating to Custom User Interface in the Group Policy editor as above, and disabling it. -------------------------------------------------------------------------------- /docs/flex-launcher.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 16 | 20 | 21 | 22 | 25 | 31 | 32 | 36 | 40 | 41 | 45 | 49 | 54 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | paths-ignore: 5 | [ 6 | "**/**.md" 7 | ] 8 | 9 | pull_request: 10 | branches: 11 | - master 12 | paths-ignore: 13 | [ 14 | "**/**.md" 15 | ] 16 | 17 | workflow_dispatch: 18 | 19 | defaults: 20 | run: 21 | shell: bash 22 | env: 23 | VCPKG_COMMITTISH: 10b7a178346f3f0abef60cecd5130e295afd8da4 24 | 25 | permissions: 26 | actions: none 27 | checks: none 28 | contents: write 29 | deployments: none 30 | issues: none 31 | packages: read 32 | pull-requests: none 33 | repository-projects: none 34 | security-events: none 35 | statuses: read 36 | 37 | jobs: 38 | build_windows: 39 | name: Windows 40 | runs-on: windows-2022 41 | strategy: 42 | fail-fast: false 43 | 44 | env: 45 | CMAKE_BUILD_TYPE: Release 46 | CMAKE_GENERATOR: Visual Studio 17 2022 47 | VCPKG_TRIPLET: x64-windows-static 48 | 49 | steps: 50 | - name: Checkout Git repository 51 | uses: actions/checkout@v3 52 | with: 53 | submodules: true 54 | 55 | - uses: friendlyanon/setup-vcpkg@v1 56 | with: 57 | committish: ${{env.VCPKG_COMMITTISH}} 58 | 59 | - name: Configure 60 | run: cmake -B build -G "${{env.CMAKE_GENERATOR}}" -D "CMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=${{env.VCPKG_TRIPLET}} 61 | 62 | - name: Build 63 | run: cmake --build build --target package --config ${{ env.CMAKE_BUILD_TYPE }} 64 | 65 | - uses: actions/upload-artifact@v3 66 | name: Upload package 67 | with: 68 | name: Windows build 69 | path: build/*.zip 70 | 71 | - name: Release 72 | uses: softprops/action-gh-release@v1 73 | if: startsWith(github.ref, 'refs/tags/') 74 | with: 75 | files: build/*.zip 76 | 77 | build_linux: 78 | name: Linux 79 | runs-on: ubuntu-latest 80 | permissions: 81 | packages: write 82 | strategy: 83 | fail-fast: false 84 | matrix: 85 | config: 86 | - name: Debian 87 | docker_image: debian:bullseye 88 | package_ext: .deb 89 | 90 | container: 91 | image: ${{matrix.config.docker_image}} 92 | 93 | env: 94 | CMAKE_BUILD_TYPE: Release 95 | 96 | steps: 97 | - name: Checkout Git repository 98 | uses: actions/checkout@v3 99 | 100 | - name: "Install dependencies" 101 | run: | 102 | if [[ "${{matrix.config.name}}" == "Debian" ]]; then 103 | apt update && apt install -y build-essential git cmake pkg-config libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libinih-dev 104 | fi 105 | 106 | - name: Build 107 | run: | 108 | if [[ "${{matrix.config.name}}" == "Debian" ]]; then 109 | cmake -B build -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=/usr -DPACKAGE=DEB -DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=amd64 110 | cmake --build build --target package --config ${{env.CMAKE_BUILD_TYPE}} 111 | fi 112 | 113 | - uses: actions/upload-artifact@v3 114 | name: Upload Package 115 | with: 116 | name: Debian build 117 | path: build/*.deb 118 | 119 | - name: Release 120 | uses: softprops/action-gh-release@v1 121 | if: startsWith(github.ref, 'refs/tags/') 122 | with: 123 | files: build/*${{matrix.config.package_ext}} 124 | token: ${{secrets.ACTIONS_SECRET}} 125 | 126 | build_rpi: 127 | name: Raspberry Pi 128 | runs-on: ubuntu-latest 129 | strategy: 130 | fail-fast: false 131 | 132 | env: 133 | CMAKE_BUILD_TYPE: Release 134 | IMAGE_VERSION: "2022-10-30" 135 | 136 | steps: 137 | - name: Setup 138 | run: | 139 | REPO=${{github.repository}} 140 | echo "REPO_TITLE=${REPO#*/}" >> ${GITHUB_ENV} 141 | sudo apt-get install -y qemu qemu-user-static binfmt-support wget tar 142 | mkdir root 143 | 144 | - name: Cache Image 145 | id: image-cache 146 | uses: actions/cache@v3 147 | with: 148 | path: image.tar.xz 149 | key: ${{env.IMAGE_VERSION}} 150 | 151 | - name: Download Image 152 | if: steps.image-cache.outputs.cache-hit != 'true' 153 | run: wget -q https://github.com/complexlogic/rpi_image/releases/download/${{env.IMAGE_VERSION}}/image.tar.xz 154 | 155 | - name: Extract Image 156 | run: tar -xf image.tar.xz 157 | 158 | - name: Checkout Git repository 159 | uses: actions/checkout@v3 160 | with: 161 | path: ${{env.REPO_TITLE}} 162 | 163 | - name: Generate Build Script 164 | run: | 165 | echo '#!/bin/bash' >> rpi 166 | echo "sudo apt update &&" >> rpi 167 | echo "sudo apt install -y libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libinih-dev &&" >> rpi 168 | echo "cd ${{env.REPO_TITLE}} &&" >> rpi 169 | echo "cmake -B build -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=/usr -DPACKAGE=DEB -DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=arm64 -DRPI=1 &&" >> rpi 170 | echo "cmake --build build --target package --config ${{env.CMAKE_BUILD_TYPE}}" >> rpi 171 | sudo chmod +x rpi 172 | 173 | - name: Build 174 | run: | 175 | LOOPBACK=$(sudo losetup -f -P --show rpi.img) 176 | sudo mount ${LOOPBACK}p2 -o rw root 177 | sudo cp /usr/bin/qemu-aarch64-static root/usr/bin 178 | sudo cp -r ${{env.REPO_TITLE}} root 179 | sudo cp rpi root/usr/bin 180 | sudo chroot root rpi 181 | 182 | - uses: actions/upload-artifact@v3 183 | name: Upload package 184 | with: 185 | name: Raspberry Pi build 186 | path: root/${{env.REPO_TITLE}}/build/*.deb 187 | 188 | - name: Release 189 | uses: softprops/action-gh-release@v1 190 | if: startsWith(github.ref, 'refs/tags/') 191 | with: 192 | files: root/${{env.REPO_TITLE}}/build/*.deb 193 | token: ${{secrets.ACTIONS_SECRET}} 194 | -------------------------------------------------------------------------------- /config/config.ini.in: -------------------------------------------------------------------------------- 1 | # @PROJECT_NAME@ v@PROJECT_VERSION@ sample configuration file 2 | # For documentation of these settings, visit: https://complexlogic.github.io/flex-launcher/configuration 3 | [General] 4 | @SETTING_DEFAULT_MENU@=@DEFAULT_MENU@ 5 | @SETTING_VSYNC@=@DEFAULT_VSYNC@ 6 | #@SETTING_FPS_LIMIT@= 7 | #@SETTING_APPLICATION_TIMEOUT@=@DEFAULT_APPLICATION_TIMEOUT@ 8 | @SETTING_ON_LAUNCH@=@DEFAULT_ON_LAUNCH@ 9 | @SETTING_WRAP_ENTRIES@=@DEFAULT_WRAP_ENTRIES@ 10 | @SETTING_RESET_ON_BACK@=@DEFAULT_RESET_ON_BACK@ 11 | @SETTING_MOUSE_SELECT@=@DEFAULT_MOUSE_SELECT@ 12 | @SETTING_INHIBIT_OS_SCREENSAVER@=@DEFAULT_INHIBIT_OS_SCREENSAVER@ 13 | #@SETTING_STARTUP_CMD@= 14 | #@SETTING_QUIT_CMD@= 15 | 16 | [Background] 17 | @SETTING_BACKGROUND_MODE@=@DEFAULT_BACKGROUND_MODE@ 18 | @SETTING_BACKGROUND_COLOR@=#@DEFAULT_BACKGROUND_COLOR_R@@DEFAULT_BACKGROUND_COLOR_G@@DEFAULT_BACKGROUND_COLOR_B@ 19 | #@SETTING_BACKGROUND_IMAGE@= 20 | #@SETTING_SLIDESHOW_DIRECTORY@= 21 | #@SETTING_SLIDESHOW_IMAGE_DURATION@=@DEFAULT_SLIDESHOW_IMAGE_DURATION_CONFIG@ 22 | #@SETTING_SLIDESHOW_TRANSITION_TIME@=@DEFAULT_SLIDESHOW_TRANSITION_TIME_CONFIG@ 23 | #@SETTING_CHROMA_KEY_COLOR@=#@DEFAULT_CHROMA_KEY_COLOR_R@@DEFAULT_CHROMA_KEY_COLOR_G@@DEFAULT_CHROMA_KEY_COLOR_B@ 24 | @SETTING_BACKGROUND_OVERLAY@=@DEFAULT_BACKGROUND_OVERLAY@ 25 | @SETTING_BACKGROUND_OVERLAY_COLOR@=#@DEFAULT_BACKGROUND_OVERLAY_COLOR_R@@DEFAULT_BACKGROUND_OVERLAY_COLOR_G@@DEFAULT_BACKGROUND_OVERLAY_COLOR_B@ 26 | @SETTING_BACKGROUND_OVERLAY_OPACITY@=@DEFAULT_BACKGROUND_OVERLAY_OPACITY@ 27 | 28 | [Layout] 29 | @SETTING_MAX_BUTTONS@=@DEFAULT_MAX_BUTTONS@ 30 | @SETTING_ICON_SIZE@=@DEFAULT_ICON_SIZE@ 31 | @SETTING_ICON_SPACING@=@DEFAULT_ICON_SPACING@ 32 | @SETTING_VCENTER@=@DEFAULT_VCENTER@ 33 | 34 | [Titles] 35 | @SETTING_TITLES_ENABLED@=@DEFAULT_TITLES_ENABLED@ 36 | @SETTING_TITLE_FONT@=@FONT_PREFIX@@DEFAULT_FONT@ 37 | @SETTING_TITLE_FONT_SIZE@=@DEFAULT_FONT_SIZE@ 38 | @SETTING_TITLE_FONT_COLOR@=#@DEFAULT_TITLE_FONT_COLOR_R@@DEFAULT_TITLE_FONT_COLOR_G@@DEFAULT_TITLE_FONT_COLOR_B@ 39 | @SETTING_TITLE_OPACITY@=@DEFAULT_TITLE_OPACITY@ 40 | @SETTING_TITLE_SHADOWS@=@DEFAULT_TITLE_SHADOWS@ 41 | @SETTING_TITLE_SHADOW_COLOR@=#@DEFAULT_TITLE_SHADOW_COLOR_R@@DEFAULT_TITLE_SHADOW_COLOR_G@@DEFAULT_TITLE_SHADOW_COLOR_B@ 42 | @SETTING_TITLE_OVERSIZE_MODE@=@DEFAULT_TITLE_OVERSIZE_MODE@ 43 | @SETTING_TITLE_PADDING@=@DEFAULT_TITLE_PADDING@ 44 | 45 | [Highlight] 46 | @SETTING_HIGHLIGHT_ENABLED@=@DEFAULT_HIGHLIGHT_ENABLED@ 47 | @SETTING_HIGHLIGHT_FILL_COLOR@=#@DEFAULT_HIGHLIGHT_FILL_COLOR_R@@DEFAULT_HIGHLIGHT_FILL_COLOR_G@@DEFAULT_HIGHLIGHT_FILL_COLOR_B@ 48 | @SETTING_HIGHLIGHT_FILL_OPACITY@=@DEFAULT_HIGHLIGHT_FILL_OPACITY@ 49 | @SETTING_HIGHLIGHT_OUTLINE_SIZE@=@DEFAULT_HIGHLIGHT_OUTLINE_SIZE@ 50 | @SETTING_HIGHLIGHT_OUTLINE_COLOR@=#@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_R@@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_G@@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_B@ 51 | @SETTING_HIGHLIGHT_OUTLINE_OPACITY@=@DEFAULT_HIGHLIGHT_OUTLINE_OPACITY@ 52 | @SETTING_HIGHLIGHT_CORNER_RADIUS@=@DEFAULT_HIGHLIGHT_CORNER_RADIUS@ 53 | @SETTING_HIGHLIGHT_VPADDING@=@DEFAULT_HIGHLIGHT_VPADDING@ 54 | @SETTING_HIGHLIGHT_HPADDING@=@DEFAULT_HIGHLIGHT_HPADDING@ 55 | 56 | [Scroll Indicators] 57 | @SETTING_SCROLL_INDICATORS@=@DEFAULT_SCROLL_INDICATORS@ 58 | @SETTING_SCROLL_INDICATOR_FILL_COLOR@=#@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_R@@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_G@@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_B@ 59 | @SETTING_SCROLL_INDICATOR_OUTLINE_SIZE@=@DEFAULT_SCROLL_INDICATOR_OUTLINE_SIZE@ 60 | @SETTING_SCROLL_INDICATOR_OUTLINE_COLOR@=#@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_R@@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_G@@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_B@ 61 | @SETTING_SCROLL_INDICATOR_OPACITY@=@DEFAULT_SCROLL_INDICATOR_OPACITY@ 62 | 63 | [Clock] 64 | @SETTING_CLOCK_ENABLED@=@DEFAULT_CLOCK_ENABLED@ 65 | @SETTING_CLOCK_SHOW_DATE@=@DEFAULT_CLOCK_SHOW_DATE@ 66 | @SETTING_CLOCK_ALIGNMENT@=@DEFAULT_CLOCK_ALIGNMENT@ 67 | @SETTING_CLOCK_FONT@=@FONT_PREFIX@@DEFAULT_CLOCK_FONT@ 68 | @SETTING_CLOCK_FONT_SIZE@=@DEFAULT_CLOCK_FONT_SIZE@ 69 | @SETTING_CLOCK_FONT_COLOR@=#@DEFAULT_CLOCK_FONT_COLOR_R@@DEFAULT_CLOCK_FONT_COLOR_G@@DEFAULT_CLOCK_FONT_COLOR_B@ 70 | @SETTING_CLOCK_SHADOWS@=@DEFAULT_CLOCK_SHADOWS@ 71 | @SETTING_CLOCK_SHADOW_COLOR@=#@DEFAULT_CLOCK_SHADOW_COLOR_R@@DEFAULT_CLOCK_SHADOW_COLOR_G@@DEFAULT_CLOCK_SHADOW_COLOR_B@ 72 | @SETTING_CLOCK_MARGIN@=@DEFAULT_CLOCK_MARGIN@ 73 | @SETTING_CLOCK_OPACITY@=@DEFAULT_CLOCK_OPACITY@ 74 | @SETTING_CLOCK_TIME_FORMAT@=@DEFAULT_CLOCK_TIME_FORMAT@ 75 | @SETTING_CLOCK_DATE_FORMAT@=@DEFAULT_CLOCK_DATE_FORMAT@ 76 | @SETTING_CLOCK_INCLUDE_WEEKDAY@=@DEFAULT_CLOCK_INCLUDE_WEEKDAY@ 77 | 78 | [Screensaver] 79 | @SETTING_SCREENSAVER_ENABLED@=@DEFAULT_SCREENSAVER_ENABLED@ 80 | @SETTING_SCREENSAVER_IDLE_TIME@=@DEFAULT_SCREENSAVER_IDLE_TIME@ 81 | @SETTING_SCREENSAVER_INTENSITY@=@DEFAULT_SCREENSAVER_INTENSITY@ 82 | @SETTING_SCREENSAVER_PAUSE_SLIDESHOW@=@DEFAULT_SCREENSAVER_PAUSE_SLIDESHOW@ 83 | 84 | [Hotkeys] 85 | # Esc to quit 86 | Hotkey1=#1B;:quit 87 | 88 | [Gamepad] 89 | @SETTING_GAMEPAD_ENABLED@=@DEFAULT_GAMEPAD_ENABLED@ 90 | @SETTING_GAMEPAD_DEVICE@=@DEFAULT_GAMEPAD_DEVICE@ 91 | #@SETTING_GAMEPAD_MAPPINGS_FILE@= 92 | @SETTING_GAMEPAD_LSTICK_XM@=:left 93 | @SETTING_GAMEPAD_LSTICK_XP@=:right 94 | #@SETTING_GAMEPAD_LSTICK_YM@= 95 | #@SETTING_GAMEPAD_LSTICK_YP@= 96 | #@SETTING_GAMEPAD_RSTICK_XM@= 97 | #@SETTING_GAMEPAD_RSTICK_XP@= 98 | #@SETTING_GAMEPAD_RSTICK_YM@= 99 | #@SETTING_GAMEPAD_RSTICK_YP@= 100 | #@SETTING_GAMEPAD_LTRIGGER@= 101 | #@SETTING_GAMEPAD_RTRIGGER@= 102 | @SETTING_GAMEPAD_BUTTON_A@=:select 103 | @SETTING_GAMEPAD_BUTTON_B@=:back 104 | #@SETTING_GAMEPAD_BUTTON_X@= 105 | #@SETTING_GAMEPAD_BUTTON_Y@= 106 | #@SETTING_GAMEPAD_BUTTON_BACK@= 107 | #@SETTING_GAMEPAD_BUTTON_GUIDE@= 108 | #@SETTING_GAMEPAD_BUTTON_START@= 109 | #@SETTING_GAMEPAD_BUTTON_LEFT_STICK@= 110 | #@SETTING_GAMEPAD_BUTTON_RIGHT_STICK@= 111 | #@SETTING_GAMEPAD_BUTTON_LEFT_SHOULDER@= 112 | #@SETTING_GAMEPAD_BUTTON_RIGHT_SHOULDER@= 113 | #@SETTING_GAMEPAD_BUTTON_DPAD_UP@= 114 | #@SETTING_GAMEPAD_BUTTON_DPAD_DOWN@= 115 | @SETTING_GAMEPAD_BUTTON_DPAD_LEFT@=:left 116 | @SETTING_GAMEPAD_BUTTON_DPAD_RIGHT@=:right 117 | 118 | # Menu configurations 119 | [@DEFAULT_MENU@] 120 | Entry1=Kodi;@ICONS_PREFIX@kodi.png;@CMD_KODI@ 121 | Entry2=Plex;@ICONS_PREFIX@plex.png;@CMD_PLEX@ 122 | Entry3=@TITLE_GAMES@;@ICONS_PREFIX@@ICON_GAMES@;@CMD_GAMES@ 123 | Entry4=System;@ICONS_PREFIX@system.png;:submenu System 124 | 125 | [System] 126 | Entry1=Shutdown;@ICONS_PREFIX@system.png;:shutdown 127 | Entry2=Restart;@ICONS_PREFIX@restart.png;:restart 128 | Entry3=Sleep;@ICONS_PREFIX@sleep.png;:sleep 129 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Setup Guide 4 | --- 5 | # Setup Guide 6 | ## Table of Contents 7 | 1. [Overview](#overview) 8 | 2. [Selecting Menu Icons](#selecting-menu-icons) 9 | 3. [Maintaining Controls](#maintaining-contrast) 10 | 4. [Launching a Web Browser](#launching-a-web-browser) 11 | 5. [Watching YouTube](#watching-youtube) 12 | 6. [Directly Launching Steam Games](#directly-launching-steam-games) 13 | 14 | ## Overview 15 | This page contains tips for setting up Flex Launcher, and HTPCs in general. The recommendations herein are broadly applicable to all platforms supported by Flex Launcher. Additionally, see the platform setup guides for platform-specfic advice: 16 | - [Windows Setup Guide](https://complexlogic.github.io/flex-launcher/setup_windows) 17 | - [Linux Setup Guide](https://complexlogic.github.io/flex-launcher/setup_linux) 18 | 19 | Make that you are generally familiar with the [configuration options](https://complexlogic.github.io/flex-launcher/configuration) as well. 20 | 21 | ## Selecting Menu Icons 22 | Transparency is essential for menu icons. Therefore, you should not use JPEG images for icons, since the JPEG format does not support transparency. Use PNG or WebP instead. PNG icons for most popular applications are easily found online in common sizes up to 256x256. 23 | 24 | Any icon that is not the same resolution as the `IconSize` setting in your config file will be stretched. If your `IconSize` setting is not a common icon resolution (e.g. 256), then it is advisable to find SVG icons instead. However, Flex Launcher does not currently support SVGs for menu icons, so you will need to rasterize them into PNG or WebP using a tool such as [Inkscape](https://inkscape.org/). An example command can quickly rasterize an SVG into your desired resolution: 25 | ```bash 26 | inkscape --export-width= --export-type=png /path/to/file.svg 27 | ``` 28 | You can easily write a script to rasterize all SVGs in a directory to PNG at a given resolution. Here is an example in Python: 29 | ```python 30 | import glob 31 | import subprocess 32 | WIDTH=300 # Width of the PNG in pixels 33 | 34 | svg_files = glob.glob("*.svg") 35 | for file in svg_files: 36 | subprocess.run(['inkscape', f'--export-width={WIDTH}', '--export-type=png', file]) 37 | ``` 38 | 39 | ## Maintaining Contrast 40 | When using an image as the background, it is often difficult to read the text that is displayed on top. This is particularly true if the image is a photograph and the text is white. Flex Launcher has several features that will improve the contrast between the background and the objects on top. 41 | 42 | The background overlay feature draws a solid color, typically black, over the background. This will darken the background to improve the contrast ratio. The user can adjust how much to darken the background with the `OverlayOpacity` setting. 43 | 44 | Text shadows will give displayed text a textured, 3 dimensional appearance, which helps it stand out from the background. 45 | 46 | The highlight and scroll indicators each have an outline setting. The user can choose how thick and which color the outline should be, to improve the contrast with the background. 47 | 48 | ## Launching a Web Browser 49 | My recommended web browser for HTPC use is Chrome/Chromium. This browser has many command line launch options which make it more flexible to configure than Firefox and its derivatives. Some launch options that have particular relevance to HTPC use: 50 | - `--start-fullscreen`: This starts the browser in a fullscreen mode. However, do note that the address bar will be hidden, so make sure to include the URL of the website you want to launch as an argument. 51 | - `--force-device-scale-factor=n`: This can be used to make web pages rendered larger for viewing from a distance. For example, try, 1.1 or 1.2 as `n`. 52 | - `--user-agent`: Sets a custom HTML user-agent string. This is necessary for [watching YouTube](#watching-youtube). 53 | 54 | ## Watching YouTube 55 | There is currently no desktop application for YouTube. However, there is a TV-friendly web interface located at [youtube.com/tv](https://www.youtube.com/tv) that is intended for use by Smart TVs . Google recently blocked access to this interface for desktop web browsers, but the block can be easily circumvented by spoofing the user-agent string of a Smart TV. A list of valid Smart TV user-agent strings is easily found online by search engine. The following example menu entries will launch an app-like YouTube experience in a browser: 56 | 57 | **Windows:** 58 | ```ini 59 | Entry=YouTube;C:\icons\youtube.png;"C:\Program Files\Google\Chrome\Application\chrome.exe" --start-fullscreen --user-agent="Mozilla/5.0 (Linux; Tizen 2.3; SmartHub; SMART-TV; SmartTV; U; Maple2012) AppleWebKit/538.1+ (KHTML, like Gecko) TV Safari/538.1+" youtube.com/tv 60 | ``` 61 | 62 | **Linux:** 63 | ```ini 64 | Entry=YouTube;/path/to/icons/youtube.png;chromium --start-fullscreen --user-agent="Mozilla/5.0 (Linux; Tizen 2.3; SmartHub; SMART-TV; SmartTV; U; Maple2012) AppleWebKit/538.1+ (KHTML, like Gecko) TV Safari/538.1+" youtube.com/tv 65 | ``` 66 | 67 | This method is far superior to other HTPC YouTube options, such as Kodi's YouTube add-on. You can install a browser extension such as [uBlock Origin](https://ublockorigin.com/) to prevent ads from being shown before videos. 68 | 69 | The web interface also supports casting videos from the YouTube app on your smartphone to your TV. You can pair your phone in the settings. You can also sign into your YouTube account in the settings if you wish. 70 | 71 | ### Exiting 72 | The one caveat to this method is that the exit button in the menu doesn't work. As such, you will need to provide an alternative method to close the web browser after you've finished watching so you can return back to the launcher. For Windows users, the most straightforward solution is to configure an [exit hotkey](https://complexlogic.github.io/flex-launcher/configuration#exit-hotkey-windows-only) on your remote. Linux users should set up a hotkey with their DE/WM to close the active window. 73 | 74 | ## Directly Launching Steam Games 75 | Steam users may desire to launch their most frequently played games directly from Flex Launcher to avoid having to navigate through the Steam client UI first. Valve provides a [protocol](https://developer.valvesoftware.com/wiki/Steam_browser_protocol) to directly launch games, among other actions. To do so, pass `steam://run/` as an argument to Steam, where `` is replaced by the id of the game you want to watch. You can find the id of a game by searching [steamdb](https://steamdb.info/). For example, the id of Portal 2 is 620. You would structure your menu entry to launch Portal 2 like so: 76 | 77 | **Windows:** 78 | ```ini 79 | Entry=Portal 2;C:\icons\portal_2.png;"C:\Program Files (x86)\Steam\steam.exe" steam://run/620 80 | ``` 81 | 82 | **Linux:** 83 | ```ini 84 | Entry=Portal 2;/path/to/icons/portal_2.png;steam steam://run/620 85 | ``` 86 | 87 | Make sure you have autologin configured in Steam, otherwise you will be prompted for your password before the game launches. 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Logo 4 | 5 | 6 | 7 | # Flex Launcher 8 |
9 |
10 | Table of Contents 11 |
    12 |
  1. 13 | About 14 |
  2. 15 |
  3. 16 | Screenshots 17 |
  4. 18 |
  5. 19 | Installation 20 | 24 |
  6. 25 |
  7. Usage
  8. 26 | 30 |
  9. Development Status
  10. 31 |
  11. Documentation
  12. 32 |
  13. Credits
  14. 33 |
34 |
35 | 36 | ## About 37 | Flex Launcher is a customizable application launcher and front end designed with a TV-friendly [10 foot user interface](https://en.wikipedia.org/wiki/10-foot_user_interface), intending to mimic the look and feel of a streaming box or game console. Flex Launcher allows you to launch applications on your HTPC or couch gaming PC entirely by use of a TV remote or a gamepad. No keyboard or mouse required! 38 | 39 | Flex Launcher is compatible with both Windows and Linux (including Raspberry Pi devices). 40 | 41 | ## Screenshots 42 | ![Screenshot 1](docs/assets/screenshots/screenshot1.png "Screenshot 1") 43 | 44 | https://user-images.githubusercontent.com/95071366/208355237-11f00cbb-9cc3-436b-98f7-8de350e584a7.mp4 45 | 46 | ## Installation 47 | Executables are available for Windows 64 bit, Linux x86-64, and Raspberry Pi. You can also compile the program yourself using the [compilation guide](docs/compilation.md). 48 | 49 | ### Windows 50 | Download the win64 .zip file from the [latest release](https://github.com/complexlogic/flex-launcher/releases/latest) and extract the contents to a directory of your choice. Flex Launcher should be run on an up-to-date Windows 10 system, or Windows 11. 51 | 52 | ### Linux 53 | Binary packages are available on the [release page](https://github.com/complexlogic/flex-launcher/releases) for APT and pacman based distributions. You may use the commands below to install. 54 | 55 | #### APT-based x86-64 Distributions (Debian, Ubuntu, etc.) 56 | This package is compatible with Debian Bullseye and later, Ubuntu 21.04 and later. 57 | ```bash 58 | wget https://github.com/complexlogic/flex-launcher/releases/download/v2.2/flex-launcher_2.2_amd64.deb 59 | sudo apt install ./flex-launcher_2.2_amd64.deb 60 | ``` 61 | #### Pacman-based x86-64 Distributions (Arch, Manjaro, etc.) 62 | ```bash 63 | wget https://github.com/complexlogic/flex-launcher/releases/download/v2.2/flex-launcher-2.2-1-x86_64.pkg.tar.zst 64 | sudo pacman -U flex-launcher-2.2-1-x86_64.pkg.tar.zst 65 | ``` 66 | #### Raspberry Pi 67 | This package is compatible with Raspbian Bullseye and later, 64 bit only. 68 | ```bash 69 | wget https://github.com/complexlogic/flex-launcher/releases/download/v2.2/flex-launcher_2.2_arm64.deb 70 | sudo apt install ./flex-launcher_2.2_arm64.deb 71 | ``` 72 | #### Copying Assets to Home Directory 73 | The Linux packages install a default config file and assets to `/usr/share/flex-launcher`. It is strongly recommended to *not* edit this config file directly, as it will be overwritten if you upgrade to a later version of Flex Launcher. Instead, copy these files to your home directory and edit it there. 74 | ```bash 75 | cp -r /usr/share/flex-launcher ~/.config 76 | sed -i "s|/usr/share/flex-launcher|$HOME/.config/flex-launcher|g" ~/.config/flex-launcher/config.ini 77 | ``` 78 | 79 | ## Usage 80 | Flex Launcher uses an INI file to configure the menus and settings. Upon startup, the program will search for a file named `config.ini` in the following locations in order: 81 | 1. The current working directory 82 | 2. The directory containing the `flex-launcher` executable 83 | 3. Linux only: `~/.config/flex-launcher` 84 | 4. Linux only: `/usr/share/flex-launcher` 85 | 86 | If your config file is in one of the above locations, Flex Launcher can be started simply by double clicking the executable file or adding it to autostart. If your config file is in a non-standard location, you must specify the path via command line argument: 87 | ```bash 88 | flex-launcher -c /path/to/config.ini 89 | ``` 90 | Flex Launcher ships with a default config file which is intended strictly for demonstration purposes. If you try to start one of the applications, it is possible that nothing will happen because the install path is different on your system, or you don't have the application installed at all. See the [configuration file documentation](docs/configuration.md#configuring-flex-launcher) for instuctions on how to change the menus and settings. 91 | 92 | ### Controls 93 | The keyboard arrow keys move the highlight cursor left and right. Enter selects the current entry, backspace goes back to the previous menu (if applicable), and Esc quits the program. 94 | 95 | #### TV Remotes 96 | Flex Launcher does not feature built-in decoding of IR or CEC signals. If you plan to use a TV remote to control the device, it is assumed that these signals are decoded by the OS or another program and mapped to keyboard presses, which can then be received by Flex Launcher. 97 | 98 | #### Gamepads 99 | Gamepad controls are built-in to the program, but are disabled by default. To enable them, open your configuration file and, under the "Gamepad" section, change the "Enabled" setting from false to true. After that, the gamepad controls should "Just Work" for most users. If your gamepad is not recognized automatically, or you want to change the default controls, see the [gamepad controls documentation](docs/configuration.md#gamepad-controls). 100 | 101 | ### Debugging 102 | Flex Launcher has a debug mode which may be enabled as follows: 103 | ```bash 104 | flex-launcher -d 105 | ``` 106 | This will output a logfile named `flex-launcher.log` in the same directory as `flex-launcher.exe` on Windows, and in `~/.local/share/flex-launcher` on Linux. 107 | 108 | ## Development Status 109 | Flex Launcher has reached a mature state, and there are currently no feature releases planned for the future. I've started a [new HTPC launcher project](https://github.com/complexlogic/big-launcher) which is similar in nature to Flex Launcher, but aims to provide a more advanced, Smart TV-like user interface. My future development effort will be focused on that new project, but I will still maintain Flex Launcher for bugfixes and dependency updates. 110 | 111 | ## Documentation 112 | Here is a list of available documentation: 113 | - [Configuration](docs/configuration.md#configuring-flex-launcher) 114 | - [General Setup Guide](docs/setup.md#setup-guide) 115 | - [Windows-specific Setup Guide](docs/setup_windows.md#windows-setup-guide) 116 | - [Linux-specific Setup Guide](docs/setup_linux.md#linux-setup-guide) 117 | - [Compilation Guide](docs/compilation.md#compilation-guide) 118 | 119 | ## Credits 120 | Flex Launcher is made possible by the following projects: 121 | - [SDL](https://github.com/libsdl-org/SDL), including the subprojects: 122 | - [SDL_image](https://github.com/libsdl-org/SDL_image) 123 | - [SDL_ttf](https://github.com/libsdl-org/SDL_ttf) 124 | - [Nanosvg](https://github.com/memononen/nanosvg) 125 | - [inih](https://github.com/benhoyt/inih) 126 | - [Numix icons](https://github.com/numixproject) 127 | 128 | The design of Flex Launcher was strongly influenced by the excellent desktop application launcher [xlunch](https://github.com/Tomas-M/xlunch). 129 | -------------------------------------------------------------------------------- /src/platform/unix.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "../launcher.h" 11 | #include 12 | #include "unix.h" 13 | #include "../util.h" 14 | #include "../debug.h" 15 | #include "platform.h" 16 | #include "slideshow.h" 17 | 18 | static int desktop_handler(void *user, const char *section, const char *name, const char *value); 19 | static void strip_field_codes(char *cmd); 20 | static bool ends_with(const char *string, const char *phrase); 21 | static int image_filter(const struct dirent *file); 22 | 23 | // A function to handle .desktop lines 24 | static int desktop_handler(void *user, const char *section, const char *name, const char *value) 25 | { 26 | Desktop *pdesktop = (Desktop*) user; 27 | if (!strcmp(pdesktop->section, section) && !strcmp(name, KEY_EXEC)) 28 | pdesktop->exec = strdup(value); 29 | return 0; 30 | } 31 | 32 | // A function to determine if a file exists in the filesystem 33 | bool file_exists(const char *path) 34 | { 35 | return access(path, R_OK) ? false : true; 36 | } 37 | 38 | // A function to determine if a directory exists in the filesystem 39 | bool directory_exists(const char *path) 40 | { 41 | struct stat directory; 42 | return stat(path, &directory) == 0 && S_ISDIR(directory.st_mode) ? true : false; 43 | } 44 | 45 | // A function to remove field codes from .desktop file Exec line 46 | static void strip_field_codes(char *cmd) 47 | { 48 | size_t start = 0; 49 | for (size_t i = 0; i < strlen(cmd); i++) { 50 | if (cmd[i] == '%' && i > 0 && cmd[i - 1] == ' ') 51 | start = i; 52 | else if (start && i > start + 2 && cmd[i] != ' ') { 53 | strcpy(cmd + start, cmd + i); 54 | start = 0; 55 | } 56 | } 57 | if (start) 58 | cmd[start - 1] ='\0'; 59 | } 60 | 61 | // A function to make a directory, including any intermediate 62 | // directories if necessary 63 | void make_directory(const char *directory) 64 | { 65 | char buffer[MAX_PATH_CHARS + 1]; 66 | char *i = NULL; 67 | size_t length; 68 | snprintf(buffer, sizeof(buffer), "%s", directory); 69 | length = strlen(buffer); 70 | if (buffer[length - 1] == '/') 71 | buffer[length - 1] = '\0'; 72 | for (i = buffer + 1; *i != '\0'; i++) { 73 | if (*i == '/') { 74 | *i = '\0'; 75 | mkdir(buffer, S_IRWXU); 76 | *i = '/'; 77 | } 78 | } 79 | mkdir(buffer, S_IRWXU); 80 | } 81 | 82 | // A function to determine if a string ends with a phrase 83 | static bool ends_with(const char *string, const char *phrase) 84 | { 85 | size_t len_string = strlen(string); 86 | size_t len_phrase = strlen(phrase); 87 | if (len_phrase > len_string) 88 | return false; 89 | char *p = (char*) string + len_string - len_phrase; 90 | return strcmp(p, phrase) ? false : true; 91 | } 92 | 93 | // A function to launch an external application 94 | bool start_process(char *cmd, bool application) 95 | { 96 | // Check if the command is an XDG .desktop file 97 | char *exec = NULL; 98 | char *tmp = strdup(cmd); 99 | char *file = strtok(tmp, DELIMITER_ACTION); 100 | if (ends_with(file, EXT_DESKTOP)) { 101 | Desktop desktop; 102 | desktop.exec = NULL; 103 | 104 | // Parse the desktop action from the command (if any) 105 | const char* const action = strtok(NULL, DELIMITER_ACTION); 106 | if (action == NULL) 107 | copy_string(desktop.section, DESKTOP_SECTION_HEADER, sizeof(desktop.section)); 108 | else 109 | snprintf(desktop.section, sizeof(desktop.section), DESKTOP_SECTION_HEADER_ACTION, action); 110 | 111 | // Parse the .desktop file for the Exec line value 112 | int error = ini_parse(file, desktop_handler, &desktop); 113 | if (error < 0) { 114 | log_error("Desktop file '%s' not found", file); 115 | free(tmp); 116 | return false; 117 | } 118 | if (desktop.exec == NULL) { 119 | log_debug("No Exec line found in desktop file '%s'", cmd); 120 | free(tmp); 121 | return false; 122 | } 123 | exec = desktop.exec; 124 | strip_field_codes(exec); 125 | cmd = exec; 126 | } 127 | free(tmp); 128 | 129 | // Fork new system shell process 130 | pid_t child_pid = fork(); 131 | switch(child_pid) { 132 | case -1: 133 | log_error("Could not fork new process for application"); 134 | free(exec); 135 | return false; 136 | 137 | // Child process 138 | case 0: 139 | setpgid(0, 0); 140 | const char *file = "/bin/sh"; 141 | const char *args[] = { 142 | "sh", 143 | "-c", 144 | cmd, 145 | NULL 146 | }; 147 | execvp(file, (char* const*) args); 148 | break; 149 | 150 | // Parent process 151 | default: 152 | if (!application) 153 | return true; 154 | int status; 155 | 156 | // Check to see if the shell successfully launched 157 | SDL_Delay(10); 158 | waitpid(child_pid, &status, WNOHANG); 159 | if (WIFEXITED(status) && WEXITSTATUS(status) > 126) { 160 | log_error("Application failed to launch"); 161 | return false; 162 | } 163 | log_debug("Application launched successfully"); 164 | break; 165 | } 166 | free(exec); 167 | return true; 168 | } 169 | 170 | // A function to determine if a file is an image file 171 | int image_filter(const struct dirent *file) 172 | { 173 | size_t len_file = strlen(file->d_name); 174 | size_t len_extension; 175 | for (size_t i = 0; i < NUM_IMAGE_EXTENSIONS; i++) { 176 | len_extension = strlen(extensions[i]); 177 | if (len_file > len_extension && 178 | !strcmp(file->d_name + len_file - len_extension, extensions[i])) 179 | return 1; 180 | } 181 | return 0; 182 | } 183 | 184 | // A function to scan a directory for images 185 | void scan_slideshow_directory(Slideshow *slideshow, const char *directory) 186 | { 187 | struct dirent **files; 188 | slideshow->num_images = scandir(directory, &files, image_filter, NULL); 189 | slideshow->images = malloc((size_t) slideshow->num_images * sizeof(char*)); 190 | char file_path[MAX_PATH_CHARS + 1]; 191 | for (int i = 0; i < slideshow->num_images; i++) { 192 | join_paths(file_path, sizeof(file_path), 2, directory, files[i]->d_name); 193 | slideshow->images[i] = strdup(file_path); 194 | free(files[i]); 195 | } 196 | free(files); 197 | } 198 | 199 | void get_region(char *buffer) 200 | { 201 | char *lang = getenv("LANG"); 202 | char *token = strtok(lang, "_"); 203 | if (token == NULL) 204 | return; 205 | token = strtok(NULL, "."); 206 | if (token != NULL && strlen(token) == 2) 207 | copy_string(buffer, token, 3); 208 | } 209 | 210 | // A function to shutdown the computer 211 | void scmd_shutdown() 212 | { 213 | start_process(CMD_SHUTDOWN, false); 214 | } 215 | 216 | // A function to restart the computer 217 | void scmd_restart() 218 | { 219 | start_process(CMD_RESTART, false); 220 | } 221 | 222 | // A function to put the computer to sleep 223 | void scmd_sleep() 224 | { 225 | start_process(CMD_SLEEP, false); 226 | } 227 | 228 | // A function to print usage to the command line 229 | void print_usage() 230 | { 231 | printf("Usage: " EXECUTABLE_TITLE " [OPTIONS]\n"); 232 | printf(" -c p, --config=p Load config file from path p.\n"); 233 | printf(" -d, --debug Enable debug messages.\n"); 234 | printf(" -h, --help Show this help message.\n"); 235 | printf(" -v, --version Print version information.\n"); 236 | } 237 | -------------------------------------------------------------------------------- /docs/assets/icons/raspberry_pi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /config/config_settings.cmake: -------------------------------------------------------------------------------- 1 | # Config setting keys 2 | set(SETTING_DEFAULT_MENU "DefaultMenu") 3 | set(SETTING_MAX_BUTTONS "MaxButtons") 4 | set(SETTING_VSYNC "VSync") 5 | set(SETTING_FPS_LIMIT "FPSLimit") 6 | set(SETTING_APPLICATION_TIMEOUT "ApplicationTimeout") 7 | set(SETTING_WRAP_ENTRIES "WrapEntries") 8 | set(SETTING_BACKGROUND_MODE "Mode") 9 | set(SETTING_BACKGROUND_COLOR "Color") 10 | set(SETTING_BACKGROUND_IMAGE "Image") 11 | set(SETTING_SLIDESHOW_DIRECTORY "SlideshowDirectory") 12 | set(SETTING_SLIDESHOW_IMAGE_DURATION "SlideshowImageDuration") 13 | set(SETTING_SLIDESHOW_TRANSITION_TIME "SlideshowTransitionTime") 14 | set(SETTING_CHROMA_KEY_COLOR "ChromaKeyColor") 15 | set(SETTING_BACKGROUND_OVERLAY "Overlay") 16 | set(SETTING_BACKGROUND_OVERLAY_COLOR "OverlayColor") 17 | set(SETTING_BACKGROUND_OVERLAY_OPACITY "OverlayOpacity") 18 | set(SETTING_ICON_SIZE "IconSize") 19 | set(SETTING_ICON_SPACING "IconSpacing") 20 | set(SETTING_TITLES_ENABLED "Enabled") 21 | set(SETTING_TITLE_FONT "Font") 22 | set(SETTING_TITLE_FONT_SIZE "FontSize") 23 | set(SETTING_TITLE_FONT_COLOR "Color") 24 | set(SETTING_TITLE_SHADOWS "Shadows") 25 | set(SETTING_TITLE_SHADOW_COLOR "ShadowColor") 26 | set(SETTING_TITLE_OPACITY "Opacity") 27 | set(SETTING_TITLE_OVERSIZE_MODE "OversizeMode") 28 | set(SETTING_TITLE_PADDING "Padding") 29 | set(SETTING_HIGHLIGHT_ENABLED "Enabled") 30 | set(SETTING_HIGHLIGHT_FILL_COLOR "FillColor") 31 | set(SETTING_HIGHLIGHT_FILL_OPACITY "FillOpacity") 32 | set(SETTING_HIGHLIGHT_OUTLINE_SIZE "OutlineSize") 33 | set(SETTING_HIGHLIGHT_OUTLINE_COLOR "OutlineColor") 34 | set(SETTING_HIGHLIGHT_OUTLINE_OPACITY "OutlineOpacity") 35 | set(SETTING_HIGHLIGHT_CORNER_RADIUS "CornerRadius") 36 | set(SETTING_HIGHLIGHT_VPADDING "VPadding") 37 | set(SETTING_HIGHLIGHT_HPADDING "HPadding") 38 | set(SETTING_VCENTER "VCenter") 39 | set(SETTING_SCROLL_INDICATORS "Enabled") 40 | set(SETTING_SCROLL_INDICATOR_FILL_COLOR "FillColor") 41 | set(SETTING_SCROLL_INDICATOR_OUTLINE_SIZE "OutlineSize") 42 | set(SETTING_SCROLL_INDICATOR_OUTLINE_COLOR "OutlineColor") 43 | set(SETTING_SCROLL_INDICATOR_OPACITY "Opacity") 44 | set(SETTING_ON_LAUNCH "OnLaunch") 45 | set(SETTING_RESET_ON_BACK "ResetOnBack") 46 | set(SETTING_MOUSE_SELECT "MouseSelect") 47 | set(SETTING_INHIBIT_OS_SCREENSAVER "InhibitOSScreensaver") 48 | set(SETTING_STARTUP_CMD "StartupCmd") 49 | set(SETTING_QUIT_CMD "QuitCmd") 50 | set(SETTING_CLOCK_ENABLED "Enabled") 51 | set(SETTING_CLOCK_SHOW_DATE "ShowDate") 52 | set(SETTING_CLOCK_ALIGNMENT "Alignment") 53 | set(SETTING_CLOCK_FONT "Font") 54 | set(SETTING_CLOCK_FONT_COLOR "FontColor") 55 | set(SETTING_CLOCK_SHADOWS "Shadows") 56 | set(SETTING_CLOCK_SHADOW_COLOR "ShadowColor") 57 | set(SETTING_CLOCK_OPACITY "Opacity") 58 | set(SETTING_CLOCK_FONT_SIZE "FontSize") 59 | set(SETTING_CLOCK_MARGIN "Margin") 60 | set(SETTING_CLOCK_TIME_FORMAT "TimeFormat") 61 | set(SETTING_CLOCK_DATE_FORMAT "DateFormat") 62 | set(SETTING_CLOCK_INCLUDE_WEEKDAY "IncludeWeekday") 63 | set(SETTING_SCREENSAVER_ENABLED "Enabled") 64 | set(SETTING_SCREENSAVER_IDLE_TIME "IdleTime") 65 | set(SETTING_SCREENSAVER_INTENSITY "Intensity") 66 | set(SETTING_SCREENSAVER_PAUSE_SLIDESHOW "PauseSlideshow") 67 | set(SETTING_GAMEPAD_ENABLED "Enabled") 68 | set(SETTING_GAMEPAD_DEVICE "DeviceIndex") 69 | set(SETTING_GAMEPAD_MAPPINGS_FILE "ControllerMappingsFile") 70 | set(SETTING_GAMEPAD_LSTICK_XM "LStickX-") 71 | set(SETTING_GAMEPAD_LSTICK_XP "LStickX+") 72 | set(SETTING_GAMEPAD_LSTICK_YM "LStickY-") 73 | set(SETTING_GAMEPAD_LSTICK_YP "LStickY+") 74 | set(SETTING_GAMEPAD_RSTICK_XM "RStickX-") 75 | set(SETTING_GAMEPAD_RSTICK_XP "RStickX+") 76 | set(SETTING_GAMEPAD_RSTICK_YM "RStickY-") 77 | set(SETTING_GAMEPAD_RSTICK_YP "RStickY+") 78 | set(SETTING_GAMEPAD_LTRIGGER "LTrigger") 79 | set(SETTING_GAMEPAD_RTRIGGER "RTrigger") 80 | set(SETTING_GAMEPAD_BUTTON_A "ButtonA") 81 | set(SETTING_GAMEPAD_BUTTON_B "ButtonB") 82 | set(SETTING_GAMEPAD_BUTTON_X "ButtonX") 83 | set(SETTING_GAMEPAD_BUTTON_Y "ButtonY") 84 | set(SETTING_GAMEPAD_BUTTON_BACK "ButtonBack") 85 | set(SETTING_GAMEPAD_BUTTON_GUIDE "ButtonGuide") 86 | set(SETTING_GAMEPAD_BUTTON_START "ButtonStart") 87 | set(SETTING_GAMEPAD_BUTTON_LEFT_STICK "ButtonLeftStick") 88 | set(SETTING_GAMEPAD_BUTTON_RIGHT_STICK "ButtonRightStick") 89 | set(SETTING_GAMEPAD_BUTTON_LEFT_SHOULDER "ButtonLeftShoulder") 90 | set(SETTING_GAMEPAD_BUTTON_RIGHT_SHOULDER "ButtonRightShoulder") 91 | set(SETTING_GAMEPAD_BUTTON_DPAD_UP "ButtonDPadUp") 92 | set(SETTING_GAMEPAD_BUTTON_DPAD_DOWN "ButtonDPadDown") 93 | set(SETTING_GAMEPAD_BUTTON_DPAD_LEFT "ButtonDPadLeft") 94 | set(SETTING_GAMEPAD_BUTTON_DPAD_RIGHT "ButtonDPadRight") 95 | 96 | # Default settings 97 | set(DEFAULT_MENU "Main") 98 | set(DEFAULT_MAX_BUTTONS 4) 99 | set(DEFAULT_VSYNC "true") 100 | set(DEFAULT_APPLICATION_TIMEOUT "15") 101 | set(DEFAULT_WRAP_ENTRIES "false") 102 | set(DEFAULT_BACKGROUND_MODE "Color") 103 | set(DEFAULT_BACKGROUND_COLOR_R "00") 104 | set(DEFAULT_BACKGROUND_COLOR_G "00") 105 | set(DEFAULT_BACKGROUND_COLOR_B "00") 106 | set(DEFAULT_SLIDESHOW_IMAGE_DURATION "30000") 107 | set(DEFAULT_SLIDESHOW_IMAGE_DURATION_CONFIG "30") 108 | set(DEFAULT_SLIDESHOW_TRANSITION_TIME "1500") 109 | set(DEFAULT_SLIDESHOW_TRANSITION_TIME_CONFIG "3") 110 | set(DEFAULT_CHROMA_KEY_COLOR_R "01") 111 | set(DEFAULT_CHROMA_KEY_COLOR_G "01") 112 | set(DEFAULT_CHROMA_KEY_COLOR_B "01") 113 | set(DEFAULT_CHROMA_KEY_COLOR_A "FF") 114 | set(DEFAULT_BACKGROUND_OVERLAY "false") 115 | set(DEFAULT_BACKGROUND_OVERLAY_COLOR_R "00") 116 | set(DEFAULT_BACKGROUND_OVERLAY_COLOR_G "00") 117 | set(DEFAULT_BACKGROUND_OVERLAY_COLOR_B "00") 118 | set(DEFAULT_BACKGROUND_OVERLAY_COLOR_A "7F") 119 | set(DEFAULT_BACKGROUND_OVERLAY_OPACITY "50%") 120 | set(DEFAULT_ICON_SIZE 256) 121 | set(DEFAULT_ICON_SPACING "5%") 122 | set(DEFAULT_FONT "OpenSans-Regular.ttf") 123 | set(DEFAULT_FONT_SIZE 36) 124 | set(DEFAULT_TITLES_ENABLED "true") 125 | set(DEFAULT_TITLE_FONT_COLOR_R "FF") 126 | set(DEFAULT_TITLE_FONT_COLOR_G "FF") 127 | set(DEFAULT_TITLE_FONT_COLOR_B "FF") 128 | set(DEFAULT_TITLE_FONT_COLOR_A "FF") 129 | set(DEFAULT_TITLE_SHADOWS "false") 130 | set(DEFAULT_TITLE_SHADOW_COLOR_R "00") 131 | set(DEFAULT_TITLE_SHADOW_COLOR_G "00") 132 | set(DEFAULT_TITLE_SHADOW_COLOR_B "00") 133 | set(DEFAULT_TITLE_SHADOW_COLOR_A "FF") 134 | set(DEFAULT_TITLE_OPACITY "100%") 135 | set(DEFAULT_TITLE_OVERSIZE_MODE "Shrink") 136 | set(DEFAULT_TITLE_PADDING 20) 137 | set(DEFAULT_HIGHLIGHT_ENABLED "true") 138 | set(DEFAULT_HIGHLIGHT_FILL_COLOR_R "FF") 139 | set(DEFAULT_HIGHLIGHT_FILL_COLOR_G "FF") 140 | set(DEFAULT_HIGHLIGHT_FILL_COLOR_B "FF") 141 | set(DEFAULT_HIGHLIGHT_FILL_COLOR_A "40") 142 | set(DEFAULT_HIGHLIGHT_FILL_OPACITY "25%") 143 | set(DEFAULT_HIGHLIGHT_OUTLINE_SIZE 0) 144 | set(DEFAULT_HIGHLIGHT_OUTLINE_COLOR_R "00") 145 | set(DEFAULT_HIGHLIGHT_OUTLINE_COLOR_G "00") 146 | set(DEFAULT_HIGHLIGHT_OUTLINE_COLOR_B "FF") 147 | set(DEFAULT_HIGHLIGHT_OUTLINE_COLOR_A "FF") 148 | set(DEFAULT_HIGHLIGHT_OUTLINE_OPACITY "100%") 149 | set(DEFAULT_HIGHLIGHT_CORNER_RADIUS 0) 150 | set(DEFAULT_HIGHLIGHT_VPADDING 30) 151 | set(DEFAULT_HIGHLIGHT_HPADDING 30) 152 | set(DEFAULT_VCENTER "50%") 153 | set(DEFAULT_SCROLL_INDICATORS "true") 154 | set(DEFAULT_SCROLL_INDICATOR_FILL_COLOR_R "FF") 155 | set(DEFAULT_SCROLL_INDICATOR_FILL_COLOR_G "FF") 156 | set(DEFAULT_SCROLL_INDICATOR_FILL_COLOR_B "FF") 157 | set(DEFAULT_SCROLL_INDICATOR_FILL_COLOR_A "FF") 158 | set(DEFAULT_SCROLL_INDICATOR_OUTLINE_SIZE 0) 159 | set(DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_R "00") 160 | set(DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_G "00") 161 | set(DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_B "00") 162 | set(DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_A "FF") 163 | set(DEFAULT_SCROLL_INDICATOR_OPACITY "100%") 164 | set(DEFAULT_ON_LAUNCH "Blank") 165 | set(DEFAULT_RESET_ON_BACK "false") 166 | set(DEFAULT_INHIBIT_OS_SCREENSAVER "true") 167 | set(DEFAULT_MOUSE_SELECT "false") 168 | set(DEFAULT_CLOCK_ENABLED "false") 169 | set(DEFAULT_CLOCK_SHOW_DATE "false") 170 | set(DEFAULT_CLOCK_ALIGNMENT "Left") 171 | set(DEFAULT_CLOCK_FONT "SourceSansPro-Regular.ttf") 172 | set(DEFAULT_CLOCK_MARGIN "5%") 173 | set(DEFAULT_CLOCK_FONT_COLOR_R "FF") 174 | set(DEFAULT_CLOCK_FONT_COLOR_G "FF") 175 | set(DEFAULT_CLOCK_FONT_COLOR_B "FF") 176 | set(DEFAULT_CLOCK_FONT_COLOR_A "FF") 177 | set(DEFAULT_CLOCK_SHADOWS "false") 178 | set(DEFAULT_CLOCK_SHADOW_COLOR_R "00") 179 | set(DEFAULT_CLOCK_SHADOW_COLOR_G "00") 180 | set(DEFAULT_CLOCK_SHADOW_COLOR_B "00") 181 | set(DEFAULT_CLOCK_SHADOW_COLOR_A "FF") 182 | set(DEFAULT_CLOCK_OPACITY "100%") 183 | set(DEFAULT_CLOCK_FONT_SIZE "50") 184 | set(DEFAULT_CLOCK_TIME_FORMAT "Auto") 185 | set(DEFAULT_CLOCK_DATE_FORMAT "Auto") 186 | set(DEFAULT_CLOCK_INCLUDE_WEEKDAY "true") 187 | set(DEFAULT_SCREENSAVER_ENABLED "false") 188 | set(DEFAULT_SCREENSAVER_IDLE_TIME "300") 189 | set(DEFAULT_SCREENSAVER_INTENSITY "70%") 190 | set(DEFAULT_SCREENSAVER_PAUSE_SLIDESHOW "true") 191 | set(DEFAULT_GAMEPAD_ENABLED "false") 192 | set(DEFAULT_GAMEPAD_DEVICE -1) 193 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project info 2 | cmake_minimum_required(VERSION 3.18) 3 | project("Flex Launcher" 4 | LANGUAGES C 5 | VERSION 2.2 6 | DESCRIPTION "Customizable HTPC Application Launcher" 7 | HOMEPAGE_URL "https://github.com/complexlogic/flex-launcher" 8 | ) 9 | set(EXECUTABLE_TITLE "flex-launcher") 10 | set(EXECUTABLE_OUTPUT_PATH "${PROJECT_BINARY_DIR}") 11 | include_directories(${PROJECT_BINARY_DIR}) 12 | 13 | option(EXTRA_WARNINGS "Enable extra compiler warnings" OFF) 14 | if (EXTRA_WARNINGS) 15 | if (MSVC) 16 | add_compile_options(/W4 /WX) 17 | else () 18 | add_compile_options(-Wall -Wextra -Wpedantic -Wconversion) 19 | endif () 20 | endif () 21 | 22 | # Set Visual Studio solution startup project 23 | if (WIN32) 24 | set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT ${EXECUTABLE_TITLE}) 25 | endif () 26 | 27 | # Minimum library versions for Linux 28 | set(MIN_SDL_VERSION "2.0.14") 29 | set(MIN_SDL_IMAGE_VERSION "2.0.5") 30 | set(MIN_SDL_TTF_VERSION "2.0.15") 31 | set(MIN_GLIBC_VERSION "2.31") # Enforced for .deb packages only 32 | 33 | # Generate PKGBUILD script for Arch packages 34 | if (PACKAGE STREQUAL "ARCH") 35 | configure_file("${PROJECT_SOURCE_DIR}/config/PKGBUILD.in" "${PROJECT_BINARY_DIR}/PKGBUILD") 36 | return() 37 | endif () 38 | 39 | # Import settings 40 | include(${PROJECT_SOURCE_DIR}/config/config_settings.cmake) 41 | 42 | # Find dependencies - Linux 43 | if (UNIX) 44 | find_package(PkgConfig MODULE REQUIRED) 45 | pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2>=${MIN_SDL_VERSION}) 46 | pkg_check_modules(SDL2_IMAGE REQUIRED IMPORTED_TARGET SDL2_image>=${MIN_SDL_IMAGE_VERSION}) 47 | pkg_check_modules(SDL2_TTF REQUIRED IMPORTED_TARGET SDL2_ttf>=${MIN_SDL_TTF_VERSION}) 48 | pkg_check_modules(INIH REQUIRED IMPORTED_TARGET inih) 49 | endif () 50 | 51 | # Find dependencies - Windows 52 | if (WIN32) 53 | find_package(SDL2 CONFIG REQUIRED) 54 | find_package(sdl2_image CONFIG REQUIRED) 55 | find_package(SDL2_ttf CONFIG REQUIRED) 56 | 57 | find_path(INIH_INCLUDE_DIR "ini.h" REQUIRED) 58 | find_library(INIH inih REQUIRED) 59 | find_path(GETOPT_INCLUDE_DIR "getopt.h" REQUIRED) 60 | find_library(GETOPT getopt REQUIRED) 61 | endif () 62 | 63 | # Build configuration - Linux 64 | if (UNIX) 65 | if (NOT CMAKE_BUILD_TYPE MATCHES "Debug") 66 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O2 -s") 67 | endif () 68 | set(CMAKE_SHARED_LINKER_FLAGS "--as-needed") 69 | endif () 70 | 71 | # Build configuration - Windows 72 | if (WIN32) 73 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE") # Workaround for Nanosvg and SDL_image header conflicts 74 | endif () 75 | 76 | # Configure default applications - Linux 77 | if (UNIX) 78 | set(DESKTOP_PATH "/usr/share/applications") 79 | set(CMD_KODI "${DESKTOP_PATH}/kodi.desktop") 80 | set(CMD_PLEX "${DESKTOP_PATH}/plexmediaplayer.desktop;TVF") 81 | if (RPI) 82 | set(TITLE_GAMES "RetroArch") 83 | set(ICON_GAMES "retroarch.png") 84 | set(CMD_GAMES "${DESKTOP_PATH}/retroarch.desktop") 85 | else () 86 | set(TITLE_GAMES "Steam") 87 | set(ICON_GAMES "steam.png") 88 | set(CMD_GAMES "${DESKTOP_PATH}/steam.desktop;BigPicture") 89 | endif () 90 | endif () 91 | 92 | # Configure default applications - Windows 93 | if (WIN32) 94 | set(CMD_KODI "\"C:\\Program Files\\Kodi\\kodi.exe\"") 95 | set(CMD_PLEX "\"C:\\Program Files\\Plex\\Plex Media Player\\PlexMediaPlayer.exe\"") 96 | set(TITLE_GAMES "Steam") 97 | set(ICON_GAMES "steam.png") 98 | set(CMD_GAMES "\"C:\\Program Files (x86)\\Steam\\steam.exe\" steam://open/bigpicture") 99 | endif () 100 | 101 | # Generate Windows application manifest 102 | if (WIN32) 103 | set(VERSION_M ${PROJECT_VERSION_MAJOR}) 104 | if (PROJECT_VERSION_MINOR) 105 | set(VERSION_N ${PROJECT_VERSION_MINOR}) 106 | else () 107 | set(VERSION_N 0) 108 | endif() 109 | if (PROJECT_VERSION_PATCH) 110 | set(VERSION_O ${PROJECT_VERSION_PATCH}) 111 | else () 112 | set(VERSION_O 0) 113 | endif() 114 | if (PROJECT_VERSION_TWEAK) 115 | set(VERSION_P ${PROJECT_VERSION_TWEAK}) 116 | else () 117 | set(VERSION_P 0) 118 | endif() 119 | configure_file( 120 | ${PROJECT_SOURCE_DIR}/config/${EXECUTABLE_TITLE}.manifest.in 121 | ${PROJECT_BINARY_DIR}/${EXECUTABLE_TITLE}.manifest 122 | ) 123 | endif() 124 | 125 | # Set time format strings 126 | if (WIN32) 127 | set(REMOVE_ZERO "#") 128 | else() 129 | set(REMOVE_ZERO "-") 130 | endif () 131 | set(TIME_STRING_12HR "%${REMOVE_ZERO}I:%M %p") 132 | set(TIME_STRING_24HR "%H:%M") 133 | set(DATE_STRING_LITTLE "%d %b") 134 | set(DATE_STRING_BIG "%b %${REMOVE_ZERO}d") 135 | 136 | # Configure main header file 137 | configure_file("${PROJECT_SOURCE_DIR}/config/launcher_config.h.in" "${PROJECT_BINARY_DIR}/launcher_config.h") 138 | 139 | #Build source files 140 | add_subdirectory("src") 141 | 142 | # Installation - Linux 143 | if (UNIX) 144 | set(INSTALL_DIR_BIN "${CMAKE_INSTALL_PREFIX}/bin") 145 | set(INSTALL_DIR_SHARE "${CMAKE_INSTALL_PREFIX}/share/${EXECUTABLE_TITLE}") 146 | set(INSTALL_DIR_DESKTOP "${CMAKE_INSTALL_PREFIX}/share/applications") 147 | set(INSTALL_DIR_CONFIGFILE "${INSTALL_DIR_SHARE}") 148 | set(INSTALL_DIR_ASSETS "${INSTALL_DIR_SHARE}") 149 | 150 | # Configure Debian packages 151 | if (PACKAGE STREQUAL "DEB") 152 | set(CPACK_DEBIAN_PACKAGE_NAME ${EXECUTABLE_TITLE}) 153 | set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT") 154 | set(CPACK_DEBIAN_PACKAGE_VERSION ${CMAKE_PROJECT_VERSION}) 155 | if (NOT CPACK_DEBIAN_PACKAGE_ARCHITECTURE) 156 | set (CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64") 157 | endif () 158 | set(CPACK_DEBIAN_PACKAGE_DEPENDS "libsdl2-2.0-0 (>= ${MIN_SDL_VERSION}), libsdl2-image-2.0-0 (>= ${MIN_SDL_IMAGE_VERSION}), libsdl2-ttf-2.0-0 (>= ${MIN_SDL_TTF_VERSION}), libc6 (>= ${MIN_GLIBC_VERSION}), libinih1") 159 | set(CPACK_DEBIAN_PACKAGE_MAINTAINER "complexlogic") 160 | set(CPACK_DEBIAN_PACKAGE_SECTION "video") 161 | set(CPACK_DEBIAN_ARCHIVE_TYPE "gnutar") 162 | set(CPACK_DEBIAN_COMPRESSION_TYPE "gzip") 163 | set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") 164 | endif () 165 | endif () 166 | 167 | # Installation - Windows 168 | if (WIN32) 169 | set(INSTALL_DIR_BIN "./") 170 | set(INSTALL_DIR_CONFIGFILE "./") 171 | set(INSTALL_DIR_ASSETS "./") 172 | 173 | configure_file(${PROJECT_SOURCE_DIR}/CHANGELOG ${PROJECT_BINARY_DIR}/CHANGELOG.txt) 174 | install(FILES ${PROJECT_BINARY_DIR}/CHANGELOG.txt DESTINATION "${INSTALL_DIR_BIN}") 175 | 176 | # Copy the VC Runtime DLL in case user doesn't have it installed 177 | set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) 178 | include(InstallRequiredSystemLibraries) 179 | foreach(lib ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}) 180 | string(FIND ${lib} "vcruntime140.dll" vcruntime) 181 | if (NOT vcruntime EQUAL -1) 182 | install(FILES ${lib} DESTINATION "./") 183 | endif () 184 | endforeach() 185 | 186 | # Set package 187 | if (NOT PACKAGE) 188 | set(PACKAGE "ZIP") 189 | endif () 190 | endif () 191 | 192 | # Installation - Common 193 | install(TARGETS ${EXECUTABLE_TITLE} DESTINATION "${INSTALL_DIR_BIN}") 194 | install(FILES "${PROJECT_BINARY_DIR}/install/config.ini" DESTINATION "${INSTALL_DIR_CONFIGFILE}") 195 | install(DIRECTORY "${PROJECT_SOURCE_DIR}/assets" DESTINATION "${INSTALL_DIR_ASSETS}") 196 | if (PACKAGE) 197 | set(CPACK_PACKAGE_NAME ${EXECUTABLE_TITLE}) 198 | set(CPACK_GENERATOR ${PACKAGE}) 199 | include(CPack) 200 | endif () 201 | 202 | #Configure Linux application icons and .desktop file 203 | if (UNIX) 204 | configure_file("${PROJECT_SOURCE_DIR}/config/launcher.desktop.in" "${PROJECT_BINARY_DIR}/install/${EXECUTABLE_TITLE}.desktop") 205 | install(FILES "${PROJECT_BINARY_DIR}/install/${EXECUTABLE_TITLE}.desktop" DESTINATION "${INSTALL_DIR_DESKTOP}") 206 | set(INSTALL_DIR_ICONS "${CMAKE_INSTALL_PREFIX}/share/icons") 207 | install(FILES "${PROJECT_SOURCE_DIR}/docs/flex-launcher.png" DESTINATION "${INSTALL_DIR_ICONS}/hicolor/48x48/apps") 208 | install(FILES "${PROJECT_SOURCE_DIR}/docs/flex-launcher.svg" DESTINATION "${INSTALL_DIR_ICONS}/hicolor/scalable/apps") 209 | endif () 210 | 211 | # Set paths for config files 212 | set(FONT_PREFIX "${PROJECT_BINARY_DIR}/assets/fonts/") 213 | set(ICONS_PREFIX "${PROJECT_BINARY_DIR}/assets/icons/") 214 | file(COPY "${PROJECT_SOURCE_DIR}/assets" DESTINATION ${PROJECT_BINARY_DIR}) 215 | 216 | # Generate build config file 217 | if (WIN32) 218 | string(REPLACE "/" "\\" FONT_PREFIX ${FONT_PREFIX}) 219 | string(REPLACE "/" "\\" ICONS_PREFIX ${ICONS_PREFIX}) 220 | endif () 221 | configure_file("${PROJECT_SOURCE_DIR}/config/config.ini.in" "${PROJECT_BINARY_DIR}/config.ini") 222 | 223 | # Generate install config file 224 | if (WIN32) 225 | set(PATH_ASSETS_RELATIVE "./assets") 226 | set(FONT_PREFIX "${PATH_ASSETS_RELATIVE}/fonts/") 227 | set(ICONS_PREFIX "${PATH_ASSETS_RELATIVE}/icons/") 228 | else () 229 | set(FONT_PREFIX "${INSTALL_DIR_ASSETS}/assets/fonts/") 230 | set(ICONS_PREFIX "${INSTALL_DIR_ASSETS}/assets/icons/") 231 | endif () 232 | 233 | if (WIN32) 234 | string(REPLACE "/" "\\" FONT_PREFIX ${FONT_PREFIX}) 235 | string(REPLACE "/" "\\" ICONS_PREFIX ${ICONS_PREFIX}) 236 | endif () 237 | configure_file("${PROJECT_SOURCE_DIR}/config/config.ini.in" "${PROJECT_BINARY_DIR}/install/config.ini") 238 | -------------------------------------------------------------------------------- /src/launcher.h: -------------------------------------------------------------------------------- 1 | // Color masking bit logic 2 | #if SDL_BYTEORDER == SDL_BIG_ENDIAN 3 | #define RMASK 0xff000000 4 | #define GMASK 0x00ff0000 5 | #define BMASK 0x0000ff00 6 | #define AMASK 0x000000ff 7 | #else 8 | #define RMASK 0x000000ff 9 | #define GMASK 0x0000ff00 10 | #define BMASK 0x00ff0000 11 | #define AMASK 0xff000000 12 | #endif 13 | #define COLOR_MASKS RMASK, GMASK, BMASK, AMASK 14 | 15 | // Launcher parameters 16 | #define MIN_FPS_LIMIT 10 17 | #define MIN_ICON_SIZE 32 18 | #define MAX_ICON_SIZE 1024 19 | #define MIN_RX_SIZE 0 20 | #define MAX_RX_SIZE 100 21 | #define PERCENT_MAX_CHARS 10 22 | #define GAMEPAD_DEADZONE 10000 23 | #define GAMEPAD_REPEAT_DELAY 500 24 | #define GAMEPAD_REPEAT_INTERVAL 25 25 | #define CLOCK_UPDATE_PERIOD 1000 26 | #define SCROLL_INDICATOR_HEIGHT 0.11F 27 | #define MAX_SCROLL_INDICATOR_OUTLINE 0.01F 28 | #define SCREEN_MARGIN 0.05F 29 | #define MAX_CLOCK_MARGIN 0.1F 30 | #define MIN_VCENTER 0.25F 31 | #define MAX_VCENTER 0.75F 32 | #define MIN_SLIDESHOW_IMAGE_DURATION 5000 33 | #define MAX_SLIDESHOW_IMAGE_DURATION 3600000 34 | #define MAX_SLIDESHOW_TRANSITION_TIME 3000 35 | #define MIN_SCREENSAVER_IDLE_TIME 3 36 | #define MAX_SCREENSAVER_IDLE_TIME 900 37 | #define SCREENSAVER_TRANSITION_TIME 1500 38 | #define APPLICATION_WAIT_PERIOD 100 39 | #define MIN_APPLICATION_TIMEOUT 3 40 | #define MAX_APPLICATION_TIMEOUT 30 41 | 42 | // Special commands 43 | #define SCMD_SELECT ":select" 44 | #define SCMD_SUBMENU ":submenu" 45 | #define SCMD_FORK ":fork" 46 | #define SCMD_EXIT ":exit" 47 | #define SCMD_LEFT ":left" 48 | #define SCMD_RIGHT ":right" 49 | #define SCMD_HOME ":home" 50 | #define SCMD_BACK ":back" 51 | #define SCMD_QUIT ":quit" 52 | #define SCMD_SHUTDOWN ":shutdown" 53 | #define SCMD_RESTART ":restart" 54 | #define SCMD_SLEEP ":sleep" 55 | 56 | typedef enum { 57 | MODE_SETTING_BACKGROUND, 58 | MODE_SETTING_ON_LAUNCH, 59 | MODE_SETTING_OVERSIZE, 60 | MODE_SETTING_ALIGNMENT, 61 | MODE_SETTING_TIME_FORMAT, 62 | MODE_SETTING_DATE_FORMAT 63 | } ModeSettingType; 64 | 65 | typedef enum { 66 | BACKGROUND_COLOR, 67 | BACKGROUND_IMAGE, 68 | BACKGROUND_SLIDESHOW, 69 | BACKGROUND_TRANSPARENT 70 | } ModeBackground; 71 | 72 | typedef enum { 73 | ON_LAUNCH_BLANK, 74 | ON_LAUNCH_NONE, 75 | ON_LAUNCH_QUIT 76 | } ModeOnLaunch; 77 | 78 | typedef enum { 79 | OVERSIZE_TRUNCATE, 80 | OVERSIZE_SHRINK, 81 | OVERSIZE_NONE 82 | } ModeOversize; 83 | 84 | typedef enum { 85 | ALIGNMENT_LEFT, 86 | ALIGNMENT_RIGHT, 87 | } Alignment; 88 | 89 | typedef enum { 90 | FORMAT_TIME_24HR, 91 | FORMAT_TIME_12HR, 92 | FORMAT_TIME_AUTO 93 | } TimeFormat; 94 | 95 | typedef enum { 96 | FORMAT_DATE_BIG, 97 | FORMAT_DATE_LITTLE, 98 | FORMAT_DATE_AUTO 99 | } DateFormat; 100 | 101 | typedef enum { 102 | TYPE_BUTTON, 103 | TYPE_AXIS_POS, 104 | TYPE_AXIS_NEG, 105 | } ControlType; 106 | 107 | typedef enum { 108 | DIRECTION_LEFT, 109 | DIRECTION_RIGHT, 110 | } Direction; 111 | 112 | // Program states 113 | typedef struct { 114 | bool application_launching; 115 | bool application_running; 116 | bool has_focus; 117 | bool slideshow_transition; 118 | bool slideshow_background_rendering; 119 | bool slideshow_background_ready; 120 | bool slideshow_paused; 121 | bool screensaver_active; 122 | bool screensaver_transition; 123 | bool clock_rendering; 124 | bool clock_ready; 125 | } State; 126 | 127 | // Timing information 128 | typedef struct { 129 | Uint32 main; 130 | Uint32 program_start; 131 | Uint32 application_launched; 132 | Uint32 slideshow_load; 133 | Uint32 last_input; 134 | Uint32 clock_update; 135 | Uint32 application_exited; 136 | } Ticks; 137 | 138 | // Linked list for menu entries 139 | typedef struct entry { 140 | char *title; 141 | char *icon_path; 142 | char *icon_selected_path; 143 | char *cmd; 144 | SDL_Texture *icon; 145 | SDL_Texture *icon_selected; 146 | SDL_Rect icon_rect; 147 | SDL_Texture *title_texture; 148 | SDL_Rect text_rect; 149 | int title_offset; 150 | struct entry *next; 151 | struct entry *previous; 152 | } Entry; 153 | 154 | // Linked list for menus 155 | typedef struct menu { 156 | char *name; 157 | unsigned int num_entries; 158 | bool rendered; 159 | unsigned int page; 160 | unsigned int highlight_position; 161 | Entry *first_entry; 162 | Entry *root_entry; 163 | Entry *last_selected_entry; 164 | struct menu *next; 165 | struct menu *back; 166 | } Menu; 167 | 168 | typedef struct gamepad { 169 | SDL_GameController *controller; 170 | int device_index; 171 | int id; 172 | struct gamepad *previous; 173 | struct gamepad *next; 174 | } Gamepad; 175 | 176 | // Linked list of gamepad controls 177 | typedef struct gamepad_control { 178 | ControlType type; 179 | int index; 180 | Uint32 repeat; 181 | const char *label; 182 | char *cmd; 183 | struct gamepad_control *next; 184 | } GamepadControl; 185 | 186 | // Linked list of hotkeys 187 | typedef struct hotkey { 188 | SDL_Keycode keycode; 189 | char *cmd; 190 | struct hotkey *next; 191 | } Hotkey; 192 | 193 | // Struct for the geometry parameters of the onscreen buttons 194 | typedef struct { 195 | int screen_width; 196 | int screen_height; 197 | int screen_margin; 198 | int font_height; 199 | int x_margin; // Distance between left edge of screen and x coordinate of root_entry icon 200 | int y_margin; // Distance between top edge of screen and y coordinate of all entry icons 201 | int x_advance; // Distance between icon x coordinate of adjacent entries 202 | int num_buttons; // Number of buttons shown on the screen 203 | } Geometry; 204 | 205 | // Struct for highlight 206 | typedef struct { 207 | SDL_Texture *texture; 208 | SDL_Rect rect; 209 | } Highlight; 210 | 211 | //Struct for scroll indicators 212 | typedef struct { 213 | SDL_Texture *texture; 214 | SDL_Rect rect_right; 215 | SDL_Rect rect_left; 216 | } Scroll; 217 | 218 | // Slideshow 219 | typedef struct { 220 | char **images; 221 | int *order; 222 | int i; 223 | int num_images; 224 | float transition_alpha; 225 | float transition_change_rate; 226 | SDL_Surface *transition_surface; 227 | SDL_Texture *transition_texture; 228 | } Slideshow; 229 | 230 | // Screensaver 231 | typedef struct { 232 | float alpha; 233 | float alpha_end_value; 234 | float transition_change_rate; 235 | SDL_Texture *texture; 236 | } Screensaver; 237 | 238 | // Configuration settings 239 | typedef struct { 240 | char *default_menu; 241 | unsigned int max_buttons; 242 | bool vsync; 243 | int fps_limit; 244 | Uint32 application_timeout; 245 | ModeBackground background_mode; // Defines image or color background mode 246 | SDL_Color background_color; // Background color 247 | SDL_Color chroma_key_color; 248 | char *background_image; // Path to background image 249 | char *slideshow_directory; 250 | bool background_overlay; 251 | SDL_Color background_overlay_color; 252 | char background_overlay_opacity[PERCENT_MAX_CHARS]; 253 | Uint16 icon_size; 254 | int icon_spacing; 255 | char icon_spacing_str[PERCENT_MAX_CHARS]; 256 | bool titles_enabled; 257 | char *title_font_path; // Path to title TTF font file 258 | unsigned int title_font_size; 259 | SDL_Color title_font_color; // Color struct for title text 260 | bool title_shadows; 261 | SDL_Color title_shadow_color; 262 | char title_opacity[PERCENT_MAX_CHARS]; 263 | ModeOversize title_oversize_mode; 264 | int title_padding; 265 | bool highlight; 266 | SDL_Color highlight_fill_color; 267 | SDL_Color highlight_outline_color; 268 | int highlight_outline_size; 269 | char highlight_fill_opacity[PERCENT_MAX_CHARS]; 270 | char highlight_outline_opacity[PERCENT_MAX_CHARS]; 271 | unsigned int highlight_rx; 272 | int highlight_vpadding; 273 | int highlight_hpadding; 274 | char vcenter[PERCENT_MAX_CHARS]; 275 | bool scroll_indicators; 276 | SDL_Color scroll_indicator_fill_color; 277 | int scroll_indicator_outline_size; 278 | SDL_Color scroll_indicator_outline_color; 279 | char scroll_indicator_opacity[PERCENT_MAX_CHARS]; 280 | bool wrap_entries; 281 | bool reset_on_back; 282 | bool mouse_select; 283 | bool inhibit_os_screensaver; 284 | char *startup_cmd; 285 | char *quit_cmd; 286 | ModeOnLaunch on_launch; 287 | bool screensaver_enabled; 288 | Uint32 screensaver_idle_time; 289 | char screensaver_intensity_str[PERCENT_MAX_CHARS]; 290 | bool screensaver_pause_slideshow; 291 | bool gamepad_enabled; 292 | int gamepad_device; 293 | char *gamepad_mappings_file; 294 | bool debug; 295 | char *exe_path; 296 | Menu *first_menu; 297 | size_t num_menus; 298 | bool clock_enabled; 299 | bool clock_show_date; 300 | Alignment clock_alignment; 301 | char *clock_font_path; 302 | char clock_margin_str[PERCENT_MAX_CHARS]; 303 | int clock_margin; 304 | SDL_Color clock_font_color; 305 | char clock_opacity[PERCENT_MAX_CHARS]; 306 | unsigned int clock_font_size; 307 | bool clock_shadows; 308 | SDL_Color clock_shadow_color; 309 | TimeFormat clock_time_format; 310 | DateFormat clock_date_format; 311 | bool clock_include_weekday; 312 | Uint32 slideshow_image_duration; 313 | Uint32 slideshow_transition_time; 314 | } Config; 315 | 316 | void quit_slideshow(void); 317 | void set_draw_color(void); 318 | void quit(int status); 319 | void print_version(FILE *stream); 320 | -------------------------------------------------------------------------------- /src/clock.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "launcher.h" 10 | #include 11 | #include "util.h" 12 | #include "image.h" 13 | #include "clock.h" 14 | #include "debug.h" 15 | #include "platform/platform.h" 16 | 17 | static void calculate_text_metrics(TTF_Font *font, const char *text, int *h, int *x_offset); 18 | static void calculate_clock_geometry(Clock *clk); 19 | static void format_time(Clock *clk); 20 | static void format_date(Clock *clk); 21 | static void calculate_clock_positioning(Clock *clk); 22 | 23 | extern Config config; 24 | extern State state; 25 | extern Geometry geo; 26 | 27 | // A function to calculate height and x offset of text 28 | static void calculate_text_metrics(TTF_Font *font, const char *text, int *h, int *x_offset) 29 | { 30 | int ymin = 0; 31 | int ymax = 0; 32 | int xmin, xmax, xadvance; 33 | int current_ymin, current_ymax; 34 | char *p = (char*) text; 35 | Uint16 code_point; 36 | int bytes; 37 | 38 | // Get text height, x offset 39 | while (*p != '\0') { 40 | code_point = get_unicode_code_point(p, &bytes); 41 | TTF_GlyphMetrics(font, 42 | code_point, 43 | &xmin, 44 | &xmax, 45 | ¤t_ymin, 46 | ¤t_ymax, 47 | &xadvance 48 | ); 49 | if (current_ymax > ymax) 50 | ymax = current_ymax; 51 | if (current_ymin < ymin) 52 | ymin = current_ymin; 53 | if (p == text && config.clock_alignment == ALIGNMENT_LEFT) 54 | *x_offset = xmin; 55 | 56 | p += bytes; 57 | } 58 | if (config.clock_alignment == ALIGNMENT_RIGHT) 59 | *x_offset = xadvance - xmax; 60 | *h = ymax - ymin; 61 | } 62 | 63 | // A function to calculate the spacing and offsets of the clock text 64 | static void calculate_clock_geometry(Clock *clk) 65 | { 66 | int line_skip = TTF_FontLineSkip(clk->text_info.font); 67 | int h_time, h_date; 68 | 69 | // Calculate text height and x offset 70 | calculate_text_metrics(clk->text_info.font, 71 | clk->time_string, 72 | &h_time, 73 | &clk->x_offset_time 74 | ); 75 | 76 | if (config.clock_show_date) { 77 | calculate_text_metrics(clk->text_info.font, 78 | clk->date_string, 79 | &h_date, 80 | &clk->x_offset_date 81 | ); 82 | 83 | int spacing = (int) (CLOCK_SPACING_FACTOR * (float) h_time); 84 | clk->y_advance = spacing + h_time; 85 | } 86 | 87 | // Calculate y offset from margin 88 | clk->y_offset = (line_skip - h_time) / 2; 89 | } 90 | 91 | // A function to get the current time from the operating system 92 | void get_time(Clock *clk) 93 | { 94 | int previous_min = 0; 95 | int previous_day = 0; 96 | if (clk->time_info != NULL) { 97 | previous_min = clk->time_info->tm_min; 98 | previous_day = clk->time_info->tm_mday; 99 | } 100 | 101 | // Get current time 102 | time(&clk->current_time); 103 | clk->time_info = localtime(&clk->current_time); 104 | 105 | // Set render flags if time and/or date changed 106 | if (clk->time_info == NULL || previous_min != clk->time_info->tm_min) { 107 | clk->render_time = true; 108 | if (clk->time_info == NULL || previous_day != clk->time_info->tm_mday) 109 | clk->render_date = true; 110 | } 111 | } 112 | 113 | // A function to format the current time according to user settings 114 | static void format_time(Clock *clk) 115 | { 116 | char *format = NULL; 117 | if (clk->time_format == FORMAT_TIME_24HR) 118 | format = TIME_STRING_24HR; 119 | else 120 | format = TIME_STRING_12HR; 121 | strftime(clk->time_string, 122 | sizeof(clk->time_string), 123 | format, 124 | clk->time_info 125 | ); 126 | } 127 | 128 | // A function to format the current date according to user settings 129 | static void format_date(Clock *clk) 130 | { 131 | char *format = NULL; 132 | 133 | // Get date format 134 | if (clk->date_format == FORMAT_DATE_LITTLE) 135 | format = DATE_STRING_LITTLE; 136 | else 137 | format = DATE_STRING_BIG; 138 | char weekday[MAX_CLOCK_CHARS + 1]; 139 | char date[MAX_CLOCK_CHARS + 1]; 140 | size_t bytes = sizeof(clk->date_string); 141 | 142 | // Get weekday name 143 | if (config.clock_include_weekday) { 144 | strftime(weekday, 145 | sizeof(weekday), 146 | "%a ", 147 | clk->time_info 148 | ); 149 | } 150 | else 151 | weekday[0] = '\0'; 152 | copy_string(clk->date_string, 153 | weekday, 154 | sizeof(clk->date_string) 155 | ); 156 | bytes -= strlen(weekday); 157 | if (bytes) { 158 | // Get date 159 | strftime(date, 160 | sizeof(date), 161 | format, 162 | clk->time_info 163 | ); 164 | bytes -= strlen(date); 165 | strncat(clk->date_string, 166 | date, 167 | bytes 168 | ); 169 | } 170 | } 171 | 172 | // A function to calculate the x and y coordinates of the clock text 173 | static void calculate_clock_positioning(Clock *clk) 174 | { 175 | if (config.clock_alignment == ALIGNMENT_LEFT) { 176 | clk->time_rect.x = config.clock_margin - clk->x_offset_time; 177 | if (config.clock_show_date) 178 | clk->date_rect.x = config.clock_margin - clk->x_offset_date; 179 | } 180 | else { 181 | clk->time_rect.x = geo.screen_width - config.clock_margin - clk->time_rect.w + clk->x_offset_time; 182 | if (config.clock_show_date) 183 | clk->date_rect.x = geo.screen_width - config.clock_margin - clk->date_rect.w + clk->x_offset_date; 184 | } 185 | clk->time_rect.y = config.clock_margin - clk->y_offset; 186 | if (config.clock_show_date) 187 | clk->date_rect.y = clk->time_rect.y + clk->y_advance; 188 | } 189 | 190 | // A function to initialize the clock 191 | void init_clock(Clock *clk) 192 | { 193 | // Initialize clock structure 194 | clk->text_info = (TextInfo) { 195 | .font = NULL, 196 | .font_size = (int) config.clock_font_size, 197 | .font_path = &config.clock_font_path, 198 | .color = &config.clock_font_color, 199 | .shadow = config.clock_shadows, 200 | .oversize_mode = OVERSIZE_NONE 201 | }; 202 | clk->time_format = config.clock_time_format; 203 | clk->date_format = config.clock_date_format; 204 | clk->time_info = NULL; 205 | if (config.clock_shadows) { 206 | clk->text_info.shadow_color = &config.clock_shadow_color; 207 | calculate_shadow_alpha(clk->text_info); 208 | } 209 | else 210 | clk->text_info.shadow_color = NULL; 211 | 212 | // Load the font 213 | int error = load_font(&clk->text_info, FILENAME_DEFAULT_CLOCK_FONT); 214 | if (error) { 215 | config.clock_enabled = false; 216 | return; 217 | } 218 | 219 | // Get time and format it into a string 220 | get_time(clk); 221 | if (clk->time_format == FORMAT_TIME_AUTO || clk->date_format == FORMAT_DATE_AUTO) { 222 | char region[3]; 223 | memset(region, '\0', sizeof(region)); 224 | get_region(region); 225 | if (clk->time_format == FORMAT_TIME_AUTO) 226 | clk->time_format = get_time_format(region); 227 | if (config.clock_show_date && clk->date_format == FORMAT_DATE_AUTO) 228 | clk->date_format = get_date_format(region); 229 | } 230 | 231 | // Render the time and date 232 | format_time(clk); 233 | clk->time_texture = render_text_texture(clk->time_string, 234 | &clk->text_info, 235 | &clk->time_rect, 236 | NULL 237 | ); 238 | if (config.clock_show_date) { 239 | format_date(clk); 240 | clk->date_texture = render_text_texture(clk->date_string, 241 | &clk->text_info, 242 | &clk->date_rect, 243 | NULL 244 | ); 245 | } 246 | 247 | // Calculate geometry 248 | calculate_clock_geometry(clk); 249 | calculate_clock_positioning(clk); 250 | clk->render_time = false; 251 | clk->render_date = false; 252 | } 253 | 254 | // A function to render the time to image 255 | void render_clock(Clock *clk) 256 | { 257 | format_time(clk); 258 | clk->time_surface = render_text(clk->time_string, 259 | &clk->text_info, 260 | &clk->time_rect, 261 | NULL 262 | ); 263 | if (clk->render_date) { 264 | format_date(clk); 265 | clk->date_surface = render_text(clk->date_string, 266 | &clk->text_info, 267 | &clk->date_rect, 268 | NULL 269 | ); 270 | calculate_clock_geometry(clk); 271 | } 272 | calculate_clock_positioning(clk); 273 | state.clock_ready = true; 274 | } 275 | 276 | // A functio nto render the time to image in a separate thread 277 | int render_clock_async(void *data) 278 | { 279 | Clock *clk = (Clock*) data; 280 | render_clock(clk); 281 | return 0; 282 | } 283 | 284 | // A function to get the time format for a region 285 | TimeFormat get_time_format(const char *region) 286 | { 287 | const char *countries[] = { 288 | "US", 289 | "CA", 290 | "GB", 291 | "AU", 292 | "NZ", 293 | "IN" 294 | }; 295 | TimeFormat format = FORMAT_TIME_24HR; 296 | for (size_t i = 0; i < sizeof(countries) / sizeof(countries[0]); i++) { 297 | if (!strcmp(region, countries[i])) { 298 | format = FORMAT_TIME_12HR; 299 | break; 300 | } 301 | } 302 | return format; 303 | } 304 | 305 | // A function to get the date format for a region 306 | DateFormat get_date_format(const char *region) 307 | { 308 | const char *countries[] = { 309 | "US", 310 | "JP", 311 | "CN" 312 | }; 313 | DateFormat format = FORMAT_DATE_LITTLE; 314 | for (size_t i = 0; i < sizeof(countries) / sizeof(countries[0]); i++) { 315 | if (!strcmp(region, countries[i])) { 316 | format = FORMAT_DATE_BIG; 317 | break; 318 | } 319 | } 320 | return format; 321 | } 322 | -------------------------------------------------------------------------------- /src/platform/win32.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "../launcher.h" 11 | #include 12 | #include "platform.h" 13 | #include "../util.h" 14 | #include "../debug.h" 15 | #include "slideshow.h" 16 | 17 | static void parse_command(char *cmd, char *file, size_t file_size, char **params); 18 | static char *path_basename(const char *path); 19 | static bool is_browser(const char *exe_basename); 20 | static UINT sdl_to_win32_keycode(SDL_Keycode keycode); 21 | static bool get_shutdown_privilege(void); 22 | 23 | extern Config config; 24 | extern SDL_SysWMinfo wm_info; 25 | bool has_shutdown_privilege = false; 26 | UINT exit_hotkey = 0; 27 | 28 | 29 | // A function to determine if a file exists on the filesystem 30 | bool file_exists(const char *path) 31 | { 32 | return _access(path, 4) ? false : true; 33 | } 34 | 35 | // A function to determine if a directory exists on the filesystem 36 | bool directory_exists(const char *path) 37 | { 38 | if (!file_exists(path)) 39 | return false; 40 | else { 41 | DWORD attributes = GetFileAttributesA(path); 42 | if (attributes & FILE_ATTRIBUTE_DIRECTORY) 43 | return true; 44 | else 45 | return false; 46 | } 47 | } 48 | 49 | // A function that parses the command string into a file and parameters 50 | static void parse_command(char *cmd, char *file, size_t file_size, char **params) 51 | { 52 | char *start = NULL; 53 | char *quote_begin = NULL; 54 | char *quote_end = NULL; 55 | char *p = cmd; 56 | file[0] = '\0'; 57 | 58 | // Skip any whitespace at beginning of command 59 | while (*p == ' ') 60 | p++; 61 | start = p; 62 | 63 | // Check for quote, in which case ignore spaces until the end quote is detecetd 64 | if (*p == '"') 65 | quote_begin = p; 66 | 67 | while (*p != '\0') { 68 | // If the quote is complete, copy the file 69 | if (*p == '"' && p != quote_begin) { 70 | quote_end = p; 71 | *p = '\0'; 72 | copy_string(file, quote_begin + 1, file_size); 73 | } 74 | 75 | else if (*p == ' ') { 76 | // If a space was detected but there hasn't been a quote detected yet, this is the end of the file 77 | if (!quote_begin) { 78 | *p = '\0'; 79 | copy_string(file, start, file_size); 80 | p++; 81 | 82 | // Skip any preceding white space for parameters 83 | while (*p == ' ') 84 | p++; 85 | 86 | // Copy parameters 87 | if (*p != '\0') 88 | *params = strdup(p); 89 | break; 90 | } 91 | 92 | // If a space was detected after the quote 93 | else if (quote_begin && quote_end) { 94 | // Skip any preceding white space for parameters 95 | while (*p == ' ') 96 | p++; 97 | 98 | // Copy rest of command as parameters 99 | if (*p != '\0') 100 | *params = strdup(p); 101 | break; 102 | } 103 | } 104 | p++; 105 | } 106 | 107 | // If there were no quotes or spaces, copy whole command into file buffer 108 | if (start && file[0] == '\0') 109 | copy_string(file, start, file_size); 110 | } 111 | 112 | void set_foreground_window() 113 | { 114 | SetForegroundWindow(wm_info.info.win.window); 115 | } 116 | 117 | void make_window_transparent() 118 | { 119 | HWND hwnd = wm_info.info.win.window; 120 | SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLong(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED); 121 | SetLayeredWindowAttributes(hwnd, 122 | RGB(config.chroma_key_color.r, config.chroma_key_color.g, config.chroma_key_color.b), 123 | 0, 124 | LWA_COLORKEY 125 | ); 126 | } 127 | 128 | // When the window is transparent, we need to hide the cursor behind the non-transparent icon 129 | void hide_cursor(Entry *entry) 130 | { 131 | SetCursorPos(entry->icon_rect.x + entry->icon_rect.w / 2, 132 | entry->icon_rect.y + entry->icon_rect.h / 2 133 | ); 134 | } 135 | 136 | // A function to launch an application 137 | bool start_process(char *cmd, bool application) 138 | { 139 | bool ret = false; 140 | char file[MAX_PATH_CHARS + 1]; 141 | char *params = NULL; 142 | int cmd_show = application ? SW_SHOWMAXIMIZED : SW_HIDE; 143 | 144 | // Parse command into file and parameters strings 145 | parse_command(cmd, file, sizeof(file), ¶ms); 146 | 147 | // Set up info struct 148 | SHELLEXECUTEINFOA info = { 149 | .cbSize = sizeof(SHELLEXECUTEINFOA), 150 | .fMask = SEE_MASK_NOCLOSEPROCESS, 151 | .hwnd = NULL, 152 | .lpVerb = "open", 153 | .lpFile = file, 154 | .lpParameters = params, 155 | .lpDirectory = NULL, 156 | .nShow = cmd_show, 157 | .lpIDList = NULL, 158 | .lpClass = NULL, 159 | }; 160 | 161 | BOOL successful = ShellExecuteExA(&info); 162 | if (!application) 163 | ret = true; 164 | else { 165 | // Go down in the window stack so the launched application can take focus 166 | if (successful) { 167 | HWND hwnd = wm_info.info.win.window; 168 | SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOREDRAW | SWP_NOSIZE | SWP_NOMOVE); 169 | ret = true; 170 | } 171 | else { 172 | log_debug("Failed to launch command"); 173 | ret = false; 174 | } 175 | } 176 | free(params); 177 | return ret; 178 | } 179 | 180 | // A function to scan the slideshow directory for image files 181 | void scan_slideshow_directory(Slideshow *slideshow, const char *directory) 182 | { 183 | WIN32_FIND_DATAA data; 184 | HANDLE handle; 185 | char file_search[MAX_PATH_CHARS + 1]; 186 | char file_output[MAX_PATH_CHARS + 1]; 187 | char extension[10]; 188 | 189 | // Generate a wildcard file search string for all supported image file extensions 190 | for (int i = 0; i < NUM_IMAGE_EXTENSIONS; i++) { 191 | copy_string(extension, "*", sizeof(extension)); 192 | strcat(extension, extensions[i]); 193 | join_paths(file_search, sizeof(file_search), 2, directory, extension); 194 | 195 | // Store every result into the slideshow struct 196 | handle = FindFirstFileA(file_search, &data); 197 | if (handle != INVALID_HANDLE_VALUE) { 198 | do { 199 | join_paths(file_output, sizeof(file_output), 2, directory, data.cFileName); 200 | slideshow->images = realloc(slideshow->images, (slideshow->num_images + 1) * sizeof(char*)); 201 | slideshow->images[slideshow->num_images] = strdup(file_output); 202 | slideshow->num_images++; 203 | } while (FindNextFileA(handle, &data) != 0); 204 | } 205 | } 206 | } 207 | 208 | // A function to get the 2 letter region code 209 | void get_region(char *buffer) 210 | { 211 | GEOID geo_id = GetUserGeoID(GEOCLASS_NATION); 212 | GetGeoInfoA(geo_id, GEO_ISO2, buffer, 3, 0); 213 | } 214 | 215 | // A function to shutdown the computer 216 | void scmd_shutdown() 217 | { 218 | if (!has_shutdown_privilege) { 219 | bool successful = get_shutdown_privilege(); 220 | if (!successful) 221 | return; 222 | } 223 | InitiateShutdownA(NULL, 224 | NULL, 225 | 0, 226 | SHUTDOWN_FORCE_OTHERS | SHUTDOWN_POWEROFF | SHUTDOWN_HYBRID, 227 | SHTDN_REASON_MAJOR_OTHER | SHTDN_REASON_MINOR_OTHER 228 | ); 229 | } 230 | 231 | // A function to restart the computer 232 | void scmd_restart() 233 | { 234 | if (!has_shutdown_privilege) { 235 | bool successful = get_shutdown_privilege(); 236 | if (!successful) 237 | return; 238 | } 239 | InitiateShutdownA(NULL, 240 | NULL, 241 | 0, 242 | SHUTDOWN_FORCE_OTHERS | SHUTDOWN_RESTART | SHUTDOWN_HYBRID, 243 | SHTDN_REASON_MAJOR_OTHER | SHTDN_REASON_MINOR_OTHER 244 | ); 245 | } 246 | 247 | // A function to put the computer to sleep 248 | void scmd_sleep() 249 | { 250 | if (!has_shutdown_privilege) { 251 | bool successful = get_shutdown_privilege(); 252 | if (!successful) 253 | return; 254 | } 255 | SetSuspendState(FALSE, FALSE, FALSE); 256 | } 257 | 258 | // A function to get the shutdown privilege from Windows 259 | static bool get_shutdown_privilege() 260 | { 261 | HANDLE token = NULL; 262 | BOOL ret = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token); 263 | if (!ret) { 264 | log_error("Could not open process token"); 265 | return false; 266 | } 267 | LUID luid; 268 | ret = LookupPrivilegeValueA(NULL, SE_SHUTDOWN_NAME, &luid); 269 | if (!ret) { 270 | log_error("Failed to lookup privilege"); 271 | CloseHandle(token); 272 | return false; 273 | } 274 | TOKEN_PRIVILEGES tp; 275 | tp.PrivilegeCount = 1; 276 | tp.Privileges[0].Luid = luid; 277 | tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 278 | ret = AdjustTokenPrivileges(token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL); 279 | if (!ret) { 280 | log_error("Failed to adjust token privileges"); 281 | CloseHandle(token); 282 | return false; 283 | } 284 | has_shutdown_privilege = true; 285 | CloseHandle(token); 286 | return true; 287 | } 288 | 289 | // A function to check if there is an exit hotkey 290 | bool has_exit_hotkey() 291 | { 292 | if (exit_hotkey) 293 | return true; 294 | return false; 295 | } 296 | 297 | // A function to store an exit hotkey 298 | void set_exit_hotkey(SDL_Keycode keycode) 299 | { 300 | if (exit_hotkey) 301 | return; 302 | exit_hotkey = sdl_to_win32_keycode(keycode); 303 | if (!exit_hotkey) 304 | log_error("Invalid exit hotkey keycode %X", keycode); 305 | } 306 | 307 | // A function to register the exit hotkey with Windows 308 | void register_exit_hotkey() 309 | { 310 | BOOL ret = RegisterHotKey(wm_info.info.win.window, 1, 0, exit_hotkey); 311 | if (!ret) { 312 | exit_hotkey = 0; 313 | log_error("Failed to register exit hotkey with Windows"); 314 | } 315 | } 316 | 317 | // A function to check if the exit hotkey was pressed, and close the active window if so 318 | void check_exit_hotkey(SDL_SysWMmsg *msg) 319 | { 320 | if (msg->msg.win.msg == WM_HOTKEY) { 321 | log_debug("Exit hotkey detected"); 322 | HWND hwnd = GetForegroundWindow(); 323 | if (hwnd == NULL) { 324 | log_error("Could not get top window"); 325 | return; 326 | } 327 | PostMessage(hwnd, WM_CLOSE, 0, 0); 328 | } 329 | } 330 | 331 | // A function to convert an SDL keycode to a WIN32 virtual keycode 332 | static UINT sdl_to_win32_keycode(SDL_Keycode keycode) 333 | { 334 | #include "keycode_convert.h" // Import the conversion table 335 | for (int i = 0; i < sizeof(table) / sizeof(table[0]); i++) { 336 | if (table[i].sdl == keycode) 337 | return table[i].win; 338 | } 339 | return 0; 340 | } 341 | -------------------------------------------------------------------------------- /docs/_sass/jekyll-theme-slate.scss: -------------------------------------------------------------------------------- 1 | @import "rouge-github"; 2 | 3 | /******************************************************************************* 4 | MeyerWeb Reset 5 | *******************************************************************************/ 6 | 7 | html, body, div, span, applet, object, iframe, 8 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 9 | a, abbr, acronym, address, big, cite, code, 10 | del, dfn, em, img, ins, kbd, q, s, samp, 11 | small, strike, strong, sub, tt, var, 12 | b, u, i, center, 13 | dl, dt, dd, ol, ul, li, 14 | fieldset, form, label, legend, 15 | table, caption, tbody, tfoot, thead, tr, th, td, 16 | article, aside, canvas, details, embed, 17 | figure, figcaption, footer, header, hgroup, 18 | menu, nav, output, ruby, section, summary, 19 | time, mark, audio, video { 20 | margin: 0; 21 | padding: 0; 22 | border: 0; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | 27 | sup { 28 | font-size: 65%; 29 | vertical-align: super; 30 | font-weight: bold; 31 | } 32 | 33 | /* HTML5 display-role reset for older browsers */ 34 | article, aside, details, figcaption, figure, 35 | footer, header, hgroup, menu, nav, section { 36 | display: block; 37 | } 38 | 39 | ol, ul { 40 | list-style: none; 41 | } 42 | 43 | table { 44 | border-collapse: collapse; 45 | border-spacing: 0; 46 | } 47 | 48 | /******************************************************************************* 49 | Theme Styles 50 | *******************************************************************************/ 51 | 52 | body { 53 | box-sizing: border-box; 54 | color:#373737; 55 | background: #212121; 56 | font-size: 16px; 57 | font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif; 58 | line-height: 1.5; 59 | -webkit-font-smoothing: antialiased; 60 | } 61 | 62 | h1, h2, h3, h4, h5, h6 { 63 | margin: 10px 0; 64 | font-weight: 700; 65 | color:#222222; 66 | font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif; 67 | letter-spacing: -1px; 68 | } 69 | 70 | h1 { 71 | font-size: 36px; 72 | font-weight: 700; 73 | } 74 | 75 | h2 { 76 | padding-bottom: 10px; 77 | font-size: 32px; 78 | background: url('../images/bg_hr.png') repeat-x bottom; 79 | } 80 | 81 | h3 { 82 | font-size: 24px; 83 | } 84 | 85 | h4 { 86 | font-size: 21px; 87 | } 88 | 89 | h5 { 90 | font-size: 18px; 91 | } 92 | 93 | h6 { 94 | font-size: 16px; 95 | } 96 | 97 | nav ul { 98 | display: flex; 99 | list-style: none; 100 | padding-left: 0px; 101 | margin-top: 5px; 102 | margin-bottom: 0px; 103 | } 104 | 105 | nav ul li { 106 | margin-right: 5px; 107 | font-size: 20px; 108 | } 109 | 110 | p { 111 | margin: 10px 0 15px 0; 112 | } 113 | 114 | footer p { 115 | color: #f2f2f2; 116 | } 117 | 118 | a { 119 | text-decoration: none; 120 | color: #0F79D0; 121 | text-shadow: none; 122 | 123 | transition: color 0.5s ease; 124 | transition: text-shadow 0.5s ease; 125 | -webkit-transition: color 0.5s ease; 126 | -webkit-transition: text-shadow 0.5s ease; 127 | -moz-transition: color 0.5s ease; 128 | -moz-transition: text-shadow 0.5s ease; 129 | -o-transition: color 0.5s ease; 130 | -o-transition: text-shadow 0.5s ease; 131 | -ms-transition: color 0.5s ease; 132 | -ms-transition: text-shadow 0.5s ease; 133 | } 134 | 135 | a:hover, a:focus { 136 | text-decoration: underline; 137 | } 138 | 139 | footer a { 140 | color: #F2F2F2; 141 | text-decoration: underline; 142 | } 143 | 144 | em, cite { 145 | font-style: italic; 146 | } 147 | 148 | strong { 149 | font-weight: bold; 150 | } 151 | 152 | img { 153 | position: relative; 154 | margin: 0 auto; 155 | max-width: 800px; 156 | padding: 5px; 157 | margin: 10px 0 10px 0; 158 | border: 1px solid #ebebeb; 159 | 160 | box-shadow: 0 0 5px #ebebeb; 161 | -webkit-box-shadow: 0 0 5px #ebebeb; 162 | -moz-box-shadow: 0 0 5px #ebebeb; 163 | -o-box-shadow: 0 0 5px #ebebeb; 164 | -ms-box-shadow: 0 0 5px #ebebeb; 165 | } 166 | 167 | p img { 168 | display: inline; 169 | margin: 0; 170 | padding: 0; 171 | vertical-align: middle; 172 | text-align: center; 173 | border: none; 174 | } 175 | 176 | pre, code { 177 | color: #222; 178 | background-color: #fff; 179 | 180 | font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace; 181 | font-size: 0.875em; 182 | 183 | border-radius: 2px; 184 | -moz-border-radius: 2px; 185 | -webkit-border-radius: 2px; 186 | } 187 | 188 | pre { 189 | padding: 10px; 190 | box-shadow: 0 0 10px rgba(0,0,0,.1); 191 | overflow: auto; 192 | } 193 | 194 | code { 195 | padding: 3px; 196 | margin: 0 3px; 197 | box-shadow: 0 0 10px rgba(0,0,0,.1); 198 | } 199 | 200 | pre code { 201 | display: block; 202 | box-shadow: none; 203 | } 204 | 205 | blockquote { 206 | color: #666; 207 | margin-bottom: 20px; 208 | padding: 0 0 0 20px; 209 | border-left: 3px solid #bbb; 210 | } 211 | 212 | 213 | ul, ol, dl { 214 | margin-bottom: 15px 215 | } 216 | 217 | ul { 218 | list-style-position: inside; 219 | list-style: disc; 220 | padding-left: 20px; 221 | } 222 | 223 | ol { 224 | list-style-position: inside; 225 | list-style: decimal; 226 | padding-left: 20px; 227 | } 228 | 229 | dl dt { 230 | font-weight: bold; 231 | } 232 | 233 | dl dd { 234 | padding-left: 20px; 235 | font-style: italic; 236 | } 237 | 238 | dl p { 239 | padding-left: 20px; 240 | font-style: italic; 241 | } 242 | 243 | hr { 244 | height: 1px; 245 | margin-bottom: 5px; 246 | border: none; 247 | background: url('../images/bg_hr.png') repeat-x center; 248 | } 249 | 250 | table { 251 | border: 1px solid #373737; 252 | margin-bottom: 20px; 253 | text-align: left; 254 | } 255 | 256 | th { 257 | font-family: 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif; 258 | padding: 10px; 259 | background: #373737; 260 | color: #fff; 261 | } 262 | 263 | td { 264 | padding: 10px; 265 | border: 1px solid #373737; 266 | } 267 | 268 | form { 269 | background: #f2f2f2; 270 | padding: 20px; 271 | } 272 | 273 | kbd { 274 | background-color: #fafbfc; 275 | border: 1px solid #c6cbd1; 276 | border-bottom-color: #959da5; 277 | border-radius: 3px; 278 | box-shadow: inset 0 -1px 0 #959da5; 279 | color: #444d56; 280 | display: inline-block; 281 | font-size: 11px; 282 | line-height: 11px; 283 | padding: 3px 5px; 284 | vertical-align: middle; 285 | } 286 | 287 | /******************************************************************************* 288 | Full-Width Styles 289 | *******************************************************************************/ 290 | 291 | .outer { 292 | width: 100%; 293 | } 294 | 295 | .inner { 296 | position: relative; 297 | max-width: 800px; 298 | padding: 20px 10px; 299 | margin: 0 auto; 300 | } 301 | 302 | .download { 303 | border: none; 304 | vertical-align: top; 305 | box-shadow: none; 306 | padding-bottom: 20px; 307 | } 308 | 309 | .menu_link { 310 | font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif; 311 | } 312 | 313 | .menu_no_link { 314 | color: #ffffff; 315 | } 316 | 317 | .menu_link_active { 318 | font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif; 319 | color: #ffffff; 320 | } 321 | 322 | #forkme_banner { 323 | display: block; 324 | position: absolute; 325 | top:0; 326 | right: 10px; 327 | z-index: 10; 328 | padding: 10px 50px 10px 10px; 329 | color: #fff; 330 | background: url('../images/blacktocat.png') #0090ff no-repeat 95% 50%; 331 | font-weight: 700; 332 | box-shadow: 0 0 10px rgba(0,0,0,.5); 333 | border-bottom-left-radius: 2px; 334 | border-bottom-right-radius: 2px; 335 | } 336 | 337 | #header_wrap { 338 | background: #212121; 339 | background: -moz-linear-gradient(top, #373737, #212121); 340 | background: -webkit-linear-gradient(top, #373737, #212121); 341 | background: -ms-linear-gradient(top, #373737, #212121); 342 | background: -o-linear-gradient(top, #373737, #212121); 343 | background: linear-gradient(to top, #373737, #212121); 344 | } 345 | 346 | #header_wrap .inner { 347 | padding: 40px 10px 5px 10px; 348 | } 349 | 350 | #project_title { 351 | margin: 0; 352 | color: #fff; 353 | font-size: 42px; 354 | font-weight: 700; 355 | text-shadow: #111 0px 0px 10px; 356 | } 357 | 358 | #project_tagline { 359 | color: #fff; 360 | font-size: 24px; 361 | font-weight: 300; 362 | background: none; 363 | text-shadow: #111 0px 0px 10px; 364 | } 365 | 366 | #downloads { 367 | position: absolute; 368 | width: 210px; 369 | z-index: 10; 370 | bottom: -40px; 371 | right: 0; 372 | height: 70px; 373 | background: url('../images/icon_download.png') no-repeat 0% 90%; 374 | } 375 | 376 | .zip_download_link { 377 | display: block; 378 | float: right; 379 | width: 90px; 380 | height:70px; 381 | text-indent: -5000px; 382 | overflow: hidden; 383 | background: url(../images/sprite_download.png) no-repeat bottom left; 384 | } 385 | 386 | .tar_download_link { 387 | display: block; 388 | float: right; 389 | width: 90px; 390 | height:70px; 391 | text-indent: -5000px; 392 | overflow: hidden; 393 | background: url(../images/sprite_download.png) no-repeat bottom right; 394 | margin-left: 10px; 395 | } 396 | 397 | .zip_download_link:hover { 398 | background: url(../images/sprite_download.png) no-repeat top left; 399 | } 400 | 401 | .tar_download_link:hover { 402 | background: url(../images/sprite_download.png) no-repeat top right; 403 | } 404 | 405 | #main_content_wrap { 406 | background: #f2f2f2; 407 | border-top: 1px solid #111; 408 | border-bottom: 1px solid #111; 409 | } 410 | 411 | #main_content { 412 | padding-top: 40px; 413 | } 414 | 415 | #footer_wrap { 416 | background: #212121; 417 | } 418 | 419 | 420 | 421 | /******************************************************************************* 422 | Small Device Styles 423 | *******************************************************************************/ 424 | 425 | @media screen and (max-width: 992px) { 426 | img { 427 | max-width: 100%; 428 | } 429 | } 430 | 431 | @media screen and (max-width: 480px) { 432 | body { 433 | font-size:14px; 434 | } 435 | 436 | #downloads { 437 | display: none; 438 | } 439 | 440 | .inner { 441 | min-width: 320px; 442 | max-width: 480px; 443 | } 444 | 445 | #project_title { 446 | font-size: 32px; 447 | } 448 | 449 | h1 { 450 | font-size: 28px; 451 | } 452 | 453 | h2 { 454 | font-size: 24px; 455 | } 456 | 457 | h3 { 458 | font-size: 21px; 459 | } 460 | 461 | h4 { 462 | font-size: 18px; 463 | } 464 | 465 | h5 { 466 | font-size: 14px; 467 | } 468 | 469 | h6 { 470 | font-size: 12px; 471 | } 472 | 473 | code, pre { 474 | font-size: 11px; 475 | } 476 | 477 | } 478 | 479 | @media screen and (max-width: 320px) { 480 | body { 481 | font-size:14px; 482 | } 483 | 484 | #downloads { 485 | display: none; 486 | } 487 | 488 | .inner { 489 | min-width: 240px; 490 | max-width: 320px; 491 | } 492 | 493 | #project_title { 494 | font-size: 28px; 495 | } 496 | 497 | h1 { 498 | font-size: 24px; 499 | } 500 | 501 | h2 { 502 | font-size: 21px; 503 | } 504 | 505 | h3 { 506 | font-size: 18px; 507 | } 508 | 509 | h4 { 510 | font-size: 16px; 511 | } 512 | 513 | h5 { 514 | font-size: 14px; 515 | } 516 | 517 | h6 { 518 | font-size: 12px; 519 | } 520 | 521 | code, pre { 522 | min-width: 240px; 523 | max-width: 320px; 524 | font-size: 11px; 525 | } 526 | 527 | } 528 | 529 | -------------------------------------------------------------------------------- /src/debug.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "launcher.h" 8 | #include 9 | #include "util.h" 10 | #include "debug.h" 11 | #include "platform/platform.h" 12 | #ifdef __unix__ 13 | #include "platform/unix.h" 14 | #endif 15 | 16 | static int init_log(void); 17 | 18 | extern Config config; 19 | extern FILE *log_file; 20 | 21 | // A function to initialize the logging subsystem 22 | static int init_log() 23 | { 24 | // Determine log path 25 | char log_file_path[MAX_PATH_CHARS + 1]; 26 | #ifdef __unix__ 27 | char log_file_directory[MAX_PATH_CHARS + 1]; 28 | join_paths(log_file_directory, sizeof(log_file_directory), 4, getenv("HOME"), ".local", "share", EXECUTABLE_TITLE); 29 | make_directory(log_file_directory); 30 | join_paths(log_file_path, sizeof(log_file_path), 2, log_file_directory, FILENAME_LOG); 31 | #else 32 | join_paths(log_file_path, sizeof(log_file_path), 2, config.exe_path, FILENAME_LOG); 33 | #endif 34 | 35 | // Open log 36 | log_file = fopen(log_file_path, "wb"); 37 | if (log_file == NULL) { 38 | #ifdef __unix__ 39 | printf("Failed to create log file"); 40 | #endif 41 | quit(EXIT_FAILURE); 42 | } 43 | print_version(log_file); 44 | fputs("\n", log_file); 45 | print_compiler_info(log_file); 46 | fputs("\n", log_file); 47 | #ifdef __unix__ 48 | if (config.debug) 49 | printf("Debug mode enabled\nLog is outputted to %s\n", log_file_path); 50 | #endif 51 | return 0; 52 | } 53 | 54 | // A function to output a printf-style formatted line to the log 55 | void output_log(LogLevel log_level, const char *format, ...) 56 | { 57 | // Don't output a debug message if we aren't in debug mode 58 | if (log_level == LOGLEVEL_DEBUG && !config.debug) 59 | return; 60 | 61 | // Initialize logging if not already initialized 62 | if (log_file == NULL) 63 | init_log(); 64 | 65 | // Output log 66 | static char buffer[MAX_LOG_LINE_BYTES]; 67 | va_list args; 68 | va_start(args, format); 69 | size_t length = (size_t) vsnprintf(buffer, MAX_LOG_LINE_BYTES - 1, format, args); 70 | fwrite(buffer, 1, length, log_file); 71 | if (config.debug) 72 | fflush(log_file); 73 | 74 | #ifdef __unix__ 75 | if (log_level > LOGLEVEL_DEBUG) 76 | fputs(buffer, stderr); 77 | #endif 78 | va_end(args); 79 | 80 | if (log_level == LOGLEVEL_FATAL) 81 | quit(EXIT_FAILURE); 82 | } 83 | 84 | void print_compiler_info(FILE *stream) 85 | { 86 | fputs("Build date: " __DATE__ "\n", stream); 87 | #ifdef __GNUC__ 88 | fprintf(stream, "Compiler: GCC %u.%u\n", __GNUC__, __GNUC_MINOR__); 89 | #endif 90 | #ifdef _MSC_VER 91 | fprintf(stream, "Compiler: Microsoft C/C++ %.2f\n", (float) _MSC_VER / 100.0f); 92 | #endif 93 | 94 | } 95 | 96 | // A function to print the parsed settings to the log 97 | void debug_settings() 98 | { 99 | log_debug("======================= General ========================\n"); 100 | DEBUG_STR(SETTING_DEFAULT_MENU, config.default_menu); 101 | DEBUG_BOOL(SETTING_VSYNC, config.vsync); 102 | DEBUG_INT(SETTING_FPS_LIMIT, config.fps_limit); 103 | DEBUG_INT(SETTING_APPLICATION_TIMEOUT, config.application_timeout / 1000); 104 | DEBUG_MODE(SETTING_ON_LAUNCH, MODE_SETTING_ON_LAUNCH, config.on_launch); 105 | DEBUG_BOOL(SETTING_WRAP_ENTRIES, config.wrap_entries); 106 | DEBUG_BOOL(SETTING_RESET_ON_BACK, config.reset_on_back); 107 | DEBUG_BOOL(SETTING_MOUSE_SELECT, config.mouse_select); 108 | DEBUG_BOOL(SETTING_INHIBIT_OS_SCREENSAVER, config.inhibit_os_screensaver); 109 | DEBUG_STR(SETTING_STARTUP_CMD, config.startup_cmd); 110 | DEBUG_STR(SETTING_QUIT_CMD, config.quit_cmd); 111 | log_debug(""); 112 | 113 | log_debug("===================== Background =======================\n"); 114 | DEBUG_MODE(SETTING_BACKGROUND_MODE, MODE_SETTING_BACKGROUND, config.background_mode); 115 | DEBUG_COLOR(SETTING_BACKGROUND_COLOR, config.background_color); 116 | DEBUG_STR(SETTING_BACKGROUND_IMAGE, config.background_image); 117 | DEBUG_STR(SETTING_SLIDESHOW_DIRECTORY, config.slideshow_directory); 118 | DEBUG_INT(SETTING_SLIDESHOW_IMAGE_DURATION, config.slideshow_image_duration / 1000); 119 | DEBUG_FLOAT(SETTING_SLIDESHOW_TRANSITION_TIME, ((float) config.slideshow_transition_time) / 1000.0f); 120 | DEBUG_BOOL(SETTING_BACKGROUND_OVERLAY, config.background_overlay); 121 | DEBUG_COLOR(SETTING_BACKGROUND_OVERLAY_COLOR, config.background_overlay_color); 122 | log_debug(""); 123 | 124 | log_debug("======================= Layout =========================\n"); 125 | DEBUG_INT(SETTING_MAX_BUTTONS, config.max_buttons); 126 | DEBUG_INT(SETTING_ICON_SIZE, config.icon_size); 127 | DEBUG_INT(SETTING_ICON_SPACING, config.icon_spacing); 128 | DEBUG_STR(SETTING_VCENTER, config.vcenter[0] != '\0' ? config.vcenter : "50%"); 129 | log_debug(""); 130 | 131 | log_debug("======================== Titles ========================\n"); 132 | DEBUG_BOOL(SETTING_TITLES_ENABLED, config.titles_enabled); 133 | DEBUG_STR(SETTING_TITLE_FONT, config.title_font_path); 134 | DEBUG_INT(SETTING_TITLE_FONT_SIZE, config.title_font_size); 135 | DEBUG_COLOR(SETTING_TITLE_FONT_COLOR, config.title_font_color); 136 | DEBUG_BOOL(SETTING_TITLE_SHADOWS, config.title_shadows); 137 | DEBUG_COLOR(SETTING_TITLE_SHADOW_COLOR, config.title_shadow_color); 138 | DEBUG_MODE(SETTING_TITLE_OVERSIZE_MODE, MODE_SETTING_OVERSIZE, config.title_oversize_mode); 139 | DEBUG_INT(SETTING_TITLE_PADDING, config.title_padding); 140 | log_debug(""); 141 | 142 | log_debug("====================== Highlight =======================\n"); 143 | DEBUG_COLOR(SETTING_HIGHLIGHT_FILL_COLOR, config.highlight_fill_color); 144 | DEBUG_INT(SETTING_HIGHLIGHT_OUTLINE_SIZE, config.highlight_outline_size); 145 | DEBUG_COLOR(SETTING_HIGHLIGHT_OUTLINE_COLOR, config.highlight_outline_color); 146 | DEBUG_INT(SETTING_HIGHLIGHT_CORNER_RADIUS, config.highlight_rx); 147 | DEBUG_INT(SETTING_HIGHLIGHT_VPADDING, config.highlight_vpadding); 148 | DEBUG_INT(SETTING_HIGHLIGHT_HPADDING, config.highlight_hpadding); 149 | log_debug(""); 150 | 151 | log_debug("================== Scroll Indicators ===================\n"); 152 | DEBUG_BOOL(SETTING_SCROLL_INDICATORS, config.scroll_indicators); 153 | DEBUG_COLOR(SETTING_SCROLL_INDICATOR_FILL_COLOR, config.scroll_indicator_fill_color); 154 | DEBUG_INT(SETTING_SCROLL_INDICATOR_OUTLINE_SIZE, config.scroll_indicator_outline_size); 155 | DEBUG_COLOR(SETTING_SCROLL_INDICATOR_OUTLINE_COLOR, config.scroll_indicator_outline_color); 156 | log_debug(""); 157 | 158 | log_debug("======================== Clock =========================\n"); 159 | DEBUG_BOOL(SETTING_CLOCK_ENABLED, config.clock_enabled); 160 | DEBUG_BOOL(SETTING_CLOCK_SHOW_DATE, config.clock_show_date); 161 | DEBUG_MODE(SETTING_CLOCK_ALIGNMENT, MODE_SETTING_ALIGNMENT, config.clock_alignment); 162 | DEBUG_STR(SETTING_CLOCK_FONT, config.clock_font_path); 163 | DEBUG_INT(SETTING_CLOCK_FONT_SIZE, config.clock_font_size); 164 | DEBUG_INT(SETTING_CLOCK_MARGIN, config.clock_margin); 165 | DEBUG_COLOR(SETTING_CLOCK_FONT_COLOR, config.clock_font_color); 166 | DEBUG_BOOL(SETTING_CLOCK_SHADOWS, config.clock_shadows); 167 | DEBUG_COLOR(SETTING_CLOCK_SHADOW_COLOR, config.clock_shadow_color); 168 | DEBUG_MODE(SETTING_CLOCK_TIME_FORMAT, MODE_SETTING_TIME_FORMAT, config.clock_time_format); 169 | DEBUG_MODE(SETTING_CLOCK_DATE_FORMAT, MODE_SETTING_DATE_FORMAT, config.clock_date_format); 170 | DEBUG_BOOL(SETTING_CLOCK_INCLUDE_WEEKDAY, config.clock_include_weekday); 171 | log_debug(""); 172 | 173 | log_debug("===================== Screensaver ======================\n"); 174 | DEBUG_BOOL(SETTING_SCREENSAVER_ENABLED, config.screensaver_enabled); 175 | DEBUG_INT(SETTING_SCREENSAVER_IDLE_TIME, config.screensaver_idle_time / 1000); 176 | DEBUG_STR(SETTING_SCREENSAVER_INTENSITY, config.screensaver_intensity_str[0] != '\0' ? config.screensaver_intensity_str : DEFAULT_SCREENSAVER_INTENSITY); 177 | DEBUG_BOOL(SETTING_SCREENSAVER_PAUSE_SLIDESHOW, config.screensaver_pause_slideshow); 178 | log_debug(""); 179 | } 180 | 181 | // A function to print the parsed menu entries to the command line 182 | void debug_menu_entries(Menu *first_menu, size_t num_menus) 183 | { 184 | if (first_menu == NULL) { 185 | log_debug("No valid menus found"); 186 | return; 187 | } 188 | log_debug("======================= Menu Entries =======================\n"); 189 | Menu *menu = first_menu; 190 | Entry *entry; 191 | for (size_t i = 0; i < num_menus; i ++) { 192 | log_debug("Menu Name: %s",menu->name); 193 | log_debug("Number of Entries: %i",menu->num_entries); 194 | entry = menu->first_entry; 195 | for (size_t j = 0; j < menu->num_entries; j++) { 196 | log_debug("Entry %i Title: %s",j,entry->title); 197 | log_debug("Entry %i Icon Path: %s",j,entry->icon_path); 198 | log_debug("Entry %i Command: %s",j,entry->cmd); 199 | if (j != menu->num_entries - 1) 200 | log_debug(""); 201 | entry = entry->next; 202 | } 203 | if (i != num_menus - 1) { 204 | log_debug("----------------------------------------------------------"); 205 | } 206 | menu = menu->next; 207 | } 208 | log_debug(""); 209 | } 210 | 211 | void debug_gamepad(GamepadControl *gamepad_controls) 212 | { 213 | log_debug("======================= Gamepad ========================\n"); 214 | DEBUG_BOOL(SETTING_GAMEPAD_ENABLED, config.gamepad_enabled); 215 | DEBUG_INT(SETTING_GAMEPAD_DEVICE, config.gamepad_device); 216 | DEBUG_STR(SETTING_GAMEPAD_MAPPINGS_FILE, config.gamepad_mappings_file); 217 | for (GamepadControl *i = gamepad_controls; i != NULL; i = i->next) 218 | log_debug("%-25s %s", i->label, i->cmd); 219 | log_debug(""); 220 | } 221 | 222 | void debug_hotkeys(Hotkey *hotkeys) 223 | { 224 | if (hotkeys == NULL) { 225 | log_debug("No hotkeys detected"); 226 | return; 227 | } 228 | log_debug("======================== Hotkeys =========================\n"); 229 | int index = 0; 230 | for (Hotkey *i = hotkeys; i != NULL; i = i->next) { 231 | log_debug("Hotkey %i Keycode: %X", index, i->keycode); 232 | log_debug("Hotkey %i Command: %s", index, i->cmd); 233 | index++; 234 | } 235 | log_debug(""); 236 | } 237 | 238 | // A function to debug the parsed slideshow files 239 | void debug_slideshow(Slideshow *slideshow) 240 | { 241 | log_debug("======================== Slideshow ========================"); 242 | log_debug("Found %i images in directory %s:", 243 | slideshow->num_images, 244 | config.slideshow_directory 245 | ); 246 | for (int i = 0; i < slideshow->num_images; i++) 247 | log_debug(" %s", slideshow->images[slideshow->order[i]]); 248 | } 249 | 250 | // A function to debug the video settings 251 | void debug_video(SDL_Renderer *renderer, SDL_DisplayMode *display_mode) 252 | { 253 | log_debug("================== Video Information ===================\n"); 254 | log_debug("Resolution: %ix%i", display_mode->w, display_mode->h); 255 | log_debug("Refresh rate: %i Hz", display_mode->refresh_rate); 256 | log_debug("Video driver: %s", SDL_GetCurrentVideoDriver()); 257 | log_debug(""); 258 | SDL_RendererInfo info; 259 | SDL_GetRendererInfo(renderer, &info); 260 | log_debug("Supported Texture formats:"); 261 | for(size_t i = 0; i < info.num_texture_formats; i++) 262 | log_debug(" %s", SDL_GetPixelFormatName(info.texture_formats[i])); 263 | log_debug(""); 264 | } 265 | -------------------------------------------------------------------------------- /config/launcher_config.h.in: -------------------------------------------------------------------------------- 1 | // Project information 2 | #define PROJECT_NAME "@CMAKE_PROJECT_NAME@" 3 | #define EXECUTABLE_TITLE "@EXECUTABLE_TITLE@" 4 | #define PROJECT_VERSION "@PROJECT_VERSION@" 5 | #define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ 6 | #define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@ 7 | #define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@ 8 | 9 | // Default filenames and paths 10 | #define FILENAME_DEFAULT_CONFIG "config.ini" 11 | #define FILENAME_DEFAULT_FONT "OpenSans-Regular.ttf" 12 | #define FILENAME_DEFAULT_CLOCK_FONT "@DEFAULT_CLOCK_FONT@" 13 | #define FILENAME_LOG "@EXECUTABLE_TITLE@.log" 14 | #define PATH_ASSETS_EXE "assets" 15 | #define PATH_FONTS_EXE "fonts" 16 | #define PATH_CONFIG_SYSTEM "@CMAKE_INSTALL_PREFIX@/share/@EXECUTABLE_TITLE@/" 17 | #define PATH_ASSETS_SYSTEM "@CMAKE_INSTALL_PREFIX@/share/@EXECUTABLE_TITLE@/assets/" 18 | #define PATH_FONTS_SYSTEM "@CMAKE_INSTALL_PREFIX@/share/@EXECUTABLE_TITLE@/assets/fonts" 19 | #ifdef _WIN32 20 | #define CURRENT_DIRECTORY ".\\" 21 | #define PATH_FONTS_RELATIVE ".\\assets\\fonts" 22 | #define PATH_ASSETS_RELATIVE ".\\assets" 23 | #else 24 | #define CURRENT_DIRECTORY "./" 25 | #endif 26 | 27 | // Clock format strings 28 | #define TIME_STRING_12HR "@TIME_STRING_12HR@" 29 | #define TIME_STRING_24HR "@TIME_STRING_24HR@" 30 | #define DATE_STRING_LITTLE "@DATE_STRING_LITTLE@" 31 | #define DATE_STRING_BIG "@DATE_STRING_BIG@" 32 | 33 | // Config file setting names 34 | #define SETTING_DEFAULT_MENU "@SETTING_DEFAULT_MENU@" 35 | #define SETTING_MAX_BUTTONS "@SETTING_MAX_BUTTONS@" 36 | #define SETTING_VSYNC "@SETTING_VSYNC@" 37 | #define SETTING_FPS_LIMIT "@SETTING_FPS_LIMIT@" 38 | #define SETTING_APPLICATION_TIMEOUT "@SETTING_APPLICATION_TIMEOUT@" 39 | #define SETTING_WRAP_ENTRIES "@SETTING_WRAP_ENTRIES@" 40 | #define SETTING_BACKGROUND_MODE "@SETTING_BACKGROUND_MODE@" 41 | #define SETTING_BACKGROUND_IMAGE "@SETTING_BACKGROUND_IMAGE@" 42 | #define SETTING_BACKGROUND_COLOR "@SETTING_BACKGROUND_COLOR@" 43 | #define SETTING_SLIDESHOW_DIRECTORY "@SETTING_SLIDESHOW_DIRECTORY@" 44 | #define SETTING_SLIDESHOW_IMAGE_DURATION "@SETTING_SLIDESHOW_IMAGE_DURATION@" 45 | #define SETTING_SLIDESHOW_TRANSITION_TIME "@SETTING_SLIDESHOW_TRANSITION_TIME@" 46 | #define SETTING_SCREENSAVER_PAUSE_SLIDESHOW "@SETTING_SCREENSAVER_PAUSE_SLIDESHOW@" 47 | #define SETTING_CHROMA_KEY_COLOR "@SETTING_CHROMA_KEY_COLOR@" 48 | #define SETTING_BACKGROUND_OVERLAY "@SETTING_BACKGROUND_OVERLAY@" 49 | #define SETTING_BACKGROUND_OVERLAY_COLOR "@SETTING_BACKGROUND_OVERLAY_COLOR@" 50 | #define SETTING_BACKGROUND_OVERLAY_OPACITY "@SETTING_BACKGROUND_OVERLAY_OPACITY@" 51 | #define SETTING_ICON_SIZE "@SETTING_ICON_SIZE@" 52 | #define SETTING_ICON_SPACING "@SETTING_ICON_SPACING@" 53 | #define SETTING_TITLES_ENABLED "@SETTING_TITLES_ENABLED@" 54 | #define SETTING_TITLE_FONT "@SETTING_TITLE_FONT@" 55 | #define SETTING_TITLE_FONT_SIZE "@SETTING_TITLE_FONT_SIZE@" 56 | #define SETTING_TITLE_FONT_COLOR "@SETTING_TITLE_FONT_COLOR@" 57 | #define SETTING_TITLE_SHADOWS "@SETTING_TITLE_SHADOWS@" 58 | #define SETTING_TITLE_SHADOW_COLOR "@SETTING_TITLE_SHADOW_COLOR@" 59 | #define SETTING_TITLE_OVERSIZE_MODE "@SETTING_TITLE_OVERSIZE_MODE@" 60 | #define SETTING_TITLE_OPACITY "@SETTING_TITLE_OPACITY@" 61 | #define SETTING_TITLE_PADDING "@SETTING_TITLE_PADDING@" 62 | #define SETTING_HIGHLIGHT_ENABLED "@SETTING_HIGHLIGHT_ENABLED@" 63 | #define SETTING_HIGHLIGHT_FILL_COLOR "@SETTING_HIGHLIGHT_FILL_COLOR@" 64 | #define SETTING_HIGHLIGHT_FILL_OPACITY "@SETTING_HIGHLIGHT_FILL_OPACITY@" 65 | #define SETTING_HIGHLIGHT_OUTLINE_COLOR "@SETTING_HIGHLIGHT_OUTLINE_COLOR@" 66 | #define SETTING_HIGHLIGHT_OUTLINE_OPACITY "@SETTING_HIGHLIGHT_OUTLINE_OPACITY@" 67 | #define SETTING_HIGHLIGHT_OUTLINE_SIZE "@SETTING_HIGHLIGHT_OUTLINE_SIZE@" 68 | #define SETTING_HIGHLIGHT_VPADDING "@SETTING_HIGHLIGHT_VPADDING@" 69 | #define SETTING_HIGHLIGHT_HPADDING "@SETTING_HIGHLIGHT_HPADDING@" 70 | #define SETTING_HIGHLIGHT_CORNER_RADIUS "@SETTING_HIGHLIGHT_CORNER_RADIUS@" 71 | #define SETTING_VCENTER "@SETTING_VCENTER@" 72 | #define SETTING_SCROLL_INDICATORS "@SETTING_SCROLL_INDICATORS@" 73 | #define SETTING_SCROLL_INDICATOR_FILL_COLOR "@SETTING_SCROLL_INDICATOR_FILL_COLOR@" 74 | #define SETTING_SCROLL_INDICATOR_OUTLINE_SIZE "@SETTING_SCROLL_INDICATOR_OUTLINE_SIZE@" 75 | #define SETTING_SCROLL_INDICATOR_OUTLINE_COLOR "@SETTING_SCROLL_INDICATOR_OUTLINE_COLOR@" 76 | #define SETTING_SCROLL_INDICATOR_OPACITY "@SETTING_SCROLL_INDICATOR_OPACITY@" 77 | #define SETTING_ON_LAUNCH "@SETTING_ON_LAUNCH@" 78 | #define SETTING_RESET_ON_BACK "@SETTING_RESET_ON_BACK@" 79 | #define SETTING_MOUSE_SELECT "@SETTING_MOUSE_SELECT@" 80 | #define SETTING_INHIBIT_OS_SCREENSAVER "@SETTING_INHIBIT_OS_SCREENSAVER@" 81 | #define SETTING_STARTUP_CMD "@SETTING_STARTUP_CMD@" 82 | #define SETTING_QUIT_CMD "@SETTING_QUIT_CMD@" 83 | #define SETTING_CLOCK_ENABLED "@SETTING_CLOCK_ENABLED@" 84 | #define SETTING_CLOCK_SHOW_DATE "@SETTING_CLOCK_SHOW_DATE@" 85 | #define SETTING_CLOCK_ALIGNMENT "@SETTING_CLOCK_ALIGNMENT@" 86 | #define SETTING_CLOCK_FONT "@SETTING_CLOCK_FONT@" 87 | #define SETTING_CLOCK_FONT_COLOR "@SETTING_CLOCK_FONT_COLOR@" 88 | #define SETTING_CLOCK_SHADOWS "@SETTING_CLOCK_SHADOWS@" 89 | #define SETTING_CLOCK_SHADOW_COLOR "@SETTING_CLOCK_SHADOW_COLOR@" 90 | #define SETTING_CLOCK_OPACITY "@SETTING_CLOCK_OPACITY@" 91 | #define SETTING_CLOCK_FONT_SIZE "@SETTING_CLOCK_FONT_SIZE@" 92 | #define SETTING_CLOCK_MARGIN "@SETTING_CLOCK_MARGIN@" 93 | #define SETTING_CLOCK_TIME_FORMAT "@SETTING_CLOCK_TIME_FORMAT@" 94 | #define SETTING_CLOCK_DATE_FORMAT "@SETTING_CLOCK_DATE_FORMAT@" 95 | #define SETTING_CLOCK_INCLUDE_WEEKDAY "@SETTING_CLOCK_INCLUDE_WEEKDAY@" 96 | #define SETTING_SCREENSAVER_ENABLED "@SETTING_SCREENSAVER_ENABLED@" 97 | #define SETTING_SCREENSAVER_IDLE_TIME "@SETTING_SCREENSAVER_IDLE_TIME@" 98 | #define SETTING_SCREENSAVER_INTENSITY "@SETTING_SCREENSAVER_INTENSITY@" 99 | #define SETTING_GAMEPAD_ENABLED "@SETTING_GAMEPAD_ENABLED@" 100 | #define SETTING_GAMEPAD_DEVICE "@SETTING_GAMEPAD_DEVICE@" 101 | #define SETTING_GAMEPAD_MAPPINGS_FILE "@SETTING_GAMEPAD_MAPPINGS_FILE@" 102 | #define SETTING_GAMEPAD_LSTICK_XM "@SETTING_GAMEPAD_LSTICK_XM@" 103 | #define SETTING_GAMEPAD_LSTICK_XP "@SETTING_GAMEPAD_LSTICK_XP@" 104 | #define SETTING_GAMEPAD_LSTICK_YM "@SETTING_GAMEPAD_LSTICK_YM@" 105 | #define SETTING_GAMEPAD_LSTICK_YP "@SETTING_GAMEPAD_LSTICK_YP@" 106 | #define SETTING_GAMEPAD_RSTICK_XM "@SETTING_GAMEPAD_RSTICK_XM@" 107 | #define SETTING_GAMEPAD_RSTICK_XP "@SETTING_GAMEPAD_RSTICK_XP@" 108 | #define SETTING_GAMEPAD_RSTICK_YM "@SETTING_GAMEPAD_RSTICK_YM@" 109 | #define SETTING_GAMEPAD_RSTICK_YP "@SETTING_GAMEPAD_RSTICK_YP@" 110 | #define SETTING_GAMEPAD_LTRIGGER "@SETTING_GAMEPAD_LTRIGGER@" 111 | #define SETTING_GAMEPAD_RTRIGGER "@SETTING_GAMEPAD_RTRIGGER@" 112 | #define SETTING_GAMEPAD_BUTTON_A "@SETTING_GAMEPAD_BUTTON_A@" 113 | #define SETTING_GAMEPAD_BUTTON_B "@SETTING_GAMEPAD_BUTTON_B@" 114 | #define SETTING_GAMEPAD_BUTTON_X "@SETTING_GAMEPAD_BUTTON_X@" 115 | #define SETTING_GAMEPAD_BUTTON_Y "@SETTING_GAMEPAD_BUTTON_Y@" 116 | #define SETTING_GAMEPAD_BUTTON_BACK "@SETTING_GAMEPAD_BUTTON_BACK@" 117 | #define SETTING_GAMEPAD_BUTTON_GUIDE "@SETTING_GAMEPAD_BUTTON_GUIDE@" 118 | #define SETTING_GAMEPAD_BUTTON_START "@SETTING_GAMEPAD_BUTTON_START@" 119 | #define SETTING_GAMEPAD_BUTTON_LEFT_STICK "@SETTING_GAMEPAD_BUTTON_LEFT_STICK@" 120 | #define SETTING_GAMEPAD_BUTTON_RIGHT_STICK "@SETTING_GAMEPAD_BUTTON_RIGHT_STICK@" 121 | #define SETTING_GAMEPAD_BUTTON_LEFT_SHOULDER "@SETTING_GAMEPAD_BUTTON_LEFT_SHOULDER@" 122 | #define SETTING_GAMEPAD_BUTTON_RIGHT_SHOULDER "@SETTING_GAMEPAD_BUTTON_RIGHT_SHOULDER@" 123 | #define SETTING_GAMEPAD_BUTTON_DPAD_UP "@SETTING_GAMEPAD_BUTTON_DPAD_UP@" 124 | #define SETTING_GAMEPAD_BUTTON_DPAD_DOWN "@SETTING_GAMEPAD_BUTTON_DPAD_DOWN@" 125 | #define SETTING_GAMEPAD_BUTTON_DPAD_LEFT "@SETTING_GAMEPAD_BUTTON_DPAD_LEFT@" 126 | #define SETTING_GAMEPAD_BUTTON_DPAD_RIGHT "@SETTING_GAMEPAD_BUTTON_DPAD_RIGHT@" 127 | 128 | // Config file default settings 129 | #define DEFAULT_MAX_BUTTONS @DEFAULT_MAX_BUTTONS@ 130 | #define DEFAULT_VSYNC @DEFAULT_VSYNC@ 131 | #define DEFAULT_APPLICATION_TIMEOUT @DEFAULT_APPLICATION_TIMEOUT@ 132 | #define DEFAULT_WRAP_ENTRIES @DEFAULT_WRAP_ENTRIES@ 133 | #define DEFAULT_BACKGROUND_COLOR_R 0x@DEFAULT_BACKGROUND_COLOR_R@ 134 | #define DEFAULT_BACKGROUND_COLOR_G 0x@DEFAULT_BACKGROUND_COLOR_G@ 135 | #define DEFAULT_BACKGROUND_COLOR_B 0x@DEFAULT_BACKGROUND_COLOR_B@ 136 | #define DEFAULT_SLIDESHOW_IMAGE_DURATION @DEFAULT_SLIDESHOW_IMAGE_DURATION@ 137 | #define DEFAULT_SLIDESHOW_TRANSITION_TIME @DEFAULT_SLIDESHOW_TRANSITION_TIME@ 138 | #define DEFAULT_CHROMA_KEY_COLOR_R 0x@DEFAULT_CHROMA_KEY_COLOR_R@ 139 | #define DEFAULT_CHROMA_KEY_COLOR_G 0x@DEFAULT_CHROMA_KEY_COLOR_G@ 140 | #define DEFAULT_CHROMA_KEY_COLOR_B 0x@DEFAULT_CHROMA_KEY_COLOR_B@ 141 | #define DEFAULT_CHROMA_KEY_COLOR_A 0x@DEFAULT_CHROMA_KEY_COLOR_A@ 142 | #define DEFAULT_BACKGROUND_OVERLAY @DEFAULT_BACKGROUND_OVERLAY@ 143 | #define DEFAULT_BACKGROUND_OVERLAY_COLOR_R 0x@DEFAULT_BACKGROUND_OVERLAY_COLOR_R@ 144 | #define DEFAULT_BACKGROUND_OVERLAY_COLOR_G 0x@DEFAULT_BACKGROUND_OVERLAY_COLOR_G@ 145 | #define DEFAULT_BACKGROUND_OVERLAY_COLOR_B 0x@DEFAULT_BACKGROUND_OVERLAY_COLOR_B@ 146 | #define DEFAULT_BACKGROUND_OVERLAY_COLOR_A 0x@DEFAULT_BACKGROUND_OVERLAY_COLOR_A@ 147 | #define DEFAULT_BACKGROUND_OVERLAY_OPACITY "@DEFAULT_BACKGROUND_OVERLAY_OPACITY@" 148 | #define DEFAULT_ICON_SIZE @DEFAULT_ICON_SIZE@ 149 | #define DEFAULT_ICON_SPACING "@DEFAULT_ICON_SPACING@" 150 | #define DEFAULT_FONT_SIZE @DEFAULT_FONT_SIZE@ 151 | #define DEFAULT_TITLES_ENABLED @DEFAULT_TITLES_ENABLED@ 152 | #define DEFAULT_TITLE_FONT_COLOR_R 0x@DEFAULT_TITLE_FONT_COLOR_R@ 153 | #define DEFAULT_TITLE_FONT_COLOR_G 0x@DEFAULT_TITLE_FONT_COLOR_G@ 154 | #define DEFAULT_TITLE_FONT_COLOR_B 0x@DEFAULT_TITLE_FONT_COLOR_B@ 155 | #define DEFAULT_TITLE_FONT_COLOR_A 0x@DEFAULT_TITLE_FONT_COLOR_A@ 156 | #define DEFAULT_TITLE_SHADOWS @DEFAULT_TITLE_SHADOWS@ 157 | #define DEFAULT_TITLE_SHADOW_COLOR_R 0x@DEFAULT_TITLE_SHADOW_COLOR_R@ 158 | #define DEFAULT_TITLE_SHADOW_COLOR_G 0x@DEFAULT_TITLE_SHADOW_COLOR_G@ 159 | #define DEFAULT_TITLE_SHADOW_COLOR_B 0x@DEFAULT_TITLE_SHADOW_COLOR_B@ 160 | #define DEFAULT_TITLE_SHADOW_COLOR_A 0x@DEFAULT_TITLE_SHADOW_COLOR_A@ 161 | #define DEFAULT_TITLE_PADDING @DEFAULT_TITLE_PADDING@ 162 | #define DEFAULT_HIGHLIGHT_ENABLED @DEFAULT_HIGHLIGHT_ENABLED@ 163 | #define DEFAULT_HIGHLIGHT_FILL_COLOR_R 0x@DEFAULT_HIGHLIGHT_FILL_COLOR_R@ 164 | #define DEFAULT_HIGHLIGHT_FILL_COLOR_G 0x@DEFAULT_HIGHLIGHT_FILL_COLOR_G@ 165 | #define DEFAULT_HIGHLIGHT_FILL_COLOR_B 0x@DEFAULT_HIGHLIGHT_FILL_COLOR_B@ 166 | #define DEFAULT_HIGHLIGHT_FILL_COLOR_A 0x@DEFAULT_HIGHLIGHT_FILL_COLOR_A@ 167 | #define DEFAULT_HIGHLIGHT_OUTLINE_COLOR_R 0x@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_R@ 168 | #define DEFAULT_HIGHLIGHT_OUTLINE_COLOR_G 0x@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_G@ 169 | #define DEFAULT_HIGHLIGHT_OUTLINE_COLOR_B 0x@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_B@ 170 | #define DEFAULT_HIGHLIGHT_OUTLINE_COLOR_A 0x@DEFAULT_HIGHLIGHT_OUTLINE_COLOR_A@ 171 | #define DEFAULT_HIGHLIGHT_OUTLINE_SIZE @DEFAULT_HIGHLIGHT_OUTLINE_SIZE@ 172 | #define DEFAULT_HIGHLIGHT_CORNER_RADIUS @DEFAULT_HIGHLIGHT_CORNER_RADIUS@ 173 | #define DEFAULT_HIGHLIGHT_VPADDING @DEFAULT_HIGHLIGHT_VPADDING@ 174 | #define DEFAULT_HIGHLIGHT_HPADDING @DEFAULT_HIGHLIGHT_HPADDING@ 175 | #define DEFAULT_SCROLL_INDICATORS @DEFAULT_SCROLL_INDICATORS@ 176 | #define DEFAULT_SCROLL_INDICATOR_FILL_COLOR_R 0x@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_R@ 177 | #define DEFAULT_SCROLL_INDICATOR_FILL_COLOR_G 0x@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_G@ 178 | #define DEFAULT_SCROLL_INDICATOR_FILL_COLOR_B 0x@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_B@ 179 | #define DEFAULT_SCROLL_INDICATOR_FILL_COLOR_A 0x@DEFAULT_SCROLL_INDICATOR_FILL_COLOR_A@ 180 | #define DEFAULT_SCROLL_INDICATOR_OUTLINE_SIZE @DEFAULT_SCROLL_INDICATOR_OUTLINE_SIZE@ 181 | #define DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_R 0x@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_R@ 182 | #define DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_G 0x@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_G@ 183 | #define DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_B 0x@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_B@ 184 | #define DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_A 0x@DEFAULT_SCROLL_INDICATOR_OUTLINE_COLOR_A@ 185 | #define DEFAULT_VCENTER "@DEFAULT_VCENTER@" 186 | #define DEFAULT_RESET_ON_BACK @DEFAULT_RESET_ON_BACK@ 187 | #define DEFAULT_MOUSE_SELECT @DEFAULT_MOUSE_SELECT@ 188 | #define DEFAULT_INHIBIT_OS_SCREENSAVER @DEFAULT_INHIBIT_OS_SCREENSAVER@ 189 | #define DEFAULT_CLOCK_ENABLED @DEFAULT_CLOCK_ENABLED@ 190 | #define DEFAULT_CLOCK_SHOW_DATE @DEFAULT_CLOCK_SHOW_DATE@ 191 | #define DEFAULT_CLOCK_FONT @DEFAULT_CLOCK_FONT@ 192 | #define DEFAULT_CLOCK_MARGIN "@DEFAULT_CLOCK_MARGIN@" 193 | #define DEFAULT_CLOCK_FONT_COLOR_R 0x@DEFAULT_CLOCK_FONT_COLOR_R@ 194 | #define DEFAULT_CLOCK_FONT_COLOR_G 0x@DEFAULT_CLOCK_FONT_COLOR_G@ 195 | #define DEFAULT_CLOCK_FONT_COLOR_B 0x@DEFAULT_CLOCK_FONT_COLOR_B@ 196 | #define DEFAULT_CLOCK_FONT_COLOR_A 0x@DEFAULT_CLOCK_FONT_COLOR_A@ 197 | #define DEFAULT_CLOCK_FONT_SIZE @DEFAULT_CLOCK_FONT_SIZE@ 198 | #define DEFAULT_CLOCK_SHADOWS @DEFAULT_CLOCK_SHADOWS@ 199 | #define DEFAULT_CLOCK_SHADOW_COLOR_R 0x@DEFAULT_CLOCK_SHADOW_COLOR_R@ 200 | #define DEFAULT_CLOCK_SHADOW_COLOR_G 0x@DEFAULT_CLOCK_SHADOW_COLOR_G@ 201 | #define DEFAULT_CLOCK_SHADOW_COLOR_B 0x@DEFAULT_CLOCK_SHADOW_COLOR_B@ 202 | #define DEFAULT_CLOCK_SHADOW_COLOR_A 0x@DEFAULT_CLOCK_SHADOW_COLOR_A@ 203 | #define DEFAULT_CLOCK_INCLUDE_WEEKDAY @DEFAULT_CLOCK_INCLUDE_WEEKDAY@ 204 | #define DEFAULT_CLOCK_ALIGNMENT ALIGNMENT_LEFT 205 | #define DEFAULT_CLOCK_TIME_FORMAT FORMAT_TIME_AUTO 206 | #define DEFAULT_CLOCK_DATE_FORMAT FORMAT_DATE_AUTO 207 | #define DEFAULT_SCREENSAVER_ENABLED @DEFAULT_SCREENSAVER_ENABLED@ 208 | #define DEFAULT_SCREENSAVER_IDLE_TIME @DEFAULT_SCREENSAVER_IDLE_TIME@ 209 | #define DEFAULT_SCREENSAVER_INTENSITY "@DEFAULT_SCREENSAVER_INTENSITY@" 210 | #define DEFAULT_SCREENSAVER_PAUSE_SLIDESHOW @DEFAULT_SCREENSAVER_PAUSE_SLIDESHOW@ 211 | #define DEFAULT_GAMEPAD_ENABLED @DEFAULT_GAMEPAD_ENABLED@ 212 | #define DEFAULT_GAMEPAD_DEVICE @DEFAULT_GAMEPAD_DEVICE@ -------------------------------------------------------------------------------- /src/image.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "launcher.h" 9 | #include 10 | #include "image.h" 11 | #include "util.h" 12 | #include "debug.h" 13 | #include 14 | #define NANOSVG_IMPLEMENTATION 15 | #include 16 | #define NANOSVGRAST_IMPLEMENTATION 17 | #include 18 | 19 | extern Config config; 20 | extern State state; 21 | extern SDL_Renderer *renderer; 22 | extern SDL_Texture *background_texture; 23 | NSVGrasterizer *rasterizer = NULL; 24 | 25 | // A function to initalize SVG rasterization 26 | int init_svg() 27 | { 28 | rasterizer = nsvgCreateRasterizer(); 29 | if (rasterizer == NULL) { 30 | log_fatal("Could not initialize SVG rasterizer."); 31 | return 1; 32 | } 33 | return 0; 34 | } 35 | 36 | // A function to quit the SVG subsystem 37 | void quit_svg() 38 | { 39 | nsvgDeleteRasterizer(rasterizer); 40 | } 41 | 42 | // A function to load the next slideshow background from the struct 43 | SDL_Surface *load_next_slideshow_background(Slideshow *slideshow, bool transition) 44 | { 45 | SDL_Surface *surface = NULL; 46 | int initial_index = slideshow->i; 47 | int attempts = 0; 48 | do { 49 | // Increment slideshow background index and load background 50 | (slideshow->i)++; 51 | if (slideshow->i >= slideshow->num_images) 52 | slideshow->i = 0; 53 | surface = IMG_Load(slideshow->images[slideshow->order[slideshow->i]]); 54 | 55 | // If the loaded image has no alpha channel (e.g. JPEG), create one 56 | // so that we can have transparency for the background transition 57 | if (surface != NULL && surface->format->format == SDL_PIXELFORMAT_RGB24 && transition) { 58 | SDL_Surface *tmp = SDL_CreateRGBSurfaceWithFormat(0, 59 | surface->w, 60 | surface->h, 61 | 32, 62 | SDL_PIXELFORMAT_ARGB8888 63 | ); 64 | Uint32 color = SDL_MapRGBA(tmp->format, 0, 0, 0, 0xFF); 65 | SDL_FillRect(tmp, NULL, color); 66 | SDL_BlitSurface(surface, NULL, tmp, NULL); 67 | SDL_FreeSurface(surface); 68 | surface = tmp; 69 | attempts++; 70 | } 71 | } while (surface == NULL && slideshow->i != initial_index && attempts < slideshow->num_images); 72 | 73 | // Switch to color background mode if we failed to load any image from the array 74 | if (surface == NULL) { 75 | log_error( 76 | "Could not load any image from slideshow directory %s\n" 77 | "Changing background to color mode", 78 | config.slideshow_directory 79 | ); 80 | quit_slideshow(); 81 | config.background_mode = BACKGROUND_COLOR; 82 | set_draw_color(); 83 | } 84 | 85 | // If only one image in the entire slideshow array was valid, switch to 86 | // single image background mode 87 | else if (slideshow->i == initial_index && surface != NULL) { 88 | log_error( 89 | "Could only load one image from slideshow directory %s\n" 90 | "Changing background to single image mode", 91 | config.slideshow_directory 92 | ); 93 | background_texture = SDL_CreateTextureFromSurface(renderer, surface); 94 | config.background_mode = BACKGROUND_IMAGE; 95 | } 96 | return surface; 97 | } 98 | 99 | // A function to load a new slideshow background in a separate thread 100 | int load_next_slideshow_background_async(void *data) 101 | { 102 | Slideshow *slideshow = (Slideshow*) data; 103 | slideshow->transition_surface = load_next_slideshow_background(slideshow, true); 104 | state.slideshow_background_rendering = false; 105 | state.slideshow_background_ready = true; 106 | return 0; 107 | } 108 | 109 | // A function to load a texture from a file 110 | SDL_Texture *load_texture_from_file(const char *path) 111 | { 112 | SDL_Surface *surface = NULL; 113 | SDL_Texture *texture = NULL; 114 | if (path != NULL) { 115 | surface = IMG_Load(path); 116 | if (surface == NULL) { 117 | log_error( 118 | "Could not load image %s\n%s", 119 | path, 120 | IMG_GetError() 121 | ); 122 | } 123 | else 124 | texture = load_texture(surface); 125 | } 126 | return texture; 127 | } 128 | 129 | // A function to load a texture from a SDL surface 130 | SDL_Texture *load_texture(SDL_Surface *surface) 131 | { 132 | SDL_Texture *texture = NULL; 133 | if (surface == NULL) 134 | return NULL; 135 | 136 | //Convert surface to screen format 137 | texture = SDL_CreateTextureFromSurface(renderer, surface); 138 | if (texture == NULL) 139 | log_error("Could not create texture %s", SDL_GetError()); 140 | SDL_FreeSurface(surface); 141 | return texture; 142 | } 143 | 144 | // A function to rasterize an SVG from an existing text buffer 145 | SDL_Texture *rasterize_svg(char *buffer, int w, int h, SDL_Rect *rect) 146 | { 147 | NSVGimage *image = NULL; 148 | unsigned char *pixel_buffer = NULL; 149 | int width, height, pitch; 150 | float scale; 151 | 152 | // Parse SVG to NSVGimage struct 153 | image = nsvgParse(buffer, "px", 96.0f); 154 | if (image == NULL) { 155 | log_error("could not open SVG image."); 156 | return NULL; 157 | } 158 | 159 | // Calculate scaling and dimensions 160 | if (w == -1 && h == -1) { 161 | scale = 1.0f; 162 | width = (int) image->width; 163 | height = (int) image->height; 164 | } 165 | else if (w == -1 && h != -1) { 166 | scale = (float) h / (float) image->height; 167 | width = (int) ceil((double) image->width * (double) scale); 168 | height = h; 169 | } 170 | else if (w != -1 && h == -1) { 171 | scale = (float) w / (float) image->width; 172 | width = w; 173 | height = (int) ceil((double) image->height * (double) scale); 174 | } 175 | else { 176 | scale = (float) w / (float) image->width; 177 | width = w; 178 | height = h; 179 | } 180 | 181 | // Allocate memory 182 | pitch = 4*width; 183 | pixel_buffer = malloc((size_t) (4*width*height)); 184 | if (pixel_buffer == NULL) { 185 | log_error("Could not alloc SVG pixel buffer."); 186 | return NULL; 187 | } 188 | 189 | // Rasterize image 190 | nsvgRasterize(rasterizer, image, 0, 0, scale, pixel_buffer, width, height, pitch); 191 | SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixel_buffer, 192 | width, 193 | height, 194 | 32, 195 | pitch, 196 | COLOR_MASKS 197 | ); 198 | SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface); 199 | if (rect != NULL) { 200 | rect->w = width; 201 | rect->h = height; 202 | } 203 | free(pixel_buffer); 204 | SDL_FreeSurface(surface); 205 | nsvgDelete(image); 206 | return texture; 207 | } 208 | 209 | // A function to render the highlight for the buttons 210 | SDL_Texture *render_highlight(int width, int height, SDL_Rect *rect) 211 | { 212 | // Insert user config variables into SVG-formatted text buffer 213 | char *buffer = NULL; 214 | char *outline_buffer = NULL; 215 | if (config.highlight_outline_size) { 216 | float stroke_opacity = ((float) config.highlight_outline_color.a) / 255.0f; 217 | format_highlight_outline(&outline_buffer, 218 | config.highlight_outline_size, 219 | config.highlight_outline_color, 220 | stroke_opacity 221 | ); 222 | } 223 | else 224 | outline_buffer = ""; 225 | 226 | float fill_opacity = ((float) config.highlight_fill_color.a) / 255.0f; 227 | format_highlight(&buffer, 228 | width, 229 | height, 230 | config.highlight_rx, 231 | config.highlight_fill_color, 232 | fill_opacity, 233 | outline_buffer 234 | ); 235 | 236 | // Rasterize the SVG 237 | SDL_Texture *texture = rasterize_svg(buffer, -1, -1, rect); 238 | 239 | // Cleanup 240 | free(buffer); 241 | if (config.highlight_outline_size) 242 | free(outline_buffer); 243 | 244 | return texture; 245 | } 246 | 247 | // A function to render the scroll indicators 248 | void render_scroll_indicators(Scroll *scroll, int height, Geometry *geo) 249 | { 250 | // Format the SVG 251 | char *buffer = NULL; 252 | float opacity = (float) config.scroll_indicator_fill_color.a / 255.0f; 253 | format_scroll_indicator(&buffer, 254 | config.scroll_indicator_fill_color, 255 | config.scroll_indicator_outline_size, 256 | config.scroll_indicator_outline_color, 257 | opacity 258 | ); 259 | 260 | // Rasterize the SVG 261 | scroll->texture = rasterize_svg(buffer, 262 | -1, 263 | height, 264 | &scroll->rect_right 265 | ); 266 | free(buffer); 267 | scroll->rect_left.w = scroll->rect_right.w; 268 | scroll->rect_left.h = scroll->rect_right.h; 269 | if (scroll->texture == NULL) { 270 | log_error("Could not render scroll indicator, disabling feature"); 271 | free(scroll); 272 | config.scroll_indicators = false; 273 | return; 274 | } 275 | 276 | // Calculate screen position 277 | scroll->rect_right.y = geo->screen_height - geo->screen_margin - scroll->rect_right.h; 278 | scroll->rect_right.x = geo->screen_width - geo->screen_margin - scroll->rect_right.w; 279 | scroll->rect_left.y = scroll->rect_right.y; 280 | scroll->rect_left.x = geo->screen_margin; 281 | } 282 | 283 | // A function to render text 284 | SDL_Surface *render_text(const char *text, TextInfo *info, SDL_Rect *rect, int *text_height) 285 | { 286 | TTF_Font *output_font = NULL; 287 | TTF_Font *reduced_font = NULL; // Font for Shrink text oversize mode 288 | int w, h; 289 | 290 | // Copy text into new buffer in case we need to manipulate it 291 | char *text_buffer = strdup(text); 292 | 293 | // Calculate size of the rendered title 294 | TTF_SizeUTF8(info->font, text_buffer, &w, &h); 295 | 296 | // If title is too large to fit 297 | if (info->oversize_mode != OVERSIZE_NONE && w > info->max_width) { 298 | 299 | // Truncate mode: 300 | if (info->oversize_mode == OVERSIZE_TRUNCATE) { 301 | utf8_truncate(text_buffer, w, info->max_width); 302 | TTF_SizeUTF8(info->font, text_buffer, &w, &h); 303 | } 304 | 305 | // Shrink mode: 306 | else if (info->oversize_mode == OVERSIZE_SHRINK) { 307 | int reduced_font_size = (int) info->font_size - 1; 308 | reduced_font = TTF_OpenFont(*info->font_path, reduced_font_size); 309 | TTF_SizeUTF8(reduced_font, text_buffer, &w, &h); 310 | 311 | // Keep trying smaller font until it fits 312 | while (w > info->max_width && reduced_font_size > 0) { 313 | TTF_CloseFont(reduced_font); 314 | reduced_font = NULL; 315 | reduced_font_size--; 316 | reduced_font = TTF_OpenFont(*info->font_path, reduced_font_size); 317 | TTF_SizeUTF8(reduced_font, text_buffer, &w, &h); 318 | } 319 | 320 | if (reduced_font_size) 321 | output_font = reduced_font; 322 | else 323 | reduced_font = NULL; 324 | } 325 | } 326 | if (reduced_font == NULL) 327 | output_font = info->font; 328 | 329 | // Render surface 330 | SDL_Surface *surface = NULL; 331 | if (info->shadow) { 332 | int shadow_offset = h / 40; 333 | if (shadow_offset < 2) 334 | shadow_offset = 2; 335 | SDL_Surface *foreground = TTF_RenderUTF8_Blended(output_font, 336 | text_buffer, 337 | *info->color 338 | ); 339 | SDL_Surface *shadow = TTF_RenderUTF8_Blended(output_font, 340 | text_buffer, 341 | *info->shadow_color 342 | ); 343 | surface = SDL_CreateRGBSurfaceWithFormat(0, 344 | foreground->w + shadow_offset, 345 | foreground->h + shadow_offset, 346 | 32, 347 | SDL_PIXELFORMAT_ARGB8888 348 | ); 349 | Uint32 color = SDL_MapRGBA(surface->format, 0, 0, 0, 0); 350 | SDL_FillRect(surface, NULL, color); 351 | SDL_Rect shadow_rect = {shadow_offset, shadow_offset, shadow->w, shadow->h}; 352 | SDL_BlitSurface(shadow, NULL, surface, &shadow_rect); 353 | SDL_Rect rect = {0, 0, foreground->w, foreground->h}; 354 | SDL_BlitSurface(foreground, NULL, surface, &rect); 355 | SDL_FreeSurface(foreground); 356 | SDL_FreeSurface(shadow); 357 | } 358 | else 359 | surface = TTF_RenderUTF8_Blended(output_font, 360 | text_buffer, 361 | *info->color 362 | ); 363 | 364 | // Set geometry 365 | rect->w = surface->w; 366 | rect->h = surface->h; 367 | if (info->oversize_mode == OVERSIZE_SHRINK && text_height != NULL) 368 | *text_height = h; 369 | 370 | // Clean up 371 | if (reduced_font != NULL) 372 | TTF_CloseFont(reduced_font); 373 | free(text_buffer); 374 | 375 | return surface; 376 | } 377 | 378 | // A function to render text into a texture 379 | SDL_Texture *render_text_texture(const char *text, TextInfo *info, SDL_Rect *rect, int *text_height) 380 | { 381 | SDL_Surface *surface = render_text(text, info, rect, text_height); 382 | return load_texture(surface); 383 | } 384 | 385 | // A function to load a font from a file 386 | int load_font(TextInfo *info, const char *default_font) 387 | { 388 | char *font_path = *info->font_path; 389 | // Load user specified font 390 | if (font_path != NULL) 391 | info->font = TTF_OpenFont(font_path, info->font_size); 392 | 393 | // Try to load default font if we failed loading from config file 394 | if (info->font == NULL) { 395 | log_error("Could not initialize font from config file"); 396 | const char *prefixes[2]; 397 | char fonts_exe_buffer[MAX_PATH_CHARS + 1]; 398 | prefixes[0] = join_paths(fonts_exe_buffer, sizeof(fonts_exe_buffer), 3, config.exe_path, PATH_ASSETS_EXE, PATH_FONTS_EXE); 399 | #ifdef __unix__ 400 | prefixes[1] = PATH_FONTS_SYSTEM; 401 | #else 402 | prefixes[1] = PATH_FONTS_RELATIVE; 403 | #endif 404 | char *default_font_path = find_file(default_font, 2, prefixes); 405 | 406 | // Replace user font with default in config 407 | if (default_font_path != NULL) { 408 | info->font = TTF_OpenFont(default_font_path, info->font_size); 409 | free(font_path); 410 | *(info->font_path) = strdup(default_font_path); 411 | free(default_font_path); 412 | } 413 | if (info->font == NULL) { 414 | log_fatal("Could not load default font"); 415 | return 1; 416 | } 417 | } 418 | return 0; 419 | } 420 | -------------------------------------------------------------------------------- /docs/setup_linux.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Linux Setup Guide 4 | --- 5 | # Linux Setup Guide 6 | ## Table of Contents 7 | 1. [Overview](#overview) 8 | 2. [Autostarting](#autostarting) 9 | 3. [Choosing a Distro](#choosing-a-distro) 10 | 4. [Desktop Enviornment](#desktop-environment) 11 | 5. [Display Protocol](#display-protocol) 12 | 6. [Transparent Backgrounds](#transparent-backgrounds) 13 | 7. [Kiosk Mode Setup](#kiosk-mode-setup) 14 | 8. [HTPC as Audio Receiver](#htpc-as-audio-receiver) 15 | 9. [Using an IR Remote](#using-an-ir-remote) 16 | 17 | ## Overview 18 | This page contains tips for setting up Flex Launcher on Linux-based systems, as well as general HTPC setup tips. 19 | 20 | ## Autostarting 21 | In a typical HTPC setup, Flex Launcher is autostarted after boot. On Linux, this can be accomplished in multiple ways. The most widely implemented is [XDG Autostart](https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html). Application .desktop files in `~/.config/autostart` will be autostarted upon user login. The .desktop file for Flex Launcher is installed to `/usr/share/applications`. Copy it to your autostart directory: 22 | ```bash 23 | mkdir -p ~/.config/autostart 24 | cp /usr/share/applications/flex-launcher.desktop ~/.config/autostart 25 | ``` 26 | Additionally, some desktop enviornments have their own, separate autostart protocol. Consult your DE's documentation for more details. 27 | 28 | ## Choosing A Distro 29 | My preferred distribution for a Linux HTPC is [Arch](https://archlinux.org/). It has a minimal base installation and very up-to-date packages, which make it well suited for HTPC use. 30 | 31 | If you dislike Arch's rolling release model, another good choice is [Debian](https://www.debian.org/). When installing, make sure to deselect a desktop environment so you can have a minimal base install, similar to Arch. 32 | 33 | ## Desktop Environment 34 | I recommend [Openbox](http://openbox.org/wiki/Main_Page), which is not a full-fledged desktop enviornment, but rather a standalone window manager. An HTPC is a very simple device, and most of the features of desktop environments are not needed and add unnecessary bloat. Since HTPC applications are always in fullscreen, not even compositing is necessary. 35 | 36 | Openbox is lightweight and highly customizable. The basic install of Openbox provides only a black root window, over which Flex Launcher and your desired applications can be drawn. 37 | 38 | ## Display Protocol 39 | Being based on SDL, Flex Launcher has support for both X11 and Wayland display protocols. I recommend using X11. The benefits that Wayland offers aren't broadly applicable to an HTPC, and Wayland support is still lacking in many areas. 40 | 41 | If you choose to run Flex Launcher in Wayland, it is strongly recommended to use it with the latest stable release of SDL, as later versions have seen significantly improved support. 42 | 43 | ## Transparent Backgrounds 44 | Transparent backgrounds requires compositor support. I recommend [picom](https://github.com/yshui/picom), which supports transparency via GLSL shaders. The method described below requires version 10 or later which, as of this writing, is not yet packaged for most Linux distros. If this is this case for your distro, you will need to build it from source yourself. 45 | 46 | The picom option `--window-shader-fg` can be used to specify a custom GLSL shader to apply to the windows. The below shader program can be used as a starting point to implement transparency with Flex Launcher. The macros should be changed to match the values in your Flex Launcher config file, if necessary. 47 | 48 | ```glsl 49 | #version 330 50 | 51 | // Set this to 1 to restore a semi-transparent highlight 52 | #define RESTORE_HIGHLIGHT 1 53 | 54 | // Set this to 1 to use a background overlay to darken the video 55 | #define BACKGROUND_OVERLAY 0 56 | 57 | #define BACKGROUND_OVERLAY_R 0x00 58 | #define BACKGROUND_OVERLAY_G 0x00 59 | #define BACKGROUND_OVERLAY_B 0x00 60 | #define BACKGROUND_OVERLAY_OPACITY 0.25 61 | 62 | // Replace with the values from your Flex Launcher config 63 | #define CHROMA_R 0x01 64 | #define CHROMA_G 0x01 65 | #define CHROMA_B 0x01 66 | #define HIGHLIGHT_R 0xFF 67 | #define HIGHLIGHT_G 0xFF 68 | #define HIGHLIGHT_B 0xFF 69 | #define HIGHLIGHT_OPACITY 0.25 70 | 71 | #define BLEND(src, dst) vec4( \ 72 | src.x * src.w + (dst.x * (1.0 - src.w)), \ 73 | src.y * src.w + (dst.y * (1.0 - src.w)), \ 74 | src.z * src.w + (dst.z * (1.0 - src.w)), \ 75 | src.w + (dst.w * (1 - src.w)) \ 76 | ) 77 | 78 | in vec2 texcoord; 79 | uniform sampler2D tex; 80 | uniform vec4 chroma_key = vec4(float(CHROMA_R) / 255.0, float(CHROMA_G) / 255.0, float(CHROMA_B) / 255.0, 1.0); 81 | uniform vec4 highlight_color = vec4(float(HIGHLIGHT_R) / 255.0, float(HIGHLIGHT_G) / 255.0, float(HIGHLIGHT_B) / 255.0, float(int(HIGHLIGHT_OPACITY * 255.0)) / 255.0); 82 | #if BACKGROUND_OVERLAY 83 | uniform vec4 overlay_color = vec4(float(BACKGROUND_OVERLAY_R) / 255.0, float(BACKGROUND_OVERLAY_G) / 255.0, float(BACKGROUND_OVERLAY_B) / 255.0, float(int(BACKGROUND_OVERLAY_OPACITY * 255.0)) / 255.0); 84 | #endif 85 | 86 | vec4 window_shader() 87 | { 88 | vec4 c = texelFetch(tex, ivec2(texcoord), 0); 89 | 90 | // Remove background 91 | vec4 vdiff = abs(chroma_key - c); 92 | float diff = max(max(max(vdiff.r, vdiff.g), vdiff.b), vdiff.a); 93 | if (diff < 0.0001) 94 | #if BACKGROUND_OVERLAY 95 | c = overlay_color; 96 | #else 97 | c.w = 0.0; 98 | #endif 99 | 100 | // Restore highlight 101 | #if RESTORE_HIGHLIGHT 102 | else { 103 | vec4 blend = BLEND(highlight_color, chroma_key); 104 | vdiff = abs(blend - c); 105 | diff = max(max(max(vdiff.r, vdiff.g), vdiff.b), vdiff.a); 106 | if (diff < 0.01) { 107 | #if BACKGROUND_OVERLAY 108 | c = BLEND(highlight_color, overlay_color); 109 | #else 110 | c = highlight_color; 111 | c *= c.w; 112 | #endif 113 | } 114 | } 115 | #endif 116 | 117 | return c; 118 | } 119 | 120 | ``` 121 | Save the shader program to a .glsl file in a directory of your choice. 122 | 123 | Since you will typically not want to run the compositor while your launched applications are running, you will need to start and stop it frequently. This is best accomplished with a systemd service. Create a new user unit file: 124 | ```bash 125 | nano /etc/systemd/user/picom-transparent.service 126 | ``` 127 | Paste the following into the file: 128 | ```ini 129 | [Unit] 130 | Description=X11 compositor with alpha transparency 131 | 132 | [Service] 133 | ExecStart=/usr/bin/picom --backend glx --force-win-blend --window-shader-fg=/path/to/shader.glsl 134 | 135 | [Install] 136 | WantedBy=multi-user.target 137 | ``` 138 | Replace `` with the appropriate path, then save the file. The compositor can be enabled/disabled with: 139 | ```bash 140 | systemctl --user start picom-transparency.service 141 | systemctl --user stop picom-transparency.service 142 | ``` 143 | Use shell scripts to launch your applications and make sure to stop the compositor prior to launch, and start it again after the application has completed. 144 | 145 | ### Animated Backgrounds 146 | For an animated transparent background implementation, I recommend [anipaper](https://github.com/Theldus/anipaper), which will play a video of your choice in a loop. anipaper should be run with the following options enabled: 147 | - `-b` (Borderless Fullscreen): Since this method requires a compositor, we will need to run anipaper as a borderless fullscreen window rather than as a wallpaper 148 | - `-p` (Pause): We will need to pause the playback when the application launches, and resume it after it finishes 149 | - `-d` (Hardware decoding): This will keep the CPU use as low as possible, and consequently fan noise and power consumption. Requires the hardware device name as an argument. This option is not absolutely required, but you should use it if your hardware is supported. 150 | 151 | In your autostart setup, make sure that anipaper starts *before* Flex Launcher. This ensures that Flex Launcher will have the window focus. 152 | 153 | #### Pausing anipaper 154 | When the `-p` option is enabled, anipaper can be paused when applications launch, and resumed when they finish. This is necessary to prevent anipaper from unecessarily consuming resources when the video is not visible. Use the following command in your scripts to pause and resume anipaper 155 | ```bash 156 | pkill -SIGUSR1 anipaper 157 | ``` 158 | 159 | #### Example Script 160 | To handle the advanced functionality of starting and stopping various services when applications are launched, you will need to use a shell script. Here is a simple example: 161 | ```bash 162 | # Pre-launch 163 | systemctl --user stop picom-transparent.service 164 | pkill -SIGUSR1 anipaper 165 | 166 | # Launch application 167 | if [[ "$1" == "kodi" ]]; then 168 | ... 169 | elif [[ "$1" == "youtube" ]]; then 170 | ... 171 | fi 172 | 173 | # Post-launch 174 | systemctl --user start picom-transparent.service 175 | pkill -SIGUSR1 anipaper 176 | ``` 177 | The above example script takes an argument which determines which application to launch. The script executes pre and post launch commands which are common to all applications, as well as commands that are specific to the particular application being launched. In your Flex Launcher config, your menu entry command should be the path to the script with the application you want to launch as the first argument. 178 | 179 | ## Kiosk Mode Setup 180 | "Kiosk Mode" typically refers to a user interface which resembles an embedded-style device that performs only a single function (as opposed to a multitasking PC with a desktop interface). This section contains instructions to set up my interpretation of a "Kiosk Mode" interface for a Linux HTPC. The design is based on my recommendations in the previous sections, consisting of Xorg, Openbox, and Flex Launcher. 181 | 182 | ### Prerequisites 183 | Before starting I assume that you have one of the following operating systems installed *without* a desktop evironnment (only a console login): 184 | - Arch Linux 185 | - Debian Bookworm or later 186 | - Raspberry Pi OS Lite 187 | 188 | I also assume that you can use the Linux command line at an intermediate level or higher. 189 | 190 | ### First Steps 191 | The first thing to do is configure a network connection and SSH server. On Raspberry Pi, both of those tasks are easily accomplished with the `raspi-config` utility. Otherwise, the exact steps will vary based on which operating system you are on (Arch/Debian) and what backends you choose. Refer to the [Arch Wiki](https://wiki.archlinux.org/) and [Debian Wiki](https://wiki.debian.org/) for more detailed help. 192 | 193 | It is recommended to set up a static IP on your LAN. Since you will be doing most of your setup and maintenance remotely via SSH, you will want to have a consistent login. 194 | 195 | You will also need to install a terminal-based text editor, since we don't have any graphical interface yet. I recommend nano, and that is what will be used in subsequent sections. You can use a different text editor if you wish, just make sure to substitute commands where appropriate. 196 | 197 | ### Set Up Autologin 198 | Configure the TTY console to log you in automatically without user or password prompt. On Raspberry Pi, you can use `raspi-config`, or you can also follow the instructions for Arch/Debian below which will also work. 199 | 200 | Create a systemd drop-in file for the getty TTY1 service: 201 | ``` 202 | sudo nano /etc/systemd/system/getty@tty1.service.d/autologin.conf 203 | ``` 204 | Paste the following into the file: 205 | 206 | ```ini 207 | [Service] 208 | ExecStart= 209 | ExecStart=-/sbin/agetty --autologin --noclear %I $TERM 210 | ``` 211 | Replace `` with your username, then save the file. Reboot your HTPC and verify that it logs you into the console automatically. 212 | 213 | ### Install Packages 214 | Install the necessary packages. 215 | 216 | **Arch:** 217 | ```bash 218 | sudo pacman -S xorg xorg-xinit openbox unclutter pulseaudio wget 219 | ``` 220 | **Debian/Raspberry Pi:** 221 | ```bash 222 | sudo apt install xorg openbox unclutter-xfixes pulseaudio wget 223 | ``` 224 | Then, install Flex Launcher according to the instructions on the [README](https://github.com/complexlogic/flex-launcher#linux). Also, make sure to [copy the assets to your home directory](https://github.com/complexlogic/flex-launcher#copying-assets-to-home-directory). 225 | 226 | ### Configure Xorg 227 | Configure X to start after user login with `.bash_profile` and `startx`: 228 | ```bash 229 | nano ~/.bash_profile 230 | ``` 231 | Paste the following into the file and save it: 232 | ```bash 233 | if [ "x${SSH_TTY}" = "x" ]; then 234 | startx 235 | fi 236 | ``` 237 | The `.bash_profile` script will execute every time the user logs in, *including remote logins via SSH*. The purpose of the `if` block is to ensure that `startx` will only be executed during a local login, not remote logins via SSH. 238 | 239 | The `startx` program will look for an `xinitrc` script to run: first in the user's home directory, then in the system directory. We will define a basic user `xinitrc` script to configure X and start Openbox: 240 | ```bash 241 | nano ~/.xinitrc 242 | ``` 243 | Paste the following into the file: 244 | ```bash 245 | #!/bin/sh 246 | userresources=$HOME/.Xresources 247 | usermodmap=$HOME/.Xmodmap 248 | sysresources=/etc/X11/xinit/.Xresources 249 | sysmodmap=/etc/X11/xinit/.Xmodmap 250 | 251 | # merge in defaults and keymaps 252 | if [ -f $sysresources ]; then 253 | xrdb -merge $sysresources 254 | fi 255 | 256 | if [ -f $sysmodmap ]; then 257 | xmodmap $sysmodmap 258 | fi 259 | 260 | if [ -f "$userresources" ]; then 261 | xrdb -merge "$userresources" 262 | fi 263 | 264 | if [ -f "$usermodmap" ]; then 265 | xmodmap "$usermodmap" 266 | fi 267 | 268 | # Startup scripts 269 | if [ -d /etc/X11/xinit/xinitrc.d ] ; then 270 | for f in /etc/X11/xinit/xinitrc.d/?*.sh ; do 271 | [ -x "$f" ] && . "$f" 272 | done 273 | unset f 274 | fi 275 | 276 | unclutter --start-hidden & 277 | exec openbox-session 278 | ``` 279 | You can make additions to this script if you wish. For example, the screen resolution can be forced using the `xrandr` utility. However, note that the `exec openbox-session` line of the script will never return. Therefore, any additions you make must come *before* this line. 280 | 281 | ### Configure Openbox 282 | Openbox ships with default configuration files installed to `/etc/xdg/openbox`. These files should be copied to your home directory: 283 | ```bash 284 | mkdir -p ~/.config/openbox 285 | cp -a /etc/xdg/openbox/ ~/.config/ 286 | ``` 287 | Among these configuration files is `autostart`, which Openbox will execute after initialization. This is the best way to autostart Flex Launcher: 288 | ```bash 289 | nano ~/.config/openbox/autostart 290 | ``` 291 | Add `flex-launcher` to the file, then save it. 292 | 293 | #### Application Menu 294 | You can access a basic application menu by right clicking anywhere on Openbox's root window. The menu entries are populated from `~/.config/openbox/menu.xml`. On Arch, this is a static menu that was pre-populated with programs, most of which you won't have installed. See the [Openbox Wiki](http://openbox.org/wiki/Help:Menus#Static_menus) for editing instructions. On Debian/Raspberry Pi, this is a dynamic menu that is automatically populated from your installed .desktop files, so you shouldn't need to edit anything. 295 | 296 | You can also install taskbars and various other graphical interfaces for Openbox, however, this is not recommended. The default interface is clean and the right-click menu provides enough basic functionality for maintenance of your HTPC. 297 | 298 | #### Keybinds 299 | [Keybinds](http://openbox.org/wiki/Help:Bindings) can be set in the `rc.xml` file, which maps a keypress to an [Action](http://openbox.org/wiki/Help:Actions). The "Close" action closes the active window, which is very useful for an HTPC. It allows you to quit the current application and return back to the launcher by using a button on your remote. For example, you can use the F10 key to quit the current application like so: 300 | ```bash 301 | nano ~/.config/openbox/rc.xml 302 | ``` 303 | In the `` section, paste the following: 304 | ```xml 305 | 306 | 307 | 308 | ``` 309 | This assumes that you have a key on your TV remote that maps to F10. 310 | 311 | ### Install Applications 312 | The last step is to install your desired applications. Edit your Flex Launcher configuration file to add menu entries for each of the applications. See the [configuration file documentation](https://complexlogic.github.io/flex-launcher/configuration) for more details. 313 | 314 | ## HTPC as Audio Receiver 315 | You can use your HTPC as a smart audio receiver for listening to music or podcasts on your living room speakers. 316 | 317 | ### Bluetooth 318 | If your HTPC has Bluetooth connectivity, you can use it as a receiver and play audio from your smartphone or other device. PulseAudio has support for the A2DP Bluetooth profile via the BlueZ stack. Make sure you have the module installed. It is packaged as `pulseaudio-bluetooth` on Arch and `pulseaudio-module-bluetooth` on Debian/Raspberry Pi. Also, make sure that the systemd bluetooth service is enabled. 319 | 320 | You will need to pair your device with your HTPC. This is best done with the `bluetoothctl` utility while you are logged in via SSH. Instructions can be found on the [Arch Wiki](https://wiki.archlinux.org/title/Bluetooth#Pairing) (applicable to all distros, not just Arch). 321 | 322 | The pairing only needs to be performed once. Subsequent connections can be initiated from the Bluetooth settings on your mobile device. Make sure to check on your device that audio output is enabled for the connection. Then, you can test playing some audio. PulseAudio should send the audio to your default sink without any additional configuration. 323 | 324 | ### Spotify Connect 325 | Spotify has a feature called Spotify Connect that allows Premium subscribers to stream audio to another device from the app. [Librespot](https://github.com/librespot-org/librespot) implements Spotify Connect as a Linux daemon. With Librespot, you can play Spotify audio on your HTPC, but control it from your smartphone. 326 | 327 | First, install Librespot. I recommend building it from source. The instructions are available on GitHub. 328 | 329 | I manage Librespot with a systemd user service: 330 | ```bash 331 | mkdir -p ~/.config/systemd/user 332 | nano ~/.config/systemd/user/librespot.service 333 | ``` 334 | Paste the following into the file: 335 | ```ini 336 | [Unit] 337 | Description=Librespot Spotify Connect daemon 338 | After=network-online.target 339 | Wants=network-online.target 340 | Wants=pulseaudio.service 341 | After=pulseaudio.service 342 | 343 | [Service] 344 | ExecStart=/usr/bin/librespot \ 345 | --backend pulseaudio \ 346 | --bitrate 320 \ 347 | --device-type computer \ 348 | --disable-audio-cache \ 349 | --name "HTPC" 350 | 351 | [Install] 352 | WantedBy=network-online.target 353 | ``` 354 | Verify that the path of the `librespot` executable is the same on your system, or change it if necessary. The `--bitrate` controls the bitrate of the streamed audio in kbps. The `--name` controls which name your HTPC will show up as in the Spotify app devices menu. Change them if desired. 355 | 356 | If you want Librespot to always run, you can simply enable the service: 357 | ```bash 358 | systemctl --user enable librespot 359 | ``` 360 | If you want Librespot to stop when you launch an application, you will have to use scripts and manually start and stop it: 361 | ```bash 362 | systemctl --user start librespot 363 | systemctl --user stop librespot 364 | ``` 365 | Verify Librespot is running with: 366 | ```bash 367 | systemctl --user status librespot 368 | ``` 369 | Then, try to connect to it with the Spotify app on your phone. 370 | 371 | ## Using an IR Remote 372 | Some mini PCs have an integrated IR receiver, which can receive signals from a conventional TV remote. The Linux kernel has built-in support for decoding IR signals, which allows you to translate them into keyboard presses. Before starting, make sure the IR receiver is enabled in your motherboard settings. 373 | 374 | The IR signal decoding can be controlled with the `ir-keytable` utility. This program is packaged with `v4l-utils` on most distros. First, check to see that your IR receiver is recognized: 375 | ```bash 376 | sudo ir-keytable 377 | ``` 378 | The result should look something like this: 379 | ``` 380 | Found /sys/class/rc/rc0/ with: 381 | Name: ITE8708 CIR transceiver 382 | Driver: ite-cir 383 | Default keymap: rc-rc6-mce 384 | Input device: /dev/input/event3 385 | LIRC device: /dev/lirc0 386 | Supported kernel protocols: lirc rc-5 rc-5-sz jvc sony nec sanyo mce_kbd rc-6 sharp xmp imon rc-mm 387 | Enabled kernel protocols: lirc nec rc-6 388 | bus: 25, vendor/product: 1283:0000, version: 0x0000 389 | Repeat delay = 500 ms, repeat period = 125 ms 390 | ``` 391 | Take note of the supported kernel protocols. Test the receiving with `-t`: 392 | ```bash 393 | ir-keytable -v -t -p irc,rc-5,rc-5-sz,jvc,sony,nec,sanyo,mce_kbd,rc-6,sharp,xmp,imon,rc-mm 394 | ``` 395 | The protocols specified with `-p` should be comma delimited, and must *not* contain a space. You should update the protocols of the above command based on your output of `sudo ir-keytable` in the previous step. 396 | 397 | The `-t` mode will output information for each received signal. Point your remote at the receiver and press various buttons to verify that the signals are being received. Take note of the protocol and the hex values for each button. You will need to create a mapping file that contains the corresponding hex value for each button name. Here is a simple example: 398 | ``` 399 | #table RCA, type: rc-6 400 | 0x800f741e KEY_UP 401 | 0x800f741f KEY_DOWN 402 | 0x800f7420 KEY_LEFT 403 | 0x800f7421 KEY_RIGHT 404 | 0x800f7422 KEY_ENTER 405 | 0x800f7423 KEY_BACKSPACE 406 | 0x800f7425 KEY_ESC 407 | ``` 408 | The table name is arbitrary, but the `type` must match one of the supported protocols from the previous step. You can find a comprehensive list of valid button names by running: 409 | ```bash 410 | irrecord -l 411 | ``` 412 | Save your mapping file as `/etc/rc_keymaps/default.txt`. Then, test the mapping: 413 | ```bash 414 | sudo ir-keytable -c -w /etc/rc_keymaps/default.txt 415 | ``` 416 | Open a program on your HTPC, press the mapped buttons on your remote, and verify that the proper keys are being triggered on the HTPC. 417 | 418 | ### Make the Mapping Persistent 419 | The mapping will not persist between reboots. Therefore, you will need to set up a way to run the `ir-keytable` command automatically after boot. One way to do that is with a systemd service. 420 | ```bash 421 | sudo nano /etc/systemd/system/remote-setup.service 422 | ``` 423 | Paste the following into the file: 424 | ```ini 425 | [Unit] 426 | Description=Set up IR remotes 427 | After=multi-user.target 428 | 429 | [Service] 430 | Type=oneshot 431 | ExecStart=/usr/bin/ir-keytable -c -w /etc/rc_keymaps/default.txt 432 | 433 | [Install] 434 | WantedBy=multi-user.target 435 | ``` 436 | Save the file, then enable the service: 437 | ```bash 438 | sudo systemctl enable remote-setup 439 | ``` 440 | Reboot your HTPC and verify that the remote still works. 441 | --------------------------------------------------------------------------------