/* Copyright 2026 Marimo. All rights reserved. */
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import {
BookMarkedIcon,
CheckIcon,
ChevronDownCircleIcon,
ChevronRightCircleIcon,
ClipboardCopyIcon,
CodeIcon,
CommandIcon,
DatabaseIcon,
DiamondPlusIcon,
DownloadIcon,
EditIcon,
ExternalLinkIcon,
EyeOffIcon,
FastForwardIcon,
FileIcon,
Files,
FileTextIcon,
FolderDownIcon,
GithubIcon,
GlobeIcon,
HardDrive,
Home,
ImageIcon,
KeyboardIcon,
LayoutTemplateIcon,
LinkIcon,
MessagesSquareIcon,
NotebookIcon,
PanelLeftIcon,
PowerSquareIcon,
PresentationIcon,
SettingsIcon,
Share2Icon,
SparklesIcon,
Undo2Icon,
XCircleIcon,
YoutubeIcon,
ZapIcon,
} from "lucide-react";
import { settingDialogAtom } from "@/components/app-config/state";
import { MarkdownIcon } from "@/components/editor/cell/code/icons";
import { MarimoPlusIcon } from "@/components/icons/marimo-icons";
import { useImperativeModal } from "@/components/modal/ImperativeModal";
import { renderShortcut } from "@/components/shortcuts/renderShortcut";
import { PairWithAgentModal } from "@/components/editor/actions/pair-with-agent-modal";
import { ShareStaticNotebookModal } from "@/components/static-html/share-modal";
import { toast } from "@/components/ui/use-toast";
import {
canUndoDeletesAtom,
getNotebook,
hasDisabledCellsAtom,
useCellActions,
} from "@/core/cells/cells";
import { disabledCellIds } from "@/core/cells/utils";
import { useResolvedMarimoConfig } from "@/core/config/config";
import { Constants } from "@/core/constants";
import {
updateCellOutputsWithScreenshots,
useEnrichCellOutputs,
} from "@/core/export/hooks";
import { useLayoutActions, useLayoutState } from "@/core/layout/layout";
import { useTogglePresenting } from "@/core/layout/useTogglePresenting";
import { kioskModeAtom, viewStateAtom } from "@/core/mode";
import { useRequestClient } from "@/core/network/requests";
import { useFilename } from "@/core/saving/filename";
import { downloadAsHTML } from "@/core/static/download-html";
import { createShareableLink } from "@/core/wasm/share";
import { isWasm } from "@/core/wasm/utils";
import { copyToClipboard } from "@/utils/copy";
import {
ADD_PRINTING_CLASS,
downloadAsPDF,
downloadBlob,
downloadHTMLAsImage,
withLoadingToast,
} from "@/utils/download";
import { Filenames } from "@/utils/filenames";
import { Objects } from "@/utils/objects";
import type { ProgressState } from "@/utils/progress";
import { Strings } from "@/utils/strings";
import { newNotebookURL } from "@/utils/urls";
import { useRunAllCells } from "../cell/useRunCells";
import { useChromeActions, useChromeState } from "../chrome/state";
import { PANELS } from "../chrome/types";
import { AddConnectionDialogContent } from "../connections/add-connection-dialog";
import { keyboardShortcutsAtom } from "../controls/keyboard-shortcuts";
import { commandPaletteAtom } from "../controls/state";
import { displayLayoutName, getLayoutIcon } from "../renderers/layout-select";
import { LAYOUT_TYPES } from "../renderers/types";
import { runServerSidePDFDownload } from "./pdf-export";
import type { ActionButton } from "./types";
import { useCopyNotebook } from "./useCopyNotebook";
import { useHideAllMarkdownCode } from "./useHideAllMarkdownCode";
import { useRestartKernel } from "./useRestartKernel";
const NOOP_HANDLER = (event?: Event) => {
event?.preventDefault();
event?.stopPropagation();
};
export function useNotebookActions() {
const filename = useFilename();
const { openModal, closeModal } = useImperativeModal();
const { toggleApplication } = useChromeActions();
const { selectedPanel } = useChromeState();
const [viewState] = useAtom(viewStateAtom);
const kioskMode = useAtomValue(kioskModeAtom);
const hideAllMarkdownCode = useHideAllMarkdownCode();
const [resolvedConfig] = useResolvedMarimoConfig();
const {
updateCellConfig,
undoDeleteCell,
clearAllCellOutputs,
addSetupCellIfDoesntExist,
collapseAllCells,
expandAllCells,
} = useCellActions();
const restartKernel = useRestartKernel();
const runAllCells = useRunAllCells();
const copyNotebook = useCopyNotebook(filename);
const setCommandPaletteOpen = useSetAtom(commandPaletteAtom);
const setSettingsDialogOpen = useSetAtom(settingDialogAtom);
const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom);
const {
exportAsIPYNB,
exportAsMarkdown,
readCode,
saveCellConfig,
updateCellOutputs,
} = useRequestClient();
const takeScreenshots = useEnrichCellOutputs();
const hasDisabledCells = useAtomValue(hasDisabledCellsAtom);
const canUndoDeletes = useAtomValue(canUndoDeletesAtom);
const { selectedLayout } = useLayoutState();
const { setLayoutView } = useLayoutActions();
const togglePresenting = useTogglePresenting();
// Fallback: if sharing is undefined, both are enabled by default
const sharingHtmlEnabled = resolvedConfig.sharing?.html ?? true;
const sharingWasmEnabled = resolvedConfig.sharing?.wasm ?? true;
// Server-side PDF export is always available outside WASM.
// Browser print fallback is used in WASM.
const serverSidePdfEnabled = !isWasm();
const isSlidesLayout = selectedLayout === "slides";
const renderCheckboxElement = (checked: boolean) => (
{checked && }
);
const renderRecommendedElement = (recommended: boolean) => {
if (!recommended) {
return null;
}
return (
Recommended
);
};
const downloadServerSidePDF = async ({
preset,
title,
}: {
preset: "document" | "slides";
title: string;
}) => {
if (!filename) {
toastNotebookMustBeNamed();
return;
}
const runDownload = async (progress: ProgressState) => {
await updateCellOutputsWithScreenshots({
takeScreenshots: () => takeScreenshots({ progress }),
updateCellOutputs,
});
await runServerSidePDFDownload({
filename,
preset,
downloadPDF: downloadAsPDF,
});
};
await withLoadingToast(title, runDownload);
};
const handleDocumentPDF = async () => {
if (serverSidePdfEnabled) {
await downloadServerSidePDF({
preset: "document",
title: "Downloading Document PDF...",
});
return;
}
const beforeprint = new Event("export-beforeprint");
const afterprint = new Event("export-afterprint");
window.dispatchEvent(beforeprint);
setTimeout(() => window.print(), 0);
setTimeout(() => window.dispatchEvent(afterprint), 0);
};
const handleDownloadAsIPYNB = async () => {
if (!filename) {
toastNotebookMustBeNamed();
return;
}
const runDownload = async (progress: ProgressState) => {
await updateCellOutputsWithScreenshots({
takeScreenshots: () => takeScreenshots({ progress }),
updateCellOutputs,
});
const ipynb = await exportAsIPYNB({ download: false });
downloadBlob(
new Blob([ipynb], { type: "application/x-ipynb+json" }),
Filenames.toIPYNB(document.title),
);
};
await withLoadingToast("Downloading IPYNB...", runDownload);
};
const actions: ActionButton[] = [
{
icon: ,
label: "Download",
handle: NOOP_HANDLER,
dropdown: [
{
icon: ,
label: "Download as HTML",
handle: async () => {
if (!filename) {
toastNotebookMustBeNamed();
return;
}
await downloadAsHTML({ filename, includeCode: true });
},
},
{
icon: ,
label: "Download as HTML (exclude code)",
handle: async () => {
if (!filename) {
toastNotebookMustBeNamed();
return;
}
await downloadAsHTML({ filename, includeCode: false });
},
},
{
icon: (
),
label: "Download as Markdown",
handle: async () => {
const md = await exportAsMarkdown({ download: false });
downloadBlob(
new Blob([md], { type: "text/plain" }),
Filenames.toMarkdown(document.title),
);
},
},
{
icon: ,
label: "Download as ipynb",
handle: handleDownloadAsIPYNB,
},
{
icon: ,
label: "Download Python code",
handle: async () => {
const code = await readCode();
downloadBlob(
new Blob([code.contents], { type: "text/plain" }),
Filenames.toPY(document.title),
);
},
},
{
divider: true,
icon: ,
label: "Download as PNG",
disabled: viewState.mode !== "present",
tooltip:
viewState.mode === "present" ? undefined : (
Only available in app view.
Toggle with: {renderShortcut("global.hideCode", false)}
),
handle: async () => {
const app = document.getElementById("App");
if (!app) {
return;
}
await downloadHTMLAsImage({
element: app,
filename: document.title,
// Add body.printing ONLY when converting the whole notebook to a screenshot
prepare: ADD_PRINTING_CLASS,
});
},
},
isSlidesLayout
? {
divider: true,
icon: ,
label: "Download as PDF",
handle: NOOP_HANDLER,
dropdown: [
{
icon: ,
label: "Document Layout",
handle: handleDocumentPDF,
},
{
icon: ,
label: "Slides Layout",
rightElement: renderRecommendedElement(true),
hidden: !serverSidePdfEnabled,
handle: async () => {
await downloadServerSidePDF({
preset: "slides",
title: "Downloading Slides PDF...",
});
},
},
],
}
: {
divider: true,
icon: ,
label: "Download as PDF",
handle: handleDocumentPDF,
},
],
},
{
icon: ,
label: "Pair with an agent",
hidden: isWasm(),
handle: async () => {
openModal();
},
},
{
icon: ,
label: "Share",
handle: NOOP_HANDLER,
hidden: !sharingHtmlEnabled && !sharingWasmEnabled,
dropdown: [
{
icon: ,
label: "Publish HTML to web",
hidden: !sharingHtmlEnabled,
handle: async () => {
openModal();
},
},
{
icon: ,
label: "Create WebAssembly link",
hidden: !sharingWasmEnabled,
handle: async () => {
const code = await readCode();
const url = createShareableLink({ code: code.contents });
await copyToClipboard(url);
toast({
title: "Copied",
description: "Link copied to clipboard.",
});
},
},
{
icon: ,
label: "Create molab notebook",
handle: async () => {
const code = await readCode();
const url = createShareableLink({
code: code.contents,
baseUrl: `${Constants.molab}/new`,
});
window.open(url, "_blank");
},
},
],
},
{
icon: ,
label: "Helper panel",
redundant: true,
handle: NOOP_HANDLER,
dropdown: PANELS.flatMap(
({ type: id, Icon, hidden, additionalKeywords }) => {
if (hidden) {
return [];
}
return {
label: Strings.startCase(id),
rightElement: renderCheckboxElement(selectedPanel === id),
icon: ,
handle: () => toggleApplication(id),
additionalKeywords,
};
},
),
},
{
icon: ,
label: "Present as",
handle: NOOP_HANDLER,
dropdown: [
{
icon:
viewState.mode === "present" ? (
) : (
),
label: "Toggle app view",
hotkey: "global.hideCode",
handle: () => {
togglePresenting();
},
},
...LAYOUT_TYPES.map((type, idx) => {
const Icon = getLayoutIcon(type);
return {
divider: idx === 0,
label: displayLayoutName(type),
icon: ,
rightElement: (
{selectedLayout === type && }
),
handle: () => {
setLayoutView(type);
// Toggle if it's not in present mode
if (viewState.mode === "edit") {
togglePresenting();
}
},
};
}),
],
},
{
icon: ,
label: "Duplicate notebook",
hidden: !filename || isWasm(),
handle: copyNotebook,
},
{
icon: ,
label: "Copy code to clipboard",
hidden: !filename,
handle: async () => {
const code = await readCode();
await copyToClipboard(code.contents);
toast({
title: "Copied",
description: "Code copied to clipboard.",
});
},
},
{
icon: ,
label: "Enable all cells",
hidden: !hasDisabledCells || kioskMode,
handle: async () => {
const notebook = getNotebook();
const ids = disabledCellIds(notebook);
const newConfigs = Objects.fromEntries(
ids.map((cellId) => [cellId, { disabled: false }]),
);
// send to BE
await saveCellConfig({ configs: newConfigs });
// update on FE
for (const cellId of ids) {
updateCellConfig({ cellId, config: { disabled: false } });
}
},
},
{
divider: true,
icon: ,
label: "Add setup cell",
handle: () => {
addSetupCellIfDoesntExist({});
},
},
{
icon: ,
label: "Add database connection",
handle: () => {
openModal();
},
},
{
icon: ,
label: "Add remote storage",
handle: () => {
openModal(
,
);
},
},
{
icon: ,
label: "Undo cell deletion",
hidden: !canUndoDeletes || kioskMode,
handle: () => {
undoDeleteCell();
},
},
{
icon: ,
label: "Restart kernel",
variant: "danger",
handle: restartKernel,
additionalKeywords: ["reset", "reload", "restart"],
},
{
icon: ,
label: "Re-run all cells",
redundant: true,
hotkey: "global.runAll",
handle: async () => {
runAllCells();
},
},
{
icon: ,
label: "Clear all outputs",
redundant: true,
handle: () => {
clearAllCellOutputs();
},
},
{
icon: ,
label: "Hide all markdown code",
handle: hideAllMarkdownCode,
redundant: true, // hidden by default
},
{
icon: ,
label: "Collapse all sections",
hotkey: "global.collapseAllSections",
handle: collapseAllCells,
redundant: true,
},
{
icon: ,
label: "Expand all sections",
hotkey: "global.expandAllSections",
handle: expandAllCells,
redundant: true,
},
{
divider: true,
icon: ,
label: "Command palette",
hotkey: "global.commandPalette",
handle: () => setCommandPaletteOpen((open) => !open),
},
{
icon: ,
label: "Keyboard shortcuts",
hotkey: "global.showHelp",
handle: () => setKeyboardShortcutsOpen((open) => !open),
},
{
icon: ,
label: "User settings",
handle: () => setSettingsDialogOpen((open) => !open),
redundant: true,
additionalKeywords: ["preferences", "options", "configuration"],
},
{
icon: ,
label: "Resources",
handle: NOOP_HANDLER,
dropdown: [
{
icon: ,
label: "Documentation",
handle: () => {
window.open(Constants.docsPage, "_blank");
},
},
{
icon: ,
label: "GitHub",
handle: () => {
window.open(Constants.githubPage, "_blank");
},
},
{
icon: ,
label: "Discord Community",
handle: () => {
window.open(Constants.discordLink, "_blank");
},
},
{
icon: ,
label: "YouTube",
handle: () => {
window.open(Constants.youtube, "_blank");
},
},
{
icon: ,
label: "Changelog",
handle: () => {
window.open(Constants.releasesPage, "_blank");
},
},
],
},
{
divider: true,
icon: ,
label: "Return home",
// If file is in the url, then we ran `marimo edit`
// without a specific file
hidden: !location.search.includes("file"),
handle: () => {
const withoutSearch = document.baseURI.split("?")[0];
window.open(withoutSearch, "_self");
},
},
{
icon: ,
label: "New notebook",
// If file is in the url, then we ran `marimo edit`
// without a specific file
hidden: !location.search.includes("file"),
handle: () => {
const url = newNotebookURL();
window.open(url, "_blank");
},
},
];
return actions
.filter((a) => !a.hidden)
.map((action) => {
if (action.dropdown) {
return {
...action,
dropdown: action.dropdown.filter((item) => !item.hidden),
};
}
return action;
});
}
function toastNotebookMustBeNamed() {
toast({
title: "Error",
description: "Notebooks must be named to be exported.",
variant: "danger",
});
}