diff --git a/src/main.cpp b/src/main.cpp index ce2c9ac..49a7544 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include // ftxui::{Render} #include // ftxui::Color #include // ftxui::{Screen, Dimension::Full} +#include // ftxui::string_width #include // std::println #include // std::ranges::{iota, to, transform} @@ -18,8 +19,6 @@ namespace ui { using ftxui::Color; using util::types::StringView, util::types::i32; - static constexpr i32 MAX_PARAGRAPH_LENGTH = 30; - // Color themes struct Theme { Color::Palette16 icon; @@ -110,26 +109,29 @@ namespace { using namespace util::logging; using namespace ftxui; - // Helper struct to hold row data before calculating max width struct RowInfo { StringView icon; StringView label; - String value; // Store the final formatted value as String + String value; }; fn CreateColorCircles() -> Element { return hbox( - std::views::iota(0, 16) | std::views::transform([](ui::i32 colorIndex) { + std::views::iota(0, 16) | std::views::transform([](i32 colorIndex) { return hbox({ text("◯") | bold | color(static_cast(colorIndex)), text(" ") }); }) | std::ranges::to() ); } + fn get_visual_width(const String& str) -> usize { return ftxui::string_width(str); } + fn get_visual_width_sv(const StringView& sview) -> usize { return ftxui::string_width(String(sview)); } + fn find_max_label_len(const std::vector& rows) -> usize { - usize max_len = 0; - for (const auto& row : rows) { max_len = std::max(max_len, row.label.length()); } - return max_len; + usize maxWidth = 0; + for (const RowInfo& row : rows) maxWidth = std::max(maxWidth, get_visual_width_sv(row.label)); + + return maxWidth; }; fn SystemInfoBox(const Config& config, const os::SystemData& data) -> Element { @@ -139,135 +141,153 @@ namespace { const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, packageIcon, deIcon, wmIcon] = ui::ICON_TYPE; - // --- Stage 1: Collect data for rows into logical sections --- - std::vector initial_rows; // Date, Weather - std::vector system_info_rows; // Host, Kernel, OS, RAM, Disk, Shell, Packages - std::vector env_info_rows; // DE, WM + std::vector initialRows; // Date, Weather + std::vector systemInfoRows; // Host, Kernel, OS, RAM, Disk, Shell, Packages + std::vector envInfoRows; // DE, WM - // --- Section 1: Date and Weather --- - if (data.date) { - initial_rows.push_back({ calendarIcon, "Date", *data.date }); - } else { + if (data.date) + initialRows.push_back({ calendarIcon, "Date", *data.date }); + else debug_at(data.date.error()); - } + if (weather.enabled && data.weather) { const weather::Output& weatherInfo = *data.weather; String weatherValue = weather.showTownName ? std::format("{}°F in {}", std::lround(weatherInfo.main.temp), weatherInfo.name) : std::format("{}°F, {}", std::lround(weatherInfo.main.temp), weatherInfo.weather[0].description); - initial_rows.push_back({ weatherIcon, "Weather", std::move(weatherValue) }); - } else if (weather.enabled) { + initialRows.push_back({ weatherIcon, "Weather", std::move(weatherValue) }); + } else if (weather.enabled) debug_at(data.weather.error()); - } - // --- Section 2: Core System Info --- - if (data.host && !data.host->empty()) { - system_info_rows.push_back({ hostIcon, "Host", *data.host }); - } else { + if (data.host && !data.host->empty()) + systemInfoRows.push_back({ hostIcon, "Host", *data.host }); + else debug_at(data.host.error()); - } - if (data.kernelVersion) { - system_info_rows.push_back({ kernelIcon, "Kernel", *data.kernelVersion }); - } else { + + if (data.kernelVersion) + systemInfoRows.push_back({ kernelIcon, "Kernel", *data.kernelVersion }); + else debug_at(data.kernelVersion.error()); - } - if (data.osVersion) { - system_info_rows.push_back({ osIcon, "OS", *data.osVersion }); - } else { + + if (data.osVersion) + systemInfoRows.push_back({ osIcon, "OS", *data.osVersion }); + else debug_at(data.osVersion.error()); - } - if (data.memInfo) { - system_info_rows.push_back({ memoryIcon, "RAM", std::format("{}", BytesToGiB { *data.memInfo }) }); - } else { + + if (data.memInfo) + systemInfoRows.push_back({ memoryIcon, "RAM", std::format("{}", BytesToGiB { *data.memInfo }) }); + else debug_at(data.memInfo.error()); - } - if (data.diskUsage) { - system_info_rows.push_back( + + if (data.diskUsage) + systemInfoRows.push_back( { diskIcon, "Disk", std::format("{}/{}", BytesToGiB { data.diskUsage->used_bytes }, BytesToGiB { data.diskUsage->total_bytes }) } ); - } else { + else debug_at(data.diskUsage.error()); - } - if (data.shell) { - system_info_rows.push_back({ shellIcon, "Shell", *data.shell }); - } else { + + if (data.shell) + systemInfoRows.push_back({ shellIcon, "Shell", *data.shell }); + else debug_at(data.shell.error()); - } + if (data.packageCount) { - if (*data.packageCount > 0) { - system_info_rows.push_back({ packageIcon, "Packages", std::format("{}", *data.packageCount) }); - } else { + if (*data.packageCount > 0) + systemInfoRows.push_back({ packageIcon, "Packages", std::format("{}", *data.packageCount) }); + else debug_log("Package count is 0, skipping"); - } - } else { + } else debug_at(data.packageCount.error()); - } - // --- Section 3: Desktop Env / Window Manager --- - bool added_de = false; + bool addedDe = false; if (data.desktopEnv && (!data.windowMgr || *data.desktopEnv != *data.windowMgr)) { - env_info_rows.push_back({ deIcon, "DE", *data.desktopEnv }); - added_de = true; - } else if (!data.desktopEnv) { /* Optional debug */ - } - if (data.windowMgr) { - if (!added_de || (data.desktopEnv && *data.desktopEnv != *data.windowMgr)) { - env_info_rows.push_back({ wmIcon, "WM", *data.windowMgr }); - } - } else { - debug_at(data.windowMgr.error()); - } + envInfoRows.push_back({ deIcon, "DE", *data.desktopEnv }); + addedDe = true; + } else if (!data.desktopEnv) + debug_at(data.desktopEnv.error()); + + if (data.windowMgr) { + if (!addedDe || (data.desktopEnv && *data.desktopEnv != *data.windowMgr)) + envInfoRows.push_back({ wmIcon, "WM", *data.windowMgr }); + } else + debug_at(data.windowMgr.error()); + + bool nowPlayingActive = false; + String npText; - // --- Section 4: Now Playing (Handled separately) --- - bool now_playing_active = false; - String np_text; if (config.nowPlaying.enabled && data.nowPlaying) { const String title = data.nowPlaying->title.value_or("Unknown Title"); const String artist = data.nowPlaying->artist.value_or("Unknown Artist"); - np_text = artist + " - " + title; - now_playing_active = true; - } else if (config.nowPlaying.enabled) { /* Optional debug */ - } + npText = artist + " - " + title; + nowPlayingActive = true; + } else if (config.nowPlaying.enabled) + debug_at(data.nowPlaying.error()); - // --- Stage 2: Calculate max width needed for Icon + Label across relevant sections --- - usize maxActualLabelLen = 0; - auto find_max_label = [&](const std::vector& rows) { - usize max_len = 0; - for (const auto& row : rows) { max_len = std::max(max_len, row.label.length()); } - return max_len; + usize maxContentWidth = 0; + + usize greetingWidth = get_visual_width_sv(userIcon) + get_visual_width_sv("Hello ") + get_visual_width(name) + + get_visual_width_sv("! "); + maxContentWidth = std::max(maxContentWidth, greetingWidth); + + usize paletteWidth = get_visual_width_sv(userIcon) + (16 * (get_visual_width_sv("◯") + get_visual_width_sv(" "))); + maxContentWidth = std::max(maxContentWidth, paletteWidth); + + usize iconActualWidth = get_visual_width_sv(userIcon); + + usize maxLabelWidthInitial = find_max_label_len(initialRows); + usize maxLabelWidthSystem = find_max_label_len(systemInfoRows); + usize maxLabelWidthEnv = find_max_label_len(envInfoRows); + + usize requiredWidthInitialW = iconActualWidth + maxLabelWidthInitial; + usize requiredWidthSystemW = iconActualWidth + maxLabelWidthSystem; + usize requiredWidthEnvW = iconActualWidth + maxLabelWidthEnv; + + fn calculateRowVisualWidth = [&](const RowInfo& row, usize requiredLabelVisualWidth) -> usize { + return requiredLabelVisualWidth + get_visual_width(row.value) + get_visual_width_sv(" "); // Use visual width }; - maxActualLabelLen = - std::max({ find_max_label(initial_rows), find_max_label(system_info_rows), find_max_label(env_info_rows) }); - // Note: We don't include "Playing" from Now Playing in this calculation - // as it's handled differently, but we could if we wanted perfect alignment. + for (const RowInfo& row : initialRows) + maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthInitialW)); - // --- Stage 2: Calculate max width needed PER SECTION --- - // Assume consistent icon width for simplicity (adjust if icons vary significantly) - usize iconLen = ui::ICON_TYPE.user.length() - 1; - // Optionally refine iconLen based on actual icons used, if needed + for (const RowInfo& row : systemInfoRows) + maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthSystemW)); - usize maxLabelLen_initial = find_max_label_len(initial_rows); - usize maxLabelLen_system = find_max_label_len(system_info_rows); - usize maxLabelLen_env = find_max_label_len(env_info_rows); + for (const RowInfo& row : envInfoRows) + maxContentWidth = std::max(maxContentWidth, calculateRowVisualWidth(row, requiredWidthEnvW)); - usize requiredWidth_initial = iconLen + maxLabelLen_initial; - usize requiredWidth_system = iconLen + maxLabelLen_system; - usize requiredWidth_env = iconLen + maxLabelLen_env; + usize targetBoxWidth = maxContentWidth + 2; - // --- Stage 3: Define the row creation function --- - auto createStandardRow = [&](const RowInfo& row, usize sectionRequiredWidth) { - Element leftPart = hbox( - { - text(String(row.icon)) | color(ui::DEFAULT_THEME.icon), - text(String(row.label)) | color(ui::DEFAULT_THEME.label), - } - ); + usize npFixedWidthLeft = 0; + usize npFixedWidthRight = 0; + + if (nowPlayingActive) { + npFixedWidthLeft = get_visual_width_sv(musicIcon) + get_visual_width_sv("Playing") + get_visual_width_sv(" "); + npFixedWidthRight = get_visual_width_sv(" "); + } + + i32 paragraphLimit = 1; + + if (nowPlayingActive) { + i32 availableForParagraph = + static_cast(targetBoxWidth) - static_cast(npFixedWidthLeft) - static_cast(npFixedWidthRight); + + availableForParagraph -= 2; + + paragraphLimit = std::max(1, availableForParagraph); + } + + fn createStandardRow = [&](const RowInfo& row, usize sectionRequiredVisualWidth) { return hbox( { - leftPart | size(WIDTH, EQUAL, static_cast(sectionRequiredWidth)), + hbox( + { + text(String(row.icon)) | color(ui::DEFAULT_THEME.icon), + text(String(row.label)) | color(ui::DEFAULT_THEME.label), + } + ) | + size(WIDTH, EQUAL, static_cast(sectionRequiredVisualWidth)), filler(), text(row.value) | color(ui::DEFAULT_THEME.value), text(" "), @@ -275,62 +295,42 @@ namespace { ); }; - // --- Stage 4: Build the final Elements list with explicit separators and section-specific widths --- Elements content; - // Greeting and Palette content.push_back(text(String(userIcon) + "Hello " + name + "! ") | bold | color(Color::Cyan)); - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); // Separator after greeting + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); content.push_back(hbox({ text(String(paletteIcon)) | color(ui::DEFAULT_THEME.icon), CreateColorCircles() })); - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); // Separator after palette - // Determine section presence - bool section1_present = !initial_rows.empty(); - bool section2_present = !system_info_rows.empty(); - bool section3_present = !env_info_rows.empty(); - bool section4_present = now_playing_active; + bool section1Present = !initialRows.empty(); + bool section2Present = !systemInfoRows.empty(); + bool section3Present = !envInfoRows.empty(); - // Add Section 1 (Date/Weather) - Use initial width - for (const auto& row : initial_rows) { content.push_back(createStandardRow(row, requiredWidth_initial)); } - - // Separator before Section 2? - if (section1_present && (section2_present || section3_present || section4_present)) { + if (section1Present) content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - } - // Add Section 2 (System Info) - Use system width - for (const auto& row : system_info_rows) { content.push_back(createStandardRow(row, requiredWidth_system)); } + for (const RowInfo& row : initialRows) content.push_back(createStandardRow(row, requiredWidthInitialW)); - // Separator before Section 3? - if (section2_present && (section3_present || section4_present)) { + if ((section1Present && (section2Present || section3Present)) || (!section1Present && section2Present)) content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - } - // Add Section 3 (DE/WM) - Use env width - for (const auto& row : env_info_rows) { content.push_back(createStandardRow(row, requiredWidth_env)); } + for (const RowInfo& row : systemInfoRows) content.push_back(createStandardRow(row, requiredWidthSystemW)); - // Separator before Section 4? - if (section3_present && section4_present) { + if (section2Present && section3Present) content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - } else if (!section3_present && (section1_present || section2_present) && section4_present) { - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - } - // Add Section 4 (Now Playing) - if (section4_present) { - // Pad "Playing" label based on the max label length of the preceding section (Env) - usize playingLabelPadding = maxLabelLen_env; + for (const RowInfo& row : envInfoRows) content.push_back(createStandardRow(row, requiredWidthEnvW)); + + if ((section1Present || section2Present || section3Present) && nowPlayingActive) + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + + if (nowPlayingActive) { content.push_back(hbox( - { - text(String(musicIcon)) | color(ui::DEFAULT_THEME.icon), - // Pad only the label part - hbox({ text("Playing") | color(ui::DEFAULT_THEME.label) }) | - size(WIDTH, EQUAL, static_cast(playingLabelPadding)), - text(" "), // Space after label - filler(), - paragraph(np_text) | color(Color::Magenta) | size(WIDTH, LESS_THAN, ui::MAX_PARAGRAPH_LENGTH), + { text(String(musicIcon)) | color(ui::DEFAULT_THEME.icon), + text("Playing") | color(ui::DEFAULT_THEME.label), text(" "), - } + filler(), + paragraphAlignRight(npText) | color(Color::Magenta) | size(WIDTH, LESS_THAN, paragraphLimit), + text(" ") } )); }