Development/Tutorials/SuperKaramba

From KDE TechBase
Revision as of 22:28, 18 October 2007 by Wirr (talk | contribs) (Use an up-to-date image)

SuperKaramba

Intro

SuperKaramba is a tool that allows one to easily create functionality enhancement modules on a KDE desktop. Such modules are interactive programs written in Python, Ruby or KDE JavaScript that are usually embedded directly into the background and do not disturb the normal view of the desktop.

Links

This screenshot shows the Aero AI Karamba Theme running on KDE4 as Plasmoid (aka Plasma Applet).

Basic Tutorial for Theme Creators

Themes in SuperKaramba consist of two parts. A theme file that defines the shape and size of the theme, and possibly many other things and a script file that adds interactivity to the theme. Basically, whenever an event happens, such as the user clicking the mouse on your theme, a function is called in your script file to let you react to the event.

Creating the Theme File

The theme files, those files with the fileextension *.theme, are text files that contain layout information and information about the values that will be shown.

Commands, Meters and Sensors

  • A list of General Commands that you can put in your theme file. This includes tags that define the widget shape, set default fonts, and create click-areas that launch programs.
  • A list of Meters that you can put in your theme file. This includes meters display the values of sensors. Meters can auto-update.
  • A list of Sensors that you can put in your theme file. Sensors allow you to display system properties automatically. There are sensors to display the status of everything from memory use to the results of shell scripts

Samples

The simplest form of Karamba is to load a file containing the word karamba. This will give you a empty transparent window with the size 300x300. A better idea is to enter something like this in a file:

karamba x=30 y=30 w=400 h=200 interval=2000

This will bring up a window at 30,30 with the size 400x200, and the window will refresh every 2000 milliseconds = 2 seconds.

The graphics elements of Karamba are called meters. The meters can be connected to sensors. A sensor can get, e.g., the current CPU load, the uptime, the date, and a lot of other things. It is also possible to display the output of an external program in a meter.

Creating a meter is not that hard. A text label can be created by putting this line in the file:

text x=10 y=10 value="This is a test"

To add some more spice we connect a sensor to a meter:

text x=15 y=25 sensor=uptime

A graph that logs the CPU load can be made like this:

graph x=10 y=40 w=200 h=60 sensor=cpu 

Adding an image (the path is either relative to the configuration file or an absolute path):

image x=10 y=270 path="picture.png"

An introduction to the format parameter for a text meter:

text x=10 y=330 sensor=memory format="You have %tm MB memory"

Using of a the "time" Plasma::DataEngine:

text x=0 y=0 sensor=plasma engine="time" source="Local" format="%Date %Time"

Please note that only one meter per line in the configuration file is allowed. No spaces are allowed around the '=' characters.

Notes

In Karamba all the graphic elements inside one configuration file have the same refresh rate. So it could be wise to split up a configuration into smaller parts, if different parts need different refresh times. An area showing a log file, for example, probably does not need the same refresh rate as an area showing the CPU load. The refresh rate of the sensors can be set individually. Also Karamba does not work with backgrounds without a picture, i.e., solid color, gradient, or pattern. One can get around this bug by creating a transparent PNG image (the size can be 1x1 pixel) and selecting this as the background picture.

Creating the Script File

The script file adds interactivity to the theme and can be written using the Python, Ruby or the KDE JavaScript language.

Super-Quick Start

  1. Make a theme file like described at the "Creating the Theme File" chapter above.
  2. Download the template.py or the template.rb or the template.js script file, depending on what scripting language you like to use, and rename this file to match your theme file.
  3. Fill in all the callbacks in the script file that you want to handle. The template files just contain all callbacks, but you can safly remove those you don't need or just leave them in.
  4. Use the API Reference to know what commands to use to fill in the callbacks.
  5. Finally save your script file and open your theme file in SuperKaramba to test it!

Regular Guide for "Mere Mortals"

Now lets add interactivity to your theme file. If you have not written a theme file yet, you need to do that first like described at the "Creating the Theme File" chapter above. It will define the size, location on screen, and other basic elements of your theme.

Event-based Programming

Now that you have a basic theme file, load it with SuperKaramba. If there is nothing defined in the theme file other than size and location, you will have made an empty, transparent square that might be hard to find on the desktop. Let's assume you have made a basic theme that just shows a few images in small area. To make your theme do anything interesting, it needs to be able to react to events.

Basically, an event is anything that can happen that SuperKaramba knows how to handle. Some examples are: the user moves or clicks the mouse, a new program is started on the desktop, a certain amount of time has passed, etc. So to make your theme do something, you add Python or Ruby scripting code to the event callback that corresponds to the event to which you want to react. This is not nearly as hard as it sounds.

First, you need the template.py or the template.rb or the template.js template script file, depending on what scripting language you like to use. Download this file to where your theme file is. Now rename those just downloaded template file from template.py to mytheme.py or from template.rb to mytheme.rb where mytheme is the name of your theme. For example, if you wrote a coolbar.theme and like to use use Python, renamed the just downloaded template.py to coolbar.py and put it in the same directory as coolbar.theme.

Now, lets open up your coolbar.py (or coolbar.rb or whatever you named it to) scripting file. Look for the event callback that reacts to mouse movement. In the case you choosed Python as your prefered scripting language, it may look like this:

  1. This gets called everytime our widget is clicked.
  2. Notes
  3. widget = reference to our widget
  4. x = x position (relative to our widget)
  5. y = y position (relative to our widget)
  6. botton = button being held:
  7. 0 = No Mouse Button
  8. 1 = Left Mouse Button
  9. 2 = Middle Mouse Button
  10. 3 = Right Mouse Button, but this will never happen
  11. because the right mouse button brings up the
  12. Karamba menu.

def widgetMouseMoved(widget, x, y, button):

   #Warning: Don't do anything too intensive here since you don't
   #want to run complex piece of code everytime the mouse moves.
   pass

Notice how the template gives your instructions on what data each callback gives you. In this case, you can see that you will know the x and y co-ordinates of the mouse, the button being held down (if any), and a reference to your widget.

Delete the line that says pass and add in some code. This code will run whenever the mouse is moved over your widget. But you say, "What in the world am I supposed to write to make something happen?". Good question! Go on to the next section below!

How to "make stuff happen" from your script file

SuperKaramba has a powerful API (Application Programmer's Interface). This is just a fancy way of saying SuperKaramba has a bunch of special commands you can run from your Python or your Ruby scripting code to make something happen in your theme. All of the commands are listed in one big list:

Here is how the callback might look after you add some code to it: def widgetMouseMoved(widget, x, y, button):

   myText = karamba.getThemeText(widget, "mouseText")
   karamba.changeText(widget, myText, "mouse at %s:%s" % (x,y))

This Python scripting code simply changes the text called "mouseText" (which we defiend in our theme file) to tell us the current position of the mouse.

How did I know what parameters karamba.changeText() and karamba.getThemeText() used? How did I even know those functions existed in the first place? Well, thats what the API tells you. The API and the template.py or template.rb script file basically contain everything you could ever need to know to write your own theme.

Testing!

To test your new Python or Ruby scripting code, just open your theme file in SuperKaramba.

You DO NOT open your .py or .rb file in SuperKaramba! Open the .theme file and if a matching .py or .rb file is found, it will be automatically loaded.

You DO NOT need to compile your scripting code. SuperKaramba will do that automatically and tell you about any errors. In fact, you can't run your .py or .rb file directly in regular python because it doesn't know anything about the karamba.* functions. Those functions only exist inside of SuperKaramba's Python and Ruby interpreters. But you are for sure able to just use SuperKaramba as your interpreter direct from within the commandline. Just start SuperKaramba with something like: superkaramba ./coolbar.theme

To ease the development, SuperKaramba will automatically reload your theme when you save your changes to the theme file or the script file. This allows you to see your modifications immediately without reloading the theme manually.

Have fun making themes!

FAQ

SuperKaramba on KDE4

While we where able to see a lot of rumours and speculations regarding SuperKaramba and it's role on KDE4, it's planned to continue to support SuperKaramba and it's unique feature-set, the bunches of themes that do already exist and those that will be written. We will stay backward-compatible and rocking stable, improve the feature-set as needed and wish you fun with the result: the next generation of SuperKaramba :)

SuperKaramba and Plasma

SuperKaramba runs as standalonea-application - just run the program "superkaramba" - as well as Plasma Applet embedded into the Plasma Workspace. Also SuperKaramba provides full and transparent access to the Plasma::DataEngine's with it's sensor functionality.

Scripting Interpreter Backends

SuperKaramba uses the Kross scripting framework to provide scripting with Python, Ruby and JavaScript as described at the Interpreter plugins section.

Defining Python Source Code Encodings

As described at the Python Enhancement Proposal Defining Python Source Code Encodings it is recommed to start a python script file with following both lines:

  1. !/usr/bin/env superkaramba
  2. coding: utf-8

The first line is the shebang that causes Unix-like operating systems to execute the script file using the SuperKaramba interpreter. You are then able to start the karamba script with; chmod 755 myKaramba.py ./myKaramba.py The second line defines that the script file is encoded with utf-8 or whatever the script file is actualy encoded in. This seems to be needed at least with Python >=2.5.

Script Samples

A clock with Ruby, Python and JavaScript

Let's take a look at one of them, the clock.rb theme written in Ruby. The theme just displays the current time in a RichText widget.

require 'karamba'

def initWidget(widget)

   Karamba.resizeWidget(widget, 300, 120)
   @richtext = Karamba.createRichText(widget, Time.now.to_s)
   Karamba.moveRichText(widget, @richtext, 10, 10)
   Karamba.setRichTextWidth(widget, @richtext, 280)
   Karamba.redrawWidget(widget)

end

def widgetUpdated(widget)

   Karamba.changeRichText(widget, @richtext, Time.now.to_s)
   Karamba.redrawWidget(widget)

end

The initWidget method will be called once if the widget got initialized. Here we setup the RichText widget where we display the current time in. The widgetUpdated will be called each second once (the interval is defined in the clock.theme themefile) and just updates the text display in the RichText widget with the new time.

Let's take a look at another theme. The text.py theme written in Python just displays some text widgets. We take this as example to create our own script, that does the same as the clock.rb above, that is to display the current time within a text widget.

import karamba, time

text = None

def getTime():

   return time.strftime("%Y-%M-%d %H:%M.%S")

def initWidget(widget):

   global text
   text = karamba.createText(widget, 10, 10, 200, 120, getTime())
   karamba.moveText(widget, richtext, 10, 10)
   karamba.redrawWidget(widget)

def widgetUpdated(widget):

   global text
   karamba.changeText(widget, text, getTime())
   karamba.redrawWidget(widget)

In the initWidget method we create our text widget that is updated once per second (or per interval as defined in the matching theme file) at the widgetUpdated method to display the new current time.

The clock.js sample provides us with the same sample written using the JavaScript language.

There we have the currentTime() function that returns the current time as string. That function is used within the initWidget() and the widgetUpdated() functions to display the current time within a textwidget. var text = 0;

function currentTime() {

   var now = new Date();
   var hours = now.getHours();
   hours = hours < 10 ? '0' + hours : hours;
   var mins = now.getMinutes();
   mins = mins < 10 ? '0' + mins : mins;
   var secs = now.getSeconds();
   secs = secs < 10 ? '0' + secs : secs;
   return hours + ':' + mins + '.' + secs;

}

function initWidget(widget) {

   text = karamba.createText(widget,0,20,200,20,currentTime());

}

function widgetUpdated(widget) {

   karamba.changeText(widget,text,currentTime());
   karamba.redrawWidget(widget)

}

See also...

Forms and Modules

Modules are libraries loaded on demand provided by Kross. One of them is the forms module that implements some basic dialog and widget functionality. To display just a simple messagebox or load widgets from a UI-file those module can be used within all supported scripting languages.

The following sample Python script demonstrates how to display a messagebox.

import karamba, Kross

def widgetClicked(widget, x, y, button):

   forms = Kross.module("forms")
   if button == 1: #left
       forms.showMessageBox("Information","Caption","Message")
   elif button == 2: #middle
       forms.showMessageBox("Error","Caption","Message")

While the next sample Python script displays a dialog with an embedded "Open File" widget.

import karamba, Kross

def widgetClicked(widget, x, y, button):

   forms = Kross.module("forms")
   dialog = forms.createDialog("MyDialog")
   dialog.setButtons("Ok|Cancel")
   openpage = dialog.addPage("Open","Open File","fileopen")
   openwidget = forms.createFileWidget(
       openpage, "kfiledialog:///openfile")
   openwidget.setMode("Opening")
   openwidget.setFilter("*.txt|Text Files\n*|All Files")
   result = dialog.exec_loop()
   if result:
       print openwidget.selectedFile()

For sure you are also able to use those handy UI-files the QtDesigner produces like demonstrated on the sample below.

import karamba, os, Kross

def widgetClicked(widget, x, y, button):

   forms = Kross.module("forms")
   dialog = forms.createDialog("Sample")
   infospage = dialog.addPage("Title","Caption","iconname")
   # the ui-file is in the same directory our script is in
   uifile = os.path.join(karamba.getThemePath(),"my.ui")
   infoswidget = forms.createWidgetFromUIFile(infospage, uifile)
   if dialog.exec_loop():
       # here we print the content of the in the ui-file defined
       # lineedit-widget that has the name MyLineEdit
       print infoswidget["MyLineEdit"].text

KSpread

The following sample uses KSpread to read a OpenDocument spreadsheet file and to display it within a table.

For this we are loading the KSpread Scripting library and control it. Compared to dbus we don't need a running KSpread instance but just load and use the library direct (so, you still need to have KSpread installed).

While the sample is written in Python, the other supported backends like Ruby are able to access the whole same rich API.

import karamba, Kross

  1. The OpenDocument spreadsheet file to read.

filename = "/home/kde4/kspreaddocument.ods"

  1. This is called when your widget is initialized

def initWidget(widget):

   # Fetch the KSpread module.
   kspread = Kross.module("kspread")
   if not kspread:
       raise "KSpread is not installed"
   # Try to open the file.
   if not kspread.openUrl(filename):
       raise "Failed to open %s" % filename
   # Get the sheet we like to display.
   sheet = kspread.sheetByName( kspread.sheetNames()[0] )

text = "

\n" # Iterate now through all cells on the sheet. for row in range(1, sheet.lastRow() + 1): # Put the content of the row into the record-list. record = [] for col in range(sheet.lastColumn() + 1, 1, -1): value = sheet.text(col, row) if value or len(record) > 0: record.insert(0,value) # If the record has at least one cell print it. if len(record) > 0: text += "" for r in record: text += "" % r text += "\n" text += "
%s

\n"

   # Create the richtext widget to display the text.
   richtext = karamba.createRichText(widget, text)
   karamba.moveRichText(widget, richtext, 10, 10)
   karamba.setRichTextWidth(widget, richtext, 345)
   karamba.redrawWidget(widget)

See also...

PyQt with Python

The following python script shows how to use PyQt which provides the nice Qt-API pythonized. The sample just displays a "hello world" dialog if you click on the widget.

import karamba from PyQt4 import QtCore, QtGui

def initWidget(widget):

   karamba.resizeWidget(widget, 200, 200)

def widgetClicked(widget, x, y, button):

   class Dialog(QtGui.QDialog):
       def __init__(self):
           QtGui.QWidget.__init__(self)
           label = QtGui.QLabel(self)
           label.setText("Hello World")
   dialog = Dialog()
   dialog.exec_()

See also...

TkInter with Python

You are also able to use the default toolkit Python comes with, that is TkInter. The following sample just displays a TkInter "hello world" dialog if you click on the widget.

import karamba from Tkinter import *

def widgetClicked(widget, x, y, button):

   root = Tk()
   w = Label(root, text="Hello, world!")
   w.pack()
   root.mainloop()

See also...

QtRuby/Korundum with Ruby

The following sample script written in Ruby demonstrates that you are able to use QtRuby/Korundum within your Ruby scripts.

require 'karamba' require 'Qt'

class Dialog < Qt::Dialog

   def initialize
       super()
       self.windowTitle = 'Hello World'
   end

end

def widgetClicked(widget, x, y, button)

   dialog = Dialog.new
   dialog.exec

end

The script does implement only the widgetClicked function that got called if the user clicks on the widget. What we do within that function is to create an instance of the Dialog class that implements a QDialog using QtRuby and then execute that modal dialog.

See also...

D-Bus

The following sample script written in Python just prints a list with services available via D-Bus;

import karamba, dbus

def initWidget(widget):

   bus = dbus.SystemBus()
   dbus_object = bus.get_object(
       'org.freedesktop.DBus','/org/freedesktop/DBus')
   dbus_iface = dbus.Interface(
       dbus_object, 'org.freedesktop.DBus')
   services = dbus_iface.ListNames()
   text = "".join( ["%s
" % s for s in services] ) karamba.resizeWidget(widget, 520, 520) richtext = karamba.createRichText(widget, text) karamba.moveRichText(widget, richtext, 10, 10) karamba.setRichTextWidth(widget, richtext, 500) karamba.redrawWidget(widget)

The following Python script uses D-Bus to display a list with all the devices accessible with HAL;

import karamba, dbus

def initWidget(widget):

   bus = dbus.SystemBus()
   dbus_object = bus.get_object(
       'org.freedesktop.Hal','/org/freedesktop/Hal/Manager')
   dbus_iface = dbus.Interface(
       dbus_object, 'org.freedesktop.Hal.Manager')
   devices = dbus_iface.GetAllDevices()
   text = "".join( ["%s
" % d for d in devices] ) karamba.resizeWidget(widget, 520, 520) richtext = karamba.createRichText(widget, text) karamba.moveRichText(widget, richtext, 10, 10) karamba.setRichTextWidth(widget, richtext, 500) karamba.redrawWidget(widget)

While those both samples are going the most easy way to just synchronously call dbus functionality, you are also able to asynchronously call methods or just write a dbus-server by using the with python dbus supported Qt main event-loop as shown in the dbus-python tutorial below - but don't wonder that the samples printed there may not run since it seems the dbus-python module got largely refactored while the with it shipped documentation did not :-/

See also...

Plasma

Following scripts are showing how to use the Plasma functionality in SuperKaramba.

See also...

Plasma::DataEngine as SuperKaramba Sensor

Following Python sample demonstrates usage of the Plasma::DataEngine functionality by using the TimeEngine to display the current local time.

import karamba def initWidget(widget):

   karamba.resizeWidget(widget, 400, 400)
   text = karamba.createText(widget, 10, 10, 380, 380)
   engine = karamba.getPlasmaSensor(widget, "time", "Local")
   connector = engine.connectSource("Local", text)
   connector.setFormat('%Date %Time')

You are also able to provide an own handle that defines what should be done if the Plasma::DataEngine has new or updated data for an widget or a meter.

  1. Import the SuperKaramba module.

import karamba

  1. This method got called by SuperKaramba on initialization.

def initWidget(widget):

   karamba.resizeWidget(widget, 400, 400)
   # Create the text-widget (aka meter).
   text = karamba.createText(widget, 10, 10, 380, 380)
   # Create the Plasma::DataEngine sensor.
   engine = karamba.getPlasmaSensor(widget, "time", "Local")
   # Connect the text-widget with the engine.
   connector = engine.connectSource("Local", text)
   # An own handler for the connector.
   def func(source,data):
       # Only display the current time but not the data["Date"].
       karamba.changeText(widget, text, data["Time"])
   # Set empty source else the sourceUpdated() signal will not
   # be emitted.
   connector.setSource("")
   # Connect the sourceUpdated() signal with the handler.
   connector.connect("sourceUpdated(QString,QVariantMap)",func)

Plasma in a SuperKaramba Theme

And here is the shortest version that needs only one single line of text. Just put following line into a SuperKaramba theme file; text x=0 y=0 sensor=plasma engine="time" source="Local" format="%Date %Time" poperties="reportSeconds:true,someOtherIntProperty:815"

PlasmaApplet

Now follows a short sample that offers a pure Plasma implementation to display the current local time.

  1. Import the PlasmaApplet module.

import PlasmaApplet

  1. Create a TimeEngine

engine = PlasmaApplet.dataEngine("time")

  1. Update each second.

engine.setProperty("reportSeconds", True)

  1. Create a Plasma LineEdit widget.

widget = PlasmaApplet.widget("LineEdit")

  1. Connect widget with the engine.

engine.connectSource("Local", widget)

Plasma and SuperKaramba

Now lets take a more detailed look at a longer sample that uses SuperKaramba and the PlasmaApplet module. While the sample above runs in all cases, the following sample does not cause the used PlasmaApplet module will be only available if SuperKaramba is running as Plasma Applet. If you are running SuperKaramba as standalone application this wan't work (is there interest for this? - but both ways provide the same rich API functionality to work with Plasma::DataEngine's anyway).

  1. Import the needed modules.

import karamba, PlasmaApplet

  1. Fetch the TimeEngine Plasma::DataEngine object.

engine = PlasmaApplet.dataEngine("time")

  1. We like to update it each second.

engine.setProperty("reportSeconds", True)

  1. Following lines are here to demonstrate that we are also able
  2. to dynamic handle the sources. It's not needed for this sample.
  3. def sourceAdded(source):
  4. print "sourceAdded '%s'" % source
  5. def sourceRemoved(source):
  6. print "sourceRemoved '%s'" % source
  7. def sourceUpdated(source,data):
  8. print "sourceUpdated '%s' '%s'" % (source,data)
  9. engine.connect("sourceAdded(QString)", sourceAdded)
  10. engine.connect("sourceRemoved(QString)", sourceRemoved)
  11. engine.connect("sourceUpdated(QString,QVariantMap)", sourceUpdated)
  1. Connect with the TimeEngine's source named "Local"

engine.connectSource("Local")

  1. This is the richedit widget we use to display something.

text = None

  1. This method just returns some text to display.

def getText():

   t = "name: %s\n" % PlasmaApplet.name()
   t += "category: %s\n" % PlasmaApplet.category()
   t += "boundingRect: %s\n" % PlasmaApplet.boundingRect()
   t += "sources: %s\n" % engine.sources()
   t += "data: %s\n" % engine.query("Local")
   return t
  1. This method got called by SuperKaramba on initialization.

def initWidget(widget):

   global text
   karamba.resizeWidget(widget, 400, 400)
   text = karamba.createText(widget, 10, 10, 380, 380, getText())
   karamba.redrawWidget(widget)
  1. This method got called by SuperKaramba on update.

def widgetUpdated(widget):

   global text
   karamba.changeText(widget, text, getText())
   karamba.redrawWidget(widget)

Following does the same as the sample above but connects the time Plasma::DataEngine with a meter-widget of SuperKaramba. The script will run once and compared to the sample above it's not needed to provide a widgetUpdated function but SuperKaramba will take care of updating the meter if the time Plasma::DataEngine provides new/updated data.

  1. Import the needed modules.

import karamba, PlasmaApplet

  1. Fetch the TimeEngine Plasma::DataEngine object.

engine = PlasmaApplet.dataEngine("time")

  1. We like to update it each second.

engine.setProperty("reportSeconds", True)

  1. This method got called by SuperKaramba on initialization.

def initWidget(widget):

   global engine
   karamba.resizeWidget(widget, 400, 400)
   # Create the text-meter widget.
   text = karamba.createText(widget, 10, 10, 380, 380, "")
   # Connect the dataengine with the text-meter.
   connector = engine.connectSource("Local", text)
   # Set the formatting. This is how the data should be
   # displayed. The local time-engine source provides the
   # '%Date' and the '%Time' values.
   connector.setFormat('Date: %Date\nTime: %Time')
   
   # Following lines are here to demonstrate that you are also
   # able to implement an own handler for the connector.
   #def func(source,data):
   #    karamba.changeText(widget, text, data["Time"])
   # Set empty source else the sourceUpdated() signal will not
   # be emitted.
   #connector.setSource("")
   # Connect the sourceUpdated() signal with the handler.
   #connector.connect("sourceUpdated(QString,QVariantMap)",func)
   
   karamba.redrawWidget(widget)