I build web, mobile and desktop apps, produce screencasts, write ebooks, and provide pairing and training

How to build a RubyMotion status bar app that updates in the background

Nov 07, 2013 - Elliott Draper

In our last post we looked at how easy it is to create an OS X menu bar app using RubyMotion. This time around, we’ll look at one of the ways we can make status bar apps more useful - having it fetch and display data from an external source in the background. In our specific example, we’ll be wrapping the top commandline tool, to pull out CPU usage and display it. It should demonstrate how you could replace that and roll in calls to other system commands for different data, or even API calls to web services to pull in live data from there too.

The code is open source and available here. It’s based on using osx-status-bar-app-template that we detailed in the last post, so if you want to build this yourself as we go along in the article, start by installing the osx-status-bar-app-template gem and creating a new app using that template.

Extensions

First of all, we have one other code file besides the app delegate in the codebase. In app/extensions/ns_menu_item.rb, we have the following:

  class NSMenuItem
    def checked
      self.state == NSOnState
    end

    def checked=(value)
      self.state = (value ? NSOnState : NSOffState)
    end
  end

If you’ve worked with Ruby before, you’ll know that you can re-open classes to add methods to them, and if you’ve worked with Objective-C much you’ll know that you can do something similar with categories. All we’re essentially doing here is adding some convenience short-hand methods to menu items to allow us to check and uncheck them, and to see if they’re checked. You’ll see how this is used shortly.

Setting up our menu

Let’s setup the menu items we need in app/app_delegate.rb. The idea is we’ll have three options, Total to show total CPU usage, User to show the user CPU usage, and System to show the sys CPU usage value. In our applicationDidFinishLaunching method, alongside the default menu items created for about and quit, add our three new menu items in:

  @total_item = createMenuItem("Total", 'clickTotal')
  @total_item.checked = true
  @status_menu.addItem(@total_item)

  @user_item = createMenuItem("User", 'clickUser')
  @user_item.checked = false
  @status_menu.addItem(@user_item)

  @system_item = createMenuItem("System", 'clickSystem')
  @system_item.checked = false
  @status_menu.addItem(@system_item)

We’ll also add the following to applicationDidFinishLaunching:

  @user = 0
  @sys = 0
  self.updateStatus

We’ll be implementing updateStatus shortly, but it’s what sets the title text in the menu bar based on the current values, which we’ve defaulted to 0 above.

Next up, we need to implement the event handlers for each menu item click. We’re going to get a bit funky with the selection, so that if you check Total, it only shows that, and unchecks the other two. You can select either or both of the other two to show, and if you deselect both, it goes back to having Total selected. Our three event handlers look like this:

  def clickTotal
    @user_item.checked = false
    @system_item.checked = false
    @total_item.checked = true
    self.updateStatus
  end

  def clickUser
    @total_item.checked = false
    @user_item.checked = !@user_item.checked
    self.mustSelectSomething
    self.updateStatus
  end

  def clickSystem
    @total_item.checked = false
    @system_item.checked = !@system_item.checked
    self.mustSelectSomething
    self.updateStatus
  end

  def mustSelectSomething
    @total_item.checked = true if !@user_item.checked && !@system_item.checked
  end

If you select the total one, it automatically unchecks the other two, and you can’t click to unselect it. If you select either user or system, it will uncheck the total one, and either uncheck or check itself, depending upon its current state. They both also use the mustSelectSomething method to double check and ensure that if after the click the result is that neither is selected, then the total becomes selected again. All three event handlers finish up by triggering a call to update the status text. Let’s look at how that is implemented next:

  def updateStatus
    if @total_item.checked
      @status_item.setTitle("CPU: #{sprintf("%.2f", @user + @sys)}%")
    else
      text = []
      text << "User: #{sprintf("%.2f", @user)}%" if @user_item.checked
      text << "Sys: #{sprintf("%.2f", @sys)}%" if @system_item.checked
      @status_item.setTitle(text.join(", "))
    end
  end

Here is where we’re using the status of each menu item, checked or unchecked, to decide what to show. If total is checked, we’re showing the sum of both the user and sys values as a total CPU usage figure. If not, then we’re showing user, sys, or both, based on which ones are checked. Hopefully by now it’s obvious why we added those convenience methods to the menu items, to make seeing if an item is checked a bit less verbose!

If you run the app, you’ll see it works, showing 0.0% for all figures, but allowing you to toggle between the various different displays. So what we’re now missing is the external source to give us data, and a way to have it run in the background to update the status bar all on its own.

Driven by data

We’re going to use the top command as the source for our values, and we’re going to use IO.popen to open up a pipe to the command sampling values continuously. top produces a lot of data, so we’ll be filtering it out and looking for the specific line that contains CPU values, then parsing those out. It’s not too tricky, and the whole thing looks like this:

  def startTop
    IO.popen("top -l 0") do |f|
      while true
        unless((line = f.gets).nil?)
          if line[0...10] == 'CPU usage:'
            line.gsub!("CPU usage: ", "")
            line.split(", ")
            @user, @sys = line.split(", ").map { |p| p.split("%").first.to_f }
            self.updateStatus
          end
        end
      end
    end
  end

Running top -l 0 is what gives us the output in a format we can work with from a script - by default top is pretty clever, and replaces the output every sample so that when you run it in a terminal, it seems like it updates on screen (rather than scrolling in new data for each sample). By passing -l we’re asking it to run for a specific amount of samples, giving us the output in a raw form, and by specifying 0 rather than a set amount, we’re telling it we want it to run infinitely. With a pipe open to that command running endlessly, we can then read each line, check to see if it’s the line we’re looking for, and then parse out the data. The lines we want look like this:

  CPU usage: 3.79% user, 3.12% sys, 93.8% idle

Once we strip out the CPU usage: heading, we can then split on the comma to separate our values, and split on the % to separate the value from the name. We can then parse it as a float, and we just pull out the first two as our user and sys values (we don’t use the idle value). This is an example of how Ruby’s superior string handle really comes in handy, you can see what our breakdown and parsing of that line looks in a Ruby console:

  > line = "CPU usage: 3.79% user, 3.12% sys, 93.8% idle"
   => "CPU usage: 3.79% user, 3.12% sys, 93.8% idle"
  > line.gsub!("CPU usage: ", "")
   => "3.79% user, 3.12% sys, 93.8% idle"
  > line.split(", ")
   => ["3.79% user", "3.12% sys", "93.8% idle"]
  > line.split(", ").map { |p| p.split("%") }
   => [["3.79", " user"], ["3.12", " sys"], ["93.8", " idle"]]
  > line.split(", ").map { |p| p.split("%").first }
   => ["3.79", "3.12", "93.8"]
  > line.split(", ").map { |p| p.split("%").first.to_f }
   => [3.79, 3.12, 93.8]

We fairly effortless and reliably go from a string output from top, to pulling out the two values we need. From there, it calls updateStatus to reflect the updated values, and we’re golden. Except, how does this method get called? If you try to call startTop directly from within applicationDidFinishLaunching, then it will fire up and continuously run to pull out values - but you can’t interact with the menu bar app at all, and it won’t update beyond the initial value being shown. This is because our startTop method runs endlessly, and thus blocks the main thread when called in that manner. Any UI interactions or any further interaction with the app doesn’t work. We need a way to explicitly call that method in the background of our app, so it happens on a separate thread. Luckily, this is fairly straightforward - add this to the end of applicationDidFinishLaunching:

  self.performSelectorInBackground('startTop', withObject: nil)

This call returns immediately, firing up that method in a background thread, and meaning that interaction with the app continues as normal, and our startTop method keeps running in the background, parsing out the latest values and updating the status bar title text. Voila, we have an app that is talking to a data source in the background, and updating the status bar all on its own. Replacing the startTop method with whatever data source you want to regularly query to return data to show in the status bar would be fairly trivial.

If you fire up the app now, you can see it update in real-time:

CPUTrackerMenu

What next?

What data sources can you think of that’d be useful to ping and show data from in your status bar? Chances are there are a bunch of things you’d like to keep an eye on, and now with just a small amount of code, and RubyMotion, you can! With CPUTrackerMenu too, you can even make sure that your own app isn’t taking up too much CPU by running in the background!

In a future post we’ll look at integrating with web service APIs from within your RubyMotion app - in the meantime, any questions related to this article, a previous article, or RubyMotion dev in general, let us know in the comments below, or catch us on Twitter @KickCode!




Available now in early access: Building Mac OS X apps with RubyMotion!


Unlock the power of Ruby in your Mac OS X apps to build everything from utility and productivity apps, to developer tools and helpers, to fully fledged desktop user interfaces. You'll integrate with web APIs, with core system functions, learn powerful ways to build user interfaces, and more. You'll learn how to best structure your apps and to take advantage of the Ruby syntax to make your development more efficient than building the app in Objective-C.


Learn more or purchase now.

blog comments powered by Disqus
Back to blog
 

Building Mac OS X apps with RubyMotion

Learn how to build Mac apps with using Ruby with this ebook, currently in early access, and with the finished version coming soon.


Purchase