text_editor 0.0.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.
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