feat: move fonts/icons/man to 0:/os, add capabilities GUI app
This commit is contained in:
1 parent
80b5beb17d
commit
75e85f24ef
35 files changed
+1035
-177
No files matched your search
@@ -0,0 +1,744 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS capability grant viewer
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/capabilities.h>
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int WIN_W = 760;
|
||||
static constexpr int WIN_H = 560;
|
||||
static constexpr int TAB_H = 36;
|
||||
static constexpr int FOOTER_H = 44;
|
||||
static constexpr int PAD = 18;
|
||||
static constexpr int BUTTON_W = 88;
|
||||
static constexpr int LIST_W = 200;
|
||||
static constexpr int LIST_ROW_H = 30;
|
||||
static constexpr int GRID_ROW_H = 22;
|
||||
static constexpr int GRID_COL_W = 86;
|
||||
static constexpr int MAX_GRANTS = 64;
|
||||
static constexpr int MAX_ISSUES = 4;
|
||||
|
||||
static constexpr int CAP_COUNT =
|
||||
(int)(sizeof(montauk::caps::NAMES) / sizeof(montauk::caps::NAMES[0]));
|
||||
|
||||
enum Tab {
|
||||
TAB_PROGRAMS = 0,
|
||||
TAB_CAPS = 1,
|
||||
TAB_COUNT = 2,
|
||||
};
|
||||
|
||||
static const char* const kTabLabels[TAB_COUNT] = {
|
||||
"Programs",
|
||||
"Capabilities",
|
||||
};
|
||||
|
||||
// What each capability gates, in the order of montauk::caps::NAMES.
|
||||
static const char* const kCapDescriptions[CAP_COUNT] = {
|
||||
"Inspect and terminate other processes",
|
||||
"Ask the session to shut down or restart",
|
||||
"Power the machine off or reset it directly",
|
||||
"Put the machine to sleep (S3)",
|
||||
"Format, mount and manage volumes",
|
||||
"Read and write raw block devices",
|
||||
"Configure network interfaces, DHCP and Wi-Fi",
|
||||
"Set the system clock",
|
||||
"Manage user accounts and session identities",
|
||||
"Change display modes and panel brightness",
|
||||
"Manage hardware devices (Bluetooth, radio)",
|
||||
"Read the kernel log",
|
||||
"Write to 0:/os and 0:/apps (never part of \"all\")",
|
||||
};
|
||||
|
||||
// ==== Grant table model ====
|
||||
|
||||
enum Field {
|
||||
FIELD_EFFECTIVE = 0,
|
||||
FIELD_DELEGABLE = 1,
|
||||
FIELD_PERMITTED = 2,
|
||||
FIELD_COUNT = 3,
|
||||
};
|
||||
|
||||
static const char* const kFieldKeys[FIELD_COUNT] = {
|
||||
"effective", "delegable", "permitted",
|
||||
};
|
||||
|
||||
static const char* const kFieldLabels[FIELD_COUNT] = {
|
||||
"Effective", "Delegable", "Permitted",
|
||||
};
|
||||
|
||||
struct Grant {
|
||||
char id[32];
|
||||
char path[96];
|
||||
uint64_t mask[FIELD_COUNT];
|
||||
bool declared[FIELD_COUNT];
|
||||
bool uses_all[FIELD_COUNT];
|
||||
bool binary_present;
|
||||
char issues[MAX_ISSUES][96];
|
||||
int issue_count;
|
||||
};
|
||||
|
||||
static Grant g_grants[MAX_GRANTS];
|
||||
static int g_grant_count = 0;
|
||||
static bool g_file_present = false;
|
||||
static bool g_truncated = false;
|
||||
|
||||
static WsWindow g_win;
|
||||
static Tab g_tab = TAB_PROGRAMS;
|
||||
static int g_mouse_x = -1;
|
||||
static int g_mouse_y = -1;
|
||||
static Color g_accent = colors::ACCENT;
|
||||
static bool g_is_admin = false;
|
||||
static int g_selected_grant = 0;
|
||||
static int g_selected_cap = 0;
|
||||
static int g_list_scroll = 0;
|
||||
static bool g_list_sb_dragging = false;
|
||||
static int g_list_sb_drag_offset = 0;
|
||||
static char g_status[128] = {};
|
||||
static uint64_t g_status_time = 0;
|
||||
|
||||
// Same test as montauk::user::is_admin(), without pulling in its BearSSL
|
||||
// password-hashing dependency.
|
||||
static bool session_is_admin() {
|
||||
char user[64] = {};
|
||||
if (montauk::getuser(user, sizeof(user)) <= 0 || !user[0]) return false;
|
||||
|
||||
char key[96];
|
||||
snprintf(key, sizeof(key), "users.%s.role", user);
|
||||
montauk::toml::Doc doc = montauk::config::load("users");
|
||||
bool admin = montauk::streq(doc.get_string(key, ""), "admin");
|
||||
doc.destroy();
|
||||
return admin;
|
||||
}
|
||||
|
||||
static void add_issue(Grant& g, const char* fmt, const char* a = "", const char* b = "") {
|
||||
if (g.issue_count >= MAX_ISSUES) return;
|
||||
snprintf(g.issues[g.issue_count++], sizeof(g.issues[0]), fmt, a, b);
|
||||
}
|
||||
|
||||
static Grant* find_or_add_grant(const char* id, int idLen) {
|
||||
for (int i = 0; i < g_grant_count; i++) {
|
||||
const char* have = g_grants[i].id;
|
||||
int k = 0;
|
||||
while (k < idLen && have[k] == id[k]) k++;
|
||||
if (k == idLen && have[k] == '\0') return &g_grants[i];
|
||||
}
|
||||
if (g_grant_count >= MAX_GRANTS) {
|
||||
g_truncated = true;
|
||||
return nullptr;
|
||||
}
|
||||
Grant* g = &g_grants[g_grant_count++];
|
||||
montauk::memset(g, 0, sizeof(*g));
|
||||
int n = idLen < (int)sizeof(g->id) - 1 ? idLen : (int)sizeof(g->id) - 1;
|
||||
montauk::memcpy(g->id, id, n);
|
||||
g->id[n] = '\0';
|
||||
return g;
|
||||
}
|
||||
|
||||
static void read_field(Grant& g, int field, montauk::toml::Value* v) {
|
||||
g.declared[field] = true;
|
||||
if (v->type != montauk::toml::Type::Array) {
|
||||
add_issue(g, "'%s' is not a list of capability names", kFieldKeys[field]);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < v->array.count; i++) {
|
||||
montauk::toml::Value* item = v->array.items[i];
|
||||
if (item == nullptr || item->type != montauk::toml::Type::String) continue;
|
||||
uint64_t bit = montauk::caps::bit_for_name(item->str);
|
||||
if (bit == 0) {
|
||||
add_issue(g, "Unknown capability \"%s\" in %s (ignored)",
|
||||
item->str, kFieldKeys[field]);
|
||||
continue;
|
||||
}
|
||||
if (montauk::streq(item->str, "all")) g.uses_all[field] = true;
|
||||
g.mask[field] |= bit;
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors montauk::caps::lookup(), but keeps everything the launcher would
|
||||
// silently discard so it can be shown.
|
||||
static void load_grants() {
|
||||
g_grant_count = 0;
|
||||
g_truncated = false;
|
||||
|
||||
int fh = montauk::open("0:/config/capabilities.toml");
|
||||
g_file_present = fh >= 0;
|
||||
if (fh >= 0) montauk::close(fh);
|
||||
|
||||
montauk::toml::Doc doc = montauk::config::load(montauk::caps::GRANT_CONFIG);
|
||||
int prefixLen = montauk::slen(montauk::caps::GRANT_PREFIX);
|
||||
|
||||
for (int i = 0; i < doc.entries.count; i++) {
|
||||
montauk::toml::Value* entry = doc.entries.items[i];
|
||||
if (entry == nullptr || entry->key == nullptr) continue;
|
||||
if (!montauk::starts_with(entry->key, montauk::caps::GRANT_PREFIX)) continue;
|
||||
|
||||
const char* rest = entry->key + prefixLen;
|
||||
int lastDot = -1;
|
||||
for (int k = 0; rest[k]; k++)
|
||||
if (rest[k] == '.') lastDot = k;
|
||||
if (lastDot <= 0) continue;
|
||||
|
||||
Grant* g = find_or_add_grant(rest, lastDot);
|
||||
if (g == nullptr) continue;
|
||||
const char* field = rest + lastDot + 1;
|
||||
|
||||
if (montauk::streq(field, "path")) {
|
||||
if (entry->type == montauk::toml::Type::String)
|
||||
snprintf(g->path, sizeof(g->path), "%s", entry->str);
|
||||
continue;
|
||||
}
|
||||
bool known = false;
|
||||
for (int f = 0; f < FIELD_COUNT; f++) {
|
||||
if (montauk::streq(field, kFieldKeys[f])) {
|
||||
read_field(*g, f, entry);
|
||||
known = true;
|
||||
}
|
||||
}
|
||||
if (!known) add_issue(*g, "Unknown key '%s' (ignored)", field);
|
||||
}
|
||||
doc.destroy();
|
||||
|
||||
for (int i = 0; i < g_grant_count; i++) {
|
||||
Grant& g = g_grants[i];
|
||||
uint64_t* m = g.mask;
|
||||
|
||||
// Same rule as caps::lookup(): an undeclared permitted set is exactly
|
||||
// what the grant can use or pass on.
|
||||
if (!g.declared[FIELD_PERMITTED] || m[FIELD_PERMITTED] == 0)
|
||||
m[FIELD_PERMITTED] = m[FIELD_EFFECTIVE] | m[FIELD_DELEGABLE];
|
||||
else if ((m[FIELD_EFFECTIVE] | m[FIELD_DELEGABLE]) & ~m[FIELD_PERMITTED])
|
||||
add_issue(g, "Effective/delegable exceed permitted; the excess is dropped at launch");
|
||||
|
||||
if (g.path[0] == '\0') {
|
||||
add_issue(g, "No path; this grant never applies");
|
||||
continue;
|
||||
}
|
||||
int bh = montauk::open(g.path);
|
||||
g.binary_present = bh >= 0;
|
||||
if (bh >= 0) montauk::close(bh);
|
||||
if (!g.binary_present) add_issue(g, "Binary not found on this system");
|
||||
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (montauk::streq(g_grants[j].path, g.path)) {
|
||||
add_issue(g, "Shadowed by [grant.%s]; the first entry for a path wins",
|
||||
g_grants[j].id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (g_selected_grant >= g_grant_count) g_selected_grant = 0;
|
||||
}
|
||||
|
||||
// ==== Layout ====
|
||||
|
||||
static mtk::Theme app_theme() {
|
||||
return mtk::make_theme(g_accent);
|
||||
}
|
||||
|
||||
static Rect tab_bar_rect() {
|
||||
return {0, 0, g_win.width, TAB_H};
|
||||
}
|
||||
|
||||
static Rect footer_rect() {
|
||||
return {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H};
|
||||
}
|
||||
|
||||
static constexpr int LIST_TOP_INSET = 10;
|
||||
|
||||
static int sidebar_w() {
|
||||
return PAD + LIST_W + PAD;
|
||||
}
|
||||
|
||||
// The whole left column, from the tab bar to the footer. The scrollbar runs
|
||||
// its full height against the divider, like a pane scrollbar.
|
||||
static Rect list_rect() {
|
||||
return {0, TAB_H, sidebar_w(), footer_rect().y - TAB_H};
|
||||
}
|
||||
|
||||
static int list_count() {
|
||||
return g_tab == TAB_PROGRAMS ? g_grant_count : CAP_COUNT;
|
||||
}
|
||||
|
||||
static int list_visible_rows() {
|
||||
return (list_rect().h - LIST_TOP_INSET) / LIST_ROW_H;
|
||||
}
|
||||
|
||||
static int list_view_extent() {
|
||||
return list_visible_rows() * LIST_ROW_H;
|
||||
}
|
||||
|
||||
static int list_content_extent() {
|
||||
return list_count() * LIST_ROW_H;
|
||||
}
|
||||
|
||||
static bool list_scrollbar_visible() {
|
||||
return mtk::scrollbar_needed(list_content_extent(), list_view_extent());
|
||||
}
|
||||
|
||||
static Rect list_scrollbar_track() {
|
||||
if (!list_scrollbar_visible()) return {0, 0, 0, 0};
|
||||
return mtk::scrollbar_track_rect(list_rect());
|
||||
}
|
||||
|
||||
static Rect list_scrollbar_thumb() {
|
||||
Rect track = list_scrollbar_track();
|
||||
if (track.empty()) return {0, 0, 0, 0};
|
||||
return mtk::scrollbar_thumb_rect(track, list_content_extent(),
|
||||
list_view_extent(), g_list_scroll * LIST_ROW_H);
|
||||
}
|
||||
|
||||
static Rect list_row_rect(int visibleIndex) {
|
||||
Rect list = list_rect();
|
||||
int right = list.w - (list_scrollbar_visible() ? mtk::SCROLLBAR_W + 6 : PAD);
|
||||
return {PAD, list.y + LIST_TOP_INSET + visibleIndex * LIST_ROW_H,
|
||||
right - PAD, LIST_ROW_H};
|
||||
}
|
||||
|
||||
static int pane_x() {
|
||||
return sidebar_w() + 1 + PAD;
|
||||
}
|
||||
|
||||
static int pane_w() {
|
||||
return g_win.width - pane_x() - PAD;
|
||||
}
|
||||
|
||||
static Rect reload_button() {
|
||||
Rect foot = footer_rect();
|
||||
return {foot.x + foot.w - PAD - BUTTON_W, foot.y + 7, BUTTON_W, 30};
|
||||
}
|
||||
|
||||
static bool mouse_in(const Rect& rect) {
|
||||
return rect.contains(g_mouse_x, g_mouse_y);
|
||||
}
|
||||
|
||||
static void clamp_list_scroll() {
|
||||
int max_scroll = list_count() - list_visible_rows();
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
if (g_list_scroll > max_scroll) g_list_scroll = max_scroll;
|
||||
if (g_list_scroll < 0) g_list_scroll = 0;
|
||||
}
|
||||
|
||||
static void set_status(const char* msg) {
|
||||
snprintf(g_status, sizeof(g_status), "%s", msg ? msg : "");
|
||||
g_status_time = montauk::get_milliseconds();
|
||||
}
|
||||
|
||||
static bool status_visible() {
|
||||
return g_status[0] && montauk::get_milliseconds() - g_status_time < 5000;
|
||||
}
|
||||
|
||||
// ==== Drawing ====
|
||||
|
||||
static void fit_text(char* out, int outCap, const char* text, int maxWidth) {
|
||||
if (!out || outCap <= 0) return;
|
||||
out[0] = '\0';
|
||||
if (!text || !text[0] || maxWidth <= 0) return;
|
||||
if (text_width(text) <= maxWidth) {
|
||||
snprintf(out, outCap, "%s", text);
|
||||
return;
|
||||
}
|
||||
|
||||
static const char* ellipsis = "...";
|
||||
int len = 0;
|
||||
while (text[len] && len < outCap - 4) {
|
||||
out[len] = text[len];
|
||||
out[len + 1] = '\0';
|
||||
if (text_width(out) + text_width(ellipsis) > maxWidth) break;
|
||||
len++;
|
||||
}
|
||||
out[len++] = '.';
|
||||
out[len++] = '.';
|
||||
out[len++] = '.';
|
||||
out[len] = '\0';
|
||||
}
|
||||
|
||||
static void draw_text_fit(Canvas& c, int x, int y, const char* text,
|
||||
int maxWidth, Color color) {
|
||||
char fitted[160];
|
||||
fit_text(fitted, sizeof(fitted), text, maxWidth);
|
||||
c.text(x, y, fitted, color);
|
||||
}
|
||||
|
||||
static void draw_list(Canvas& c, const mtk::Theme& theme) {
|
||||
int fh = system_font_height();
|
||||
int count = list_count();
|
||||
int visible = list_visible_rows();
|
||||
for (int row = 0; row < visible; row++) {
|
||||
int index = g_list_scroll + row;
|
||||
if (index >= count) break;
|
||||
Rect r = list_row_rect(row);
|
||||
bool selected = index == (g_tab == TAB_PROGRAMS ? g_selected_grant : g_selected_cap);
|
||||
mtk::draw_list_row(c, r, selected, false, theme);
|
||||
if (!selected && mouse_in(r))
|
||||
c.fill_rounded_rect(r.x, r.y, r.w, r.h, theme.radius_md, theme.surface_hover);
|
||||
|
||||
const char* label = g_tab == TAB_PROGRAMS
|
||||
? g_grants[index].id : montauk::caps::NAMES[index].name;
|
||||
Color fg = selected ? theme.accent_fg : theme.text;
|
||||
int ty = r.y + (r.h - fh) / 2;
|
||||
draw_text_fit(c, r.x + 10, ty, label, r.w - 34, fg);
|
||||
|
||||
if (g_tab == TAB_PROGRAMS && g_grants[index].issue_count > 0) {
|
||||
Color dot = selected ? theme.accent_fg : theme.danger;
|
||||
fill_circle(c, r.x + r.w - 14, r.y + r.h / 2, 4, dot);
|
||||
}
|
||||
}
|
||||
|
||||
if (list_scrollbar_visible()) {
|
||||
Rect track = list_scrollbar_track();
|
||||
bool hovered = track.contains(g_mouse_x, g_mouse_y) || g_list_sb_dragging;
|
||||
mtk::draw_scrollbar(c, track, list_scrollbar_thumb(), hovered,
|
||||
g_list_sb_dragging, theme);
|
||||
}
|
||||
|
||||
int sx = sidebar_w();
|
||||
c.fill_rect(sx, TAB_H, 1, footer_rect().y - TAB_H, theme.border);
|
||||
}
|
||||
|
||||
static void draw_mark(Canvas& c, int cx, int cy, bool on, const mtk::Theme& theme) {
|
||||
Rect cell = {cx - 8, cy - 8, 16, 16};
|
||||
mtk::draw_checkbox(c, cell, "", mtk::check_state(on), theme, true, false);
|
||||
}
|
||||
|
||||
static void draw_programs_pane(Canvas& c, const mtk::Theme& theme) {
|
||||
int x = pane_x();
|
||||
int w = pane_w();
|
||||
int fh = system_font_height();
|
||||
int y = TAB_H + PAD;
|
||||
|
||||
if (g_grant_count == 0) {
|
||||
c.text(x, y, g_file_present ? "No grants are declared."
|
||||
: "0:/config/capabilities.toml was not found.",
|
||||
theme.text_subtle);
|
||||
c.text(x, y + fh + 8, "Programs without a grant run with no capabilities.",
|
||||
theme.text_muted);
|
||||
return;
|
||||
}
|
||||
|
||||
const Grant& g = g_grants[g_selected_grant];
|
||||
char title[64];
|
||||
snprintf(title, sizeof(title), "[grant.%s]", g.id);
|
||||
c.text(x, y, title, theme.text);
|
||||
|
||||
if (g.issue_count > 0) {
|
||||
const char* state = "Needs attention";
|
||||
c.text(x + w - text_width(state), y, state, theme.danger);
|
||||
}
|
||||
|
||||
y += fh + 6;
|
||||
draw_text_fit(c, x, y, g.path[0] ? g.path : "(no path)", w, theme.text_subtle);
|
||||
|
||||
y += fh + 14;
|
||||
mtk::draw_separator(c, x, y, w, theme);
|
||||
y += 12;
|
||||
|
||||
// Grid: one row per capability, one column per mask.
|
||||
int colX = x + w - GRID_COL_W * FIELD_COUNT;
|
||||
c.text(x, y, "CAPABILITY", theme.text_muted);
|
||||
for (int f = 0; f < FIELD_COUNT; f++) {
|
||||
const char* label = kFieldLabels[f];
|
||||
int cx = colX + f * GRID_COL_W + GRID_COL_W / 2;
|
||||
c.text(cx - text_width(label) / 2, y, label, theme.text_muted);
|
||||
}
|
||||
y += fh + 4;
|
||||
for (int f = 0; f < FIELD_COUNT; f++) {
|
||||
const char* note = nullptr;
|
||||
if (g.uses_all[f]) note = "(all)";
|
||||
else if (f == FIELD_PERMITTED && !g.declared[f]) note = "(implied)";
|
||||
if (!note) continue;
|
||||
int cx = colX + f * GRID_COL_W + GRID_COL_W / 2;
|
||||
c.text(cx - text_width(note) / 2, y, note, theme.text_muted);
|
||||
}
|
||||
y += fh + 4;
|
||||
|
||||
for (int i = 0; i < CAP_COUNT; i++) {
|
||||
uint64_t bit = montauk::caps::NAMES[i].bit;
|
||||
bool any = (g.mask[FIELD_PERMITTED] & bit) != 0;
|
||||
if (i % 2 == 1)
|
||||
c.fill_rect(x, y, w, GRID_ROW_H, theme.surface_alt);
|
||||
c.text(x + 6, y + (GRID_ROW_H - fh) / 2, montauk::caps::NAMES[i].name,
|
||||
any ? theme.text : theme.text_muted);
|
||||
for (int f = 0; f < FIELD_COUNT; f++) {
|
||||
int cx = colX + f * GRID_COL_W + GRID_COL_W / 2;
|
||||
draw_mark(c, cx, y + GRID_ROW_H / 2, (g.mask[f] & bit) != 0, theme);
|
||||
}
|
||||
y += GRID_ROW_H;
|
||||
}
|
||||
|
||||
y += 10;
|
||||
for (int i = 0; i < g.issue_count; i++) {
|
||||
draw_text_fit(c, x, y, g.issues[i], w, theme.danger);
|
||||
y += fh + 4;
|
||||
}
|
||||
}
|
||||
|
||||
static int draw_holder_list(Canvas& c, int x, int y, int w, int bottom,
|
||||
const char* heading, int field, uint64_t bit,
|
||||
const mtk::Theme& theme) {
|
||||
int fh = system_font_height();
|
||||
c.text(x, y, heading, theme.text_muted);
|
||||
y += fh + 8;
|
||||
|
||||
int total = 0;
|
||||
for (int i = 0; i < g_grant_count; i++)
|
||||
if (g_grants[i].mask[field] & bit) total++;
|
||||
if (total == 0) {
|
||||
c.text(x + 10, y, "No program", theme.text_subtle);
|
||||
return y + fh + 16;
|
||||
}
|
||||
|
||||
int shown = 0;
|
||||
for (int i = 0; i < g_grant_count; i++) {
|
||||
const Grant& g = g_grants[i];
|
||||
if (!(g.mask[field] & bit)) continue;
|
||||
if (y + (fh + 6) * 2 > bottom && shown < total - 1) {
|
||||
char more[48];
|
||||
snprintf(more, sizeof(more), "+%d more", total - shown);
|
||||
c.text(x + 10, y, more, theme.text_muted);
|
||||
return y + fh + 16;
|
||||
}
|
||||
c.text(x + 10, y, g.id, theme.text);
|
||||
draw_text_fit(c, x + 130, y, g.path, w - 130, theme.text_subtle);
|
||||
y += fh + 6;
|
||||
shown++;
|
||||
}
|
||||
return y + 12;
|
||||
}
|
||||
|
||||
static void draw_caps_pane(Canvas& c, const mtk::Theme& theme) {
|
||||
int x = pane_x();
|
||||
int w = pane_w();
|
||||
int fh = system_font_height();
|
||||
int y = TAB_H + PAD;
|
||||
int bottom = footer_rect().y - PAD;
|
||||
|
||||
const auto& cap = montauk::caps::NAMES[g_selected_cap];
|
||||
c.text(x, y, cap.name, theme.text);
|
||||
y += fh + 6;
|
||||
draw_text_fit(c, x, y, kCapDescriptions[g_selected_cap], w, theme.text_subtle);
|
||||
y += fh + 14;
|
||||
mtk::draw_separator(c, x, y, w, theme);
|
||||
y += 14;
|
||||
|
||||
y = draw_holder_list(c, x, y, w, bottom - (fh + 6) * 4,
|
||||
"USES IT (EFFECTIVE)", FIELD_EFFECTIVE, cap.bit, theme);
|
||||
draw_holder_list(c, x, y, w, bottom,
|
||||
"MAY PASS IT ON (DELEGABLE)", FIELD_DELEGABLE, cap.bit, theme);
|
||||
}
|
||||
|
||||
static void draw_locked(Canvas& c, const mtk::Theme& theme) {
|
||||
int fh = system_font_height();
|
||||
int y = TAB_H + 60;
|
||||
const char* title = "Administrator access required";
|
||||
c.text((g_win.width - text_width(title)) / 2, y, title, theme.text);
|
||||
const char* body = "Only administrators can review the capability grant table.";
|
||||
c.text((g_win.width - text_width(body)) / 2, y + fh + 10, body, theme.text_subtle);
|
||||
}
|
||||
|
||||
static void draw_footer(Canvas& c, const mtk::Theme& theme) {
|
||||
Rect foot = footer_rect();
|
||||
c.fill_rect(foot.x, foot.y, foot.w, foot.h, theme.surface);
|
||||
mtk::draw_separator(c, 0, foot.y, g_win.width, theme);
|
||||
|
||||
char summary[128];
|
||||
if (status_visible()) {
|
||||
snprintf(summary, sizeof(summary), "%s", g_status);
|
||||
} else if (!g_is_admin) {
|
||||
snprintf(summary, sizeof(summary), "Read-only");
|
||||
} else {
|
||||
int flagged = 0;
|
||||
for (int i = 0; i < g_grant_count; i++)
|
||||
if (g_grants[i].issue_count > 0) flagged++;
|
||||
snprintf(summary, sizeof(summary), "%d grant%s%s, %d flagged - read-only view",
|
||||
g_grant_count, g_grant_count == 1 ? "" : "s",
|
||||
g_truncated ? " (list truncated)" : "", flagged);
|
||||
}
|
||||
c.text(PAD, foot.y + (foot.h - system_font_height()) / 2, summary, theme.text_subtle);
|
||||
|
||||
if (g_is_admin) {
|
||||
Rect reload = reload_button();
|
||||
mtk::draw_button(c, reload, "Reload", mtk::BUTTON_SECONDARY,
|
||||
mtk::widget_state(false, mouse_in(reload), true), theme);
|
||||
}
|
||||
}
|
||||
|
||||
static void render() {
|
||||
mtk::StandaloneHost host(&g_win);
|
||||
Canvas c = host.canvas();
|
||||
mtk::Theme theme = app_theme();
|
||||
c.fill(theme.window_bg);
|
||||
if (g_is_admin) {
|
||||
mtk::draw_tab_bar(c, tab_bar_rect(), kTabLabels, TAB_COUNT, (int)g_tab, theme);
|
||||
draw_list(c, theme);
|
||||
if (g_tab == TAB_PROGRAMS) draw_programs_pane(c, theme);
|
||||
else draw_caps_pane(c, theme);
|
||||
} else {
|
||||
draw_locked(c, theme);
|
||||
}
|
||||
draw_footer(c, theme);
|
||||
host.present();
|
||||
}
|
||||
|
||||
// ==== Input ====
|
||||
|
||||
static void reload() {
|
||||
load_grants();
|
||||
clamp_list_scroll();
|
||||
set_status("Reloaded 0:/config/capabilities.toml");
|
||||
}
|
||||
|
||||
static void select_index(int index) {
|
||||
int count = list_count();
|
||||
if (count <= 0) return;
|
||||
if (index < 0) index = 0;
|
||||
if (index >= count) index = count - 1;
|
||||
if (g_tab == TAB_PROGRAMS) g_selected_grant = index;
|
||||
else g_selected_cap = index;
|
||||
|
||||
int visible = list_visible_rows();
|
||||
if (index < g_list_scroll) g_list_scroll = index;
|
||||
else if (index >= g_list_scroll + visible) g_list_scroll = index - visible + 1;
|
||||
clamp_list_scroll();
|
||||
}
|
||||
|
||||
static void switch_tab(Tab tab) {
|
||||
g_tab = tab;
|
||||
g_list_scroll = 0;
|
||||
select_index(tab == TAB_PROGRAMS ? g_selected_grant : g_selected_cap);
|
||||
}
|
||||
|
||||
static void handle_mouse(const montauk::abi::WinEvent& ev) {
|
||||
if (!g_is_admin) return;
|
||||
int mx = ev.mouse.x;
|
||||
int my = ev.mouse.y;
|
||||
bool pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
|
||||
Rect track = list_scrollbar_track();
|
||||
Rect thumb = list_scrollbar_thumb();
|
||||
if (g_list_sb_dragging) {
|
||||
if ((ev.mouse.buttons & 1) && !track.empty()) {
|
||||
int offset = mtk::scrollbar_offset_from_thumb_top(
|
||||
track, list_content_extent(), list_view_extent(),
|
||||
thumb.h, my - g_list_sb_drag_offset);
|
||||
g_list_scroll = offset / LIST_ROW_H;
|
||||
clamp_list_scroll();
|
||||
return;
|
||||
}
|
||||
g_list_sb_dragging = false;
|
||||
}
|
||||
|
||||
if (ev.mouse.scroll != 0 && list_rect().contains(mx, my)) {
|
||||
g_list_scroll += ev.mouse.scroll > 0 ? -1 : 1;
|
||||
clamp_list_scroll();
|
||||
return;
|
||||
}
|
||||
if (!pressed) return;
|
||||
|
||||
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my);
|
||||
if (tab >= 0) {
|
||||
switch_tab((Tab)tab);
|
||||
return;
|
||||
}
|
||||
if (!track.empty() && track.contains(mx, my)) {
|
||||
if (thumb.contains(mx, my)) {
|
||||
g_list_sb_dragging = true;
|
||||
g_list_sb_drag_offset = my - thumb.y;
|
||||
} else {
|
||||
int visible = list_visible_rows();
|
||||
int page = visible > 1 ? visible - 1 : 1;
|
||||
g_list_scroll += my < thumb.y ? -page : page;
|
||||
clamp_list_scroll();
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int row = 0; row < list_visible_rows(); row++) {
|
||||
int index = g_list_scroll + row;
|
||||
if (index >= list_count()) break;
|
||||
if (list_row_rect(row).contains(mx, my)) {
|
||||
select_index(index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (reload_button().contains(mx, my)) reload();
|
||||
}
|
||||
|
||||
static bool handle_key(const montauk::abi::KeyEvent& key) {
|
||||
if (!key.pressed) return false;
|
||||
if (key.scancode == 0x01) {
|
||||
g_win.closed = true;
|
||||
return true;
|
||||
}
|
||||
if (!g_is_admin) return false;
|
||||
if (key.ascii == '\t') {
|
||||
switch_tab(g_tab == TAB_PROGRAMS ? TAB_CAPS : TAB_PROGRAMS);
|
||||
return true;
|
||||
}
|
||||
int current = g_tab == TAB_PROGRAMS ? g_selected_grant : g_selected_cap;
|
||||
if (key.scancode == 0x48) { select_index(current - 1); return true; } // Up
|
||||
if (key.scancode == 0x50) { select_index(current + 1); return true; } // Down
|
||||
if (key.ascii == 'r' || key.ascii == 'R') {
|
||||
reload();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
if (!fonts::init()) montauk::exit(1);
|
||||
g_accent = mtk::load_system_accent();
|
||||
|
||||
g_is_admin = session_is_admin();
|
||||
if (g_is_admin) load_grants();
|
||||
|
||||
if (!g_win.create("Capabilities", WIN_W, WIN_H)) montauk::exit(1);
|
||||
render();
|
||||
|
||||
while (g_win.id >= 0 && !g_win.closed) {
|
||||
montauk::abi::WinEvent ev;
|
||||
int result = g_win.poll(&ev);
|
||||
if (result < 0) break;
|
||||
if (result == 0) {
|
||||
// Keep drawing while a status message is up, plus once more to
|
||||
// clear it when it expires.
|
||||
static bool status_drawn = false;
|
||||
if (status_visible() || status_drawn) {
|
||||
status_drawn = status_visible();
|
||||
render();
|
||||
}
|
||||
montauk::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool redraw = false;
|
||||
if (ev.type == 3) break;
|
||||
if (ev.type == 2 || ev.type == 4) {
|
||||
clamp_list_scroll();
|
||||
redraw = true;
|
||||
} else if (ev.type == 1) {
|
||||
g_mouse_x = ev.mouse.x;
|
||||
g_mouse_y = ev.mouse.y;
|
||||
handle_mouse(ev);
|
||||
redraw = true;
|
||||
} else if (ev.type == 0) {
|
||||
redraw = handle_key(ev.key);
|
||||
}
|
||||
if (redraw) render();
|
||||
}
|
||||
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
Reference in new issue
Block a user