window_blessing 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/bin/buffered_screen_demo.rb +92 -0
- data/bin/color_picker_demo.rb +176 -0
- data/bin/foiled_demo.rb +27 -0
- data/bin/text_editor_demo.rb +292 -0
- data/bin/windowed_screen_demo.rb +71 -0
- data/bin/xterm_screen_demo.rb +33 -0
- data/lib/window_blessing.rb +54 -0
- data/lib/window_blessing/buffer.rb +216 -0
- data/lib/window_blessing/buffered_screen.rb +29 -0
- data/lib/window_blessing/color.rb +71 -0
- data/lib/window_blessing/constants.rb +3 -0
- data/lib/window_blessing/event_manager.rb +75 -0
- data/lib/window_blessing/event_queue.rb +20 -0
- data/lib/window_blessing/evented.rb +19 -0
- data/lib/window_blessing/evented_variable.rb +47 -0
- data/lib/window_blessing/tools.rb +124 -0
- data/lib/window_blessing/version.rb +3 -0
- data/lib/window_blessing/widgets/draggable_background.rb +13 -0
- data/lib/window_blessing/widgets/label.rb +23 -0
- data/lib/window_blessing/widgets/slider.rb +53 -0
- data/lib/window_blessing/widgets/text_field.rb +92 -0
- data/lib/window_blessing/window.rb +273 -0
- data/lib/window_blessing/windowed_screen.rb +53 -0
- data/lib/window_blessing/xterm_event_parser.rb +156 -0
- data/lib/window_blessing/xterm_input.rb +40 -0
- data/lib/window_blessing/xterm_log.rb +7 -0
- data/lib/window_blessing/xterm_output.rb +213 -0
- data/lib/window_blessing/xterm_screen.rb +109 -0
- data/lib/window_blessing/xterm_state.rb +27 -0
- data/spec/buffer_spec.rb +170 -0
- data/spec/color_spec.rb +36 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/tools_spec.rb +142 -0
- data/spec/window_spec.rb +61 -0
- data/window_blessing.gemspec +28 -0
- metadata +226 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
require File.expand_path File.join(File.dirname(__FILE__), %w{.. lib window_blessing})
|
4
|
+
include GuiGeo
|
5
|
+
include WindowBlessing
|
6
|
+
include Widgets
|
7
|
+
include Tools
|
8
|
+
|
9
|
+
class DragWindow < Window
|
10
|
+
include DraggableBackground
|
11
|
+
def initialize(loc,buffer)
|
12
|
+
super rect(loc,buffer.size)
|
13
|
+
@buffer.draw_buffer point, buffer
|
14
|
+
clean
|
15
|
+
|
16
|
+
on :key_press do |event|
|
17
|
+
XtermLog.log "key_press: #{event[:key]}"
|
18
|
+
r = area.clone
|
19
|
+
case event[:key]
|
20
|
+
when :home then r.loc.x = 0
|
21
|
+
when :page_up then r.loc.y = 0
|
22
|
+
when :page_down then r.loc.y = parent.size.y
|
23
|
+
when :end then r.loc.x = parent.size.x
|
24
|
+
when :left then r.loc.x -= 1
|
25
|
+
when :right then r.loc.x += 1
|
26
|
+
when :up then r.loc.y -= 1
|
27
|
+
when :down then r.loc.y += 1
|
28
|
+
end
|
29
|
+
self.area = r
|
30
|
+
self.move_onscreen
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def color_window(r)
|
36
|
+
size = r.size
|
37
|
+
DragWindow.new r.loc, (Buffer.new(size).tap do |buffer|
|
38
|
+
size.y.times do |y|
|
39
|
+
size.x.times do |x|
|
40
|
+
c1 = rgb_screen_color y / (size.y-1).to_f, x / (size.x-1).to_f, 0
|
41
|
+
c2 = rgb_screen_color 0, 1 - y / (size.y-1).to_f, 1 - x / (size.x-1).to_f
|
42
|
+
buffer.draw_rect rect(point(x,y),point(1,1)), :bg => c1, :fg => c2, :string => "▒"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end)
|
46
|
+
end
|
47
|
+
|
48
|
+
def gray_window(r)
|
49
|
+
size = r.size
|
50
|
+
DragWindow.new r.loc, (Buffer.new(size).tap do |buffer|
|
51
|
+
size.y.times do |y|
|
52
|
+
size.x.times do |x|
|
53
|
+
g1 = gray_screen_color(x / (size.x-1).to_f)
|
54
|
+
g2 = gray_screen_color(y / (size.y-1).to_f)
|
55
|
+
buffer.draw_rect rect(point(x,y),point(1,1)), :bg => g1, :fg => g2, :string => "o"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end)
|
59
|
+
end
|
60
|
+
|
61
|
+
WindowedScreen.new.start(:full=>true, :utf8 => true) do |screen|
|
62
|
+
|
63
|
+
root_window = screen.root_window
|
64
|
+
|
65
|
+
root_window.add_child gray_win = gray_window(rect(10,10,25,13))
|
66
|
+
root_window.add_child color_win = color_window(rect(30,15,24,12))
|
67
|
+
root_window.add_child Label.new rect(point,point(1000,1)),
|
68
|
+
" Arrows, Home, End, PgUp, PgDown or drag with Mouse to move. Space to toggle. Ctrl-Q to quit.",
|
69
|
+
:bg => color(0.25)
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require File.expand_path File.join(File.dirname(__FILE__), %w{.. lib window_blessing})
|
3
|
+
include GuiGeo
|
4
|
+
|
5
|
+
WindowBlessing::XtermScreen.new.start(:full=>true) do |screen|
|
6
|
+
event_manager = screen.event_manager
|
7
|
+
|
8
|
+
last_event = nil
|
9
|
+
event_count = 0
|
10
|
+
|
11
|
+
event_manager.add_handler :tick do
|
12
|
+
screen.output.instance_eval do
|
13
|
+
cursor(0,0)
|
14
|
+
reset_color
|
15
|
+
puts Time.now
|
16
|
+
puts "Ctrl-Q to quit"
|
17
|
+
puts "size: #{screen.state.size.inspect}"
|
18
|
+
|
19
|
+
if last_event
|
20
|
+
e = last_event.inspect
|
21
|
+
e += "\nfailure_info (raw=#{last_event[:raw].inspect}): #{last_event[:failure_info]}" if last_event[:failure_info]
|
22
|
+
e += "\ntrace:\n "+last_event[:exception].backtrace.join("\n ") if last_event[:type]==:event_exception
|
23
|
+
puts "event #{event_count}: #{e} "
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
event_manager.add_handler do |event|
|
29
|
+
event_count += 1
|
30
|
+
WindowBlessing::XtermLog.log "last_event = #{event.inspect}"
|
31
|
+
last_event = event
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
def add_load_path(path)
|
2
|
+
full_path = File.expand_path path
|
3
|
+
$LOAD_PATH.unshift full_path unless $LOAD_PATH.include?(path) || $LOAD_PATH.include?(full_path)
|
4
|
+
end
|
5
|
+
add_load_path File.dirname(__FILE__)
|
6
|
+
|
7
|
+
=begin
|
8
|
+
Copyright 2013 Shane Brinkman-Davis
|
9
|
+
See README for licence information.
|
10
|
+
=end
|
11
|
+
|
12
|
+
%w{
|
13
|
+
constants
|
14
|
+
color
|
15
|
+
tools
|
16
|
+
buffer
|
17
|
+
version
|
18
|
+
event_queue
|
19
|
+
event_manager
|
20
|
+
evented
|
21
|
+
evented_variable
|
22
|
+
xterm_event_parser
|
23
|
+
xterm_log
|
24
|
+
xterm_output
|
25
|
+
xterm_state
|
26
|
+
xterm_input
|
27
|
+
xterm_screen
|
28
|
+
buffered_screen
|
29
|
+
window
|
30
|
+
windowed_screen
|
31
|
+
widgets/draggable_background
|
32
|
+
widgets/label
|
33
|
+
widgets/slider
|
34
|
+
widgets/text_field
|
35
|
+
}.each do |file|
|
36
|
+
require "window_blessing/#{file}"
|
37
|
+
end
|
38
|
+
|
39
|
+
module WindowBlessing
|
40
|
+
# Your code goes here...
|
41
|
+
class << self
|
42
|
+
include Tools
|
43
|
+
def main(&block)
|
44
|
+
main_window = Window.new
|
45
|
+
Screen.new.open do
|
46
|
+
instance_exec(main_window, &block)
|
47
|
+
on_tick do
|
48
|
+
main_window.area = rect(screen_buffer.size)
|
49
|
+
main_window.draw screen_buffer
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
=begin
|
2
|
+
NOTE: Unlike bitmap graphics, it is often going to be faster to clone bitmap frames rather than do special logic to extract data from frames in-place. This is due the relatively small amount of data for a console-window "buffer" (cols * lines ~= a few kilobytes) and Ruby having optimized routines to handle strings as single units.
|
3
|
+
|
4
|
+
For example, it's actually going to be faster to draw a sub-region from one buffer onto another by first copying out the subbuffer from the source buffer and then copying it into the target buffer.
|
5
|
+
|
6
|
+
NOTE: I'm just making intelligent guesses here. Haven't actually profiled. It keeps the code simpler, too.
|
7
|
+
|
8
|
+
=end
|
9
|
+
module WindowBlessing
|
10
|
+
class Buffer
|
11
|
+
include Tools
|
12
|
+
|
13
|
+
attr_reader :size
|
14
|
+
attr_accessor :contents
|
15
|
+
|
16
|
+
# used as the default color when resizing
|
17
|
+
attr_accessor :bg, :fg
|
18
|
+
|
19
|
+
# color buffers. 2D arrays. Each element is a number, 0-255.
|
20
|
+
attr_accessor :fg_buffer, :bg_buffer
|
21
|
+
|
22
|
+
attr_accessor :crop_area
|
23
|
+
attr_reader :dirty_area
|
24
|
+
|
25
|
+
def Buffer.default_bg; 0; end
|
26
|
+
def Buffer.default_fg; 7; end
|
27
|
+
|
28
|
+
# init-options
|
29
|
+
# :bg_bufer => 2D array of 0-255 values
|
30
|
+
# :fg_bufer => 2D array of 0-255 values
|
31
|
+
# :contents => array of strings or string with new-lines
|
32
|
+
# fill options (will override init-options)
|
33
|
+
# :string, :bg, :fg -- see #fill
|
34
|
+
def initialize(size, options={})
|
35
|
+
@size = size
|
36
|
+
|
37
|
+
@contents = options[:contents]
|
38
|
+
@fg_buffer = options[:fg_buffer]
|
39
|
+
@bg_buffer = options[:bg_buffer]
|
40
|
+
|
41
|
+
fill options
|
42
|
+
normalize
|
43
|
+
clean
|
44
|
+
end
|
45
|
+
|
46
|
+
def each_line(&block)
|
47
|
+
@contents.zip(fg_buffer,bg_buffer).each &block
|
48
|
+
end
|
49
|
+
|
50
|
+
def fg_buffer=(fg_buffer)
|
51
|
+
@fg_buffer = fg_buffer
|
52
|
+
normalize
|
53
|
+
end
|
54
|
+
|
55
|
+
def bg_buffer=(bg_buffer)
|
56
|
+
@bg_buffer = bg_buffer
|
57
|
+
normalize
|
58
|
+
end
|
59
|
+
|
60
|
+
def contents=(contents)
|
61
|
+
@contents = contents
|
62
|
+
normalize
|
63
|
+
end
|
64
|
+
|
65
|
+
def sanitize_contents(range=0..-1)
|
66
|
+
@contents[range] = @contents[range].collect {|l| l.gsub(/[\x00-\x1f]/,'?')}
|
67
|
+
end
|
68
|
+
|
69
|
+
def normalize(range=0..-1)
|
70
|
+
@contents||=[]
|
71
|
+
@fg_buffer||=[]
|
72
|
+
@bg_buffer||=[]
|
73
|
+
@contents = @contents.gsub(/[\x00-\x09\x11-\x1f]/,'?').split("\n") if @contents.kind_of?(String)
|
74
|
+
@contents[range] = resize2d @contents[range] , size, " "
|
75
|
+
@fg_buffer[range] = resize2d @fg_buffer[range], size, Buffer.default_fg
|
76
|
+
@bg_buffer[range] = resize2d @bg_buffer[range], size, Buffer.default_bg
|
77
|
+
end
|
78
|
+
|
79
|
+
def on_dirty(&block)
|
80
|
+
@on_dirty = block
|
81
|
+
end
|
82
|
+
|
83
|
+
def crop_area
|
84
|
+
@crop_area || rect(size)
|
85
|
+
end
|
86
|
+
|
87
|
+
def cropped(area)
|
88
|
+
old_crop_area = @crop_area
|
89
|
+
@crop_area = area | crop_area
|
90
|
+
yield
|
91
|
+
self
|
92
|
+
ensure
|
93
|
+
@crop_area = old_crop_area
|
94
|
+
end
|
95
|
+
|
96
|
+
def cropped?
|
97
|
+
crop_area != rect(size)
|
98
|
+
end
|
99
|
+
|
100
|
+
def inspect
|
101
|
+
"<Buffer size:#{size.inspect}>"
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_s
|
105
|
+
contents.join "\n"
|
106
|
+
end
|
107
|
+
|
108
|
+
def internal_area
|
109
|
+
rect(size)
|
110
|
+
end
|
111
|
+
|
112
|
+
def subbuffer(area)
|
113
|
+
area = internal_area | area
|
114
|
+
return buffer unless area.present?
|
115
|
+
|
116
|
+
x_range = area.x_range
|
117
|
+
|
118
|
+
buffer area.size,
|
119
|
+
:contents => subarray2d(contents,area),
|
120
|
+
:fg_buffer => subarray2d(fg_buffer,area),
|
121
|
+
:bg_buffer => subarray2d(bg_buffer,area),
|
122
|
+
:fg => fg,
|
123
|
+
:bg => bg
|
124
|
+
end
|
125
|
+
|
126
|
+
def dirty_subbuffer
|
127
|
+
@dirty_area && subbuffer(@dirty_area)
|
128
|
+
end
|
129
|
+
|
130
|
+
#########
|
131
|
+
# dirty?
|
132
|
+
#########
|
133
|
+
def dirty?
|
134
|
+
!!@dirty_area
|
135
|
+
end
|
136
|
+
|
137
|
+
def clean
|
138
|
+
@dirty_area = nil
|
139
|
+
end
|
140
|
+
|
141
|
+
def dirty(area = internal_area)
|
142
|
+
@dirty_area = (area & @dirty_area) | internal_area
|
143
|
+
@on_dirty.call if @on_dirty
|
144
|
+
@dirty_area
|
145
|
+
end
|
146
|
+
|
147
|
+
#########
|
148
|
+
# DRAWING
|
149
|
+
#########
|
150
|
+
|
151
|
+
def clear
|
152
|
+
@contents = @bg_buffer = @fg_buffer = nil
|
153
|
+
normalize
|
154
|
+
self
|
155
|
+
end
|
156
|
+
|
157
|
+
# Fills characters, foreground and/or background buffers with the specified values.
|
158
|
+
#
|
159
|
+
# If one of the buffers is not specified to be filled, it is not changed.
|
160
|
+
#
|
161
|
+
# For example, you can set the foreground color without changing the text:
|
162
|
+
#
|
163
|
+
# fill :fg => rgb_screen_color(1,0,0)
|
164
|
+
#
|
165
|
+
# options
|
166
|
+
# :area => Rectangle # only fill the specified area (intersected with the crop_area)
|
167
|
+
# :bg => background color OR 1d array of bg-color pattern - nil => don't touch bg
|
168
|
+
# :fg => foreground color OR 1d array of fg-color pattern - nil => don't touch fb
|
169
|
+
# :string => string - length 1 or more, use to fill-init @contents - nil => don't touch @contents
|
170
|
+
def fill(options = {})
|
171
|
+
area = crop_area
|
172
|
+
area = area | options[:area] if options[:area]
|
173
|
+
string = options[:string]
|
174
|
+
fg = options[:fg]
|
175
|
+
bg = options[:bg]
|
176
|
+
fg = fg.to_screen_color if fg.kind_of?(Color)
|
177
|
+
bg = bg.to_screen_color if bg.kind_of?(Color)
|
178
|
+
|
179
|
+
if area != internal_area
|
180
|
+
@contents = overlay2d(area.loc, gen_array2d(area.size, string), contents) if string
|
181
|
+
@fg_buffer = overlay2d(area.loc, gen_array2d(area.size, fg), fg_buffer) if fg
|
182
|
+
@bg_buffer = overlay2d(area.loc, gen_array2d(area.size, bg), bg_buffer) if bg
|
183
|
+
else
|
184
|
+
@contents = gen_array2d(size, string) if string
|
185
|
+
@fg_buffer = gen_array2d(size, fg) if fg
|
186
|
+
@bg_buffer = gen_array2d(size, bg) if bg
|
187
|
+
end
|
188
|
+
dirty area
|
189
|
+
self
|
190
|
+
end
|
191
|
+
|
192
|
+
# Just like #fill except all buffers are filled. Default values are used if not specified.
|
193
|
+
# options - see #fill options
|
194
|
+
def draw_rect(rectangle, options={})
|
195
|
+
fill({:area => rectangle, :string => " ", :fg => Buffer.default_fg, :bg => Buffer.default_bg}.merge options)
|
196
|
+
end
|
197
|
+
|
198
|
+
def draw_buffer(loc, buffer, source_area = nil)
|
199
|
+
source_area = (source_area || buffer.internal_area) | (crop_area - loc)
|
200
|
+
return unless source_area.present?
|
201
|
+
|
202
|
+
unless source_area == buffer.internal_area
|
203
|
+
loc += source_area.loc
|
204
|
+
buffer = buffer.subbuffer(source_area)
|
205
|
+
end
|
206
|
+
|
207
|
+
@contents = overlay2d(loc, buffer.contents, contents)
|
208
|
+
@fg_buffer = overlay2d(loc, buffer.fg_buffer, fg_buffer)
|
209
|
+
@bg_buffer = overlay2d(loc, buffer.bg_buffer, bg_buffer)
|
210
|
+
|
211
|
+
dirty rect(loc, buffer.size)
|
212
|
+
self
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module WindowBlessing
|
2
|
+
|
3
|
+
class BufferedScreen < XtermScreen
|
4
|
+
attr_accessor :screen_buffer
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
super
|
8
|
+
@screen_buffer = Buffer.new point(20,20)
|
9
|
+
|
10
|
+
event_manager.add_handler :tick do
|
11
|
+
update_from_screen_buffer
|
12
|
+
end
|
13
|
+
|
14
|
+
event_manager.add_handler :resize do |event|
|
15
|
+
@screen_buffer = Buffer.new event[:size]
|
16
|
+
@screen_buffer.dirty
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def update_from_screen_buffer
|
21
|
+
if dirty_buffer = screen_buffer.dirty_subbuffer
|
22
|
+
# XtermLog.log "#{self.class}#update_from_screen_buffer() diry_area: #{screen_buffer.dirty_area}"
|
23
|
+
output.draw_buffer screen_buffer.dirty_area.loc, dirty_buffer
|
24
|
+
screen_buffer.clean
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module WindowBlessing
|
2
|
+
class Color < Struct.new(:r, :g, :b)
|
3
|
+
class <<self
|
4
|
+
|
5
|
+
def black; Color.new(0) end
|
6
|
+
def white; Color.new(1) end
|
7
|
+
def gray; Color.new(0.5) end
|
8
|
+
|
9
|
+
def red; Color.new(1,0,0) end
|
10
|
+
def green; Color.new(0,1,0) end
|
11
|
+
def blue; Color.new(0,0,1) end
|
12
|
+
|
13
|
+
def yellow; Color.new(1,1,0) end
|
14
|
+
def cyan; Color.new(0,1,1) end
|
15
|
+
def magenta;Color.new(1,0,1) end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(r=0.0, g=r, b=r)
|
19
|
+
case r
|
20
|
+
when String then self.hex=r
|
21
|
+
else super r, g, b
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def br
|
26
|
+
(r + g + b) / 3.0
|
27
|
+
end
|
28
|
+
|
29
|
+
def hex=(hex)
|
30
|
+
raise "invalid hex color #{hex.inspect}" unless hex[/#?(((..)(..)(..))|((.)(.)(.)))/]
|
31
|
+
self.r = ($3 || ($7*2)).hex/255.0
|
32
|
+
self.g = ($4 || ($8*2)).hex/255.0
|
33
|
+
self.b = ($5 || ($9*2)).hex/255.0
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect; "color#{self}" end
|
37
|
+
def to_s; "(#{r},#{g},#{b})" end
|
38
|
+
def to_hex; "#%02x%02x%02x"%to_a256 end
|
39
|
+
def to_a256; [r256,g256,b256] end
|
40
|
+
def to_a; [r,g,b] end
|
41
|
+
|
42
|
+
# to xterm pallette color
|
43
|
+
def to_screen_color; rgb_screen_color(r,g,b) end
|
44
|
+
|
45
|
+
def [](i)
|
46
|
+
case i
|
47
|
+
when 0, :r then r
|
48
|
+
when 1, :g then g
|
49
|
+
when 2, :b then b
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def []=(key,v)
|
54
|
+
case key
|
55
|
+
when 0, :r then self.r = v
|
56
|
+
when 1, :g then self.g = v
|
57
|
+
when 2, :b then self.b = v
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def r256; (r*255).to_i end
|
62
|
+
def g256; (g*255).to_i end
|
63
|
+
def b256; (b*255).to_i end
|
64
|
+
|
65
|
+
def +(v) v.kind_of?(Color) ? Color.new(r + v.r, g + v.g, b + v.b) : Color.new(r + v, g + v, b + v) end
|
66
|
+
def -(v) v.kind_of?(Color) ? Color.new(r - v.r, g - v.g, b - v.b) : Color.new(r - v, g - v, b - v) end
|
67
|
+
def *(v) v.kind_of?(Color) ? Color.new(r * v.r, g * v.g, b * v.b) : Color.new(r * v, g * v, b * v) end
|
68
|
+
def /(v) v.kind_of?(Color) ? Color.new(r / v.r, g / v.g, b / v.b) : Color.new(r / v, g / v, b / v) end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|