text_editor 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +33 -0
  5. data/.ruby-version +1 -0
  6. data/.text_editor.rb +9 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +58 -0
  9. data/LICENSE +20 -0
  10. data/README.org +29 -0
  11. data/bin/console +9 -0
  12. data/bin/te +3 -0
  13. data/lib/core_ext/hash.rb +12 -0
  14. data/lib/core_ext/pathname.rb +9 -0
  15. data/lib/text_editor/buffer.rb +85 -0
  16. data/lib/text_editor/clipboard/cygwin.rb +20 -0
  17. data/lib/text_editor/clipboard/linux.rb +17 -0
  18. data/lib/text_editor/clipboard/mac.rb +18 -0
  19. data/lib/text_editor/clipboard/unknown.rb +17 -0
  20. data/lib/text_editor/clipboard/windows.rb +17 -0
  21. data/lib/text_editor/clipboard.rb +18 -0
  22. data/lib/text_editor/command/backspace.rb +26 -0
  23. data/lib/text_editor/command/cursor_down.rb +14 -0
  24. data/lib/text_editor/command/cursor_end.rb +10 -0
  25. data/lib/text_editor/command/cursor_left.rb +16 -0
  26. data/lib/text_editor/command/cursor_right.rb +19 -0
  27. data/lib/text_editor/command/cursor_start.rb +10 -0
  28. data/lib/text_editor/command/cursor_up.rb +13 -0
  29. data/lib/text_editor/command/insert_char.rb +11 -0
  30. data/lib/text_editor/command/insert_line.rb +16 -0
  31. data/lib/text_editor/command/insert_tab.rb +9 -0
  32. data/lib/text_editor/command/quit.rb +9 -0
  33. data/lib/text_editor/command/resolver.rb +36 -0
  34. data/lib/text_editor/command/write_file.rb +13 -0
  35. data/lib/text_editor/command.rb +26 -0
  36. data/lib/text_editor/component.rb +13 -0
  37. data/lib/text_editor/configuration/bootstrap.rb +43 -0
  38. data/lib/text_editor/configuration/dsl.rb +11 -0
  39. data/lib/text_editor/configuration/resolver.rb +45 -0
  40. data/lib/text_editor/configuration.rb +15 -0
  41. data/lib/text_editor/cursor.rb +42 -0
  42. data/lib/text_editor/keyboard.rb +54 -0
  43. data/lib/text_editor/mode/nano.rb +30 -0
  44. data/lib/text_editor/mode.rb +23 -0
  45. data/lib/text_editor/operating_system.rb +19 -0
  46. data/lib/text_editor/reactor.rb +38 -0
  47. data/lib/text_editor/state.rb +10 -0
  48. data/lib/text_editor/terminal.rb +57 -0
  49. data/lib/text_editor/window.rb +44 -0
  50. data/lib/text_editor.rb +121 -0
  51. data/log/.keep +0 -0
  52. data/spec/core_ext/hash_spec.rb +15 -0
  53. data/spec/core_ext/pathname_spec.rb +26 -0
  54. data/spec/spec_helper.rb +15 -0
  55. data/spec/text_editor/buffer_spec.rb +181 -0
  56. data/spec/text_editor/clipboard_spec.rb +28 -0
  57. data/spec/text_editor/command/quit_spec.rb +14 -0
  58. data/spec/text_editor/command/resolver_spec.rb +56 -0
  59. data/spec/text_editor/command_spec.rb +85 -0
  60. data/spec/text_editor/component_spec.rb +40 -0
  61. data/spec/text_editor/configuration_spec.rb +25 -0
  62. data/spec/text_editor/cursor_spec.rb +93 -0
  63. data/spec/text_editor/operating_system_spec.rb +55 -0
  64. data/spec/text_editor/reactor_spec.rb +34 -0
  65. data/spec/text_editor/state_spec.rb +20 -0
  66. data/text_editor.gemspec +18 -0
  67. metadata +143 -0
@@ -0,0 +1,13 @@
1
+ class TextEditor
2
+ module Component
3
+ extend Forwardable
4
+
5
+ def_delegators :editor, :config, :logger, :terminal
6
+
7
+ attr_reader :editor
8
+
9
+ def initialize(editor)
10
+ @editor = editor
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,43 @@
1
+ class TextEditor
2
+ class Configuration
3
+ class Bootstrap
4
+ include Component
5
+
6
+ def configure(args)
7
+ namespace.instance = editor
8
+
9
+ options.order!(args)
10
+ options.parse!(args)
11
+
12
+ config.files = args.map do |file|
13
+ Pathname.new(file)
14
+ end
15
+
16
+ resolver.files.each do |file|
17
+ require file
18
+ end
19
+ end
20
+
21
+ def options
22
+ @options ||= OptionParser.new do |opts|
23
+ opts.on("--log [FILE]") { |log| config.log = log }
24
+
25
+ opts.on("--version") do
26
+ puts editor.version
27
+ raise SystemExit
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def resolver
35
+ @resolver ||= Resolver.new(editor)
36
+ end
37
+
38
+ def namespace
39
+ Module.nesting.last
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,11 @@
1
+ class TextEditor
2
+ class Configuration
3
+ module DSL
4
+ attr_accessor :instance
5
+
6
+ def configure(&block)
7
+ instance.config.instance_eval(&block)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ class TextEditor
2
+ class Configuration
3
+ class Resolver
4
+ include Component
5
+
6
+ FILE = ".text_editor.rb"
7
+
8
+ def files
9
+ @files ||= configs.select(&:exist?)
10
+ end
11
+
12
+ private
13
+
14
+ def configs
15
+ directories.map do |directory|
16
+ directory.join(FILE)
17
+ end
18
+ end
19
+
20
+ def directories
21
+ [editor.root, etc, home, *tree].uniq
22
+ end
23
+
24
+ def etc
25
+ Pathname.new("/etc")
26
+ end
27
+
28
+ def home
29
+ Pathname.new(ENV["HOME"])
30
+ end
31
+
32
+ def pwd
33
+ Pathname.new(Dir.pwd)
34
+ end
35
+
36
+ def tree
37
+ [pwd].tap do |tree|
38
+ until [home, tree.first].include?(tree.first.dirname)
39
+ tree.unshift(tree.first.dirname)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ class TextEditor
2
+ class Configuration
3
+ include Component
4
+
5
+ attr_accessor :files
6
+
7
+ def log
8
+ @log ||= OpenStruct.new
9
+ end
10
+
11
+ def tab
12
+ @tab ||= OpenStruct.new
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ class TextEditor
2
+ class Cursor
3
+ attr_reader :line, :column
4
+
5
+ def initialize(line = 0, column = 0)
6
+ @line = line
7
+ @column = column
8
+ end
9
+
10
+ def clamp(line, column)
11
+ line = [@line, line].min
12
+ column = [@column, column].min
13
+ move(line, column)
14
+ end
15
+
16
+ def down(count = 1)
17
+ move(line + count, column)
18
+ end
19
+
20
+ def left(count = 1)
21
+ move(line, column - count)
22
+ end
23
+
24
+ def move(line, column)
25
+ line = [line, 0].max
26
+ column = [column, 0].max
27
+ self.class.new(line, column)
28
+ end
29
+
30
+ def position
31
+ [line, column]
32
+ end
33
+
34
+ def right(count = 1)
35
+ move(line, column + count)
36
+ end
37
+
38
+ def up(count = 1)
39
+ move(line - count, column)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ class TextEditor
2
+ class Keyboard
3
+ include Component
4
+ using CoreExt::Hash
5
+
6
+ def aliases
7
+ @aliases ||= {
8
+ "\e" => :escape,
9
+ ctrl_m: :enter,
10
+ dc: :delete,
11
+ ic: :insert,
12
+ npage: :page_down,
13
+ ppage: :page_up,
14
+ }
15
+ end
16
+
17
+ def codes
18
+ @codes ||= {
19
+ 0 => [:ctrl, :space],
20
+ 9 => :tab,
21
+ 27 => :escape,
22
+ 127 => :backspace,
23
+ 263 => :backspace,
24
+ 353 => [:shift, :tab],
25
+ 527 => [:ctrl, :shift, :end],
26
+ 532 => [:ctrl, :shift, :home],
27
+ }
28
+ end
29
+
30
+ def keys
31
+ @keys ||= terminal.keys.transform_keys do |key|
32
+ key.to_s.downcase.sub("key_", "").to_sym
33
+ end
34
+ end
35
+
36
+ def listen
37
+ translate(terminal.input)
38
+ end
39
+
40
+ def translate(key)
41
+ return unless key
42
+
43
+ key = codes[key] if codes.key?(key)
44
+ key = keys.key(key) if keys.value?(key)
45
+ key = aliases[key] if aliases.key?(key)
46
+
47
+ case key
48
+ when 0..255 then key.chr.to_sym
49
+ when Array then key
50
+ else key.to_sym
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,30 @@
1
+ class TextEditor
2
+ class Mode
3
+ class Nano < Mode
4
+ bind :ctrl_c, Command::Quit
5
+
6
+ bind :up, Command::CursorUp
7
+ bind :down, Command::CursorDown
8
+ bind :left, Command::CursorLeft
9
+ bind :right, Command::CursorRight
10
+
11
+ bind :ctrl_p, Command::CursorUp
12
+ bind :ctrl_n, Command::CursorDown
13
+ bind :ctrl_b, Command::CursorLeft
14
+ bind :ctrl_f, Command::CursorRight
15
+
16
+ bind :ctrl_a, Command::CursorStart
17
+ bind :ctrl_e, Command::CursorEnd
18
+
19
+ bind :backspace, Command::Backspace
20
+ bind :ctrl_h, Command::Backspace
21
+
22
+ bind :ctrl_o, Command::WriteFile
23
+
24
+ bind :enter, Command::InsertLine
25
+ bind :tab, Command::InsertTab
26
+
27
+ bind(/^[[:print:]]$/, Command::InsertChar)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ class TextEditor
2
+ class Mode
3
+ def self.bind(key, command = nil, &block)
4
+ keybindings[key] = command || block
5
+ end
6
+
7
+ def self.keybindings
8
+ @keybindings ||= {}
9
+ end
10
+
11
+ def keybindings
12
+ self.class.keybindings
13
+ end
14
+
15
+ def resolve(key)
16
+ keybindings.each do |keybinding, command|
17
+ return command if keybinding === key
18
+ end
19
+
20
+ nil
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ class TextEditor
2
+ class OperatingSystem
3
+ attr_reader :version
4
+
5
+ def initialize(version)
6
+ @version = version
7
+ end
8
+
9
+ def type
10
+ case version
11
+ when /cygwin/ then :Cygwin
12
+ when /bsd|linux/ then :Linux
13
+ when /darwin|mac/ then :Mac
14
+ when /mingw|mswin/ then :Windows
15
+ else :Unknown
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ class TextEditor
2
+ class Reactor
3
+ include Component
4
+
5
+ def running?
6
+ !!@running
7
+ end
8
+
9
+ def start
10
+ @running = true
11
+ render
12
+ listen
13
+ end
14
+
15
+ def stop
16
+ @running = false
17
+ end
18
+
19
+ private
20
+
21
+ def listen
22
+ while running?
23
+ key = editor.keyboard.listen
24
+ next unless key
25
+
26
+ command = editor.mode.resolve(key)
27
+ next unless command
28
+
29
+ command.call(editor, key.to_s)
30
+ render
31
+ end
32
+ end
33
+
34
+ def render
35
+ editor.window.render
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,10 @@
1
+ class TextEditor
2
+ class State
3
+ attr_reader :buffer, :cursor
4
+
5
+ def initialize(buffer, cursor)
6
+ @buffer = buffer
7
+ @cursor = cursor
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,57 @@
1
+ class TextEditor
2
+ class Terminal
3
+ include Component
4
+ include Curses
5
+
6
+ public :beep, :clear, :lines
7
+
8
+ def columns
9
+ cols
10
+ end
11
+
12
+ def delete_line
13
+ deleteln
14
+ end
15
+
16
+ def keys
17
+ self.class.constants.sort.grep(/^KEY/).reduce({}) do |hash, constant|
18
+ value = self.class.const_get(constant)
19
+ hash.update(constant => value)
20
+ end
21
+ end
22
+
23
+ def input
24
+ getch
25
+ end
26
+
27
+ def move(*coords)
28
+ setpos(*coords)
29
+ end
30
+
31
+ def render
32
+ noecho # hide typed characters
33
+ nonl # disable newline translation
34
+ stdscr.keypad(true) # enable arrow keys
35
+ raw # enable all other keys
36
+ init_screen
37
+ start_color
38
+ yield
39
+ ensure
40
+ clear
41
+ close_screen
42
+ end
43
+
44
+ def write(string)
45
+ padded = string.ljust(columns)
46
+ addstr(padded)
47
+ end
48
+
49
+ def x
50
+ stdscr.curx
51
+ end
52
+
53
+ def y
54
+ stdscr.cury
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,44 @@
1
+ class TextEditor
2
+ class Window
3
+ extend Forwardable
4
+ include Component
5
+
6
+ def_delegators :state, :buffer, :cursor
7
+
8
+ def cache
9
+ @cache ||= {}
10
+ end
11
+
12
+ def file
13
+ config.files.first
14
+ end
15
+
16
+ def render
17
+ buffer.each_line.with_index do |line, index|
18
+ if cache[index] != line
19
+ cache[index] = line
20
+ terminal.move(index, 0)
21
+ terminal.write(line)
22
+ end
23
+ end
24
+
25
+ deleted_lines = cache.keys - (0...buffer.size).to_a
26
+
27
+ deleted_lines.each do |index|
28
+ cache.delete(index)
29
+ terminal.move(index, 0)
30
+ terminal.delete_line
31
+ end
32
+
33
+ terminal.move(*cursor.position)
34
+ end
35
+
36
+ def state
37
+ @state || update(Buffer.new(file), Cursor.new)
38
+ end
39
+
40
+ def update(buffer, cursor)
41
+ @state = State.new(buffer, cursor)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,121 @@
1
+ require "forwardable"
2
+ require "logger"
3
+ require "optparse"
4
+ require "ostruct"
5
+ require "pathname"
6
+
7
+ require "bundler/setup"
8
+ require "curses"
9
+
10
+ require_relative "core_ext/hash"
11
+ require_relative "core_ext/pathname"
12
+
13
+ require_relative "text_editor/component"
14
+
15
+ require_relative "text_editor/buffer"
16
+ require_relative "text_editor/cursor"
17
+ require_relative "text_editor/keyboard"
18
+ require_relative "text_editor/operating_system"
19
+ require_relative "text_editor/reactor"
20
+ require_relative "text_editor/state"
21
+ require_relative "text_editor/terminal"
22
+ require_relative "text_editor/window"
23
+
24
+ require_relative "text_editor/clipboard"
25
+ require_relative "text_editor/clipboard/cygwin"
26
+ require_relative "text_editor/clipboard/linux"
27
+ require_relative "text_editor/clipboard/mac"
28
+ require_relative "text_editor/clipboard/unknown"
29
+ require_relative "text_editor/clipboard/windows"
30
+
31
+ require_relative "text_editor/command"
32
+ require_relative "text_editor/command/backspace"
33
+ require_relative "text_editor/command/quit"
34
+ require_relative "text_editor/command/cursor_down"
35
+ require_relative "text_editor/command/cursor_end"
36
+ require_relative "text_editor/command/cursor_left"
37
+ require_relative "text_editor/command/cursor_right"
38
+ require_relative "text_editor/command/cursor_start"
39
+ require_relative "text_editor/command/cursor_up"
40
+ require_relative "text_editor/command/insert_char"
41
+ require_relative "text_editor/command/insert_line"
42
+ require_relative "text_editor/command/insert_tab"
43
+ require_relative "text_editor/command/resolver"
44
+ require_relative "text_editor/command/write_file"
45
+
46
+ require_relative "text_editor/configuration"
47
+ require_relative "text_editor/configuration/bootstrap"
48
+ require_relative "text_editor/configuration/dsl"
49
+ require_relative "text_editor/configuration/resolver"
50
+
51
+ require_relative "text_editor/mode"
52
+ require_relative "text_editor/mode/nano"
53
+
54
+ class TextEditor
55
+ extend Configuration::DSL
56
+
57
+ GEMSPEC = File.expand_path("../../text_editor.gemspec", __FILE__)
58
+
59
+ def initialize(args)
60
+ @args = args
61
+ end
62
+
63
+ def bootstrap
64
+ @bootstrap ||= Configuration::Bootstrap.new(self)
65
+ end
66
+
67
+ def clipboard
68
+ @clipboard ||= Clipboard.new(self)
69
+ end
70
+
71
+ def config
72
+ @config ||= Configuration.new(self)
73
+ end
74
+
75
+ def gemspec
76
+ @gemspec ||= Gem::Specification.load(GEMSPEC)
77
+ end
78
+
79
+ def keyboard
80
+ @keyboard ||= Keyboard.new(self)
81
+ end
82
+
83
+ def logger
84
+ @logger ||= Logger.new(config.log.file, *config.log.options).tap do |log|
85
+ log.level = config.log.level
86
+ end
87
+ end
88
+
89
+ def mode
90
+ @mode ||= Mode::Nano.new
91
+ end
92
+
93
+ def os
94
+ @os ||= OperatingSystem.new(RbConfig::CONFIG["host_os"])
95
+ end
96
+
97
+ def reactor
98
+ @reactor ||= Reactor.new(self)
99
+ end
100
+
101
+ def root
102
+ @root ||= Pathname.new(__FILE__).dirname.dirname
103
+ end
104
+
105
+ def start
106
+ bootstrap.configure(@args)
107
+ terminal.render { reactor.start }
108
+ end
109
+
110
+ def terminal
111
+ @terminal ||= Terminal.new(self)
112
+ end
113
+
114
+ def window
115
+ @window ||= Window.new(self)
116
+ end
117
+
118
+ def version
119
+ gemspec.version.to_s
120
+ end
121
+ end
data/log/.keep ADDED
File without changes
@@ -0,0 +1,15 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe CoreExt::Hash do
4
+ using described_class
5
+
6
+ subject { { one: 1, two: 2 } }
7
+
8
+ describe "#transform_keys" do
9
+ it "transforms the keys" do
10
+ actual = subject.transform_keys(&:to_s)
11
+ expected = { "one" => 1, "two" => 2 }
12
+ expect(actual).to eq(expected)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe CoreExt::Pathname do
4
+ using described_class
5
+
6
+ subject { Pathname.new(file) }
7
+
8
+ describe "#safe_read" do
9
+ context "with an existing file" do
10
+ let(:file) { __FILE__ }
11
+
12
+ it "returns file contents" do
13
+ expected = File.read(file)
14
+ expect(subject.safe_read).to eq(expected)
15
+ end
16
+ end
17
+
18
+ context "with a non existing file" do
19
+ let(:file) { "this-does-not-exist.txt " }
20
+
21
+ it "returns file contents" do
22
+ expect(subject.safe_read).to be_nil
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ if ENV["CODECLIMATE_REPO_TOKEN"]
2
+ require "codeclimate-test-reporter"
3
+ CodeClimate::TestReporter.start
4
+ else
5
+ require "simplecov"
6
+ SimpleCov.start { add_filter("/vendor/bundle/") }
7
+ end
8
+
9
+ require File.expand_path("../../lib/text_editor", __FILE__)
10
+
11
+ RSpec.configure do |config|
12
+ config.filter_run :focus
13
+ config.raise_errors_for_deprecations!
14
+ config.run_all_when_everything_filtered = true
15
+ end