less_curse 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +4 -0
- data/LICENSE +674 -0
- data/README.md +106 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/3col.rb +52 -0
- data/examples/file_browser.rb +40 -0
- data/examples/grid.rb +49 -0
- data/examples/list.rb +46 -0
- data/examples/list_with_display_func.rb +26 -0
- data/examples/long_list.rb +21 -0
- data/examples/popup.rb +32 -0
- data/less_curse.gemspec +26 -0
- data/lib/less_curse.rb +76 -0
- data/lib/less_curse/actions.rb +7 -0
- data/lib/less_curse/geometry.rb +24 -0
- data/lib/less_curse/grid.rb +25 -0
- data/lib/less_curse/null_logger.rb +9 -0
- data/lib/less_curse/renderer.rb +31 -0
- data/lib/less_curse/screen.rb +147 -0
- data/lib/less_curse/version.rb +3 -0
- data/lib/less_curse/widgets.rb +4 -0
- data/lib/less_curse/widgets/base.rb +42 -0
- data/lib/less_curse/widgets/list.rb +108 -0
- data/lib/less_curse/widgets/text_area.rb +42 -0
- data/lib/less_curse/widgets/text_view.rb +15 -0
- metadata +114 -0
@@ -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,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,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
|
+
|