Skip to content
Closed
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
4 changes: 3 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away

> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-26 · **Change:** minor (Python cells in the interface; earlier: Python cells stored and kept in files; earlier: AutoSum over a selected row puts its total to the right, and fills a selection's empty last row and column) · **Target:** Microsoft Excel (Microsoft 365)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-26 · **Change:** minor (the Python editor window; earlier: Python cells in the interface; earlier: Python cells stored and kept in files; earlier: AutoSum over a selected row puts its total to the right, and fills a selection's empty last row and column) · **Target:** Microsoft Excel (Microsoft 365)

GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX
is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web.
Expand Down Expand Up @@ -135,6 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in

| Date | What landed |
|---|---|
| 2026-10-26 | The Python editor: a resizable window for a Python cell's code, with line numbers and Python colours, opened from a button beside the formula bar's PY badge, from Edit in Python Editor in a Python cell's menu, or with `ui.pythonEditor`; Ctrl+Enter or Apply puts the code in the cell through `python.set`, and closing it, opening it on another cell, closing or leaving its workbook, or quitting first asks about unsaved code. Typing or pasting past what a cell holds is refused as it happens. The code still doesn't run |
| 2026-10-26 | Python cells in the interface: Python mode in the formula bar (Ctrl+Alt+Shift+P, `=PY` then Tab, editing a Python cell, Formulas › Insert Python) with Enter for a new line, Tab to indent and Ctrl+Enter to commit through `python.set`; PY tags on Python cells and a card with the cached result or why the cell shows `#BLOCKED!`; Ctrl+Alt+Shift+M and the cell menu switch between Python Object and Excel Value (`python.output`). The code still doesn't run |
| 2026-10-25 | Python cells as data: `python.set` stores code in a cell, Python cells copy, move, fill and undo like formulas, and `.xlsx` reads and writes Python cells as [MS-XLSX] specifies (`_xlfn._xlws.PY(index,0 or 1)`, the code in the workbook's Python part) with cached values and spills, so such workbooks open with their results; nothing runs Python (new code shows `#BLOCKED!`), and opening a file limits code size, result size and spill ranges |
| 2026-10-25 | AutoSum over a selection does what Excel does: a selected row gets one total to its right (in its last cell if that is empty), not a one-cell total under each cell; a block whose last column is empty gets each row's total there, one whose last row is empty each column's, and with both the corner gets the total of the block; otherwise totals go under each column as before |
Expand Down Expand Up @@ -244,6 +245,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in

| Date | Change | Summary |
|---|---|---|
| 2026-10-26 | minor | Progress log: the Python editor window |
| 2026-10-26 | minor | Progress log: Python cells in the interface |
| 2026-10-25 | minor | Progress log: Python cells as data |
| 2026-10-25 | minor | Progress log: AutoSum over a selected row, and a selection's empty last row and column |
Expand Down
3 changes: 2 additions & 1 deletion crates/ui-egui/examples/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
//! [--dialog 'name={json}'] [--hf-edit header|footer] [--ui 'id={json}']... out.png`
//!
//! `--cmd` runs an engine command on the session before the app starts; `--ui` runs a command
//! through the app (UI commands too, such as `ui.pythonMode` or `ui.pythonCard`) after it starts.
//! through the app (UI commands too, such as `ui.pythonMode`, `ui.pythonCard` or `ui.pythonEditor`)
//! after it starts.

use gridcraft_engine::Session;
use gridcraft_ui_egui::SheetApp;
Expand Down
233 changes: 211 additions & 22 deletions crates/ui-egui/src/code_editor.rs

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions crates/ui-egui/src/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ pub fn inspect(app: &SheetApp, ctx: &egui::Context) -> Json {
"scroll": [v.scroll.x, v.scroll.y],
"editor": app.editor.as_ref().map(|e| json!({"cell": e.cell.a1(), "text": e.text, "formulaBar": e.from_formula_bar, "point": e.can_point(), "python": e.python})),
"dialog": app.dialog.as_ref().map(|d| json!({"name": d.name, "title": d.title, "values": d.values, "tab": d.tab})),
"pythonEditor": crate::python_window::state(app),
"backstage": app.backstage.as_ref().map(|b| b.page.id()),
"message": app.message.as_ref().map(|(t, m)| json!({"title": t, "text": m})),
"selectedChart": app.selected_chart,
Expand Down Expand Up @@ -846,12 +847,17 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) -
}
}
"app.save" => wrap(app.run("file.save", json!({"path": s("path")}))),
"app.quit" => {
// Programmatic calls never open dialogs: quitting from here doesn't ask about unsaved work.
app.allow_close = true;
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
ok(Json::Null)
}
// Programmatic calls never open dialogs: quitting from here doesn't ask about unsaved
// workbooks. Unsaved code in the Python editor refuses the quit instead, as it refuses
// `file.close` (the code is in no workbook, so nothing could save it).
"app.quit" => match crate::python_window::quit_refused(app) {
Some(e) => err(e),
None => {
app.allow_close = true;
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
ok(Json::Null)
}
},
// Anything else that names a command runs it.
other if gridcraft_engine::find_command(other).is_some() => wrap(direct(app, other, p, true)),
other => err(format!("unknown method `{other}`")),
Expand Down
22 changes: 22 additions & 0 deletions crates/ui-egui/src/formula_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ pub fn show(app: &mut SheetApp, ui: &mut egui::Ui) {
if let Some(badge) = python_badge(app, ui, &t) {
layout.buttons = layout.buttons.union(badge);
}
if let Some(button) = python_editor_button(app, ui, &t) {
layout.buttons = layout.buttons.union(button);
}
ui.add_space(4.0);
// The formula field fills the rest, as tall as the bar.
let avail = ui.available_width() - 24.0;
Expand Down Expand Up @@ -276,6 +279,25 @@ fn python_badge(app: &mut SheetApp, ui: &mut egui::Ui, t: &Tokens) -> Option<Rec
Some(rect)
}

/// The Python editor button beside the PY badge, shown with it: opens the editor window on the
/// cell, with the code as typed so far in Python mode. Returns its rect when shown.
fn python_editor_button(app: &mut SheetApp, ui: &mut egui::Ui, t: &Tokens) -> Option<Rect> {
let show = match &app.editor {
Some(ed) => ed.python,
None => active_python(app).is_some(),
};
// Not on code hidden on a protected sheet: the window would show it.
if !show || !crate::python_window::bar_offers(app) {
return None;
}
let label = app.ui.language.tr(crate::python_window::EDIT_IN_EDITOR).to_string();
let resp = crate::widgets::icon_button(ui, Icon::PythonEditor, t.text, &label, vec2(24.0, 24.0));
if resp.clicked() {
crate::python_window::open_from_bar(app);
}
Some(resp.rect)
}

/// Draws the Name Box and its name dropdown; Enter jumps to a reference or known name (or defines a new name) and is consumed so the grid does not move down.
fn name_box(app: &mut SheetApp, ui: &mut egui::Ui, t: &Tokens) -> Rect {
let id = egui::Id::unique("gridcraft.name_box");
Expand Down
18 changes: 18 additions & 0 deletions crates/ui-egui/src/i18n/cs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"This version of GridCraft stores Python code but does not run it." => "Tato verze GridCraftu ukládá kód Pythonu, ale nespouští ho.",
"Insert\nPython" => "Vložit\nPython",
"Insert Python (Ctrl+Alt+Shift+P)" => "Vložit Python (Ctrl+Alt+Shift+P)",
"Python Editor" => "Editor Pythonu",
"Edit in Python Editor" => "Upravit v editoru Pythonu",
"Unsaved changes" => "Neuložené změny",
"The code for {cell} has unsaved changes." => "Kód pro {cell} obsahuje neuložené změny.",
"The code for {cell} in {workbook} has unsaved changes." => "Kód pro {cell} v sešitu {workbook} obsahuje neuložené změny.",
"The code can't be longer: a Python cell holds {max} characters, counted as a PY formula." => {
"Kód nemůže být delší: buňka Pythonu pojme {max} znaků počítaných jako vzorec PY."
}
"{cell} changed while it was open in the Python editor. Copy the code, or close the editor and open it again on the cell." => {
"Buňka {cell} se změnila, zatímco byla otevřená v editoru Pythonu. Zkopírujte kód, nebo editor zavřete a otevřete ho na buňce znovu."
}
"{cell} changed while it was open in the Python editor, so the code was not applied. Copy the code, or close the editor and open it again on the cell." => {
"Buňka {cell} se změnila, zatímco byla otevřená v editoru Pythonu, takže kód nebyl použit. Zkopírujte kód, nebo editor zavřete a otevřete ho na buňce znovu."
}
"{apply} applies the code. Esc closes the editor." => "{apply} použije kód. Esc zavře editor.",
"The Python editor's unsaved code was copied to the clipboard." => "Neuložený kód z editoru Pythonu byl zkopírován do schránky.",
"Apply" => "Použít",
"Discard" => "Zahodit",
"Evaluate Formula" => "Vyhodnotit vzorec",
"Watch Window" => "Okno sledování",
"Calculation\nOptions" => "Možnosti\nvýpočtu",
Expand Down
21 changes: 21 additions & 0 deletions crates/ui-egui/src/i18n/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,27 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
}
"Insert\nPython" => "Python\neinfügen",
"Insert Python (Ctrl+Alt+Shift+P)" => "Python einfügen (Ctrl+Alt+Shift+P)",
"Python Editor" => "Python-Editor",
"Edit in Python Editor" => "Im Python-Editor bearbeiten",
"Unsaved changes" => "Nicht gespeicherte Änderungen",
"The code for {cell} has unsaved changes." => "Der Code für {cell} hat nicht gespeicherte Änderungen.",
"The code for {cell} in {workbook} has unsaved changes." => "Der Code für {cell} in {workbook} hat nicht gespeicherte Änderungen.",
"The code can't be longer: a Python cell holds {max} characters, counted as a PY formula." => {
"Der Code kann nicht länger werden: Eine Python-Zelle fasst {max} Zeichen, als PY-Formel gezählt."
}
"{cell} changed while it was open in the Python editor. Copy the code, or close the editor and open it again on the cell." => {
"{cell} hat sich geändert, während die Zelle im Python-Editor geöffnet war. Kopieren Sie den Code, oder schließen Sie den Editor und öffnen Sie ihn erneut für die Zelle."
}
"{cell} changed while it was open in the Python editor, so the code was not applied. Copy the code, or close the editor and open it again on the cell." => {
"{cell} hat sich geändert, während die Zelle im Python-Editor geöffnet war, daher wurde der Code nicht übernommen. Kopieren Sie den Code, oder schließen Sie den Editor und öffnen Sie ihn erneut für die Zelle."
}
"{apply} applies the code. Esc closes the editor." => "{apply} übernimmt den Code. Esc schließt den Editor.",
"The Python editor's unsaved code was copied to the clipboard." => {
"Der nicht gespeicherte Code des Python-Editors wurde in die Zwischenablage kopiert."
}
"Apply" => "Übernehmen",
"Discard" => "Verwerfen",
"Cancel" => "Abbrechen",
"Evaluate Formula" => "Formel auswerten",
"Watch Window" => "Überwachungsfenster",
"Calculation\nOptions" => "Berechnungs-\noptionen",
Expand Down
20 changes: 20 additions & 0 deletions crates/ui-egui/src/i18n/el.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,26 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
}
"Insert\nPython" => "Εισαγωγή\nPython",
"Insert Python (Ctrl+Alt+Shift+P)" => "Εισαγωγή Python (Ctrl+Alt+Shift+P)",
"Python Editor" => "Πρόγραμμα επεξεργασίας Python",
"Edit in Python Editor" => "Επεξεργασία στο πρόγραμμα επεξεργασίας Python",
"Unsaved changes" => "Μη αποθηκευμένες αλλαγές",
"The code for {cell} has unsaved changes." => "Ο κώδικας για το {cell} έχει μη αποθηκευμένες αλλαγές.",
"The code for {cell} in {workbook} has unsaved changes." => "Ο κώδικας για το {cell} στο {workbook} έχει μη αποθηκευμένες αλλαγές.",
"The code can't be longer: a Python cell holds {max} characters, counted as a PY formula." => {
"Ο κώδικας δεν μπορεί να γίνει μεγαλύτερος: ένα κελί Python χωρά {max} χαρακτήρες, μετρημένους ως τύπος PY."
}
"{cell} changed while it was open in the Python editor. Copy the code, or close the editor and open it again on the cell." => {
"Το κελί {cell} άλλαξε ενώ ήταν ανοιχτό στο πρόγραμμα επεξεργασίας Python. Αντιγράψτε τον κώδικα ή κλείστε το πρόγραμμα επεξεργασίας και ανοίξτε το ξανά στο κελί."
}
"{cell} changed while it was open in the Python editor, so the code was not applied. Copy the code, or close the editor and open it again on the cell." => {
"Το κελί {cell} άλλαξε ενώ ήταν ανοιχτό στο πρόγραμμα επεξεργασίας Python, επομένως ο κώδικας δεν εφαρμόστηκε. Αντιγράψτε τον κώδικα ή κλείστε το πρόγραμμα επεξεργασίας και ανοίξτε το ξανά στο κελί."
}
"{apply} applies the code. Esc closes the editor." => "Το {apply} εφαρμόζει τον κώδικα. Το Esc κλείνει το πρόγραμμα επεξεργασίας.",
"The Python editor's unsaved code was copied to the clipboard." => {
"Ο μη αποθηκευμένος κώδικας του προγράμματος επεξεργασίας Python αντιγράφηκε στο πρόχειρο."
}
"Apply" => "Εφαρμογή",
"Discard" => "Απόρριψη",
"Evaluate Formula" => "Αξιολόγηση τύπου",
"Watch Window" => "Παράθυρο παρακολούθησης",
"Calculation\nOptions" => "Επιλογές\nυπολογισμού",
Expand Down
19 changes: 19 additions & 0 deletions crates/ui-egui/src/i18n/es.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,25 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
}
"Insert\nPython" => "Insertar\nPython",
"Insert Python (Ctrl+Alt+Shift+P)" => "Insertar Python (Ctrl+Alt+Shift+P)",
"Python Editor" => "Editor de Python",
"Edit in Python Editor" => "Editar en el editor de Python",
"Unsaved changes" => "Cambios sin guardar",
"The code for {cell} has unsaved changes." => "El código de {cell} tiene cambios sin guardar.",
"The code for {cell} in {workbook} has unsaved changes." => "El código de {cell} en {workbook} tiene cambios sin guardar.",
"The code can't be longer: a Python cell holds {max} characters, counted as a PY formula." => {
"El código no puede ser más largo: una celda de Python admite {max} caracteres, contados como fórmula PY."
}
"{cell} changed while it was open in the Python editor. Copy the code, or close the editor and open it again on the cell." => {
"{cell} cambió mientras estaba abierta en el editor de Python. Copie el código, o cierre el editor y vuelva a abrirlo en la celda."
}
"{cell} changed while it was open in the Python editor, so the code was not applied. Copy the code, or close the editor and open it again on the cell." => {
"{cell} cambió mientras estaba abierta en el editor de Python, así que el código no se aplicó. Copie el código, o cierre el editor y vuelva a abrirlo en la celda."
}
"{apply} applies the code. Esc closes the editor." => "{apply} aplica el código. Esc cierra el editor.",
"The Python editor's unsaved code was copied to the clipboard." => "El código sin guardar del editor de Python se copió en el portapapeles.",
"Apply" => "Aplicar",
"Discard" => "Descartar",
"Cancel" => "Cancelar",
"Evaluate Formula" => "Evaluar fórmula",
"Watch Window" => "Ventana Inspección",
"Calculation\nOptions" => "Opciones\nde cálculo",
Expand Down
21 changes: 21 additions & 0 deletions crates/ui-egui/src/i18n/fr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,27 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
}
"Insert\nPython" => "Insérer\nPython",
"Insert Python (Ctrl+Alt+Shift+P)" => "Insérer Python (Ctrl+Alt+Shift+P)",
"Python Editor" => "Éditeur Python",
"Edit in Python Editor" => "Modifier dans l'éditeur Python",
"Unsaved changes" => "Modifications non enregistrées",
"The code for {cell} has unsaved changes." => "Le code de {cell} contient des modifications non enregistrées.",
"The code for {cell} in {workbook} has unsaved changes." => "Le code de {cell} dans {workbook} contient des modifications non enregistrées.",
"The code can't be longer: a Python cell holds {max} characters, counted as a PY formula." => {
"Le code ne peut pas être plus long : une cellule Python contient {max} caractères, comptés comme formule PY."
}
"{cell} changed while it was open in the Python editor. Copy the code, or close the editor and open it again on the cell." => {
"{cell} a changé pendant son ouverture dans l'éditeur Python. Copiez le code, ou fermez l'éditeur et rouvrez-le sur la cellule."
}
"{cell} changed while it was open in the Python editor, so the code was not applied. Copy the code, or close the editor and open it again on the cell." => {
"{cell} a changé pendant son ouverture dans l'éditeur Python, le code n'a donc pas été appliqué. Copiez le code, ou fermez l'éditeur et rouvrez-le sur la cellule."
}
"{apply} applies the code. Esc closes the editor." => "{apply} applique le code. Échap ferme l'éditeur.",
"The Python editor's unsaved code was copied to the clipboard." => {
"Le code non enregistré de l'éditeur Python a été copié dans le Presse-papiers."
}
"Apply" => "Appliquer",
"Discard" => "Ignorer",
"Cancel" => "Annuler",
"Evaluate Formula" => "Évaluation de formule",
"Watch Window" => "Fenêtre Espion",
"Calculation\nOptions" => "Options de\ncalcul",
Expand Down
Loading
Loading