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 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: []