handy_toolbox 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/.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
|
+
![Main menu](promo/1.png?raw=true)
|
180
|
+
|
181
|
+
![Quick tasks](promo/2.png?raw=true)
|
182
|
+
|
183
|
+
![Nested menu with loader not executed yet](promo/3.png?raw=true)
|
184
|
+
|
185
|
+
![Loaded data from loader](promo/4.png?raw=true)
|
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: []
|