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.