window_blessing 0.0.1

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.
Files changed (42) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +4 -0
  3. data/Guardfile +7 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +43 -0
  6. data/Rakefile +12 -0
  7. data/bin/buffered_screen_demo.rb +92 -0
  8. data/bin/color_picker_demo.rb +176 -0
  9. data/bin/foiled_demo.rb +27 -0
  10. data/bin/text_editor_demo.rb +292 -0
  11. data/bin/windowed_screen_demo.rb +71 -0
  12. data/bin/xterm_screen_demo.rb +33 -0
  13. data/lib/window_blessing.rb +54 -0
  14. data/lib/window_blessing/buffer.rb +216 -0
  15. data/lib/window_blessing/buffered_screen.rb +29 -0
  16. data/lib/window_blessing/color.rb +71 -0
  17. data/lib/window_blessing/constants.rb +3 -0
  18. data/lib/window_blessing/event_manager.rb +75 -0
  19. data/lib/window_blessing/event_queue.rb +20 -0
  20. data/lib/window_blessing/evented.rb +19 -0
  21. data/lib/window_blessing/evented_variable.rb +47 -0
  22. data/lib/window_blessing/tools.rb +124 -0
  23. data/lib/window_blessing/version.rb +3 -0
  24. data/lib/window_blessing/widgets/draggable_background.rb +13 -0
  25. data/lib/window_blessing/widgets/label.rb +23 -0
  26. data/lib/window_blessing/widgets/slider.rb +53 -0
  27. data/lib/window_blessing/widgets/text_field.rb +92 -0
  28. data/lib/window_blessing/window.rb +273 -0
  29. data/lib/window_blessing/windowed_screen.rb +53 -0
  30. data/lib/window_blessing/xterm_event_parser.rb +156 -0
  31. data/lib/window_blessing/xterm_input.rb +40 -0
  32. data/lib/window_blessing/xterm_log.rb +7 -0
  33. data/lib/window_blessing/xterm_output.rb +213 -0
  34. data/lib/window_blessing/xterm_screen.rb +109 -0
  35. data/lib/window_blessing/xterm_state.rb +27 -0
  36. data/spec/buffer_spec.rb +170 -0
  37. data/spec/color_spec.rb +36 -0
  38. data/spec/spec_helper.rb +6 -0
  39. data/spec/tools_spec.rb +142 -0
  40. data/spec/window_spec.rb +61 -0
  41. data/window_blessing.gemspec +28 -0
  42. metadata +226 -0
@@ -0,0 +1,40 @@
1
+
2
+ module WindowBlessing
3
+ class XtermInput
4
+ attr_reader :event_parser
5
+
6
+ def initialize
7
+ @event_parser = XtermEventParser.new
8
+ end
9
+
10
+ def read_events
11
+ events = []
12
+ if raw = read_pending_input
13
+ parsed = event_parser.parse(raw)
14
+ if parsed
15
+ new_events = parsed.events
16
+ events += new_events
17
+ new_events.length
18
+ else
19
+ events << {:type => :event_parser_failure, :raw => raw, :failure_info => event_parser.parser_failure_info}
20
+ end
21
+ end
22
+ events
23
+ end
24
+
25
+ private
26
+ def read_pending_input
27
+ read = nil
28
+ begin
29
+ while c = STDIN.read_nonblock(1000)
30
+ read = read ? read + c : c
31
+ end
32
+ rescue Errno::EAGAIN # nothing was ready to be read
33
+ rescue Errno::EINTR
34
+ rescue EOFError
35
+ end
36
+ read
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,7 @@
1
+ module WindowBlessing
2
+ class XtermLog
3
+ def self.log(str)
4
+ File.open("xterm.log","a+") {|f| f.puts str}
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,213 @@
1
+ # ref: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
2
+ # ref: http://www.vt100.net/docs/vt102-ug/chapter5.html
3
+
4
+ module WindowBlessing
5
+
6
+ class XtermOutput
7
+ module SetColor
8
+ def change_fg(fg)
9
+ set_fg(fg) if fg!=@current_fg
10
+ @current_fg = fg
11
+ end
12
+
13
+ def change_bg(bg)
14
+ set_bg(bg) if bg!=@current_bg
15
+ @current_bg = bg
16
+ end
17
+
18
+ def set_fg(fg)
19
+ out "\x1b[38;5;#{fg}m"
20
+ end
21
+
22
+ def set_bg(bg)
23
+ out "\x1b[48;5;#{bg}m"
24
+ end
25
+
26
+ attr_reader :current_fg, :current_bg
27
+
28
+ def set_color(fg, bg=nil)
29
+ set_fg(fg) if fg
30
+ set_bg(bg) if bg
31
+ end
32
+
33
+ # fg and bg are r-g-b arrays: [0..255, 0..255, 0..255]
34
+ # This is not supported by iTerm2: http://code.google.com/p/iterm2/issues/detail?id=218
35
+ # konsole supports it: https://github.com/robertknight/konsole/blob/master/user-doc/README.moreColors
36
+ def set_color_24bit(fg, bg=nil)
37
+ out "\x1b[38;2;#{fg.join(';')}m" if fg
38
+ out "\x1b[48;2;#{bg.join(';')}m" if bg
39
+ end
40
+
41
+ def reset_color
42
+ @current_fg = 7
43
+ @current_bg = 0
44
+ out "\x1b[0m"
45
+ end
46
+
47
+ def out_color(txt, fg, bg)
48
+ set_color(fg, bg)
49
+ out txt
50
+ reset_color
51
+ end
52
+
53
+ def set_bold; out "\x1b[1m" end
54
+ def set_underline; out "\x1b[4m" end
55
+ end
56
+ include SetColor
57
+
58
+ module SetState
59
+ def show_cursor; out "\e[?25h"; end
60
+ def hide_cursor; out "\e[?25l"; end
61
+
62
+ def enable_mouse; out "\e[?1003h"; end
63
+ def disable_mouse; out "\e[?1003l"; end
64
+
65
+ def enable_focus_events; out "\e[?1004h" end
66
+ def disable_focus_events; out "\e[?1004l" end
67
+
68
+ def enable_utf8; out "\e%G" end
69
+ def disable_utf8; out "\e%@" end
70
+
71
+ def enable_alternate_screen
72
+ out "\e7"
73
+ out "\e[?47h"
74
+ end
75
+
76
+ def disable_alternate_screen
77
+ out "\e[?47l"
78
+ out "\e8"
79
+ end
80
+
81
+ # TODO: find out what stty raw -echo sends to xterm
82
+ # INTERNAL NOTE: out "\e[12h" # should turn off echo according to doc, but didn't work
83
+ # http://stackoverflow.com/questions/174933/how-to-get-a-single-character-without-pressing-enter
84
+ def echo_off; system "stty raw -echo"; end
85
+
86
+ # INTERNAL NOTE: out "\e[12l" # should turn echo back on
87
+ def echo_on; system "stty -raw echo"; end
88
+
89
+ =begin
90
+ raw may set the following:
91
+ 00830 option(x + "-icrnl", ""); /* map CR to NL on input */
92
+ 00831 option(x + "-ixon", ""); /* enable start/stop output control */
93
+ 00832 option(x + "-opost", ""); /* perform output processing */
94
+ 00833 option(x + "-onlcr", ""); /* Map NL to CR-NL on output */
95
+ 00834 option(x + "-isig", ""); /* enable signals */
96
+ 00835 option(x + "-icanon", "");/* canonical input (erase and kill enabled) */
97
+ 00836 option(x + "-iexten", "");/* enable extended functions */
98
+ 00837 option(x + "-echo", ""); /* enable echoing of input characters */
99
+
100
+ =end
101
+
102
+ # This seems to work - or at least it does SOMETHING
103
+ # def insert_mode; out "\e[4m"; end
104
+ # def replace_mode; out "\e[4l"; end
105
+
106
+ end
107
+ include SetState
108
+
109
+ attr_accessor :xterm_state
110
+
111
+ def initialize(xterm_state)
112
+ @xterm_state = xterm_state
113
+ end
114
+
115
+ # cursor 0, 0 is the upper left hand corner
116
+ # cursor point(0, 0) is also accepted
117
+ def cursor(loc_or_x, y=nil)
118
+ loc = y ? point(loc_or_x,y) : loc_or_x
119
+ out "\e[#{loc.y+1};#{loc.x+1}H"
120
+ end
121
+
122
+ # raw screen output
123
+ def out(str)
124
+ $stdout.print str
125
+ str
126
+ end
127
+
128
+ def out_at(loc, str)
129
+ cursor(loc)
130
+ out str
131
+ end
132
+
133
+ def out_at_with_color(loc, str, fg, bg)
134
+ return unless str.length > 0
135
+ cursor loc
136
+
137
+ change_fg fg[0]
138
+ change_bg bg[0]
139
+
140
+ current_attrs = [current_fg, current_bg]
141
+
142
+ next_output_pos = 0
143
+ pos = 0
144
+
145
+ fg.zip(bg).each do |attrs|
146
+ if current_attrs!=attrs
147
+ out str[next_output_pos..pos-1]
148
+ change_fg attrs[0]
149
+ change_bg attrs[1]
150
+ current_attrs = attrs
151
+ next_output_pos = pos
152
+ end
153
+ pos += 1
154
+ end
155
+ out str[next_output_pos..-1]
156
+ end
157
+
158
+ def draw_buffer(loc, buffer)
159
+ loc = loc.clone
160
+ reset_color
161
+ buffer.each_line do |line,fg,bg|
162
+ out_at_with_color loc, line, fg, bg
163
+ loc.y += 1
164
+ end
165
+ end
166
+
167
+ # convert all \n to \n\r
168
+ def puts(s=nil)
169
+ width = xterm_state.size.x
170
+ lines = "#{s}\n".split("\n").collect do |line|
171
+ line + " " * (width - (line.length % width))
172
+ end
173
+ out lines.flatten.join "\n"
174
+ end
175
+
176
+ def clear; out "\e[2J"; end
177
+
178
+
179
+ # INTERNAL NOTE: xterm returns 3 numbers: ?, height, width
180
+ # Xterm sends back response as an escape sequence. XtermEventParser knows how to capture and interpret the result.
181
+ def request_xterm_size; out "\e[18t"; end
182
+
183
+ # INTERNAL NOTE: xterm returns 3 numbers: ?, height, width
184
+ # This returns the entire screen size in pixels - not just the pixel-size of the x-term
185
+ def request_display_pixel_size; out "\e[14t"; end
186
+
187
+ def request_cursor_position; out "\e[?6n"; end
188
+
189
+ def request_state_update
190
+ request_xterm_size
191
+ request_display_pixel_size
192
+ end
193
+
194
+ def enable_resize_events
195
+ Signal.trap "SIGWINCH" do
196
+ request_xterm_size
197
+ end
198
+ end
199
+
200
+ def disable_resize_events
201
+ Signal.trap "SIGWINCH", "DEFAULT"
202
+ end
203
+
204
+ def reset_all
205
+ disable_focus_events
206
+ disable_mouse
207
+ disable_resize_events
208
+ reset_color
209
+ show_cursor
210
+ echo_on
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,109 @@
1
+ module WindowBlessing
2
+ class XtermScreen
3
+ include GuiGeo
4
+ include GuiGeo::Tools
5
+
6
+ attr_accessor :input, :output, :event_manager, :state, :event_queue
7
+
8
+ def initialize
9
+ @event_manager = EventManager.new(self)
10
+ @state = XtermState.new @event_manager
11
+ @input = XtermInput.new
12
+ @output = XtermOutput.new(@state)
13
+ @running = true
14
+ @pending_events = []
15
+ @event_queue = EventQueue.new
16
+
17
+ @event_manager.add_handler :key_press do |event|
18
+ quit if event[:key]==:control_q
19
+ end
20
+ end
21
+
22
+ def inspect
23
+ "<#{self.class}:#{object_id}>"
24
+ end
25
+
26
+ def quit; @running = false; end
27
+ def running?; @running; end
28
+
29
+ def queue_event(e); event_queue << e end
30
+ def queued_events?; !event_queue.empty? end
31
+
32
+ def queue_pending_xterm_events
33
+ event_queue << input.read_events
34
+ end
35
+
36
+ def wait_for_events(max=100)
37
+ count = max
38
+ while !queued_events? && count > 0
39
+ queue_pending_xterm_events
40
+ count -= 1
41
+ sleep 0.01
42
+ end
43
+ raise "no events!" unless queued_events?
44
+ end
45
+
46
+ def process_queued_events
47
+ event_manager.handle_events event_queue.pop_all
48
+ end
49
+
50
+ def process_events
51
+ queue_pending_xterm_events
52
+ queue_event :type => :tick
53
+ process_queued_events
54
+ end
55
+
56
+ def event_loop
57
+ while running?
58
+ process_events
59
+ sleep 1/60.0
60
+ end
61
+ end
62
+
63
+ def initialize_screen
64
+ output.request_state_update
65
+ wait_for_events
66
+ process_events
67
+ end
68
+
69
+ # run xterm raw-session
70
+ # options
71
+ #
72
+ def start(options={})
73
+ in_xterm_state(options) do
74
+ initialize_screen
75
+ yield self
76
+ event_loop
77
+ end
78
+ end
79
+
80
+
81
+ # options
82
+ # :mouse => true
83
+ # :no_cursor => true
84
+ # :alternate_screen => true
85
+ # :full => true (enables all above features)
86
+ # :utf8
87
+ def in_xterm_state(options = {})
88
+ output.echo_off
89
+ output.enable_alternate_screen if options[:full] || options[:alternate_screen]
90
+ output.enable_mouse if options[:full] || options[:mouse]
91
+ output.hide_cursor if options[:full] || options[:no_cursor]
92
+ output.enable_utf8 if options[:utf8]
93
+ output.enable_focus_events
94
+ output.enable_resize_events
95
+ output.clear
96
+
97
+ yield self
98
+ ensure
99
+ output.reset_all
100
+ output.disable_utf8 if options[:utf8]
101
+ if options[:full] || options[:alternate_screen]
102
+ output.reset_color
103
+ output.clear
104
+ output.disable_alternate_screen
105
+ end
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,27 @@
1
+ module WindowBlessing
2
+ class XtermState
3
+ attr_accessor :state
4
+
5
+ def initialize(event_manager)
6
+ @state = {:size => point(-1,-1)}
7
+
8
+ event_manager.add_handler :xterm_state do |event|
9
+ state_type = event[:state_type]
10
+ old_state = state[state_type]
11
+ new_state = event[:state]
12
+ state[state_type] = new_state
13
+ if old_state!=new_state
14
+ case state_type
15
+ when :size
16
+ event_manager.handle_event :type => :resize, :old_size => old_state, :size => new_state, :raw => event[:raw]
17
+ else
18
+ event_manager.handle_event :type => :state_change, :state_type => state_type, :old_state => old_state, :state => new_state, :raw => event[:raw]
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+
25
+ def size; state[:size] end
26
+ end
27
+ end
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+
3
+ module WindowBlessing
4
+ describe "Buffer" do
5
+ include Tools
6
+
7
+ def test_frame(options={})
8
+ buffer(point(4,4), {:contents => %w{1234 2345 3456 4567}}.merge(options))
9
+ end
10
+
11
+ it "blank fb" do
12
+ buffer(point(4,4)).to_s.should == " \n \n \n "
13
+ end
14
+
15
+ it "string init" do
16
+ buffer(point(4,4),:contents => ["hi","there"]).to_s.should == "hi \nther\n \n "
17
+ buffer(point(4,4),:contents => "hi\nthere").to_s.should == "hi \nther\n \n "
18
+ buffer(point(4,4),:contents => "hi\n\t\0here").to_s.should == "hi \n??he\n \n "
19
+ end
20
+
21
+ it "inspect" do
22
+ buffer(point 4,4).inspect.class.should == String
23
+ end
24
+
25
+ it "invalid init" do
26
+ expect { buffer(point(4,4),:contents => 1) }.to raise_error
27
+ end
28
+
29
+ it "subbuffer" do
30
+ test_frame.subbuffer(rect(1,1,2,2)).to_s.should == "34\n45"
31
+ end
32
+
33
+ it "array init" do
34
+ test_frame.to_s.should == "1234\n2345\n3456\n4567"
35
+ end
36
+
37
+ it "crop" do
38
+ (f=buffer(point 4,4)).cropped(rect(1,1,2,2)) do
39
+ f.crop_area.should == rect(1,1,2,2)
40
+ f.cropped?.should == true
41
+ end
42
+ end
43
+
44
+ it "fill" do
45
+ fb = buffer(point(4,4))
46
+ fb.fill :string => "-"
47
+ fb.to_s.should == "----\n----\n----\n----"
48
+ end
49
+
50
+ it "cropped fill" do
51
+ (f=test_frame).cropped(rect(1,1,2,1)) do
52
+ f.fill :string => '-'
53
+ end.to_s.should == "1234\n2--5\n3456\n4567"
54
+ end
55
+
56
+ it "draw_rect" do
57
+ (f=buffer(point(4,4))).to_s.should == " \n \n \n "
58
+ f.draw_rect(rect(1,0,2,2),:string => "-")
59
+ f.to_s.should == " -- \n -- \n \n "
60
+ end
61
+
62
+ it "clear" do
63
+ fb = buffer(point 2,2).fill(:string => '-')
64
+ fb.to_s.should == "--\n--"
65
+ fb.clear
66
+ fb.to_s.should == " \n "
67
+ end
68
+
69
+ it "draw_buffer" do
70
+ f1 = test_frame
71
+ f2 = buffer(point(2,2), :contents => "ab\nbc")
72
+ f1.draw_buffer(point(1,1),f2)
73
+ f1.to_s.should == "1234\n2ab5\n3bc6\n4567"
74
+ end
75
+
76
+ it "cropped draw_buffer" do
77
+ f1 = test_frame
78
+ f2 = buffer(point(2,2), :contents => "ab\nbc")
79
+ f1.cropped(rect(1,1,2,1)) do
80
+ f1.draw_buffer(point(1,1),f2)
81
+ end.to_s.should == "1234\n2ab5\n3456\n4567"
82
+ end
83
+
84
+ it "fill only overwrites what is provided" do
85
+ f0 = test_frame :bg => 9, :fg => 8
86
+
87
+ f1 = f0.clone
88
+ f1.to_s.should == "1234\n2345\n3456\n4567"
89
+ f1.fg_buffer.should == [[8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8]]
90
+ f1.bg_buffer.should == [[9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9]]
91
+
92
+ f1.fill :string => "!"
93
+ f1.to_s.should == "!!!!\n!!!!\n!!!!\n!!!!"
94
+ f1.fg_buffer.should == [[8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8]]
95
+ f1.bg_buffer.should == [[9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9]]
96
+
97
+ f1 = f0.clone
98
+ f1.fill :bg => 0
99
+ f1.to_s.should == "1234\n2345\n3456\n4567"
100
+ f1.fg_buffer.should == [[8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8]]
101
+ f1.bg_buffer.should == [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
102
+
103
+ f1 = f0.clone
104
+ f1.fill :fg => 0
105
+ f1.to_s.should == "1234\n2345\n3456\n4567"
106
+ f1.fg_buffer.should == [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
107
+ f1.bg_buffer.should == [[9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9]]
108
+ end
109
+
110
+ it "cropped fill only overwrites what is provided" do
111
+ f0 = test_frame :bg => 9, :fg => 8
112
+
113
+ f1 = f0.clone
114
+ f1.to_s.should == "1234\n2345\n3456\n4567"
115
+ f1.fg_buffer.should == [[8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8]]
116
+ f1.bg_buffer.should == [[9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9]]
117
+
118
+ f1.cropped(rect(1,1,2,2)) {f1.fill :string => "!"}
119
+ f1.to_s.should == "1234\n2!!5\n3!!6\n4567"
120
+ f1.fg_buffer.should == [[8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8]]
121
+ f1.bg_buffer.should == [[9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9]]
122
+
123
+ f1 = f0.clone
124
+ f1.cropped(rect(1,1,2,2)) {f1.fill :bg => 0}
125
+ f1.to_s.should == "1234\n2345\n3456\n4567"
126
+ f1.fg_buffer.should == [[8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8], [8, 8, 8, 8]]
127
+ f1.bg_buffer.should == [[9, 9, 9, 9], [9, 0, 0, 9], [9, 0, 0, 9], [9, 9, 9, 9]]
128
+
129
+ f1 = f0.clone
130
+ f1.cropped(rect(1,1,2,2)) {f1.fill :fg => 0}
131
+ f1.to_s.should == "1234\n2345\n3456\n4567"
132
+ f1.fg_buffer.should == [[8, 8, 8, 8], [8, 0, 0, 8], [8, 0, 0, 8], [8, 8, 8, 8]]
133
+ f1.bg_buffer.should == [[9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9]]
134
+ end
135
+
136
+ it "dirty" do
137
+ f1 = test_frame
138
+ f1.dirty?.should == false
139
+ f1.dirty rect(1,2,3,4)
140
+ f1.dirty?.should == true
141
+
142
+ f1.dirty_area.should == rect(1,2,3,2)
143
+
144
+ s = f1.dirty_subbuffer
145
+ s.to_s.should == "456\n567"
146
+
147
+ f1.clean
148
+ f1.dirty?.should == false
149
+ end
150
+
151
+ it "on_dirty" do
152
+ f1 = test_frame
153
+
154
+ is_now_dirty = false
155
+ f1.on_dirty do
156
+ is_now_dirty = true
157
+ end
158
+ is_now_dirty.should == false
159
+ f1.dirty
160
+ is_now_dirty.should == true
161
+ end
162
+
163
+ it "color buffers" do
164
+ f1 = test_frame
165
+
166
+ f1.fg_buffer.should == [[7, 7, 7, 7], [7, 7, 7, 7], [7, 7, 7, 7], [7, 7, 7, 7]]
167
+ f1.bg_buffer.should == [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
168
+ end
169
+ end
170
+ end