-- System Shell for Mosis -- Provides persistent navigation, notifications, toasts, and dialogs local shell_document = nil local app_container = nil -- Navigation state (persistent in shell) local nav_history = {} local current_app = "home" local current_app_path = nil -- Dialog callback local dialog_callback = nil -- Notification state local notifications_expanded = false -- ===== INITIALIZATION ===== function initShell(doc) print("[Shell] Initializing system shell...") shell_document = doc app_container = doc:GetElementById("app-container") if not app_container then print("[Shell] ERROR: app-container not found!") return end -- Update time display updateTime() -- Load home screen by default loadAppContent_internal("home", "apps/home/home_content.rml") print("[Shell] Shell initialized") end -- Update status bar time function updateTime() local time_elem = shell_document:GetElementById("shell-time") if time_elem then time_elem.inner_rml = "12:30" end end -- ===== APP LOADING ===== -- Internal function to load app content function loadAppContent_internal(app_id, app_path) if not app_container then print("[Shell] ERROR: No app container") return false end print("[Shell] Loading app: " .. app_id .. " from " .. app_path) -- Show loading overlay showLoading(true) -- Load app content using C++ function if loadAppContent then local success = loadAppContent(app_container, app_path) showLoading(false) if success then current_app = app_id current_app_path = app_path print("[Shell] App loaded: " .. app_id) -- If home was loaded, populate apps dynamically if app_id == "home" then populateHomeApps() end return true else print("[Shell] Failed to load app: " .. app_id) showToast("Failed to load app", "error") return false end else showLoading(false) print("[Shell] ERROR: loadAppContent not available, using fallback") -- Fallback: try to load via inner_rml if we can read the file -- This won't work without C++ support, but shows the intent return false end end -- Navigate to a system app by ID function shellNavigateTo(app_id) local app_paths = { home = "apps/home/home_content.rml", dialer = "apps/dialer/dialer_content.rml", calling = "apps/dialer/calling_content.rml", contacts = "apps/contacts/contacts_content.rml", contact_detail = "apps/contacts/contact_detail_content.rml", messages = "apps/messages/messages_content.rml", chat = "apps/messages/chat_content.rml", settings = "apps/settings/settings_content.rml", browser = "apps/browser/browser_content.rml", store = "apps/store/store_content.rml", camera = "apps/camera/camera_content.rml", music = "apps/music/music_content.rml" } local path = app_paths[app_id] if path then -- Push current app to history if current_app and current_app ~= app_id then table.insert(nav_history, { app_id = current_app, app_path = current_app_path }) print("[Shell] Pushed to history: " .. current_app .. " (depth: " .. #nav_history .. ")") end return loadAppContent_internal(app_id, path) else print("[Shell] Unknown app: " .. tostring(app_id)) return false end end -- ===== NAVIGATION ===== -- Go back to previous app function shellGoBack() print("[Shell] goBack called (history depth: " .. #nav_history .. ")") -- Hide notifications if open if notifications_expanded then toggleNotifications() end if #nav_history > 0 then local previous = table.remove(nav_history) print("[Shell] Going back to: " .. previous.app_id) -- Don't push to history when going back local temp_app = current_app current_app = nil current_app_path = nil local success = loadAppContent_internal(previous.app_id, previous.app_path) if not success then -- Restore state on failure current_app = temp_app end return success else print("[Shell] No history - already at root") if current_app ~= "home" then shellGoHome() end end return false end -- Go to home screen (clear history) function shellGoHome() print("[Shell] goHome called") -- Hide notifications if open if notifications_expanded then toggleNotifications() end -- Clear history nav_history = {} -- Load home current_app = nil current_app_path = nil loadAppContent_internal("home", "apps/home/home_content.rml") end -- Show recents (placeholder) function shellShowRecents() print("[Shell] showRecents called") showToast("Recent apps not implemented", "warning") end -- Launch external app (from base-apps) function shellLaunchApp(package_id, app_path, entry_point) print("[Shell] Launching external app: " .. package_id) -- Push current to history if current_app then table.insert(nav_history, { app_id = current_app, app_path = current_app_path }) end -- For external apps, we need content version of the entry -- Convert "app.rml" to "app_content.rml" or load directly local content_path = app_path .. "/" .. entry_point -- Switch sandbox context if available if switchAppSandbox then switchAppSandbox(package_id, app_path) end return loadAppContent_internal(package_id, content_path) end -- ===== TOASTS ===== function showToast(message, type) type = type or "default" local container = shell_document:GetElementById("toast-container") if not container then print("[Shell] Toast container not found") return end local class = "toast" if type == "success" then class = "toast toast-success" elseif type == "error" then class = "toast toast-error" elseif type == "warning" then class = "toast toast-warning" end -- Add toast element local toast_html = '
' .. message .. '
' container.inner_rml = container.inner_rml .. toast_html -- Auto-remove after 3 seconds (would need timer API) if mosis and mosis.timer then mosis.timer.setTimeout(function() -- Remove first toast container.inner_rml = "" end, 3000) end print("[Shell] Toast: " .. message .. " (" .. type .. ")") end -- ===== DIALOGS ===== function showDialog(title, message, on_confirm, on_cancel) local overlay = shell_document:GetElementById("dialog-overlay") local title_elem = shell_document:GetElementById("dialog-title") local message_elem = shell_document:GetElementById("dialog-message") if overlay and title_elem and message_elem then title_elem.inner_rml = title message_elem.inner_rml = message overlay:SetClass("visible", true) dialog_callback = { confirm = on_confirm, cancel = on_cancel } print("[Shell] Dialog shown: " .. title) end end function dialogConfirm() local overlay = shell_document:GetElementById("dialog-overlay") if overlay then overlay:SetClass("visible", false) end if dialog_callback and dialog_callback.confirm then dialog_callback.confirm() end dialog_callback = nil end function dialogCancel() local overlay = shell_document:GetElementById("dialog-overlay") if overlay then overlay:SetClass("visible", false) end if dialog_callback and dialog_callback.cancel then dialog_callback.cancel() end dialog_callback = nil end -- ===== NOTIFICATIONS ===== function toggleNotifications() local panel = shell_document:GetElementById("notification-panel") if panel then notifications_expanded = not notifications_expanded panel:SetClass("expanded", notifications_expanded) print("[Shell] Notifications " .. (notifications_expanded and "expanded" or "collapsed")) end end function addNotification(title, text, icon, app_id) local list = shell_document:GetElementById("notification-list") if not list then return end icon = icon or "../../icons/message.tga" local notification_html = [[
]] .. title .. [[
]] .. text .. [[
now
]] list.inner_rml = notification_html .. list.inner_rml print("[Shell] Notification added: " .. title) end function onNotificationClick(app_id) toggleNotifications() if app_id and app_id ~= "" then shellNavigateTo(app_id) end end function clearNotifications() local list = shell_document:GetElementById("notification-list") if list then list.inner_rml = "" print("[Shell] Notifications cleared") end end -- ===== LOADING OVERLAY ===== function showLoading(visible) local overlay = shell_document:GetElementById("loading-overlay") if overlay then overlay:SetClass("visible", visible) end end -- ===== UTILITY ===== function shellGetCurrentApp() return current_app end function shellCanGoBack() return #nav_history > 0 end -- ===== DYNAMIC APP DISCOVERY ===== -- Default colors for apps without custom colors local app_colors = { ["com.mosis.browser"] = "#F44336", ["com.mosis.camera"] = "#9C27B0", ["com.mosis.contacts"] = "#FF9800", ["com.mosis.dialer"] = "#4CAF50", ["com.mosis.messages"] = "#2196F3", ["com.mosis.music"] = "#E91E63", ["com.mosis.settings"] = "#607D8B", ["com.mosis.store"] = "#3F51B5", } -- Default color for unknown apps local default_colors = { "#FF5722", "#009688", "#795548", "#673AB7", "#3F51B5", "#00BCD4", "#8BC34A", "#CDDC39" } local color_index = 1 function getAppColor(package_id) if app_colors[package_id] then return app_colors[package_id] end -- Assign a default color local color = default_colors[color_index] color_index = (color_index % #default_colors) + 1 app_colors[package_id] = color return color end -- Built-in system apps (fallback when mosis.apps not available or empty) local builtin_apps = { {package_id = "com.mosis.browser", name = "Browser", icon = "../../icons/browser.tga"}, {package_id = "com.mosis.camera", name = "Camera", icon = "../../icons/camera.tga"}, {package_id = "com.mosis.contacts", name = "Contacts", icon = "../../icons/contacts.tga"}, {package_id = "com.mosis.dialer", name = "Phone", icon = "../../icons/phone.tga"}, {package_id = "com.mosis.messages", name = "Messages", icon = "../../icons/message.tga"}, {package_id = "com.mosis.music", name = "Music", icon = "../../icons/music.tga"}, {package_id = "com.mosis.settings", name = "Settings", icon = "../../icons/settings.tga"}, {package_id = "com.mosis.store", name = "Store", icon = "../../icons/store.tga"}, } -- Populate home screen with discovered apps function populateHomeApps() local apps = nil -- Try to get installed apps from mosis.apps API if mosis and mosis.apps then apps = mosis.apps.getInstalled() end -- Fall back to built-in apps if none discovered if not apps or #apps == 0 then print("[Shell] Using built-in system apps") apps = builtin_apps end print("[Shell] Populating home with " .. #apps .. " apps") local grid_container = shell_document:GetElementById("installed-apps") if not grid_container then print("[Shell] installed-apps container not found") return end local apps_html = "" local dock_apps = {} for _, app in ipairs(apps) do -- Skip home app itself if app.package_id ~= "com.mosis.home" then local color = getAppColor(app.package_id) local icon = app.icon or "../../icons/app.tga" local name = app.name or app.package_id -- Build app icon HTML local app_html = [[
]] .. name .. [[
]] apps_html = apps_html .. app_html -- Track dock apps (dialer, messages, contacts, browser) if app.package_id == "com.mosis.dialer" then dock_apps.dialer = {icon = icon, color = color, id = app.package_id} elseif app.package_id == "com.mosis.messages" then dock_apps.messages = {icon = icon, color = color, id = app.package_id} elseif app.package_id == "com.mosis.contacts" then dock_apps.contacts = {icon = icon, color = color, id = app.package_id} elseif app.package_id == "com.mosis.browser" then dock_apps.browser = {icon = icon, color = color, id = app.package_id} end end end -- Set grid content grid_container.inner_rml = apps_html -- Populate dock local dock_container = shell_document:GetElementById("home-dock") if dock_container and (dock_apps.dialer or dock_apps.messages or dock_apps.contacts or dock_apps.browser) then local dock_html = "" local dock_order = {"dialer", "messages", "contacts", "browser"} for _, key in ipairs(dock_order) do local app = dock_apps[key] if app then dock_html = dock_html .. [[
]] end end dock_container.inner_rml = dock_html end print("[Shell] Home populated with " .. #apps - 1 .. " apps") end -- Launch a discovered app by package_id function launchDiscoveredApp(package_id) print("[Shell] Launching app: " .. package_id) -- Check if it's a built-in system app local app_id = package_id:gsub("com.mosis.", "") local builtin_ids = { browser = true, camera = true, contacts = true, dialer = true, messages = true, music = true, settings = true, store = true } if builtin_ids[app_id] then -- Use shellNavigateTo for built-in apps return shellNavigateTo(app_id) end -- Try to find in installed apps if mosis and mosis.apps then local apps = mosis.apps.getInstalled() for _, app in ipairs(apps) do if app.package_id == package_id then print("[Shell] Launching installed app: " .. package_id) -- Push current to history if current_app then table.insert(nav_history, { app_id = current_app, app_path = current_app_path }) print("[Shell] Pushed to history: " .. current_app .. " (depth: " .. #nav_history .. ")") end -- Build content path - convert entry.rml to entry_content.rml local entry = app.entry_point or "main.rml" local content_entry = entry:gsub("%.rml$", "_content.rml") local content_path = "apps/" .. package_id:gsub("com.mosis.", "") .. "/" .. content_entry -- Switch sandbox if available if switchAppSandbox then switchAppSandbox(package_id, app.install_path) end return loadAppContent_internal(package_id, content_path) end end end print("[Shell] App not found: " .. package_id) showToast("App not found", "error") return false end -- Make shell functions globally available for apps _G.showToast = showToast _G.showDialog = showDialog _G.addNotification = addNotification _G.shellNavigateTo = shellNavigateTo _G.shellLaunchApp = shellLaunchApp _G.launchDiscoveredApp = launchDiscoveredApp _G.populateHomeApps = populateHomeApps print("[Shell] Shell script loaded")