1use 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 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 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 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 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 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 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 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
338fn 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
348fn 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
361pub const CHECKBOX_FORMAT: &str = "\"☐\";\"☐\";\"☐\";[=1]\"☑\"";
363
364fn object_params(p: &Json) -> Json {
366 Json::Object(p.as_object().cloned().unwrap_or_default())
367}