ncumbra 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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