Files
2026-06-12 15:25:13 +02:00

285 lines
12 KiB
C++

/**
* @file main.cpp
* @brief StreamHub ImGui desktop client entry point.
*
* Usage: StreamHubClient [-host <ip>] [-port <N>]
*
* Defaults: host=127.0.0.1 port=8090
*/
#include "App.h"
#include "Icons.h"
#include "imgui.h"
#include "imgui_impl_sdl2.h"
#include "imgui_impl_opengl3.h"
#include "implot.h"
#include <SDL.h>
#include <SDL_opengl.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <string>
#include <unistd.h>
int main(int argc, char** argv) {
/* ── Parse args ─────────────────────────────────────────────────── */
std::string host = "127.0.0.1";
uint16_t port = 8090u;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-host") == 0 && i + 1 < argc) {
host = argv[++i];
} else if (strcmp(argv[i], "-port") == 0 && i + 1 < argc) {
port = static_cast<uint16_t>(atoi(argv[++i]));
} else if (strcmp(argv[i], "-h") == 0 ||
strcmp(argv[i], "--help") == 0) {
printf("Usage: StreamHubClient [-host <ip>] [-port <N>]\n");
return 0;
}
}
/* ── SDL2 + OpenGL ──────────────────────────────────────────────── */
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
fprintf(stderr, "SDL_Init error: %s\n", SDL_GetError());
return 1;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_WindowFlags wf = static_cast<SDL_WindowFlags>(
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
SDL_Window* window = SDL_CreateWindow(
"StreamHub Client",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
1280, 800, wf);
if (!window) {
fprintf(stderr, "SDL_CreateWindow error: %s\n", SDL_GetError());
return 1;
}
SDL_GLContext glCtx = SDL_GL_CreateContext(window);
SDL_GL_MakeCurrent(window, glCtx);
SDL_GL_SetSwapInterval(1); /* vsync */
/* ── ImGui setup ─────────────────────────────────────────────────── */
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
/* ── Fonts: bundled resources (Fira Sans + Font Awesome) ─────────────── *
* The font dir is resolved relative to the executable so both build-tree
* and installed layouts work; the source tree is the last fallback. */
const float kFontSizePx = 17.0f;
{
std::string exeDir;
{
char buf[4096];
ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
if (char* slash = strrchr(buf, '/')) { *slash = '\0'; }
exeDir = buf;
}
}
const std::string fontDirCandidates[] = {
exeDir + "/../share/streamhub-client/fonts", /* installed */
exeDir + "/resources/fonts", /* build tree */
std::string(APP_RESOURCE_DIR) + "/fonts", /* source tree */
};
auto findFont = [&](const char* file) -> std::string {
for (const auto& d : fontDirCandidates) {
std::string p = d + "/" + file;
if (access(p.c_str(), R_OK) == 0) { return p; }
}
return "";
};
std::string uiFont = findFont("FiraSans-Regular.ttf");
bool loaded = !uiFont.empty() &&
io.Fonts->AddFontFromFileTTF(uiFont.c_str(), kFontSizePx);
if (loaded) {
fprintf(stderr, "Font: loaded %s\n", uiFont.c_str());
} else {
io.Fonts->AddFontDefault();
fprintf(stderr, "Font: bundled font not found, using built-in.\n");
}
#ifdef HAVE_FONT_AWESOME
/* Merge Font Awesome 6 solid glyphs into the base font */
std::string faFont = findFont("fa-solid-900.ttf");
static const ImWchar faRanges[] = { ICON_MIN_FA, ICON_MAX_16_FA, 0 };
ImFontConfig faCfg;
faCfg.MergeMode = true;
faCfg.PixelSnapH = true;
faCfg.GlyphMinAdvanceX = kFontSizePx * 0.9f; /* monospace-ish icons */
faCfg.GlyphOffset = ImVec2(0.f, 1.f);
if (faFont.empty() ||
!io.Fonts->AddFontFromFileTTF(faFont.c_str(), kFontSizePx * 0.80f,
&faCfg, faRanges)) {
fprintf(stderr, "Font: failed to merge Font Awesome glyphs\n");
}
#endif
}
ImGui::StyleColorsDark();
ImGuiStyle& style = ImGui::GetStyle();
style.WindowRounding = 10.f;
style.ChildRounding = 8.f;
style.FrameRounding = 7.f; /* rounded buttons/inputs */
style.PopupRounding = 8.f;
style.GrabRounding = 7.f;
style.ScrollbarRounding = 12.f;
style.TabRounding = 7.f;
style.FramePadding = ImVec2(9.f, 5.f);
style.ItemSpacing = ImVec2(8.f, 6.f);
style.ItemInnerSpacing = ImVec2(6.f, 4.f);
style.CellPadding = ImVec2(8.f, 4.f);
style.ScrollbarSize = 11.f;
style.WindowPadding = ImVec2(10.f, 10.f);
style.PopupBorderSize = 1.f;
style.FrameBorderSize = 0.f;
style.WindowBorderSize = 1.f;
style.SeparatorTextBorderSize = 2.f;
style.WindowTitleAlign = ImVec2(0.5f, 0.5f);
/* ── Catppuccin Mocha palette ─────────────────────────────────────── */
const ImVec4 kCrust (0.067f,0.067f,0.106f,1.f); /* #11111b */
const ImVec4 kMantle (0.094f,0.094f,0.145f,1.f); /* #181825 */
const ImVec4 kBase (0.118f,0.118f,0.180f,1.f); /* #1e1e2e */
const ImVec4 kSurface0 (0.192f,0.196f,0.267f,1.f); /* #313244 */
const ImVec4 kSurface1 (0.271f,0.278f,0.353f,1.f); /* #45475a */
const ImVec4 kSurface2 (0.345f,0.357f,0.439f,1.f); /* #585b70 */
const ImVec4 kOverlay0 (0.424f,0.439f,0.525f,1.f); /* #6c7086 */
const ImVec4 kText (0.804f,0.839f,0.957f,1.f); /* #cdd6f4 */
const ImVec4 kBlue (0.537f,0.706f,0.980f,1.f); /* #89b4fa */
const ImVec4 kLavender (0.706f,0.745f,0.996f,1.f); /* #b4befe */
const ImVec4 kMauve (0.796f,0.651f,0.969f,1.f); /* #cba6f7 */
const ImVec4 kPeach (0.980f,0.702f,0.529f,1.f); /* #fab387 */
ImVec4* c = style.Colors;
c[ImGuiCol_WindowBg] = kBase;
c[ImGuiCol_ChildBg] = kMantle;
c[ImGuiCol_PopupBg] = kMantle;
c[ImGuiCol_Border] = kSurface0;
c[ImGuiCol_BorderShadow] = ImVec4(0.f,0.f,0.f,0.f);
c[ImGuiCol_FrameBg] = kSurface0;
c[ImGuiCol_FrameBgHovered] = kSurface1;
c[ImGuiCol_FrameBgActive] = kSurface1;
c[ImGuiCol_TitleBg] = kCrust;
c[ImGuiCol_TitleBgActive] = kMantle;
c[ImGuiCol_TitleBgCollapsed] = kCrust;
c[ImGuiCol_MenuBarBg] = kCrust;
c[ImGuiCol_ScrollbarBg] = kCrust;
c[ImGuiCol_ScrollbarGrab] = kSurface1;
c[ImGuiCol_ScrollbarGrabHovered] = kSurface2;
c[ImGuiCol_ScrollbarGrabActive] = kBlue;
c[ImGuiCol_CheckMark] = kBlue;
c[ImGuiCol_SliderGrab] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.8f);
c[ImGuiCol_SliderGrabActive] = kBlue;
c[ImGuiCol_Button] = kSurface0;
c[ImGuiCol_ButtonHovered] = kSurface1;
c[ImGuiCol_ButtonActive] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.35f);
c[ImGuiCol_Header] = kSurface0;
c[ImGuiCol_HeaderHovered] = kSurface1;
c[ImGuiCol_HeaderActive] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.4f);
c[ImGuiCol_Separator] = kSurface0;
c[ImGuiCol_SeparatorHovered] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.6f);
c[ImGuiCol_SeparatorActive] = kBlue;
c[ImGuiCol_ResizeGrip] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.2f);
c[ImGuiCol_ResizeGripHovered] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.6f);
c[ImGuiCol_ResizeGripActive] = kBlue;
c[ImGuiCol_Tab] = kSurface0;
c[ImGuiCol_TabHovered] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.3f);
c[ImGuiCol_TabActive] = kSurface1;
c[ImGuiCol_TabUnfocused] = kMantle;
c[ImGuiCol_TabUnfocusedActive]= kSurface0;
c[ImGuiCol_TableHeaderBg] = kSurface0;
c[ImGuiCol_TableBorderStrong] = kSurface1;
c[ImGuiCol_TableBorderLight] = kSurface0;
c[ImGuiCol_TableRowBg] = ImVec4(0.f,0.f,0.f,0.f);
c[ImGuiCol_TableRowBgAlt] = ImVec4(1.f,1.f,1.f,0.025f);
c[ImGuiCol_Text] = kText;
c[ImGuiCol_TextDisabled] = kOverlay0;
c[ImGuiCol_TextSelectedBg] = ImVec4(kBlue.x,kBlue.y,kBlue.z,0.35f);
c[ImGuiCol_DragDropTarget] = kPeach;
c[ImGuiCol_NavHighlight] = kLavender;
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.f,0.f,0.f,0.55f);
(void)kMauve;
// ImPlot style
ImPlotStyle& ps = ImPlot::GetStyle();
ps.PlotPadding = ImVec2(8.f,8.f);
ps.LabelPadding = ImVec2(5.f,3.f);
ps.LegendPadding = ImVec2(8.f,8.f);
ps.MinorAlpha = 0.15f;
ps.Colors[ImPlotCol_PlotBg] = kCrust;
ps.Colors[ImPlotCol_PlotBorder]= kSurface0;
ps.Colors[ImPlotCol_AxisGrid] = ImVec4(kSurface0.x,kSurface0.y,kSurface0.z,0.6f);
ps.Colors[ImPlotCol_AxisTick] = kSurface1;
ps.Colors[ImPlotCol_AxisText] = ImVec4(0.651f,0.678f,0.784f,1.f);
ps.Colors[ImPlotCol_LegendBg] = ImVec4(kMantle.x,kMantle.y,kMantle.z,0.9f);
ps.Colors[ImPlotCol_LegendBorder] = kSurface0;
ps.Colors[ImPlotCol_Crosshairs] = ImVec4(kText.x,kText.y,kText.z,0.5f);
ImGui_ImplSDL2_InitForOpenGL(window, glCtx);
ImGui_ImplOpenGL3_Init("#version 330 core");
/* ── Application ─────────────────────────────────────────────────── */
StreamHubClient::App app;
app.init(host, port);
bool quit = false;
while (!quit) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT) { quit = true; }
if (event.type == SDL_KEYDOWN &&
event.key.keysym.sym == SDLK_q &&
(event.key.keysym.mod & KMOD_ALT)) {
quit = true;
}
}
/* ── New frame ───────────────────────────────────────────────── */
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
app.update();
/* ── Render ──────────────────────────────────────────────────── */
ImGui::Render();
glViewport(0, 0, static_cast<int>(io.DisplaySize.x),
static_cast<int>(io.DisplaySize.y));
glClearColor(0.118f, 0.118f, 0.180f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(window);
}
/* ── Cleanup ─────────────────────────────────────────────────────── */
app.shutdown();
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImPlot::DestroyContext();
ImGui::DestroyContext();
SDL_GL_DeleteContext(glCtx);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}