feat: add Power GUI app (powermgr)

This commit is contained in:
2026-07-05 11:34:27 +02:00
parent 35e5aba23f
commit fcf8ae1e0b
7 changed files with 505 additions and 5 deletions
+77
View File
@@ -0,0 +1,77 @@
# Makefile for powermgr (standalone Power Manager) on MontaukOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
else
CXX := g++
endif
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unused-function \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-MMD -MP \
-I $(PROG_INC) \
-isystem $(PROG_INC)/libc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
SRCS := main.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
TARGET := $(BINDIR)/apps/powermgr/powermgr.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/apps/powermgr
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
-include $(OBJS:.o=.d)
clean:
rm -rf $(OBJDIR) $(TARGET)
+373
View File
@@ -0,0 +1,373 @@
/*
* main.cpp
* MontaukOS Power Manager - GUI equivalent of the `power` command.
*
* Polls the kernel's CPU power/thermal snapshot (SYS_POWERINFO) once per
* second and plots package temperature, average active frequency, and the
* thermal governor's frequency ceiling as scrolling line graphs, plus a
* details panel with the HWP/EPP/idle configuration.
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <gui/mtk.hpp>
extern "C" {
#include <stdio.h>
}
using namespace gui;
static constexpr int INIT_W = 640;
static constexpr int INIT_H = 520;
static constexpr int PW_TOOLBAR_H = 36;
static constexpr int PW_POLL_MS = 1000;
static constexpr int PW_GRAPH_CAP = 120;
static constexpr Color PW_TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
static constexpr Color PW_HEADER_BG = Color::from_rgb(0xF0, 0xF0, 0xF0);
static constexpr Color PW_BORDER = Color::from_rgb(0xD0, 0xD0, 0xD0);
static constexpr Color PW_DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66);
static constexpr Color PW_SUCCESS = Color::from_rgb(0x22, 0x88, 0x22);
static constexpr Color PW_WARNING = Color::from_rgb(0xCC, 0x88, 0x00);
static constexpr Color PW_DANGER = Color::from_rgb(0xCC, 0x33, 0x33);
static constexpr Color PW_INFO = Color::from_rgb(0x33, 0x66, 0xCC);
static constexpr Color PW_CEILING = Color::from_rgb(0x00, 0x88, 0x88);
struct PowerMgrState {
montauk::abi::PowerInfo info;
bool supported;
int mouse_x;
int mouse_y;
uint64_t last_poll_ms;
uint64_t temp_samples[PW_GRAPH_CAP];
uint64_t freq_samples[PW_GRAPH_CAP];
uint64_t ceiling_samples[PW_GRAPH_CAP];
mtk::GraphHistory temp_history;
mtk::GraphHistory freq_history;
mtk::GraphHistory ceiling_history;
};
static WsWindow g_win;
static PowerMgrState g_pw = {};
static mtk::Theme pw_theme() {
mtk::Theme theme = mtk::make_theme();
theme.surface = PW_TOOLBAR_BG;
theme.surface_alt = PW_HEADER_BG;
theme.border = PW_BORDER;
theme.text_muted = PW_DIM_TEXT;
theme.text_subtle = PW_DIM_TEXT;
return theme;
}
static void init_graph_histories() {
mtk::graph_history_init(&g_pw.temp_history, g_pw.temp_samples, PW_GRAPH_CAP);
mtk::graph_history_init(&g_pw.freq_history, g_pw.freq_samples, PW_GRAPH_CAP);
mtk::graph_history_init(&g_pw.ceiling_history, g_pw.ceiling_samples, PW_GRAPH_CAP);
}
static bool read_info() {
return montauk::syscall1(montauk::abi::SYS_POWERINFO, (uint64_t)&g_pw.info) == 0;
}
static bool refresh_state(bool force) {
if (!g_pw.supported)
return false;
uint64_t now = montauk::get_milliseconds();
if (!force && now - g_pw.last_poll_ms < PW_POLL_MS)
return false;
g_pw.last_poll_ms = now;
if (!read_info())
return false;
if (g_pw.info.tempC != 0)
mtk::graph_history_push(&g_pw.temp_history, g_pw.info.tempC);
mtk::graph_history_push(&g_pw.freq_history, g_pw.info.effMHz);
mtk::graph_history_push(&g_pw.ceiling_history, g_pw.info.curMaxPerf);
return true;
}
static Rect toolbar_rect() {
return {0, 0, g_win.width, PW_TOOLBAR_H};
}
static Rect refresh_button_rect() {
int btn_h = 26;
int btn_y = (PW_TOOLBAR_H - btn_h) / 2;
return {8, btn_y, 82, btn_h};
}
static bool mouse_in_rect(const Rect& rect) {
return rect.contains(g_pw.mouse_x, g_pw.mouse_y);
}
static void render_toolbar(Canvas& c, const mtk::Theme& theme, int fh) {
Rect bar = toolbar_rect();
c.fill_rect(bar.x, bar.y, bar.w, bar.h, theme.surface);
c.hline(bar.x, bar.y + bar.h - 1, bar.w, theme.border);
Rect refresh_rect = refresh_button_rect();
mtk::draw_button(c, refresh_rect, "Refresh", mtk::BUTTON_TONAL,
mtk::widget_state(false, mouse_in_rect(refresh_rect),
g_pw.supported), theme);
char status[96];
if (!g_pw.supported) {
snprintf(status, sizeof(status), "No power management");
} else if (g_pw.info.throttling) {
snprintf(status, sizeof(status), "%u C, avg %u MHz - THROTTLING",
(unsigned)g_pw.info.tempC, (unsigned)g_pw.info.effMHz);
} else if (g_pw.info.tempC != 0) {
snprintf(status, sizeof(status), "%u C, avg %u MHz",
(unsigned)g_pw.info.tempC, (unsigned)g_pw.info.effMHz);
} else {
snprintf(status, sizeof(status), "avg %u MHz", (unsigned)g_pw.info.effMHz);
}
int status_w = text_width(status);
Color status_color = (g_pw.supported && g_pw.info.throttling)
? PW_DANGER : theme.text;
c.text(g_win.width - status_w - 12, (PW_TOOLBAR_H - fh) / 2, status, status_color);
}
static void render_graph(Canvas& c,
const Rect& rect,
const mtk::GraphHistory& history,
uint64_t max_value,
const char* title,
const char* value,
Color line,
const mtk::Theme& theme) {
mtk::GraphStyle style = mtk::make_graph_style(theme, line);
style.background = theme.window_bg;
style.plot_bg = Color::from_rgb(0xFA, 0xFA, 0xFA);
style.grid = mtk::mix(style.plot_bg, line, 16);
style.fill = mtk::lighten(line, 225);
mtk::draw_line_graph(c, rect, history, max_value, title, value, theme, style);
}
static constexpr int PW_DETAIL_ROWS = 4;
static int details_row_h(int fh) {
return fh + 10;
}
static int details_panel_h(int fh) {
// Mirrors the graph cards: padding, title line, 7px gap, then rows.
return 8 * 2 + fh + 7 + PW_DETAIL_ROWS * details_row_h(fh);
}
static void render_details(Canvas& c, const Rect& rect,
const mtk::Theme& theme, int fh) {
mtk::draw_rounded_frame(c, rect, theme.radius_md, theme.window_bg, theme.border);
const montauk::abi::PowerInfo& info = g_pw.info;
int pad = 8;
int x = rect.x + pad;
int y = rect.y + pad;
int w = rect.w - pad * 2;
int row_h = details_row_h(fh);
char buf[96];
c.text(x, y, "Details", theme.text_subtle);
y += fh + 7;
struct Row {
const char* label;
const char* value;
Color color;
} rows[PW_DETAIL_ROWS];
char hwp[64];
if (info.hwpActive) {
snprintf(hwp, sizeof(hwp), "active (EPP 0x%02X, perf range %u-%u)",
(unsigned)info.epp, (unsigned)info.lowestPerf,
(unsigned)info.highestPerf);
} else {
snprintf(hwp, sizeof(hwp), "not active");
}
rows[0] = {"HWP", hwp, theme.text};
snprintf(buf, sizeof(buf), "base %u MHz, turbo %u MHz",
(unsigned)info.baseMHz, (unsigned)info.maxMHz);
rows[1] = {"Frequency", buf, theme.text};
char gov[64];
Color gov_color = theme.text;
if (!info.hwpActive) {
snprintf(gov, sizeof(gov), "inactive");
} else if (info.throttling) {
snprintf(gov, sizeof(gov), "THROTTLING - ceiling %u of %u",
(unsigned)info.curMaxPerf, (unsigned)info.highestPerf);
gov_color = PW_DANGER;
} else {
snprintf(gov, sizeof(gov), "not throttling (ceiling %u)",
(unsigned)info.curMaxPerf);
gov_color = PW_SUCCESS;
}
rows[2] = {"Governor", gov, gov_color};
char idle[48];
snprintf(idle, sizeof(idle), "MWAIT hint 0x%02X%s",
(unsigned)(info.apIdleHint & 0xFF),
info.apIdleHint == 0 ? " (C1 only)" : "");
rows[3] = {"AP idle", idle, theme.text};
for (int i = 0; i < PW_DETAIL_ROWS; i++) {
c.text(x, y, rows[i].label, theme.text);
int value_w = text_width(rows[i].value);
c.text(x + w - value_w, y, rows[i].value, rows[i].color);
y += row_h;
if (i < PW_DETAIL_ROWS - 1)
mtk::draw_separator(c, x, y - 5, w, theme);
}
}
static void render_unsupported(Canvas& c, const mtk::Theme& theme, int fh) {
const char* line1 = "No CPU power management available";
const char* line2 = "(non-Intel CPU or running under emulation)";
int y = gui_max((g_win.height - fh * 2 - 8) / 2, PW_TOOLBAR_H + 12);
c.text(gui_max((g_win.width - text_width(line1)) / 2, 12), y,
line1, theme.text);
c.text(gui_max((g_win.width - text_width(line2)) / 2, 12), y + fh + 8,
line2, theme.text_muted);
}
static void render_body(Canvas& c, const mtk::Theme& theme, int fh) {
const montauk::abi::PowerInfo& info = g_pw.info;
Rect body = {0, PW_TOOLBAR_H, g_win.width,
gui_max(g_win.height - PW_TOOLBAR_H, 0)};
int margin = 12;
int gap = 10;
int details_h = details_panel_h(fh);
int graph_area_h = gui_max(body.h - margin * 2 - gap * 2 - details_h, 96);
int temp_h = (graph_area_h * 3) / 5;
int row_h = graph_area_h - temp_h - gap;
int col_w = gui_max((body.w - margin * 2 - gap) / 2, 80);
int x0 = body.x + margin;
int y0 = body.y + margin;
int y1 = y0 + temp_h + gap;
int y2 = y1 + row_h + gap;
Rect temp_rect = {x0, y0, body.w - margin * 2, temp_h};
Rect freq_rect = {x0, y1, col_w, row_h};
Rect ceiling_rect = {x0 + col_w + gap, y1, col_w, row_h};
Rect details_rect = {x0, y2, body.w - margin * 2, details_h};
// Scale the plot to TjMax so the top edge is the hardware throttle
// point; the line turns red while the governor is actively limiting.
char temp_value[48];
if (info.tempC != 0) {
snprintf(temp_value, sizeof(temp_value), "%u C (throttle at %u C)",
(unsigned)info.tempC, (unsigned)info.tjMaxC);
} else {
snprintf(temp_value, sizeof(temp_value), "unavailable");
}
render_graph(c, temp_rect, g_pw.temp_history,
info.tjMaxC > 0 ? info.tjMaxC : 100,
"Package Temperature", temp_value,
info.throttling ? PW_DANGER : PW_WARNING, theme);
char freq_value[32];
snprintf(freq_value, sizeof(freq_value), "%u MHz", (unsigned)info.effMHz);
render_graph(c, freq_rect, g_pw.freq_history,
info.maxMHz > 0 ? info.maxMHz : 1,
"Avg Frequency", freq_value, PW_INFO, theme);
char ceiling_value[32];
snprintf(ceiling_value, sizeof(ceiling_value), "%u / %u",
(unsigned)info.curMaxPerf, (unsigned)info.highestPerf);
render_graph(c, ceiling_rect, g_pw.ceiling_history,
info.highestPerf > 0 ? info.highestPerf : 1,
"Frequency Ceiling", ceiling_value, PW_CEILING, theme);
render_details(c, details_rect, theme, fh);
}
static void render() {
mtk::StandaloneHost host(&g_win);
Canvas c = host.canvas();
mtk::Theme theme = pw_theme();
c.fill(theme.window_bg);
int fh = system_font_height();
render_toolbar(c, theme, fh);
if (g_pw.supported)
render_body(c, theme, fh);
else
render_unsupported(c, theme, fh);
}
static bool handle_mouse(const montauk::abi::WinEvent& ev) {
bool refresh_hover_before = mouse_in_rect(refresh_button_rect());
g_pw.mouse_x = ev.mouse.x;
g_pw.mouse_y = ev.mouse.y;
bool redraw = refresh_hover_before != mouse_in_rect(refresh_button_rect());
bool left_pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (!left_pressed)
return redraw;
if (refresh_button_rect().contains(ev.mouse.x, ev.mouse.y))
return refresh_state(true) || redraw;
return redraw;
}
extern "C" void _start() {
if (!fonts::init())
montauk::exit(1);
if (!g_win.create("Power", INIT_W, INIT_H))
montauk::exit(1);
montauk::memset(&g_pw, 0, sizeof(g_pw));
init_graph_histories();
g_pw.mouse_x = -1;
g_pw.mouse_y = -1;
g_pw.supported = read_info();
refresh_state(true);
render();
g_win.present();
while (!g_win.closed) {
bool redraw = refresh_state(false);
montauk::abi::WinEvent ev;
int r = g_win.poll(&ev);
if (r < 0) break;
if (r == 0) {
if (redraw) {
render();
g_win.present();
}
montauk::sleep_ms(16);
continue;
}
if (ev.type == 3)
break;
if (ev.type == 1) redraw |= handle_mouse(ev);
else if (ev.type == 2 || ev.type == 4) redraw = true;
if (redraw) {
render();
g_win.present();
}
}
g_win.destroy();
montauk::exit(0);
}
+11
View File
@@ -0,0 +1,11 @@
[app]
name = "Power"
binary = "powermgr.elf"
icon = "gnome-power-statistics.svg"
[menu]
category = "System"
visible = true
[launch]
pass_home_dir = false
@@ -0,0 +1,33 @@
/*
* stb_truetype_impl.cpp
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/stb_math.h>
#define STBTT_ifloor(x) ((int) stb_floor(x))
#define STBTT_iceil(x) ((int) stb_ceil(x))
#define STBTT_sqrt(x) stb_sqrt(x)
#define STBTT_pow(x,y) stb_pow(x,y)
#define STBTT_fmod(x,y) stb_fmod(x,y)
#define STBTT_cos(x) stb_cos(x)
#define STBTT_acos(x) stb_acos(x)
#define STBTT_fabs(x) stb_fabs(x)
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
#define STBTT_strlen(x) montauk::slen(x)
#define STBTT_assert(x) ((void)(x))
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>