textbringer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,646 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ncursesw"
4
+ require "unicode/display_width"
5
+
6
+ module Textbringer
7
+ class Window
8
+ KEY_NAMES = {}
9
+ Ncurses.constants.grep(/\AKEY_/).each do |name|
10
+ KEY_NAMES[Ncurses.const_get(name)] =
11
+ name.slice(/\AKEY_(.*)/, 1).downcase.intern
12
+ end
13
+
14
+ UTF8_CHAR_LEN =
15
+ Buffer::UTF8_CHAR_LEN.each_with_object(Hash.new(1)) { |(k, v), h|
16
+ h[k.ord] = v
17
+ }
18
+
19
+ @@windows = []
20
+ @@current = nil
21
+ @@echo_area = nil
22
+
23
+ def self.windows
24
+ @@windows
25
+ end
26
+
27
+ def self.current
28
+ @@current
29
+ end
30
+
31
+ def self.current=(window)
32
+ if window.deleted?
33
+ window = @@windows.first
34
+ end
35
+ @@current.save_point if @@current && !@@current.deleted?
36
+ @@current = window
37
+ @@current.restore_point
38
+ Buffer.current = window.buffer
39
+ end
40
+
41
+ def self.delete_window
42
+ if @@current.echo_area?
43
+ raise EditorError, "Can't delete the echo area"
44
+ end
45
+ if @@windows.size == 2
46
+ raise EditorError, "Can't delete the sole window"
47
+ end
48
+ i = @@windows.index(@@current)
49
+ if i == 0
50
+ window = @@windows[1]
51
+ window.move(0, 0)
52
+ else
53
+ window = @@windows[i - 1]
54
+ end
55
+ window.resize(@@current.lines + window.lines, window.columns)
56
+ @@current.delete
57
+ @@windows.delete_at(i)
58
+ self.current = window
59
+ end
60
+
61
+ def self.delete_other_windows
62
+ if @@current.echo_area?
63
+ raise EditorError, "Can't expand the echo area to full screen"
64
+ end
65
+ @@windows.delete_if do |window|
66
+ if window.current? || window.echo_area?
67
+ false
68
+ else
69
+ window.delete
70
+ true
71
+ end
72
+ end
73
+ @@current.move(0, 0)
74
+ @@current.resize(Window.lines - 1, @@current.columns)
75
+ end
76
+
77
+ def self.other_window
78
+ i = @@windows.index(@@current)
79
+ begin
80
+ i += 1
81
+ window = @@windows[i % @@windows.size]
82
+ end while !window.active?
83
+ self.current = window
84
+ end
85
+
86
+ def self.echo_area
87
+ @@echo_area
88
+ end
89
+
90
+ def self.start
91
+ Ncurses.initscr
92
+ Ncurses.noecho
93
+ Ncurses.raw
94
+ begin
95
+ window =
96
+ Textbringer::Window.new(Window.lines - 1, Window.columns, 0, 0)
97
+ window.buffer = Buffer.new_buffer("*scratch*")
98
+ @@windows.push(window)
99
+ Window.current = window
100
+ @@echo_area = Textbringer::EchoArea.new(1, Window.columns,
101
+ Window.lines - 1, 0)
102
+ Buffer.minibuffer.keymap = MINIBUFFER_LOCAL_MAP
103
+ @@echo_area.buffer = Buffer.minibuffer
104
+ @@windows.push(@@echo_area)
105
+ yield
106
+ ensure
107
+ Ncurses.echo
108
+ Ncurses.noraw
109
+ Ncurses.endwin
110
+ end
111
+ end
112
+
113
+ def self.redisplay
114
+ @@windows.each do |window|
115
+ window.redisplay unless window.current?
116
+ end
117
+ current.redisplay
118
+ update
119
+ end
120
+
121
+ def self.redraw
122
+ @@windows.each do |window|
123
+ window.redraw unless window.current?
124
+ end
125
+ current.redraw
126
+ update
127
+ end
128
+
129
+ def self.update
130
+ Ncurses.doupdate
131
+ end
132
+
133
+ def self.lines
134
+ Ncurses.LINES
135
+ end
136
+
137
+ def self.columns
138
+ Ncurses.COLS
139
+ end
140
+
141
+ def self.resize
142
+ @@windows.delete_if do |window|
143
+ if window.y > Window.lines - 4
144
+ window.delete
145
+ true
146
+ else
147
+ false
148
+ end
149
+ end
150
+ @@windows.each_with_index do |window, i|
151
+ if i < @@windows.size - 1
152
+ window.resize(window.lines, Window.columns)
153
+ else
154
+ window.resize(Window.lines - 1 - window.y, Window.columns)
155
+ end
156
+ end
157
+ @@echo_area.move(Window.lines - 1, 0)
158
+ @@echo_area.resize(1, Window.columns)
159
+ end
160
+
161
+ def self.beep
162
+ Ncurses.beep
163
+ end
164
+
165
+ attr_reader :buffer, :lines, :columns, :y, :x
166
+
167
+ def initialize(lines, columns, y, x)
168
+ @lines = lines
169
+ @columns = columns
170
+ @y = y
171
+ @x = x
172
+ initialize_window(lines, columns, y, x)
173
+ @window.keypad(true)
174
+ @window.scrollok(false)
175
+ @window.idlok(true)
176
+ @buffer = nil
177
+ @top_of_window = nil
178
+ @bottom_of_window = nil
179
+ @point_mark = nil
180
+ @deleted = false
181
+ end
182
+
183
+ def echo_area?
184
+ false
185
+ end
186
+
187
+ def active?
188
+ true
189
+ end
190
+
191
+ def deleted?
192
+ @deleted
193
+ end
194
+
195
+ def delete
196
+ unless @deleted
197
+ if current?
198
+ Window.current = @@windows.first
199
+ end
200
+ delete_marks
201
+ @window.del
202
+ @deleted = true
203
+ end
204
+ end
205
+
206
+ def buffer=(buffer)
207
+ delete_marks
208
+ @buffer = buffer
209
+ @top_of_window = @buffer.new_mark(@buffer.point_min)
210
+ if @buffer[:top_of_window]
211
+ @top_of_window.location = @buffer[:top_of_window].location
212
+ end
213
+ @bottom_of_window = @buffer.new_mark(@buffer.point_min)
214
+ if @buffer[:bottom_of_window]
215
+ @bottom_of_window.location = @buffer[:bottom_of_window].location
216
+ end
217
+ @point_mark = @buffer.new_mark
218
+ end
219
+
220
+ def save_point
221
+ @buffer[:top_of_window] ||= @buffer.new_mark
222
+ @buffer[:top_of_window].location = @top_of_window.location
223
+ @buffer[:bottom_of_window] ||= @buffer.new_mark
224
+ @buffer[:bottom_of_window].location = @bottom_of_window.location
225
+ @buffer.mark_to_point(@point_mark)
226
+ end
227
+
228
+ def restore_point
229
+ @buffer.point_to_mark(@point_mark)
230
+ end
231
+
232
+ def current?
233
+ self == @@current
234
+ end
235
+
236
+ def getch
237
+ key = @window.getch
238
+ if key.nil?
239
+ nil
240
+ elsif key > 0xff
241
+ KEY_NAMES[key]
242
+ else
243
+ len = UTF8_CHAR_LEN[key]
244
+ if len == 1
245
+ key
246
+ else
247
+ buf = [key]
248
+ (len - 1).times do
249
+ c = @window.getch
250
+ if c.nil? || c < 0x80 || c > 0xbf
251
+ raise EditorError, "Malformed UTF-8 input"
252
+ end
253
+ buf.push(c)
254
+ end
255
+ s = buf.pack("C*").force_encoding(Encoding::UTF_8)
256
+ if s.valid_encoding?
257
+ s.ord
258
+ else
259
+ raise EditorError, "Malformed UTF-8 input"
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ def getch_nonblock
266
+ @window.nodelay(true)
267
+ begin
268
+ getch
269
+ ensure
270
+ @window.nodelay(false)
271
+ end
272
+ end
273
+
274
+ def wait_input(msecs)
275
+ @window.timeout(msecs)
276
+ begin
277
+ c = @window.getch
278
+ if c && c >= 0
279
+ Ncurses.ungetch(c)
280
+ end
281
+ c
282
+ ensure
283
+ @window.timeout(-1)
284
+ end
285
+ end
286
+
287
+ def redisplay
288
+ return if @buffer.nil?
289
+ redisplay_mode_line
290
+ @buffer.save_point do |saved|
291
+ if current?
292
+ point = saved
293
+ else
294
+ point = @point_mark
295
+ @buffer.point_to_mark(@point_mark)
296
+ end
297
+ framer
298
+ y = x = 0
299
+ @buffer.point_to_mark(@top_of_window)
300
+ @window.erase
301
+ @window.move(0, 0)
302
+ if current? && @buffer.visible_mark &&
303
+ @buffer.point_after_mark?(@buffer.visible_mark)
304
+ @window.attron(Ncurses::A_REVERSE)
305
+ end
306
+ while !@buffer.end_of_buffer?
307
+ if @buffer.point_at_mark?(point)
308
+ y, x = @window.getcury, @window.getcurx
309
+ if current? && @buffer.visible_mark
310
+ if @buffer.point_after_mark?(@buffer.visible_mark)
311
+ @window.attroff(Ncurses::A_REVERSE)
312
+ elsif @buffer.point_before_mark?(@buffer.visible_mark)
313
+ @window.attron(Ncurses::A_REVERSE)
314
+ end
315
+ end
316
+ end
317
+ if current? && @buffer.visible_mark &&
318
+ @buffer.point_at_mark?(@buffer.visible_mark)
319
+ if @buffer.point_after_mark?(point)
320
+ @window.attroff(Ncurses::A_REVERSE)
321
+ elsif @buffer.point_before_mark?(point)
322
+ @window.attron(Ncurses::A_REVERSE)
323
+ end
324
+ end
325
+ c = @buffer.char_after
326
+ if c == "\n"
327
+ @window.clrtoeol
328
+ break if @window.getcury == lines - 2 # lines include mode line
329
+ elsif c == "\t"
330
+ n = calc_tab_width(@window.getcurx)
331
+ c = " " * n
332
+ else
333
+ c = escape(c)
334
+ end
335
+ @window.addstr(c)
336
+ break if @window.getcury == lines - 2 && # lines include mode line
337
+ @window.getcurx == columns
338
+ @buffer.forward_char
339
+ end
340
+ if current? && @buffer.visible_mark
341
+ @window.attroff(Ncurses::A_REVERSE)
342
+ end
343
+ @buffer.mark_to_point(@bottom_of_window)
344
+ if @buffer.point_at_mark?(point)
345
+ y, x = @window.getcury, @window.getcurx
346
+ end
347
+ if x == columns - 1
348
+ c = @buffer.char_after(point.location)
349
+ if c && Buffer.display_width(c) > 1
350
+ y += 1
351
+ x = 0
352
+ end
353
+ end
354
+ @window.move(y, x)
355
+ @window.noutrefresh
356
+ end
357
+ end
358
+
359
+ def redraw
360
+ @window.redrawwin
361
+ @mode_line.redrawwin
362
+ end
363
+
364
+ def move(y, x)
365
+ @y = y
366
+ @x = x
367
+ @window.mvwin(y, x)
368
+ @mode_line.mvwin(y + @window.getmaxy, x)
369
+ end
370
+
371
+ def resize(lines, columns)
372
+ @lines = lines
373
+ @columns = columns
374
+ @window.resize(lines - 1, columns)
375
+ @mode_line.mvwin(@y + lines - 1, @x)
376
+ @mode_line.resize(1, columns)
377
+ end
378
+
379
+ def recenter
380
+ @buffer.save_point do |saved|
381
+ max = (lines - 1) / 2
382
+ count = beginning_of_line_and_count(max)
383
+ while count < max
384
+ break if @buffer.point == 0
385
+ @buffer.backward_char
386
+ count += beginning_of_line_and_count(max - count - 1) + 1
387
+ end
388
+ @buffer.mark_to_point(@top_of_window)
389
+ end
390
+ end
391
+
392
+ def recenter_if_needed
393
+ if @buffer.point_before_mark?(@top_of_window) ||
394
+ @buffer.point_after_mark?(@bottom_of_window)
395
+ recenter
396
+ end
397
+ end
398
+
399
+ def scroll_up
400
+ @buffer.point_to_mark(@bottom_of_window)
401
+ @buffer.previous_line
402
+ @buffer.beginning_of_line
403
+ @buffer.mark_to_point(@top_of_window)
404
+ end
405
+
406
+ def scroll_down
407
+ @buffer.point_to_mark(@top_of_window)
408
+ @buffer.next_line
409
+ @buffer.beginning_of_line
410
+ @top_of_window.location = 0
411
+ end
412
+
413
+ def split
414
+ if lines < 6
415
+ raise EditorError, "Window too small"
416
+ end
417
+ old_lines = lines
418
+ new_lines = (old_lines / 2.0).ceil
419
+ resize(new_lines, columns)
420
+ new_window = Window.new(old_lines - new_lines, columns, y + new_lines, x)
421
+ new_window.buffer = buffer
422
+ i = @@windows.index(self)
423
+ @@windows.insert(i + 1, new_window)
424
+ end
425
+
426
+ private
427
+
428
+ def initialize_window(num_lines, num_columns, y, x)
429
+ @window = Ncurses::WINDOW.new(num_lines - 1, num_columns, y, x)
430
+ @mode_line = Ncurses::WINDOW.new(1, num_columns, y + num_lines - 1, x)
431
+ end
432
+
433
+ def framer
434
+ @buffer.save_point do |saved|
435
+ max = lines - 1 # lines include mode line
436
+ count = beginning_of_line_and_count(max)
437
+ new_start_loc = @buffer.point
438
+ if @buffer.point_before_mark?(@top_of_window)
439
+ @buffer.mark_to_point(@top_of_window)
440
+ return
441
+ end
442
+ while count < max
443
+ break if @buffer.point_at_mark?(@top_of_window)
444
+ break if @buffer.point == 0
445
+ new_start_loc = @buffer.point
446
+ @buffer.backward_char
447
+ count += beginning_of_line_and_count(max - count - 1) + 1
448
+ end
449
+ if count >= lines - 1 # lines include mode line
450
+ @top_of_window.location = new_start_loc
451
+ end
452
+ end
453
+ end
454
+
455
+ def redisplay_mode_line
456
+ @mode_line.erase
457
+ @mode_line.move(0, 0)
458
+ @mode_line.attron(Ncurses::A_REVERSE)
459
+ @mode_line.addstr("#{@buffer.name} ")
460
+ @mode_line.addstr("[+]") if @buffer.modified?
461
+ @mode_line.addstr("[RO]") if @buffer.read_only?
462
+ @mode_line.addstr("[#{@buffer.file_encoding.name}/")
463
+ @mode_line.addstr("#{@buffer.file_format}] ")
464
+ if current? || @buffer.point_at_mark?(@point_mark)
465
+ c = @buffer.char_after
466
+ line = @buffer.current_line
467
+ column = @buffer.current_column
468
+ else
469
+ c = @buffer.char_after(@point_mark.location)
470
+ line, column = @buffer.get_line_and_column(@point_mark.location)
471
+ end
472
+ @mode_line.addstr(unicode_codepoint(c))
473
+ @mode_line.addstr(" #{line},#{column}")
474
+ @mode_line.addstr(" (#{@buffer.mode&.name || 'None'})")
475
+ @mode_line.addstr(" " * (@mode_line.getmaxx - @mode_line.getcurx))
476
+ @mode_line.attroff(Ncurses::A_REVERSE)
477
+ @mode_line.noutrefresh
478
+ end
479
+
480
+ def unicode_codepoint(c)
481
+ if c.nil?
482
+ "<EOF>"
483
+ else
484
+ "U+%04X" % c.ord
485
+ end
486
+ end
487
+
488
+ def escape(s)
489
+ if @buffer.binary?
490
+ s.gsub(/[\0-\b\v-\x1f]/) { |c|
491
+ "^" + (c.ord ^ 0x40).chr
492
+ }.gsub(/[\x80-\xff]/n) { |c|
493
+ "<%02X>" % c.ord
494
+ }
495
+ else
496
+ s.gsub(/[\0-\b\v-\x1f]/) { |c|
497
+ "^" + (c.ord ^ 0x40).chr
498
+ }
499
+ end
500
+ end
501
+
502
+ def calc_tab_width(column)
503
+ tw = @buffer[:tab_width]
504
+ n = tw - column % tw
505
+ n.nonzero? || tw
506
+ end
507
+
508
+ def beginning_of_line_and_count(max_lines)
509
+ e = @buffer.point
510
+ @buffer.beginning_of_line
511
+ s = @buffer.substring(@buffer.point, e)
512
+ bols = [@buffer.point]
513
+ column = 0
514
+ while @buffer.point < e
515
+ c = @buffer.char_after
516
+ if c == ?\t
517
+ n = calc_tab_width(column)
518
+ str = " " * n
519
+ else
520
+ str = escape(c)
521
+ end
522
+ column += Buffer.display_width(str)
523
+ if column > @columns
524
+ # Don't forward_char if column > @window.columns
525
+ # to handle multibyte characters across the end of lines.
526
+ bols.push(@buffer.point)
527
+ column = 0
528
+ else
529
+ @buffer.forward_char
530
+ if column == @columns
531
+ bols.push(@buffer.point)
532
+ column = 0
533
+ end
534
+ end
535
+ end
536
+ if bols.size > max_lines
537
+ @buffer.goto_char(bols[-max_lines])
538
+ max_lines
539
+ else
540
+ @buffer.goto_char(bols.first)
541
+ bols.size - 1
542
+ end
543
+ end
544
+
545
+ def delete_marks
546
+ if @top_of_window
547
+ @top_of_window.delete
548
+ @top_of_window = nil
549
+ end
550
+ if @bottom_of_window
551
+ @bottom_of_window.delete
552
+ @bottom_of_window = nil
553
+ end
554
+ if @point_mark
555
+ @point_mark.delete
556
+ @point_mark = nil
557
+ end
558
+ end
559
+ end
560
+
561
+ class EchoArea < Window
562
+ attr_accessor :prompt
563
+ attr_writer :active
564
+
565
+ def initialize(*args)
566
+ super
567
+ @message = nil
568
+ @prompt = ""
569
+ @active = false
570
+ end
571
+
572
+ def echo_area?
573
+ true
574
+ end
575
+
576
+ def active?
577
+ @active
578
+ end
579
+
580
+ def clear
581
+ @buffer.clear
582
+ @message = nil
583
+ @prompt = ""
584
+ end
585
+
586
+ def clear_message
587
+ @message = nil
588
+ end
589
+
590
+ def show(message)
591
+ @message = message
592
+ end
593
+
594
+ def redisplay
595
+ return if @buffer.nil?
596
+ @buffer.save_point do |saved|
597
+ @window.erase
598
+ @window.move(0, 0)
599
+ if @message
600
+ @window.addstr @message
601
+ else
602
+ @window.addstr @prompt
603
+ @buffer.beginning_of_line
604
+ while !@buffer.end_of_buffer?
605
+ if @buffer.point_at_mark?(saved)
606
+ y, x = @window.getcury, @window.getcurx
607
+ end
608
+ c = @buffer.char_after
609
+ if c == "\n"
610
+ break
611
+ end
612
+ @window.addstr escape(c)
613
+ @buffer.forward_char
614
+ end
615
+ if @buffer.point_at_mark?(saved)
616
+ y, x = @window.getcury, @window.getcurx
617
+ end
618
+ @window.move(y, x)
619
+ end
620
+ @window.noutrefresh
621
+ end
622
+ end
623
+
624
+ def redraw
625
+ @window.redrawwin
626
+ end
627
+
628
+ def move(y, x)
629
+ @y = y
630
+ @x = x
631
+ @window.mvwin(y, x)
632
+ end
633
+
634
+ def resize(lines, columns)
635
+ @lines = lines
636
+ @columns = columns
637
+ @window.resize(lines, columns)
638
+ end
639
+
640
+ private
641
+
642
+ def initialize_window(num_lines, num_columns, y, x)
643
+ @window = Ncurses::WINDOW.new(num_lines, num_columns, y, x)
644
+ end
645
+ end
646
+ end
@@ -0,0 +1,10 @@
1
+ require_relative "textbringer/version"
2
+ require_relative "textbringer/config"
3
+ require_relative "textbringer/errors"
4
+ require_relative "textbringer/buffer"
5
+ require_relative "textbringer/window"
6
+ require_relative "textbringer/keymap"
7
+ require_relative "textbringer/utils"
8
+ require_relative "textbringer/commands"
9
+ require_relative "textbringer/modes"
10
+ require_relative "textbringer/controller"
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'textbringer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "textbringer"
8
+ spec.version = Textbringer::VERSION
9
+ spec.authors = ["Shugo Maeda"]
10
+ spec.email = ["shugo@ruby-lang.org"]
11
+
12
+ spec.summary = "A text editor"
13
+ spec.description = "Textbringer is a member of a demon race that takes on the form of a text editor."
14
+ spec.homepage = "https://github.com/shugo/textbringer"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency "ncursesw", "~> 1.4"
23
+ spec.add_runtime_dependency "unicode-display_width", "~> 1.1"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.11"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "simplecov"
28
+ spec.add_development_dependency "test-unit"
29
+ spec.add_development_dependency "bundler-audit"
30
+ end