hilfer 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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