pads 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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