├── .gitignore ├── lib ├── termistat │ ├── version.rb │ ├── tee_io.rb │ └── config.rb └── termistat.rb ├── examples ├── file_copy.png └── file_copy.rb ├── test ├── helper.rb └── units │ ├── test_config.rb │ ├── test_tee_io.rb │ └── test_termistat.rb ├── Gemfile ├── Rakefile ├── termistat.gemspec ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .rvmrc 4 | Gemfile.lock 5 | pkg/* 6 | -------------------------------------------------------------------------------- /lib/termistat/version.rb: -------------------------------------------------------------------------------- 1 | module Termistat 2 | VERSION = "0.0.3" 3 | end 4 | -------------------------------------------------------------------------------- /examples/file_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubysolo/termistat/HEAD/examples/file_copy.png -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | $:.unshift File.expand_path('../../lib', __FILE__) 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in termistat.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new('test') do |t| 5 | t.pattern = 'test/**/test_*.rb' 6 | t.warning = true 7 | end 8 | 9 | task :default => :test 10 | 11 | -------------------------------------------------------------------------------- /test/units/test_config.rb: -------------------------------------------------------------------------------- 1 | require_relative '../helper' 2 | require 'termistat/tee_io' 3 | 4 | class TestTeeIO < MiniTest::Unit::TestCase 5 | def setup 6 | @default = Termistat::Config.new 7 | @custom = Termistat::Config.new( 8 | :position => :top_right, 9 | :align => :left, 10 | ) 11 | end 12 | 13 | def test_configuration 14 | assert_equal :top_right, @default.position 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /examples/file_copy.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('../../lib', __FILE__) 2 | require 'termistat' 3 | 4 | class FileCopy 5 | include Termistat 6 | 7 | def simulate 8 | ('a'..'z').to_a.each_with_index do |letter, index| 9 | status_bar("%0.1f%% complete" % ((index / 26.to_f) * 100)) 10 | puts "copying /path/to/file/#{ letter }..." 11 | sleep rand(0.5) 12 | end 13 | end 14 | end 15 | 16 | fc = FileCopy.new 17 | fc.simulate 18 | -------------------------------------------------------------------------------- /test/units/test_tee_io.rb: -------------------------------------------------------------------------------- 1 | require_relative '../helper' 2 | require 'termistat/tee_io' 3 | 4 | class TestTeeIO < MiniTest::Unit::TestCase 5 | def test_callback 6 | @capture = nil 7 | @tee_io = Termistat::TeeIO.new {|msg| @capture = msg } 8 | @tee_io.print "foo" 9 | assert_equal "foo", @capture 10 | end 11 | 12 | def test_output_is_recorded 13 | @tee_io = Termistat::TeeIO.new {|*args|} 14 | @tee_io.puts "foo" 15 | assert_equal "foo\n", @tee_io.string 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/termistat/tee_io.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | 3 | module Termistat 4 | # 5 | # TeeIO wraps a StringIO object such that writes to the IO can trigger events 6 | # immediately, but the entirety of the string is available later. 7 | # 8 | # initialize method takes a block that will be called on every write. 9 | # 10 | class TeeIO < StringIO 11 | # create a new TeeIO object 12 | # 13 | # == Example 14 | # $stdout = TeeIO.new {|str| STDOUT.puts str.reverse } 15 | # 16 | def initialize(&block) 17 | @io = StringIO.new 18 | @callback = block 19 | end 20 | 21 | def write(chars) 22 | @io.write(chars) 23 | @callback.call(chars) 24 | end 25 | 26 | def string 27 | @io.string 28 | end 29 | 30 | def flush 31 | @io.flush 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /termistat.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "termistat/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "termistat" 7 | s.version = Termistat::VERSION 8 | s.authors = ["Solomon White"] 9 | s.email = ["rubysolo@gmail.com"] 10 | s.homepage = "https://github.com/rubysolo/termistat" 11 | s.summary = %q{Terminal status bar} 12 | s.description = %q{Display status bar overlay for summary information} 13 | 14 | s.rubyforge_project = "termistat" 15 | 16 | s.add_runtime_dependency "ffi-ncurses" 17 | s.add_development_dependency "rake" 18 | s.add_development_dependency "minitest" 19 | 20 | s.files = `git ls-files`.split("\n") 21 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 22 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 23 | s.require_paths = ["lib"] 24 | end 25 | -------------------------------------------------------------------------------- /test/units/test_termistat.rb: -------------------------------------------------------------------------------- 1 | require_relative '../helper' 2 | require 'termistat' 3 | 4 | class Included 5 | include Termistat 6 | end 7 | 8 | class TestTermistat < MiniTest::Unit::TestCase 9 | def setup 10 | Termistat.config = nil 11 | @included = Included.new 12 | end 13 | 14 | def test_status_bar_method 15 | assert @included.respond_to?(:status_bar) 16 | end 17 | 18 | def test_ncurses_initialization 19 | # @included.status_bar "hello" 20 | end 21 | 22 | def test_default_configuration 23 | assert_equal :top_right, Termistat.config.position 24 | end 25 | 26 | def test_configuration 27 | Termistat.config do 28 | position :top_left 29 | align :center 30 | end 31 | 32 | assert_equal :top_left, Termistat.config.position 33 | end 34 | 35 | def test_text_alignment 36 | assert_equal "foo ", Termistat.formatted_message("foo", :left, 10) 37 | assert_equal " foo", Termistat.formatted_message("foo", :right, 10) 38 | assert_equal " foo ", Termistat.formatted_message("foo", :center, 10) 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2011 Solomon White 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/termistat/config.rb: -------------------------------------------------------------------------------- 1 | module Termistat 2 | # 3 | # The configuration class and DSL for Termistat 4 | # 5 | class Config 6 | def initialize(options={}) #:notnew: 7 | @options = { 8 | :position => :top_right, 9 | :align => :left, 10 | :foreground => :white, 11 | :background => :blue, 12 | }.merge(options) 13 | end 14 | 15 | # 16 | # position of the status bar on the terminal 17 | # 18 | # === Supported Options 19 | # * +:top_left+ : left half of top line 20 | # * +:top+ : full top line 21 | # * +:top_right+ : right half of top line 22 | # 23 | def position(position=nil) 24 | @options[:position] = position unless position.nil? 25 | @options[:position] 26 | end 27 | 28 | # 29 | # alignment of text within the status bar 30 | # 31 | # === Supported Options 32 | # * +:left+ 33 | # * +:center+ 34 | # * +:right+ 35 | # 36 | def align(align=nil) 37 | @options[:align] = align unless align.nil? 38 | @options[:align] 39 | end 40 | 41 | # 42 | # color of foreground (text) 43 | # 44 | # === Supported Options 45 | # * +:black+ 46 | # * +:red+ 47 | # * +:green+ 48 | # * +:yellow+ 49 | # * +:blue+ 50 | # * +:magenta+ 51 | # * +:cyan+ 52 | # * +:white+ 53 | # 54 | def foreground(color=nil) 55 | @options[:foreground] = color unless color.nil? 56 | @options[:foreground] 57 | end 58 | 59 | # 60 | # color of background 61 | # 62 | # === Supported Options 63 | # * +:black+ 64 | # * +:red+ 65 | # * +:green+ 66 | # * +:yellow+ 67 | # * +:blue+ 68 | # * +:magenta+ 69 | # * +:cyan+ 70 | # * +:white+ 71 | # 72 | def background(color=nil) 73 | @options[:background] = color unless color.nil? 74 | @options[:background] 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Termistat 2 | 3 | Termistat displays an ncurses-based status bar atop your STDOUT. 4 | 5 | ## Scenario: 6 | 7 | You have a long-running process that writes detailed log information to 8 | standard output. You would like to also be able to determine overall 9 | progress at a glance. You could scatter status lines among the log 10 | lines, but that's messy and can be hard to find. You could ditch the 11 | detail output and display status only, but then you lose the sense of 12 | what's going on at the detail level. 13 | 14 | ## The Solution 15 | 16 | Termistat gives you the best of both worlds. You still have your 17 | detailed information, but you also have a status bar overlay with which 18 | you can display summary information. 19 | 20 | ## Screenshot 21 | 22 | Here's what termistat looks like in action: 23 | 24 | ![screenshot](https://github.com/rubysolo/termistat/raw/master/examples/file_copy.png) 25 | 26 | 27 | ## Installation: 28 | 29 | gem install termistat 30 | 31 | ## Usage: 32 | 33 | Include the module in your class, and you get a `status_bar` method. 34 | 35 | class VerboseProcess 36 | include Termistat 37 | 38 | def perform 39 | 1000.times do |index| 40 | status_bar progress(index / 1000.0) 41 | $stdout.puts "This is normal log entry number #{ index }" 42 | 43 | # do some hard work 44 | sleep(rand(10)) 45 | end 46 | end 47 | 48 | def progress(pct) 49 | "status: %0.2f% complete" % (pct * 100) 50 | end 51 | end 52 | 53 | Don't want to or can't include? No problem, you can use the module 54 | method. 55 | 56 | class VerboseProcess 57 | def perform 58 | 1000.times do |index| 59 | Termistat.status_bar progress(index / 1000.0) 60 | $stdout.puts "This is normal log entry number #{ index }" 61 | 62 | # do some hard work 63 | sleep(rand(10)) 64 | end 65 | end 66 | 67 | def progress(pct) 68 | "status: %0.2f complete" % pct 69 | end 70 | end 71 | 72 | ## Configuration: 73 | 74 | Termistat provides a default configuration that will display your status 75 | bar in the top right corner of your terminal. Using the configuration 76 | DSL, you can customize the status bar to your liking. 77 | 78 | Termistat.config do 79 | position :top 80 | align :center 81 | background :black 82 | foreground :magenta 83 | end 84 | 85 | ## Requirements 86 | 87 | * ffi-ncurses gem 88 | * ncurses (tested with homebrew-installed ncurses) 89 | 90 | ## License 91 | 92 | The MIT License (MIT) 93 | Copyright (c) 2011 Solomon White 94 | 95 | Permission is hereby granted, free of charge, to any person obtaining a copy 96 | of this software and associated documentation files (the "Software"), to deal 97 | in the Software without restriction, including without limitation the rights 98 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 99 | copies of the Software, and to permit persons to whom the Software is 100 | furnished to do so, subject to the following conditions: 101 | 102 | The above copyright notice and this permission notice shall be included in all 103 | copies or substantial portions of the Software. 104 | 105 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 106 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 107 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 108 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 109 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 110 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 111 | SOFTWARE. 112 | -------------------------------------------------------------------------------- /lib/termistat.rb: -------------------------------------------------------------------------------- 1 | require 'termistat/config' 2 | require 'termistat/tee_io' 3 | require 'termistat/version' 4 | 5 | begin 6 | require 'ffi-ncurses' 7 | rescue LoadError 8 | require 'rubygems' 9 | require 'ffi-ncurses' 10 | end 11 | 12 | # 13 | # Termistat is a status bar for your terminal 14 | # 15 | # :title:Termistat 16 | 17 | module Termistat 18 | # 19 | # The +status_bar+ instance method initializes the status bar if necessary 20 | # and displays your message. 21 | # 22 | # === Parameters 23 | # * message = the messge you want to display 24 | # 25 | # === Example 26 | # include Termistat 27 | # status_bar "37% complete" 28 | # 29 | def status_bar(message) 30 | Termistat.status_bar message 31 | end 32 | 33 | class << self 34 | # 35 | # the +status_bar+ class method initializes the status bar if necessary 36 | # and displays your message. 37 | # 38 | # === Parameters 39 | # * message = the messge you want to display 40 | # 41 | # === Example 42 | # Termistat.status_bar "37% complete" 43 | # 44 | def status_bar(message) 45 | setup unless @stdscr 46 | m = formatted_message(message, config.align, status_bar_width) 47 | 48 | FFI::NCurses.wmove @status, 0, 0 49 | FFI::NCurses.wattr_set @status, FFI::NCurses::A_NORMAL, 1, nil 50 | FFI::NCurses.waddstr @status, m 51 | FFI::NCurses.wrefresh @status 52 | end 53 | 54 | # 55 | # +config+ either returns the active configuration or (when a block is 56 | # passed), sets up the configuration DSL. See Termistat::Config for 57 | # supported parameters and options. 58 | # 59 | # === Example 60 | # Termistat.config do 61 | # align :left 62 | # end 63 | # 64 | def config(&block) 65 | if block_given? 66 | c = Config.new 67 | c.instance_eval(&block) 68 | @config = c 69 | end 70 | 71 | @config ||= Config.new 72 | end 73 | 74 | #:enddoc: 75 | def config=(config) 76 | @config = config 77 | end 78 | 79 | def setup 80 | # set up ncurses standard screen 81 | @stdscr = FFI::NCurses.initscr 82 | FFI::NCurses.scrollok @stdscr, 1 83 | 84 | # set up ncurses status bar 85 | @height, @width = FFI::NCurses.getmaxyx(@stdscr) 86 | 87 | @status = FFI::NCurses.newwin(*status_bar_params) 88 | FFI::NCurses.scrollok @status, 0 89 | 90 | # set up colors 91 | FFI::NCurses.start_color 92 | background = FFI::NCurses::Color.const_get(config.background.to_s.upcase) 93 | foreground = FFI::NCurses::Color.const_get(config.foreground.to_s.upcase) 94 | FFI::NCurses.init_pair(1, foreground, background) 95 | 96 | # hide cursor 97 | FFI::NCurses.curs_set 0 98 | 99 | # redirect stdout 100 | $stdout = TeeIO.new do |msg| 101 | FFI::NCurses.addstr msg 102 | FFI::NCurses.refresh 103 | 104 | FFI::NCurses.touchwin @status 105 | FFI::NCurses.wrefresh @status 106 | end 107 | 108 | at_exit do 109 | FFI::NCurses.endwin 110 | 111 | output = $stdout.string 112 | $stdout = STDOUT 113 | puts output 114 | end 115 | end 116 | 117 | def status_bar_params 118 | case config.position 119 | when Array 120 | config.position 121 | when :top 122 | [1, @width, 0, 0] 123 | 124 | when :top_right 125 | x = @width / 2 126 | w = @width - x 127 | [1, w, 0, x] 128 | 129 | when :top_left 130 | x = @width / 2 131 | w = @width - x 132 | [1, w, 0, 0] 133 | 134 | end 135 | end 136 | 137 | def status_bar_width 138 | status_bar_params[1] 139 | end 140 | 141 | def formatted_message(string, alignment, width) 142 | return string.center(width) if :center === alignment 143 | "%#{ :left === alignment ? '-' : '' }#{ width }s" % string 144 | end 145 | end 146 | end 147 | --------------------------------------------------------------------------------