ncumbra 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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