amun 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6430d19551a385610563d0305e205315fb57afd3
4
- data.tar.gz: d17d801b580e7cb7207713e43f7b635167aa159d
3
+ metadata.gz: 10726ce656cbf6445cbee3177cda26541982d3b7
4
+ data.tar.gz: 0da415d031b8e63ff1fb5fb4b92537117ed1ad4f
5
5
  SHA512:
6
- metadata.gz: 33457f2a5eb0d21f8aefee6277673e9473661c78e8c471ff4627a2c3b36676a822a1af8e1d53f70918ff94ee8346c659d65baaaef8d958742a14e83e9ba11585
7
- data.tar.gz: b7f75bb84a7875b23be10d43a3513158c1da27dccd82944a620f1ebd5b715ce852b7712b5024ca8509ad84532049ea52d652cef473498c5d924ef6f90f3b6ee9
6
+ metadata.gz: a6eef223a7d0a123259da8edea864b7a701b8d8d048fdef6ff0dc1002a187b4008c4ed0b967e1db95fb5345156c611063ef4525e67550efbe7da2017c925d486
7
+ data.tar.gz: b475175d8acf045f52ac748be2243c7b46d1a4576334a768d671a42295bcf899f8cdd0e8bbf12152135b1bf22ce7537f8fb9ac38b8481a75be651a2e63f88d7b
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'test/**/*'
4
+ - '*.gemspec'
5
+ DisplayCopNames: true
6
+ Style/FrozenStringLiteralComment:
7
+ Enabled: false
8
+ Style/StringLiterals:
9
+ Enabled: false
10
+ Style/WordArray:
11
+ Enabled: false
data/.travis.yml CHANGED
@@ -1,5 +1,8 @@
1
1
  sudo: false
2
2
  language: ruby
3
- rvm:
4
- - 2.4.0
5
3
  before_install: gem install bundler -v 1.14.4
4
+ addons:
5
+ code_climate:
6
+ repo_token: 9aa20c762ad050635e6c5c5cc13e6edb28d79b12e6929f101f23590273dcda3e
7
+ after_success:
8
+ - bundle exec codeclimate-test-reporter
data/README.md CHANGED
@@ -1,25 +1,62 @@
1
1
  # Amun (Work in progress)
2
2
 
3
+ ## "King of the gods and god of the wind"
4
+ [![Gem Version](https://badge.fury.io/rb/amun.svg)](http://badge.fury.io/rb/amun)
5
+ [![Build Status](https://travis-ci.org/blazeeboy/amun.svg?branch=master)](https://travis-ci.org/blazeeboy/amun)
6
+ [![Code Climate](https://codeclimate.com/github/blazeeboy/amun/badges/gpa.svg)](https://codeclimate.com/github/blazeeboy/amun)
7
+ [![Test Coverage](https://codeclimate.com/github/blazeeboy/amun/badges/coverage.svg)](https://codeclimate.com/github/blazeeboy/amun)
8
+
3
9
  A minimal CLI text editor, built on Ruby, looking for Emacs as it's father and idol.
4
10
 
5
11
  As developing packages for Emacs with Elisp wasn't always a fun or easy task, Starting a project that leverage ruby ability for fast development will be a good move towards
6
12
  an open, easy to extend editor.
7
13
 
8
- When I started this project I had 2 options, taking the VIM way or emacs way, looking in the current state of the two editors, It's obvious that emacs abbroach has a better
14
+ When I started this project I had 2 options, taking the VIM way or emacs way, looking in the current state of the two editors, It's obvious that emacs approach has a better
9
15
  extensibility over VIM, emacs customizability is far superior to VIM, so building this project as a minimal and emacs-like would open the door for vim users to have their own
10
16
  bindings as a package like emacs Evil mode, but doing the other way around won't help emacs users.
11
17
 
18
+ ## Advantages of building an editor in ruby
19
+
20
+ * We can use ruby gems as package management
21
+ * we already have bundler to fix dependencies, upgrade, downgrade gems (plugins in this case), you can even add sources for gems or get a gem from github or company inhouse gems.
22
+ * you can reflect on the runtime and autocomplete commands
23
+ * plugins can mutate all parts of the runtime application classes/objects included
24
+ * ruby is easy to learn so it'll be easier to build gems that is specifically for this editor
25
+ * lots of gems already exists and could be loaded into the editor environment
26
+ * you can use it locally or remotly as it's terminal based
27
+ * documentation included, rdoc is already there to be used
28
+
12
29
  ## Installation
13
30
 
14
31
  $ gem install amun
15
32
 
16
33
  ## Usage
17
34
 
18
- amun install an executable to your path, so executing `amun` from your commandline should launch amun
35
+ amun install an executable to your path, so executing `amun` from your command-line should launch amun
36
+
37
+ ## Structure
38
+
39
+
40
+ ### Helpers
41
+
42
+ Helpers are modules that any class can use to do side tasks, think of it like Ruby on rails helpers.
43
+
44
+ * Only modules no classes
45
+ * doesn't depend on each other
46
+ * depends on the project dependencies only like "Curses"
47
+
48
+ ### MajorModes
49
+
50
+ Classes that are responsible the following for a buffer object:
51
+
52
+ * event handling
53
+ * manipulating IO
54
+ * Rendering IO into a curses window
55
+
19
56
 
20
57
  ## Development
21
58
 
22
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
59
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
23
60
 
24
61
  ## Contributing
25
62
 
data/Rakefile CHANGED
@@ -7,4 +7,4 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList['test/**/*_test.rb']
8
8
  end
9
9
 
10
- task :default => :test
10
+ task default: :test
data/amun.gemspec CHANGED
@@ -28,4 +28,6 @@ Gem::Specification.new do |spec|
28
28
  spec.add_development_dependency "pry"
29
29
  spec.add_development_dependency "guard"
30
30
  spec.add_development_dependency "guard-minitest"
31
+ spec.add_development_dependency "simplecov"
32
+ spec.add_development_dependency "codeclimate-test-reporter", "~> 1.0.0"
31
33
  end
@@ -0,0 +1,51 @@
1
+ require 'curses'
2
+ require 'singleton'
3
+ require 'amun/ui/windows/frame'
4
+ require 'amun/event_manager'
5
+ require 'amun/features_loader'
6
+ require 'amun/helpers/keyboard'
7
+
8
+ module Amun
9
+ # singleton Amun application, it initialize curses,
10
+ # have the frame and handles keyboard
11
+ class Application
12
+ include Singleton
13
+
14
+ attr_writer :frame
15
+
16
+ def frame
17
+ @frame ||= UI::Windows::Frame.new
18
+ end
19
+
20
+ def run
21
+ init_curses
22
+ frame.render
23
+ FeaturesLoader.load
24
+ keyboard_thread.join
25
+ end
26
+
27
+ private
28
+
29
+ def init_curses
30
+ Curses.init_screen
31
+ Curses.curs_set 0
32
+ Curses.raw
33
+ Curses.noecho
34
+ Curses.start_color
35
+ Curses.stdscr.keypad = true
36
+ Curses.ESCDELAY = 0
37
+ end
38
+
39
+ def keyboard_thread
40
+ Thread.new do
41
+ chain = []
42
+ while (ch = Helpers::Keyboard.char)
43
+ chain << ch
44
+ if EventManager.join(chain.join(' '), frame) != EventManager::CHAINED
45
+ chain.clear
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,131 @@
1
+ require 'amun/ui/buffer'
2
+ require 'curses'
3
+
4
+ module Amun
5
+ module Behaviours
6
+ module Emacs
7
+ def emacs_behaviour_initialize(event_manager)
8
+ event_manager.bind "\C-f", self, :forward_char
9
+ event_manager.bind Curses::KEY_RIGHT.to_s, self, :forward_char
10
+
11
+ event_manager.bind "\C-b", self, :backward_char
12
+ event_manager.bind Curses::KEY_LEFT.to_s, self, :backward_char
13
+
14
+ event_manager.bind "\C-n", self, :next_line
15
+ event_manager.bind Curses::KEY_DOWN.to_s, self, :next_line
16
+
17
+ event_manager.bind "\C-p", self, :previous_line
18
+ event_manager.bind Curses::KEY_UP.to_s, self, :previous_line
19
+
20
+ event_manager.bind "\C-a", self, :beginning_of_line
21
+ event_manager.bind Curses::KEY_HOME.to_s, self, :beginning_of_line
22
+
23
+ event_manager.bind "\C-e", self, :end_of_line
24
+ event_manager.bind Curses::KEY_END.to_s, self, :end_of_line
25
+
26
+ event_manager.bind "\C-d", self, :delete_char
27
+ event_manager.bind Curses::Key::BACKSPACE.to_s, self, :backward_delete_char # this doesn't work, check linux
28
+ event_manager.bind "\C-?", self, :backward_delete_char # C-? is backspace on mac terminal for some reason
29
+
30
+ event_manager.bind Curses::Key::DC.to_s, self, :forward_delete_char
31
+
32
+ event_manager.bind "\C-k", self, :kill_line
33
+ event_manager.bind "\M-d", self, :kill_word
34
+ end
35
+
36
+ def forward_char(*)
37
+ buffer.point += 1
38
+ true
39
+ end
40
+
41
+ def backward_char(*)
42
+ buffer.point -= 1
43
+ true
44
+ end
45
+
46
+ def next_line(*)
47
+ line_begin = buffer.text.rindex("\n", buffer.point) || 0
48
+ line_end = buffer.text.index("\n", buffer.point + 1) || buffer.text.size + 1
49
+ next_line_end = buffer.text.index("\n", line_end + 1) || buffer.text.size + 1
50
+ point_offset = buffer.point - line_begin
51
+ buffer.point = [line_end + point_offset, next_line_end].min
52
+ true
53
+ end
54
+
55
+ def previous_line(*)
56
+ line_begin = buffer.text.rindex("\n", buffer.point) || 0
57
+ previous_line_begin = buffer.text.rindex("\n", line_begin - 1) || 0
58
+ point_offset = buffer.point - line_begin
59
+ buffer.point = [previous_line_begin + point_offset, line_begin - 1].min
60
+ true
61
+ end
62
+
63
+ def beginning_of_line(*)
64
+ point = buffer.point
65
+ return true if point.zero?
66
+ return true if buffer.text[point - 1] == "\n"
67
+
68
+ line_start = buffer.text.rindex("\n", point - 1)
69
+ buffer.point = line_start.nil? ? 0 : line_start + 1
70
+ true
71
+ end
72
+
73
+ def end_of_line(*)
74
+ point = buffer.point
75
+ return true if buffer.text[point] == "\n"
76
+
77
+ line_end = buffer.text.index("\n", point)
78
+ buffer.point = line_end || buffer.text.length
79
+ true
80
+ end
81
+
82
+ def delete_char(*)
83
+ buffer.text.slice!(buffer.point)
84
+ true
85
+ end
86
+
87
+ def backward_delete_char(*)
88
+ return true if buffer.point.zero?
89
+
90
+ buffer.point -= 1
91
+ buffer.text.slice!(buffer.point)
92
+ true
93
+ end
94
+
95
+ def forward_delete_char(*)
96
+ delete_char
97
+ end
98
+
99
+ # TODO should move text to kill ring
100
+ def kill_line(*)
101
+ if buffer.text[buffer.point] == "\n"
102
+ buffer.text.slice!(buffer.point)
103
+ return true
104
+ end
105
+
106
+ line_end = buffer.text.index(/$/, buffer.point)
107
+ buffer.text.slice!(buffer.point...line_end)
108
+ true
109
+ end
110
+
111
+ # TODO should move text to kill ring
112
+ def kill_word(*)
113
+ first_non_letter = buffer.text.index(/\P{L}/, buffer.point) || buffer.text.size
114
+ word_beginning = buffer.text.index(/\p{L}/, first_non_letter) || buffer.text.size
115
+ buffer.text.slice!(buffer.point...word_beginning)
116
+ true
117
+ end
118
+
119
+ # This should be bound to \M-BACKSPACE or \M-DEL but I think the terminal doesn't send it
120
+ # So the implementation will remain there until we find a way to catch this key
121
+ #
122
+ # TODO should move text to kill ring
123
+ def backward_kill_word(*)
124
+ first_letter_backward = buffer.text.rindex(/\p{L}/, buffer.point) || 0
125
+ first_non_letter_before_word = buffer.text.rindex(/\P{L}/, first_letter_backward) || -1
126
+ buffer.text.slice!(first_non_letter_before_word + 1 .. buffer.point)
127
+ true
128
+ end
129
+ end
130
+ end
131
+ end
@@ -1,3 +1,6 @@
1
+ require 'forwardable'
2
+ require 'set'
3
+
1
4
  module Amun
2
5
  # = Event manager
3
6
  # Stores a stack of methods to call for each event,
@@ -17,20 +20,31 @@ module Amun
17
20
  # when the *Buffer* module saves a file it should trigger
18
21
  # that event, which executed the *update_title*.
19
22
  class EventManager
23
+ # Event is interrupted and no need to continue
24
+ INTERRUPTED = :interrupted
25
+ # Event is to be continued
26
+ CONTINUE = :continue
27
+ # Event needs to be chained, will wait for next event
28
+ CHAINED = :chained
29
+
20
30
  def initialize
21
31
  @bindings = Hash.new { |h, k| h[k] = [] }
32
+ @chains = Set.new
22
33
  end
23
34
 
24
- # register an *objects*' *method* to be executed when the *event* is triggered
35
+ # register an *objects*' *method* to be executed
36
+ # when the *event* is triggered
25
37
  #
26
38
  # event(String):: and event to bind the method to
27
39
  # object(Object):: an object or class, that respond to +method+
28
- # method(Symbol):: a method name that should be executed when the event is triggered
40
+ # method(Symbol):: a method name that should be executed when the event
41
+ # is triggered
29
42
  def bind(event, object, method)
30
- unless object.respond_to? method
43
+ if !object.nil? && !object.respond_to?(method)
31
44
  raise ArgumentError, "#{method} : is not a method for #{object}"
32
45
  end
33
46
 
47
+ add_chain(event)
34
48
  @bindings[event].unshift(object: object, method: method)
35
49
  end
36
50
 
@@ -43,6 +57,8 @@ module Amun
43
57
  @bindings[event].delete_if do |binding|
44
58
  binding[:object] == object && binding[:method] == method
45
59
  end
60
+ @bindings.delete(event) if @bindings[event].empty?
61
+ update_chains
46
62
  end
47
63
 
48
64
  # bind an object method to be executed when any event is triggered
@@ -50,7 +66,7 @@ module Amun
50
66
  # be executed after the event specific stack is executed and didn't
51
67
  # stop execution
52
68
  def bind_all(object, method)
53
- bind :all, object, method
69
+ bind "all", object, method
54
70
  end
55
71
 
56
72
  # remove the *method* from executing after every event,
@@ -58,7 +74,7 @@ module Amun
58
74
  #
59
75
  # this won't remove the method if it was registered with #bind
60
76
  def unbind_all(object, method)
61
- unbind :all, object, method
77
+ unbind "all", object, method
62
78
  end
63
79
 
64
80
  # execute *event* stack of methods in Last-In-First-Out,
@@ -66,8 +82,77 @@ module Amun
66
82
  # to be executed after all events with #bind_all method
67
83
  # if any method in this chain returned false, it will stop the rest
68
84
  # of the stack.
85
+ #
86
+ # a chained event is an event that contain a space
87
+ # between 2 or more events lie "\C-c \C-x" so this event
88
+ # should be executed when these 2 events occure after
89
+ # each other,
90
+ # if you tried to trigger the first part \C-c the
91
+ # trigger method will return CHAINED, assuming that \C-c
92
+ # is not handled and also if handled the handle execution
93
+ # didn't return false to interrupt the execution
69
94
  def trigger(event)
70
- trigger_for_event(event, event) && trigger_for_event(:all, event)
95
+ return INTERRUPTED unless trigger_for_event(event, event) &&
96
+ trigger_for_event("all", event)
97
+ return CHAINED if chained?(event)
98
+ CONTINUE
99
+ end
100
+
101
+ # class will have the same methods as the EventManager
102
+ # instance, so you can attack events and trigger it globally
103
+ # like if you have a global instance if EventManager and
104
+ # you're attaching and triggering events from it instead
105
+ # if talking to a specific instance, this global trigger
106
+ # will be handled by one of the editor toplevel components
107
+ # and it should be the lowest priority, if every thing else
108
+ # can't handle the event it should ask this class if anyone
109
+ # registered something globally to handle this event.
110
+ #
111
+ # if you want to handle events in your major mode, minor modes
112
+ # or anything that the application will ask it to trigger event
113
+ # you need to instanciate an object, if you need your action to
114
+ # the default and should be handled globally, like exiting the application
115
+ # or changing theme, or changing some global variable, update packages
116
+ # or any similar global actions, then using the class methods will
117
+ # make sense here.
118
+ class << self
119
+ extend Forwardable
120
+
121
+ def_delegators :instance, :bind, :unbind, :bind_all, :unbind_all, :trigger
122
+
123
+ # trigger an array of event_managers with an event
124
+ # and return one of the 3 statuses (INTERRUPTED, CHAINED, CONTINUE)
125
+ #
126
+ # if any manager returned false or INTERRUPTED will
127
+ # not execute further and return INTERRUPTED,
128
+ #
129
+ # if it faced an event that wants to chain the event
130
+ # it will return CHAINED if all next managers returned
131
+ # CONTINUE or also CHAINED
132
+ #
133
+ # will return CONTINUE if all manangers returned continue
134
+ #
135
+ # event(Symbol):: an event to trigger on all provided managers
136
+ # *event_managers(*Array):: you can pass as many event managers as
137
+ # you like as parameters to this method, it will be triggered in order
138
+ def join(event, *event_managers)
139
+ event_managers.inject(CONTINUE) do |result, manager|
140
+ case manager.trigger(event)
141
+ when INTERRUPTED, false
142
+ break INTERRUPTED
143
+ when CHAINED
144
+ CHAINED
145
+ else
146
+ result
147
+ end
148
+ end
149
+ end
150
+
151
+ private
152
+
153
+ def instance
154
+ @instance ||= new
155
+ end
71
156
  end
72
157
 
73
158
  private
@@ -77,5 +162,25 @@ module Amun
77
162
  binding[:object].send binding[:method], event
78
163
  end
79
164
  end
165
+
166
+ def update_chains
167
+ @chains = Set.new
168
+ @bindings.each do |event|
169
+ add_chain(event)
170
+ end
171
+ end
172
+
173
+ def add_chain(event)
174
+ return unless event.to_s.include?(' ')
175
+ event.to_s.split(" ").inject("") do |chain, evt|
176
+ new_chain = (chain + " " + evt).strip
177
+ @chains << new_chain
178
+ new_chain
179
+ end
180
+ end
181
+
182
+ def chained?(event)
183
+ @chains.include? event
184
+ end
80
185
  end
81
186
  end
@@ -0,0 +1,6 @@
1
+ require 'amun/event_manager'
2
+
3
+ def log(event)
4
+ Amun::UI::Buffer.messages.text << "#{event}\n"
5
+ end
6
+ Amun::EventManager.bind_all nil, :log
@@ -0,0 +1,6 @@
1
+ require 'amun/event_manager'
2
+
3
+ def kill_amun(*)
4
+ exit 0
5
+ end
6
+ Amun::EventManager.bind("\C-x \C-c", nil, :kill_amun)
@@ -0,0 +1,14 @@
1
+ module Amun
2
+ # load all files in the features directory
3
+ module FeaturesLoader
4
+ module_function
5
+
6
+ def load
7
+ path = File.expand_path('../features', __FILE__)
8
+ features = Dir.glob(File.join(path, '**/*'))
9
+ features.each do |feature|
10
+ require feature
11
+ end
12
+ end
13
+ end
14
+ end
@@ -3,13 +3,18 @@ require 'curses'
3
3
  module Amun
4
4
  module Helpers
5
5
  ##
6
- # Colors is responsible for registering new colors pairs (foreground and background)
7
- # associate the pair with a name, then #use that pair for the next text printed
8
- # on screen, also contains styles constants could be passed to #use to print bold
6
+ # Colors is responsible for registering new colors
7
+ # pairs (foreground and background)
8
+ # associate the pair with a name, then #use that pair
9
+ # for the next text printed
10
+ # on screen, also contains styles constants could be
11
+ # passed to #use to print bold
9
12
  # or underline text for example
10
13
  #
11
- # it's important to understand that the number of pairs that coul'd be registered
12
- # is limited to a number the current terminal specify, so registering, more color
14
+ # it's important to understand that the number of pairs
15
+ # that coul'd be registered
16
+ # is limited to a number the current terminal specify,
17
+ # so registering, more color
13
18
  # pairs than allowed will result in a RuntimeError.
14
19
  #
15
20
  # color values could be taken from the xterm-256 color schema from here:
@@ -37,6 +42,7 @@ module Amun
37
42
  RIGHT = Curses::A_RIGHT
38
43
 
39
44
  COLORS = Hash.new { |h, k| h[k] = h.length + 1 }
45
+ DEFAULT_COLOR = :default
40
46
  private_constant :COLORS
41
47
 
42
48
  # register or update a color pair with *foreground* and *background*
@@ -47,19 +53,57 @@ module Amun
47
53
  # background(Number):: background color in current terminal color schema
48
54
  def register(name, foreground, background)
49
55
  if COLORS.size >= Curses.color_pairs - 1
50
- raise "Can't register color #{name} as the colors exceeded the limit #{Curses.color_pairs}"
56
+ raise "Can't register color: #{name}, max: #{Curses.color_pairs}"
51
57
  end
52
58
 
53
59
  Curses.init_pair(COLORS[name], foreground, background)
54
60
  end
55
61
 
62
+ # check if a color pair is registered or not, returns true if registered
63
+ def registered?(name)
64
+ COLORS.key? name
65
+ end
66
+
67
+ # works like #register but doesn't override your color if it's already
68
+ # registered, this is a better method for any
69
+ # module to use to define colors
70
+ # #register should be used for redefining a color pair values
71
+ def register_default(name, foreground, background)
72
+ return if registered? name
73
+ register(name, foreground, background)
74
+ end
75
+
56
76
  # use color pair for the next printed text
77
+ # window(Curses Window):: that we need to change it's colors
57
78
  # name(Symbol):: a color pair name registered before with #register
58
- # type(Colors::Constant):: a text style constant defined in Colors, that manipulate the text style (Bold, Underline, Invert colors)
59
- def use(name, type = NORMAL)
79
+ # type(Colors::Constant):: a text style constant
80
+ # defined in Colors, that manipulate the text style
81
+ # (Bold, Underline, Invert colors)
82
+ def use(window, name, type = NORMAL)
60
83
  index = COLORS.key?(name) ? COLORS[name] : 0
61
- Curses.attron(Curses.color_pair(index)| type)
84
+ window.attron(Curses.color_pair(index) | type)
85
+ end
86
+
87
+ # print string in Curses window in the choosen color and style
88
+ def print(window, *strings)
89
+ strings.each do |string|
90
+ use(window, string.color || DEFAULT_COLOR, string.style || NORMAL)
91
+ window << string
92
+ end
62
93
  end
63
94
  end
64
95
  end
65
96
  end
97
+
98
+ # for easier printing of colored string on the screen
99
+ # we can add color property to any string and then
100
+ # use it to print the string on the screen
101
+ class String
102
+ attr_accessor :color, :style
103
+
104
+ def colorize(color, style = Amun::Helpers::Colors::NORMAL)
105
+ self.color = color
106
+ self.style = style
107
+ self
108
+ end
109
+ end
@@ -0,0 +1,28 @@
1
+ require 'curses'
2
+
3
+ module Amun
4
+ module Helpers
5
+ module Keyboard
6
+ module_function
7
+
8
+ TIMEOUT = 100
9
+
10
+ def char
11
+ ch = Curses.stdscr.get_char
12
+ Curses.stdscr.timeout = TIMEOUT
13
+ modified_char = Curses.stdscr.get_char if ch == "\e"
14
+ Curses.stdscr.timeout = -1
15
+
16
+ return ch if modified_char.nil?
17
+ return "#{ch} #{modified_char}" if modified_char.is_a? Numeric
18
+ return "#{ch} #{modified_char}" if modified_char.size > 1
19
+
20
+ begin
21
+ eval "?\\M-#{modified_char}"
22
+ rescue SyntaxError
23
+ return "#{ch} #{modified_char}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,60 @@
1
+ require 'amun/helpers/colors'
2
+ require 'amun/behaviours/emacs'
3
+
4
+ module Amun
5
+ module MajorModes
6
+ # Basic mode that show any IO
7
+ class Fundamental
8
+ include Behaviours::Emacs
9
+
10
+ def initialize(buffer)
11
+ @buffer = buffer
12
+
13
+ @events = EventManager.new
14
+ @events.bind_all self, :event_handler
15
+ emacs_behaviour_initialize(@events)
16
+
17
+ read_io if buffer.text.nil?
18
+ end
19
+
20
+ def trigger(event)
21
+ EventManager.join(event, @events)
22
+ end
23
+
24
+ def render(window)
25
+ window.clear
26
+ point = buffer.point
27
+
28
+ window << buffer.text[0...point]
29
+ window.attron(Helpers::Colors::REVERSE)
30
+
31
+ at_point = buffer.text[point]
32
+ window << (at_point == "\n" || at_point.nil? ? " \n" : at_point)
33
+ window.attroff(Helpers::Colors::REVERSE)
34
+ window << buffer.text[(point + 1)..-1]
35
+ end
36
+
37
+ def event_handler(event)
38
+ return true unless event.is_a? String
39
+ return true unless event.length == 1
40
+ return true unless event.valid_encoding?
41
+
42
+ case event
43
+ when /[^[:print:]\n\t]/
44
+ true
45
+ else
46
+ buffer.text.insert(buffer.point, event)
47
+ buffer.point += 1
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ attr_accessor :buffer
54
+
55
+ def read_io
56
+ buffer.text = buffer.io.read
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,90 @@
1
+ require 'set'
2
+ require 'amun/event_manager'
3
+ require 'amun/major_modes/fundamental'
4
+ require 'amun/ui/mode_line'
5
+
6
+ module Amun
7
+ module UI
8
+ # A buffer could present any kind of IO object (File, StringIO...etc)
9
+ # also it has a major mode responsible update lines and visual lines
10
+ class Buffer
11
+ attr_accessor :name, :io, :text
12
+ attr_writer :major_mode, :minor_modes, :mode_line, :point, :mark
13
+
14
+ def initialize(name, io = StringIO.new)
15
+ self.io = io
16
+ self.name = name
17
+ self.point = 0
18
+ end
19
+
20
+ def major_mode
21
+ @major_mode ||= MajorModes::Fundamental.new(self)
22
+ end
23
+
24
+ def mode_line
25
+ @mode_line ||= UI::ModeLine.new(self)
26
+ end
27
+
28
+ def minor_modes
29
+ @minor_modes ||= Set.new
30
+ end
31
+
32
+ def point
33
+ return 0 if @point < 0
34
+ max = text.size
35
+ return max if @point > max
36
+ @point
37
+ end
38
+
39
+ def mark
40
+ return 0 if @mark < 0
41
+ max = text.size
42
+ return max if @mark > max
43
+ @mark
44
+ end
45
+
46
+ def trigger(event)
47
+ EventManager.join(
48
+ event,
49
+ *(minor_modes + [major_mode])
50
+ )
51
+ end
52
+
53
+ def render(window)
54
+ major_mode_window = window.subwin(window.maxy - 1, window.maxx, 0, 0)
55
+ mode_line_window = window.subwin(1, window.maxx, window.maxy - 1, 0)
56
+
57
+ major_mode.render(major_mode_window)
58
+ mode_line.render(mode_line_window)
59
+ ensure
60
+ major_mode_window.close
61
+ mode_line_window.close
62
+ end
63
+
64
+ class << self
65
+ attr_writer :current, :instances
66
+
67
+ def instances
68
+ @instances ||= Set.new
69
+ end
70
+
71
+ def current
72
+ @current ||= scratch
73
+ end
74
+
75
+ def scratch
76
+ @scratch ||= new('*Scratch*')
77
+ instances << @scratch
78
+ @scratch
79
+ end
80
+
81
+ def messages
82
+ @messages ||= new('*Messages*')
83
+ instances << @messages
84
+ @messages.text = '' if @messages.text.nil?
85
+ @messages
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,25 +1,41 @@
1
- require 'curses'
1
+ require 'amun/ui/buffer'
2
2
 
3
3
  module Amun
4
4
  module UI
5
5
  # a line that is rendered by default at the end on the screen
6
6
  # takes one line height and extends to take the whole width of screen
7
- # should be linked to \*messages\* memory buffer and each time the #log
8
- # is called it will display the message next time #render is called
7
+ # should be linked to \*messages\* memory buffer and display new messages
8
+ # in the buffer text
9
9
  class EchoArea
10
+ attr_writer :events
11
+
12
+ def events
13
+ @events ||= EventManager.new
14
+ end
15
+
10
16
  def initialize
11
- @window = Curses::Window.new(1, Curses.cols, Curses.lines - 1, 0)
17
+ @last_messages_size = 0
12
18
  end
13
19
 
14
- # display a *message* in the echo area
15
- def echo(message)
16
- @window << message
20
+ def trigger(event)
21
+ EventManager.join(event, events)
17
22
  end
18
23
 
19
24
  # render the echo area window
20
- def refresh
21
- @window.noutrefresh
22
- @window.clear
25
+ def render(window)
26
+ window.clear
27
+ window << message
28
+ update_last_messages_size
29
+ end
30
+
31
+ private
32
+
33
+ def message
34
+ Buffer.messages.text[@last_messages_size..-1]
35
+ end
36
+
37
+ def update_last_messages_size
38
+ @last_messages_size = Buffer.messages.text.size
23
39
  end
24
40
  end
25
41
  end
@@ -0,0 +1,44 @@
1
+ require 'amun/helpers/colors'
2
+ require 'amun/ui/mode_line_segments/major_mode'
3
+ require 'amun/ui/mode_line_segments/buffer_name'
4
+
5
+ module Amun
6
+ module UI
7
+ # a line of small segments that display
8
+ # information about the current buffer,
9
+ # like mode name, line number, buffer name...etc
10
+ class ModeLine
11
+ attr_reader :left_segments, :right_segments
12
+
13
+ def initialize(buffer)
14
+ @buffer = buffer
15
+ @right_segments = []
16
+ @left_segments = [
17
+ ModeLineSegments::BufferName.new(buffer),
18
+ ModeLineSegments::MajorMode.new(buffer)
19
+ ]
20
+
21
+ Helpers::Colors.register_default(:mode_line, 0, 255)
22
+ end
23
+
24
+ def render(window)
25
+ right_output = render_segments(right_segments, window)
26
+ left_output = render_segments(left_segments, window)
27
+
28
+ size = (right_output + left_output).map(&:size).inject(0, :+)
29
+ empty_space = [0, window.maxx - size].max
30
+ filler = (' ' * empty_space).colorize(:mode_line)
31
+
32
+ Helpers::Colors.print(window, *left_output, filler, *right_output)
33
+ end
34
+
35
+ private
36
+
37
+ def render_segments(segments, window)
38
+ segments.map do |segment|
39
+ segment.render(window)
40
+ end.flatten
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ module Amun
2
+ module UI
3
+ module ModeLineSegments
4
+ # display buffer name in modeline
5
+ class BufferName
6
+ def initialize(buffer)
7
+ @buffer = buffer
8
+ end
9
+
10
+ def render(*)
11
+ "#{@buffer.name} ".colorize(:mode_line)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ module Amun
2
+ module UI
3
+ module ModeLineSegments
4
+ # display major mode name in mode line
5
+ class MajorMode
6
+ def initialize(buffer)
7
+ @buffer = buffer
8
+ end
9
+
10
+ def render(*)
11
+ mode = @buffer.major_mode.class.name.split('::').last
12
+ "(#{mode}) ".colorize(:mode_line)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ require 'amun/ui/buffer'
2
+
3
+ module Amun
4
+ module UI
5
+ module Windows
6
+ # a window to display any buffer
7
+ # or the current buffer
8
+ class BufferWindow
9
+ def initialize(buffer = nil)
10
+ @buffer = buffer
11
+ end
12
+
13
+ # set a specific buffer to be displayed in this window
14
+ def display_buffer(buffer)
15
+ @buffer = buffer
16
+ end
17
+
18
+ # render current buffer from the Buffer class
19
+ def display_current_buffer
20
+ @buffer = nil
21
+ end
22
+
23
+ # get current buffer that this window is rendering
24
+ def buffer
25
+ @buffer || Buffer.current
26
+ end
27
+
28
+ # render buffer
29
+ def render(window)
30
+ buffer.render(window)
31
+ end
32
+
33
+ # trigger the event in buffer
34
+ def trigger(event)
35
+ buffer.trigger(event)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,75 @@
1
+ require 'curses'
2
+ require 'amun/event_manager'
3
+ require 'amun/ui/windows/buffer_window'
4
+ require 'amun/ui/echo_area'
5
+
6
+ module Amun
7
+ module UI
8
+ module Windows
9
+ # a Frame fills all the space in terminal
10
+ # renders an echo area and an object that
11
+ # respond to #render and #trigger, like buffer,
12
+ # or another window or so
13
+ class Frame
14
+ attr_writer :screen, :echo_area, :window
15
+
16
+ def window
17
+ @window ||= BufferWindow.new
18
+ end
19
+
20
+ def echo_area
21
+ @echo_area ||= EchoArea.new
22
+ end
23
+
24
+ def trigger(event)
25
+ EventManager.join(event, echo_area, window, EventManager)
26
+ rescue StandardError => e
27
+ handle_exception(e)
28
+ ensure
29
+ render
30
+ end
31
+
32
+ def render
33
+ render_content
34
+ render_echo_area
35
+ end
36
+
37
+ private
38
+
39
+ def screen
40
+ @screen ||= Curses.stdscr
41
+ end
42
+
43
+ def content_window
44
+ @content_window ||= screen.subwin(screen.maxy - 1, screen.maxx, 0, 0)
45
+ end
46
+
47
+ def echo_window
48
+ @echo_window ||= screen.subwin(1, screen.maxx, screen.maxy - 1, 0)
49
+ end
50
+
51
+ def render_content
52
+ begin
53
+ window.render(content_window)
54
+ rescue StandardError => e
55
+ handle_exception(e)
56
+ end
57
+ content_window.refresh
58
+ end
59
+
60
+ def render_echo_area
61
+ begin
62
+ echo_area.render(echo_window)
63
+ rescue StandardError => e
64
+ handle_exception(e)
65
+ end
66
+ echo_window.refresh
67
+ end
68
+
69
+ def handle_exception(e)
70
+ Buffer.messages.text << "#{e.message} (#{e.backtrace.first})\n"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
data/lib/amun/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Amun
2
- VERSION = "0.1.2"
2
+ VERSION = '0.1.3'.freeze
3
3
  end
data/lib/amun.rb CHANGED
@@ -1,65 +1,6 @@
1
1
  require 'amun/version'
2
- require 'amun/event_manager'
3
- require 'amun/ui/echo_area'
4
- require 'curses'
2
+ require 'amun/application'
5
3
 
4
+ # King of the gods and god of the wind
6
5
  module Amun
7
- module_function
8
-
9
- class Application
10
- attr_accessor :keyboard, :events, :echo_area
11
-
12
- def self.instance
13
- @instance ||= new
14
- end
15
-
16
- def quit(_event)
17
- exit 0
18
- end
19
-
20
- def write_char(char)
21
- screen.addstr(char.to_s)
22
- end
23
-
24
- def run
25
- init_curses
26
- init_ui
27
-
28
- echo_area.echo 'Press ESC to exit.'
29
-
30
- Thread.new do
31
- while ch = screen.get_char
32
- keyboard.trigger(ch)
33
- echo_area.refresh
34
- end
35
- end.join
36
- end
37
-
38
- def screen
39
- @screen ||= Curses.stdscr
40
- end
41
-
42
- private
43
-
44
- def initialize(keyboard = EventManager.new, events: EventManager.new)
45
- self.keyboard = keyboard
46
- self.events = events
47
-
48
- keyboard.bind "\e", self, :quit
49
- keyboard.bind_all self, :write_char
50
- end
51
-
52
- def init_curses
53
- Curses.init_screen
54
- Curses.raw
55
- Curses.noecho
56
- Curses.start_color
57
- Curses.stdscr.keypad(true)
58
- Curses.ESCDELAY = 0
59
- end
60
-
61
- def init_ui
62
- self.echo_area = Amun::UI::EchoArea.new
63
- end
64
- end
65
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amun
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emad Elsaid
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-12 00:00:00.000000000 Z
11
+ date: 2017-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: curses
@@ -108,6 +108,34 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: codeclimate-test-reporter
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 1.0.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 1.0.0
111
139
  description: A CLI editor built to have an Emacs similar development environment,
112
140
  with ruby in the heart of it instead of Elisp, that will make developing plugins
113
141
  and extensions faster and more enjoyable, this editor is kept to the minimum, anything
@@ -120,6 +148,7 @@ extensions: []
120
148
  extra_rdoc_files: []
121
149
  files:
122
150
  - ".gitignore"
151
+ - ".rubocop.yml"
123
152
  - ".ruby-version"
124
153
  - ".travis.yml"
125
154
  - Gemfile
@@ -129,12 +158,24 @@ files:
129
158
  - Rakefile
130
159
  - amun.gemspec
131
160
  - bin/console
132
- - bin/setup
133
161
  - exe/amun
134
162
  - lib/amun.rb
163
+ - lib/amun/application.rb
164
+ - lib/amun/behaviours/emacs.rb
135
165
  - lib/amun/event_manager.rb
166
+ - lib/amun/features/echo_event.rb
167
+ - lib/amun/features/quit.rb
168
+ - lib/amun/features_loader.rb
136
169
  - lib/amun/helpers/colors.rb
170
+ - lib/amun/helpers/keyboard.rb
171
+ - lib/amun/major_modes/fundamental.rb
172
+ - lib/amun/ui/buffer.rb
137
173
  - lib/amun/ui/echo_area.rb
174
+ - lib/amun/ui/mode_line.rb
175
+ - lib/amun/ui/mode_line_segments/buffer_name.rb
176
+ - lib/amun/ui/mode_line_segments/major_mode.rb
177
+ - lib/amun/ui/windows/buffer_window.rb
178
+ - lib/amun/ui/windows/frame.rb
138
179
  - lib/amun/version.rb
139
180
  homepage: http://www.github.com/blazeeboy/amun
140
181
  licenses:
data/bin/setup DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install