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.
data/examples/tt.rb ADDED
@@ -0,0 +1,462 @@
1
+ #!/usr/bin/env ruby
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: tt.rb
4
+ # Description: a quick small directory lister aimed at being simple fast with minimal
5
+ # features, and mostly for viewing files quickly through PAGER
6
+ # Author: j kepler http://github.com/mare-imbrium/
7
+ # Date: 2018-03-09
8
+ # License: MIT
9
+ # Last update: 2018-04-12 08:45
10
+ # ----------------------------------------------------------------------------- #
11
+ # tt.rb Copyright (C) 2012-2018 j kepler
12
+ # == TODO
13
+ # [ ] make a help screen on ?
14
+ # [ ] move to lyra or some gem and publish
15
+ # [ ] pop directories
16
+ # [ ] go back to start directory
17
+ # [ ] go to given directory
18
+ # [x] open files on RIGHT arrow in view (?)
19
+ # [ ] in a long listing, how to get to a file name. first char or pattern TODO
20
+ # [ ] pressing p should open PAGER, e EDITOR, m MOST, v - view
21
+ # [x] on zip file show contents in pager. x to extract.
22
+ # [x] when going up a directory keep cursor on the directory we came from
23
+ # [x] space bar to page down. also page up on c-n c-p top bottom
24
+ # [x] hide dot files
25
+ # [x] reveal dot files on toggle
26
+ # [x] long listing files on toggle
27
+ # [x] long file names not getting cleared
28
+ # [ ] allow entry of command and page output or show in PAGER
29
+ # [x] pressing ENTER should invoke EDITOR
30
+ # [x] scrolling up behavior not correct. we should scroll up from first row not last.
31
+ # see vifm for correct way. mc has different behavior
32
+ # ----------
33
+ # == CHANGELOG
34
+ #
35
+ #
36
+ # --------
37
+ #
38
+ require 'umbra/window'
39
+ require 'umbra/menu'
40
+ TOPLINE="| ` Menu | = Toggle | q Quit | lyra 0.1"
41
+ $sorto = "on"
42
+ $hidden = nil
43
+ $long_listing = false
44
+ $patt = nil
45
+ _LINES = FFI::NCurses.LINES-1
46
+ include Umbra
47
+ def create_footer_window h = 2 , w = FFI::NCurses.COLS, t = FFI::NCurses.LINES-2, l = 0
48
+ ewin = Window.new(h, w , t, l)
49
+ end
50
+ def create_input_window h = 1 , w = FFI::NCurses.COLS, t = FFI::NCurses.LINES-1, l = 0
51
+ ewin = Window.new(h, w , t, l)
52
+ end
53
+ # accepts user input in current window
54
+ # and returns characters after RETURN pressed
55
+ def getchars win, max=20
56
+ str = ""
57
+ pos = 0
58
+ filler = " "*max
59
+ y, x = win.getyx()
60
+ pointer = win.pointer
61
+ while (ch = win.getkey) != FFI::NCurses::KEY_RETURN
62
+ #str << ch.chr
63
+ if ch > 27 and ch < 127
64
+ str.insert(pos, ch.chr)
65
+ pos += 1
66
+ #FFI::NCurses.waddstr(win.pointer, ch.chr)
67
+ end
68
+ case ch
69
+ when FFI::NCurses::KEY_LEFT
70
+ pos -= 1
71
+ pos = 0 if pos < 0
72
+ when FFI::NCurses::KEY_RIGHT
73
+ pos += 1
74
+ pos = str.size if pos >= str.size
75
+ when 127
76
+ pos -= 1 if pos > 0
77
+ str.slice!(pos,1) if pos >= 0 # no backspace if on first pos
78
+ when 27, FFI::NCurses::KEY_CTRL_C
79
+ return nil
80
+ end
81
+ FFI::NCurses.wmove(pointer, y,x)
82
+ FFI::NCurses.waddstr(pointer, filler)
83
+ FFI::NCurses.wmove(pointer, y,x)
84
+ FFI::NCurses.waddstr(pointer, str)
85
+ FFI::NCurses.wmove(pointer, y,pos+1) # set cursor to correct position
86
+ break if str.size >= max
87
+ end
88
+ str
89
+ end
90
+ # runs given command and returns.
91
+ # Does not wait, so command should be like an editor or be paged to less.
92
+ def shell_out command
93
+ FFI::NCurses.endwin
94
+ ret = system command
95
+ FFI::NCurses.refresh
96
+ end
97
+
98
+ ## code related to long listing of files
99
+ GIGA_SIZE = 1073741824.0
100
+ MEGA_SIZE = 1048576.0
101
+ KILO_SIZE = 1024.0
102
+
103
+ # Return the file size with a readable style.
104
+ def readable_file_size(size, precision)
105
+ case
106
+ #when size == 1 : "1 B"
107
+ when size < KILO_SIZE then "%d B" % size
108
+ when size < MEGA_SIZE then "%.#{precision}f K" % (size / KILO_SIZE)
109
+ when size < GIGA_SIZE then "%.#{precision}f M" % (size / MEGA_SIZE)
110
+ else "%.#{precision}f G" % (size / GIGA_SIZE)
111
+ end
112
+ end
113
+ ## format date for file given stat
114
+ def date_format t
115
+ t.strftime "%Y/%m/%d"
116
+ end
117
+ # clears window but leaves top line
118
+ def clearwin(win)
119
+ win.wmove(1,0)
120
+ win.wclrtobot
121
+ end
122
+ ##
123
+ def file_edit win, fp
124
+ #$log.debug " edit #{fp}"
125
+ editor = ENV['EDITOR'] || 'vi'
126
+ vimp = %x[which #{editor}].chomp
127
+ shell_out "#{vimp} #{fp}"
128
+ end
129
+ def file_open win, fp
130
+ unless File.exists? fp
131
+ pwd = %x[pwd]
132
+ #alert "No such file. My pwd is #{pwd} "
133
+ alert win, "No such file. My pwd is #{pwd} "
134
+ return
135
+ end
136
+ ft=%x[file #{fp}]
137
+ if ft.index("text")
138
+ file_edit win, fp
139
+ elsif ft.index(/zip/i)
140
+ shell_out "tar tvf #{fp} | less"
141
+ elsif ft.index(/directory/i)
142
+ shell_out "ls -lh #{fp} | less"
143
+ else
144
+ alert "#{fp} is not text, not opening (#{ft}) "
145
+ end
146
+ end
147
+ def file_page win, fp
148
+ unless File.exists? fp
149
+ pwd = %x[pwd]
150
+ alert "No such file. My pwd is #{pwd} "
151
+ return
152
+ end
153
+ ft=%x[file #{fp}]
154
+ if ft.index("text")
155
+ pager = ENV['PAGER'] || 'less'
156
+ vimp = %x[which #{pager}].chomp
157
+ shell_out "#{vimp} #{fp}"
158
+ elsif ft.index(/zip/i)
159
+ shell_out "tar tvf #{fp} | less"
160
+ elsif ft.index(/directory/i)
161
+ shell_out "ls -lh #{fp} | less"
162
+ else
163
+ alert "#{fp} is not text, not paging "
164
+ #use_on_file "als", fp # only zip or archive
165
+ end
166
+ end
167
+ def get_files
168
+ #files = Dir.glob("*")
169
+ files = `zsh -c 'print -rl -- *(#{$sorto}#{$hidden}M)'`.split("\n")
170
+ if $patt
171
+ files = files.grep(/#{$patt}/)
172
+ end
173
+ return files
174
+ end
175
+ # a quick simple list with highlight row, and scrolling
176
+ #
177
+ # mark directories in color
178
+ # @return start row
179
+ def listing win, path, files, cur=0, pstart
180
+ curpos = 1
181
+ width = win.width-1
182
+ y = x = 1
183
+ ht = win.height-2
184
+ #st = 0
185
+ st = pstart # previous start
186
+ pend = pstart + ht -1 # previous end
187
+ if cur > pend
188
+ st = (cur -ht) +1
189
+ elsif cur < pstart
190
+ st = cur
191
+ end
192
+ hl = cur
193
+ #if cur >= ht
194
+ #st = (cur - ht ) +1
195
+ #hl = cur
196
+ ## we need to scroll the rows
197
+ #end
198
+ y = 0
199
+ ctr = 0
200
+ filler = " "*width
201
+ files.each_with_index {|f, y|
202
+ next if y < st
203
+ colr = CP_WHITE # white on bg -1
204
+ ctr += 1
205
+ mark = " "
206
+ if y == hl
207
+ attr = FFI::NCurses::A_REVERSE
208
+ mark = ">"
209
+ curpos = ctr
210
+ else
211
+ attr = FFI::NCurses::A_NORMAL
212
+ end
213
+ fullp = path + "/" + f
214
+
215
+ if $long_listing
216
+ begin
217
+ unless File.exist? f
218
+ last = f[-1]
219
+ if last == " " || last == "@" || last == '*'
220
+ stat = File.stat(f.chop)
221
+ end
222
+ else
223
+ stat = File.stat(f)
224
+ end
225
+ f = "%10s %s %s" % [readable_file_size(stat.size,1), date_format(stat.mtime), f]
226
+ rescue Exception => e
227
+ f = "%10s %s %s" % ["?", "??????????", f]
228
+ end
229
+ end
230
+ if File.directory? fullp
231
+ #ff = "#{mark} #{f}/"
232
+ # 2018-03-12 - removed slash at end since zsh puts it there
233
+ ff = "#{mark} #{f}"
234
+ colr = CP_BLUE # blue on background color_pair COLOR_PAIR
235
+ attr = attr | FFI::NCurses::A_BOLD
236
+ elsif File.executable? fullp
237
+ ff = "#{mark} #{f}*"
238
+ colr = CP_WHITE # yellow on background color_pair COLOR_PAIR
239
+ attr = attr | FFI::NCurses::A_BOLD
240
+ else
241
+ ff = "#{mark} #{f}"
242
+ end
243
+ win.printstring(ctr, x, filler, colr )
244
+ win.printstring(ctr, x, ff, colr, attr)
245
+ break if ctr >= ht
246
+ }
247
+ #curpos = cur + 1
248
+ #if curpos > ht
249
+ #curpos = ht
250
+ #end
251
+ #statusline(win, "#{cur+1}/#{files.size} #{files[cur]}. cur = #{cur}, pos:#{curpos},ht = #{ht} , hl #{hl}")
252
+ statusline(win, "#{cur+1}/#{files.size} #{files[cur]}. (#{$sorto}) ")
253
+ win.wmove( curpos , 0) # +1 depends on offset of ctr
254
+ win.wrefresh
255
+ return st
256
+ end
257
+ def statusline win, str
258
+ win.printstring(win.height-1, 2, str, 1) # white on default
259
+ #win.mvchgat(win.height-1,2, 1, FFI::NCurses::A_REVERSE, 0, nil)
260
+ end
261
+ def alert str
262
+ win = create_footer_window
263
+ # 10 is too much BLACK on CYAN
264
+ win.wbkgd(FFI::NCurses.COLOR_PAIR(12))
265
+ win.printstring(0,1, str)
266
+ win.wrefresh
267
+ win.getkey
268
+ win.destroy
269
+ end
270
+ def main_menu
271
+ h = { :s => :sort_menu, :M => :newdir, "%" => :newfile }
272
+ m = Menu.new "Main Menu", h
273
+ ch = m.getkey
274
+ return nil if !ch
275
+
276
+ binding = h[ch]
277
+ binding = h[ch.to_sym] unless binding
278
+ if binding
279
+ if respond_to?(binding, true)
280
+ send(binding)
281
+ end
282
+ end
283
+ return ch, binding
284
+ end
285
+
286
+ def sort_menu
287
+ lo = nil
288
+ h = { :n => :newest, :a => :accessed, :o => :oldest,
289
+ :l => :largest, :s => :smallest , :m => :name , :r => :rname, :d => :dirs, :c => :clear }
290
+ m = Menu.new "Sort Menu", h
291
+ ch = m.getkey
292
+ menu_text = h[ch.to_sym]
293
+ case menu_text
294
+ when :newest
295
+ lo="om"
296
+ when :accessed
297
+ lo="oa"
298
+ when :oldest
299
+ lo="Om"
300
+ when :largest
301
+ lo="OL"
302
+ when :smallest
303
+ lo="oL"
304
+ when :name
305
+ lo="on"
306
+ when :rname
307
+ lo="On"
308
+ when :dirs
309
+ lo="/"
310
+ when :clear
311
+ lo=""
312
+ end
313
+ ## This needs to persist and be a part of all listings, put in change_dir.
314
+ $sorto = lo
315
+ #$files = `zsh -c 'print -rl -- *(#{lo}#{$hidden}M)'`.split("\n") if lo
316
+ #$title = nil
317
+ end
318
+
319
+ begin
320
+ init_curses
321
+ txt = "Press cursor keys to move window"
322
+ win = Window.new
323
+ $ht = win.height
324
+ $wid = win.width
325
+ $pagecols = $ht / 2
326
+ $spacecols = $ht
327
+ #win.printstr txt
328
+ win.printstr("Press Ctrl-Q to quit #{win.height}:#{win.width}", win.height-1, 20)
329
+
330
+ path = File.expand_path("./")
331
+ win.printstring(0,0, "PATH: #{path} #{TOPLINE}",0)
332
+ files = get_files
333
+ current = 0
334
+ prevstart = listing(win, path, files, current, 0)
335
+
336
+ ch = 0
337
+ xx = 1
338
+ yy = 1
339
+ y = x = 1
340
+ while (ch = win.getkey) != 113
341
+ #y, x = win.getbegyx(pointer)
342
+ old_y, old_x = y, x
343
+ case ch
344
+ when -1
345
+ next
346
+ when FFI::NCurses::KEY_RIGHT
347
+ # if directory then open it
348
+ fullp = path + "/" + files[current]
349
+ if File.directory? fullp
350
+ Dir.chdir(files[current])
351
+ $patt = nil
352
+ path = Dir.pwd
353
+ #win.printstring(0,0, "PATH: #{path} ",0)
354
+ win.printstring(0,0, "PATH: #{path} #{TOPLINE}",0)
355
+ files = get_files
356
+ current = 0
357
+ #FFI::NCurses.wclrtobot(pointer)
358
+ win.wclrtobot
359
+ elsif File.readable? fullp
360
+ file_page win, fullp
361
+ win.wrefresh
362
+ # open file
363
+ end
364
+ x += 1
365
+ when FFI::NCurses::KEY_LEFT
366
+ # go back higher level
367
+ oldpath = path
368
+ Dir.chdir("..")
369
+ path = Dir.pwd
370
+ $patt = nil
371
+ win.printstring(0,0, "PATH: #{path} #{TOPLINE}",0)
372
+ files = get_files
373
+ # when going up, keep focus on the dir we came from
374
+ current = files.index(File.basename(oldpath) + "/")
375
+ current = 0 if current.nil? or current == -1
376
+ win.wclrtobot
377
+ x -= 1
378
+ when FFI::NCurses::KEY_RETURN
379
+ # if directory then open it
380
+ fullp = path + "/" + files[current]
381
+ if File.directory? fullp
382
+ Dir.chdir(files[current])
383
+ $patt = nil
384
+ path = Dir.pwd
385
+ #win.printstring(0,0, "PATH: #{path} ",0)
386
+ win.printstring(0,0, "PATH: #{path} #{TOPLINE}",0)
387
+ files = get_files
388
+ #files = Dir.entries("./")
389
+ #files.delete(".")
390
+ #files.delete("..")
391
+ current = 0
392
+ win.wclrtobot
393
+ elsif File.readable? fullp
394
+ # open file
395
+ file_open win, fullp
396
+ win.wrefresh
397
+ end
398
+ when FFI::NCurses::KEY_UP
399
+ current -=1
400
+ when FFI::NCurses::KEY_DOWN
401
+ current +=1
402
+ when FFI::NCurses::KEY_CTRL_N
403
+ current += $pagecols
404
+ when FFI::NCurses::KEY_CTRL_P
405
+ current -= $pagecols
406
+ when 32
407
+ current += $spacecols
408
+ when FFI::NCurses::KEY_BACKSPACE, 127
409
+ current -= $spacecols
410
+ when FFI::NCurses::KEY_CTRL_X
411
+ when ?=.getbyte(0)
412
+ #list = ["x this", "y that","z other","a foo", "b bar"]
413
+ list = { "h" => "hidden files toggle", "l" => "long listing toggle", "z" => "the other", "a" => "another one", "b" => "yet another" }
414
+ m = Menu.new "Toggle Options", list
415
+ key = m.getkey
416
+ win.wrefresh # otherwise menu popup remains till next key press.
417
+ case key
418
+ when 'h'
419
+ $hidden = $hidden ? nil : "D"
420
+ files = get_files
421
+ clearwin(win)
422
+ when 'l'
423
+ $long_listing = !$long_listing
424
+ clearwin(win)
425
+ end
426
+ when ?/.getbyte(0)
427
+ # search grep
428
+ # this is writing over the last line of the listing
429
+ ewin = create_input_window
430
+ ewin.printstr("/", 0, 0)
431
+ #win.wmove(1, _LINES-1)
432
+ str = getchars(ewin, 10)
433
+ ewin.destroy
434
+ #alert "Got #{str}"
435
+ $patt = str #if str
436
+ files = get_files
437
+ clearwin(win)
438
+ when ?`.getbyte(0)
439
+ main_menu
440
+ files = get_files
441
+ clearwin(win)
442
+ else
443
+ alert("key #{ch} not known")
444
+ end
445
+ #win.printstr("Pressed #{ch} on #{files[current]} ", 0, 70)
446
+ current = 0 if current < 0
447
+ current = files.size-1 if current >= files.size
448
+ # listing does not refresh files, so if files has changed, you need to refresh
449
+ prevstart = listing(win, path, files, current, prevstart)
450
+ win.wrefresh
451
+ end
452
+
453
+ rescue Object => e
454
+ @window.destroy if @window
455
+ FFI::NCurses.endwin
456
+ puts e
457
+ puts e.backtrace.join("\n")
458
+ ensure
459
+ @window.destroy if @window
460
+ FFI::NCurses.endwin
461
+ puts
462
+ end
data/lib/umbra/box.rb ADDED
@@ -0,0 +1,137 @@
1
+ # ----------------------------------------------------------------------------- #
2
+ # File: box.rb
3
+ # Description: a box or border around a container
4
+ # Author: j kepler http://github.com/mare-imbrium/canis/
5
+ # Date: 2018-04-07
6
+ # License: MIT
7
+ # Last update: 2018-04-08 09:12
8
+ # ----------------------------------------------------------------------------- #
9
+ # box.rb Copyright (C) 2018 j kepler
10
+ module Umbra
11
+ ##
12
+ # A box is a container around one, maybe more, widgets.
13
+ #
14
+ class Box < Widget
15
+ attr_accessor :title
16
+ attr_accessor :width
17
+ attr_accessor :height
18
+ attr_accessor :row_offset # not used yet
19
+ attr_accessor :col_offset # not used yet
20
+ attr_accessor :visible
21
+ attr_reader :widgets
22
+ attr_reader :widget
23
+ attr_accessor :justify # right, left or center TODO
24
+
25
+ def initialize config={}, &block
26
+ @focusable = false
27
+ @visible = true
28
+ super
29
+ @int_height = @height - 2
30
+ @int_width = @width - 2
31
+ @hlines = []
32
+ @vlines = []
33
+ end
34
+ def repaint
35
+ return unless @visible
36
+ print_border @row, @col, @height, @width, @color_pair || CP_BLACK, @attr || NORMAL
37
+ print_title @title
38
+ if !@hlines.empty?
39
+ @hlines.each {|e| hline(e.first, e[1]) }
40
+ end
41
+ # what about asking for painting of widgets
42
+ end
43
+ # should we take in an array and apportion them
44
+ # since we are keeping a row in between as divider, need to adjust heights.
45
+ # Depending on how many components
46
+ def add *w
47
+ @widgets = w
48
+ num = w.size
49
+ wid = @int_width
50
+ ht = (@int_height / num)
51
+ srow = @row + 1
52
+ scol = @col + 1
53
+ w.each_with_index do |e, ix|
54
+ e.width = wid
55
+ e.height = ht
56
+ e.row = srow
57
+ e.col = scol
58
+ srow += ht + 1
59
+ @hlines << [ srow-1, scol ]
60
+ end
61
+ # FIXME there will be one additional hline in the end.
62
+ w[-1].height -= (num-1)
63
+ end
64
+ # this is best used for widgets that can be resized.
65
+ # Prefer not to use for buttons since the looks gets messed (inconsistency betwewn button and highlight).
66
+ # Therefore now, button calculates its own width which means that this program cannot determine what the width is
67
+ # and thus cannot center it.
68
+ def flow *w
69
+ @widgets = w
70
+ num = w.size
71
+ wid = (@int_width / num).floor
72
+ ht = @int_height
73
+ srow = @row + 1
74
+ scol = @col + 1
75
+ w.each_with_index do |e, ix|
76
+ # unfortunately this is messing with button width calculation
77
+ # maybe field and button should have resizable or expandable ?
78
+ e.width = wid unless e.width
79
+ e.height = ht
80
+ e.row = srow
81
+ e.col = scol
82
+ scol += wid + 1
83
+ #@hlines << [ srow-1, scol ]
84
+ end
85
+ # FIXME there will be one additional hline in the end.
86
+ # we added 1 to the scol each time, so decrement
87
+ w[-1].width -= (num-1)
88
+ end
89
+ # use if only one widget will expand into this box
90
+ def fill w
91
+ # should have been nice if I could add widget to form, but then order might get wrong
92
+ w.row = @row + 1
93
+ w.col = @col + 1
94
+ w.width = @width - 2 if w.respond_to? :width
95
+ w.height = @height - 2 if w.respond_to? :height
96
+ @widget = w
97
+ end
98
+ def hline row, col
99
+ return if row >= @row + @height
100
+ $log.debug " hline: #{row} ... #{@row} #{@height} "
101
+ FFI::NCurses.mvwhline( @graphic.pointer, row, col, FFI::NCurses::ACS_HLINE, @width-2)
102
+ end
103
+
104
+ # print a title over the box on zeroth row
105
+ # TODO right or left or center align
106
+ private def print_title stitle
107
+ return unless stitle
108
+ stitle = "| #{stitle} |"
109
+ @justify ||= :center
110
+ col = case @justify
111
+ when :left
112
+ 4
113
+ when :right
114
+ @width -stitle.size - 3
115
+ else
116
+ (@width-stitle.size)/2
117
+ end
118
+ #FFI::NCurses.mvwaddstr(@pointer, 0, col, stitle)
119
+ @graphic.printstring(@row, col, stitle)
120
+ end
121
+
122
+ private def print_border row, col, height, width, color, att=FFI::NCurses::A_NORMAL
123
+ pointer = @graphic.pointer
124
+ FFI::NCurses.wattron(pointer, FFI::NCurses.COLOR_PAIR(color) | att)
125
+ FFI::NCurses.mvwaddch pointer, row, col, FFI::NCurses::ACS_ULCORNER
126
+ FFI::NCurses.mvwhline( pointer, row, col+1, FFI::NCurses::ACS_HLINE, width-2)
127
+ FFI::NCurses.mvwaddch pointer, row, col+width-1, FFI::NCurses::ACS_URCORNER
128
+ FFI::NCurses.mvwvline( pointer, row+1, col, FFI::NCurses::ACS_VLINE, height-2)
129
+
130
+ FFI::NCurses.mvwaddch pointer, row+height-1, col, FFI::NCurses::ACS_LLCORNER
131
+ FFI::NCurses.mvwhline(pointer, row+height-1, col+1, FFI::NCurses::ACS_HLINE, width-2)
132
+ FFI::NCurses.mvwaddch pointer, row+height-1, col+width-1, FFI::NCurses::ACS_LRCORNER
133
+ FFI::NCurses.mvwvline( pointer, row+1, col+width-1, FFI::NCurses::ACS_VLINE, height-2)
134
+ FFI::NCurses.wattroff(pointer, FFI::NCurses.COLOR_PAIR(color) | att)
135
+ end
136
+ end # class
137
+ end # module