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. |