SSHMenu Hacker's Guide

Welcome to the SSHMenu Hacker's Guide. You might use this document if you wish to:

  • run SSHMenu in an environment other than the GNOME panel
  • customise SSHMenu to suit your needs
  • understand how the SSHMenu program works
  • criticise my Ruby code and tell me how I could do it better
  • etc

As a hacker, you probably don't want to be bogged down with background information. You just want to get coding right? Well, if you do want the background stuff, you can skip down to the section entitled Overview and start from there. For everyone else, here's some code ...

Hack 1: A Standalone SSHMenu

Run this command:

sshmenu-gnome

The command listed above is an alternative to the 'sshmenu-applet' wrapper script which is invoked by the GNOME panel. Instead of running the SSHMenu as a GNOME panel applet, this command will run the menu in its own window. That might be handy if you want to 'swallow' the menu into the panel of another desktop environment such as Xfce or AfterStep.

It's also handy to run the SSHMenu as a standalone window when you're debugging your later hacks, since anything sent to STDOUT or STDERR will go to the terminal window where you started the program.

Hack 2: A GNOME-Free SSHMenu

Run this command:

sshmenu

This command is exactly the same as the previous one except it starts a version of the SSHMenu which has no GNOME dependencies - it uses Xterm instead of gnome-terminal and does not attempt to query the gconf database.

Hack 3: A Custom Wrapper Script

Create your own script containing these lines:

#!/usr/bin/ruby

require 'sshmenu'

app = SSHMenu::Factory.make_app()

app.run

All the logic for the SSHMenu application lives in two Ruby library files - sshmenu.rb and gnome-sshmenu.rb. A wrapper script provides a context in which the application can run. The default context is the GNOME panel, but you can use your own wrapper script to create an instance of the application in another context - in this case, a standalone window.

Note, the make_app method can accept number of optional parameters as documented here. If a window object is supplied, the Factory class will build the SSHMenu application in the supplied window. If not, a new top-level will be created.

The app.run method simply calls Gtk.main. If you are embedding the SSHMenu object in a larger Ruby/Gtk application then your program should call Gtk.main as normal and not app.run.

Hack 4: Custom Launch Code

Modify your wrapper script to look like this (add the do |host| ... end block):

#!/usr/bin/ruby

require 'sshmenu'

app = SSHMenu::Factory.make_app() do |host|
  system("xterm -bg '#FFFF66' -fg '#000066' -e ssh #{host.sshparams} &")
end

app.run

Now when you select a host from the menu it will be started in an Xterm window with custom background and foreground colours. You might also like to include "-T '#{host.title}'" in the xterm command line to set the window title (be sure to add it before the '-e' option which must come last). Beware that your shell prompt may include an escape sequence which overrides the window title.

At the risk of stating the obvious, you can put whatever code you like between the 'do |host|' and the 'end' lines.

Hack 5: Disable the SSH Agent Checks

Before launching an SSH connection, SSHMenu always attempts to confirm that your SSH agent has knowledge of at least one key. If you want to disable that check (perhaps because you have a password-less key - but I hope not), you can disable the check by defining a new version of the 'add_key' method which does nothing. Rather than modifying the sshmenu.rb which defines this method, you can simply define your own version of the method directly in your wrapper script:

#!/usr/bin/ruby

require 'sshmenu'

class SSHMenu::App
  def add_key
    # like the goggles, this does nothing
  end
end

app = SSHMenu::Factory.make_app() do |host|
  system("xterm -bg '#FFFF66' -fg '#000066' -e ssh #{host.sshparams} &")
end

app.run

Hack 6: Command-Line Arguments

SSHMenu supports command-line argument handling. A simple example of why you might want this is if you want to run multiple instances of SSHMenu - each with its own config file. You'd achieve this using the --config-file option like this:

sshmenu-gnome --config-file $HOME/.sshmenu.other

If you're using a custom wrapper script, the command line arguments would normally be parsed automatically from the global ARGV array. If you want to override this behaviour, you can supply an array of alternative arguments to make_app():

app = SSHMenu::Factory.make_app(:args => [ '--args-here' ])

If you want to implement your own options, you'll need to do two things:

  1. Override the getopt_defs method in your app class to add to the list of recognised options.
  2. Add a method to handle setting the option. For example if you added the option --foo-bar then you would need to define the method set_foo_bar(). Your method will be called automatically when the option needs to be set.

Hack 7: Handling Environment Settings

In hacks 4 and 5, we used host.sshparams to get the parameters for building the terminal window and SSH command line. If your host definition includes an environment setting like this ...

LC_ALL="pl_PL.iso-8859-2" www.example.pl

... you'll need to take care with any custom wrapper code which builds a command string. In particular, you probably want the environment setting to apply to the local terminal process and all its child processes. SSHMenu provides methods which will give you just the environment settings without the ssh params and also just the ssh params without the environment settings. This sample wrapper script shows you how to achieve that:

#!/usr/bin/ruby

require 'sshmenu'

app = SSHMenu::Factory.make_app() do |host|
  system("#{host.env_settings} xterm -e ssh #{host.sshparams_noenv} &")
end

app.run

Hack 8: Embedding SSHMenu in Another Window

SSHMenu supports the use of the XEmbed protocol to allow the menu to be embedded in the user interface of a separate process (such as a window manager panel).

The embedder process should create a child window 'socket' to host the SSHMenu user interface. The process should determine the window ID of the child window and then execute a new SSHMenu instance, passing it the window ID, e.g.:

sshmenu --socket-window-id 56623138

For a complete working example, here's a Perl GTK program which embeds an SSHMenu instance by creating a Gtk2::Socket object and then passing its window ID to a new SSHMenu process:

#!/usr/bin/perl -w

use strict;

use Gtk2 -init;
use Glib qw(TRUE FALSE);


my $window = Gtk2::Window->new;
$window->signal_connect(destroy => sub { Gtk2->main_quit; });

my $socket = Gtk2::Socket->new();
$socket->show;
$window->add($socket);
my $window_id = $socket->get_id;

$window->show_all;

system("/usr/bin/sshmenu --socket-window-id $window_id &");

Gtk2->main;

More Advanced Hacks

To achieve more extensive modifications to SSHMenu's behaviour, you'll need to override some of its classes and methods. In particular, you'll need to understand the function of the class mapper and factory described below.

You might like to work through this case study to see things in action before returning here for the detailed description.

Overview

SSHMenu was developed as a very simple GNOME panel applet which would make establishing an SSH connection to another host as easy as selecting that hostname from a menu. A number of additional features have been added, but the user interface remains simple:

  • one 'button' which provides access to the menu
  • a preferences dialog for managing the menu entries

SSHMenu uses the GTK GUI toolkit and, by default, it will open SSH sessions in gnome-terminal windows. However, SSHMenu is not tied to the GNOME panel or any other part of GNOME.

Configuration

A typical user of SSHMenu would do all configuration via the preferences dialog. The configuration details would then be saved to a YAML file called .sshmenu in the user's home directory. A typical user would never need to edit this file, or even look at it.

You, of course, are not a typical user. You've probably just come back from looking at the config file right now, haven't you?

SSHMenu reads the config file at two times:

  1. When the program is initially started.
  2. When the menu needs to be displayed (the button was clicked) AND the config file has been modified since it was last read

So, changes to the menu will be reflected immediately, without having to stop and start the SSHMenu program.

Running SSHMenu

The SSHMenu distribution includes three wrapper scripts which can be used either for starting the program in different configurations or as a base for your own customised wrapper:

sshmenu-applet
This wrapper would typically be installed in the /usr/lib/gnome-panel directory. It is the wrapper script which will be run if you add the SSHMenu to your GNOME panel.
sshmenu-gnome
This wrapper starts up a standalone instance of SSHMenu which is functionally equivalent to the applet version. This mode is particularly useful for testing your hacks. It can also be 'swallowed' into the panel of an alternative desktop environment, but it will still use the gnome-terminal.
sshmenu
This wrapper also starts up a standalone instance of SSHMenu, but this version omits all GNOME dependencies. The most obvious difference is that SSH sessions are launched in xterm windows rather than gnome-terminal.

You will most likely want to copy one of the latter two wrappers to use as a base for your hacking. The SSHMenu code is in two Ruby libraries called sshmenu.rb and gnome-sshmenu.rb, installed in your system ruby library (e.g.: /usr/lib/ruby/1.8/sshmenu.rb). Ideally you will be able to achieve your desired customisations by deriving your own classes from the classes in these libraries. This (in theory at least) will allow you to benefit from future upgrades without having your changes overwritten.

SSHMenu Internals

SSHMenu is written in Ruby and implemented as a collection of classes.

SSHMenu::App (in sshmenu.rb)
The main user interface logic (both the 'View' and the 'Controller' in 'MVC').
GnomeSSHMenu::App (in gnome-sshmenu.rb)
A sub-class of SSHMenu::App which adds the GNOME-specific bits. This is the class used by the panel applet wrapper.
SSHMenu::Config (in sshmenu.rb)
Manages the configuration details (the 'Model' in 'MVC').
GnomeSSHMenu::Config (in gnome-sshmenu.rb)
Subclasses SSHMenu::Config to add support for listing gnome-terminal profiles.

There are additional classes which implement the user interfaces for the preferences dialog and related edit dialogs; as well as the data models for individual host menu items and submenus.

Class Mapper and Factory

The sshmenu.rb file also defines SSHMenu::ClassMapper. This is a class which is used to map symbolic names to class names, e.g.:

'app'                => SSHMenu::App
'app.model'          => SSHMenu::Config
'app.model.item'     => SSHMenu::Item
'app.model.hostitem' => SSHMenu::HostItem
'app.model.menuitem' => SSHMenu::MenuItem
'app.model.autoconf' => SSHMenu::SetupWizard
'app.dialog.prefs'   => SSHMenu::PrefsDialog
'app.dialog.host'    => SSHMenu::HostDialog
'app.dialog.menu'    => SSHMenu::MenuDialog
'app.geograbber'     => SSHMenu::GeoGrabber

The SSHMenu::Factory builds the SSHMenu application from components using the mapper to determine the class names for each component.

You can 'inject' a mapping into the ClassMapper from your wrapper script, like this:

SSHMenu::Factory.mapper.inject('app' => MySSHMenu::App)

before you call the 'make_app' method:

app = SSHMenu::Factory.make_app()

then when SSHMenu needs to create an instance of the specified component, it will use the class you specified instead of the default class.

Note, you'll need to actually define your class methods and possibly 'require' your class file before injecting a class mapping that refers to it.

Hooks

You can 'hook into' different phases of the SSHMenu program's execution by defining a subclass that wraps an existing method. You'll need to inject a class mapping that points to your new class, as described above.

For example, when the menu is being built, each host is added to the menu by a method called 'menu_add_host' in the SSHMenu::App class (or the GnomeSSHMenu::App). You can create you own class that overrides this method and then add your own code to execute before, after or instead of the standard method, like this:

class MySSHMenuApp <SSHMenu::App    # or <GnomeSSHMenu::App

  def menu_add_host(mif, parents, item)
    # put 'before' code here
    super(mif, parents, item) # skip this if your code works 'instead'
    # put 'after' code here
  end

end

You can define your class either directly in the wrapper script or in a separate *.rb file that you 'require' from the wrapper script.

Class Mapping Configuration

You can influence the class mappings directly from the .sshmenu configuration file in your home directory. This is particularly useful for modifying the behaviour of the SSHMenu applet in the GNOME panel when you don't want to modify the system-wide SSHMenu applet wrapper script.

Add a 'classes' section to the .sshmenu file. For example:

classes:
  require: /home/USERNAME/rubylib/mysshmenu.rb
  app: MySSHMenu::App

If you need to 'require' a file, you'll probably want to use an absolute pathname. If you use an unqualified name like 'company-sshmenu', the corresponding .rb file must exist in one of the standard lib directories Ruby would normally look in.

You can have as many app.* lines as you need to define your class mappings, although obviously the key for each one must be unique.

Caveats:

  • you can only have one 'require' line, so if you need to require more than one file, your require line must point to a file that 'require's the other files
  • since the app.model class is used to read the config file, its class name will already have been injected and cannot be overridden in the config file
  • when your wrapper script calls Factory.make_app() it can specify an alternative app.model class as the optional second parameter (following the parent window object)

To flesh out the example a little further, combining the above configuration with the following code (and a standard, unmodified wrapper script) would give an instance of SSHMenu which displayed all hostnames in upper case:

module MySSHMenu          # use a namespace to avoid class name collisions

  class App <GnomeSSHMenu::App  # inherit from one of the standard classes

    def menu_add_host(mif, parents, item)           # override this method
      item.title.upcase!
      super(mif, parents, item)
    end

  end

end