aboutsummaryrefslogtreecommitdiff
path: root/src/screen.ml
blob: e11180babf98c9827dde5f67749717ebd9044c4b (plain)
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
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
(*
This file is part of licht.

licht is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

licht is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with licht.  If not, see <http://www.gnu.org/licenses/>.
*)

(** Curses submodules *)
module Attrs = Curses.A
module Color = Curses.Color

module T2 = Tools.Tuple2

module Option = Tools.Option

let cell_size = 10

let u = UTF8.from_utf8string

type t = {
  window: Curses.window;    (* the main window *)
  sheet: Curses.window;     (* the spreadsheet *)
  input: Curses.window;     (* the input window *)
  status: Curses.window;    (* status bar *)
  start: (int * int);       (* Left corner *)
  left_margin: int;         (* Reserved margin for rows name *)

  mutable size: int * int;  (* Terminal size *)
}

let get_cell screen (x, y) = begin

  let x' = (fst screen.start) + (x - screen.left_margin) / cell_size
  and y' = (snd screen.start) + y - 3 in

  if (x' < 1 || y' < 1) then
    None
  else
    Some (x', y')
end

let center data position screen height width = begin
  let height' = height - 3
  and width'  = (width - screen.left_margin) / cell_size in
  let end_x = (fst screen.start) + width' -1 in
  let end_y = (snd screen.start) + height' -2 in

  let center_axis f replace max_limit shift screen = begin
    let selected_axis = f position in
    match (selected_axis >= f screen.start), (selected_axis > max_limit) with
    | true, false  -> screen
    | _ -> { screen with  start = replace (max 1 (selected_axis - shift)) screen.start }
  end in
    center_axis T2.fst T2.replace1 end_x (width' / 2) screen
 |> center_axis T2.snd T2.replace2 end_y (height' / 2)
end

let status screen msg = begin
  let height, width = screen.size in
  UTF8.encode msg |> Option.iter (fun encoded ->
    let status = Bytes.make (width -1) ' ' in
    String.blit encoded 0 status 0 (String.length encoded);
    Curses.werase screen.status;
    if not (
      Curses.mvwaddstr screen.status 0 0 encoded
      && Curses.wrefresh screen.status
    ) then
    raise (Failure "Status update")
  )
end

(** Draw the spreadsheet *)
let draw data selected screen = begin

  let position = Selection.extract selected in

  let expr, _, sink = Sheet.get_cell position data in
  let referenced = Expression.collect_sources expr in

  let height, width = screen.size in
  let screen = center data position screen height width in

  for y = 1 to (height-2) do
    let pos_y = (y + (snd screen.start) - 1) in
    if (Curses.has_colors ()) then begin
      if Selection.is_selected (Selection.Vertical pos_y) selected then
        Curses.wattrset screen.sheet (Attrs.color_pair 2 )
      else
        Curses.wattrset screen.sheet (Attrs.color_pair 1 )
      end;
    ignore
    @@ Curses.mvwaddstr screen.sheet y 0
    @@ Printf.sprintf "%-*d" screen.left_margin pos_y
  done;
  Curses.wattrset screen.sheet Attrs.normal;

  for x = 0 to ((width - cell_size - screen.left_margin) / cell_size) do
    let pos_x = x + (fst screen.start) in
    if (Curses.has_colors ()) then begin
      if Selection.is_selected (Selection.Horizontal pos_x) selected then
        Curses.wattrset screen.sheet (Attrs.color_pair 2 )
      else
        Curses.wattrset screen.sheet (Attrs.color_pair 1 )
      end;

    ignore
    @@ Curses.mvwaddstr screen.sheet 0 (x * cell_size + screen.left_margin)
    @@ Printf.sprintf "%-*s" cell_size (UTF8.raw_encode @@ Cell.to_hname pos_x);

    Curses.wattrset screen.sheet Attrs.normal;

    for y = 1 to (height-2) do

      let pos_y = y + (snd screen.start) - 1 in

      if (Curses.has_colors ()) then begin
        if Selection.is_selected (Selection.Cell (pos_x, pos_y)) selected then
          Curses.wattrset screen.sheet (Attrs.color_pair 3 )
        else if Cell.Set.mem (pos_x, pos_y) referenced then
          Curses.wattrset screen.sheet (Attrs.color_pair 4 )
        else if Cell.Set.mem (pos_x, pos_y) sink then
          Curses.wattrset screen.sheet (Attrs.color_pair 5 )
        else
          Curses.wattrset screen.sheet Attrs.normal;
      end;


      (* Get the content from the cell *)

      let _, value, _ = Sheet.get_cell (pos_x, pos_y) data in
      let content = Option.map (fun x -> UTF8.split ~sep:(u"\n") (ScTypes.Result.show x)) value
        |> Option.default UTF8.empty in

      (* If the content is defined, try to encode it and print it*)
      UTF8.encode content |> Tools.Option.iter (fun value ->
        let length = UTF8.length content in
        let strlength = String.length value in
        let blank = cell_size - length in
        let padding = if blank > 0
          then String.make blank ' '
          else "" in
        ignore
        @@ Curses.mvwaddnstr screen.sheet y (x * cell_size + screen.left_margin)
           (Printf.sprintf "%s%s" value padding)
           0 (blank + strlength)
      )
    done
  done;
  ignore @@ Curses.wrefresh screen.sheet

end

let init () = begin
  (* Do not set delay after ESC keycode *)
  begin try ignore @@ Unix.getenv "ESCDELAY" with
  | Not_found -> Unix.putenv "ESCDELAY" "25" end;

  let window = Curses.initscr () in
  let height, width = Curses.getmaxyx window in

  Tools.NCurses.set_mouse_event (Tools.NCurses.BUTTON1_CLICKED::[]);

  let init = Curses.keypad window true
  && Curses.noecho ()
  && Curses.start_color ()

  (* Build the color map *)
  && Curses.init_pair 1 Color.white Color.blue  (* Titles *)
  && Curses.init_pair 2 Color.blue Color.white  (* Selected titles *)
  && Curses.init_pair 3 Color.black Color.white (* Selected cell *)
  && Curses.init_pair 4 Color.black Color.red   (* referenced cell *)
  && Curses.init_pair 5 Color.black Color.green (* sink cell *)
  in

  if not init then
    raise (Failure "Could not intialize the screen")
  else
    {
      window = window;
      input = Curses.subwin window 2 width 0 0;
      sheet = Curses.subwin window (height - 3) width 2 0;
      status = Curses.subwin window 1 width (height - 1) 0;
      start = 1, 1;
      left_margin = 4;
      size = height, width;
    }
end

let close {window} = begin
  ignore @@ (Curses.keypad window false && Curses.echo ());
  Curses.endwin()
end

let draw_input t selected screen = begin

  let height, width = screen.size in

  let expr, value, _ = Sheet.get_cell (Selection.extract selected) t in

  let result = Option.map ScTypes.Result.show value
            |> Option.default UTF8.empty in

  UTF8.encode result |> Option.iter (fun encoded_result ->
    (* Compute the difference between number of bytes in the string, and the
       number of character printed : Printf.sprintf format use the bytes number
       in the string, while Curses print the characters in the user encoding *)
    let result_length_delta = (UTF8.length result) - (String.length encoded_result) in

    ignore (
       encoded_result
    |> Printf.sprintf "%-*s" (width - 11 - result_length_delta)
    |> Curses.mvwaddstr screen.input 0 10

    && Curses.wrefresh screen.input)
  );
  status screen (Expression.show expr);
  ()
end

(** Wait for an event and return the key pressed.

    Non blocking for other running Lwt thread.

    If the key code contains more than one char, they are both returned
 *)
let read_key {window} = begin
  let buff = Buffer.create 2 in
  let int_val = Curses.wgetch window in
  if int_val > 255 then
    Buffer.add_string buff @@ Tools.String.string_of_ints int_val
  else
    Buffer.add_char buff @@ char_of_int int_val;
  (** Check for a second key code *)
  ignore @@ Curses.nodelay window true;
  begin match Curses.wgetch window with
  | -1 -> ()
  | x ->  Buffer.add_char buff @@ char_of_int x;
  end;
  ignore @@ Curses.nodelay window false;
  Buffer.contents buff
end

let resize data selection t = begin
  let size = Curses.getmaxyx t.window in
  if (size <> t.size) then (
    let height, width = size in
    t.size <- size;
    ignore (
       Curses.wresize t.input 2 width

    && Curses.wresize t.sheet (height - 3) width

    (* The status window *)
    && Curses.wresize t.status 1 width
    && Curses.mvwin t.status (height - 1) 0);
    Curses.wclear t.status;
    ignore @@ Curses.wrefresh t.status;
    draw data selection t;
    t
  ) else t
end

let editor ?position ?(prefix=UTF8.empty) ?(init=UTF8.empty) t = begin
  let encodedPrefix = UTF8.raw_encode prefix
  and encodedInit = UTF8.raw_encode init in
  let with_refs, position = match position with
  | None -> false, (1, 1)
  | Some x -> true, x in
  Curses.werase t.status;
  ignore @@ Curses.mvwaddstr t.status 0 0 (encodedPrefix^encodedInit);
  ignore @@ Curses.wrefresh t.status;

  (** Rewrite all the text after the cursor *)
  let rewrite_after = begin function
  | [] -> ()
  | elems -> ( (* Rewrite each char after the cursor *)
    let y, x = Curses.getyx t.status in
    List.iter (fun x -> ignore @@ Curses.waddstr t.status (UTF8.raw_encode x)) elems;
    ignore @@ Curses.wmove t.status y x )
  end

  (** Delete the previous text (or block of text) *)
  and delete_previous hd = begin
    let y, x = Curses.getyx t.status in
    let length = UTF8.length hd in
    ignore @@ Curses.wmove t.status y (x - length);
    for position = 1 to length do
      ignore @@ Curses.wdelch t.status
    done;
  end in

  (* Text edition, handle the keycode.

    [before] contains all the caracters inserted before the cursor (reverse
     ordered), and [after] contains all the caracters after the cursor.
   *)
  let rec _edit before after = begin function
  | "\027" -> (* Escape, cancel the modifications *)
    None
  | "\010" -> (* Enter, validate the input *)

    (* We concatenate all the characters. This can create an invalid string in
     * the current locale (if there are copy/paste, or other events).
     *)
    Some (UTF8.implode @@ (UTF8.rev_implode before)::after)

  | "\001\004" -> (* Left *)
    begin match before with
    | [] -> _edit before after @@ read_key t
    | hd::tl ->
      let y, x = Curses.getyx t.status
      and length = UTF8.length hd in
      ignore @@ Curses.wmove t.status y (x - length);
      ignore @@ Curses.wrefresh t.status;
      _edit tl (hd::after) @@ read_key t
    end

  | "\001\005" -> (* Right *)
    begin match after with
    | [] -> _edit before after @@ read_key t
    | hd::tl ->
      let y, x = Curses.getyx t.status
      and length = UTF8.length hd in
      ignore @@ Curses.wmove t.status y (x + length);
      ignore @@ Curses.wrefresh t.status;
      _edit (hd::before) tl @@ read_key t
    end

  | "\001\007" ->   (* Backspace *)
    begin match before with
    | [] -> _edit before after @@ read_key t
    | hd::tl ->
      delete_previous hd;
      ignore @@ Curses.wrefresh t.status;
      _edit tl after @@ read_key t
    end

  | "\001\006" ->   (* Home *)
    begin match before with
    | [] -> _edit before after @@ read_key t
    | elems ->
        let to_left (size, after) elem = begin
          let size' = size + UTF8.length elem in
          size', elem::after
        end in
        let chars, after' = List.fold_left to_left (0, after) elems in
        let y, x = Curses.getyx t.status in
        ignore @@ Curses.wmove t.status y (x - chars);
        ignore @@ Curses.wrefresh t.status;
        _edit [] after' @@ read_key t
    end

  | "\001\104" ->   (* End *)
    begin match after with
    | [] -> _edit before after @@ read_key t
    | elems ->
        let to_rigth (size, before) elem = begin
          let size' = size + UTF8.length elem in
          size', elem::before
        end in
        let chars, before' = List.fold_left to_rigth (0, before) elems in
        let y, x = Curses.getyx t.status in
        ignore @@ Curses.wmove t.status y (x + chars);
        ignore @@ Curses.wrefresh t.status;
        _edit before' [] @@ read_key t
    end
  | "\001\074"
  | "\127" ->       (* Del *)
    begin match after with
    | [] ->  _edit before after @@ read_key t
    | hd::tl ->
      let y, x = Curses.getyx t.status in
      ignore @@ Curses.wmove t.status y (x + 1);
      delete_previous hd;
      ignore @@ Curses.wrefresh t.status;
      _edit before tl @@ read_key t
    end

  | ("\001\002" as key)     (* Down *)
  | ("\001\003" as key)     (* Up *)
  | ("\001\153" as key) ->  (* click *)
    if with_refs then
      select_content position (UTF8.empty) before after key
    else
      _edit before after @@ read_key t

  | any ->
    ignore @@ Curses.waddstr t.status any;
    rewrite_after after;
    ignore @@ Curses.wrefresh t.status;
    _edit (UTF8.decode any::before) after @@ read_key t
  end

  (* Selection mode, Left and Right keys allow to select a cell, and not to
     move inside the edition *)
  and select_content (x, y) name before after = begin
  function
  | "\001\002" -> (* Down *) insert_cell_name (x, y + 1) name before after
  | "\001\005" -> (* Right *) insert_cell_name (x + 1, y) name before after

  | "\001\003" -> (* Up *)
    if y > 1 then
      insert_cell_name (x, y - 1) name before after
    else select_content (x, y) name before after @@ read_key t

  | "\001\004" -> (* Left *)
    if x > 1 then
      insert_cell_name (x - 1, y) name before after
    else select_content (x, y) name before after @@ read_key t

  | "\001\153" -> (* click *)
    let position =
    begin match Tools.NCurses.get_mouse_event () with
    | None -> None
    | Some (id, ev, (x, y, z)) ->
        if Tools.NCurses.is_event_of_type Tools.NCurses.BUTTON1_CLICKED ev
        || Tools.NCurses.is_event_of_type Tools.NCurses.BUTTON1_PRESSED ev then
          get_cell t (x,y)
        else
          None
    end in
    begin match position with
    | None -> select_content (x, y) name before after @@ read_key t
    | Some (x, y) -> insert_cell_name (x, y) name before after
    end

  | key ->
    _edit (UTF8.fold List.cons name before) after key
  end

  and insert_cell_name position name before after = begin
    let cell_name = Cell.to_string @@ (position, (false, false)) in
    ignore @@ delete_previous name;
    ignore @@ Curses.waddstr t.status (UTF8.raw_encode cell_name);
    rewrite_after after;
    ignore @@ Curses.wrefresh t.status;
    select_content position cell_name before after @@ read_key t
  end
  in

  Tools.try_finally
    (fun () ->
      ignore @@ Curses.curs_set 1;
      try _edit (UTF8.rev_explode init) [] @@ read_key t
      with _ -> None)
    (fun () -> ignore @@ Curses.curs_set 0)
end

let run f =
  let window = init () in
  Tools.try_finally
    (fun () -> f window)
    (fun () -> ignore @@ close window )