-
-
Save fecundf/be3e37c905e3c309f22603e55fce6ed9 to your computer and use it in GitHub Desktop.
Unix + windows version of Nigel Hamilton's jmp browser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl6 | |
#-------------------------------------------------------------------------------- | |
# Nigel Hamilton (2016) - Artistic Licence 2.0 | |
# | |
# jmp - Perl6 powered tools for quickly jmping to the next thing in your workflow | |
# | |
# - please add subcommands for your own workflow. For example, you could jmp to: | |
# | |
# - log files | |
# - directory listings | |
# - command-line history | |
# - test output | |
# - todo lists | |
# - error messages | |
# - documentation | |
# - web pages | |
# | |
# - To get started: | |
# | |
# shell> jmp help # display help | |
# shell> jmp find something in your codebase # search your codebase | |
# shell> jmp edit Filename.pm 12 # open your editor | |
# shell> jmp config # edit your ~/.jmp config file | |
# | |
#-------------------------------------------------------------------------------- | |
#| simple Template engine | |
class JMP::Template { | |
# parameters are passed by reference unless "is copy" makes a local copy | |
# you can optionally add a type to a parameter Str | |
method render (Str $string is copy, %params) { | |
# globally substitute tokens found in the string for values from the params hash | |
$string ~~ s:g/'[-' (<[a..z-]>+) '-]'/{ %params{$0} }/; | |
return $string; | |
} | |
} | |
#| Operating system abstraction, a singleton | |
class JMP::System { | |
my $only; | |
method instance { $only } | |
method new { !!! } | |
# Default implementation | |
method width { qx[tput cols] } | |
method height { qx[tput lines] } | |
method clear { shell 'clear' } | |
method home { %*ENV{'HOME'} } | |
method get-key { | |
ENTER shell "stty raw -echo"; | |
LEAVE shell "stty sane"; | |
return $*IN.getc; | |
} | |
# Private subclass for alternate implementation | |
class Win { | |
method width { qx[powershell.exe -noprofile -command $host.ui.rawui.WindowSize.Width] } | |
method height { qx[powershell.exe -noprofile -command $host.ui.rawui.WindowSize.Height] } | |
method clear { shell 'cls' } | |
method home { %*ENV{'HOMEPATH'} } | |
method get-key { | |
qx[powershell.exe -noprofile -command $host.UI.RawUI.ReadKey('NoEcho,IncludeKeyUp').Character].substr(0,1) | |
} | |
} | |
$only = ($*SPEC ~~ IO::Spec::Win32 ?? | |
JMP::System::Win !! JMP::System).bless; | |
} | |
#| simple config file handling | |
class JMP::Config { | |
has Str $.filename; | |
has %!fields; | |
# get a simple value for a config field | |
multi method get ($key) { | |
die "Key $key does not exist in config file. Please add a value for $key to $.filename" | |
unless %!fields{$key}:exists; | |
return %!fields{$key}; | |
} | |
# get a config template value and render params into it | |
multi method get ($key, %params) { | |
my $value = $.get($key); | |
return JMP::Template.new.render($value, %params); | |
} | |
# extra key value pairs from the config | |
# a helper sub called at object BUILD time | |
sub parse-config($config-file) { | |
my %fields; | |
for $config-file.IO.lines { | |
# skip comments | |
next if .starts-with('#'); | |
# config_keys.CAN.look-like.this | |
# ^ $ - mark the start an end of line | |
next unless /^ (.*?) '=' (.*?) $/; | |
# save the key and value in config | |
# $0 and $1 refer to the match objects in the regex | |
# ~$0 stringifies the match object | |
# trim removes leading and trailing whitespace | |
%fields{~$0.trim} = ~$1.trim; | |
} | |
return %fields; | |
} | |
# set up the user with a default config | |
# this is a helper sub called at object BUILD time | |
sub populate-default-config ($config-file) { | |
# populate the default config file | |
# HEREdocs can be indented! The CONFIG end marker provides the | |
# indentation level | |
# .IO provides simple methods for slurping/spurting files to disk | |
$config-file.IO.spurt(q:to"CONFIG"); | |
# uncomment your favourite editor | |
# editor.command.template = nano +[-line-number-] | |
editor.command.template = C:\Users\yhluc00\emacs\bin\emacsclient.exe +[-line-number-] "[-filename-]" | |
# atom has Perl 6 syntax highlighting and other plugins for Perl 6 | |
# editor.command.template = atom :[-line-number-] & | |
# editor.command.template = subl :[-line-number-] & | |
# editor.command.template = emacs +[-line-number-] | |
# editor.command.template = vim +[-line-number-] | |
# uncomment your preferred code searching tool | |
# classic recursive grep | |
find.command.template = grep -rHn '[-search-terms-]' | |
# ag - the silver searcher for fast generic file jmping | |
# find.command.template = ag --nogroup '[-search-terms-]' | |
# git grep - for jmping around git repos | |
# find.command.template = git grep --full-name --untracked --text --line-number -e '[-search-terms-]' | |
# App::Ack - Perl-powered improvement to grep | |
# find.command.template = ack --nogroup '[-search-terms-]' | |
CONFIG | |
} | |
# called at object construction time | |
# Mu - is the top level class and calls to new() start there | |
# Objects are constructed by calling BUILD from least derived to most derived | |
# Mu blesses the class into its type | |
# the BUILD submethod sets the attributes for the instance | |
submethod BUILD (:$filename) { | |
# variables-in-perl6-can-be-kebab-case you include apostrophes too, cool-isn't-it ? | |
# kebab-case makes variables easy to type and read | |
# conditional assigment uses ?? and !! instead or ? and : | |
my $config-file = $filename | |
?? $filename | |
!! JMP::System.instance.home ~ '/.jmp'; | |
# binding assignment is denoted with := | |
# this sets the attribute value for $!filename | |
$!filename := $config-file; | |
# .IO is handy for IO operations | |
# populate the config if the file does not exist | |
populate-default-config($config-file) | |
unless $config-file.IO.e; | |
# parse the config file | |
# bind the results := | |
# to a private hash variable %! | |
%!fields := parse-config($config-file); | |
} | |
} | |
# declare a role - the implementation and interface is shared with classes that "do" the role | |
role JMP::Screen::Action { | |
# all actions will need a key and takes up a height in lines on the screen | |
has $.key = ''; | |
has $.line-height = 1; | |
# assign a key depending on where it appears on the screen | |
method assign-key($key) { | |
$!key = $key; | |
} | |
method does-action($key) returns Bool { | |
return $key eq $.key; | |
} | |
method do-action { ... } | |
method render { ... } | |
} | |
# this class does the JMP::Screen::Action role - the methods and attributes of the role above are merged into it | |
# the class provides its own implementation of do-action and render | |
# the implementation of assign-key comes from the role above | |
class JMP::Screen::Action::EditFileLine does JMP::Screen::Action { | |
has $.file-path; | |
has $.context; | |
has $.line-number; | |
has $.template = q:to"ACTION"; | |
[[-key-]] ([-line-number-]) [-context-] | |
ACTION | |
method render { | |
my %params = (:$.key, :$.line-number, :$.context); | |
return JMP::Template.new.render($.template, %params); | |
} | |
method do-action { | |
MAIN('edit', $*CWD ~ '/' ~ $.file-path, +$.line-number); | |
} | |
} | |
class JMP::Screen::Action::EditFile does JMP::Screen::Action { | |
has $.file-path; | |
has $.template = q:to"ACTION"; | |
[[-key-]] [-file-path-] | |
ACTION | |
method render { | |
my %params = (:$.file-path, :$!key); | |
return JMP::Template.new.render($.template, %params); | |
} | |
method do-action { | |
# open file with an absolute path - start at line 1 | |
MAIN('edit', $*CWD ~ '/' ~ $.file-path, 1); | |
} | |
} | |
class JMP::Screen::Page { | |
has Int $.page-number = 1; | |
has @.actions; | |
method get-matching-action($key-pressed) { | |
# search through all the actions - does one match the key pressed? | |
# | flatten the element found in the list | |
return |@!actions.grep({ $_.does-action($key-pressed) }); | |
} | |
method render($available-lines) { | |
# iterate through all the actions >> call render on them | |
# and [~] concatentate them together | |
my $rendered-actions = [~](@!actions>>.render); | |
# iterate through all the actions >> call line-height on them | |
# and [+] sum the line heights | |
my $lines-used = [+](@!actions>>.line-height); | |
# if there are more available lines - add padding | |
my $vertical-padding = "\n" x $available-lines - $lines-used; | |
$rendered-actions ~= $vertical-padding; | |
return $rendered-actions; | |
} | |
submethod BUILD (:$page-number, :@actions) { | |
# create a list of all the available keys | |
# stop at 'W' so we have 'X' to eXit the program | |
my @keys = 'a' ... 'z', 'A' ... 'W'; | |
for @actions -> $action { | |
$action.assign-key(@keys.shift); | |
} | |
# bind into the attributes of the instance | |
$!page-number := $page-number; | |
@!actions := @actions; | |
} | |
} | |
class JMP::Screen { | |
has $.title = 'jmp'; | |
# qqx - is similar to backticks - execute this shell command | |
has $!rule = '_' x JMP::System.instance.width; | |
has $!content-line-height; | |
has $!total-pages; | |
has %!pages; | |
# HEREdocs can now be indented! | |
has $!template = q:to"SCREEN"; | |
[-title-] [-page-number-] of [-total-pages-] | |
[-rule-] | |
[-contents-] | |
[-rule-] | |
SCREEN | |
# handle going out of bounds with multi methods and type constraints | |
multi method display-page ($page-number where * < 1) { self.display-page(1); } | |
multi method display-page ($page-number where * > $!total-pages) { self.display-page($!total-pages); } | |
multi method display-page ($page-number) { | |
# grab the actions on this page | |
my $page = %!pages{$page-number}; | |
my $contents = $page.render($!content-line-height); | |
JMP::System.instance.clear; | |
say JMP::Template.new.render($!template, { :$!title, :$page-number, :$!total-pages, :$!rule, :$contents }); | |
$.prompt($page); | |
} | |
# display the prompt and respond | |
method prompt ($page) { | |
print '[<] Previous e[X]it [>] Next'; | |
loop { | |
my $key-pressed = JMP::System.instance.get-key(); | |
given $key-pressed { | |
when /<[a..zA..W]>/ { | |
my $action = $page.get-matching-action($key-pressed); | |
$action.do-action; | |
} | |
when '<' { $.display-page($page.page-number - 1) } | |
when '>' { $.display-page($page.page-number + 1) } | |
when 'X' { say ''; exit; } | |
} | |
} | |
} | |
sub paginate-actions($content-line-height, @actions) { | |
my @action-keys = 'a' ... 'z', 'A' ... 'W'; # reserve X for exit | |
my $actions-per-page = ($content-line-height > @action-keys.elems) | |
?? @action-keys.elems | |
!! $content-line-height; | |
my $page-number = 1; | |
my %pages; | |
while @actions { | |
# splice off a page's worth of actions | |
my @page-actions = @actions.splice(0, $actions-per-page); | |
%pages{$page-number} = JMP::Screen::Page.new(:$page-number, actions => @page-actions, :@action-keys); | |
$page-number++; | |
} | |
return %pages; | |
} | |
submethod BUILD (:$!title, :@actions) { | |
# the number of available lines - 6 for the header and footer | |
my $content-line-height = JMP::System.instance.height - (4 + 4); | |
$!content-line-height = $content-line-height; | |
# place the action on their pages | |
%!pages := paginate-actions($content-line-height, @actions); | |
$!total-pages := [max] keys %!pages; | |
} | |
} | |
my $config = JMP::Config.new; | |
#| open the .jmp config file in your editor | |
multi sub MAIN ('config') { | |
MAIN('edit', $config.filename, 0); | |
} | |
# Perl 6 is optionally typed - providing types is useful in multi method dispatch | |
#| edit a text file at a given line number | |
multi sub MAIN ('edit', $filename, Int $line-number = 0) { | |
# render the editor command from the config | |
my $editor-command = $config.get('editor.command.template', { :$filename, :$line-number }); | |
# launch the editor via the shell | |
shell($editor-command); | |
} | |
#| edit a text file starting on a matching line | |
multi sub MAIN ('edit', $filename, *@search-terms) { | |
my $matching-line-number = 0; | |
# find the first line in the file that matches the pattern | |
for $filename.IO.lines { | |
++$matching-line-number; | |
if / @search-terms / { | |
MAIN('edit', $filename, $matching-line-number); | |
exit; | |
} | |
} | |
# nothing match, open at the first line | |
MAIN('edit', $filename, 0); | |
} | |
#| find search terms in the codebase | |
multi sub MAIN ('find', *@search-terms) { | |
my $search-terms = @search-terms.join(' '); | |
# render the find command from the config | |
my $find-command = $config.get('find.command.template', { :$search-terms }); | |
my $search-results = qqx{$find-command}; | |
# finish if nothing found | |
return unless $search-results; | |
my @actions; | |
my $previous-file-path; | |
for $search-results.lines -> $line { | |
my ($file-path, $line-number, $context) = $line.split(':', 3); | |
if ($previous-file-path ne $file-path) { | |
@actions.push(JMP::Screen::Action::EditFile.new(:$file-path, line-number => 0)); | |
$previous-file-path = $file-path; | |
} | |
@actions.push(JMP::Screen::Action::EditFileLine.new(:$file-path, :$line-number, :$context)); | |
} | |
my $screen = JMP::Screen.new(title => 'jmp find ' ~ $search-terms, :@actions); | |
$screen.display-page(1); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment