Im Rampenlicht (Linux-Magazin, Juni 2008)

Die Mac-Utility ``Spotlight'' führt selbst hartgesottene Apple-Fanboys von der Maus wieder zurück zur Tastatur. Ein kurzes Perlskript implementiert die praktische Utility für den Linux-Desktop nach.

Wer schon einmal auf einem zugekleisterten Desktop nach dem Icon einer bestimmten Applikation gesucht hat, wird sich vielleicht gefragt haben: Wer ist nur auf die hirnrissige Idee gekommen, Applikationen mit der Maus auszuwählen? Wenn man weiß, wie die Applikation heißt, gibt es doch keinen Grund, 10 Sekunden damit zu verplempern, das entsprechende Icon aus dutzenden auf dem Desktop herauszusuchen oder sich durch mehrere verschachtelte Dropdown-Menüs zu hangeln, um das mühsam Gefundene dann endlich per Mausklick zu starten.

Abbildung 1: Der Benutzer hat Spotty mit dem Hotkey CTRL-u aufgerufen und 'gi' ins Eingabefeld getippt. Spotty hat erkannt, dass es sich bei der gewünschten Applikation nur um den Gimp handeln kann. Die Tab-Taste komplettiert "gimp" und startet den Gimp.

Statt dessen aktiviert man lieber einen selbstdefinierten Hotkey und zack! poppt rechts oben im Desktop der in Perl geschriebene Spotty auf (Abbildung 1). Das Textfeld im Spotty-Fenster hat nun bereits den Tastaturfokus, man tippt einfach ``fi'' und schon weiß Spotty, dass es sich nur um ``Firefox'' handeln kann, pumpt diesen in der Auswahlliste ganz nach oben und der Benutzer drückt die ``Tab''-Taste, um den Vorschlag zu übernehmen und die gewählte Applikation auch gleich zu starten. Verstrichene Zeit: Nicht mal 2 Sekunden.

Spotty lernt aus erfolgreichen Starts und merkt sich die gefundenen Programmnamen in einer persistenten Datenbank. Beim ersten Aufruf tippt der Benutzer noch ``firefox'' ein und schickt den String mit der Enter-Taste ab, dann sucht Spotty in sämtlichen Pfaden der Umgebungsvariablen $PATH und führt das gefundene Programm aus. Beim nächsten Aufruf versucht es schon, die Eingabe des Benutzers mit bereits gelernten Programmnamen zur Deckung zu bringen und zeigt Treffer rechts des Eingabefelds an. Sieht der Benutzer, dass Spotty das gewünschte Programm ganz oben in der Trefferliste anzeigt, braucht er nur noch die Tab-Taste zu drücken, damit Spotty die entsprechende Applikation startet.

Kein Zurück nach Exec

Spotty führt die gewünschte Applikation mit exec aus. Sie überlädt den aktuellen Prozess (also das Perlskript) mit der externen Applikation, was zur Folge hat, dass Spotty aus der Prozesstabelle verschwindet und nur die gestartete Applikation übrigbleibt. Die exec-Zeile 110 in Funktion launch ist also tatsächlich das Ende des Skripts, da der Prozess aus ihr nicht mehr zurückkehrt.

Die Datenbank für die gemerkten Programmnamen ist ein persistenter Hash, der mit dem CPAN-Modul DB_File mit einer Berkely-DB-Datei verknüpft ist. Die Datenbank wird aufgefrischt, sobald das Skript den mit tie verbundenen Hash verändert. Damit später beim Programmschluss auch alles ordentlich herunterfährt, setzt Zeile 106 kurz vor dem exec-Kommando noch ein untie-Kommando ab, um den Hash von der Datenbankdatei loszueisen und alle Änderungen permanent abzuspeichern.

Malen mit Tk

Wie aus Listing 1 ersichtlich, nutzt Spotty das Tk-Modul vom CPAN, um das Applikationsfenster mit dem Eingabefeld zu zeichnen. Damit dieses nicht irgendwo auf dem Desktop landet sondern genau in der rechten oberen Ecke, ruft es anschließend die Methode geometry() mit dem Parameter ``-0+0'' auf. ``-0'' steht hierbei für die rechteste X-Koordinate und +0 für die oberste Y-Koordinate.

Das Hauptfenster mit dem Namen $top ist vom Typ MainWindow und enthält zwei sogenannte Widgets: Links ein Eingabefeld vom Typ Entry und rechts davon eine Anzeige vom Typ Label. Am Eingabefeld-Widget $entry hängt eine Textvariable $input, in der Tk den vom Benutzer eingetippten Text ablegt und nach jedem Tatendruck auffrischt. Da die Option -validate den Wert "key" aufweist, springt Tk bei jedem Tastendruck im Eingabefeld auch noch die Funktion validate() (definiert ab Zeile 63) an, deren Referenz das Widget in der Option -validatecommand mitbekam. Diese Funktion validiert hier aber nichts, denn sie gibt immer 1 zurück, ist also mit allem einverstanden. Sie dient nur dazu, nach jedem Tastendruck des Benutzers einen Callback aufzurufen, der in der Datenbank nachsieht, ob schon Treffer vorliegen. Das rechts vom Entry-Widget liegende Label-Widget hingegen überwacht eine Textvariable $label_text, und sobald sich deren Wert ändert, frischt der Tk-Manager die Anzeige auf. Findet die Funktion validate() also mit matches() (Zeile 71) Treffer zum gerade eingegebenen Wort, fügt sie diese durch Zeilenumbrüche getrennt zu einem String zusammen und pflanzt diesen in $label_text ein, worauf Spotty die Treffer rechts vom Eingabefenster anzeigt, ohne dass hierzu weitere Programmierschritte notwendig wären.

Der Packer (Zeile 44/45) packt beide Widgets mit der Option -side => "left" in das Containerobjekt, das Hauptfenster $top. Wandern mehrere Objekte mit ``left'' in den Container, reiht der Packer sie von links nach rechts auf. Dies liegt daran, dass ``left'' letztendlich nur heißt, dass der Packer das Widget an den linken Rand des noch verfügbaren Platzes klebt. Ist links schon ein Widget, wandert das nächste also an den linken Rand des rechts verbliebenen freien Raums.

Spotty reagiert auf die Return- und die Tab-Taste. Return übernimmt den bisher eingegebenen String und Tab schnappt sich das erste Element aus der Vorschlagsliste. Perl-Tk bindet die Tasten durch bind-Aufrufe in den Zeilen 48 und 50 an die Funktionen launch() (definiert ab Zeile 94) und complete() (ab Zeile 86). Letztere setzt lediglich die Variable des Entry-Widgets auf den den obersten Treffer, der in $first_match abgelegt wurde und ruft anschließend launch() auf, damit der Benutzer nicht mal mehr die Enter-Taste betätigen muss, um die gefundene Applikation zu starten. Der Bind-Eintrag in Zeile 47 legt fest, dass Spotty abbricht, falls jemand CTRL-q drückt und somit aussteigen möchte, ohne eine Applikation zu suchen.

Im Brennglas des Users

Der Aufruf der Methode focus() in Zeile 52 setzt den Tastaturfokus auf das Entry-Widget. Dies ist wichtig, denn sonst müsste der Benutzer erst mühsam mit der Maus ins Eingabefeld klicken, damit das Widget Tastatureingaben verarbeitet. Und das Einsparen von Mausklicks war ja der ursprüngliche Sinn der Übung, oder?

Ist alles definiert, setzt MainLoop in Zeile 53 die GUI in Gang und läuft, bis eine Applikation startet oder der Benutzer sich dazu entschließt mit CTRL-Q das Programm abzubrechen. In diesem Fall oder in Fehlerfällen hilft die Funktion bail (Zeile 56) beim Aufräumen. Sie ruft die destroy-Methode des Top-Fensters auf und faltet damit die GUI zusammen.

Heiße Tasten

Für den Desktop-Hotkey, der Spotty startet, empfiehlt es sich, eine Tastaturkombination zu wählen, die in keinem Anwendungsprogramm vorkommt, da dieses sie sonst schluckt, wenn der Tastaturfokus zufällig auf der Applikation ist. Bei Tastenschluckern wie vim ist das nicht zu einfach. Ich habe CTRL-u gewählt, da das einfach zu tippen ist und nicht zu meinen oft ausgeführten vim-Kommandos gehört. (Vim hat die Kombination natürlich belegt und scrollt damit den editierten Text nach oben, aber ich verwende stattdessen CTRL-b).

Abbildung 2: Die Utility gconf-editor definiert unter Apps/Metacity/global_keybindings den run_command_1 mit den Hotkey "u"

Abbildung 3: Der Eintrag unter "command_1" im Verzeichnis "keybinding_commands" versieht den vorher definierten Hotkey mit einem benutzerdefinierten Kommando.

Der Gnome-Desktop meiner Ubuntu-Installation meint aber leider, er wisse alles besser und erlaubt nur, einen ausgewählten Fundus von Applikationen an Hotkeys zu binden, nicht aber beliebige Programme. Aber ein beherztes Aufrufen von gconf-editor löst das Problem. das Paket kann mit sudo apt-get install gconf-editor instaliert werden, falls es fehlt.

Unter ``Apps'' bietet es im ``Metacity''-Eintrag (Metacity ist der Windows-Manager unter Gnome) die Einträge ``global_keybindings'' und ``keybindings_commands'' an. Unter ``global_keybindings'' setzt man dann ``run_command_1'' auf die gewünschte Hotkey-Kombination (z.B. ``<Control>u'') und stellt anschließend unter ``keybindings_commands'' als ``Value'' den Pfad zu Spotty als ``/pfad/zu/spotty'' ein (Abbildung 3).

Erweiterungen

Falls das auszuführende Programm Root-Rechte verlangt, also mit sudo aufgerufen wird, gibt es ein kleines Problem, denn der Benutzer muss zunächst sein Passwort eingeben. Der Ubuntu-Package-Manager synaptic ist so ein Beispiel. Er läuft zwar auch ohne Root-rechte, kann dann aber nur Pakete abfragen und keine neuen installieren. Spotty hilft sich mit einem in Zeile 7 definierten Hash %sudo_programs weiter. Gibt der Benutzer eines der dort gelisteten Programme ein, findet in Zeile 108 nicht nur ein exec statt, sondern Spotty startet ein xterm-Terminal, das das gewünschte Programm mit sudo startet. Der Effekt: Im aufpoppenden xterm frag die Shell zuerst das Passwort ab und falls dieses richtig eingegeben wird, startet sie das gewünschte Programm tatsächlich mit Root-Rechten.

Wer außer den in PATH voreingestellten Pfaden noch in weiteren Verzeichnissen nach Kommandos suchen möchte, kann zum Array @misc_paths in Zeile 10 noch weitere Einträge hinzufügen. path_search() findet Programme dann automatisch auch im erweiterten Verzeichnisfundus. Wer statt Buchstaben lieber mit Cursortasten manövriert, kann rechts vom Eingabefeld eine Listbox zeichnen, die mit Treffern gefüllt ist und deren Selektion erlaubt.

In jedem Fall: Schluss mit dem Herumgesuche, wenn man weiß, wonach man sucht!

Listing 1: spotty

    001 #!/usr/local/bin/perl -w
    002 use strict;
    003 use Log::Log4perl qw(:easy);
    004 use DB_File;
    005 use Tk;
    006 
    007 my %sudo_programs = map { $_ => 1 } 
    008                     qw(synaptic);
    009 
    010 my @misc_paths = qw(/usr/sbin);
    011 
    012 my($home) = glob "~";
    013 my $spotty_dir = "$home/.spotty";
    014 
    015 #Log::Log4perl->easy_init();
    016 
    017 if(! -d $spotty_dir) {
    018   mkdir $spotty_dir, 0755 or
    019     LOGDIE "Cannot mkdir $spotty_dir ($!)";
    020 }
    021 
    022   # Init database
    023 my %DB_FILE;
    024 tie %DB_FILE, 
    025     "DB_File", "$spotty_dir/db_file.dat" 
    026         or LOGDIE "$!";
    027 
    028   # Application window
    029 my $top = MainWindow->new();
    030 $top->geometry("-0+0");
    031 
    032 my($input, $first_match, $label_text);
    033 
    034 my $label = $top->Label(
    035   -textvariable => \$label_text, 
    036   -width        => 20 );
    037 
    038 my $entry = $top->Entry( 
    039   -textvariable    => \$input, 
    040   -validatecommand => \&validate,
    041   -validate        => "key",
    042 );
    043 
    044 $entry->pack( -side => "left" );
    045 $label->pack( -side => "left" );
    046 
    047 $entry->bind("<Control-Key-q>", \&bail);
    048 $entry->bind("<Return>", 
    049              sub { launch($input) });
    050 $entry->bind("<Tab>", \&complete);
    051 
    052 $entry->focus();
    053 MainLoop;
    054 
    055 ###########################################
    056 sub bail {
    057 ###########################################
    058 
    059   $top->destroy();
    060 }
    061 
    062 ###########################################
    063 sub validate {
    064 ###########################################
    065   my($got) = @_;
    066   $label_text = join "\n", matches($got);
    067   return 1;
    068 }
    069 
    070 ###########################################
    071 sub matches {
    072 ###########################################
    073   my($got) = @_;
    074 
    075   my @all = sort keys %DB_FILE;
    076   my @matches = grep { /^$got/ } @all;
    077   if(@matches) {
    078       $first_match = $matches[0];
    079   } else {
    080       $first_match = undef;
    081   }
    082   return @matches;
    083 }
    084 
    085 ###########################################
    086 sub complete {
    087 ###########################################
    088 
    089   $input = $first_match;
    090   launch($input);
    091 }
    092 
    093 ###########################################
    094 sub launch {
    095 ###########################################
    096   my($program) = @_;
    097 
    098   my $path = path_search( $program );
    099 
    100   LOGDIE "$program not found ",
    101          "in path ($ENV{PATH})" unless
    102              defined $path;
    103 
    104   $DB_FILE{ $program }++ if defined $path;
    105   DEBUG "Launching $path";
    106   untie %DB_FILE;
    107   if(exists $sudo_programs{ $program } ) {
    108       exec "xterm", "-e", "sudo", "$path";
    109   } else {
    110       exec $path;
    111   }
    112   LOGDIE "exec $path failed: $!";
    113 }
    114 
    115 ###########################################
    116 sub path_search {
    117 ###########################################
    118   my($program) = @_;
    119 
    120   DEBUG "PATH is $ENV{PATH}";
    121 
    122   for my $path ( split(/:/, $ENV{PATH}), 
    123                  @misc_paths ) {
    124       if(-x "$path/$program") {
    125           DEBUG "$program found in $path";
    126           return "$path/$program";
    127       }
    128   }
    129 
    130   ERROR "$program not found";
    131   return undef;
    132 }

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2008/06/Perl

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.