/* * battery.cpp * Panel battery indicator: the icon that only appears on a machine with a * battery, and the popout that puts a number on it. * * Copyright (c) 2026 Daniel Hammer */ #include "desktop_internal.hpp" #include using namespace gui; // ============================================================================ // Geometry // ============================================================================ static constexpr int BATT_POPUP_W = 210; static constexpr int BATT_POLL_MS = 5000; // the kernel caches for 2s anyway static int battery_popup_x(const DesktopState* ds) { int x = ds->battery_icon_rect.x + ds->battery_icon_rect.w - BATT_POPUP_W; return x < 4 ? 4 : x; } // Rows below the headline: state, then whatever of capacity/time we can say. static int battery_detail_rows(const DesktopState* ds) { const montauk::abi::BatteryInfo& b = ds->battery_info; if (!b.present) return 0; int rows = 2; // "Charge", then "State" if (b.minutesRemaining >= 0) rows++; // "Time left" if (b.fullCapacity > 0) rows++; // "Capacity" return rows; } static int battery_popup_h(const DesktopState* ds) { int fh = system_font_height(); int header = fh + 10; // title row int rows = battery_detail_rows(ds) * (fh + 6); return 10 + header + rows + 12; } static Rect battery_popup_rect(const DesktopState* ds) { return {battery_popup_x(ds), PANEL_HEIGHT + 2, BATT_POPUP_W, battery_popup_h(ds)}; } // ============================================================================ // Icon selection // ============================================================================ // The icon pack ships one glyph per 10% step. Round to the nearest step so the // drawn level matches the number in the popout rather than always sitting a // bucket low. static int battery_bucket(int percent) { if (percent < 0) percent = 0; if (percent > 100) percent = 100; int bucket = ((percent + 5) / 10) * 10; return bucket > 100 ? 100 : bucket; } static void battery_icon_path(const DesktopState* ds, char* out, int cap) { const montauk::abi::BatteryInfo& b = ds->battery_info; if (!b.present) { montauk::strncpy(out, "0:/os/icons/battery-missing-symbolic.svg", cap); return; } int bucket = battery_bucket(b.percent); // A full battery on the charger gets the "charged" glyph; the pack has no // 100% charging variant, and "charged" is what that state means anyway. if (b.charging && bucket >= 100) { montauk::strncpy(out, "0:/os/icons/battery-level-100-charged-symbolic.svg", cap); return; } snprintf(out, cap, "0:/os/icons/battery-level-%d%s-symbolic.svg", bucket, b.charging ? "-charging" : ""); } // Reload the glyph only when the one we want changes: at a 5 second poll a // laptop crosses a bucket boundary every few minutes at most, and each load // rasterises an SVG. static bool battery_refresh_icon(DesktopState* ds) { char path[64]; battery_icon_path(ds, path, sizeof(path)); if (ds->icon_battery.pixels && montauk::streq(path, ds->battery_icon_name)) return false; SvgIcon loaded = svg_load(path, 16, 16, colors::PANEL_TEXT); if (!loaded.pixels) return false; svg_free(ds->icon_battery); ds->icon_battery = loaded; montauk::strncpy(ds->battery_icon_name, path, sizeof(ds->battery_icon_name)); return true; } // ============================================================================ // Startup and polling // ============================================================================ void desktop_battery_init(DesktopState* ds) { ds->icon_battery = {nullptr, 0, 0}; ds->battery_icon_name[0] = '\0'; ds->battery_popup_open = false; ds->battery_icon_rect = {0, 0, 0, 0}; ds->battery_last_poll = 0; montauk::memset(&ds->battery_info, 0, sizeof(ds->battery_info)); // A machine with no ACPI battery device fails the call outright, and the // indicator never appears. This does not change while we are running, so // unlike Wi-Fi there is nothing to keep looking for. ds->battery_present = montauk::battery_info(&ds->battery_info) == 0; if (ds->battery_present) battery_refresh_icon(ds); } bool desktop_battery_poll(DesktopState* ds, uint64_t now) { if (!ds->battery_present) return false; if (ds->battery_last_poll != 0 && now - ds->battery_last_poll < BATT_POLL_MS) return false; ds->battery_last_poll = now; montauk::abi::BatteryInfo fresh; if (montauk::battery_info(&fresh) != 0) return false; bool changed = fresh.percent != ds->battery_info.percent || fresh.present != ds->battery_info.present || fresh.charging != ds->battery_info.charging || fresh.acOnline != ds->battery_info.acOnline || fresh.minutesRemaining != ds->battery_info.minutesRemaining; ds->battery_info = fresh; if (battery_refresh_icon(ds)) changed = true; // While the popout is up its numbers are on screen, so any change at all // has to be repainted. return changed || ds->battery_popup_open; } // ============================================================================ // Popout // ============================================================================ void desktop_draw_battery_popup(DesktopState* ds) { Framebuffer& fb = ds->fb; const montauk::abi::BatteryInfo& b = ds->battery_info; Rect popup = battery_popup_rect(ds); int fh = system_font_height(); Color dim = Color::from_rgb(0x66, 0x66, 0x66); draw_shadow(fb, popup.x, popup.y, popup.w, popup.h, 4, colors::SHADOW); fill_rounded_rect(fb, popup.x, popup.y, popup.w, popup.h, 8, colors::MENU_BG); draw_rect(fb, popup.x, popup.y, popup.w, popup.h, colors::BORDER); int lx = popup.x + 14; int vx = popup.x + 92; int ty = popup.y + 10; draw_text(fb, lx, ty, "Battery", colors::TEXT_COLOR); // A coloured dot mirrors the Wi-Fi popup: green on the charger, amber when // the charge is getting short, red once the firmware calls it critical. const char* status_str; Color dot_color; if (!b.present) { status_str = "Not present"; dot_color = Color::from_rgb(0x88, 0x88, 0x88); } else if (b.charging) { status_str = "Charging"; dot_color = Color::from_rgb(0x4C, 0xAF, 0x50); } else if (b.critical || b.percent <= 10) { status_str = "Low"; dot_color = Color::from_rgb(0xCC, 0x33, 0x33); } else if (b.acOnline) { status_str = "Plugged in"; dot_color = Color::from_rgb(0x4C, 0xAF, 0x50); } else { status_str = "On battery"; dot_color = (b.percent <= 25) ? Color::from_rgb(0xD0, 0x9A, 0x2E) : Color::from_rgb(0x66, 0x66, 0x66); } int sw = text_width(status_str); int status_x = popup.x + popup.w - 14 - sw; fill_circle(fb, status_x - 9, ty + fh / 2, 4, dot_color); draw_text(fb, status_x, ty, status_str, dim); ty += fh + 10; if (!b.present) return; char value[40]; snprintf(value, sizeof(value), "%d%%", (int)b.percent); draw_text(fb, lx, ty, "Charge", dim); draw_text(fb, vx, ty, value, colors::TEXT_COLOR); ty += fh + 6; snprintf(value, sizeof(value), "%s", b.charging ? "Charging" : (b.acOnline ? "Not charging" : "Discharging")); draw_text(fb, lx, ty, "State", dim); draw_text(fb, vx, ty, value, colors::TEXT_COLOR); ty += fh + 6; if (b.minutesRemaining >= 0) { int mins = b.minutesRemaining; if (mins >= 60) snprintf(value, sizeof(value), "%dh %02dm", mins / 60, mins % 60); else snprintf(value, sizeof(value), "%dm", mins); draw_text(fb, lx, ty, "Time left", dim); draw_text(fb, vx, ty, value, colors::TEXT_COLOR); ty += fh + 6; } if (b.fullCapacity > 0) { const char* unit = b.powerUnitMilliAmps ? "mAh" : "mWh"; snprintf(value, sizeof(value), "%u/%u %s", (unsigned)b.remainingCapacity, (unsigned)b.fullCapacity, unit); draw_text(fb, lx, ty, "Capacity", dim); draw_text(fb, vx, ty, value, colors::TEXT_COLOR); } } // A click anywhere outside the popout closes it; the popout itself has nothing // to click, so it only has to swallow the event. bool desktop_battery_handle_mouse(DesktopState* ds, int mx, int my) { if (!ds->battery_popup_open) return false; if (battery_popup_rect(ds).contains(mx, my)) return true; if (!ds->battery_icon_rect.contains(mx, my)) ds->battery_popup_open = false; return false; }