window_blessing 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in window_blessing.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,7 @@
1
+ guard 'rspec', :cli => "--color" do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+
6
+ end
7
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Shane Brinkman-Davis
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # WindowBlessing
2
+
3
+ Forget Curses! Try Blessings instead! WindowBlessing is an evented windowing framework for terminal apps.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'window_blessing'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install window_blessing
18
+
19
+ ## Potentially useful Unicode characters
20
+ http://www.csbruce.com/software/utf-8.html
21
+
22
+ * ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹
23
+ * ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋
24
+ * ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬
25
+ * ◠ ◡ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮
26
+ * ◰ ◱ ◲ ◳ ◴ ◵ ◶ ◷ ◸ ◹ ◺ ◿
27
+ * ▀▁▂▃▄▅▆▇█
28
+ * ▉▊▋▌▍▎▏▐
29
+ * ░▒▓
30
+ * ▔▕▖▗▘▙▚▛▜▝▞▟
31
+ * ▸▾
32
+
33
+ ## Usage
34
+
35
+ TODO: Write usage instructions here
36
+
37
+ ## Contributing
38
+
39
+ 1. Fork it
40
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
41
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
42
+ 4. Push to the branch (`git push origin my-new-feature`)
43
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task :default => :spec
5
+
6
+ desc "Run specs"
7
+ RSpec::Core::RakeTask.new do |task|
8
+ task.pattern = "**/spec/*_spec.rb"
9
+ task.rspec_opts = Dir.glob("[0-9][0-9][0-9]_*").collect { |x| "-I#{x}" }.sort
10
+ task.rspec_opts << '--color'
11
+ task.rspec_opts << '-f documentation'
12
+ end
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path File.join(File.dirname(__FILE__), %w{.. lib window_blessing})
3
+ include GuiGeo
4
+ include WindowBlessing::Tools
5
+
6
+ def colorful_buffer(size)
7
+ WindowBlessing::Buffer.new(size).tap do |buffer|
8
+ size.y.times do |y|
9
+ size.x.times do |x|
10
+ c1 = rgb_screen_color y / (size.y-1).to_f, x / (size.x-1).to_f, 0
11
+ c2 = rgb_screen_color 0, 1 - y / (size.y-1).to_f, 1 - x / (size.x-1).to_f
12
+ buffer.draw_rect rect(point(x,y),point(1,1)), :bg => c1, :fg => c2, :string => "o"
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ def gray_buffer(size)
19
+ WindowBlessing::Buffer.new(size).tap do |buffer|
20
+ size.y.times do |y|
21
+ size.x.times do |x|
22
+ g1 = gray_screen_color(x / (size.x-1).to_f)
23
+ g2 = gray_screen_color(y / (size.y-1).to_f)
24
+ buffer.draw_rect rect(point(x,y),point(1,1)), :bg => g1, :fg => g2, :string => "o"
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def draw_instructions(screen)
31
+ b = WindowBlessing::Buffer.new point(screen.state.size.x,1), contents: " Arrows, Home, End, PgUp, PgDown or drag with Mouse to move. Space to toggle. Ctrl-Q to quit.", bg: gray_screen_color(0.6), fg: rgb_screen_color(0.8,0.8,1.0)
32
+
33
+ screen.screen_buffer.draw_buffer point, b
34
+ end
35
+
36
+ WindowBlessing::BufferedScreen.new.start(:full=>true) do |screen|
37
+ r = rect 10, 10, 6, 3
38
+
39
+ grayb = gray_buffer(point 23,12)
40
+ colorb = colorful_buffer(point 12,6)
41
+
42
+ demo_buffer = colorb
43
+ screen.screen_buffer.draw_buffer r.loc, demo_buffer
44
+ draw_instructions(screen)
45
+
46
+ r.size = demo_buffer.size
47
+ old_r = r.clone
48
+
49
+ screen.event_manager.add_handler :tick do |event|
50
+ if r != old_r
51
+ r = rect(point(0,1), screen.state.size-point(0,1)).bound(r)
52
+ if r != old_r
53
+ screen.screen_buffer.draw_rect old_r, :string => " ", :bg => 0
54
+ screen.screen_buffer.draw_buffer r.loc, demo_buffer
55
+ old_r = r.clone
56
+ end
57
+ end
58
+ end
59
+
60
+ screen.event_manager.add_handler :characters do |event|
61
+ case event[:raw]
62
+ when " " then
63
+ demo_buffer = demo_buffer == grayb ? colorb : grayb
64
+ r.size = demo_buffer.size
65
+ end
66
+ end
67
+
68
+ screen.event_manager.add_handler :key_press do |event|
69
+ WindowBlessing::XtermLog.log "key_press: #{event[:key]}"
70
+ case event[:key]
71
+ when :home then r.loc.x = 0
72
+ when :page_up then r.loc.y = 0
73
+ when :page_down then r.loc.y = screen.state.size.y
74
+ when :end then r.loc.x = screen.state.size.x
75
+ when :left then r.loc.x -= 1
76
+ when :right then r.loc.x += 1
77
+ when :up then r.loc.y -= 1
78
+ when :down then r.loc.y += 1
79
+ end
80
+ end
81
+
82
+ screen.event_manager.add_last_handler :resize do |event|
83
+ WindowBlessing::XtermLog.log "#{__FILE__} resize: #{event.inspect}"
84
+ draw_instructions screen
85
+ screen.screen_buffer.draw_buffer r.loc, demo_buffer
86
+ end
87
+
88
+ screen.event_manager.add_handler :pointer do |event|
89
+ WindowBlessing::XtermLog.log "mouse: drag #{event[:loc]}"
90
+ r.loc = event[:loc] - demo_buffer.size/2
91
+ end
92
+ end
@@ -0,0 +1,176 @@
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::Tools
6
+ include WindowBlessing
7
+ include Widgets
8
+
9
+ class FadeSlider < Slider
10
+ attr_accessor_with_redraw :c1, :c2
11
+ def initialize(area, ev, c1, c2)
12
+ super area, ev, key_press_step: 1/20.0
13
+ @c1 = c1
14
+ @c2 = c2
15
+ end
16
+
17
+ def draw_background
18
+ size = buffer.size
19
+ step = (c2 - c1) / (size.x-1).to_f
20
+ line = size.x.times.collect do |x|
21
+ (c1 + step * x).to_screen_color
22
+ end
23
+ buffer.bg_buffer = size.y.times.collect {line.clone}
24
+ buffer.fill :string => " ", :fg => value > 0.5 ? Color.black : Color.white
25
+ end
26
+ end
27
+
28
+ class ColorPicker2D < Window
29
+ include Evented
30
+ attr_accessor_with_redraw :fixed_channel
31
+ attr_reader :color_ev
32
+
33
+ def initialize(area,color_ev,fixed_channel = :g)
34
+ super area
35
+ @color_ev = color_ev
36
+ @fixed_channel = fixed_channel
37
+ request_redraw_internal
38
+
39
+ on :pointer do |event|
40
+ loc = event[:loc]
41
+ color = color_ev.get
42
+ old_color = color.clone
43
+
44
+ chan1, chan2 = variable_channels
45
+ p = loc / (size - point(1.0,1.0))
46
+ color[chan2] = bound(0.0, p.x, 1.0)
47
+ color[chan1] = bound(0.0, p.y, 1.0)
48
+
49
+ color_ev.set color
50
+ end
51
+
52
+ color_ev.on :refresh do
53
+ request_redraw_internal
54
+ end
55
+ end
56
+
57
+ def variable_channels
58
+ [:r, :g, :b].select {|a| a!=fixed_channel}
59
+ end
60
+
61
+ def draw_background
62
+ chan1, chan2 = variable_channels
63
+ color = color_ev.get
64
+ s = size
65
+ c = color.clone
66
+ c[chan1] = 0
67
+ c[chan2] = 0
68
+ buffer.bg_buffer = s.y.times.collect do |y|
69
+ c[chan1] = c1 = y / (s.y-1.0)
70
+ s.x.times.collect do |x|
71
+ c[chan2] = c2 =x / (s.x-1.0)
72
+ c.to_screen_color
73
+ end
74
+ end
75
+
76
+ l = point color[chan2]*(size.x-1), color[chan1]*(size.y-1)
77
+ buffer.fill :string => " "
78
+ buffer.fill :area => rect(l.x.to_i,l.y.to_i,1,1), :string => "+"
79
+ end
80
+ end
81
+
82
+ class ColorPreview < Window
83
+ def initialize(area, color_ev)
84
+ super area
85
+ @color_ev = color_ev
86
+
87
+ color_ev.on :refresh do
88
+ request_redraw_internal
89
+ end
90
+ end
91
+
92
+ def draw_background
93
+ buffer.fill :string => "Mr. Hungerton, her father, really was the most tactless person upon earth, -- a fluffy, feathery, untidy cockatoo of a man, perfectly good-natured, but absolutely centered upon his own silly self. If anything could have driven me from Gladys, it would have been the thought of such a father-in-law. I am convinced that he really believed in his heart that I came round to the Chestnuts three days a week for the pleasure of his company, and very especially to hear his views upon bimetallism, a subject upon which he was by way of being an authority. ",
94
+ :bg => @color_ev.get
95
+ end
96
+ end
97
+
98
+ class ColorPicker < Window
99
+ include DraggableBackground
100
+ attr_accessor :gray_slider, :color_preview, :color2d
101
+ attr_accessor :color_ev, :r_ev, :g_ev, :b_ev, :gray_ev
102
+
103
+ def create_evented_variables(initial_color)
104
+ @color_ev = EventedVariable.new(initial_color)
105
+ @r_ev = EventedVariable.new(initial_color.r).on(:change) {|event| color = color_ev.get; color.r = event[:value]; color_ev.set color}
106
+ @g_ev = EventedVariable.new(initial_color.g).on(:change) {|event| color = color_ev.get; color.g = event[:value]; color_ev.set color}
107
+ @b_ev = EventedVariable.new(initial_color.b).on(:change) {|event| color = color_ev.get; color.b = event[:value]; color_ev.set color}
108
+ @gray_ev = EventedVariable.new(initial_color.br).on(:change) {|event| color_ev.set color(event[:value]) }
109
+
110
+ color_ev.on(:change) do |event|
111
+ color = event[:value]
112
+ r_ev.set color.r
113
+ g_ev.set color.g
114
+ b_ev.set color.b
115
+ gray_ev.refresh color.br
116
+
117
+ update_label
118
+ @r_value_field.text = color.r256.to_s if @r_value_field.text != color.r256.to_s
119
+ @g_value_field.text = color.g256.to_s if @g_value_field.text != color.g256.to_s
120
+ @b_value_field.text = color.b256.to_s if @b_value_field.text != color.b256.to_s
121
+ end
122
+ end
123
+
124
+ def current_color
125
+ @color_ev.get
126
+ end
127
+
128
+ def initialize *args
129
+ super rect(2,2,60,30)
130
+ self.bg = gray_screen_color 0.2
131
+ self.fg = gray_screen_color 0.5
132
+
133
+ create_evented_variables(Color.white)
134
+
135
+ add_child Label.new(rect(2,0,100,1),"Color Picker - Ctrl-Q to quit", :bg => self.bg, :fg => self.fg)
136
+
137
+ @r_slider = add_child FadeSlider.new(rect(10,area.size.y - 10,25,1), r_ev, Color.black, Color.red )
138
+ @g_slider = add_child FadeSlider.new(rect(10,area.size.y - 8,25,1), g_ev, Color.black, Color.green)
139
+ @b_slider = add_child FadeSlider.new(rect(10,area.size.y - 6,25,1), b_ev, Color.black, Color.blue )
140
+ @gray_slider = add_child FadeSlider.new(rect(10,area.size.y - 4,25,1), gray_ev, Color.black, Color.white)
141
+
142
+ text_field_options = {:bg => color(0.1), :fg => Color.gray, :validator => /^[0-9]{0,3}$/}
143
+ @r_value_field = add_child TextField.new(rect(2,area.size.y - 10,5,1), "123", text_field_options)
144
+ @g_value_field = add_child TextField.new(rect(2,area.size.y - 8 ,5,1), "123", text_field_options)
145
+ @b_value_field = add_child TextField.new(rect(2,area.size.y - 6 ,5,1), "123", text_field_options)
146
+
147
+ @color2d = add_child ColorPicker2D.new(rect(area.size.x-15,area.size.y-9,12,6), color_ev)
148
+
149
+ @color_preview = add_child ColorPreview.new(rect(2,2,area.size.x - 20, area.size.y - 14), color_ev)
150
+
151
+ @color_info_label = add_child Label.new(rect(2,area.size.y - 2,100,1),"info", :bg => self.bg, :fg => rgb_screen_color(1,1,1))
152
+
153
+ @r_slider.on(:pointer, :button1_down) {@color2d.fixed_channel = :r}
154
+ @g_slider.on(:pointer, :button1_down) {@color2d.fixed_channel = :g}
155
+ @b_slider.on(:pointer, :button1_down) {@color2d.fixed_channel = :b}
156
+
157
+ @r_value_field.evented_value.on(:change) {|event|r_ev.set bound(0.0,event[:value].to_i/255.0,1.0)}
158
+ @g_value_field.evented_value.on(:change) {|event|g_ev.set bound(0.0,event[:value].to_i/255.0,1.0)}
159
+ @b_value_field.evented_value.on(:change) {|event|b_ev.set bound(0.0,event[:value].to_i/255.0,1.0)}
160
+
161
+ @color_info_label.name = "info_label"
162
+
163
+ color_ev.set Color.black
164
+
165
+ update_label
166
+ end
167
+
168
+ def update_label
169
+ @color_info_label.text = "Color: #{current_color.to_hex} / (#{"%.2f, %.2f, %.2f"%current_color.to_a}) / (#{current_color.to_a256.join(', ')})"
170
+ end
171
+ end
172
+
173
+ WindowedScreen.new.start(:full=>true, :utf8 => true) do |screen|
174
+ screen.root_window.add_child ColorPicker.new
175
+ screen.root_window.add_child(ColorPicker.new).loc = point
176
+ end
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path File.join(File.dirname(__FILE__), %w{.. lib window_blessing})
3
+
4
+
5
+ WindowBlessing.main do |main_window|
6
+ # raise main_window.inspect
7
+ (c1 = window(rect(10,10,15,7))).background='-='
8
+ main_window.add_child c1
9
+
10
+ on_key do |key|
11
+ case key
12
+ when ?Q, ?q then break
13
+ when Curses::Key::UP then c1.area += point(0,-1)
14
+ when Curses::Key::DOWN then c1.area += point(0,1)
15
+ when Curses::Key::LEFT then c1.area += point(-1,0)
16
+ when Curses::Key::RIGHT then c1.area += point(1,0)
17
+ end
18
+ end
19
+
20
+ on_tick do
21
+ time = Time.now
22
+ if time.sec != @last_sec
23
+ write point(0,0), time.to_s
24
+ @last_sec = time.sec
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,292 @@
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::Tools
6
+ include WindowBlessing
7
+ include Widgets
8
+
9
+ class Theme
10
+ include WindowBlessing::Tools
11
+ COLORS = {
12
+ background: color("ffc").to_screen_color,
13
+ foreground: color(0.25).to_screen_color,
14
+ comment: color(0.5).to_screen_color,
15
+ keyword: color("4a744a").to_screen_color,
16
+ string: color("4a7494").to_screen_color,
17
+ number: color("4a7494").to_screen_color,
18
+ regexp: color("4a7494").to_screen_color,
19
+ constant: color("c96600").to_screen_color,
20
+ operator: color("4a744a").to_screen_color
21
+ }
22
+ end
23
+
24
+ class CodeMarkup < BabelBridge::Parser
25
+
26
+ rule :file, :space, many?(:element) do
27
+ def colors
28
+ [[Theme::COLORS[:foreground]]*space.match_length,element.collect{|a| a.colors}].flatten
29
+ end
30
+ end
31
+
32
+ rule :element, :comment, :space do
33
+ def colors; [Theme::COLORS[:comment]] * match_length; end
34
+ end
35
+
36
+ rule :element, :keyword, :space do
37
+ def colors; [Theme::COLORS[:keyword]] * match_length; end
38
+ end
39
+
40
+ rule :element, :string, :space do
41
+ def colors; [Theme::COLORS[:string]] * match_length; end
42
+ end
43
+
44
+ rule :element, :regexp, :space do
45
+ def colors; [Theme::COLORS[:regex]] * match_length; end
46
+ end
47
+
48
+ rule :element, :constant, :space do
49
+ def colors; [Theme::COLORS[:constant]] * match_length; end
50
+ end
51
+
52
+ rule :element, :operator, :space do
53
+ def colors; [Theme::COLORS[:operator]] * match_length; end
54
+ end
55
+
56
+ rule :element, :number, :space do
57
+ def colors; [Theme::COLORS[:number]] * match_length; end
58
+ end
59
+
60
+ rule :element, :identifier, :space do
61
+ def colors; [Theme::COLORS[:foreground]] * match_length; end
62
+ end
63
+
64
+ rule :element, :non_space, :space do
65
+ def colors; [Theme::COLORS[:foreground]] * match_length; end
66
+ end
67
+
68
+ rule :space, /\s*/
69
+ rule :number, /[0-9]+(\.[0-9]+)?/
70
+ rule :comment, /#([^\n]|$)*/
71
+ rule :string, /"(\\.|[^\\"])*"/
72
+ rule :string, /:[_a-zA-Z0-9]+[?!]?/
73
+ rule :regexp, /\/(\\.|[^\\\/])*\//
74
+ rule :operator, /[-!@$%^&*()_+={}|\[\];:<>\?,\.\/~]+/
75
+ rule :keyword, /class|end|def|and|or|do|if|then/
76
+ rule :keyword, /else|elsif|case|then|when|require|include/
77
+ rule :identifier, /[_a-zA-Z][0-9_a-zA-Z]*/
78
+ rule :constant, /[A-Z][0-9_a-zA-Z]*/
79
+ rule :non_space, /[^\s]/
80
+ end
81
+
82
+ class TextEditor < Window
83
+ attr_accessor_with_redraw :edit_buffer, :cursor_loc, :cursor_bg
84
+
85
+ def initialize(filename)
86
+ super rect(0,0,80,20)
87
+ self.bg = Theme::COLORS[:background]
88
+ self.fg = Theme::COLORS[:foreground]
89
+ @cursor_loc = point
90
+ @cursor_bg = Color.yellow
91
+
92
+ init_event_handlers
93
+ self.text = File.read(filename)
94
+
95
+ on :parent_resize do |event|
96
+ self.size = event[:size]
97
+ end
98
+ on :parent_set do |event|
99
+ self.size = parent.size
100
+ end
101
+ end
102
+
103
+ def init_event_handlers
104
+ on :string_input do |event|
105
+ string = event[:string]
106
+ line = current_line
107
+ x,y = cursor_loc.x,cursor_loc.y
108
+ if x > line.length
109
+ line += " "*(x - line.length)
110
+ end
111
+ new_line = if x == 0
112
+ string + line
113
+ elsif x == line.length
114
+ line + string
115
+ else
116
+ line[0..x-1] + string + line[x..-1]
117
+ end
118
+ new_lines = new_line.split "\n"
119
+
120
+ #cop-out for now
121
+ edit_buffer[cursor_loc.y]= new_lines[0]
122
+ request_redraw_internal(rect(cursor_loc,point(size.x,1)))
123
+ cursor_loc.x += string.length
124
+ color_lines[y]=nil
125
+ end
126
+
127
+ on :key_press do |event|
128
+ c = cursor_loc.clone
129
+ case event[:key]
130
+ when :control_m then c = newline_at_cursor
131
+ when :backspace then c = backspace
132
+ when :home then c = point(0,c.y)
133
+ when :page_up then #c = point(0,c.y)
134
+ when :page_down then #c = point(0,c.y)
135
+ when :end then c = point(edit_buffer[c.y].length,c.y)
136
+ when :left then
137
+ if c.x == 0
138
+ if c.y > 0
139
+ c.y-=1
140
+ c.x = edit_buffer[c.y].length
141
+ end
142
+ else
143
+ c.x -= 1
144
+ end
145
+ when :right then c.x += 1
146
+ when :up then c.y = max(0, c.y-1)
147
+ when :down then c.y = min(c.y+1, edit_buffer.length-1)
148
+ end
149
+ self.cursor_loc = c
150
+ end
151
+
152
+ on :focus do
153
+ request_redraw_internal
154
+ end
155
+
156
+ on :blur do
157
+ request_redraw_internal
158
+ end
159
+ end
160
+
161
+ def cursor_area
162
+ rect(cursor_loc - scroll_pos,point(1,1))
163
+ end
164
+
165
+ def cursor_loc=(c)
166
+ request_redraw_internal cursor_area
167
+ @cursor_loc = c
168
+ request_redraw_internal cursor_area
169
+ end
170
+
171
+ def newline_at_cursor
172
+ c = cursor_loc.clone
173
+ x,y = c.x, c.y
174
+ if x == 0
175
+ edit_buffer.insert(y,"")
176
+ color_lines.insert(y,nil)
177
+ elsif x >= current_line.length
178
+ edit_buffer.insert(y+1,"")
179
+ color_lines.insert(y+1,nil)
180
+ else
181
+ l = current_line
182
+ edit_buffer.insert(y,l[0..x-1])
183
+ color_lines.insert(y,nil)
184
+ edit_buffer[y+1] = l[x..-1]
185
+ end
186
+ color_lines[y]=color_lines[y+1]=nil
187
+ request_redraw_internal
188
+ point(0,y+1)
189
+ end
190
+
191
+ def backspace
192
+ c = cursor_loc.clone
193
+ x,y = c.x, c.y
194
+ if x == 0
195
+ if y > 0
196
+ c.x = edit_buffer[y-1].length
197
+ edit_buffer[y-1] += edit_buffer[y]
198
+ color_lines[y-1] = nil
199
+ c.y -= 1
200
+
201
+ edit_buffer.delete_at(y)
202
+ color_lines.delete_at(y)
203
+
204
+ request_redraw_internal
205
+ end
206
+ else
207
+ edit_buffer[y] = if x == 1
208
+ current_line[1..-1]
209
+ elsif x == current_line.length
210
+ current_line[0..-2]
211
+ else
212
+ current_line[0..x-2] + current_line[x..-1]
213
+ end
214
+ c.x -= 1
215
+ request_redraw_current_line
216
+ end
217
+ color_lines[y]=nil
218
+ c
219
+ end
220
+
221
+ def request_redraw_current_line
222
+ request_redraw_internal current_line_area
223
+ end
224
+
225
+ def line_area(line_number)
226
+ rect(point(0,line_number),point(edit_buffer[line_number].length,1))
227
+ end
228
+
229
+ def current_line_area
230
+ line_area(cursor_loc.y)
231
+ end
232
+
233
+ def current_line
234
+ edit_buffer[cursor_loc.y]
235
+ end
236
+
237
+ def text
238
+ edit_buffer.join("\n")
239
+ end
240
+
241
+ def text=(t)
242
+ self.edit_buffer = t.split("\n")
243
+ end
244
+
245
+ def scroll_pos
246
+ point
247
+ end
248
+
249
+ def line_range
250
+ scroll_pos.y..scroll_pos.y+size.y-1
251
+ end
252
+
253
+ def visible_lines
254
+ edit_buffer[line_range]
255
+ end
256
+
257
+ def color_lines
258
+ @color_lines ||= []
259
+ end
260
+
261
+ def draw_background
262
+ syntax_highlighter = CodeMarkup.new
263
+ crop_area = buffer.crop_area
264
+
265
+ range = (crop_area+scroll_pos).y_range
266
+ lines = edit_buffer[range]
267
+ clines = color_lines[range]
268
+
269
+ lines.each_with_index do |a,i|
270
+ clines[i] ||= begin
271
+ p = syntax_highlighter.parse(a.clone)
272
+ log syntax_highlighter.parser_failure_info unless p
273
+ p.colors
274
+ end
275
+ buffer.contents[i+crop_area.y] = a
276
+ buffer.fg_buffer[i+crop_area.y] = clines[i]
277
+ end
278
+
279
+ color_lines[range] = clines
280
+
281
+ buffer.sanitize_contents crop_area.y_range
282
+ buffer.normalize crop_area.y_range
283
+ buffer.fill bg:bg
284
+ buffer.fill :area => cursor_area, :bg => cursor_bg if focused?
285
+ end
286
+ end
287
+
288
+ WindowedScreen.new.start(:full=>true, :utf8 => true) do |screen|
289
+ filename = __FILE__
290
+ screen.root_window.add_child te=TextEditor.new(filename)
291
+ te.focus
292
+ end