├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── termux_ruby_api.rb └── termux_ruby_api │ ├── base.rb │ ├── sub_systems.rb │ ├── sub_systems │ ├── base.rb │ ├── call_log.rb │ ├── clipboard.rb │ ├── dialog.rb │ ├── location.rb │ ├── sensor.rb │ ├── sms.rb │ └── tts.rb │ ├── utils.rb │ ├── utils │ └── xformer.rb │ └── version.rb ├── spec ├── spec_helper.rb ├── termux_ruby_api │ ├── base_spec.rb │ └── sub_systems │ │ ├── call_log_spec.rb │ │ ├── clipboard_spec.rb │ │ ├── dialog_spec.rb │ │ ├── location_spec.rb │ │ ├── sensor_spec.rb │ │ ├── sms_spec.rb │ │ └── tts_spec.rb └── termux_ruby_api_spec.rb └── termux_ruby_api.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | cache: bundler 5 | rvm: 6 | - 2.5.1 7 | before_install: gem install bundler -v 1.17.2 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jes@josepegea.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in termux_ruby_api.gemspec 6 | gemspec 7 | 8 | group :development, :test do 9 | gem 'pry' 10 | gem 'pry-byebug' 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | termux_ruby_api (0.1.0) 5 | activesupport (~> 5.2.2) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activesupport (5.2.2) 11 | concurrent-ruby (~> 1.0, >= 1.0.2) 12 | i18n (>= 0.7, < 2) 13 | minitest (~> 5.1) 14 | tzinfo (~> 1.1) 15 | byebug (10.0.2) 16 | coderay (1.1.2) 17 | concurrent-ruby (1.1.4) 18 | diff-lcs (1.3) 19 | i18n (1.5.2) 20 | concurrent-ruby (~> 1.0) 21 | method_source (0.9.2) 22 | minitest (5.11.3) 23 | pry (0.12.2) 24 | coderay (~> 1.1.0) 25 | method_source (~> 0.9.0) 26 | pry-byebug (3.6.0) 27 | byebug (~> 10.0) 28 | pry (~> 0.10) 29 | rake (10.5.0) 30 | rspec (3.8.0) 31 | rspec-core (~> 3.8.0) 32 | rspec-expectations (~> 3.8.0) 33 | rspec-mocks (~> 3.8.0) 34 | rspec-core (3.8.0) 35 | rspec-support (~> 3.8.0) 36 | rspec-expectations (3.8.2) 37 | diff-lcs (>= 1.2.0, < 2.0) 38 | rspec-support (~> 3.8.0) 39 | rspec-mocks (3.8.0) 40 | diff-lcs (>= 1.2.0, < 2.0) 41 | rspec-support (~> 3.8.0) 42 | rspec-support (3.8.0) 43 | thread_safe (0.3.6) 44 | tzinfo (1.2.5) 45 | thread_safe (~> 0.1) 46 | 47 | PLATFORMS 48 | ruby 49 | 50 | DEPENDENCIES 51 | bundler (~> 1.17) 52 | pry 53 | pry-byebug 54 | rake (~> 10.0) 55 | rspec (~> 3.0) 56 | termux_ruby_api! 57 | 58 | BUNDLED WITH 59 | 1.17.2 60 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Josep Egea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TermuxRubyApi 2 | 3 | TermuxRubyApi is a Gem that provides a Ruby interface to a lot of 4 | Android functionality thanks to [Termux](https://termux.com/) and 5 | [Termux API](https://wiki.termux.com/wiki/Termux:API). 6 | 7 | 8 | ```ruby 9 | require 'termux_ruby_api' 10 | 11 | # Connect to the API 12 | t_api = TermuxRubyApi::Base.new 13 | 14 | # Get a list of the latest 50 calls 15 | call_log = t_api.call_log.log(limit: 50) 16 | 17 | # Get the total seconds spent in outgoing calls 18 | total_duration = call_log.select { |c| c[:type] == :OUTGOING }.map {|c| c[:duration] }.sum 19 | 20 | # Get the result in Text To Speak 21 | t_api.tts.speak("The total duration is #{total_duration} seconds") 22 | ``` 23 | 24 | [Termux](https://termux.com/) is a set of Android apps that provide a 25 | complete Debian-like environment for Android, including Ruby, RubyGems 26 | and a whole lot of other packages. 27 | 28 | Part of that package, Termux API provides a way to invoke specific 29 | Android functionality, like sending SMS, reading the call log, 30 | accessing the GPS, initiating a phone call and more. 31 | 32 | Termux API offers this connection by means of shell scripts that 33 | accept command arguments and return data in JSON format. 34 | 35 | TermuxRubyApi provides a convenient access to these scripts from the 36 | confort of a set of Ruby classes that encapsulate all their 37 | functionality and that allows the user to apply all the power and 38 | flexibility of Ruby to access and manipulate this data, interacting 39 | with the Android host. 40 | 41 | ## Installation 42 | 43 | Add this line to your application's Gemfile: 44 | 45 | ```ruby 46 | gem 'termux_ruby_api' 47 | ``` 48 | 49 | And then execute: 50 | 51 | $ bundle 52 | 53 | Or install it yourself as: 54 | 55 | $ gem install termux_ruby_api 56 | 57 | ## Usage 58 | 59 | TODO: Write usage instructions here 60 | 61 | ## Development 62 | 63 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 64 | 65 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 66 | 67 | ## Contributing 68 | 69 | Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/termux_ruby_api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 70 | 71 | ## License 72 | 73 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 74 | 75 | ## Code of Conduct 76 | 77 | Everyone interacting in the TermuxRubyApi project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/termux_ruby_api/blob/master/CODE_OF_CONDUCT.md). 78 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "termux_ruby_api" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/termux_ruby_api.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/all' 2 | require "termux_ruby_api/version" 3 | require "termux_ruby_api/base" 4 | require "termux_ruby_api/sub_systems" 5 | require "termux_ruby_api/utils" 6 | 7 | module TermuxRubyApi 8 | class Error < StandardError; end 9 | end 10 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/base.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'date' 3 | require 'time' 4 | require_relative 'sub_systems' 5 | require 'shellwords' 6 | require 'open3' 7 | 8 | require 'pry' 9 | 10 | module TermuxRubyApi 11 | 12 | class CommandError < StandardError 13 | attr_accessor :status, :stderr 14 | 15 | def initialize(status: nil, stderr: nil) 16 | @status = status 17 | @stderr = stderr 18 | end 19 | end 20 | 21 | class Base 22 | 23 | # Returns the `clipboard` subsystem 24 | # @return [TermuxRubyApi::SubSystems::Clipboard] 25 | def clipboard 26 | @clipboard ||= TermuxRubyApi::SubSystems::Clipboard.new(self) 27 | end 28 | 29 | # Returns the `tts` subsystem 30 | # @return [TermuxRubyApi::SubSystems::Tts] 31 | def tts 32 | @tts ||= TermuxRubyApi::SubSystems::Tts.new(self) 33 | end 34 | 35 | # Returns the `call_log` subsystem 36 | # @return [TermuxRubyApi::SubSystems::CallLog] 37 | def call_log 38 | @call_log ||= TermuxRubyApi::SubSystems::CallLog.new(self) 39 | end 40 | 41 | # Returns the `location` subsystem 42 | # @return [TermuxRubyApi::SubSystems::Location] 43 | def location 44 | @location ||= TermuxRubyApi::SubSystems::Location.new(self) 45 | end 46 | 47 | # Returns the `sms` subsystem 48 | # @return [TermuxRubyApi::SubSystems::Sms] 49 | def sms 50 | @sms ||= TermuxRubyApi::SubSystems::Sms.new(self) 51 | end 52 | 53 | # Returns the `sensor` subsystem 54 | # @return [TermuxRubyApi::SubSystems::Sensor] 55 | def sensor 56 | @sensor ||= TermuxRubyApi::SubSystems::Sensor.new(self) 57 | end 58 | 59 | # Returns the `dialog` subsystem 60 | # @return [TermuxRubyApi::SubSystems::Dialog] 61 | def dialog 62 | @dialog ||= TermuxRubyApi::SubSystems::Dialog.new(self) 63 | end 64 | 65 | # General executor of Termux API commands 66 | # @param command [String] the termux command to execute (except the `termux-` prefix. 67 | # @param stdin [String, nil] If not nil, the contents of `stdin` are passed as STDIN to the command 68 | # @param args The rest of the arguments are passed verbatim to the executed command. 69 | # @return [String] the STDOUT of the executed command 70 | def api_command(command, stdin = nil, *args) 71 | command, args = prepare_command_args(command, args) 72 | stdout, stderr, status = Open3.capture3(command, *args, stdin_data: stdin.to_s) 73 | raise CommandError.new(status: status, stderr: stderr) unless status.success? 74 | stdout 75 | end 76 | 77 | # Executes a Termux API command returning the results parsed as JSON 78 | # @param (see #api_command) 79 | # @return native Ruby classes parsed from JSON 80 | # @note JSON objects are converted to ActiveSupport::HashWithIndifferentAccess 81 | def json_api_command(command, stdin = nil, *args) 82 | res = api_command(command, stdin, *args) 83 | return res if res.nil? || res == '' 84 | JSON.parse(res, symbolize_names: true, object_class: ActiveSupport::HashWithIndifferentAccess) 85 | end 86 | 87 | # Executes a Termux API command and streams its results, line by line to the provided block 88 | # @param (see #api_command) 89 | # If a block is given, each new line output by the executed command is yielded to the block 90 | # If a block is not given, it returns the output stream and the wait thread. 91 | # @return [nil, [Io,Thread]] 92 | def streamed_api_command(command, stdin = nil, *args, &block) 93 | command, args = prepare_command_args(command, args) 94 | i, o, t = Open3.popen2(command, *args) 95 | i.puts(stdin) unless stdin.blank? # If we have any input, send it to the child 96 | i.close # Afterwards we can close child's stdin 97 | if block_given? 98 | o.each_line do |line| 99 | yield line 100 | end 101 | o.close 102 | raise "#{command} failed" unless t.value.success? 103 | else 104 | return o, t # The caller has to close o and wait for t 105 | end 106 | end 107 | 108 | # Executes the Termux API command and streams its results as parsed JSON, as soon as JSON structures are complete. 109 | # @param (see #streamed_api_command) 110 | # Only works if a block is given. 111 | # @return [nil] 112 | def json_streamed_api_command(command, stdin = nil, *args, &block) 113 | partial_out = '' 114 | streamed_api_command(command, stdin, *args) do |line| 115 | partial_out << line 116 | begin 117 | parsed_json = JSON.parse(partial_out, symbolize_names: true, object_class: ActiveSupport::HashWithIndifferentAccess) 118 | partial_out = '' 119 | yield parsed_json 120 | rescue 121 | end 122 | end 123 | end 124 | 125 | # Utility method to generate args lists to be passed to `api_command` and the like 126 | # @param [Array ] 127 | # @return [Array ] 128 | # The array is expected to contain Arrays as elements. Each of them is passed to #generate_args. 129 | # Returns a flattened array with the resulting arguments. 130 | # @example 131 | # generate_args_list([['-l', 3], ['-i'], ['-t', nil], ['My text']]) #=> ['-l', 3, '-i', 'My text'] 132 | def generate_args_list(args_list) 133 | args = args_list.map { |args| generate_args(args) }.flatten.compact 134 | end 135 | 136 | # Utility method to generate the arguments for a single command option 137 | # @param args [Array] 138 | # @return [Array, nil] 139 | # If the last argument is blank, the whole list is ignored and returns nil 140 | # @example Single element 141 | # generate_args(['-h']) #=> ['-h'] 142 | # @example Two non-nil elements 143 | # generate_args(['-t', "My title"]) #=> ['-t', "My title"] 144 | # @example Two elements, with the last being nil 145 | # generate_args(['-t', nil]) #=> nil 146 | def generate_args(args) 147 | args.empty? || args.last.blank? ? nil : args 148 | end 149 | 150 | protected 151 | 152 | def prepare_command_args(command, args) 153 | command = "termux-" + Shellwords.escape(command.to_s) 154 | args = args.map { |arg| arg.to_s } 155 | return command, args 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/sub_systems.rb: -------------------------------------------------------------------------------- 1 | require_relative 'sub_systems/base' 2 | require_relative 'sub_systems/call_log' 3 | require_relative 'sub_systems/clipboard' 4 | require_relative 'sub_systems/location' 5 | require_relative 'sub_systems/sms' 6 | require_relative 'sub_systems/tts' 7 | require_relative 'sub_systems/sensor' 8 | require_relative 'sub_systems/dialog' 9 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/sub_systems/base.rb: -------------------------------------------------------------------------------- 1 | module TermuxRubyApi 2 | module SubSystems 3 | class Base 4 | attr_accessor :owner 5 | 6 | def initialize(owner) 7 | @owner = owner 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/sub_systems/call_log.rb: -------------------------------------------------------------------------------- 1 | module TermuxRubyApi 2 | module SubSystems 3 | class CallLog < Base 4 | # Gets part of the list of phone calls in the phone 5 | # @param limit [Fixnum] Number of messages to return 6 | # @param offset [Fixnum] Start from message 7 | # @return [Array ] 8 | def log(limit:nil, offset:nil) 9 | args = owner.generate_args_list([['-l', limit&.to_s], 10 | ['-o', offset&.to_s] 11 | ]) 12 | res = owner.json_api_command('call-log', nil, *args) 13 | TermuxRubyApi::Utils::Xformer.xform(res, date: :time, duration: :duration, type: :symbol) 14 | end 15 | 16 | # Gets the whole list of phone calls in the phone, with no pagination 17 | # @return (see #log) 18 | def log_all 19 | log(limit: -1) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/sub_systems/clipboard.rb: -------------------------------------------------------------------------------- 1 | module TermuxRubyApi 2 | module SubSystems 3 | class Clipboard < TermuxRubyApi::SubSystems::Base 4 | # Stores `value` in the Android system clipboard 5 | # @param value [String] 6 | def set(value) 7 | owner.api_command('clipboard-set', value) 8 | end 9 | 10 | # Gets the contents of the Android system clipboard 11 | # @return value [String] 12 | def get 13 | owner.api_command('clipboard-get') 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/sub_systems/dialog.rb: -------------------------------------------------------------------------------- 1 | module TermuxRubyApi 2 | module SubSystems 3 | class Dialog < TermuxRubyApi::SubSystems::Base 4 | # Shows a dialog asking for a simple text message 5 | # @param title [String] the title of the dialog 6 | # @param hint [String] aclaratory text 7 | # @return [String] the text entered by the user 8 | def text(title = nil, hint: nil) 9 | args = owner.generate_args_list([['text'], 10 | ['-t', title], 11 | ['-i', hint] 12 | ]) 13 | single_result(args, :text) 14 | end 15 | 16 | # Shows a dialog asking for a Yes/No confirmation 17 | # @param title [String] the title of the dialog 18 | # @param hint [String] aclaratory text 19 | # @param result_type [:boolean, :text, raw] How you want the result 20 | # :boolean #=> true for "Yes" false for "No" 21 | # :text #=> "Yes" or "No" 22 | # :raw #=> A parsed JSON object with the result from Termux API 23 | # @return Depends on @result_type 24 | def confirm(title = nil, hint: nil, result_type: :boolean) 25 | args = owner.generate_args_list([['confirm'], 26 | ['-t', title], 27 | ['-i', hint] 28 | ]) 29 | single_result(args, result_type) 30 | end 31 | 32 | # Shows a dialog asking for a multiple choice question, with checkboxes 33 | # @param title [String] the title of the dialog 34 | # @param options [Array ] the options the user has to select from 35 | # @param result_type [:index, :text, raw] How you want the result 36 | # :index #=> The index (0 based) of the selected options 37 | # :text #=> The texts for the selected options 38 | # :raw #=> A parsed JSON object with the result from Termux API 39 | # @return [Array] 40 | def checkbox(title = nil, result_type: :index, options:) 41 | args = owner.generate_args_list([['checkbox'], 42 | ['-t', title], 43 | ['-v', options.join(',')] 44 | ]) 45 | res = owner.json_api_command('dialog', nil, *args) 46 | return res if result_type == :raw 47 | res[:values]&.map { |r| extract_result(r, result_type) } 48 | end 49 | 50 | # Shows a dialog asking for a multiple choice question, with radio buttons 51 | # @param title [String] the title of the dialog 52 | # @param options [Array ] the options the user has to select from 53 | # @param result_type [:index, :text, raw] How you want the result 54 | # :index #=> The index (0 based) of the selected option 55 | # :text #=> The text for the selected option 56 | # :raw #=> A parsed JSON object with the result from Termux API 57 | # @return Depends on the result_type 58 | def radio(title = nil, result_type: :index, options:) 59 | args = owner.generate_args_list([[__callee__.to_s], 60 | ['-t', title], 61 | ['-v', options.join(',')] 62 | ]) 63 | single_result(args, result_type) 64 | end 65 | 66 | # Shows a dialog asking for a multiple choice question, with a spinner 67 | # @param (see #radio) 68 | # @return (see #radio) 69 | alias :spinner :radio 70 | 71 | # Shows a dialog asking for a multiple choice question, with an Android sheet 72 | # @param (see #radio) 73 | # @return (see #radio) 74 | alias :sheet :radio 75 | 76 | # Shows a dialog asking for a calendar date 77 | # @param title [String] the title of the dialog 78 | # @param result_type [:date, :text, raw] How you want the result 79 | # :date #=> A Ruby Date class instance 80 | # :text #=> The String for the selected date 81 | # :raw #=> A parsed JSON object with the result from Termux API 82 | # @return Depends on @result_type 83 | def date(title = nil, result_type: :date) 84 | args = owner.generate_args_list([['date'], 85 | ['-t', title] 86 | ]) 87 | single_result(args, result_type) 88 | end 89 | 90 | # Shows a dialog asking for spoken input, to be converted to text 91 | # @param title [String] the title of the dialog 92 | # @param hint [String] aclaratory text 93 | # @param result_type [:boolean, :text, raw] How you want the result 94 | # :text #=> The recognized text 95 | # :raw #=> A parsed JSON object with the result from Termux API 96 | # @return Depends on @result_type 97 | def speech(title = nil, hint: nil, result_type: :text) 98 | args = owner.generate_args_list([['speech'], 99 | ['-t', title], 100 | ['-i', hint] 101 | ]) 102 | single_result(args, result_type) 103 | end 104 | 105 | private 106 | 107 | def single_result(args, result_type = :raw) 108 | res = owner.json_api_command('dialog', nil, *args) 109 | extract_result(res, result_type) 110 | end 111 | 112 | def extract_result(res, result_type = :text) 113 | return case result_type 114 | when :raw 115 | res 116 | when :boolean 117 | res.present? && res[:text].present? && res[:text].downcase == 'yes' 118 | when :date 119 | res && res[:text].presence && Date.parse(res[:text]) 120 | else 121 | res && res[result_type] 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/sub_systems/location.rb: -------------------------------------------------------------------------------- 1 | module TermuxRubyApi 2 | module SubSystems 3 | class Location < Base 4 | # Returns the current location 5 | # @param provider [:gps, :network, :passive] 6 | # :gps #=> Use GPS 7 | # :network #=> Use mobile or Wifi network 8 | # :passive #=> Use a low battery method 9 | # @param request [:once, :last, :updates] 10 | # :once #=> Return after the location is obtained 11 | # :last #=> Return the last obtained location 12 | # :updates #=> Send repeated updates as location is updated. Doesn't work in current version. 13 | # @return [Hash] 14 | def get(provider: nil, request: nil) 15 | args = owner.generate_args_list([['-p', provider], 16 | ['-r', request] 17 | ]) 18 | res = owner.json_api_command('location', nil, *args) 19 | TermuxRubyApi::Utils::Xformer.xform(res, provider: :symbol) 20 | end 21 | 22 | # Returns the current location, using GPS 23 | # @param request (see #get) 24 | # @return (see #get) 25 | def gps(request: nil) 26 | get(provider: :gps, request: request) 27 | end 28 | 29 | # Returns the current location, using Mobile/Wifi network 30 | # @param request (see #get) 31 | # @return (see #get) 32 | def network(request: nil) 33 | get(provider: :network, request: request) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/sub_systems/sensor.rb: -------------------------------------------------------------------------------- 1 | module TermuxRubyApi 2 | module SubSystems 3 | class Sensor < Base 4 | # Produces continues updates of the selected sensors 5 | # If a block is passed, the results are yielded to the block as soon as they're ready 6 | # in the form of an array of hashes, where each element of the array corresponds to one 7 | # of the captured sensors. 8 | # If no block is passed, the IO object for the output string and the wait Thread 9 | # are returned. See {TermuxRubyApi::Base#json_streamed_api_command} for details. 10 | # @param delay [Fixnum] time in milliseconds to wait between updates 11 | # @param limit [Fixnum] number of updates. If nil, it sends updates forever 12 | # @param sensors [Array ] names of sensors to capture (as returned from #list) 13 | # @note If limit==nil this method never returns. 14 | # In that case is normal to invoke it from inside a thread or 15 | # a child process. 16 | def capture(delay: 1000, limit: nil, sensors:, &block) 17 | args = [] 18 | args += ['-n', limit.to_s] unless limit.nil? 19 | args += ['-d', delay.to_s] unless delay.nil? 20 | args += ['-s', sensors.join(',')] 21 | if block_given? 22 | owner.json_streamed_api_command('sensor', nil, *args) do |json_result| 23 | yield json_result 24 | end 25 | else 26 | return owner.json_streamed_api_command('sensor', nil, *args) 27 | end 28 | end 29 | 30 | # Gets a single update of the selected sensors 31 | # If a block is passed, the results are yielded to the block as soon as they're ready 32 | # in the form of an array of hashes, where each element of the array corresponds to one 33 | # of the captured sensors. 34 | # If no block is passed, the array of hashes is returned. 35 | # @param sensors [String] names of sensors to capture (as returned from #list) 36 | def capture_once(*sensors, &block) 37 | data = nil 38 | capture(limit: 1, sensors: sensors) do |json_result| 39 | data = json_result 40 | end 41 | if block_given? 42 | yield data 43 | else 44 | return data 45 | end 46 | end 47 | 48 | # Returns the list of available sensors 49 | # @return [String] 50 | def list 51 | owner.json_api_command('sensor', nil, '-l')&.fetch(:sensors) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/sub_systems/sms.rb: -------------------------------------------------------------------------------- 1 | module TermuxRubyApi 2 | module SubSystems 3 | class Sms < TermuxRubyApi::SubSystems::Base 4 | # Gets part of the list of SMS messages in the phone 5 | # @param limit [Fixnum] Number of messages to return 6 | # @param offset [Fixnum] Start from message 7 | # @param type [:inbox, :outbox, :sent] 8 | # @return [Array ] 9 | def list(limit: nil, offset: nil, type: nil) 10 | args = [] 11 | args = owner.generate_args_list([['-l', limit&.to_s], 12 | ['-o', offset&.to_s], 13 | ['-t', type] 14 | ]) 15 | res = owner.json_api_command('sms-list', nil, *args) 16 | TermuxRubyApi::Utils::Xformer.xform(res, received: :time, type: :symbol) 17 | end 18 | 19 | # Lists all the SMS messages in the phone, with no pagination 20 | # @param type [:inbox, :outbox, :sent] 21 | # @return (see #list) 22 | def list_all(type: nil) 23 | list(limit: -1, type: type) 24 | end 25 | 26 | # Lists the SMS messages in the inbox folder of the phone 27 | # @param limit [Fixnum] Number of messages to return 28 | # @param offset [Fixnum] Start from message 29 | # @return (see #list) 30 | def inbox(limit: nil, offset: nil) 31 | list(limit: limit, offset: offset, type: :inbox) 32 | end 33 | 34 | # Lists all the SMS messages in the inbox folder of the phone, with no pagination 35 | # @return (see #list) 36 | def inbox_all 37 | list_all(type: :inbox) 38 | end 39 | 40 | # Lists the SMS messages in the outbox folder of the phone 41 | # @param limit [Fixnum] Number of messages to return 42 | # @param offset [Fixnum] Start from message 43 | # @return (see #list) 44 | def outbox(limit: nil, offset: nil) 45 | list(limit: limit, offset: offset, type: :outbox) 46 | end 47 | 48 | # Lists all the SMS messages in the outbox folder of the phone, with no pagination 49 | # @return (see #list) 50 | def outbox_all 51 | list_all(type: :outbox) 52 | end 53 | 54 | # Lists the SMS messages in the sent folder of the phone 55 | # @param limit [Fixnum] Number of messages to return 56 | # @param offset [Fixnum] Start from message 57 | # @return (see #list) 58 | def sent(limit: nil, offset: nil) 59 | list(limit: limit, offset: offset, type: :sent) 60 | end 61 | 62 | # Lists all the SMS messages in the sent folder of the phone, with no pagination 63 | # @return (see #list) 64 | def sent_all 65 | list_all(type: :sent) 66 | end 67 | 68 | # Lists the SMS messages in the draft folder of the phone 69 | # @param limit [Fixnum] Number of messages to return 70 | # @param offset [Fixnum] Start from message 71 | # @return (see #list) 72 | def draft(limit: nil, offset: nil) 73 | list(limit: limit, offset: offset, type: :draft) 74 | end 75 | 76 | # Lists all the SMS messages in the draft folder of the phone, with no pagination 77 | # @return (see #list) 78 | def draft_all 79 | list_all(type: :draft) 80 | end 81 | 82 | # Sends an SMS message 83 | # @param msg [String] the text of the message 84 | # @param numbers [String] all subsequent params are interpreted as numbers to send the message to 85 | def send(msg, *numbers) 86 | args = owner.generate_args(["-n", "#{numbers.join(',')}"]) 87 | owner.api_command('sms-send', msg, *args) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/sub_systems/tts.rb: -------------------------------------------------------------------------------- 1 | module TermuxRubyApi 2 | module SubSystems 3 | class Tts < TermuxRubyApi::SubSystems::Base 4 | # Speaks a text through the TTS system 5 | # @param text [String] the text to be spoken 6 | # @param engine [String] the engine to use (see #engines) 7 | # @param language [String] the string code for the language to use 8 | # @param region [String] the string code for the regional variaion of the selected language 9 | # @param variant [String] the voice of the selected language 10 | # @param pitch [Fixnum] the desired pitch: 1 neutral. <1 more grave. >1 more acute 11 | # @param rate [Fixnum] the desired speak rate. 1 neutral. <1 slower. >1 faster 12 | # @param stream [String] Android audio stream to use: One of ALARM, MUSIC, NOTIFICATION, RING, SYSTEM, VOICE_CALL 13 | 14 | def speak(text, engine: nil, language: nil, region: nil, variant: nil, pitch: nil, rate: nil, stream: nil) 15 | args = owner.generate_args_list([['-e', engine&.to_s], 16 | ['-l', language&.to_s], 17 | ['-n', region&.to_s], 18 | ['-v', variant&.to_s], 19 | ['-p', pitch&.to_s], 20 | ['-r', rate&.to_s], 21 | ['-s', stream&.to_s] 22 | ]) 23 | owner.api_command('tts-speak', text, *args) 24 | end 25 | 26 | # Returns the list of available engines 27 | # @return [Array ] 28 | def engines 29 | owner.json_api_command('tts-engines') 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/utils.rb: -------------------------------------------------------------------------------- 1 | require_relative 'utils/xformer' 2 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/utils/xformer.rb: -------------------------------------------------------------------------------- 1 | module TermuxRubyApi 2 | module Utils 3 | class Xformer 4 | def self.xform(object, args) 5 | collection = object.is_a?(Array) ? object : [object] 6 | collection.each { |i| xform_item(i, args) } 7 | object 8 | end 9 | 10 | private 11 | 12 | def self.xform_item(item, args) 13 | return unless item.is_a?(Hash) 14 | args.each do |arg, type| 15 | next unless item.key?(arg) 16 | item[arg] = xform_prop(item[arg], type) 17 | end 18 | end 19 | 20 | def self.xform_prop(value, type) 21 | case type 22 | when :date 23 | Date.parse(value) 24 | when :time 25 | Time.parse(fix_time(value)) 26 | when :duration 27 | to_duration(value) 28 | when :integer 29 | value.to_i 30 | when :float 31 | value.to_f 32 | when :symbol 33 | value.to_sym 34 | else 35 | value 36 | end 37 | end 38 | 39 | def self.to_duration(value) 40 | parts = value.split(':') 41 | res = parts.pop.to_i 42 | res += (parts.pop.to_i * 60) if parts.any? 43 | res += (parts.pop.to_i * 60 * 60) if parts.any? 44 | res 45 | end 46 | 47 | def self.fix_time(time_str) 48 | # Termux likes to give hours in 24:xx:xx or 24:xx format instead of 00:xx:xx or 00:xx 49 | time_str.gsub(/\ (24)(:\d\d)/, ' 00' + '\2') 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/termux_ruby_api/version.rb: -------------------------------------------------------------------------------- 1 | module TermuxRubyApi 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "termux_ruby_api" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/termux_ruby_api/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'termux_ruby_api/base' 2 | require 'ostruct' 3 | 4 | RSpec.describe TermuxRubyApi::Base do 5 | 6 | let (:success) { OpenStruct.new(success?: true) } 7 | let (:failure) { OpenStruct.new(success?: false) } 8 | 9 | describe "parsing arguments and standard input" do 10 | it "works with a basic command (successful)" do 11 | expect(Open3).to receive(:capture3).with('termux-test', stdin_data: '').once.and_return(['', '', success]) 12 | subject.api_command('test') 13 | end 14 | 15 | it "works with a basic command (unsuccessful)" do 16 | expect(Open3).to receive(:capture3).with('termux-test', stdin_data: '').once.and_return(['', '', failure]) 17 | expect { subject.api_command('test') }.to raise_error(TermuxRubyApi::CommandError) 18 | end 19 | 20 | it "works with a command with args (successful)" do 21 | expect(Open3).to receive(:capture3).with('termux-test', 'x', 'y', stdin_data: '').once.and_return(['', '', success]) 22 | subject.api_command('test', nil, 'x', 'y') 23 | end 24 | 25 | it "works with a command with args (unsuccessful)" do 26 | expect(Open3).to receive(:capture3).with('termux-test', 'x', 'y', stdin_data: '').once.and_return(['', '', failure]) 27 | expect { subject.api_command('test', nil, 'x', 'y') }.to raise_error(TermuxRubyApi::CommandError) 28 | end 29 | 30 | it "works with a command with standard input (successful)" do 31 | expect(Open3).to receive(:capture3).with('termux-test', stdin_data: 'input data').once.and_return(['', '', success]) 32 | subject.api_command('test', 'input data') 33 | end 34 | 35 | it "works with a command with standard input (unsuccessful)" do 36 | expect(Open3).to receive(:capture3).with('termux-test', stdin_data: 'input data').once.and_return(['', '', failure]) 37 | expect { subject.api_command('test', 'input data') }.to raise_error(TermuxRubyApi::CommandError) 38 | end 39 | 40 | it "works with a command with standard input and args (successful)" do 41 | expect(Open3).to receive(:capture3) 42 | .with('termux-test', 'x', 'y', stdin_data: 'input data') 43 | .once 44 | .and_return(['', '', success]) 45 | subject.api_command('test', 'input data', 'x', 'y') 46 | end 47 | 48 | it "works with a command with standard input and args (unsuccessful)" do 49 | expect(Open3).to receive(:capture3) 50 | .with('termux-test', 'x', 'y', stdin_data: 'input data') 51 | .once 52 | .and_return(['', '', failure]) 53 | expect { subject.api_command('test', 'input data', 'x', 'y') }.to raise_error(TermuxRubyApi::CommandError) 54 | end 55 | end 56 | 57 | describe "escaping arguments" do 58 | it "escapes special chars in command name but not in args" do 59 | expect(Open3).to receive(:capture3) 60 | .with("termux-test\\&\\&rm\\ -rf", 61 | '"Hello there" said O\'Donnell', 62 | stdin_data: '') 63 | .once 64 | .and_return(['', '', success]) 65 | subject.api_command('test&&rm -rf', nil, '"Hello there" said O\'Donnell') 66 | end 67 | end 68 | 69 | describe "working with JSON" do 70 | let (:json_result) { '{ "code": 1, "name": "string", "values": [1,2,3] }' } 71 | 72 | it "works with a basic command" do 73 | expect(Open3).to receive(:capture3).with('termux-test', stdin_data: '').once.and_return([json_result, '', success]) 74 | expect(subject.json_api_command('test')).to eq({ code: 1, name: 'string', values: [1,2,3] }.with_indifferent_access) 75 | end 76 | 77 | it "works with a command with args" do 78 | expect(Open3).to receive(:capture3).with('termux-test', 'x', 'y', stdin_data: '').once.and_return([json_result, '', success]) 79 | expect(subject.json_api_command('test', nil, 'x', 'y')).to eq({ code: 1, name: 'string', values: [1,2,3] }.with_indifferent_access) 80 | end 81 | 82 | it "works with a command with standard input" do 83 | expect(Open3).to receive(:capture3).with('termux-test', stdin_data: 'input data').once.and_return([json_result, '', success]) 84 | expect(subject.json_api_command('test', 'input data')).to eq({ code: 1, name: 'string', values: [1,2,3] }.with_indifferent_access) 85 | end 86 | 87 | it "works with a command with standard input and args" do 88 | expect(Open3).to receive(:capture3).with('termux-test', 'x', 'y', stdin_data: 'input data').once.and_return([json_result, '', success]) 89 | expect(subject.json_api_command('test', 'input data', 'x', 'y')).to eq({ code: 1, name: 'string', values: [1,2,3] }.with_indifferent_access) 90 | end 91 | end 92 | 93 | describe "streamed command" do 94 | it "passes the streamed results to a block with no stdin data" do 95 | i = double("stdin") 96 | o = double("stdout") 97 | t = double("wait_thread") 98 | tv = double("thread_value") 99 | expect(i).to receive(:close).once 100 | expect(o).to receive(:each_line).once.and_yield("line 1").and_yield("line 2") 101 | expect(o).to receive(:close).once 102 | expect(t).to receive(:value).once.and_return(tv) 103 | expect(tv).to receive(:success?).once.and_return(true) 104 | expect(Open3).to receive(:popen2).with('termux-test', 'x', 'y').once.and_return([i, o, t]) 105 | expect { |b| subject.streamed_api_command('test', nil, 'x', 'y', &b) }.to yield_successive_args("line 1", "line 2") 106 | end 107 | 108 | it "passes the streamed results to a block with stdin data" do 109 | i = double("stdin") 110 | o = double("stdout") 111 | t = double("wait_thread") 112 | tv = double("thread_value") 113 | expect(i).to receive(:puts).once.with("Input data") 114 | expect(i).to receive(:close).once 115 | expect(o).to receive(:each_line).once.and_yield("line 1").and_yield("line 2") 116 | expect(o).to receive(:close).once 117 | expect(t).to receive(:value).once.and_return(tv) 118 | expect(tv).to receive(:success?).once.and_return(true) 119 | expect(Open3).to receive(:popen2).with('termux-test', 'x', 'y').once.and_return([i, o, t]) 120 | expect { |b| subject.streamed_api_command('test', "Input data", 'x', 'y', &b) }.to yield_successive_args("line 1", "line 2") 121 | end 122 | end 123 | 124 | describe "JSON streamed command" do 125 | 126 | JSON_PAYLOAD = <<~JSON 127 | { 128 | "a": 1, 129 | "b": [ 130 | 2, 131 | 3 132 | ], 133 | "c": { 134 | "d": "string" 135 | } 136 | } 137 | { 138 | "x": [ 139 | { 140 | "a": 3, 141 | "b": [ 142 | 1, 143 | 2 144 | ] 145 | }, 146 | { 147 | "a": null, 148 | "b": "nothing" 149 | } 150 | ], 151 | "y": 23, 152 | "z": { 153 | "key": "value" 154 | } 155 | } 156 | JSON 157 | it "passes the streamed JSON to a block" do 158 | i = double("stdin") 159 | o = double("stdout") 160 | t = double("wait_thread") 161 | tv = double("thread_value") 162 | expect(i).to receive(:close).once 163 | out_double = expect(o).to receive(:each_line).once 164 | JSON_PAYLOAD.each_line do |l| 165 | out_double.and_yield(l) 166 | end 167 | expect(o).to receive(:close).once 168 | expect(t).to receive(:value).once.and_return(tv) 169 | expect(tv).to receive(:success?).once.and_return(true) 170 | expect(Open3).to receive(:popen2).with('termux-test', 'x', 'y').once.and_return([i, o, t]) 171 | expect { |b| subject.json_streamed_api_command('test', nil, 'x', 'y', &b) } 172 | .to yield_successive_args( 173 | { 174 | a: 1, 175 | b: [2, 3], 176 | c: { d: "string" } 177 | }, 178 | { 179 | x: [ 180 | { a: 3, 181 | b: [1, 2] 182 | }, 183 | { a: nil, 184 | b: "nothing" 185 | } 186 | ], 187 | y: 23, 188 | z: { key: "value" } 189 | } 190 | ) 191 | end 192 | end 193 | end 194 | 195 | -------------------------------------------------------------------------------- /spec/termux_ruby_api/sub_systems/call_log_spec.rb: -------------------------------------------------------------------------------- 1 | require 'termux_ruby_api' 2 | 3 | RSpec.describe TermuxRubyApi::SubSystems::CallLog do 4 | 5 | let (:result) { < { 10 | values: [23456] 11 | }, 12 | :"Fake Sensor" => { 13 | field1: 3, 14 | field2: "string", 15 | field3: [1, 2, 3] 16 | } 17 | } 18 | } 19 | 20 | before do 21 | @base = TermuxRubyApi::Base.new 22 | end 23 | 24 | it "gets the sensor list" do 25 | expect(@base).to receive(:api_command).with('sensor', nil, '-l') 26 | .once 27 | .and_return(sensor_list) 28 | expect(@base.sensor.list).to match_array(["Step Counter", "Fake Sensor"]) 29 | end 30 | 31 | it "gets data once, without a block" do 32 | expect(@base).to receive(:json_streamed_api_command) 33 | .with('sensor', nil, '-n', '1', '-d', '1000', '-s', 34 | "Step Counter,Fake Sensor") 35 | .once 36 | .and_yield(sensor_data) 37 | expect(@base.sensor.capture_once("Step Counter", "Fake Sensor")).to eq(sensor_data) 38 | end 39 | 40 | it "gets data once, with a block" do 41 | expect(@base).to receive(:json_streamed_api_command) 42 | .with('sensor', nil, '-n', '1', '-d', '1000', '-s', 43 | "Step Counter,Fake Sensor") 44 | .once 45 | .and_yield(sensor_data) 46 | expect { |b| @base.sensor.capture_once("Step Counter", "Fake Sensor", &b) }.to yield_successive_args(sensor_data) 47 | end 48 | 49 | it "gets all the arguments for capture" do 50 | expect(@base).to receive(:json_streamed_api_command) 51 | .with('sensor', nil, '-n', '4', '-d', '100', '-s', 52 | "Step Counter,Fake Sensor") 53 | .once 54 | @base.sensor.capture(sensors: ["Step Counter", "Fake Sensor"], 55 | limit: 4, 56 | delay: 100) 57 | end 58 | 59 | it "gets streamed data, with a block" do 60 | expect(@base).to receive(:json_streamed_api_command) 61 | .with('sensor', nil, '-d', '1000', '-s', 62 | "Step Counter,Fake Sensor") 63 | .once 64 | .and_yield(sensor_data) 65 | .and_yield(sensor_data) 66 | .and_yield(sensor_data) 67 | expect { |b| @base.sensor.capture(sensors: ["Step Counter", "Fake Sensor"], &b) }.to yield_successive_args(sensor_data, 68 | sensor_data, 69 | sensor_data) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/termux_ruby_api/sub_systems/sms_spec.rb: -------------------------------------------------------------------------------- 1 | require 'termux_ruby_api' 2 | 3 | RSpec.describe TermuxRubyApi::SubSystems::Sms do 4 | 5 | let (:result) { < 5.2.2" 25 | spec.add_development_dependency "bundler", "~> 1.17" 26 | spec.add_development_dependency "rake", "~> 10.0" 27 | spec.add_development_dependency "rspec", "~> 3.0" 28 | end 29 | --------------------------------------------------------------------------------