textbringer 0.1.0

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