823 lines
27 KiB
C++
823 lines
27 KiB
C++
/*
|
|
* 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::setuser(pid, ls->username);
|
|
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::setuser(pid, ls->username);
|
|
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 for setup environment (installation medium / ramdisk boot).
|
|
// If setup.toml exists with environment.mode = "setup", skip login
|
|
// entirely and launch the desktop in a passwordless live session.
|
|
{
|
|
auto doc = montauk::config::load("setup");
|
|
const char* mode = doc.get_string("environment.mode", "");
|
|
if (montauk::streq(mode, "setup")) {
|
|
const char* user = doc.get_string("session.username", "liveuser");
|
|
const char* display = doc.get_string("session.display_name", "Live User");
|
|
const char* role = doc.get_string("session.role", "admin");
|
|
|
|
// Create a temporary user entry so the desktop and apps can
|
|
// query session info normally.
|
|
montauk::fmkdir("0:/users");
|
|
montauk::user::create_user(user, display, "", role);
|
|
montauk::user::set_session(user);
|
|
doc.destroy();
|
|
|
|
// Launch desktop directly -- no login required
|
|
int pid = montauk::spawn("0:/os/desktop.elf", user);
|
|
if (pid >= 0) {
|
|
montauk::setuser(pid, user);
|
|
montauk::waitpid(pid);
|
|
}
|
|
// Desktop exited (reboot/shutdown expected in setup mode).
|
|
// Clear session and fall through to normal login in case
|
|
// setup.toml was removed during installation.
|
|
montauk::user::clear_session();
|
|
} else {
|
|
doc.destroy();
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|