Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/core/islands/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ export async function initialize() {
case "model-lifecycle":
handleWidgetMessage(MODEL_MANAGER, msg.data);
return;
case "update-css":
return;
default:
logNever(msg.data);
}
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/core/websocket/useMarimoKernelConnection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,16 @@ export function useMarimoKernelConnection(opts: {
case "update-cell-ids":
setCellIds({ cellIds: msg.data.cell_ids as CellId[] });
return;
case "update-css": {
let el = document.querySelector("style[title='marimo-custom']");
if (!el) {
el = document.createElement("style");
el.setAttribute("title", "marimo-custom");
document.head.append(el);
}
el.textContent = msg.data.css;
return;
}
default:
logNever(msg.data);
}
Expand Down
1 change: 1 addition & 0 deletions marimo/_cli/development/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def _generate_server_api_schema() -> dict[str, Any]:
notifications.UpdateCellCodesNotification,
notifications.UpdateCellIdsNotification,
notifications.FocusCellNotification,
notifications.UpdateCssNotification,
notifications.NotificationMessage,
# ai
ChatMessage,
Expand Down
13 changes: 13 additions & 0 deletions marimo/_messaging/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,17 @@ class UpdateCellIdsNotification(Notification, tag="update-cell-ids"):
cell_ids: list[CellId_t]


class UpdateCssNotification(Notification, tag="update-css"):
"""Pushes updated CSS content to the frontend.

Attributes:
css: The new CSS content (empty string clears custom CSS).
"""

name: ClassVar[str] = "update-css"
css: str


NotificationMessage = Union[
# Cell operations
CellNotification,
Expand Down Expand Up @@ -779,4 +790,6 @@ class UpdateCellIdsNotification(Notification, tag="update-cell-ids"):
FocusCellNotification,
UpdateCellCodesNotification,
UpdateCellIdsNotification,
# CSS hot reload
UpdateCssNotification,
]
26 changes: 26 additions & 0 deletions marimo/_server/api/endpoints/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,32 @@ async def save_app_config(
app_state = AppState(request)
body = await parse_request(request, cls=SaveAppConfigurationRequest)
session = app_state.require_current_session()

old_css_file = session.app_file_manager.app.config.css_file
contents = session.app_file_manager.save_app_config(body.config)

# If css_file changed, push new CSS to the frontend and update watcher
if "css_file" in body.config:
new_css_file = session.app_file_manager.app.config.css_file
if old_css_file != new_css_file:
from marimo._messaging.notification import (
UpdateCssNotification,
)
from marimo._session.file_watcher_integration import (
SessionFileWatcherExtension,
)
from marimo._session.session import SessionImpl

css_content = session.app_file_manager.read_css_file() or ""
session.notify(
UpdateCssNotification(css=css_content),
from_consumer_id=None,
)

if isinstance(session, SessionImpl):
for ext in session.extensions:
if isinstance(ext, SessionFileWatcherExtension):
ext.update_css_watcher(session)
break

return PlainTextResponse(content=contents)
77 changes: 75 additions & 2 deletions marimo/_session/file_watcher_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import TYPE_CHECKING, Callable

from marimo import _loggers
from marimo._messaging.notification import UpdateCssNotification
from marimo._session.events import (
SessionEventBus,
SessionEventListener,
Expand All @@ -26,6 +27,20 @@
from collections.abc import Awaitable


def _resolve_css_path(session: Session) -> Path | None:
"""Resolve the absolute path to the CSS file from session config."""
css_file = session.app_file_manager.app.config.css_file
notebook_path = session.app_file_manager.path
if not css_file or not notebook_path:
return None

filepath = Path(css_file)
if not filepath.is_absolute():
filepath = Path(notebook_path).parent / filepath

return filepath


class SessionFileWatcherExtension(SessionExtension, SessionEventListener):
"""Manages file watcher lifecycle for sessions."""

Expand All @@ -44,6 +59,7 @@ def __init__(
self._event_bus: SessionEventBus | None = None
self._session: Session | None = None
self._on_change_callback = on_change_callback
self._css_path: Path | None = None

def on_attach(self, session: Session, event_bus: SessionEventBus) -> None:
"""Attach the file watcher extension to a session."""
Expand All @@ -54,24 +70,78 @@ def on_attach(self, session: Session, event_bus: SessionEventBus) -> None:
if not session.app_file_manager.path:
return

# Register with the watcher manager
# Register notebook file watcher
self._watcher_manager.add_callback(
Path(session.app_file_manager.path), self._handle_file_change
)

# Register CSS file watcher if configured
self._start_css_watcher(session)

LOGGER.info(
"Attached file watcher for session %s at path %s",
session.initialization_id,
session.app_file_manager.path,
)

def _start_css_watcher(self, session: Session) -> None:
"""Start watching the CSS file if configured."""
css_path = _resolve_css_path(session)
if css_path and css_path.exists():
self._css_path = css_path
self._watcher_manager.add_callback(
css_path, self._handle_css_change
)
LOGGER.debug("Watching CSS file: %s", css_path)

def _stop_css_watcher(self) -> None:
"""Stop watching the current CSS file if any."""
if self._css_path:
self._watcher_manager.remove_callback(
self._css_path, self._handle_css_change
)
LOGGER.debug("Stopped watching CSS file: %s", self._css_path)
self._css_path = None

def update_css_watcher(self, session: Session) -> None:
"""Update the CSS file watcher after a config change.

Compares the currently watched path with the new config and
registers/deregisters watchers as needed.
"""
new_css_path = _resolve_css_path(session)

# Nothing changed
if self._css_path == new_css_path:
return

self._stop_css_watcher()
if new_css_path and new_css_path.exists():
self._css_path = new_css_path
self._watcher_manager.add_callback(
new_css_path, self._handle_css_change
)
LOGGER.debug("Updated CSS watcher to: %s", new_css_path)

async def _handle_file_change(self, path: Path) -> None:
"""Handle a file change."""
if not self._session:
return

await self._on_change_callback(path, self._session)

async def _handle_css_change(self, path: Path) -> None:
"""Handle a CSS file change by pushing new content to the frontend."""
if not self._session:
return

css_content = self._session.app_file_manager.read_css_file() or ""
self._session.notify(
UpdateCssNotification(css=css_content),
from_consumer_id=None,
)
LOGGER.debug("Sent CSS update for: %s", path)

def on_detach(self) -> None:
"""Detach the file watcher extension from a session."""
if not self._session:
Expand All @@ -80,12 +150,15 @@ def on_detach(self) -> None:
if not self._session.app_file_manager.path:
return

# Remove from watcher manager
# Remove notebook file watcher
self._watcher_manager.remove_callback(
self._canonicalize_path(self._session.app_file_manager.path),
self._handle_file_change,
)

# Remove CSS file watcher
self._stop_css_watcher()

LOGGER.info(
"Detached file watcher for session %s from path %s",
self._session.initialization_id,
Expand Down
19 changes: 18 additions & 1 deletion packages/openapi/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2453,6 +2453,7 @@ components:
- $ref: '#/components/schemas/FocusCellNotification'
- $ref: '#/components/schemas/UpdateCellCodesNotification'
- $ref: '#/components/schemas/UpdateCellIdsNotification'
- $ref: '#/components/schemas/UpdateCssNotification'
discriminator:
mapping:
alert: '#/components/schemas/AlertNotification'
Expand Down Expand Up @@ -2488,6 +2489,7 @@ components:
storage-namespaces: '#/components/schemas/StorageNamespacesNotification'
update-cell-codes: '#/components/schemas/UpdateCellCodesNotification'
update-cell-ids: '#/components/schemas/UpdateCellIdsNotification'
update-css: '#/components/schemas/UpdateCssNotification'
validate-sql-result: '#/components/schemas/ValidateSQLResultNotification'
variable-values: '#/components/schemas/VariableValuesNotification'
variables: '#/components/schemas/VariablesNotification'
Expand Down Expand Up @@ -4581,6 +4583,20 @@ components:
- cellIdsToOutput
title: UpdateCellOutputsRequest
type: object
UpdateCssNotification:
description: "Pushes updated CSS content to the frontend.\n\n Attributes:\n\
\ css: The new CSS content (empty string clears custom CSS)."
properties:
css:
type: string
op:
enum:
- update-css
required:
- op
- css
title: UpdateCssNotification
type: object
UpdateUIElementCommand:
description: "Update UI element values.\n\n Triggered when users interact\
\ with UI elements (sliders, inputs, dropdowns, etc.).\n Updates element\
Expand Down Expand Up @@ -4938,7 +4954,7 @@ components:
type: object
info:
title: marimo API
version: 0.19.10
version: 0.19.11
openapi: 3.1.0
paths:
/@file/{filename_and_length}:
Expand Down Expand Up @@ -6311,3 +6327,4 @@ paths:
summary: Submit login form
tags:
- auth

15 changes: 14 additions & 1 deletion packages/openapi/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4555,7 +4555,8 @@ export interface components {
| components["schemas"]["CacheInfoNotification"]
| components["schemas"]["FocusCellNotification"]
| components["schemas"]["UpdateCellCodesNotification"]
| components["schemas"]["UpdateCellIdsNotification"];
| components["schemas"]["UpdateCellIdsNotification"]
| components["schemas"]["UpdateCssNotification"];
};
/**
* LanguageServersConfig
Expand Down Expand Up @@ -5838,6 +5839,18 @@ export interface components {
];
};
};
/**
* UpdateCssNotification
* @description Pushes updated CSS content to the frontend.
*
* Attributes:
* css: The new CSS content (empty string clears custom CSS).
*/
UpdateCssNotification: {
css: string;
/** @enum {unknown} */
op: "update-css";
};
/**
* UpdateUIElementCommand
* @description Update UI element values.
Expand Down
Loading
Loading