spotify_cli 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +10 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/README.md +101 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/spotify +5 -0
- data/lib/dex/ui/README.md +20 -0
- data/lib/dex/ui/ansi.rb +48 -0
- data/lib/dex/ui/box.rb +15 -0
- data/lib/dex/ui/color.rb +57 -0
- data/lib/dex/ui/formatter.rb +155 -0
- data/lib/dex/ui/frame.rb +166 -0
- data/lib/dex/ui/glyph.rb +49 -0
- data/lib/dex/ui/progress.rb +19 -0
- data/lib/dex/ui/prompt.rb +121 -0
- data/lib/dex/ui/spinner.rb +168 -0
- data/lib/dex/ui/stdout_router.rb +186 -0
- data/lib/dex/ui/terminal.rb +18 -0
- data/lib/dex/ui.rb +83 -0
- data/lib/helpers/doc.rb +62 -0
- data/lib/spotify_cli/.DS_Store +0 -0
- data/lib/spotify_cli/api.rb +182 -0
- data/lib/spotify_cli/app.rb +101 -0
- data/lib/spotify_cli/version.rb +3 -0
- data/lib/spotify_cli.rb +38 -0
- data/spotify_cli.gemspec +36 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6e1247de0d1d420585ce171e2e6fba3cd32fb9cc
|
4
|
+
data.tar.gz: 6a40b2f27518872bb8f5aa06e47e605738465cbc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a90aa024cfca00b317f9a85107f32eab284fa644013bbdae24ff73b1a784bb384aeed27b28cc9d3bd547b335a04d9f34f5ac3a4fecb4fae8259139f822667ceb
|
7
|
+
data.tar.gz: 8dd351594a77d92b3fae470c13d8024dbf84e438eb33b85b73a824929e45dd942e345a11f653ef45db5bab976ac8085e6df6c751d1ff59a2717343c85f7c0e95
|
data/.DS_Store
ADDED
Binary file
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
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 julian@jnadeau.ca. 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/
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# Spotify Cli
|
2
|
+
|
3
|
+
Spotify Application wrapper for control via command line
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'spotify_cli'
|
11
|
+
```
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
#### next
|
16
|
+
Changes to the next song
|
17
|
+
|
18
|
+
Usage:
|
19
|
+
spotify next
|
20
|
+
|
21
|
+
Aliases:
|
22
|
+
- n
|
23
|
+
|
24
|
+
#### previous
|
25
|
+
Changes to the previous song
|
26
|
+
|
27
|
+
Usage:
|
28
|
+
spotify previous
|
29
|
+
|
30
|
+
Aliases:
|
31
|
+
- pr
|
32
|
+
|
33
|
+
#### set_pos
|
34
|
+
Sets the position in the song
|
35
|
+
|
36
|
+
Usage:
|
37
|
+
spotify set_pos 60
|
38
|
+
|
39
|
+
Aliases:
|
40
|
+
- pos
|
41
|
+
|
42
|
+
#### replay
|
43
|
+
Replays the current song
|
44
|
+
|
45
|
+
Usage:
|
46
|
+
spotify replay
|
47
|
+
|
48
|
+
Aliases:
|
49
|
+
- rep
|
50
|
+
- restart
|
51
|
+
|
52
|
+
#### pause
|
53
|
+
Pause/stop the current song
|
54
|
+
|
55
|
+
Usage:
|
56
|
+
spotify pause
|
57
|
+
spotify stop
|
58
|
+
|
59
|
+
Aliases:
|
60
|
+
- stop
|
61
|
+
|
62
|
+
#### play_pause
|
63
|
+
Play/Pause the current song, or play a specified artist,
|
64
|
+
track, album, or uri
|
65
|
+
|
66
|
+
Usage:
|
67
|
+
spotify play artist [name]
|
68
|
+
spotify play track [name]
|
69
|
+
spotify play album [name]
|
70
|
+
spotify play uri [spotify uri]
|
71
|
+
|
72
|
+
Aliases:
|
73
|
+
- play
|
74
|
+
- p
|
75
|
+
|
76
|
+
#### status
|
77
|
+
Show the current song
|
78
|
+
|
79
|
+
Usage:
|
80
|
+
spotify status
|
81
|
+
|
82
|
+
Aliases:
|
83
|
+
- s
|
84
|
+
|
85
|
+
#### help
|
86
|
+
Display Help
|
87
|
+
|
88
|
+
Usage:
|
89
|
+
spotify
|
90
|
+
spotify help
|
91
|
+
|
92
|
+
## Development
|
93
|
+
|
94
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
95
|
+
|
96
|
+
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).
|
97
|
+
|
98
|
+
## Contributing
|
99
|
+
|
100
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/spotify_cli. 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.
|
101
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "spotify_cli"
|
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__)
|
data/bin/setup
ADDED
data/bin/spotify
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
```ruby
|
2
|
+
require 'dex/ui'
|
3
|
+
|
4
|
+
Dex::UI::Frame.open('{{*}} {{bold:a}}', color: :green) do
|
5
|
+
Dex::UI::Frame.open('{{i}} b', color: :magenta) do
|
6
|
+
Dex::UI::Frame.open('{{?}} c', color: :cyan) do
|
7
|
+
sg = Dex::UI::SpinGroup.new
|
8
|
+
sg.add('wow') { sleep(2.5) }
|
9
|
+
sg.add('such spin') { sleep(1.6) }
|
10
|
+
sg.add('many glyph') { sleep(2.0) }
|
11
|
+
sg.wait
|
12
|
+
end
|
13
|
+
end
|
14
|
+
Dex::UI::Frame.divider('{{v}} lol')
|
15
|
+
puts 'words'
|
16
|
+
sg = Dex::UI::SpinGroup.new
|
17
|
+
sg.add('more spins') { sleep(0.5) ; raise 'oh no' }
|
18
|
+
sg.wait
|
19
|
+
end
|
20
|
+
```
|
data/lib/dex/ui/ansi.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'dex/ui'
|
2
|
+
|
3
|
+
module Dex
|
4
|
+
module UI
|
5
|
+
module ANSI
|
6
|
+
ESC = "\x1b"
|
7
|
+
|
8
|
+
# ANSI escape sequences (like \x1b[31m) have zero width.
|
9
|
+
# when calculating the padding width, we must exclude them.
|
10
|
+
def self.printing_width(str)
|
11
|
+
strip_codes(str).size
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.strip_codes(str)
|
15
|
+
str.gsub(/\x1b\[[\d;]+[A-z]|\r/, '')
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.control(args, cmd)
|
19
|
+
ESC + "[" + args + cmd
|
20
|
+
end
|
21
|
+
|
22
|
+
# https://en.wikipedia.org/wiki/ANSI_escape_code#graphics
|
23
|
+
def self.sgr(params)
|
24
|
+
control(params.to_s, 'm')
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.cursor_up(n = 1)
|
28
|
+
return '' if n.zero?
|
29
|
+
control(n.to_s, 'A')
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.cursor_down(n = 1)
|
33
|
+
return '' if n.zero?
|
34
|
+
control(n.to_s, 'B')
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.cursor_forward(n = 1)
|
38
|
+
return '' if n.zero?
|
39
|
+
control(n.to_s, 'C')
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.cursor_back(n = 1)
|
43
|
+
return '' if n.zero?
|
44
|
+
control(n.to_s, 'D')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/dex/ui/box.rb
ADDED
data/lib/dex/ui/color.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'dex/ui'
|
2
|
+
|
3
|
+
module Dex
|
4
|
+
module UI
|
5
|
+
class Color
|
6
|
+
attr_reader :sgr, :name, :code
|
7
|
+
def initialize(sgr, name)
|
8
|
+
@sgr = sgr
|
9
|
+
@code = Dex::UI::ANSI.sgr(sgr)
|
10
|
+
@name = name
|
11
|
+
end
|
12
|
+
|
13
|
+
RED = new('31', :red)
|
14
|
+
GREEN = new('32', :green)
|
15
|
+
YELLOW = new('33', :yellow)
|
16
|
+
BLUE = new('34', :blue)
|
17
|
+
MAGENTA = new('35', :magenta)
|
18
|
+
CYAN = new('36', :cyan)
|
19
|
+
WHITE = new('97', :white)
|
20
|
+
RESET = new('0', :reset)
|
21
|
+
BOLD = new('1', :bold)
|
22
|
+
|
23
|
+
MAP = {
|
24
|
+
red: RED,
|
25
|
+
green: GREEN,
|
26
|
+
yellow: YELLOW,
|
27
|
+
blue: BLUE,
|
28
|
+
magenta: MAGENTA,
|
29
|
+
cyan: CYAN,
|
30
|
+
reset: RESET,
|
31
|
+
bold: BOLD,
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
class InvalidColorName < ArgumentError
|
35
|
+
def initialize(name)
|
36
|
+
@name = name
|
37
|
+
end
|
38
|
+
|
39
|
+
def message
|
40
|
+
keys = Color.available.map(&:inspect).join(',')
|
41
|
+
"invalid color: #{@name.inspect} " \
|
42
|
+
"-- must be one of Dex::UI::Color.available (#{keys})"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.lookup(name)
|
47
|
+
MAP.fetch(name)
|
48
|
+
rescue KeyError
|
49
|
+
raise InvalidColorName, name
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.available
|
53
|
+
MAP.keys
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dex/ui'
|
4
|
+
require 'strscan'
|
5
|
+
|
6
|
+
module Dex
|
7
|
+
module UI
|
8
|
+
class Formatter
|
9
|
+
SGR_MAP = {
|
10
|
+
# presentational
|
11
|
+
'red' => '31',
|
12
|
+
'green' => '32',
|
13
|
+
'yellow' => '33',
|
14
|
+
'blue' => '34',
|
15
|
+
'magenta' => '35',
|
16
|
+
'cyan' => '36',
|
17
|
+
'bold' => '1',
|
18
|
+
'reset' => '0',
|
19
|
+
|
20
|
+
# semantic
|
21
|
+
'error' => '31', # red
|
22
|
+
'success' => '32', # success
|
23
|
+
'warning' => '33', # yellow
|
24
|
+
'info' => '34', # blue
|
25
|
+
'command' => '36', # cyan
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
BEGIN_EXPR = '{{'
|
29
|
+
END_EXPR = '}}'
|
30
|
+
|
31
|
+
SCAN_FUNCNAME = /\w+:/
|
32
|
+
SCAN_GLYPH = /.}}/
|
33
|
+
SCAN_BODY = /
|
34
|
+
.*?
|
35
|
+
(
|
36
|
+
#{BEGIN_EXPR} |
|
37
|
+
#{END_EXPR} |
|
38
|
+
\z
|
39
|
+
)
|
40
|
+
/mx
|
41
|
+
|
42
|
+
DISCARD_BRACES = 0..-3
|
43
|
+
|
44
|
+
LITERAL_BRACES = :__literal_braces__
|
45
|
+
|
46
|
+
class FormatError < StandardError
|
47
|
+
attr_accessor :input, :index
|
48
|
+
|
49
|
+
def initialize(message = nil, input = nil, index = nil)
|
50
|
+
super(message)
|
51
|
+
@input = input
|
52
|
+
@index = index
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(text)
|
57
|
+
@text = text
|
58
|
+
end
|
59
|
+
|
60
|
+
def format(sgr_map = SGR_MAP, enable_color: true)
|
61
|
+
@nodes = []
|
62
|
+
stack = parse_body(StringScanner.new(@text))
|
63
|
+
prev_fmt = nil
|
64
|
+
content = @nodes.each_with_object(String.new) do |(text, fmt), str|
|
65
|
+
if prev_fmt != fmt && enable_color
|
66
|
+
text = apply_format(text, fmt, sgr_map)
|
67
|
+
end
|
68
|
+
str << text
|
69
|
+
prev_fmt = fmt
|
70
|
+
end
|
71
|
+
|
72
|
+
stack.reject! { |e| e == LITERAL_BRACES }
|
73
|
+
|
74
|
+
return content unless enable_color
|
75
|
+
return content if stack == prev_fmt
|
76
|
+
|
77
|
+
unless stack.empty? && (@nodes.size.zero? || @nodes.last[1].empty?)
|
78
|
+
content << apply_format('', stack, sgr_map)
|
79
|
+
end
|
80
|
+
content
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def apply_format(text, fmt, sgr_map)
|
86
|
+
sgr = fmt.each_with_object(String.new('0')) do |name, str|
|
87
|
+
next if name == LITERAL_BRACES
|
88
|
+
begin
|
89
|
+
str << ';' << sgr_map.fetch(name)
|
90
|
+
rescue KeyError
|
91
|
+
raise FormatError.new(
|
92
|
+
"invalid format specifier: #{name}",
|
93
|
+
@text,
|
94
|
+
-1
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
Dex::UI::ANSI.sgr(sgr) + text
|
99
|
+
end
|
100
|
+
|
101
|
+
def parse_expr(sc, stack)
|
102
|
+
if match = sc.scan(SCAN_GLYPH)
|
103
|
+
glyph_handle = match[0]
|
104
|
+
begin
|
105
|
+
glyph = Glyph.lookup(glyph_handle)
|
106
|
+
emit(glyph.char, [glyph.color.name.to_s])
|
107
|
+
rescue Glyph::InvalidGlyphHandle
|
108
|
+
index = sc.pos - 2 # rewind past '}}'
|
109
|
+
raise FormatError.new(
|
110
|
+
"invalid glyph handle at index #{index}: '#{glyph_handle}'",
|
111
|
+
@text,
|
112
|
+
index
|
113
|
+
)
|
114
|
+
end
|
115
|
+
elsif match = sc.scan(SCAN_FUNCNAME)
|
116
|
+
funcname = match.chop
|
117
|
+
stack.push(funcname)
|
118
|
+
else
|
119
|
+
# We read a {{ but it's not apparently Formatter syntax.
|
120
|
+
# We could error, but it's nicer to just pass through as text.
|
121
|
+
# We do kind of assume that the text will probably have balanced
|
122
|
+
# pairs of {{ }} at least.
|
123
|
+
emit('{{', stack)
|
124
|
+
stack.push(LITERAL_BRACES)
|
125
|
+
end
|
126
|
+
parse_body(sc, stack)
|
127
|
+
stack
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_body(sc, stack = [])
|
131
|
+
match = sc.scan(SCAN_BODY)
|
132
|
+
if match && match.end_with?(BEGIN_EXPR)
|
133
|
+
emit(match[DISCARD_BRACES], stack)
|
134
|
+
parse_expr(sc, stack)
|
135
|
+
elsif match && match.end_with?(END_EXPR)
|
136
|
+
emit(match[DISCARD_BRACES], stack)
|
137
|
+
if stack.pop == LITERAL_BRACES
|
138
|
+
emit('}}', stack)
|
139
|
+
end
|
140
|
+
parse_body(sc, stack)
|
141
|
+
elsif match
|
142
|
+
emit(match, stack)
|
143
|
+
else
|
144
|
+
emit(sc.rest, stack)
|
145
|
+
end
|
146
|
+
stack
|
147
|
+
end
|
148
|
+
|
149
|
+
def emit(text, stack)
|
150
|
+
return if text.nil? || text.empty?
|
151
|
+
@nodes << [text, stack.reject { |n| n == LITERAL_BRACES }]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|