luck 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|