handy_toolbox 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
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
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
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,8 @@
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
@@ -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,15 @@
1
+ module HandyToolbox
2
+
3
+ class Ids
4
+ # 0..99 are reserved
5
+ BACK = 0
6
+ FIRST = 100
7
+ @@id = FIRST - 1
8
+
9
+ def self.next
10
+ @@id += 1
11
+ @@id
12
+ end
13
+ end
14
+
15
+ 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,14 @@
1
+ module HandyToolbox
2
+
3
+ class Loader
4
+ LoaderNotConfigured = StandardError.new(
5
+ 'Loader not configured (missing #on_load definition)'
6
+ )
7
+
8
+ def on_load(node)
9
+ raise LoaderNotConfigured
10
+ end
11
+
12
+ end
13
+
14
+ 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,14 @@
1
+ module HandyToolbox
2
+
3
+ class Plugin
4
+ PluginNotConfigured = StandardError.new(
5
+ 'Plugin not configured (missing #on_attach definition)'
6
+ )
7
+
8
+ def on_attach(node)
9
+ raise PluginNotConfigured
10
+ end
11
+
12
+ end
13
+
14
+ 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,22 @@
1
+ module HandyToolbox
2
+
3
+ class ToolRunner
4
+
5
+ def initialize
6
+ @tool = nil
7
+ end
8
+
9
+ def queue(tool)
10
+ @tool = tool
11
+ end
12
+
13
+ def run
14
+ if !@tool.nil?
15
+ puts @tool.cmd
16
+ Kernel.exec @tool.cmd
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ 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
@@ -0,0 +1,3 @@
1
+ module HandyToolbox
2
+ VERSION = '0.1.0'
3
+ 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: []