Class | SSHMenu::App |
In: |
lib/sshmenu.rb
(Git)
|
Parent: | Object |
The SSHMenu::App class implements the framework of the application - a simple menu. Each item on the menu represents an SSH connection to a host, to be opened in a new terminal window.
This class is responsible for rendering the menu and taking appropriate action when the user makes a selection from the menu.
The application class uses the ClassMapper to delegate chunks of functionality to different classes as follows:
AskpassPaths | = | [ '/usr/bin/ssh-askpass', '/etc/alternatives/ssh-askpass', '/usr/lib/ssh/gnome-ssh-askpass', '/usr/lib/ssh/x11-ssh-askpass', ] |
config | [R] | The ‘app.model’ object |
display | [R] | The X11 DISPLAY object |
Uses a pop-up dialog to display a message and optional further detail.
# File lib/sshmenu.rb, line 826 def App.alert(message, extra_msg = nil) dialog = Gtk::Dialog.new( nil, nil, Gtk::Dialog::DESTROY_WITH_PARENT, [ Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NONE ] ) dialog.has_separator = false stock_id = nil if message =~ /error/i dialog.title = 'Error' stock_id = Gtk::Stock::DIALOG_ERROR else dialog.title = 'Warning' stock_id = Gtk::Stock::DIALOG_WARNING end label = Gtk::Label.new(message) label.selectable = true icon = Gtk::Image.new(stock_id, Gtk::IconSize::DIALOG) box = Gtk::HBox.new(false, 10) box.add(icon) box.add(label) box.border_width = 10 dialog.vbox.add(box) if extra_msg expander = Gtk::Expander.new('Detail') expander.border_width = 10 extra_label = Gtk::Label.new(extra_msg) extra_label.selectable = true expander.add(extra_label) dialog.vbox.add(expander) end dialog.screen = @@display if @@display dialog.show_all dialog.run dialog.destroy end
Takes an exception object and displays the error message and the backtrace using SSHMenu::App#alert.
# File lib/sshmenu.rb, line 820 def App.fatal_error(exception) alert('Fatal error: ' + exception.message, exception.backtrace.join("\n")) end
Called by SSHMenu::Factory#make_app
# File lib/sshmenu.rb, line 272 def initialize(config, options, launch_proc) @config = config @options = options @launch_proc = launch_proc @have_bcvi = false @socket_window_id = nil @debug_level = 0 @context_menu_active = false @deferred_actions = [] getopts(@options[:args]) @app_win = @options[:window] || default_container @entry_box = nil inject_defaults get_initial_config check_for_bcvi @history = mapper.get_class('app.history').new(config) @have_key = false @is_applet = @app_win.respond_to?('popup_component') if not @deferred_actions.empty? @deferred_actions.each { |a| a.call() } end build_ui() unless @app_win == :none end
Invoked if the user selects the ‘Add SSH key to agent’ option from the main menu. Attempts to set up the environment to allow an askpass dialog window can be displayed and then runs the ssh-add command.
# File lib/sshmenu.rb, line 1006 def add_key return if @have_key if !ENV['SSH_AUTH_SOCK'] alert("$SSH_AUTH_SOCK is not set.\nIs the ssh-agent running?") return end if !File.exists?(ENV['SSH_AUTH_SOCK']) alert( "$SSH_AUTH_SOCK points to #{ENV['SSH_AUTH_SOCK']},\n" + "but it does not exist!" ) return end keylist = `ssh-add -l` if $? == 0 @have_key = true return end setup_askpass_env or return shell_command("ssh-add </dev/null >/dev/null 2>&1") end
Adds the menu selections at the bottom of the main menu:
# File lib/sshmenu.rb, line 962 def add_tools_menu_items(mif) mif.create_item("/tools-separator", '<Separator>') if not context_menu_active? mif.create_item( "/Preferences", "<StockItem>", nil, Gtk::Stock::PROPERTIES ){ edit_preferences } end mif.create_item("/Add SSH key to Agent", "<Item>") { add_key } mif.create_item("/Remove SSH keys from Agent", "<Item>") { remove_keys } end
Proxy for SSHMenu::App#alert class method
# File lib/sshmenu.rb, line 866 def alert(message, detail = nil) self.class.alert(message, detail) end
Callback invoked when the ‘About’ option on the context menu is selected.
# File lib/sshmenu.rb, line 523 def applet_menu_about dialog = Gtk::Dialog.new( nil, nil, Gtk::Dialog::DESTROY_WITH_PARENT, [ Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NONE ] ) dialog.has_separator = false dialog.title = 'About SSHMenu' about_pane = make_about_pane dialog.vbox.add(about_pane) dialog.screen = @@display if @@display dialog.show_all dialog.run dialog.destroy end
Helper routine called from open_homepage. Attempts to find a browser program by looking for known browser executable names in each directory in the search path. Returns the name of the first program found.
# File lib/sshmenu.rb, line 1150 def browser_program progs = %w{ gnome-open sensible-browser firefox konqueror opera galeon } ENV['PATH'].split(':').each do |dir| progs.each do |p| path = "#{dir}/#{p}" return path if FileTest.executable?(path) end end alert( 'Unable to locate a web browser program', "Tried:\n#{progs.join(', ')}" ) return end
Build a text entry box with resize handle for display next to the main button (if required)
# File lib/sshmenu.rb, line 379 def build_text_entry hbox = Gtk::HBox.new(false, 0) entry = Gtk::Entry.new completion = Gtk::EntryCompletion.new store = Gtk::ListStore.new(String) @completion_actions = [] entry.width_chars = 1 entry.width_request = @config.get('entry_width', 70) completion.model = store completion.popup_completion = true completion.text_column = 0 completion.popup_set_width = false completion.set_match_func { true } entry.completion = completion entry.signal_connect('activate') do @history.add_line(entry.text) open_win(@config.host_from_text(entry.text)) entry.text = '' end completion.signal_connect('action-activated') do |c,i| target = @completion_actions[i] if target.is_a?(String) prefs = mapper.get_class('app.dialog.prefs').new(self, @config) prefs.append_host(@config.host_from_text(entry.text)) else open_win(@completion_actions[i]) end entry.text = '' end entry.signal_connect('button-press-event') do |w,e| if @app_win.respond_to?('request_focus') @app_win.request_focus(e.time) end false end entry.signal_connect('focus-in-event') do @history.freshen false end entry.signal_connect('changed') do update_entry_completions(store, entry.text) update_entry_actions(completion, entry.text) end hbox.pack_start(entry, true, true) # Expand and fill handle = entry_resize_handle(entry) hbox.pack_start(handle, false, true) # No expand but fill (y?) return hbox end
Called from the constructor to handle building the main user interface (a button).
# File lib/sshmenu.rb, line 342 def build_ui() hbox = Gtk::HBox.new(false, 0) @app_win.add(hbox) evbox = Gtk::EventBox.new evbox.signal_connect('button-press-event') { |w,e| on_click(w,e) } hbox.pack_start(evbox, false, false) @frame = Gtk::Frame.new set_button_border; evbox.add(@frame) label = Gtk::Label.new("SSH") label.set_padding(2, 2) @frame.add(label) tooltips = Gtk::Tooltips.new tooltips.set_tip(evbox, @config.tooltip_text, nil); @app_win.show_all @entry_box = build_text_entry hbox.pack_start(@entry_box, true, true) unless @entry_box.nil? show_hide_text_entry set_up_applet_menu # For multi-DISPLAY setups @@display = evbox.screen ENV['DISPLAY'] = @@display.display_name appease_popcon end
Takes a SSHMenu::HostItem object, builds a command line for invoking SSH in an xterm window, to connect to the specified host.
# File lib/sshmenu.rb, line 1095 def build_window_command(host) command = "#{host.env_settings}xterm -T " + shell_quote(host.title) if host.geometry and host.geometry.length > 0 command += " -geometry #{host.geometry}" end ssh_cmnd = ssh_command(host) command += ' -e sh -c ' + shell_quote("#{ssh_cmnd} #{host.sshparams_noenv}") + ' &' return command end
This method returns a boolean value which controls whether or not the preferences dialog includes an option to display a text entry box. If the application is running in a panel applet, version 0.19 of the Ruby bindings is required to display a text entry. For non-applet contexts, the return value is always true.
# File lib/sshmenu.rb, line 314 def can_show_entry? if @is_applet return GLib.check_binding_version?(0, 19, 0) end return true end
Called from the constructor to check if the ‘bcvi’ program is installed anywhere in the search path. If bcvi is found then a checkbox will be displayed in the host edit dialog.
# File lib/sshmenu.rb, line 658 def check_for_bcvi path = ENV['PATH'] || '' path.split(':').each do |dir| file = Pathname.new(dir) + 'bcvi' if FileTest.executable?(file) @have_bcvi = true break end end end
Returns a boolean flag indicating whether the program is running in an applet with context menu support.
# File lib/sshmenu.rb, line 672 def context_menu_active? return @context_menu_active end
Output diagnostic information to STDOUT if debugging is enabled
# File lib/sshmenu.rb, line 1175 def debug(level, message) return if @debug_level < level puts message end
Called if no container window was supplied to the constructor. Most commonly, the method would create and return a new top-level window object. However if the —socket-window-id option was supplied, a Gtk::Plug object will be created instead. This allows another application to embed the SSHMenu user interface.
# File lib/sshmenu.rb, line 327 def default_container() window = nil if @socket_window_id window = Gtk::Plug.new(@socket_window_id) else window = Gtk::Window.new( Gtk::Window::TOPLEVEL ) window.title = 'SSH Menu' end window.signal_connect('destroy') { Gtk.main_quit } return window end
Takes a code block and schedules it to be called when option processing is complete
# File lib/sshmenu.rb, line 813 def defer_action(&action_proc) @deferred_actions.push action_proc end
Called when the ‘Preferences’ option is selected from the main menu. Instantiates an ‘app.dialog.prefs’ object and calls its invoke method (SSHMenu::PrefsDialog#invoke by default).
# File lib/sshmenu.rb, line 1131 def edit_preferences dialog_class = mapper.get_class('app.dialog.prefs') dialog_class.new(self, @config).invoke show_hide_text_entry set_button_border end
Add a resize handle to the right of the entry box
# File lib/sshmenu.rb, line 442 def entry_resize_handle(entry) handle = Gtk::DrawingArea.new handle.set_size_request(3,10) handle.events = Gdk::Event::BUTTON_PRESS_MASK | Gdk::Event::BUTTON_RELEASE_MASK | Gdk::Event::POINTER_MOTION_MASK handle.signal_connect('realize') do handle.window.cursor = Gdk::Cursor.new(Gdk::Cursor::RIGHT_SIDE) end dragging = false x_start = 0 w_start = 0 handle.signal_connect('button-press-event') do |w,e| if e.button = 1 dragging = true (x,y,w,h) = entry.window.geometry w_start = w x_start = e.x_root end end handle.signal_connect('button-release-event') do |w,e| if e.button = 1 dragging = false @config.set('entry_width', entry.width_request) @config.save end end handle.signal_connect('motion-notify-event') do |w,e| if dragging w = w_start + e.x_root - x_start if w >= 20 entry.width_request = w @app_win.resize(1,1) if @app_win.respond_to?('resize') end end end return handle end
Reads the config file. If no config file exists at all, calls SSHMenu::Config#autoconfigure to invoke the configuration wizard.
# File lib/sshmenu.rb, line 698 def get_initial_config if @config.not_configured? @config.autoconfigure end get_latest_config end
Called before the menu is displayed. Ensures the latest config data has been loaded. This allows manual edits of the config file to be reflected without having to restart the app.
# File lib/sshmenu.rb, line 946 def get_latest_config begin @config.load rescue Exception => detail alert( "Error reading config file: #{@config.filename}", detail.message + "\n" + detail.backtrace.join("\n") ) end end
Returns a list of command-line option definitions for use by GetoptLong.
# File lib/sshmenu.rb, line 731 def getopt_defs return( [ [ "--version", "-V", GetoptLong::NO_ARGUMENT ], [ "--debug", "-d", GetoptLong::OPTIONAL_ARGUMENT ], [ "--config-file", "-c", GetoptLong::REQUIRED_ARGUMENT ], [ "--socket-window-id", "-s", GetoptLong::REQUIRED_ARGUMENT ], [ "--list-completions", "-l", GetoptLong::NO_ARGUMENT ] ] ) end
Called from the wrapper script to handle the parsing of command-line options. Calls getopt_defs to determine which options are recognised and then calls the set_* method for each option as it is encountered (eg: set_config_file).
# File lib/sshmenu.rb, line 711 def getopts(argv) begin argv = argv.flatten # Copy argument (which might already be ARGV) ARGV.clear # Then copy contents into ARGV argv.each { |a| ARGV.push(a) } opts = GetoptLong.new( *getopt_defs ) opts.quiet = true opts.each do |opt, arg| method = opt.gsub(/^-*/, 'set_').gsub(/\W/, '_') self.send(method, arg) end @options[:window] = :none if not ARGV.empty? rescue Exception => detail $stderr.puts detail.message exit 1 end end
Accessor for the @have_bcvi attribute.
# File lib/sshmenu.rb, line 678 def have_bcvi? return @have_bcvi end
Called from the constructor to set up default class mappings for application components.
# File lib/sshmenu.rb, line 685 def inject_defaults mapper.inject( 'app.history' => SSHMenu::History, 'app.dialog.prefs' => SSHMenu::PrefsDialog, 'app.dialog.host' => SSHMenu::HostDialog, 'app.dialog.menu' => SSHMenu::MenuDialog, 'app.geograbber' => SSHMenu::GeoGrabber ) end
Returns true if the application window is a panel applet or false if it‘s a normal application window
# File lib/sshmenu.rb, line 304 def is_applet? return @is_applet end
Helper method for calculating menu item paths
# File lib/sshmenu.rb, line 935 def item_path(parents, item) path = [parents, item].flatten.map do |i| i.title.gsub(/\//, '\/').gsub(/_/, '__') end return '/' + path.join('/') end
Expects a single argument in ARGV after options have been processed. Returns a list of possible expansions to host title definitions and hostnames from the history file
# File lib/sshmenu.rb, line 779 def list_completions() if prefix = ARGV.shift prefix = prefix.gsub(/\\(.)/, "\\1") chars = prefix.size seen = { } @config.each_item() do |parents, item| if item.host? and item.title.slice(0,chars) == prefix if not seen[item.title] puts item.title seen[item.title] = true end end end @history.each_match(prefix, true) do |name| if not seen[name] puts name seen[name] = true end end end exit end
Constructs the contents of the ‘About’ box, including: program name and version, copyright information and a link to the project home page.
# File lib/sshmenu.rb, line 543 def make_about_pane pane = Gtk::VBox.new(false, 12) panel = Gtk::VBox.new(false, 12) title = Gtk::Label.new title.set_markup("<span font_desc='sans bold 36'>SSHMenu</span>"); title.selectable = true panel.pack_start(title, false, false, 0) version = Gtk::Label.new version.set_markup("<span font_desc='sans 24'>Version: #{SSHMenu.version}</span>"); version.selectable = true panel.pack_start(version, false, false, 0) author = Gtk::Label.new detail = '(c) 2005-2009 Grant McLean <grant@mclean.net.nz>' author.set_markup(" <span font_desc='sans 10'>#{detail}</span> "); author.selectable = true panel.pack_start(author, false, false, 10) evbox = Gtk::EventBox.new evbox.signal_connect('button-press-event') { |w,e| open_homepage() } evbox.signal_connect('realize') { |w| w.window.cursor = Gdk::Cursor.new(Gdk::Cursor::HAND2) } site_link = Gtk::Label.new site_link.set_markup("<span font_desc='sans 10' foreground='#0000FF' " + "underline='single'>#{SSHMenu.homepage_url}</span>"); evbox.add(site_link) panel.pack_start(evbox, false, false, 0) pane.pack_start(panel, true, false, 10) return pane end
Accessor for the SSHMenu::ClassMapper singleton object
# File lib/sshmenu.rb, line 633 def mapper ClassMapper.instance end
Called from show_hosts_menu to add a host to the menu
# File lib/sshmenu.rb, line 909 def menu_add_host(mif, parents, item) mif.create_item(item_path(parents, item), "<Item>") { open_win(item) } end
Called from show_hosts_menu to add the optional parts at the top of a sub-menu:
# File lib/sshmenu.rb, line 918 def menu_add_menu_options(mif, parents, item) return unless item.has_children? need_sep = false if @config.menus_tearoff? mif.create_item(item_path(parents, item) + '/<tearoff>', '<Tearoff>') end if @config.menus_open_all? mif.create_item( item_path(parents, item) + '/Open all windows', "<Item>" ) { open_all(item) } need_sep = true end return need_sep end
Called from show_hosts_menu to add a separator to the menu
# File lib/sshmenu.rb, line 903 def menu_add_separator(mif, parents, item) mif.create_item(item_path(parents, item), '<Separator>') end
Helper method to calculate where to place the main menu
# File lib/sshmenu.rb, line 977 def menu_position(menu, event) (w, h) = event.window.size x = event.x_root - event.x - 1 y = event.y_root - event.y + h + 1 # Correct if window is near bottom (mw, mh) = menu.size_request sh = menu.screen.height sw = menu.screen.width if y > 200 and y + mh > sh y = event.y_root - event.y - mh - 1 y = 0 if y < 0 end # Correct if window is near right if x > 200 and x + mw > sw x = sw - mw x = 0 if x < 0 end return [x, y] end
Called when the main application button is clicked. Responds by displaying the main menu.
# File lib/sshmenu.rb, line 805 def on_click(widget, event) return show_hosts_menu(event) if event.button == 1 return false end
Invoked if the user selects ‘Open all windows’ from a sub-menu. Does the same as open_win but for each host on the menu.
# File lib/sshmenu.rb, line 1078 def open_all(menu) add_key menu.items.each do |item| if item.host? if @launch_proc @launch_proc.call(item) else shell_command(build_window_command(item)) end sleep 0.1 # to avoid .xauth lock conflicts with parallel connects end end end
Called from the SSHMenu::PrefsDialog if the user clicks on the home page URL in the ‘About’ box. Attempts to open the URL in a browser window.
# File lib/sshmenu.rb, line 1141 def open_homepage prog = browser_program or return shell_command("#{prog} #{SSHMenu.homepage_url}") end
Invoked if the user selects a host from the menu. Yields the SSHMenu::HostItem object to the wrapper script block if a block was supplied, otherwise builds a command line with build_window_command and executes it.
# File lib/sshmenu.rb, line 1066 def open_win(host) add_key if @launch_proc @launch_proc.call(host) else shell_command(build_window_command(host)) end end
Invoked if the ‘Remove SSH keys from agent’ option is selected. Runs ssh-add -D.
# File lib/sshmenu.rb, line 1056 def remove_keys shell_command("ssh-add -D </dev/null >/dev/null 2>&1") @have_key = false end
Thin wrapper around the Gtk.main loop
# File lib/sshmenu.rb, line 639 def run return shell_run if @app_win == :none Gtk.main end
Show/hide the border around the main UI ‘button‘
# File lib/sshmenu.rb, line 627 def set_button_border @frame.shadow_type = @config.hide_border? ? Gtk::SHADOW_NONE : Gtk::SHADOW_OUT; end
Called by GetoptLong if the ’—config-file’ option was supplied.
# File lib/sshmenu.rb, line 758 def set_config_file(file) @config.set_config_file(file) end
Called by GetoptLong if the ’—list-completions’ option was supplied.
# File lib/sshmenu.rb, line 770 def set_list_completions(arg) defer_action { list_completions() } @options[:window] = :none end
Called by GetoptLong if the ’—socket-window-id’ option was supplied.
# File lib/sshmenu.rb, line 764 def set_socket_window_id(window_id) @socket_window_id = window_id.to_i end
Adds the ‘Properties’ and ‘About’ options to the applet context (right-click) menu - if the container is an applet.
# File lib/sshmenu.rb, line 504 def set_up_applet_menu return unless @is_applet and @app_win.respond_to?('set_menu') xml = %Q{<popup name="button3"> <menuitem name="prefs" verb="prefs" _label="Preferences" pixtype="stock" pixname="gtk-properties" /> <menuitem name="about" verb="about" _label="About" pixtype="stock" pixname="gtk-about" /> </popup>} verbs = [['prefs', Proc.new{edit_preferences}], ['about', Proc.new{applet_menu_about}]] @app_win.set_menu xml, verbs @context_menu_active = true end
Called by GetoptLong if the ’—version’ option was supplied.
# File lib/sshmenu.rb, line 745 def set_version(arg) raise ShowVersionException end
Helper method called from add_key. Sets up the environment for an askpass helper window.
# File lib/sshmenu.rb, line 1035 def setup_askpass_env if(ENV['SSH_ASKPASS'] and File.executable?(ENV['SSH_ASKPASS'])) return true end AskpassPaths.each do |path| if File.executable?(path) ENV['SSH_ASKPASS'] = path return true end end alert( "Can't find ssh-askpass.\nPerhaps you need to install a package." ) return false end
Run a shell command via ‘system’. Optionally print command to STDOUT if debugging is enabled.
# File lib/sshmenu.rb, line 1168 def shell_command(command) debug(1, "shell_command(#{command})"); system(command); end
Helper routine used by build_window_command to transform a string into a double-quoted string in which special characters have been escaped with backslashes as per standard Bourne shell quoting rules.
# File lib/sshmenu.rb, line 1123 def shell_quote(string) return '"' + string.gsub(/([\\"$`])/, '\\\\\1') + '"' end
Run method for non-GUI actions. Attempts to initiate a connection to each host listed in ARGV.
# File lib/sshmenu.rb, line 647 def shell_run return if ARGV.empty? ARGV.each do |name| open_win(@config.host_by_name(name)) end end
Toggles the visibility of the text entry widget based on the value of the ‘show text entry’ option (requires at least version 0.19 of the Ruby Gtk bindings)
# File lib/sshmenu.rb, line 490 def show_hide_text_entry return unless @entry_box if @config.show_entry? return unless can_show_entry? @entry_box.show_all if not @entry_box.visible? elsif @entry_box.visible? @entry_box.hide @app_win.resize(1,1) if @app_win.respond_to?('resize') end end
Called by the main application button click handler. Makes sure the latest config data has been loaded; constructs a menu from that config and displays the menu.
# File lib/sshmenu.rb, line 874 def show_hosts_menu(event) get_latest_config mif = Gtk::ItemFactory.new(Gtk::ItemFactory::TYPE_MENU, "<main>", nil) @config.each_item() do |parents, item| if item.host? menu_add_host(mif, parents, item) elsif item.separator? menu_add_separator(mif, parents, item) elsif item.menu? if menu_add_menu_options(mif, parents, item) sep_path = item_path(parents, item) + '/<opt_sep>' mif.create_item(sep_path, '<Separator>') end end end add_tools_menu_items(mif) menu = mif.get_widget('<main>') menu.screen = @@display menu.popup(nil, nil, event.button, event.time){ menu_position(menu, event) } return false # allow button press handling to continue end
Called from build_window_command to determine the name of the ssh command to use to connect to the specified host. Normally returns ‘ssh’ but if the supplied SSHMenu::HostItem object has its enable_bcvi property set to true then ‘bcvi —wrap-ssh —’ will be returned instead.
# File lib/sshmenu.rb, line 1111 def ssh_command(host) if host.enable_bcvi return 'bcvi --wrap-ssh --' else return 'ssh' end end
Signal handler called to update the list of matching host connection actions when text is typed into the entry box
# File lib/sshmenu.rb, line 594 def update_entry_actions(completion, text) # Clear out current actions while not @completion_actions.empty? do completion.delete_action(0) @completion_actions.shift end return unless text.length > 0 # Build a new list of actions match_start = [] match_other = [] pattern = Regexp.quote(text) @config.each_item do |parents, item| next unless item.host? if item.title =~ /^#{pattern}/i match_start.push(item) elsif item.title =~ /#{pattern}/i match_other.push(item) end end @completion_actions = [match_start, match_other].flatten[0..9] @completion_actions.each_with_index do |item,i| completion.insert_action_markup(i, "<b>Host:</b> #{item.title}") end i = @completion_actions.length @completion_actions.push(text) completion.insert_action_markup(i, "<b>Add menu item:</b> #{text}") end
Signal handler called to update the list of matching auto-completions when text is typed into the entry box
# File lib/sshmenu.rb, line 579 def update_entry_completions(store, text) store.clear return unless text.length > 0 i = 0 @history.each_match(text) do |line| store.set_value(store.append, 0, line) i += 1 break if i > 10 end end