spectral-m2 1.2.1

A syntax-aware terminal shell for Macaulay2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
//! Shared rendering context and helpers for terminal UI elements. Two shapes cover
//! the feed: a full-width *code band* and an inset bordered *box*. Each block's
//! `render` is a thin call into here, so no block re-implements drawing. Measurement
//! and rendering consume the same visual-line ranges produced by the shared layout
//! policies.

use crate::ui::block::{Element, HLSpan, Text};
use crate::ui::config::UI;
use crate::ui::controller::settings::theme::groups::{scheme, Groups};
use crate::ui::render::maths::{FractionStyle, MathDisplay, MathStyle};
use crate::ui::render::selection::Exclusions;
use ratatui::buffer::Buffer;
use ratatui::layout::{Position, Rect};
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block as TuiBlock, BorderType, Padding, Paragraph, Widget};
use unicode_width::UnicodeWidthStr;

pub mod markup;
pub mod maths;
pub mod selection;
#[allow(dead_code)] // groundwork: not yet wired into the running UI
pub mod spin;
pub mod typeset;

/// Horizontal placement of a bordered feed element.
#[derive(Clone, Copy)]
enum BorderPlacement {
    Gutter,
    /// Centred over the middle third, widened into the margins as far as `content`
    /// columns of text need.
    MiddleThird {
        content: u16,
    },
}

/// How many of the latest outputs M2 also names `oo`, `ooo` and `oooo`.
pub const RECENT_OUTPUTS: usize = 3;

/// Runtime rendering inputs shared by every block without owning application state.
#[derive(Clone, Copy)]
pub struct RenderContext {
    width: u16,
    prefix: u16,
    output_limit: Option<usize>,
    math: MathStyle,
    recent_outputs: [Option<u32>; RECENT_OUTPUTS],
    numbers_printed: bool,
}

impl RenderContext {
    pub fn new(width: u16, prefix: u16) -> Self {
        Self {
            width,
            prefix,
            output_limit: Some(UI.max_output_rows),
            math: MathStyle::default(),
            recent_outputs: [None; RECENT_OUTPUTS],
            numbers_printed: false,
        }
    }

    /// Fold returned values taller than `rows`.
    pub fn with_output_limit(self, rows: usize) -> Self {
        Self {
            output_limit: Some(rows.max(UI.min_output_rows)),
            ..self
        }
    }

    /// Space the rows of typeset matrices at `percent` of TeX's own distance.
    pub fn with_matrix_row_spacing(self, percent: u16) -> Self {
        Self {
            math: MathStyle {
                matrix_row_spacing_percent: percent.clamp(
                    UI.min_matrix_row_spacing_percent,
                    UI.max_matrix_row_spacing_percent,
                ),
                ..self.math
            },
            ..self
        }
    }

    /// Set returned maths with fractions in `fractions`.
    pub fn with_fractions(self, fractions: FractionStyle) -> Self {
        Self {
            math: MathStyle {
                fractions,
                ..self.math
            },
            ..self
        }
    }

    /// Track the latest outputs, newest first, for highlighting.
    pub fn with_recent_outputs(self, recent_outputs: [Option<u32>; RECENT_OUTPUTS]) -> Self {
        Self {
            recent_outputs,
            ..self
        }
    }

    pub fn recent_outputs(&self) -> [Option<u32>; RECENT_OUTPUTS] {
        self.recent_outputs
    }

    /// Whether output `number` is one of the latest, which M2 recalls as `oo`, `ooo`
    /// and `oooo`.
    pub fn is_recent_output(&self, number: u32) -> bool {
        self.recent_outputs.contains(&Some(number))
    }

    /// Draw rings as blackboard glyphs (`ℤ`) rather than by name (`ZZ`).
    pub fn with_blackboard(self, blackboard: bool) -> Self {
        Self {
            math: MathStyle {
                blackboard,
                ..self.math
            },
            ..self
        }
    }

    /// Draw returned maths with `display`.
    pub fn with_display(self, display: MathDisplay) -> Self {
        Self {
            math: MathStyle {
                display,
                ..self.math
            },
            ..self
        }
    }

    /// Label printed text with the number of the cell that printed it, as the
    /// views apart from the shell do.
    pub fn with_printed_numbers(self) -> Self {
        Self {
            numbers_printed: true,
            ..self
        }
    }

    pub fn numbers_printed(&self) -> bool {
        self.numbers_printed
    }

    pub fn width(&self) -> u16 {
        self.width
    }

    /// Retain rendering services at a nested element's local width.
    pub fn with_width(&self, width: u16) -> Self {
        Self { width, ..*self }
    }

    pub fn math(&self) -> MathStyle {
        self.math
    }

    /// Render returned values without the shell's row limit.
    pub fn full_output(&self) -> Self {
        Self {
            output_limit: None,
            ..*self
        }
    }

    pub fn output_limit(&self) -> Option<usize> {
        self.output_limit
    }

    pub fn prefix(&self) -> u16 {
        self.prefix
    }

    /// The scheme in effect, read rather than retained.
    pub fn groups(&self) -> &'static Groups {
        scheme()
    }
}

/// Styled prefix containing the live input prompt.
pub struct Prompt {
    label: String,
    style: Style,
}

impl Prompt {
    pub fn new(label: impl Into<String>, style: Style) -> Self {
        Self {
            label: label.into(),
            style,
        }
    }
}

impl Text for Prompt {
    type Context = ();

    /// A label wider than one column takes its extra width out of the gap
    /// after it, so the text never moves when the label changes.
    fn spans(&self, (): &()) -> Vec<Span<'static>> {
        let overhang = UnicodeWidthStr::width(self.label.as_str()).saturating_sub(1);
        vec![Span::styled(
            format!(
                "{}{:1}{}",
                " ".repeat(UI.prompt_margin.into()),
                self.label,
                " ".repeat(usize::from(UI.prompt_text_gap).saturating_sub(overhang))
            ),
            self.style,
        )]
    }
}

/// Semantic highlight ranges that paint source text into styled spans.
pub trait Highlights {
    fn paint(
        &self,
        abs_start: usize,
        seg: &str,
        base: Style,
        groups: &Groups,
    ) -> Vec<Span<'static>>;
}

impl Highlights for [HLSpan] {
    fn paint(
        &self,
        abs_start: usize,
        seg: &str,
        base: Style,
        groups: &Groups,
    ) -> Vec<Span<'static>> {
        let seg_end = abs_start + seg.len();
        let mut out = Vec::new();
        let mut cur = abs_start;
        for s in self
            .iter()
            .filter(|s| s.content.start < seg_end && s.content.end > abs_start)
        {
            let st = s.content.start.max(abs_start);
            let en = s.content.end.min(seg_end);
            if st > cur {
                out.push(Span::styled(
                    seg[cur - abs_start..st - abs_start].to_string(),
                    base,
                ));
            }
            out.push(Span::styled(
                seg[st - abs_start..en - abs_start].to_string(),
                base.patch(s.style(groups)),
            ));
            cur = en;
        }
        if cur < seg_end {
            out.push(Span::styled(seg[cur - abs_start..].to_string(), base));
        }
        if out.is_empty() {
            out.push(Span::styled(seg.to_string(), base));
        }
        out
    }
}

/// Content geometry for a generic bordered element.
#[derive(Clone, Copy)]
pub struct FrameLayout {
    prefix: u16,
    inset: u16,
    placement: BorderPlacement,
}

impl FrameLayout {
    /// Place a frame at an explicit inset with independent content padding.
    pub const fn gutter(prefix: u16, inset: u16) -> Self {
        Self {
            prefix,
            inset,
            placement: BorderPlacement::Gutter,
        }
    }

    /// Centre a box over the middle third of its available width, giving up margin
    /// rather than wrapping text up to `content` columns wide.
    pub const fn middle_third(content: u16) -> Self {
        Self {
            prefix: 0,
            inset: 0,
            placement: BorderPlacement::MiddleThird { content },
        }
    }

    /// Width reserved before bordered content.
    pub const fn prefix(self) -> u16 {
        self.prefix
    }

    /// Horizontal coordinate of the frame's left edge.
    pub const fn inset(self) -> u16 {
        self.inset
    }
}

impl Bordered {
    pub fn area(area: Rect, layout: FrameLayout) -> Rect {
        if let BorderPlacement::MiddleThird { content } = layout.placement {
            let left = area.width / 3;
            let third = (area.width.saturating_mul(2) / 3 - left + 1).min(area.width - left);
            let chrome = third.saturating_sub(Self::content_width(third, layout));
            let width = third.max(content.saturating_add(chrome)).min(area.width);
            let start = match width > third {
                true => (area.width - width) / 2,
                false => left,
            };
            return Rect::new(area.x + start, area.y, width, area.height);
        }
        let width = area
            .width
            .saturating_sub(layout.inset + UI.right_margin)
            .max(UI.min_box_width);
        Rect::new(area.x + layout.inset, area.y, width, area.height)
    }

    pub fn inner(area: Rect, layout: FrameLayout) -> Rect {
        Self::frame(layout).inner(Self::area(area, layout))
    }

    /// The border and padding drawn around content, without a title or colours.
    fn frame(layout: FrameLayout) -> TuiBlock<'static> {
        TuiBlock::bordered().padding(Padding::new(
            layout.prefix.saturating_sub(layout.inset + 1),
            1,
            0,
            0,
        ))
    }

    /// Columns left for content inside a frame `width` wide.
    fn content_width(width: u16, layout: FrameLayout) -> u16 {
        Self::frame(layout).inner(Rect::new(0, 0, width, 1)).width
    }

    pub fn text_width(context: &RenderContext, layout: FrameLayout) -> u16 {
        Self::inner(Rect::new(0, 0, context.width(), 0), layout).width
    }

    /// Whether `position` lies on the frame's border line itself.
    pub fn on_border(area: Rect, layout: FrameLayout, position: Position) -> bool {
        let frame = Self::area(area, layout);
        frame.contains(position)
            && (position.x == frame.x
                || position.x == frame.right() - 1
                || position.y == frame.y
                || position.y == frame.bottom() - 1)
    }

    /// Exclude the frame and its padding while retaining rendered body glyphs.
    pub fn exclude_frame(area: Rect, layout: FrameLayout, exclusions: &mut Exclusions) {
        let frame = Self::area(area, layout);
        let inner = Self::inner(area, layout);
        match (frame.width, frame.height) {
            (0, _) | (_, 0) => {}
            _ => exclusions.extend([
                Rect::new(frame.x, frame.y, frame.width, 1),
                Rect::new(frame.x, frame.bottom() - 1, frame.width, 1),
                Rect::new(frame.x, frame.y, inner.x - frame.x, frame.height),
                Rect::new(
                    inner.right(),
                    frame.y,
                    frame.right() - inner.right(),
                    frame.height,
                ),
            ]),
        }
    }
}

/// Styled text decorated by the shared rounded feed border.
pub struct Bordered {
    layout: FrameLayout,
    surface: Style,
    border_style: Style,
    title: Option<Line<'static>>,
    body: Vec<Line<'static>>,
}

impl Bordered {
    pub fn new(
        layout: FrameLayout,
        surface: Style,
        border_style: Style,
        title: Option<Line<'static>>,
        body: Vec<Line<'static>>,
    ) -> Self {
        Self {
            layout,
            surface,
            border_style,
            title,
            body,
        }
    }
}

impl Element for Bordered {
    type Context = ();

    fn render(&self, (): &(), area: Rect, buf: &mut Buffer) {
        let mut block = Self::frame(self.layout)
            .style(self.surface)
            .border_type(BorderType::Rounded)
            .border_style(self.border_style);
        if let Some(title) = &self.title {
            block = block.title(title.clone());
        }
        block.render(Self::area(area, self.layout), buf);
        Paragraph::new(self.body.clone())
            .style(self.surface)
            .render(Self::inner(area, self.layout), buf);
    }
}