greeenboii 0.1.6 → 0.1.8

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.idea/greeenboii.iml +30 -25
  3. data/.idea/jsLibraryMappings.xml +6 -0
  4. data/.idea/material_theme_project_new.xml +0 -0
  5. data/.idea/modules.xml +0 -0
  6. data/.idea/vcs.xml +0 -0
  7. data/.idea/workspace.xml +189 -110
  8. data/.rubocop.sorbet.yml +0 -0
  9. data/.rubocop.yml +0 -0
  10. data/CHANGELOG.md +16 -0
  11. data/CODE_OF_CONDUCT.md +0 -0
  12. data/CONTRIBUTING.md +0 -0
  13. data/LICENSE.txt +0 -0
  14. data/Makefile +0 -0
  15. data/README.md +5 -0
  16. data/Rakefile +0 -0
  17. data/exe/greeenboii +0 -0
  18. data/ext/greeenboii/extconf.rb +0 -0
  19. data/ext/greeenboii/greeenboii.c +0 -0
  20. data/ext/greeenboii/greeenboii.h +0 -0
  21. data/greeenboii_todo.db +0 -0
  22. data/lib/greeenboii/version.rb +5 -5
  23. data/lib/greeenboii.rb +880 -235
  24. data/lib/libsql.rb +159 -0
  25. data/logs/turso_requests.log +54 -0
  26. data/logs/turso_responses.log +27 -0
  27. data/sig/greeenboii/options.rbs +0 -0
  28. data/sig/greeenboii/search.rbs +0 -0
  29. data/sig/greeenboii/todo_list.rbs +0 -0
  30. data/sig/greeenboii.rbs +0 -0
  31. data/sorbet/config +0 -0
  32. data/sorbet/rbi/annotations/.gitattributes +0 -0
  33. data/sorbet/rbi/annotations/minitest.rbi +0 -0
  34. data/sorbet/rbi/annotations/rainbow.rbi +0 -0
  35. data/sorbet/rbi/gems/.gitattributes +0 -0
  36. data/sorbet/rbi/gems/ast@2.4.2.rbi +0 -0
  37. data/sorbet/rbi/gems/benchmark@0.4.0.rbi +0 -0
  38. data/sorbet/rbi/gems/bigdecimal@3.1.9.rbi +0 -0
  39. data/sorbet/rbi/gems/cli-ui@2.3.0.rbi +0 -0
  40. data/sorbet/rbi/gems/console_table@0.3.1.rbi +0 -0
  41. data/sorbet/rbi/gems/csv@3.3.2.rbi +0 -0
  42. data/sorbet/rbi/gems/erubi@1.13.1.rbi +0 -0
  43. data/sorbet/rbi/gems/httparty@0.22.0.rbi +0 -0
  44. data/sorbet/rbi/gems/json@2.10.1.rbi +0 -0
  45. data/sorbet/rbi/gems/language_server-protocol@3.17.0.4.rbi +0 -0
  46. data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +0 -0
  47. data/sorbet/rbi/gems/mini_mime@1.1.5.rbi +0 -0
  48. data/sorbet/rbi/gems/minitest@5.25.4.rbi +0 -0
  49. data/sorbet/rbi/gems/multi_xml@0.7.1.rbi +0 -0
  50. data/sorbet/rbi/gems/netrc@0.11.0.rbi +0 -0
  51. data/sorbet/rbi/gems/nokogiri@1.18.3.rbi +0 -0
  52. data/sorbet/rbi/gems/parallel@1.26.3.rbi +0 -0
  53. data/sorbet/rbi/gems/parser@3.3.7.1.rbi +0 -0
  54. data/sorbet/rbi/gems/prism@1.3.0.rbi +0 -0
  55. data/sorbet/rbi/gems/racc@1.8.1.rbi +0 -0
  56. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +0 -0
  57. data/sorbet/rbi/gems/rake-compiler@1.2.9.rbi +0 -0
  58. data/sorbet/rbi/gems/rake@13.2.1.rbi +0 -0
  59. data/sorbet/rbi/gems/rbi@0.2.4.rbi +0 -0
  60. data/sorbet/rbi/gems/regexp_parser@2.10.0.rbi +0 -0
  61. data/sorbet/rbi/gems/rubocop-ast@1.38.0.rbi +0 -0
  62. data/sorbet/rbi/gems/rubocop@1.72.2.rbi +0 -0
  63. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +0 -0
  64. data/sorbet/rbi/gems/spoom@1.5.4.rbi +0 -0
  65. data/sorbet/rbi/gems/sqlite3@2.6.0.rbi +0 -0
  66. data/sorbet/rbi/gems/tapioca@0.16.11.rbi +0 -0
  67. data/sorbet/rbi/gems/thor@1.3.2.rbi +0 -0
  68. data/sorbet/rbi/gems/unicode-display_width@3.1.4.rbi +0 -0
  69. data/sorbet/rbi/gems/unicode-emoji@4.0.4.rbi +0 -0
  70. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -0
  71. data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -0
  72. data/sorbet/tapioca/config.yml +0 -0
  73. data/sorbet/tapioca/require.rb +0 -0
  74. data/src/main.c +0 -0
  75. metadata +39 -9
  76. data/.idea/dataSources/28fe2501-d682-44de-9f2e-9ff4bf02ce84/storage_v2/_src_/schema/main.uQUzAA.meta +0 -2
  77. data/.idea/dataSources/28fe2501-d682-44de-9f2e-9ff4bf02ce84.xml +0 -1617
  78. data/.idea/dataSources.local.xml +0 -18
  79. data/.idea/dataSources.xml +0 -12
  80. data/.idea/sqldialects.xml +0 -7
data/lib/greeenboii.rb CHANGED
@@ -1,235 +1,880 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "greeenboii/version"
4
- require "cli/ui"
5
- require "date"
6
- require "console_table"
7
-
8
- require "nokogiri"
9
- require "httparty"
10
-
11
- require "sqlite3"
12
-
13
- module Greeenboii
14
- class Error < StandardError; end
15
-
16
- class Search
17
- SEARCH_ENGINES = {
18
- "Google" => "https://www.google.com/search?client=opera-gx&q= ",
19
- "Bing" => "https://www.bing.com/search?q=",
20
- "DuckDuckGo" => "https://duckduckgo.com/?q="
21
- }.freeze
22
-
23
- def self.perform_search
24
- query = CLI::UI.ask("Enter your search query:", default: "")
25
- return if query.empty?
26
-
27
- results = []
28
- CLI::UI::SpinGroup.new do |spin_group|
29
- SEARCH_ENGINES.each do |engine, base_url|
30
- spin_group.add("Searching #{engine}...") do |spinner|
31
- suffix = case engine
32
- when "Google"
33
- "&sourceid=opera&ie=UTF-8&oe=UTF-8"
34
- when "Bing"
35
- "&sp=-1&pq=test&sc=6-4&qs=n&sk=&cvid=#{SecureRandom.hex(16)}"
36
- when "DuckDuckGo"
37
- "&t=h_&ia=web"
38
- end
39
- search_url = "#{base_url}#{URI.encode_www_form_component(query)}#{suffix}"
40
- links = scrape_links(engine, search_url)
41
- results << { engine: engine, links: links }
42
- sleep 1.0 # Simulating search delay
43
- spinner.update_title("#{engine} search complete!")
44
- end
45
- end
46
- end
47
-
48
- display_results(results)
49
- end
50
-
51
- def self.scrape_links(engine, url)
52
- headers = {
53
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36",
54
- "Accept-Language" => "en-US,en;q=0.5"
55
- }
56
-
57
- response = HTTParty.get(url, headers: headers)
58
- puts "Debug: HTTP Status: #{response.code}"
59
-
60
- doc = Nokogiri::HTML(response.body)
61
- puts "Debug: Page title: #{doc.title}"
62
-
63
- results = []
64
-
65
- case engine
66
- when "Google"
67
- doc.css("div.g").each do |result|
68
- link = result.css(".yuRUbf > a").first
69
- next unless link
70
-
71
- title = result.css("h3").text.strip
72
- url = link["href"]
73
- result.css(".VwiC3b").text.strip
74
-
75
- puts "Debug: Found Google result - Title: #{title}"
76
- results << url if url.start_with?("http")
77
- end
78
-
79
- when "Bing"
80
- doc.css("#b_results li.b_algo").each do |result|
81
- link = result.css("h2 a").first
82
- next unless link
83
-
84
- url = link["href"]
85
- puts "Debug: Found Bing result - URL: #{url}"
86
- results << url if url.start_with?("http")
87
- end
88
-
89
- else # DuckDuckGo
90
- doc.css(".result__body").each do |result|
91
- link = result.css(".result__title a").first
92
- next unless link
93
-
94
- url = link["href"]
95
- puts "Debug: Found DuckDuckGo result - URL: #{url}"
96
- results << url if url.start_with?("http")
97
- end
98
- end
99
-
100
- puts "Debug: Total results found: #{results.length}"
101
- results.take(8)
102
- end
103
-
104
- def self.display_results(results)
105
- CLI::UI.frame_style = :bracket
106
- CLI::UI::Frame.open(CLI::UI.fmt("{{green:Search Results}}")) do
107
- results.each do |result|
108
- puts CLI::UI.fmt("{{v}} {{cyan:#{result[:engine]}}}")
109
- result[:links].each_with_index do |link, idx|
110
- puts CLI::UI.fmt(" {{*}} ##{idx + 1}: #{link}")
111
- end
112
- end
113
- end
114
- end
115
- end
116
-
117
- class TodoList
118
- def initialize
119
- @db = setup_database
120
- end
121
-
122
- private def setup_database
123
- db = SQLite3::Database.new("greeenboii_todo.db")
124
- db.execute <<~SQL
125
- CREATE TABLE IF NOT EXISTS todos (
126
- id INTEGER PRIMARY KEY,
127
- title TEXT NOT NULL,
128
- completed BOOLEAN DEFAULT 0,
129
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
130
- )
131
- SQL
132
- db
133
- end
134
-
135
- def add_task
136
- title = CLI::UI.ask("Enter task title:")
137
- return if title.empty?
138
-
139
- @db.execute("INSERT INTO todos (title) VALUES (?)", [title])
140
- puts CLI::UI.fmt "{{green:✓}} Task added successfully!"
141
- end
142
-
143
- def list_tasks
144
- tasks = @db.execute("SELECT id, title, completed, created_at FROM todos ORDER BY created_at DESC")
145
- if tasks.empty?
146
- puts CLI::UI.fmt "{{yellow:⚠}} No tasks found"
147
- return
148
- end
149
-
150
- ConsoleTable.define(%w[ID Title Status Created]) do |table|
151
- tasks.each do |id, title, completed, created_at|
152
- status = completed == 1 ? "{{green:✓}} Done" : "{{red:✗}} Pending"
153
- table << [id, title, CLI::UI.fmt(status), created_at]
154
- end
155
- end
156
- end
157
-
158
- def mark_done
159
- list_tasks
160
- id = CLI::UI.ask("Enter task ID to mark as done:")
161
- return if id.empty?
162
-
163
- @db.execute("UPDATE todos SET completed = 1 WHERE id = ?", [id])
164
- puts CLI::UI.fmt "{{green:✓}} Task marked as done!"
165
- end
166
-
167
- def delete_task
168
- list_tasks
169
- id = CLI::UI.ask("Enter task ID to delete:")
170
- return if id.empty?
171
-
172
- @db.execute("DELETE FROM todos WHERE id = ?", [id])
173
- puts CLI::UI.fmt "{{green:✓}} Task deleted!"
174
- end
175
-
176
- def update_task
177
- list_tasks
178
- id = CLI::UI.ask("Enter task ID to update:")
179
- return if id.empty?
180
-
181
- title = CLI::UI.ask("Enter new title:")
182
- return if title.empty?
183
-
184
- @db.execute("UPDATE todos SET title = ? WHERE id = ?", [title, id])
185
- puts CLI::UI.fmt "{{green:✓}} Task updated!"
186
- end
187
-
188
- def show_menu
189
- CLI::UI::Frame.divider("{{v}} Todo List")
190
- loop do
191
- CLI::UI::Prompt.ask("Todo List Options:") do |handler|
192
- handler.option("List Tasks") { list_tasks }
193
- handler.option("Add Task") { add_task }
194
- handler.option("Mark Done") { mark_done }
195
- handler.option("Update Task") { update_task }
196
- handler.option("Delete Task") { delete_task }
197
- handler.option("Exit") { return }
198
- end
199
- end
200
- end
201
- end
202
-
203
- class Options
204
- def self.show_options
205
- CLI::UI::Prompt.instructions_color = CLI::UI::Color::GRAY
206
-
207
- CLI::UI::Prompt.ask("Choose an option:") do |handler|
208
- handler.option("{{gray:Search Files}}") { |selection| puts "Placeholder, Replaced soon. #{selection}" }
209
- handler.option("{{gray:Search Directory}}") { |selection| puts "Placeholder, Replaced soon. #{selection}" }
210
- handler.option("{{gray:Search Content}}") { |selection| puts "Placeholder, Replaced soon. #{selection}" }
211
- handler.option("{{yellow:Todo List}}") { |_selection| TodoList.new.show_menu }
212
- handler.option("{{cyan:Search Engine}}") { |_selection| Search.perform_search }
213
- handler.option("{{red:Exit}}") { |_selection| exit }
214
- end
215
- end
216
- end
217
-
218
- class Main
219
- CLI::UI::StdoutRouter.enable
220
- current_time = DateTime.now.strftime("%d-%m-%Y %H:%M:%S")
221
- CLI::UI::Frame.open("{{v}} Greeenboi : #{current_time}") do
222
- puts "Welcome to Greeenboii"
223
- puts "Lets do some magic!"
224
- CLI::UI.frame_style = :bracket
225
- CLI::UI::Frame.open(CLI::UI.fmt("{{green:Welcome to Greeenboii CLI}}")) do
226
- puts CLI::UI.fmt("{{cyan:Version}}: #{Greeenboii::VERSION}")
227
- # ConsoleTable.define(%w[Name Version]) do |table|
228
- # table << ["Greeenboii", Greeenboii::VERSION]
229
- # end
230
- puts CLI::UI.fmt("{{yellow:Type 'help' to see available commands}}")
231
- Options.show_options
232
- end
233
- end
234
- end
235
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "greeenboii/version"
4
+ require_relative "libsql"
5
+ require "cli/ui"
6
+ require "date"
7
+ require "console_table"
8
+
9
+ require "nokogiri"
10
+ require "httparty"
11
+
12
+ require "sqlite3"
13
+
14
+ require 'json'
15
+ require 'net/http'
16
+ require 'uri'
17
+
18
+ require 'dotenv'
19
+ require 'fileutils'
20
+
21
+ module Greeenboii
22
+ class Error < StandardError; end
23
+
24
+ class WebsiteBuilder
25
+ def self.build_website
26
+ CLI::UI::Frame.open("{{v}} Website Builder") do
27
+ CLI::UI::Prompt.ask("Choose a template:") do |handler|
28
+ handler.option("{{green:NextJs-CoolStack}}") { |selection| create_nextjs(selection) }
29
+ handler.option("{{yellow:Hono-API}}") { |selection| create_hono(selection) }
30
+ # handler.option("{{blue:Rails}}") { |selection| create_rails(selection) }
31
+ end
32
+ end
33
+ end
34
+
35
+ def self.create_nextjs(_selection)
36
+ # Ask for a relative path instead
37
+ location = CLI::UI.ask("Where to install? (relative path)", default: "nextjs-project")
38
+ CLI::UI.puts("Checking prerequisites...")
39
+
40
+ # Check prerequisites
41
+ node_version = begin
42
+ `node -v`.strip
43
+ rescue StandardError
44
+ nil
45
+ end
46
+ unless node_version
47
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Node.js not found. Please install Node.js first"))
48
+ return
49
+ end
50
+
51
+ git_version = begin
52
+ `git --version`.strip
53
+ rescue StandardError
54
+ nil
55
+ end
56
+ unless git_version
57
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Git not found. Please install Git first"))
58
+ return
59
+ end
60
+
61
+ # Create a path relative to current directory
62
+ full_path = File.expand_path(location, Dir.pwd)
63
+
64
+ # Check if directory already exists
65
+ if Dir.exist?(full_path)
66
+ # If directory exists, check if it's empty
67
+ unless Dir.empty?(full_path) # . and .. entries
68
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Directory not empty: #{full_path}"))
69
+ return
70
+ end
71
+ else
72
+ # Try to create the directory
73
+ begin
74
+ FileUtils.mkdir_p(full_path)
75
+ rescue StandardError => e
76
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Cannot create directory: #{e.message}"))
77
+ return
78
+ end
79
+ end
80
+
81
+ success = false
82
+ CLI::UI::Spinner.spin("Setting up NextJS + Supabase template") do |spinner|
83
+ spinner.update_title("Cloning repository...")
84
+
85
+ # Clone directly into the target directory
86
+ clone_cmd = "git clone https://github.com/greeenboi/nextjs-supabase-template.git \"#{full_path}\""
87
+ result = system(clone_cmd)
88
+
89
+ unless result
90
+ # Try alternative approach if direct cloning fails
91
+ spinner.update_title("Direct clone failed, trying alternative...")
92
+
93
+ # Clone to a temporary directory first
94
+ temp_dir = "#{Dir.pwd}/temp_clone_#{Time.now.to_i}"
95
+ FileUtils.mkdir_p(temp_dir)
96
+ result = system("git clone https://github.com/greeenboi/nextjs-supabase-template.git \"#{temp_dir}\"")
97
+
98
+ if result
99
+ # Copy files to destination
100
+ FileUtils.cp_r(Dir.glob("#{temp_dir}/*"), full_path)
101
+ FileUtils.cp_r(Dir.glob("#{temp_dir}/.*").reject { |f| f =~ /\/\.\.?$/ }, full_path)
102
+ FileUtils.rm_rf(temp_dir)
103
+ else
104
+ spinner.update_title("Clone failed")
105
+ next
106
+ end
107
+ end
108
+
109
+ spinner.update_title("Installing dependencies...")
110
+ Dir.chdir(full_path) do
111
+ package_manager = if system("bun -v > /dev/null 2>&1")
112
+ "bun"
113
+ elsif system("pnpm -v > /dev/null 2>&1")
114
+ "pnpm"
115
+ else
116
+ "npm"
117
+ end
118
+ spinner.update_title("Installing dependencies with #{package_manager}...")
119
+ system("#{package_manager} install")
120
+ end
121
+
122
+ success = true
123
+ spinner.update_title("Setup complete!")
124
+ rescue StandardError => e
125
+ spinner.update_title("Error: #{e.message}")
126
+ end
127
+
128
+ if success
129
+ CLI::UI.puts(CLI::UI.fmt("{{v}} NextJS template installed successfully in #{full_path}"))
130
+ CLI::UI.puts(CLI::UI.fmt("{{*}} To start: cd \"#{location}\" && #{system} run dev"))
131
+ else
132
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Installation failed"))
133
+ end
134
+ end
135
+
136
+ def self.create_hono(_selection)
137
+ # Ask for a relative path instead
138
+ location = CLI::UI.ask("Where to install? (relative path)", default: "hono-api")
139
+ CLI::UI.puts("Checking prerequisites...")
140
+
141
+ # Check prerequisites
142
+ node_version = begin
143
+ `node -v`.strip
144
+ rescue StandardError
145
+ nil
146
+ end
147
+ unless node_version
148
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Node.js not found. Please install Node.js first"))
149
+ return
150
+ end
151
+
152
+ git_version = begin
153
+ `git --version`.strip
154
+ rescue StandardError
155
+ nil
156
+ end
157
+ unless git_version
158
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Git not found. Please install Git first"))
159
+ return
160
+ end
161
+
162
+ # Check for Deno
163
+ deno_version = begin
164
+ `deno --version`.strip
165
+ rescue StandardError
166
+ nil
167
+ end
168
+ unless deno_version
169
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Deno not found. Please install Deno first"))
170
+ return
171
+ end
172
+
173
+ # Create a path relative to current directory
174
+ full_path = File.expand_path(location, Dir.pwd)
175
+
176
+ # Check if directory already exists
177
+ if Dir.exist?(full_path)
178
+ # If directory exists, check if it's empty
179
+ unless Dir.empty?(full_path) # . and .. entries
180
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Directory not empty: #{full_path}"))
181
+ return
182
+ end
183
+ else
184
+ # Try to create the directory
185
+ begin
186
+ FileUtils.mkdir_p(full_path)
187
+ rescue StandardError => e
188
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Cannot create directory: #{e.message}"))
189
+ return
190
+ end
191
+ end
192
+
193
+ success = false
194
+ CLI::UI::Spinner.spin("Setting up Hono Deno backend template") do |spinner|
195
+ spinner.update_title("Cloning repository...")
196
+
197
+ # Clone directly into the target directory
198
+ clone_cmd = "git clone https://github.com/greeenboi/hono-deno-backend-template.git \"#{full_path}\""
199
+ result = system(clone_cmd)
200
+
201
+ unless result
202
+ # Try alternative approach if direct cloning fails
203
+ spinner.update_title("Direct clone failed, trying alternative...")
204
+
205
+ # Clone to a temporary directory first
206
+ temp_dir = "#{Dir.pwd}/temp_clone_#{Time.now.to_i}"
207
+ FileUtils.mkdir_p(temp_dir)
208
+ result = system("git clone https://github.com/greeenboi/hono-deno-backend-template.git \"#{temp_dir}\"")
209
+
210
+ if result
211
+ # Copy files to destination
212
+ FileUtils.cp_r(Dir.glob("#{temp_dir}/*"), full_path)
213
+ FileUtils.cp_r(Dir.glob("#{temp_dir}/.*").reject { |f| f =~ /\/\.\.?$/ }, full_path)
214
+ FileUtils.rm_rf(temp_dir)
215
+ else
216
+ spinner.update_title("Clone failed")
217
+ next
218
+ end
219
+ end
220
+
221
+ success = true
222
+ spinner.update_title("Setup complete!")
223
+ rescue StandardError => e
224
+ spinner.update_title("Error: #{e.message}")
225
+ end
226
+
227
+ if success
228
+ CLI::UI.puts(CLI::UI.fmt("{{v}} Hono API template installed successfully in #{full_path}"))
229
+ CLI::UI.puts(CLI::UI.fmt("{{*}} To start: cd \"#{location}\" && deno task start"))
230
+ else
231
+ CLI::UI.puts(CLI::UI.fmt("{{x}} Installation failed"))
232
+ end
233
+ end
234
+ end
235
+
236
+ class Search
237
+ SEARCH_ENGINES = {
238
+ "Google" => "https://www.google.com/search?client=opera-gx&q= ",
239
+ "Bing" => "https://www.bing.com/search?q=",
240
+ "DuckDuckGo" => "https://duckduckgo.com/?q="
241
+ }.freeze
242
+
243
+ def self.perform_search
244
+ query = CLI::UI.ask("Enter your search query:", default: "")
245
+ return if query.empty?
246
+
247
+ results = []
248
+ CLI::UI::SpinGroup.new do |spin_group|
249
+ SEARCH_ENGINES.each do |engine, base_url|
250
+ spin_group.add("Searching #{engine}...") do |spinner|
251
+ suffix = case engine
252
+ when "Google"
253
+ "&sourceid=opera&ie=UTF-8&oe=UTF-8"
254
+ when "Bing"
255
+ "&sp=-1&pq=test&sc=6-4&qs=n&sk=&cvid=#{SecureRandom.hex(16)}"
256
+ when "DuckDuckGo"
257
+ "&t=h_&ia=web"
258
+ end
259
+ search_url = "#{base_url}#{URI.encode_www_form_component(query)}#{suffix}"
260
+ links = scrape_links(engine, search_url)
261
+ results << { engine: engine, links: links }
262
+ sleep 1.0 # Simulating search delay
263
+ spinner.update_title("#{engine} search complete!")
264
+ end
265
+ end
266
+ end
267
+
268
+ display_results(results)
269
+ end
270
+
271
+ def self.scrape_links(engine, url)
272
+ headers = {
273
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36",
274
+ "Accept-Language" => "en-US,en;q=0.5"
275
+ }
276
+
277
+ response = HTTParty.get(url, headers: headers)
278
+ puts "Debug: HTTP Status: #{response.code}"
279
+
280
+ doc = Nokogiri::HTML(response.body)
281
+ puts "Debug: Page title: #{doc.title}"
282
+
283
+ results = []
284
+
285
+ case engine
286
+ when "Google"
287
+ doc.css("div.g").each do |result|
288
+ link = result.css(".yuRUbf > a").first
289
+ next unless link
290
+
291
+ title = result.css("h3").text.strip
292
+ url = link["href"]
293
+ result.css(".VwiC3b").text.strip
294
+
295
+ puts "Debug: Found Google result - Title: #{title}"
296
+ results << url if url.start_with?("http")
297
+ end
298
+
299
+ when "Bing"
300
+ doc.css("#b_results li.b_algo").each do |result|
301
+ link = result.css("h2 a").first
302
+ next unless link
303
+
304
+ url = link["href"]
305
+ puts "Debug: Found Bing result - URL: #{url}"
306
+ results << url if url.start_with?("http")
307
+ end
308
+
309
+ else # DuckDuckGo
310
+ doc.css(".result__body").each do |result|
311
+ link = result.css(".result__title a").first
312
+ next unless link
313
+
314
+ url = link["href"]
315
+ puts "Debug: Found DuckDuckGo result - URL: #{url}"
316
+ results << url if url.start_with?("http")
317
+ end
318
+ end
319
+
320
+ puts "Debug: Total results found: #{results.length}"
321
+ results.take(8)
322
+ end
323
+
324
+ def self.display_results(results)
325
+ CLI::UI.frame_style = :bracket
326
+ CLI::UI::Frame.open(CLI::UI.fmt("{{green:Search Results}}")) do
327
+ results.each do |result|
328
+ puts CLI::UI.fmt("{{v}} {{cyan:#{result[:engine]}}}")
329
+ result[:links].each_with_index do |link, idx|
330
+ puts CLI::UI.fmt(" {{*}} ##{idx + 1}: #{link}")
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
336
+
337
+ class TodoList
338
+ def initialize
339
+ @db = setup_database
340
+ end
341
+
342
+ private def setup_database
343
+ db = SQLite3::Database.new("greeenboii_todo.db")
344
+ db.execute <<~SQL
345
+ CREATE TABLE IF NOT EXISTS todos (
346
+ id INTEGER PRIMARY KEY,
347
+ title TEXT NOT NULL,
348
+ completed BOOLEAN DEFAULT 0,
349
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
350
+ )
351
+ SQL
352
+ db
353
+ end
354
+
355
+ def add_task
356
+ title = CLI::UI.ask("Enter task title:")
357
+ return if title.empty?
358
+
359
+ @db.execute("INSERT INTO todos (title) VALUES (?)", [title])
360
+ puts CLI::UI.fmt "{{green:✓}} Task added successfully!"
361
+ end
362
+
363
+ def list_tasks
364
+ tasks = @db.execute("SELECT id, title, completed, created_at FROM todos ORDER BY created_at DESC")
365
+ if tasks.empty?
366
+ puts CLI::UI.fmt "{{yellow:⚠}} No tasks found"
367
+ return
368
+ end
369
+
370
+ ConsoleTable.define(%w[ID Title Status Created]) do |table|
371
+ tasks.each do |id, title, completed, created_at|
372
+ status = completed == 1 ? "{{green:✓}} Done" : "{{red:✗}} Pending"
373
+ table << [id, title, CLI::UI.fmt(status), created_at]
374
+ end
375
+ end
376
+ end
377
+
378
+ def mark_done
379
+ list_tasks
380
+ id = CLI::UI.ask("Enter task ID to mark as done:")
381
+ return if id.empty?
382
+
383
+ @db.execute("UPDATE todos SET completed = 1 WHERE id = ?", [id])
384
+ puts CLI::UI.fmt "{{green:✓}} Task marked as done!"
385
+ end
386
+
387
+ def delete_task
388
+ list_tasks
389
+ id = CLI::UI.ask("Enter task ID to delete:")
390
+ return if id.empty?
391
+
392
+ @db.execute("DELETE FROM todos WHERE id = ?", [id])
393
+ puts CLI::UI.fmt "{{green:✓}} Task deleted!"
394
+ end
395
+
396
+ def update_task
397
+ list_tasks
398
+ id = CLI::UI.ask("Enter task ID to update:")
399
+ return if id.empty?
400
+
401
+ title = CLI::UI.ask("Enter new title:")
402
+ return if title.empty?
403
+
404
+ @db.execute("UPDATE todos SET title = ? WHERE id = ?", [title, id])
405
+ puts CLI::UI.fmt "{{green:✓}} Task updated!"
406
+ end
407
+
408
+ def show_menu
409
+ CLI::UI::Frame.divider("{{v}} Todo List")
410
+ loop do
411
+ CLI::UI::Prompt.ask("Todo List Options:") do |handler|
412
+ handler.option("List Tasks") { list_tasks }
413
+ handler.option("Add Task") { add_task }
414
+ handler.option("Mark Done") { mark_done }
415
+ handler.option("Update Task") { update_task }
416
+ handler.option("Delete Task") { delete_task }
417
+ handler.option("Exit") { return }
418
+ end
419
+ end
420
+ end
421
+ end
422
+
423
+ class GistManager
424
+ def initialize
425
+ Dotenv.load
426
+ ensure_connection
427
+ end
428
+
429
+ private def ensure_connection
430
+ return true
431
+
432
+ CLI::UI.puts(CLI::UI.fmt("{{yellow:⚠}} Turso credentials not found"))
433
+ manage_credentials
434
+ end
435
+
436
+ def add_gist
437
+ unless true
438
+ CLI::UI.puts(CLI::UI.fmt("{{red:✗}} Cloud credentials required"))
439
+ return
440
+ end
441
+
442
+ title = CLI::UI.ask("Enter a title for this gist:")
443
+ return if title.empty?
444
+
445
+ url = CLI::UI.ask("Enter GitHub Gist URL:")
446
+ return if url.empty? || !url.match?(/https:\/\/gist\.github\.com\//)
447
+
448
+ description = CLI::UI.ask("Enter a description (optional):", default: "")
449
+ tags = CLI::UI.ask("Enter tags (comma separated):", default: "")
450
+
451
+ # Extract gist ID from URL
452
+ gist_id = url.split('/').last
453
+ created_at = Time.now.strftime("%Y-%m-%d %H:%M:%S")
454
+
455
+ CLI::UI::Spinner.spin("Saving gist...") do |spinner|
456
+ begin
457
+ # First create table if needed
458
+ create_table_result = turso_execute(
459
+ "CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)"
460
+ )
461
+
462
+ # Then insert the data
463
+ insert_result = turso_execute(
464
+ "INSERT OR REPLACE INTO gists (gist_id, title, url, description, tags, created_at) VALUES (?, ?, ?, ?, ?, ?)",
465
+ [gist_id, title, url, description, tags, created_at]
466
+ )
467
+
468
+ spinner.update_title("Gist saved successfully")
469
+ rescue => e
470
+ spinner.update_title("Error saving gist: #{e.message}")
471
+ end
472
+ end
473
+ end
474
+
475
+ def list_gists
476
+ unless true
477
+ CLI::UI.puts(CLI::UI.fmt("{{red:✗}} Cloud credentials required"))
478
+ return
479
+ end
480
+
481
+ CLI::UI::Spinner.spin("Fetching gists...") do |spinner|
482
+ begin
483
+ # Create table if needed
484
+ create_table_result = turso_execute(
485
+ "CREATE TABLE IF NOT EXISTS gists (gist_id TEXT PRIMARY KEY, title TEXT NOT NULL, url TEXT NOT NULL, description TEXT, tags TEXT, created_at TEXT)"
486
+ )
487
+
488
+ # Get all gists
489
+ result = turso_execute(
490
+ "SELECT gist_id, title, url, description, tags, created_at FROM gists ORDER BY created_at DESC"
491
+ )
492
+
493
+ @gists = extract_rows(result)
494
+
495
+ if @gists.empty?
496
+ spinner.update_title("No gists found")
497
+ else
498
+ spinner.update_title("Found #{@gists.length} gists")
499
+ end
500
+ rescue => e
501
+ spinner.update_title("Error fetching gists: #{e.message}")
502
+ end
503
+ end
504
+
505
+ return if !@gists || @gists.empty?
506
+
507
+ CLI::UI.puts("\nYour Gists:")
508
+ @gists.each do |gist|
509
+ gist_id, title, url, description, tags, created_at = gist
510
+ CLI::UI.puts(CLI::UI.fmt("{{cyan:#{gist_id}}}: {{bold:#{title}}} - #{url}"))
511
+ CLI::UI.puts(CLI::UI.fmt(" Description: #{description}")) if description && !description.empty?
512
+ CLI::UI.puts(CLI::UI.fmt(" Tags: #{tags}")) if tags && !tags.empty?
513
+ CLI::UI.puts(CLI::UI.fmt(" Created: #{created_at}"))
514
+ CLI::UI.puts("")
515
+ end
516
+ end
517
+
518
+ def search_gists
519
+ unless true
520
+ CLI::UI.puts(CLI::UI.fmt("{{red:✗}} Cloud credentials required"))
521
+ return
522
+ end
523
+
524
+ term = CLI::UI.ask("Enter search term:")
525
+ return if term.empty?
526
+
527
+ CLI::UI::Spinner.spin("Searching gists...") do |spinner|
528
+ begin
529
+ result = turso_execute(
530
+ "SELECT gist_id, title, url, description, tags, created_at FROM gists WHERE title LIKE ? OR description LIKE ? OR tags LIKE ? ORDER BY created_at DESC",
531
+ ["%#{term}%", "%#{term}%", "%#{term}%"]
532
+ )
533
+
534
+ @search_results = extract_rows(result)
535
+
536
+ if @search_results.empty?
537
+ spinner.update_title("No matching gists found")
538
+ else
539
+ spinner.update_title("Found #{@search_results.length} matching gists")
540
+ end
541
+ rescue => e
542
+ spinner.update_title("Search error: #{e.message}")
543
+ end
544
+ end
545
+
546
+ return if !@search_results || @search_results.empty?
547
+
548
+ CLI::UI.puts(CLI::UI.fmt("\n{{bold:Search Results:}}"))
549
+ @search_results.each do |gist|
550
+ gist_id, title, url, description, tags, created_at = gist
551
+ CLI::UI.puts(CLI::UI.fmt("{{cyan:#{gist_id}}}: {{bold:#{title}}} - #{url}"))
552
+ CLI::UI.puts(CLI::UI.fmt(" Description: #{description}")) if description && !description.empty?
553
+ CLI::UI.puts(CLI::UI.fmt(" Tags: #{tags}")) if tags && !tags.empty?
554
+ CLI::UI.puts(CLI::UI.fmt(" Created: #{created_at}"))
555
+ CLI::UI.puts("")
556
+ end
557
+ end
558
+
559
+ def open_gist
560
+ list_gists
561
+
562
+ return if !@gists || @gists.empty?
563
+
564
+ gist_id = CLI::UI.ask("Enter gist ID to open:")
565
+ return if gist_id.empty?
566
+
567
+ # Find the matching gist
568
+ gist = @gists.find { |g| g[0] == gist_id }
569
+
570
+ unless gist
571
+ CLI::UI.puts(CLI::UI.fmt("{{red:✗}} Gist not found"))
572
+ return
573
+ end
574
+
575
+ url = gist[2] # URL is at index 2
576
+
577
+ if RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
578
+ system("start #{url}")
579
+ elsif RUBY_PLATFORM.match?(/darwin/)
580
+ system("open #{url}")
581
+ elsif RUBY_PLATFORM.match?(/linux/)
582
+ system("xdg-open #{url}")
583
+ else
584
+ CLI::UI.puts(CLI::UI.fmt("{{yellow:⚠}} Couldn't determine how to open URL on your platform. URL: #{url}"))
585
+ end
586
+ end
587
+
588
+ def delete_gist
589
+ list_gists
590
+
591
+ return if !@gists || @gists.empty?
592
+
593
+ gist_id = CLI::UI.ask("Enter gist ID to delete:")
594
+ return if gist_id.empty?
595
+
596
+ CLI::UI::Spinner.spin("Deleting gist...") do |spinner|
597
+ begin
598
+ result = turso_execute("DELETE FROM gists WHERE gist_id = ?", [gist_id])
599
+
600
+ # Try to determine if rows were affected
601
+ affected_rows = get_affected_rows(result)
602
+
603
+ if affected_rows > 0
604
+ spinner.update_title("Gist deleted successfully")
605
+ else
606
+ spinner.update_title("Gist not found")
607
+ end
608
+ rescue => e
609
+ spinner.update_title("Error deleting gist: #{e.message}")
610
+ end
611
+ end
612
+ end
613
+
614
+ def manage_credentials
615
+ CLI::UI::Frame.divider("{{v}} Cloud Credentials")
616
+
617
+ current_url = 'https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline'
618
+ current_token = "[Hidden]"
619
+
620
+ CLI::UI.puts(CLI::UI.fmt("Current settings:"))
621
+ CLI::UI.puts(CLI::UI.fmt("Database URL: {{cyan:#{current_url}}}"))
622
+ CLI::UI.puts(CLI::UI.fmt("Auth Token: {{cyan:#{current_token}}}"))
623
+ CLI::UI.puts("")
624
+
625
+ CLI::UI::Prompt.ask("Credential Options:") do |handler|
626
+ handler.option("Update Database URL") do
627
+ url = CLI::UI.ask("Enter Turso Database URL:")
628
+ update_env_file('TURSO_DATABASE_URL', url) unless url.empty?
629
+ end
630
+
631
+ handler.option("Update Auth Token") do
632
+ token = CLI::UI.ask("Enter Turso Auth Token:")
633
+ update_env_file('TURSO_AUTH_TOKEN', token) unless token.empty?
634
+ end
635
+
636
+ handler.option("Test Connection") do
637
+ test_connection
638
+ end
639
+
640
+ handler.option("Back") { return }
641
+ end
642
+ end
643
+
644
+ private def update_env_file(key, value)
645
+ env_file = '.env'
646
+
647
+ # Read existing .env content
648
+ content = File.exist?(env_file) ? File.read(env_file) : ""
649
+ lines = content.split("\n")
650
+
651
+ # Find and replace the line with the key, or add it
652
+ key_found = false
653
+ lines.map! do |line|
654
+ if line.start_with?("#{key}=")
655
+ key_found = true
656
+ "#{key}=#{value}"
657
+ else
658
+ line
659
+ end
660
+ end
661
+
662
+ lines << "#{key}=#{value}" unless key_found
663
+
664
+ # Write back to file
665
+ File.write(env_file, lines.join("\n"))
666
+
667
+ # Reload environment
668
+ ENV[key] = value
669
+
670
+ CLI::UI.puts(CLI::UI.fmt("{{v}} Updated #{key} in .env file"))
671
+ end
672
+
673
+ private def test_connection
674
+ CLI::UI::Spinner.spin("Testing Turso connection...") do |spinner|
675
+ begin
676
+ unless true
677
+ spinner.update_title("Cloud credentials not found")
678
+ next
679
+ end
680
+
681
+ result = turso_execute("SELECT 1")
682
+ spinner.update_title("Connection successful!")
683
+ rescue => e
684
+ spinner.update_title("Connection failed: #{e.message}")
685
+ end
686
+ end
687
+ end
688
+
689
+ def show_menu
690
+ CLI::UI::Frame.divider("{{v}} Gist Manager")
691
+ loop do
692
+ CLI::UI::Prompt.ask("Gist Manager Options:") do |handler|
693
+ handler.option("Add Gist") { add_gist }
694
+ handler.option("List Gists") { list_gists }
695
+ handler.option("Search Gists") { search_gists }
696
+ handler.option("Open Gist") { open_gist }
697
+ handler.option("Delete Gist") { delete_gist }
698
+ handler.option("Cloud Settings") { manage_credentials }
699
+ handler.option("Exit") { return }
700
+ end
701
+ end
702
+ end
703
+
704
+ private def turso_execute(sql, params = [])
705
+ url = 'https://greeenboii-cli-db-greeenboi.aws-ap-south-1.turso.io/v2/pipeline'
706
+ auth_token = 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3NDY0NzY5MjUsImlkIjoiMmMyYjBmYmItMzg0OC00ZDM2LThmYzQtZTNkMmFhMTU4Nzk4IiwicmlkIjoiNDc5ZmE0YjMtY2YwNS00ZmI2LWFlOGUtOWY0YWQ1Yjk5MzRjIn0.qdU6U9kLYGqUXVs62XswYsKGxS4tpO7WkbGePvtCI5IlOhQlxqUVL9nMP87ppd7T6IQ3Irksdn5PGMS8zI_yAQ'
707
+
708
+ # Ensure URL has the correct endpoint path for v2 pipeline
709
+ url = url.end_with?('/v2/pipeline') ? url : "#{url}/v2/pipeline"
710
+
711
+ uri = URI(url)
712
+ request = Net::HTTP::Post.new(uri)
713
+ request["Authorization"] = "Bearer #{auth_token}"
714
+ request["Content-Type"] = "application/json"
715
+
716
+ # Format parameters into the required format
717
+ formatted_params = params.map do |param|
718
+ type = case param
719
+ when Integer, /^\d+$/
720
+ "integer"
721
+ when Float, /^\d+\.\d+$/
722
+ "float"
723
+ when TrueClass, FalseClass, /^(true|false)$/i
724
+ "boolean"
725
+ when NilClass
726
+ "null"
727
+ else
728
+ "text"
729
+ end
730
+
731
+ {
732
+ "type": type,
733
+ "value": param.to_s
734
+ }
735
+ end
736
+
737
+ # Build the request body
738
+ stmt = {
739
+ "sql": sql,
740
+ "args": formatted_params
741
+ }
742
+
743
+ payload = {
744
+ "requests": [
745
+ { "type": "execute", "stmt": stmt },
746
+ { "type": "close" }
747
+ ]
748
+ }
749
+
750
+ request.body = JSON.generate(payload)
751
+
752
+ begin
753
+ # Log the request for debugging
754
+ ensure_log_directory
755
+ File.open('logs/turso_requests.log', 'a') do |f|
756
+ f.puts "#{Time.now} - REQUEST to #{uri}"
757
+ f.puts "SQL: #{sql}"
758
+ f.puts "Params: #{params.inspect}"
759
+ f.puts "Formatted Params: #{formatted_params.inspect}"
760
+ f.puts "Payload: #{payload.to_json}"
761
+ f.puts "-" * 80
762
+ end
763
+
764
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
765
+ http.request(request)
766
+ end
767
+
768
+ # Log the response
769
+ ensure_log_directory
770
+ File.open('logs/turso_responses.log', 'a') do |f|
771
+ f.puts "#{Time.now} - RESPONSE (#{response.code})"
772
+ f.puts "Body: #{response.body}"
773
+ f.puts "-" * 80
774
+ end
775
+
776
+ if response.code != "200"
777
+ error_msg = "HTTP error: #{response.code} - #{response.body}"
778
+ log_error(error_msg)
779
+ raise error_msg
780
+ end
781
+
782
+ JSON.parse(response.body)
783
+ rescue JSON::ParserError => e
784
+ error_msg = "JSON parse error: #{e.message}. Response body: #{response&.body}"
785
+ log_error(error_msg)
786
+ raise error_msg
787
+ rescue => e
788
+ error_msg = "Query failed: #{e.message}"
789
+ log_error(error_msg)
790
+ raise error_msg
791
+ end
792
+ end
793
+
794
+ private def log_error(message)
795
+ ensure_log_directory
796
+ File.open('logs/turso_errors.log', 'a') do |f|
797
+ f.puts "#{Time.now} - ERROR"
798
+ f.puts message
799
+ f.puts "-" * 80
800
+ end
801
+ # Also output to console
802
+ CLI::UI.puts(CLI::UI.fmt("{{red:ERROR}}: #{message}"))
803
+ end
804
+
805
+ private def ensure_log_directory
806
+ FileUtils.mkdir_p('logs') unless Dir.exist?('logs')
807
+ end
808
+
809
+ private def extract_rows(result)
810
+ rows = []
811
+
812
+ if result &&
813
+ result["results"] &&
814
+ result["results"][0] &&
815
+ result["results"][0]["type"] == "ok" &&
816
+ result["results"][0]["response"] &&
817
+ result["results"][0]["response"]["type"] == "execute" &&
818
+ result["results"][0]["response"]["result"]
819
+
820
+ result_data = result["results"][0]["response"]["result"]
821
+
822
+ if result_data["rows"]
823
+ rows = result_data["rows"]
824
+ end
825
+ end
826
+
827
+ rows
828
+ end
829
+
830
+ private def get_affected_rows(result)
831
+ if result &&
832
+ result["results"] &&
833
+ result["results"][0] &&
834
+ result["results"][0]["type"] == "ok" &&
835
+ result["results"][0]["response"] &&
836
+ result["results"][0]["response"]["type"] == "execute" &&
837
+ result["results"][0]["response"]["result"] &&
838
+ result["results"][0]["response"]["result"]["affected_row_count"]
839
+
840
+ return result["results"][0]["response"]["result"]["affected_row_count"]
841
+ end
842
+
843
+ 0
844
+ end
845
+ end
846
+
847
+ class Options
848
+ def self.show_options
849
+ CLI::UI::Prompt.instructions_color = CLI::UI::Color::GRAY
850
+
851
+ CLI::UI::Prompt.ask("Choose an option:") do |handler|
852
+ handler.option("{{gray:Search Files}}") { |selection| puts "Placeholder, Replaced soon. #{selection}" }
853
+ handler.option("{{gray:Search Directory}}") { |selection| puts "Placeholder, Replaced soon. #{selection}" }
854
+ handler.option("{{green:Website Builder}}") { |_selection| WebsiteBuilder.build_website }
855
+ handler.option("{{yellow:Todo List}}") { |_selection| TodoList.new.show_menu }
856
+ handler.option("{{cyan:Search Engine}}") { |_selection| Search.perform_search }
857
+ handler.option("{{blue:Gist Manager}}") { |_selection| GistManager.new.show_menu }
858
+ handler.option("{{red:Exit}}") { |_selection| exit }
859
+ end
860
+ end
861
+ end
862
+
863
+ class Main
864
+ CLI::UI::StdoutRouter.enable
865
+ current_time = DateTime.now.strftime("%d-%m-%Y %H:%M:%S")
866
+ CLI::UI::Frame.open("{{v}} Greeenboi : #{current_time}") do
867
+ puts "Welcome to Greeenboii"
868
+ puts "Lets do some magic!"
869
+ CLI::UI.frame_style = :bracket
870
+ CLI::UI::Frame.open(CLI::UI.fmt("{{green:Welcome to Greeenboii CLI}}")) do
871
+ puts CLI::UI.fmt("{{cyan:Version}}: #{Greeenboii::VERSION}")
872
+ # ConsoleTable.define(%w[Name Version]) do |table|
873
+ # table << ["Greeenboii", Greeenboii::VERSION]
874
+ # end
875
+ puts CLI::UI.fmt("{{yellow:Type 'help' to see available commands}}")
876
+ Options.show_options
877
+ end
878
+ end
879
+ end
880
+ end