hiiro 0.1.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/hiiro.rb ADDED
@@ -0,0 +1,389 @@
1
+ require "fileutils"
2
+ require "yaml"
3
+
4
+ require_relative "hiiro/version"
5
+
6
+ class Hiiro
7
+ def self.init(*args, plugins: [], logging: false, **values, &block)
8
+ args = ARGV if args.empty?
9
+
10
+ new($0, *args, logging: logging, **values).tap do |hiiro|
11
+ hiiro.load_plugins(*plugins)
12
+
13
+ hiiro.add_subcmd(:edit, **values) { |*args|
14
+ system(ENV['EDITOR'] || 'nvim', hiiro.bin)
15
+ }
16
+
17
+ block.call(hiiro) if block
18
+ end
19
+ end
20
+
21
+ def self.load_env
22
+ Config.plugin_files.each do |plugin_file|
23
+ require plugin_file
24
+ end
25
+ end
26
+
27
+ attr_reader :bin, :bin_name, :all_args
28
+ attr_reader :subcmd, :args
29
+ attr_reader :loaded_plugins
30
+ attr_reader :logging
31
+ attr_reader :global_values
32
+
33
+ def initialize(bin, *args, logging: false, **values)
34
+ @bin = bin
35
+ @bin_name = File.basename(bin)
36
+ @all_args = args
37
+ @subcmd, *@args = args # normally i would never do this
38
+ @loaded_plugins = []
39
+ @logging = logging
40
+ @global_values = values
41
+ end
42
+
43
+ def run
44
+ result = runner.run(*args)
45
+
46
+ handle_result(result)
47
+
48
+ exit 1
49
+ rescue => e
50
+ puts "ERROR: #{e.message}"
51
+ puts e.backtrace
52
+ exit 1
53
+ end
54
+
55
+ def handle_result(result)
56
+ exit 0 if result.nil? || result
57
+
58
+ exit 1
59
+ end
60
+
61
+ def runnable?
62
+ runner
63
+ end
64
+
65
+ def runner
66
+ runners.runner || runners.default_subcommand
67
+ end
68
+
69
+ def runners
70
+ @runners ||= Runners.new(self)
71
+ end
72
+
73
+ def add_default(**values, &handler)
74
+ runners.add_subcommand(:DEFAULT, handler, **global_values, **values)
75
+ end
76
+
77
+ def add_subcommand(name, **values, &handler)
78
+ runners.add_subcommand(name, handler, **global_values, **values)
79
+ end
80
+ alias add_subcmd add_subcommand
81
+
82
+ def full_name
83
+ runner&.full_name || [bin_name, subcmd].join(?-)
84
+ end
85
+
86
+ def subcommand_names
87
+ runners.subcommand_names
88
+ end
89
+
90
+ def pins = @pins ||= Pin.new(self)
91
+
92
+ def load_plugins(*plugins)
93
+ plugins.flatten.each { |plugin| load_plugin(plugin) }
94
+ end
95
+
96
+ def load_plugin(plugin_const)
97
+ return if @loaded_plugins.include?(plugin_const)
98
+
99
+ plugin_const.load(self)
100
+ @loaded_plugins.push(plugin_const)
101
+ end
102
+
103
+ def help
104
+ ambiguous = runners.ambiguous_matches
105
+
106
+ if ambiguous.any?
107
+ puts "Ambiguous subcommand #{subcmd.inspect}!"
108
+ puts ""
109
+ puts "Did you mean one of these?"
110
+ list_runners(ambiguous)
111
+ else
112
+ subcmd_msg = "Subcommand required!"
113
+ subcmd_msg = "Subcommand #{subcmd.inspect} not found!" if subcmd
114
+
115
+ puts subcmd_msg
116
+ puts ""
117
+ puts "Possible subcommands:"
118
+ list_runners(runners.all_runners)
119
+ end
120
+ end
121
+
122
+ def list_runners(list)
123
+ max_name = list.map { |r| r.subcommand_name.length }.max || 0
124
+ max_type = list.map { |r| r.type.to_s.length }.max || 0
125
+ max_params = list.map { |r| r.params_string.to_s.length }.max || 0
126
+
127
+ list.each do |r|
128
+ name = r.subcommand_name.ljust(max_name)
129
+ type = "(#{r.type})".ljust(max_type + 2)
130
+ params = r.params_string
131
+ params_col = params ? params.ljust(max_params) : ''.ljust(max_params)
132
+ location = r.location
133
+ puts " #{name} #{params_col} #{type} #{location}"
134
+ end
135
+ end
136
+
137
+ def log(message)
138
+ return unless logging
139
+
140
+ puts "[Hiiro: #{bin_name} #{(runner&.subcommand_name || subcmd).inspect}]: #{message}"
141
+ end
142
+
143
+ def parsed_args
144
+ i = Args.new(*args)
145
+ end
146
+
147
+ def get_value(name)
148
+ runner&.values&.[](name)
149
+ end
150
+
151
+ class Config
152
+ class << self
153
+ def plugin_files
154
+ Dir.glob(File.join(plugin_dir, '*'))
155
+ end
156
+
157
+ def plugin_dir
158
+ config_dir('plugins')
159
+ end
160
+
161
+ def config_dir(subdir=nil)
162
+ File.join(Dir.home, '.config/hiiro', *[subdir].compact).tap do |config_path|
163
+ FileUtils.mkdir_p(config_path) unless Dir.exist?(config_path)
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ class Runners
170
+ attr_reader :hiiro, :bin_name, :subcmd, :subcommands
171
+
172
+ def initialize(hiiro)
173
+ @hiiro = hiiro
174
+ @bin_name = hiiro.bin_name
175
+ @subcmd = hiiro.subcmd
176
+ @subcommands = {}
177
+ end
178
+
179
+ def runner
180
+ return default_subcommand if subcmd.to_s == ''
181
+
182
+ exact_runner || unambiguous_runner
183
+ end
184
+
185
+ def default_subcommand
186
+ Subcommand.new(bin_name, :default, lambda { hiiro.help; false })
187
+ end
188
+
189
+ def subcommand_names
190
+ all_runners.map(&:subcommand_name)
191
+ end
192
+
193
+ def all_runners
194
+ [*all_bins, *subcommands.values]
195
+ end
196
+
197
+ def paths
198
+ @paths ||= ENV['PATH'].split(?:).uniq
199
+ end
200
+
201
+ def all_bins
202
+ pattern = format('{%s}/%s-*', paths.join(?,), bin_name)
203
+
204
+ Dir.glob(pattern).map { |path| Bin.new(bin_name, path) }
205
+ end
206
+
207
+ def add_subcommand(name, handler, **values)
208
+ @subcommands[name] = Subcommand.new(bin_name, name, handler, values)
209
+ end
210
+
211
+ def exact_runner
212
+ all_runners.find { |r| r.exact_match?(subcmd) }
213
+ end
214
+
215
+ def unambiguous_runner
216
+ return nil if subcmd.nil?
217
+
218
+ matches = matching_runners
219
+ return matches.first if matches.count == 1
220
+
221
+ nil
222
+ end
223
+
224
+ def ambiguous_matches
225
+ return [] if subcmd.nil?
226
+
227
+ matches = matching_runners
228
+ return matches if matches.count > 1
229
+
230
+ []
231
+ end
232
+
233
+ def matching_runners
234
+ remove_child_runners(all_matching_runners)
235
+ end
236
+
237
+ def all_matching_runners
238
+ all_runners.select { |r| r.match?(subcmd) }
239
+ end
240
+
241
+ def remove_child_runners(list)
242
+ list.reject do |r|
243
+ list.any? { |other| r != other && r.full_name.start_with?(other.full_name + ?-) }
244
+ end
245
+ end
246
+
247
+ class Bin
248
+ attr_reader :bin_name, :path, :name
249
+ alias full_name name
250
+
251
+ def initialize(bin_name, path)
252
+ @bin_name = bin_name
253
+ @path = path
254
+ @name = File.basename(path)
255
+ end
256
+
257
+ def run(*args)
258
+ system(path, *args)
259
+ end
260
+
261
+ def exact_match?(subcmd)
262
+ subcommand_name == subcmd.to_s
263
+ end
264
+
265
+ def match?(subcmd)
266
+ subcommand_name.start_with?(subcmd.to_s)
267
+ end
268
+
269
+ def subcommand_name
270
+ name.sub("#{bin_name}-", '')
271
+ end
272
+
273
+ def values
274
+ {}
275
+ end
276
+
277
+ def type
278
+ :bin
279
+ end
280
+
281
+ def location
282
+ path
283
+ end
284
+
285
+ def params_string
286
+ nil
287
+ end
288
+ end
289
+
290
+ class Subcommand
291
+ attr_reader :bin_name, :name, :handler, :values
292
+ alias subcommand_name name
293
+
294
+ def initialize(bin_name, name, handler, values={})
295
+ @bin_name = bin_name
296
+ @name = name.to_s
297
+ @handler = handler
298
+ @values = values || {}
299
+ end
300
+
301
+ def run(*args)
302
+ handler.call(*args)
303
+ end
304
+
305
+ def exact_match?(subcmd)
306
+ name == subcmd.to_s
307
+ end
308
+
309
+ def match?(subcmd)
310
+ name.start_with?(subcmd.to_s)
311
+ end
312
+
313
+ def full_name
314
+ [bin_name, name].join(?-)
315
+ end
316
+
317
+ def type
318
+ :subcommand
319
+ end
320
+
321
+ def location
322
+ handler.source_location&.join(':')
323
+ end
324
+
325
+ def params_string
326
+ return nil unless handler.respond_to?(:parameters)
327
+
328
+ params = handler.parameters
329
+ return nil if params.empty?
330
+ return nil if params == [[:rest]] || params == [[:rest, :args]]
331
+
332
+ params.map { |type, name|
333
+ case type
334
+ when :req then "<#{name}>"
335
+ when :opt then "[#{name}]"
336
+ when :rest then "[*#{name}]" if name
337
+ when :keyreq then "<#{name}:>"
338
+ when :key then "[#{name}:]"
339
+ when :keyrest then "[**#{name}]" if name
340
+ when :block then "[&#{name}]" if name
341
+ end
342
+ }.compact.join(' ')
343
+ end
344
+ end
345
+ end
346
+
347
+ class Args
348
+ attr_reader :raw_args
349
+
350
+ def initialize(*raw_args)
351
+ @raw_args = raw_args
352
+ end
353
+
354
+ def flags
355
+ @flags ||= proc {
356
+ raw_args.select { |arg|
357
+ arg.match?(/^-[^-]/)
358
+ }.flat_map { |arg|
359
+ arg.sub(/^-/, '').chars
360
+ }
361
+ }.call
362
+ end
363
+
364
+ def flag?(flag)
365
+ flags.include?(flag)
366
+ end
367
+
368
+ def flag_value(flag)
369
+ found_flag = false
370
+ raw_args.each do |arg|
371
+ if found_flag
372
+ return arg
373
+ end
374
+
375
+ if arg.match?(/^-\w*#{flag}/)
376
+ found_flag = true
377
+ end
378
+ end
379
+
380
+ nil
381
+ end
382
+
383
+ def values
384
+ raw_args.reject do |arg|
385
+ arg.match?(/^-/)
386
+ end
387
+ end
388
+ end
389
+ end
data/plugins/notify.rb ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Notify
4
+ def self.load(hiiro)
5
+ hiiro.log "Plugin loaded: #{name}"
6
+
7
+ hiiro.load_plugin(Tmux)
8
+ attach_methods(hiiro)
9
+ add_subcommands(hiiro)
10
+ end
11
+
12
+ def self.add_subcommands(hiiro)
13
+ hiiro.add_subcmd(:notify) do |message, link=nil, title=nil, command=nil|
14
+ hiiro.notify(message, title:, link:, command:)
15
+ end
16
+ end
17
+
18
+ def self.attach_methods(hiiro)
19
+ hiiro.instance_eval do
20
+ def notify(message, title: nil, link: nil, command: nil)
21
+ args = ['terminal-notifier', '-message', message]
22
+ args += ['-title', title] if title
23
+ args += ['-open', link] if link
24
+ args += ['-execute', command] if command
25
+
26
+ system(*args) if system('which', 'terminal-notifier')
27
+ end
28
+ end
29
+ end
30
+ end
data/plugins/pins.rb ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Pins
4
+ def self.load(hiiro)
5
+ hiiro.log "Plugin loaded: #{name}"
6
+
7
+ attach_methods(hiiro)
8
+ add_subcommands(hiiro)
9
+ end
10
+
11
+ def self.add_subcommands(hiiro)
12
+ hiiro.add_subcmd(:pin) do |*args|
13
+ pins = hiiro.pins
14
+
15
+ case args
16
+ in [] then pins.pins.each {|k,v| puts "#{k} => #{v.inspect}" }
17
+ in ['all'] then pins.pins.each {|k,v| puts "#{k} => #{v.inspect}" }
18
+ in ['get', name] then puts pins.get(name)
19
+ in [name] then puts pins.get(name)
20
+ in ['rm', name] then puts pins.remove_and_save(name)
21
+ in ['remove', name] then puts pins.remove_and_save(name)
22
+ in ['set', name, value] then pins.set_and_save(name, value)
23
+ in [name, value] then pins.set_and_save(name, value)
24
+ in [name, *values] then pins.set_and_save(name, values.join(' '))
25
+ else
26
+ puts "No matching pin subcommand for #{args.inspect}"
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.attach_methods(hiiro)
32
+ hiiro.instance_eval do
33
+ def pins
34
+ @pins ||= Pin.new(self)
35
+ end
36
+ end
37
+ end
38
+
39
+ class Pin
40
+ attr_reader :hiiro
41
+
42
+ def initialize(hiiro) = @hiiro = hiiro
43
+
44
+ def get(name)
45
+ pins[find(name)]
46
+ end
47
+
48
+ def set(name, value)
49
+ pins[name.to_s] = value
50
+ end
51
+
52
+ def set_and_save(name, value)
53
+ set(name, value)
54
+ save_pins
55
+ value
56
+ end
57
+
58
+ def find(partial)
59
+ pins.keys.map(&:to_s).find do |pin_name|
60
+ pin_name.start_with?(partial)
61
+ end
62
+ end
63
+
64
+ def find_all(partial)
65
+ pins.keys.map(&:to_s).select do |pin_name|
66
+ pin_name.start_with?(partial)
67
+ end
68
+ end
69
+
70
+ def remove(name)
71
+ all_matches = find_all(name)
72
+
73
+ if all_matches.count > 1
74
+ puts "Unable to remove pin. Multiple matches: #{all_matches.inspect}"
75
+ return
76
+ end
77
+
78
+ pin_name = all_matches.first
79
+
80
+ pins.delete(pin_name.to_s)
81
+ end
82
+
83
+ def remove_and_save(name)
84
+ remove(name)
85
+ save_pins
86
+ pins
87
+ end
88
+
89
+ def pin_filename = hiiro.bin_name
90
+ def pin_dir = Hiiro::Config.config_dir('pins').tap {|dir| FileUtils.mkdir_p(dir) }
91
+ def pin_file = File.join(pin_dir, pin_filename)
92
+ def pins
93
+ return @pins if @pins
94
+
95
+ unless File.exist?(pin_file)
96
+ File.write(pin_file, YAML.dump({}, stringify_names: true))
97
+ end
98
+
99
+ @pins = YAML.safe_load_file(pin_file)
100
+ end
101
+
102
+ def save_pins(pins = nil)
103
+ pins = pins || @pins || {}
104
+
105
+ File.write(pin_file, YAML.dump(pins, stringify_names: true))
106
+ end
107
+
108
+ def pins!
109
+ @pins = nil
110
+ pins
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Project
4
+ def self.load(hiiro)
5
+ hiiro.log "Plugin loaded: #{name}"
6
+
7
+ hiiro.load_plugin(Tmux)
8
+ attach_methods(hiiro)
9
+ add_subcommands(hiiro)
10
+ end
11
+
12
+ def self.add_subcommands(hiiro)
13
+ hiiro.add_subcmd(:project) do |project_name|
14
+ re = /#{project_name}/i
15
+
16
+ conf_matches = hiiro.projects_from_config.select{|k,v| k.match?(re) }
17
+ dir_matches = hiiro.project_dirs.select{|proj, path| proj.match?(re) }
18
+
19
+ puts(conf_matches:,dir_matches:)
20
+ matches = dir_matches.merge(conf_matches)
21
+ if matches.count > 1
22
+ matches = matches.select{|name, path| name == project_name }
23
+ end
24
+
25
+ puts matches_two: matches
26
+ case matches.count
27
+ when 0
28
+ name = 'proj'
29
+ path = File.join(Dir.home, 'proj')
30
+
31
+ unless Dir.exist?(path)
32
+ puts "Error: #{path.inspect} does not exist"
33
+ exit 1
34
+ end
35
+
36
+ puts "changing dir: #{path}"
37
+ Dir.chdir(path)
38
+
39
+ hiiro.start_tmux_session(name)
40
+ when 1
41
+ name, path = matches.first
42
+
43
+ puts "changing dir: #{path}"
44
+ Dir.chdir(path)
45
+
46
+ hiiro.start_tmux_session(name)
47
+ when (2..)
48
+ puts "ERROR: Multiple matches found"
49
+ puts
50
+ puts "Matches:"
51
+ matches.each { |name, path|
52
+ puts format(" %s: %s", name, path)
53
+ }
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.attach_methods(hiiro)
59
+ hiiro.instance_eval do
60
+ def project_dirs
61
+ Dir.glob(File.join(Dir.home, 'proj', '*/')).map { |path|
62
+ [File.basename(path), path]
63
+ }.to_h
64
+ end
65
+
66
+ def projects_from_config
67
+ projects_file = File.join(Dir.home, '.config/hiiro', 'projects.yml')
68
+
69
+ return {} unless File.exist?(projects_file)
70
+
71
+ YAML.safe_load_file(projects_file)
72
+ end
73
+ end
74
+ end
75
+ end