luck 0.0.0 → 0.1.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.
- data/VERSION +1 -1
- data/lib/luck/alert.rb +36 -0
- data/lib/luck/ansi.rb +222 -0
- data/lib/luck/button.rb +35 -0
- data/lib/luck/control.rb +4 -0
- data/lib/luck/display.rb +197 -113
- data/lib/luck/listbox.rb +139 -5
- data/lib/luck/pane.rb +70 -14
- data/lib/luck/textbox.rb +56 -11
- data/lib/luck.rb +6 -3
- data/luck.gemspec +57 -0
- metadata +6 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.1.0
|
data/lib/luck/alert.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Luck
|
2
|
+
class Alert < Pane
|
3
|
+
attr_accessor :width, :height, :handler
|
4
|
+
|
5
|
+
def initialize display, width, height, title, controls={}, &blck
|
6
|
+
super display, nil, nil, nil, nil, title, controls, &blck
|
7
|
+
|
8
|
+
@width, @height = width, height
|
9
|
+
end
|
10
|
+
|
11
|
+
def x1
|
12
|
+
(@display.width / 2).to_i - (@width / 2).to_i
|
13
|
+
end
|
14
|
+
def y1
|
15
|
+
(@display.height / 2).to_i - (@height / 2).to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
def x2
|
19
|
+
x1 + @width
|
20
|
+
end
|
21
|
+
def y2
|
22
|
+
y1 + @height
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_dismiss &blck
|
26
|
+
@handler = blck
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle_char char
|
30
|
+
if char == "\n"
|
31
|
+
@handler.call @text if @handler
|
32
|
+
end
|
33
|
+
#redraw
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/luck/ansi.rb
ADDED
@@ -0,0 +1,222 @@
|
|
1
|
+
module Luck
|
2
|
+
class ANSIDriver
|
3
|
+
attr_reader :width, :height
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
prepare_modes
|
7
|
+
|
8
|
+
@height, @width = terminal_size
|
9
|
+
end
|
10
|
+
|
11
|
+
def flush
|
12
|
+
$stdout.flush
|
13
|
+
end
|
14
|
+
|
15
|
+
def put row, col, text
|
16
|
+
text.each_line do |line|
|
17
|
+
print "\e[#{row.to_i};#{col.to_i}H#{line}"
|
18
|
+
row += 1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def color codes
|
23
|
+
"\e[#{codes}m"
|
24
|
+
end
|
25
|
+
|
26
|
+
def cursor=(show)
|
27
|
+
print "\e[?25" + (show ? 'h' : 'l')
|
28
|
+
end
|
29
|
+
|
30
|
+
def linedrawing=(toggle)
|
31
|
+
print(toggle ? "\x0E\e)0" : "\x0F")
|
32
|
+
end
|
33
|
+
|
34
|
+
def resized?
|
35
|
+
size = terminal_size
|
36
|
+
|
37
|
+
return false if [@height, @width] == size
|
38
|
+
|
39
|
+
@height, @width = size
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def clear; print "\e[H"; end
|
44
|
+
def cursor_to_home; print "\e[J"; end
|
45
|
+
|
46
|
+
def save_cursor; print "\e[s"; end
|
47
|
+
def restore_cursor; print "\e[u"; end
|
48
|
+
def set_cursor row, col; put row, col, "\e[s"; end
|
49
|
+
|
50
|
+
def set_title title; print "\e]2;#{title}\007"; end
|
51
|
+
|
52
|
+
#~ BUTTONS = {
|
53
|
+
#~ 0 => :left,
|
54
|
+
#~ 1 => :middle,
|
55
|
+
#~ 2 => :right,
|
56
|
+
#~ 3 => :release,
|
57
|
+
#~ 64 => :scrollup,
|
58
|
+
#~ 65 => :scrolldown
|
59
|
+
#~ }
|
60
|
+
#~
|
61
|
+
#~ # alt-anything => \e*KEY* (same as Esc, key)
|
62
|
+
#~ # alt-[ would become \e[ which is an ANSI escape
|
63
|
+
#~ #
|
64
|
+
#~ # ctrl-stuff becomes weird stuff, i.e. ctrl-space = \x00, ctrl-a = \x01, ctrl-b = \x02
|
65
|
+
#~ #
|
66
|
+
#~ # super is not sent?
|
67
|
+
#~ def handle_stdin
|
68
|
+
#~ @escapes ||= 0
|
69
|
+
#~ @ebuff ||= ''
|
70
|
+
#~
|
71
|
+
#~ $stdin.read_nonblock(1024).each_char do |chr|
|
72
|
+
#~
|
73
|
+
#~ if @escapes == 0
|
74
|
+
#~ if chr == "\e"
|
75
|
+
#~ @escapes = 1
|
76
|
+
#~ elsif chr == "\t"
|
77
|
+
#~ cycle_controls
|
78
|
+
#~ elsif chr == "\177"
|
79
|
+
#~ route_key :backspace
|
80
|
+
#~ else
|
81
|
+
#~ route_key chr
|
82
|
+
#~ end
|
83
|
+
#~
|
84
|
+
#~ elsif @escapes == 1 && chr == '['
|
85
|
+
#~ @escapes = 2
|
86
|
+
#~ elsif @escapes == 1 && chr == 'O'
|
87
|
+
#~ @escapes = 5
|
88
|
+
#~
|
89
|
+
#~ elsif @escapes == 2
|
90
|
+
#~ if chr == 'A'
|
91
|
+
#~ route_key :up
|
92
|
+
#~ elsif chr == 'B'
|
93
|
+
#~ route_key :down
|
94
|
+
#~ elsif chr == 'C'
|
95
|
+
#~ route_key :right
|
96
|
+
#~ elsif chr == 'D'
|
97
|
+
#~ route_key :left
|
98
|
+
#~ elsif chr == 'E'
|
99
|
+
#~ route_key :center
|
100
|
+
#~ elsif chr == 'Z'
|
101
|
+
#~ cycle_controls_back
|
102
|
+
#~ else
|
103
|
+
#~ @ebuff = chr
|
104
|
+
#~ @escapes = 3
|
105
|
+
#~ end
|
106
|
+
#~ @escapes = 0 if @escapes == 2
|
107
|
+
#~
|
108
|
+
#~ elsif @escapes == 3
|
109
|
+
#~ if chr == '~' && @ebuff.to_i.to_s == @ebuff
|
110
|
+
#~ route_key case @ebuff.to_i
|
111
|
+
#~ when 2; :insert
|
112
|
+
#~ when 3; :delete
|
113
|
+
#~ when 5; :pageup
|
114
|
+
#~ when 6; :pagedown
|
115
|
+
#~ when 15; :f5
|
116
|
+
#~ when 17; :f6
|
117
|
+
#~ when 18; :f7
|
118
|
+
#~ when 19; :f8
|
119
|
+
#~ when 20; :f9
|
120
|
+
#~ when 24; :f12
|
121
|
+
#~ else; raise @ebuff.inspect
|
122
|
+
#~ end
|
123
|
+
#~ elsif @ebuff[0,1] == 'M' && @ebuff.size == 3
|
124
|
+
#~ @ebuff += chr
|
125
|
+
#~ info, x, y = @ebuff.unpack('xCCC').map{|i| i - 32}
|
126
|
+
#~ modifiers = []
|
127
|
+
#~ modifiers << :shift if info & 4 == 4
|
128
|
+
#~ modifiers << :meta if info & 8 == 8
|
129
|
+
#~ modifiers << :control if info & 16 == 16
|
130
|
+
#~ pane = pane_at(x, y)
|
131
|
+
#~
|
132
|
+
#~ unless modal && modal != pane
|
133
|
+
#~ pane.handle_click BUTTONS[info & 71], modifiers, x, y if pane
|
134
|
+
#~ end
|
135
|
+
#~ elsif @ebuff.size > 10
|
136
|
+
#~ raise "long ebuff #{@ebuff.inspect} - #{chr.inspect}"
|
137
|
+
#~ else
|
138
|
+
#~ @ebuff += chr
|
139
|
+
#~ @escapes = 4
|
140
|
+
#~ end
|
141
|
+
#~ @escapes = 0 if @escapes == 3
|
142
|
+
#~ @escapes = 3 if @escapes == 4
|
143
|
+
#~ @ebuff = '' if @escapes == 0
|
144
|
+
#~
|
145
|
+
#~ elsif @escapes == 5
|
146
|
+
#~ if chr == 'H'
|
147
|
+
#~ route_key :home
|
148
|
+
#~ elsif chr == 'F'
|
149
|
+
#~ route_key :end
|
150
|
+
#~ elsif chr == 'Q'
|
151
|
+
#~ route_key :f2
|
152
|
+
#~ elsif chr == 'R'
|
153
|
+
#~ route_key :f3
|
154
|
+
#~ elsif chr == 'S'
|
155
|
+
#~ route_key :f4
|
156
|
+
#~ else
|
157
|
+
#~ raise "escape 5 #{chr.inspect}"
|
158
|
+
#~ end
|
159
|
+
#~ @escapes = 0
|
160
|
+
#~
|
161
|
+
#~ else
|
162
|
+
#~ @escapes = 0
|
163
|
+
#~ end
|
164
|
+
#~ end
|
165
|
+
#~
|
166
|
+
#~ $stdout.flush
|
167
|
+
#~
|
168
|
+
#~ rescue Errno::EAGAIN
|
169
|
+
#~ rescue EOFError
|
170
|
+
#~ end
|
171
|
+
|
172
|
+
|
173
|
+
######################################
|
174
|
+
# this is all copied from cashreg :P #
|
175
|
+
######################################
|
176
|
+
|
177
|
+
# yay grep
|
178
|
+
TIOCGWINSZ = 0x5413
|
179
|
+
TCGETS = 0x5401
|
180
|
+
TCSETS = 0x5402
|
181
|
+
ECHO = 8 # 0000010
|
182
|
+
ICANON = 2 # 0000002
|
183
|
+
|
184
|
+
# thanks google for all of this
|
185
|
+
def terminal_size
|
186
|
+
rows, cols = 25, 80
|
187
|
+
buf = [0, 0, 0, 0].pack("SSSS")
|
188
|
+
if $stdout.ioctl(TIOCGWINSZ, buf) >= 0 then
|
189
|
+
rows, cols, row_pixels, col_pixels = buf.unpack("SSSS")
|
190
|
+
end
|
191
|
+
return [rows, cols]
|
192
|
+
end
|
193
|
+
|
194
|
+
# had to convert these from C... fun
|
195
|
+
def prepare_modes
|
196
|
+
buf = [0, 0, 0, 0, 0, 0, ''].pack("IIIICCA*")
|
197
|
+
$stdout.ioctl(TCGETS, buf)
|
198
|
+
@old_modes = buf.unpack("IIIICCA*")
|
199
|
+
new_modes = @old_modes.clone
|
200
|
+
new_modes[3] &= ~ECHO # echo off
|
201
|
+
new_modes[3] &= ~ICANON # one char @ a time
|
202
|
+
$stdout.ioctl(TCSETS, new_modes.pack("IIIICCA*"))
|
203
|
+
print "\e[2J" # clear screen
|
204
|
+
print "\e[H" # go home
|
205
|
+
print "\e[?47h" # kick xterm into the alt screen
|
206
|
+
print "\e[?1000h" # kindly ask for mouse positions to make up for it
|
207
|
+
self.cursor = false
|
208
|
+
flush
|
209
|
+
end
|
210
|
+
|
211
|
+
def undo_modes # restore previous terminal mode
|
212
|
+
$stdout.ioctl(TCSETS, @old_modes.pack("IIIICCA*"))
|
213
|
+
print "\e[2J" # clear screen
|
214
|
+
print "\e[H" # go home
|
215
|
+
print "\e[?47l" # kick xterm back into the normal screen
|
216
|
+
print "\e[?1000l" # turn off mouse reporting
|
217
|
+
self.linedrawing = false
|
218
|
+
self.cursor = true # show the mouse
|
219
|
+
flush
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
data/lib/luck/button.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Luck
|
2
|
+
class Button < Label
|
3
|
+
attr_accessor :handler
|
4
|
+
|
5
|
+
def on_submit &blck
|
6
|
+
@handler = blck
|
7
|
+
end
|
8
|
+
|
9
|
+
def handle_char char
|
10
|
+
if char == "\n"
|
11
|
+
handler.call self, @text if handler
|
12
|
+
end
|
13
|
+
#redraw
|
14
|
+
end
|
15
|
+
|
16
|
+
def handle_click button, modifiers, x, y
|
17
|
+
handler.call self, @text if button == :left && handler
|
18
|
+
end
|
19
|
+
|
20
|
+
def text
|
21
|
+
"[ #{@text} ]"
|
22
|
+
end
|
23
|
+
|
24
|
+
def handler
|
25
|
+
@handler || @pane.handler
|
26
|
+
end
|
27
|
+
|
28
|
+
def redraw
|
29
|
+
@display.driver.cursor = false
|
30
|
+
print @display.color(@display.active_control == self ? '1;34' : '0;36')
|
31
|
+
super
|
32
|
+
print @display.color('0')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/luck/control.rb
CHANGED
data/lib/luck/display.rb
CHANGED
@@ -1,160 +1,244 @@
|
|
1
1
|
module Luck
|
2
2
|
class Display
|
3
|
-
attr_accessor :
|
3
|
+
attr_accessor :driver, :client, :panes, :dirty, :active_pane, :active_control, :modal
|
4
4
|
|
5
5
|
def initialize client
|
6
6
|
@client = client
|
7
7
|
@panes = {}
|
8
8
|
@dirty = true
|
9
|
-
prepare_modes
|
10
9
|
|
11
|
-
|
12
|
-
@width = size[1]
|
13
|
-
@height = size[0]
|
10
|
+
@driver = ANSIDriver.new
|
14
11
|
end
|
15
12
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
13
|
+
def width; @driver.width; end
|
14
|
+
def height; @driver.height; end
|
15
|
+
|
16
|
+
def place x,y,t; @driver.put x,y,t; end
|
17
|
+
def color c; @driver.color c; end
|
19
18
|
|
20
|
-
def
|
21
|
-
|
19
|
+
def close
|
20
|
+
@driver.undo_modes
|
22
21
|
end
|
23
|
-
|
24
|
-
|
22
|
+
|
23
|
+
def pane name, *args, &blck
|
24
|
+
@panes[name] = Pane.new(self, *args, &blck)
|
25
25
|
end
|
26
26
|
|
27
|
-
def
|
28
|
-
|
29
|
-
print "\e[?25l" unless show
|
30
|
-
$stdout.flush
|
27
|
+
def alert name, *args, &blck
|
28
|
+
@panes[name] = Alert.new(self, *args, &blck)
|
31
29
|
end
|
32
30
|
|
33
31
|
def dirty! pane=nil
|
34
|
-
if pane
|
35
|
-
@dirty
|
36
|
-
|
37
|
-
@dirty = [pane]
|
38
|
-
elsif !pane
|
32
|
+
if pane
|
33
|
+
@panes[pane].dirty!
|
34
|
+
else
|
39
35
|
@dirty = true
|
40
36
|
end
|
41
37
|
end
|
42
38
|
|
39
|
+
def [] pane, control=nil
|
40
|
+
if control
|
41
|
+
@panes[pane].controls[control]
|
42
|
+
else
|
43
|
+
@panes[pane]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def focus *path
|
48
|
+
self.active_control = self[*path]
|
49
|
+
end
|
50
|
+
|
43
51
|
def handle
|
44
52
|
handle_stdin
|
45
53
|
|
46
|
-
|
47
|
-
if @width != size[1] || @height != size[0]
|
48
|
-
@width = size[1]
|
49
|
-
@height = size[0]
|
54
|
+
if @driver.resized?
|
50
55
|
dirty!
|
51
56
|
end
|
52
57
|
|
53
|
-
|
54
|
-
|
55
|
-
|
58
|
+
if @dirty
|
59
|
+
redraw
|
60
|
+
@dirty = false
|
61
|
+
else
|
62
|
+
panes = @panes.values.select {|pane| pane.dirty? && pane.visible? }
|
63
|
+
redraw panes if panes.any?
|
64
|
+
end
|
65
|
+
|
66
|
+
@panes.each_value {|pane| pane.dirty = false }
|
56
67
|
end
|
57
68
|
|
58
|
-
def redraw panes=
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
panes = @panes.keys if panes == true
|
63
|
-
|
64
|
-
panes.each do |key|
|
65
|
-
@panes[key].redraw
|
69
|
+
def redraw panes=nil
|
70
|
+
unless panes
|
71
|
+
@driver.clear
|
72
|
+
@driver.cursor_to_home
|
66
73
|
end
|
67
|
-
print "\e[u"
|
68
74
|
|
69
|
-
|
75
|
+
panes ||= @panes.values
|
76
|
+
panes.each {|pane| pane.redraw if pane.visible? }
|
77
|
+
modal.redraw if modal
|
78
|
+
@panes.each_value {|pane| pane.draw_title if pane.visible? }
|
79
|
+
|
80
|
+
@driver.restore_cursor
|
81
|
+
@driver.flush
|
82
|
+
end
|
83
|
+
|
84
|
+
def active_control= control
|
85
|
+
@active_pane = control.pane
|
86
|
+
@active_control = control
|
87
|
+
end
|
88
|
+
|
89
|
+
def cycle_controls
|
90
|
+
index = @active_pane.controls.keys.index @active_pane.controls.key(@active_control)
|
91
|
+
begin
|
92
|
+
index += 1
|
93
|
+
index = 0 if index >= @active_pane.controls.size
|
94
|
+
end until @active_pane.controls[@active_pane.controls.keys[index]].respond_to? :handle_char
|
95
|
+
old = @active_control
|
96
|
+
@active_control = @active_pane.controls[@active_pane.controls.keys[index]]
|
97
|
+
old.redraw
|
98
|
+
@active_control.redraw
|
70
99
|
end
|
71
100
|
|
101
|
+
def cycle_controls_back
|
102
|
+
index = @active_pane.controls.keys.index @active_pane.controls.key(@active_control)
|
103
|
+
begin
|
104
|
+
index -= 1
|
105
|
+
index = @active_pane.controls.size - 1 if index < 0
|
106
|
+
end until @active_pane.controls[@active_pane.controls.keys[index]].respond_to? :handle_char
|
107
|
+
old = @active_control
|
108
|
+
@active_control = @active_pane.controls[@active_pane.controls.keys[index]]
|
109
|
+
old.redraw
|
110
|
+
@active_control.redraw
|
111
|
+
end
|
112
|
+
|
113
|
+
BUTTONS = {
|
114
|
+
0 => :left,
|
115
|
+
1 => :middle,
|
116
|
+
2 => :right,
|
117
|
+
3 => :release,
|
118
|
+
64 => :scrollup,
|
119
|
+
65 => :scrolldown
|
120
|
+
}
|
121
|
+
|
122
|
+
# alt-anything => \e*KEY* (same as Esc, key)
|
123
|
+
# alt-[ would become \e[ which is an ANSI escape
|
124
|
+
#
|
125
|
+
# ctrl-stuff becomes weird stuff, i.e. ctrl-space = \x00, ctrl-a = \x01, ctrl-b = \x02
|
126
|
+
#
|
127
|
+
# super is not sent?
|
72
128
|
def handle_stdin
|
129
|
+
@escapes ||= 0
|
130
|
+
@ebuff ||= ''
|
131
|
+
|
73
132
|
$stdin.read_nonblock(1024).each_char do |chr|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
133
|
+
|
134
|
+
if @escapes == 0
|
135
|
+
if chr == "\e"
|
136
|
+
@escapes = 1
|
137
|
+
elsif chr == "\t"
|
138
|
+
cycle_controls
|
139
|
+
elsif chr == "\177"
|
140
|
+
route_key :backspace
|
141
|
+
else
|
142
|
+
route_key chr
|
143
|
+
end
|
144
|
+
|
145
|
+
elsif @escapes == 1 && chr == '['
|
146
|
+
@escapes = 2
|
147
|
+
elsif @escapes == 1 && chr == 'O'
|
148
|
+
@escapes = 5
|
149
|
+
|
150
|
+
elsif @escapes == 2
|
151
|
+
if chr == 'A'
|
152
|
+
route_key :up
|
153
|
+
elsif chr == 'B'
|
154
|
+
route_key :down
|
155
|
+
elsif chr == 'C'
|
156
|
+
route_key :right
|
157
|
+
elsif chr == 'D'
|
158
|
+
route_key :left
|
159
|
+
elsif chr == 'E'
|
160
|
+
route_key :center
|
161
|
+
elsif chr == 'Z'
|
162
|
+
cycle_controls_back
|
163
|
+
else
|
164
|
+
@ebuff = chr
|
165
|
+
@escapes = 3
|
166
|
+
end
|
167
|
+
@escapes = 0 if @escapes == 2
|
168
|
+
|
169
|
+
elsif @escapes == 3
|
170
|
+
if chr == '~' && @ebuff.to_i.to_s == @ebuff
|
171
|
+
route_key case @ebuff.to_i
|
172
|
+
when 2; :insert
|
173
|
+
when 3; :delete
|
174
|
+
when 5; :pageup
|
175
|
+
when 6; :pagedown
|
176
|
+
when 15; :f5
|
177
|
+
when 17; :f6
|
178
|
+
when 18; :f7
|
179
|
+
when 19; :f8
|
180
|
+
when 20; :f9
|
181
|
+
when 24; :f12
|
182
|
+
else; raise @ebuff.inspect
|
183
|
+
end
|
184
|
+
elsif @ebuff[0,1] == 'M' && @ebuff.size == 3
|
185
|
+
@ebuff += chr
|
186
|
+
info, x, y = @ebuff.unpack('xCCC').map{|i| i - 32}
|
187
|
+
modifiers = []
|
188
|
+
modifiers << :shift if info & 4 == 4
|
189
|
+
modifiers << :meta if info & 8 == 8
|
190
|
+
modifiers << :control if info & 16 == 16
|
191
|
+
pane = pane_at(x, y)
|
192
|
+
|
193
|
+
unless modal && modal != pane
|
194
|
+
pane.handle_click BUTTONS[info & 71], modifiers, x, y if pane
|
195
|
+
end
|
196
|
+
elsif @ebuff.size > 10
|
197
|
+
raise "long ebuff #{@ebuff.inspect} - #{chr.inspect}"
|
198
|
+
else
|
199
|
+
@ebuff += chr
|
200
|
+
@escapes = 4
|
201
|
+
end
|
202
|
+
@escapes = 0 if @escapes == 3
|
203
|
+
@escapes = 3 if @escapes == 4
|
204
|
+
@ebuff = '' if @escapes == 0
|
205
|
+
|
206
|
+
elsif @escapes == 5
|
207
|
+
if chr == 'H'
|
208
|
+
route_key :home
|
209
|
+
elsif chr == 'F'
|
210
|
+
route_key :end
|
211
|
+
elsif chr == 'Q'
|
212
|
+
route_key :f2
|
213
|
+
elsif chr == 'R'
|
214
|
+
route_key :f3
|
215
|
+
elsif chr == 'S'
|
216
|
+
route_key :f4
|
217
|
+
else
|
218
|
+
raise "escape 5 #{chr.inspect}"
|
219
|
+
end
|
220
|
+
@escapes = 0
|
221
|
+
|
222
|
+
else
|
223
|
+
@escapes = 0
|
224
|
+
end
|
113
225
|
end
|
114
226
|
|
115
|
-
|
116
|
-
$stdout.flush
|
227
|
+
@driver.flush
|
117
228
|
|
118
229
|
rescue Errno::EAGAIN
|
119
230
|
rescue EOFError
|
120
231
|
end
|
121
232
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
# yay grep
|
127
|
-
TIOCGWINSZ = 0x5413
|
128
|
-
TCGETS = 0x5401
|
129
|
-
TCSETS = 0x5402
|
130
|
-
ECHO = 8 # 0000010
|
131
|
-
ICANON = 2 # 0000002
|
132
|
-
|
133
|
-
# thanks google for all of this
|
134
|
-
def terminal_size
|
135
|
-
rows, cols = 25, 80
|
136
|
-
buf = [0, 0, 0, 0].pack("SSSS")
|
137
|
-
if $stdout.ioctl(TIOCGWINSZ, buf) >= 0 then
|
138
|
-
rows, cols, row_pixels, col_pixels = buf.unpack("SSSS")
|
233
|
+
def pane_at x, y
|
234
|
+
@panes.values.reverse.each do |pane|
|
235
|
+
return pane if pane.visible? && (pane.x1..pane.x2).include?(x) && (pane.y1..pane.y2).include?(y)
|
139
236
|
end
|
140
|
-
|
141
|
-
end
|
142
|
-
|
143
|
-
# had to convert these from C... fun
|
144
|
-
def prepare_modes
|
145
|
-
buf = [0, 0, 0, 0, 0, 0, ''].pack("IIIICCA*")
|
146
|
-
$stdout.ioctl(TCGETS, buf)
|
147
|
-
@old_modes = buf.unpack("IIIICCA*")
|
148
|
-
new_modes = @old_modes.clone
|
149
|
-
new_modes[3] &= ~ECHO # echo off
|
150
|
-
new_modes[3] &= ~ICANON # one char @ a time
|
151
|
-
$stdout.ioctl(TCSETS, new_modes.pack("IIIICCA*"))
|
152
|
-
self.cursor = false
|
237
|
+
nil
|
153
238
|
end
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
self.cursor = true # show the mouse
|
239
|
+
|
240
|
+
def route_key chr
|
241
|
+
@active_control.handle_char chr if @active_control
|
158
242
|
end
|
159
243
|
end
|
160
244
|
end
|
data/lib/luck/listbox.rb
CHANGED
@@ -1,10 +1,21 @@
|
|
1
1
|
module Luck
|
2
2
|
class ListBox < Control
|
3
|
-
attr_accessor :data, :numbering, :hanging_indent
|
3
|
+
attr_accessor :data, :numbering, :hanging_indent, :offset, :index, :lastclick
|
4
|
+
|
5
|
+
def on_submit &blck
|
6
|
+
@handler = blck
|
7
|
+
end
|
8
|
+
|
9
|
+
def handler
|
10
|
+
@handler || @pane.handler
|
11
|
+
end
|
4
12
|
|
5
13
|
def initialize *args
|
6
14
|
@data = []
|
7
15
|
@hanging_indent = 0
|
16
|
+
@offset = 0
|
17
|
+
@index = 0
|
18
|
+
@lastclick = Time.at 0
|
8
19
|
super
|
9
20
|
end
|
10
21
|
|
@@ -13,21 +24,144 @@ class ListBox < Control
|
|
13
24
|
@hanging_indent = 4
|
14
25
|
end
|
15
26
|
|
27
|
+
def fit_data offset=nil
|
28
|
+
lines = 0
|
29
|
+
items = 0
|
30
|
+
|
31
|
+
data[offset || @offset, height].each do |line|
|
32
|
+
_height = line_height(line)
|
33
|
+
if _height + lines > height
|
34
|
+
break
|
35
|
+
else
|
36
|
+
lines += _height
|
37
|
+
items += 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
[lines, items]
|
42
|
+
end
|
43
|
+
|
44
|
+
def line_height line
|
45
|
+
lines = 0
|
46
|
+
|
47
|
+
line = line.to_s
|
48
|
+
line = "##. #{line}" if @numbering
|
49
|
+
length = line.size
|
50
|
+
return 1 if length < 1
|
51
|
+
offset = 0
|
52
|
+
while offset < length
|
53
|
+
lines += 1
|
54
|
+
offset += width - ((offset > 0) ? @hanging_indent : 0)
|
55
|
+
end
|
56
|
+
|
57
|
+
lines
|
58
|
+
end
|
59
|
+
|
60
|
+
def fit_data_back offset=nil
|
61
|
+
lines = -1
|
62
|
+
items = 0
|
63
|
+
|
64
|
+
offset ||= @offset
|
65
|
+
data[([offset - height,0].max)..([offset,0].max)].reverse.each do |line|
|
66
|
+
_height = line_height(line)
|
67
|
+
if _height + lines >= height
|
68
|
+
break
|
69
|
+
else
|
70
|
+
lines += _height
|
71
|
+
items += 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
[lines, items]
|
76
|
+
end
|
77
|
+
|
16
78
|
def redraw
|
79
|
+
@display.driver.cursor = false if @display.active_control == self
|
80
|
+
|
17
81
|
row = y1
|
18
|
-
|
82
|
+
return unless data
|
83
|
+
data[@offset, height].each_with_index do |line, index|
|
19
84
|
line = line.to_s
|
20
|
-
|
85
|
+
number = index + @offset + 1
|
86
|
+
line = "#{number.to_s.rjust 2}. #{line}" if @numbering
|
87
|
+
print @display.color '1;34' if number == @index + 1
|
21
88
|
length = line.size
|
22
89
|
offset = 0
|
23
90
|
while offset < length || offset == 0
|
24
|
-
@display.place row, x1
|
91
|
+
@display.place row, x1, ((' ' * ((offset > 0) ? @hanging_indent : 0)) + line[offset, width - ((offset > 0) ? @hanging_indent : 0)]).ljust(width, ' ')
|
25
92
|
row += 1
|
26
93
|
offset += width - ((offset > 0) ? @hanging_indent : 0)
|
27
|
-
|
94
|
+
if row >= y2
|
95
|
+
print @display.color '0' if number == @index + 1
|
96
|
+
break
|
97
|
+
end
|
28
98
|
end
|
99
|
+
print @display.color '0' if number == @index + 1
|
29
100
|
break if row >= y2
|
30
101
|
end
|
102
|
+
|
103
|
+
until row >= y2
|
104
|
+
@display.place row, x1, ' ' * width
|
105
|
+
row += 1
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def handle_char char
|
110
|
+
if char == "\n"
|
111
|
+
handler.call self, @data[@index] if handler
|
112
|
+
elsif char == :up
|
113
|
+
@index -= 1 if @index > 0
|
114
|
+
@offset -= 1 if @offset > @index
|
115
|
+
elsif char == :down
|
116
|
+
if @index < @data.size - 1
|
117
|
+
@index += 1
|
118
|
+
@offset += 1 if @offset + fit_data[1] <= @index
|
119
|
+
end
|
120
|
+
elsif char == :pageup
|
121
|
+
@offset = [0, @offset - fit_data_back[1]].max
|
122
|
+
@index = [@offset + fit_data[1] - 1, @index].min
|
123
|
+
elsif char == :pagedown
|
124
|
+
@offset = [@data.size - 1, @offset + fit_data[1]].min
|
125
|
+
@offset = [@offset, @data.size - fit_data_back(@data.size)[1]].min
|
126
|
+
@index = [@offset, @index].max
|
127
|
+
end
|
128
|
+
redraw
|
129
|
+
end
|
130
|
+
|
131
|
+
def handle_click button, modifiers, x, y
|
132
|
+
if button == :scrollup
|
133
|
+
@offset -= 1 if @offset > 0
|
134
|
+
@index = [@offset + fit_data[1] - 1, @index].min
|
135
|
+
elsif button == :scrolldown
|
136
|
+
@offset += 1 if @offset < (@data.size - fit_data_back(@data.size)[1])
|
137
|
+
@index = [@offset, @index].max
|
138
|
+
|
139
|
+
elsif button == :left
|
140
|
+
if Time.now - @lastclick < 0.5
|
141
|
+
button = :double
|
142
|
+
end
|
143
|
+
@lastclick = Time.now
|
144
|
+
|
145
|
+
previous = @index
|
146
|
+
|
147
|
+
row = y1
|
148
|
+
data[@offset, height].each_with_index do |line, index|
|
149
|
+
_height = line_height(line)
|
150
|
+
if _height + row > height
|
151
|
+
break
|
152
|
+
else
|
153
|
+
row += _height
|
154
|
+
end
|
155
|
+
if row > y
|
156
|
+
@index = index + @offset
|
157
|
+
break
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
handler.call self, @data[@index] if button == :double && previous == @index && handler
|
162
|
+
|
163
|
+
end
|
164
|
+
redraw
|
31
165
|
end
|
32
166
|
end
|
33
167
|
end
|
data/lib/luck/pane.rb
CHANGED
@@ -1,16 +1,45 @@
|
|
1
1
|
module Luck
|
2
2
|
class Pane
|
3
|
-
attr_accessor :display, :x1, :y1, :x2, :y2, :title, :controls
|
3
|
+
attr_accessor :display, :x1, :y1, :x2, :y2, :title, :controls, :handler, :dirty, :visible
|
4
4
|
|
5
5
|
def initialize display, x1, y1, x2, y2, title, controls={}, &blck
|
6
6
|
@display = display
|
7
7
|
@x1, @y1 = x1, y1
|
8
8
|
@x2, @y2 = x2, y2
|
9
9
|
@title, @controls = title, controls
|
10
|
+
@dirty = false
|
11
|
+
@visible = true
|
10
12
|
|
11
13
|
instance_eval &blck if blck
|
12
14
|
end
|
13
15
|
|
16
|
+
def [] control
|
17
|
+
@controls[control]
|
18
|
+
end
|
19
|
+
|
20
|
+
alias dirty? dirty
|
21
|
+
def dirty!
|
22
|
+
@dirty = true
|
23
|
+
end
|
24
|
+
|
25
|
+
alias visible? visible
|
26
|
+
def show!; @visible = true; end
|
27
|
+
def hide!; @visible = false; end
|
28
|
+
|
29
|
+
def yank_values
|
30
|
+
vals = {}
|
31
|
+
@controls.each_pair do |key, control|
|
32
|
+
next unless control.respond_to? :value
|
33
|
+
vals[key] = control.value
|
34
|
+
control.value = ''
|
35
|
+
end
|
36
|
+
vals
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_submit &blck
|
40
|
+
@handler = blck
|
41
|
+
end
|
42
|
+
|
14
43
|
def control name, type, *args, &blck
|
15
44
|
@controls[name] = type.new(self, *args, &blck)
|
16
45
|
end
|
@@ -39,6 +68,7 @@ class Pane
|
|
39
68
|
def redraw
|
40
69
|
draw_frame
|
41
70
|
draw_contents
|
71
|
+
draw_title
|
42
72
|
end
|
43
73
|
|
44
74
|
def draw_contents
|
@@ -47,29 +77,55 @@ class Pane
|
|
47
77
|
end
|
48
78
|
end
|
49
79
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
80
|
+
def control_at x, y
|
81
|
+
@controls.values.each do |control|
|
82
|
+
return control if (control.x1..control.x2).include?(x) && (control.y1..control.y2).include?(y)
|
83
|
+
end
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_click button, modifiers, x, y
|
88
|
+
control = control_at x, y
|
89
|
+
if control
|
90
|
+
if button == :left
|
91
|
+
control.focus! if control.respond_to? :handle_char
|
92
|
+
control.redraw
|
93
|
+
end
|
94
|
+
control.handle_click button, modifiers, x, y if control.respond_to?(:handle_click)
|
59
95
|
end
|
60
96
|
end
|
61
97
|
|
62
98
|
def draw_frame
|
63
|
-
|
99
|
+
linebar = 'q' * (width - 1)
|
64
100
|
fillerbar = ' ' * (width - 1)
|
65
101
|
|
102
|
+
@display.driver.linedrawing = true
|
66
103
|
print @display.color('0;2')
|
67
|
-
|
68
|
-
@display.place
|
104
|
+
|
105
|
+
@display.place y1, x1, "n#{linebar}n"
|
106
|
+
@display.place y2, x1, "n#{linebar}n"
|
107
|
+
#~ @display.place y1, x1, "l#{topbar}k"
|
108
|
+
#~ @display.place y2, x1, "m#{bottombar}j"
|
69
109
|
|
70
110
|
(y1 + 1).upto y2 - 1 do |row|
|
71
|
-
@display.place row, x1, "
|
111
|
+
@display.place row, x1, "x#{fillerbar}x"
|
112
|
+
end
|
113
|
+
|
114
|
+
@display.driver.linedrawing = false
|
115
|
+
print @display.color('0')
|
116
|
+
end
|
117
|
+
|
118
|
+
def draw_title
|
119
|
+
title = " * #{@title} * "
|
120
|
+
left = (((width - 1).to_f / 2) - (title.size.to_f / 2)).to_i
|
121
|
+
|
122
|
+
if title.size >= width
|
123
|
+
title = @title[0, width - 3]
|
124
|
+
left = 0
|
72
125
|
end
|
126
|
+
|
127
|
+
print @display.color('1;34')
|
128
|
+
@display.place y1, x1 + 1 + left, title
|
73
129
|
print @display.color('0')
|
74
130
|
end
|
75
131
|
end
|
data/lib/luck/textbox.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
module Luck
|
2
2
|
class TextBox < Label
|
3
|
-
attr_accessor :multiline, :handler, :label
|
3
|
+
attr_accessor :multiline, :handler, :label, :mask, :index
|
4
4
|
|
5
5
|
def initialize *args
|
6
6
|
@label = ''
|
7
7
|
@text = 'Input box'
|
8
|
+
@index = 0
|
9
|
+
|
8
10
|
super
|
9
11
|
end
|
10
12
|
|
@@ -17,30 +19,73 @@ class TextBox < Label
|
|
17
19
|
when :right
|
18
20
|
text.rjust width
|
19
21
|
else
|
20
|
-
@display.
|
22
|
+
@display.driver.set_cursor y1, x1 + text.size - @text.size + @index if @display.active_control == self
|
21
23
|
end
|
22
|
-
|
24
|
+
@display.driver.cursor = true if @display.active_control == self
|
23
25
|
end
|
24
26
|
|
25
27
|
def on_submit &blck
|
26
28
|
@handler = blck
|
27
29
|
end
|
28
30
|
|
31
|
+
def value= val
|
32
|
+
@text = val
|
33
|
+
@index = @text.size if @index > @text.size
|
34
|
+
end
|
35
|
+
def value
|
36
|
+
@text
|
37
|
+
end
|
38
|
+
|
29
39
|
def text
|
30
|
-
|
31
|
-
"#{
|
40
|
+
text = @mask ? (@mask * value.size) : value
|
41
|
+
text = "#{label}: #{text}" unless label.empty?
|
42
|
+
text
|
43
|
+
end
|
44
|
+
|
45
|
+
def handler
|
46
|
+
@handler || @pane.handler
|
32
47
|
end
|
33
48
|
|
34
49
|
def handle_char char
|
35
50
|
if char == "\n" && !@multiline
|
36
|
-
|
37
|
-
|
38
|
-
elsif char ==
|
39
|
-
@
|
40
|
-
|
41
|
-
|
51
|
+
handler.call self, @text if handler
|
52
|
+
self.value = ''
|
53
|
+
elsif char == :backspace
|
54
|
+
if @index > 0
|
55
|
+
@index -= 1
|
56
|
+
@text.slice! @index, 1
|
57
|
+
end
|
58
|
+
elsif char == :delete
|
59
|
+
if @index < @text.size
|
60
|
+
@text.slice! @index, 1
|
61
|
+
end
|
62
|
+
elsif char == :left
|
63
|
+
@index -= 1 if @index > 0
|
64
|
+
elsif char == :right
|
65
|
+
@index += 1 if @index < @text.size
|
66
|
+
elsif char == :home
|
67
|
+
@index = 0
|
68
|
+
elsif char == :end
|
69
|
+
@index = @text.size
|
70
|
+
elsif char.is_a? String
|
71
|
+
@text.insert @index, char
|
72
|
+
@index += 1
|
42
73
|
end
|
43
74
|
redraw
|
44
75
|
end
|
45
76
|
end
|
77
|
+
|
78
|
+
class CommandBox < TextBox
|
79
|
+
def command?
|
80
|
+
@text[0,1] == '/'
|
81
|
+
end
|
82
|
+
|
83
|
+
def label
|
84
|
+
command? ? 'Command' : @label
|
85
|
+
end
|
86
|
+
|
87
|
+
def value
|
88
|
+
command? ? super[1..-1] : super
|
89
|
+
end
|
90
|
+
end
|
46
91
|
end
|
data/lib/luck.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
Luck = Module.new
|
2
2
|
|
3
|
-
require 'luck/
|
4
|
-
require 'luck/
|
3
|
+
require 'luck/alert'
|
4
|
+
require 'luck/ansi'
|
5
|
+
require 'luck/button'
|
5
6
|
require 'luck/control'
|
7
|
+
require 'luck/display'
|
8
|
+
require 'luck/label'
|
6
9
|
require 'luck/listbox'
|
10
|
+
require 'luck/pane'
|
7
11
|
require 'luck/progressbar'
|
8
|
-
require 'luck/label'
|
9
12
|
require 'luck/textbox'
|
data/luck.gemspec
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{luck}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Daniel Danopia"]
|
12
|
+
s.date = %q{2010-04-10}
|
13
|
+
s.description = %q{Pure-ruby CLI UI system. Includes multiple panes in a display and multiple controls in a pane. luck is lucky (as opposed to ncurses being cursed)}
|
14
|
+
s.email = %q{me.github@danopia.net}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".gitignore",
|
21
|
+
"LICENSE",
|
22
|
+
"README.md",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION",
|
25
|
+
"lib/luck.rb",
|
26
|
+
"lib/luck/alert.rb",
|
27
|
+
"lib/luck/ansi.rb",
|
28
|
+
"lib/luck/button.rb",
|
29
|
+
"lib/luck/control.rb",
|
30
|
+
"lib/luck/display.rb",
|
31
|
+
"lib/luck/label.rb",
|
32
|
+
"lib/luck/listbox.rb",
|
33
|
+
"lib/luck/pane.rb",
|
34
|
+
"lib/luck/progressbar.rb",
|
35
|
+
"lib/luck/textbox.rb",
|
36
|
+
"luck.gemspec"
|
37
|
+
]
|
38
|
+
s.homepage = %q{http://github.com/danopia/luck}
|
39
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
40
|
+
s.require_paths = ["lib"]
|
41
|
+
s.rubygems_version = %q{1.3.5}
|
42
|
+
s.summary = %q{Pure-ruby CLI UI system}
|
43
|
+
|
44
|
+
if s.respond_to? :specification_version then
|
45
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
46
|
+
s.specification_version = 3
|
47
|
+
|
48
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
49
|
+
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
50
|
+
else
|
51
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
52
|
+
end
|
53
|
+
else
|
54
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: luck
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Danopia
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-
|
12
|
+
date: 2010-04-10 00:00:00 -04:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -38,6 +38,9 @@ files:
|
|
38
38
|
- Rakefile
|
39
39
|
- VERSION
|
40
40
|
- lib/luck.rb
|
41
|
+
- lib/luck/alert.rb
|
42
|
+
- lib/luck/ansi.rb
|
43
|
+
- lib/luck/button.rb
|
41
44
|
- lib/luck/control.rb
|
42
45
|
- lib/luck/display.rb
|
43
46
|
- lib/luck/label.rb
|
@@ -45,6 +48,7 @@ files:
|
|
45
48
|
- lib/luck/pane.rb
|
46
49
|
- lib/luck/progressbar.rb
|
47
50
|
- lib/luck/textbox.rb
|
51
|
+
- luck.gemspec
|
48
52
|
has_rdoc: true
|
49
53
|
homepage: http://github.com/danopia/luck
|
50
54
|
licenses: []
|