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.
- 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
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
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
|
data/bin/foiled_demo.rb
ADDED
@@ -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
|