belajar 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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