ncumbra 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,97 @@
1
+ # ----------------------------------------------------------------------------- #
2
+ # File: labeledfield.rb
3
+ # Description:
4
+ # Author: j kepler http://github.com/mare-imbrium/canis/
5
+ # Date: 2018-04-12 - 23:35
6
+ # License: MIT
7
+ # Last update: 2018-04-21 23:58
8
+ # ----------------------------------------------------------------------------- #
9
+ # labeledfield.rb Copyright (C) 2018 j kepler
10
+ require 'umbra/field'
11
+ module Umbra
12
+ # TODO should be able to add a mnemonic here for the label since the association exists
13
+ # TODO we should consider creating a Label, so user can have more control. Or allow user
14
+ # to supply a Label i/o a String ???
15
+ #
16
+ # NOTE: If using LabeledField in a messagebox, pls specify messagebox width explicitly
17
+ # since the width here is the field width, and messagebox has no way of knowing that we are
18
+ # placing a label too.
19
+
20
+ # Other options:
21
+ # This could contain a Labal and a Field and extend Widget. Actually, it could be LabeledWidget
22
+ # so that any other widget is sent in and associated with a a label.
23
+ #
24
+ class LabeledField < Field
25
+ # This stores a +String+ and prints it before the +Field+.
26
+ # This label is gauranteed to print to the left of the Field.
27
+ # This label prints on +lrow+ and +lcol+ if supplied, else it will print on the left of the field
28
+ # at +col+ minus the width of the label.
29
+ #
30
+ # It is initialized exactly like a Field, with the addition of label (and optionally label_color_pair,
31
+ # label_attr, and lcol, lrow)
32
+ #
33
+ attr_accessor :label # label of field, just a String
34
+ # if lrow and lcol are specified then label is printed exactly at that spot.
35
+ # If they are omitted, then label is printed on left of field. Omit the lcol if you want
36
+ # the fields to be aligned, one under another, with the labels right-aligned.
37
+ attr_accessor :lrow, :lcol # coordinates of the label
38
+ attr_accessor :label_color_pair # label of field color_pair
39
+ attr_accessor :label_attr # label of field attribute
40
+ attr_accessor :label_highlight_color_pair # label of field high color_pair
41
+ attr_accessor :label_highlight_attr # label of field high attribute
42
+ attr_accessor :mnemonic # mnemonic of field which shows up on label
43
+ attr_accessor :related_widget # to keep sync with label
44
+ def initialize config={}, &block
45
+ @related_widget = self
46
+ super
47
+ end
48
+
49
+ def repaint
50
+ return unless @repaint_required
51
+ _lrow = @lrow || @row
52
+ # the next was nice, but in some cases this goes out of screen. and the container
53
+ # only sets row and col for whatever is added, it does not know that lcol has to be
54
+ # taken into account
55
+ _lcol = @lcol || (@col - @label.length - 2)
56
+ if _lcol < 1
57
+ @lcol = @col
58
+ @col = @lcol + @label.length + 2
59
+ _lcol = @lcol
60
+ end
61
+
62
+ =begin
63
+ # This actually uses the col of the field, and pushes field ahead. We need to get the above to work.
64
+ unless @lcol
65
+ @lcol = @col
66
+ @col = @lcol + @label.length + 2
67
+ end
68
+ _lcol = @lcol
69
+ =end
70
+ lcolor = @label_color_pair || CP_BLACK
71
+ lattr = @label_attr || NORMAL
72
+
73
+ # this gives the effect of `pine` (aka alpine) email client, of highlighting the label
74
+ # when the field is in focus.
75
+ if @state == :HIGHLIGHTED
76
+ lcolor = @label_highlight_color_pair || lcolor
77
+ lattr = @label_highlight_attr || lattr
78
+ end
79
+
80
+ $log.debug " repaint labeledfield lrow: #{_lrow} lcol #{_lcol} "
81
+ # print the label
82
+ @graphic.printstring _lrow, _lcol, @label, lcolor, lattr
83
+ # print the mnemonic
84
+ if @mnemonic
85
+ index = label.index(@mnemonic) || label.index(@mnemonic.swapcase)
86
+ if index
87
+ y = _lcol + index
88
+ x = _lrow
89
+ @graphic.mvchgat(x, y, max=1, FFI::NCurses::A_BOLD|UNDERLINE, FFI::NCurses.COLOR_PAIR(lcolor || 1), nil)
90
+ end
91
+ end
92
+
93
+ # print the field
94
+ super
95
+ end
96
+ end
97
+ end # module
@@ -0,0 +1,384 @@
1
+ require 'umbra/widget'
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: listbox.rb
4
+ # Description: list widget that displays a list of items
5
+ # Author: j kepler http://github.com/mare-imbrium/canis/
6
+ # Date: 2018-03-19
7
+ # License: MIT
8
+ # Last update: 2018-04-20 12:35
9
+ # ----------------------------------------------------------------------------- #
10
+ # listbox.rb Copyright (C) 2012-2018 j kepler
11
+ # == TODO
12
+ # currently only do single selection, we may do multiple at a later date.
13
+ # insert/delete a row ??
14
+ # ----------------
15
+ module Umbra
16
+ class Listbox < Widget
17
+ attr_reader :list # list containing data
18
+ #
19
+ # index of focussed row, starting 0, index into the list supplied
20
+ attr_reader :current_index
21
+
22
+ attr_accessor :selection_key # key used to select a row
23
+ attr_accessor :selected_index # row selected, may change to plural
24
+ attr_accessor :selected_color_pair # row selected color_pair
25
+ attr_accessor :selected_attr # row selected color_pair
26
+ attr_accessor :selected_mark # row selected character
27
+ attr_accessor :unselected_mark # row unselected character (usually blank)
28
+ attr_accessor :current_mark # row current character (default is >)
29
+
30
+
31
+ def initialize config={}, &block
32
+ @focusable = false
33
+ @editable = false
34
+ @pstart = 0 # which row does printing start from
35
+ @current_index = 0 # index of row on which cursor is
36
+ @selected_index = nil # index of row selected
37
+ @selection_key = ?s.getbyte(0) # 's' used to select/deselect
38
+ @selected_color_pair = CP_RED
39
+ @selected_attr = REVERSE
40
+ @row_offset = 0
41
+ @selected_mark = 'x' # row selected character
42
+ @unselected_mark = ' ' # row unselected character (usually blank)
43
+ @current_mark = '>' # row current character (default is >)
44
+ register_events([:LEAVE_ROW, :ENTER_ROW, :LIST_SELECTION_EVENT])
45
+ super
46
+
47
+ map_keys
48
+ @pcol = 0
49
+ @repaint_required = true
50
+ end
51
+ # set list of data to be displayed.
52
+ # NOTE this can be called again and again, so we need to take care of change in size of data
53
+ # as well as things like current_index and selected_index or indices.
54
+ # clear the listbox is list is smaller or empty FIXME
55
+ def list=(alist)
56
+ if !alist or alist.size == 0
57
+ $log.debug " setting focusable to false in listbox "
58
+ self.focusable=(false)
59
+ # should we return here
60
+ else
61
+ $log.debug " setting focusable to true in listbox #{alist.count} "
62
+ self.focusable=(true)
63
+ end
64
+ @list = alist
65
+ @repaint_required = true
66
+ @pstart = @current_index = 0
67
+ @selected_index = nil
68
+ @pcol = 0
69
+ fire_handler(:CHANGED, alist)
70
+ end
71
+ # Calculate dimensions as late as possible, since we can have some other container such as a box,
72
+ # determine the dimensions after creation.
73
+ private def _calc_dimensions
74
+ raise "Dimensions not supplied to listbox" if @row.nil? or @col.nil? or @width.nil? or @height.nil?
75
+ @_calc_dimensions = true
76
+ @int_width = @width # internal width NOT USED ELSEWHERE
77
+ @int_height = @height # internal height USED HERE ONLy REDUNDANT FIXME
78
+ @scroll_lines ||= @int_height/2
79
+ @page_lines = @int_height
80
+ end
81
+ # Each row can be in one of the following states:
82
+ # 1. HIGHLIGHTED: cursor is on the row, and the list is focussed (user is in it)
83
+ # 2. CURRENT : cursor was on this row, now user has exited the list
84
+ # 3. SELECTED : user has selected this row (this can also have above two states actually)
85
+ # 4. NORMAL : All other rows: not selected, not under cursor
86
+ # returns color, attrib and left marker for given row
87
+ # @param index of row in the list
88
+ # @param state of row in the list (see above states)
89
+ def _format_color index, state
90
+ arr = case state
91
+ when :SELECTED
92
+ [@selected_color_pair, @selected_attr]
93
+ when :HIGHLIGHTED
94
+ [@highlight_color_pair || CP_WHITE, @highlight_attr || REVERSE]
95
+ when :CURRENT
96
+ [@color_pair, @attr]
97
+ when :NORMAL
98
+ #@alt_color_pair ||= create_color_pair(COLOR_BLUE, COLOR_WHITE)
99
+ _color = CP_CYAN
100
+ _color = CP_WHITE if index % 2 == 0
101
+ #_color = @alt_color_pair if index % 2 == 0
102
+ [@color_pair || _color, @attr || NORMAL]
103
+ end
104
+ return arr
105
+ end
106
+ # do the actual printing of the row, depending on index and state
107
+ # This method starts with underscore since it is only required to be overriden
108
+ # if an object has special printing needs.
109
+ def _print_row(win, row, col, str, index, state)
110
+ arr = _format_color index, state
111
+ win.printstring(row, col, str, arr[0], arr[1])
112
+ end
113
+ def _format_mark index, state
114
+ mark = case state
115
+ when :SELECTED
116
+ @selected_mark
117
+ when :HIGHLIGHTED, :CURRENT
118
+ @current_mark
119
+ else
120
+ @unselected_mark
121
+ end
122
+ end
123
+
124
+ def repaint
125
+ _calc_dimensions unless @_calc_dimensions
126
+
127
+ return unless @repaint_required
128
+ win = @graphic
129
+ r,c = @row, @col
130
+ _attr = @attr || NORMAL
131
+ _color = @color_pair || CP_WHITE
132
+ curpos = 1
133
+ coffset = 0
134
+ width = @width
135
+ #files = @list
136
+ files = getvalue
137
+
138
+ ht = @height
139
+ cur = @current_index
140
+ st = pstart = @pstart # previous start
141
+ pend = pstart + ht -1 # previous end
142
+ if cur > pend
143
+ st = (cur -ht) + 1
144
+ elsif cur < pstart
145
+ st = cur
146
+ end
147
+ $log.debug "LISTBOX: cur = #{cur} st = #{st} pstart = #{pstart} pend = #{pend} listsize = #{@list.size} "
148
+ hl = cur
149
+ y = 0
150
+ ctr = 0
151
+ filler = " "*(width)
152
+ files.each_with_index {|_f, y|
153
+ next if y < st
154
+ f = _format_value(_f)
155
+
156
+ # determine state of this row: NORMAL CURRENT HIGHLIGHTED SELECTED {{{
157
+ _st = :NORMAL
158
+ if y == hl # current row, row on which cursor is or was
159
+ # highlight only if object is focussed, otherwise just show mark
160
+ if @state == :HIGHLIGHTED
161
+ _st = :HIGHLIGHTED
162
+ else
163
+ # cursor was on this row, but now user has tabbed out
164
+ _st = :CURRENT
165
+ end
166
+ curpos = ctr
167
+ end
168
+ if y == @selected_index
169
+ _st = :SELECTED
170
+ end # }}}
171
+ #colr, attr, mark = _format_color y, _st
172
+
173
+ mark = _format_mark(y, _st)
174
+ =begin
175
+ mark = case _st
176
+ when :SELECTED
177
+ @selected_mark
178
+ when :HIGHLIGHTED, :CURRENT
179
+ @current_mark
180
+ else
181
+ @unselected_mark
182
+ end
183
+ =end
184
+
185
+ ff = "#{mark} #{f}"
186
+ # truncate string to width, and handle panning {{{
187
+ if ff
188
+ if ff.size > width
189
+ # pcol can be greater than width then we get null
190
+ if @pcol < ff.size
191
+ ff = ff[@pcol..@pcol+width-1]
192
+ else
193
+ ff = ""
194
+ end
195
+ else
196
+ if @pcol < ff.size
197
+ ff = ff[@pcol..-1]
198
+ else
199
+ ff = ""
200
+ end
201
+ end
202
+ end # }}}
203
+ ff = "" unless ff
204
+
205
+ win.printstring(ctr + r, coffset+c, filler, _color ) # print filler
206
+ #win.printstring(ctr + r, coffset+c, ff, colr, attr)
207
+ _print_row(win, ctr + r, coffset+c, ff, y, _st)
208
+ ctr += 1
209
+ @pstart = st
210
+ break if ctr >= ht
211
+ }
212
+ ## if counter < ht then we need to clear the rest in case there was data earlier {{{
213
+ if ctr < ht
214
+ while ctr < ht
215
+ win.printstring(ctr + r, coffset+c, filler, _color )
216
+ ctr += 1
217
+ end
218
+ end # }}}
219
+ @row_offset = curpos #+ border_offset
220
+ @col_offset = coffset
221
+ @repaint_required = false
222
+ end
223
+
224
+ def getvalue
225
+ @list
226
+ end
227
+
228
+ #
229
+ # how to convert the line of the array to a simple String.
230
+ # This is only required to be overridden if the list passed in is not an array of Strings.
231
+ # @param the current row which could be a string or array or whatever was passed in in +list=()+.
232
+ # @return [String] string to print. A String must be returned.
233
+ def _format_value line
234
+ line
235
+ end
236
+ #alias :_format_value :getvalue_for_paint
237
+
238
+
239
+ def map_keys
240
+ bind_keys([?k,FFI::NCurses::KEY_UP], "Up") { cursor_up }
241
+ bind_keys([?j,FFI::NCurses::KEY_DOWN], "Down") { cursor_down }
242
+ bind_keys([?l,FFI::NCurses::KEY_RIGHT], "Right") { cursor_forward }
243
+ bind_keys([?h,FFI::NCurses::KEY_LEFT], "Left") { cursor_backward }
244
+ bind_key(?g, 'goto_start') { goto_start }
245
+ bind_key(?G, 'goto_end') { goto_end }
246
+ bind_key(FFI::NCurses::KEY_CTRL_A, 'cursor_home') { cursor_home }
247
+ bind_key(FFI::NCurses::KEY_CTRL_E, 'cursor_end') { cursor_end }
248
+ bind_key(FFI::NCurses::KEY_CTRL_F, 'page_forward') { page_forward }
249
+ bind_key(32, 'page_forward') { page_forward }
250
+ bind_key(FFI::NCurses::KEY_CTRL_B, 'page_backward'){ page_backward }
251
+ bind_key(FFI::NCurses::KEY_CTRL_U, 'scroll_up') { scroll_up }
252
+ bind_key(FFI::NCurses::KEY_CTRL_D, 'scroll_down') { scroll_down }
253
+ return if @keys_mapped
254
+ end
255
+
256
+ def on_enter
257
+ super
258
+ on_enter_row @current_index
259
+ # basically I need to only highlight the current index, not repaint all OPTIMIZE
260
+ touch ; repaint
261
+ end
262
+ def on_leave
263
+ super
264
+ on_leave_row @current_index
265
+ # basically I need to only unhighlight the current index, not repaint all OPTIMIZE
266
+ touch ; repaint
267
+ end
268
+ # called when object leaves a row and when object is exited.
269
+ def on_leave_row index
270
+ fire_handler(:LEAVE_ROW, [index]) # 2018-03-26 - improve this
271
+ end
272
+ # called whenever a row entered.
273
+ # Call when object entered, also.
274
+ def on_enter_row index
275
+ fire_handler(:ENTER_ROW, [@current_index]) # 2018-03-26 - improve this
276
+ end
277
+ def cursor_up
278
+ @current_index -= 1
279
+ end
280
+ # go to next row
281
+ def cursor_down
282
+ @current_index += 1
283
+ end
284
+ # position cursor at start of field
285
+ def cursor_home
286
+ @curpos = 0 # UNUSED RIGHT NOW
287
+ @pcol = 0
288
+ end
289
+ # goto end of line.
290
+ # This should be consistent with moving the cursor to the end of the row with right arrow
291
+ def cursor_end
292
+ blen = current_row().length
293
+ if blen < @width
294
+ @pcol = 0
295
+ else
296
+ @pcol = blen-@width+2 # 2 is due to mark and space
297
+ end
298
+ @curpos = blen # this is position in array where editing or motion is to happen regardless of what you see
299
+ # regardless of pcol (panning)
300
+ end
301
+ # returns current row as String
302
+ # 2018-04-11 - NOTE this may not be a String so we convert it to string before returning
303
+ # @return [String] row the cursor/user is on
304
+ def current_row
305
+ s = @list[@current_index]
306
+ _format_value s
307
+ end
308
+ def cursor_forward
309
+ blen = current_row().size-1
310
+ @pcol += 1 if @pcol < blen
311
+ end
312
+ def cursor_backward
313
+ @pcol -= 1 if @pcol > 0
314
+ end
315
+ # go to start of file (first line)
316
+ def goto_start
317
+ @current_index = 0
318
+ @pcol = @curpos = 0
319
+ end
320
+ # go to end of file (last line)
321
+ def goto_end
322
+ @current_index = @list.size-1
323
+ @pcol = @curpos = 0
324
+ end
325
+ def scroll_down
326
+ @current_index += @scroll_lines
327
+ end
328
+ def scroll_up
329
+ @current_index -= @scroll_lines
330
+ end
331
+ def page_backward
332
+ @current_index -= @page_lines
333
+ end
334
+ def page_forward
335
+ @current_index += @page_lines
336
+ end
337
+ # listbox key handling
338
+ def handle_key ch
339
+ old_current_index = @current_index
340
+ old_pcol = @pcol
341
+ case ch
342
+ when @selection_key
343
+ @repaint_required = true
344
+ if @selected_index == @current_index
345
+ @selected_index = nil
346
+ else
347
+ @selected_index = @current_index
348
+ end
349
+ fire_handler :LIST_SELECTION_EVENT, self # use selected_index to know which one
350
+ else
351
+ ret = super
352
+ return ret
353
+ end
354
+ ensure
355
+ @current_index = 0 if @current_index < 0
356
+ @current_index = @list.size-1 if @current_index >= @list.size
357
+ if @current_index != old_current_index
358
+ on_leave_row old_current_index
359
+ on_enter_row @current_index
360
+ @repaint_required = true
361
+ end
362
+ @repaint_required = true if old_pcol != @pcol
363
+ end
364
+
365
+ def command *args, &block
366
+ bind_event :ENTER_ROW, *args, &block
367
+ end
368
+ def print_border row, col, height, width, color, att=FFI::NCurses::A_NORMAL
369
+ raise "deprecated"
370
+ pointer = @graphic.pointer
371
+ FFI::NCurses.wattron(pointer, FFI::NCurses.COLOR_PAIR(color) | att)
372
+ FFI::NCurses.mvwaddch pointer, row, col, FFI::NCurses::ACS_ULCORNER
373
+ FFI::NCurses.mvwhline( pointer, row, col+1, FFI::NCurses::ACS_HLINE, width-2)
374
+ FFI::NCurses.mvwaddch pointer, row, col+width-1, FFI::NCurses::ACS_URCORNER
375
+ FFI::NCurses.mvwvline( pointer, row+1, col, FFI::NCurses::ACS_VLINE, height-2)
376
+
377
+ FFI::NCurses.mvwaddch pointer, row+height-1, col, FFI::NCurses::ACS_LLCORNER
378
+ FFI::NCurses.mvwhline(pointer, row+height-1, col+1, FFI::NCurses::ACS_HLINE, width-2)
379
+ FFI::NCurses.mvwaddch pointer, row+height-1, col+width-1, FFI::NCurses::ACS_LRCORNER
380
+ FFI::NCurses.mvwvline( pointer, row+1, col+width-1, FFI::NCurses::ACS_VLINE, height-2)
381
+ FFI::NCurses.wattroff(pointer, FFI::NCurses.COLOR_PAIR(color) | att)
382
+ end
383
+ end
384
+ end # module
data/lib/umbra/menu.rb ADDED
@@ -0,0 +1,93 @@
1
+ # ----------------------------------------------------------------------------- #
2
+ # File: menu.rb
3
+ # Description: a popup menu like mc/midnight commander
4
+ # Author: j kepler http://github.com/mare-imbrium/canis/
5
+ # Date: 2018-03-13
6
+ # License: MIT
7
+ # Last update: 2018-04-16 15:20
8
+ # ----------------------------------------------------------------------------- #
9
+ # menu.rb Copyright (C) 2012-2018 j kepler
10
+
11
+ # a midnight commander like mc_menu
12
+ # Pass a hash of key and label.
13
+ # menu will only accept keys or arrow keys or C-c Esc to cancel
14
+ # returns nil if C-c or Esc pressed.
15
+ # Otherwise returns character pressed.
16
+ # == TODO
17
+ # depends on our window class which is minimal.
18
+ # [ ] cursor should show on the row that is highlighted
19
+ # [ ] Can we remove that dependency so this is independent
20
+ # Currently, we paint window each time user pressed up or down, but we can just repaint the attribute
21
+ # [ ] width of array items not checked. We could do that or have user pass it in.
22
+ # [ ] we are not scrolling if user sends in a large number of items. we should cap it to 10 or 20
23
+ # == CHANGELOG
24
+ #
25
+ require 'umbra/window'
26
+ module Umbra
27
+ class Menu
28
+
29
+ def initialize title, hash, config={}
30
+
31
+ @list = hash.values
32
+ @keys = hash.keys.collect { |x| x.to_s }
33
+ @hash = hash
34
+ bkgd = config[:bkgd] || FFI::NCurses.COLOR_PAIR(14) | BOLD
35
+ @attr = BOLD
36
+ @color_pair = config[:color_pair] || 14
37
+ ht = @list.size+2
38
+ wid = config[:width] || 40
39
+ top = (FFI::NCurses.LINES - ht)/2
40
+ left = (FFI::NCurses.COLS - wid)/2
41
+ @window = Window.new(ht, wid, top, left)
42
+ @window.wbkgd(bkgd)
43
+ @window.box
44
+ @window.title(title)
45
+ @current = 0
46
+ print_items @hash
47
+ end
48
+ def print_items hash
49
+ ix = 0
50
+ hash.each_pair {|k, val|
51
+ attr = @attr
52
+ attr = REVERSE if ix == @current
53
+ @window.printstring(ix+1 , 2, "#{k} #{val}", @color_pair, attr )
54
+ ix += 1
55
+ }
56
+ @window.refresh
57
+ end
58
+ def getkey
59
+ ch = 0
60
+ char = nil
61
+ begin
62
+ while (ch = @window.getkey) != FFI::NCurses::KEY_CTRL_C
63
+ break if ch == 27 # ESC
64
+ tmpchar = FFI::NCurses.keyname(ch) rescue '?'
65
+ if @keys.include? tmpchar
66
+ $log.debug " menu #{tmpchar.class}:#{tmpchar} "
67
+ char = ch.chr
68
+ $log.debug " menu #{ch.class}:#{char} "
69
+ #char = tmpchar
70
+ break
71
+ end
72
+ case ch
73
+ when FFI::NCurses::KEY_DOWN
74
+ @current += 1
75
+ when FFI::NCurses::KEY_UP
76
+ @current -= 1
77
+ when FFI::NCurses::KEY_RETURN
78
+ char = @keys[@current]
79
+ break
80
+ end
81
+ @current = 0 if @current < 0
82
+ @current = @list.size-1 if @current >= @list.size
83
+ print_items @hash
84
+
85
+ # trap arrow keys here
86
+ end
87
+ ensure
88
+ @window.destroy
89
+ end
90
+ return char
91
+ end
92
+ end
93
+ end # module