Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8cb8b82
basic functionality
May 21, 2025
1a718f3
not working version - but already refactored hugely
May 23, 2025
6f2280f
fully working version after refactoring
May 27, 2025
4816bd0
applying relevant github-advanced-security suggestions
Jun 17, 2025
d744c63
handling better when jupyter is not installed or is launched on Windo…
Oct 1, 2025
0b77318
add info about working dir to status msg
Oct 17, 2025
c00740d
new start dialog for working dir and template settings
Oct 20, 2025
9fa46b4
refactoring and other refinement edits
Oct 21, 2025
09a6d9f
hide Jupyter UI elements
Nov 3, 2025
0169fa0
Fix making template_notebooks/ sub diretory
tmszi Nov 16, 2025
8d5acae
adding the check for wx.html2 presence and making Jupyter button inac…
Jan 2, 2026
d2db982
incorporating notes from Tomas in stop_server method
Jan 2, 2026
2668c3f
Update python/grass/workflows/__init__.py
echoix Jan 2, 2026
a997e5a
fixes from Tomas Z.
Jan 4, 2026
039dd3b
edit in except handling
Jan 5, 2026
e062d68
Update python/grass/workflows/template_notebooks/welcome.ipynb
lindakarlovska Feb 5, 2026
90610fa
Update python/grass/workflows/template_notebooks/welcome.ipynb
lindakarlovska Feb 5, 2026
b02f201
Update python/grass/workflows/template_notebooks/new.ipynb
lindakarlovska Feb 5, 2026
b932a13
Update python/grass/workflows/template_notebooks/welcome.ipynb
lindakarlovska Feb 5, 2026
c35b1be
Update python/grass/workflows/server.py
lindakarlovska Feb 5, 2026
a0f51e1
Update python/grass/workflows/template_notebooks/welcome.ipynb
lindakarlovska Feb 5, 2026
f569302
Update python/grass/workflows/directory.py
lindakarlovska Feb 5, 2026
9461909
works also for multi-window GUI
Feb 5, 2026
bce91a2
changes in icons order in toolbar, button for Jupyter start always ac…
Feb 5, 2026
3a8f766
add Jupyter Notebook to file menu and change the order of items to ha…
Feb 6, 2026
6825d39
fix in the parameter calling
Feb 6, 2026
1148d73
applying suggestions from Claude regarding mainly zombie processes an…
Feb 6, 2026
a6a9bf5
shutil used to find in jupyter is in PATH - needs to be tested also o…
Feb 6, 2026
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
Binary file added gui/icons/grass/jupyter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 85 additions & 0 deletions gui/icons/grass/jupyter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions gui/wxpython/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ include $(MODULE_TOPDIR)/include/Make/Python.make
DSTDIR = $(GUIDIR)/wxpython

SRCFILES := $(wildcard icons/*.py xml/*) \
$(wildcard animation/*.py core/*.py datacatalog/*.py history/*.py dbmgr/*.py gcp/*.py gmodeler/*.py \
$(wildcard animation/*.py core/*.py datacatalog/*.py jupyter_notebook/*.py history/*.py dbmgr/*.py gcp/*.py gmodeler/*.py \
gui_core/*.py iclass/*.py lmgr/*.py location_wizard/*.py main_window/*.py mapwin/*.py mapdisp/*.py \
mapswipe/*.py modules/*.py nviz/*.py psmap/*.py rdigit/*.py \
rlisetup/*.py startup/*.py timeline/*.py vdigit/*.py \
Expand All @@ -19,7 +19,7 @@ SRCFILES := $(wildcard icons/*.py xml/*) \
DSTFILES := $(patsubst %,$(DSTDIR)/%,$(SRCFILES)) \
$(patsubst %.py,$(DSTDIR)/%.pyc,$(filter %.py,$(SRCFILES)))

PYDSTDIRS := $(patsubst %,$(DSTDIR)/%,animation core datacatalog history dbmgr gcp gmodeler \
PYDSTDIRS := $(patsubst %,$(DSTDIR)/%,animation core datacatalog jupyter_notebook history dbmgr gcp gmodeler \
gui_core iclass lmgr location_wizard main_window mapwin mapdisp modules nviz psmap \
mapswipe vdigit wxplot web_services rdigit rlisetup startup \
vnet timeline iscatt tplot photo2image image2target)
Expand Down
122 changes: 122 additions & 0 deletions gui/wxpython/jupyter_notebook/dialogs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""
@package jupyter_notebook.dialogs

@brief Integration of Jupyter Notebook to GUI.

Classes:
- dialog::JupyterStartDialog

(C) 2025 by the GRASS Development Team

This program is free software under the GNU General Public License
(>=v2). Read the file COPYING that comes with GRASS for details.

@author Linda Karlovska <linda.karlovska seznam.cz>
"""

import os
from pathlib import Path

import wx

from grass.workflows.directory import get_default_jupyter_workdir


class JupyterStartDialog(wx.Dialog):
"""Dialog for selecting Jupyter startup options."""
Comment on lines +25 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a basically a starter of jupyter notebook --no-browser with JupyterServerInstance being the controller for it. So, it seems it wouldn't be difficult to add a button here to start jupyter notebook with browser or jupyter lab. This would help users get the full Jupyter Notebook or Jupyter Lab experience without running these from the command line. Button in the GUI dialog:

Cancel | Open Notebook in Browser | Open Lab in Browser | Open Integrated Notebook

For Open Notebook in Browser and Open Lab in Browser, the notebook tab shows only controls for the shutting down the server. Open Integrated Notebook is the current experience.

This may align well with something like a Run RStudio button. We already do things to make running RStudio easier from within a GRASS session on Windows (see grass/mswindows), but in this case, we don't do anything for it in GUI (unlike Python scripts and now Jupyter notebooks).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notably, this would be still available even when the user does not have wx.html2.

Copy link
Contributor Author

@lindakarlovska lindakarlovska Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great suggestion, I like it!!


def __init__(self, parent):
super().__init__(parent, title=_("Start Jupyter Notebook"), size=(500, 300))

self.default_dir = get_default_jupyter_workdir()

self.selected_dir = self.default_dir
self.create_template = True

sizer = wx.BoxSizer(wx.VERTICAL)

# Working directory section
dir_box = wx.StaticBox(self, label=_("Notebook working directory"))
dir_sizer = wx.StaticBoxSizer(dir_box, wx.VERTICAL)

self.radio_default = wx.RadioButton(
self,
label=_("Use default: {}").format(self.default_dir),
style=wx.RB_GROUP,
)
self.radio_custom = wx.RadioButton(self, label=_("Select another directory:"))

self.dir_picker = wx.DirPickerCtrl(
self,
message=_("Choose a working directory"),
style=wx.DIRP_USE_TEXTCTRL | wx.DIRP_DIR_MUST_EXIST,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wx.DIRP_DIR_MUST_EXIST? I don't see why you shouldn't be able to create a new directory.

)
self.dir_picker.Enable(False)

dir_sizer.Add(self.radio_default, 0, wx.ALL, 5)
dir_sizer.Add(self.radio_custom, 0, wx.ALL, 5)
dir_sizer.Add(self.dir_picker, 0, wx.EXPAND | wx.ALL, 5)
sizer.Add(dir_sizer, 0, wx.EXPAND | wx.ALL, 10)

# Template preference section
options_box = wx.StaticBox(self, label=_("Options"))
options_sizer = wx.StaticBoxSizer(options_box, wx.VERTICAL)

self.checkbox_template = wx.CheckBox(self, label=_("Create welcome notebook"))
self.checkbox_template.SetValue(True)
self.checkbox_template.SetToolTip(
_(
"If selected, a welcome notebook (welcome.ipynb) will be created,\n"
"but only if the selected directory contains no .ipynb files."
)
)
options_sizer.Add(self.checkbox_template, 0, wx.ALL, 5)

info = wx.StaticText(
self,
label=_(
"Note: The welcome notebook will be created only if the directory contains no .ipynb files."
),
)

options_sizer.Add(info, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)

sizer.Add(options_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 10)

btns = self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL)
sizer.Add(btns, 0, wx.EXPAND | wx.ALL, 10)

self.SetSizer(sizer)

self.radio_default.Bind(wx.EVT_RADIOBUTTON, self.OnRadioToggle)
self.radio_custom.Bind(wx.EVT_RADIOBUTTON, self.OnRadioToggle)

self.Fit()
self.Layout()
self.SetMinSize(self.GetSize())
self.CentreOnParent()

def OnRadioToggle(self, event):
"""Enable/disable directory picker based on user choice."""
self.dir_picker.Enable(self.radio_custom.GetValue())

def GetValues(self):
"""Return selected working directory and template preference."""
if self.radio_custom.GetValue():
path = Path(self.dir_picker.GetPath())

if not os.access(path, os.W_OK) or not os.access(path, os.X_OK):
wx.MessageBox(
_("You do not have permission to write to the selected directory."),
_("Error"),
wx.ICON_ERROR,
)
return None
self.selected_dir = path
else:
self.selected_dir = self.default_dir

return {
"directory": self.selected_dir,
"create_template": self.checkbox_template.GetValue(),
}
61 changes: 61 additions & 0 deletions gui/wxpython/jupyter_notebook/frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
@package jupyter_notebook.frame

@brief Manages the Jupyter frame widget for multi-window GUI

Classes:
- frame::JupyterFrame

(C) 2025-2026 by the GRASS Development Team

This program is free software under the GNU General Public License
(>=v2). Read the file COPYING that comes with GRASS for details.

@author Linda Karlovska <linda.karlovska seznam.cz>
"""

import os
import wx

from core import globalvar
from jupyter_notebook.panel import JupyterPanel


class JupyterFrame(wx.Frame):
"""Main window for the Jupyter Notebook interface."""

def __init__(
self,
parent,
giface,
workdir=None,
create_template=False,
id=wx.ID_ANY,
title=_("Jupyter Notebook"),
**kwargs,
):
super().__init__(parent=parent, id=id, title=title, **kwargs)

self.SetName("JupyterFrame")

icon_path = os.path.join(globalvar.ICONDIR, "grass.ico")
self.SetIcon(wx.Icon(icon_path, wx.BITMAP_TYPE_ICO))

self.statusbar = self.CreateStatusBar(number=1)

self.panel = JupyterPanel(
parent=self,
giface=giface,
workdir=workdir,
create_template=create_template,
statusbar=self.statusbar,
)
self.panel.SetUpNotebookInterface()

sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.panel, 1, wx.EXPAND)
self.SetSizer(sizer)

self.SetSize((800, 600))

self.Bind(wx.EVT_CLOSE, self.panel.OnCloseWindow)
109 changes: 109 additions & 0 deletions gui/wxpython/jupyter_notebook/notebook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
@package jupyter_notebook.notebook

@brief Manages the jupyter notebook widget.

Classes:
- page::JupyterAuiNotebook

(C) 2025 by the GRASS Development Team

This program is free software under the GNU General Public License
(>=v2). Read the file COPYING that comes with GRASS for details.

@author Linda Karlovska <linda.karlovska seznam.cz>
"""

import wx
from wx.lib.agw import aui

try:
import wx.html2 as html # wx.html2 is available in wxPython 4.0 and later
except ImportError as e:
raise RuntimeError(_("wx.html2 is required for Jupyter integration.")) from e

from gui_core.wrap import SimpleTabArt


class JupyterAuiNotebook(aui.AuiNotebook):
def __init__(
self,
parent,
agwStyle=aui.AUI_NB_DEFAULT_STYLE
| aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
| aui.AUI_NB_TAB_EXTERNAL_MOVE
| aui.AUI_NB_BOTTOM
| wx.NO_BORDER,
):
"""
Wrapper for the notebook widget that manages notebook pages.
"""
self.parent = parent
self.webview = None

super().__init__(parent=self.parent, id=wx.ID_ANY, agwStyle=agwStyle)

self.SetArtProvider(SimpleTabArt())

self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.OnPageClose)

def _inject_javascript(self, event):
"""
Inject JavaScript into the Jupyter notebook page to hide top UI bars.

Works for:
- Jupyter Notebook 6 and older (classic interface)
- Jupyter Notebook 7+ (Jupyter Lab interface)

This is called once the WebView has fully loaded the Jupyter page.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but then the code uses interval to run the code, so this text should probably reflect that, possibly only before the JS code block , like "...but Jupyter UI may be (is?) create(ing?) the elements dynamically afterwards, so we need to wait for them to appear."

"""
webview = event.GetEventObject()
js = """
var interval = setInterval(function() {
// --- Jupyter Notebook 7+ (new UI) ---
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want the --- ... --- comment graphic elsewhere, so let's not add that here.

var topPanel = document.getElementById('top-panel-wrapper');
var menuPanel = document.getElementById('menu-panel-wrapper');
if (topPanel) topPanel.style.display = 'none';
if (menuPanel) menuPanel.style.display = 'none';

// --- Jupyter Notebook 6 and older (classic UI) ---
var headerContainer = document.getElementById('header-container');
var menubar = document.getElementById('menubar');
if (headerContainer) headerContainer.style.display = 'none';
if (menubar) menubar.style.display = 'none';

// --- Stop once everything is hidden ---
if ((topPanel || headerContainer) && (menuPanel || menubar)) {
clearInterval(interval);
}
}, 500);
"""
webview.RunScript(js)
Comment on lines +60 to +81
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest discussing this with Copilot or something. At least max tries would be nice.


def AddPage(self, url, title):
"""
Add a new aui notebook page with a Jupyter WebView.
:param url: URL of the Jupyter file (str).
:param title: Tab title (str).
"""
browser = html.WebView.New(self)
wx.CallAfter(browser.LoadURL, url)
wx.CallAfter(browser.Bind, html.EVT_WEBVIEW_LOADED, self._inject_javascript)
super().AddPage(browser, title)

def OnPageClose(self, event):
"""Close the aui notebook page with confirmation dialog."""
index = event.GetSelection()
title = self.GetPageText(index)

dlg = wx.MessageDialog(
self,
message=_("Really close page '{}'?").format(title),
caption=_("Close page"),
style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION,
)

if dlg.ShowModal() != wx.ID_YES:
event.Veto()

dlg.Destroy()
Loading
Loading