hilfer 0.9.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.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 0.1.1 / 2008-03-10
2
+
3
+ * package as Gem using Hoe
4
+
5
+ * yes, really.
6
+
data/README.txt ADDED
@@ -0,0 +1,49 @@
1
+ = hilfer
2
+
3
+ * http://www.semiosix.com/hilfer
4
+
5
+ == DESCRIPTION:
6
+
7
+ Directory browser with plenty of keyboard shortcuts and integration with scite
8
+ rails and subversion
9
+
10
+ == FEATURES/PROBLEMS:
11
+
12
+ See TODO list
13
+
14
+ == SYNOPSIS:
15
+
16
+ hilfer [directory]
17
+
18
+ == REQUIREMENTS:
19
+
20
+ ruby-gtk2
21
+
22
+ == INSTALL:
23
+
24
+ sudo gem install
25
+
26
+ == LICENSE:
27
+
28
+ (The MIT License)
29
+
30
+ Copyright (c) 2008 FIX
31
+
32
+ Permission is hereby granted, free of charge, to any person obtaining
33
+ a copy of this software and associated documentation files (the
34
+ 'Software'), to deal in the Software without restriction, including
35
+ without limitation the rights to use, copy, modify, merge, publish,
36
+ distribute, sublicense, and/or sell copies of the Software, and to
37
+ permit persons to whom the Software is furnished to do so, subject to
38
+ the following conditions:
39
+
40
+ The above copyright notice and this permission notice shall be
41
+ included in all copies or substantial portions of the Software.
42
+
43
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
44
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
45
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
46
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
47
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
48
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
49
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/ec ADDED
@@ -0,0 +1,9 @@
1
+ #! /bin/bash
2
+ pipe=` cat /tmp/$USER.hilfer.scite`
3
+ if [ x$1 == x ]; then
4
+ wd=`pwd`
5
+ else
6
+ wd=$1
7
+ fi
8
+
9
+ test -e $pipe && echo cwd:$wd >$pipe
data/bin/hilfer ADDED
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'yaml'
4
+ require 'pathname'
5
+
6
+ require 'hilfer/tree_viewer.rb'
7
+
8
+ require 'optparse'
9
+
10
+ # defaults
11
+ $options = { :debug => false, :hidden => false, :auto_sync => false, :quit_editor => false }
12
+
13
+ oparser = OptionParser.new
14
+ oparser.on( '-y', '--auto-sync', 'track editor current file changes') { |o| $options[:host] = true }
15
+ oparser.on( '-s', '--show-hidden', 'Show hidden files' ) { |o| $options[:user] = true }
16
+ oparser.on( '-D', '-d', '-v', '--debug' ) { |o| $options[:debug] = true }
17
+ oparser.on( '-q', '--quit-editor', 'close editor on shutdown' ) { |o| puts "o: #{o.inspect}"; $options[:quit_editor] = true }
18
+
19
+ oparser.on( '-h', '-?', '--help' ) do |o|
20
+ puts oparser.to_s
21
+ exit( 0 )
22
+ end
23
+
24
+ # remove parsed options
25
+ ARGV = args = oparser.parse( ARGV )
26
+
27
+ # this must come after options parsing or it tries to eat the options
28
+ require 'gtk2'
29
+
30
+ # NOTE no space after comma, trailing comma is so
31
+ # that bold, italic etc and font size can be specified
32
+ FIXED_FONT = "Lucida Console,Courier New,"
33
+ CONFIG_FILE = File.expand_path( '~/.hilfer' )
34
+
35
+ =begin rdoc
36
+ HilferItems are stored in the TreeModel
37
+ - item is the display name (ie filename and extension)
38
+ - path is the full pathname, expanded, unqualified etc etc
39
+ - last_child_used is the GtkTreeView path of the last child of a directory
40
+ node that had selection
41
+ - status is either :expanded or :collapsed
42
+ - populated is whether or not this directory node has been populated
43
+ from the filesystem. To aid in recursively populating the tree
44
+ =end
45
+ class HilferItem
46
+ attr_accessor :item, :path, :last_child_used, :status, :svn_status, :colour
47
+ attr_writer :populated
48
+
49
+ def initialize( item, path )
50
+ @item = item
51
+ @path = path
52
+ @dir = File.directory? path
53
+ @populated = false
54
+ @status = :collapsed
55
+ @colour = '#000'
56
+ end
57
+
58
+ def dir?
59
+ @dir
60
+ end
61
+
62
+ def to_s
63
+ @item
64
+ end
65
+
66
+ # always return true for file nodes
67
+ def populated?
68
+ if !dir?
69
+ true
70
+ else
71
+ @populated
72
+ end
73
+ end
74
+
75
+ # always return false for file nodes
76
+ def expanded?
77
+ if !dir?
78
+ false
79
+ else
80
+ @status == :expanded
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ =begin rdoc
87
+ Knows how to lauch gnome-terminal with mutiple tabs,
88
+ each opened on a different directory
89
+ =end
90
+ class GnomeTerminal
91
+ def launch( dirs )
92
+ # construct arguments and launch
93
+ args = [ "gnome-terminal" ]
94
+ dirs.uniq.each do |x|
95
+ # first one will be a window, subsequent ones are tabs
96
+ args << ( args.size == 1 ? '--window' : '--tab' )
97
+ args << "--working-directory=#{x}"
98
+ end
99
+ system( *args )
100
+ end
101
+ end
102
+
103
+ =begin rdoc
104
+ Just launches xterm. I couldn't figure out how to open it in different
105
+ directories.
106
+
107
+ use it by saying
108
+
109
+ $options[:terminal] = Xterm.new
110
+
111
+ before the call to TreeViewer.new
112
+ =end
113
+ class Xterm
114
+ def launch( dirs )
115
+ system( 'xterm &' )
116
+ end
117
+ end
118
+
119
+ # yes, this actually does catch method calls polymorphically
120
+ # but you can't change the TreePath or TreeIter that are returned
121
+ class HilferModel < Gtk::TreeStore
122
+ def initialize( *args )
123
+ super
124
+ end
125
+
126
+ def ancestor?( iter, descendant )
127
+ print "testing #{iter[0]} against #{descendant}\n" if $options[:debug]
128
+ super
129
+ end
130
+ end
131
+
132
+ =begin rdoc
133
+ Read and write configuration to ~/.hilfer in yaml.
134
+ Currently only writes window position
135
+ =end
136
+ class HilferConfig
137
+
138
+ def initialize( file, window, count )
139
+ @file, @window = file, window
140
+
141
+ if File.file? CONFIG_FILE
142
+ config = YAML.load_file CONFIG_FILE
143
+ @window.set_default_size( *config[:size] )
144
+ @window.show_all
145
+ # move must be after show_all otherwise it
146
+ # gets overridden
147
+ @window.move( *config[:position] ) if count == 1
148
+ else
149
+ @window.set_default_size( 300,700 ).show_all
150
+ end
151
+ end
152
+
153
+ # save config
154
+ def save
155
+ config = {
156
+ :path => @window.title,
157
+ :position => @window.position,
158
+ :size => @window.size
159
+ }
160
+ File.open( CONFIG_FILE, 'w' ) { |file| file.print config.to_yaml }
161
+ end
162
+
163
+ end
164
+
165
+ =begin rdoc
166
+ Layout of the Tree Viewer Window.
167
+ =end
168
+ class TreeViewerWindow
169
+ @@window_count = 0
170
+
171
+ def initialize( root_fs_path, editor )
172
+ @window = Gtk::Window.new
173
+
174
+ # initialize viewer with a path
175
+ @editor = editor
176
+
177
+ # location bar
178
+ @location = Gtk::Entry.new
179
+
180
+ @tv = TreeViewer.new( root_fs_path, editor, @window, @location )
181
+
182
+ # add scrollbars
183
+ @scroll = Gtk::ScrolledWindow.new( nil, nil )
184
+ @scroll.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC )
185
+ @scroll.add( @tv.view )
186
+
187
+ # expand and fill
188
+ @vbox = Gtk::VBox.new( false )
189
+ @vbox.pack_start( @location, false, false )
190
+ @vbox.pack_start( @scroll, true, true )
191
+ @vbox.focus_chain = [@scroll, @location]
192
+
193
+ icon_path = find_icon_path
194
+ if icon_path != nil
195
+ @window.icon = Gdk::Pixbuf.new( icon_path )
196
+ end
197
+ @window.title = 'Hilfer ' + @tv.root_item.path
198
+ @window.add( @vbox )
199
+
200
+ # read config. OK, the last parameter is a hack
201
+ # but we need it so that new windows aren't show in
202
+ # exactly the same position and it looks like nothing
203
+ # has happened
204
+ @@window_count += 1
205
+ @hc = HilferConfig.new( CONFIG_FILE, @window, @@window_count )
206
+
207
+ # set first line selected
208
+ @tv.select_first
209
+
210
+ # The program will directly end upon 'delete_event', ie window close
211
+ @window.signal_connect( 'delete_event' ) do
212
+ # unregister view from editor
213
+ @editor.unregister_view( @tv )
214
+ # finish
215
+ @@window_count -= 1
216
+ if @@window_count == 0
217
+ @hc.save
218
+ # this MUST come before cleanup, otherwise
219
+ # there will be no pipes to write the command to
220
+ @editor.quit
221
+ # delete command pipe files
222
+ @editor.cleanup
223
+ Gtk.main_quit
224
+ false
225
+ end
226
+ end
227
+
228
+ # to allow the sd command to find the correct editor instance
229
+ @window.signal_connect 'focus-in-event' do |widget,event|
230
+ @editor.write_pipe_name
231
+ false
232
+ end
233
+
234
+ # to allow the sd command to find the correct editor instance
235
+ @window.signal_connect 'focus-out-event' do |widget,event|
236
+ @editor.write_pipe_name
237
+ false
238
+ end
239
+ end
240
+
241
+ def find_icon_path
242
+ $LOAD_PATH.grep( /hilfer/ ).each do |path|
243
+ png_file = Pathname.new( path ) + 'hilfer-icon.png'
244
+ return png_file.to_s if png_file.exist?
245
+ end
246
+ nil
247
+ end
248
+ end
249
+
250
+
251
+ ################
252
+ # Main script
253
+
254
+ tvw = TreeViewerWindow.new(
255
+ File.expand_path( ARGV[0] || '.' ),
256
+ SciteEditor.new
257
+ )
258
+
259
+ # and off we go...
260
+ Gtk.main
Binary file
data/bin/sd ADDED
@@ -0,0 +1,17 @@
1
+ #! /bin/bash
2
+ pipe=`cat /tmp/$USER.hilfer.scite`
3
+ director=/tmp/$USER.scite.director
4
+ wd=`pwd`
5
+ if [ -e $pipe ]; then
6
+ for name in $@; do
7
+ echo open:$wd/$name >$pipe;
8
+ done
9
+ else
10
+ test -e $pipe && /bin/rm $pipe
11
+ test -e $director && /bin/rm $director
12
+ /usr/bin/mkfifo $director
13
+ /usr/bin/scite \
14
+ -ipc.director.name=$director \
15
+ -ipc.scite.name=$pipe \
16
+ $@ &
17
+ fi
@@ -0,0 +1,150 @@
1
+ require 'active_support'
2
+
3
+ =begin
4
+ This handles various keypresses that jump to specific
5
+ locations in a Rails project. If a specific file isn't
6
+ selected, the jump will go to the directory. Otherwise
7
+ it will go to a specific file.
8
+
9
+ For example, if user_controller.rb is currently selected
10
+ and Ctrl-Shift-T is pressed, user_controller_test.rb will
11
+ be selected.
12
+
13
+ TODO fix multiple selections
14
+ =end
15
+ class RailsLocator
16
+ attr_accessor :treeviewer
17
+
18
+ def initialize( treeviewer )
19
+ @treeviewer = treeviewer
20
+
21
+ @controller_re = %r|^(.*?)/app/controllers/(.*?)/?(.*?)_controller.rb$|
22
+ @view_re = %r|^(.*?)/app/views/(.*?)/?([^/.]*?\..*?)?$|
23
+ @model_re = %r|^(.*?)/app/models/(.*?/?)([^/.]*?).rb$|
24
+ @unit_test_re = %r|^(.*?)/test/unit/(.*?/?)(.*?)_test.rb$|
25
+ @functional_test_re = %r|^(.*?)/test/functional/(.*?/?)(.*?)_controller_test.rb$|
26
+ @fixture_re = %r|^(.*?)/test/fixtures/(.*?/?)(.*?).yml$|
27
+ end
28
+
29
+ # this does the common code for each keypress
30
+ # the block should return a related path for
31
+ # the particular keypress on the currently selected
32
+ # item
33
+ def select_related_paths( widget, event, &block )
34
+ items = []
35
+ paths_to_select = []
36
+ widget.selection.selected_each do
37
+ |model, path, iter|
38
+ item = model.get_value( iter, 0 )
39
+ puts "item: #{item.path}" if $options[:debug]
40
+ new_rails_path = yield( item )
41
+ puts "new_rails_path #{new_rails_path}" if $options[:debug]
42
+ paths_to_select << rails_root + new_rails_path
43
+ end
44
+
45
+ treeviewer.select_fs_paths( paths_to_select, true )
46
+ end
47
+
48
+ def rails_root
49
+ treeviewer.root_item.path
50
+ end
51
+
52
+ def goto_rails_path( fs_path )
53
+ treeviewer.select_fs_path( rails_root + fs_path, true )
54
+ end
55
+
56
+ def handle_keypress( widget, event )
57
+ retval = true
58
+ case
59
+ # Shift-Ctrl-V - go to view, or view dir
60
+ when event.hardware_keycode == 55 && event.state.control_mask? && event.state.shift_mask?
61
+ select_related_paths( widget, event ) do |item|
62
+ case
63
+ when @controller_re.match( item.path )
64
+ '/app/views/' + $2 + $3
65
+ when @functional_test_re.match( item.path )
66
+ '/app/views/' + $2 + $3
67
+ else
68
+ '/app/views'
69
+ end
70
+ end
71
+
72
+ # Shift-Ctrl-C - go to controller, or controller dir
73
+ when event.hardware_keycode == 54 && event.state.control_mask? && event.state.shift_mask?
74
+ select_related_paths( widget, event ) do |item|
75
+ case
76
+ when md = @view_re.match( item.path )
77
+ '/app/controllers/' + $2 + '_controller.rb'
78
+ when md = @functional_test_re.match( item.path )
79
+ '/app/controllers/' + $2 + $3 + '_controller.rb'
80
+ else
81
+ '/app/controllers'
82
+ end
83
+ end
84
+
85
+ # Shift-Ctrl-M - go to model, or model dir
86
+ when event.hardware_keycode == 58 && event.state.control_mask? && event.state.shift_mask?
87
+ select_related_paths( widget, event ) do |item|
88
+ case
89
+ when md = @unit_test_re.match( item.path )
90
+ '/app/models' + $2 + '/' + $3 + '.rb'
91
+ when md = @fixture_re.match( item.path )
92
+ '/app/models' + $2 + '/' + $3.singularize + '.rb'
93
+ else
94
+ '/app/models'
95
+ end
96
+ end
97
+
98
+ # Shift-Ctrl-T - go to test, or test dir
99
+ when event.hardware_keycode == 28 && event.state.control_mask? && event.state.shift_mask?
100
+ select_related_paths( widget, event ) do |item|
101
+ case
102
+ when md = @model_re.match( item.path )
103
+ '/test/unit/' + $2 + $3 + '_test.rb'
104
+ when md = @view_re.match( item.path )
105
+ '/test/functional/' + $2 + '_controller_test.rb'
106
+ when md = @controller_re.match( item.path )
107
+ '/test/functional/' + $2 + $3 + '_controller_test.rb'
108
+ when md = @fixture_re.match( item.path )
109
+ '/test/unit/' + $2 + $3.singularize + '_test.rb'
110
+ else
111
+ '/test'
112
+ end
113
+ end
114
+
115
+ # Shift-Ctrl-H - go to helpers dir
116
+ when event.hardware_keycode == 43 && event.state.control_mask? && event.state.shift_mask?
117
+ goto_rails_path '/app/helpers'
118
+
119
+ # Shift-Ctrl-L - go to layouts dir
120
+ when event.hardware_keycode == 46 && event.state.control_mask? && event.state.shift_mask?
121
+ goto_rails_path '/app/views/layouts'
122
+
123
+ # Shift-Ctrl-I - go to migrations
124
+ when event.hardware_keycode == 31 && event.state.control_mask? && event.state.shift_mask?
125
+ goto_rails_path '/db/migrate'
126
+
127
+ # Shift-Ctrl-S - go to stylesheets
128
+ when event.hardware_keycode == 39 && event.state.control_mask? && event.state.shift_mask?
129
+ goto_rails_path '/public/stylesheets'
130
+
131
+ # Shift-Ctrl-F - go to fixtures
132
+ when event.hardware_keycode == 41 && event.state.control_mask? && event.state.shift_mask?
133
+ select_related_paths( widget, event ) do |item|
134
+ case
135
+ when md = @model_re.match( item.path )
136
+ '/test/fixtures' + $2 + '/' + $3.pluralize + '.yml'
137
+ when md = @unit_test_re.match( item.path )
138
+ '/test/fixtures' + $2 + '/' + $3.pluralize + '.yml'
139
+ else
140
+ '/test/fixtures'
141
+ end
142
+ end
143
+
144
+ else
145
+ retval = false
146
+ end
147
+ retval
148
+ end
149
+
150
+ end
@@ -0,0 +1,182 @@
1
+ # handle director interface to scite
2
+ class SciteEditor
3
+
4
+ # options can contain :debug
5
+ # view is a GtkTreeView
6
+ def initialize( options = {} )
7
+ @scite_pipe_name = "/tmp/#{ENV['USER']}.#{Process.pid}.scite"
8
+ @director_pipe_name = "/tmp/#{ENV['USER']}.#{Process.pid}.director"
9
+
10
+ # delete these if they already exist because we
11
+ # need to use their existence to determine if scite
12
+ # is running
13
+ File.exists?( @scite_pipe_name ) && File.unlink( @scite_pipe_name )
14
+ File.exists?( @director_pipe_name ) && File.unlink( @director_pipe_name )
15
+
16
+ # this is an array of TreeViewer objects
17
+ @views = []
18
+ # the command-line options
19
+ @options = options
20
+ end
21
+
22
+ def cleanup
23
+ FileUtils.rm @scite_pipe_name if File.exist? @scite_pipe_name
24
+ FileUtils.rm @director_pipe_name if File.exist? @director_pipe_name
25
+ FileUtils.rm pipe_name_file if File.exist? pipe_name_file
26
+ end
27
+
28
+ # send some command
29
+ def send( cmd, arg = '' )
30
+ launch
31
+ File.open( @scite_pipe_name, 'a' ) do |file|
32
+ file.print( cmd + ":" + arg.to_s, "\n" )
33
+ puts "sending: #{cmd}:#{arg.to_s}" if $options[:debug]
34
+ end
35
+ end
36
+
37
+ # open files in scite
38
+ def open_action( files )
39
+ files.each { |x| send "open", x.path }
40
+ send 'identity',0
41
+ end
42
+
43
+ def dump
44
+ %w{dyn local user base embed}.each {|x| send 'enumproperties', x}
45
+ #~ send 'askproperty','dyn:CurrentWord'
46
+ end
47
+
48
+ # fetch the current file in scite
49
+ def synchronize_path
50
+ send 'askfilename'
51
+ end
52
+
53
+ # shut down editor
54
+ def quit
55
+ send 'quit'
56
+ end
57
+
58
+ # insert text to editor, at current caret, or overwriting the current selection
59
+ def insert( arg )
60
+ value =
61
+ case
62
+ # note use of single quote - SciTE wants escaped characters
63
+ when arg.respond_to?( :join ); arg.join( '\\n' ) + '\\n'
64
+ else; arg.to_s
65
+ end
66
+ send 'insert', value
67
+ end
68
+
69
+ # send selection to all registered views
70
+ def set_selection ( fs_path )
71
+ @views.each { |v| v.synchronise_editor_path( fs_path ) }
72
+ end
73
+
74
+ def register_view( view )
75
+ @views << view
76
+ end
77
+
78
+ def unregister_view( view )
79
+ @views.delete( view )
80
+ end
81
+
82
+ def pipe_name_file
83
+ @pipe_name_file ||= "/tmp/#{ENV['USER']}.hilfer.scite"
84
+ end
85
+
86
+ def write_pipe_name
87
+ File.open( pipe_name_file, 'w' ) do |f|
88
+ f.write @scite_pipe_name
89
+ f.write "\n"
90
+ end
91
+ end
92
+
93
+ protected
94
+
95
+ # start up the editor if there isn't already one
96
+ # calling it when the editor is already open does nothing
97
+ def launch
98
+ # create the director pipe if it isn't there already
99
+ if !File.exists?( @director_pipe_name )
100
+ system( "mkfifo #{@director_pipe_name}" )
101
+ end
102
+
103
+ # scite already open, so don't launch another instance
104
+ return if File.exists?( @scite_pipe_name )
105
+
106
+ cmd = <<EOF
107
+ /usr/bin/scite
108
+ -ipc.director.name=#{@director_pipe_name}
109
+ -ipc.scite.name=#{@scite_pipe_name}
110
+ EOF
111
+ oneline = cmd.gsub( "\n", " ")
112
+ child_pid = fork
113
+ if child_pid.nil?
114
+ system( oneline )
115
+
116
+ # scite ended, so delete the pipes
117
+ File.unlink @director_pipe_name if File.exist? @director_pipe_name
118
+ File.unlink @scite_pipe_name if File.exist? @scite_pipe_name
119
+
120
+ # unfork, don't trigger a SystemException
121
+ exit! true
122
+ else
123
+ # start the listener thread
124
+ start_listener
125
+ end
126
+ end
127
+
128
+ def start_listener
129
+
130
+ sleep 0.1 while !File.exists? @scite_pipe_name
131
+
132
+ Thread.new( self ) do |arg|
133
+ begin
134
+ arg.listen
135
+ rescue Errno::ENOENT
136
+ break
137
+ rescue Exception => e
138
+ print "listener thread ended: ", e.inspect, "\n"
139
+ e.backtrace.each { |x| print x, "\n" }
140
+ end
141
+ end
142
+
143
+ end
144
+
145
+ # respond to strings from scite
146
+ def listen
147
+ File.open( @director_pipe_name ).each( "\x0" ) do |line|
148
+ begin
149
+ line = line.slice(0...-1) if line[-1] = 0
150
+ print "heard #{line}\n" if $options[:debug]
151
+ case line
152
+ # scite sends one of these for each file opened
153
+ when /^opened:(.*)$/
154
+ set_selection( $1 )
155
+
156
+ # scite sends one of these each time the buffer is switched
157
+ when /^switched:(.*)$/
158
+ set_selection( $1 )
159
+
160
+ # response to askfilename, as sent by synchronize_path
161
+ when /^filename:(.*)$/
162
+ set_selection( $1 )
163
+
164
+ # the specified file has just been saved. Do nothing.
165
+ # TODO could check that it exists and add it if not.
166
+ when /^saved:(.*)$/
167
+
168
+ when /^closed$/
169
+ puts "SciTE closing"
170
+ break
171
+
172
+ # print it out
173
+ else
174
+ print "unknown: ", line, "\n" if $options[:debug]
175
+ end
176
+ rescue Exception => e
177
+ print "caught Exception in listen: ", e.inspect, "\n"
178
+ end
179
+ end
180
+ end
181
+
182
+ end