Wednesday, October 31, 2007

runners.improve()

krunner is a launcher application of sorts, among other things (it also does screen locking, provides a tasks lists via ksysguard, etc...). think of it as a "run command" dialog on steroids.

the aim is to provide something that not only gives us the ability to run commands as we always have been able to, but also as a one stop shop to do, well ... anything. type in some text and you get results relevant to what you typed in. it's kind of like google for your desktop, and with the search plugin ... it pretty much is.

krunner is plugin based (though it has a few built ins too; i'll probably end up moving most/all of them out to plugins, however) so you can extend it with new functionality. each plugin gets asked what it knows about what you are typing in and those answers are returned to you in a cute little interface.

these plugins, known as runners, are based on a class in libplasma called Plasma::AbastractRunner. this means that any app that links against libplasma can get the same functionality. neat.

not so neat: Plasma::AbstractRunner was a first-draft API. while i've told everyone that libplasma will be changing between 4.0 and 4.1, i want to keep the changes as minimal as possible, especially when it comes to things that affect plugins. why? because i hate rewriting code and i really hate making others do the same. changes that affect plugins affect every programmer who has written a plugin. not good.

so i spent the day today reviewing the API for Plasma::AbstractRunner. i posted revisions on pastebin.ca for #plasma to review. Richard Moore in particular was really helpful and provided some excellent feedback and thoughts that helped make the resulting API better than i could've made it on my own.

essentially instead of only getting a search term, runners now get a full blown SearchContext object. this object handles all the results as well, sorting them into Informational, Exact and Possible categories. the results can be ranked for relevancy from 0.0 to 1.0 as well as have an associated mimetype.

the end result is a much nicer design and one that should be able to survive as is from 4.0 onwards. (having said that, i'm probably going to get hit by the "famous last words" principle ;).

the improvements include:

  • future proofing. we should be able to add features and capabilities without breaking plugins.

  • informational answers, such as the value of a numerical calculation, can now be selected. this means that if you enter "=14*.5" you'll see "14*.5 = 7" and if you press enter, the text area will get replace with "= 7". this was a request in a comment from my last blog as well as from Maksim-of-khtml on irc. hugs.

  • runners will be able to 'mutate' the search as well as respond to it. this means we could have filtering runners, for instance, or runners that change the state of the SearchContext based on their own returned results. this should help runners keep out of each other's way better in the future.

  • expensive analysis of the search term (e.g. "is this a local directory or file? what is its mimetype? is it a network protocol?") can now be centralized and done just once in the SearchContext versus in every runner that needs this information

  • it's possible to have multiple searches operating in parallel if so desired

  • runners can now provide a configuration for the runner itself

  • handling of the options widgets for both matches and the runners is more sensible

  • it will be possible to add actions (verbs) to results (nouns). this means that searching for "pizza" might result not only in a match for pizza, but the associated verb "order from pizza hut"

  • we'll probably end up supporting non-C++ runners, as the new design should make it trivial to use kross for this: essentially just export the SearchContext and SearchAction objects into the Kross object

  • writing a Runner is much simpler and, i think, more obvious

  • probably other stuff that i've already forgotten or haven't yet realized



some regressions have been introduced, however. these include:


  • the results are refreshed on every search, even if they remain the same. performance still seems to be better than the previous design, however.

  • there is no asynchronous interface anymore. no runners were using the old one, anyways, and we can add a better one on top of the new design. i'm actually curious if, with a bit of work, we couldn't put the runners in their own thread(s) and thereby obviate the need for an async interface in the API (at the cost of making some of the key bits of API thread safe).



future avenues of exploration may include:


  • multi-threading the searching (as noted above)

  • adding a completion API so that as the user types they can get possible completions in real time

  • custom visualizations of actions, so a weather response might actually provide a live weather report visualization

  • researching what sorts of additional metadata would be interesting for the SearchContext

  • adding action verbs to individual search returns and a user interface to make using them feel natural



as for the "writing a runner is simpler" claim, this is because there is really only one method you are required to override now: void match(Plasma::SearchContext *search) and for most runners that need to do something when a match is selected there is the void exec(Plasma::SearchAction *action) method.

to see what this all means, here is the code for the calculator runner:

CalculatorRunner::CalculatorRunner( QObject* parent, const QVariantList &args )
: Plasma::AbstractRunner(parent)
{
Q_UNUSED(args)
setObjectName(i18n("Calculator"));
}

CalculatorRunner::~CalculatorRunner()
{
}

void CalculatorRunner::match(Plasma::SearchContext *search)
{
QString cmd = search->term();

if (cmd[0] != '=') {
return;
}

cmd.remove(0, 1);
if (QRegExp("[a-zA-Z\\]\\[]").indexIn(cmd) == -1) {
QString result = calculate(cmd);

if (!result.isEmpty()) {
Plasma::SearchAction *action = search->addInformationalMatch(this);
action->setIcon(KIcon("accessories-calculator"));
action->setText(QString("%1 = %2").arg(cmd, result));
action->setData("= " + result);
}
}
}

QString CalculatorRunner::calculate( const QString& term )
{
QScriptEngine eng;
QScriptValue result = eng.evaluate(term);

if (result.isError()) {
return QString();
}

return result.toString();
}


and here are the two relevant methods from the much more complex location runner, which handles opening paths, urls and web shortcuts like "gg:kde" for google, "wp:kde" for wikipedia (and unlike in kde3, it uses your default browser properly =):


void WebshortcutRunner::match(Plasma::SearchContext *search)
{
QString term = search->term().trimmed().toLower();
m_type = search->type();

foreach (KService::Ptr service, m_offers) {
foreach (QString key, service->property("Keys").toStringList()) {
key = key.toLower() + ":";
if (term.size() > key.size() &&
term.startsWith(key, Qt::CaseInsensitive)) {
QString actionText = QString("Search %1 for %2");
actionText = actionText.arg(service->name(),
term.right(term.length() - term.indexOf(':') - 1));

QAction *action = search->addExactMatch(this);
action->setText(actionText);
QString url = getSearchQuery(service->property("Query").toString(), term);
action->setData(url);

// let's try if we can get a proper icon from the favicon cache
QIcon icon = getFavicon(url);
if (icon.isNull()){
action->setIcon(m_icon);
} else {
action->setIcon(icon);
}

return;
}
}
}

if (m_type == Plasma::SearchContext::Directory ||
m_type == Plasma::SearchContext::Help) {
//kDebug() << "Locations matching because of" << m_type;
QAction *action = search->addExactMatch(this);
action->setText(i18n("Open %1", term));
action->setIcon(m_icon);
return;
}

if (m_type == Plasma::SearchContext::NetworkLocation) {
QAction *action = search->addPossibleMatch(this);
KUrl url(term);

if (url.protocol().isEmpty()) {
url.clear();
url.setHost(term);
url.setProtocol("http");
}

action->setText(i18n("Go to %1", url.prettyUrl()));
action->setIcon(m_icon);
action->setData(url.url());
return;
}
}

void WebshortcutRunner::exec(Plasma::SearchAction *action)
{
QString location = action->data().toString();

if (location.isEmpty()) {
location = action->term();
}

if (m_type == Plasma::SearchContext::UnknownType ||
(m_type == Plasma::SearchContext::NetworkLocation &&
location.left(4) == "http")) {
KToolInvocation::invokeBrowser(location);
} else {
new KRun(location, 0);
}
}


now i'm off to techbase to write a tutorial on AbstractRunner so that people can start implementing all those great ideas in the comments on my my last blog entry. ;)

tomorrow i'll start working on the gui of krunner to make it non-fugly. if all goes well, i won't have to look much more at krunner before 4.0 is out.

22 comments:

Anonymous said...

I like that. I want to ask, if the following behavior might also get integrated:
1) While entering Text one could scroll through the results and the command currently highlighted is replacing the entered Text
2) But the focus for entering Text will remain on the searchbar, that way I could pass commandline-arguments to the currently selected command

Thanks for a nice krunner, I already like it more than the one of KDE3

Diwaker said...

I'd really like to see krunner mature into something like Quicksilver. Quicksilver is a wonderful application and I think there are a lot of good take aways from its architecture and design. Keep up the great work!

Anonymous said...

just a little question: why do you immediately check all offers instead of first checking if the given command includes a ":"? Could this be an optimisation? Maybe let the plugins give a list of regexp's they are only interested in?

Federico Gherardini said...

Hi Aaron!
First of all congratulations for all the great work you are doing to make kde4 totally rock! As Diwaker said I was wondering whether
"it will be possible to add actions (verbs) to results (nouns)."
means that it will be possible for krunner to behave somewhat like Quicksilver. I personally use alt+f2 all the time in kde3 and it would be great to use krunner also for sending files, playing songs etc. (i.e. associating relevant actions to files in general) without interrupting your current workflow.

Anonymous said...

multi-threading the searching (as noted above)

With ThreadWeaver::Job this should be really easy to do, right?

Anonymous said...

Please don't assume that web-shortcuts are triggered with ":". At least in KDE3 it is possible to change the keyword-delimiter to 'space' too.

Anonymous said...

I have the same request as the previous anonymous. In kde3 I've changed the delimiter to 'space' and it is much faster.

Also, it would be nice if the calculator accepted strings like "7*9", not just the ones starting with "=".

Thank you very much!
You all are doing a great job :)

Michael Rudolph said...

Hi Aaron,
I'm really glad to hear that krunner will be able to make sense of verbs as well as objects. Is it planned to make use of our existing servicemenu infrastructure? Mimetype based actions seems like something sensible to do.
(There would even be a tutorial already available, some guy wrote it once :-)

michael

Javier said...

The part of adding verbs to actions made me think of the procedural methods in Spore, since it's really similar to how Willl Right announced it.

I was surprised to see how short the code was for the runners, if a person was inspired and had a lot of ideas, he/she could easily write a bunch of them in a fairly short amount of time o_O

Aaron J. Seigo said...

@anon[0]: "scroll through the results and the command currently highlighted is replacing the entered Text"

well, that's not how it's intended to work, really: the matches are responses to your text ...

"I could pass commandline-arguments to the currently selected command"

that might be interesting, however. you can already pass commandline arguments, but you need the full command, right now.

@diwaker: "mature into something like Quicksilve"

it already is; however it will never be a quicksilver clone due to other requirements, and i don't think that's a bad thing.

@anonyous[1]: "Maybe let the plugins give a list of regexp's they are only interested in?"

that's what each plugin essentially does in match(). except that it's much more powerful since the plugin can do whatever heuristics make sense for that given plugin.

see the calculator plugin, for instance.

@frederico: "great to use krunner also for sending files, playing songs etc"

that is indeed the idea behind "adding verbs to the nouns". that particular feature may not make it in 4.0's interface, but it is at least feasible to add now.

@anonymous[2]: "With ThreadWeaver::Job this should be really easy to do, right?"

well, easier perhaps. but there are data structures that would still need to be protected against concurrent access.

@anonymous[3]: 'Please don't assume that web-shortcuts are triggered with ":"'

there's a TODO in the code for the webshortcuts runner that notes we probably need to check this setting =) currently we don't.

@anonymous[4]: 'it would be nice if the calculator accepted strings like "7*9"'

the '=' is familiar from both elementary math as well as spreadsheets and makes it much clearer to the runner when you actually want a calculation versus when you are inputting numbers for some other purpose.

@michael rudolph: "Is it planned to make use of our existing servicemenu infrastructure"

that would be up to each runner, though i expect the mimetype runner would do so (servicemenus are also mimetype based)

@javier: "The part of adding verbs to actions made me think of the procedural methods in Spore, since it's really similar to how Willl Right announced it."

yeah, it's a fairly hot concept right now in various comp sci areas. i can't claim to have originated it =)

"I was surprised to see how short the code was for the runners, if a person was inspired and had a lot of ideas, he/she could easily write a bunch of them in a fairly short amount of time"

that is exactly the point. =)

mmmm said...

Aaron, please don't make calculator runner dependent on "=2*5" syntax, it is step back from old kde3 run dialog (which understood simple "2*5" syntax). I understand it is better for parsing, but it isn't too hard to make it work without "=".

Also please fix webshortcuts runner to support other delimiters than ":", I know you said it is in TODO, but this really should be fixed in KDE 4.0, not after that.

Aaron J. Seigo said...

@mmmm:

first, you nick makes me hungry. it reminds me of what food makes me say. damn you! ;)

second, i'm going to play devil's advocate with you. enjoy =)

'please don't make calculator runner dependent on "=2*5" syntax, it is step back from old kde3 run dialog (which understood simple "2*5" syntax)'

i'm not sure how much more "complex" having to put an '=' sign is. as to discoverability, most people never discovered the calc feature in kdesktop's minicli.

the '=' neatly namespaces the whole issue, which is a step forward from kdestop's minicli.

your suggestion is akin to saying that "email aseigo" is wrong and that it should just be "aseigo" which, upon pressing enter, would unconditionally email me.

krunner is not minicli. it provides all of its features, but does so in its own pimp style. there will be changes, and we get a ton of possibilities in exchange.

please try it for a week and see if the '=' really makes a reasonably negative difference.

"this really should be fixed in KDE 4.0"

so should hundreds of other items, many of which are way more important than this. not all things will be. one can either (a) live with it or (b) contribute a patch.

it will get addressed at some point, with "some" being defined by more people than just me.

personally, i'd rather see my time spent on design and architectural issues at this point rather than detail level improvements.

that said, you have no idea how painful it is to not be able to hack on fun and easy stuff like that due to the # of outstanding "serious" issues that need my attention.

Anonymous said...

@Aaron:

I pity the decision, that scrolling through the list isn't intended. Look at the "corresponding" program in E17, it has that feature and I liked it. (You can't edit the commandline easily though.)

I thought that it would be a usability improvement, because now I would either have to type in the whole command like it is listed or have to manually select the entry. Scrolling with the up/down-arrows should speed this up.


Benjamin

Aaron J. Seigo said...

@Benjamin: "I pity the decision, that scrolling through the list isn't intended."

one of us is confused as to what anonymous[0] asked for, because that isn't what i was talking about.

the request was to replace the current text with whatever item you have selected; well, that just won't work in many cases because there is no command to place in there.

that said, it would be trivial to add an informational item that stands in as a command line text completion object. that's what the informational-with-data-and-not-disabled actions are for, after all.

Anonymous said...

@Aaron:

"one of us is confused as to what anonymous[0] asked for, because that isn't what i was talking about."

As I happen to be the author of that comment, I admit that I thought, that I had stated my wish (and my answer to you) clearly. But reading your comment

"the request was to replace the current text with whatever item you have selected; well, that just won't work in many cases because there is no command to place in there."

it's now clear to me why it wouldn't work well. But couldn't it be done the following way:
a) replace the text shown with the current selected entry
b) if the user presses Enter or the Buttons proceed as it's been done now
c) if the user presses a "magic" key (perhaps ctrl only) expand the text shown to the invoked command (if this would make sense) or ignore it (perhaps with es short message "sorry you can't do that here").

I think this way it would improve usability (I can obviously only speak for me), but I don't know if it could be implemented like that.

As for the last paragraph of your answer, I must say, that I didn't really understand what it means.

Benjamin

Anonymous said...

Great work on krunner, I can't wait for KDE4 to go stable so I can try it :)

But this whole discussion about the "=" before maths has confused me, before I go on it would probobly be helpfull to say that I still think of krunner as something like this
http://kde.org.pl/wiki/images/2/24/RoadToKDE4_update_krunner.png

If I typed in aseigo then it might create a list of options like:
E-mail aseigo
Web favourites: aseigo's blog

Given this I can't see why the = sign would create any real advantage, if krunner receives what could be a maths question, or could be something else why can't it display a list with the action at the top, and the result of the calculator at the bottom without any disadvantage?

Besides woldn't people typing actual maths into Krunner, and not wanting the calculator, be rare enough for it to not cause any problems?

Its not like the = sign is bad, but it is an extra character to type so if its not too much trouble I'd like to know why I'm typing it :)



On a couple of unrelated matters, firstly if you type and then the runner you want appears in the list, do you have to click on it or can you use the down arrow?

And I know you can type something like "ifconfig eth0 up", but could you type "if", then select ifconfig from the menu (arrow or mouse) then type "eth0 up"

Just to end on a positive note, thanks for all the hard work and I can't wait for KDE4

Aaron J. Seigo said...

@Benjamin: you had me until the phrase "magic key" =) i think we'll have something that is quite useful and compelling without such things.

as for the last paragraph, what i mean is this: if a runner can offer *suggsted* completions for an entry, it can provide those now via an Informational match result. in the future we'll probably have a tab-completion system.

but selecting items from the list to replace what's in the search line is going to be clunky at best...

@anonymous[N]: "the = sign would create any real advantage"

it:

* increases performance
* ensures we don't get whacky results in the list such as when types "42" and you get back "42" down below. (i also got rid of showing errors as they weren't helpful at all, so that's already an improvement ...)
* sets an example for other runners to follow (e.g. claim predicates)

"if you type and then the runner you want appears in the list, do you have to click on it or can you use the down arrow"

right now you have to click, but in the final interface you'll be able to keyboard all the way. i spent time on kickoff ensure the same thing there, btw.

"ifconfig from the menu "

not from the list; that's the wrong place. this would be something we could add to the API to provide a "tab completion" style interface.

"I can't wait for KDE4"

me neither =)

Anonymous said...

@Aaron:

thank a lot for spending your time replying to all those comments.

So to sum up your answer: In the end we will (at one time) be able to easily pick up an entry via keyboard and (if provided) expand it via Tab-Completion.

Yippieh!

Benjamin

Chani said...

shortcuts like "gg:" and "wp:" aren't really discoverable. how are newbies going to find out they exist?

maybe a little "shortcuts" link in the UI that brings up a list - but that'd only work for builtin ones, not new plugins....

Anonymous said...

Thanks for takeing the time to respond, (I asked about the reasons for the = sign among other things), I just have one more question based on your last awnser.

You say that you can't type "if" select ifconfig from the list then type "eth0 up", but you will(may?) be able to type if and then use tab compleate, that's a nice feature but I would like to be able select from the list then start typeing if only to make my life easier when the option I want is one of many in a large list.

maxauthority said...

First, I am also much FOR using the = syntax in the runner.

But second (which enforces my view on the = issue), I see no reason to restrict the calculator runner to non [a-zA-Z] things if we start with = anyway.

I don't know if QScriptEngine can do it easily, but at least support for sin(), cos() sqrt(), pi would be nice or why not just provide a direct interface to QScriptEngine if we start the runner with = without any checking on the input value?

Anyway, i know that's all future improvments of the runners themselves and that the state of krunner is more important now, just my opinion that the calculater runner shouldn't be too basic.

Anonymous said...

Great stuff.
Just for the record (re previous comment):
=a=1; a+1
works in 4.0.2 and results in 2.

"a" gets mapped to Math.a, so it seems that quite some more is possible (e.g. sin() is available)