This reference file serves as the definitive engineering and behavioral blueprint for AI coding agents developing, debugging, or maintaining RDesk applications. It maps out the package’s multi-directory structure, core contracts, API behaviors, design constraints, and common implementation pitfalls.
RDesk applications utilize a dual-process desktop architecture:
[rdesk-launcher.exe] (Frontend Shell) <--Standard I/O Pipes--> [R Process] (Backend Logic)
- WebView2 rendering control - R6 event loop (App$run())
- Virtual HTTPS folder mapping - Logic handlers (on_message())
- Native windows, tray & menus - Background tasks (mirai)
- Watchdog process cleanup - Offline computation
R/server.R) and UI assets (www/). Developers
only write and modify this layer.async(), rdesk_auto_update(), and
hot-reloading watchers.App class that services messaging callbacks.httpuv,
WebSockets). Communication uses standard I/O streams. Never
introduce code that opens local TCP/UDP ports.Every RDesk application must adhere to this standardized file layout:
MyApp/
├── app.R # Main application entry point (keep thin)
├── DESCRIPTION # Application metadata and dependency list
├── R/ # Backend R modules
│ ├── server.R # Message handlers & initialization (primary logic file)
│ ├── data.R # Data loading, extraction, and transformation
│ └── plots.R # ggplot2 and base chart generation functions
└── www/ # Frontend UI assets
├── index.html # HTML5 markup structure
├── css/
│ └── style.css # Application styling
└── js/
├── app.js # Client event handlers and UI updates
└── rdesk.js # RDesk frontend runtime bridge (NEVER EDIT THIS FILE)
app.R Entry Point:app_dir <- tryCatch(
if (nzchar(Sys.getenv("R_BUNDLE_APP"))) getwd()
else dirname(rstudioapi::getActiveDocumentContext()$path),
error = function(e) getwd()
)
library(RDesk)
# Dynamically source all backend modules
lapply(
list.files(file.path(app_dir, "R"), pattern = "\\.R$", full.names = TRUE),
source
)
app <- App$new(
title = "My RDesk App",
width = 1100L,
height = 740L,
www = file.path(app_dir, "www")
)
init_handlers(app)
app$run()app.R. Sourcing modular R files and calling
init_handlers(app) must be the only operations in
app.R.lapply(list.files(...)) pattern. Individual explicit
source() calls by filename break RDesk’s directory
packaging and hot-reloading hooks.www/js/rdesk.js is
the framework client library. Never import it from a CDN or alter its
contents.All messaging across the R-to-JS bridge uses this exact JSON envelope structure:
{
"id": "msg_unique_hash",
"type": "message_type",
"version": "1.0",
"payload": {},
"timestamp": 1716382104.123
}"foo" automatically returns its response payload to a JS
listener registered for "foo_result".on_message()
handlers must return a standard R list or
NULL. The framework automatically serialises and wraps the
output in the envelope. Do not wrap return values in
rdesk_message() inside handlers.app$send() on the R side, and
rdesk.send() on the JS side.__loading__,
__progress__, __reload_ui__) are reserved for
core framework signaling. Never define custom application messages with
the __ prefix.R/server.R)Define event handlers inside init_handlers(app) in
R/server.R.
init_handlers <- function(app) {
# Triggered once when the UI window is fully initialized
app$on_ready(function() {
# Define native window menu
app$set_menu(list(
File = list(
"Open..." = function() app$send("trigger_open", list()),
"---",
"Exit" = app$quit
),
Help = list(
"About" = function() app$toast("RDesk v1.0.5", type = "info")
)
))
# Push initial dataset
df <- load_initial_data()
app$send("data_ready", rdesk_df_to_list(df))
})
# Standard request-response handler
app$on_message("get_filtered_data", function(payload) {
# payload matches the JS payload object
df <- load_subset(payload$filter_val)
rdesk_df_to_list(df)
})
}app$toast("message", type = "success" | "error" | "info" | "warning")app$dialog_open(title = "Open", filters = "CSV (*.csv)|*.csv")
-> Returns character path or NULLapp$dialog_save(title = "Save", filters = "CSV (*.csv)|*.csv", default = "export.csv")app$dialog_folder(title = "Select Folder")app$loading_start("text", cancellable = TRUE),
app$loading_progress(50),
app$loading_done()To prevent blocking the single-threaded R event loop during computation, RDesk utilizes a three-tier async architecture:
async())Applies to 95% of background processing needs. Automatically manages loading overlays, routing, and task cancellation.
app$on_message("compute_stats", async(function(payload) {
# Executed inside an isolated background worker process
# DO NOT call app$ methods inside this closure
# DO NOT access global variables; pass them via payload or capture them explicitly
df <- load_raw_data(payload$source)
result <- run_calculation(df)
list(stats = result)
}, app = app, loading_message = "Processing calculation..."))To push real-time progress updates from a background worker:
rdesk_async())Used when fine-grained callback control is required.
Communicate with the backend R process using the globally available
rdesk object:
// Wrap initialization inside ready() to ensure bridge is fully established
rdesk.ready(function() {
rdesk.send("get_initial_state", {})
.then(res => renderUI(res))
.catch(err => console.error(err));
});
// Listen for push notifications from R
rdesk.on("update_data", function(data) {
updateChart(data);
});
// Clean up listeners
rdesk.off("update_data");Base64 Charts:
Structured Tables:
send() calls inside
rdesk.ready(). Calling send() before the
native bridge is established throws silent errors.fetch(),
XMLHttpRequest, or WebSocket to communicate
with the R process. Always use rdesk.send() and
rdesk.on().Convert plots to strings for transmission over the standard input/output pipe:
# In R/plots.R
generate_trend_plot <- function(df) {
p <- ggplot2::ggplot(df, ggplot2::aes(x = date, y = value)) +
ggplot2::geom_line(colour = "#1a1a2e", linewidth = 1) +
ggplot2::theme_minimal()
rdesk_plot_to_base64(p)
}
# In R/server.R handler
app$on_message("get_trend", function(payload) {
df <- load_data()
list(chart = generate_trend_plot(df))
})| Shiny Concept | RDesk Desktop Equivalent |
|---|---|
input$x |
payload$x inside
app$on_message("x_changed", ...) |
reactive({...}) |
Standard R function called explicitly by the handler |
renderPlot({...}) |
rdesk_plot_to_base64() returned inside a
list |
renderTable({...}) |
rdesk_df_to_list() returned inside a
list |
observeEvent(input$x, {...}) |
app$on_message("x_changed", function(payload) {...}) |
showNotification() |
app$toast() |
withProgress() |
async(..., loading_message = "...") |
fileInput() |
app$dialog_open() |
downloadHandler() |
app$dialog_save() |
# --- 1. SHINY ORIGINAL (server.R) ---
server <- function(input, output, session) {
filtered <- reactive({
mtcars[mtcars$cyl == input$cyl_filter, ]
})
output$scatter <- renderPlot({
ggplot2::ggplot(filtered(), ggplot2::aes(wt, mpg)) + ggplot2::geom_point()
})
output$table <- renderTable({ filtered() })
}# --- 2. RDESK REWRITE (R/server.R) ---
init_handlers <- function(app) {
app$on_ready(function() {
app$send("data_ready", rdesk_df_to_list(mtcars))
})
app$on_message("filter_changed", async(function(payload) {
df <- mtcars[mtcars$cyl == payload$cyl_filter, ]
p <- ggplot2::ggplot(df, ggplot2::aes(wt, mpg)) + ggplot2::geom_point()
list(
chart = rdesk_plot_to_base64(p),
table = rdesk_df_to_list(df)
)
}, app = app))
}// --- 3. RDESK FRONTEND (www/js/app.js) ---
rdesk.ready(function() {
rdesk.send("get_data", {});
});
document.getElementById("cyl-filter").addEventListener("change", function() {
rdesk.send("filter_changed", { cyl_filter: parseInt(this.value) });
});
rdesk.on("filter_changed_result", function(data) {
document.getElementById("chart").src = "data:image/png;base64," + data.chart;
renderTable(data.table.rows, data.table.cols);
});async()
workers# WRONG
app$on_message("task", async(function(payload) {
res <- run_calculation(payload)
app$toast("Task Completed!") # ERROR: app$ is not available in isolated worker processes
list(data = res)
}, app = app))
# CORRECT
app$on_message("task", async(function(payload) {
res <- run_calculation(payload)
list(data = res) # Return value is pushed automatically to task_result
}, app = app))# WRONG
dataset <- load_heavy_data()
app$on_message("run", async(function(payload) {
process(dataset) # ERROR: dataset is not bound in the worker process environment
}, app = app))
# CORRECT
dataset <- load_heavy_data()
app$on_message("run", async(function(payload) {
process(payload$data) # Pass the object explicitly via payload
}, app = app))
# OR
local_data <- dataset
app$on_message("run", async(function(payload) {
process(local_data) # Captured in the closure, serialised to the worker
}, app = app))installed.packages() to check
dependenciesapp.R launches the application window. Close the window,
edit files in R/server.R or www/, and source
app.R again.rdesk_watch(app) during development to live-reload UI
assets when saved.ci_mode)To run unit tests on message handlers without spawning a browser window:
renvrenv is active, RDesk automatically detects the
local environment.build_app() writes a frozen renv.lock into
the distributable, copying your exact package versions to ensure ABI
compatibility and prevent package-mismatch crashes.# Create scaffolding
rdesk_create_app("MyApp")
# Core Window Setup
app <- App$new(title = "App", width = 1100L, height = 740L, www = "www/")
app$on_ready(function() { ... })
app$on_message("msg_type", function(payload) { list(...) })
app$run()
# Server-Side Push
app$send("type", list(key = value))
# Dialogs
app$dialog_open(title = "Open File", filters = "CSV (*.csv)|*.csv")
app$dialog_save(title = "Save File", filters = "CSV (*.csv)|*.csv", default = "data.csv")
app$dialog_folder(title = "Select Export Folder")
# System Control
app$toast("message", type = "success|error|info|warning")
app$notify("Title", "Body Text")
app$maximize()
app$quit()
# Loading States
app$loading_start("Message...", cancellable = TRUE, job_id = "job_1")
app$loading_progress(50)
app$loading_done()
# Async Operations
rdesk_async(task, args, on_done, on_error)
rdesk_cancel_job("job_id")
rdesk_jobs_pending()// Send data to R, returns Promise
rdesk.send("msg_type", { payload_key: "value" })
.then(res => { ... })
.catch(err => { ... });
// Listen for push notifications from R
rdesk.on("push_type", function(data) { ... });
// Run when the browser bridge is active
rdesk.ready(function() { ... });
// Deregister listener
rdesk.off("push_type");