imsg-grep 0.1.2-darwin

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.
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # NSKeyedArchive decoder for unpacking Apple's keyed archive format from binary plists
5
+
6
+ require "json"
7
+ require "base64"
8
+ require_relative "bplist"
9
+
10
+ class KeyedArchive
11
+ attr_reader :data
12
+
13
+ class BinaryString < String
14
+ def initialize(s) = super(s).force_encoding("BINARY")
15
+ def to_json(...) = Base64.strict_encode64(self).to_json(...)
16
+ end
17
+
18
+ def self.unarchive(...) = new(...).unarchive
19
+ def self.json(...) = new(...).json
20
+
21
+ def initialize(data)
22
+ return unless data
23
+ return if data[0, 4] == "\x08\x08\x11\x00" # DigitalTouchBalloonProvider row; another format, ignore
24
+ raise "no bplist: #{data}" unless data.start_with?("bplist")
25
+ @data = BPList.parse data
26
+ end
27
+
28
+ def unarchive
29
+ return unless @data # initialize failed
30
+ top = @data["$top"] or raise "no top"
31
+ root = top["root"] or raise "no root"
32
+ objs = @data["$objects"] or raise "no objects"
33
+ decode_objects dereference_uids(root, objs)
34
+ end
35
+
36
+ def json = unarchive&.to_json
37
+
38
+ private
39
+
40
+ def dereference_uids(obj, objects, visited = Set.new, depth = 0)
41
+ # Prevent infinite recursion
42
+ raise "Maximum recursion depth exceeded" if depth > 1000
43
+ case obj
44
+ when Hash
45
+ if obj.size == 1 && obj.has_key?("CF$UID")
46
+ # This is a UID reference, expand it
47
+ uid = obj["CF$UID"]
48
+ return obj if visited.include?(uid) # Prevent infinite recursion
49
+ return obj if uid >= objects.length || uid < 0
50
+
51
+ visited.add(uid)
52
+ result = dereference_uids(objects[uid], objects, visited, depth + 1)
53
+ visited.delete(uid)
54
+ return result
55
+ else
56
+ # Regular hash, expand all values
57
+ result = {}
58
+ obj.each { |k, v| result[k] = dereference_uids(v, objects, visited, depth + 1) }
59
+ return result
60
+ end
61
+ when Array
62
+ obj.map { |item| dereference_uids(item, objects, visited, depth + 1) }
63
+ else
64
+ obj
65
+ end
66
+ end
67
+
68
+ def decode_objects(obj)
69
+ case obj
70
+ when Hash
71
+ case obj.dig("$class", "$classname")
72
+ in "NSArray" if obj.key? "NS.objects"
73
+ obj["NS.objects"].map { |item| decode_objects(item) }
74
+
75
+ in "NSDictionary" if obj.key?("NS.keys") && obj.key?("NS.objects")
76
+ obj["NS.keys"].zip(obj["NS.objects"].map{ decode_objects it }).to_h
77
+
78
+ in "NSURL" if obj["NS.base"] == "$null" && obj["NS.relative"]
79
+ obj["NS.relative"]
80
+ else
81
+ obj.transform_values { decode_objects it }
82
+ end
83
+
84
+ when Array then obj.map { |item| decode_objects(item) }
85
+ when "$null" then nil # Transform $null strings to actual null
86
+
87
+ when String # Transform binary data to Base64 ; detect if string is binary (ASCII-8BIT) or invalid UTF-8
88
+ case
89
+ when obj.encoding == Encoding::BINARY then BinaryString.new(obj)
90
+ when obj.force_encoding('UTF-8').valid_encoding? then obj
91
+ else BinaryString.new(obj)
92
+ end
93
+
94
+ else obj
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+
101
+ if __FILE__ == $0
102
+ input = $stdin.read
103
+ out = KeyedArchive.unarchive(input)
104
+ puts JSON.pretty_generate(out)
105
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Query result formatter that prints SQL results in Very Nice tables
4
+
5
+ require "io/console"
6
+
7
+ module Print
8
+ @term_width = IO.console.winsize[1]
9
+ @term_width -= 3 if ENV["TERM_PROGRAM"] == "zed" # safe zone for https://github.com/zed-industries/zed/issues/43629
10
+
11
+ def self.c256(c, s) = "\e[38;5;#{c}m#{s}\e[39m"
12
+ def self.mark(s) = "\e[38;5;178m#{s}\e[39m"
13
+ def self.sep(s) = "\e[30m#{s}\e[39m"
14
+ def self.clr = "\e[0m"
15
+
16
+ def self.query(q, *args, title: nil, db: Messages.db)
17
+ table db.execute2(q, *args), title:
18
+ end
19
+
20
+ def self.table(table, title: nil)
21
+ bg1 = "\e[48;5;4m"
22
+ bga = ["\e[48;5;235m", "\e[48;5;238m"]
23
+ bg = ->{ bg1 ? (bg1.tap{bg1=nil}) : bga.rotate!.first }
24
+
25
+
26
+ # puts "\n#{'=' * 80}"
27
+ hrow, *rows = table
28
+ # max lengths for each col, but capping the header rows at 5 so big headers with short content don't take up space
29
+ lens = [hrow.map{it[0,5]}].concat(rows).transpose.map{|col|col.map{it.to_s.size}.max||0}
30
+ # is each column number? used for aligning them right
31
+ nums = rows.transpose.map{|col| col.compact.all?{ Numeric===it }}
32
+ hex = rows.transpose.map{|col| col.compact.all?{ String===it && it !~ /\H/ }}
33
+ hex.zip(0..).each{|h,i| lens[i] = 20 if h } # hex cols start short
34
+
35
+ # available term width for all cols excluding separators
36
+ colsw = @term_width - hrow.size + 1
37
+ # max width per col; start with an even col width then redistribute next
38
+ maxcw = colsw / hrow.size
39
+
40
+ # begin redistributing
41
+ bigs = lens
42
+ taken = 0
43
+ while true
44
+ smalls, bigs = bigs.partition{ it <= maxcw } # partition cols under/at max col width
45
+ break if bigs.empty? || smalls.empty? # can't redistribute if all on either side
46
+ taken += smalls.sum # accumulate used up width
47
+ maxcw = (colsw - taken) / bigs.size # max for remaining bigs: split even
48
+ end
49
+ lens.map!{ [it, maxcw].min }
50
+
51
+ # expand col header width while fits; those we capped earlier
52
+ trunc_headers = lens.zip(hrow.map{it.size}, 0..).filter_map{|l,hl,i| [i,hl] if l<hl } # [col index, full header length]
53
+ while lens.sum < colsw && trunc_headers.any? # while still fits and any left to widen
54
+ i, hl = trunc_headers.first # take first
55
+ next trunc_headers.shift if lens[i] >= hl # drop and nove on if wide enough
56
+ lens[i] += 1 # widen
57
+ trunc_headers.rotate! # move on to next index
58
+ end
59
+
60
+ total_width = lens.sum + hrow.size - 1 # full width table will take
61
+
62
+ row_count = "(#{rows.size} row#{?s if rows.size != 1})"
63
+ title = [title, row_count].join(" ")
64
+ puts "\e[48;5;18m" + title.ljust(total_width) + "\e[49m"
65
+
66
+ lines = table.map do |row|
67
+ line = row.zip(lens, nums).map do |val, len, num|
68
+ just = ->s{ s.send(num ? :rjust : :ljust, len) }
69
+ next c256(244, just[?∅]) if val.nil? # special handling for nil
70
+ next c256(112, just["1"]) if val == 1 # color 1 bool true
71
+ next c256(9, just["0"]) if val == 0 # color 0 bool false
72
+ s ||= val.to_s
73
+ s = s.gsub(/\p{Extended_Pictographic}/, "•").gsub(/\R/, "␍").gsub(/\t/, ' ') # remove emojis, nl, tabs (better chance of not mangling columns)
74
+ s = s[0, len-1] + mark(?›) if s.size > len # ellipsis
75
+ just[s]
76
+ end
77
+ bg[] + line*sep(?│) + clr
78
+ end
79
+ puts lines
80
+ puts "\e[48;5;18m" + row_count.ljust(total_width) + "\e[49m"
81
+ puts
82
+ end
83
+
84
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Simple timer for profiling with lap times and progress bars
4
+ module Timer
5
+ # Initialize timer state
6
+ def self.start
7
+ @t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
8
+ @last_lap = @t0
9
+ @total_time = 0.0
10
+ @laps = []
11
+ end
12
+
13
+ # Record lap time with message
14
+ def self.lap(msg)
15
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
+ lap_time = (now - @last_lap) * 1000
17
+ @total_time += lap_time
18
+ line = "%5.0fms / %5.0fms: #{msg}" % [lap_time, @total_time]
19
+ $stderr.puts line
20
+ @laps << { msg: msg, time: lap_time, line: line, line_num: @laps.size }
21
+ @last_lap = now
22
+ end
23
+
24
+ # Display final timing report with bars and percentages
25
+ def self.finish
26
+ max_lap_time = @laps.map { |lap| lap[:time] }.max
27
+ longest_line = @laps.map { |lap| lap[:line].length }.max
28
+ start_col = longest_line + 3
29
+ @laps.reverse.each do |lap|
30
+ pct = "%4.1f" % (lap[:time] / @total_time * 100)
31
+ bar_length = (lap[:time] / max_lap_time * 20).round
32
+ bar = "█" * bar_length + "░" * (20 - bar_length)
33
+ padding = " " * [0, start_col - lap[:line].length].max
34
+ $stderr.print "\e[#{@laps.size - lap[:line_num]}A\r#{lap[:line]}#{padding}#{bar} #{pct}%\e[#{@laps.size - lap[:line_num]}B\r"
35
+ end
36
+ $stderr.puts
37
+ end
38
+ end
@@ -0,0 +1,135 @@
1
+ require "io/console"
2
+ require "concurrent-ruby"
3
+ require_relative "img2png"
4
+
5
+ module Imaginator
6
+ begin
7
+ require_relative "img2png"
8
+ EXTENSION_AVAILABLE = true
9
+ rescue LoadError
10
+ EXTENSION_AVAILABLE = false
11
+ end
12
+
13
+ extend self
14
+
15
+ class Image
16
+ Fit = Data.define :w, :h, :c, :r, :pad_h, :pad_w
17
+
18
+ def initialize(path) = @path = path
19
+ def dimensions = @img.dimensions
20
+ def release = @img.release
21
+
22
+ def load
23
+ return unless File.exist? @path
24
+ # @img ||= Img2png::Image.new path: @path # do the the IO in swift, turns out not any faster
25
+ @img ||= Img2png::Image.new data: IO.binread(@path)
26
+ self
27
+ end
28
+
29
+ def png_transform w:, h:, pad_h:nil, pad_w:nil
30
+ pad = pad_h || pad_w
31
+ pad_w ||= w if pad
32
+ pad_h ||= h if pad
33
+ @img.convert fit_w:w, fit_h:h, box_w:pad_w, box_h:pad_h
34
+ end
35
+
36
+ def fit_cols target_cols
37
+ w_cell, h_cell = Imaginator.cell_size
38
+ w_img, h_img = dimensions
39
+ w = target_cols * w_cell # target width in px
40
+ h = w / w_img * h_img # target height in px
41
+ r = (h / h_cell).ceil # target rows
42
+ Fit.new w:w.floor, h:h.floor, c:target_cols, r:r, pad_h:(r*h_cell), pad_w:w
43
+ end
44
+
45
+ def fit_rows target_rows
46
+ w_cell, h_cell = Imaginator.cell_size
47
+ w_img, h_img = dimensions
48
+ h = target_rows * h_cell # target height in px
49
+ w = h / h_img * w_img # target width in px
50
+ c = (w / w_cell).ceil # target cols
51
+ Fit.new w:w.floor, h:h.floor, c:c, r:target_rows, pad_w:(c*w_cell), pad_h:h
52
+ end
53
+
54
+ def fit cols, rows
55
+ cfit = fit_cols(cols)
56
+ rfit = fit_rows(rows)
57
+ smol = [cfit, rfit].sort_by { [it.w, it.h] }.first
58
+ # ^ they're proportional so whichever smallest is the one containable in both dimensions
59
+ [smol, cfit:, rfit:]
60
+ end
61
+
62
+ end
63
+
64
+
65
+ def term_seq(seq, end_marker)
66
+ t = ->{ Process.clock_gettime Process::CLOCK_MONOTONIC }
67
+ buf = ""
68
+ IO.console.raw do |tty|
69
+ tty << seq
70
+ tty.flush
71
+ timeout = t.() + 0.1
72
+ loop do
73
+ buf << tty.read_nonblock(1)
74
+ break if buf.end_with? end_marker
75
+ rescue IO::WaitReadable
76
+ break if t.() > timeout
77
+ IO.select([tty], nil, nil, 0.01)
78
+ end
79
+ end
80
+ buf
81
+ end
82
+
83
+ def term_features = @term_features ||= (term_seq("\e]1337;Capabilities\e\\", ?\a) =~ /Capabilities=([A-Za-z0-9]*)/ and $1.scan(/[A-Z][a-z]?\d*/))
84
+ def iterm_images? = @iterm_images ||= term_features&.include?(?F)
85
+ def kitty_images? = @kitty_images ||= term_seq("\e_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\e\\", "\e\\") == "\e_Gi=31;OK\e\\"
86
+
87
+ def cell_size # [cell width px, cell height px] (Floats)
88
+ @cell_size ||= case
89
+ when iterm = term_seq("\e]1337;ReportCellSize\e\\", ?\a)[/ReportCellSize=(.*)\e/, 1]
90
+ iterm.split(?;).map{ Float it }.then{ |h, w, s| s ||= 1; [w*s, h*s] } # multiply by retina scale factor
91
+ when csi16t = term_seq("\e[16t", ?t)[/\e\[6;(\d+;\d+)t/, 1] # ghostty
92
+ csi16t.split(?;).map{ Float it }.reverse
93
+ # else calc from TIOCGWINSZ or CSI14/18t, but this is enough for iterm, ghostty
94
+ end
95
+ end
96
+
97
+ def term_image_protocol
98
+ return @term_image_protocol unless @term_image_protocol.nil?
99
+ @term_image_protocol = case
100
+ when ENV["TERM_PROGRAM"] == "Apple_Terminal" then false # it echoes back the kitty query and I don't wanna figure out how not to
101
+ when iterm_images? then :iterm # has to go before kitty as iterm responds ok to kitty query but can't render them
102
+ when kitty_images? then :kitty # you're a kitty! yes you are! and you're sitting there! hi, kitty!
103
+ else false
104
+ end
105
+ end
106
+
107
+ def image_tooling? = !!(EXTENSION_AVAILABLE && term_image_protocol && cell_size)
108
+
109
+ def iterm_print_image data:, r:nil, c:nil, io:$stdout
110
+ head = "\e]1337;"
111
+ io << head << "MultipartFile=inline=1;preserveAspectRatio=1"
112
+ io << ";width=" << c if c
113
+ io << ";height=" << r if r
114
+ io << ?\a
115
+ data = [data].pack("m0")
116
+ (0...data.size).step(200).each{ io << head << "FilePart=" << data[it, 200] << ?\a }
117
+ io << head << "FileEnd" << ?\a
118
+ end
119
+
120
+ def kitty_print_image data:, r:nil, c:nil, io:$stdout
121
+ io << "\e_Gf=100,a=T,t=d"
122
+ io << ",c=" << c if c
123
+ io << ",r=" << r if r
124
+ io << ?; << [data].pack("m0") << "\e\\"
125
+ end
126
+
127
+ def print_image(...)
128
+ return unless image_tooling?
129
+ case term_image_protocol
130
+ when :iterm then iterm_print_image(...)
131
+ when :kitty then kitty_print_image(...)
132
+ end
133
+ end
134
+
135
+ end
Binary file
@@ -0,0 +1,84 @@
1
+ require "ffi"
2
+
3
+ module Img2png
4
+ extend FFI::Library
5
+ ffi_lib "#{__dir__}/img2png.dylib"
6
+
7
+ attach_function :img2png_load_path, [:string, :pointer, :pointer], :pointer, blocking: true
8
+ attach_function :img2png_load, [:pointer, :int, :pointer, :pointer], :pointer, blocking: true
9
+ attach_function :img2png_convert, [:pointer, :int, :int, :int, :int, :pointer, :pointer], :bool, blocking: true
10
+ attach_function :img2png_release, [:pointer], :void
11
+ attach_function :img2png_free, [:pointer], :void
12
+
13
+ class Image
14
+ def initialize(path:nil, data:nil)
15
+ [path, data].compact.size == 1 or raise ArgumentError, "One of path: or data: must be specified"
16
+ out_w = FFI::MemoryPointer.new(:int)
17
+ out_h = FFI::MemoryPointer.new(:int)
18
+
19
+ @handle = case
20
+ when data then Img2png.img2png_load(data, data.bytesize, out_w, out_h)
21
+ when path then Img2png.img2png_load_path(path, out_w, out_h)
22
+ end
23
+ raise "Failed to load image" if @handle.null?
24
+
25
+ @width = out_w.read_int
26
+ @height = out_h.read_int
27
+ end
28
+
29
+ def dimensions = [@width, @height]
30
+
31
+ def convert(fit_w: nil, fit_h: nil, box_w: nil, box_h: nil)
32
+ out_data = FFI::MemoryPointer.new(:pointer)
33
+ out_len = FFI::MemoryPointer.new(:int)
34
+
35
+ success = Img2png.img2png_convert(
36
+ @handle,
37
+ fit_w || 0, fit_h || 0,
38
+ box_w || 0, box_h || 0,
39
+ out_data, out_len
40
+ )
41
+
42
+ return nil unless success
43
+
44
+ ptr = out_data.read_pointer
45
+ len = out_len.read_int
46
+ result = ptr.read_bytes(len)
47
+ Img2png.img2png_free(ptr)
48
+ result
49
+ end
50
+
51
+ def release
52
+ Img2png.img2png_release(@handle) unless @handle.null?
53
+ @handle = FFI::Pointer::NULL
54
+ end
55
+
56
+ def self.finalize(handle)
57
+ proc { Img2png.img2png_release(handle) unless handle.null? }
58
+ end
59
+ end
60
+ end
61
+
62
+ # # Usage examples
63
+ # input = File.binread("x.heic")
64
+
65
+ # # Load once, use multiple times
66
+ # img = Img2png::Image.new(input)
67
+
68
+ # # Get dimensions
69
+ # p img.dimensions # => [4032, 3024]
70
+
71
+ # # Convert without scaling
72
+ # png = img.convert
73
+ # File.binwrite("output.png", png)
74
+
75
+ # # Fit to 800x600
76
+ # png = img.convert(fit_w: 800, fit_h: 600)
77
+ # File.binwrite("fitted.png", png)
78
+
79
+ # # Fit to 800x600 and box in 1024x768
80
+ # png = img.convert(fit_w: 800, fit_h: 600, box_w: 1024, box_h: 768)
81
+ # File.binwrite("boxed.png", png)
82
+
83
+ # # Manual cleanup (or let GC handle it)
84
+ # img.release