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.
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.
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.
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.
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 " |
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).
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!
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 }
Michael Schilliarbeitet 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. |