feat: Intel HDA audio driver, audio streaming syscalls, userspace Music app, fixes and improvements, rudimentary Bluetooth support

This commit is contained in:
2026-03-10 17:14:33 +01:00
parent 807c2602fe
commit 576ad34f95
58 changed files with 11275 additions and 137 deletions
+19 -4
View File
@@ -59,7 +59,7 @@ BINDIR := bin
PROGRAMS := $(notdir $(wildcard src/*))
# Programs with custom Makefiles (built separately).
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer desktop
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth desktop
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
@@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
# Home directory placeholder.
HOMEKEEP := $(BINDIR)/home/.keep
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer desktop icons fonts bearssl libc tls libjpeg install-apps
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth desktop icons fonts bearssl libc tls libjpeg install-apps
all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer doom desktop icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
all: bearssl libc libjpeg tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth doom desktop icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(HOMEKEEP)
# Build BearSSL static library (cross-compiled for freestanding x86_64).
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
@@ -152,6 +152,18 @@ devexplorer: libc
installer: libc
$(MAKE) -C src/installer
# Build volume control standalone GUI tool (depends on libc).
volume: libc
$(MAKE) -C src/volume
# Build music player standalone GUI tool (depends on libc).
music: libc
$(MAKE) -C src/music
# Build bluetooth manager standalone GUI tool (depends on libc).
bluetooth: libc
$(MAKE) -C src/bluetooth
# Build desktop via its own Makefile (depends on libc and libjpeg for wallpaper).
desktop: libc libjpeg
$(MAKE) -C src/desktop
@@ -169,7 +181,7 @@ doom:
$(MAKE) -C src/doom
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
install-apps: doom spreadsheet weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer
install-apps: doom spreadsheet weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music bluetooth
../scripts/install_apps.sh
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
@@ -216,3 +228,6 @@ clean:
$(MAKE) -C src/disks clean
$(MAKE) -C src/devexplorer clean
$(MAKE) -C src/installer clean
$(MAKE) -C src/volume clean
$(MAKE) -C src/music clean
$(MAKE) -C src/bluetooth clean
+52 -2
View File
@@ -100,6 +100,28 @@ namespace Montauk {
static constexpr uint64_t SYS_FSMOUNT = 75;
static constexpr uint64_t SYS_FSFORMAT = 76;
// Audio syscalls
static constexpr uint64_t SYS_AUDIOOPEN = 80;
static constexpr uint64_t SYS_AUDIOCLOSE = 81;
static constexpr uint64_t SYS_AUDIOWRITE = 82;
static constexpr uint64_t SYS_AUDIOCTL = 83;
// Bluetooth syscalls
static constexpr uint64_t SYS_BTSCAN = 84;
static constexpr uint64_t SYS_BTCONNECT = 85;
static constexpr uint64_t SYS_BTDISCONNECT = 86;
static constexpr uint64_t SYS_BTLIST = 87;
static constexpr uint64_t SYS_BTINFO = 88;
// Audio control commands (for SYS_AUDIOCTL)
static constexpr int AUDIO_CTL_SET_VOLUME = 0;
static constexpr int AUDIO_CTL_GET_VOLUME = 1;
static constexpr int AUDIO_CTL_GET_POS = 2;
static constexpr int AUDIO_CTL_PAUSE = 3;
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth connection status
static constexpr int SOCK_TCP = 1;
static constexpr int SOCK_UDP = 2;
@@ -188,8 +210,8 @@ namespace Montauk {
};
struct DiskInfo {
uint8_t port; // AHCI port index
uint8_t type; // 0=none, 1=SATA, 2=SATAPI
uint8_t port; // block device index
uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe
uint8_t sataGen; // SATA gen (1/2/3)
uint8_t _pad0;
uint64_t sectorCount; // Total user-addressable sectors
@@ -249,6 +271,34 @@ namespace Montauk {
char label[32]; // volume label
};
// Bluetooth scan result (returned by SYS_BTSCAN)
struct BtScanResult {
uint8_t bdAddr[6];
uint8_t _pad[2];
uint32_t classOfDevice;
int8_t rssi;
uint8_t _pad2[3];
char name[64];
};
// Bluetooth connected device info (returned by SYS_BTLIST)
struct BtDevInfo {
uint8_t bdAddr[6];
uint8_t connected;
uint8_t encrypted;
uint16_t handle;
uint8_t linkType;
uint8_t _pad;
};
// Bluetooth adapter info (returned by SYS_BTINFO)
struct BtAdapterInfo {
uint8_t bdAddr[6];
uint8_t initialized;
uint8_t scanning;
char name[64];
};
struct ProcInfo {
int32_t pid;
int32_t parentPid;
+7
View File
@@ -111,6 +111,13 @@ struct DesktopState {
int screen_w, screen_h;
// IDs of external windows we've sent a close event to but that haven't
// been destroyed yet by their owning process. Prevents the poll loop
// from re-creating them at the default position (visible flicker).
static constexpr int MAX_CLOSING = 8;
int closing_ext_ids[MAX_CLOSING];
int closing_ext_count;
DesktopSettings settings;
};
+1
View File
@@ -13,6 +13,7 @@ void *memcpy(void *dest, const void *src, size_t n);
void *memset(void *s, int c, size_t n);
void *memmove(void *dest, const void *src, size_t n);
int memcmp(const void *s1, const void *s2, size_t n);
void *memchr(const void *s, int c, size_t n);
size_t strlen(const char *s);
int strcmp(const char *s1, const char *s2);
+55
View File
@@ -336,6 +336,61 @@ namespace montauk {
return (int)syscall1(Montauk::SYS_FSFORMAT, (uint64_t)params);
}
// Audio
inline int audio_open(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
return (int)syscall3(Montauk::SYS_AUDIOOPEN, (uint64_t)sampleRate,
(uint64_t)channels, (uint64_t)bitsPerSample);
}
inline void audio_close(int handle) {
syscall1(Montauk::SYS_AUDIOCLOSE, (uint64_t)handle);
}
inline int audio_write(int handle, const void* data, uint32_t size) {
return (int)syscall3(Montauk::SYS_AUDIOWRITE, (uint64_t)handle,
(uint64_t)data, (uint64_t)size);
}
inline int audio_ctl(int handle, int cmd, int value) {
return (int)syscall3(Montauk::SYS_AUDIOCTL, (uint64_t)handle,
(uint64_t)cmd, (uint64_t)value);
}
inline int audio_set_volume(int handle, int percent) {
return audio_ctl(handle, Montauk::AUDIO_CTL_SET_VOLUME, percent);
}
inline int audio_get_volume(int handle) {
return audio_ctl(handle, Montauk::AUDIO_CTL_GET_VOLUME, 0);
}
inline int audio_get_pos(int handle) {
return audio_ctl(handle, Montauk::AUDIO_CTL_GET_POS, 0);
}
inline int audio_pause(int handle) {
return audio_ctl(handle, Montauk::AUDIO_CTL_PAUSE, 1);
}
inline int audio_resume(int handle) {
return audio_ctl(handle, Montauk::AUDIO_CTL_PAUSE, 0);
}
inline int audio_get_output(int handle) {
return audio_ctl(handle, Montauk::AUDIO_CTL_GET_OUTPUT, 0);
}
inline int audio_bt_status(int handle) {
return audio_ctl(handle, Montauk::AUDIO_CTL_BT_STATUS, 0);
}
// Bluetooth
inline int bt_scan(Montauk::BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
return (int)syscall3(Montauk::SYS_BTSCAN, (uint64_t)buf, (uint64_t)maxCount, (uint64_t)timeoutMs);
}
inline int bt_connect(const uint8_t* bdAddr) {
return (int)syscall1(Montauk::SYS_BTCONNECT, (uint64_t)bdAddr);
}
inline int bt_disconnect(const uint8_t* bdAddr) {
return (int)syscall1(Montauk::SYS_BTDISCONNECT, (uint64_t)bdAddr);
}
inline int bt_list(Montauk::BtDevInfo* buf, int maxCount) {
return (int)syscall2(Montauk::SYS_BTLIST, (uint64_t)buf, (uint64_t)maxCount);
}
inline int bt_info(Montauk::BtAdapterInfo* buf) {
return (int)syscall1(Montauk::SYS_BTINFO, (uint64_t)buf);
}
// Kernel introspection
inline void memstats(Montauk::MemStats* out) { syscall1(Montauk::SYS_MEMSTATS, (uint64_t)out); }
+10
View File
@@ -135,6 +135,16 @@ int memcmp(const void *s1, const void *s2, size_t n) {
return 0;
}
void *memchr(const void *s, int c, size_t n) {
const unsigned char *p = (const unsigned char *)s;
unsigned char uc = (unsigned char)c;
for (size_t i = 0; i < n; i++) {
if (p[i] == uc)
return (void *)(p + i);
}
return (void *)0;
}
size_t strlen(const char *s) {
size_t len = 0;
while (s[len]) len++;
Binary file not shown.
+86
View File
@@ -0,0 +1,86 @@
# Makefile for bluetooth (standalone Bluetooth Manager) 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
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
# ---- 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 \
-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
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
TARGET := $(BINDIR)/apps/bluetooth/bluetooth.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/apps/bluetooth
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
+670
View File
@@ -0,0 +1,670 @@
/*
* main.cpp
* MontaukOS Bluetooth Manager
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <string.h>
#include <stdio.h>
}
using namespace gui;
// ============================================================================
// Constants
// ============================================================================
static constexpr int WIN_W = 360;
static constexpr int WIN_H = 420;
static constexpr int FONT_SIZE = 18;
static constexpr int FONT_SIZE_SM = 14;
static constexpr int ROW_H = 56;
static constexpr int TAB_H = 36;
static constexpr int PAD = 16;
static constexpr int BTN_W = 90;
static constexpr int BTN_H = 28;
static constexpr int BTN_RAD = 6;
static constexpr int STATUS_H = 28;
static constexpr int MAX_SCAN = 16;
static constexpr int MAX_CONNECTED = 8;
static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color TEXT_COLOR = Color::from_rgb(0x33, 0x33, 0x33);
static constexpr Color DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66);
static constexpr Color ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0);
static constexpr Color ROW_HOVER = Color::from_rgb(0xF0, 0xF4, 0xFF);
static constexpr Color BORDER = Color::from_rgb(0xCC, 0xCC, 0xCC);
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color GREEN = Color::from_rgb(0x2E, 0xA0, 0x43);
static constexpr Color RED = Color::from_rgb(0xCC, 0x33, 0x33);
static constexpr Color TAB_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
static constexpr Color TAB_ACTIVE = Color::from_rgb(0x36, 0x7B, 0xF0);
static constexpr Color TAB_INACTIVE = Color::from_rgb(0x66, 0x66, 0x66);
// ============================================================================
// State
// ============================================================================
enum Tab { TAB_DEVICES = 0, TAB_SCAN = 1 };
static TrueTypeFont* g_font = nullptr;
static int g_win_w = WIN_W;
static int g_win_h = WIN_H;
static Tab g_tab = TAB_DEVICES;
static int g_hover_row = -1;
// Adapter info
static Montauk::BtAdapterInfo g_adapter;
static bool g_adapter_ok = false;
// Connected/paired devices
static Montauk::BtDevInfo g_devices[MAX_CONNECTED];
static char g_device_names[MAX_CONNECTED][64];
static int g_device_count = 0;
// Scan results
static Montauk::BtScanResult g_scan[MAX_SCAN];
static int g_scan_count = 0;
static bool g_scanning = false;
// Scroll
static int g_scroll = 0;
// Status message
static char g_status[80];
static uint64_t g_status_time = 0;
// ============================================================================
// Pixel helpers
// ============================================================================
static void px_fill(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, Color c) {
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
int x1 = x + w > bw ? bw : x + w;
int y1 = y + h > bh ? bh : y + h;
for (int row = y0; row < y1; row++)
for (int col = x0; col < x1; col++)
px[row * bw + col] = v;
}
static void px_fill_rounded(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, int r, Color c) {
uint32_t v = c.to_pixel();
for (int row = 0; row < h; row++) {
int dy = y + row;
if (dy < 0 || dy >= bh) continue;
for (int col = 0; col < w; col++) {
int dx = x + col;
if (dx < 0 || dx >= bw) continue;
bool skip = false;
int cx2, cy2;
if (col < r && row < r) { cx2 = r - col - 1; cy2 = r - row - 1; if (cx2*cx2 + cy2*cy2 >= r*r) skip = true; }
else if (col >= w - r && row < r) { cx2 = col - (w - r); cy2 = r - row - 1; if (cx2*cx2 + cy2*cy2 >= r*r) skip = true; }
else if (col < r && row >= h - r) { cx2 = r - col - 1; cy2 = row - (h - r); if (cx2*cx2 + cy2*cy2 >= r*r) skip = true; }
else if (col >= w - r && row >= h - r) { cx2 = col - (w - r); cy2 = row - (h - r); if (cx2*cx2 + cy2*cy2 >= r*r) skip = true; }
if (!skip) px[dy * bw + dx] = v;
}
}
}
static void px_hline(uint32_t* px, int bw, int bh, int x, int y, int w, Color c) {
if (y < 0 || y >= bh) return;
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x;
int x1 = x + w > bw ? bw : x + w;
for (int col = x0; col < x1; col++)
px[y * bw + col] = v;
}
static void px_circle(uint32_t* px, int bw, int bh,
int cx, int cy, int r, Color c) {
uint32_t v = c.to_pixel();
for (int dy = -r; dy <= r; dy++) {
int py = cy + dy;
if (py < 0 || py >= bh) continue;
for (int dx = -r; dx <= r; dx++) {
int ppx = cx + dx;
if (ppx < 0 || ppx >= bw) continue;
if (dx * dx + dy * dy <= r * r)
px[py * bw + ppx] = v;
}
}
}
static void px_text(uint32_t* px, int bw, int bh,
int x, int y, const char* text, Color c, int size = FONT_SIZE) {
if (g_font)
g_font->draw_to_buffer(px, bw, bh, x, y, text, c, size);
}
static int text_w(const char* text, int size = FONT_SIZE) {
return g_font ? g_font->measure_text(text, size) : 0;
}
static int font_h(int size = FONT_SIZE) {
if (!g_font) return 14;
auto* cache = g_font->get_cache(size);
return cache->ascent - cache->descent;
}
static void px_button(uint32_t* px, int bw, int bh,
int x, int y, int w, int h,
const char* label, Color bg, Color fg, int r = BTN_RAD) {
px_fill_rounded(px, bw, bh, x, y, w, h, r, bg);
int tw = text_w(label, FONT_SIZE_SM);
int fh = font_h(FONT_SIZE_SM);
px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2 - 1, label, fg, FONT_SIZE_SM);
}
// ============================================================================
// Bluetooth helpers
// ============================================================================
static void set_status(const char* msg) {
snprintf(g_status, sizeof(g_status), "%s", msg);
g_status_time = montauk::get_milliseconds();
}
static void refresh_adapter() {
montauk::memset(&g_adapter, 0, sizeof(g_adapter));
int r = montauk::bt_info(&g_adapter);
g_adapter_ok = (r >= 0 && g_adapter.initialized);
}
static void refresh_devices() {
g_device_count = montauk::bt_list(g_devices, MAX_CONNECTED);
if (g_device_count < 0) g_device_count = 0;
// Resolve names from scan results or use address string
for (int i = 0; i < g_device_count; i++) {
bool found = false;
for (int j = 0; j < g_scan_count; j++) {
if (memcmp(g_devices[i].bdAddr, g_scan[j].bdAddr, 6) == 0 && g_scan[j].name[0]) {
montauk::memcpy(g_device_names[i], g_scan[j].name, 64);
found = true;
break;
}
}
if (!found) {
snprintf(g_device_names[i], 64, "%02X:%02X:%02X:%02X:%02X:%02X",
g_devices[i].bdAddr[0], g_devices[i].bdAddr[1],
g_devices[i].bdAddr[2], g_devices[i].bdAddr[3],
g_devices[i].bdAddr[4], g_devices[i].bdAddr[5]);
}
}
}
static void do_scan() {
g_scanning = true;
g_scan_count = 0;
set_status("Scanning...");
}
static void finish_scan() {
int r = montauk::bt_scan(g_scan, MAX_SCAN, 3000);
g_scan_count = r > 0 ? r : 0;
g_scanning = false;
char msg[64];
snprintf(msg, sizeof(msg), "Found %d device%s", g_scan_count, g_scan_count == 1 ? "" : "s");
set_status(msg);
}
static void do_connect(const uint8_t* addr) {
int r = montauk::bt_connect(addr);
if (r >= 0) {
set_status("Connected");
} else {
set_status("Connection failed");
}
refresh_devices();
}
static void do_disconnect(const uint8_t* addr) {
int r = montauk::bt_disconnect(addr);
if (r >= 0) {
set_status("Disconnected");
} else {
set_status("Disconnect failed");
}
refresh_devices();
}
static bool is_connected(const uint8_t* addr) {
for (int i = 0; i < g_device_count; i++) {
if (memcmp(g_devices[i].bdAddr, addr, 6) == 0 && g_devices[i].connected)
return true;
}
return false;
}
// ============================================================================
// Format helpers
// ============================================================================
static const char* device_class_str(uint32_t cod) {
uint32_t major = (cod >> 8) & 0x1F;
switch (major) {
case 1: return "Computer";
case 2: return "Phone";
case 3: return "Network";
case 4: return "Audio/Video";
case 5: return "Peripheral";
case 6: return "Imaging";
case 7: return "Wearable";
default: return "Device";
}
}
static void format_addr(char* buf, int len, const uint8_t* addr) {
snprintf(buf, len, "%02X:%02X:%02X:%02X:%02X:%02X",
addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
}
static const char* rssi_bar(int8_t rssi) {
if (rssi >= -50) return "Strong";
if (rssi >= -70) return "Good";
if (rssi >= -85) return "Weak";
return "Very weak";
}
// ============================================================================
// Render
// ============================================================================
static void render(uint32_t* pixels) {
int W = g_win_w;
int H = g_win_h;
// Background
px_fill(pixels, W, H, 0, 0, W, H, BG_COLOR);
// Tab bar
int tab_y = 0;
px_fill(pixels, W, H, 0, tab_y, W, TAB_H, TAB_BG);
px_hline(pixels, W, H, 0, tab_y + TAB_H - 1, W, BORDER);
int tab_w = W / 2;
const char* tab_labels[] = { "Devices", "Scan" };
for (int i = 0; i < 2; i++) {
// Active tab gets white background to merge with content area
if (g_tab == i)
px_fill(pixels, W, H, i * tab_w, tab_y, tab_w, TAB_H, BG_COLOR);
Color tc = (g_tab == i) ? TAB_ACTIVE : TAB_INACTIVE;
int tw = text_w(tab_labels[i], FONT_SIZE);
int tx = i * tab_w + (tab_w - tw) / 2;
px_text(pixels, W, H, tx, tab_y + (TAB_H - font_h(FONT_SIZE)) / 2,
tab_labels[i], tc, FONT_SIZE);
// Active indicator underline
if (g_tab == i) {
px_fill(pixels, W, H, i * tab_w + 4, tab_y + TAB_H - 3, tab_w - 8, 3, TAB_ACTIVE);
}
}
int content_y = tab_y + TAB_H;
int content_h = H - content_y;
// Status bar at bottom
int list_h = content_h - STATUS_H;
if (g_tab == TAB_DEVICES) {
// Connected devices list
if (g_device_count == 0) {
const char* msg = "No connected devices";
int mw = text_w(msg, FONT_SIZE);
px_text(pixels, W, H, (W - mw) / 2, content_y + list_h / 2 - font_h(FONT_SIZE) / 2,
msg, DIM_TEXT, FONT_SIZE);
} else {
for (int i = 0; i < g_device_count; i++) {
int ry = content_y + i * ROW_H - g_scroll;
if (ry + ROW_H < content_y || ry >= content_y + list_h) continue;
// Hover highlight
if (g_hover_row == i)
px_fill(pixels, W, H, 0, ry, W, ROW_H, ROW_HOVER);
// Connected indicator dot
Color dot_c = g_devices[i].connected ? GREEN : DIM_TEXT;
px_circle(pixels, W, H, PAD + 6, ry + ROW_H / 2, 5, dot_c);
// Device name
px_text(pixels, W, H, PAD + 20, ry + 8,
g_device_names[i], TEXT_COLOR, FONT_SIZE);
// Address below name
char addr_str[24];
format_addr(addr_str, sizeof(addr_str), g_devices[i].bdAddr);
px_text(pixels, W, H, PAD + 20, ry + 8 + font_h(FONT_SIZE) + 2,
addr_str, DIM_TEXT, FONT_SIZE_SM);
// Disconnect button
if (g_devices[i].connected) {
int bx = W - PAD - BTN_W;
int by = ry + (ROW_H - BTN_H) / 2;
px_button(pixels, W, H, bx, by, BTN_W, BTN_H,
"Disconnect", RED, WHITE);
}
// Divider
px_hline(pixels, W, H, PAD, ry + ROW_H - 1, W - 2 * PAD, BORDER);
}
}
} else {
// Scan tab
// Scan button at top of content area
int scan_btn_y = content_y + 12;
int scan_btn_w = 100;
int scan_btn_x = (W - scan_btn_w) / 2;
if (g_scanning) {
px_button(pixels, W, H, scan_btn_x, scan_btn_y, scan_btn_w, BTN_H,
"Scanning...", BORDER, DIM_TEXT);
} else {
px_button(pixels, W, H, scan_btn_x, scan_btn_y, scan_btn_w, BTN_H,
"Scan", ACCENT, WHITE);
}
int list_top = scan_btn_y + BTN_H + 12;
int scan_list_h = content_y + list_h - list_top;
if (g_scan_count == 0 && !g_scanning) {
const char* msg = "Press Scan to find devices";
int mw = text_w(msg, FONT_SIZE);
px_text(pixels, W, H, (W - mw) / 2, list_top + scan_list_h / 2 - font_h(FONT_SIZE) / 2,
msg, DIM_TEXT, FONT_SIZE);
} else {
for (int i = 0; i < g_scan_count; i++) {
int ry = list_top + i * ROW_H - g_scroll;
if (ry + ROW_H < list_top || ry >= list_top + scan_list_h) continue;
// Hover highlight
if (g_hover_row == i)
px_fill(pixels, W, H, 0, ry, W, ROW_H, ROW_HOVER);
// Device type indicator
const char* type_str = device_class_str(g_scan[i].classOfDevice);
px_circle(pixels, W, H, PAD + 6, ry + ROW_H / 2, 5, ACCENT);
// Device name (or address if unnamed)
const char* display_name = g_scan[i].name[0] ? g_scan[i].name : "Unknown Device";
px_text(pixels, W, H, PAD + 20, ry + 4,
display_name, TEXT_COLOR, FONT_SIZE);
// Type + signal below name
char detail[80];
char addr_str[24];
format_addr(addr_str, sizeof(addr_str), g_scan[i].bdAddr);
snprintf(detail, sizeof(detail), "%s | %s | %s",
type_str, addr_str, rssi_bar(g_scan[i].rssi));
px_text(pixels, W, H, PAD + 20, ry + 4 + font_h(FONT_SIZE) + 2,
detail, DIM_TEXT, FONT_SIZE_SM);
// Connect/Disconnect button
bool conn = is_connected(g_scan[i].bdAddr);
int bx = W - PAD - BTN_W;
int by = ry + (ROW_H - BTN_H) / 2;
if (conn) {
px_button(pixels, W, H, bx, by, BTN_W, BTN_H,
"Disconnect", RED, WHITE);
} else {
px_button(pixels, W, H, bx, by, BTN_W, BTN_H,
"Connect", ACCENT, WHITE);
}
// Divider
px_hline(pixels, W, H, PAD, ry + ROW_H - 1, W - 2 * PAD, BORDER);
}
}
}
// Status bar
int status_y = H - STATUS_H;
px_fill(pixels, W, H, 0, status_y, W, STATUS_H, TAB_BG);
px_hline(pixels, W, H, 0, status_y, W, BORDER);
// Status bar content: adapter info on the left, status message on the right
uint64_t now = montauk::get_milliseconds();
int sy = status_y + (STATUS_H - font_h(FONT_SIZE_SM)) / 2;
if (g_adapter_ok) {
char adapter_info[96];
char addr_str[24];
format_addr(addr_str, sizeof(addr_str), g_adapter.bdAddr);
const char* name = g_adapter.name[0] ? g_adapter.name : "Adapter";
snprintf(adapter_info, sizeof(adapter_info), "%s | %s | %d connected",
name, addr_str, g_device_count);
px_text(pixels, W, H, PAD, sy, adapter_info, DIM_TEXT, FONT_SIZE_SM);
} else {
px_text(pixels, W, H, PAD, sy, "No adapter found", RED, FONT_SIZE_SM);
}
// Temporary status message (right-aligned, fades after 5s)
if (g_status[0] && (now - g_status_time) < 5000) {
int sw = text_w(g_status, FONT_SIZE_SM);
px_text(pixels, W, H, W - PAD - sw, sy, g_status, DIM_TEXT, FONT_SIZE_SM);
}
}
// ============================================================================
// Hit testing
// ============================================================================
static bool handle_click(int mx, int my) {
int W = g_win_w;
// Tab bar
int tab_y = 0;
if (my >= tab_y && my < tab_y + TAB_H) {
Tab new_tab = (mx < W / 2) ? TAB_DEVICES : TAB_SCAN;
if (new_tab != g_tab) {
g_tab = new_tab;
g_scroll = 0;
g_hover_row = -1;
return true;
}
return false;
}
int content_y = tab_y + TAB_H;
if (g_tab == TAB_SCAN) {
// Scan button
int scan_btn_y = content_y + 8;
int scan_btn_w = 100;
int scan_btn_x = (W - scan_btn_w) / 2;
if (mx >= scan_btn_x && mx < scan_btn_x + scan_btn_w &&
my >= scan_btn_y && my < scan_btn_y + BTN_H && !g_scanning) {
do_scan();
return true;
}
// Scan result rows
int list_top = scan_btn_y + BTN_H + 12;
for (int i = 0; i < g_scan_count; i++) {
int ry = list_top + i * ROW_H - g_scroll;
if (my >= ry && my < ry + ROW_H) {
// Check if disconnect/connect button clicked
int bx = W - PAD - BTN_W;
int by = ry + (ROW_H - BTN_H) / 2;
if (mx >= bx && mx < bx + BTN_W && my >= by && my < by + BTN_H) {
if (is_connected(g_scan[i].bdAddr)) {
do_disconnect(g_scan[i].bdAddr);
} else {
do_connect(g_scan[i].bdAddr);
}
return true;
}
}
}
} else {
// Device rows
for (int i = 0; i < g_device_count; i++) {
int ry = content_y + i * ROW_H - g_scroll;
if (my >= ry && my < ry + ROW_H) {
// Disconnect button
if (g_devices[i].connected) {
int bx = W - PAD - BTN_W;
int by = ry + (ROW_H - BTN_H) / 2;
if (mx >= bx && mx < bx + BTN_W && my >= by && my < by + BTN_H) {
do_disconnect(g_devices[i].bdAddr);
return true;
}
}
}
}
}
return false;
}
static int get_hover_row(int mx, int my) {
int content_y = TAB_H;
if (g_tab == TAB_DEVICES) {
int count = g_device_count;
for (int i = 0; i < count; i++) {
int ry = content_y + i * ROW_H - g_scroll;
if (my >= ry && my < ry + ROW_H)
return i;
}
} else {
int list_top = content_y + 8 + BTN_H + 12;
for (int i = 0; i < g_scan_count; i++) {
int ry = list_top + i * ROW_H - g_scroll;
if (my >= ry && my < ry + ROW_H)
return i;
}
}
return -1;
}
// ============================================================================
// Entry point
// ============================================================================
extern "C" void _start() {
// Load font
{
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
if (f) {
montauk::memset(f, 0, sizeof(TrueTypeFont));
if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; }
}
g_font = f;
}
// Query adapter
refresh_adapter();
refresh_devices();
g_status[0] = 0;
// Create window
Montauk::WinCreateResult wres;
if (montauk::win_create("Bluetooth", WIN_W, WIN_H, &wres) < 0 || wres.id < 0)
montauk::exit(1);
int win_id = wres.id;
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
render(pixels);
montauk::win_present(win_id);
// Periodic refresh timer
uint64_t last_refresh = montauk::get_milliseconds();
while (true) {
Montauk::WinEvent ev;
int r = montauk::win_poll(win_id, &ev);
if (r < 0) break;
// Handle pending scan (non-blocking: kick it off after render)
if (g_scanning) {
render(pixels);
montauk::win_present(win_id);
finish_scan();
refresh_devices();
render(pixels);
montauk::win_present(win_id);
continue;
}
if (r == 0) {
// Periodic refresh every 5 seconds
uint64_t now = montauk::get_milliseconds();
if (now - last_refresh >= 5000) {
refresh_adapter();
refresh_devices();
last_refresh = now;
render(pixels);
montauk::win_present(win_id);
}
montauk::sleep_ms(16);
continue;
}
bool redraw = false;
if (ev.type == 3) break; // close
// Resize
if (ev.type == 2) {
g_win_w = ev.resize.w;
g_win_h = ev.resize.h;
pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h);
redraw = true;
}
// Keyboard
if (ev.type == 0 && ev.key.pressed) {
if (ev.key.scancode == 0x01) break; // Escape
if (ev.key.ascii == 's' || ev.key.ascii == 'S') {
if (!g_scanning) { do_scan(); redraw = true; }
}
if (ev.key.ascii == '1') { g_tab = TAB_DEVICES; g_scroll = 0; redraw = true; }
if (ev.key.ascii == '2') { g_tab = TAB_SCAN; g_scroll = 0; redraw = true; }
}
// Mouse
if (ev.type == 1) {
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
int new_hover = get_hover_row(ev.mouse.x, ev.mouse.y);
if (new_hover != g_hover_row) {
g_hover_row = new_hover;
redraw = true;
}
if (clicked) {
if (handle_click(ev.mouse.x, ev.mouse.y))
redraw = true;
}
// Scroll
if (ev.mouse.scroll) {
g_scroll -= ev.mouse.scroll * 20;
if (g_scroll < 0) g_scroll = 0;
redraw = true;
}
}
if (redraw) {
render(pixels);
montauk::win_present(win_id);
}
}
montauk::win_destroy(win_id);
montauk::exit(0);
}
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Bluetooth"
binary = "bluetooth.elf"
icon = "bluetooth.svg"
[menu]
category = "System"
visible = true
@@ -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>
+91 -12
View File
@@ -77,6 +77,14 @@ static bool is_spreadsheet_file(const char* name) {
return str_ends_with(name, ".mss");
}
static bool is_video_file(const char* name) {
return str_ends_with(name, ".mp4") || str_ends_with(name, ".m4v");
}
static bool is_audio_file(const char* name) {
return str_ends_with(name, ".mp3") || str_ends_with(name, ".wav");
}
static int detect_file_type(const char* name, bool is_dir) {
if (is_dir) return 1;
if (str_ends_with(name, ".elf")) return 2;
@@ -174,11 +182,7 @@ static void filemanager_read_dir(FileManagerState* fm) {
fm->is_dir[i] = true;
fm->entry_names[i][len - 1] = '\0';
} else {
bool has_dot = false;
for (int j = 0; j < len; j++) {
if (fm->entry_names[i][j] == '.') { has_dot = true; break; }
}
fm->is_dir[i] = !has_dot;
fm->is_dir[i] = false;
}
fm->entry_types[i] = detect_file_type(fm->entry_names[i], fm->is_dir[i]);
@@ -240,6 +244,66 @@ static void filemanager_read_dir(FileManagerState* fm) {
fm->last_click_time = 0;
}
// ============================================================================
// Recursive directory deletion
// ============================================================================
static bool filemanager_delete_recursive(const char* path) {
// Try deleting as a file (or empty directory) first
if (montauk::fdelete(path) == 0) return true;
// If that failed, it may be a non-empty directory — enumerate and delete children
const char* names[64];
int count = montauk::readdir(path, names, 64);
if (count < 0) return false;
// Compute the prefix to strip (readdir returns paths relative to drive root)
const char* after_drive = path;
for (int k = 0; after_drive[k]; k++) {
if (after_drive[k] == ':' && after_drive[k + 1] == '/') {
after_drive += k + 2;
break;
}
}
char prefix[256] = {0};
int prefix_len = 0;
if (after_drive[0] != '\0') {
montauk::strcpy(prefix, after_drive);
prefix_len = montauk::slen(prefix);
if (prefix_len > 0 && prefix[prefix_len - 1] != '/') {
prefix[prefix_len++] = '/';
prefix[prefix_len] = '\0';
}
}
for (int i = 0; i < count; i++) {
const char* raw = names[i];
if (prefix_len > 0) {
bool match = true;
for (int k = 0; k < prefix_len; k++) {
if (raw[k] != prefix[k]) { match = false; break; }
}
if (match) raw += prefix_len;
}
char child[512];
montauk::strcpy(child, path);
int plen = montauk::slen(child);
if (plen > 0 && child[plen - 1] != '/')
str_append(child, "/", 512);
str_append(child, raw, 512);
// Strip trailing slash if present (directory marker)
int clen = montauk::slen(child);
if (clen > 0 && child[clen - 1] == '/') child[clen - 1] = '\0';
filemanager_delete_recursive(child);
}
// Now the directory should be empty — delete it
return montauk::fdelete(path) == 0;
}
// ============================================================================
// History management
// ============================================================================
@@ -373,11 +437,12 @@ static void filemanager_draw_header(Canvas& c, FileManagerState* fm,
}
}
// Delete button (6th toolbar button) — only active when a file is selected
// Delete button (6th toolbar button) — active when a file or directory is selected
{
int bx = 148, by = 4;
bool has_sel = fm->selected >= 0 && fm->selected < fm->entry_count
&& !fm->at_drives_root && !fm->is_dir[fm->selected];
&& !fm->at_drives_root
&& fm->entry_types[fm->selected] != 3;
Color del_bg = has_sel ? btn_bg : Color::from_rgb(0xF0, 0xF0, 0xF0);
c.fill_rect(bx, by, 24, 24, del_bg);
if (ds && ds->icon_delete.pixels) {
@@ -634,16 +699,19 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
fm->scrollbar.scroll_offset = 0;
}
else if (local_x >= 148 && local_x < 172) {
// Delete selected file
// Delete selected file or directory
if (fm->selected >= 0 && fm->selected < fm->entry_count
&& !fm->at_drives_root && !fm->is_dir[fm->selected]) {
&& !fm->at_drives_root && fm->entry_types[fm->selected] != 3) {
char fullpath[512];
montauk::strcpy(fullpath, fm->current_path);
int plen = montauk::slen(fullpath);
if (plen > 0 && fullpath[plen - 1] != '/')
str_append(fullpath, "/", 512);
str_append(fullpath, fm->entry_names[fm->selected], 512);
montauk::fdelete(fullpath);
if (fm->is_dir[fm->selected])
filemanager_delete_recursive(fullpath);
else
montauk::fdelete(fullpath);
filemanager_read_dir(fm);
}
}
@@ -695,6 +763,10 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath);
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath);
} else if (is_video_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/apps/video/video.elf", fullpath);
} else if (is_audio_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/apps/music/music.elf", fullpath);
} else if (str_ends_with(fm->entry_names[clicked_idx], ".elf")) {
montauk::spawn(fullpath);
} else if (fm->desktop) {
@@ -753,6 +825,10 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) {
montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath);
} else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath);
} else if (is_video_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/apps/video/video.elf", fullpath);
} else if (is_audio_file(fm->entry_names[clicked_idx])) {
montauk::spawn("0:/apps/music/music.elf", fullpath);
} else if (str_ends_with(fm->entry_names[clicked_idx], ".elf")) {
montauk::spawn(fullpath);
} else if (fm->desktop) {
@@ -842,14 +918,17 @@ static void filemanager_on_key(Window* win, const Montauk::KeyEvent& key) {
} else if (key.scancode == 0x53) {
// Delete key
if (fm->selected >= 0 && fm->selected < fm->entry_count
&& !fm->at_drives_root && !fm->is_dir[fm->selected]) {
&& !fm->at_drives_root && fm->entry_types[fm->selected] != 3) {
char fullpath[512];
montauk::strcpy(fullpath, fm->current_path);
int plen = montauk::slen(fullpath);
if (plen > 0 && fullpath[plen - 1] != '/')
str_append(fullpath, "/", 512);
str_append(fullpath, fm->entry_names[fm->selected], 512);
montauk::fdelete(fullpath);
if (fm->is_dir[fm->selected])
filemanager_delete_recursive(fullpath);
else
montauk::fdelete(fullpath);
filemanager_read_dir(fm);
}
} else if (key.alt && key.scancode == 0x4B) {
+23
View File
@@ -262,6 +262,8 @@ void gui::desktop_init(DesktopState* ds) {
ds->net_cfg_last_poll = montauk::get_milliseconds();
ds->net_icon_rect = {0, 0, 0, 0};
ds->closing_ext_count = 0;
}
// ============================================================================
@@ -302,6 +304,16 @@ void desktop_poll_external_windows(DesktopState* ds) {
}
if (!found && ds->window_count < MAX_WINDOWS) {
// Skip windows we've already sent a close event to — the owning
// process hasn't destroyed them yet, so they still appear in
// win_enumerate. Re-creating them would place them at the
// default position instead of where the user dragged them.
bool closing = false;
for (int c = 0; c < ds->closing_ext_count; c++) {
if (ds->closing_ext_ids[c] == extId) { closing = true; break; }
}
if (closing) continue;
// Map the pixel buffer into our address space
uint64_t va = montauk::win_map(extId);
if (va == 0) continue;
@@ -367,6 +379,17 @@ void desktop_poll_external_windows(DesktopState* ds) {
gui::desktop_close_window(ds, i);
}
}
// Purge closing IDs that the kernel no longer reports (fully destroyed)
for (int c = ds->closing_ext_count - 1; c >= 0; c--) {
bool alive = false;
for (int e = 0; e < extCount; e++) {
if (extWins[e].id == ds->closing_ext_ids[c]) { alive = true; break; }
}
if (!alive) {
ds->closing_ext_ids[c] = ds->closing_ext_ids[--ds->closing_ext_count];
}
}
}
// ============================================================================
+6 -1
View File
@@ -55,8 +55,13 @@ void gui::desktop_close_window(DesktopState* ds, int idx) {
Window* win = &ds->windows[idx];
// For external windows, send a close event instead of freeing the buffer
// For external windows, send a close event instead of freeing the buffer.
// Track the ID so the poll loop won't re-create it at the default position
// before the owning process has finished destroying it.
if (win->external) {
if (ds->closing_ext_count < DesktopState::MAX_CLOSING) {
ds->closing_ext_ids[ds->closing_ext_count++] = win->ext_win_id;
}
Montauk::WinEvent ev;
montauk::memset(&ev, 0, sizeof(ev));
ev.type = 3; // close
+60 -5
View File
@@ -29,20 +29,70 @@ void disktool_refresh() {
}
// ============================================================================
// Create partition
// Create partition — confirmation dialog
// ============================================================================
void do_create_partition() {
void open_newpart_dialog() {
auto& dt = g_state;
if (dt.selected_disk < 0 || dt.selected_disk >= dt.disk_count) return;
if (dt.np_dlg.open) return;
int part_indices[MAX_PARTS];
int nparts = get_disk_parts(part_indices, MAX_PARTS);
if (nparts == 0) {
auto& dlg = dt.np_dlg;
dlg.will_init_gpt = (nparts == 0);
dlg.hover_confirm = false;
dlg.hover_cancel = false;
Montauk::DiskInfo& disk = dt.disks[dt.selected_disk];
char sz[24];
format_disk_size(sz, sizeof(sz), disk.sectorCount, disk.sectorSizeLog);
snprintf(dlg.disk_desc, sizeof(dlg.disk_desc), "Disk %d: %s (%s)",
dt.selected_disk, disk.model, sz);
if (dlg.will_init_gpt) {
snprintf(dlg.warn_line1, sizeof(dlg.warn_line1),
"This will initialize a new GPT on this disk.");
snprintf(dlg.warn_line2, sizeof(dlg.warn_line2),
"ALL existing partitions will be destroyed!");
} else {
snprintf(dlg.warn_line1, sizeof(dlg.warn_line1),
"This will add a new partition in the largest");
snprintf(dlg.warn_line2, sizeof(dlg.warn_line2),
"free region on this disk.");
}
Montauk::WinCreateResult wres;
const char* title = dlg.will_init_gpt ? "Initialize Disk" : "New Partition";
if (montauk::win_create(title, NP_DLG_W, NP_DLG_H, &wres) < 0 || wres.id < 0) {
set_status("Failed to open dialog");
return;
}
dlg.win_id = wres.id;
dlg.pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
dlg.open = true;
render_newpart_window();
montauk::win_present(dlg.win_id);
}
void close_newpart_dialog() {
auto& dlg = g_state.np_dlg;
if (!dlg.open) return;
montauk::win_destroy(dlg.win_id);
dlg.open = false;
}
void newpart_dialog_confirm() {
auto& dt = g_state;
auto& dlg = dt.np_dlg;
if (dlg.will_init_gpt) {
int r = montauk::gpt_init(dt.selected_disk);
if (r < 0) {
set_status("Failed to initialize GPT on disk");
close_newpart_dialog();
return;
}
set_status("Initialized GPT");
@@ -69,13 +119,18 @@ void do_create_partition() {
int r = montauk::gpt_add(&params);
if (r < 0) {
set_status("Failed to create partition");
return;
} else {
set_status("Partition created successfully");
}
set_status("Partition created successfully");
close_newpart_dialog();
disktool_refresh();
}
void do_create_partition() {
open_newpart_dialog();
}
// ============================================================================
// Mount partition
// ============================================================================
+19
View File
@@ -72,6 +72,8 @@ static constexpr int NUM_FS_TYPES = 1;
static constexpr int FMT_DLG_W = 280;
static constexpr int FMT_DLG_H = 220;
static constexpr int NP_DLG_W = 340;
static constexpr int NP_DLG_H = 200;
struct FormatDialog {
bool open;
@@ -84,6 +86,18 @@ struct FormatDialog {
uint32_t* pixels;
};
struct NewPartDialog {
bool open;
bool will_init_gpt; // true if GPT init is needed (most destructive)
bool hover_confirm;
bool hover_cancel;
char disk_desc[80];
char warn_line1[96];
char warn_line2[96];
int win_id;
uint32_t* pixels;
};
struct DiskToolState {
Montauk::DiskInfo disks[MAX_DISKS];
int disk_count;
@@ -95,6 +109,7 @@ struct DiskToolState {
char status[80];
uint64_t status_time;
FormatDialog fmt_dlg;
NewPartDialog np_dlg;
};
// ============================================================================
@@ -125,6 +140,7 @@ void format_disk_size(char* buf, int bufsize, uint64_t sectors, uint16_t sectorS
void render(uint32_t* pixels);
void render_format_window();
void render_newpart_window();
// ============================================================================
// Function declarations — actions.cpp
@@ -136,3 +152,6 @@ void do_mount_partition();
void open_format_dialog();
void close_format_dialog();
void format_dialog_do_format();
void open_newpart_dialog();
void close_newpart_dialog();
void newpart_dialog_confirm();
+70 -1
View File
@@ -180,6 +180,40 @@ static bool handle_content_click(int mx, int my) {
return false;
}
// ============================================================================
// New partition dialog mouse handling
// ============================================================================
static bool handle_newpart_dialog_click(int mx, int my, bool clicked) {
auto& dlg = g_state.np_dlg;
if (!dlg.open) return false;
int dw = NP_DLG_W, dh = NP_DLG_H;
int btn_w = 90, btn_h = 30;
int btn_y = dh - btn_h - 16;
int gap = 16;
int total_w = btn_w * 2 + gap;
int bx = (dw - total_w) / 2;
dlg.hover_confirm = (mx >= bx && mx < bx + btn_w && my >= btn_y && my < btn_y + btn_h);
dlg.hover_cancel = (mx >= bx + btn_w + gap && mx < bx + btn_w * 2 + gap && my >= btn_y && my < btn_y + btn_h);
if (!clicked) return true;
if (dlg.hover_confirm) {
newpart_dialog_confirm();
return true;
}
if (dlg.hover_cancel) {
close_newpart_dialog();
return true;
}
return true;
}
// ============================================================================
// Format dialog mouse handling
// ============================================================================
@@ -236,6 +270,16 @@ static bool handle_key(const Montauk::KeyEvent& key) {
auto& dt = g_state;
// New partition dialog keys
if (dt.np_dlg.open) {
if (key.scancode == 0x01) { // Escape
close_newpart_dialog();
} else if (key.ascii == '\n' || key.ascii == '\r') {
newpart_dialog_confirm();
}
return true;
}
// Format dialog keys
if (dt.fmt_dlg.open) {
if (key.scancode == 0x01) { // Escape
@@ -313,6 +357,28 @@ extern "C" void _start() {
bool redraw_main = false;
bool redraw_dlg = false;
bool redraw_np = false;
// Poll new partition dialog window
if (g_state.np_dlg.open) {
int dr = montauk::win_poll(g_state.np_dlg.win_id, &ev);
if (dr > 0) {
if (ev.type == 3) { // close
close_newpart_dialog();
redraw_main = true;
} else if (ev.type == 0 && ev.key.pressed) {
handle_key(ev.key);
redraw_np = g_state.np_dlg.open;
redraw_main = true;
} else if (ev.type == 1) {
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
handle_newpart_dialog_click(ev.mouse.x, ev.mouse.y, clicked);
redraw_np = g_state.np_dlg.open;
redraw_main = true;
}
}
}
// Poll format dialog window
if (g_state.fmt_dlg.open) {
int dr = montauk::win_poll(g_state.fmt_dlg.win_id, &ev);
@@ -341,7 +407,8 @@ extern "C" void _start() {
// Even with no main event, dialog may have triggered redraws
if (redraw_main) { render(pixels); montauk::win_present(win_id); }
if (redraw_dlg) { render_format_window(); montauk::win_present(g_state.fmt_dlg.win_id); }
if (!redraw_main && !redraw_dlg) montauk::sleep_ms(16);
if (redraw_np) { render_newpart_window(); montauk::win_present(g_state.np_dlg.win_id); }
if (!redraw_main && !redraw_dlg && !redraw_np) montauk::sleep_ms(16);
continue;
}
@@ -383,8 +450,10 @@ extern "C" void _start() {
if (redraw_main) { render(pixels); montauk::win_present(win_id); }
if (redraw_dlg) { render_format_window(); montauk::win_present(g_state.fmt_dlg.win_id); }
if (redraw_np) { render_newpart_window(); montauk::win_present(g_state.np_dlg.win_id); }
}
close_newpart_dialog();
close_format_dialog();
montauk::win_destroy(win_id);
montauk::exit(0);
+55
View File
@@ -345,6 +345,61 @@ void render_format_window() {
px_button(px, dw, dh, bx + btn_w + gap, btn_y, btn_w, btn_h, "Cancel", can_bg, WHITE, 6);
}
// ============================================================================
// Render: new partition confirmation dialog
// ============================================================================
void render_newpart_window() {
auto& dlg = g_state.np_dlg;
if (!dlg.open) return;
uint32_t* px = dlg.pixels;
int dw = NP_DLG_W, dh = NP_DLG_H;
int fh = font_h();
px_fill(px, dw, dh, 0, 0, dw, dh, BG_COLOR);
// Title
const char* title = dlg.will_init_gpt ? "Initialize Disk" : "New Partition";
int tw = text_w(title);
px_text(px, dw, dh, (dw - tw) / 2, 12, title, TEXT_COLOR);
// Disk description
int ddw = text_w(dlg.disk_desc);
px_text(px, dw, dh, (dw - ddw) / 2, 12 + fh + 6, dlg.disk_desc, DIM_TEXT);
// Warning lines
Color warn_color = dlg.will_init_gpt
? Color::from_rgb(0xCC, 0x22, 0x22)
: TEXT_COLOR;
int wy = 12 + fh * 2 + 20;
int w1w = text_w(dlg.warn_line1);
px_text(px, dw, dh, (dw - w1w) / 2, wy, dlg.warn_line1, warn_color);
int w2w = text_w(dlg.warn_line2);
px_text(px, dw, dh, (dw - w2w) / 2, wy + fh + 4, dlg.warn_line2, warn_color);
// Buttons
int btn_w = 90, btn_h = 30;
int btn_y = dh - btn_h - 16;
int gap = 16;
int total_w = btn_w * 2 + gap;
int bx = (dw - total_w) / 2;
Color confirm_bg = dlg.will_init_gpt
? (dlg.hover_confirm ? Color::from_rgb(0xDD, 0x44, 0x44)
: Color::from_rgb(0xCC, 0x33, 0x33))
: (dlg.hover_confirm ? Color::from_rgb(0x4A, 0x88, 0xC8)
: Color::from_rgb(0x42, 0x7A, 0xB5));
const char* confirm_label = dlg.will_init_gpt ? "Initialize" : "Create";
px_button(px, dw, dh, bx, btn_y, btn_w, btn_h, confirm_label, confirm_bg, WHITE, 6);
Color can_bg = dlg.hover_cancel
? Color::from_rgb(0x99, 0x99, 0x99)
: Color::from_rgb(0x88, 0x88, 0x88);
px_button(px, dw, dh, bx + btn_w + gap, btn_y, btn_w, btn_h, "Cancel", can_bg, WHITE, 6);
}
// ============================================================================
// Top-level render
// ============================================================================
+87
View File
@@ -0,0 +1,87 @@
# Makefile for music (standalone Music Player) 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
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
# ---- 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) \
-I . \
-isystem $(PROG_INC)/libc \
-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 minimp3_impl.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
TARGET := $(BINDIR)/apps/music/music.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/apps/music
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Music"
binary = "music.elf"
icon = "audio-player.svg"
[menu]
category = "Applications"
visible = true
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
/*
* minimp3_impl.cpp
* Single compilation unit for minimp3 in MontaukOS freestanding environment
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdint>
#include <cstddef>
extern "C" {
#include <string.h>
#include <stdlib.h>
}
#define MINIMP3_IMPLEMENTATION
#include "minimp3.h"
+35
View File
@@ -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>
+86
View File
@@ -0,0 +1,86 @@
# Makefile for volume (standalone Volume Control) 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
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
# ---- 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 \
-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
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
# ---- Target ----
TARGET := $(BINDIR)/apps/volume/volume.elf
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile
mkdir -p $(BINDIR)/apps/volume
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(OBJDIR)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
+368
View File
@@ -0,0 +1,368 @@
/*
* main.cpp
* MontaukOS Volume Control
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <string.h>
#include <stdio.h>
}
using namespace gui;
// ============================================================================
// Constants
// ============================================================================
static constexpr int WIN_W = 280;
static constexpr int WIN_H = 164;
static constexpr int FONT_SIZE = 16;
static constexpr int FONT_SIZE_LG = 28;
static constexpr int SLIDER_X = 24;
static constexpr int SLIDER_W = WIN_W - 48;
static constexpr int SLIDER_Y = 78;
static constexpr int SLIDER_H = 8;
static constexpr int KNOB_R = 10;
static constexpr int BTN_W = 48;
static constexpr int BTN_H = 28;
static constexpr int BTN_Y = 118;
static constexpr int BTN_RAD = 6;
static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color TEXT_COLOR = Color::from_rgb(0x22, 0x22, 0x22);
static constexpr Color DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66);
static constexpr Color ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0);
static constexpr Color ACCENT_HOVER = Color::from_rgb(0x2A, 0x62, 0xC8);
static constexpr Color TRACK_BG = Color::from_rgb(0xDD, 0xDD, 0xDD);
static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF);
static constexpr Color MUTE_COLOR = Color::from_rgb(0xCC, 0x33, 0x33);
// ============================================================================
// State
// ============================================================================
static TrueTypeFont* g_font = nullptr;
static int g_volume = 80;
static bool g_muted = false;
static int g_pre_mute_vol = 80;
static bool g_dragging = false;
// ============================================================================
// Pixel helpers (same pattern as disks app)
// ============================================================================
static void px_fill(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, Color c) {
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y;
int x1 = x + w > bw ? bw : x + w;
int y1 = y + h > bh ? bh : y + 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 bh, int x, int y, int w, Color c) {
if (y < 0 || y >= bh) return;
uint32_t v = c.to_pixel();
int x0 = x < 0 ? 0 : x;
int x1 = x + w > bw ? bw : x + w;
for (int col = x0; col < x1; col++)
px[y * bw + col] = v;
}
static void px_fill_rounded(uint32_t* px, int bw, int bh,
int x, int y, int w, int h, int r, Color c) {
uint32_t v = c.to_pixel();
for (int row = 0; row < h; row++) {
int dy = y + row;
if (dy < 0 || dy >= bh) continue;
for (int col = 0; col < w; col++) {
int dx = x + col;
if (dx < 0 || dx >= bw) continue;
bool skip = false;
int cx, cy;
if (col < r && row < r) { cx = r - col - 1; cy = r - row - 1; if (cx*cx + cy*cy >= r*r) skip = true; }
else if (col >= w - r && row < r) { cx = col - (w - r); cy = r - row - 1; if (cx*cx + cy*cy >= r*r) skip = true; }
else if (col < r && row >= h - r) { cx = r - col - 1; cy = row - (h - r); if (cx*cx + cy*cy >= r*r) skip = true; }
else if (col >= w - r && row >= h - r) { cx = col - (w - r); cy = row - (h - r); if (cx*cx + cy*cy >= r*r) skip = true; }
if (!skip) px[dy * bw + dx] = v;
}
}
}
static void px_circle(uint32_t* px, int bw, int bh,
int cx, int cy, int r, Color c) {
uint32_t v = c.to_pixel();
for (int dy = -r; dy <= r; dy++) {
int py = cy + dy;
if (py < 0 || py >= bh) continue;
for (int dx = -r; dx <= r; dx++) {
int ppx = cx + dx;
if (ppx < 0 || ppx >= bw) continue;
if (dx * dx + dy * dy <= r * r)
px[py * bw + ppx] = v;
}
}
}
static void px_text(uint32_t* px, int bw, int bh,
int x, int y, const char* text, Color c, int size = FONT_SIZE) {
if (g_font)
g_font->draw_to_buffer(px, bw, bh, x, y, text, c, size);
}
static int text_w(const char* text, int size = FONT_SIZE) {
return g_font ? g_font->measure_text(text, size) : 0;
}
static int font_h(int size = FONT_SIZE) {
if (!g_font) return 16;
auto* cache = g_font->get_cache(size);
return cache->ascent - cache->descent;
}
static void px_button(uint32_t* px, int bw, int bh,
int x, int y, int w, int h,
const char* label, Color bg, Color fg, int r) {
px_fill_rounded(px, bw, bh, x, y, w, h, r, bg);
int tw = text_w(label);
int fh = font_h();
px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2, label, fg);
}
// ============================================================================
// Volume helpers
// ============================================================================
static void apply_volume(int vol) {
if (vol < 0) vol = 0;
if (vol > 100) vol = 100;
g_volume = vol;
montauk::audio_set_volume(0, g_volume);
}
static void refresh_volume() {
int v = montauk::audio_get_volume(0);
if (v >= 0) g_volume = v;
}
// ============================================================================
// Render
// ============================================================================
static void render(uint32_t* pixels) {
int fh_lg = font_h(FONT_SIZE_LG);
// Background
px_fill(pixels, WIN_W, WIN_H, 0, 0, WIN_W, WIN_H, BG_COLOR);
// Volume percentage (large, centered)
char vol_str[8];
snprintf(vol_str, sizeof(vol_str), "%d%%", g_muted ? 0 : g_volume);
int vw = text_w(vol_str, FONT_SIZE_LG);
Color vol_color = g_muted ? MUTE_COLOR : ACCENT;
px_text(pixels, WIN_W, WIN_H, (WIN_W - vw) / 2, 20, vol_str, vol_color, FONT_SIZE_LG);
// "Muted" label
if (g_muted) {
const char* muted_label = "Muted";
int mw = text_w(muted_label);
px_text(pixels, WIN_W, WIN_H, (WIN_W - mw) / 2,
20 + fh_lg + 4, muted_label, MUTE_COLOR);
}
// Slider track
px_fill_rounded(pixels, WIN_W, WIN_H, SLIDER_X, SLIDER_Y, SLIDER_W, SLIDER_H, 4, TRACK_BG);
// Filled portion
int display_vol = g_muted ? 0 : g_volume;
int fill_w = (display_vol * SLIDER_W) / 100;
if (fill_w > 0)
px_fill_rounded(pixels, WIN_W, WIN_H, SLIDER_X, SLIDER_Y, fill_w, SLIDER_H, 4, ACCENT);
// Knob
int knob_x = SLIDER_X + fill_w;
int knob_y = SLIDER_Y + SLIDER_H / 2;
px_circle(pixels, WIN_W, WIN_H, knob_x, knob_y, KNOB_R, ACCENT);
px_circle(pixels, WIN_W, WIN_H, knob_x, knob_y, KNOB_R - 3, WHITE);
// Buttons: [-] and [+] and [Mute]
int total_btn_w = BTN_W * 2 + 60 + 12 * 2; // minus, plus, mute + gaps
int bx = (WIN_W - total_btn_w) / 2;
px_button(pixels, WIN_W, WIN_H, bx, BTN_Y, BTN_W, BTN_H,
"-", Color::from_rgb(0xE0, 0xE0, 0xE0), TEXT_COLOR, BTN_RAD);
bx += BTN_W + 12;
px_button(pixels, WIN_W, WIN_H, bx, BTN_Y, BTN_W, BTN_H,
"+", Color::from_rgb(0xE0, 0xE0, 0xE0), TEXT_COLOR, BTN_RAD);
bx += BTN_W + 12;
Color mute_bg = g_muted ? MUTE_COLOR : Color::from_rgb(0xE0, 0xE0, 0xE0);
Color mute_fg = g_muted ? WHITE : TEXT_COLOR;
px_button(pixels, WIN_W, WIN_H, bx, BTN_Y, 60, BTN_H,
"Mute", mute_bg, mute_fg, BTN_RAD);
}
// ============================================================================
// Hit testing
// ============================================================================
static int vol_from_slider_x(int mx) {
int v = ((mx - SLIDER_X) * 100) / SLIDER_W;
if (v < 0) v = 0;
if (v > 100) v = 100;
return v;
}
static bool handle_click(int mx, int my) {
// Slider area (generous vertical hit zone)
if (my >= SLIDER_Y - KNOB_R && my <= SLIDER_Y + SLIDER_H + KNOB_R &&
mx >= SLIDER_X - KNOB_R && mx <= SLIDER_X + SLIDER_W + KNOB_R) {
g_muted = false;
apply_volume(vol_from_slider_x(mx));
g_dragging = true;
return true;
}
// Buttons
int total_btn_w = BTN_W * 2 + 60 + 12 * 2;
int bx = (WIN_W - total_btn_w) / 2;
if (my >= BTN_Y && my < BTN_Y + BTN_H) {
// [-] button
if (mx >= bx && mx < bx + BTN_W) {
g_muted = false;
apply_volume(g_volume - 5);
return true;
}
bx += BTN_W + 12;
// [+] button
if (mx >= bx && mx < bx + BTN_W) {
g_muted = false;
apply_volume(g_volume + 5);
return true;
}
bx += BTN_W + 12;
// [Mute] button
if (mx >= bx && mx < bx + 60) {
if (g_muted) {
g_muted = false;
apply_volume(g_pre_mute_vol);
} else {
g_pre_mute_vol = g_volume;
g_muted = true;
montauk::audio_set_volume(0, 0);
}
return true;
}
}
return false;
}
// ============================================================================
// Entry point
// ============================================================================
extern "C" void _start() {
// Load font
{
TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont));
if (f) {
montauk::memset(f, 0, sizeof(TrueTypeFont));
if (!f->init("0:/fonts/Roboto-Medium.ttf")) { montauk::mfree(f); f = nullptr; }
}
g_font = f;
}
refresh_volume();
// Create window
Montauk::WinCreateResult wres;
if (montauk::win_create("Volume", WIN_W, WIN_H, &wres) < 0 || wres.id < 0)
montauk::exit(1);
int win_id = wres.id;
uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
render(pixels);
montauk::win_present(win_id);
while (true) {
Montauk::WinEvent ev;
int r = montauk::win_poll(win_id, &ev);
if (r < 0) break;
if (r == 0) { montauk::sleep_ms(16); continue; }
bool redraw = false;
if (ev.type == 3) break; // close
// Keyboard
if (ev.type == 0 && ev.key.pressed) {
if (ev.key.scancode == 0x01) break; // Escape
if (ev.key.scancode == 0x4D || ev.key.ascii == '+' || ev.key.ascii == '=') { // Right / +
g_muted = false;
apply_volume(g_volume + 5);
redraw = true;
} else if (ev.key.scancode == 0x4B || ev.key.ascii == '-') { // Left / -
g_muted = false;
apply_volume(g_volume - 5);
redraw = true;
} else if (ev.key.ascii == 'm' || ev.key.ascii == 'M') {
if (g_muted) {
g_muted = false;
apply_volume(g_pre_mute_vol);
} else {
g_pre_mute_vol = g_volume;
g_muted = true;
montauk::audio_set_volume(0, 0);
}
redraw = true;
}
}
// Mouse
if (ev.type == 1) {
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
bool released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1);
if (clicked) {
if (handle_click(ev.mouse.x, ev.mouse.y))
redraw = true;
}
if (g_dragging && (ev.mouse.buttons & 1)) {
g_muted = false;
apply_volume(vol_from_slider_x(ev.mouse.x));
redraw = true;
}
if (released) {
g_dragging = false;
}
}
if (redraw) {
render(pixels);
montauk::win_present(win_id);
}
}
montauk::win_destroy(win_id);
montauk::exit(0);
}
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Volume"
binary = "volume.elf"
icon = "pavucontrol.svg"
[menu]
category = "System"
visible = true
+35
View File
@@ -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>