feat: multi-user system, bug fixes, security & performance fixes, and more
This commit is contained in:
@@ -18,6 +18,7 @@ endif
|
||||
PROG_INC := ../../include
|
||||
JPEG_LIB := ../../lib/libjpeg
|
||||
LIBC_LIB := ../../lib/libc
|
||||
BEARSSL := ../../lib/bearssl
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
@@ -48,6 +49,8 @@ CXXFLAGS := \
|
||||
-mcmodel=small \
|
||||
-MMD -MP \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-I $(BEARSSL)/inc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
@@ -79,7 +82,7 @@ all: $(TARGET)
|
||||
|
||||
# ---- Libraries ----
|
||||
|
||||
LIBS := $(JPEG_LIB)/libjpeg.a $(LIBC_LIB)/liblibc.a
|
||||
LIBS := $(JPEG_LIB)/libjpeg.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a
|
||||
|
||||
$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/os
|
||||
|
||||
@@ -360,7 +360,12 @@ static void filemanager_go_up(FileManagerState* fm) {
|
||||
if (fm->current_path[i] == '/') { last_slash = i; break; }
|
||||
}
|
||||
if (last_slash >= 0) {
|
||||
fm->current_path[last_slash + 1] = '\0';
|
||||
// Keep the slash only if it's the root slash (right after "N:")
|
||||
if (last_slash > 0 && fm->current_path[last_slash - 1] == ':') {
|
||||
fm->current_path[last_slash + 1] = '\0';
|
||||
} else {
|
||||
fm->current_path[last_slash] = '\0';
|
||||
}
|
||||
}
|
||||
filemanager_push_history(fm);
|
||||
filemanager_read_dir(fm);
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#include "apps_common.hpp"
|
||||
#include "../wallpaper.hpp"
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/user.h>
|
||||
|
||||
// ============================================================================
|
||||
// Settings state
|
||||
@@ -13,11 +15,20 @@
|
||||
|
||||
struct SettingsState {
|
||||
DesktopState* desktop;
|
||||
int active_tab; // 0=Appearance, 1=Display, 2=About
|
||||
int active_tab; // 0=Appearance, 1=Display, 2=Users, 3=About
|
||||
Montauk::SysInfo sys_info;
|
||||
uint64_t uptime_ms;
|
||||
WallpaperFileList wp_files;
|
||||
bool wp_scanned;
|
||||
|
||||
// Users tab
|
||||
montauk::user::UserInfo users[16];
|
||||
int user_count;
|
||||
int selected_user; // index into users[], or -1
|
||||
bool users_loaded;
|
||||
|
||||
char status_msg[128];
|
||||
uint64_t status_time;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -69,8 +80,8 @@ static const Color accent_palette[SWATCH_COUNT] = {
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int TAB_BAR_H = 36;
|
||||
static constexpr int TAB_COUNT = 3;
|
||||
static const char* tab_labels[TAB_COUNT] = { "Appearance", "Display", "About" };
|
||||
static constexpr int TAB_COUNT = 4;
|
||||
static const char* tab_labels[TAB_COUNT] = { "Appearance", "Display", "Users", "About" };
|
||||
|
||||
// ============================================================================
|
||||
// Helper: check if two colors match
|
||||
@@ -166,11 +177,11 @@ static void settings_draw_appearance(Canvas& c, SettingsState* st) {
|
||||
if (mode_image) {
|
||||
// Scan for images lazily
|
||||
if (!st->wp_scanned) {
|
||||
wallpaper_scan_dir("0:/home", &st->wp_files);
|
||||
wallpaper_scan_dir(st->desktop->home_dir, &st->wp_files);
|
||||
st->wp_scanned = true;
|
||||
}
|
||||
|
||||
c.text(x, y, "Images in 0:/home/", dim);
|
||||
c.text(x, y, "Wallpapers", dim);
|
||||
y += sfh + 6;
|
||||
|
||||
if (st->wp_files.count == 0) {
|
||||
@@ -180,7 +191,8 @@ static void settings_draw_appearance(Canvas& c, SettingsState* st) {
|
||||
for (int i = 0; i < st->wp_files.count && i < 8; i++) {
|
||||
// Build full path for comparison
|
||||
char fullpath[256];
|
||||
montauk::strcpy(fullpath, "0:/home/");
|
||||
montauk::strcpy(fullpath, st->desktop->home_dir);
|
||||
str_append(fullpath, "/", 256);
|
||||
str_append(fullpath, st->wp_files.names[i], 256);
|
||||
|
||||
bool selected = s.bg_image &&
|
||||
@@ -333,6 +345,702 @@ static void settings_draw_about(Canvas& c, SettingsState* st) {
|
||||
c.text(x, y, "Copyright (c) 2026 Daniel Hammer", dim);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Config persistence
|
||||
// ============================================================================
|
||||
|
||||
static int64_t color_to_int(Color c) {
|
||||
return ((int64_t)c.r << 16) | ((int64_t)c.g << 8) | c.b;
|
||||
}
|
||||
|
||||
static Color int_to_color(int64_t v) {
|
||||
return Color::from_rgb((uint8_t)((v >> 16) & 0xFF),
|
||||
(uint8_t)((v >> 8) & 0xFF),
|
||||
(uint8_t)(v & 0xFF));
|
||||
}
|
||||
|
||||
static void settings_persist(SettingsState* st) {
|
||||
DesktopSettings& s = st->desktop->settings;
|
||||
const char* user = st->desktop->current_user;
|
||||
|
||||
montauk::toml::Doc doc;
|
||||
doc.init();
|
||||
|
||||
// Background mode
|
||||
const char* mode = s.bg_image ? "image" : (s.bg_gradient ? "gradient" : "solid");
|
||||
montauk::config::set_string(&doc, "background.mode", mode);
|
||||
|
||||
// Wallpaper path
|
||||
if (s.bg_image && s.bg_image_path[0])
|
||||
montauk::config::set_string(&doc, "wallpaper.path", s.bg_image_path);
|
||||
|
||||
// Background colors
|
||||
montauk::config::set_int(&doc, "background.solid_color", color_to_int(s.bg_solid));
|
||||
montauk::config::set_int(&doc, "background.grad_top", color_to_int(s.bg_grad_top));
|
||||
montauk::config::set_int(&doc, "background.grad_bottom", color_to_int(s.bg_grad_bottom));
|
||||
|
||||
// Appearance
|
||||
montauk::config::set_int(&doc, "appearance.panel_color", color_to_int(s.panel_color));
|
||||
montauk::config::set_int(&doc, "appearance.accent_color", color_to_int(s.accent_color));
|
||||
|
||||
// Display
|
||||
montauk::config::set_int(&doc, "display.ui_scale", s.ui_scale);
|
||||
montauk::config::set_bool(&doc, "display.clock_24h", s.clock_24h);
|
||||
montauk::config::set_bool(&doc, "display.show_shadows", s.show_shadows);
|
||||
|
||||
montauk::config::save_user(user, "desktop", &doc);
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Users tab
|
||||
// ============================================================================
|
||||
|
||||
static void users_reload(SettingsState* st) {
|
||||
st->user_count = montauk::user::load_users(st->users, 16);
|
||||
st->users_loaded = true;
|
||||
}
|
||||
|
||||
static constexpr int USER_BTN_H = 30;
|
||||
static constexpr int USER_BTN_W = 100;
|
||||
static constexpr int USER_FIELD_H = 32;
|
||||
|
||||
// Forward declarations for dialog openers
|
||||
static void open_add_user_dialog(SettingsState* st);
|
||||
static void open_change_pwd_dialog(SettingsState* st);
|
||||
static void open_delete_user_dialog(SettingsState* st);
|
||||
|
||||
static int user_row_height() {
|
||||
return system_font_height() * 2 + 12; // Two lines of text + padding
|
||||
}
|
||||
|
||||
static void settings_draw_users(Canvas& c, SettingsState* st) {
|
||||
if (!st->users_loaded) users_reload(st);
|
||||
|
||||
Color accent = st->desktop->settings.accent_color;
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
Color card_bg = Color::from_rgb(0xF8, 0xF8, 0xF8);
|
||||
int x = 16;
|
||||
int y = 20;
|
||||
int sfh = system_font_height();
|
||||
int content_w = c.w - 2 * x;
|
||||
int row_h = user_row_height();
|
||||
|
||||
if (!st->desktop->is_admin) {
|
||||
c.text(x, y, "Admin access required to manage users.", dim);
|
||||
return;
|
||||
}
|
||||
|
||||
// User list
|
||||
for (int i = 0; i < st->user_count; i++) {
|
||||
bool sel = (i == st->selected_user);
|
||||
bool is_current = montauk::streq(st->users[i].username, st->desktop->current_user);
|
||||
|
||||
if (sel) {
|
||||
c.fill_rounded_rect(x, y, content_w, row_h, 4, accent);
|
||||
} else if (i % 2 == 0) {
|
||||
c.fill_rounded_rect(x, y, content_w, row_h, 4, card_bg);
|
||||
}
|
||||
|
||||
Color tc = sel ? colors::WHITE : colors::TEXT_COLOR;
|
||||
Color sc = sel ? Color::from_rgb(0xDD, 0xDD, 0xFF) : dim;
|
||||
|
||||
// Display name (primary line)
|
||||
int text_y = y + 4;
|
||||
c.text(x + 12, text_y, st->users[i].display_name, tc);
|
||||
|
||||
// Username (secondary line)
|
||||
char sub[48];
|
||||
if (is_current) {
|
||||
snprintf(sub, sizeof(sub), "@%s (you)", st->users[i].username);
|
||||
} else {
|
||||
snprintf(sub, sizeof(sub), "@%s", st->users[i].username);
|
||||
}
|
||||
c.text(x + 12, text_y + sfh + 2, sub, sc);
|
||||
|
||||
// Role badge
|
||||
const char* role = st->users[i].role;
|
||||
int rw = text_width(role) + 12;
|
||||
int badge_x = c.w - x - rw - 8;
|
||||
int badge_y = y + (row_h - sfh - 4) / 2;
|
||||
if (sel) {
|
||||
c.fill_rounded_rect(badge_x, badge_y, rw, sfh + 4, (sfh + 4) / 2,
|
||||
Color::from_rgb(0xFF, 0xFF, 0xFF));
|
||||
c.text(badge_x + 6, badge_y + 2, role, accent);
|
||||
} else {
|
||||
bool is_admin_role = montauk::streq(role, "admin");
|
||||
Color badge_bg = is_admin_role
|
||||
? Color::from_rgb(0xE8, 0xD8, 0xF0)
|
||||
: Color::from_rgb(0xE0, 0xE8, 0xF0);
|
||||
Color badge_fg = is_admin_role
|
||||
? Color::from_rgb(0x7B, 0x3E, 0xB8)
|
||||
: Color::from_rgb(0x36, 0x7B, 0xF0);
|
||||
c.fill_rounded_rect(badge_x, badge_y, rw, sfh + 4, (sfh + 4) / 2, badge_bg);
|
||||
c.text(badge_x + 6, badge_y + 2, role, badge_fg);
|
||||
}
|
||||
|
||||
y += row_h + 4;
|
||||
}
|
||||
|
||||
// Separator
|
||||
y += 8;
|
||||
c.hline(x, y, content_w, colors::BORDER);
|
||||
y += 16;
|
||||
|
||||
// Status message
|
||||
if (st->status_msg[0] && (montauk::get_milliseconds() - st->status_time < 3000)) {
|
||||
c.text(x, y, st->status_msg, accent);
|
||||
y += sfh + 8;
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
int btn_x = x;
|
||||
|
||||
// Add User
|
||||
c.fill_rounded_rect(btn_x, y, 90, USER_BTN_H, 4, accent);
|
||||
{
|
||||
int tw = text_width("Add User");
|
||||
c.text(btn_x + (90 - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Add User", colors::WHITE);
|
||||
}
|
||||
btn_x += 98;
|
||||
|
||||
// Change Password (disabled when no selection)
|
||||
{
|
||||
bool enabled = st->selected_user >= 0;
|
||||
Color bg = enabled ? accent : Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
c.fill_rounded_rect(btn_x, y, USER_BTN_W, USER_BTN_H, 4, bg);
|
||||
int tw = text_width("Change Pwd");
|
||||
c.text(btn_x + (USER_BTN_W - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Change Pwd", colors::WHITE);
|
||||
}
|
||||
btn_x += USER_BTN_W + 8;
|
||||
|
||||
// Delete (disabled when no selection)
|
||||
{
|
||||
bool enabled = st->selected_user >= 0;
|
||||
Color del_bg = enabled ? Color::from_rgb(0xD0, 0x3E, 0x3E) : Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
c.fill_rounded_rect(btn_x, y, 70, USER_BTN_H, 4, del_bg);
|
||||
int tw = text_width("Delete");
|
||||
c.text(btn_x + (70 - tw) / 2, y + (USER_BTN_H - sfh) / 2, "Delete", colors::WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Dialog helpers
|
||||
// ============================================================================
|
||||
|
||||
static void dialog_append(char* buf, int* len, int max, char ch) {
|
||||
if (*len < max - 1) { buf[*len] = ch; (*len)++; buf[*len] = '\0'; }
|
||||
}
|
||||
static void dialog_backspace(char* buf, int* len) {
|
||||
if (*len > 0) { (*len)--; buf[*len] = '\0'; }
|
||||
}
|
||||
|
||||
static void dialog_close_self(DesktopState* ds, void* app_data) {
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (ds->windows[i].app_data == app_data) {
|
||||
desktop_close_window(ds, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_input_field(Canvas& c, int x, int y, int w, int h,
|
||||
const char* label, const char* value,
|
||||
bool focused, bool masked, Color accent) {
|
||||
int sfh = system_font_height();
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
|
||||
c.text(x, y, label, dim);
|
||||
y += sfh + 2;
|
||||
Color border = focused ? accent : colors::BORDER;
|
||||
c.fill_rounded_rect(x, y, w, h, 3, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
c.rect(x, y, w, h, border);
|
||||
|
||||
if (masked) {
|
||||
int vlen = montauk::slen(value);
|
||||
char dots[65];
|
||||
for (int i = 0; i < vlen && i < 64; i++) dots[i] = '*';
|
||||
dots[vlen < 64 ? vlen : 64] = '\0';
|
||||
c.text(x + 8, y + (h - sfh) / 2, dots, colors::TEXT_COLOR);
|
||||
} else {
|
||||
c.text(x + 8, y + (h - sfh) / 2, value, colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
if (focused) {
|
||||
int tw = masked ? text_width("*") * montauk::slen(value) : text_width(value);
|
||||
c.fill_rect(x + 8 + tw, y + 6, 2, h - 12, accent);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Add User Dialog
|
||||
// ============================================================================
|
||||
|
||||
struct AddUserDialogState {
|
||||
DesktopState* ds;
|
||||
SettingsState* parent;
|
||||
Color accent;
|
||||
int field; // 0=username, 1=display_name, 2=password
|
||||
char username[32]; int username_len;
|
||||
char display[64]; int display_len;
|
||||
char password[64]; int password_len;
|
||||
bool role_admin;
|
||||
char error[64];
|
||||
};
|
||||
|
||||
static void adduser_on_draw(Window* win, Framebuffer& fb) {
|
||||
AddUserDialogState* st = (AddUserDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
Color accent = st->accent;
|
||||
int sfh = system_font_height();
|
||||
int pad = 16;
|
||||
int fw = c.w - 2 * pad;
|
||||
int y = pad;
|
||||
|
||||
// Fields
|
||||
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Username", st->username,
|
||||
st->field == 0, false, accent);
|
||||
y += sfh + 2 + USER_FIELD_H + 10;
|
||||
|
||||
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Display Name", st->display,
|
||||
st->field == 1, false, accent);
|
||||
y += sfh + 2 + USER_FIELD_H + 10;
|
||||
|
||||
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Password", st->password,
|
||||
st->field == 2, true, accent);
|
||||
y += sfh + 2 + USER_FIELD_H + 12;
|
||||
|
||||
// Role toggle
|
||||
Color dim = Color::from_rgb(0x88, 0x88, 0x88);
|
||||
c.text(pad, y + 4, "Role:", dim);
|
||||
int rbx = pad + 60;
|
||||
draw_toggle_btn(c, rbx, y, 60, USER_BTN_H, "User", !st->role_admin, accent);
|
||||
draw_toggle_btn(c, rbx + 68, y, 60, USER_BTN_H, "Admin", st->role_admin, accent);
|
||||
y += USER_BTN_H + 14;
|
||||
|
||||
// Error message
|
||||
if (st->error[0]) {
|
||||
c.text(pad, y, st->error, Color::from_rgb(0xD0, 0x3E, 0x3E));
|
||||
y += sfh + 6;
|
||||
}
|
||||
|
||||
// Buttons at bottom
|
||||
int btn_y = c.h - USER_BTN_H - pad;
|
||||
c.fill_rounded_rect(pad, btn_y, 80, USER_BTN_H, 4, accent);
|
||||
int tw = text_width("Create");
|
||||
c.text(pad + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Create", colors::WHITE);
|
||||
|
||||
c.fill_rounded_rect(pad + 88, btn_y, 80, USER_BTN_H, 4, colors::WINDOW_BG);
|
||||
c.rect(pad + 88, btn_y, 80, USER_BTN_H, colors::BORDER);
|
||||
tw = text_width("Cancel");
|
||||
c.text(pad + 88 + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Cancel", colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
static void adduser_submit(AddUserDialogState* st) {
|
||||
if (st->username_len == 0) {
|
||||
montauk::strcpy(st->error, "Username is required");
|
||||
return;
|
||||
}
|
||||
if (st->password_len == 0) {
|
||||
montauk::strcpy(st->error, "Password is required");
|
||||
return;
|
||||
}
|
||||
const char* dname = st->display_len > 0 ? st->display : st->username;
|
||||
const char* role = st->role_admin ? "admin" : "user";
|
||||
if (montauk::user::create_user(st->username, dname, st->password, role)) {
|
||||
st->parent->users_loaded = false;
|
||||
montauk::strcpy(st->parent->status_msg, "User created");
|
||||
st->parent->status_time = montauk::get_milliseconds();
|
||||
dialog_close_self(st->ds, st);
|
||||
} else {
|
||||
montauk::strcpy(st->error, "Failed (username taken?)");
|
||||
}
|
||||
}
|
||||
|
||||
static void adduser_on_mouse(Window* win, MouseEvent& ev) {
|
||||
AddUserDialogState* st = (AddUserDialogState*)win->app_data;
|
||||
if (!st || !ev.left_pressed()) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int mx = ev.x - cr.x;
|
||||
int my = ev.y - cr.y;
|
||||
int pad = 16;
|
||||
int sfh = system_font_height();
|
||||
int fw = win->content_w - 2 * pad;
|
||||
int y = pad;
|
||||
|
||||
// Username field hit
|
||||
y += sfh + 2;
|
||||
if (my >= y && my < y + USER_FIELD_H) { st->field = 0; return; }
|
||||
y += USER_FIELD_H + 10;
|
||||
|
||||
// Display field hit
|
||||
y += sfh + 2;
|
||||
if (my >= y && my < y + USER_FIELD_H) { st->field = 1; return; }
|
||||
y += USER_FIELD_H + 10;
|
||||
|
||||
// Password field hit
|
||||
y += sfh + 2;
|
||||
if (my >= y && my < y + USER_FIELD_H) { st->field = 2; return; }
|
||||
y += USER_FIELD_H + 12;
|
||||
|
||||
// Role toggle
|
||||
int rbx = pad + 60;
|
||||
if (mx >= rbx && mx < rbx + 60 && my >= y && my < y + USER_BTN_H) {
|
||||
st->role_admin = false; return;
|
||||
}
|
||||
if (mx >= rbx + 68 && mx < rbx + 128 && my >= y && my < y + USER_BTN_H) {
|
||||
st->role_admin = true; return;
|
||||
}
|
||||
|
||||
// Bottom buttons
|
||||
int btn_y = win->content_h - USER_BTN_H - pad;
|
||||
if (my >= btn_y && my < btn_y + USER_BTN_H) {
|
||||
if (mx >= pad && mx < pad + 80) { adduser_submit(st); return; }
|
||||
if (mx >= pad + 88 && mx < pad + 168) { dialog_close_self(st->ds, st); return; }
|
||||
}
|
||||
}
|
||||
|
||||
static void adduser_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
AddUserDialogState* st = (AddUserDialogState*)win->app_data;
|
||||
if (!st || !key.pressed) return;
|
||||
|
||||
if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) {
|
||||
adduser_submit(st); return;
|
||||
}
|
||||
if (key.scancode == 0x01) { dialog_close_self(st->ds, st); return; }
|
||||
if (key.scancode == 0x0F) { st->field = (st->field + 1) % 3; return; }
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
switch (st->field) {
|
||||
case 0: dialog_backspace(st->username, &st->username_len); break;
|
||||
case 1: dialog_backspace(st->display, &st->display_len); break;
|
||||
case 2: dialog_backspace(st->password, &st->password_len); break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
switch (st->field) {
|
||||
case 0: dialog_append(st->username, &st->username_len, 31, key.ascii); break;
|
||||
case 1: dialog_append(st->display, &st->display_len, 63, key.ascii); break;
|
||||
case 2: dialog_append(st->password, &st->password_len, 63, key.ascii); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void adduser_on_close(Window* win) {
|
||||
if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; }
|
||||
}
|
||||
|
||||
static void open_add_user_dialog(SettingsState* parent) {
|
||||
DesktopState* ds = parent->desktop;
|
||||
int w = 340, h = 340;
|
||||
int wx = (ds->screen_w - w) / 2;
|
||||
int wy = (ds->screen_h - h) / 2;
|
||||
int idx = desktop_create_window(ds, "Add User", wx, wy, w, h);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
AddUserDialogState* st = (AddUserDialogState*)montauk::malloc(sizeof(AddUserDialogState));
|
||||
montauk::memset(st, 0, sizeof(AddUserDialogState));
|
||||
st->ds = ds;
|
||||
st->parent = parent;
|
||||
st->accent = ds->settings.accent_color;
|
||||
|
||||
win->app_data = st;
|
||||
win->on_draw = adduser_on_draw;
|
||||
win->on_mouse = adduser_on_mouse;
|
||||
win->on_key = adduser_on_key;
|
||||
win->on_close = adduser_on_close;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Change Password Dialog
|
||||
// ============================================================================
|
||||
|
||||
struct ChPwdDialogState {
|
||||
DesktopState* ds;
|
||||
SettingsState* parent;
|
||||
Color accent;
|
||||
char username[32];
|
||||
char display_name[64];
|
||||
int field; // 0=new, 1=confirm
|
||||
char new_pwd[64]; int new_len;
|
||||
char confirm[64]; int confirm_len;
|
||||
char error[64];
|
||||
};
|
||||
|
||||
static void chpwd_on_draw(Window* win, Framebuffer& fb) {
|
||||
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
Color accent = st->accent;
|
||||
int sfh = system_font_height();
|
||||
int pad = 16;
|
||||
int fw = c.w - 2 * pad;
|
||||
int y = pad;
|
||||
|
||||
// Title
|
||||
char title[80];
|
||||
snprintf(title, sizeof(title), "Change password for %s", st->display_name);
|
||||
c.text(pad, y, title, colors::TEXT_COLOR);
|
||||
y += sfh + 12;
|
||||
|
||||
draw_input_field(c, pad, y, fw, USER_FIELD_H, "New Password", st->new_pwd,
|
||||
st->field == 0, true, accent);
|
||||
y += sfh + 2 + USER_FIELD_H + 10;
|
||||
|
||||
draw_input_field(c, pad, y, fw, USER_FIELD_H, "Confirm Password", st->confirm,
|
||||
st->field == 1, true, accent);
|
||||
y += sfh + 2 + USER_FIELD_H + 12;
|
||||
|
||||
// Error
|
||||
if (st->error[0]) {
|
||||
c.text(pad, y, st->error, Color::from_rgb(0xD0, 0x3E, 0x3E));
|
||||
}
|
||||
|
||||
// Buttons at bottom
|
||||
int btn_y = c.h - USER_BTN_H - pad;
|
||||
c.fill_rounded_rect(pad, btn_y, 80, USER_BTN_H, 4, accent);
|
||||
int tw = text_width("Save");
|
||||
c.text(pad + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Save", colors::WHITE);
|
||||
|
||||
c.fill_rounded_rect(pad + 88, btn_y, 80, USER_BTN_H, 4, colors::WINDOW_BG);
|
||||
c.rect(pad + 88, btn_y, 80, USER_BTN_H, colors::BORDER);
|
||||
tw = text_width("Cancel");
|
||||
c.text(pad + 88 + (80 - tw) / 2, btn_y + (USER_BTN_H - sfh) / 2, "Cancel", colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
static void chpwd_submit(ChPwdDialogState* st) {
|
||||
if (st->new_len == 0) {
|
||||
montauk::strcpy(st->error, "Password cannot be empty");
|
||||
return;
|
||||
}
|
||||
if (!montauk::streq(st->new_pwd, st->confirm)) {
|
||||
montauk::strcpy(st->error, "Passwords don't match");
|
||||
return;
|
||||
}
|
||||
montauk::user::change_password(st->username, st->new_pwd);
|
||||
montauk::strcpy(st->parent->status_msg, "Password changed");
|
||||
st->parent->status_time = montauk::get_milliseconds();
|
||||
dialog_close_self(st->ds, st);
|
||||
}
|
||||
|
||||
static void chpwd_on_mouse(Window* win, MouseEvent& ev) {
|
||||
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
|
||||
if (!st || !ev.left_pressed()) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int mx = ev.x - cr.x;
|
||||
int my = ev.y - cr.y;
|
||||
int pad = 16;
|
||||
int sfh = system_font_height();
|
||||
int y = pad + sfh + 12;
|
||||
|
||||
// New password field
|
||||
y += sfh + 2;
|
||||
if (my >= y && my < y + USER_FIELD_H) { st->field = 0; return; }
|
||||
y += USER_FIELD_H + 10;
|
||||
|
||||
// Confirm field
|
||||
y += sfh + 2;
|
||||
if (my >= y && my < y + USER_FIELD_H) { st->field = 1; return; }
|
||||
|
||||
// Bottom buttons
|
||||
int btn_y = win->content_h - USER_BTN_H - pad;
|
||||
if (my >= btn_y && my < btn_y + USER_BTN_H) {
|
||||
if (mx >= pad && mx < pad + 80) { chpwd_submit(st); return; }
|
||||
if (mx >= pad + 88 && mx < pad + 168) { dialog_close_self(st->ds, st); return; }
|
||||
}
|
||||
}
|
||||
|
||||
static void chpwd_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
ChPwdDialogState* st = (ChPwdDialogState*)win->app_data;
|
||||
if (!st || !key.pressed) return;
|
||||
|
||||
if (key.ascii == '\n' || key.ascii == '\r' || key.scancode == 0x1C) {
|
||||
chpwd_submit(st); return;
|
||||
}
|
||||
if (key.scancode == 0x01) { dialog_close_self(st->ds, st); return; }
|
||||
if (key.scancode == 0x0F) { st->field = (st->field + 1) % 2; return; }
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (st->field == 0) dialog_backspace(st->new_pwd, &st->new_len);
|
||||
else dialog_backspace(st->confirm, &st->confirm_len);
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
if (st->field == 0) dialog_append(st->new_pwd, &st->new_len, 63, key.ascii);
|
||||
else dialog_append(st->confirm, &st->confirm_len, 63, key.ascii);
|
||||
}
|
||||
}
|
||||
|
||||
static void chpwd_on_close(Window* win) {
|
||||
if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; }
|
||||
}
|
||||
|
||||
static void open_change_pwd_dialog(SettingsState* parent) {
|
||||
if (parent->selected_user < 0) return;
|
||||
DesktopState* ds = parent->desktop;
|
||||
int w = 340, h = 260;
|
||||
int wx = (ds->screen_w - w) / 2;
|
||||
int wy = (ds->screen_h - h) / 2;
|
||||
int idx = desktop_create_window(ds, "Change Password", wx, wy, w, h);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
ChPwdDialogState* st = (ChPwdDialogState*)montauk::malloc(sizeof(ChPwdDialogState));
|
||||
montauk::memset(st, 0, sizeof(ChPwdDialogState));
|
||||
st->ds = ds;
|
||||
st->parent = parent;
|
||||
st->accent = ds->settings.accent_color;
|
||||
montauk::strncpy(st->username, parent->users[parent->selected_user].username, 31);
|
||||
montauk::strncpy(st->display_name, parent->users[parent->selected_user].display_name, 63);
|
||||
|
||||
win->app_data = st;
|
||||
win->on_draw = chpwd_on_draw;
|
||||
win->on_mouse = chpwd_on_mouse;
|
||||
win->on_key = chpwd_on_key;
|
||||
win->on_close = chpwd_on_close;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Delete User Dialog
|
||||
// ============================================================================
|
||||
|
||||
struct DeleteUserDialogState {
|
||||
DesktopState* ds;
|
||||
SettingsState* parent;
|
||||
char username[32];
|
||||
bool hover_delete, hover_cancel;
|
||||
};
|
||||
|
||||
static void deluser_on_draw(Window* win, Framebuffer& fb) {
|
||||
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Canvas c(win);
|
||||
c.fill(colors::WINDOW_BG);
|
||||
int sfh = system_font_height();
|
||||
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg), "Delete user \"%s\"?", st->username);
|
||||
int tw = text_width(msg);
|
||||
c.text((c.w - tw) / 2, 24, msg, colors::TEXT_COLOR);
|
||||
|
||||
const char* warn = "This action cannot be undone.";
|
||||
tw = text_width(warn);
|
||||
c.text((c.w - tw) / 2, 24 + sfh + 8, warn, Color::from_rgb(0x88, 0x88, 0x88));
|
||||
|
||||
// Buttons
|
||||
int btn_w = 100, btn_h = 32;
|
||||
int btn_y = c.h - btn_h - 20;
|
||||
int gap = 20;
|
||||
int total = btn_w * 2 + gap;
|
||||
int bx = (c.w - total) / 2;
|
||||
|
||||
Color del_bg = st->hover_delete
|
||||
? Color::from_rgb(0xDD, 0x44, 0x44)
|
||||
: Color::from_rgb(0xD0, 0x3E, 0x3E);
|
||||
c.button(bx, btn_y, btn_w, btn_h, "Delete", del_bg, colors::WHITE, 4);
|
||||
|
||||
Color cancel_bg = st->hover_cancel
|
||||
? Color::from_rgb(0x99, 0x99, 0x99)
|
||||
: Color::from_rgb(0x88, 0x88, 0x88);
|
||||
c.button(bx + btn_w + gap, btn_y, btn_w, btn_h, "Cancel", cancel_bg, colors::WHITE, 4);
|
||||
}
|
||||
|
||||
static void deluser_on_mouse(Window* win, MouseEvent& ev) {
|
||||
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int lx = ev.x - cr.x;
|
||||
int ly = ev.y - cr.y;
|
||||
|
||||
int btn_w = 100, btn_h = 32;
|
||||
int btn_y = win->content_h - btn_h - 20;
|
||||
int gap = 20;
|
||||
int total = btn_w * 2 + gap;
|
||||
int bx = (win->content_w - total) / 2;
|
||||
|
||||
Rect db = {bx, btn_y, btn_w, btn_h};
|
||||
Rect cb = {bx + btn_w + gap, btn_y, btn_w, btn_h};
|
||||
st->hover_delete = db.contains(lx, ly);
|
||||
st->hover_cancel = cb.contains(lx, ly);
|
||||
|
||||
if (ev.left_pressed()) {
|
||||
if (st->hover_delete) {
|
||||
montauk::user::delete_user(st->username);
|
||||
st->parent->users_loaded = false;
|
||||
st->parent->selected_user = -1;
|
||||
montauk::strcpy(st->parent->status_msg, "User deleted");
|
||||
st->parent->status_time = montauk::get_milliseconds();
|
||||
dialog_close_self(st->ds, st);
|
||||
return;
|
||||
}
|
||||
if (st->hover_cancel) {
|
||||
dialog_close_self(st->ds, st);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void deluser_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
DeleteUserDialogState* st = (DeleteUserDialogState*)win->app_data;
|
||||
if (!st || !key.pressed) return;
|
||||
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
montauk::user::delete_user(st->username);
|
||||
st->parent->users_loaded = false;
|
||||
st->parent->selected_user = -1;
|
||||
montauk::strcpy(st->parent->status_msg, "User deleted");
|
||||
st->parent->status_time = montauk::get_milliseconds();
|
||||
dialog_close_self(st->ds, st);
|
||||
}
|
||||
if (key.scancode == 0x01) {
|
||||
dialog_close_self(st->ds, st);
|
||||
}
|
||||
}
|
||||
|
||||
static void deluser_on_close(Window* win) {
|
||||
if (win->app_data) { montauk::mfree(win->app_data); win->app_data = nullptr; }
|
||||
}
|
||||
|
||||
static void open_delete_user_dialog(SettingsState* parent) {
|
||||
if (parent->selected_user < 0) return;
|
||||
DesktopState* ds = parent->desktop;
|
||||
|
||||
// Don't allow deleting yourself
|
||||
if (montauk::streq(parent->users[parent->selected_user].username, ds->current_user)) {
|
||||
montauk::strcpy(parent->status_msg, "Cannot delete current user");
|
||||
parent->status_time = montauk::get_milliseconds();
|
||||
return;
|
||||
}
|
||||
|
||||
int w = 300, h = 150;
|
||||
int wx = (ds->screen_w - w) / 2;
|
||||
int wy = (ds->screen_h - h) / 2;
|
||||
int idx = desktop_create_window(ds, "Delete User", wx, wy, w, h);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
DeleteUserDialogState* st = (DeleteUserDialogState*)montauk::malloc(sizeof(DeleteUserDialogState));
|
||||
montauk::memset(st, 0, sizeof(DeleteUserDialogState));
|
||||
st->ds = ds;
|
||||
st->parent = parent;
|
||||
montauk::strncpy(st->username, parent->users[parent->selected_user].username, 31);
|
||||
|
||||
win->app_data = st;
|
||||
win->on_draw = deluser_on_draw;
|
||||
win->on_mouse = deluser_on_mouse;
|
||||
win->on_key = deluser_on_key;
|
||||
win->on_close = deluser_on_close;
|
||||
}
|
||||
|
||||
static void settings_on_draw(Window* win, Framebuffer& fb) {
|
||||
SettingsState* st = (SettingsState*)win->app_data;
|
||||
if (!st) return;
|
||||
@@ -372,7 +1080,8 @@ static void settings_on_draw(Window* win, Framebuffer& fb) {
|
||||
switch (st->active_tab) {
|
||||
case 0: settings_draw_appearance(content, st); break;
|
||||
case 1: settings_draw_display(content, st); break;
|
||||
case 2: settings_draw_about(content, st); break;
|
||||
case 2: settings_draw_users(content, st); break;
|
||||
case 3: settings_draw_about(content, st); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,12 +1141,14 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
if (mx >= x && mx < x + 100 && cy >= y && cy < y + 16) {
|
||||
s.bg_gradient = true;
|
||||
s.bg_image = false;
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
// Radio: Solid
|
||||
if (mx >= x + 120 && mx < x + 210 && cy >= y && cy < y + 16) {
|
||||
s.bg_gradient = false;
|
||||
s.bg_image = false;
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
// Radio: Image
|
||||
@@ -445,16 +1156,17 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
s.bg_image = true;
|
||||
s.bg_gradient = false;
|
||||
if (!st->wp_scanned) {
|
||||
wallpaper_scan_dir("0:/home", &st->wp_files);
|
||||
wallpaper_scan_dir(st->desktop->home_dir, &st->wp_files);
|
||||
st->wp_scanned = true;
|
||||
}
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
y += line_h + 4;
|
||||
|
||||
int idx;
|
||||
if (mode_image) {
|
||||
// "Images in 0:/home/" label
|
||||
// Wallpaper file list
|
||||
y += sfh + 6;
|
||||
|
||||
// File list clicks
|
||||
@@ -463,10 +1175,12 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
mx >= x && mx < win->content_w - x) {
|
||||
// Build full path and load wallpaper
|
||||
char fullpath[256];
|
||||
montauk::strcpy(fullpath, "0:/home/");
|
||||
montauk::strcpy(fullpath, st->desktop->home_dir);
|
||||
str_append(fullpath, "/", 256);
|
||||
str_append(fullpath, st->wp_files.names[i], 256);
|
||||
wallpaper_load(&s, fullpath,
|
||||
st->desktop->screen_w, st->desktop->screen_h);
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
y += WP_ITEM_H;
|
||||
@@ -477,6 +1191,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
// Top swatches
|
||||
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
|
||||
s.bg_grad_top = bg_palette[idx];
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
@@ -484,6 +1199,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
// Bottom swatches
|
||||
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
|
||||
s.bg_grad_bottom = bg_palette[idx];
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
@@ -491,6 +1207,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
// Solid color swatches
|
||||
if (swatch_hit(mx, cy, x + 70, y, &idx)) {
|
||||
s.bg_solid = bg_palette[idx];
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
@@ -502,6 +1219,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
// Panel color swatches
|
||||
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
|
||||
s.panel_color = panel_palette[idx];
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
y += SWATCH_SIZE + 14;
|
||||
@@ -512,6 +1230,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
// Accent color swatches
|
||||
if (swatch_hit(mx, cy, x + 110, y, &idx)) {
|
||||
s.accent_color = accent_palette[idx];
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
} else if (st->active_tab == 1) {
|
||||
@@ -525,11 +1244,13 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
// Window Shadows: On
|
||||
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
|
||||
s.show_shadows = true;
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
// Window Shadows: Off
|
||||
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
|
||||
s.show_shadows = false;
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
y += btn_h + 20 + 16;
|
||||
@@ -537,11 +1258,13 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
// Clock: 24h
|
||||
if (mx >= bx && mx < bx + btn_w && cy >= y && cy < y + btn_h) {
|
||||
s.clock_24h = true;
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
// Clock: 12h
|
||||
if (mx >= bx + btn_w + 8 && mx < bx + btn_w * 2 + 8 && cy >= y && cy < y + btn_h) {
|
||||
s.clock_24h = false;
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
y += btn_h + 20 + 16;
|
||||
@@ -553,6 +1276,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
s.ui_scale = 0;
|
||||
apply_ui_scale(0);
|
||||
montauk::win_setscale(0);
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
// Default
|
||||
@@ -560,6 +1284,7 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
s.ui_scale = 1;
|
||||
apply_ui_scale(1);
|
||||
montauk::win_setscale(1);
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
// Large
|
||||
@@ -567,11 +1292,71 @@ static void settings_on_mouse(Window* win, MouseEvent& ev) {
|
||||
s.ui_scale = 2;
|
||||
apply_ui_scale(2);
|
||||
montauk::win_setscale(2);
|
||||
settings_persist(st);
|
||||
return;
|
||||
}
|
||||
} else if (st->active_tab == 2) {
|
||||
// Users tab
|
||||
if (!st->desktop->is_admin) return;
|
||||
|
||||
int x = 16;
|
||||
int sfh = system_font_height();
|
||||
int y = 20;
|
||||
int content_w = win->content_w - 2 * x;
|
||||
int row_h = user_row_height();
|
||||
|
||||
// User list rows
|
||||
for (int i = 0; i < st->user_count; i++) {
|
||||
if (cy >= y && cy < y + row_h && mx >= x && mx < x + content_w) {
|
||||
st->selected_user = (st->selected_user == i) ? -1 : i;
|
||||
return;
|
||||
}
|
||||
y += row_h + 4;
|
||||
}
|
||||
|
||||
// Separator + gap
|
||||
y += 8 + 1 + 16;
|
||||
|
||||
// Status message (if shown, takes space)
|
||||
if (st->status_msg[0] && (montauk::get_milliseconds() - st->status_time < 3000)) {
|
||||
y += sfh + 8;
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
int btn_x = x;
|
||||
|
||||
// Add User
|
||||
if (mx >= btn_x && mx < btn_x + 90 && cy >= y && cy < y + USER_BTN_H) {
|
||||
open_add_user_dialog(st);
|
||||
return;
|
||||
}
|
||||
btn_x += 98;
|
||||
|
||||
// Change Password
|
||||
if (mx >= btn_x && mx < btn_x + USER_BTN_W && cy >= y && cy < y + USER_BTN_H) {
|
||||
if (st->selected_user >= 0) open_change_pwd_dialog(st);
|
||||
return;
|
||||
}
|
||||
btn_x += USER_BTN_W + 8;
|
||||
|
||||
// Delete
|
||||
if (mx >= btn_x && mx < btn_x + 70 && cy >= y && cy < y + USER_BTN_H) {
|
||||
if (st->selected_user >= 0) open_delete_user_dialog(st);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Keyboard
|
||||
// ============================================================================
|
||||
|
||||
static void settings_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
// Settings main window no longer needs keyboard handling —
|
||||
// text input is handled by dialog windows.
|
||||
(void)win; (void)key;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cleanup
|
||||
// ============================================================================
|
||||
@@ -600,9 +1385,13 @@ void open_settings(DesktopState* ds) {
|
||||
st->uptime_ms = montauk::get_milliseconds();
|
||||
st->wp_scanned = false;
|
||||
st->wp_files.count = 0;
|
||||
st->selected_user = -1;
|
||||
st->users_loaded = false;
|
||||
st->status_msg[0] = '\0';
|
||||
|
||||
win->app_data = st;
|
||||
win->on_draw = settings_on_draw;
|
||||
win->on_mouse = settings_on_mouse;
|
||||
win->on_key = settings_on_key;
|
||||
win->on_close = settings_on_close;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,149 @@
|
||||
|
||||
#include "desktop_internal.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Lock Screen Drawing
|
||||
// ============================================================================
|
||||
|
||||
static constexpr Color LOCK_CARD_BG = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color LOCK_FIELD_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
||||
static constexpr Color LOCK_BORDER = Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
static constexpr Color LOCK_ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0);
|
||||
static constexpr Color LOCK_TEXT = Color::from_rgb(0x33, 0x33, 0x33);
|
||||
static constexpr Color LOCK_DIM = Color::from_rgb(0x66, 0x66, 0x66);
|
||||
static constexpr Color LOCK_ERROR = Color::from_rgb(0xE0, 0x40, 0x40);
|
||||
static constexpr Color LOCK_BTN_TEXT = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr int LOCK_CARD_W = 360;
|
||||
static constexpr int LOCK_FIELD_H = 36;
|
||||
static constexpr int LOCK_BTN_H = 40;
|
||||
|
||||
void desktop_draw_lock_screen(DesktopState* ds) {
|
||||
Framebuffer& fb = ds->fb;
|
||||
int sw = ds->screen_w;
|
||||
int sh = ds->screen_h;
|
||||
int sfh = system_font_height();
|
||||
|
||||
// Dark overlay on top of background
|
||||
fb.fill_rect_alpha(0, 0, sw, sh, Color::from_rgba(0, 0, 0, 0x80));
|
||||
|
||||
// Clock display above card
|
||||
Montauk::DateTime dt;
|
||||
montauk::gettime(&dt);
|
||||
char time_str[12];
|
||||
snprintf(time_str, sizeof(time_str), "%02d:%02d", (int)dt.Hour, (int)dt.Minute);
|
||||
|
||||
// Calculate card dimensions
|
||||
int error_h = ds->lock_show_error ? sfh + 8 : 0;
|
||||
int card_h = 20 + sfh + 16 + sfh + 12 + sfh + 4 + LOCK_FIELD_H + 16 + LOCK_BTN_H + error_h + 20;
|
||||
int card_x = (sw - LOCK_CARD_W) / 2;
|
||||
int card_y = (sh - card_h) / 2;
|
||||
|
||||
// Draw time above card
|
||||
int time_w = text_width(time_str);
|
||||
draw_text(fb, (sw - time_w) / 2, card_y - sfh * 3, time_str, Color::from_rgb(0xFF, 0xFF, 0xFF));
|
||||
|
||||
// Date below time
|
||||
{
|
||||
int month_idx = dt.Month > 0 && dt.Month <= 12 ? dt.Month - 1 : 0;
|
||||
char date_str[32];
|
||||
snprintf(date_str, sizeof(date_str), "%s %d", month_names[month_idx], (int)dt.Day);
|
||||
int dw = text_width(date_str);
|
||||
draw_text(fb, (sw - dw) / 2, card_y - sfh * 2 + 4, date_str, Color::from_rgb(0xCC, 0xCC, 0xCC));
|
||||
}
|
||||
|
||||
// Card background with rounded corners
|
||||
fill_rounded_rect(fb, card_x, card_y, LOCK_CARD_W, card_h, 12, LOCK_CARD_BG);
|
||||
|
||||
int x = card_x + 24;
|
||||
int content_w = LOCK_CARD_W - 48;
|
||||
int y = card_y + 20;
|
||||
|
||||
// Title (with optional lock icon beside it)
|
||||
{
|
||||
const char* title = "Locked";
|
||||
int tw = text_width(title);
|
||||
int icon_w = ds->icon_lock.pixels ? 20 : 0;
|
||||
int gap = icon_w ? 8 : 0;
|
||||
int total = icon_w + gap + tw;
|
||||
int tx = card_x + (LOCK_CARD_W - total) / 2;
|
||||
|
||||
if (ds->icon_lock.pixels) {
|
||||
fb.blit_alpha(tx, y + (sfh - 20) / 2, ds->icon_lock.width, ds->icon_lock.height, ds->icon_lock.pixels);
|
||||
}
|
||||
draw_text(fb, tx + icon_w + gap, y, title, LOCK_TEXT);
|
||||
y += sfh + 16;
|
||||
}
|
||||
|
||||
// Username display (cached at lock time)
|
||||
{
|
||||
int nw = text_width(ds->lock_display_name);
|
||||
draw_text(fb, card_x + (LOCK_CARD_W - nw) / 2, y, ds->lock_display_name, LOCK_DIM);
|
||||
y += sfh + 12;
|
||||
}
|
||||
|
||||
// Password label
|
||||
draw_text(fb, x, y, "Password", LOCK_DIM);
|
||||
y += sfh + 4;
|
||||
|
||||
// Password field
|
||||
{
|
||||
fb.fill_rect(x, y, content_w, LOCK_FIELD_H, LOCK_FIELD_BG);
|
||||
|
||||
// 2px accent border (always active)
|
||||
for (int i = 0; i < content_w; i++) { fb.put_pixel(x + i, y, LOCK_ACCENT); fb.put_pixel(x + i, y + 1, LOCK_ACCENT); }
|
||||
for (int i = 0; i < content_w; i++) { fb.put_pixel(x + i, y + LOCK_FIELD_H - 1, LOCK_ACCENT); fb.put_pixel(x + i, y + LOCK_FIELD_H - 2, LOCK_ACCENT); }
|
||||
for (int i = 0; i < LOCK_FIELD_H; i++) { fb.put_pixel(x, y + i, LOCK_ACCENT); fb.put_pixel(x + 1, y + i, LOCK_ACCENT); }
|
||||
for (int i = 0; i < LOCK_FIELD_H; i++) { fb.put_pixel(x + content_w - 1, y + i, LOCK_ACCENT); fb.put_pixel(x + content_w - 2, y + i, LOCK_ACCENT); }
|
||||
|
||||
// Masked password text (clipped to field width)
|
||||
char masked[65];
|
||||
int len = ds->lock_password_len;
|
||||
if (len > 64) len = 64;
|
||||
int max_text_w = content_w - 10 - 10 - 2; // left pad, right pad, cursor
|
||||
// Show only the trailing portion that fits
|
||||
int vis = len;
|
||||
{
|
||||
char tmp[65];
|
||||
for (int i = 0; i < len; i++) tmp[i] = '*';
|
||||
tmp[len] = '\0';
|
||||
while (vis > 0 && text_width(tmp + (len - vis)) > max_text_w)
|
||||
vis--;
|
||||
}
|
||||
for (int i = 0; i < vis; i++) masked[i] = '*';
|
||||
masked[vis] = '\0';
|
||||
|
||||
int ty = y + (LOCK_FIELD_H - sfh) / 2;
|
||||
draw_text(fb, x + 10, ty, masked, LOCK_TEXT);
|
||||
|
||||
// Cursor
|
||||
int cx = x + 10 + text_width(masked);
|
||||
fb.fill_rect(cx, ty, 2, sfh, LOCK_ACCENT);
|
||||
|
||||
y += LOCK_FIELD_H + 16;
|
||||
}
|
||||
|
||||
// Unlock button
|
||||
{
|
||||
fill_rounded_rect(fb, x, y, content_w, LOCK_BTN_H, 6, LOCK_ACCENT);
|
||||
const char* label = "Unlock";
|
||||
int tw = text_width(label);
|
||||
int ty = y + (LOCK_BTN_H - sfh) / 2;
|
||||
draw_text(fb, x + (content_w - tw) / 2, ty, label, LOCK_BTN_TEXT);
|
||||
y += LOCK_BTN_H;
|
||||
}
|
||||
|
||||
// Error message
|
||||
if (ds->lock_show_error) {
|
||||
y += 8;
|
||||
int ew = text_width(ds->lock_error);
|
||||
draw_text(fb, card_x + (LOCK_CARD_W - ew) / 2, y, ds->lock_error, LOCK_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Desktop Composition
|
||||
// ============================================================================
|
||||
|
||||
void gui::desktop_compose(DesktopState* ds) {
|
||||
Framebuffer& fb = ds->fb;
|
||||
|
||||
@@ -45,6 +188,13 @@ void gui::desktop_compose(DesktopState* ds) {
|
||||
}
|
||||
}
|
||||
|
||||
// Lock screen: draw overlay and card, then cursor, and return early
|
||||
if (ds->screen_locked) {
|
||||
desktop_draw_lock_screen(ds);
|
||||
draw_cursor(fb, ds->mouse.x, ds->mouse.y);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw windows from bottom to top
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (ds->windows[i].state != WIN_MINIMIZED && ds->windows[i].state != WIN_CLOSED) {
|
||||
@@ -65,6 +215,11 @@ void gui::desktop_compose(DesktopState* ds) {
|
||||
desktop_draw_net_popup(ds);
|
||||
}
|
||||
|
||||
// Draw volume popup if open
|
||||
if (ds->vol_popup_open) {
|
||||
desktop_draw_vol_popup(ds);
|
||||
}
|
||||
|
||||
// Draw right-click context menu if open
|
||||
if (ds->ctx_menu_open) {
|
||||
static constexpr int CTX_MENU_W = 180;
|
||||
|
||||
@@ -115,6 +115,10 @@ gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge);
|
||||
// panel.cpp
|
||||
void desktop_draw_app_menu(gui::DesktopState* ds);
|
||||
void desktop_draw_net_popup(gui::DesktopState* ds);
|
||||
void desktop_draw_vol_popup(gui::DesktopState* ds);
|
||||
|
||||
// compose.cpp
|
||||
void desktop_draw_lock_screen(gui::DesktopState* ds);
|
||||
|
||||
// main.cpp
|
||||
void desktop_scan_apps(gui::DesktopState* ds);
|
||||
|
||||
+247
-11
@@ -5,8 +5,133 @@
|
||||
*/
|
||||
|
||||
#include "desktop_internal.hpp"
|
||||
#include <montauk/user.h>
|
||||
|
||||
// ============================================================================
|
||||
// Lock Screen Input
|
||||
// ============================================================================
|
||||
|
||||
static void lock_screen(DesktopState* ds) {
|
||||
ds->screen_locked = true;
|
||||
ds->lock_password[0] = '\0';
|
||||
ds->lock_password_len = 0;
|
||||
ds->lock_error[0] = '\0';
|
||||
ds->lock_show_error = false;
|
||||
ds->app_menu_open = false;
|
||||
ds->ctx_menu_open = false;
|
||||
ds->net_popup_open = false;
|
||||
ds->vol_popup_open = false;
|
||||
|
||||
// Cache display name for lock screen rendering
|
||||
montauk::strncpy(ds->lock_display_name, ds->current_user, sizeof(ds->lock_display_name));
|
||||
montauk::user::UserInfo users[16];
|
||||
int count = montauk::user::load_users(users, 16);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (montauk::streq(users[i].username, ds->current_user)) {
|
||||
if (users[i].display_name[0])
|
||||
montauk::strncpy(ds->lock_display_name, users[i].display_name, sizeof(ds->lock_display_name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool try_unlock(DesktopState* ds) {
|
||||
ds->lock_show_error = false;
|
||||
|
||||
if (montauk::user::authenticate(ds->current_user, ds->lock_password)) {
|
||||
ds->screen_locked = false;
|
||||
ds->lock_password[0] = '\0';
|
||||
ds->lock_password_len = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
montauk::strcpy(ds->lock_error, "Incorrect password");
|
||||
ds->lock_show_error = true;
|
||||
ds->lock_password[0] = '\0';
|
||||
ds->lock_password_len = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void handle_lock_mouse(DesktopState* ds) {
|
||||
int mx = ds->mouse.x;
|
||||
int my = ds->mouse.y;
|
||||
uint8_t buttons = ds->mouse.buttons;
|
||||
uint8_t prev = ds->prev_buttons;
|
||||
bool left_pressed = (buttons & 0x01) && !(prev & 0x01);
|
||||
|
||||
if (!left_pressed) return;
|
||||
|
||||
int sfh = system_font_height();
|
||||
int card_w = 360;
|
||||
int content_w = card_w - 48;
|
||||
int field_h = 36;
|
||||
int btn_h = 40;
|
||||
|
||||
// Calculate card layout to find button position
|
||||
int error_h = ds->lock_show_error ? sfh + 8 : 0;
|
||||
int card_h = 20 + sfh + 16 + sfh + 12 + sfh + 4 + field_h + 16 + btn_h + error_h + 20;
|
||||
int card_x = (ds->screen_w - card_w) / 2;
|
||||
int card_y = (ds->screen_h - card_h) / 2;
|
||||
int x = card_x + 24;
|
||||
|
||||
// Walk through the layout to find the Unlock button y position
|
||||
int y = card_y + 20;
|
||||
y += sfh + 16; // title
|
||||
y += sfh + 12; // username
|
||||
y += sfh + 4; // "Password" label
|
||||
y += field_h + 16; // field + gap
|
||||
|
||||
// Check Unlock button
|
||||
if (mx >= x && mx < x + content_w && my >= y && my < y + btn_h) {
|
||||
try_unlock(ds);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check password field click (just keep focus, nothing else to switch to)
|
||||
int field_y = y - field_h - 16;
|
||||
if (mx >= x && mx < x + content_w && my >= field_y && my < field_y + field_h) {
|
||||
ds->lock_show_error = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_lock_keyboard(DesktopState* ds, const Montauk::KeyEvent& key) {
|
||||
if (!key.pressed) return;
|
||||
|
||||
// Enter submits
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
try_unlock(ds);
|
||||
return;
|
||||
}
|
||||
|
||||
// Backspace
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (ds->lock_password_len > 0) {
|
||||
ds->lock_password_len--;
|
||||
ds->lock_password[ds->lock_password_len] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Printable characters
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
if (ds->lock_password_len < 63) {
|
||||
ds->lock_password[ds->lock_password_len] = key.ascii;
|
||||
ds->lock_password_len++;
|
||||
ds->lock_password[ds->lock_password_len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Desktop Mouse Handling
|
||||
// ============================================================================
|
||||
|
||||
void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
if (ds->screen_locked) {
|
||||
handle_lock_mouse(ds);
|
||||
return;
|
||||
}
|
||||
|
||||
int mx = ds->mouse.x;
|
||||
int my = ds->mouse.y;
|
||||
uint8_t buttons = ds->mouse.buttons;
|
||||
@@ -213,8 +338,8 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
menu_cat_expanded[cur_cat] = !menu_cat_expanded[cur_cat];
|
||||
} else if (!row.is_category) {
|
||||
if (row.external) {
|
||||
// Launch external app from manifest
|
||||
montauk::spawn(row.binary_path);
|
||||
// Launch external app with user's home dir
|
||||
montauk::spawn(row.binary_path, ds->home_dir);
|
||||
} else {
|
||||
// Dispatch embedded app
|
||||
switch (row.app_id) {
|
||||
@@ -230,6 +355,8 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
case 12: open_reboot_dialog(ds); break;
|
||||
case 14: open_shutdown_dialog(ds); break;
|
||||
case 15: open_wordprocessor(ds); break;
|
||||
case 16: montauk::exit(0); break; // Log Out
|
||||
case 17: lock_screen(ds); break; // Lock Screen
|
||||
}
|
||||
}
|
||||
ds->app_menu_open = false;
|
||||
@@ -244,10 +371,104 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle volume popup interaction
|
||||
if (ds->vol_popup_open) {
|
||||
int popup_x = ds->vol_icon_rect.x + ds->vol_icon_rect.w - 200;
|
||||
int popup_y = PANEL_HEIGHT + 2;
|
||||
if (popup_x < 4) popup_x = 4;
|
||||
Rect vol_rect = {popup_x, popup_y, 200, 120};
|
||||
|
||||
// Handle drag continuity
|
||||
if (ds->vol_dragging) {
|
||||
if (left_held) {
|
||||
int slider_abs_x = popup_x + 16;
|
||||
int v = ((mx - slider_abs_x) * 100) / (200 - 32);
|
||||
if (v < 0) v = 0;
|
||||
if (v > 100) v = 100;
|
||||
ds->vol_muted = false;
|
||||
ds->vol_level = v;
|
||||
montauk::audio_set_volume(0, v);
|
||||
return;
|
||||
}
|
||||
if (left_released) {
|
||||
ds->vol_dragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (left_pressed) {
|
||||
if (vol_rect.contains(mx, my)) {
|
||||
// Slider area (y = popup_y + 56, height = 8, with generous hit zone)
|
||||
int slider_abs_x = popup_x + 16;
|
||||
int slider_abs_y = popup_y + 56;
|
||||
int slider_w = 200 - 32;
|
||||
if (my >= slider_abs_y - 10 && my <= slider_abs_y + 8 + 10 &&
|
||||
mx >= slider_abs_x - 8 && mx <= slider_abs_x + slider_w + 8) {
|
||||
int v = ((mx - slider_abs_x) * 100) / slider_w;
|
||||
if (v < 0) v = 0;
|
||||
if (v > 100) v = 100;
|
||||
ds->vol_muted = false;
|
||||
ds->vol_level = v;
|
||||
montauk::audio_set_volume(0, v);
|
||||
ds->vol_dragging = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Buttons (y = popup_y + 78, h = 24)
|
||||
int btn_h = 24;
|
||||
int btn_y = popup_y + 78;
|
||||
int minus_w = 36, plus_w = 36, mute_w = 50, gap = 8;
|
||||
int total_w = minus_w + plus_w + mute_w + gap * 2;
|
||||
int bx = popup_x + (200 - total_w) / 2;
|
||||
|
||||
if (my >= btn_y && my < btn_y + btn_h) {
|
||||
// [-]
|
||||
if (mx >= bx && mx < bx + minus_w) {
|
||||
ds->vol_muted = false;
|
||||
int v = ds->vol_level - 5;
|
||||
if (v < 0) v = 0;
|
||||
ds->vol_level = v;
|
||||
montauk::audio_set_volume(0, v);
|
||||
return;
|
||||
}
|
||||
bx += minus_w + gap;
|
||||
// [+]
|
||||
if (mx >= bx && mx < bx + plus_w) {
|
||||
ds->vol_muted = false;
|
||||
int v = ds->vol_level + 5;
|
||||
if (v > 100) v = 100;
|
||||
ds->vol_level = v;
|
||||
montauk::audio_set_volume(0, v);
|
||||
return;
|
||||
}
|
||||
bx += plus_w + gap;
|
||||
// [Mute]
|
||||
if (mx >= bx && mx < bx + mute_w) {
|
||||
if (ds->vol_muted) {
|
||||
ds->vol_muted = false;
|
||||
montauk::audio_set_volume(0, ds->vol_pre_mute);
|
||||
ds->vol_level = ds->vol_pre_mute;
|
||||
} else {
|
||||
ds->vol_pre_mute = ds->vol_level;
|
||||
ds->vol_muted = true;
|
||||
montauk::audio_set_volume(0, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
return; // click inside popup but not on any control
|
||||
} else if (!ds->vol_icon_rect.contains(mx, my)) {
|
||||
ds->vol_popup_open = false;
|
||||
ds->vol_dragging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle net popup clicks
|
||||
if (ds->net_popup_open && left_pressed) {
|
||||
int popup_w = 220;
|
||||
int popup_h = 130;
|
||||
int fh_net = system_font_height();
|
||||
int row_h_net = fh_net + 8;
|
||||
int popup_h = (row_h_net + 8) + row_h_net * 5 + 12;
|
||||
int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w;
|
||||
int popup_y = PANEL_HEIGHT + 2;
|
||||
if (popup_x < 4) popup_x = 4;
|
||||
@@ -266,6 +487,17 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
if (mx < 36) {
|
||||
ds->app_menu_open = !ds->app_menu_open;
|
||||
ds->net_popup_open = false;
|
||||
ds->vol_popup_open = false;
|
||||
ds->ctx_menu_open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Volume icon
|
||||
if (ds->vol_icon_rect.w > 0 && ds->vol_icon_rect.contains(mx, my)) {
|
||||
ds->vol_popup_open = !ds->vol_popup_open;
|
||||
ds->vol_dragging = false;
|
||||
ds->app_menu_open = false;
|
||||
ds->net_popup_open = false;
|
||||
ds->ctx_menu_open = false;
|
||||
return;
|
||||
}
|
||||
@@ -274,6 +506,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
if (ds->net_icon_rect.w > 0 && ds->net_icon_rect.contains(mx, my)) {
|
||||
ds->net_popup_open = !ds->net_popup_open;
|
||||
ds->app_menu_open = false;
|
||||
ds->vol_popup_open = false;
|
||||
ds->ctx_menu_open = false;
|
||||
return;
|
||||
}
|
||||
@@ -369,11 +602,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
if (win->state != WIN_MAXIMIZED) {
|
||||
ResizeEdge edge = hit_test_resize_edge(win->frame, mx, my);
|
||||
if (edge != RESIZE_NONE) {
|
||||
win->resizing = true;
|
||||
win->resize_edge = edge;
|
||||
win->resize_start_frame = win->frame;
|
||||
win->resize_start_mx = mx;
|
||||
win->resize_start_my = my;
|
||||
desktop_raise_window(ds, i);
|
||||
int new_idx = ds->window_count - 1;
|
||||
ds->windows[new_idx].resizing = true;
|
||||
@@ -388,9 +616,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
// Check titlebar (start drag)
|
||||
Rect tb = win->titlebar_rect();
|
||||
if (tb.contains(mx, my)) {
|
||||
win->dragging = true;
|
||||
win->drag_offset_x = mx - win->frame.x;
|
||||
win->drag_offset_y = my - win->frame.y;
|
||||
desktop_raise_window(ds, i);
|
||||
int new_idx = ds->window_count - 1;
|
||||
ds->windows[new_idx].dragging = true;
|
||||
@@ -433,6 +658,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
|
||||
ds->app_menu_open = false;
|
||||
ds->ctx_menu_open = false;
|
||||
ds->vol_popup_open = false;
|
||||
}
|
||||
|
||||
// Forward continuous mouse events to focused window (hover, drag, release)
|
||||
@@ -499,13 +725,23 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
ds->ctx_menu_y = my;
|
||||
ds->app_menu_open = false;
|
||||
ds->net_popup_open = false;
|
||||
ds->vol_popup_open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gui::desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key) {
|
||||
if (ds->screen_locked) {
|
||||
handle_lock_keyboard(ds, key);
|
||||
return;
|
||||
}
|
||||
|
||||
// Global shortcuts (only on key press)
|
||||
if (key.pressed && key.ctrl && key.alt) {
|
||||
if (key.ascii == 'l' || key.ascii == 'L') {
|
||||
lock_screen(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 't' || key.ascii == 'T') {
|
||||
open_terminal(ds);
|
||||
return;
|
||||
|
||||
+117
-16
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
#include "desktop_internal.hpp"
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/user.h>
|
||||
|
||||
// ============================================================================
|
||||
// App Manifest Scanning
|
||||
@@ -168,9 +170,11 @@ void desktop_build_menu(DesktopState* ds) {
|
||||
|
||||
// Divider + always-visible entries
|
||||
menu_add_category(""); // divider (cat 4, always expanded)
|
||||
menu_add_embedded("Settings", 11, &ds->icon_settings);
|
||||
menu_add_embedded("Reboot", 12, &ds->icon_reboot);
|
||||
menu_add_embedded("Shutdown", 14, &ds->icon_shutdown);
|
||||
menu_add_embedded("Settings", 11, &ds->icon_settings);
|
||||
menu_add_embedded("Lock Screen", 17, &ds->icon_lock);
|
||||
menu_add_embedded("Log Out", 16, &ds->icon_logout);
|
||||
menu_add_embedded("Reboot", 12, &ds->icon_reboot);
|
||||
menu_add_embedded("Shutdown", 14, &ds->icon_shutdown);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -224,9 +228,12 @@ void gui::desktop_init(DesktopState* ds) {
|
||||
ds->icon_settings = svg_load("0:/icons/help-about.svg", 20, 20, defColor);
|
||||
ds->icon_reboot = svg_load("0:/icons/system-reboot.svg", 20, 20, defColor);
|
||||
ds->icon_shutdown = svg_load("0:/icons/system-shutdown.svg", 20, 20, defColor);
|
||||
ds->icon_logout = svg_load("0:/icons/gnome-logout.svg", 20, 20, defColor);
|
||||
|
||||
ds->icon_procmgr = svg_load("0:/icons/system-monitor.svg", 20, 20, defColor);
|
||||
ds->icon_mandelbrot = svg_load("0:/icons/applications-science.svg", 20, 20, defColor);
|
||||
ds->icon_volume = svg_load("0:/icons/audio-volume-high-symbolic.svg", 16, 16, colors::PANEL_TEXT);
|
||||
ds->icon_lock = svg_load("0:/icons/lock.svg", 20, 20, defColor);
|
||||
|
||||
// Scan 0:/apps/ for external app manifests and build the menu
|
||||
desktop_scan_apps(ds);
|
||||
@@ -248,10 +255,61 @@ void gui::desktop_init(DesktopState* ds) {
|
||||
ds->settings.clock_24h = true;
|
||||
ds->settings.ui_scale = 1;
|
||||
|
||||
// Try to load default wallpaper
|
||||
wallpaper_load(&ds->settings, "0:/home/lucas-alexander-2dJn8XoIKCg-unsplash.jpg",
|
||||
ds->screen_w, ds->screen_h);
|
||||
montauk::win_setscale(1);
|
||||
// Load per-user desktop settings
|
||||
{
|
||||
auto doc = montauk::config::load_user(ds->current_user, "desktop");
|
||||
|
||||
const char* wp = doc.get_string("wallpaper.path", "");
|
||||
if (wp[0] != '\0') {
|
||||
wallpaper_load(&ds->settings, wp, ds->screen_w, ds->screen_h);
|
||||
} else {
|
||||
// Fall back to system-wide default wallpaper from 0:/config/desktop.toml
|
||||
auto sys = montauk::config::load("desktop");
|
||||
const char* def_wp = sys.get_string("wallpaper.path", "");
|
||||
if (def_wp[0] != '\0') {
|
||||
wallpaper_load(&ds->settings, def_wp, ds->screen_w, ds->screen_h);
|
||||
}
|
||||
sys.destroy();
|
||||
}
|
||||
|
||||
// Restore other settings from user config
|
||||
const char* bg_mode = doc.get_string("background.mode", "");
|
||||
if (montauk::streq(bg_mode, "solid")) {
|
||||
ds->settings.bg_gradient = false;
|
||||
ds->settings.bg_image = false;
|
||||
} else if (montauk::streq(bg_mode, "gradient")) {
|
||||
ds->settings.bg_gradient = true;
|
||||
ds->settings.bg_image = false;
|
||||
}
|
||||
// (image mode is set by wallpaper_load above)
|
||||
|
||||
// Background colors
|
||||
int64_t solid = doc.get_int("background.solid_color", -1);
|
||||
if (solid >= 0) ds->settings.bg_solid = Color::from_rgb(
|
||||
(uint8_t)((solid >> 16) & 0xFF), (uint8_t)((solid >> 8) & 0xFF), (uint8_t)(solid & 0xFF));
|
||||
int64_t gtop = doc.get_int("background.grad_top", -1);
|
||||
if (gtop >= 0) ds->settings.bg_grad_top = Color::from_rgb(
|
||||
(uint8_t)((gtop >> 16) & 0xFF), (uint8_t)((gtop >> 8) & 0xFF), (uint8_t)(gtop & 0xFF));
|
||||
int64_t gbot = doc.get_int("background.grad_bottom", -1);
|
||||
if (gbot >= 0) ds->settings.bg_grad_bottom = Color::from_rgb(
|
||||
(uint8_t)((gbot >> 16) & 0xFF), (uint8_t)((gbot >> 8) & 0xFF), (uint8_t)(gbot & 0xFF));
|
||||
|
||||
// Appearance colors
|
||||
int64_t panel = doc.get_int("appearance.panel_color", -1);
|
||||
if (panel >= 0) ds->settings.panel_color = Color::from_rgb(
|
||||
(uint8_t)((panel >> 16) & 0xFF), (uint8_t)((panel >> 8) & 0xFF), (uint8_t)(panel & 0xFF));
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) ds->settings.accent_color = Color::from_rgb(
|
||||
(uint8_t)((accent >> 16) & 0xFF), (uint8_t)((accent >> 8) & 0xFF), (uint8_t)(accent & 0xFF));
|
||||
|
||||
int64_t scale = doc.get_int("display.ui_scale", 1);
|
||||
ds->settings.ui_scale = (int)scale;
|
||||
ds->settings.clock_24h = doc.get_bool("display.clock_24h", true);
|
||||
ds->settings.show_shadows = doc.get_bool("display.show_shadows", true);
|
||||
|
||||
doc.destroy();
|
||||
}
|
||||
montauk::win_setscale(ds->settings.ui_scale);
|
||||
|
||||
ds->ctx_menu_open = false;
|
||||
ds->ctx_menu_x = 0;
|
||||
@@ -262,8 +320,22 @@ void gui::desktop_init(DesktopState* ds) {
|
||||
ds->net_cfg_last_poll = montauk::get_milliseconds();
|
||||
ds->net_icon_rect = {0, 0, 0, 0};
|
||||
|
||||
ds->vol_popup_open = false;
|
||||
ds->vol_icon_rect = {0, 0, 0, 0};
|
||||
int vol = montauk::audio_get_volume(0);
|
||||
ds->vol_level = vol >= 0 ? vol : 80;
|
||||
ds->vol_muted = false;
|
||||
ds->vol_pre_mute = ds->vol_level;
|
||||
ds->vol_dragging = false;
|
||||
ds->vol_last_poll = montauk::get_milliseconds();
|
||||
|
||||
ds->closing_ext_count = 0;
|
||||
|
||||
ds->screen_locked = false;
|
||||
ds->lock_password[0] = '\0';
|
||||
ds->lock_password_len = 0;
|
||||
ds->lock_error[0] = '\0';
|
||||
ds->lock_show_error = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -412,21 +484,25 @@ void gui::desktop_run(DesktopState* ds) {
|
||||
// Poll external windows (discover new, remove dead, update dirty)
|
||||
desktop_poll_external_windows(ds);
|
||||
|
||||
// Poll windows that have a poll callback
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
Window* win = &ds->windows[i];
|
||||
if (win->state == WIN_CLOSED) continue;
|
||||
if (win->on_poll) {
|
||||
win->on_poll(win);
|
||||
if (!ds->screen_locked) {
|
||||
// Poll windows that have a poll callback
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
Window* win = &ds->windows[i];
|
||||
if (win->state == WIN_CLOSED) continue;
|
||||
if (win->on_poll) {
|
||||
win->on_poll(win);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle mouse events
|
||||
desktop_handle_mouse(ds);
|
||||
|
||||
// Re-poll external windows so that any killed during mouse/key
|
||||
// handling are removed before we touch their pixel buffers.
|
||||
desktop_poll_external_windows(ds);
|
||||
if (!ds->screen_locked) {
|
||||
// Re-poll external windows so that any killed during mouse/key
|
||||
// handling are removed before we touch their pixel buffers.
|
||||
desktop_poll_external_windows(ds);
|
||||
}
|
||||
|
||||
// Compose and present
|
||||
desktop_compose(ds);
|
||||
@@ -450,6 +526,31 @@ extern "C" void _start() {
|
||||
// Placement-new the Framebuffer since it has a constructor
|
||||
new (&ds->fb) Framebuffer();
|
||||
|
||||
// Read username from spawn args
|
||||
char username[32] = {};
|
||||
montauk::getargs(username, 32);
|
||||
if (username[0] == '\0') {
|
||||
montauk::strcpy(username, "default");
|
||||
}
|
||||
montauk::strncpy(ds->current_user, username, 31);
|
||||
|
||||
// Build user paths
|
||||
montauk::user::home_dir(username, ds->home_dir, sizeof(ds->home_dir));
|
||||
montauk::user::config_dir(username, ds->user_config_dir, sizeof(ds->user_config_dir));
|
||||
|
||||
// Check if user is admin
|
||||
ds->is_admin = false;
|
||||
{
|
||||
montauk::user::UserInfo users[16];
|
||||
int count = montauk::user::load_users(users, 16);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (montauk::streq(users[i].username, username)) {
|
||||
ds->is_admin = montauk::streq(users[i].role, "admin");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_desktop = ds;
|
||||
|
||||
desktop_init(ds);
|
||||
|
||||
+216
-36
@@ -105,14 +105,42 @@ void gui::desktop_draw_panel(DesktopState* ds) {
|
||||
int date_x = clock_x - date_w - 10;
|
||||
draw_text(fb, date_x, clock_y, date_str, colors::PANEL_TEXT);
|
||||
|
||||
// Network icon (to the left of the date)
|
||||
// Volume icon (to the left of the date)
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (now - ds->vol_last_poll > 5000) {
|
||||
int v = montauk::audio_get_volume(0);
|
||||
if (v >= 0 && !ds->vol_muted) ds->vol_level = v;
|
||||
ds->vol_last_poll = now;
|
||||
}
|
||||
|
||||
int vol_icon_x = date_x - 16 - 12;
|
||||
int vol_icon_y = (PANEL_HEIGHT - 16) / 2;
|
||||
ds->vol_icon_rect = {vol_icon_x, vol_icon_y, 16, 16};
|
||||
|
||||
if (ds->icon_volume.pixels) {
|
||||
if (ds->vol_muted) {
|
||||
// Tint red-ish when muted
|
||||
uint32_t* src = ds->icon_volume.pixels;
|
||||
int npx = 16 * 16;
|
||||
uint32_t tinted[256];
|
||||
for (int p = 0; p < npx; p++) {
|
||||
uint32_t px = src[p];
|
||||
uint8_t a = (px >> 24) & 0xFF;
|
||||
tinted[p] = ((uint32_t)a << 24) | 0x00CC3333;
|
||||
}
|
||||
fb.blit_alpha(vol_icon_x, vol_icon_y, 16, 16, tinted);
|
||||
} else {
|
||||
fb.blit_alpha(vol_icon_x, vol_icon_y, ds->icon_volume.width, ds->icon_volume.height, ds->icon_volume.pixels);
|
||||
}
|
||||
}
|
||||
|
||||
// Network icon (to the left of the volume icon)
|
||||
if (now - ds->net_cfg_last_poll > 5000) {
|
||||
montauk::get_netcfg(&ds->cached_net_cfg);
|
||||
ds->net_cfg_last_poll = now;
|
||||
}
|
||||
|
||||
int net_icon_x = date_x - 16 - 12;
|
||||
int net_icon_x = vol_icon_x - 16 - 10;
|
||||
int net_icon_y = (PANEL_HEIGHT - 16) / 2;
|
||||
ds->net_icon_rect = {net_icon_x, net_icon_y, 16, 16};
|
||||
|
||||
@@ -237,51 +265,203 @@ void desktop_draw_app_menu(DesktopState* ds) {
|
||||
|
||||
void desktop_draw_net_popup(DesktopState* ds) {
|
||||
Framebuffer& fb = ds->fb;
|
||||
Montauk::NetCfg& nc = ds->cached_net_cfg;
|
||||
bool connected = nc.ipAddress != 0;
|
||||
|
||||
int popup_w = 220;
|
||||
int popup_h = 130;
|
||||
int fh = system_font_height();
|
||||
int row_h = fh + 8;
|
||||
int header_h = row_h + 8; // title + status row + padding
|
||||
int body_rows = 5; // IP, Subnet, Gateway, DNS, MAC
|
||||
int popup_h = header_h + row_h * body_rows + 12;
|
||||
int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w;
|
||||
int popup_y = PANEL_HEIGHT + 2;
|
||||
if (popup_x < 4) popup_x = 4;
|
||||
|
||||
draw_shadow(fb, popup_x, popup_y, popup_w, popup_h, 4, colors::SHADOW);
|
||||
fb.fill_rect(popup_x, popup_y, popup_w, popup_h, colors::MENU_BG);
|
||||
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 tx = popup_x + 12;
|
||||
int lx = popup_x + 14;
|
||||
int ty = popup_y + 10;
|
||||
int line_h = system_font_height() + 6;
|
||||
char line[64];
|
||||
|
||||
Montauk::NetCfg& nc = ds->cached_net_cfg;
|
||||
// Header: "Ethernet" + status dot
|
||||
draw_text(fb, lx, ty, "Ethernet", colors::TEXT_COLOR);
|
||||
|
||||
if (nc.ipAddress != 0) {
|
||||
char ipbuf[20];
|
||||
format_ip(ipbuf, nc.ipAddress);
|
||||
snprintf(line, sizeof(line), "IP: %s", ipbuf);
|
||||
} else {
|
||||
snprintf(line, sizeof(line), "IP: Not connected");
|
||||
// Status dot + label (right-aligned in header)
|
||||
Color dot_color = connected
|
||||
? Color::from_rgb(0x4C, 0xAF, 0x50) // green
|
||||
: Color::from_rgb(0xCC, 0x33, 0x33); // red
|
||||
const char* status_str = connected ? "Connected" : "Disconnected";
|
||||
int sw = text_width(status_str);
|
||||
int dot_r = 4;
|
||||
int status_x = popup_x + popup_w - 14 - sw;
|
||||
int dot_x = status_x - dot_r * 2 - 5;
|
||||
int dot_cy = ty + fh / 2;
|
||||
for (int dy = -dot_r; dy <= dot_r; dy++)
|
||||
for (int dx = -dot_r; dx <= dot_r; dx++)
|
||||
if (dx * dx + dy * dy <= dot_r * dot_r)
|
||||
fb.put_pixel(dot_x + dot_r + dx, dot_cy + dy, dot_color);
|
||||
Color dim = Color::from_rgb(0x66, 0x66, 0x66);
|
||||
draw_text(fb, status_x, ty, status_str, dim);
|
||||
|
||||
ty += row_h + 2;
|
||||
|
||||
// Separator line
|
||||
for (int sx = popup_x + 10; sx < popup_x + popup_w - 10; sx++)
|
||||
fb.put_pixel(sx, ty, colors::BORDER);
|
||||
ty += 6;
|
||||
|
||||
// Body rows: label (dim) + value (dark), two-column
|
||||
int val_x = popup_x + 76; // fixed column for values
|
||||
|
||||
struct NetRow { const char* label; char value[24]; };
|
||||
NetRow rows[5];
|
||||
|
||||
rows[0].label = "IP";
|
||||
if (connected) format_ip(rows[0].value, nc.ipAddress);
|
||||
else montauk::strcpy(rows[0].value, "\xE2\x80\x94"); // em dash
|
||||
|
||||
rows[1].label = "Subnet";
|
||||
format_ip(rows[1].value, nc.subnetMask);
|
||||
|
||||
rows[2].label = "Gateway";
|
||||
format_ip(rows[2].value, nc.gateway);
|
||||
|
||||
rows[3].label = "DNS";
|
||||
format_ip(rows[3].value, nc.dnsServer);
|
||||
|
||||
rows[4].label = "MAC";
|
||||
format_mac(rows[4].value, nc.macAddress);
|
||||
|
||||
for (int i = 0; i < body_rows; i++) {
|
||||
draw_text(fb, lx, ty, rows[i].label, dim);
|
||||
draw_text(fb, val_x, ty, rows[i].value, colors::TEXT_COLOR);
|
||||
ty += row_h;
|
||||
}
|
||||
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
|
||||
ty += line_h;
|
||||
|
||||
char buf[20];
|
||||
format_ip(buf, nc.subnetMask);
|
||||
snprintf(line, sizeof(line), "Subnet: %s", buf);
|
||||
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
|
||||
ty += line_h;
|
||||
|
||||
format_ip(buf, nc.gateway);
|
||||
snprintf(line, sizeof(line), "Gateway: %s", buf);
|
||||
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
|
||||
ty += line_h;
|
||||
|
||||
format_ip(buf, nc.dnsServer);
|
||||
snprintf(line, sizeof(line), "DNS: %s", buf);
|
||||
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
|
||||
ty += line_h;
|
||||
|
||||
format_mac(buf, nc.macAddress);
|
||||
snprintf(line, sizeof(line), "MAC: %s", buf);
|
||||
draw_text(fb, tx, ty, line, colors::TEXT_COLOR);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Volume Popup
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int VOL_POPUP_W = 200;
|
||||
static constexpr int VOL_POPUP_H = 120;
|
||||
static constexpr int VOL_SLIDER_X = 16;
|
||||
static constexpr int VOL_SLIDER_W = VOL_POPUP_W - 32;
|
||||
static constexpr int VOL_SLIDER_H = 8;
|
||||
static constexpr int VOL_KNOB_R = 8;
|
||||
|
||||
void desktop_draw_vol_popup(DesktopState* ds) {
|
||||
Framebuffer& fb = ds->fb;
|
||||
|
||||
int popup_x = ds->vol_icon_rect.x + ds->vol_icon_rect.w - VOL_POPUP_W;
|
||||
int popup_y = PANEL_HEIGHT + 2;
|
||||
if (popup_x < 4) popup_x = 4;
|
||||
|
||||
draw_shadow(fb, popup_x, popup_y, VOL_POPUP_W, VOL_POPUP_H, 4, colors::SHADOW);
|
||||
fill_rounded_rect(fb, popup_x, popup_y, VOL_POPUP_W, VOL_POPUP_H, 8, colors::MENU_BG);
|
||||
draw_rect(fb, popup_x, popup_y, VOL_POPUP_W, VOL_POPUP_H, colors::BORDER);
|
||||
|
||||
int display_vol = ds->vol_muted ? 0 : ds->vol_level;
|
||||
|
||||
// Volume percentage label
|
||||
char vol_str[8];
|
||||
snprintf(vol_str, sizeof(vol_str), "%d%%", display_vol);
|
||||
int vw = text_width(vol_str);
|
||||
Color vol_color = ds->vol_muted ? Color::from_rgb(0xCC, 0x33, 0x33) : ds->settings.accent_color;
|
||||
draw_text(fb, popup_x + (VOL_POPUP_W - vw) / 2, popup_y + 12, vol_str, vol_color);
|
||||
|
||||
// "Muted" sub-label
|
||||
if (ds->vol_muted) {
|
||||
const char* ml = "Muted";
|
||||
int mw = text_width(ml);
|
||||
draw_text(fb, popup_x + (VOL_POPUP_W - mw) / 2,
|
||||
popup_y + 12 + system_font_height() + 2, ml,
|
||||
Color::from_rgb(0xCC, 0x33, 0x33));
|
||||
}
|
||||
|
||||
// Slider track
|
||||
int slider_abs_x = popup_x + VOL_SLIDER_X;
|
||||
int slider_abs_y = popup_y + 56;
|
||||
fill_rounded_rect(fb, slider_abs_x, slider_abs_y, VOL_SLIDER_W, VOL_SLIDER_H, 4,
|
||||
Color::from_rgb(0xDD, 0xDD, 0xDD));
|
||||
|
||||
// Filled portion
|
||||
int fill_w = (display_vol * VOL_SLIDER_W) / 100;
|
||||
if (fill_w > 0)
|
||||
fill_rounded_rect(fb, slider_abs_x, slider_abs_y, fill_w, VOL_SLIDER_H, 4,
|
||||
ds->settings.accent_color);
|
||||
|
||||
// Knob
|
||||
int knob_cx = slider_abs_x + fill_w;
|
||||
int knob_cy = slider_abs_y + VOL_SLIDER_H / 2;
|
||||
// Draw filled circle for knob
|
||||
for (int dy = -VOL_KNOB_R; dy <= VOL_KNOB_R; dy++) {
|
||||
for (int dx = -VOL_KNOB_R; dx <= VOL_KNOB_R; dx++) {
|
||||
if (dx * dx + dy * dy <= VOL_KNOB_R * VOL_KNOB_R) {
|
||||
int px = knob_cx + dx;
|
||||
int py = knob_cy + dy;
|
||||
if (px >= 0 && px < ds->screen_w && py >= 0 && py < ds->screen_h)
|
||||
fb.put_pixel(px, py, ds->settings.accent_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
// White center
|
||||
int inner_r = VOL_KNOB_R - 3;
|
||||
for (int dy = -inner_r; dy <= inner_r; dy++) {
|
||||
for (int dx = -inner_r; dx <= inner_r; dx++) {
|
||||
if (dx * dx + dy * dy <= inner_r * inner_r) {
|
||||
int px = knob_cx + dx;
|
||||
int py = knob_cy + dy;
|
||||
if (px >= 0 && px < ds->screen_w && py >= 0 && py < ds->screen_h)
|
||||
fb.put_pixel(px, py, Color::from_rgb(0xFF, 0xFF, 0xFF));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons: [-] [+] [Mute]
|
||||
int btn_h = 24;
|
||||
int btn_y = popup_y + 78;
|
||||
int btn_rad = 6;
|
||||
|
||||
int minus_w = 36;
|
||||
int plus_w = 36;
|
||||
int mute_w = 50;
|
||||
int gap = 8;
|
||||
int total_w = minus_w + plus_w + mute_w + gap * 2;
|
||||
int bx = popup_x + (VOL_POPUP_W - total_w) / 2;
|
||||
|
||||
int mmx = ds->mouse.x;
|
||||
int mmy = ds->mouse.y;
|
||||
|
||||
// [-] button
|
||||
Color minus_bg = Color::from_rgb(0xE0, 0xE0, 0xE0);
|
||||
Rect minus_r = {bx, btn_y, minus_w, btn_h};
|
||||
if (minus_r.contains(mmx, mmy)) minus_bg = Color::from_rgb(0xD0, 0xD0, 0xD0);
|
||||
fill_rounded_rect(fb, bx, btn_y, minus_w, btn_h, btn_rad, minus_bg);
|
||||
int tw = text_width("-");
|
||||
draw_text(fb, bx + (minus_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "-", colors::TEXT_COLOR);
|
||||
bx += minus_w + gap;
|
||||
|
||||
// [+] button
|
||||
Color plus_bg = Color::from_rgb(0xE0, 0xE0, 0xE0);
|
||||
Rect plus_r = {bx, btn_y, plus_w, btn_h};
|
||||
if (plus_r.contains(mmx, mmy)) plus_bg = Color::from_rgb(0xD0, 0xD0, 0xD0);
|
||||
fill_rounded_rect(fb, bx, btn_y, plus_w, btn_h, btn_rad, plus_bg);
|
||||
tw = text_width("+");
|
||||
draw_text(fb, bx + (plus_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "+", colors::TEXT_COLOR);
|
||||
bx += plus_w + gap;
|
||||
|
||||
// [Mute] button
|
||||
Color mute_bg = ds->vol_muted ? Color::from_rgb(0xCC, 0x33, 0x33) : Color::from_rgb(0xE0, 0xE0, 0xE0);
|
||||
Color mute_fg = ds->vol_muted ? Color::from_rgb(0xFF, 0xFF, 0xFF) : colors::TEXT_COLOR;
|
||||
Rect mute_r = {bx, btn_y, mute_w, btn_h};
|
||||
if (mute_r.contains(mmx, mmy)) {
|
||||
if (ds->vol_muted) mute_bg = Color::from_rgb(0xAA, 0x22, 0x22);
|
||||
else mute_bg = Color::from_rgb(0xD0, 0xD0, 0xD0);
|
||||
}
|
||||
fill_rounded_rect(fb, bx, btn_y, mute_w, btn_h, btn_rad, mute_bg);
|
||||
tw = text_width("Mute");
|
||||
draw_text(fb, bx + (mute_w - tw) / 2, btn_y + (btn_h - system_font_height()) / 2, "Mute", mute_fg);
|
||||
}
|
||||
|
||||
@@ -267,7 +267,26 @@ static int generate_index_page(char* buf, int bufSize) {
|
||||
(unsigned)hours, (unsigned)mins, (unsigned)secs);
|
||||
}
|
||||
|
||||
// HTML-escape a string to prevent XSS (escapes <, >, &, ", ')
|
||||
static int html_escape(const char* in, char* out, int outMax) {
|
||||
int j = 0;
|
||||
for (int i = 0; in[i] && j < outMax - 6; i++) {
|
||||
switch (in[i]) {
|
||||
case '<': out[j++]='&'; out[j++]='l'; out[j++]='t'; out[j++]=';'; break;
|
||||
case '>': out[j++]='&'; out[j++]='g'; out[j++]='t'; out[j++]=';'; break;
|
||||
case '&': out[j++]='&'; out[j++]='a'; out[j++]='m'; out[j++]='p'; out[j++]=';'; break;
|
||||
case '"': out[j++]='&'; out[j++]='q'; out[j++]='u'; out[j++]='o'; out[j++]='t'; out[j++]=';'; break;
|
||||
case '\'': out[j++]='&'; out[j++]='#'; out[j++]='3'; out[j++]='9'; out[j++]=';'; break;
|
||||
default: out[j++] = in[i]; break;
|
||||
}
|
||||
}
|
||||
out[j] = '\0';
|
||||
return j;
|
||||
}
|
||||
|
||||
static int generate_404_page(char* buf, int bufSize, const char* path) {
|
||||
char escaped[512];
|
||||
html_escape(path, escaped, sizeof(escaped));
|
||||
return snprintf(buf, bufSize,
|
||||
"<!DOCTYPE html>\n"
|
||||
"<html>\n"
|
||||
@@ -278,7 +297,7 @@ static int generate_404_page(char* buf, int bufSize, const char* path) {
|
||||
"<p><a href=\"/\">Back to home</a></p>\n"
|
||||
"</body>\n"
|
||||
"</html>\n",
|
||||
path);
|
||||
escaped);
|
||||
}
|
||||
|
||||
static int generate_dir_listing(char* buf, int bufSize, const char* urlPath, const char* vfsDir) {
|
||||
@@ -322,10 +341,12 @@ static int generate_dir_listing(char* buf, int bufSize, const char* urlPath, con
|
||||
}
|
||||
if (*name == '\0') continue;
|
||||
|
||||
// Build the URL for this entry
|
||||
// Build the URL for this entry (HTML-escape the name)
|
||||
char esc_name[256];
|
||||
html_escape(name, esc_name, sizeof(esc_name));
|
||||
pos += snprintf(buf + pos, bufSize - pos,
|
||||
"<li><a href=\"%s%s\">%s</a></li>\n",
|
||||
urlPath, name, name);
|
||||
urlPath, name, esc_name);
|
||||
}
|
||||
|
||||
pos += snprintf(buf + pos, bufSize - pos,
|
||||
@@ -415,6 +436,29 @@ static void handle_client(int clientFd) {
|
||||
// Serve file or directory from VFS
|
||||
const char* relPath = path + 7; // skip "/files/"
|
||||
|
||||
// Reject path traversal attempts
|
||||
if (starts_with(relPath, "..") || starts_with(relPath, "/")) {
|
||||
static char body[] = "<!DOCTYPE html><html><body><h1>403 Forbidden</h1></body></html>";
|
||||
send_response(clientFd, 403, "Forbidden", "text/html", body, slen(body));
|
||||
log_request("GET", path, 403, slen(body));
|
||||
montauk::closesocket(clientFd);
|
||||
return;
|
||||
}
|
||||
// Check for /../ or trailing /.. anywhere in the path
|
||||
for (int ci = 0; relPath[ci]; ci++) {
|
||||
if (relPath[ci] == '.' && relPath[ci+1] == '.') {
|
||||
if (ci == 0 || relPath[ci-1] == '/') {
|
||||
if (relPath[ci+2] == '\0' || relPath[ci+2] == '/') {
|
||||
static char body[] = "<!DOCTYPE html><html><body><h1>403 Forbidden</h1></body></html>";
|
||||
send_response(clientFd, 403, "Forbidden", "text/html", body, slen(body));
|
||||
log_request("GET", path, 403, slen(body));
|
||||
montauk::closesocket(clientFd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build VFS path: "0:/<relPath>"
|
||||
char vfsPath[256];
|
||||
int pi = 0;
|
||||
|
||||
@@ -157,10 +157,13 @@ extern "C" void _start() {
|
||||
// ---- Stage 1: Network configuration (non-blocking) ----
|
||||
run_service("0:/os/dhcp.elf", "dhcp", false);
|
||||
|
||||
// ---- Stage 2: Desktop environment (falls back to shell) ----
|
||||
if (!run_service("0:/os/desktop.elf", "desktop")) {
|
||||
log_warn("Desktop failed, falling back to shell");
|
||||
run_service("0:/os/shell.elf", "shell");
|
||||
// ---- Stage 2: Login screen -> desktop (falls back to desktop, then shell) ----
|
||||
if (!run_service("0:/os/login.elf", "login")) {
|
||||
log_warn("Login failed, falling back to desktop");
|
||||
if (!run_service("0:/os/desktop.elf", "desktop")) {
|
||||
log_warn("Desktop failed, falling back to shell");
|
||||
run_service("0:/os/shell.elf", "shell");
|
||||
}
|
||||
}
|
||||
|
||||
log_warn("All services exited");
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Makefile for login (graphical login screen) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
# ---- Paths ----
|
||||
|
||||
PROG_INC := ../../include
|
||||
LIBC_LIB := ../../lib/libc
|
||||
JPEG_LIB := ../../lib/libjpeg
|
||||
BEARSSL := ../../lib/bearssl
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
|
||||
# ---- Compiler flags ----
|
||||
|
||||
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 \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-I $(BEARSSL)/inc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
# ---- Linker flags ----
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
# ---- Source files ----
|
||||
|
||||
SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
DEPS := $(OBJS:.o=.d)
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/os/login.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
LIBS := $(JPEG_LIB)/libjpeg.a $(BEARSSL)/libbearssl.a $(LIBC_LIB)/liblibc.a
|
||||
|
||||
$(TARGET): $(OBJS) $(LIBS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/os
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
-include $(DEPS)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,783 @@
|
||||
/*
|
||||
* font_data.cpp
|
||||
* Standard VGA 8x16 bitmap font (Code Page 437)
|
||||
* 256 characters, 16 bytes each (8 pixels wide, 1 bit per pixel, MSB left)
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "gui/font.hpp"
|
||||
|
||||
namespace gui {
|
||||
|
||||
const uint8_t font_data[256 * 16] = {
|
||||
// Character 0 (NUL)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 1 (smiley face)
|
||||
0x00, 0x00, 0x7E, 0x81, 0xA5, 0x81, 0x81, 0xBD,
|
||||
0x99, 0x81, 0x81, 0x7E, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 2 (inverse smiley)
|
||||
0x00, 0x00, 0x7E, 0xFF, 0xDB, 0xFF, 0xFF, 0xC3,
|
||||
0xE7, 0xFF, 0xFF, 0x7E, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 3 (heart)
|
||||
0x00, 0x00, 0x00, 0x00, 0x6C, 0xFE, 0xFE, 0xFE,
|
||||
0xFE, 0x7C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 4 (diamond)
|
||||
0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x7C, 0xFE,
|
||||
0x7C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 5 (club)
|
||||
0x00, 0x00, 0x00, 0x18, 0x3C, 0x3C, 0xE7, 0xE7,
|
||||
0xE7, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 6 (spade)
|
||||
0x00, 0x00, 0x00, 0x18, 0x3C, 0x7E, 0xFF, 0xFF,
|
||||
0x7E, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 7 (bullet)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x3C,
|
||||
0x3C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 8 (inverse bullet)
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xC3,
|
||||
0xC3, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
// Character 9 (circle)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x66, 0x42,
|
||||
0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 10 (inverse circle)
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0x99, 0xBD,
|
||||
0xBD, 0x99, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
// Character 11 (male sign)
|
||||
0x00, 0x00, 0x1E, 0x0E, 0x1A, 0x32, 0x78, 0xCC,
|
||||
0xCC, 0xCC, 0xCC, 0x78, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 12 (female sign)
|
||||
0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x3C,
|
||||
0x18, 0x7E, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 13 (note)
|
||||
0x00, 0x00, 0x3F, 0x33, 0x3F, 0x30, 0x30, 0x30,
|
||||
0x30, 0x70, 0xF0, 0xE0, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 14 (double note)
|
||||
0x00, 0x00, 0x7F, 0x63, 0x7F, 0x63, 0x63, 0x63,
|
||||
0x63, 0x67, 0xE7, 0xE6, 0xC0, 0x00, 0x00, 0x00,
|
||||
// Character 15 (sun)
|
||||
0x00, 0x00, 0x00, 0x18, 0x18, 0xDB, 0x3C, 0xE7,
|
||||
0x3C, 0xDB, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 16 (right triangle)
|
||||
0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFE, 0xF8,
|
||||
0xF0, 0xE0, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 17 (left triangle)
|
||||
0x00, 0x02, 0x06, 0x0E, 0x1E, 0x3E, 0xFE, 0x3E,
|
||||
0x1E, 0x0E, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 18 (up-down arrow)
|
||||
0x00, 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x18,
|
||||
0x7E, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 19 (double exclamation)
|
||||
0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x00, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 20 (paragraph)
|
||||
0x00, 0x00, 0x7F, 0xDB, 0xDB, 0xDB, 0x7B, 0x1B,
|
||||
0x1B, 0x1B, 0x1B, 0x1B, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 21 (section)
|
||||
0x00, 0x7C, 0xC6, 0x60, 0x38, 0x6C, 0xC6, 0xC6,
|
||||
0x6C, 0x38, 0x0C, 0xC6, 0x7C, 0x00, 0x00, 0x00,
|
||||
// Character 22 (thick underscore)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xFE, 0xFE, 0xFE, 0xFE, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 23 (up-down arrow underlined)
|
||||
0x00, 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x18,
|
||||
0x7E, 0x3C, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 24 (up arrow)
|
||||
0x00, 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 25 (down arrow)
|
||||
0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
0x18, 0x7E, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 26 (right arrow)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0C, 0xFE,
|
||||
0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 27 (left arrow)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x60, 0xFE,
|
||||
0x60, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 28 (right angle)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0,
|
||||
0xC0, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 29 (left-right arrow)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x66, 0xFF,
|
||||
0x66, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 30 (up triangle)
|
||||
0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x38, 0x7C,
|
||||
0x7C, 0xFE, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 31 (down triangle)
|
||||
0x00, 0x00, 0x00, 0x00, 0xFE, 0xFE, 0x7C, 0x7C,
|
||||
0x38, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 32 (space)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 33 '!'
|
||||
0x00, 0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x18,
|
||||
0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 34 '"'
|
||||
0x00, 0x66, 0x66, 0x66, 0x24, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 35 '#'
|
||||
0x00, 0x00, 0x00, 0x6C, 0x6C, 0xFE, 0x6C, 0x6C,
|
||||
0x6C, 0xFE, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 36 '$'
|
||||
0x18, 0x18, 0x7C, 0xC6, 0xC2, 0xC0, 0x7C, 0x06,
|
||||
0x06, 0x86, 0xC6, 0x7C, 0x18, 0x18, 0x00, 0x00,
|
||||
// Character 37 '%'
|
||||
0x00, 0x00, 0x00, 0x00, 0xC2, 0xC6, 0x0C, 0x18,
|
||||
0x30, 0x60, 0xC6, 0x86, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 38 '&'
|
||||
0x00, 0x00, 0x38, 0x6C, 0x6C, 0x38, 0x76, 0xDC,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 39 '''
|
||||
0x00, 0x30, 0x30, 0x30, 0x60, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 40 '('
|
||||
0x00, 0x00, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x30,
|
||||
0x30, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 41 ')'
|
||||
0x00, 0x00, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x0C,
|
||||
0x0C, 0x0C, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 42 '*'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x3C, 0xFF,
|
||||
0x3C, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 43 '+'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7E,
|
||||
0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 44 ','
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x18, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00,
|
||||
// Character 45 '-'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 46 '.'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 47 '/'
|
||||
0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x0C, 0x18,
|
||||
0x30, 0x60, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 48 '0'
|
||||
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xCE, 0xDE, 0xF6,
|
||||
0xE6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 49 '1'
|
||||
0x00, 0x00, 0x18, 0x38, 0x78, 0x18, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 50 '2'
|
||||
0x00, 0x00, 0x7C, 0xC6, 0x06, 0x0C, 0x18, 0x30,
|
||||
0x60, 0xC0, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 51 '3'
|
||||
0x00, 0x00, 0x7C, 0xC6, 0x06, 0x06, 0x3C, 0x06,
|
||||
0x06, 0x06, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 52 '4'
|
||||
0x00, 0x00, 0x0C, 0x1C, 0x3C, 0x6C, 0xCC, 0xFE,
|
||||
0x0C, 0x0C, 0x0C, 0x1E, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 53 '5'
|
||||
0x00, 0x00, 0xFE, 0xC0, 0xC0, 0xC0, 0xFC, 0x06,
|
||||
0x06, 0x06, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 54 '6'
|
||||
0x00, 0x00, 0x38, 0x60, 0xC0, 0xC0, 0xFC, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 55 '7'
|
||||
0x00, 0x00, 0xFE, 0xC6, 0x06, 0x06, 0x0C, 0x18,
|
||||
0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 56 '8'
|
||||
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7C, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 57 '9'
|
||||
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7E, 0x06,
|
||||
0x06, 0x06, 0x0C, 0x78, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 58 ':'
|
||||
0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00,
|
||||
0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 59 ';'
|
||||
0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00,
|
||||
0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 60 '<'
|
||||
0x00, 0x00, 0x00, 0x06, 0x0C, 0x18, 0x30, 0x60,
|
||||
0x30, 0x18, 0x0C, 0x06, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 61 '='
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00,
|
||||
0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 62 '>'
|
||||
0x00, 0x00, 0x00, 0x60, 0x30, 0x18, 0x0C, 0x06,
|
||||
0x0C, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 63 '?'
|
||||
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0x0C, 0x18, 0x18,
|
||||
0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 64 '@'
|
||||
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xDE, 0xDE,
|
||||
0xDE, 0xDC, 0xC0, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 65 'A'
|
||||
0x00, 0x00, 0x10, 0x38, 0x6C, 0xC6, 0xC6, 0xFE,
|
||||
0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 66 'B'
|
||||
0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x66,
|
||||
0x66, 0x66, 0x66, 0xFC, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 67 'C'
|
||||
0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xC0,
|
||||
0xC0, 0xC2, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 68 'D'
|
||||
0x00, 0x00, 0xF8, 0x6C, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x66, 0x6C, 0xF8, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 69 'E'
|
||||
0x00, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68,
|
||||
0x60, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 70 'F'
|
||||
0x00, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68,
|
||||
0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 71 'G'
|
||||
0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xDE,
|
||||
0xC6, 0xC6, 0x66, 0x3A, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 72 'H'
|
||||
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xFE, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 73 'I'
|
||||
0x00, 0x00, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 74 'J'
|
||||
0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
|
||||
0xCC, 0xCC, 0xCC, 0x78, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 75 'K'
|
||||
0x00, 0x00, 0xE6, 0x66, 0x66, 0x6C, 0x78, 0x78,
|
||||
0x6C, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 76 'L'
|
||||
0x00, 0x00, 0xF0, 0x60, 0x60, 0x60, 0x60, 0x60,
|
||||
0x60, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 77 'M'
|
||||
0x00, 0x00, 0xC6, 0xEE, 0xFE, 0xFE, 0xD6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 78 'N'
|
||||
0x00, 0x00, 0xC6, 0xE6, 0xF6, 0xFE, 0xDE, 0xCE,
|
||||
0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 79 'O'
|
||||
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 80 'P'
|
||||
0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x60,
|
||||
0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 81 'Q'
|
||||
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6,
|
||||
0xC6, 0xD6, 0xDE, 0x7C, 0x0C, 0x0E, 0x00, 0x00,
|
||||
// Character 82 'R'
|
||||
0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x6C,
|
||||
0x66, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 83 'S'
|
||||
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0x60, 0x38, 0x0C,
|
||||
0x06, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 84 'T'
|
||||
0x00, 0x00, 0xFF, 0xDB, 0x99, 0x18, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 85 'U'
|
||||
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 86 'V'
|
||||
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6,
|
||||
0xC6, 0x6C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 87 'W'
|
||||
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xD6, 0xD6,
|
||||
0xD6, 0xFE, 0xEE, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 88 'X'
|
||||
0x00, 0x00, 0xC6, 0xC6, 0x6C, 0x7C, 0x38, 0x38,
|
||||
0x7C, 0x6C, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 89 'Y'
|
||||
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x18,
|
||||
0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 90 'Z'
|
||||
0x00, 0x00, 0xFE, 0xC6, 0x86, 0x0C, 0x18, 0x30,
|
||||
0x60, 0xC2, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 91 '['
|
||||
0x00, 0x00, 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30,
|
||||
0x30, 0x30, 0x30, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 92 '\'
|
||||
0x00, 0x00, 0x00, 0x80, 0xC0, 0xE0, 0x70, 0x38,
|
||||
0x1C, 0x0E, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 93 ']'
|
||||
0x00, 0x00, 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
|
||||
0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 94 '^'
|
||||
0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 95 '_'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
|
||||
// Character 96 '`'
|
||||
0x30, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 97 'a'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x0C, 0x7C,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 98 'b'
|
||||
0x00, 0x00, 0xE0, 0x60, 0x60, 0x78, 0x6C, 0x66,
|
||||
0x66, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 99 'c'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC0,
|
||||
0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 100 'd'
|
||||
0x00, 0x00, 0x1C, 0x0C, 0x0C, 0x3C, 0x6C, 0xCC,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 101 'e'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xFE,
|
||||
0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 102 'f'
|
||||
0x00, 0x00, 0x38, 0x6C, 0x64, 0x60, 0xF0, 0x60,
|
||||
0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 103 'g'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xCC, 0xCC,
|
||||
0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0xCC, 0x78, 0x00,
|
||||
// Character 104 'h'
|
||||
0x00, 0x00, 0xE0, 0x60, 0x60, 0x6C, 0x76, 0x66,
|
||||
0x66, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 105 'i'
|
||||
0x00, 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 106 'j'
|
||||
0x00, 0x00, 0x06, 0x06, 0x00, 0x0E, 0x06, 0x06,
|
||||
0x06, 0x06, 0x06, 0x06, 0x66, 0x66, 0x3C, 0x00,
|
||||
// Character 107 'k'
|
||||
0x00, 0x00, 0xE0, 0x60, 0x60, 0x66, 0x6C, 0x78,
|
||||
0x78, 0x6C, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 108 'l'
|
||||
0x00, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 109 'm'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xEC, 0xFE, 0xD6,
|
||||
0xD6, 0xD6, 0xD6, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 110 'n'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x66, 0x66,
|
||||
0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 111 'o'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 112 'p'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x66, 0x66,
|
||||
0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xF0, 0x00,
|
||||
// Character 113 'q'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xCC, 0xCC,
|
||||
0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0x0C, 0x1E, 0x00,
|
||||
// Character 114 'r'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x76, 0x66,
|
||||
0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 115 's'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0x60,
|
||||
0x38, 0x0C, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 116 't'
|
||||
0x00, 0x00, 0x10, 0x30, 0x30, 0xFC, 0x30, 0x30,
|
||||
0x30, 0x30, 0x36, 0x1C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 117 'u'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xCC, 0xCC, 0xCC,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 118 'v'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6,
|
||||
0xC6, 0x6C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 119 'w'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xD6,
|
||||
0xD6, 0xD6, 0xFE, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 120 'x'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0x6C, 0x38,
|
||||
0x38, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 121 'y'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x0C, 0xF8, 0x00,
|
||||
// Character 122 'z'
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xCC, 0x18,
|
||||
0x30, 0x60, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 123 '{'
|
||||
0x00, 0x00, 0x0E, 0x18, 0x18, 0x18, 0x70, 0x18,
|
||||
0x18, 0x18, 0x18, 0x0E, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 124 '|'
|
||||
0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18,
|
||||
0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 125 '}'
|
||||
0x00, 0x00, 0x70, 0x18, 0x18, 0x18, 0x0E, 0x18,
|
||||
0x18, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 126 '~'
|
||||
0x00, 0x00, 0x76, 0xDC, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 127 (DEL - block)
|
||||
0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x6C, 0xC6,
|
||||
0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 128 (C cedilla)
|
||||
0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xC0,
|
||||
0xC2, 0x66, 0x3C, 0x0C, 0x06, 0x7C, 0x00, 0x00,
|
||||
// Character 129 (u umlaut)
|
||||
0x00, 0x00, 0xCC, 0x00, 0x00, 0xCC, 0xCC, 0xCC,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 130 (e acute)
|
||||
0x00, 0x0C, 0x18, 0x30, 0x00, 0x7C, 0xC6, 0xFE,
|
||||
0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 131 (a circumflex)
|
||||
0x00, 0x10, 0x38, 0x6C, 0x00, 0x78, 0x0C, 0x7C,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 132 (a umlaut)
|
||||
0x00, 0x00, 0xCC, 0x00, 0x00, 0x78, 0x0C, 0x7C,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 133 (a grave)
|
||||
0x00, 0x60, 0x30, 0x18, 0x00, 0x78, 0x0C, 0x7C,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 134 (a ring)
|
||||
0x00, 0x38, 0x6C, 0x38, 0x00, 0x78, 0x0C, 0x7C,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 135 (c cedilla)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC0,
|
||||
0xC0, 0xC6, 0x7C, 0x0C, 0x06, 0x7C, 0x00, 0x00,
|
||||
// Character 136 (e circumflex)
|
||||
0x00, 0x10, 0x38, 0x6C, 0x00, 0x7C, 0xC6, 0xFE,
|
||||
0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 137 (e umlaut)
|
||||
0x00, 0x00, 0xC6, 0x00, 0x00, 0x7C, 0xC6, 0xFE,
|
||||
0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 138 (e grave)
|
||||
0x00, 0x60, 0x30, 0x18, 0x00, 0x7C, 0xC6, 0xFE,
|
||||
0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 139 (i umlaut)
|
||||
0x00, 0x00, 0x66, 0x00, 0x00, 0x38, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 140 (i circumflex)
|
||||
0x00, 0x18, 0x3C, 0x66, 0x00, 0x38, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 141 (i grave)
|
||||
0x00, 0x60, 0x30, 0x18, 0x00, 0x38, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 142 (A umlaut)
|
||||
0x00, 0xC6, 0x00, 0x10, 0x38, 0x6C, 0xC6, 0xC6,
|
||||
0xFE, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 143 (A ring)
|
||||
0x38, 0x6C, 0x38, 0x10, 0x38, 0x6C, 0xC6, 0xC6,
|
||||
0xFE, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 144 (E acute)
|
||||
0x0C, 0x18, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78,
|
||||
0x68, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 145 (ae)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x3B, 0x1B,
|
||||
0x7E, 0xD8, 0xDC, 0x77, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 146 (AE)
|
||||
0x00, 0x00, 0x3E, 0x6C, 0xCC, 0xCC, 0xFE, 0xCC,
|
||||
0xCC, 0xCC, 0xCC, 0xCE, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 147 (o circumflex)
|
||||
0x00, 0x10, 0x38, 0x6C, 0x00, 0x7C, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 148 (o umlaut)
|
||||
0x00, 0x00, 0xC6, 0x00, 0x00, 0x7C, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 149 (o grave)
|
||||
0x00, 0x60, 0x30, 0x18, 0x00, 0x7C, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 150 (u circumflex)
|
||||
0x00, 0x30, 0x78, 0xCC, 0x00, 0xCC, 0xCC, 0xCC,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 151 (u grave)
|
||||
0x00, 0x60, 0x30, 0x18, 0x00, 0xCC, 0xCC, 0xCC,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 152 (y umlaut)
|
||||
0x00, 0x00, 0xC6, 0x00, 0x00, 0xC6, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x0C, 0x78, 0x00,
|
||||
// Character 153 (O umlaut)
|
||||
0x00, 0xC6, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 154 (U umlaut)
|
||||
0x00, 0xC6, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 155 (cent)
|
||||
0x00, 0x18, 0x18, 0x7C, 0xC6, 0xC0, 0xC0, 0xC0,
|
||||
0xC6, 0x7C, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 156 (pound)
|
||||
0x00, 0x38, 0x6C, 0x64, 0x60, 0xF0, 0x60, 0x60,
|
||||
0x60, 0x60, 0xE6, 0xFC, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 157 (yen)
|
||||
0x00, 0x00, 0xC6, 0x6C, 0x38, 0x38, 0xFE, 0x38,
|
||||
0xFE, 0x38, 0x38, 0x38, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 158 (Pt)
|
||||
0x00, 0xF8, 0xCC, 0xCC, 0xF8, 0xC4, 0xCC, 0xDE,
|
||||
0xCC, 0xCC, 0xCC, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 159 (f hook)
|
||||
0x00, 0x0E, 0x1B, 0x18, 0x18, 0x18, 0x7E, 0x18,
|
||||
0x18, 0x18, 0xD8, 0x70, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 160 (a acute)
|
||||
0x00, 0x18, 0x30, 0x60, 0x00, 0x78, 0x0C, 0x7C,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 161 (i acute)
|
||||
0x00, 0x0C, 0x18, 0x30, 0x00, 0x38, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 162 (o acute)
|
||||
0x00, 0x18, 0x30, 0x60, 0x00, 0x7C, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 163 (u acute)
|
||||
0x00, 0x18, 0x30, 0x60, 0x00, 0xCC, 0xCC, 0xCC,
|
||||
0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 164 (n tilde)
|
||||
0x00, 0x00, 0x76, 0xDC, 0x00, 0xDC, 0x66, 0x66,
|
||||
0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 165 (N tilde)
|
||||
0x76, 0xDC, 0x00, 0xC6, 0xE6, 0xF6, 0xFE, 0xDE,
|
||||
0xCE, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 166 (feminine ordinal)
|
||||
0x00, 0x3C, 0x6C, 0x6C, 0x3E, 0x00, 0x7E, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 167 (masculine ordinal)
|
||||
0x00, 0x38, 0x6C, 0x6C, 0x38, 0x00, 0x7C, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 168 (inverted question)
|
||||
0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x60,
|
||||
0xC0, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 169 (not left)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xC0,
|
||||
0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 170 (not right)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x06,
|
||||
0x06, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 171 (fraction 1/2)
|
||||
0x00, 0xC0, 0xC0, 0xC2, 0xC6, 0xCC, 0x18, 0x30,
|
||||
0x60, 0xDC, 0x86, 0x0C, 0x18, 0x3E, 0x00, 0x00,
|
||||
// Character 172 (fraction 1/4)
|
||||
0x00, 0xC0, 0xC0, 0xC2, 0xC6, 0xCC, 0x18, 0x30,
|
||||
0x66, 0xCE, 0x9E, 0x3E, 0x06, 0x06, 0x00, 0x00,
|
||||
// Character 173 (inverted exclamation)
|
||||
0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18,
|
||||
0x3C, 0x3C, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 174 (double left angle)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x6C, 0xD8,
|
||||
0x6C, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 175 (double right angle)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xD8, 0x6C, 0x36,
|
||||
0x6C, 0xD8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 176 (light shade)
|
||||
0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44,
|
||||
0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44,
|
||||
// Character 177 (medium shade)
|
||||
0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA,
|
||||
0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA,
|
||||
// Character 178 (dark shade)
|
||||
0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77,
|
||||
0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77,
|
||||
// Character 179 (box vertical)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 180 (box vertical-left)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xF8,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 181 (box double vertical-left single)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0xF8, 0x18, 0xF8,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 182 (box double vertical-left)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xF6,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 183 (box double horizontal-down)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 184 (box double vertical-left single)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x18, 0xF8,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 185 (box double vertical-left double)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0xF6, 0x06, 0xF6,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 186 (box double vertical)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 187 (box double down-left)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x06, 0xF6,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 188 (box double up-left)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0xF6, 0x06, 0xFE,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 189 (box double up-right)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xFE,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 190 (box horizontal-down)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0xF8, 0x18, 0xF8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 191 (box down-left)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 192 (box up-right)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1F,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 193 (box horizontal-up)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 194 (box horizontal-down)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 195 (box vertical-right)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1F,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 196 (box horizontal)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 197 (box cross)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 198 (box single vert-right double)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x1F, 0x18, 0x1F,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 199 (box double vert-right)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x37,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 200 (box double up-right)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x37, 0x30, 0x3F,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 201 (box double down-right)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x30, 0x37,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 202 (box double horizontal-up)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0xF7, 0x00, 0xFF,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 203 (box double horizontal-down)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xF7,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 204 (box double vertical-right)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x37, 0x30, 0x37,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 205 (box double horizontal)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 206 (box double cross)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0xF7, 0x00, 0xF7,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 207 (box single horiz-up double)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x00, 0xFF,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 208 (box double horizontal-up single)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xFF,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 209 (box single horiz-down double)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 210 (box double horizontal-down single)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 211 (box double up-right single)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x3F,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 212 (box single up-right double)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x1F, 0x18, 0x1F,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 213 (box single down-right double)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x18, 0x1F,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 214 (box double down-right single)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 215 (box double cross single)
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xFF,
|
||||
0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36,
|
||||
// Character 216 (box single cross double)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0xFF,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 217 (box up-left)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xF8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 218 (box down-right)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 219 (full block)
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
// Character 220 (bottom half block)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
// Character 221 (left half block)
|
||||
0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
|
||||
0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
|
||||
// Character 222 (right half block)
|
||||
0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F,
|
||||
0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F,
|
||||
// Character 223 (top half block)
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 224 (alpha)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xDC, 0xD8,
|
||||
0xD8, 0xD8, 0xDC, 0x76, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 225 (beta/sharp s)
|
||||
0x00, 0x00, 0x78, 0xCC, 0xCC, 0xCC, 0xD8, 0xCC,
|
||||
0xC6, 0xC6, 0xC6, 0xCC, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 226 (gamma)
|
||||
0x00, 0x00, 0xFE, 0xC6, 0xC6, 0xC0, 0xC0, 0xC0,
|
||||
0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 227 (pi)
|
||||
0x00, 0x00, 0x00, 0x00, 0xFE, 0x6C, 0x6C, 0x6C,
|
||||
0x6C, 0x6C, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 228 (sigma uppercase)
|
||||
0x00, 0x00, 0x00, 0xFE, 0xC6, 0x60, 0x30, 0x18,
|
||||
0x30, 0x60, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 229 (sigma lowercase)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0xD8, 0xD8,
|
||||
0xD8, 0xD8, 0xD8, 0x70, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 230 (mu)
|
||||
0x00, 0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x7C, 0x60, 0x60, 0xC0, 0x00, 0x00, 0x00,
|
||||
// Character 231 (tau)
|
||||
0x00, 0x00, 0x00, 0x00, 0x76, 0xDC, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 232 (phi uppercase)
|
||||
0x00, 0x00, 0x00, 0x7E, 0x18, 0x3C, 0x66, 0x66,
|
||||
0x66, 0x3C, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 233 (theta)
|
||||
0x00, 0x00, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xFE,
|
||||
0xC6, 0xC6, 0x6C, 0x38, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 234 (omega)
|
||||
0x00, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xC6, 0xC6,
|
||||
0x6C, 0x6C, 0x6C, 0xEE, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 235 (delta)
|
||||
0x00, 0x00, 0x1E, 0x30, 0x18, 0x0C, 0x3E, 0x66,
|
||||
0x66, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 236 (infinity)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xDB, 0xDB,
|
||||
0xDB, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 237 (phi lowercase)
|
||||
0x00, 0x00, 0x00, 0x02, 0x06, 0x7C, 0xCE, 0xDE,
|
||||
0xF6, 0xE6, 0x7C, 0xC0, 0x80, 0x00, 0x00, 0x00,
|
||||
// Character 238 (epsilon)
|
||||
0x00, 0x00, 0x1C, 0x30, 0x60, 0x60, 0x7C, 0x60,
|
||||
0x60, 0x60, 0x30, 0x1C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 239 (intersection)
|
||||
0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6,
|
||||
0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 240 (triple bar)
|
||||
0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xFE,
|
||||
0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 241 (plus-minus)
|
||||
0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7E, 0x18,
|
||||
0x18, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 242 (greater-equal)
|
||||
0x00, 0x00, 0x00, 0x30, 0x18, 0x0C, 0x06, 0x0C,
|
||||
0x18, 0x30, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 243 (less-equal)
|
||||
0x00, 0x00, 0x00, 0x0C, 0x18, 0x30, 0x60, 0x30,
|
||||
0x18, 0x0C, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 244 (top integral)
|
||||
0x00, 0x00, 0x0E, 0x1B, 0x1B, 0x18, 0x18, 0x18,
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
// Character 245 (bottom integral)
|
||||
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
|
||||
0x18, 0xD8, 0xD8, 0xD8, 0x70, 0x00, 0x00, 0x00,
|
||||
// Character 246 (division)
|
||||
0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x7E,
|
||||
0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 247 (approximately equal)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xDC, 0x00,
|
||||
0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 248 (degree)
|
||||
0x00, 0x38, 0x6C, 0x6C, 0x38, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 249 (bullet operator)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18,
|
||||
0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 250 (middle dot)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 251 (square root)
|
||||
0x00, 0x0F, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0xEC,
|
||||
0x6C, 0x6C, 0x3C, 0x1C, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 252 (superscript n)
|
||||
0x00, 0xD8, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 253 (superscript 2)
|
||||
0x00, 0x70, 0xD8, 0x30, 0x60, 0xC8, 0xF8, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 254 (filled square)
|
||||
0x00, 0x00, 0x00, 0x00, 0x7C, 0x7C, 0x7C, 0x7C,
|
||||
0x7C, 0x7C, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Character 255 (non-breaking space)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
} // namespace gui
|
||||
@@ -0,0 +1,790 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS graphical login screen
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/user.h>
|
||||
#include <montauk/config.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/framebuffer.hpp>
|
||||
#include <gui/draw.hpp>
|
||||
#include <gui/svg.hpp>
|
||||
#include <gui/font.hpp>
|
||||
|
||||
// Forward-declare stb_image functions (implementation in libjpeg.a).
|
||||
extern "C" {
|
||||
unsigned char* stbi_load_from_memory(const unsigned char* buffer, int len,
|
||||
int* x, int* y, int* channels_in_file,
|
||||
int desired_channels);
|
||||
void stbi_image_free(void* retval_from_stbi_load);
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
// Placement new
|
||||
inline void* operator new(unsigned long, void* p) { return p; }
|
||||
|
||||
// ---- Minimal snprintf ----
|
||||
|
||||
using va_list = __builtin_va_list;
|
||||
#define va_start __builtin_va_start
|
||||
#define va_end __builtin_va_end
|
||||
#define va_arg __builtin_va_arg
|
||||
|
||||
struct PfState { char* buf; int pos; int max; };
|
||||
static void pf_putc(PfState* st, char c) {
|
||||
if (st->pos < st->max) st->buf[st->pos] = c;
|
||||
st->pos++;
|
||||
}
|
||||
static void pf_putnum(PfState* st, unsigned long val, int base, int width, char pad, int neg) {
|
||||
char tmp[24]; int i = 0;
|
||||
const char* digits = "0123456789abcdef";
|
||||
if (val == 0) { tmp[i++] = '0'; }
|
||||
else { while (val > 0) { tmp[i++] = digits[val % base]; val /= base; } }
|
||||
int total = (neg ? 1 : 0) + i;
|
||||
if (neg && pad == '0') pf_putc(st, '-');
|
||||
for (int w = total; w < width; w++) pf_putc(st, pad);
|
||||
if (neg && pad != '0') pf_putc(st, '-');
|
||||
while (i > 0) pf_putc(st, tmp[--i]);
|
||||
}
|
||||
static int snprintf(char* buf, int size, const char* fmt, ...) {
|
||||
va_list ap; va_start(ap, fmt);
|
||||
PfState st; st.buf = buf; st.pos = 0; st.max = size > 0 ? size - 1 : 0;
|
||||
while (*fmt) {
|
||||
if (*fmt != '%') { pf_putc(&st, *fmt++); continue; }
|
||||
fmt++;
|
||||
char pad = ' ';
|
||||
if (*fmt == '0') { pad = '0'; fmt++; }
|
||||
int width = 0;
|
||||
while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; }
|
||||
if (*fmt == 'l') fmt++;
|
||||
switch (*fmt) {
|
||||
case 'd': case 'i': {
|
||||
long val = va_arg(ap, int);
|
||||
int neg = 0; unsigned long uval;
|
||||
if (val < 0) { neg = 1; uval = (unsigned long)(-val); } else uval = (unsigned long)val;
|
||||
pf_putnum(&st, uval, 10, width, pad, neg); break;
|
||||
}
|
||||
case 'u': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 10, width, pad, 0); break; }
|
||||
case 'x': { unsigned val = va_arg(ap, unsigned); pf_putnum(&st, val, 16, width, pad, 0); break; }
|
||||
case 's': {
|
||||
const char* s = va_arg(ap, const char*); if (!s) s = "(null)";
|
||||
int slen = 0; while (s[slen]) slen++;
|
||||
for (int w = slen; w < width; w++) pf_putc(&st, ' ');
|
||||
for (int j = 0; j < slen; j++) pf_putc(&st, s[j]);
|
||||
break;
|
||||
}
|
||||
case 'c': { char c = (char)va_arg(ap, int); pf_putc(&st, c); break; }
|
||||
case '%': pf_putc(&st, '%'); break;
|
||||
default: pf_putc(&st, '%'); pf_putc(&st, *fmt); break;
|
||||
}
|
||||
if (*fmt) fmt++;
|
||||
}
|
||||
if (size > 0) { if (st.pos < size) st.buf[st.pos] = '\0'; else st.buf[size - 1] = '\0'; }
|
||||
va_end(ap); return st.pos;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Login state
|
||||
// ============================================================================
|
||||
|
||||
enum LoginMode {
|
||||
MODE_FIRST_BOOT, // Create admin account
|
||||
MODE_LOGIN, // Normal login
|
||||
};
|
||||
|
||||
struct LoginState {
|
||||
Framebuffer fb;
|
||||
int screen_w, screen_h;
|
||||
|
||||
LoginMode mode;
|
||||
int active_field; // 0=username, 1=password, 2=confirm (first-boot only)
|
||||
|
||||
char username[32];
|
||||
int username_len;
|
||||
char display_name[64];
|
||||
int display_name_len;
|
||||
char password[64];
|
||||
int password_len;
|
||||
char confirm[64];
|
||||
int confirm_len;
|
||||
|
||||
char error_msg[128];
|
||||
bool show_error;
|
||||
|
||||
Montauk::MouseState mouse;
|
||||
uint8_t prev_buttons;
|
||||
|
||||
// Power option icons (loaded once at startup)
|
||||
SvgIcon icon_shutdown;
|
||||
SvgIcon icon_reboot;
|
||||
|
||||
// Background wallpaper (loaded from config)
|
||||
uint32_t* bg_wallpaper;
|
||||
int bg_wallpaper_w, bg_wallpaper_h;
|
||||
bool has_wallpaper;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Wallpaper loading
|
||||
// ============================================================================
|
||||
|
||||
static bool load_login_wallpaper(LoginState* ls) {
|
||||
// Load system-wide desktop config and read wallpaper.path
|
||||
auto doc = montauk::config::load("desktop");
|
||||
const char* wp = doc.get_string("wallpaper.path", "");
|
||||
if (wp[0] == '\0') return false;
|
||||
|
||||
// Read JPEG file
|
||||
int fd = montauk::open(wp);
|
||||
if (fd < 0) return false;
|
||||
|
||||
uint64_t size = montauk::getsize(fd);
|
||||
if (size == 0 || size > 16 * 1024 * 1024) {
|
||||
montauk::close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t* filedata = (uint8_t*)montauk::malloc(size);
|
||||
if (!filedata) { montauk::close(fd); return false; }
|
||||
|
||||
int bytes_read = montauk::read(fd, filedata, 0, size);
|
||||
montauk::close(fd);
|
||||
if (bytes_read <= 0) { montauk::mfree(filedata); return false; }
|
||||
|
||||
// Decode JPEG
|
||||
int img_w, img_h, channels;
|
||||
unsigned char* rgb = stbi_load_from_memory(filedata, bytes_read,
|
||||
&img_w, &img_h, &channels, 3);
|
||||
montauk::mfree(filedata);
|
||||
if (!rgb) return false;
|
||||
|
||||
// Scale to cover screen (same algorithm as desktop wallpaper.hpp)
|
||||
int dst_w = ls->screen_w;
|
||||
int dst_h = ls->screen_h;
|
||||
|
||||
uint32_t* scaled = (uint32_t*)montauk::malloc((uint64_t)dst_w * dst_h * 4);
|
||||
if (!scaled) { stbi_image_free(rgb); return false; }
|
||||
|
||||
int src_crop_w, src_crop_h, src_x0, src_y0;
|
||||
if ((int64_t)img_w * dst_h > (int64_t)img_h * dst_w) {
|
||||
src_crop_h = img_h;
|
||||
src_crop_w = (int)((int64_t)img_h * dst_w / dst_h);
|
||||
src_x0 = (img_w - src_crop_w) / 2;
|
||||
src_y0 = 0;
|
||||
} else {
|
||||
src_crop_w = img_w;
|
||||
src_crop_h = (int)((int64_t)img_w * dst_h / dst_w);
|
||||
src_x0 = 0;
|
||||
src_y0 = (img_h - src_crop_h) / 2;
|
||||
}
|
||||
|
||||
for (int y = 0; y < dst_h; y++) {
|
||||
int sy = src_y0 + (int)((int64_t)y * src_crop_h / dst_h);
|
||||
if (sy < 0) sy = 0;
|
||||
if (sy >= img_h) sy = img_h - 1;
|
||||
for (int x = 0; x < dst_w; x++) {
|
||||
int sx = src_x0 + (int)((int64_t)x * src_crop_w / dst_w);
|
||||
if (sx < 0) sx = 0;
|
||||
if (sx >= img_w) sx = img_w - 1;
|
||||
int si = (sy * img_w + sx) * 3;
|
||||
scaled[y * dst_w + x] = 0xFF000000u
|
||||
| ((uint32_t)rgb[si] << 16)
|
||||
| ((uint32_t)rgb[si + 1] << 8)
|
||||
| (uint32_t)rgb[si + 2];
|
||||
}
|
||||
}
|
||||
|
||||
stbi_image_free(rgb);
|
||||
|
||||
ls->bg_wallpaper = scaled;
|
||||
ls->bg_wallpaper_w = dst_w;
|
||||
ls->bg_wallpaper_h = dst_h;
|
||||
ls->has_wallpaper = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Drawing helpers
|
||||
// ============================================================================
|
||||
|
||||
static constexpr Color BG_COLOR = Color::from_rgb(0x2B, 0x3E, 0x50);
|
||||
static constexpr Color CARD_BG = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color FIELD_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
||||
static constexpr Color FIELD_BORDER = Color::from_rgb(0xCC, 0xCC, 0xCC);
|
||||
static constexpr Color FIELD_ACTIVE = Color::from_rgb(0x36, 0x7B, 0xF0);
|
||||
static constexpr Color BTN_COLOR = Color::from_rgb(0x36, 0x7B, 0xF0);
|
||||
static constexpr Color BTN_TEXT = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
static constexpr Color TEXT_COLOR = Color::from_rgb(0x33, 0x33, 0x33);
|
||||
static constexpr Color LABEL_COLOR = Color::from_rgb(0x66, 0x66, 0x66);
|
||||
static constexpr Color ERROR_COLOR = Color::from_rgb(0xE0, 0x40, 0x40);
|
||||
static constexpr Color TITLE_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF);
|
||||
|
||||
static constexpr int CARD_W = 360;
|
||||
static constexpr int FIELD_H = 36;
|
||||
static constexpr int FIELD_PAD = 10;
|
||||
static constexpr int BTN_H = 40;
|
||||
static constexpr int LABEL_GAP = 4;
|
||||
static constexpr int POWER_ICON_SZ = 32;
|
||||
static constexpr int POWER_GAP = 48; // horizontal spacing between icon centers
|
||||
static constexpr int POWER_TOP_PAD = 24; // padding below error message area
|
||||
|
||||
static void draw_field(Framebuffer& fb, int x, int y, int w, const char* text,
|
||||
bool is_password, bool active) {
|
||||
// Background
|
||||
fb.fill_rect(x, y, w, FIELD_H, FIELD_BG);
|
||||
|
||||
// Border
|
||||
Color border = active ? FIELD_ACTIVE : FIELD_BORDER;
|
||||
// Top
|
||||
for (int i = 0; i < w; i++) fb.put_pixel(x + i, y, border);
|
||||
// Bottom
|
||||
for (int i = 0; i < w; i++) fb.put_pixel(x + i, y + FIELD_H - 1, border);
|
||||
// Left
|
||||
for (int i = 0; i < FIELD_H; i++) fb.put_pixel(x, y + i, border);
|
||||
// Right
|
||||
for (int i = 0; i < FIELD_H; i++) fb.put_pixel(x + w - 1, y + i, border);
|
||||
|
||||
if (active) {
|
||||
// Draw 2px border for active field
|
||||
for (int i = 0; i < w; i++) fb.put_pixel(x + i, y + 1, border);
|
||||
for (int i = 0; i < w; i++) fb.put_pixel(x + i, y + FIELD_H - 2, border);
|
||||
for (int i = 0; i < FIELD_H; i++) fb.put_pixel(x + 1, y + i, border);
|
||||
for (int i = 0; i < FIELD_H; i++) fb.put_pixel(x + w - 2, y + i, border);
|
||||
}
|
||||
|
||||
// Text (clipped to field width, showing trailing portion)
|
||||
int ty = y + (FIELD_H - system_font_height()) / 2;
|
||||
int max_text_w = w - FIELD_PAD * 2 - 2; // left pad, right pad, cursor
|
||||
if (is_password) {
|
||||
int len = montauk::slen(text);
|
||||
if (len > 64) len = 64;
|
||||
char full[65];
|
||||
for (int i = 0; i < len; i++) full[i] = '*';
|
||||
full[len] = '\0';
|
||||
// Show only trailing portion that fits
|
||||
int vis = len;
|
||||
while (vis > 0 && text_width(full + (len - vis)) > max_text_w)
|
||||
vis--;
|
||||
char masked[65];
|
||||
for (int i = 0; i < vis; i++) masked[i] = '*';
|
||||
masked[vis] = '\0';
|
||||
draw_text(fb, x + FIELD_PAD, ty, masked, TEXT_COLOR);
|
||||
if (active) {
|
||||
int cx = x + FIELD_PAD + text_width(masked);
|
||||
fb.fill_rect(cx, ty, 2, system_font_height(), FIELD_ACTIVE);
|
||||
}
|
||||
} else {
|
||||
int len = montauk::slen(text);
|
||||
// Show only trailing portion that fits
|
||||
int offset = 0;
|
||||
while (text_width(text + offset) > max_text_w && offset < len)
|
||||
offset++;
|
||||
draw_text(fb, x + FIELD_PAD, ty, text + offset, TEXT_COLOR);
|
||||
if (active) {
|
||||
int cx = x + FIELD_PAD + text_width(text + offset);
|
||||
fb.fill_rect(cx, ty, 2, system_font_height(), FIELD_ACTIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_button(Framebuffer& fb, int x, int y, int w, int h,
|
||||
const char* label, Color bg) {
|
||||
fb.fill_rect(x, y, w, h, bg);
|
||||
int tw = text_width(label);
|
||||
int tx = x + (w - tw) / 2;
|
||||
int ty = y + (h - system_font_height()) / 2;
|
||||
draw_text(fb, tx, ty, label, BTN_TEXT);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Screen drawing
|
||||
// ============================================================================
|
||||
|
||||
static void draw_login_screen(LoginState* ls) {
|
||||
Framebuffer& fb = ls->fb;
|
||||
|
||||
// Background
|
||||
if (ls->has_wallpaper) {
|
||||
fb.blit(0, 0, ls->bg_wallpaper_w, ls->bg_wallpaper_h, ls->bg_wallpaper);
|
||||
} else {
|
||||
fb.clear(BG_COLOR);
|
||||
}
|
||||
|
||||
int card_h;
|
||||
const char* title;
|
||||
int sfh = system_font_height();
|
||||
int power_section_h = POWER_ICON_SZ + sfh + 6 + 4; // icon + label + gap + padding
|
||||
int error_section_h = ls->show_error ? sfh + 8 : 0; // error text + padding
|
||||
if (ls->mode == MODE_FIRST_BOOT) {
|
||||
card_h = 420 + error_section_h + power_section_h;
|
||||
title = "Create Administrator Account";
|
||||
} else {
|
||||
card_h = 284 + error_section_h + power_section_h;
|
||||
title = "Log In";
|
||||
}
|
||||
|
||||
int card_x = (ls->screen_w - CARD_W) / 2;
|
||||
int card_y = (ls->screen_h - card_h) / 2;
|
||||
|
||||
// Card background
|
||||
fb.fill_rect(card_x, card_y, CARD_W, card_h, CARD_BG);
|
||||
|
||||
int x = card_x + 24;
|
||||
int content_w = CARD_W - 48;
|
||||
int y = card_y + 20;
|
||||
|
||||
// Card title
|
||||
{
|
||||
int tw = text_width(title);
|
||||
draw_text(fb, card_x + (CARD_W - tw) / 2, y, title, TEXT_COLOR);
|
||||
y += sfh + 16;
|
||||
}
|
||||
|
||||
if (ls->mode == MODE_FIRST_BOOT) {
|
||||
// Username field
|
||||
draw_text(fb, x, y, "Username", LABEL_COLOR);
|
||||
y += sfh + LABEL_GAP;
|
||||
draw_field(fb, x, y, content_w, ls->username, false, ls->active_field == 0);
|
||||
y += FIELD_H + 12;
|
||||
|
||||
// Display name field
|
||||
draw_text(fb, x, y, "Display Name", LABEL_COLOR);
|
||||
y += sfh + LABEL_GAP;
|
||||
draw_field(fb, x, y, content_w, ls->display_name, false, ls->active_field == 1);
|
||||
y += FIELD_H + 12;
|
||||
|
||||
// Password field
|
||||
draw_text(fb, x, y, "Password", LABEL_COLOR);
|
||||
y += sfh + LABEL_GAP;
|
||||
draw_field(fb, x, y, content_w, ls->password, true, ls->active_field == 2);
|
||||
y += FIELD_H + 12;
|
||||
|
||||
// Confirm password field
|
||||
draw_text(fb, x, y, "Confirm Password", LABEL_COLOR);
|
||||
y += sfh + LABEL_GAP;
|
||||
draw_field(fb, x, y, content_w, ls->confirm, true, ls->active_field == 3);
|
||||
y += FIELD_H + 16;
|
||||
|
||||
// Create button
|
||||
draw_button(fb, x, y, content_w, BTN_H, "Create Account", BTN_COLOR);
|
||||
y += BTN_H;
|
||||
} else {
|
||||
// Username field
|
||||
draw_text(fb, x, y, "Username", LABEL_COLOR);
|
||||
y += sfh + LABEL_GAP;
|
||||
draw_field(fb, x, y, content_w, ls->username, false, ls->active_field == 0);
|
||||
y += FIELD_H + 12;
|
||||
|
||||
// Password field
|
||||
draw_text(fb, x, y, "Password", LABEL_COLOR);
|
||||
y += sfh + LABEL_GAP;
|
||||
draw_field(fb, x, y, content_w, ls->password, true, ls->active_field == 1);
|
||||
y += FIELD_H + 16;
|
||||
|
||||
// Login button
|
||||
draw_button(fb, x, y, content_w, BTN_H, "Log In", BTN_COLOR);
|
||||
y += BTN_H;
|
||||
}
|
||||
|
||||
// Error message (inside card, between button and separator)
|
||||
if (ls->show_error) {
|
||||
y += 8;
|
||||
int tw = text_width(ls->error_msg);
|
||||
draw_text(fb, card_x + (CARD_W - tw) / 2, y, ls->error_msg, ERROR_COLOR);
|
||||
y += sfh;
|
||||
}
|
||||
|
||||
// Separator line
|
||||
y += 16;
|
||||
fb.fill_rect(x, y, content_w, 1, FIELD_BORDER);
|
||||
y += 16;
|
||||
|
||||
// Power options (inside card, centered)
|
||||
{
|
||||
int total_w = POWER_ICON_SZ + POWER_GAP + POWER_ICON_SZ;
|
||||
int start_x = card_x + (CARD_W - total_w) / 2;
|
||||
|
||||
// Shutdown icon
|
||||
if (ls->icon_shutdown.pixels) {
|
||||
int ix = start_x;
|
||||
fb.blit_alpha(ix, y, ls->icon_shutdown.width, ls->icon_shutdown.height,
|
||||
ls->icon_shutdown.pixels);
|
||||
const char* lbl = "Shut Down";
|
||||
int lw = text_width(lbl);
|
||||
draw_text(fb, ix + (POWER_ICON_SZ - lw) / 2, y + POWER_ICON_SZ + 6,
|
||||
lbl, LABEL_COLOR);
|
||||
}
|
||||
|
||||
// Reboot icon
|
||||
if (ls->icon_reboot.pixels) {
|
||||
int ix = start_x + POWER_ICON_SZ + POWER_GAP;
|
||||
fb.blit_alpha(ix, y, ls->icon_reboot.width, ls->icon_reboot.height,
|
||||
ls->icon_reboot.pixels);
|
||||
const char* lbl = "Restart";
|
||||
int lw = text_width(lbl);
|
||||
draw_text(fb, ix + (POWER_ICON_SZ - lw) / 2, y + POWER_ICON_SZ + 6,
|
||||
lbl, LABEL_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse cursor (shared with desktop)
|
||||
draw_cursor(fb, ls->mouse.x, ls->mouse.y);
|
||||
|
||||
fb.flip();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Input handling
|
||||
// ============================================================================
|
||||
|
||||
static void append_char(char* buf, int* len, int max, char c) {
|
||||
if (*len < max - 1) {
|
||||
buf[*len] = c;
|
||||
(*len)++;
|
||||
buf[*len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static void backspace(char* buf, int* len) {
|
||||
if (*len > 0) {
|
||||
(*len)--;
|
||||
buf[*len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static int max_fields(LoginState* ls) {
|
||||
return ls->mode == MODE_FIRST_BOOT ? 4 : 2;
|
||||
}
|
||||
|
||||
static bool try_first_boot_submit(LoginState* ls) {
|
||||
ls->show_error = false;
|
||||
|
||||
if (ls->username_len == 0) {
|
||||
montauk::strcpy(ls->error_msg, "Username is required");
|
||||
ls->show_error = true;
|
||||
return false;
|
||||
}
|
||||
if (ls->password_len == 0) {
|
||||
montauk::strcpy(ls->error_msg, "Password is required");
|
||||
ls->show_error = true;
|
||||
return false;
|
||||
}
|
||||
if (!montauk::streq(ls->password, ls->confirm)) {
|
||||
montauk::strcpy(ls->error_msg, "Passwords do not match");
|
||||
ls->show_error = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the users directory
|
||||
montauk::fmkdir("0:/users");
|
||||
|
||||
const char* dname = ls->display_name_len > 0 ? ls->display_name : ls->username;
|
||||
if (!montauk::user::create_user(ls->username, dname, ls->password, "admin")) {
|
||||
montauk::strcpy(ls->error_msg, "Failed to create user");
|
||||
ls->show_error = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool try_login(LoginState* ls) {
|
||||
ls->show_error = false;
|
||||
|
||||
if (ls->username_len == 0) {
|
||||
montauk::strcpy(ls->error_msg, "Username is required");
|
||||
ls->show_error = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!montauk::user::authenticate(ls->username, ls->password)) {
|
||||
montauk::strcpy(ls->error_msg, "Invalid username or password");
|
||||
ls->show_error = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write session so any app can query the current user
|
||||
montauk::user::set_session(ls->username);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void handle_key(LoginState* ls, const Montauk::KeyEvent& key) {
|
||||
if (!key.pressed) return;
|
||||
|
||||
// Tab switches fields
|
||||
if (key.scancode == 0x0F) {
|
||||
int mf = max_fields(ls);
|
||||
if (key.shift) {
|
||||
ls->active_field = (ls->active_field + mf - 1) % mf;
|
||||
} else {
|
||||
ls->active_field = (ls->active_field + 1) % mf;
|
||||
}
|
||||
ls->show_error = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter submits
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (ls->mode == MODE_FIRST_BOOT) {
|
||||
if (try_first_boot_submit(ls)) {
|
||||
// Switch to login mode
|
||||
ls->mode = MODE_LOGIN;
|
||||
ls->active_field = 0;
|
||||
// Keep username, clear password fields
|
||||
ls->password[0] = '\0'; ls->password_len = 0;
|
||||
ls->confirm[0] = '\0'; ls->confirm_len = 0;
|
||||
ls->show_error = false;
|
||||
}
|
||||
} else {
|
||||
if (try_login(ls)) {
|
||||
// Spawn desktop with username
|
||||
int pid = montauk::spawn("0:/os/desktop.elf", ls->username);
|
||||
if (pid >= 0) {
|
||||
montauk::waitpid(pid);
|
||||
}
|
||||
// Desktop exited (logout) — clear session and fields
|
||||
montauk::user::clear_session();
|
||||
ls->password[0] = '\0'; ls->password_len = 0;
|
||||
ls->active_field = 1; // Focus password field
|
||||
ls->show_error = false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Backspace
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
if (ls->mode == MODE_FIRST_BOOT) {
|
||||
switch (ls->active_field) {
|
||||
case 0: backspace(ls->username, &ls->username_len); break;
|
||||
case 1: backspace(ls->display_name, &ls->display_name_len); break;
|
||||
case 2: backspace(ls->password, &ls->password_len); break;
|
||||
case 3: backspace(ls->confirm, &ls->confirm_len); break;
|
||||
}
|
||||
} else {
|
||||
switch (ls->active_field) {
|
||||
case 0: backspace(ls->username, &ls->username_len); break;
|
||||
case 1: backspace(ls->password, &ls->password_len); break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Printable characters
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
if (ls->mode == MODE_FIRST_BOOT) {
|
||||
switch (ls->active_field) {
|
||||
case 0: append_char(ls->username, &ls->username_len, 31, key.ascii); break;
|
||||
case 1: append_char(ls->display_name, &ls->display_name_len, 63, key.ascii); break;
|
||||
case 2: append_char(ls->password, &ls->password_len, 63, key.ascii); break;
|
||||
case 3: append_char(ls->confirm, &ls->confirm_len, 63, key.ascii); break;
|
||||
}
|
||||
} else {
|
||||
switch (ls->active_field) {
|
||||
case 0: append_char(ls->username, &ls->username_len, 31, key.ascii); break;
|
||||
case 1: append_char(ls->password, &ls->password_len, 63, key.ascii); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_mouse(LoginState* ls) {
|
||||
int mx = ls->mouse.x;
|
||||
int my = ls->mouse.y;
|
||||
uint8_t buttons = ls->mouse.buttons;
|
||||
uint8_t prev = ls->prev_buttons;
|
||||
bool left_pressed = (buttons & 0x01) && !(prev & 0x01);
|
||||
|
||||
if (!left_pressed) return;
|
||||
|
||||
int sfh = system_font_height();
|
||||
int power_section_h = POWER_ICON_SZ + sfh + 6 + 8;
|
||||
int error_section_h = ls->show_error ? sfh + 8 : 0;
|
||||
int card_h;
|
||||
if (ls->mode == MODE_FIRST_BOOT) {
|
||||
card_h = 420 + error_section_h + power_section_h;
|
||||
} else {
|
||||
card_h = 284 + error_section_h + power_section_h;
|
||||
}
|
||||
int card_x = (ls->screen_w - CARD_W) / 2;
|
||||
int card_y = (ls->screen_h - card_h) / 2;
|
||||
int x = card_x + 24;
|
||||
int content_w = CARD_W - 48;
|
||||
int y = card_y + 20;
|
||||
|
||||
// Skip title
|
||||
y += sfh + 16;
|
||||
|
||||
if (ls->mode == MODE_FIRST_BOOT) {
|
||||
// Username label + field
|
||||
y += sfh + LABEL_GAP;
|
||||
if (mx >= x && mx < x + content_w && my >= y && my < y + FIELD_H) {
|
||||
ls->active_field = 0;
|
||||
return;
|
||||
}
|
||||
y += FIELD_H + 12;
|
||||
|
||||
// Display name label + field
|
||||
y += sfh + LABEL_GAP;
|
||||
if (mx >= x && mx < x + content_w && my >= y && my < y + FIELD_H) {
|
||||
ls->active_field = 1;
|
||||
return;
|
||||
}
|
||||
y += FIELD_H + 12;
|
||||
|
||||
// Password label + field
|
||||
y += sfh + LABEL_GAP;
|
||||
if (mx >= x && mx < x + content_w && my >= y && my < y + FIELD_H) {
|
||||
ls->active_field = 2;
|
||||
return;
|
||||
}
|
||||
y += FIELD_H + 12;
|
||||
|
||||
// Confirm label + field
|
||||
y += sfh + LABEL_GAP;
|
||||
if (mx >= x && mx < x + content_w && my >= y && my < y + FIELD_H) {
|
||||
ls->active_field = 3;
|
||||
return;
|
||||
}
|
||||
y += FIELD_H + 16;
|
||||
|
||||
// Create button
|
||||
if (mx >= x && mx < x + content_w && my >= y && my < y + BTN_H) {
|
||||
if (try_first_boot_submit(ls)) {
|
||||
ls->mode = MODE_LOGIN;
|
||||
ls->active_field = 0;
|
||||
ls->password[0] = '\0'; ls->password_len = 0;
|
||||
ls->confirm[0] = '\0'; ls->confirm_len = 0;
|
||||
ls->show_error = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
y += BTN_H;
|
||||
} else {
|
||||
// Username label + field
|
||||
y += sfh + LABEL_GAP;
|
||||
if (mx >= x && mx < x + content_w && my >= y && my < y + FIELD_H) {
|
||||
ls->active_field = 0;
|
||||
return;
|
||||
}
|
||||
y += FIELD_H + 12;
|
||||
|
||||
// Password label + field
|
||||
y += sfh + LABEL_GAP;
|
||||
if (mx >= x && mx < x + content_w && my >= y && my < y + FIELD_H) {
|
||||
ls->active_field = 1;
|
||||
return;
|
||||
}
|
||||
y += FIELD_H + 16;
|
||||
|
||||
// Login button
|
||||
if (mx >= x && mx < x + content_w && my >= y && my < y + BTN_H) {
|
||||
if (try_login(ls)) {
|
||||
int pid = montauk::spawn("0:/os/desktop.elf", ls->username);
|
||||
if (pid >= 0) {
|
||||
montauk::waitpid(pid);
|
||||
}
|
||||
montauk::user::clear_session();
|
||||
ls->password[0] = '\0'; ls->password_len = 0;
|
||||
ls->active_field = 1;
|
||||
ls->show_error = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
y += BTN_H;
|
||||
}
|
||||
|
||||
// Skip error section if visible
|
||||
if (ls->show_error) y += error_section_h;
|
||||
|
||||
// Power options (inside card, after separator)
|
||||
y += 16 + 1 + 16; // padding + separator + padding
|
||||
{
|
||||
int total_w = POWER_ICON_SZ + POWER_GAP + POWER_ICON_SZ;
|
||||
int start_x = card_x + (CARD_W - total_w) / 2;
|
||||
int hit_h = POWER_ICON_SZ + sfh + 6;
|
||||
|
||||
// Shutdown
|
||||
if (mx >= start_x && mx < start_x + POWER_ICON_SZ &&
|
||||
my >= y && my < y + hit_h) {
|
||||
montauk::shutdown();
|
||||
}
|
||||
|
||||
// Reboot
|
||||
int rx = start_x + POWER_ICON_SZ + POWER_GAP;
|
||||
if (mx >= rx && mx < rx + POWER_ICON_SZ &&
|
||||
my >= y && my < y + hit_h) {
|
||||
montauk::reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
|
||||
extern "C" void _start() {
|
||||
LoginState* ls = (LoginState*)montauk::malloc(sizeof(LoginState));
|
||||
montauk::memset(ls, 0, sizeof(LoginState));
|
||||
|
||||
new (&ls->fb) Framebuffer();
|
||||
ls->screen_w = ls->fb.width();
|
||||
ls->screen_h = ls->fb.height();
|
||||
|
||||
// Load TrueType fonts
|
||||
fonts::init();
|
||||
|
||||
// Set mouse bounds
|
||||
montauk::set_mouse_bounds(ls->screen_w - 1, ls->screen_h - 1);
|
||||
|
||||
// Load power option icons
|
||||
Color icon_color = Color::from_rgb(0x66, 0x66, 0x66);
|
||||
ls->icon_shutdown = svg_load("0:/icons/system-shutdown.svg", POWER_ICON_SZ, POWER_ICON_SZ, icon_color);
|
||||
ls->icon_reboot = svg_load("0:/icons/system-reboot.svg", POWER_ICON_SZ, POWER_ICON_SZ, icon_color);
|
||||
|
||||
// Load wallpaper from system desktop config
|
||||
load_login_wallpaper(ls);
|
||||
|
||||
// Check if users.toml exists (first boot detection)
|
||||
int fh = montauk::open("0:/config/users.toml");
|
||||
if (fh < 0) {
|
||||
ls->mode = MODE_FIRST_BOOT;
|
||||
ls->active_field = 0;
|
||||
// Pre-fill username with "admin"
|
||||
montauk::strcpy(ls->username, "admin");
|
||||
ls->username_len = 5;
|
||||
} else {
|
||||
montauk::close(fh);
|
||||
ls->mode = MODE_LOGIN;
|
||||
ls->active_field = 0;
|
||||
}
|
||||
|
||||
// Main loop
|
||||
for (;;) {
|
||||
// Poll mouse
|
||||
ls->prev_buttons = ls->mouse.buttons;
|
||||
montauk::mouse_state(&ls->mouse);
|
||||
|
||||
// Poll keyboard
|
||||
while (montauk::is_key_available()) {
|
||||
Montauk::KeyEvent key;
|
||||
montauk::getkey(&key);
|
||||
handle_key(ls, key);
|
||||
}
|
||||
|
||||
// Handle mouse
|
||||
handle_mouse(ls);
|
||||
|
||||
// Draw
|
||||
draw_login_screen(ls);
|
||||
|
||||
// ~30fps (login doesn't need 60)
|
||||
montauk::sleep_ms(33);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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>
|
||||
|
||||
// Override all stb_truetype dependencies before including the implementation
|
||||
|
||||
#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>
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/config.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
#include <gui/svg.hpp>
|
||||
@@ -922,6 +923,22 @@ extern "C" void _start() {
|
||||
g.hovered_item = -1;
|
||||
|
||||
// Parse arguments: accept a directory path or a file path
|
||||
// Helper: set dir_path to current user's home via session config
|
||||
auto set_user_home = [&]() {
|
||||
auto doc = montauk::config::load("session");
|
||||
const char* name = doc.get_string("session.username", "");
|
||||
if (name[0]) {
|
||||
int p = 0;
|
||||
const char* pfx = "0:/users/";
|
||||
while (*pfx && p < (int)sizeof(g.dir_path) - 1) g.dir_path[p++] = *pfx++;
|
||||
while (*name && p < (int)sizeof(g.dir_path) - 1) g.dir_path[p++] = *name++;
|
||||
g.dir_path[p] = '\0';
|
||||
} else {
|
||||
memcpy(g.dir_path, "0:/home", 8);
|
||||
}
|
||||
doc.destroy();
|
||||
};
|
||||
|
||||
char arg_file[128] = {};
|
||||
{
|
||||
char args[256];
|
||||
@@ -943,7 +960,7 @@ extern "C" void _start() {
|
||||
arg_file[flen] = '\0';
|
||||
}
|
||||
} else {
|
||||
memcpy(g.dir_path, "0:/home", 8);
|
||||
set_user_home();
|
||||
}
|
||||
} else {
|
||||
int len = montauk::slen(args);
|
||||
@@ -952,7 +969,7 @@ extern "C" void _start() {
|
||||
g.dir_path[len] = '\0';
|
||||
}
|
||||
} else {
|
||||
memcpy(g.dir_path, "0:/home", 8);
|
||||
set_user_home();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Makefile for shell (interactive shell) on MontaukOS
|
||||
# Copyright (c) 2025-2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
# ---- Toolchain ----
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
# ---- Paths ----
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
|
||||
# ---- C++ compiler flags ----
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-mno-80387 \
|
||||
-mno-mmx \
|
||||
-mno-sse \
|
||||
-mno-sse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-MMD -MP \
|
||||
-I . \
|
||||
-I $(PROG_INC) \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
# ---- Linker flags ----
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
# ---- Source files ----
|
||||
|
||||
SRCS := main.cpp vars.cpp builtins.cpp exec.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/os/shell.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/os
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp shell.h Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
-include $(OBJS:.o=.d)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* builtins.cpp
|
||||
* Shell builtin commands: help, ls, cd, man
|
||||
* Copyright (c) 2025-2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "shell.h"
|
||||
|
||||
// ---- help ----
|
||||
|
||||
void cmd_help() {
|
||||
montauk::print("Shell builtins:\n");
|
||||
montauk::print(" help Show this help message\n");
|
||||
montauk::print(" ls [dir] List files in directory\n");
|
||||
montauk::print(" cd [dir] Change working directory\n");
|
||||
montauk::print(" pwd Print working directory\n");
|
||||
montauk::print(" echo [-n] ... Print arguments\n");
|
||||
montauk::print(" set [VAR=val] Show or set shell variables\n");
|
||||
montauk::print(" unset VAR Remove a shell variable\n");
|
||||
montauk::print(" true / false Return success / failure\n");
|
||||
montauk::print(" N: Switch to drive N (e.g. 1:)\n");
|
||||
montauk::print(" exit Exit the shell\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("Syntax:\n");
|
||||
montauk::print(" VAR=value Set a shell variable\n");
|
||||
montauk::print(" $VAR ${VAR} Variable expansion\n");
|
||||
montauk::print(" ~ Expands to home directory\n");
|
||||
montauk::print(" cmd1 ; cmd2 Run commands sequentially\n");
|
||||
montauk::print(" cmd1 && cmd2 Run cmd2 if cmd1 succeeds\n");
|
||||
montauk::print(" cmd1 || cmd2 Run cmd2 if cmd1 fails\n");
|
||||
montauk::print(" # comment Comment (ignored)\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("Built-in variables:\n");
|
||||
montauk::print(" $USER $HOME $PWD $?\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("System commands:\n");
|
||||
montauk::print(" man <topic> View manual pages\n");
|
||||
montauk::print(" cat <file> Display file contents\n");
|
||||
montauk::print(" edit [file] Text editor\n");
|
||||
montauk::print(" whoami Print current username\n");
|
||||
montauk::print(" info Show system information\n");
|
||||
montauk::print(" date Show current date and time\n");
|
||||
montauk::print(" uptime Show uptime\n");
|
||||
montauk::print(" clear Clear the screen\n");
|
||||
montauk::print(" fontscale [n] Set terminal font scale (1-8)\n");
|
||||
montauk::print(" reset Reboot the system\n");
|
||||
montauk::print(" shutdown Shut down the system\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("Network commands:\n");
|
||||
montauk::print(" ping <ip> Send ICMP echo requests\n");
|
||||
montauk::print(" nslookup DNS lookup\n");
|
||||
montauk::print(" ifconfig Show/set network configuration\n");
|
||||
montauk::print(" tcpconnect Connect to a TCP server\n");
|
||||
montauk::print(" irc IRC client\n");
|
||||
montauk::print(" dhcp DHCP client\n");
|
||||
montauk::print(" fetch <url> HTTP client\n");
|
||||
montauk::print(" httpd HTTP server\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("Games:\n");
|
||||
montauk::print(" doom DOOM\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("Any .elf on the ramdisk is executable.\n");
|
||||
}
|
||||
|
||||
// ---- ls ----
|
||||
|
||||
void cmd_ls(const char* arg) {
|
||||
arg = skip_spaces(arg);
|
||||
|
||||
char dir[128];
|
||||
int drive = current_drive;
|
||||
if (*arg) {
|
||||
if (has_drive_prefix(arg)) {
|
||||
drive = parse_drive_prefix(arg);
|
||||
int plen = drive_prefix_len(arg);
|
||||
scopy(dir, arg + plen + 1, sizeof(dir));
|
||||
} else if (arg[0] == '/') {
|
||||
scopy(dir, arg + 1, sizeof(dir));
|
||||
} else if (cwd[0]) {
|
||||
scopy(dir, cwd, sizeof(dir));
|
||||
scat(dir, "/", sizeof(dir));
|
||||
scat(dir, arg, sizeof(dir));
|
||||
} else {
|
||||
scopy(dir, arg, sizeof(dir));
|
||||
}
|
||||
} else {
|
||||
scopy(dir, cwd, sizeof(dir));
|
||||
}
|
||||
|
||||
char path[128];
|
||||
build_drive_path(drive, dir, path, sizeof(path));
|
||||
|
||||
const char* entries[64];
|
||||
int count = montauk::readdir(path, entries, 64);
|
||||
if (count <= 0) {
|
||||
montauk::print("(empty)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
int prefixLen = 0;
|
||||
if (dir[0]) prefixLen = slen(dir) + 1;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
montauk::print(" ");
|
||||
if (prefixLen > 0 && starts_with(entries[i], dir)) {
|
||||
montauk::print(entries[i] + prefixLen);
|
||||
} else {
|
||||
montauk::print(entries[i]);
|
||||
}
|
||||
montauk::putchar('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// ---- cd ----
|
||||
|
||||
bool switch_drive(int drive) {
|
||||
char path[8];
|
||||
build_drive_path(drive, "", path, sizeof(path));
|
||||
const char* entries[1];
|
||||
if (montauk::readdir(path, entries, 1) < 0) return false;
|
||||
current_drive = drive;
|
||||
cwd[0] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
int cmd_cd(const char* arg) {
|
||||
arg = skip_spaces(arg);
|
||||
|
||||
// cd with no argument -> go to home directory (or root if no session)
|
||||
if (*arg == '\0') {
|
||||
if (session_home[0] && has_drive_prefix(session_home)) {
|
||||
int drive = parse_drive_prefix(session_home);
|
||||
int plen = drive_prefix_len(session_home);
|
||||
const char* rel = session_home + plen;
|
||||
if (*rel == '/') rel++;
|
||||
current_drive = drive;
|
||||
if (*rel) scopy(cwd, rel, sizeof(cwd));
|
||||
else cwd[0] = '\0';
|
||||
} else {
|
||||
cwd[0] = '\0';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// cd / -> go to root
|
||||
if (streq(arg, "/")) {
|
||||
cwd[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Strip trailing slashes from argument
|
||||
static char argBuf[128];
|
||||
int aLen = 0;
|
||||
while (arg[aLen] && aLen < 127) { argBuf[aLen] = arg[aLen]; aLen++; }
|
||||
argBuf[aLen] = '\0';
|
||||
while (aLen > 0 && argBuf[aLen - 1] == '/') argBuf[--aLen] = '\0';
|
||||
arg = argBuf;
|
||||
|
||||
if (*arg == '\0') {
|
||||
cwd[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
// cd .. -> go up one level
|
||||
if (streq(arg, "..")) {
|
||||
int len = slen(cwd);
|
||||
int last = -1;
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (cwd[i] == '/') last = i;
|
||||
}
|
||||
if (last >= 0) {
|
||||
cwd[last] = '\0';
|
||||
} else {
|
||||
cwd[0] = '\0';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// cd /path -> absolute path from root
|
||||
if (arg[0] == '/') {
|
||||
arg++;
|
||||
if (*arg == '\0') { cwd[0] = '\0'; return 0; }
|
||||
char path[128];
|
||||
build_dir_path(arg, path, sizeof(path));
|
||||
const char* entries[1];
|
||||
if (montauk::readdir(path, entries, 1) < 0) {
|
||||
montauk::print("cd: no such directory: ");
|
||||
montauk::print(arg);
|
||||
montauk::putchar('\n');
|
||||
return 1;
|
||||
}
|
||||
scopy(cwd, arg, sizeof(cwd));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// cd N:/ or cd N:/path -> switch drive
|
||||
if (has_drive_prefix(arg)) {
|
||||
int drive = parse_drive_prefix(arg);
|
||||
int plen = drive_prefix_len(arg);
|
||||
const char* rel = arg + plen;
|
||||
if (*rel == '/') rel++;
|
||||
|
||||
char rootPath[8];
|
||||
build_drive_path(drive, "", rootPath, sizeof(rootPath));
|
||||
const char* rootEntries[1];
|
||||
if (montauk::readdir(rootPath, rootEntries, 1) < 0) {
|
||||
montauk::print("cd: no such drive: ");
|
||||
montauk::print(arg);
|
||||
montauk::putchar('\n');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (*rel != '\0') {
|
||||
char path[128];
|
||||
build_drive_path(drive, rel, path, sizeof(path));
|
||||
const char* entries[1];
|
||||
if (montauk::readdir(path, entries, 1) < 0) {
|
||||
montauk::print("cd: no such directory: ");
|
||||
montauk::print(arg);
|
||||
montauk::putchar('\n');
|
||||
return 1;
|
||||
}
|
||||
current_drive = drive;
|
||||
scopy(cwd, rel, sizeof(cwd));
|
||||
} else {
|
||||
current_drive = drive;
|
||||
cwd[0] = '\0';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Relative path
|
||||
char target[128];
|
||||
if (cwd[0]) {
|
||||
scopy(target, cwd, sizeof(target));
|
||||
scat(target, "/", sizeof(target));
|
||||
scat(target, arg, sizeof(target));
|
||||
} else {
|
||||
scopy(target, arg, sizeof(target));
|
||||
}
|
||||
|
||||
char path[128];
|
||||
build_dir_path(target, path, sizeof(path));
|
||||
const char* entries[1];
|
||||
int count = montauk::readdir(path, entries, 1);
|
||||
if (count < 0) {
|
||||
montauk::print("cd: no such directory: ");
|
||||
montauk::print(arg);
|
||||
montauk::putchar('\n');
|
||||
return 1;
|
||||
}
|
||||
|
||||
scopy(cwd, target, sizeof(cwd));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- man ----
|
||||
|
||||
int cmd_man(const char* arg) {
|
||||
arg = skip_spaces(arg);
|
||||
if (*arg == '\0') {
|
||||
montauk::print("Usage: man <topic>\n");
|
||||
montauk::print(" man <section> <topic>\n");
|
||||
montauk::print("Try: man intro\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int pid = montauk::spawn("0:/os/man.elf", arg);
|
||||
if (pid < 0) {
|
||||
montauk::print("Error: failed to start man viewer\n");
|
||||
return 1;
|
||||
}
|
||||
montauk::waitpid(pid);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* exec.cpp
|
||||
* External command search and execution
|
||||
* Copyright (c) 2025-2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "shell.h"
|
||||
|
||||
// ---- Try to spawn an ELF at the given path ----
|
||||
|
||||
static bool try_exec(const char* path, const char* args) {
|
||||
int h = montauk::open(path);
|
||||
if (h < 0) return false;
|
||||
montauk::close(h);
|
||||
|
||||
int pid = montauk::spawn(path, args);
|
||||
if (pid < 0) return false;
|
||||
montauk::waitpid(pid);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Resolve arguments: expand relative file paths against CWD ----
|
||||
|
||||
static void resolve_args(const char* args, char* out, int outMax) {
|
||||
if (!args || !args[0]) { out[0] = '\0'; return; }
|
||||
|
||||
int o = 0;
|
||||
const char* p = args;
|
||||
|
||||
while (*p && o < outMax - 1) {
|
||||
while (*p == ' ' && o < outMax - 1) { out[o++] = *p++; }
|
||||
if (!*p) break;
|
||||
|
||||
const char* tokStart = p;
|
||||
int tokLen = 0;
|
||||
while (p[tokLen] && p[tokLen] != ' ') tokLen++;
|
||||
|
||||
bool resolve = (cwd[0] || current_drive != 0) && !has_drive_prefix(tokStart) && tokStart[0] != '-';
|
||||
|
||||
if (resolve) {
|
||||
char candidate[256];
|
||||
int r = 0;
|
||||
if (current_drive >= 10) candidate[r++] = '0' + current_drive / 10;
|
||||
candidate[r++] = '0' + current_drive % 10;
|
||||
candidate[r++] = ':'; candidate[r++] = '/';
|
||||
int j = 0;
|
||||
while (cwd[j] && r < 255) candidate[r++] = cwd[j++];
|
||||
if (cwd[0] && r < 255) candidate[r++] = '/';
|
||||
for (int k = 0; k < tokLen && r < 255; k++) candidate[r++] = tokStart[k];
|
||||
candidate[r] = '\0';
|
||||
|
||||
int h = montauk::open(candidate);
|
||||
if (h >= 0) {
|
||||
montauk::close(h);
|
||||
for (int k = 0; k < r && o < outMax - 1; k++) out[o++] = candidate[k];
|
||||
} else {
|
||||
bool looksLikeFile = false;
|
||||
for (int k = 0; k < tokLen; k++) {
|
||||
if (tokStart[k] == '.') { looksLikeFile = true; break; }
|
||||
}
|
||||
if (looksLikeFile) {
|
||||
for (int k = 0; k < r && o < outMax - 1; k++) out[o++] = candidate[k];
|
||||
} else {
|
||||
for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k];
|
||||
}
|
||||
|
||||
p = tokStart + tokLen;
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
// ---- Search and execute an external command ----
|
||||
|
||||
int exec_external(const char* cmd, const char* args) {
|
||||
char path[256];
|
||||
|
||||
char resolvedArgs[512];
|
||||
resolve_args(args, resolvedArgs, sizeof(resolvedArgs));
|
||||
const char* finalArgs = resolvedArgs[0] ? resolvedArgs : nullptr;
|
||||
|
||||
// 1. Try 0:/os/<cmd>.elf
|
||||
scopy(path, "0:/os/", sizeof(path));
|
||||
scat(path, cmd, sizeof(path));
|
||||
scat(path, ".elf", sizeof(path));
|
||||
if (try_exec(path, finalArgs)) return 0;
|
||||
|
||||
// 2. Try 0:/games/<cmd>.elf
|
||||
scopy(path, "0:/games/", sizeof(path));
|
||||
scat(path, cmd, sizeof(path));
|
||||
scat(path, ".elf", sizeof(path));
|
||||
if (try_exec(path, finalArgs)) return 0;
|
||||
|
||||
// 3. Try N:/<cwd>/<cmd>.elf on current drive
|
||||
if (cwd[0]) {
|
||||
build_drive_path(current_drive, "", path, sizeof(path));
|
||||
scat(path, cwd, sizeof(path));
|
||||
scat(path, "/", sizeof(path));
|
||||
scat(path, cmd, sizeof(path));
|
||||
scat(path, ".elf", sizeof(path));
|
||||
if (try_exec(path, finalArgs)) return 0;
|
||||
}
|
||||
|
||||
// 4. Try N:/<cmd>.elf on current drive
|
||||
build_drive_path(current_drive, "", path, sizeof(path));
|
||||
scat(path, cmd, sizeof(path));
|
||||
scat(path, ".elf", sizeof(path));
|
||||
if (try_exec(path, finalArgs)) return 0;
|
||||
|
||||
// 5. If on a non-zero drive, also try 0:/<cmd>.elf
|
||||
if (current_drive != 0) {
|
||||
scopy(path, "0:/", sizeof(path));
|
||||
scat(path, cmd, sizeof(path));
|
||||
scat(path, ".elf", sizeof(path));
|
||||
if (try_exec(path, finalArgs)) return 0;
|
||||
}
|
||||
|
||||
montauk::print(cmd);
|
||||
montauk::print(": command not found\n");
|
||||
return 127;
|
||||
}
|
||||
+230
-470
@@ -1,75 +1,30 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* Interactive shell for MontaukOS
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
* Entry point, input loop, command dispatch, and chaining
|
||||
* Copyright (c) 2025-2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include "shell.h"
|
||||
|
||||
using montauk::slen;
|
||||
using montauk::streq;
|
||||
using montauk::starts_with;
|
||||
using montauk::skip_spaces;
|
||||
// ---- Shared state definitions ----
|
||||
|
||||
static void scopy(char* dst, const char* src, int maxLen) {
|
||||
int i = 0;
|
||||
while (src[i] && i < maxLen - 1) { dst[i] = src[i]; i++; }
|
||||
dst[i] = '\0';
|
||||
}
|
||||
char cwd[128] = "";
|
||||
int current_drive = 0;
|
||||
int last_exit = 0;
|
||||
char session_user[32] = "";
|
||||
char session_home[64] = "";
|
||||
|
||||
static void scat(char* dst, const char* src, int maxLen) {
|
||||
int dLen = slen(dst);
|
||||
int i = 0;
|
||||
while (src[i] && dLen + i < maxLen - 1) {
|
||||
dst[dLen + i] = src[i];
|
||||
i++;
|
||||
// ---- Session info (read once at startup) ----
|
||||
|
||||
void read_session() {
|
||||
auto doc = montauk::config::load("session");
|
||||
const char* name = doc.get_string("session.username", "");
|
||||
if (name[0]) {
|
||||
scopy(session_user, name, sizeof(session_user));
|
||||
scopy(session_home, "0:/users/", sizeof(session_home));
|
||||
scat(session_home, session_user, sizeof(session_home));
|
||||
}
|
||||
dst[dLen + i] = '\0';
|
||||
}
|
||||
|
||||
// Current working directory (relative to N:/).
|
||||
// "" = root, "man" = 0:/man, "man/sub" = 0:/man/sub
|
||||
static char cwd[128] = "";
|
||||
static int current_drive = 0;
|
||||
|
||||
// Build VFS path with explicit drive number
|
||||
static void build_drive_path(int drive, const char* dir, char* out, int outMax) {
|
||||
int i = 0;
|
||||
if (drive >= 10) { out[i++] = '0' + drive / 10; }
|
||||
out[i++] = '0' + drive % 10;
|
||||
out[i++] = ':'; out[i++] = '/';
|
||||
if (dir && dir[0]) {
|
||||
int j = 0;
|
||||
while (dir[j] && i < outMax - 1) out[i++] = dir[j++];
|
||||
}
|
||||
out[i] = '\0';
|
||||
}
|
||||
|
||||
// Build VFS directory path: "N:/" or "N:/<dir>" using current_drive
|
||||
static void build_dir_path(const char* dir, char* out, int outMax) {
|
||||
build_drive_path(current_drive, dir, out, outMax);
|
||||
}
|
||||
|
||||
// Parse drive number from a drive prefix like "0:" or "12:". Returns -1 if invalid.
|
||||
static int parse_drive_prefix(const char* s) {
|
||||
if (s[0] < '0' || s[0] > '9') return -1;
|
||||
int n = s[0] - '0';
|
||||
if (s[1] >= '0' && s[1] <= '9') {
|
||||
n = n * 10 + (s[1] - '0');
|
||||
if (s[2] != ':') return -1;
|
||||
} else if (s[1] == ':') {
|
||||
// single digit drive
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// Length of drive prefix ("0:" = 2, "12:" = 3). Assumes valid prefix.
|
||||
static int drive_prefix_len(const char* s) {
|
||||
if (s[1] >= '0' && s[1] <= '9') return 3; // e.g. "12:"
|
||||
return 2; // e.g. "0:"
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
// ---- Command history ----
|
||||
@@ -77,11 +32,10 @@ static int drive_prefix_len(const char* s) {
|
||||
static constexpr int HISTORY_MAX = 32;
|
||||
static char history[HISTORY_MAX][256];
|
||||
static int history_count = 0;
|
||||
static int history_next = 0; // ring-buffer write index
|
||||
static int history_next = 0;
|
||||
|
||||
static void history_add(const char* line) {
|
||||
if (line[0] == '\0') return;
|
||||
// Don't add duplicate of last entry
|
||||
if (history_count > 0) {
|
||||
int prev = (history_next + HISTORY_MAX - 1) % HISTORY_MAX;
|
||||
if (streq(history[prev], line)) return;
|
||||
@@ -91,7 +45,6 @@ static void history_add(const char* line) {
|
||||
if (history_count < HISTORY_MAX) history_count++;
|
||||
}
|
||||
|
||||
// Get history entry by index (0 = most recent, 1 = one before that, ...)
|
||||
static const char* history_get(int idx) {
|
||||
if (idx < 0 || idx >= history_count) return nullptr;
|
||||
int pos = (history_next + HISTORY_MAX - 1 - idx) % HISTORY_MAX;
|
||||
@@ -116,17 +69,14 @@ static void prompt() {
|
||||
montauk::print("> ");
|
||||
}
|
||||
|
||||
// ---- Erase current input line on screen ----
|
||||
// ---- Line editing ----
|
||||
|
||||
static void erase_input(int len) {
|
||||
// Move cursor to start of input and overwrite with spaces
|
||||
for (int i = 0; i < len; i++) montauk::putchar('\b');
|
||||
for (int i = 0; i < len; i++) montauk::putchar(' ');
|
||||
for (int i = 0; i < len; i++) montauk::putchar('\b');
|
||||
}
|
||||
|
||||
// ---- Replace visible line with new content ----
|
||||
|
||||
static void replace_line(char* line, int* pos, const char* newContent) {
|
||||
int oldLen = *pos;
|
||||
erase_input(oldLen);
|
||||
@@ -140,386 +90,43 @@ static void replace_line(char* line, int* pos, const char* newContent) {
|
||||
*pos = newLen;
|
||||
}
|
||||
|
||||
// ---- Builtin: help ----
|
||||
// ---- Command dispatch (single command, already expanded) ----
|
||||
|
||||
static void cmd_help() {
|
||||
montauk::print("Shell builtins:\n");
|
||||
montauk::print(" help Show this help message\n");
|
||||
montauk::print(" ls [dir] List files in directory\n");
|
||||
montauk::print(" cd [dir] Change working directory\n");
|
||||
montauk::print(" N: Switch to drive N (e.g. 1:)\n");
|
||||
montauk::print(" exit Exit the shell\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("System commands:\n");
|
||||
montauk::print(" man <topic> View manual pages\n");
|
||||
montauk::print(" cat <file> Display file contents\n");
|
||||
montauk::print(" edit [file] Text editor\n");
|
||||
montauk::print(" info Show system information\n");
|
||||
montauk::print(" date Show current date and time\n");
|
||||
montauk::print(" uptime Show uptime\n");
|
||||
montauk::print(" clear Clear the screen\n");
|
||||
montauk::print(" fontscale [n] Set terminal font scale (1-8)\n");
|
||||
montauk::print(" reset Reboot the system\n");
|
||||
montauk::print(" shutdown Shut down the system\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("Network commands:\n");
|
||||
montauk::print(" ping <ip> Send ICMP echo requests\n");
|
||||
montauk::print(" nslookup DNS lookup\n");
|
||||
montauk::print(" ifconfig Show/set network configuration\n");
|
||||
montauk::print(" tcpconnect Connect to a TCP server\n");
|
||||
montauk::print(" irc IRC client\n");
|
||||
montauk::print(" dhcp DHCP client\n");
|
||||
montauk::print(" fetch <url> HTTP client\n");
|
||||
montauk::print(" httpd HTTP server\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("Games:\n");
|
||||
montauk::print(" doom DOOM\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("Any .elf on the ramdisk is executable.\n");
|
||||
}
|
||||
|
||||
// Check if a string already has a VFS drive prefix (e.g. "0:/" or "12:/")
|
||||
static bool has_drive_prefix(const char* s) {
|
||||
return parse_drive_prefix(s) >= 0;
|
||||
}
|
||||
|
||||
// ---- Builtin: ls ----
|
||||
|
||||
static void cmd_ls(const char* arg) {
|
||||
arg = skip_spaces(arg);
|
||||
|
||||
// Build the target directory (relative path from root)
|
||||
char dir[128];
|
||||
int drive = current_drive;
|
||||
if (*arg) {
|
||||
if (has_drive_prefix(arg)) {
|
||||
// Absolute VFS path: "N:/something"
|
||||
drive = parse_drive_prefix(arg);
|
||||
int plen = drive_prefix_len(arg);
|
||||
scopy(dir, arg + plen + 1, sizeof(dir)); // skip "N:/"
|
||||
} else if (arg[0] == '/') {
|
||||
// Absolute path from root: "/something"
|
||||
scopy(dir, arg + 1, sizeof(dir));
|
||||
} else if (cwd[0]) {
|
||||
// Relative path with CWD
|
||||
scopy(dir, cwd, sizeof(dir));
|
||||
scat(dir, "/", sizeof(dir));
|
||||
scat(dir, arg, sizeof(dir));
|
||||
} else {
|
||||
// Relative path at root
|
||||
scopy(dir, arg, sizeof(dir));
|
||||
}
|
||||
} else {
|
||||
// ls with no arg -- use cwd
|
||||
scopy(dir, cwd, sizeof(dir));
|
||||
}
|
||||
|
||||
char path[128];
|
||||
build_drive_path(drive, dir, path, sizeof(path));
|
||||
|
||||
const char* entries[64];
|
||||
int count = montauk::readdir(path, entries, 64);
|
||||
if (count <= 0) {
|
||||
montauk::print("(empty)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefix to strip: "dir/" (if dir is non-empty)
|
||||
int prefixLen = 0;
|
||||
if (dir[0]) prefixLen = slen(dir) + 1;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
montauk::print(" ");
|
||||
if (prefixLen > 0 && starts_with(entries[i], dir)) {
|
||||
montauk::print(entries[i] + prefixLen);
|
||||
} else {
|
||||
montauk::print(entries[i]);
|
||||
}
|
||||
montauk::putchar('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Builtin: cd ----
|
||||
|
||||
// Switch to a drive. Returns true if valid.
|
||||
static bool switch_drive(int drive) {
|
||||
// Validate drive exists by trying to readdir its root
|
||||
char path[8];
|
||||
build_drive_path(drive, "", path, sizeof(path));
|
||||
const char* entries[1];
|
||||
if (montauk::readdir(path, entries, 1) < 0) return false;
|
||||
current_drive = drive;
|
||||
cwd[0] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cmd_cd(const char* arg) {
|
||||
arg = skip_spaces(arg);
|
||||
|
||||
// Strip trailing slashes from argument (ls shows dirs as "www/", user may type that)
|
||||
static char argBuf[128];
|
||||
int aLen = 0;
|
||||
while (arg[aLen] && aLen < 127) { argBuf[aLen] = arg[aLen]; aLen++; }
|
||||
argBuf[aLen] = '\0';
|
||||
while (aLen > 0 && argBuf[aLen - 1] == '/') argBuf[--aLen] = '\0';
|
||||
arg = argBuf;
|
||||
|
||||
// cd or cd / -> go to root
|
||||
if (*arg == '\0' || streq(arg, "/")) {
|
||||
cwd[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// cd .. -> go up one level
|
||||
if (streq(arg, "..")) {
|
||||
int len = slen(cwd);
|
||||
int last = -1;
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (cwd[i] == '/') last = i;
|
||||
}
|
||||
if (last >= 0) {
|
||||
cwd[last] = '\0';
|
||||
} else {
|
||||
cwd[0] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// cd /path -> absolute path from root
|
||||
if (arg[0] == '/') {
|
||||
arg++;
|
||||
if (*arg == '\0') { cwd[0] = '\0'; return; }
|
||||
char path[128];
|
||||
build_dir_path(arg, path, sizeof(path));
|
||||
const char* entries[1];
|
||||
if (montauk::readdir(path, entries, 1) < 0) {
|
||||
montauk::print("cd: no such directory: ");
|
||||
montauk::print(arg);
|
||||
montauk::putchar('\n');
|
||||
return;
|
||||
}
|
||||
scopy(cwd, arg, sizeof(cwd));
|
||||
return;
|
||||
}
|
||||
|
||||
// cd N:/ or cd N:/path -> switch drive and optionally cd into path
|
||||
if (has_drive_prefix(arg)) {
|
||||
int drive = parse_drive_prefix(arg);
|
||||
int plen = drive_prefix_len(arg);
|
||||
const char* rel = arg + plen; // points at ":/" or ":"
|
||||
if (*rel == '/') rel++; // skip the '/'
|
||||
|
||||
// Validate drive exists
|
||||
char rootPath[8];
|
||||
build_drive_path(drive, "", rootPath, sizeof(rootPath));
|
||||
const char* rootEntries[1];
|
||||
if (montauk::readdir(rootPath, rootEntries, 1) < 0) {
|
||||
montauk::print("cd: no such drive: ");
|
||||
montauk::print(arg);
|
||||
montauk::putchar('\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's a path after the drive, validate it
|
||||
if (*rel != '\0') {
|
||||
char path[128];
|
||||
build_drive_path(drive, rel, path, sizeof(path));
|
||||
const char* entries[1];
|
||||
if (montauk::readdir(path, entries, 1) < 0) {
|
||||
montauk::print("cd: no such directory: ");
|
||||
montauk::print(arg);
|
||||
montauk::putchar('\n');
|
||||
return;
|
||||
}
|
||||
current_drive = drive;
|
||||
scopy(cwd, rel, sizeof(cwd));
|
||||
} else {
|
||||
current_drive = drive;
|
||||
cwd[0] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Build target directory path (relative to CWD)
|
||||
char target[128];
|
||||
if (cwd[0]) {
|
||||
scopy(target, cwd, sizeof(target));
|
||||
scat(target, "/", sizeof(target));
|
||||
scat(target, arg, sizeof(target));
|
||||
} else {
|
||||
scopy(target, arg, sizeof(target));
|
||||
}
|
||||
|
||||
// Validate: try readdir on the target
|
||||
char path[128];
|
||||
build_dir_path(target, path, sizeof(path));
|
||||
const char* entries[1];
|
||||
int count = montauk::readdir(path, entries, 1);
|
||||
if (count < 0) {
|
||||
montauk::print("cd: no such directory: ");
|
||||
montauk::print(arg);
|
||||
montauk::putchar('\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set cwd
|
||||
scopy(cwd, target, sizeof(cwd));
|
||||
}
|
||||
|
||||
// ---- Builtin: man ----
|
||||
|
||||
static void cmd_man(const char* arg) {
|
||||
arg = skip_spaces(arg);
|
||||
if (*arg == '\0') {
|
||||
montauk::print("Usage: man <topic>\n");
|
||||
montauk::print(" man <section> <topic>\n");
|
||||
montauk::print("Try: man intro\n");
|
||||
return;
|
||||
}
|
||||
|
||||
int pid = montauk::spawn("0:/os/man.elf", arg);
|
||||
if (pid < 0) {
|
||||
montauk::print("Error: failed to start man viewer\n");
|
||||
} else {
|
||||
montauk::waitpid(pid);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- External command execution ----
|
||||
|
||||
// Try to spawn an ELF at the given path. Returns true on success.
|
||||
static bool try_exec(const char* path, const char* args) {
|
||||
// Check if the file exists before asking the kernel to load it
|
||||
int h = montauk::open(path);
|
||||
if (h < 0) return false;
|
||||
montauk::close(h);
|
||||
|
||||
int pid = montauk::spawn(path, args);
|
||||
if (pid < 0) return false;
|
||||
montauk::waitpid(pid);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve arguments: expand relative file paths against CWD.
|
||||
// Tokens that already have a drive prefix (e.g. "0:/foo") are left as-is.
|
||||
// Everything before the first space-delimited token that looks like a path
|
||||
// option (starts with '-') is also left as-is.
|
||||
static void resolve_args(const char* args, char* out, int outMax) {
|
||||
if (!args || !args[0]) { out[0] = '\0'; return; }
|
||||
|
||||
int o = 0;
|
||||
const char* p = args;
|
||||
|
||||
while (*p && o < outMax - 1) {
|
||||
// Skip spaces, copy them through
|
||||
while (*p == ' ' && o < outMax - 1) { out[o++] = *p++; }
|
||||
if (!*p) break;
|
||||
|
||||
// Extract the token
|
||||
const char* tokStart = p;
|
||||
int tokLen = 0;
|
||||
while (p[tokLen] && p[tokLen] != ' ') tokLen++;
|
||||
|
||||
// Decide whether to resolve this token as a path.
|
||||
// Don't resolve if it already has a drive prefix, or starts with '-'
|
||||
bool resolve = (cwd[0] || current_drive != 0) && !has_drive_prefix(tokStart) && tokStart[0] != '-';
|
||||
|
||||
if (resolve) {
|
||||
// Build candidate resolved path and check if the file exists
|
||||
char candidate[256];
|
||||
int r = 0;
|
||||
if (current_drive >= 10) candidate[r++] = '0' + current_drive / 10;
|
||||
candidate[r++] = '0' + current_drive % 10;
|
||||
candidate[r++] = ':'; candidate[r++] = '/';
|
||||
int j = 0;
|
||||
while (cwd[j] && r < 255) candidate[r++] = cwd[j++];
|
||||
if (cwd[0] && r < 255) candidate[r++] = '/';
|
||||
for (int k = 0; k < tokLen && r < 255; k++) candidate[r++] = tokStart[k];
|
||||
candidate[r] = '\0';
|
||||
|
||||
// Use the resolved path if the file exists. If it doesn't
|
||||
// exist, still use the resolved path when the token looks
|
||||
// like a filename (contains a dot) so that programs creating
|
||||
// new files get the correct drive/directory prefix.
|
||||
int h = montauk::open(candidate);
|
||||
if (h >= 0) {
|
||||
montauk::close(h);
|
||||
for (int k = 0; k < r && o < outMax - 1; k++) out[o++] = candidate[k];
|
||||
} else {
|
||||
bool looksLikeFile = false;
|
||||
for (int k = 0; k < tokLen; k++) {
|
||||
if (tokStart[k] == '.') { looksLikeFile = true; break; }
|
||||
}
|
||||
if (looksLikeFile) {
|
||||
for (int k = 0; k < r && o < outMax - 1; k++) out[o++] = candidate[k];
|
||||
} else {
|
||||
for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int k = 0; k < tokLen && o < outMax - 1; k++) out[o++] = tokStart[k];
|
||||
}
|
||||
|
||||
p = tokStart + tokLen;
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
static void exec_external(const char* cmd, const char* args) {
|
||||
char path[256];
|
||||
|
||||
// Resolve arguments against CWD so external programs get full VFS paths
|
||||
char resolvedArgs[512];
|
||||
resolve_args(args, resolvedArgs, sizeof(resolvedArgs));
|
||||
const char* finalArgs = resolvedArgs[0] ? resolvedArgs : nullptr;
|
||||
|
||||
// Always search drive 0 for system commands (os/, games/)
|
||||
// 1. Try 0:/os/<cmd>.elf
|
||||
scopy(path, "0:/os/", sizeof(path));
|
||||
scat(path, cmd, sizeof(path));
|
||||
scat(path, ".elf", sizeof(path));
|
||||
if (try_exec(path, finalArgs)) return;
|
||||
|
||||
// 2. Try 0:/games/<cmd>.elf
|
||||
scopy(path, "0:/games/", sizeof(path));
|
||||
scat(path, cmd, sizeof(path));
|
||||
scat(path, ".elf", sizeof(path));
|
||||
if (try_exec(path, finalArgs)) return;
|
||||
|
||||
// 3. Try N:/<cwd>/<cmd>.elf on current drive (if cwd is set)
|
||||
if (cwd[0]) {
|
||||
build_drive_path(current_drive, "", path, sizeof(path));
|
||||
scat(path, cwd, sizeof(path));
|
||||
scat(path, "/", sizeof(path));
|
||||
scat(path, cmd, sizeof(path));
|
||||
scat(path, ".elf", sizeof(path));
|
||||
if (try_exec(path, finalArgs)) return;
|
||||
}
|
||||
|
||||
// 4. Try N:/<cmd>.elf on current drive
|
||||
build_drive_path(current_drive, "", path, sizeof(path));
|
||||
scat(path, cmd, sizeof(path));
|
||||
scat(path, ".elf", sizeof(path));
|
||||
if (try_exec(path, finalArgs)) return;
|
||||
|
||||
// 5. If on a non-zero drive, also try 0:/<cmd>.elf
|
||||
if (current_drive != 0) {
|
||||
scopy(path, "0:/", sizeof(path));
|
||||
scat(path, cmd, sizeof(path));
|
||||
scat(path, ".elf", sizeof(path));
|
||||
if (try_exec(path, finalArgs)) return;
|
||||
}
|
||||
|
||||
// Not found
|
||||
montauk::print(cmd);
|
||||
montauk::print(": command not found\n");
|
||||
}
|
||||
|
||||
// ---- Command dispatch ----
|
||||
|
||||
static void process_command(const char* line) {
|
||||
static int process_command(const char* line) {
|
||||
line = skip_spaces(line);
|
||||
if (*line == '\0') return;
|
||||
if (*line == '\0') return 0;
|
||||
|
||||
// Variable assignment: NAME=VALUE
|
||||
{
|
||||
int eq = -1;
|
||||
bool valid = (line[0] >= 'A' && line[0] <= 'Z') ||
|
||||
(line[0] >= 'a' && line[0] <= 'z') || line[0] == '_';
|
||||
if (valid) {
|
||||
for (int i = 1; line[i]; i++) {
|
||||
if (line[i] == '=') { eq = i; break; }
|
||||
if (!is_var_char(line[i])) break;
|
||||
}
|
||||
}
|
||||
if (eq > 0) {
|
||||
char name[32];
|
||||
int nlen = eq < 31 ? eq : 31;
|
||||
for (int i = 0; i < nlen; i++) name[i] = line[i];
|
||||
name[nlen] = '\0';
|
||||
const char* val = line + eq + 1;
|
||||
int vlen = slen(val);
|
||||
char vbuf[128];
|
||||
if (vlen >= 2 && ((val[0] == '"' && val[vlen-1] == '"') ||
|
||||
(val[0] == '\'' && val[vlen-1] == '\''))) {
|
||||
int j = 0;
|
||||
for (int i = 1; i < vlen - 1 && j < 127; i++) vbuf[j++] = val[i];
|
||||
vbuf[j] = '\0';
|
||||
var_set(name, vbuf);
|
||||
} else {
|
||||
var_set(name, val);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command name and arguments
|
||||
char cmd[128];
|
||||
@@ -543,26 +150,174 @@ static void process_command(const char* line) {
|
||||
montauk::print("No such drive: ");
|
||||
montauk::print(cmd);
|
||||
montauk::print("/\n");
|
||||
return 1;
|
||||
}
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Builtins
|
||||
if (streq(cmd, "help")) {
|
||||
cmd_help();
|
||||
} else if (streq(cmd, "ls")) {
|
||||
cmd_ls(args ? args : "");
|
||||
} else if (streq(cmd, "cd")) {
|
||||
cmd_cd(args ? args : "");
|
||||
} else if (streq(cmd, "man")) {
|
||||
cmd_man(args ? args : "");
|
||||
} else if (streq(cmd, "exit")) {
|
||||
montauk::print("Goodbye.\n");
|
||||
montauk::exit(0);
|
||||
} else {
|
||||
// External command -- pass full argument string
|
||||
exec_external(cmd, args);
|
||||
if (streq(cmd, "help")) { cmd_help(); return 0; }
|
||||
if (streq(cmd, "ls")) { cmd_ls(args ? args : ""); return 0; }
|
||||
if (streq(cmd, "cd")) { return cmd_cd(args ? args : ""); }
|
||||
if (streq(cmd, "man")) { return cmd_man(args ? args : ""); }
|
||||
if (streq(cmd, "true")) { return 0; }
|
||||
if (streq(cmd, "false")) { return 1; }
|
||||
|
||||
if (streq(cmd, "pwd")) {
|
||||
char path[128];
|
||||
build_dir_path(cwd, path, sizeof(path));
|
||||
montauk::print(path);
|
||||
montauk::putchar('\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (streq(cmd, "echo")) {
|
||||
if (!args) {
|
||||
montauk::putchar('\n');
|
||||
return 0;
|
||||
}
|
||||
bool no_newline = false;
|
||||
if (starts_with(args, "-n ")) {
|
||||
no_newline = true;
|
||||
args = skip_spaces(args + 3);
|
||||
} else if (streq(args, "-n")) {
|
||||
return 0;
|
||||
}
|
||||
montauk::print(args);
|
||||
if (!no_newline) montauk::putchar('\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (streq(cmd, "set")) {
|
||||
if (!args) {
|
||||
// List all variables
|
||||
if (session_user[0]) {
|
||||
montauk::print("USER=");
|
||||
montauk::print(session_user);
|
||||
montauk::putchar('\n');
|
||||
}
|
||||
if (session_home[0]) {
|
||||
montauk::print("HOME=");
|
||||
montauk::print(session_home);
|
||||
montauk::putchar('\n');
|
||||
}
|
||||
char path[128];
|
||||
build_dir_path(cwd, path, sizeof(path));
|
||||
montauk::print("PWD=");
|
||||
montauk::print(path);
|
||||
montauk::putchar('\n');
|
||||
int vc = var_user_count();
|
||||
for (int j = 0; j < vc; j++) {
|
||||
montauk::print(var_user_name(j));
|
||||
montauk::putchar('=');
|
||||
montauk::print(var_user_value(j));
|
||||
montauk::putchar('\n');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// set VAR=value
|
||||
int eq = -1;
|
||||
for (int j = 0; args[j]; j++) {
|
||||
if (args[j] == '=') { eq = j; break; }
|
||||
}
|
||||
if (eq > 0) {
|
||||
char name[32];
|
||||
int nlen = eq < 31 ? eq : 31;
|
||||
for (int j = 0; j < nlen; j++) name[j] = args[j];
|
||||
name[nlen] = '\0';
|
||||
var_set(name, args + eq + 1);
|
||||
return 0;
|
||||
}
|
||||
// set VAR (show value)
|
||||
const char* val = var_get(args);
|
||||
if (val) {
|
||||
montauk::print(args);
|
||||
montauk::putchar('=');
|
||||
montauk::print(val);
|
||||
montauk::putchar('\n');
|
||||
} else {
|
||||
montauk::print(args);
|
||||
montauk::print(": not set\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (streq(cmd, "unset")) {
|
||||
if (!args) {
|
||||
montauk::print("Usage: unset <variable>\n");
|
||||
return 1;
|
||||
}
|
||||
var_unset(args);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (streq(cmd, "exit")) {
|
||||
montauk::print("Goodbye.\n");
|
||||
montauk::exit(last_exit);
|
||||
}
|
||||
|
||||
// External command
|
||||
return exec_external(cmd, args);
|
||||
}
|
||||
|
||||
// ---- Command line execution with chaining ----
|
||||
|
||||
static void execute_line(const char* raw) {
|
||||
// Step 1: expand tilde
|
||||
char texp[512];
|
||||
expand_tilde(raw, texp, sizeof(texp));
|
||||
|
||||
// Step 2: expand variables
|
||||
char expanded[512];
|
||||
expand_vars(texp, expanded, sizeof(expanded));
|
||||
|
||||
// Step 3: strip comments
|
||||
strip_comment(expanded);
|
||||
|
||||
// Step 4: split on ;, &&, || and execute with chaining logic
|
||||
const char* p = expanded;
|
||||
int prev = 0;
|
||||
enum { OP_NONE, OP_SEMI, OP_AND, OP_OR } pending = OP_NONE;
|
||||
|
||||
while (true) {
|
||||
p = skip_spaces(p);
|
||||
if (!*p) break;
|
||||
|
||||
// Extract next command segment
|
||||
char seg[256];
|
||||
int si = 0;
|
||||
int op = OP_NONE;
|
||||
bool in_sq = false, in_dq = false;
|
||||
|
||||
while (*p && si < 255) {
|
||||
if (*p == '\'' && !in_dq) { in_sq = !in_sq; seg[si++] = *p++; continue; }
|
||||
if (*p == '"' && !in_sq) { in_dq = !in_dq; seg[si++] = *p++; continue; }
|
||||
if (!in_sq && !in_dq) {
|
||||
if (*p == ';') { op = OP_SEMI; p++; break; }
|
||||
if (*p == '&' && p[1] == '&') { op = OP_AND; p += 2; break; }
|
||||
if (*p == '|' && p[1] == '|') { op = OP_OR; p += 2; break; }
|
||||
}
|
||||
seg[si++] = *p++;
|
||||
}
|
||||
seg[si] = '\0';
|
||||
|
||||
// Trim trailing spaces
|
||||
while (si > 0 && seg[si - 1] == ' ') seg[--si] = '\0';
|
||||
|
||||
// Decide whether to run based on pending operator
|
||||
bool run = true;
|
||||
if (pending == OP_AND && prev != 0) run = false;
|
||||
if (pending == OP_OR && prev == 0) run = false;
|
||||
|
||||
if (run && seg[0]) {
|
||||
prev = process_command(seg);
|
||||
}
|
||||
|
||||
pending = (decltype(pending))op;
|
||||
if (op == OP_NONE) break;
|
||||
}
|
||||
|
||||
last_exit = prev;
|
||||
}
|
||||
|
||||
// ---- Arrow key scancodes ----
|
||||
@@ -575,17 +330,26 @@ static constexpr uint8_t SC_RIGHT = 0x4D;
|
||||
// ---- Entry point ----
|
||||
|
||||
extern "C" void _start() {
|
||||
read_session();
|
||||
|
||||
montauk::print("\n");
|
||||
montauk::print(" MontaukOS\n");
|
||||
montauk::print(" Copyright (c) 2025-2026 Daniel Hammer\n");
|
||||
montauk::print("\n");
|
||||
|
||||
if (session_user[0]) {
|
||||
montauk::print(" Logged in as ");
|
||||
montauk::print(session_user);
|
||||
montauk::putchar('\n');
|
||||
montauk::print("\n");
|
||||
}
|
||||
|
||||
montauk::print(" Type 'help' for available commands.\n");
|
||||
montauk::print("\n");
|
||||
|
||||
char line[256];
|
||||
int pos = 0;
|
||||
int hist_nav = -1; // -1 = not navigating history
|
||||
int hist_nav = -1;
|
||||
|
||||
prompt();
|
||||
|
||||
@@ -603,7 +367,6 @@ extern "C" void _start() {
|
||||
// Arrow keys: ascii == 0, check scancode
|
||||
if (ev.ascii == 0) {
|
||||
if (ev.scancode == SC_UP) {
|
||||
// Navigate to older history entry
|
||||
int next = hist_nav + 1;
|
||||
const char* entry = history_get(next);
|
||||
if (entry) {
|
||||
@@ -611,7 +374,6 @@ extern "C" void _start() {
|
||||
replace_line(line, &pos, entry);
|
||||
}
|
||||
} else if (ev.scancode == SC_DOWN) {
|
||||
// Navigate to newer history entry
|
||||
if (hist_nav > 0) {
|
||||
hist_nav--;
|
||||
const char* entry = history_get(hist_nav);
|
||||
@@ -619,14 +381,12 @@ extern "C" void _start() {
|
||||
replace_line(line, &pos, entry);
|
||||
}
|
||||
} else if (hist_nav == 0) {
|
||||
// Back to empty line
|
||||
hist_nav = -1;
|
||||
erase_input(pos);
|
||||
pos = 0;
|
||||
line[0] = '\0';
|
||||
}
|
||||
}
|
||||
// Left/Right arrows: ignore for now (no cursor movement within line)
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -634,7 +394,7 @@ extern "C" void _start() {
|
||||
montauk::putchar('\n');
|
||||
line[pos] = '\0';
|
||||
history_add(line);
|
||||
process_command(line);
|
||||
execute_line(line);
|
||||
pos = 0;
|
||||
hist_nav = -1;
|
||||
prompt();
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* shell.h
|
||||
* Shared declarations for the MontaukOS interactive shell
|
||||
* Copyright (c) 2025-2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/config.h>
|
||||
|
||||
using montauk::slen;
|
||||
using montauk::streq;
|
||||
using montauk::starts_with;
|
||||
using montauk::skip_spaces;
|
||||
|
||||
// ---- Inline string helpers ----
|
||||
|
||||
inline void scopy(char* dst, const char* src, int maxLen) {
|
||||
int i = 0;
|
||||
while (src[i] && i < maxLen - 1) { dst[i] = src[i]; i++; }
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
inline void scat(char* dst, const char* src, int maxLen) {
|
||||
int dLen = slen(dst);
|
||||
int i = 0;
|
||||
while (src[i] && dLen + i < maxLen - 1) {
|
||||
dst[dLen + i] = src[i];
|
||||
i++;
|
||||
}
|
||||
dst[dLen + i] = '\0';
|
||||
}
|
||||
|
||||
inline void int_to_str(int n, char* buf, int sz) {
|
||||
if (n == 0) { buf[0] = '0'; buf[1] = '\0'; return; }
|
||||
bool neg = false;
|
||||
unsigned int u;
|
||||
if (n < 0) { neg = true; u = (unsigned)(-n); } else { u = (unsigned)n; }
|
||||
char tmp[12];
|
||||
int i = 0;
|
||||
while (u > 0) { tmp[i++] = '0' + u % 10; u /= 10; }
|
||||
int o = 0;
|
||||
if (neg && o < sz - 1) buf[o++] = '-';
|
||||
while (i > 0 && o < sz - 1) buf[o++] = tmp[--i];
|
||||
buf[o] = '\0';
|
||||
}
|
||||
|
||||
// ---- Shared state (defined in main.cpp) ----
|
||||
|
||||
extern char cwd[128];
|
||||
extern int current_drive;
|
||||
extern int last_exit;
|
||||
extern char session_user[32];
|
||||
extern char session_home[64];
|
||||
|
||||
// ---- Inline path helpers ----
|
||||
|
||||
inline void build_drive_path(int drive, const char* dir, char* out, int outMax) {
|
||||
int i = 0;
|
||||
if (drive >= 10) { out[i++] = '0' + drive / 10; }
|
||||
out[i++] = '0' + drive % 10;
|
||||
out[i++] = ':'; out[i++] = '/';
|
||||
if (dir && dir[0]) {
|
||||
int j = 0;
|
||||
while (dir[j] && i < outMax - 1) out[i++] = dir[j++];
|
||||
}
|
||||
out[i] = '\0';
|
||||
}
|
||||
|
||||
inline void build_dir_path(const char* dir, char* out, int outMax) {
|
||||
build_drive_path(current_drive, dir, out, outMax);
|
||||
}
|
||||
|
||||
inline int parse_drive_prefix(const char* s) {
|
||||
if (s[0] < '0' || s[0] > '9') return -1;
|
||||
int n = s[0] - '0';
|
||||
if (s[1] >= '0' && s[1] <= '9') {
|
||||
n = n * 10 + (s[1] - '0');
|
||||
if (s[2] != ':') return -1;
|
||||
} else if (s[1] == ':') {
|
||||
// single digit drive
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
inline int drive_prefix_len(const char* s) {
|
||||
if (s[1] >= '0' && s[1] <= '9') return 3;
|
||||
return 2;
|
||||
}
|
||||
|
||||
inline bool has_drive_prefix(const char* s) {
|
||||
return parse_drive_prefix(s) >= 0;
|
||||
}
|
||||
|
||||
// ---- Variables (vars.cpp) ----
|
||||
|
||||
void var_set(const char* name, const char* value);
|
||||
void var_unset(const char* name);
|
||||
const char* var_get(const char* name);
|
||||
bool is_var_char(char c);
|
||||
void expand_vars(const char* in, char* out, int outMax);
|
||||
void expand_tilde(const char* in, char* out, int outMax);
|
||||
void strip_comment(char* line);
|
||||
int var_user_count();
|
||||
const char* var_user_name(int idx);
|
||||
const char* var_user_value(int idx);
|
||||
|
||||
// ---- Session (main.cpp) ----
|
||||
|
||||
void read_session();
|
||||
|
||||
// ---- Builtins (builtins.cpp) ----
|
||||
|
||||
void cmd_help();
|
||||
void cmd_ls(const char* arg);
|
||||
int cmd_cd(const char* arg);
|
||||
int cmd_man(const char* arg);
|
||||
bool switch_drive(int drive);
|
||||
|
||||
// ---- External execution (exec.cpp) ----
|
||||
|
||||
int exec_external(const char* cmd, const char* args);
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* vars.cpp
|
||||
* Shell variable storage, expansion, and tilde/comment processing
|
||||
* Copyright (c) 2025-2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "shell.h"
|
||||
|
||||
// ---- Storage ----
|
||||
|
||||
static constexpr int MAX_VARS = 32;
|
||||
|
||||
struct ShellVar {
|
||||
char name[32];
|
||||
char value[128];
|
||||
};
|
||||
|
||||
static ShellVar vars[MAX_VARS];
|
||||
static int var_count = 0;
|
||||
|
||||
// ---- Core operations ----
|
||||
|
||||
void var_set(const char* name, const char* value) {
|
||||
for (int i = 0; i < var_count; i++) {
|
||||
if (streq(vars[i].name, name)) {
|
||||
scopy(vars[i].value, value, sizeof(vars[i].value));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (var_count < MAX_VARS) {
|
||||
scopy(vars[var_count].name, name, sizeof(vars[var_count].name));
|
||||
scopy(vars[var_count].value, value, sizeof(vars[var_count].value));
|
||||
var_count++;
|
||||
}
|
||||
}
|
||||
|
||||
void var_unset(const char* name) {
|
||||
for (int i = 0; i < var_count; i++) {
|
||||
if (streq(vars[i].name, name)) {
|
||||
for (int j = i; j < var_count - 1; j++) vars[j] = vars[j + 1];
|
||||
var_count--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get variable value. Built-in dynamic vars ($?, $PWD) are synthesized on
|
||||
// demand. Returns nullptr if not found. The returned pointer for dynamic
|
||||
// vars points to a static buffer overwritten on the next call.
|
||||
const char* var_get(const char* name) {
|
||||
// User-defined vars take priority
|
||||
for (int i = 0; i < var_count; i++) {
|
||||
if (streq(vars[i].name, name)) return vars[i].value;
|
||||
}
|
||||
// Synthesize built-in vars
|
||||
static char synth[128];
|
||||
if (streq(name, "?")) {
|
||||
int_to_str(last_exit, synth, sizeof(synth));
|
||||
return synth;
|
||||
}
|
||||
if (streq(name, "USER")) return session_user[0] ? session_user : nullptr;
|
||||
if (streq(name, "HOME")) return session_home[0] ? session_home : nullptr;
|
||||
if (streq(name, "PWD")) {
|
||||
build_dir_path(cwd, synth, sizeof(synth));
|
||||
return synth;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---- Helpers used by the set builtin (main.cpp) ----
|
||||
|
||||
// The set builtin needs to iterate user-defined vars and show built-in
|
||||
// vars. We expose a small iteration API rather than the raw array.
|
||||
|
||||
int var_user_count() { return var_count; }
|
||||
|
||||
const char* var_user_name(int idx) {
|
||||
return (idx >= 0 && idx < var_count) ? vars[idx].name : nullptr;
|
||||
}
|
||||
|
||||
const char* var_user_value(int idx) {
|
||||
return (idx >= 0 && idx < var_count) ? vars[idx].value : nullptr;
|
||||
}
|
||||
|
||||
// ---- Character classification ----
|
||||
|
||||
bool is_var_char(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_';
|
||||
}
|
||||
|
||||
// ---- Variable expansion ----
|
||||
|
||||
void expand_vars(const char* in, char* out, int outMax) {
|
||||
int o = 0;
|
||||
while (*in && o < outMax - 1) {
|
||||
if (*in == '\\' && in[1] == '$') {
|
||||
if (o < outMax - 1) out[o++] = '$';
|
||||
in += 2;
|
||||
continue;
|
||||
}
|
||||
if (*in == '$') {
|
||||
in++;
|
||||
char name[32];
|
||||
int n = 0;
|
||||
if (*in == '{') {
|
||||
in++;
|
||||
while (*in && *in != '}' && n < 31) name[n++] = *in++;
|
||||
if (*in == '}') in++;
|
||||
} else if (*in == '?') {
|
||||
name[n++] = '?';
|
||||
in++;
|
||||
} else {
|
||||
while (is_var_char(*in) && n < 31) name[n++] = *in++;
|
||||
}
|
||||
name[n] = '\0';
|
||||
if (n > 0) {
|
||||
const char* val = var_get(name);
|
||||
if (val) {
|
||||
while (*val && o < outMax - 1) out[o++] = *val++;
|
||||
}
|
||||
} else {
|
||||
if (o < outMax - 1) out[o++] = '$';
|
||||
}
|
||||
} else {
|
||||
out[o++] = *in++;
|
||||
}
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
// ---- Tilde expansion ----
|
||||
|
||||
void expand_tilde(const char* in, char* out, int outMax) {
|
||||
int o = 0;
|
||||
bool at_start = true;
|
||||
while (*in && o < outMax - 1) {
|
||||
if (at_start && *in == '~' && session_home[0]) {
|
||||
if (in[1] == '\0' || in[1] == '/' || in[1] == ' ') {
|
||||
const char* h = session_home;
|
||||
while (*h && o < outMax - 1) out[o++] = *h++;
|
||||
in++;
|
||||
} else {
|
||||
out[o++] = *in++;
|
||||
}
|
||||
} else {
|
||||
at_start = (*in == ' ');
|
||||
out[o++] = *in++;
|
||||
}
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
// ---- Comment stripping ----
|
||||
|
||||
void strip_comment(char* line) {
|
||||
bool in_sq = false, in_dq = false;
|
||||
for (int i = 0; line[i]; i++) {
|
||||
if (line[i] == '\'' && !in_dq) in_sq = !in_sq;
|
||||
else if (line[i] == '"' && !in_sq) in_dq = !in_dq;
|
||||
else if (line[i] == '#' && !in_sq && !in_dq) {
|
||||
line[i] = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* whoami - Print the current username
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/config.h>
|
||||
|
||||
extern "C" void _start() {
|
||||
auto doc = montauk::config::load("session");
|
||||
const char* name = doc.get_string("session.username", "");
|
||||
if (name[0]) {
|
||||
montauk::print(name);
|
||||
montauk::putchar('\n');
|
||||
} else {
|
||||
montauk::print("unknown\n");
|
||||
}
|
||||
doc.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -90,12 +90,14 @@ static void px_fill(uint32_t* px, int bw, int x, int y, int w, int h, Color c) {
|
||||
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
|
||||
int x1 = x + w, y1 = y + h;
|
||||
if (x1 > bw) x1 = bw;
|
||||
if (y1 > g_win_h) y1 = g_win_h;
|
||||
for (int row = y0; row < y1; row++)
|
||||
for (int col = x0; col < x1; col++)
|
||||
px[row * bw + col] = v;
|
||||
}
|
||||
|
||||
static void px_hline(uint32_t* px, int bw, int x, int y, int len, Color c) {
|
||||
if (y < 0 || y >= g_win_h) return;
|
||||
uint32_t v = c.to_pixel();
|
||||
int x1 = x + len;
|
||||
if (x < 0) x = 0;
|
||||
@@ -105,8 +107,12 @@ static void px_hline(uint32_t* px, int bw, int x, int y, int len, Color c) {
|
||||
}
|
||||
|
||||
static void px_vline(uint32_t* px, int bw, int x, int y, int len, Color c) {
|
||||
if (x < 0 || x >= bw) return;
|
||||
uint32_t v = c.to_pixel();
|
||||
for (int row = y; row < y + len; row++)
|
||||
int y1 = y + len;
|
||||
if (y < 0) y = 0;
|
||||
if (y1 > g_win_h) y1 = g_win_h;
|
||||
for (int row = y; row < y1; row++)
|
||||
px[row * bw + x] = v;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user