aboutsummaryrefslogtreecommitdiff
path: root/lib/grep.ml
blob: 6ed2a98bb28ad1ec695de268c4f1ec55fe4acf6b (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
let execution_directory =
  Sys.getenv_opt "STICH_DIRECTORY" |> Option.value ~default:"/home/mccd/notes-example"


let grep_cmd = Sys.getenv_opt "STICH_GREP_CMD" |> Option.value ~default:"ugrep"

let tag_pattern =
  Sys.getenv_opt "STITCH_TAG_PATTERN" |> Option.value ~default:"\\:[a-z-]+\\:"


let headline_pattern_regexp =
  Sys.getenv_opt "STITCH_HEADLINE_PATTERN_REGEXP" |> Option.value ~default:"^\\* "


let headline_pattern =
  Sys.getenv_opt "STITCH_HEADLINE_PATTERN" |> Option.value ~default:"* "


let todo_pattern = Sys.getenv_opt "STITCH_TODO" |> Option.value ~default:"* TODO"

let todo_pattern_regexp =
  Sys.getenv_opt "STITCH_TODO_REGEXP" |> Option.value ~default:"\\* TODO"


let done_pattern = Sys.getenv_opt "STITCH_DONE" |> Option.value ~default:"* DONE"

let done_pattern_regexp =
  Sys.getenv_opt "STITCH_DONE_REGEXP" |> Option.value ~default:"\\* DONE"


(* Utils *)

let run_calls calls =
  let open Shexp_process in
  let open Shexp_process.Infix in
  eval (chdir execution_directory (calls |- read_all))


let get_padding_arr arr =
  Array.fold_left (fun n (file_name, _) -> Int.max n (String.length file_name)) 0 arr


let get_padding_list list =
  List.fold_left (fun n (file_name, _) -> Int.max n (String.length file_name)) 0 list


let pad str n =
  let padding = n - String.length str in
  String.concat "" [ str; String.make padding ' ' ]


exception Not_A_Tuple of string * string

(* todo parsing *)
let todo_get_args = [ grep_cmd; todo_pattern_regexp; "-H"; "-r"; "-n"; "--no-messages" ]

let get_todos () =
  let open Shexp_process in
  let open Shexp_process.Infix in
  run_calls (call todo_get_args |- call [ "sort"; "-n"; "-r" ])


let parse_todo_files files =
  List.concat
  @@ List.mapi
       (fun file_number ((file_name : string), content) ->
         let content = String.split_on_char '\n' content in
         List.mapi
           (fun line_number line -> file_name, line_number, line, file_number)
           content)
       files


let parse_todo_string s =
  String.split_on_char '\n' s
  |> List.filter_map (fun message ->
    if String.equal message ""
    then None
    else (
      let split = Str.bounded_split (Str.regexp ":[0-9]+:") message 2 in
      match split with
      (* file, line, content *)
      | [ file_name; content ] -> Some (file_name, content)
      | _ -> raise (Not_A_Tuple (String.concat " SPLIT " split, message))))


let pretty_format_todo parsed_headlines =
  let padding = get_padding_list parsed_headlines in
  List.map
    (fun (file_name, content) -> String.concat " | " [ pad file_name padding; content ])
    parsed_headlines


let toggle_done file_name =
  let open Shexp_process in
  run_calls
    (call
       [ "sed"; "-i"; "s/" ^ todo_pattern_regexp ^ "/" ^ done_pattern ^ "/g"; file_name ])


let toggle_todo file_name =
  let open Shexp_process in
  run_calls
    (call
       [ "sed"; "-i"; "s/" ^ done_pattern_regexp ^ "/" ^ todo_pattern ^ "/g"; file_name ])


(* Headline parsing *)
let headline_pattern_length = String.length headline_pattern

let find_sort_modification () =
  let open Shexp_process in
  let open Shexp_process.Infix in
  call [ "find"; "."; "-printf"; "%Ts/%f\\n" ]
  |- call [ "sort"; "-n" ]
  |- call [ "cut"; "-c12-" ]


let find_sort_name () =
  let open Shexp_process in
  let open Shexp_process.Infix in
  call [ "find"; "." ] |- call [ "cut"; "-c3-" ]


let run_print ~dir args =
  let open Shexp_process in
  let open Shexp_process.Infix in
  eval (chdir dir (call args |- read_all))


let filter_todos_args = [ grep_cmd; todo_pattern_regexp; "--no-messages"; "-v" ]
let filter_done_args = [ grep_cmd; done_pattern_regexp; "--no-messages"; "-v" ]

let headline_args =
  [ "xargs"; grep_cmd; headline_pattern_regexp; "-H"; "-r"; "-n"; "--no-messages" ]


let get_headlines () =
  let open Shexp_process in
  let open Shexp_process.Infix in
  eval
    (chdir
       execution_directory
       (find_sort_name ()
        |- call headline_args
        |- call filter_todos_args
        |- call filter_done_args
        |- call [ "sort"; "-n"; "-r" ]
        |- read_all))


let get_tagged_headlines tag () =
  let open Shexp_process in
  let open Shexp_process.Infix in
  eval
    (chdir
       execution_directory
       (find_sort_name ()
        |- call headline_args
        |- call [ grep_cmd; "--no-messages"; "-E"; tag ]
        |- call [ "sort"; "-n"; "-r" ]
        |- read_all))


let get_tags () =
  let open Shexp_process in
  let open Shexp_process.Infix in
  eval
    (chdir
       execution_directory
       (call headline_args |- call [ grep_cmd; "-E"; tag_pattern; "-o" ] |- read_all))


(** Returns a tuple of file name and Content *)
let parse_headlines s =
  String.split_on_char '\n' s
  (* Testing in utop it seems like there is maybe a bug with bounded_split, 1 doesn't work for ':'. Therefore using a slower implementation. *)
  |> List.filter_map (fun message ->
    if String.equal message ""
    then None
    else (
      let split = Str.bounded_split (Str.regexp ":[0-9]+:") message 2 in
      match split with
      (* file, line, content *)
      | [ file_name; content ] -> Some (file_name, content)
      | _ -> raise (Not_A_Tuple (String.concat " SPLIT " split, message))))
  |> Array.of_list


(** Turns "2024-03-05.org:* Hello world" into "2024-03-05    | * Hello world" *)
let pretty_format parsed_headlines =
  let padding = get_padding_arr parsed_headlines in
  Array.map
    (fun (file_name, content) -> String.concat " | " [ pad file_name padding; content ])
    parsed_headlines


(** Full body parsing *)

let get_full_content_command file = [ "cat"; file ]

let read_whole_file filename =
  (* open_in_bin works correctly on Unix and Windows *)
  let ch = open_in_bin filename in
  let s = really_input_string ch (in_channel_length ch) in
  close_in ch;
  s


let get_full_file_content_content file =
  file, read_whole_file (execution_directory ^ "/" ^ file)


let parse_full_content files =
  List.concat
  @@ List.mapi
       (fun file_number ((file_name : string), content) ->
         let content = String.split_on_char '\n' content in
         List.mapi
           (fun line_number line -> file_name, line_number, line, file_number)
           content)
       files


type display_type =
  | Bold of string
  | Normal of string

let pretty_print_parsed_content parsed_files =
  let padding = String.make headline_pattern_length ' ' in
  List.concat_map
    (fun (file_name, line_number, line_content, _) ->
      if line_number == 0
      then [ Bold ("------- " ^ file_name); Normal line_content ]
      else [ Normal (padding ^ line_content) ])
    parsed_files