Skip to main content

ravenrs_sheet/engine/cmd/
extra.rs

1//! More ribbon commands: Chart Design and Format, object arrangement, workbook views, table
2//! tools, Get Data, scripts (Automate), cell checkboxes, theme colours and fonts.
3
4use crate::base::{CellRef, RangeRef, Value};
5use crate::model::{
6    Anchor, Chart, Color, DefinedName, HAlign, LegendPos, NumFmt, Shape, ShapeKind, Sheet, SheetViewMode, TableColumn, TotalsFn, Trendline,
7    TrendlineKind, Workbook,
8};
9use serde_json::{Value as Json, json};
10
11use super::{
12    CommandSpec, EngineError, Result, Session, always, anchor_mode_name, bad, bool_param, cell_param, cell_param_on, cmd, edit, f64_param, has_doc,
13    ok, split_sheet, str_param, target_range, target_ranges, target_sheet, u32_param,
14};
15
16mod cells;
17mod chart;
18mod data;
19mod objects;
20mod scripts;
21mod window;
22
23pub(crate) use cells::is_checkbox_cell;
24use cells::{background, checkbox, draw_border, new_cell_style, recently_used, theme_colors, theme_fonts, toggle_checkbox, use_in_formula};
25use chart::{add_element, change_colors, format_series, move_chart, quick_layout, select_data};
26use data::{from_csv, from_table_range, queries, table_resize};
27use objects::{align, arrange, rotate, selection_pane, shape_fill, shape_outline};
28pub(crate) use scripts::has_scripts;
29use scripts::{all_scripts, delete_script, new_script, record, run_script, stop_action, trust_scripts};
30use window::{arrange_all, custom_views, hide_window, new_window, ruler, set_view, split, switch_windows, unhide_window, watch_window};
31pub(crate) use window::{keep_view_modes, keep_watch_list, window_param};
32
33pub fn specs() -> Vec<CommandSpec> {
34    vec![
35        // Chart Design / Format
36        cmd!(
37            "chart.addElement",
38            "Add Chart Element",
39            ["Chart Design", "Chart Layouts"],
40            None,
41            "{chart?, element: title|legend|dataLabels|dataTable|gridlines|axisTitles|smooth|trendline, on?: bool, position?: bottom|top|left|right; dataTable: dataTableKeys?: bool, dataTableHorizontalBorder?: bool, dataTableVerticalBorder?: bool, dataTableOutline?: bool; trendline: series?: index (default all), type?: linear|exponential|linearForecast|movingAverage|logarithmic|power|polynomial, order?: 2..6, period?: 2..255, forward?, backward?, displayEquation?, displayRSquared?}",
42            has_doc,
43            add_element
44        ),
45        cmd!("chart.quickLayout", "Quick Layout", ["Chart Design", "Chart Layouts"], None, "{chart?, layout: 1..6}", has_doc, quick_layout),
46        cmd!(
47            "chart.changeColors",
48            "Change Colors",
49            ["Chart Design", "Chart Styles"],
50            None,
51            "{chart?, palette: colorful|monochrome|accent1..accent6 | colors: [hex…]}",
52            has_doc,
53            change_colors
54        ),
55        cmd!(
56            "chart.selectData",
57            "Select Data…",
58            ["Chart Design", "Data"],
59            None,
60            "{chart?, range?: \"Sheet1!A1:D7\", byRows?: bool, categories?: \"Sheet1!A2:A7\" (Horizontal (Category) Axis Labels for every series; \"\" clears), removeSeries?: [index…]}",
61            has_doc,
62            select_data
63        ),
64        cmd!("chart.changeType", "Change Chart Type…", ["Chart Design", "Type"], None, "{chart?, type, subtype?}", has_doc, |s, p| s
65            .execute("chart.set", p.clone())),
66        cmd!(
67            "chart.move",
68            "Move Chart…",
69            ["Chart Design", "Location"],
70            None,
71            "{chart?, sheet?: name (moves to that sheet), at?: \"B2\"}",
72            has_doc,
73            move_chart
74        ),
75        cmd!(
76            "chart.formatSelection",
77            "Format Selection",
78            ["Format", "Current Selection"],
79            None,
80            "{chart?, series?: index, color?: hex}",
81            has_doc,
82            format_series
83        ),
84        cmd!(
85            "chart.shapeFill",
86            "Shape Fill",
87            ["Format", "Shape Styles"],
88            None,
89            "{chart? | kind: shape, id?, series?, color: hex}",
90            has_doc,
91            shape_fill
92        ),
93        cmd!("chart.shapeOutline", "Shape Outline", ["Format", "Shape Styles"], None, "{kind: shape, id?, color: hex}", has_doc, shape_outline),
94        // Arrange
95        cmd!(
96            "arrange.bringForward",
97            "Bring Forward",
98            ["Page Layout", "Arrange"],
99            None,
100            "{kind: chart|image|shape|slicer|equation|control, id, toFront?: bool}",
101            has_doc,
102            |s, p| arrange(s, p, true)
103        ),
104        cmd!(
105            "arrange.sendBackward",
106            "Send Backward",
107            ["Page Layout", "Arrange"],
108            None,
109            "{kind: chart|image|shape|slicer|equation|control, id, toBack?: bool}",
110            has_doc,
111            |s, p| arrange(s, p, false)
112        ),
113        cmd!(query "arrange.selectionPane", "Selection Pane", ["Page Layout", "Arrange"], None, "{} → objects on the sheet in stacking order", has_doc, selection_pane),
114        cmd!(
115            "arrange.align",
116            "Align",
117            ["Page Layout", "Arrange"],
118            None,
119            "{kind: chart|image|shape|slicer|equation|control, ids: [..], align: left|center|right|top|middle|bottom}",
120            has_doc,
121            align
122        ),
123        cmd!(
124            "arrange.group",
125            "Group",
126            ["Page Layout", "Arrange"],
127            None,
128            "{kind, ids} (moves objects together: aligns their anchors to the first)",
129            has_doc,
130            |s, p| align(s, &super::format::merge_json(p.clone(), &json!({"align": "left"})))
131        ),
132        cmd!(
133            "arrange.rotate",
134            "Rotate",
135            ["Page Layout", "Arrange"],
136            None,
137            "{kind: shape, id} (swaps width/height for a 90° turn; `flip` is rejected: shapes have no flip state)",
138            has_doc,
139            rotate
140        ),
141        // Views
142        cmd!(noundo "view.normal", "Normal", ["View", "Workbook Views"], None, "{sheet?} → {view} (per sheet, saved with the file)", has_doc, |s, p| set_view(
143            s,
144            p,
145            SheetViewMode::Normal
146        )),
147        cmd!(
148            noundo "view.pageLayout",
149            "Page Layout",
150            ["View", "Workbook Views"],
151            None,
152            "{sheet?} → {view}: pages on paper with rulers; headers and footers are edited in place (per sheet, saved with the file)",
153            has_doc,
154            |s, p| set_view(s, p, SheetViewMode::PageLayout)
155        ),
156        cmd!(
157            noundo "view.pageBreakPreview",
158            "Page Break Preview",
159            ["View", "Workbook Views"],
160            None,
161            "{sheet?} → {view} (per sheet, saved with the file)",
162            has_doc,
163            |s, p| set_view(s, p, SheetViewMode::PageBreakPreview)
164        ),
165        cmd!(noundo "view.customViews", "Custom Views…", ["View", "Workbook Views"], None, "{save?: name, show?: name, delete?: name} → list", has_doc, custom_views),
166        cmd!(noundo "view.split", "Split", ["View", "Window"], None, "{} (toggles a split at the active cell; shown as frozen panes in this version)", has_doc, split),
167        cmd!(noundo "view.newWindow", "New Window", ["View", "Window"], None, "{} → {window, doc, title}: opens another window on the active workbook (the same workbook: saving from either window saves it)", has_doc, new_window),
168        cmd!(
169            noundo "view.arrangeAll",
170            "Arrange All",
171            ["View", "Window"],
172            None,
173            "{arrange?: tiled|horizontal|vertical|cascade, activeWorkbook?: bool} → {arrange, windows}: lays out the visible windows (tiled by default; `activeWorkbook` only the active workbook's), the active window first",
174            has_doc,
175            arrange_all
176        ),
177        cmd!(noundo "view.hideWindow", "Hide", ["View", "Window"], None, "{window?, doc?} (the active window by default)", has_doc, hide_window),
178        cmd!(noundo "view.unhideWindow", "Unhide…", ["View", "Window"], None, "{window?, doc?, index?} (the most recently hidden window by default)", has_doc, unhide_window),
179        cmd!(
180            noundo "view.switchWindows",
181            "Switch Windows",
182            ["View", "Window"],
183            None,
184            "{window?, doc?} → open windows [{window, doc, index, title, dirty, active, hidden}]; with `window` (an id from the list), or `doc` (a workbook id: its most recent window), shows and activates that window first",
185            always,
186            switch_windows
187        ),
188        cmd!(noundo "view.ruler", "Ruler", ["View", "Show"], None, "{on?} → {on}: rulers in Page Layout view (an application option)", has_doc, ruler),
189        cmd!(query "view.macros", "Macros", ["View", "Macros"], None, "{} → scripts", has_doc, all_scripts),
190        // Table tools
191        cmd!("table.resize", "Resize Table", ["Table Design", "Properties"], None, "{table?, range: \"A1:F20\"}", has_doc, table_resize),
192        cmd!("table.removeDuplicates", "Remove Duplicates", ["Table Design", "Tools"], None, "{table?, columns?}", has_doc, |s, p| s
193            .execute("data.removeDuplicates", p.clone())),
194        // Get & Transform
195        cmd!(
196            "data.fromTextCsv",
197            "From Text/CSV",
198            ["Data", "Get & Transform Data"],
199            None,
200            "{path | text, delimiter?: \",\", sheet?: new sheet name} → imports into a new sheet as a table",
201            has_doc,
202            from_csv
203        ),
204        cmd!(
205            "data.getData",
206            "Get Data",
207            ["Data", "Get & Transform Data"],
208            None,
209            "{path | text} (CSV/TSV/JSON array of objects) → new sheet table",
210            has_doc,
211            from_csv
212        ),
213        cmd!(
214            "data.fromTableRange",
215            "From Table/Range",
216            ["Data", "Get & Transform Data"],
217            None,
218            "{range?} → copies the table/range to a new sheet as a table",
219            has_doc,
220            from_table_range
221        ),
222        cmd!(query "data.queriesConnections", "Queries & Connections", ["Data", "Queries & Connections"], None, "{} → imported tables", has_doc, queries),
223        // Automate
224        cmd!(noundo "automate.recordActions", "Record Actions", ["Automate", "Scripting Tools"], None, "{stop?: bool, name?: script name when stopping}", has_doc, record),
225        cmd!(
226            "automate.newScript",
227            "New Script",
228            ["Automate", "Scripting Tools"],
229            None,
230            "{name, commands: [{command, params}] }",
231            has_doc,
232            new_script
233        ),
234        cmd!(query "automate.allScripts", "All Scripts", ["Automate", "Scripting Tools"], None, "{} → saved scripts", has_doc, all_scripts),
235        cmd!(
236            "automate.runScript",
237            "Run Script",
238            [],
239            None,
240            "{name, trusted?: bool (runs a script from an untrusted workbook this once)} — refuses scripts in a workbook opened from a file until it is trusted, and steps that reach outside the workbook (file.*, link.*, any `path` param)",
241            has_doc,
242            run_script
243        ),
244        cmd!(
245            noundo "automate.trustScripts",
246            "Trust Scripts",
247            ["Automate", "Scripting Tools"],
248            None,
249            "{trusted?: bool (default true)} → {trusted}: lets this workbook's scripts run for the rest of the session",
250            has_doc,
251            trust_scripts
252        ),
253        cmd!("automate.deleteScript", "Delete Script", [], None, "{name}", has_doc, delete_script),
254        cmd!(
255            noundo "automate.stopAction",
256            "Stop Action",
257            [],
258            None,
259            "{} → {stopped: bool}: stops an action left running on the active workbook (an open undo group, such as a debugger's) and puts back the workbook and its undo history as they were when it began",
260            has_doc,
261            stop_action
262        ),
263        // Cell controls, themes, borders, styles
264        cmd!(
265            "insert.checkbox",
266            "Checkbox",
267            ["Insert", "Cell Controls"],
268            None,
269            "{range?} (TRUE/FALSE and empty cells draw as checkboxes, empty ones become FALSE, other values show as they are; click or Space toggles; Delete unchecks, then removes; Clear Formats removes and keeps the values)",
270            has_doc,
271            checkbox
272        ),
273        cmd!("cell.toggleCheckbox", "Toggle Checkbox", [], Some("Space"), "{cell?}", has_doc, toggle_checkbox),
274        cmd!("pageLayout.themeColors", "Colors", ["Page Layout", "Themes"], None, "{name: theme name | colors: [12 hex]}", has_doc, theme_colors),
275        cmd!("pageLayout.themeFonts", "Fonts", ["Page Layout", "Themes"], None, "{major, minor}", has_doc, theme_fonts),
276        cmd!(
277            "pageLayout.themeEffects",
278            "Effects",
279            ["Page Layout", "Themes"],
280            None,
281            "{name} (chart effects preset; stored with the theme name)",
282            has_doc,
283            |_, _| ok()
284        ),
285        cmd!(
286            "pageLayout.background",
287            "Background",
288            ["Page Layout", "Page Setup"],
289            None,
290            "{path? | clear?: bool} (sheet background picture)",
291            has_doc,
292            background
293        ),
294        cmd!(
295            "home.drawBorder",
296            "Draw Border",
297            ["Home", "Font"],
298            None,
299            "{range, edge: top|bottom|left|right|grid, style?, color?}",
300            has_doc,
301            draw_border
302        ),
303        cmd!(
304            "home.newCellStyle",
305            "New Cell Style…",
306            ["Home", "Styles"],
307            None,
308            "{name, fromCell?: \"A1\" (default: active cell)}",
309            has_doc,
310            new_cell_style
311        ),
312        cmd!(query "home.addIns", "Add-ins", ["Home", "Add-ins"], None, "{} → GridCraft extends through MCP and scripts instead of add-ins", always, |_, _| Ok(json!({"addIns": [], "hint": "Use gridcraft-cli mcp or Automate › scripts"}))),
313        cmd!(query "formulas.recentlyUsed", "Recently Used", ["Formulas", "Function Library"], None, "{} → the functions most recently entered in formulas, newest first", has_doc, recently_used),
314        cmd!(query "formulas.financial", "Financial", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "Financial"}))),
315        cmd!(query "formulas.logical", "Logical", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "Logical"}))),
316        cmd!(query "formulas.text", "Text", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "Text"}))),
317        cmd!(query "formulas.dateTime", "Date & Time", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "DateTime"}))),
318        cmd!(query "formulas.lookupReference", "Lookup & Reference", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "Lookup"}))),
319        cmd!(query "formulas.mathTrig", "Math & Trig", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "MathTrig"}))),
320        cmd!(query "formulas.moreFunctions", "More Functions", ["Formulas", "Function Library"], None, "{category?: Statistical|Engineering|Information|Database|Compatibility|Web|Cube}", always, |s, p| s.execute("formulas.functions", json!({"category": str_param(p, "category").unwrap_or("Statistical")}))),
321        cmd!(
322            "formulas.useInFormula",
323            "Use in Formula",
324            ["Formulas", "Defined Names"],
325            None,
326            "{name} (inserts the name into the cell being edited)",
327            has_doc,
328            use_in_formula
329        ),
330        cmd!(query "formulas.watchWindow", "Watch Window", ["Formulas", "Formula Auditing"], None, "{add?: \"Sheet1!B5\", remove?: \"…\"} → watched cells with current values", has_doc, watch_window),
331    ]
332}
333
334const VIEWS_NAME: &str = "_sc_custom_views";
335const SCRIPTS_NAME: &str = "_sc_scripts";
336const WATCH_NAME: &str = "_sc_watch";
337
338/// Workbook-level JSON stored in a hidden defined name (round-trips through XLSX).
339fn stash_get(wb: &Workbook, key: &str) -> Json {
340    wb.names
341        .iter()
342        .find(|n| n.name == key)
343        .and_then(|n| n.formula.strip_prefix('"').and_then(|t| t.strip_suffix('"')).map(|t| t.replace("\"\"", "\"")))
344        .and_then(|t| serde_json::from_str(&t).ok())
345        .unwrap_or(json!({}))
346}
347
348/// Like `stash_get`, for the name-keyed lists (views, scripts): a stored value that is not a JSON object is treated as empty.
349fn stash_map(wb: &Workbook, key: &str) -> Json {
350    let v = stash_get(wb, key);
351    if v.is_object() { v } else { json!({}) }
352}
353
354fn stash_set(wb: &mut Workbook, key: &str, v: &Json) {
355    let text = v.to_string();
356    let formula = format!("\"{}\"", text.replace('"', "\"\""));
357    wb.names.retain(|n| n.name != key);
358    wb.names.push(DefinedName { name: key.into(), scope: None, formula, comment: String::new(), hidden: true });
359}
360
361/// Number format that marks checkbox cells (TRUE/FALSE values drawn as boxes).
362pub const CHECKBOX_FORMAT: &str = "\"☐\";\"☐\";\"☐\";[=1]\"☑\"";
363
364/// Copy of the params that is always a JSON object, so keys can be assigned; any other JSON value counts as no params (like null).
365fn object_params(p: &Json) -> Json {
366    Json::Object(p.as_object().cloned().unwrap_or_default())
367}