less_curse 0.6.0

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