package quickterface

  1. Overview
  2. Docs

Source file log_item.ml

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
open! Core

type t =
  | Output_text of {
      text : string;
      options : Quickterface.Output_text_options.t;
    }
  | Output_math of {
      math : Quickterface.Math.t;
      options : Quickterface.Output_text_options.t;
    }
  | Input_text of { prompt : string; text : string }

let output_text ?(options = Quickterface.Output_text_options.default) text =
  Output_text { text; options }

let output_math ?(options = Quickterface.Output_text_options.default) math =
  Output_math { math; options }

let input_text ?(prompt = "> ") text = Input_text { prompt; text }

let attr = function
  | Output_text { options; _ } -> Theme.text_output_from_options options
  | Output_math { options; _ } -> Theme.math_output_from_options options
  | Input_text _ -> Theme.text_input_frozen

module Math_renderer = struct
  module I = Notty.I

  type image = Notty.image

  module Horizontally_aligned_image : sig
    type t

    val make_from_single_image : image -> center_line_index:int -> unit -> t

    val make_from_parts :
      ?top:image -> ?center:image -> ?bottom:image -> unit -> t

    val height : t -> int
    val to_notty : t -> image
    val ( <|> ) : t -> t -> t
    val hcat : t list -> t
  end = struct
    open I

    type t = { image : image; center_line_index : int }

    let make ~image ~center_line_index =
      if not (center_line_index < height image) then
        raise_s
          [%message
            "Center line index must be less than image height"
              (center_line_index : int)];
      if not (center_line_index >= 0) then
        raise_s
          [%message
            "Center line index must be non-negative" (center_line_index : int)];

      { image; center_line_index }

    let left_align_parts top center bottom =
      let max_width = max (width top) (max (width center) (width bottom)) in
      let padder img = pad ~r:(max_width - width img) img in
      (padder top, padder center, padder bottom)

    let make_from_single_image image ~center_line_index () =
      make ~image ~center_line_index

    let make_from_parts ?(top = empty) ?(center = void 1 1) ?(bottom = empty) ()
        =
      if not (height center = 1) then
        raise_s [%message "Center image must be single row"];

      let padded_top, padded_center, padded_bottom =
        left_align_parts top center bottom
      in

      make
        ~image:(padded_top <-> padded_center <-> padded_bottom)
        ~center_line_index:(height padded_top)

    let height { image; center_line_index = _ } = height image

    let to_parts { image; center_line_index } =
      let top_height = center_line_index in
      let bottom_height = I.height image - center_line_index - 1 in

      let top = I.crop ~b:(1 + bottom_height) image in
      let center = I.crop ~t:top_height ~b:bottom_height image in
      let bottom = I.crop ~t:(1 + top_height) image in

      (top, center, bottom)

    let to_notty { image; center_line_index = _ } = image

    let ( <|> ) img_x img_y =
      let img_x_top, img_x_center, img_x_bottom = to_parts img_x in
      let img_y_top, img_y_center, img_y_bottom = to_parts img_y in

      make_from_parts ~top:(img_x_top <|> img_y_top)
        ~center:(img_x_center <|> img_y_center)
        ~bottom:(img_x_bottom <|> img_y_bottom)
        ()

    let hcat = function
      | [] -> make_from_parts ()
      | h :: ts -> List.fold ts ~init:h ~f:( <|> )
  end

  let render_math ~render_info:_ attr math =
    let open I in
    let open Horizontally_aligned_image in
    let rec render_math =
      let plain_string s =
        make_from_single_image (string attr s) ~center_line_index:0 ()
      in
      let super_sub_script_helper ~base ~script ~side =
        let base_img = render_math base |> to_notty in
        let script_img = render_math script |> to_notty in

        let script_img_height = I.height script_img in
        let base_img_height = I.height base_img in

        let whole_image =
          match side with
          | `Superscript ->
              I.(pad ~l:(I.width base_img) script_img <-> base_img)
          | `Subscript -> I.(base_img <-> pad ~l:(I.width base_img) script_img)
        in

        let center_line_index =
          match side with
          | `Superscript -> script_img_height + (base_img_height / 2)
          | `Subscript -> base_img_height / 2
        in

        make_from_single_image whole_image ~center_line_index ()
      in
      function
      | Quickterface.Math.Char c -> plain_string (Char.to_string c)
      | Literal s -> plain_string s
      | Infinity -> plain_string "∞"
      | Pi -> plain_string "π"
      | E -> plain_string "e"
      | Equals -> plain_string "="
      | Plus -> plain_string "+"
      | Minus -> plain_string "-"
      | Star -> plain_string "*"
      | C_dot -> plain_string "·"
      | Times -> plain_string "×"
      | Divide -> plain_string "÷"
      | Plus_minus -> plain_string "±"
      | Superscript { base; superscript } ->
          super_sub_script_helper ~base ~script:superscript ~side:`Superscript
      | Subscript { base; subscript } ->
          super_sub_script_helper ~base ~script:subscript ~side:`Subscript
      | Exp -> plain_string "exp"
      | Ln -> plain_string "ln"
      | Sin -> plain_string "sin"
      | Cos -> plain_string "cos"
      | List elements ->
          let element_imgs = List.map elements ~f:render_math in
          hcat element_imgs
      | Frac (num, denom) ->
          let num_img = render_math num |> to_notty |> pad ~l:1 ~r:1 in
          let denom_img = render_math denom |> to_notty |> pad ~l:1 ~r:1 in

          let max_width = max (I.width num_img) (I.width denom_img) in
          let line_img =
            uchar attr Notty_utils.uchar_box_drawing_light_horizontal max_width
              1
          in

          let centered_num_img =
            pad ~l:((max_width - I.width num_img) / 2) num_img
          in
          let centered_denom_img =
            pad ~l:((max_width - I.width denom_img) / 2) denom_img
          in

          make_from_parts ~top:centered_num_img ~center:line_img
            ~bottom:centered_denom_img ()
      | Bracketed inner ->
          let inner_img = render_math inner |> to_notty in
          let bracket_height = I.height inner_img in

          let make_bracket_img ~single_line ~top ~mid ~bottom =
            if bracket_height = 1 then string attr single_line
            else
              uchar attr top 1 1
              <-> uchar attr mid 1 (bracket_height - 2)
              <-> uchar attr bottom 1 1
          in
          let left_bracket_img =
            make_bracket_img ~single_line:"("
              ~top:Notty_utils.uchar_paren_drawing_light_top_left
              ~mid:Notty_utils.uchar_paren_drawing_light_mid_left
              ~bottom:Notty_utils.uchar_paren_drawing_light_bottom_left
          in
          let right_bracket_img =
            make_bracket_img ~single_line:")"
              ~top:Notty_utils.uchar_paren_drawing_light_top_right
              ~mid:Notty_utils.uchar_paren_drawing_light_mid_right
              ~bottom:Notty_utils.uchar_paren_drawing_light_bottom_right
          in

          let notty_image =
            I.(left_bracket_img <|> inner_img <|> right_bracket_img)
          in

          make_from_single_image notty_image
            ~center_line_index:(bracket_height / 2) ()
      | Partial -> plain_string "∂"
      | Less_than -> plain_string "<"
      | Less_than_or_equal_to -> plain_string "≤"
      | Greater_than -> plain_string ">"
      | Greater_than_or_equal_to -> plain_string "≥"
      | Not_equal -> plain_string "≠"
      | Approximately_equals -> plain_string "≈"
      | Equivalent_to -> plain_string "≡"
      | Integral { lower; upper; body } ->
          let lower_img_notty_opt =
            Option.(lower >>| render_math >>| to_notty)
          in
          let upper_img_notty_opt =
            Option.(upper >>| render_math >>| to_notty)
          in
          let body_img = render_math body in

          let body_height = height body_img in

          let integral_symbol_img =
            if body_height <= 1 then plain_string "∫"
            else
              let notty_image =
                uchar attr Notty_utils.uchar_paren_top_half_integral 1 1
                <-> uchar attr Notty_utils.uchar_paren_integral_extender 1
                      (body_height - 2)
                <-> uchar attr Notty_utils.uchar_paren_bottom_half_integral 1 1
              in
              make_from_single_image notty_image
                ~center_line_index:(body_height / 2) ()
          in

          let limits_image =
            let upper = Option.value ~default:I.empty upper_img_notty_opt in
            let lower = Option.value ~default:I.empty lower_img_notty_opt in
            let notty_image = I.(upper <-> void 0 1 <-> lower) in

            make_from_single_image notty_image
              ~center_line_index:(I.height upper) ()
          in

          integral_symbol_img <|> limits_image <|> body_img
    in
    render_math math
end

let render_math ~render_info attr img =
  Math_renderer.(
    Horizontally_aligned_image.to_notty (render_math ~render_info attr img))

let render_string_handling_newlines attr string =
  string |> String.split ~on:'\n'
  |> List.map ~f:(fun x -> Notty.I.string attr x)
  |> Notty.I.vcat

let render ~render_info t =
  let t_attr = attr t in
  (match t with
    | Output_text { text; _ } -> render_string_handling_newlines t_attr text
    | Output_math { math; _ } -> render_math ~render_info t_attr math
    | Input_text { prompt; text } ->
        render_string_handling_newlines t_attr [%string "%{prompt}%{text}"])
  |> Notty_utils.boxed
       ~padding_control:
         (`To_min_boxed_size
            (Some (render_info.Render_info.screen_width, Right), None))

module For_testing = struct
  let render_math = render_math
end