-- 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 = [[
]]
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")