belajar 0.1.1

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 (91) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +5 -0
  6. data/README.md +5 -0
  7. data/Rakefile +5 -0
  8. data/belajar.gemspec +38 -0
  9. data/bin/belajar +12 -0
  10. data/lib/belajar.rb +24 -0
  11. data/lib/belajar/chapter.rb +23 -0
  12. data/lib/belajar/configuration.rb +89 -0
  13. data/lib/belajar/congratulator.rb +13 -0
  14. data/lib/belajar/course.rb +80 -0
  15. data/lib/belajar/exceptions.rb +19 -0
  16. data/lib/belajar/generator.rb +60 -0
  17. data/lib/belajar/github_client.rb +30 -0
  18. data/lib/belajar/loadable.rb +23 -0
  19. data/lib/belajar/loading/chapters.rb +9 -0
  20. data/lib/belajar/loading/courses.rb +9 -0
  21. data/lib/belajar/loading/units.rb +9 -0
  22. data/lib/belajar/reference_solution.rb +18 -0
  23. data/lib/belajar/solution.rb +53 -0
  24. data/lib/belajar/storeable.rb +32 -0
  25. data/lib/belajar/task.rb +10 -0
  26. data/lib/belajar/terminal.rb +12 -0
  27. data/lib/belajar/terminal/cli.rb +59 -0
  28. data/lib/belajar/terminal/courses.rb +179 -0
  29. data/lib/belajar/terminal/output.rb +78 -0
  30. data/lib/belajar/terminal/setup.rb +115 -0
  31. data/lib/belajar/terminal/solutions.rb +46 -0
  32. data/lib/belajar/terminal/texts/about.txt +19 -0
  33. data/lib/belajar/terminal/texts/congratulations.txt +12 -0
  34. data/lib/belajar/terminal/texts/courses_empty.txt +3 -0
  35. data/lib/belajar/terminal/texts/hint_course_download.txt +13 -0
  36. data/lib/belajar/terminal/texts/welcome.txt +12 -0
  37. data/lib/belajar/terminal/welcome.rb +98 -0
  38. data/lib/belajar/test.rb +46 -0
  39. data/lib/belajar/test_result.rb +86 -0
  40. data/lib/belajar/unit.rb +28 -0
  41. data/lib/belajar/version.rb +3 -0
  42. data/lib/belajar/views.rb +51 -0
  43. data/lib/belajar/views/chapters_menu.rb +60 -0
  44. data/lib/belajar/views/courses_menu.rb +52 -0
  45. data/lib/belajar/views/main_menu.rb +37 -0
  46. data/lib/belajar/views/menu.rb +89 -0
  47. data/lib/belajar/views/splash.rb +57 -0
  48. data/lib/belajar/views/task_view.rb +236 -0
  49. data/lib/belajar/views/top_bar.rb +40 -0
  50. data/lib/belajar/views/units_menu.rb +61 -0
  51. data/lib/belajar/window.rb +215 -0
  52. data/spec/belajar/chapter_spec.rb +76 -0
  53. data/spec/belajar/configuration_spec.rb +161 -0
  54. data/spec/belajar/congratulator_spec.rb +24 -0
  55. data/spec/belajar/course_spec.rb +201 -0
  56. data/spec/belajar/generator_spec.rb +82 -0
  57. data/spec/belajar/github_client_spec.rb +53 -0
  58. data/spec/belajar/loading/chapters_spec.rb +16 -0
  59. data/spec/belajar/loading/courses_spec.rb +16 -0
  60. data/spec/belajar/loading/units_spec.rb +21 -0
  61. data/spec/belajar/reference_solution_spec.rb +41 -0
  62. data/spec/belajar/solution_spec.rb +86 -0
  63. data/spec/belajar/storeable_spec.rb +35 -0
  64. data/spec/belajar/task_spec.rb +23 -0
  65. data/spec/belajar/terminal/cli_spec.rb +51 -0
  66. data/spec/belajar/terminal/courses_spec.rb +293 -0
  67. data/spec/belajar/terminal/output_spec.rb +151 -0
  68. data/spec/belajar/terminal/setup_spec.rb +10 -0
  69. data/spec/belajar/terminal/solutions_spec.rb +8 -0
  70. data/spec/belajar/terminal/welcome_spec.rb +12 -0
  71. data/spec/belajar/terminal_spec.rb +24 -0
  72. data/spec/belajar/test_example_spec.rb +54 -0
  73. data/spec/belajar/test_result_spec.rb +91 -0
  74. data/spec/belajar/test_spec.rb +48 -0
  75. data/spec/belajar/unit_spec.rb +85 -0
  76. data/spec/belajar/views/chapters_menu_spec.rb +6 -0
  77. data/spec/belajar/views/courses_menu_spec.rb +6 -0
  78. data/spec/belajar/views/menu_spec.rb +19 -0
  79. data/spec/belajar/views/task_view_spec.rb +7 -0
  80. data/spec/belajar/views/units_menu_spec.rb +6 -0
  81. data/spec/belajar/views_spec.rb +21 -0
  82. data/spec/belajar_spec.rb +51 -0
  83. data/spec/path_helpers_spec.rb +60 -0
  84. data/spec/resource_helpers_spec.rb +33 -0
  85. data/spec/spec_helper.rb +28 -0
  86. data/spec/support/macros/content_helpers.rb +129 -0
  87. data/spec/support/macros/mock_helpers.rb +25 -0
  88. data/spec/support/macros/path_helpers.rb +139 -0
  89. data/spec/support/macros/resource_helpers.rb +157 -0
  90. data/spec/support/macros/test_helpers.rb +6 -0
  91. metadata +385 -0
@@ -0,0 +1,28 @@
1
+ module Belajar
2
+ class Unit
3
+
4
+ attr_reader :title, :task
5
+
6
+ def initialize(path)
7
+ @path = path
8
+ @title = File.basename(path).gsub(/\_+/, ' ')
9
+ end
10
+
11
+ def task
12
+ @task ||= Task.new(@path)
13
+ end
14
+
15
+ def reference_solution
16
+ @reference_solution ||= ReferenceSolution.new(@path)
17
+ end
18
+
19
+ def solution
20
+ @solution = Solution.new(@path)
21
+ end
22
+
23
+ def mastered?
24
+ solution.verified?
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Belajar
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,51 @@
1
+ require 'curses'
2
+ require 'active_support/concern'
3
+ require 'belajar/views/top_bar'
4
+
5
+ module Belajar
6
+ module Views
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include Curses
11
+
12
+ def reset_menu_position
13
+ @position = 0
14
+ end
15
+
16
+ private
17
+
18
+ def default_window(height = nil, width = nil, top = 0, left = 0)
19
+ init_screen
20
+
21
+ noecho
22
+ crmode
23
+ curs_set(0) # invisible cursor
24
+
25
+ height ||= lines
26
+ width ||= cols + 1
27
+
28
+ window = Belajar::Window.new(height, width, top, left)
29
+
30
+ Curses.lines.times do |line|
31
+ window.setpos(line, 0)
32
+ window.clear_line
33
+ end
34
+
35
+ window.keypad(true)
36
+ window.scrollok(true)
37
+ window.refresh
38
+ window
39
+ end
40
+
41
+ def sub_window_below_top_bar(window, top_bar)
42
+ top_bar.show
43
+ top = top_bar.height
44
+ sub_window = window.subwin(window.maxy - top, window.maxx, top, 0)
45
+ sub_window.keypad(true)
46
+ sub_window
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ require 'belajar/views/menu'
2
+
3
+ module Belajar
4
+ module Views
5
+
6
+ class ChaptersMenu < Menu
7
+
8
+ private
9
+
10
+ def before_enter(*args)
11
+ @course = args[0]
12
+ end
13
+
14
+ def before_reenter(*args)
15
+ @course = args[0]
16
+ @chapter = args[1]
17
+ @position = @course.chapters.find_index(@chapter)
18
+ end
19
+
20
+ def header_text
21
+ "*#{@course.title}* - available chapters:"
22
+ end
23
+
24
+ def interact_with(window)
25
+ while char = window.getch
26
+ case char
27
+ when KEY_UP
28
+ @position -= 1
29
+ broadcast(:reset_menu_position)
30
+ when KEY_DOWN
31
+ @position += 1
32
+ broadcast(:reset_menu_position)
33
+ when 10 # Enter
34
+ broadcast(:enter, @course, models[@position])
35
+ return
36
+ when 263 # Backspace
37
+ broadcast(:reenter, @course)
38
+ return
39
+ when 27 # ESC
40
+ exit
41
+ end
42
+
43
+ @position = items.length - 1 if @position < 0
44
+ @position = 0 if @position >= items.length
45
+ draw(window, @position)
46
+ end
47
+ end
48
+
49
+ def models
50
+ @course.chapters
51
+ end
52
+
53
+ def items
54
+ models.map(&:title)
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,52 @@
1
+ require 'belajar/views/menu'
2
+
3
+ module Belajar
4
+ module Views
5
+
6
+ class CoursesMenu < Menu
7
+
8
+ private
9
+
10
+ def header_text
11
+ 'Available belajar courses:'
12
+ end
13
+
14
+ def interact_with(window)
15
+ while char = window.getch
16
+ case char
17
+ when KEY_UP
18
+ @position -= 1
19
+ broadcast(:reset_menu_position)
20
+ when KEY_DOWN
21
+ @position += 1
22
+ broadcast(:reset_menu_position)
23
+ when 10 # Enter
24
+ broadcast(:enter, models[@position])
25
+ return
26
+ when 27 # ESC
27
+ exit
28
+ end
29
+
30
+ @position = items.length - 1 if @position < 0
31
+ @position = 0 if @position >= items.length
32
+ draw(window, @position)
33
+ end
34
+ end
35
+
36
+ def models
37
+ Loading::Courses.load(Belajar.config.courses_path)
38
+ end
39
+
40
+ def items
41
+ non_empty_courses = models.select { |course| !course.chapters.empty? }
42
+
43
+ non_empty_courses.map do |course|
44
+ line = "#{course.title}"
45
+ self.items_info <<= [(course.author ? "(by #{course.author})" : '')]
46
+ line
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,37 @@
1
+ module Belajar
2
+ module Views
3
+
4
+ class MainMenu
5
+ include Views
6
+
7
+ def initialize
8
+ courses_menu = Views::CoursesMenu.new
9
+ chapters_menu = Views::ChaptersMenu.new
10
+ units_menu = Views::UnitsMenu.new
11
+ task_view = Views::TaskView.new
12
+
13
+ # Subscription: `first.subscribe(second)` means
14
+ # first subscribes second on the first's broadcast.
15
+ # second has to have method that is broadcasted.
16
+
17
+ # top down navigation
18
+ courses_menu.subscribe(chapters_menu, on: :enter)
19
+ chapters_menu.subscribe(units_menu, on: :enter)
20
+ units_menu.subscribe(task_view, on: :enter)
21
+
22
+ # bottom up navigation
23
+ chapters_menu.subscribe(courses_menu, on: :reenter)
24
+ units_menu.subscribe(chapters_menu, on: :reenter)
25
+ task_view.subscribe(units_menu, on: :reenter)
26
+
27
+ # position reset
28
+ courses_menu.subscribe(chapters_menu, on: :reset_menu_position)
29
+ courses_menu.subscribe(units_menu, on: :reset_menu_position)
30
+ chapters_menu.subscribe(units_menu, on: :reset_menu_position)
31
+
32
+ courses_menu.enter
33
+ end
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,89 @@
1
+ require 'wisper'
2
+
3
+ module Belajar
4
+ module Views
5
+
6
+ class Menu
7
+ include Views
8
+ include Wisper::Publisher
9
+
10
+ TOP_BAR_TEXT = [
11
+ 'Use *UP KEY* and *DOWN KEY* for menu navigation',
12
+ 'Enter menu with *RETURN*',
13
+ 'Go back with *BACKSPACE*',
14
+ 'Exit with *ESC*'
15
+ ].join(' | ')
16
+
17
+ def initialize
18
+ @position = 0
19
+ end
20
+
21
+ def enter(*args)
22
+ if self.class.private_method_defined?(:before_enter)
23
+ before_enter(*args)
24
+ end
25
+
26
+ @window = default_window
27
+ top_bar = TopBar.new(@window, TOP_BAR_TEXT)
28
+ show sub_window_below_top_bar(@window, top_bar)
29
+ end
30
+
31
+ def reenter(*args)
32
+ if self.class.private_method_defined?(:before_reenter)
33
+ before_reenter(*args)
34
+ end
35
+
36
+ enter(*args)
37
+ end
38
+
39
+ protected
40
+
41
+ def show(window)
42
+ draw(window, @position)
43
+ interact_with(window)
44
+ end
45
+
46
+ def draw(window, active_index = 0)
47
+ window.attrset(A_NORMAL)
48
+ window.setpos(0, 1)
49
+ window.print_markdown header_text
50
+
51
+ items.each_with_index do |item, index|
52
+ window.setpos(index + 2, 1)
53
+ window.print_indicator(models[index])
54
+ window.attrset(index == active_index ? A_STANDOUT : A_NORMAL)
55
+ window.write " #{item.to_s} "
56
+ window.attrset(A_NORMAL)
57
+ window.write " #{items_info[index].try(:join, ' ')}"
58
+ end
59
+
60
+ window.refresh
61
+ end
62
+
63
+ def interact_with(window)
64
+ raise "Please implement the method #interact_with!"
65
+ end
66
+
67
+ def models
68
+ raise "Please implement the method #models!"
69
+ end
70
+
71
+ def items
72
+ raise "Please implement the method #items!"
73
+ end
74
+
75
+ def header_text
76
+ raise "Please implement the method #header_text!"
77
+ end
78
+
79
+ def items_info
80
+ @items_info || []
81
+ end
82
+
83
+ def items_info=(items_info)
84
+ @items_info = items_info
85
+ end
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,57 @@
1
+ module Belajar
2
+ module Views
3
+
4
+ class Splash
5
+ include Views
6
+
7
+ def initialize
8
+ title = "BELAJAR"
9
+ subtitle = "Learning the Ruby programming language dead easy."
10
+
11
+ panel = default_window
12
+
13
+ lines.times do |line|
14
+ panel.setpos(line, 0)
15
+ panel.red(' ' * cols, Curses::A_STANDOUT)
16
+ end
17
+
18
+ panel.setpos((lines / 4), (cols - title.length) / 2)
19
+ panel.red(title, Curses::A_STANDOUT)
20
+ panel.refresh
21
+
22
+ sleep 0.5
23
+
24
+ ruby_ascii_art.each_with_index do |line, index|
25
+ panel.setpos(lines / 4 + 2 + index, (cols - line.length) / 2)
26
+ panel.red(line, Curses::A_STANDOUT)
27
+ sleep 0.06
28
+ panel.refresh
29
+ end
30
+
31
+ panel.setpos(lines / 4 + 11, (cols - subtitle.length) / 2)
32
+
33
+ subtitle.chars do |char|
34
+ panel.red(char, Curses::A_STANDOUT)
35
+ panel.refresh
36
+ sleep 0.02
37
+ end
38
+
39
+ sleep 2.5
40
+
41
+ close_screen
42
+ end
43
+
44
+ def ruby_ascii_art
45
+ [
46
+ " ___________ ",
47
+ " /.\\ /.\\ /.\\ ",
48
+ "/___\\/___\\/___\\",
49
+ " \\ \\ . / . / ",
50
+ " \\ \\ ./ ./ ",
51
+ " \\\\ / / ",
52
+ " \\./ "
53
+ ]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,236 @@
1
+ module Belajar
2
+ module Views
3
+ require 'wisper'
4
+ require 'os'
5
+
6
+ class TaskView
7
+ include Views
8
+ include Wisper::Publisher
9
+
10
+ TOP_BAR_TEXT = [
11
+ 'Scroll with *UP KEY* and *DOWN KEY*',
12
+ 'Verify solution with *v*',
13
+ 'Open solution file with *o*',
14
+ 'Clear validation with *c*',
15
+ 'Exit with *ESC*'
16
+ ].join(' | ')
17
+
18
+ def initialize
19
+ @lines = []
20
+ @top = nil
21
+ end
22
+
23
+ def enter(course, chapter, unit)
24
+ @course = course
25
+ @chapter = chapter
26
+ @unit = unit
27
+
28
+ @test_result_lines = nil
29
+ @lines = @unit.task.markdown.lines
30
+ @top_bar_height = 4
31
+ @head_height = 2
32
+
33
+ initialize_window(@lines.count + @top_bar_height + @head_height)
34
+ end
35
+
36
+ private
37
+
38
+ def set_head(window)
39
+ window.setpos(0, 1)
40
+ window.clear_line
41
+
42
+ text = "*#{@course.title}* > *#{@chapter.title}* > *#{@unit.title}*:"
43
+ window.print_markdown(text)
44
+
45
+ window.setpos(1, 1)
46
+ window.clear_line
47
+ end
48
+
49
+ def show(window)
50
+ window.scrollok(true)
51
+ draw(window)
52
+ interact_with(window)
53
+ end
54
+
55
+ def draw(window)
56
+ @top = 0
57
+ window.attrset(A_NORMAL)
58
+ set_head(window)
59
+
60
+ @lines.each_with_index do |line, index|
61
+ window.setpos(index + 2, 1)
62
+ print_line(window, line, index)
63
+ end
64
+
65
+ window.setpos(0, 1)
66
+ window.refresh
67
+ end
68
+
69
+ def scroll_up(window)
70
+ if @top > 0
71
+ window.scrl(-1)
72
+ set_head(window)
73
+
74
+ @top -= 1
75
+ line = @lines[@top]
76
+
77
+ if line
78
+ window.setpos(2, 1)
79
+ print_line(window, line, @top)
80
+ end
81
+ end
82
+ end
83
+
84
+ def scroll_down(window)
85
+ if @top + Curses.lines <= @lines.count + @top_bar_height + @head_height
86
+ window.scrl(1)
87
+ set_head(window)
88
+
89
+ @top += 1
90
+ line = @lines[@top + window.maxy - 1]
91
+ window.setpos(window.maxy - 1, 1)
92
+
93
+ if line
94
+ print_line(window, line, @top)
95
+ else
96
+ window.clear_line
97
+ end
98
+
99
+ return true
100
+ else
101
+ return false
102
+ end
103
+ end
104
+
105
+ def print_line(window, line, index)
106
+ if @test_result_lines && index.between?(0, @test_result_lines.count + 1)
107
+ if @unit.mastered?
108
+ window.green(line, A_STANDOUT, full_line: true)
109
+ else
110
+ example_index = example_index(@example_heights, index)
111
+
112
+ if @examples[example_index].passed?
113
+ window.green(line, A_STANDOUT, full_line: true)
114
+ else
115
+ window.red(line, A_STANDOUT, full_line: true)
116
+ end
117
+ end
118
+ else
119
+ window.print_markdown(line.strip)
120
+ end
121
+ end
122
+
123
+ def interact_with(window)
124
+ while char = window.getch
125
+ scrollable = true
126
+
127
+ case char
128
+ when 'v' # verify
129
+ print_test_results(window)
130
+ return
131
+ when 'c' # clear
132
+ reset_screen(window)
133
+ return
134
+ when 'o' # open solution file
135
+ open_editor(@unit.solution.path)
136
+ when Curses::KEY_DOWN, Curses::KEY_CTRL_N
137
+ scrollable = scroll_down(window)
138
+ when Curses::KEY_UP, Curses::KEY_CTRL_P
139
+ scrollable = scroll_up(window)
140
+ when Curses::KEY_NPAGE, ?\s # white space
141
+ 0.upto(window.maxy - 2) do |n|
142
+ scrolled = scroll_down(window)
143
+
144
+ unless scrolled
145
+ scrollable = false if n == 0
146
+ break
147
+ end
148
+ end
149
+ when Curses::KEY_PPAGE
150
+ 0.upto(window.maxy - 2) do |n|
151
+ scrolled = scroll_up(window)
152
+
153
+ unless scrolled
154
+ scrollable = false if n == 0
155
+ break
156
+ end
157
+ end
158
+ when Curses::KEY_LEFT, Curses::KEY_CTRL_T
159
+ while scroll_up(window)
160
+ end
161
+ when Curses::KEY_RIGHT, Curses::KEY_CTRL_B
162
+ while scroll_down(window)
163
+ end
164
+ when Curses::KEY_BACKSPACE
165
+ broadcast(:reenter, @course, @chapter, @unit)
166
+ return
167
+ when 27 # ESC
168
+ exit
169
+ end
170
+
171
+ Curses.beep unless scrollable
172
+ window.setpos(0, 1)
173
+ window.refresh
174
+ end
175
+ end
176
+
177
+ def open_editor(file_path)
178
+ if OS.windows?
179
+ system "start '' '#{file_path}'"
180
+ elsif OS.mac?
181
+ system "open '#{file_path}'"
182
+ elsif OS.linux?
183
+ system "xdg-open '#{file_path}'"
184
+ end
185
+ end
186
+
187
+ def print_test_results(window)
188
+ result = @unit.solution.verify!
189
+ @test_result_lines = result.summary_lines
190
+
191
+ if result.passed?
192
+ code_lines = @unit.reference_solution.code_lines
193
+
194
+ unless code_lines.empty?
195
+ code_lines.map! { |line| " #{line}" }
196
+ code_lines.unshift('', "Reference code:", '')
197
+ end
198
+
199
+ @test_result_lines += code_lines
200
+ end
201
+
202
+ @lines = [''] + @test_result_lines + ['', ''] + @unit.task.markdown.lines
203
+ @examples = result.examples
204
+
205
+ @example_heights = @examples.reduce({}) do |hash, example|
206
+ start = hash.values.reduce(0) { |sum, r| sum += r.count }
207
+ range = (start..(start + example.message.split("\n").count) + 2)
208
+ hash[hash.keys.count] = range
209
+ hash
210
+ end
211
+
212
+ height = [@lines.count + @top_bar_height + @head_height, Curses.lines].max
213
+ initialize_window(height)
214
+ end
215
+
216
+ def reset_screen(window)
217
+ @test_result_lines = nil
218
+ @lines = @unit.task.markdown.lines
219
+ height = [@lines.count + @top_bar_height + @head_height, Curses.lines].max
220
+ initialize_window(height)
221
+ end
222
+
223
+ def initialize_window(height)
224
+ @window = default_window(height)
225
+
226
+ top_bar = TopBar.new(@window, TOP_BAR_TEXT)
227
+ show sub_window_below_top_bar(@window, top_bar)
228
+ end
229
+
230
+ def example_index(heights, index)
231
+ heights.values.index { |range| range.include?(index) }
232
+ end
233
+ end
234
+
235
+ end
236
+ end