less_curse 0.6.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,7 @@
1
+ module LessCurse
2
+ module Actions
3
+ QUIT = lambda do |arg|
4
+ # do a sane quit.
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ module LessCurse
2
+ module Geometry
3
+ class Point
4
+ attr_accessor :x, :y
5
+ def initialize(x, y)
6
+ @x, @y = x, y
7
+ end
8
+ end
9
+
10
+ class Size
11
+ attr_accessor :width, :height
12
+ def initialize(width, height)
13
+ @width, @height = width, height
14
+ end
15
+ end
16
+
17
+ class Rectangle
18
+ attr_accessor :position, :size
19
+ def initialize(x, y, width, height)
20
+ @position, @size = Point.new(x, y), Size.new(width, height)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ module LessCurse
2
+ class Grid
3
+ attr_accessor :widget_grid
4
+
5
+ def initialize widget_grid=[[]]
6
+ @widget_grid = widget_grid
7
+ end
8
+
9
+ def rows
10
+ @widget_grid
11
+ end
12
+
13
+ def cols_in_row row_nr
14
+ @widget_grid[row_nr].count
15
+ end
16
+
17
+ def widgets
18
+ @widget_grid.flatten
19
+ end
20
+
21
+ def add widget
22
+ @widget_grid.last << widget
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ require 'logger'
2
+
3
+ module LessCurse
4
+ # NullLogger â la hawkins: http://hawkins.io/2013/08/using-the-ruby-logger/
5
+ class NullLogger < Logger
6
+ def initialize(*args) ; end
7
+ def add(*args, &block) ; end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ module LessCurse
2
+ module Renderer
3
+ # Draw box and title in top border of box
4
+ def self.box_with_title window, title
5
+ FFI::NCurses.box window, 0, 0
6
+ FFI::NCurses.mvwaddstr window, 0, 1, title
7
+ end
8
+
9
+ # Draw into lower border of a box
10
+ def self.box_foot window, text
11
+ height,width = FFI::NCurses::getmaxyx(window)
12
+ FFI::NCurses.mvwaddstr window, height - 1, 1, text
13
+ end
14
+
15
+ # Write a line in window
16
+ def self.write_line window, line_number, text
17
+ FFI::NCurses.mvwaddstr window, line_number + 1, 1, text
18
+ end
19
+
20
+ # Switch on boldness depending on condition
21
+ def self.bold_if condition, window
22
+ if condition
23
+ FFI::NCurses.wattron window, FFI::NCurses::A_BOLD
24
+ end
25
+ yield
26
+ if condition
27
+ FFI::NCurses.wattroff window, FFI::NCurses::A_BOLD
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,147 @@
1
+ module LessCurse
2
+ class Screen
3
+ attr_accessor :grid
4
+ attr_accessor :windows
5
+ attr_accessor :size
6
+ attr_accessor :focused_widget
7
+ attr_accessor :header
8
+ attr_accessor :footer
9
+ attr_accessor :popups
10
+
11
+ def initialize
12
+ # Need to initialize screen to access the terminal size
13
+ FFI::NCurses.initscr
14
+
15
+ height,width = FFI::NCurses::getmaxyx(FFI::NCurses::stdscr)
16
+ @size = LessCurse::Geometry::Size.new(width,height)
17
+ @windows = {}
18
+ @focused_widget = nil
19
+ @grid = LessCurse::Grid.new [[]]
20
+ @popups = {}
21
+ end
22
+
23
+ def add widget_or_grid
24
+ if widget_or_grid.is_a? LessCurse::Grid
25
+ @grid = widget_or_grid
26
+ else
27
+ @grid.add widget_or_grid
28
+ end
29
+ recalc_window_sizes
30
+ end
31
+
32
+ def widgets
33
+ @grid.widgets
34
+ end
35
+
36
+ def show
37
+ @focused_widget = widgets.first
38
+ @focused_widget.focus if @focused_widget
39
+ # Note that FFI::NCurses.initscr is called in initialize
40
+ FFI::NCurses.cbreak # can ctrl-c, not waiting for newlines to end input.
41
+ #FFI::NCurses.raw # TODO this overrides cbreak ...
42
+ FFI::NCurses.noecho # do not echo input in win.
43
+ FFI::NCurses.keypad FFI::NCurses::stdscr, true # recognize KEY_UP etc.
44
+ FFI::NCurses.clear
45
+ FFI::NCurses.refresh
46
+ repaint
47
+ end
48
+
49
+ # Repaint the screen and all contained widgets
50
+ def repaint
51
+ FFI::NCurses.refresh
52
+
53
+ # 'Draw' header and/or footer
54
+ if @header && !@header.empty?
55
+ FFI::NCurses::mvaddstr 0, 0, @header
56
+ end
57
+ if @footer && !@footer.empty?
58
+ FFI::NCurses::mvaddstr @size.height - 1, 0, @footer
59
+ end
60
+
61
+ # Let all Widgets redraw themselfes
62
+ widgets.each do |widget|
63
+ window = @windows[widget]
64
+ FFI::NCurses.wclear window
65
+ widget.draw window
66
+ FFI::NCurses.wrefresh window
67
+ end
68
+
69
+ @popups.each do |popup, window|
70
+ FFI::NCurses.wclear window
71
+ popup.draw window
72
+ FFI::NCurses.wrefresh window
73
+ end
74
+ end
75
+
76
+ def show_popup type=:info, content='Popup'
77
+ # Lets take up quarter of screen and draw a boxed window
78
+ popup_widget = LessCurse::Widgets::Button.new title: content.to_s
79
+ popup_widget.focus = true
80
+ area = LessCurse::Geometry::Rectangle.new @size.width / 4,
81
+ @size.height / 4,
82
+ @size.width / 2,
83
+ @size.height / 2
84
+
85
+ @popups[popup_widget] = LessCurse.window area
86
+ end
87
+
88
+ # Focus next element in #widgets
89
+ def focus_next
90
+ cycle_focus(+1)
91
+ end
92
+
93
+ # Focus next element in #widgets
94
+ def focus_previous
95
+ cycle_focus(-1)
96
+ end
97
+
98
+ # Set header text (first, top line of screen)
99
+ def header= new_header
100
+ @header = new_header
101
+ recalc_window_sizes
102
+ end
103
+
104
+ # Set footer text (last, bottom line of screen)
105
+ def footer= new_footer
106
+ @footer = new_footer
107
+ recalc_window_sizes
108
+ end
109
+
110
+ private
111
+
112
+ # Switch focus to step next (or previous) widget
113
+ def cycle_focus step=1
114
+ focused_widget_idx = widgets.index(@focused_widget) || 0
115
+ @focused_widget.unfocus
116
+ @focused_widget = widgets[(focused_widget_idx + step) % widgets.size]
117
+ @focused_widget.focus
118
+ end
119
+
120
+ def recalc_window_sizes
121
+ header_height = (@header.nil? || @header.empty?) ? 0 : 1
122
+ footer_height = (@footer.nil? || @footer.empty?) ? 0 : 1
123
+
124
+ row_height = (@size.height - header_height - footer_height) / @grid.rows.count
125
+
126
+ @grid.rows.each_with_index do |row_widgets, row_idx|
127
+ element_width = @size.width / (row_widgets.size)
128
+ row_y = header_height + row_idx * row_height
129
+ row_widgets.each_with_index do |widget, idx|
130
+ area = LessCurse::Geometry::Rectangle.new element_width * idx, #x
131
+ row_y, #y
132
+ element_width - 1, #width
133
+ row_height #height
134
+ if @windows[widget].nil?
135
+ @windows[widget] = LessCurse.window area
136
+ else
137
+ FFI::NCurses.wresize(@windows[widget],
138
+ area.size.height, area.size.width)
139
+ FFI::NCurses.mvwin(@windows[widget],
140
+ area.position.y, area.position.x)
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,3 @@
1
+ module LessCurse
2
+ VERSION = "0.6.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'less_curse/widgets/base'
2
+ require 'less_curse/widgets/text_area'
3
+ require 'less_curse/widgets/text_view'
4
+ require 'less_curse/widgets/list'
@@ -0,0 +1,42 @@
1
+ module LessCurse
2
+ module Widgets
3
+ class Base
4
+ attr_accessor :data
5
+ attr_accessor :title
6
+ attr_accessor :focus
7
+ attr_accessor :actions
8
+
9
+ def initialize data: nil, title: ""
10
+ @data, @title = data, title
11
+ set_default_actions
12
+ end
13
+
14
+ # Draw portions of screen, probably using ncurses primitives.
15
+ # Expect an already clean/red window.
16
+ def draw(window) ; end
17
+
18
+ # Populate actions with proper code
19
+ def set_default_actions ; end
20
+
21
+ # Handle input or return false if doesnt care
22
+ def handle_input key
23
+ false
24
+ end
25
+
26
+ # Receive Focus
27
+ def focus
28
+ @focus = true
29
+ end
30
+
31
+ # Loose Focus
32
+ def unfocus
33
+ @focus = false
34
+ end
35
+
36
+ # Is focused?
37
+ def focused?
38
+ return @focus
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,108 @@
1
+ module LessCurse
2
+ module Widgets
3
+ class List < Base
4
+ attr_accessor :on_select
5
+ attr_accessor :selected_data
6
+ attr_accessor :top_element_idx
7
+ attr_accessor :display_func
8
+
9
+ def initialize data: [], title: ""
10
+ super(data: data, title: title)
11
+ end
12
+
13
+ @top_element_idx = 0
14
+
15
+ def set_default_actions
16
+ @actions = { FFI::NCurses::KEY_UP => :select_previous,
17
+ FFI::NCurses::KEY_DOWN => :select_next}
18
+ end
19
+
20
+ def draw window
21
+ LessCurse::Renderer::bold_if focused?, window do
22
+ LessCurse::Renderer::box_with_title window, @title
23
+ end
24
+
25
+ # Do we have kind of a display-window?
26
+ visible_data.each_with_index do |d, idx|
27
+ LessCurse::Renderer::bold_if(@selected_data == d, window) do
28
+ if @display_func
29
+ item_text = display_func.call(d)
30
+ else
31
+ item_text = d.to_s
32
+ end
33
+ LessCurse::Renderer::write_line window, idx, item_text
34
+ end
35
+ end
36
+
37
+ if visible_data.size != @data.size
38
+ LessCurse::Renderer::box_foot window, "..."
39
+ end
40
+ end
41
+
42
+ def handle_input key
43
+ action = @actions[key]
44
+ LessCurse::debug_msg "List will execute action: #{action}"
45
+ return false if !action
46
+ send(action)
47
+ end
48
+
49
+ # Select previous data element in list (roll over if nexessary)
50
+ def select_previous
51
+ if @selected_data.nil? && @data.size >= 0
52
+ @selected_data = @data.to_a[0]
53
+ else
54
+ @selected_data = @data.to_a[(selected_data_index - 1) % @data.size]
55
+ end
56
+ recalc_top_index
57
+ if @on_select
58
+ @on_select.call @selected_data
59
+ end
60
+ @selected_data
61
+ end
62
+
63
+ # Select next data element in list (roll over if nexessary)
64
+ def select_next
65
+ if @selected_data.nil? && @data.size >= 0
66
+ @selected_data = @data.to_a[0]
67
+ else
68
+ @selected_data = @data.to_a[(selected_data_index + 1) % @data.size]
69
+ end
70
+ recalc_top_index
71
+ if @on_select
72
+ @on_select.call @selected_data
73
+ end
74
+ @selected_data
75
+ end
76
+
77
+ private
78
+
79
+ # Get index of given element in @data
80
+ def data_index element
81
+ @data.to_a.index(element)
82
+ end
83
+
84
+ # Get index of selected element in @data
85
+ def selected_data_index
86
+ data_index @selected_data
87
+ end
88
+
89
+ def recalc_top_index
90
+ window = LessCurse.screen.windows[self]
91
+ height,width = FFI::NCurses::getmaxyx(window)
92
+ if @data.size <= (height - 2)
93
+ @top_element_idx = 0
94
+ else
95
+ @top_element_idx = [selected_data_index, (@data.size - (height - 2)).abs].min
96
+ end
97
+ end
98
+
99
+ def visible_data
100
+ @top_element_idx ||= 0
101
+ window = LessCurse.screen.windows[self]
102
+ height,width = FFI::NCurses::getmaxyx(window)
103
+ # -2: border, -1: 0-based indexing
104
+ @data.to_a[@top_element_idx..(@top_element_idx + height - 2 - 1)]
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,42 @@
1
+ module LessCurse
2
+ module Widgets
3
+ class TextArea < Base
4
+ def draw window
5
+ LessCurse::Renderer::bold_if(focused?, window) do
6
+ LessCurse::Renderer::box_with_title window, @title
7
+ end
8
+ FFI::NCurses.wmove window, 1, 1
9
+ @data.to_s.split("\n").each_with_index do |line, idx|
10
+ FFI::NCurses.mvwaddstr window, idx + 1, 1, line
11
+ end
12
+ end
13
+
14
+ def handle_input key
15
+ # Its a PITA to redo all the readline loveliness, but it gets us right
16
+ # into doing things. Would be cool to have moving cursor on ENTER
17
+ if key == FFI::NCurses::KEY_BACKSPACE
18
+ @data = @data[0..-2]
19
+ else
20
+ # Handle out of range stuff
21
+ @data += key.chr rescue false
22
+ end
23
+ end
24
+
25
+ def focus
26
+ @focus = true
27
+ ## Initial experiments where done with cbreak and echo
28
+ #FFI::NCurses.echo
29
+ #FFI::NCurses.nocbreak # can ctrl-c, not waiting for newlines to end input.
30
+ ##@data = ... but master.refresh afterwards ..
31
+ #window = LessCurse.screen.windows[self]
32
+ ##FFI::NCurses::mvwgetstr window, 4, 3, @data
33
+ ##@data += FFI::NCurses::wget_wstr window
34
+ #@data += FFI::NCurses::wgetch(window).chr
35
+ ## from ffi/ncurses getkey example
36
+ # #buffer = FFI::Buffer.new(FFI::NCurses.find_type(:wint_t))
37
+ refresh
38
+ end
39
+ end
40
+ end
41
+ end
42
+