pads 0.1.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be715c4688e1f082e10af7c2385da9247d79f28bbb2eddb72df8b8815dce4731
4
- data.tar.gz: dbc239568a2d963a89fdde625c8c5ba43ff92265f35d209dda9689dca312a5a0
3
+ metadata.gz: ef83cd4cd517c8201f18b8208a6adb8ca42d5dadef3e810c5007822662768b1f
4
+ data.tar.gz: 77a457197b5c98ec963a95b2f62a6b9e1e59a2d69cdfd268112279e280248df5
5
5
  SHA512:
6
- metadata.gz: f6d64adf601c1470c62c2e6fb98f20588a0a4b22bd7c79af1272f7351eef6d9f99844ff92292dc644f8004c983fb48d2c44bba97126d10b4be20a1583f5c92f4
7
- data.tar.gz: 203534cc26d0b207657765665c6444ae44a506b23510e325f5b4f953549ab242f7e216d333df48b86591646cd3c3cb65425db2828d60ce60df01d2b8842366ba
6
+ metadata.gz: 48cd6de1372be26d55b682c4de3ad1119c6855b588190f05ea1c9ce4b34412a15ccc851c760e8e8e388a5dd786f596b93e21bac9975f0c9011bc9d33e166101d
7
+ data.tar.gz: 98939e84ce761264920b1a039ff62dd50fd31ee8a2c497b55467ede72a505f1a7ea7a163d6a181e6b2f005c956e83637c25d8ad890dc792ea42df1150cd72d20
data/.rubocop.yml CHANGED
@@ -1,13 +1,43 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.6
3
+ NewCops: enable
4
+
5
+ Style/Alias:
6
+ EnforcedStyle: prefer_alias
7
+
8
+ Style/BlockDelimiters:
9
+ Exclude:
10
+ - spec/**/*_spec.rb
3
11
 
4
12
  Style/StringLiterals:
5
13
  Enabled: true
6
- EnforcedStyle: double_quotes
14
+ EnforcedStyle: single_quotes
7
15
 
8
16
  Style/StringLiteralsInInterpolation:
9
17
  Enabled: true
10
- EnforcedStyle: double_quotes
18
+ EnforcedStyle: single_quotes
11
19
 
12
20
  Layout/LineLength:
13
21
  Max: 120
22
+
23
+ Layout/HashAlignment:
24
+ EnforcedColonStyle: table
25
+ EnforcedHashRocketStyle: table
26
+
27
+ Lint/AmbiguousBlockAssociation:
28
+ Enabled: false
29
+
30
+ Lint/AmbiguousOperator:
31
+ Enabled: false
32
+
33
+ Metrics/BlockLength:
34
+ IgnoredMethods:
35
+ - describe
36
+ - it
37
+
38
+ Metrics/MethodLength:
39
+ Max: 12
40
+
41
+ Naming/MethodName:
42
+ IgnoredPatterns:
43
+ - !ruby/regexp /^([A-Z][a-z]*)+$/
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.1.0
1
+ 2.6.8
data/Gemfile CHANGED
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source "https://rubygems.org"
3
+ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in pads.gemspec
6
6
  gemspec
7
7
 
8
- gem "rake", "~> 13.0"
8
+ gem 'rake', '~> 13.0'
9
9
 
10
- gem "rspec", "~> 3.0"
10
+ gem 'rspec', '~> 3.0'
11
11
 
12
- gem "rubocop", "~> 1.21"
12
+ gem 'rubocop', '~> 1.21'
13
+ gem 'rubocop-rake'
14
+ gem 'rubocop-rspec'
data/Gemfile.lock CHANGED
@@ -1,13 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pads (0.1.0)
4
+ pads (1.0.0)
5
+ interop (~> 0.3.1)
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
8
9
  specs:
9
10
  ast (2.4.2)
10
11
  diff-lcs (1.5.0)
12
+ interop (0.3.1)
11
13
  parallel (1.21.0)
12
14
  parser (3.1.1.0)
13
15
  ast (~> 2.4.1)
@@ -39,18 +41,26 @@ GEM
39
41
  unicode-display_width (>= 1.4.0, < 3.0)
40
42
  rubocop-ast (1.16.0)
41
43
  parser (>= 3.1.1.0)
44
+ rubocop-rake (0.6.0)
45
+ rubocop (~> 1.0)
46
+ rubocop-rspec (2.9.0)
47
+ rubocop (~> 1.19)
42
48
  ruby-progressbar (1.11.0)
43
49
  unicode-display_width (2.1.0)
44
50
 
45
51
  PLATFORMS
52
+ x86_64-darwin-19
46
53
  x86_64-darwin-20
47
54
  x86_64-darwin-21
55
+ x86_64-linux
48
56
 
49
57
  DEPENDENCIES
50
58
  pads!
51
59
  rake (~> 13.0)
52
60
  rspec (~> 3.0)
53
61
  rubocop (~> 1.21)
62
+ rubocop-rake
63
+ rubocop-rspec
54
64
 
55
65
  BUNDLED WITH
56
66
  2.3.5
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Pads
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/pads`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Provide pads to the Pads macOS app.
6
4
 
7
5
  ## Installation
8
6
 
@@ -22,7 +20,7 @@ Or install it yourself as:
22
20
 
23
21
  ## Usage
24
22
 
25
- TODO: Write usage instructions here
23
+ Check the examples.
26
24
 
27
25
  ## Development
28
26
 
@@ -32,7 +30,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
30
 
33
31
  ## Contributing
34
32
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pads. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/pads/blob/master/CODE_OF_CONDUCT.md).
33
+ Bug reports and pull requests are welcome on GitHub at https://github.com/hx/pads. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/hx/pads/blob/master/CODE_OF_CONDUCT.md).
36
34
 
37
35
  ## License
38
36
 
@@ -40,4 +38,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
40
38
 
41
39
  ## Code of Conduct
42
40
 
43
- Everyone interacting in the Pads project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/pads/blob/master/CODE_OF_CONDUCT.md).
41
+ Everyone interacting in the Pads project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/hx/pads/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
8
+ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'socket'
5
+ require 'json'
6
+
7
+ module Pads
8
+ class Client
9
+ # Creates raw connections based on the macOS app's configuration.
10
+ class LocalResolver
11
+ APP_CONTAINER_SUFFIX = 'Library/Containers/hx.pads/Data'
12
+ LISTENERS_PATH = '.pads/listeners.json'
13
+
14
+ def initialize(home: Dir.home)
15
+ @home = Pathname(home)
16
+ end
17
+
18
+ def connect
19
+ errors = []
20
+ each_config do |type, config|
21
+ conn, error = __send__(:"try_#{type}", config)
22
+ return conn if conn
23
+
24
+ errors << error
25
+ end
26
+ raise errors.compact.first || 'Unable to connect to local server'
27
+ end
28
+
29
+ private
30
+
31
+ def each_config
32
+ %w[unix tcp].each do |type|
33
+ parsed_listeners.each do |listeners|
34
+ listeners[type]&.each do |config|
35
+ yield type, config
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ # @return [Array<Hash>]
42
+ def parsed_listeners
43
+ listeners_path_candidates
44
+ .map do |path|
45
+ next unless path.exist?
46
+
47
+ JSON.parse path.readlines.delete_if { |l| l.start_with? '//' }.join
48
+ end
49
+ .compact
50
+ end
51
+
52
+ # @return [Array<Pathname>]
53
+ def listeners_path_candidates
54
+ [
55
+ @home + LISTENERS_PATH, # Regular app version
56
+ @home + APP_CONTAINER_SUFFIX + LISTENERS_PATH # Sandboxed (app store) app version
57
+ ]
58
+ end
59
+
60
+ def try_unix(config)
61
+ try { UNIXSocket.new Pathname(config.fetch('path')).to_s }
62
+ end
63
+
64
+ def try_tcp(config)
65
+ try { TCPSocket.new config.fetch('host', '127.0.0.1'), config.fetch('port') }
66
+ end
67
+
68
+ def try
69
+ [yield, nil]
70
+ rescue StandardError => e
71
+ [nil, e]
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pads/events/dispatcher'
4
+ require 'pads/mutable_pad_state'
5
+
6
+ module Pads
7
+ class Client
8
+ # Proxy to a Client for a specific pad
9
+ class Pad < MutablePadState
10
+ attr_reader :events, :pad_id
11
+
12
+ def initialize(pad_id, client, &on_destroy)
13
+ @pad_id = pad_id
14
+ @client = client
15
+ @on_destroy = on_destroy
16
+ @events = Events::Dispatcher.new
17
+
18
+ super()
19
+ end
20
+
21
+ def destroy
22
+ call :destroy_pad
23
+ @client = nil
24
+ @on_destroy.call
25
+ @on_destroy = nil
26
+ end
27
+
28
+ def title=(new_title)
29
+ call :set_pad_title, new_title.to_s
30
+ super
31
+ end
32
+
33
+ def subtitle=(new_subtitle)
34
+ call :set_pad_subtitle, new_subtitle.to_s
35
+ super
36
+ end
37
+
38
+ def activity=(fraction)
39
+ call :set_pad_activity, transform_activity(fraction)
40
+ super
41
+ end
42
+
43
+ def clickable=(clickable)
44
+ call :set_pad_clickable, clickable ? true : false
45
+ super
46
+ end
47
+
48
+ def buttons=(buttons)
49
+ call :set_pad_buttons, buttons&.map { |i| { label: i.to_s } } || []
50
+ super
51
+ end
52
+
53
+ def is_drop_target=(is_drop_target)
54
+ call :set_pad_is_drop_target, is_drop_target ? true : false
55
+ super
56
+ end
57
+
58
+ def on_pad_clicked(&handler)
59
+ @events.on :pad_clicked, &handler
60
+ end
61
+
62
+ # @yieldparam [Pads::Events::ButtonClicked]
63
+ def on_button_clicked(&handler)
64
+ @events.on :button_clicked, &handler
65
+ end
66
+
67
+ # @yieldparam [Pads::Events::FilesDropped]
68
+ def on_files_dropped(&handler)
69
+ @events.on :files_dropped, &handler
70
+ end
71
+
72
+ alias == equal?
73
+
74
+ private
75
+
76
+ def call(class_name, arg = nil)
77
+ raise 'Pad was destroyed' unless @client
78
+
79
+ @client.call class_name, Hx::Interop::ContentType::JSON.encode(arg), pad_id: @pad_id
80
+ end
81
+
82
+ def transform_activity(fraction_or_bool)
83
+ case fraction_or_bool
84
+ when true, false, nil
85
+ { busy: fraction_or_bool == true, progress: nil }
86
+ when Numeric
87
+ { busy: false, progress: fraction_or_bool.to_f.clamp(0, 1) }
88
+ else
89
+ raise ArgumentError, 'Expected nil, boolean, or a number'
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'interop'
4
+
5
+ require 'pads/client/pad'
6
+ require 'pads/client/local_resolver'
7
+
8
+ module Pads
9
+ # Low-level client for Pads servers
10
+ class Client
11
+ def initialize(reader = nil, writer = nil)
12
+ reader ||= LocalResolver.new.connect
13
+
14
+ @pads = {}
15
+ @rpc_client = Hx::Interop::RPC::Client.new(reader, writer || reader)
16
+ @rpc_client.on(//) do |message|
17
+ next unless (pad_id = message[:pad_id])
18
+
19
+ @pads[pad_id]&.events&.dispatch(
20
+ message[Hx::Interop::Headers::CLASS],
21
+ message[Hx::Interop::Headers::CONTENT_TYPE] ? message.decode : message.body
22
+ )
23
+ end
24
+ end
25
+
26
+ def create_pad(before: nil, after: nil)
27
+ raise 'You cannot specify before and after' if before && after
28
+
29
+ headers = {
30
+ before_pad_id: before && pad_id(before),
31
+ after_pad_id: after && pad_id(after)
32
+ }.compact
33
+
34
+ pad_id = call(:create_pad, headers)[:pad_id]
35
+ @pads[pad_id] = Pad.new(pad_id, self) { @pads.delete pad_id }
36
+ end
37
+
38
+ def swap_pads(pad_or_range_a, pad_or_range_b)
39
+ call :swap_pads, Hx::Interop::ContentType::JSON.encode(
40
+ range_a: pad_range(*Array(pad_or_range_a)),
41
+ range_b: pad_range(*Array(pad_or_range_b))
42
+ )
43
+ end
44
+
45
+ def wait
46
+ @rpc_client.wait
47
+ end
48
+
49
+ def call(*args, &block)
50
+ @rpc_client.call(*args, &block)
51
+ end
52
+
53
+ private
54
+
55
+ def pad_id(pad_or_id)
56
+ case pad_or_id
57
+ when String
58
+ pad_or_id
59
+ when Pad
60
+ @pads.key(pad_or_id) or
61
+ raise 'The given pad does not belong to this client'
62
+ else
63
+ raise ArgumentError, "Unexpected #{pad_or_id.inspect}"
64
+ end
65
+ end
66
+
67
+ def pad_range(first, last = first)
68
+ {
69
+ first_pad_id: pad_id(first),
70
+ last_pad_id: pad_id(last)
71
+ }
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pads
4
+ module Events
5
+ class Base # rubocop:disable Lint/EmptyClass
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pads/events/base'
4
+
5
+ module Pads
6
+ module Events
7
+ # Occurs when one of a pad's buttons are clicked.
8
+ class ButtonClicked < Base
9
+ # Zero-based index of the button that was clicked.
10
+ # @return Integer
11
+ attr_reader :index
12
+
13
+ # Label of the button that was clicked.
14
+ # @return String
15
+ attr_reader :label
16
+
17
+ def initialize(event_data)
18
+ @index = event_data.fetch('index')
19
+ @label = event_data.fetch('label')
20
+ super()
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pads/events/publisher'
4
+ require 'pads/events/button_clicked'
5
+ require 'pads/events/files_dropped'
6
+
7
+ module Pads
8
+ module Events
9
+ # A central object for dispatch all events related to an event subject.
10
+ class Dispatcher
11
+ DEFAULT_PUBLISHERS = {
12
+ pad_clicked: nil,
13
+ button_clicked: Events::ButtonClicked,
14
+ files_dropped: Events::FilesDropped
15
+ }.freeze
16
+
17
+ attr_reader :publishers
18
+
19
+ def initialize(publishers = DEFAULT_PUBLISHERS)
20
+ @publishers = publishers.to_h do |event_name, event_class|
21
+ [event_name.to_s, Publisher.new(event_class)]
22
+ end.freeze
23
+ end
24
+
25
+ def on(event_name, &handler)
26
+ @publishers.fetch(event_name.to_s).subscribe(&handler)
27
+ end
28
+
29
+ def dispatch(event_name, payload = nil)
30
+ @publishers[event_name.to_s]&.publish payload
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pads/events/base'
4
+
5
+ require 'pathname'
6
+ require 'uri'
7
+
8
+ module Pads
9
+ module Events
10
+ # Occurs when files are dropped on a pad.
11
+ class FilesDropped < Base
12
+ File = Struct.new(:path)
13
+
14
+ # @return Array<File>
15
+ attr_reader :files
16
+
17
+ def initialize(event_data)
18
+ @files = event_data.map do |file|
19
+ File.new Pathname URI.decode_www_form_component URI(file.fetch('url')).path
20
+ end
21
+ super()
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'pads/events/subscription'
5
+
6
+ module Pads
7
+ module Events
8
+ # Manages event subscribers and distributes events to them.
9
+ class Publisher
10
+ def initialize(payload_class = nil)
11
+ @payload_class = payload_class
12
+ @handlers = Set.new.compare_by_identity
13
+ end
14
+
15
+ def subscribe(&block)
16
+ Subscription.new(self, block).tap { |h| @handlers << h }
17
+ end
18
+
19
+ def unsubscribe_all
20
+ @handlers.clear
21
+ self
22
+ end
23
+
24
+ def publish(payload = nil)
25
+ payload = @payload_class.new(payload) if @payload_class && !payload.is_a?(@payload_class)
26
+ @handlers.each do |handler|
27
+ handler.__send__ :call, payload
28
+ end
29
+ payload
30
+ end
31
+
32
+ private
33
+
34
+ def unsubscribe(handler)
35
+ @handlers.delete handler
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Pads
6
+ module Events
7
+ # Represents an event subscription, and can be used to unsubscribe.
8
+ class Subscription
9
+ def initialize(publisher, proc)
10
+ @publisher = publisher
11
+ @proc = proc
12
+ end
13
+
14
+ def unsubscribe
15
+ @publisher.__send__ :unsubscribe, self
16
+ self
17
+ end
18
+
19
+ private
20
+
21
+ def call(*args, &block)
22
+ @proc.call(*args, &block)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'observer'
4
+
5
+ module Pads
6
+ # An array that can be mapped to pads, and will update a pad group when it changes.
7
+ class LiveArray < Array
8
+ include Observable
9
+
10
+ OBSERVED_METHODS = Set.new(
11
+ %i[
12
+ []= << push append prepend pop shift unshift insert
13
+ delete delete_at delete_if keep_if fill clear replace
14
+ ] + instance_methods.grep(/!$/)
15
+ )
16
+
17
+ OBSERVED_METHODS.each do |method|
18
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
+ def #{method}(*) # def push(*)
20
+ original = dup # original = dup
21
+ result = super # result = super
22
+ changed self != original # changed self != original
23
+ notify_observers # notify_observers
24
+ result # result
25
+ end # end
26
+ RUBY
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pads/pad_group'
4
+ require 'pads/live_array'
5
+
6
+ module Pads
7
+ # A Mapper binds an ObservableArray to a PadGroup
8
+ class Mapper
9
+ def self.map(source, id: :object_id, &mapper)
10
+ if source.is_a? LiveArray
11
+ new(source, id: id, &mapper).target
12
+ else
13
+ PadGroup.new Array(source).map(&mapper)
14
+ end
15
+ end
16
+
17
+ # @return [PadGroup]
18
+ attr_reader :target
19
+
20
+ def initialize(source, id: :object_id, &mapper)
21
+ raise ArgumentError, 'Expected a LiveArray' unless source.is_a? LiveArray
22
+
23
+ @id = id.to_proc
24
+ @mapper = mapper
25
+ @source = source
26
+ @target = PadGroup.new
27
+ @actual = []
28
+ source.add_observer self
29
+ update
30
+ end
31
+
32
+ def update
33
+ ids = @source.map(&@id)
34
+ @target.batch do
35
+ remove_old_members ids
36
+ sort_members ids
37
+ add_new_members ids
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def remove_old_members(ids)
44
+ expected = Set.new(ids)
45
+ @actual.each.with_index.reverse_each do |id, index|
46
+ next if expected.include? id
47
+
48
+ @actual.delete_at index
49
+ @target.delete_at index
50
+ end
51
+ end
52
+
53
+ def sort_members(ids)
54
+ return unless @actual.length > 1
55
+
56
+ expected = (ids & @actual)
57
+
58
+ (0..(@actual.length - 2)).each do |i|
59
+ next if expected[i] == @actual[i]
60
+
61
+ j = @actual.index(expected[i])
62
+ @actual[j], @actual[i] = @actual.values_at(i, j)
63
+ @target.swap i, j
64
+ end
65
+ end
66
+
67
+ def add_new_members(ids)
68
+ skip = Set.new(@actual)
69
+ @source.each.with_index do |object, index|
70
+ id = ids[index]
71
+ next if skip.include? id
72
+
73
+ @actual.insert index, id
74
+ @target.insert index, @mapper.call(object)
75
+ end
76
+ end
77
+ end
78
+ end