-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathdict_edit_tui.jl
574 lines (509 loc) · 15.8 KB
/
dict_edit_tui.jl
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
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
# A Tty GUI for editing dictionaries!
using DisplayStructure
const DS = DisplayStructure
using Terming
const T = Terming
using DataFrames
using CrystalInfoFramework
using Dates
#== Model - View - Controller idea
The model contains the dictionary being edited, the view is simply
regenerated after every change to the model, and the rest is the
controller.
==#
#== Models
Currently the model is
==#
mutable struct DictModel
base_dict::DDLm_Dictionary
cur_def::String
cur_attribute::String
cur_val::Vector{Any}
att_index::Int64
edit_val::String
edit_pos::Int64
message::String
ref_dic::Union{DDLm_Dictionary,Nothing}
end
const base_help = "(q) Quit (f) next def (b) prev def (c) category (o) object (n) new (x) delete (h) help (k) keys"
const edit_help = "EDIT MODE (esc) leave"
DictModel(ddlm_dict,ref_dic) = begin
all_defs = collect(keys(ddlm_dict))
start_name = length(all_defs) > 0 ? all_defs[1] : ""
println("Starting def is $start_name")
d = DictModel(ddlm_dict,start_name,
"_definition.id",["?"],1,"",1,
base_help,ref_dic)
d.cur_val = val_for_att(d)
return d
end
# The current definition is updated with the new value
update_model!(d,new_val) = begin
if d.edit_val == strip(d.cur_val[d.att_index]) return end #no change
done = auto_changes!(d,new_val)
if !done
update_dict!(d.base_dict,d.cur_def,d.cur_attribute,d.cur_val[d.att_index],new_val)
end
d.cur_val = val_for_att(d)
@assert d.cur_val[d.att_index] == new_val
end
auto_changes!(d,new_val) = begin
# Fix any category changes
old = d.cur_val[d.att_index]
if d.cur_attribute == "_name.object_id" && is_category(d.base_dict,d.cur_def)
rename_category!(d.base_dict,old,new_val)
d.cur_def = new_val
head_chk = get_attribute(d.base_dict,d.cur_def,"_definition.class")
if !ismissing(head_chk) && head_chk[] == "Head"
update_dict!(d.base_dict,d.cur_def,"_name.category_id",new_val)
end
return true # all done
end
if d.cur_attribute == "_name.object_id" && !is_category(d.base_dict,d.cur_def)
old_cat = d.base_dict[d.cur_def][:name].category_id[]
new_name = "_"*old_cat*"."*new_val
rename_name!(d.base_dict,d.cur_def,new_name)
d.cur_def = new_name
elseif d.cur_attribute == "_name.category_id" && !is_category(d.base_dict,d.cur_def)
old_obj = d.base_dict[d.cur_def][:name].object_id[]
new_name = "_"*new_val*"."*old_obj
rename_name!(d.base_dict,d.cur_def,new_name)
d.cur_def = new_name
end
if d.cur_attribute == "_definition.scope" && new_val == "Head"
# cat / obj same for head definition
obj_id = d.base_dict[d.cur_def][:name].object_id[]
update_dict!(d.base_dict,d.cur_def,"_name.category_id",obj_id)
end
return false #not all done
# This should be done right at the end
#today = Dates.format(Dates.now(),"yyyy-mm-dd")
#update_dict!(d.base_dict,d.cur_def,"_definition.update",today)
end
update_model!(d) = begin
update_model!(d,d.edit_val)
end
possible_values(d) = begin
if d.cur_attribute == "_name.category_id"
return get_categories(d.base_dict)
end
full_list = []
try
full_list = d.ref_dic[d.cur_attribute][:enumeration_set].state
catch
return []
end
# Prune based on other information
if !is_category(d.base_dict,d.cur_def) && d.cur_attribute == "_definition.class"
deleteat!(full_list,findall(x->x in ("Head","Set","Loop","Functions")))
end
return full_list
end
val_for_att(d) = begin
attr = d.cur_attribute
cat,obj = split(attr,".")
cat = Symbol(cat[2:end])
info = d.base_dict[d.cur_def]
if haskey(info,cat) && obj in names(info[cat])
att_val = info[cat][!,obj]
else
att_val = ["?"]
end
return att_val
end
next_def(d) = begin
all_defs = sort!(collect(keys(d.base_dict)))
cur_index = findnext(isequal(lowercase(d.cur_def)),all_defs,1)
if cur_index != length(all_defs)
d.cur_def = all_defs[cur_index+1]
d.att_index = 1
d.cur_val= val_for_att(d)
end
true
end
back_def(d) = begin
all_defs = sort!(collect(keys(d.base_dict)))
cur_index = findnext(isequal(lowercase(d.cur_def)),all_defs,1)
if cur_index != 1
d.cur_def = all_defs[cur_index-1]
d.att_index = 1
d.cur_val = val_for_att(d)
end
true
end
## For editing
start_edit(d) = begin
d.edit_val = strip(d.cur_val[d.att_index])
d.edit_pos = length(d.edit_val)+1
d.message = edit_help
true
end
move_left(d) = d.edit_pos = d.edit_pos > 1 ? d.edit_pos-1 : d.edit_pos
move_right(d) = d.edit_pos = d.edit_pos <= length(d.edit_val) ? d.edit_pos+1 : d.edit_pos
delete_char(d) = begin
if length(d.edit_val) > 0 && d.edit_pos > 0
if d.edit_pos > length(d.edit_val)
d.edit_val = d.edit_val[1:end-1]
else
d.edit_val = d.edit_val[1:d.edit_pos-2]*d.edit_val[d.edit_pos:end]
end
end
move_left(d)
true
end
insert_char(d,char) = begin
pos = d.edit_pos
d.edit_val = d.edit_val[1:pos-1]*char*d.edit_val[pos:end]
move_right(d)
true
end
finish_edit(d) = begin
update_model!(d)
d.edit_val = ""
d.message = base_help
end
"""
Make a new definition that is a copy of the current
definition with the object id changed.
"""
make_new_def!(d) = begin
old_def = d.base_dict[d.cur_def]
new_def_name = "_"*old_def[:name].category_id[]*"."*old_def[:name].object_id[]*"_new"
old_def[:name].object_id = ["xxx"]
old_def[:definition].id = [new_def_name]
old_def[:definition].update = [Dates.format(Dates.now(),"yyyy-mm-dd")]
old_def[:description].text = ["Please edit me"]
add_definition!(d.base_dict,old_def)
d.cur_def = new_def_name
true
end
"""
Remove a definition
"""
delete_def!(d) = begin
all_keys = sort!(collect(keys(d.base_dict)))
if length(all_keys) == 1 return false end #can't delete final def
cur_pos = indexin([d.cur_def],all_keys)[]
delete!(d.base_dict,d.cur_def)
if cur_pos == 1 d.cur_def = all_keys[2] else d.cur_def = all_keys[cur_pos-1] end
true
end
set_attr(d,attr) = begin
if attr != ""
d.cur_attribute = attr
d.att_index = 1
d.cur_val = val_for_att(d)
end
true
end
step_forward(d) = begin
if length(d.cur_val) >= d.att_index+1
d.att_index += 1
end
true
end
step_back(d) = begin
if d.att_index > 1
d.att_index -= 1
end
true
end
# Actually change the definition
change_fwd(d) = begin
if !isnothing(d.ref_dic)
poss = possible_values(d)
cur_index = indexin([d.cur_val[d.att_index]],poss)[]
if isnothing(cur_index) cur_index = 0 end #bad value
if cur_index + 1 <= length(poss)
update_model!(d,poss[cur_index+1])
end
end
true
end
change_bwd(d) = begin
if !isnothing(d.ref_dic)
poss = possible_values(d)
cur_index = indexin([d.cur_val[d.att_index]],poss)[]
if isnothing(cur_index) cur_index = 2 end
if cur_index - 1 >= 1
update_model!(d,poss[cur_index-1])
end
end
true
end
#TODO: fix all dates for correctness
write_out(d,outfile) = begin
f = open(outfile,"w")
show(f,MIME("text/cif"),d.base_dict)
close(f)
d.message = "Written to $outfile"
true
end
#== Views
A simple view that simply takes the key information of the model
and presents it in a text ui.
==#
"""
Display definition for `title` contained in `info`, with
value at `index` for `attr` contained in editing pane.
Somewhat clunky as it redoes the view every time, but we
could consider an additional method that just updates the
various elements in `ds`.
"""
gen_view(d::DictModel) = begin
h,w = T.displaysize()
ds = DS.DisplayStack()
title = d.cur_def
info = d.base_dict[title]
attr = d.cur_attribute
val = d.cur_val[d.att_index]
push!(ds, :full_def => DS.Panel(title,[h-6,w],[1,1]))
push!(ds, :attr => DS.Panel(attr,[5,w],[h-6,1]))
# Show definition
def_lines = prepare_def(title,info,lines=h-6)
push!(ds, :base => DS.Label(def_lines,[2,2]))
# Show attribute value
push!(ds, :instructions => DS.Label(d.message ,[h,5]))
if d.edit_val == ""
push!(ds, :attribute => DS.Label(val,[h-5,2]))
else
push!(ds, :attribute => DS.Label(d.edit_val,[h-5,2]))
end
return ds
end
# Should be added to DisplayStructure
Base.setindex!(ds::DisplayStack,v,k) = ds.elements[k] = v
prepare_def(title,info;start=1,lines=20) = begin
strbuf = IOBuffer()
CrystalInfoFramework.show_one_def(strbuf,title,info)
full = String(take!(strbuf))
# drop first start lines
start_char = 1
lines = 1
so_far = findnext("\n",full,start_char)
while so_far != nothing
so_far = so_far[]
lines = lines + 1
if lines < start start_char = so_far end
if lines >= start + lines return full[start_char:so_far-1] end
so_far = findnext("\n",full,so_far+1)
end
return full[start_char:end]
end
# Return a bit of val suitable for display in the provided room
display_length(val,width) = begin
val = strip(val)
first_line = findnext('\n',val,1)
if first_line != nothing
val = val[1:first_line]
end
if length(val) > width return val[1:width-3]*"..." else return val end
end
# the help screen
const help_text = """
Notes:
1. If you change a category name, all children of
this category are automatically renamed accordingly
"""
## Controls
init_term() = begin
T.raw!(true)
T.alt_screen(true)
T.cshow(false)
T.clear()
end
reset_term() = begin
T.raw!(false)
T.alt_screen(false)
T.cshow(true)
end
add_ref_dic(d::DictModel,ref_dic) = begin
d.ref_dic = ref_dic
end
handle_quit() = begin
keep_running = false
T.cmove_line_last()
T.println("\nShutting down")
return keep_running
end
# Select a new attribute to work on
att_select(f_ch,sec_ch) = begin
if f_ch == "d"
if sec_ch == "t"
return "_description.text"
elseif sec_ch == "c"
return "_definition.class"
elseif sec_ch == "s"
return "_definition.scope"
end
elseif f_ch == "t"
if sec_ch == "s"
return "_type.source"
elseif sec_ch == "p"
return "_type.purpose"
elseif sec_ch == "c"
return "_type.container"
elseif sec_ch == "o"
return "_type.contents"
end
end
return ""
end
# Update the view based on the model
update(d::DictModel) = begin
v = gen_view(d)
DS.render(v)
if d.edit_val != ""
h,w = T.displaysize()
T.cshow(true)
T.cmove(h-5,d.edit_pos+1)
end
end
handle_event(d::DictModel,name::String) = begin
is_running = true
options_list = possible_values(d)
while is_running
sequence = T.read_stream()
event = T.parse_sequence(sequence)
if sequence == "q"
is_running = handle_quit()
elseif sequence == "f"
next_def(d) && update(d)
elseif sequence == "b"
back_def(d) && update(d)
elseif sequence == "c"
set_attr(d,"_name.category_id") && update(d)
elseif sequence == "o"
set_attr(d,"_name.object_id") && handle_edit_event(d) && update(d)
elseif sequence == "s"
set_attr(d,"_enumeration_set.state") && update(d)
elseif sequence == "e"
handle_edit_event(d) && update(d)
elseif sequence == "k"
set_attr(d,"_category_key.name") && update(d)
elseif sequence == "n"
make_new_def!(d) && update(d)
elseif sequence == "x"
delete_def!(d) && update(d)
elseif sequence == "w"
write_out(d,name) && update(d)
# double-letter sequences for attributes
elseif sequence in("d","t")
second_let = T.read_stream()
if second_let == "q" continue end
set_attr(d,att_select(sequence,second_let)) && update(d)
elseif event == T.KeyPressedEvent(T.RIGHT)
step_forward(d) && update(d)
elseif event == T.KeyPressedEvent(T.LEFT)
step_back(d) && update(d)
# fast editing for enumerated states
elseif event == T.KeyPressedEvent(T.UP)
change_bwd(d) && update(d)
elseif event == T.KeyPressedEvent(T.DOWN)
change_fwd(d) && update(d)
end
end
end
# Escape - abandon edit
# Return - keep edit
handle_edit_event(d::DictModel) = begin
start_edit(d)
update(d)
while true
sequence = T.read_stream()
event = T.parse_sequence(sequence)
if sequence == "\e"
start_edit(d); break
elseif event == T.KeyPressedEvent(T.BACKSPACE)
delete_char(d) && update(d)
elseif event == T.KeyPressedEvent(T.DELETE)
delete_char(d) && update(d)
elseif event == T.KeyPressedEvent(T.RIGHT)
move_right(d); update(d)
elseif event == T.KeyPressedEvent(T.LEFT)
move_left(d); update(d)
elseif event == T.KeyPressedEvent(T.ENTER)
break
else # insert character
insert_char(d,sequence)
update(d)
end
end
finish_edit(d)
update(d)
return true
end
Base.run(start_dict::DDLm_Dictionary;ref_dict=nothing,out_name="") = begin
init_term()
start_model = DictModel(start_dict,ref_dict)
view = gen_view(start_model)
DS.render(view)
handle_event(start_model,out_name)
reset_term()
return start_model.base_dict
end
blank_text = """
#\\#CIF_2.0
###################################################################
# #
# Starting dictionary #
# #
###################################################################
data_starting
_dictionary.title STARTING
_dictionary.class Instance
_dictionary.version 0.0.1
_dictionary.date $(Dates.format(Dates.now(),"yyyy-mm-dd"))
_dictionary.ddl_conformance 4.1.0
_description.text
;
This is a blank starting dictionary. This text should be edited
to reflect the true purpose of the dictionary
;
save_BLANK
_definition.id BLANK
_definition.scope Category
_definition.class Head
_definition.update $(Dates.format(Dates.now(),"yyyy-mm-dd"))
_description.text
;
This category is the top category. It should be renamed above
and below.
;
_name.category_id BLANK
_name.object_id BLANK
save_
save_SUBCAT
_definition.id SUBCAT
_definition.scope Category
_definition.class Loop
_definition.update $(Dates.format(Dates.now(),"yyyy-mm-dd"))
_description.text
;
This category is a placeholder example category. Please edit me
;
_name.category_id BLANK
_name.object_id SUBCAT
_category_key.name "_subcat.dataname"
save_
save_subcat.dataname
_definition.id '_subcat.dataname'
_definition.update $(Dates.format(Dates.now(),"yyyy-mm-dd"))
_description.text
;
A simple empty dataname. Please edit
;
_name.category_id subcat
_name.object_id dataname
_type.source Recorded
_type.container Single
_type.contents Text
_type.purpose Key
save_
"""
# Create a dictionary from nothing
create_run(out_name::String;ref_dict=nothing) = begin
as_cif = Cif(blank_text)
as_dic = DDLm_Dictionary(as_cif)
return run(as_dic,ref_dict=ref_dict,out_name=out_name)
end