daigaku 0.0.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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/Gemfile +4 -0
  4. data/Guardfile +5 -0
  5. data/README.md +62 -0
  6. data/Rakefile +2 -0
  7. data/bin/daigaku +12 -0
  8. data/daigaku.gemspec +37 -0
  9. data/lib/daigaku.rb +27 -0
  10. data/lib/daigaku/chapter.rb +23 -0
  11. data/lib/daigaku/configuration.rb +86 -0
  12. data/lib/daigaku/course.rb +23 -0
  13. data/lib/daigaku/database.rb +64 -0
  14. data/lib/daigaku/exceptions.rb +19 -0
  15. data/lib/daigaku/generator.rb +53 -0
  16. data/lib/daigaku/loadable.rb +23 -0
  17. data/lib/daigaku/loading/chapters.rb +9 -0
  18. data/lib/daigaku/loading/courses.rb +9 -0
  19. data/lib/daigaku/loading/units.rb +9 -0
  20. data/lib/daigaku/reference_solution.rb +10 -0
  21. data/lib/daigaku/solution.rb +42 -0
  22. data/lib/daigaku/task.rb +10 -0
  23. data/lib/daigaku/terminal.rb +11 -0
  24. data/lib/daigaku/terminal/cli.rb +59 -0
  25. data/lib/daigaku/terminal/courses.rb +114 -0
  26. data/lib/daigaku/terminal/output.rb +72 -0
  27. data/lib/daigaku/terminal/setup.rb +115 -0
  28. data/lib/daigaku/terminal/solutions.rb +46 -0
  29. data/lib/daigaku/terminal/texts/about.txt +19 -0
  30. data/lib/daigaku/terminal/texts/courses_empty.txt +3 -0
  31. data/lib/daigaku/terminal/texts/hint_course_download.txt +13 -0
  32. data/lib/daigaku/terminal/texts/welcome.txt +12 -0
  33. data/lib/daigaku/terminal/welcome.rb +98 -0
  34. data/lib/daigaku/test.rb +46 -0
  35. data/lib/daigaku/test_result.rb +69 -0
  36. data/lib/daigaku/unit.rb +28 -0
  37. data/lib/daigaku/version.rb +3 -0
  38. data/lib/daigaku/views.rb +59 -0
  39. data/lib/daigaku/views/chapters_menu.rb +91 -0
  40. data/lib/daigaku/views/courses_menu.rb +87 -0
  41. data/lib/daigaku/views/main_menu.rb +37 -0
  42. data/lib/daigaku/views/splash.rb +57 -0
  43. data/lib/daigaku/views/task_view.rb +206 -0
  44. data/lib/daigaku/views/top_bar.rb +48 -0
  45. data/lib/daigaku/views/units_menu.rb +92 -0
  46. data/lib/daigaku/window.rb +160 -0
  47. data/spec/daigaku/chapter_spec.rb +76 -0
  48. data/spec/daigaku/configuration_spec.rb +161 -0
  49. data/spec/daigaku/course_spec.rb +75 -0
  50. data/spec/daigaku/database_spec.rb +79 -0
  51. data/spec/daigaku/generator_spec.rb +82 -0
  52. data/spec/daigaku/loading/chapters_spec.rb +16 -0
  53. data/spec/daigaku/loading/courses_spec.rb +16 -0
  54. data/spec/daigaku/loading/units_spec.rb +21 -0
  55. data/spec/daigaku/reference_solution_spec.rb +23 -0
  56. data/spec/daigaku/solution_spec.rb +79 -0
  57. data/spec/daigaku/task_spec.rb +23 -0
  58. data/spec/daigaku/terminal/cli_spec.rb +51 -0
  59. data/spec/daigaku/terminal/courses_spec.rb +60 -0
  60. data/spec/daigaku/terminal/output_spec.rb +123 -0
  61. data/spec/daigaku/terminal/setup_spec.rb +10 -0
  62. data/spec/daigaku/terminal/solutions_spec.rb +8 -0
  63. data/spec/daigaku/terminal/welcome_spec.rb +12 -0
  64. data/spec/daigaku/terminal_spec.rb +14 -0
  65. data/spec/daigaku/test_example_spec.rb +54 -0
  66. data/spec/daigaku/test_result_spec.rb +81 -0
  67. data/spec/daigaku/test_spec.rb +48 -0
  68. data/spec/daigaku/unit_spec.rb +85 -0
  69. data/spec/daigaku/views/chapters_menu_spec.rb +8 -0
  70. data/spec/daigaku/views/courses_menu_spec.rb +8 -0
  71. data/spec/daigaku/views/task_view_spec.rb +7 -0
  72. data/spec/daigaku/views/units_menu_spec.rb +8 -0
  73. data/spec/daigaku/views_spec.rb +23 -0
  74. data/spec/daigaku_spec.rb +57 -0
  75. data/spec/path_helpers_spec.rb +60 -0
  76. data/spec/resource_helpers_spec.rb +33 -0
  77. data/spec/spec_helper.rb +28 -0
  78. data/spec/support/macros/content_helpers.rb +129 -0
  79. data/spec/support/macros/mock_helpers.rb +20 -0
  80. data/spec/support/macros/path_helpers.rb +133 -0
  81. data/spec/support/macros/resource_helpers.rb +119 -0
  82. data/spec/support/macros/test_helpers.rb +6 -0
  83. metadata +361 -0
@@ -0,0 +1,87 @@
1
+ module Daigaku
2
+ module Views
3
+ require 'wisper'
4
+
5
+ class CoursesMenu
6
+ include Views
7
+ include Wisper::Publisher
8
+
9
+ def initialize
10
+ @position = 0
11
+ end
12
+
13
+ def enter
14
+ @window = default_window
15
+
16
+ main_panel(@window) do |window|
17
+ show sub_window_below_top_bar(window)
18
+ end
19
+ end
20
+
21
+ def reenter_courses_menu(course)
22
+ @current_course = course
23
+ enter
24
+ end
25
+
26
+ private
27
+
28
+ def show(window)
29
+ draw(window, @position)
30
+ interact_with(window)
31
+ end
32
+
33
+ def draw(window, active_index = 0)
34
+ window.attrset(A_NORMAL)
35
+ window.setpos(0, 1)
36
+ window.write 'Available daigaku courses:'
37
+
38
+ course_entries.each_with_index do |item, index|
39
+ window.setpos(index + 2, 1)
40
+ window.print_indicator(courses[index])
41
+ window.attrset(index == active_index ? A_STANDOUT : A_NORMAL)
42
+ window.write " #{item.to_s} "
43
+ end
44
+
45
+ window.refresh
46
+ end
47
+
48
+ def interact_with(window)
49
+ while char = window.getch
50
+ case char
51
+ when KEY_UP
52
+ @position -= 1
53
+ broadcast(:reset_menu_position)
54
+ when KEY_DOWN
55
+ @position += 1
56
+ broadcast(:reset_menu_position)
57
+ when 10 # Enter
58
+ broadcast(:enter_chapters_menu, courses[@position])
59
+ return
60
+ when 27 # ESC
61
+ exit
62
+ end
63
+
64
+ entries = course_entries
65
+ @position = entries.length - 1 if @position < 0
66
+ @position = 0 if @position >= entries.length
67
+ draw(window, @position)
68
+ end
69
+ end
70
+
71
+ def courses
72
+ @courses = Loading::Courses.load(Daigaku.config.courses_path)
73
+ end
74
+
75
+ def course_entries
76
+ non_empty_courses = courses.select { |course| !course.chapters.empty? }
77
+
78
+ @course_entries = non_empty_courses.map do |course|
79
+ line = "#{course.title}"
80
+ line << "(#{course.author})" if course.author
81
+ line
82
+ end
83
+ end
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,37 @@
1
+ module Daigaku
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_chapters_menu)
19
+ chapters_menu.subscribe(units_menu, on: :enter_units_menu)
20
+ units_menu.subscribe(task_view, on: :enter_task_view)
21
+
22
+ # bottom up navigation
23
+ chapters_menu.subscribe(courses_menu, on: :reenter_courses_menu)
24
+ units_menu.subscribe(chapters_menu, on: :reenter_chapters_menu)
25
+ task_view.subscribe(units_menu, on: :reenter_units_menu)
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,57 @@
1
+ module Daigaku
2
+ module Views
3
+
4
+ class Splash
5
+ include Views
6
+
7
+ def initialize
8
+ title = "DAIGAKU"
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,206 @@
1
+ module Daigaku
2
+ module Views
3
+ require 'wisper'
4
+ require 'os'
5
+
6
+ class TaskView
7
+ include Views
8
+ include Wisper::Publisher
9
+
10
+ def initialize
11
+ @lines = []
12
+ @top = nil
13
+ end
14
+
15
+ def enter_task_view(course, chapter, unit)
16
+ @course = course
17
+ @chapter = chapter
18
+ @unit = unit
19
+
20
+ @test_result_lines = nil
21
+ @lines = @unit.task.markdown.lines
22
+ @top_bar_height = 4
23
+ @head_height = 2
24
+
25
+ initialize_window(@lines.count + @top_bar_height + @head_height)
26
+ end
27
+
28
+ private
29
+
30
+ def set_head(window)
31
+ window.setpos(0, 1)
32
+ window.clear_line
33
+ window.emphasize @course.title
34
+ window.write ' > '
35
+ window.emphasize @chapter.title
36
+ window.write ' > '
37
+ window.emphasize @unit.title
38
+ window.write ':'
39
+
40
+ window.setpos(1, 1)
41
+ window.clear_line
42
+ end
43
+
44
+ def show(window)
45
+ window.scrollok(true)
46
+ draw(window)
47
+ interact_with(window)
48
+ end
49
+
50
+ def draw(window)
51
+ @top = 0
52
+ window.attrset(A_NORMAL)
53
+ set_head(window)
54
+
55
+ @lines.each_with_index do |line, index|
56
+ window.setpos(index + 2, 1)
57
+ print_line(window, line, index)
58
+ end
59
+
60
+ window.setpos(0, 1)
61
+ window.refresh
62
+ end
63
+
64
+ def scroll_up(window)
65
+ if @top > 0
66
+ window.scrl(-1)
67
+ set_head(window)
68
+
69
+ @top -= 1
70
+ line = @lines[@top]
71
+
72
+ if line
73
+ window.setpos(2, 1)
74
+ print_line(window, line, @top)
75
+ end
76
+ end
77
+ end
78
+
79
+ def scroll_down(window)
80
+ if @top + Curses.lines <= @lines.count + @top_bar_height + @head_height
81
+ window.scrl(1)
82
+ set_head(window)
83
+
84
+ @top += 1
85
+ line = @lines[@top + window.maxy - 1]
86
+ window.setpos(window.maxy - 1, 1)
87
+
88
+ if line
89
+ print_line(window, line, @top)
90
+ else
91
+ window.clear_line
92
+ end
93
+
94
+ return true
95
+ else
96
+ return false
97
+ end
98
+ end
99
+
100
+ def print_line(window, line, index)
101
+ if @test_result_lines && index.between?(0, @test_result_lines.count + 1)
102
+ if @unit.mastered?
103
+ window.green(line, A_STANDOUT, full_line: true)
104
+ else
105
+ example_index = ((index + 1) / 4.0).ceil - 1
106
+
107
+ if @examples[example_index].passed?
108
+ window.green(line, A_STANDOUT, full_line: true)
109
+ else
110
+ window.red(line, A_STANDOUT, full_line: true)
111
+ end
112
+ end
113
+ else
114
+ window.print_markdown(line.strip)
115
+ end
116
+ end
117
+
118
+ def interact_with(window)
119
+ while char = window.getch
120
+ scrollable = true
121
+
122
+ case char
123
+ when 'v' # Verifiy
124
+ print_test_results(window)
125
+ return
126
+ when 'c' # clear
127
+ reset_screen(window)
128
+ return
129
+ when 'o' # open solution file
130
+ open_editor(@unit.solution.path)
131
+ when Curses::KEY_DOWN, Curses::KEY_CTRL_N
132
+ scrollable = scroll_down(window)
133
+ when Curses::KEY_UP, Curses::KEY_CTRL_P
134
+ scrollable = scroll_up(window)
135
+ when Curses::KEY_NPAGE, ?\s # white space
136
+ 0.upto(window.maxy - 2) do |n|
137
+ scrolled = scroll_down(window)
138
+
139
+ unless scrolled
140
+ scrollable = false if n == 0
141
+ break
142
+ end
143
+ end
144
+ when Curses::KEY_PPAGE
145
+ 0.upto(window.maxy - 2) do |n|
146
+ scrolled = scroll_up(window)
147
+
148
+ unless scrolled
149
+ scrollable = false if n == 0
150
+ break
151
+ end
152
+ end
153
+ when Curses::KEY_LEFT, Curses::KEY_CTRL_T
154
+ while scroll_up(window)
155
+ end
156
+ when Curses::KEY_RIGHT, Curses::KEY_CTRL_B
157
+ while scroll_down(window)
158
+ end
159
+ when Curses::KEY_BACKSPACE
160
+ broadcast(:reenter_units_menu, @course, @chapter, @unit)
161
+ return
162
+ when 27 # ESC
163
+ exit
164
+ end
165
+
166
+ Curses.beep unless scrollable
167
+ window.setpos(0, 1)
168
+ window.refresh
169
+ end
170
+ end
171
+
172
+ def open_editor(file_path)
173
+ if OS.windows?
174
+ system "start '' '#{file_path}'"
175
+ elsif OS.mac?
176
+ system "open '#{file_path}'"
177
+ elsif OS.linux?
178
+ system "xdg-open '#{file_path}'"
179
+ end
180
+ end
181
+
182
+ def print_test_results(window)
183
+ result = @unit.solution.verify!
184
+
185
+ @test_result_lines = result.summary.strip.split("\n")
186
+ @lines = [''] + @test_result_lines + ['', ''] + @unit.task.markdown.lines
187
+ @examples = result.examples
188
+
189
+ initialize_window(@lines.count + @top_bar_height + @head_height)
190
+ end
191
+
192
+ def reset_screen(window)
193
+ @test_result_lines = nil
194
+ @lines = @unit.task.markdown.lines
195
+ height = [@lines.count + @top_bar_height + @head_height, Curses.lines].max
196
+ initialize_window(height)
197
+ end
198
+
199
+ def initialize_window(height)
200
+ @window = default_window(height)
201
+ main_panel(@window) { |window| show sub_window_below_top_bar(window) }
202
+ end
203
+ end
204
+
205
+ end
206
+ end
@@ -0,0 +1,48 @@
1
+ module Daigaku
2
+ module Views
3
+
4
+ class TopBar
5
+ include Curses
6
+
7
+ attr_reader :height, :width, :panel
8
+
9
+ def initialize(window)
10
+ @height = 4
11
+ @width = window.maxx
12
+ @panel = create_panel(window, @width, @height)
13
+ end
14
+
15
+ def show
16
+ @panel.refresh
17
+ end
18
+
19
+ private
20
+
21
+ def create_panel(window, width, heigth)
22
+ panel = window.subwin(heigth, window.maxx, 0, 0)
23
+
24
+ panel.setpos(1, 1)
25
+ panel.write 'Use '
26
+ panel.emphasize 'UP KEY'
27
+ panel.write ' and '
28
+ panel.emphasize 'DOWN KEY'
29
+ panel.write ' for menu navigation'
30
+ panel.write ' | Enter menu with '
31
+ panel.emphasize 'RETURN'
32
+ panel.write ' | Go back with '
33
+ panel.emphasize 'BACKSPACE'
34
+ panel.write ' | Exit with '
35
+ panel.emphasize 'ESC'
36
+ panel.setpos(2, 1)
37
+ panel.write '_' * (window.maxx - 3)
38
+
39
+ panel
40
+ end
41
+
42
+ def emphasized(panel, text)
43
+ panel.write(text, Window::COLOR_2)
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,92 @@
1
+ module Daigaku
2
+ module Views
3
+ require 'wisper'
4
+
5
+ class UnitsMenu
6
+ include Views
7
+ include Wisper::Publisher
8
+
9
+ def initialize
10
+ @position = 0
11
+ end
12
+
13
+ def enter_units_menu(course, chapter)
14
+ @window = default_window
15
+ @course = course
16
+ @chapter = chapter
17
+
18
+ main_panel(@window) do |window|
19
+ show sub_window_below_top_bar(window)
20
+ end
21
+ end
22
+
23
+ def reenter_units_menu(course, chapter, unit)
24
+ @course = course
25
+ @chapter = chapter
26
+
27
+ @position = chapter.units.find_index(unit)
28
+ enter_units_menu(@course, @chapter)
29
+ end
30
+
31
+ private
32
+
33
+ def show(window)
34
+ draw(window, @position)
35
+ interact_with(window)
36
+ end
37
+
38
+ def draw(window, active_index = 0)
39
+ window.attrset(A_NORMAL)
40
+ window.setpos(0, 1)
41
+ window.emphasize @course.title
42
+ window.write ' > '
43
+ window.emphasize @chapter.title
44
+ window.write ' > - available units:'
45
+
46
+ menu_items.each_with_index do |item, index|
47
+ window.setpos(index + 2, 1)
48
+ window.print_indicator(units[index])
49
+ window.attrset(index == active_index ? A_STANDOUT : A_NORMAL)
50
+ window.write " #{item.to_s} "
51
+ end
52
+
53
+ window.refresh
54
+ end
55
+
56
+ def interact_with(window)
57
+ while char = window.getch
58
+ case char
59
+ when KEY_UP
60
+ @position -= 1
61
+ when KEY_DOWN
62
+ @position += 1
63
+ when 10 # Enter
64
+ broadcast(:enter_task_view, @course, @chapter, units[@position])
65
+ return
66
+ when 263 # Backspace
67
+ broadcast(:reenter_chapters_menu, @course, @chapter)
68
+ return
69
+ when 27 # ESC
70
+ exit
71
+ end
72
+
73
+ @position = menu_items.length - 1 if @position < 0
74
+ @position = 0 if @position >= menu_items.length
75
+ draw(window, @position)
76
+ end
77
+ end
78
+
79
+ def units
80
+ @chapter.units
81
+ end
82
+
83
+ def menu_items
84
+ @menu_items = units.map do |unit|
85
+ unit.title
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ end
92
+ end