/** * @file main.cpp * @brief StreamHub ImGui desktop client entry point. * * Usage: StreamHubClient [-host ] [-port ] * * 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 #include #include #include #include #include #include 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(atoi(argv[++i])); } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { printf("Usage: StreamHubClient [-host ] [-port ]\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_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(io.DisplaySize.x), static_cast(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; }