├── .gitignore ├── CONTRIBUTING ├── COPYING ├── INSTALLING ├── README ├── RELEASING ├── Rakefile ├── bcat.gemspec ├── bin ├── a2h ├── bcat └── btee ├── contrib └── bman ├── lib ├── bcat.rb └── bcat │ ├── ansi.rb │ ├── browser.rb │ ├── html.rb │ ├── reader.rb │ └── server.rb ├── man ├── a2h.1 ├── a2h.1.ronn ├── bcat.1 ├── bcat.1.ronn ├── btee.1 ├── btee.1.ronn └── index.html └── test ├── contest.rb ├── test_bcat_a2h.rb ├── test_bcat_ansi.rb ├── test_bcat_browser.rb └── test_bcat_head_parser.rb /.gitignore: -------------------------------------------------------------------------------- 1 | bcat.1 2 | bcat.1.html 3 | btee.1 4 | btee.1.html 5 | a2h.1 6 | a2h.1.html 7 | bcat-modules.7 8 | bcat-modules.7.html 9 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Please submit pull requests to: 2 | 3 | 4 | 5 | For information on forking repositories and sending pull requests: 6 | 7 | 8 | 9 | If you prefer sending text patches, please use git-format-patch(1) to generate 10 | them so I can attribute you properly. Text patches can be submitted to the issue 11 | tracker: 12 | 13 | 14 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | bcat 2 | Copyright (c) 2010 Ryan Tomayko 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 6 | deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | sell 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 12 | all 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 17 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /INSTALLING: -------------------------------------------------------------------------------- 1 | bcat is known to work under MacOS X, Linux, and FreeBSD (other UNIX-like 2 | environments with freedesktop.org integration should work fine too). Progressive 3 | output has been tested under Safari, Firefox, Chrome, and GNOME Epiphany. 4 | 5 | bcat is written in Ruby and requires a basic Ruby installation. Install bcat 6 | using the gem command: 7 | 8 | $ gem install bcat 9 | 10 | bcat depends on the rack package. 11 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | bcat 2 | http://github.com/rtomayko/bcat 3 | git clone git://github.com/rtomayko/bcat.git 4 | gem install bcat 5 | 6 | NOTE: This repository is archived and no longer actively maintained by @rtomayko 7 | as of 2017-11-08. 8 | 9 | bcat is a pipe to browser utility for use at the shell and within editors like 10 | Vim or Emacs. It reads from standard input, or one or more files, and streams 11 | output to a browser window: 12 | 13 | $ echo "hi mom" |bcat 14 | $ echo "hi mom" |bcat -T 'Important Message' 15 | $ echo "hi mom" |bcat -b chrome 16 | 17 | Plain text is converted to simple preformatted HTML with rudimentary support for 18 | ANSI/VT100 escape sequences (color, background, bold, underline, etc.) 19 | 20 | You can also pipe HTML into bcat, in which case input is written through to the 21 | browser unmodified: 22 | 23 | $ echo "

hi mom

" |bcat 24 | $ echo "*hi mom*" |markdown |bcat 25 | 26 | Output is displayed progressively as it's being read, making bcat especially 27 | useful with build tools or even commands like tail(1) that generate output over 28 | longer periods of time: 29 | 30 | $ make all |bcat 31 | $ rake test |bcat 32 | $ tail -f /var/log/syslog |bcat 33 | $ (while printf .; do sleep 1; done) |bcat 34 | 35 | See the bcat(1) EXAMPLES section for more on using bcat from the shell or within 36 | editors like Vim: 37 | 38 | 39 | bcat was inspired by TextMate's HTML output capabilities and a desire to have 40 | them at the shell and also within editors like Vim or Emacs. See: 41 | 42 | 43 | 44 | Only a small portion of TextMate's HTML output features are currently supported, 45 | although more will be added in the future (like hyperlinking file:line 46 | references and injecting CSS/JavaScript). 47 | 48 | See the INSTALLING, COPYING, and CONTRIBUTING files for more information on 49 | those things. 50 | 51 | Copyright (c) 2010 by Ryan Tomayko 52 | -------------------------------------------------------------------------------- /RELEASING: -------------------------------------------------------------------------------- 1 | RELEASING 2 | 3 | 1. Change Bcat::VERSION in lib/bcat.rb 4 | 2. rake bcat.gemspec 5 | 3. rake package 6 | 4. git add lib/bcat.rb bcat.gemspec 7 | 5. git commit -m "0.5.3 release" 8 | 6. git tag v0.5.3 9 | 7. git push origin master v0.5.3 10 | 8. gem push bcat-0.5.3.gem 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'date' 2 | task :default => :test 3 | 4 | ROOTDIR = File.expand_path('..', __FILE__).sub(/#{Dir.pwd}(?=\/)/, '.') 5 | LIBDIR = "#{ROOTDIR}/lib" 6 | BINDIR = "#{ROOTDIR}/bin" 7 | 8 | task :environment do 9 | $:.unshift ROOTDIR if !$:.include?(ROOTDIR) 10 | $:.unshift LIBDIR if !$:.include?(LIBDIR) 11 | ENV['RUBYLIB'] = $LOAD_PATH.join(':') 12 | ENV['PATH'] = "#{BINDIR}:#{ENV['PATH']}" 13 | end 14 | 15 | desc 'Run tests' 16 | task :test => :environment do 17 | $LOAD_PATH.unshift "#{ROOTDIR}/test" 18 | Dir['test/test_*.rb'].each { |f| require(f) } 19 | end 20 | 21 | def source_version 22 | @source_version ||= `ruby -Ilib -rbcat -e 'puts Bcat::VERSION'`.chomp 23 | end 24 | 25 | require 'rubygems' 26 | $spec = eval(File.read('bcat.gemspec')) 27 | 28 | desc "Build gem package" 29 | task :package => 'bcat.gemspec' do 30 | sh "gem build bcat.gemspec" 31 | end 32 | 33 | desc 'Build the manual' 34 | task :man do 35 | ENV['RONN_MANUAL'] = "Bcat #{source_version}" 36 | ENV['RONN_ORGANIZATION'] = "Ryan Tomayko" 37 | sh "ronn -stoc -w -r5 man/*.ronn" 38 | end 39 | 40 | desc 'Publish to github pages' 41 | task :pages => :man do 42 | puts '----------------------------------------------' 43 | puts 'Rebuilding pages ...' 44 | verbose(false) { 45 | rm_rf 'pages' 46 | push_url = `git remote show origin`.grep(/Push.*URL/).first[/git@.*/] 47 | sh " 48 | set -e 49 | git fetch -q origin 50 | rev=$(git rev-parse origin/gh-pages) 51 | git clone -q -b gh-pages . pages 52 | cd pages 53 | git reset --hard $rev 54 | rm -f *.html 55 | cp -rp ../man/*.html ../man/index.* ./ 56 | git add -u *.html index.* 57 | git commit -m 'rebuild manual' 58 | git push #{push_url} gh-pages 59 | ", :verbose => false 60 | } 61 | end 62 | 63 | file 'bcat.gemspec' => FileList['{lib,test,bin}/**','Rakefile'] do |f| 64 | # read spec file and split out manifest section 65 | spec = File.read(f.name) 66 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 67 | # replace version and date 68 | head.sub!(/\.version = '.*'/, ".version = '#{source_version}'") 69 | head.sub!(/\.date = '.*'/, ".date = '#{Date.today.to_s}'") 70 | # determine file list from git ls-files 71 | files = `git ls-files`. 72 | split("\n"). 73 | sort. 74 | reject{ |file| file =~ /^\./ }. 75 | reject { |file| file =~ /^doc/ }. 76 | map{ |file| " #{file}" }. 77 | join("\n") 78 | # piece file back together and write... 79 | manifest = " s.files = %w[\n#{files}\n ]\n" 80 | spec = [head,manifest,tail].join(" # = MANIFEST =\n") 81 | File.open(f.name, 'w') { |io| io.write(spec) } 82 | puts "updated #{f.name}" 83 | end 84 | -------------------------------------------------------------------------------- /bcat.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'bcat' 3 | s.version = '0.6.2' 4 | s.date = '2011-09-10' 5 | 6 | s.description = "pipe to browser utility" 7 | s.summary = 8 | "Concatenate input from standard input, or one or more files, " + 9 | "and write progressive output to a browser." 10 | 11 | s.authors = ["Ryan Tomayko"] 12 | s.email = "rtomayko@gmail.com" 13 | 14 | # = MANIFEST = 15 | s.files = %w[ 16 | CONTRIBUTING 17 | COPYING 18 | INSTALLING 19 | README 20 | RELEASING 21 | Rakefile 22 | bcat.gemspec 23 | bin/a2h 24 | bin/bcat 25 | bin/btee 26 | contrib/bman 27 | lib/bcat.rb 28 | lib/bcat/ansi.rb 29 | lib/bcat/browser.rb 30 | lib/bcat/html.rb 31 | lib/bcat/reader.rb 32 | lib/bcat/server.rb 33 | man/a2h.1 34 | man/a2h.1.ronn 35 | man/bcat.1 36 | man/bcat.1.ronn 37 | man/btee.1 38 | man/btee.1.ronn 39 | man/index.html 40 | test/contest.rb 41 | test/test_bcat_a2h.rb 42 | test/test_bcat_ansi.rb 43 | test/test_bcat_browser.rb 44 | test/test_bcat_head_parser.rb 45 | ] 46 | # = MANIFEST = 47 | 48 | s.default_executable = 'bcat' 49 | s.executables = ['a2h', 'bcat', 'btee'] 50 | 51 | s.test_files = s.files.select {|path| path =~ /^test\/.*_test.rb/} 52 | s.add_dependency 'rack', '~> 1.0' 53 | 54 | s.extra_rdoc_files = %w[COPYING] 55 | 56 | s.has_rdoc = true 57 | s.homepage = "http://rtomayko.github.com/bcat/" 58 | s.rdoc_options = ["--line-numbers", "--inline-source"] 59 | s.require_paths = %w[lib] 60 | end 61 | -------------------------------------------------------------------------------- /bin/a2h: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Usage: a2h [-] [...] 3 | # Convert ANSI/VT100 escape sequences to HTML. 4 | require 'bcat/ansi' 5 | require 'bcat/reader' 6 | 7 | $stdin.sync = true 8 | $stdout.sync = true 9 | 10 | ARGV.push '-' if ARGV.empty? 11 | 12 | reader = Bcat::Reader.new(false, ARGV.to_a) 13 | reader.open 14 | filter = Bcat::ANSI.new(reader) 15 | filter.each { |text| $stdout.write(text) } 16 | -------------------------------------------------------------------------------- /bin/bcat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #/ Usage: bcat [-htp] [-a] [-b ] [-T ] [<file>]... 3 | #/ bcat [-htp] [-a] [-b <browser>] [-T <title>] -c command... 4 | #/ btee <options> [<file>]... 5 | #/ Pipe to browser utility. Read standard input, possibly one or more <file>s, 6 | #/ and write concatenated / formatted output to browser. When invoked as btee, 7 | #/ also write all input back to standard output. 8 | #/ 9 | #/ Display options: 10 | #/ -b, --browser=<browser> open <browser> instead of system default browser 11 | #/ -T, --title=<text> use <text> as the browser title 12 | #/ -a, --ansi convert ANSI (color) escape sequences to HTML 13 | #/ 14 | #/ Input format (auto detected by default): 15 | #/ -h, --html input is already HTML encoded, doc or fragment 16 | #/ -t, --text input is unencoded text 17 | #/ 18 | #/ Misc options: 19 | #/ -c, --command read the standard output of command 20 | #/ -p, --persist serve until interrupted, allowing reload 21 | #/ -d, --debug enable verbose debug logging on stderr 22 | require 'optparse' 23 | 24 | options = { 25 | :Host => '127.0.0.1', 26 | :Port => 0, 27 | :format => nil, 28 | :title => nil, 29 | :browser => (ENV['BCAT_BROWSER'].to_s.size > 0 ? ENV['BCAT_BROWSER'] : 'default'), 30 | :ansi => false, 31 | :persist => false, 32 | :command => false, 33 | :debug => false, 34 | :tee => !!($0 =~ /tee$/) 35 | } 36 | 37 | (class <<self;self;end).send(:define_method, :notice) { |message| 38 | warn "#{File.basename($0)}: #{message}" if options[:debug] } 39 | 40 | ARGV.options do |argv| 41 | argv.on('-h', '--html') { options[:format] = 'html' } 42 | argv.on('-t', '--text') { options[:format] = 'text' } 43 | argv.on('-b', '--browser=v') { |app| options[:browser] = app } 44 | argv.on('-T', '--title=v') { |text| options[:title] = text } 45 | argv.on('-a', '--ansi') { options[:ansi] = true } 46 | argv.on('-p', '--persist') { options[:persist] = true } 47 | argv.on('-c', '--command') { options[:command] = true } 48 | argv.on('-d', '--debug') { options[:debug] = true } 49 | argv.on('--host=v') { |addr| options[:Host] = addr } 50 | argv.on('--port=v') { |port| options[:Port] = port.to_i } 51 | argv.on_tail('--help') { exec "grep ^#/ <#{__FILE__} | cut -c4-" } 52 | argv.parse! 53 | end 54 | ARGV.push '-' if ARGV.empty? 55 | 56 | require 'bcat' 57 | notice "loaded bcat v#{Bcat::VERSION}" 58 | 59 | browser = Bcat::Browser.new(options[:browser]) 60 | notice "env BCAT_BROWSER=#{options[:browser].inspect}" 61 | notice "env BCAT_COMMAND='#{browser.command}'" 62 | 63 | notice "starting server" 64 | pid = nil 65 | begin 66 | bcat = Bcat.new(ARGV, options) 67 | bcat.serve! do |sock| 68 | port = sock.addr[1] 69 | url = "http://#{bcat[:Host]}:#{port}/#{File.basename(Dir.pwd)}" 70 | pid = browser.open(url) 71 | end 72 | rescue Interrupt 73 | notice "interrupt" 74 | end 75 | 76 | Process.wait(pid) if pid 77 | status = $?.exitstatus 78 | notice "browser [pid: #{pid}] exited with #{status}" 79 | exit status 80 | -------------------------------------------------------------------------------- /bin/btee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | load File.expand_path('../bcat', __FILE__) 3 | -------------------------------------------------------------------------------- /contrib/bman: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Usage: bman <man-args> 3 | # Open manual page in browser with bcat. 4 | # 5 | # Original zsh version by Nick Stenning <http://github.com/nickstenning> 6 | # http://github.com/rtomayko/bcat/issues#issue/8 7 | # 8 | # Ported to POSIX shell by Ryan Tomayko <http://github.com/rtomayko> 9 | set -e 10 | 11 | # we use normal bold and underline 12 | BOLD="\033[1m" 13 | EM="\033[3m" 14 | RESET="\033[0m" 15 | 16 | # pass argv directly over to man 17 | man "$@" | 18 | 19 | # replace ^H based bold and underline with ansi equivelants 20 | sed " 21 | s/_\(.\)/"$(echo "$EM")"\1"$(echo "$RESET")"/g 22 | s/\(.\)\1/"$(echo "$BOLD")"\1"$(echo "$RESET")"/g 23 | " | 24 | 25 | # pipe it into bcat 26 | bcat 27 | -------------------------------------------------------------------------------- /lib/bcat.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require 'bcat/reader' 3 | require 'bcat/ansi' 4 | require 'bcat/html' 5 | require 'bcat/server' 6 | require 'bcat/browser' 7 | 8 | class Bcat 9 | VERSION = '0.6.2' 10 | include Rack::Utils 11 | 12 | attr_reader :format 13 | 14 | def initialize(args=[], config={}) 15 | @config = {:Host => '127.0.0.1', :Port => 8091}.merge(config) 16 | @reader = Bcat::Reader.new(@config[:command], args) 17 | @format = @config[:format] 18 | end 19 | 20 | def [](key) 21 | @config[key] 22 | end 23 | 24 | def to_app 25 | app = self 26 | Rack::Builder.new do 27 | use Rack::Chunked 28 | run app 29 | end 30 | end 31 | 32 | def serve!(&bk) 33 | Bcat::Server.run to_app, @config, &bk 34 | end 35 | 36 | def call(env) 37 | notice "#{env['REQUEST_METHOD']} #{env['PATH_INFO'].inspect}" 38 | [200, {"Content-Type" => "text/html;charset=utf-8"}, self] 39 | end 40 | 41 | def assemble 42 | @reader.open 43 | 44 | @format = @reader.sniff if @format.nil? 45 | 46 | @filter = @reader 47 | @filter = TeeFilter.new(@filter) if @config[:tee] 48 | @filter = TextFilter.new(@filter) if @format == 'text' 49 | @filter = ANSI.new(@filter) if @format == 'text' || @config[:ansi] 50 | end 51 | 52 | def each 53 | assemble 54 | 55 | head_parser = Bcat::HeadParser.new 56 | 57 | @filter.each do |buf| 58 | if head_parser.nil? 59 | yield buf 60 | elsif head_parser.feed(buf) 61 | yield content_for_head(inject=head_parser.head) 62 | yield "\n" 63 | yield head_parser.body 64 | head_parser = nil 65 | end 66 | end 67 | 68 | if head_parser 69 | yield content_for_head(inject=head_parser.head) + 70 | "\n" + 71 | head_parser.body 72 | end 73 | 74 | yield foot 75 | rescue Errno::EINVAL 76 | # socket was closed 77 | notice "browser client went away" 78 | rescue => boom 79 | notice "boom: #{boom.class}: #{boom.to_s}" 80 | raise 81 | end 82 | 83 | def content_for_head(inject='') 84 | [ 85 | "\n" * 1000, 86 | "<!DOCTYPE html>", 87 | "<html>", 88 | "<head>", 89 | "<!-- bcat was here -->", 90 | inject.to_s, 91 | "<link href=\"data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAF0lEQVRIx2NgGAWjYBSMglEwCkbBSAcACBAAAeaR9cIAAAAASUVORK5CYII=\" rel=\"icon\" type=\"image/x-icon\" />", 92 | "<title>#{self[:title] || 'bcat'}", 93 | "" 94 | ].join("\n") 95 | end 96 | 97 | def foot 98 | "\n\n" 99 | end 100 | 101 | def escape_js(string) 102 | string = string.gsub(/['\\]/) { |char| "\\#{char}" } 103 | string.gsub!(/\n/, '\n') 104 | string 105 | end 106 | 107 | def close 108 | unless @config[:persist] 109 | notice "closing with interrupt" 110 | raise Interrupt, "connection closed" 111 | end 112 | end 113 | 114 | def notice(message) 115 | return if !@config[:debug] 116 | warn "#{File.basename($0)}: #{message}" 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/bcat/ansi.rb: -------------------------------------------------------------------------------- 1 | class Bcat 2 | 3 | # Converts ANSI color sequences to HTML. 4 | # 5 | # The ANSI module is based on code from the following libraries: 6 | # 7 | # ansi2html.sh: 8 | # http://github.com/pixelb/scripts/blob/master/scripts/ansi2html.sh 9 | # 10 | # HTML::FromANSI: 11 | # http://cpansearch.perl.org/src/NUFFIN/HTML-FromANSI-2.03/lib/HTML/FromANSI.pm 12 | class ANSI 13 | ESCAPE = "\x1b" 14 | 15 | # Linux console palette 16 | STYLES = { 17 | 'ef0' => 'color:#000', 18 | 'ef1' => 'color:#A00', 19 | 'ef2' => 'color:#0A0', 20 | 'ef3' => 'color:#A50', 21 | 'ef4' => 'color:#00A', 22 | 'ef5' => 'color:#A0A', 23 | 'ef6' => 'color:#0AA', 24 | 'ef7' => 'color:#AAA', 25 | 'ef8' => 'color:#555', 26 | 'ef9' => 'color:#F55', 27 | 'ef10' => 'color:#5F5', 28 | 'ef11' => 'color:#FF5', 29 | 'ef12' => 'color:#55F', 30 | 'ef13' => 'color:#F5F', 31 | 'ef14' => 'color:#5FF', 32 | 'ef15' => 'color:#FFF', 33 | 'eb0' => 'background-color:#000', 34 | 'eb1' => 'background-color:#A00', 35 | 'eb2' => 'background-color:#0A0', 36 | 'eb3' => 'background-color:#A50', 37 | 'eb4' => 'background-color:#00A', 38 | 'eb5' => 'background-color:#A0A', 39 | 'eb6' => 'background-color:#0AA', 40 | 'eb7' => 'background-color:#AAA', 41 | 'eb8' => 'background-color:#555', 42 | 'eb9' => 'background-color:#F55', 43 | 'eb10' => 'background-color:#5F5', 44 | 'eb11' => 'background-color:#FF5', 45 | 'eb12' => 'background-color:#55F', 46 | 'eb13' => 'background-color:#F5F', 47 | 'eb14' => 'background-color:#5FF', 48 | 'eb15' => 'background-color:#FFF' 49 | } 50 | 51 | ## 52 | # The default xterm 256 colour palette 53 | 54 | (0..5).each do |red| 55 | (0..5).each do |green| 56 | (0..5).each do |blue| 57 | c = 16 + (red * 36) + (green * 6) + blue 58 | r = red > 0 ? red * 40 + 55 : 0 59 | g = green > 0 ? green * 40 + 55 : 0 60 | b = blue > 0 ? blue * 40 + 55 : 0 61 | STYLES["ef#{c}"] = "color:#%2.2x%2.2x%2.2x" % [r, g, b] 62 | STYLES["eb#{c}"] = "background-color:#%2.2x%2.2x%2.2x" % [r, g, b] 63 | end 64 | end 65 | end 66 | 67 | (0..23).each do |gray| 68 | c = gray+232 69 | l = gray*10 + 8 70 | STYLES["ef#{c}"] = "color:#%2.2x%2.2x%2.2x" % [l, l, l] 71 | STYLES["eb#{c}"] = "background-color:#%2.2x%2.2x%2.2x" % [l, l, l] 72 | end 73 | 74 | def initialize(input) 75 | @input = 76 | if input.respond_to?(:to_str) 77 | [input] 78 | elsif !input.respond_to?(:each) 79 | raise ArgumentError, "input must respond to each" 80 | else 81 | input 82 | end 83 | @stack = [] 84 | end 85 | 86 | def to_html 87 | buf = [] 88 | each { |chunk| buf << chunk } 89 | buf.join 90 | end 91 | 92 | def each 93 | buf = '' 94 | @input.each do |chunk| 95 | buf << chunk 96 | tokenize(buf) do |tok, data| 97 | case tok 98 | when :text 99 | yield data 100 | when :display 101 | case code = data 102 | when 0 ; yield reset_styles if @stack.any? 103 | when 1 ; yield push_tag("b") # bright 104 | when 2 ; #dim 105 | when 3, 4 ; yield push_tag("u") 106 | when 5, 6 ; yield push_tag("blink") 107 | when 7 ; #reverse 108 | when 8 ; yield push_style("display:none") 109 | when 9 ; yield push_tag("strike") 110 | when 30..37 ; yield push_style("ef#{code - 30}") 111 | when 40..47 ; yield push_style("eb#{code - 40}") 112 | when 90..97 ; yield push_style("ef#{8 + code - 90}") 113 | when 100..107 ; yield push_style("eb#{8 + code - 100}") 114 | end 115 | when :xterm256 116 | code = data 117 | yield push_style("ef#{code}") 118 | end 119 | end 120 | end 121 | yield buf if !buf.empty? 122 | yield reset_styles if @stack.any? 123 | self 124 | end 125 | 126 | def push_tag(tag, style=nil) 127 | style = STYLES[style] if style && !style.include?(':') 128 | @stack.push tag 129 | [ "<#{tag}", 130 | (" style='#{style}'" if style), 131 | ">" 132 | ].join 133 | end 134 | 135 | def push_style(style) 136 | push_tag "span", style 137 | end 138 | 139 | def reset_styles 140 | stack, @stack = @stack, [] 141 | stack.reverse.map { |tag| "" }.join 142 | end 143 | 144 | def tokenize(text) 145 | tokens = [ 146 | # characters to remove completely 147 | [/\A\x08+/, lambda { |m| '' }], 148 | 149 | [/\A\x1b\[38;5;(\d+)m/, lambda { |m| yield :xterm256, $1.to_i; '' } ], 150 | 151 | # ansi escape sequences that mess with the display 152 | [/\A\x1b\[((?:\d{1,3};?)+|)m/, lambda { |m| 153 | m = '0' if m.strip.empty? 154 | m.chomp(';').split(';'). 155 | each { |code| yield :display, code.to_i }; 156 | '' }], 157 | 158 | # malformed sequences 159 | [/\A\x1b\[?[\d;]{0,3}/, lambda { |m| '' }], 160 | 161 | # real text 162 | [/\A([^\x1b\x08]+)/m, lambda { |m| yield :text, m; '' }] 163 | ] 164 | 165 | while (size = text.size) > 0 166 | tokens.each do |pattern, sub| 167 | break if text.sub!(pattern) { sub.call($1) } 168 | end 169 | break if text.size == size 170 | end 171 | end 172 | 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /lib/bcat/browser.rb: -------------------------------------------------------------------------------- 1 | class Bcat 2 | class Browser 3 | ENVIRONMENT = 4 | case `uname` 5 | when /Darwin/ ; 'Darwin' 6 | when /Linux/, /BSD/ ; 'X11' 7 | else 'X11' 8 | end 9 | 10 | # browser name -> command mappings 11 | COMMANDS = { 12 | 'Darwin' => { 13 | 'default' => "open", 14 | 'safari' => "open -a Safari", 15 | 'firefox' => "open -a Firefox", 16 | 'chrome' => "open -a Google\\ Chrome", 17 | 'chromium' => "open -a Chromium", 18 | 'opera' => "open -a Opera", 19 | 'curl' => "curl -s" 20 | }, 21 | 22 | 'X11' => { 23 | 'default' => "xdg-open", 24 | 'firefox' => "firefox", 25 | 'chrome' => "google-chrome", 26 | 'chromium' => "chromium", 27 | 'mozilla' => "mozilla", 28 | 'epiphany' => "epiphany", 29 | 'curl' => "curl -s" 30 | } 31 | } 32 | 33 | # alternative names for browsers 34 | ALIASES = { 35 | 'google-chrome' => 'chrome', 36 | 'google chrome' => 'chrome', 37 | 'gnome' => 'epiphany' 38 | } 39 | 40 | def initialize(browser, command=ENV['BCAT_COMMAND']) 41 | @browser = browser 42 | @command = command 43 | end 44 | 45 | def open(url) 46 | fork do 47 | $stdin.close 48 | exec "#{command} #{shell_quote(url)}" 49 | exit! 128 50 | end 51 | end 52 | 53 | def command 54 | @command || browser_command 55 | end 56 | 57 | def browser_command(browser=@browser) 58 | browser ||= 'default' 59 | browser = browser.downcase 60 | browser = ALIASES[browser] || browser 61 | COMMANDS[ENVIRONMENT][browser] 62 | end 63 | 64 | def shell_quote(argument) 65 | '"' + argument.to_s.gsub(/([\\"`$])/) { "\\" + $1 } + '"' 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/bcat/html.rb: -------------------------------------------------------------------------------- 1 | class Bcat 2 | 3 | # Parses HTML until the first displayable body character and provides methods 4 | # for accessing head and body contents. 5 | class HeadParser 6 | attr_accessor :buf 7 | 8 | def initialize 9 | @buf = '' 10 | @head = [] 11 | @body = nil 12 | @html = nil 13 | end 14 | 15 | # Called to parse new data as it arrives. 16 | def feed(data) 17 | if complete? 18 | @body << data 19 | else 20 | @buf << data 21 | parse(@buf) 22 | end 23 | complete? 24 | end 25 | 26 | # Truthy once the first displayed character of the body has arrived. 27 | def complete? 28 | !@body.nil? 29 | end 30 | 31 | # Determine if the input is HTML. This is nil before the first non-whitespace 32 | # character is received, true if the first non-whitespace character is a 33 | # '<', and false if the first non-whitespace character is something other 34 | # than '<'. 35 | def html? 36 | @html 37 | end 38 | 39 | # The head contents without any DOCTYPE, , or tags. This should 40 | # consist of only 60 | 61 | 62 |
63 |

|bcat

64 |

pipe to browser utility

65 | 66 |

67 | README, 68 | INSTALLING, 69 | COPYING, 70 | CONTRIBUTING 71 |

72 | 73 |

Manuals

74 |
75 |
76 | bcat(1), 77 | btee(1) 78 |
79 |
browser cat and browser tee.
80 | 81 |
a2h(1)
82 |
VT100/ANSI escape sequence to HTML converter.
83 | 84 | 88 |
89 | 90 |

Examples

91 |

With build tools:

92 | 93 |
make test |bcat
 94 | rake test |bcat
 95 | 
96 | 97 |

As a clipboard viewer:

98 | 99 |
pbpaste  |bcat   # macos
100 | xclip -o |bcat   # X11
101 | 
102 | 103 |

For previewing HTML:

104 | 105 |
markdown README.md |bcat
106 | redcloth README.textile |bcat
107 | erb -T - template.erb |bcat
108 | mustache < template.mustache |bcat
109 | pygmentize -Ofull,style=colorful -f html main.c |bcat
110 | 
111 | 112 |

As a simple man pager:

113 | 114 |
export MANPAGER='col -b |bcat'
115 | man grep
116 | 
117 | 118 |

With git, selectively:

119 | 120 |
git log -p --color |bcat
121 | git diff --color HEAD@{5d} HEAD |bcat
122 | 
123 | 124 |

With git, as the default PAGER:

125 | 126 |
export GIT_PAGER=bcat
127 | git log -p
128 | git diff HEAD@{5d} HEAD
129 | 
130 | 131 |

As a log viewer:

132 | 133 |
tail -n 1000 -f /var/log/messages |bcat
134 | tail -f $RAILS_ROOT/log/development.log |bcat
135 | 
136 | 137 |

Or, a remote log viewer:

138 | 139 |
ssh example.org 'tail -n 1000 -f /var/log/syslog' |bcat
140 | 
141 | 142 |

Vim and vi Examples

143 | 144 |

Preview current buffer as HTML:

145 | 146 |
:!markdown % |bcat
147 | :!ronn -5 --pipe % |bcat
148 | 
149 | 150 |

Create keymappings:

151 | 152 |
:map ,pm :!markdown % \|bcat
153 | :map ,pp :!pygmentize -Ofull,style=colorful -f html % \|bcat
154 | 
155 | 156 |

Use with makeprg:

157 | 158 |
:set makeprg=make\ \\\|bcat
159 | :set makeprg=markdown\ %\ \\\|bcat
160 | :set makeprg=testrb\ %\ \\\|bcat
161 | 
162 | 163 |

See Also

164 |
    165 |
  • 166 |

    167 | Chris Wanstrath's 168 | browser program 169 | pipes standard input to a browser using a temporary file. It's many fewer 170 | lines of code than bcat. 171 |

    172 |
  • 173 |
  • 174 |

    175 | This excellent 176 | 177 | introduction to TextMate's HTML output features 178 | can be thought of as a bcat feature roadmap. It should one day 179 | be possible to do all of those things with bcat. 180 |

    181 |
  • 182 |
  • 183 |

    184 | uzbl is a graphical web 185 | browser (X11+gtk only) that adheres to the UNIX philosophy. 186 |

    187 |
  • 188 |
189 | 190 |

191 | Copyright © 2010 Ryan Tomayko 192 |

193 |
194 | 195 | 196 | -------------------------------------------------------------------------------- /test/contest.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | 3 | # Test::Unit loads a default test if the suite is empty, whose purpose is to 4 | # fail. Since having empty contexts is a common practice, we decided to 5 | # overwrite TestSuite#empty? in order to allow them. Having a failure when no 6 | # tests have been defined seems counter-intuitive. 7 | class Test::Unit::TestSuite 8 | def empty? 9 | false 10 | end 11 | end 12 | 13 | # Contest adds +teardown+, +test+ and +context+ as class methods, and the 14 | # instance methods +setup+ and +teardown+ now iterate on the corresponding 15 | # blocks. Note that all setup and teardown blocks must be defined with the 16 | # block syntax. Adding setup or teardown instance methods defeats the purpose 17 | # of this library. 18 | class Test::Unit::TestCase 19 | def self.setup(&block) 20 | define_method :setup do 21 | super(&block) 22 | instance_eval(&block) 23 | end 24 | end 25 | 26 | def self.teardown(&block) 27 | define_method :teardown do 28 | instance_eval(&block) 29 | super(&block) 30 | end 31 | end 32 | 33 | def self.context(name, &block) 34 | subclass = Class.new(self) 35 | remove_tests(subclass) 36 | subclass.class_eval(&block) if block_given? 37 | const_set(context_name(name), subclass) 38 | end 39 | 40 | def self.test(name, &block) 41 | define_method(test_name(name), &block) 42 | end 43 | 44 | class << self 45 | alias_method :should, :test 46 | alias_method :describe, :context 47 | end 48 | 49 | private 50 | 51 | def self.context_name(name) 52 | "Test#{sanitize_name(name).gsub(/(^| )(\w)/) { $2.upcase }}".to_sym 53 | end 54 | 55 | def self.test_name(name) 56 | "test_#{sanitize_name(name).gsub(/\s+/,'_')}".to_sym 57 | end 58 | 59 | def self.sanitize_name(name) 60 | name.gsub(/\W+/, ' ').strip 61 | end 62 | 63 | def self.remove_tests(subclass) 64 | subclass.public_instance_methods.grep(/^test_/).each do |meth| 65 | subclass.send(:undef_method, meth.to_sym) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/test_bcat_a2h.rb: -------------------------------------------------------------------------------- 1 | require 'contest' 2 | 3 | ENV['PATH'] = [File.expand_path('../../bin'), ENV['PATH']].join(':') 4 | 5 | class ANSI2HTMLCommandTest < Test::Unit::TestCase 6 | test "piping stuff through a2h" do 7 | IO.popen("a2h", 'w+') do |io| 8 | io.sync = true 9 | io.puts "hello there" 10 | io.flush 11 | assert_equal "hello there\n", io.read("hello there\n".size) 12 | io.puts "and \x1b[1mhere's some bold" 13 | assert_equal "and here's some bold\n", io.read(24) 14 | io.close_write 15 | assert_equal "", io.read(4) 16 | io.close 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/test_bcat_ansi.rb: -------------------------------------------------------------------------------- 1 | require 'contest' 2 | require 'bcat/ansi' 3 | 4 | class ANSITest < Test::Unit::TestCase 5 | test 'should not modify input string' do 6 | text = "some text" 7 | Bcat::ANSI.new(text).to_html 8 | assert_equal "some text", text 9 | end 10 | 11 | test 'passing through text with no escapes' do 12 | text = "hello\nthis is bcat\n" 13 | ansi = Bcat::ANSI.new(text) 14 | assert_equal text, ansi.to_html 15 | end 16 | 17 | test "removing backspace characters" do 18 | text = "like this" 19 | ansi = Bcat::ANSI.new(text) 20 | assert_equal "like this", ansi.to_html 21 | end 22 | 23 | test "foreground colors" do 24 | text = "colors: \x1b[30mblack\x1b[37mwhite" 25 | expect = "colors: " + 26 | "black" + 27 | "white" + 28 | "" 29 | assert_equal expect, Bcat::ANSI.new(text).to_html 30 | end 31 | 32 | test "light foreground colors" do 33 | text = "colors: \x1b[90mblack\x1b[97mwhite" 34 | expect = "colors: " + 35 | "black" + 36 | "white" + 37 | "" 38 | assert_equal expect, Bcat::ANSI.new(text).to_html 39 | end 40 | 41 | test "background colors" do 42 | text = "colors: \x1b[40mblack\x1b[47mwhite" 43 | expect = "colors: " + 44 | "black" + 45 | "white" + 46 | "" 47 | assert_equal expect, Bcat::ANSI.new(text).to_html 48 | end 49 | 50 | test "light background colors" do 51 | text = "colors: \x1b[100mblack\x1b[107mwhite" 52 | expect = "colors: " + 53 | "black" + 54 | "white" + 55 | "" 56 | assert_equal expect, Bcat::ANSI.new(text).to_html 57 | end 58 | 59 | test "strikethrough" do 60 | text = "strike: \x1b[9mthat" 61 | expect = "strike: that" 62 | assert_equal expect, Bcat::ANSI.new(text).to_html 63 | end 64 | 65 | test "blink!" do 66 | text = "blink: \x1b[5mwhat" 67 | expect = "blink: what" 68 | assert_equal expect, Bcat::ANSI.new(text).to_html 69 | end 70 | 71 | test "underline" do 72 | text = "underline: \x1b[3mstuff" 73 | expect = "underline: stuff" 74 | assert_equal expect, Bcat::ANSI.new(text).to_html 75 | end 76 | 77 | test "bold" do 78 | text = "bold: \x1b[1mstuff" 79 | expect = "bold: stuff" 80 | assert_equal expect, Bcat::ANSI.new(text).to_html 81 | end 82 | 83 | test "resetting a single sequence" do 84 | text = "\x1b[1mthis is bold\x1b[0m, but this isn't" 85 | expect = "this is bold, but this isn't" 86 | assert_equal expect, Bcat::ANSI.new(text).to_html 87 | end 88 | 89 | test "resetting many sequences" do 90 | text = "normal, \x1b[1mbold, \x1b[3munderline, \x1b[31mred\x1b[0m, normal" 91 | expect = "normal, bold, underline, " + 92 | "red, normal" 93 | assert_equal expect, Bcat::ANSI.new(text).to_html 94 | end 95 | 96 | test "resetting without an implicit 0 argument" do 97 | text = "\x1b[1mthis is bold\x1b[m, but this isn't" 98 | expect = "this is bold, but this isn't" 99 | assert_equal expect, Bcat::ANSI.new(text).to_html 100 | end 101 | 102 | test "multi-attribute sequences" do 103 | text = "normal, \x1b[1;3;31mbold, underline, and red\x1b[0m, normal" 104 | expect = "normal, " + 105 | "bold, underline, and red, normal" 106 | assert_equal expect, Bcat::ANSI.new(text).to_html 107 | end 108 | 109 | test "multi-attribute sequences with a trailing semi-colon" do 110 | text = "normal, \x1b[1;3;31;mbold, underline, and red\x1b[0m, normal" 111 | expect = "normal, " + 112 | "bold, underline, and red, normal" 113 | assert_equal expect, Bcat::ANSI.new(text).to_html 114 | end 115 | 116 | test "eating malformed sequences" do 117 | text = "\x1b[25oops forgot the 'm'" 118 | expect = "oops forgot the 'm'" 119 | assert_equal expect, Bcat::ANSI.new(text).to_html 120 | end 121 | 122 | test "xterm-256" do 123 | text = "\x1b[38;5;196mhello" 124 | expect = "hello" 125 | assert_equal expect, Bcat::ANSI.new(text).to_html 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/test_bcat_browser.rb: -------------------------------------------------------------------------------- 1 | require 'contest' 2 | require 'bcat/browser' 3 | 4 | class BrowserTest < Test::Unit::TestCase 5 | 6 | setup { @browser = Bcat::Browser.new('default', nil) } 7 | 8 | test 'shell quotes double-quotes, backticks, and parameter expansion' do 9 | assert_equal "\"http://example.com/\\\"/\\$(echo oops)/\\`echo howdy\\`\"", 10 | @browser.shell_quote("http://example.com/\"/$(echo oops)/`echo howdy`") 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/test_bcat_head_parser.rb: -------------------------------------------------------------------------------- 1 | require 'contest' 2 | require 'bcat/html' 3 | 4 | class HeadParserTest < Test::Unit::TestCase 5 | 6 | setup { @parser = Bcat::HeadParser.new } 7 | 8 | test 'starts in an unknown state' do 9 | assert @parser.html?.nil? 10 | assert @parser.buf.empty? 11 | end 12 | 13 | test 'detects non-HTML input' do 14 | @parser.feed("HOWDY

") 15 | assert_equal false, @parser.html? 16 | assert_equal '', @parser.head 17 | end 18 | 19 | test 'separates head elements from body' do 20 | @parser.feed("") 21 | @parser.feed("

HOLLA

") 22 | assert_equal "", @parser.head.strip 23 | assert_equal "\n

HOLLA

", @parser.body 24 | end 25 | 26 | test 'handles multiple head elements' do 27 | stuff = [ 28 | "", 29 | "", 30 | "" 31 | ] 32 | stuff.each { |html| @parser.feed(html) } 33 | @parser.feed("\n \n\n\n

HOLLA

") 34 | 35 | assert_equal stuff.join, @parser.head.strip 36 | end 37 | 38 | test 'handles full documents' do 39 | @parser.feed("\n") 40 | @parser.feed("YO") 41 | @parser.feed("

OY

") 42 | assert_equal "YO", @parser.head.strip 43 | assert_equal "\n

OY

", @parser.body 44 | end 45 | 46 | test 'knows when the head is fully parsed' do 47 | @parser.feed("\n") 48 | assert !@parser.complete? 49 | 50 | @parser.feed("YO") 51 | assert !@parser.complete? 52 | 53 | @parser.feed("

OY

") 54 | assert @parser.complete? 55 | end 56 | end 57 | --------------------------------------------------------------------------------