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
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