#Stringscan: grep-like tool, written in Ruby-Tk. (c) 2017 Kevin Walzer/WordTech Communications LLC. License: MIT license.
#encoding: UTF-8
require 'tk'
#find additional Tcl libraries
Tk::AUTO_PATH.list <<= File.dirname(__FILE__)
require_relative 'tkballoonhelp'
require 'tkextlib/tile'
require 'find'
require 'tkextlib/tcllib/tablelist_tile'
require 'mime/types'
require 'tkextlib/tkDND'
$platform = Tk.windowingsystem
if $platform == 'aqua'
TkPackage.require('windowlist')
TkPackage.require('fullscreen')
require 'tk/tk_mac'
end
TkPackage.require('regproc')
TkPackage.require('machelp')
TkPackage.require('softwareupdate')
TkPackage.require('xplat')
if $platform == 'aqua'
$accelkey = 'Command'
else
$accelkey = 'Control'
end
class StringscanApp
#here we initialize our app class
def initialize
$dirname = ""
$searchterm = ""
$appname = 'Stringscan'
$appversion = '1.0'
Tk.tk_call('machelp::setAppName', $appname, $appversion)
Tk.tk_call('softwareupdate::setAppName', $appname)
Tk.tk_call('softwareupdate::setVersion', $appname, $appversion)
bgerror = Tk.install_cmd(proc{
|*args|
puts args
})
Tk.ip_eval("proc bgerror {args} {#{bgerror} $args}")
if $platform == 'aqua'
##map ruby proc to "odoc" Apple Event
setDir = Tk.install_cmd(proc{
|*args|
filename=(args[0]).delete('{}')
begin
if File.directory?(filename)
$dirname = filename
$direntry.value = filename
end
rescue
raise
end
})
Tk.ip_eval("proc ::tk::mac::OpenDocument {args} {#{setDir} $args}")
Tk.ip_eval("proc ::tk::mac::ShowHelp {} {#{Tk.install_cmd(proc{showHelp})}}")
end
drawgui
Tk.tk_call('regproc::readLic', $appname, $appversion)
end
#core method; here we search for a string in text files within a directory and display a list of matching files in the listbox
def stringgrep
$file_list = []
$grep_list = []
$lbox.delete(0, 'end')
Find.find("#{$dirname}") do |path|
$file_list << path unless FileTest.directory?(path)
end
for i in $file_list
begin
#we are only reading text files here, not binary.
text = MIME::Types.type_for(i).first.to_s
type = text.split('/')[0]
if type =~ /text/
f = File.open(i, "r:iso-8859-1:utf-8")
result = f.read
if result =~ /#{$searchterm}/ then
$lbox.insert('end', "#{i}")
f.close
end
end
rescue
#puts "Search term not found.\n"
end
end
rescue
puts "Directory not found.\n"
puts "Search complete.\n"
end
def drawgui
begin
Tk.ip_eval("console hide")
rescue
puts "could not hide console.\n"
end
#image data
$folderdata = 'R0lGODlhGAAYAIABAAQHB////yH+EUNyZWF0ZWQgd2l0aCBHSU1QACH5BAEKAAEALAAAAAAYABgAAAI1hI8Wy70JgZshJurOtFzmHm0ghJXVZVJkKo7uC8cr28z0g944ojP2/aMFWcNU0SRLxk5KSQEAOw=='
$glassdata = 'R0lGODlhGAAYAIABAAAAAP///yH+EUNyZWF0ZWQgd2l0aCBHSU1QACH5BAEKAAEALAAAAAAYABgAAAJHjI8Gy+nJYntQWkBVRDLv9zkTNXKleAYhuaBYpr2m7LWVXdOxfvMd67OATpeJcFa89JK/A1O5PMJUySlyZaVis1oel/o9FAAAOw=='
$folderimage = TkPhotoImage.new(:data => $folderdata)
$glassimage = TkPhotoImage.new(:data => $glassdata)
$icondata = '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'
$icon = TkPhotoImage.new(:data => $icondata)
#initialize variables for entry
$dirname = TkVariable.new
$searchterm = TkVariable.new
#top window and frame
Tk::TkDND::DND
$root = TkRoot.new {
title "Stringscan"
}
if $platform == 'aqua'
Tk.tk_call('fullscreen::fullscreen', $root)
end
#menu
$menubar = TkMenu.new($root)
TkOption.add '*tearOff', 0
if $platform == 'aqua'
$appmenu = TkSysMenu_Apple.new($menubar)
$menubar.add :cascade, :menu => $appmenu
$appmenu.add :command, :label => 'About Stringscan', :command=> proc{aboutWindow}
$appmenu.add :command, :label => 'License', :command=>proc{getReg}
$appmenu.add :command, :label=>'Check for Updates', :command=>proc{checkUpdate}
$appmenu.add :separator
end
$searchmenu = TkMenu.new($menubar)
$menubar.add :cascade, :menu => $searchmenu, :label => 'Search'
$searchmenu.add :command, :label => 'Choose Directory...', 'accelerator' => "#{$accelkey}+O", :command => proc{choosedir}
$searchmenu.add :command, :label => 'Run Search', 'accelerator' => "#{$accelkey}+R", :command => proc{stringgrep}
if $platform == 'win32'
$searchmenu.add :command, :label => "Exit", :command=>proc{exit}, 'accelerator' => "#{$accelkey}+Q"
end
if $platform == 'aqua'
Tk.tk_call("windowlist::windowMenu", $menubar)
end
$helpmenu = TkSysMenu_Help.new($menubar)
$menubar.add :cascade, :menu => $helpmenu, :label => 'Help'
if $platform != 'aqua'
$helpmenu.add :command, :label=>'Stringscan Help', :command=>proc{
Tk.tk_call('machelp::userhelp')
}
end
$helpmenu.add :command, :label=>'Contact Code by Kevin', :command=>proc{
Tk.tk_call('xplat::launch', 'mailto:kw@codebykevin.com?subject=Stringscan')
}
$helpmenu.add :command, :label=>'Web Site', :command=>proc{
Tk.tk_call('xplat::launch', 'https://www.codebykevin.com/stringscan.html')
}
if $platform != 'aqua'
$helpmenu.add :command, :label => 'About Stringscan', :command=> proc{aboutWindow}
$helpmenu.add :command, :label => 'License', :command=>proc{getReg}
$helpmenu.add :command, :label=>'Check for Updates', :command=>proc{checkUpdate}
end
$root['menu'] = $menubar
$mainframe = Tk::Tile::Frame.new($root).pack('side' => 'top','fill' => 'both','expand' => 'yes')
#button frame and buttons
$buttonframe = Tk::Tile::Frame.new($mainframe){padding 2}.pack('side' => 'top','fill' => 'both','expand' => 'no')
choosedirproc = proc{choosedir}
$choosebutton = Tk::Tile::Button.new($buttonframe) {
image $folderimage
takefocus 0
padding 5
command choosedirproc
}.pack('side' => 'left','fill' => 'both','expand' => 'no')
Tk::RbWidget::BalloonHelp.new($choosebutton, 'text'=>'Select Directory', 'background'=>'lightyellow', 'relief'=>'flat')
stringgrepProc = proc {stringgrep}
$runbutton = Tk::Tile::Button.new($buttonframe) {
image $glassimage
takefocus 0
padding 5
command stringgrepProc
}.pack('side' => 'left','fill' => 'both','expand' => 'no')
Tk::RbWidget::BalloonHelp.new($runbutton, 'text'=>'Run Search', 'background'=>'lightyellow', 'relief'=>'flat')
$sep = Tk::Tile::Separator.new($mainframe) { orient 'horizontal' }.pack('side'=> 'top', 'fill'=>'both')
#labels and entries
$topframe = Tk::Tile::Frame.new($mainframe){padding 5}.pack('side' => 'top','fill' => 'both','expand' => 'no')
$chooselabel = Tk::Tile::Label.new($topframe) {text "Directory:"}.pack('side' => 'left','fill' => 'both','expand' => 'no')
$direntry = Tk::Tile::Entry.new($topframe) {textvariable $dirname }.pack('side' => 'left','fill' => 'both','expand' => 'no')
$termlabel = Tk::Tile::Label.new($topframe) {text "Search Term:"}.pack('side' => 'left','fill' => 'both','expand' => 'no')
$termentry = Tk::Tile::Entry.new($topframe) {textvariable $searchterm}.pack('side' => 'left','fill' => 'both','expand' => 'no')
$termentry.bind('Return', proc {stringgrep})
$nextsep = Tk::Tile::Separator.new($mainframe) {orient 'horizontal'}.pack('side'=>'top', 'fill'=>'both')
#listbox, textbox, scrollbars
$bottomframe = Tk::Frame.new($mainframe).pack('side'=>'bottom','fill' => 'both', 'expand' => 'yes')
$bottomleftframe = Tk::Tile::Frame.new($bottomframe).pack('side'=>'left','fill' => 'both', 'expand' => 'yes')
$listlabel = Tk::Tile::Label.new($bottomleftframe) {
text 'File Name'
style 'TablelistHeader.TLabel'
padding 1
}.pack('side'=>'top', 'fill'=>'both', 'expand' => 'no')
$lbox = Tk::Listbox.new($bottomleftframe){
width 50
height 30
borderwidth 0
takefocus 0
activestyle "none"
exportselection 0
selectbackground "RoyalBlue1"
selectforeground "white"
dnd_bindtarget('text/uri-list', '<Drop>', '%D') {|d|
filename=d.join(' ')
begin
if File.directory?(filename)
$dirname = filename
$direntry.value = filename
end
rescue
raise
end
}
}.pack('side'=>'left','fill' => 'both', 'expand' => 'yes')
$lbox.bind '<ListboxSelect>', proc {
begin
if $lbox.curselection.empty?
puts "No selection in listbox."
else
highlighttext $lbox.get($lbox.curselection[0])
end
rescue
puts "Cannot get selection in listbox."
end
}
$scrollframe = Tk::Frame.new($bottomleftframe).pack('side'=>'right','fill' => 'y', 'expand' => 'no')
$listscroll = Tk::Scrollbar.new($scrollframe).pack('side'=>'right','fill' => 'y', 'expand' => 'no')
$lbox.yscrollbar($listscroll)
$bottomrightframe =Tk::Tile::Frame.new($bottomframe).pack('side'=>'right','fill' => 'both', 'expand' => 'yes')
$tbox = Tk::Text::new($bottomrightframe){
borderwidth 0.1
relief 'sunken'
wrap 'word'
highlightcolor 'SlateGray3'
bg 'white'
state 'disabled'
}.pack('side'=>'left','fill' => 'both', 'expand' => 'yes')
$tscrollframe = Tk::Frame.new($bottomrightframe).pack('side'=>'right','fill' => 'y', 'expand' => 'no')
$tscroll = Tk::Scrollbar.new($tscrollframe).pack('side'=>'right','fill' => 'y', 'expand' => 'no')
$tbox.yscrollbar($tscroll)
$root.bind("#{$accelkey}-Q", proc{exit})
$root.bind("#{$accelkey}-q", proc{exit})
$root.bind("#{$accelkey}-O", proc{choosedir})
$root.bind("#{$accelkey}-o", proc{choosedir})
$root.bind("#{$accelkey}-R", proc{stringgrep})
$root.bind("#{$accelkey}-r", proc{stringgrep})
end
#select search directory
def choosedir
$dirname = Tk::chooseDirectory('initialdir'=>Dir.home, 'parent'=>$root)
$direntry.value = $dirname
end
#read file for display in text widget
def highlighttext(file)
$tbox.configure('state'=>'normal')
$tbox.delete('1.0', 'end')
f = open(file, 'rb')
while(!f.eof?)
$tbox.insert('end', f.read(1000))
end
f.close
$tbox.configure('state'=>'disabled')
$tbox.tag_configure('search', :background=>'yellow')
sethighlight
end
#set highlight color for search term
def sethighlight
$tbox.configure('state'=>'normal')
$tbox.tag_remove('search', '0.0', 'end')
return if $searchterm == ""
cur = '1.0'
loop {
cur, len = $tbox.search_with_length("#{$searchterm}", cur, 'end')
break if cur == ""
$tbox.tag_add('search', cur, "#{cur} + #{len} char")
cur = $tbox.index("#{cur} + #{len} char")
}
$tbox.configure('state'=>'disabled')
end
#about window for app
def aboutWindow
Tk::messageBox :type => 'ok',
:message => 'Stringscan: Text Search Tool',
:icon => 'info', :title => 'About Stringscan',
:detail => "Version 1.0\n(c) 2017 Wordtech Communications LLC",
:parent => $root
end
#check version of installed software
def checkUpdate
Tk.tk_call('softwareupdate::setIcon', $icon)
Tk.tk_call('softwareupdate::checkVersion', $appname, $appversion)
end
#check version of installed software
def getReg
Tk.tk_call('regproc::getReg')
end
#user help
def showHelp
Tk.tk_call('machelp::userhelp')
end
#end of app class
end
#run app
app = StringscanApp.new
Tk.mainloop