handy_toolbox 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/EXAMPLE.md +42 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +21 -0
- data/README.md +200 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/handy_toolbox.gemspec +32 -0
- data/lib/handy_toolbox.rb +20 -0
- data/lib/handy_toolbox/app.rb +169 -0
- data/lib/handy_toolbox/cmd.rb +30 -0
- data/lib/handy_toolbox/ids.rb +15 -0
- data/lib/handy_toolbox/keys.rb +20 -0
- data/lib/handy_toolbox/loader.rb +14 -0
- data/lib/handy_toolbox/menu.rb +49 -0
- data/lib/handy_toolbox/menu_back.rb +25 -0
- data/lib/handy_toolbox/menu_loader.rb +25 -0
- data/lib/handy_toolbox/navigator.rb +65 -0
- data/lib/handy_toolbox/plugin.rb +14 -0
- data/lib/handy_toolbox/screen.rb +50 -0
- data/lib/handy_toolbox/scroll.rb +73 -0
- data/lib/handy_toolbox/tool.rb +26 -0
- data/lib/handy_toolbox/tool_menu_item.rb +29 -0
- data/lib/handy_toolbox/tool_runner.rb +22 -0
- data/lib/handy_toolbox/ui.rb +79 -0
- data/lib/handy_toolbox/version.rb +3 -0
- data/promo/1.png +0 -0
- data/promo/2.png +0 -0
- data/promo/3.png +0 -0
- data/promo/4.png +0 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a8153ab3dd9d135545418c75c5b394a0d05c1462
|
4
|
+
data.tar.gz: 3610c4584474c30ecc9fe3d7567c4ff62acbb650
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cf1fd975e5ec0e617243b43fa9a85eeb8f75912dfbe9355e1dea6c86efabba5b2ccc5d6dd8bd8621b3eaf61537b539626ad2ca7fd6502c24159b4ee974f85429
|
7
|
+
data.tar.gz: e903eec79411f7040fdc0a18b44b5a3ca964e6a8a999b5c285f886e26ef268e20d56d73d541460534cb7480e3e6d1f10cce52b4c0e6cd8b942b9c6783186ed27
|
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
gem_handy_toolbox
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.3
|
data/EXAMPLE.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
```ruby
|
2
|
+
#!/usr/bin/env ruby
|
3
|
+
|
4
|
+
require_relative '../config/boot'
|
5
|
+
require 'handy_toolbox'
|
6
|
+
include HandyToolbox
|
7
|
+
|
8
|
+
# dummy rake loader
|
9
|
+
class RakeLoader < Loader
|
10
|
+
def on_load(node)
|
11
|
+
`rake -T`.strip.split("\n").each do |rake|
|
12
|
+
name, desc = rake.split("#")
|
13
|
+
node.tool name.strip, desc: desc.strip
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# db tasks
|
19
|
+
class RailsDatabasePlugin < Plugin
|
20
|
+
def on_attach(node)
|
21
|
+
node.menu 'Database' do |db|
|
22
|
+
db.tool 'rails db:migrate', name: 'Migrate database'
|
23
|
+
db.tool 'rails db:test:prepare', name: 'Prepare test database'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
branch = Cmd.exec('git branch').match(/^\* (?<name>.+)$/)[:name]
|
29
|
+
app = App.new(title: "My Project (on #{branch})")
|
30
|
+
app.menu_loader 'Rake', RakeLoader
|
31
|
+
app.menu 'Rails' do |rails|
|
32
|
+
rails.plugin RailsDatabasePlugin
|
33
|
+
rails.menu_loader 'Rake', RakeLoader
|
34
|
+
end
|
35
|
+
app.menu 'Deployment' do |deploy|
|
36
|
+
deploy.tool 'cap staging deploy', name: 'Deploy to staging'
|
37
|
+
deploy.menu 'Deploy to production' do |production|
|
38
|
+
production.tool 'cap production deploy', name: 'I understand consequences, do it!'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
app.run
|
42
|
+
```
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 bart
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
# HandyToolbox
|
2
|
+
|
3
|
+
HandyToolbox is a text based user interface that will help you with every day tasks, so:
|
4
|
+
|
5
|
+
1. Define your tasks and organize them into groups.
|
6
|
+
2. And from now on you can forget all rake, capistrano, heroku, npm, ... commands.
|
7
|
+
|
8
|
+
Gem was built for the Rails apps in mind but can be used standalone as well and not only for Ruby related stuff.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'handy_toolbox'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install handy_toolbox
|
25
|
+
|
26
|
+
## Setup
|
27
|
+
|
28
|
+
### Rails On Rails project integration
|
29
|
+
|
30
|
+
Create `tools` file in your `project/bin/` folder and paste this code:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
#!/usr/bin/env ruby
|
34
|
+
|
35
|
+
require_relative '../config/boot'
|
36
|
+
require 'handy_toolbox'
|
37
|
+
include HandyToolbox
|
38
|
+
|
39
|
+
app = App.new(title: 'Project name')
|
40
|
+
# your config
|
41
|
+
app.run
|
42
|
+
```
|
43
|
+
|
44
|
+
### Defining your tasks
|
45
|
+
|
46
|
+
Other example [here](EXAMPLE.md).
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
...
|
50
|
+
app.menu 'Quick' do |quick|
|
51
|
+
quick.tool 'rubocop -c config/rubocop.yml', name: 'RuboCop'
|
52
|
+
quick.tool 'heroku pg:pull HEROKU_POSTGRESQL_SOMETHING mylocaldb --app my_app', name: 'Download production database'
|
53
|
+
quick.tool 'pkill -f puma', name: 'Kill all puma workers'
|
54
|
+
end
|
55
|
+
|
56
|
+
app.menu 'Tests' do |tests|
|
57
|
+
tests.tool 'npm test', name: 'Run frontend tests'
|
58
|
+
tests.tool 'rspec spec/', name: 'Run RSpec tests'
|
59
|
+
tests.tool 'imagine very complicated command here', name: 'Run those tests you always forget how to run'
|
60
|
+
end
|
61
|
+
|
62
|
+
app.menu 'Rails' do |rails|
|
63
|
+
rails.menu 'Database' do |db|
|
64
|
+
db.tool 'rails db:migrate'
|
65
|
+
db.tool 'rails db:test:prepare', name: 'Prepare test database'
|
66
|
+
end
|
67
|
+
rails.menu 'Assets' do |assets|
|
68
|
+
assets.tool 'rake assets:precompile', desc: 'Precompile all the assets'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
app.menu 'Deployment' do |deploy|
|
73
|
+
# for heroku
|
74
|
+
deploy.tool 'git push staging master', name: 'Deploy to staging'
|
75
|
+
deploy.tool 'git push heroku master', name: 'Deploy to production'
|
76
|
+
|
77
|
+
# for capistrano
|
78
|
+
deploy.tool 'cap staging deploy', name: 'Deploy to staging'
|
79
|
+
deploy.tool 'cap production deploy', name: 'Deploy to production'
|
80
|
+
end
|
81
|
+
|
82
|
+
...
|
83
|
+
```
|
84
|
+
|
85
|
+
#### Configuration details
|
86
|
+
|
87
|
+
##### Menu
|
88
|
+
|
89
|
+
Menus can be nested, they aggregate other menus or tasks. On every menu node you can define: other menus, tools, plugins and loaders.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
app.menu 'Menu' do |node|
|
93
|
+
node.menu 'Sub menu' do |sub|
|
94
|
+
...
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
##### Tool
|
100
|
+
|
101
|
+
Tools are your commands that will be passed to your shell and executed.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
app.tool 'ls -la'
|
105
|
+
# you will see this task as 'ls -la', and 'ls -la' will be executed
|
106
|
+
|
107
|
+
app.tool 'ls -la', name: 'Show files'
|
108
|
+
# you will see 'Show files', and 'ls -la' will be executed
|
109
|
+
|
110
|
+
app.tool 'ls -la', name: 'Show files', desc: 'Lists all the files and their details'
|
111
|
+
# you will see this:
|
112
|
+
# # Lists all the files and their details
|
113
|
+
# Show files
|
114
|
+
# and will execute 'ls -la'
|
115
|
+
```
|
116
|
+
|
117
|
+
##### Plugin
|
118
|
+
|
119
|
+
Plugins allow you to build your task tree more modularly (rather than keeping everything in one file).
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
# For every plugin you need to implement '#on_attach' method and you can build your tree from there.
|
123
|
+
class RailsDatabasePlugin < Plugin # HandyToolbox::Plugin
|
124
|
+
def on_attach(node)
|
125
|
+
node.menu 'Database' do |db|
|
126
|
+
db.tool 'rails db:migrate', name: 'Migrate database'
|
127
|
+
db.tool 'rails db:test:prepare', name: 'Prepare test database'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Install a plugin (#on_attach is called immediately)
|
133
|
+
app.plugin RailsDatabasePlugin
|
134
|
+
```
|
135
|
+
|
136
|
+
##### Loader
|
137
|
+
|
138
|
+
Loaders allow you to build your task tree in a lazy fashion. When toolbox starts only menu node is created.
|
139
|
+
Once you enter the menu, loader will populate your tree.
|
140
|
+
|
141
|
+
Be aware that loader will freeze UI for a moment (until loading is finished).
|
142
|
+
|
143
|
+
Personally I encourage you to define your tasks explicitly (you know what you have and you can keep only important stuff) rather than loading them via long running procedures.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
# For every loader you need to implement '#on_load' method and you can build your tree from there.
|
147
|
+
# Method is run only once per toolbox lifecycle.
|
148
|
+
# Here: Dummy rake tasks loader, 'rake -T' takes some time
|
149
|
+
class RakeLoader < Loader
|
150
|
+
def on_load(node)
|
151
|
+
`rake -T`.strip.split("\n").each do |rake|
|
152
|
+
name, desc = rake.split("#")
|
153
|
+
node.tool name.strip, desc: desc.strip
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
app.menu_loader 'Rake tasks', RakeLoader
|
159
|
+
# This will create menu 'Rake tasks' and once you decide to enter this menu it will call #on_load.
|
160
|
+
```
|
161
|
+
|
162
|
+
### Usage
|
163
|
+
|
164
|
+
In terminal in your project's folder execute:
|
165
|
+
|
166
|
+
```
|
167
|
+
./bin/tools # you can also create some alias to it, it all depends how lazy you are
|
168
|
+
```
|
169
|
+
|
170
|
+
Shortcuts:
|
171
|
+
```
|
172
|
+
q - quits the toolbox
|
173
|
+
arrow up / arrow down - navigate through menus and tasks
|
174
|
+
enter - open menu or execute task
|
175
|
+
page up / page down - navigate faster (skips 10 items)
|
176
|
+
home / end - go to first/last item
|
177
|
+
```
|
178
|
+
|
179
|
+

|
180
|
+
|
181
|
+

|
182
|
+
|
183
|
+

|
184
|
+
|
185
|
+

|
186
|
+
|
187
|
+
## TODO
|
188
|
+
|
189
|
+
- Test it on OSX
|
190
|
+
- Allow tasks to be loaded from YML file (and from user's HOME dir i.e. ~/.handy.yml)
|
191
|
+
|
192
|
+
|
193
|
+
## Contributing
|
194
|
+
|
195
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/qbart/handy_toolbox.
|
196
|
+
|
197
|
+
|
198
|
+
## License
|
199
|
+
|
200
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "handy_toolbox"
|
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
|
data/bin/setup
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'handy_toolbox/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "handy_toolbox"
|
8
|
+
spec.version = HandyToolbox::VERSION
|
9
|
+
spec.authors = ["Bartłomiej Wójtowicz"]
|
10
|
+
spec.email = ["wojtowicz.bartlomiej@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Task manager without the need of typing}
|
13
|
+
spec.description = <<-DESC
|
14
|
+
HandyToolbox is a text based user interface that will help you with every day tasks.
|
15
|
+
Define your tasks and organize them into groups.'
|
16
|
+
And from now on you can forget all rake, capistrano, heroku, npm, ... commands.
|
17
|
+
DESC
|
18
|
+
spec.homepage = "https://github.com/qbart/handy_toolbox"
|
19
|
+
spec.license = "MIT"
|
20
|
+
|
21
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
22
|
+
f.match(%r{^(test|spec|features)/})
|
23
|
+
end
|
24
|
+
spec.bindir = "exe"
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_dependency "curses"
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'handy_toolbox/version'
|
2
|
+
require 'handy_toolbox/ids'
|
3
|
+
require 'handy_toolbox/keys'
|
4
|
+
require 'handy_toolbox/cmd'
|
5
|
+
require 'handy_toolbox/ui'
|
6
|
+
require 'handy_toolbox/loader'
|
7
|
+
require 'handy_toolbox/plugin'
|
8
|
+
require 'handy_toolbox/menu_back'
|
9
|
+
require 'handy_toolbox/menu'
|
10
|
+
require 'handy_toolbox/menu_loader'
|
11
|
+
require 'handy_toolbox/tool_menu_item'
|
12
|
+
require 'handy_toolbox/tool'
|
13
|
+
require 'handy_toolbox/tool_runner'
|
14
|
+
require 'handy_toolbox/navigator'
|
15
|
+
require 'handy_toolbox/scroll'
|
16
|
+
require 'handy_toolbox/screen'
|
17
|
+
require 'handy_toolbox/app'
|
18
|
+
|
19
|
+
module HandyToolbox
|
20
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module HandyToolbox
|
2
|
+
|
3
|
+
class App
|
4
|
+
attr_reader :title
|
5
|
+
|
6
|
+
def initialize(title: "Tools")
|
7
|
+
@title = title
|
8
|
+
@loop = true
|
9
|
+
@builder = Menu.new(nil, nil)
|
10
|
+
@screen = Screen.new
|
11
|
+
@navigator = Navigator.new
|
12
|
+
@tool_runner = ToolRunner.new
|
13
|
+
@positions = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
screen.init
|
18
|
+
navigator.enter(builder)
|
19
|
+
|
20
|
+
begin
|
21
|
+
|
22
|
+
while @loop
|
23
|
+
screen.draw do
|
24
|
+
draw_title
|
25
|
+
draw_tools
|
26
|
+
end
|
27
|
+
handle_input
|
28
|
+
end
|
29
|
+
|
30
|
+
ensure
|
31
|
+
screen.close
|
32
|
+
tool_runner.run
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def plugin(plugin_class)
|
37
|
+
builder.plugin(plugin_class)
|
38
|
+
end
|
39
|
+
|
40
|
+
def menu(group, &block)
|
41
|
+
builder.menu(group, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def menu_loader(group, loader_class)
|
45
|
+
builder.menu_loader(group, loader_class)
|
46
|
+
end
|
47
|
+
|
48
|
+
def tool(cmd, opts = {})
|
49
|
+
builder.tool(cmd, opts)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :screen, :builder, :navigator, :tool_runner
|
55
|
+
|
56
|
+
def handle_input
|
57
|
+
key = Keys.get
|
58
|
+
|
59
|
+
case key
|
60
|
+
when Keys::ESC
|
61
|
+
close_app
|
62
|
+
when Keys::UP
|
63
|
+
navigator.up
|
64
|
+
adjust_scroll_position
|
65
|
+
when Keys::DOWN
|
66
|
+
navigator.down
|
67
|
+
adjust_scroll_position
|
68
|
+
when Keys::PAGE_UP
|
69
|
+
navigator.up(10)
|
70
|
+
adjust_scroll_position
|
71
|
+
when Keys::PAGE_DOWN
|
72
|
+
navigator.down(10)
|
73
|
+
adjust_scroll_position
|
74
|
+
when Keys::FIRST
|
75
|
+
screen.scroll.to_first
|
76
|
+
navigator.select_first
|
77
|
+
when Keys::LAST
|
78
|
+
screen.scroll.to_last
|
79
|
+
navigator.select_last
|
80
|
+
when *Keys::ENTER_ARR
|
81
|
+
if navigator.tool_selected?
|
82
|
+
tool_runner.queue(navigator.tool)
|
83
|
+
close_app
|
84
|
+
else
|
85
|
+
screen.clear
|
86
|
+
navigator.enter_selected
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def adjust_scroll_position
|
92
|
+
if !screen.scroll.fits_into_pane?(@positions[navigator.selection.id])
|
93
|
+
screen.scroll.to(@positions[navigator.selection.id] - 5)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def draw_title
|
98
|
+
horizontal_line = "-" * (title.size + 4)
|
99
|
+
padded_title = "| #{title} |"
|
100
|
+
|
101
|
+
screen.text_at(0, 0, horizontal_line)
|
102
|
+
screen.text_at(0, 1, padded_title)
|
103
|
+
screen.text_at(0, 2, horizontal_line)
|
104
|
+
end
|
105
|
+
|
106
|
+
def draw_tools
|
107
|
+
y = 4
|
108
|
+
longest = find_longest_child_name
|
109
|
+
@positions = {}
|
110
|
+
current.children.each_with_index do |child|
|
111
|
+
str = child.to_s
|
112
|
+
is_dir = !child.is_a?(ToolMenuItem)
|
113
|
+
is_selected = (navigator.selection.id == child.id)
|
114
|
+
is_multiline = str.is_a?(Array)
|
115
|
+
offset = (is_multiline ? 3 : 1)
|
116
|
+
|
117
|
+
Ui.bold(is_dir) do
|
118
|
+
Ui.highlight(is_selected) do
|
119
|
+
if is_multiline
|
120
|
+
Ui.dim do
|
121
|
+
text = format_desc(str[1])
|
122
|
+
screen.text_at(2, y + 1, text)
|
123
|
+
end
|
124
|
+
text = format_child_name(child.icon, str[0], longest)
|
125
|
+
screen.text_at(2, y + 2, text)
|
126
|
+
@positions[child.id] = y + 2
|
127
|
+
else
|
128
|
+
text = format_child_name(child.icon, str, longest)
|
129
|
+
screen.text_at(2, y, text)
|
130
|
+
@positions[child.id] = y
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
y += offset
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def current
|
140
|
+
navigator.current_parent
|
141
|
+
end
|
142
|
+
|
143
|
+
def format_desc(str)
|
144
|
+
" # #{str}"
|
145
|
+
end
|
146
|
+
|
147
|
+
def format_child_name(icon, name, max_len)
|
148
|
+
" #{icon}#{name.ljust(max_len, ' ')} "
|
149
|
+
end
|
150
|
+
|
151
|
+
def find_longest_child_name
|
152
|
+
longest_element = current.children.max do |a, b|
|
153
|
+
first = a.to_s
|
154
|
+
second = b.to_s
|
155
|
+
first = first[0] if first.is_a?(Array)
|
156
|
+
second = second[0] if second.is_a?(Array)
|
157
|
+
first.size <=> second.size
|
158
|
+
end
|
159
|
+
str = longest_element.to_s
|
160
|
+
str.is_a?(Array) ? str[0].size : str.size
|
161
|
+
end
|
162
|
+
|
163
|
+
def close_app
|
164
|
+
@loop = false
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module HandyToolbox
|
2
|
+
|
3
|
+
class Cmd
|
4
|
+
|
5
|
+
attr_reader :output
|
6
|
+
|
7
|
+
def initialize(cmd)
|
8
|
+
@cmd = cmd
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.exec(cmd)
|
12
|
+
Cmd.new(cmd).exec
|
13
|
+
end
|
14
|
+
|
15
|
+
def exec
|
16
|
+
@output = `#{@cmd} 2> /dev/null`.rstrip
|
17
|
+
if ok?
|
18
|
+
output
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def ok?
|
25
|
+
!output.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
module HandyToolbox
|
4
|
+
|
5
|
+
class Keys
|
6
|
+
ESC = 'q'
|
7
|
+
UP = Curses::KEY_UP
|
8
|
+
DOWN = Curses::KEY_DOWN
|
9
|
+
FIRST = Curses::KEY_HOME
|
10
|
+
LAST = Curses::KEY_END
|
11
|
+
PAGE_UP = Curses::KEY_PPAGE
|
12
|
+
PAGE_DOWN = Curses::KEY_NPAGE
|
13
|
+
ENTER_ARR = [Curses::KEY_ENTER, 13, 10]
|
14
|
+
|
15
|
+
def self.get
|
16
|
+
Curses.getch
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module HandyToolbox
|
2
|
+
|
3
|
+
class Menu
|
4
|
+
ICON = ' + '.freeze
|
5
|
+
|
6
|
+
attr_reader :id, :tools, :children, :parent
|
7
|
+
|
8
|
+
def initialize(parent, group)
|
9
|
+
@id = Ids.next
|
10
|
+
@parent = parent
|
11
|
+
@group = group
|
12
|
+
@children = []
|
13
|
+
|
14
|
+
if !parent.nil?
|
15
|
+
@children << MenuBack.new(parent)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def menu(group, &block)
|
20
|
+
menu = Menu.new(self, group)
|
21
|
+
children << menu
|
22
|
+
yield menu if block_given?
|
23
|
+
end
|
24
|
+
|
25
|
+
def menu_loader(group, loader_class)
|
26
|
+
loader = loader_class.new
|
27
|
+
menu = MenuLoader.new(self, group, loader)
|
28
|
+
children << menu
|
29
|
+
end
|
30
|
+
|
31
|
+
def tool(cmd, opts = {})
|
32
|
+
children << ToolMenuItem.new(parent, Tool.new(cmd, opts))
|
33
|
+
end
|
34
|
+
|
35
|
+
def plugin(plugin_class)
|
36
|
+
plugin = plugin_class.new
|
37
|
+
plugin.on_attach(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@group
|
42
|
+
end
|
43
|
+
|
44
|
+
def icon
|
45
|
+
ICON
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module HandyToolbox
|
2
|
+
|
3
|
+
class MenuBack
|
4
|
+
|
5
|
+
TEXT = '..'.freeze
|
6
|
+
ICON = '<- '.freeze
|
7
|
+
|
8
|
+
attr_reader :id, :parent
|
9
|
+
|
10
|
+
def initialize(parent)
|
11
|
+
@id = Ids::BACK
|
12
|
+
@parent = parent
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
TEXT
|
17
|
+
end
|
18
|
+
|
19
|
+
def icon
|
20
|
+
ICON
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module HandyToolbox
|
2
|
+
|
3
|
+
class MenuLoader < Menu
|
4
|
+
NOT_LOADED_YET_ICON = ' % '
|
5
|
+
|
6
|
+
def initialize(parent, group, loader)
|
7
|
+
super(parent, group)
|
8
|
+
@loader = loader
|
9
|
+
@loaded = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_load
|
13
|
+
if !@loaded
|
14
|
+
@loader.on_load(self)
|
15
|
+
@loaded = true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def icon
|
20
|
+
@loaded ? super : NOT_LOADED_YET_ICON
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module HandyToolbox
|
2
|
+
|
3
|
+
class Navigator
|
4
|
+
|
5
|
+
def tool_selected?
|
6
|
+
selection.is_a?(ToolMenuItem)
|
7
|
+
end
|
8
|
+
|
9
|
+
def tool
|
10
|
+
if tool_selected?
|
11
|
+
selection.tool
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def selection
|
16
|
+
@children[@current_index]
|
17
|
+
end
|
18
|
+
|
19
|
+
def select_first
|
20
|
+
select_by_index(0)
|
21
|
+
end
|
22
|
+
|
23
|
+
def select_last
|
24
|
+
select_by_index(@children.size - 1)
|
25
|
+
end
|
26
|
+
|
27
|
+
def select_by_index(index)
|
28
|
+
if index >= 0 && index < @children.size
|
29
|
+
@current_index = index
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def current_parent
|
34
|
+
@parent
|
35
|
+
end
|
36
|
+
|
37
|
+
def enter_selected
|
38
|
+
old = selection
|
39
|
+
if selection.id == Ids::BACK
|
40
|
+
enter(old.parent)
|
41
|
+
else
|
42
|
+
enter(selection)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def enter(parent)
|
47
|
+
@parent = parent
|
48
|
+
parent.on_load if parent.is_a?(MenuLoader)
|
49
|
+
|
50
|
+
@children = parent.children
|
51
|
+
@current_index = 0
|
52
|
+
selection
|
53
|
+
end
|
54
|
+
|
55
|
+
def up(by = 1)
|
56
|
+
@current_index = [0, @current_index - by].max
|
57
|
+
end
|
58
|
+
|
59
|
+
def down(by = 1)
|
60
|
+
@current_index = [@current_index + by, @children.size - 1].min
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
module HandyToolbox
|
4
|
+
|
5
|
+
class Screen
|
6
|
+
|
7
|
+
attr_reader :scroll
|
8
|
+
|
9
|
+
def init
|
10
|
+
Curses.init_screen
|
11
|
+
Curses.start_color
|
12
|
+
Ui.hide_cursor
|
13
|
+
Curses.cbreak
|
14
|
+
Curses.crmode
|
15
|
+
Curses.noecho
|
16
|
+
Curses.nonl
|
17
|
+
Curses.stdscr.keypad(true)
|
18
|
+
@scroll = Scroll.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear
|
22
|
+
scroll.reset
|
23
|
+
Curses.clear
|
24
|
+
end
|
25
|
+
|
26
|
+
def draw
|
27
|
+
@max_y = 0
|
28
|
+
yield
|
29
|
+
@scroll.update(@max_y)
|
30
|
+
Curses.refresh
|
31
|
+
end
|
32
|
+
|
33
|
+
def text_at(x, y, str)
|
34
|
+
if scroll.fits_into_pane?(y)
|
35
|
+
Ui.text_at(x, y - scroll.top, str)
|
36
|
+
end
|
37
|
+
@max_y = y if @max_y < y
|
38
|
+
end
|
39
|
+
|
40
|
+
def close
|
41
|
+
Curses.close_screen
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :screen
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module HandyToolbox
|
2
|
+
|
3
|
+
class Scroll
|
4
|
+
|
5
|
+
attr_reader :top
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@screen = Curses.stdscr
|
9
|
+
@screen.scrollok(true)
|
10
|
+
reset
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset
|
14
|
+
@top = 0
|
15
|
+
@max_y = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def update(max_y)
|
19
|
+
@max_y = max_y
|
20
|
+
end
|
21
|
+
|
22
|
+
def fits_into_pane?(y)
|
23
|
+
(y - top) >= 0 && (y - top) < screen.maxy
|
24
|
+
end
|
25
|
+
|
26
|
+
def up(by = 1)
|
27
|
+
by = @top if @top - by < 0
|
28
|
+
if by > 0
|
29
|
+
screen.scrl(-by)
|
30
|
+
@top -= by
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def down(by = 1)
|
35
|
+
by = max_top - @top if @top + by > max_top
|
36
|
+
if by > 0
|
37
|
+
screen.scrl(by)
|
38
|
+
@top += by
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def to(index)
|
43
|
+
if index != top
|
44
|
+
if index >= 0 && index <= max_top
|
45
|
+
by = index - top
|
46
|
+
screen.scrl(by)
|
47
|
+
@top = index
|
48
|
+
elsif index < 0
|
49
|
+
to_first
|
50
|
+
elsif index > max_top
|
51
|
+
to_last
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_first
|
57
|
+
to(0)
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_last
|
61
|
+
to(max_top)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def max_top
|
67
|
+
max_y - (screen.maxy - 1)
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_reader :screen, :max_y
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module HandyToolbox
|
2
|
+
|
3
|
+
class Tool
|
4
|
+
|
5
|
+
attr_reader :cmd
|
6
|
+
|
7
|
+
def initialize(cmd, opts = {})
|
8
|
+
@cmd = cmd
|
9
|
+
@opts = opts
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
opts[:name] || @cmd
|
14
|
+
end
|
15
|
+
|
16
|
+
def desc
|
17
|
+
opts[:desc]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :opts
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module HandyToolbox
|
2
|
+
|
3
|
+
class ToolMenuItem
|
4
|
+
|
5
|
+
ICON = ' '.freeze
|
6
|
+
|
7
|
+
attr_reader :id, :parent, :tool
|
8
|
+
|
9
|
+
def initialize(parent, tool)
|
10
|
+
@id = Ids.next
|
11
|
+
@parent = parent
|
12
|
+
@tool = tool
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
if @tool.desc.nil?
|
17
|
+
@tool.name
|
18
|
+
else
|
19
|
+
[@tool.name, @tool.desc]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def icon
|
24
|
+
ICON
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
module HandyToolbox
|
4
|
+
|
5
|
+
class Ui
|
6
|
+
|
7
|
+
def self.pos(x, y)
|
8
|
+
Curses.setpos(y, x)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.text(str)
|
12
|
+
Curses.addstr(str)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.text_at(x, y, str)
|
16
|
+
pos(x, y)
|
17
|
+
text(str)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.highlight_on
|
21
|
+
attr_on Curses::A_STANDOUT
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.highlight_off
|
25
|
+
attr_off Curses::A_STANDOUT
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.highlight(condition = true)
|
29
|
+
highlight_on if condition
|
30
|
+
yield
|
31
|
+
highlight_off if condition
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.dim_on
|
35
|
+
attr_on Curses::A_DIM
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.dim_off
|
39
|
+
attr_off Curses::A_DIM
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.dim(condition = true)
|
43
|
+
dim_on if condition
|
44
|
+
yield
|
45
|
+
dim_off if condition
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.bold_on
|
49
|
+
attr_on Curses::A_BOLD
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.bold_off
|
53
|
+
attr_off Curses::A_BOLD
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.bold(condition = true)
|
57
|
+
bold_on if condition
|
58
|
+
yield
|
59
|
+
bold_off if condition
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.hide_cursor
|
63
|
+
Curses.curs_set(0)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.attr_on(value)
|
67
|
+
Curses.attron(value)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.attr_off(value)
|
71
|
+
Curses.attroff(value)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.scroll_by(val)
|
75
|
+
Curses.scrl(val)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/promo/1.png
ADDED
Binary file
|
data/promo/2.png
ADDED
Binary file
|
data/promo/3.png
ADDED
Binary file
|
data/promo/4.png
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: handy_toolbox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bartłomiej Wójtowicz
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: curses
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.13'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.13'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
description: |
|
56
|
+
HandyToolbox is a text based user interface that will help you with every day tasks.
|
57
|
+
Define your tasks and organize them into groups.'
|
58
|
+
And from now on you can forget all rake, capistrano, heroku, npm, ... commands.
|
59
|
+
email:
|
60
|
+
- wojtowicz.bartlomiej@gmail.com
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- ".gitignore"
|
66
|
+
- ".ruby-gemset"
|
67
|
+
- ".ruby-version"
|
68
|
+
- EXAMPLE.md
|
69
|
+
- Gemfile
|
70
|
+
- LICENSE.txt
|
71
|
+
- README.md
|
72
|
+
- Rakefile
|
73
|
+
- bin/console
|
74
|
+
- bin/setup
|
75
|
+
- handy_toolbox.gemspec
|
76
|
+
- lib/handy_toolbox.rb
|
77
|
+
- lib/handy_toolbox/app.rb
|
78
|
+
- lib/handy_toolbox/cmd.rb
|
79
|
+
- lib/handy_toolbox/ids.rb
|
80
|
+
- lib/handy_toolbox/keys.rb
|
81
|
+
- lib/handy_toolbox/loader.rb
|
82
|
+
- lib/handy_toolbox/menu.rb
|
83
|
+
- lib/handy_toolbox/menu_back.rb
|
84
|
+
- lib/handy_toolbox/menu_loader.rb
|
85
|
+
- lib/handy_toolbox/navigator.rb
|
86
|
+
- lib/handy_toolbox/plugin.rb
|
87
|
+
- lib/handy_toolbox/screen.rb
|
88
|
+
- lib/handy_toolbox/scroll.rb
|
89
|
+
- lib/handy_toolbox/tool.rb
|
90
|
+
- lib/handy_toolbox/tool_menu_item.rb
|
91
|
+
- lib/handy_toolbox/tool_runner.rb
|
92
|
+
- lib/handy_toolbox/ui.rb
|
93
|
+
- lib/handy_toolbox/version.rb
|
94
|
+
- promo/1.png
|
95
|
+
- promo/2.png
|
96
|
+
- promo/3.png
|
97
|
+
- promo/4.png
|
98
|
+
homepage: https://github.com/qbart/handy_toolbox
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
metadata: {}
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 2.5.2
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: Task manager without the need of typing
|
122
|
+
test_files: []
|