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.
- 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
|
+
|