booker 1.2.1 → 2.0.0

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.
data/lib/bookmarks.rb DELETED
@@ -1,324 +0,0 @@
1
- # grab/parse bookmarks from json file on computer
2
- require "fileutils"
3
-
4
- # get int number of columns in half of screen
5
- def terminal_width
6
- guess = `tput cols`.to_i
7
- (guess == 0) ? 100 : guess
8
- end
9
- TERMWIDTH = terminal_width
10
-
11
- # compl. color codes space
12
- CODEWIDTH = 16
13
-
14
- # add some colors, windowing methods
15
- class String
16
- def window(width)
17
- if length >= width
18
- self[0..width - 1]
19
- else
20
- ljust(width)
21
- end
22
- end
23
-
24
- def colorize(color, mod)
25
- "\033[#{mod};#{color};49m#{self}\033[0;0m"
26
- end
27
-
28
- def reset
29
- colorize(0, 0)
30
- end
31
-
32
- def blu
33
- colorize(34, 0)
34
- end
35
-
36
- def cyan
37
- colorize(36, 0)
38
- end
39
-
40
- def yel
41
- colorize(33, 0)
42
- end
43
-
44
- def grn
45
- colorize(32, 0)
46
- end
47
-
48
- def red
49
- colorize(31, 0)
50
- end
51
- end
52
-
53
- # Abstract base class for bookmark parsers
54
- class BookmarkParser
55
- attr_reader :allurls
56
-
57
- def initialize(file_path, search_term)
58
- @file_path = file_path
59
- @searching = /#{search_term}/i
60
- @allurls = []
61
- end
62
-
63
- # Abstract method - must be implemented by subclasses
64
- def parse
65
- raise NotImplementedError, "Subclasses must implement parse method"
66
- end
67
-
68
- def results
69
- @allurls
70
- end
71
-
72
- protected
73
-
74
- def matches_search?(values)
75
- values.any? { |v| v && @searching.match(v.to_s) }
76
- end
77
- end
78
-
79
- # Chrome/Chromium bookmark parser (JSON format)
80
- class ChromeBookmarkParser < BookmarkParser
81
- def parse
82
- begin
83
- local_bookmarks = JSON.parse(File.read(@file_path))
84
- @chrome_bookmarks = local_bookmarks["roots"]["bookmark_bar"]["children"]
85
- rescue
86
- puts "Warning: ".yel + "Bookmarks file not found or invalid."
87
- puts "Suggest: ".grn + "booker --install bookmarks"
88
- @chrome_bookmarks = []
89
- return
90
- end
91
-
92
- parse_recursive
93
- end
94
-
95
- private
96
-
97
- def parse_recursive(root = nil)
98
- root = Folder.new(@chrome_bookmarks, "|") if root.nil?
99
-
100
- root.json.each { |x| parse_link(root.title, x) }
101
- root.json.each { |x| parse_folder(root, x) }
102
- end
103
-
104
- def parse_link(base, link)
105
- checking = [base, link["name"], link["url"], link["id"]]
106
- if matches_search?(checking)
107
- if link["type"] == "url"
108
- @allurls.push(Bookmark.new(base, link["name"], link["url"], link["id"]))
109
- end
110
- end
111
- end
112
-
113
- def parse_folder(base, link)
114
- if link["type"] == "folder"
115
- title = base.title + link["name"] + "/"
116
- subdir = Folder.new(link["children"], title)
117
- parse_recursive(subdir)
118
- end
119
- end
120
- end
121
-
122
- # Firefox bookmark parser (SQLite format)
123
- class FirefoxBookmarkParser < BookmarkParser
124
- def parse
125
- begin
126
- require "sqlite3"
127
- rescue LoadError
128
- puts "Error: ".red + "sqlite3 gem not installed"
129
- puts "Run: ".grn + "gem install sqlite3"
130
- return
131
- end
132
-
133
- db_path = @file_path
134
-
135
- # Copy database to temp file if Firefox is running (to avoid lock)
136
- if firefox_running?
137
- require "tempfile"
138
- temp = Tempfile.new(["places", ".sqlite"])
139
- temp.close
140
- FileUtils.cp(@file_path, temp.path)
141
- db_path = temp.path
142
- end
143
-
144
- begin
145
- db = SQLite3::Database.new(db_path)
146
- db.results_as_hash = true
147
-
148
- # Recursive CTE query to build folder paths
149
- query = <<-SQL
150
- WITH RECURSIVE bookmark_tree(id, parent, title, url, path) AS (
151
- -- Start with bookmarks toolbar root
152
- SELECT b.id, b.parent, b.title, p.url,
153
- CAST(b.title as TEXT) as path
154
- FROM moz_bookmarks b
155
- LEFT JOIN moz_places p ON b.fk = p.id
156
- WHERE b.parent = (
157
- SELECT id FROM moz_bookmarks
158
- WHERE guid = 'toolbar_____'
159
- )
160
-
161
- UNION ALL
162
-
163
- -- Recursively get children
164
- SELECT b.id, b.parent, b.title, p.url,
165
- CAST(bt.path || '/' || b.title as TEXT) as path
166
- FROM moz_bookmarks b
167
- INNER JOIN bookmark_tree bt ON b.parent = bt.id
168
- LEFT JOIN moz_places p ON b.fk = p.id
169
- )
170
- SELECT id, title, path, url
171
- FROM bookmark_tree
172
- WHERE url IS NOT NULL
173
- ORDER BY path, title
174
- SQL
175
-
176
- db.execute(query).each do |row|
177
- folder_path = row["path"].to_s.split("/")[0..-2].join("/") + "/"
178
- folder_path = "|" + folder_path.gsub(/[:,'"]/, "-").downcase
179
-
180
- values = [folder_path, row["title"], row["url"], row["id"].to_s]
181
- if matches_search?(values)
182
- @allurls << Bookmark.new(
183
- folder_path,
184
- row["title"] || "",
185
- row["url"] || "",
186
- row["id"].to_s
187
- )
188
- end
189
- end
190
- rescue SQLite3::Exception => e
191
- puts "Error: ".red + "Could not read Firefox bookmarks database"
192
- puts e.message
193
- ensure
194
- db&.close
195
- temp&.unlink
196
- end
197
- end
198
-
199
- private
200
-
201
- def firefox_running?
202
- # Simple check - try to get a shared lock
203
- # If Firefox is running, it has an exclusive lock
204
- return false unless File.exist?(@file_path)
205
-
206
- begin
207
- File.open(@file_path, "r") do |f|
208
- # If we can open for reading, Firefox might still be running
209
- # but we'll handle it by copying to temp
210
- end
211
- # Check for Firefox process
212
- `pgrep -x firefox 2>/dev/null`.strip != ""
213
- rescue
214
- false
215
- end
216
- end
217
- end
218
-
219
- # Main Bookmarks facade class
220
- class Bookmarks
221
- def initialize(search_term = "")
222
- @conf = BConfig.new
223
- file_paths = @conf.bookmarks # Now returns an array
224
- @allurls = []
225
-
226
- # Parse bookmarks from all sources
227
- file_paths.each_with_index do |file_path, source_index|
228
- next unless file_path && File.exist?(file_path)
229
-
230
- parser_class = detect_parser(file_path)
231
- parser = parser_class.new(file_path, search_term)
232
- parser.parse
233
-
234
- # Merge results, prefixing IDs to ensure uniqueness across sources
235
- parser.results.each do |bookmark|
236
- # Create unique ID: source_index + original_id
237
- unique_bookmark = Bookmark.new(
238
- bookmark.folder,
239
- bookmark.title,
240
- bookmark.url,
241
- "#{source_index}_#{bookmark.id}"
242
- )
243
- @allurls << unique_bookmark
244
- end
245
- end
246
- end
247
-
248
- # output for zsh autocompetion, print out id, title and cleaned url
249
- def autocomplete
250
- @allurls.each do |url|
251
- name = clean_name(url)
252
- link = clean_link(url)
253
- puts url.id + ":" + name + ":" + link
254
- end
255
- end
256
-
257
- # clean title for completion, delete anything not allowed in linktitle
258
- def clean_name(url)
259
- # Clean up folder display (remove leading |, show [root] for top-level)
260
- folder = url.folder.gsub(/^\|/, "")
261
- folder = (folder == "/") ? "[root]" : folder.chomp("/")
262
-
263
- # Format: folder | title
264
- name = folder + " |" + url.title.gsub(/[^a-z0-9\-\/_ ]/i, "")
265
- name.squeeze!("-")
266
- name.squeeze!(" ")
267
- # Use half terminal width for name to leave room for URL
268
- name.window([TERMWIDTH / 2, 50].min)
269
- end
270
-
271
- # clean link for completion, remove strange things from any linkurls
272
- def clean_link(url)
273
- link = url.url.gsub(/[,'"&?].*/, "")
274
- link.gsub!(/.*:\/+/, "")
275
- link.delete!(" ")
276
- # Use half terminal width for URL
277
- max_width = [TERMWIDTH / 2 - CODEWIDTH, 50].min
278
- link[0..max_width]
279
- end
280
-
281
- # get link (from id number)
282
- def bookmark_url(id)
283
- @allurls.each do |url|
284
- return url.url if id == url.id
285
- end
286
- end
287
-
288
- private
289
-
290
- def detect_parser(file_path)
291
- if file_path&.end_with?(".sqlite")
292
- FirefoxBookmarkParser
293
- else
294
- ChromeBookmarkParser
295
- end
296
- end
297
- end # close bookmarks class
298
-
299
- # for recursively parsing bookmarks
300
- class Folder
301
- include Enumerable
302
-
303
- attr_reader :json, :title
304
- def initialize(json, title = "|")
305
- @title = title.gsub(/[:,'"]/, "-").downcase
306
- @json = json
307
- end
308
-
309
- # needed for Enumerable
310
- def each
311
- @json.each
312
- end
313
- end
314
-
315
- # clean bookmark title, set attrs
316
- class Bookmark
317
- attr_reader :title, :folder, :url, :id
318
- def initialize(f, t, u, id)
319
- @title = t.gsub(/[:'"+]/, " ").downcase
320
- @folder = f
321
- @url = u
322
- @id = id
323
- end
324
- end
data/lib/config.rb DELETED
@@ -1,187 +0,0 @@
1
- # configuation - get where bookmarks are
2
-
3
- # detect operating system
4
- module OS
5
- def self.windows?
6
- (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
7
- end
8
-
9
- def self.mac?
10
- (/darwin/ =~ RUBY_PLATFORM) != nil
11
- end
12
-
13
- def self.linux?
14
- !(OS.windows? or OS.mac?)
15
- end
16
- end
17
-
18
- # return browser (chrome) opening command
19
- module Browser
20
- extend OS
21
-
22
- def browse
23
- if OS.windows?
24
- # alternatively, start seems to work - probably check if powershell v cygwin?
25
- '/cygdrive/c/Program\ Files\ \(x86\)/Google/Chrome/Application/chrome.exe '
26
- elsif OS.mac?
27
- "open "
28
- elsif OS.linux?
29
- "xdg-open "
30
- end
31
- end
32
-
33
- def domain
34
- /.*(io|com|web|net|org|gov|edu|xyz)(\/.*)?$/i
35
- end
36
-
37
- # helper methods
38
- def prep(url)
39
- if /^http/.match?(url)
40
- url
41
- else
42
- "http://" + url
43
- end
44
- end
45
-
46
- def wrap(url)
47
- "\"#{url}\""
48
- end
49
- end
50
-
51
- # configuration
52
- class BConfig
53
- VALID = [:searcher, :bookmarks, :browser]
54
- HOME = ENV["HOME"].nil? ? "/usr/local/" : ENV["HOME"]
55
- YAMLCONF = HOME + "/.booker.yml"
56
-
57
- def initialize
58
- # config defaults (for osx, default chrome profile)
59
- readyaml = read(YAMLCONF)
60
- default_bookmarks = detect_default_bookmarks
61
- default_config = {
62
- browser: "open ",
63
- searcher: "https://google.com/search?q=",
64
- bookmarks: default_bookmarks
65
- }
66
-
67
- # configure w/ yaml config file, if it exists
68
- @config = readyaml || default_config
69
-
70
- # prune bad config keys
71
- @config.each do |k, v|
72
- if !VALID.include? k.to_sym
73
- puts "Failure:".red + " Bad key found in config file: #{k}"
74
- exit 1
75
- end
76
- end
77
- end
78
-
79
- def detect_default_bookmarks
80
- # Return ALL available bookmark sources (both Chrome and Firefox)
81
- all_sources = discover_all_bookmark_sources
82
-
83
- # If we found sources, return them all; otherwise return a default single path
84
- if all_sources.empty?
85
- # Fallback to macOS Chrome default for backward compatibility
86
- HOME + "/Library/Application Support/Google/Chrome/Profile 1/Bookmarks"
87
- else
88
- all_sources
89
- end
90
- end
91
-
92
- def discover_all_bookmark_sources
93
- sources = []
94
-
95
- # Discover all Chrome/Chromium bookmarks
96
- chrome_base_paths = [
97
- HOME + "/Library/Application Support/Google/Chrome", # macOS
98
- HOME + "/Library/Application Support/Chromium", # macOS Chromium
99
- HOME + "/.config/google-chrome", # Linux
100
- HOME + "/.config/chromium", # Linux Chromium
101
- HOME + "/snap/chromium/common/chromium", # Linux (snap)
102
- HOME + "/snap/chromium/current/.config/chromium", # Linux (snap alt)
103
- HOME + "/AppData/Local/Google/Chrome/User Data" # Windows
104
- ]
105
-
106
- chrome_base_paths.each do |base|
107
- next unless Dir.exist?(base)
108
-
109
- # Find all profile directories and their Bookmarks files
110
- begin
111
- Dir.glob(File.join(base, "**/Bookmarks")).each do |bookmark_file|
112
- sources << bookmark_file if File.file?(bookmark_file)
113
- end
114
- rescue
115
- # Skip if we can't read this directory
116
- end
117
- end
118
-
119
- # Discover all Firefox profiles
120
- firefox_base_paths = [
121
- HOME + "/.mozilla/firefox", # Linux
122
- HOME + "/snap/firefox/common/.mozilla/firefox", # Linux (snap)
123
- HOME + "/Library/Application Support/Firefox", # macOS
124
- HOME + "/AppData/Roaming/Mozilla/Firefox" # Windows
125
- ]
126
-
127
- firefox_base_paths.each do |firefox_base|
128
- next unless Dir.exist?(firefox_base)
129
-
130
- profiles_ini = File.join(firefox_base, "profiles.ini")
131
- next unless File.exist?(profiles_ini)
132
-
133
- # Parse all Firefox profiles
134
- File.readlines(profiles_ini).each do |line|
135
- if line.include?("Path=")
136
- profile_path = line.split("=", 2)[1].strip
137
- db_path = File.join(firefox_base, profile_path, "places.sqlite")
138
- sources << db_path if File.exist?(db_path)
139
- end
140
- end
141
- end
142
-
143
- sources.uniq
144
- end
145
-
146
- def read(file)
147
- begin
148
- config = YAML.load(IO.read(file))
149
- rescue Errno::ENOENT
150
- warn "Warning: ".yel +
151
- "YAML configuration file couldn't be found. Using defaults."
152
- warn "Suggest: ".grn + "booker --install config"
153
- return false
154
- rescue Psych::SyntaxError
155
- warn "Warning: ".red +
156
- "YAML configuration file contains invalid syntax. Using defaults."
157
- return false
158
- end
159
- config
160
- end
161
-
162
- # used for creating and updating the default configuration
163
- def write(k = nil, v = nil)
164
- if k.nil? && v.nil?
165
- else
166
- @config[k] = v # update users yaml config file
167
- end
168
- File.write(YAMLCONF, @config.to_yaml)
169
- end
170
-
171
- def bookmarks
172
- # Return array of bookmark sources
173
- # Handles both old single-path configs and new multi-source configs
174
- result = @config[:bookmarks]
175
-
176
- if result.is_a?(Array)
177
- result
178
- else
179
- # Single path - wrap in array for consistent interface
180
- [result]
181
- end
182
- end
183
-
184
- def searcher
185
- @config[:searcher]
186
- end
187
- end
data/lib/consts.rb DELETED
@@ -1,114 +0,0 @@
1
- # file for large constant strings
2
-
3
- # todo - wrap in module
4
-
5
- HELP_BANNER = <<~EOS
6
- Open browser:
7
- $ booker [option] [arguments]
8
-
9
- Main options:
10
- --bookmark, -b: explicity open bookmark
11
- --install, -i: install [bookmarks, completion, config]
12
- --search, -s: explicity search arguments
13
-
14
- Others:
15
- --complete, -c: show tab completions
16
- --version, -v: print version
17
- --help, -h: show help
18
- EOS
19
-
20
- DEF_CONFIG = <<~EOS
21
- ---
22
- :searcher: https://google.com/search?q=
23
- :bookmarks: null
24
- EOS
25
-
26
- COMPLETION = <<~EOS
27
- #compdef booker
28
- #autoload
29
-
30
-
31
- _booker_bookmarks() {
32
- #http://zsh.sourceforge.net/Guide/zshguide06.html#l147
33
- setopt glob bareglobqual nullglob rcexpandparam extendedglob automenu
34
- unsetopt allexport aliases errexit octalzeroes ksharrays cshnullglob
35
-
36
- #get bookmarks
37
- local -a bookmarks sites search_terms
38
- search_terms=()
39
-
40
- #filter out 'booker' and already-selected bookmark IDs (numeric with underscores)
41
- for arg in "$@"; do
42
- if [[ "$arg" != "booker" && ! "$arg" =~ ^[0-9_]+$ ]]; then
43
- search_terms+=("$arg")
44
- fi
45
- done
46
-
47
- #join search terms and get matching bookmarks
48
- local search="${search_terms[*]}"
49
- sites=`booker --complete $search`
50
- bookmarks=("${(f)${sites}}")
51
-
52
- #terminal colors
53
- BLUE=`echo -e "\033[4;34m"`
54
- RESET=`echo -e "\033[0;0m"`
55
-
56
- #parse parts, color
57
- local -a ids links
58
- for bookmark in ${bookmarks[@]}; do
59
- local -a split
60
- split=("${(@s/:/)bookmark}")
61
- ids+=( $split[1] )
62
- links+=("$split[2] $BLUE$split[3]$RESET")
63
- done
64
-
65
- #finally, add completions
66
- compstate[insert]=menu
67
- compadd -U -l -X 'found bookmarks:' -d links -a ids
68
- }
69
-
70
-
71
- _booker() {
72
- compstate[insert]=menu
73
- local curcontext state line
74
- typeset -A opt_args
75
-
76
- _arguments -C \
77
- '(-b)-b[do bookmark completion]'\
78
- '(--bookmark)--bookmark[do bookmark completion]'\
79
- '(-c)-c[show bookmark completions]'\
80
- '(--complete)--complete[show bookmark completions]'\
81
- '(-i)-i[perform installations (bookmarks, completion, config)]'\
82
- '(--install)--install[perform installations (bookmarks, completion, config)]'\
83
- '(-s)-s[search google for...]'\
84
- '(--search)--search[search google for...]'\
85
- '(-h)-h[show booker help]'\
86
- '(--help)--help[show booker help]'\
87
- '(-v)-v[show version]'\
88
- '(--version)--version[show version]'\
89
- '*::bookmarks:->bookmarks'
90
-
91
- # Handle the bookmarks state for repeated completions
92
- case $state in
93
- bookmarks)
94
- _booker_bookmarks $words
95
- ;;
96
- esac
97
- }
98
-
99
-
100
- _booker
101
- EOS
102
-
103
- ZSH_SCRIPT = <<~EOS
104
- # load zsh booker completions
105
- install_loc=~/.oh-my-zsh/completions
106
- script=_booker
107
-
108
- install:
109
- mkdir -vp $(install_loc)
110
- cp -v $(script) $(install_loc)/
111
-
112
- uninstall:
113
- rm -v $(install_loc)/$(script)
114
- EOS