rubyterm 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,580 @@
1
+ # The X11 terminal application: owns the X window, the pty controller, the
2
+ # input-processing thread and the blink/flush timers, and wires the
3
+ # interpreter (Term) + damage tracker (TrackChanges) + buffer to the X11
4
+ # backend (Window via WindowAdapter). The reusable terminal engine lives in
5
+ # the other lib/ files; this class is the executable front end (bin/rubyterm).
6
+ #
7
+ # Component classes and the X11/skrift/toml dependencies are loaded by
8
+ # lib/rubyterm.rb, which requires this file last.
9
+ class RubyTerm
10
+ TOPMOST= 0
11
+ LEFTMOST=0
12
+
13
+ attr_reader :blink_state, :rblink_state
14
+
15
+ def char_w = @adapter.char_w
16
+ def char_h = @adapter.char_h
17
+ def term_width = @term.width
18
+ def term_height = @term.height
19
+
20
+ def initconfig
21
+ cname = File.expand_path("~/.config/rterm/config.toml")
22
+ if File.exist?(cname)
23
+ @config = TomlRB.load_file(cname, symbolize_keys: true)
24
+ end
25
+ @config ||= {}
26
+ end
27
+
28
+ def inspect = "<RubyTerm #{self.object_id}>"
29
+
30
+ def get_x = @term.x
31
+ def get_y = @term.y
32
+
33
+ def initialize(args)
34
+ initconfig
35
+
36
+ @queue = Queue.new
37
+ # Coalesce redraw-causing events (resize/expose): a drag fires a
38
+ # flood of ConfigureNotify + Expose, and repainting on each one makes
39
+ # the display lag while it drains the backlog. Keep at most one
40
+ # redraw request queued, always for the latest pending size.
41
+ @redraw_mutex = Mutex.new
42
+ @redraw_pending = false
43
+ @pending_resize = nil
44
+
45
+ # DECCOLM (80/132 column switch) mode: :font rescales the glyph cell to
46
+ # fit the new column count in the current window (reliable everywhere);
47
+ # :window asks the WM to resize the window. Default :font.
48
+ @deccolm_mode = (@config[:deccolm] || "font").to_s.to_sym
49
+
50
+ @window = Window.new(fonts: @config[:fonts], fontsize: @config[:fontsize])
51
+ @adapter = WindowAdapter.new(@window, self)
52
+
53
+ # Yes, this is "bad" and we should define our
54
+ # own, however, I'd prefer to match rxvt or similar
55
+ # sufficiently that we can rely on a TERM setting that
56
+ # "everyone" already has in their termcap. rxvt seems to
57
+ # work better than xterm, but will adjust and consider
58
+ # providing multiple modes
59
+ ENV["TERM"] = "rxvt-256color"
60
+ ENV["COLORTERM"] = "truecolor"
61
+
62
+ while args[0].to_s[0] == ?-
63
+ case args[0]
64
+ when "--"
65
+ args.shift
66
+ break
67
+ when "-c"
68
+ args.shift
69
+ @term_instance = args.shift
70
+ else break
71
+ end
72
+ end
73
+
74
+ @window.dpy.change_property(:replace,
75
+ @window.wid, "WM_CLASS",
76
+ @window.dpy.atom("STRING"), 8, "rterm\0#{@term_instance||'rterm'}\0".unpack("C*"))
77
+
78
+ @window.map_window
79
+
80
+ @buffer = TrackChanges.new(TermBuffer.new, @adapter)
81
+ @buffer.defer = true # damage-driven rendering: set() mutates, flush draws
82
+ @term = Term.new(@buffer)
83
+ @buffer.on_resize(@term.width, @term.height)
84
+
85
+ # Give window access to the buffer for scrollback
86
+ @window.set_buffer(@buffer)
87
+
88
+ end
89
+
90
+ def redraw
91
+ # Always clear the entire screen before redrawing to ensure all areas are cleaned up
92
+ # This ensures no artifacts remain at the ends of lines
93
+ @window.clear(0, 0, @window.width, @window.height)
94
+
95
+ if @window.scrollback_mode
96
+ @buffer.redraw_all(@window.scrollback_count)
97
+ else
98
+ @buffer.redraw_all(0)
99
+ end
100
+
101
+ # Make sure changes are rendered and cursor is shown
102
+ @buffer.draw_flush
103
+ @term.draw_cursor
104
+ # A full repaint draws only buffer content; restore the selection
105
+ # overlay on top so it survives resizes, exposes and scrollback redraws.
106
+ reapply_selection
107
+ end
108
+
109
+ def render_text_buffer
110
+ (0...@term.height).each {|y|
111
+ puts @buffer.buffer.getline(y).map {|a| a ? a[0].chr(Encoding::UTF_8):" " }.join
112
+ }
113
+ end
114
+
115
+ def each_character(&block) = @buffer.each_character(&block)
116
+
117
+ def resize(w,h)
118
+ @pixelw||=0
119
+ @pixelh||=0
120
+ should_redraw = w >= @pixelw || h >= @pixelh
121
+ @pixelw=w
122
+ @pixelh=h
123
+ @window.on_resize(w,h)
124
+
125
+ w = w/char_w
126
+ h = h/char_h
127
+ return if w <= 0 || h <= 0 # FIXME: WTF?!?
128
+ #if w != @term.width && h == @term.height
129
+ @buffer.on_resize(w,h)
130
+ ow, oh = @term.width, @term.height
131
+
132
+ if ow != w || oh != h
133
+ @term.resize(w,h)
134
+ @controller.report_size(w,h)
135
+ end
136
+
137
+ if should_redraw
138
+ redraw
139
+ end
140
+ end
141
+
142
+ # DECCOLM: realise an 80/132 column switch (called via the adapter from
143
+ # Term#set_width_and_clear). font mode rescales the glyph cell so `cols`
144
+ # columns fit the current window, keeping the row count; window mode asks
145
+ # the WM to resize. Either way the pty is told the new size.
146
+ def set_columns(cols)
147
+ return if @deccolm_mode == :off # ignore DECCOLM entirely
148
+ cols = cols.to_i
149
+ return if cols <= 0 || @pixelw.to_i <= 0
150
+
151
+ if @deccolm_mode == :window
152
+ # WM-driven: the resulting ConfigureNotify completes the change via resize().
153
+ @window.request_pixel_size(cols * char_w, @pixelh.to_i)
154
+ return
155
+ end
156
+
157
+ # font mode. Rescale the glyph cell (up or down) so `cols` columns fit
158
+ # the current window, keeping the row count. The window is NOT resized;
159
+ # integer cell widths mean the rightmost columns may not reach the window
160
+ # edge exactly (an accepted artefact - we don't do sub-pixel placement).
161
+ rows = @term.height
162
+ @window.fit_columns(cols, @pixelw.to_i)
163
+ @buffer.on_resize(cols, rows)
164
+ @term.resize(cols, rows)
165
+ @controller.report_size(cols, rows)
166
+ redraw
167
+ end
168
+
169
+
170
+ def fg = @term.fg
171
+ def bg = @term.bg
172
+
173
+ # Escape/control/character interpretation lives in Term (lib/term.rb).
174
+ # RubyTerm only owns the X11 window, the pty controller and the
175
+ # threading; this keeps the terminal core testable headlessly (see
176
+ # harness/).
177
+
178
+ def write(str)
179
+ @queue << str
180
+ end
181
+
182
+ def process_queue = process_chunk(@queue.shift)
183
+
184
+ # Everything that touches the buffer/window runs here, on the single
185
+ # input-processing thread. The blink and flush timers enqueue :blink
186
+ # and :flush rather than touching the buffer from their own threads:
187
+ # the buffer/renderer is not thread-safe, and concurrent mutation
188
+ # (e.g. blink's redraw racing a feed mid-scroll) corrupts cells
189
+ # non-deterministically. Serializing through the queue is the
190
+ # synchronization.
191
+ # When more than this many items are already queued behind the chunk we
192
+ # just processed, we treat it as a flood and jump-scroll: keep
193
+ # interpreting (the buffer + scrollback still update) but stop rendering
194
+ # every frame. The screen catches up at the flush tick or when we drain.
195
+ JUMP_BACKLOG = 8
196
+
197
+ def process_chunk(str)
198
+ case str
199
+ when :blink then return blink
200
+ when :flush
201
+ # Mid-flood: jump the display to the current state at the flush rate
202
+ # instead of painting every scrolled-off frame.
203
+ jump_redraw if @buffer.suspend
204
+ return @window.flush
205
+ when :do_redraw then return coalesced_redraw
206
+ end
207
+
208
+ # FIXME: Could be smarter about this; it's only needed if the
209
+ # first character being written won't clear the same square.
210
+ @term.clear_cursor
211
+
212
+ @term.feed(str)
213
+
214
+ if @queue.size > JUMP_BACKLOG
215
+ # A flood is backing up. Render this frame, then suspend per-chunk
216
+ # rendering: subsequent chunks only mutate the buffer until the flush
217
+ # tick jumps the display forward or we catch up.
218
+ @buffer.draw_flush
219
+ @buffer.suspend = true
220
+ elsif @buffer.suspend
221
+ jump_redraw # drained after a flood: redraw the final state
222
+ else
223
+ # Draw the chunk's damaged content (damage-driven flush), then the
224
+ # cursor overlay on top.
225
+ @buffer.draw_flush
226
+ @term.draw_cursor
227
+ @buffer.draw_flush # Ensure everything has been rendered
228
+ # Output just repainted cells without the selection overlay; re-stamp
229
+ # it so a streaming program (top, full-screen apps) doesn't erase the
230
+ # highlight out from under an in-progress copy.
231
+ reapply_selection
232
+ end
233
+ end
234
+
235
+ # Resume rendering and repaint the whole current screen at once - the
236
+ # jump-scroll "catch up", skipping every frame that scrolled past while
237
+ # suspended.
238
+ def jump_redraw
239
+ @buffer.suspend = false
240
+ redraw
241
+ end
242
+
243
+ # Request a redraw (from the event thread). Records the latest pending
244
+ # size and enqueues a single :do_redraw marker; while one is already
245
+ # queued, further requests just update the target. This collapses a
246
+ # drag's flood of ConfigureNotify+Expose into one repaint per
247
+ # processing slot at the most recent size, instead of one per event.
248
+ def request_redraw(size = nil)
249
+ enqueue = false
250
+ @redraw_mutex.synchronize do
251
+ @pending_resize = size if size
252
+ enqueue = true unless @redraw_pending
253
+ @redraw_pending = true
254
+ end
255
+ @queue << :do_redraw if enqueue
256
+ end
257
+
258
+ # Process a coalesced redraw on the processing thread: resize to the
259
+ # latest pending size if it changed, otherwise just repaint.
260
+ def coalesced_redraw
261
+ size = @redraw_mutex.synchronize do
262
+ @redraw_pending = false
263
+ @pending_resize
264
+ end
265
+ if size && size != @last_resize
266
+ @last_resize = size
267
+ resize(size[0], size[1]) # resize repaints as needed
268
+ else
269
+ redraw
270
+ end
271
+ end
272
+
273
+ def adjust_fontsize(delta)
274
+ @window.adjust_fontsize(delta)
275
+ resize(@pixelw,@pixelh)
276
+ @window.clear(0,0,@pixelw,@pixelh)
277
+ redraw
278
+ end
279
+
280
+ def key(event)
281
+ #p event
282
+ ks, str = lookup_string(@window.dpy, event)
283
+ case ks
284
+
285
+ when :"ctrl_+" then adjust_fontsize(1.0)
286
+ when :"ctrl_-" then adjust_fontsize(-1.0)
287
+ when :shift_page_up
288
+ @window.scrollback_page_up
289
+ # Full redraw to show scrollback buffer
290
+ redraw
291
+ return
292
+ when :shift_page_down
293
+ # Don't do anything if not in scrollback mode
294
+ return if !@window.scrollback_mode
295
+
296
+ # If we're exiting scrollback mode or still in it
297
+ changed = @window.scrollback_page_down
298
+
299
+ # Redraw everything from scratch
300
+ redraw
301
+
302
+ # Explicitly force cursor to be redrawn if exiting scrollback mode
303
+ if changed
304
+ # Clear cursor if it exists
305
+ @term.clear_cursor
306
+ # Force draw cursor again
307
+ @term.draw_cursor
308
+ # Ensure changes are flushed
309
+ @buffer.draw_flush
310
+ @window.flush
311
+ end
312
+ return
313
+ when :XK_Insert # Paste primary selection
314
+ # FIXME: Giant hack
315
+ primary = `xsel -p`
316
+ if primary.chomp.empty?
317
+ primary = @primary
318
+ else
319
+ @primary = primary
320
+ end
321
+ exit_scrollback
322
+ @controller.paste(primary)
323
+ return
324
+ when "C"
325
+ # FIXME. Cstrl + shift + c
326
+ if str == "\x03" # Copy primary selection into clipboard
327
+ system("xsel -o -p | xsel -i -b")
328
+ return
329
+ end
330
+ when "V"
331
+ # FIXME. Cstrl + shift + v
332
+ if str == "\x16" # Paste clipboard
333
+ clipboard = `xsel -b`
334
+ if clipboard.chomp.empty?
335
+ clipboard = @clipboard
336
+ else
337
+ @clipboard = clipboard
338
+ end
339
+ exit_scrollback
340
+ @controller.paste(clipboard)
341
+ return
342
+ end
343
+ when :XK_Menu;
344
+ # FIXME: Want deskmenu here, but as long as we're not running the shell, we don't know pwd.
345
+ puts "FIXME: deskmenu"
346
+ # FIXME: In the meantime we use it as a debugging tool to force redraw.
347
+ redraw
348
+ render_text_buffer
349
+ end
350
+ payload = keysym_to_vt102(ks) || str
351
+ # Only snap to the live screen when we're actually sending input;
352
+ # bare modifiers (Ctrl/Shift) produce no payload and must not disturb
353
+ # scrollback (e.g. while setting up a Ctrl+Shift+C copy from history).
354
+ exit_scrollback unless payload.nil? || payload.empty?
355
+ @controller.keypress(payload)
356
+ end
357
+
358
+ def blink
359
+ t = Time.now
360
+ doblink = false
361
+ #p [@blink_state, @rblink_state, @lastblink, @lastrblink]
362
+ if ((t - @lastblink)*10).to_i > 6
363
+ @lastblink = t
364
+ @blink_state = !@blink_state
365
+ doblink = true
366
+ end
367
+ if ((t - @lastrblink)*10).to_i >= 2
368
+ @lastrblink = t
369
+ @rblink_state = !@rblink_state
370
+ doblink = true
371
+ end
372
+ # FIXME: It bugs out at some point?
373
+ @buffer.redraw_blink if doblink
374
+ end
375
+
376
+ # Sending input to the pty must snap the view back to the live screen;
377
+ # otherwise typed/echoed output is drawn over the scrolled-back display.
378
+ def exit_scrollback
379
+ redraw if @window.scrollback_reset
380
+ end
381
+
382
+ def redraw_positions(positions) = positions.each { |pos| @buffer.redraw(*pos) }
383
+
384
+ # Re-stamp the active selection highlight on top of freshly drawn
385
+ # content. The selection is an overlay that is NOT stored in the buffer,
386
+ # so any output - or a full redraw - that repaints those cells erases the
387
+ # highlight. Re-applying it after each draw keeps the selection visible
388
+ # while a program streams output (e.g. top repainting, or a full-screen
389
+ # app), which a one-shot paint at mouse-time cannot do.
390
+ def reapply_selection
391
+ return unless @select_startpos && @select_endpos
392
+ sb = @window.scrollback_count
393
+ @buffer.each_character_between(@select_startpos[0]..@select_startpos[1], @select_endpos[0]..@select_endpos[1]) do |x,y,cell|
394
+ sy = y + sb
395
+ next if sy < 0 || sy >= @term.height
396
+ @buffer.redraw_cell_at(x, sy, cell, fg: 0xffffff, bg: 0xff00ff)
397
+ end
398
+ @buffer.draw_flush
399
+ end
400
+
401
+ # FIXME: Cursor, selection etc. are "special" overlays on top of attributes.
402
+ # Allow the terminal to set a set of positions + fg/bg, and a set of ranges.
403
+ def render_selection
404
+ # Selection positions are in *buffer* coordinates (negative rows are
405
+ # scrollback); the screen row is buffer_row + scrollback_count. Damage
406
+ # is tracked in screen coordinates so it can be repainted later.
407
+ sb = @window.scrollback_count
408
+ olddamage = @selection_damage || Set.new
409
+ @selection_damage = Set.new
410
+ @buffer.each_character_between(@select_startpos[0]..@select_startpos[1], @select_endpos[0]..@select_endpos[1]) do |x,y,cell|
411
+ sy = y + sb
412
+ next if sy < 0 || sy >= @term.height
413
+ @selection_damage << [x,sy]
414
+ @buffer.redraw_cell_at(x, sy, cell, fg: 0xffffff, bg: 0xff00ff)
415
+ end
416
+ # Repaint cells that left the selection with their displayed content.
417
+ (olddamage - @selection_damage).each { |x,sy| @buffer.redraw_display(x, sy, sb) }
418
+ @buffer.draw_flush
419
+ #@window.flush
420
+ end
421
+
422
+ def get_selection
423
+ startpos = @select_startpos
424
+ endpos = @select_endpos
425
+ str = ""
426
+ ypos = nil
427
+ @buffer.each_character_between(startpos[0]..startpos[1], endpos[0]..endpos[1]) do |x,y,cell|
428
+ str += "\n" if ypos && y != ypos
429
+ ypos = y
430
+ str << (cell[0].chr(Encoding::UTF_8) rescue "")
431
+ end
432
+ str
433
+ end
434
+
435
+ def clear_selection_if_set
436
+ return if !@select_startpos
437
+ sb = @window.scrollback_count
438
+ (@selection_damage || []).each { |x,sy| @buffer.redraw_display(x, sy, sb) }
439
+ @select_startpos = nil
440
+ # FIXME
441
+ redraw
442
+ end
443
+
444
+ def handle_mouse(pkt)
445
+ @term.mouse_buttons = button = pkt.detail > 0 ? pkt.detail : @term.mouse_buttons
446
+ release = pkt.is_a?(X11::Form::ButtonRelease)
447
+ x = pkt.event_x / char_w
448
+ y = pkt.event_y / char_h
449
+ # Holding Shift forces a local text selection even when the
450
+ # application has grabbed the mouse (mouse reporting on - e.g. Claude's
451
+ # agent picker, or any full-screen app with clickable UI). This is the
452
+ # standard xterm override so you can always select/copy.
453
+ shift = pkt.state.anybits?(0x01) # ShiftMask
454
+ case shift ? nil : @term.mouse_mode
455
+ when nil
456
+ # Selection works in buffer coordinates: when scrolled back, the
457
+ # row under the pointer is a scrollback line (buffer row
458
+ # screen_y - scrollback_count, negative for scrollback). Without
459
+ # this, selection/copy reads the live screen instead of what is
460
+ # actually displayed.
461
+ y -= @window.scrollback_count
462
+ # New selection, but the old has not been cleared yet
463
+ if @released
464
+ clear_selection_if_set
465
+ @released = false
466
+ end
467
+
468
+ @select_startpos ||= [x,y]
469
+ if [x,y] != @select_endpos
470
+ @select_endpos = [x,y]
471
+ # FIXME: Optimize rendering of selection further
472
+ render_selection
473
+ end
474
+
475
+ if release
476
+ @released = true
477
+ if @select_startpos != @select_endpos
478
+ sel = get_selection
479
+ io = IO.popen("xsel -i", "a+")
480
+ io.write(sel)
481
+ io.close
482
+ else
483
+ clear_selection_if_set
484
+ end
485
+ end
486
+ when :vt200, :btn_event
487
+ # FIXME: This is only right for @mouse_reporting == :digits
488
+ # FIXME: Report modifiers.
489
+ # Not reporting release for scroll wheel
490
+ return if release && button >= 4
491
+ event = [0,1,2,64,65][button-1]
492
+ event += 32 if pkt.is_a?(X11::Form::MotionNotify)
493
+ #button = [0,1,2,4,5][button-1]
494
+ @controller.mouse_report(@term.mouse_reporting, event, x,y, release)
495
+ end
496
+ end
497
+
498
+ def process(pkt)
499
+ # p pkt
500
+ case pkt
501
+ when X11::Form::ButtonPress, X11::Form::MotionNotify, X11::Form::ButtonRelease
502
+ handle_mouse(pkt)
503
+ when X11::Form::KeyPress
504
+ key(pkt)
505
+ when X11::Form::KeyRelease,
506
+ X11::Form::NoExposure
507
+ # Intentionally ignored
508
+ when X11::Form::ConfigureNotify
509
+ # Real size change: pkt.width/height are the new window size.
510
+ request_redraw([pkt.width, pkt.height])
511
+ when X11::Form::Expose
512
+ # Damage, NOT a size change: pkt.width/height are the exposed
513
+ # rectangle, not the window. Repaint only; never resize here (that
514
+ # would shrink the terminal to the strip size).
515
+ request_redraw
516
+ else
517
+ # Other X events (MapNotify, etc.) are not acted on.
518
+ end
519
+ end
520
+
521
+ def event_thread
522
+ Thread.new do
523
+ loop do
524
+ pkt = @window.dpy.next_packet
525
+ process(pkt)
526
+ Thread.pass
527
+ end
528
+ end
529
+ end
530
+
531
+
532
+ def run(args)
533
+ @controller = Controller.new(self, @config)
534
+ @controller.run(*args)
535
+ @term.responder = @controller
536
+
537
+ @lastblink ||= Time.now
538
+ @lastrblink ||= Time.now
539
+
540
+ Thread.abort_on_exception = true
541
+ threads =[]
542
+
543
+ threads << Thread.new do
544
+ loop do
545
+ process_queue
546
+ Thread.pass
547
+ end
548
+ end
549
+
550
+ threads << event_thread
551
+
552
+ threads << Thread.new do
553
+ loop do
554
+ sleep(0.1)
555
+ # Enqueue rather than calling blink directly: blink redraws and
556
+ # so touches the buffer, which must only be mutated on the
557
+ # processing thread. blink self-gates on elapsed time, so a
558
+ # fixed tick is fine.
559
+ @queue << :blink
560
+ end
561
+ end
562
+
563
+ # Flush on the processing thread too, so the @buf->window copy never
564
+ # races a concurrent buffer mutation.
565
+ threads << Thread.new do
566
+ loop do
567
+ @queue << :flush
568
+ sleep(1/30.0)
569
+ end
570
+ end
571
+
572
+ if ENV["DEBUG"].to_s.strip != ""
573
+ while cmd = STDIN.gets&.strip
574
+ binding.pry if cmd == "pry"
575
+ end
576
+ end
577
+
578
+ threads.each(&:join)
579
+ end
580
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RubyTerm
4
+ VERSION = "0.1.0"
5
+ end
data/lib/rubyterm.rb ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyterm - a pure-Ruby X11 terminal emulator and reusable terminal engine.
4
+ #
5
+ # Requiring "rubyterm" loads the whole stack:
6
+ #
7
+ # * the terminal *engine* - escape interpreter, virtual buffer, damage
8
+ # tracking and the swappable backends (Term, TrackChanges, TermBuffer,
9
+ # AnsiBackend, BitmapWindow); usable headlessly / embedded in a TUI; and
10
+ # * the X11 *front end* - the Window backend, pty Controller, keymap and
11
+ # the RubyTerm application class that bin/rubyterm runs.
12
+ #
13
+ # The engine carries no X11 dependency itself, but this entry point pulls in
14
+ # the X11/skrift libraries so a single `require "rubyterm"` gives you a
15
+ # runnable terminal. (They are pure Ruby, so requiring them needs no running
16
+ # X server - only instantiating Window does.)
17
+
18
+ require "pty"
19
+ require "io/console"
20
+ require "set"
21
+
22
+ require "X11"
23
+ require "skrift"
24
+ require "skrift/x11"
25
+ require "toml-rb"
26
+
27
+ require_relative "rubyterm/version"
28
+
29
+ # Engine
30
+ require_relative "palette"
31
+ require_relative "charsets"
32
+ require_relative "escapeparser"
33
+ require_relative "utf8decoder"
34
+ require_relative "termbuffer"
35
+ require_relative "trackchanges"
36
+ require_relative "term"
37
+
38
+ # Backends
39
+ require_relative "ansibackend"
40
+ require_relative "bitmapwindow"
41
+
42
+ # X11 front end
43
+ require_relative "keymap"
44
+ require_relative "window"
45
+ require_relative "windowadapter"
46
+ require_relative "controller"
47
+ require_relative "rubyterm/app"