Ajax Chat App using Yesod

July 22, 2010

I’ve finally recovered from fourth year enough that I feel like coding a serious personal project again. That project is a turn-based tabletop game I’m adapting to code, I’ll go into more detail about it later. For the interface, I want to do an Ajax-powered web app, and I’m using Michael Snoyman’s excellent Haskell web framework, Yesod, to make that happen.

There’s Ajax support built into Yesod, but nothing for push updates sent to clients. In order to prove that Yesod (and I) were up to the job, I wrote a basic many-user chat room app. It uses the fairly standard hack for Ajax push: clients send a dummy request and the server just leaves the connection open, and uses the response as a form of push.

The code, both the Haskell and the Javascript (uses jQuery), follows. I’m not going to bother explaining how Yesod works, Michael Snoyman has already done an excellent job of that at docs.yesodweb.com. This is basically a combination of the Ajax and “Chat” (really a message board) tutorials from there. The only tricky part is that the site argument, a read-only parameter passed to request handlers by Yesod, contains a couple of TVars that hold one duplicate of a single TChan for each client (dupTChan is awesome for this kind of independent-read/broadcast-write application).

Clients send an Ajax request to post a message, of course, but they also make a check-in request. That handler (getCheckR) finds that client’s TChan and blocks until data is available on it, which it then sends to the client. The clients displays it and makes another check-in request.

That leads me to my question to my readers: The Javascript function checkIn makes an Ajax request whose callback calls checkIn again. Is that a safe thing to do? Does it leak stack frames? It depends, I suppose, on the internals of jQuery’s implementation, and possibly on the Javascript engine. If anyone could enlighten me, I would be very grateful.

Edit: You may have noticed that WordPress mangled the code below. It’s also out of date with the modern versions of Yesod. A cleaned up and modernized version of my code can be found in the yesod-examples package.

{-# LANGUAGE TemplateHaskell, QuasiQuotes, TypeFamilies #-}


module Main where

import Yesod
import Yesod.Helpers.Static

import Control.Concurrent.STM
import Control.Concurrent.STM.TChan
import Control.Concurrent.STM.TVar

import Control.Arrow ((***))

-- speaker and content
data Message = Message String String

-- all those TChans are dupes, so writing to any one writes to them all, but reading is separate
data Chat = Chat
  { chatClients    :: TVar [(Int, TChan Message)]
  , nextClient     :: TVar Int
  , chatStatic     :: Static
  }

staticFiles "static"

mkYesod "Chat" [$parseRoutes|
/          HomeR   GET
/check     CheckR  GET
/post      PostR   GET
/static    StaticR Static chatStatic
|]

instance Yesod Chat where
  approot _ = ""
  defaultLayout content = hamletToContent [$hamlet|
    !!!
    %html
        %head
            %title $pageTitle.content$
            %script!src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"
            %script!src=@StaticR.chat_js@
            ^pageHead.content^
        %body
            ^pageBody.content^
    |]


getHomeR :: Handler Chat RepHtml
getHomeR = do
  Chat clients next _ <- getYesod
  client <- liftIO . atomically $ do
    c <- readTVar next
    writeTVar next (c+1)
    cs <- readTVar clients
    chan  newTChan
              (_,x):_ -> dupTChan x
    writeTVar clients ((c,chan) : cs)
    return c
  applyLayout "Chat Page" mempty [$hamlet|
    !!!
    %h1 Chat Example
    %form
        %textarea!cols=80!rows=20!name=chat
        %p
            %input!type=text!size=15!name=name#name
            %input!type=text!size=60!name=send#send
            %input!type=submit!value=Send
     var clientNumber = $show client$ 
    |]

    

getCheckR :: Handler Chat RepJson
getCheckR = do
  liftIO $ putStrLn "Check"
  Chat clients _ _ <- getYesod
  client <- do
    c  invalidArgs ["No client value in Check request"]
      Just c' -> return $ read c'
  cs <- liftIO . atomically $ readTVar clients
  chan  invalidArgs ["Bad client value"]
            Just ch -> return ch
  -- block until there's something there
  first <- liftIO . atomically $ readTChan chan
  let Message s c = first
  jsonToRepJson $ zipJson ["sender", "content"] [s,c]


zipJson x y = jsonMap $ map (id *** (jsonScalar.string)) $ zip x y


getPostR :: Handler Chat RepJson
getPostR = do
  liftIO $ putStrLn "Post"
  Chat clients _ _ <- getYesod
  (sender,content) <- do
    s <- lookupGetParam "name"
    c  return (s', c')
      _                  -> invalidArgs ["Either name or send not provided."]
  liftIO . atomically $ do
    cs <- readTVar clients
    let chan = snd . head $ cs -- doesn't matter which one we use, they're all duplicates
    writeTChan chan (Message sender content)
  
  jsonToRepJson $ jsonScalar (string "success")
  

main :: IO ()
main = do
  clients <- newTVarIO []
  next <- newTVarIO 0
  let static = fileLookupDir "static" typeByExt
  basicHandler 3000 $ Chat clients next static

And the JS, which must be called static/chat.js to be loaded properly.


$(document).ready(function () {
    $("form").submit(function (e) {
        e.preventDefault();
        $.getJSON("/post", { name: $("#name").attr("value"), send: $("#send").attr("value") }, function(o) { });
        $("#send").attr("value", "");
    });

    checkIn();

});


function checkIn () {
    $.getJSON("/check", { client: clientNumber }, function(o) {
        //alert("response: " + o);
        var ta = $("textarea");
        ta.html(ta.html() + o.sender + ": " + o.content + "\n");
        ta.scrollTop(10000);

        checkIn();
    });
}


Pimp Your XMonad #4: Urgency Hooks

March 14, 2009

In a traditional desktop environment such as Gnome or KDE, one has a Windows-style taskbar and tray, with small icons for various apps. When an app wants your attention, it generally changes its tray icon, and might highlight or flash the taskbar entry. In a pure xmonad setup, one typically has neither a taskbar nor a tray, so how do you know when someone addresses you on IRC, or sends a Pidgin message?

Xmonad offers a solution, the XMonad.Hooks.UrgencyHook module. This module offers a variety of ways for xmonad to notify you that an app is requesting your attention, and some ways for you to respond.

XMonad Setup
When an app sets the X WM_URGENT option (see below on how to configure this in some common apps), UrgencyHook will notify you. Precisely how it does so is up to you. One uncommon choice is popping up a temporary dzen2 window, which I won’t be covering here (see the XMonad.Hooks.UrgencyHook docs). By far the most common technique is highlighting the workspace in your dzen or xmobar workspace list.

To start, import XMonad.Hooks.UrgencyHook. Then your xmonad defaultConfig { ... } line changes to: xmonad $ withUrgencyHook NoUrgencyHook defaultConfig { ... }. If you run the darcs version of xmonad and use the dzen function or dzenPP for your logHook you’re all set! Both of those functions set up dzen to show urgency by highlighting the workspace with the urgent window.

If you use the xmobar or xmobarPP functions, or want custom colours for your dzen, a little more work is required. You probably have a logHook defined, and it probably uses xmobarPP or dzenPP. If not, see the documentation for XMonad.Hooks.DynamicLog, that’s outside of the scope of this article.

You’ll have to add the field ppUrgent = xmobarColor "yellow" "red" . xmobarStrip to your xmobarPP for xmobar, or ppUrgent = dzenColor "yellow" "red" . dzenStrip for dzen. Of course, you can choose whatever colours you like.

N.B. The dzenStrip and xmobarStrip functions only exist in darcs. If you don’t run darcs xmonad, you can get the code from here and paste it into your xmonad.hs.

A Handy Keybinding: focusUrgent
XMonad.Hooks.UrgencyHook contains a useful keybinding function, focusUrgent, which takes no arguments and focuses the most recently urgent window.

Configuring Pidgin
The second part of setting up UrgencyHook is configuring apps to set WM_URGENT. To do this for Pidgin, you need to enable the Message Notificaton plugin (included with Pidgin), and in the plugin configuration tick the “Set window manager ‘URGENT’ hint” option.

Configuring urxvt
Another common thing to want highlighting for is when someone uses your name in IRC. To make urxvt set urgent on a bell, add the line URxvt.urgentOnBell: true to your ~/.Xdefaults.

Of course, you’ll also have to set your IRC client to trigger a bell on highlight (in irssi: /set bell_beeps ON and /set beep_msg_level MSGS NOTICES DCC DCCMSGS HILIGHT). If you run it in screen, make sure screen isn’t set to use visual bell either.

The XMonad.Hooks.UrgencyHook docs also have information on setting up these apps and others. If you use a different terminal or IRC client, Google knows how to set it up for urgency hints.

UrgencyHook in Action
Here’s a screenshot of me hard at work on xmonad, with UrgencyHook notifying me that I’ve received a Pidgin message on workspace 2: IM.

P.S. Twitter
I’m now on Twitter as bradenshep.


Pimp Your XMonad #3: Prompt

November 29, 2008

This is a big post that I’ve been putting off for a long time. There are a lot of awesome modules in the XMonad.Prompt.* family, and so there’s a lot to cover here.

XMonad.Prompt itself defines a library for displaying prompts to the user, with autocompletion and customizable look and feel. This module is a back-end library, not intended for use in configurations. What follows is a description and use cases for what I consider the most important modules in the Prompt family, a breakdown of how to customize the look and feel, and a quick rundown of the remaining Prompt modules as of 0.8.

Binding a Prompt action to a key
Nearly all Prompt actions are performed using key bindings. You can bind them to whichever keys you wish, and they all take the same basic form. First, you need to import the relevant module, for example

import XMonad.Prompt.Shell

Second, you need to create a keybinding to call a function from it

( modMask conf .|. xK_s, shellPrompt myXPConfig )

or using XMonad.Util.EZConfig (which will be the subject of a future PYXM)

("M-s", shellPrompt myXPConfig)

The myXPConfig argument is how you customize the look and feel of the prompts. See the section called “Look and Feel Customization” below on how to do this.

Running Commands and Launching Applications: Shell, RunOrRaise, DirExec
One obvious application of Prompt is using it to run shell commands and launch applications. XMonad.Prompt.Shell.shellPrompt works similarly to dmenu, though it allows command-line arguments to be provided as well, and more complete look-and-feel customization.

XMonad.Prompt.RunOrRaise.runOrRaisePrompt uses the XMonad.Actions.RunOrRaise functionality to prompt for an application, launch it if it isn’t running, and summon it if it is.

XMonad.Prompt.DirExec.dirExecPrompt can be used when you want to run programs from a directory not necessarily in your path.

Navigating by Window Titles: Window
XMonad.Prompt.Window‘s windowPromptGoto and windowPromptBring functions present an autocompleting list of window titles for all the windows in your session, and either take you to the window you name, or bring it to your location. This can be very helpful both for tracking down windows you’ve misplaced, and for bringing remote windows here without going to find them.

Minimally Disruptive Note-Taking: AppendFile
XMonad.Prompt.AppendFile is my personal favourite find from my research for this article. That might not be fair to Shell and Window, I knew they were there so they weren’t “finds”. What AppendFile does is prompt you for one line of text, which it then appends to a file named in your xmonad.hs. The intended purpose (there are probably others) is to be able to jot down quick notes by hitting a key binding and typing one line, without disrupting whatever other task you were doing.

Arbitrary XMonad Actions: XMonad
XMonad.Prompt.XMonad can be viewed as an extension of your key bindings. It displays an autocompleting list of actions, each action bound to an arbitrary X (), like a key binding. You can use the provided default bindings using xmonadPrompt (which seem to correspond to roughly the same actions as the default key bindings) or create your own list of (name,action) pairs with xmonadPromptC.

Look and Feel Customization: XPConfig
The XPConfig data type is defined in XMonad.Prompt, and it defines the various colours and other parameters that determine the appearance, position, font, size and behaviour of the Prompt box. Following is a rundown of the parameters and what they control.

  • font — a String naming a font in the X format, eg. "-*-terminus-*-*-*-*-16-*-*-*-*-*-*-*" (default: "-misc-fixed-*-*-*-*-10-*-*-*-*-*-*-*")
  • fgColor,bgColorStrings in "#rrggbb" format, defining the fore- and background colours. (defaults: "#333333" and "#FFFFFF")
  • fgHLight,bgHLightStrings as above, defining the highlight colour for a completion entry. (defaults: "#000000" and "#BBBBBB")
  • borderColor — a String as above, defining the border colour. (default: "#FFFFFF")
  • promptBorderWidth — the border width in pixels. (default: 1)
  • position — either Top or Bottom, the constructors of the XPPosition data type. (default: Bottom)
  • height — the height of the Prompt bar in pixels (make sure it’s large enough for your font!) (default: 18)
  • historySize — the number of history entries to save. (default: 256)
  • defaultText — a String defining the text initially in the Prompt box when it opens. (default: "")
  • autoComplete — a Maybe Int that controls the autocompletion behaviour. If Nothing, you must press Enter to select the entry. If Just x, it will select a unique entry after x microseconds. For example, Just 1000000 would wait 1 full second. (default: Nothing)

Remaining modules
These modules are well worth mentioning but are straightforward enough to just list quickly:

  • Workspace — takes a String -> X () function and gives it the name of the workspace you specify in the prompt. Could be used to focus the named workspace, bring all the windows on it to the current workspace, or any other action.
  • Ssh — given a host name in the prompt, creates an SSH session in a terminal.
  • Man — displays the man page for the subject you provide in the prompt.
  • AppLauncher — functionality similar to dmenu.

Conclusion
The XMonad.Prompt.* family is a prime example of the awesome functionality just waiting to be discovered. Depending on how they use XMonad, I have seen Ssh, Window and RunOrRaise all make an xmonad user’s day.


Pimp Your XMonad #2: SmartBorders

November 15, 2008

This is a quick one. The focused window border is sometimes unnecessary, particularly when using Full or Tabbed layouts, since only one window is visible. XMonad.Layout.NoBorders was built to fix this.

The simplest option is the noBorders modifier, which will simply never show the border on the layouts it is applied to. It is typically applied thus:

layoutHook = ... ||| noBorders Full ||| ... ||| noBorders Tabbed ||| ...

Using it on a layout that shows multiple windows, such as Tall, would be a bad idea. But what about Tall with only one window? There is also the smartBorders modifier that will hide the borders under either of two conditions:

  • There is only one screen and only one window. (Note that on a Xinerama setup, this will never be true.)
  • A floating window covers the entire screen (great for mplayer, probably the largest use case)

Since smartBorders handles all layouts, even those that can have multiple windows, it is best applied to your whole layoutHook:

layoutHook = avoidStruts $ ... $ smartBorders $ ... ||| tiled ||| ...

Adding to core
It has been argued that SmartBorders should be in core, and even be the default behaviour. The main argument against this is that it could be confusing, and that it is rather straightforward to set up. For now, setting up smartBorders is easy, and improves the fullscreen video experience.


Pimp Your XMonad #1: Status bars

November 2, 2008

I’ve been hanging around in #xmonad for a long time, and two things have become clear. First, we answer a certain subset of questions many times. Most of the answers are in the FAQ, but the reality is that #xmonad tends to be more efficient, and it gives the answerers a means of procrastination more useful to society than reading whatever has hit reddit this minute.

Second, some relatively new user whose config is as he wants it will be hanging around in the channel when another new user asks if some feature exists. The first user says “I never even thought of something like this!” So I conceived the idea which eventually turned into this series of posts, a place for new users to discover features they never knew they were lacking.

Status bars

This arguably doesn’t belong, since it’s unlikely any user is going to reel in their jaw and say “I can get it to show me a list of workspaces?!” Don’t get me wrong, it’s an awesome feature, and I would get lost all the time without it, especially on a dual-head setup. But it might not belong since it’s a relatively easy-to-conceive feature.

Why should you use a status bar? Well, it will list all your currently populated workspaces (meaning those with at least one window on it, as well as those currently visible whether or not they have windows on), which can help with the “Oh, I forgot I sent this to workspace 8 six hours ago” problem. Using one of the pretty-printers (available for xmobar and dzen, with customizable behaviour) It highlights the currently focused workspace, as well as the other visible workspaces under a Xinerama setup. Also shown are the current layout, handy to avoid the “I forgot about these other windows because I was in Full” problem; and the window title, handy for the subtle jokes of some web comics, and… well I actually turned that one off.

Which status bar app?

xmonad users generally pick one of two: dzen2 and xmobar. The former is a general-purpose Do-It-Yourself status bar program, with sophisticated color and formatting directives that can be inlined in the input. It also supports XFT fonts and XPM icons. However, it is only an engine for displaying the input; you must write a program to generate the necessary input yourself. If that program is xmonad, the logHook in xmonad.hs, and XMonad.Log.DynamicLog‘s dynamicLogWithPP and dzenPP functions make that easy. If you want time, CPU usage, memory usage, etc., you’ll need to write your own script, with help from the dzen wiki.

xmobar, as the name hints, was designed for use with xmonad. Since xmonad doesn’t have any parts that run periodically, it can’t output a reliable time, or fresh CPU/memory/disk/network data to any status bar app. xmobar handles this itself, allowing the user to specify in a config file what information they want, and in what format it should appear.

Setting it up

With future articles, I intend to describe in more detail how to set up and get full use out of the features I’m discussing. In this case, though, the question is so frequently asked and answered in #xmonad that I’m going to punt. Either see John Goerzen’s excellent guide to setting up xmonad for its xmobar sections, or see that and replace xmobarPP with dzenPP, and remove the sTitle line from the example xmonad.hs seen there.


xmonad-light 0.8 Released

September 17, 2008

Following the recent release of xmonad 0.8, xmonad-light has a 0.8 version out. This version of xmonad-light is built against the xmonad 0.8 release version. The binary (i686) distribution can be downloaded here.

New Features

As I said in my previous post, I have implemented ManageDocks. Also implemented is defaultKeys, which can be set to False to ignore the default key bindings and define them all yourself.

The --to-haskell command-line option will output Haskell code for an xmonad.hs equivalent to your xmonad.conf.

Binary Distributions

The binary distributions do not require GHC or xmonad to already be installed.

They include a README describing usage and xmonad.conf syntax, an INSTALL file, and a TODO with future improvements. I duplicate these below.

Distro Packages

The whole point of xmonad-light is to lower the bar to trying out xmonad by removing the GHC dependency and making it much more lightweight. Anyone looking to package xmonad-light for their distro is invited and encouraged to do so. Comment on this post or email me if you need my support with any part of it.

Future Features

In no particular order:

  • An xmonad-like restart system that keeps running with the current configuration on a restart that fails to compile the xmonad.hs or parse the xmonad.conf.
  • xmessage error popups and use of the ~/.xmonad/xmonad.errors file.
  • logHook support, for output to dzen and xmobar.
  • more layouts and other contrib modules

I will be working on all of these, but patches are always welcome.

Bugs

There are almost certainly bugs in xmonad-light. I have been testing it, of course, but I haven’t really used it. Real use by actual users will, I’m sure, expose some bugs. Emails to the xmonad mailing list with bugs and feature requests will come to my attention, and I will endeavour to fix them or implement them.

Comments

Any comments, suggestions, gripes, “it would be nice if”, or other such are definitely welcome.

Contributing

Throughout this post I say “me”. I’m the sole developer of the xmonad-light project only by circumstance and not by desire. I would welcome any patches, and if you want to hack away on xmonad-light you’re most welcome. If you think I’m doing it all wrong, come to #xmonad or the xmonad mailing list and say so!


Announcing xmonad-light

August 28, 2008

After further work on PlainConfig, and in the light of the impending 0.8 release, it was decided that PlainConfig really doesn’t belong in xmonad-contrib. No one is going to use PlainConfig from their xmonad.hs, that completely misses the point. Cabal doesn’t provide an easy parallel to having make for ordinary contrib and make plainconfig for a PlainConfig-ready binary.

Considering all this, it would be far simpler to have the PlainConfig functionality as a separate Cabalized project with the binary target, that can be configured using ~/.xmonad/xmonad.conf and doesn’t depend on GHC for configuration.

With that, xmonad-light was born. It bears only some resemblance to the XMonad.Config.PlainConfig module currently in xmonad-contrib. I had modified it heavily in my local repository before splitting it into xmonad-light, and never pushed it due to the 0.8 release freeze.

Misconceptions

To be perfectly clear, let me explain a few things xmonad-light is, and is not.

Most importantly, xmonad-light is not a fork of xmonad! It depends on xmonad and xmonad-contrib for building. Rather it is an alternative configuration framework for xmonad, and will be distributed as a binary which has been statically linked to xmonad and xmonad-contrib.

xmonad-light is not a sign that xmonad has crippled its configuration, or has moved away from Haskell configurations. The goal of xmonad-light is not to replace Haskell configurations, but to provide an alternative with a much more gradual learning curve. The xmonad.conf to xmonad.hs compiler (see below) makes the transition to a more powerful config easy. This is almost inevitable given the often-seen ramp-up in users’ desire for customization.

xmonad-light is a standalone binary, that does not depend on GHC, xmonad or xmonad-contrib to run on a user’s machine. It has been statically linked against a version of xmonad and xmonad-contrib, namely whatever version was registered with ghc-pkg when xmonad-light was built.

When xmonad-light is executed, it will look for ~/.xmonad/xmonad.hs. Yes, the Haskell file. If it exists, xmonad-light will try to execute it as xmonad normally does. If this fails (GHC error, GHC doesn’t exist, xmonad not installed/registered, etc.) or the xmonad.hs doesn’t exist, it then looks for ~/.xmonad/xmonad.conf and uses that. If neither file exists, it will run the default xmonad config.

When moving to Haskell configuration files, users will need to install xmonad, xmonad-contrib, and GHC normally, as they do now to install and configure xmonad. It will be possible (due to the sequence described above) to mod+q from xmonad-light to normal xmonad.

Current Features

xmonad-light currently supports everything PlainConfig did, which is to say basic configuration of most of the parts of an xmonad.hs. It’s easier to list the omissions and caveats, which are:

  • Layouts are selected from a predefined list, which currently includes only the standard three.
  • Any key sequence, including submaps, can be bound using EZConfig syntax, but the actions are selected from a list. This list includes all the actions required to reproduce the defaults, and can spawn anything.
  • ManageHook conditions are fully expressive, but the actions are selected only from doIgnore, doFloat and doShift "workspace".
  • No support for logHooks.
  • No support for ManageDocks.

The former three are easily expanded to include more layouts and actions as necessary. The latter two are both on my to-do list before releasing a first version of xmonad-light.

The other notable feature is that the compiler from xmonad.conf to xmonad.hs is complete, and supports all the same features as the main part of xmonad-light. Testing (calling it thorough would be to describe having an umbrella as being prepared for a hurricane) has shown that using the generated xmonad.hs works (ie. ghc accepts it) and that the resulting configuration appears to match the xmonad.conf. This functionality will be accessed with a command-line option to the xmonad-light binary.

Usability and Stability

xmonad-light is usabile in the sense that it works, and one could actually use it to configure xmonad without GHC. It is not usable in the sense that there’s no easy way to get and install it yet. There are no known bugs in the code.

xmonad-light is stable in the sense that the API (the xmonad.conf format) is likely to change only by addition of new features. It is unstable in the sense that code and API changes are coming (see below for features I plan to add imminently and in the longer term).

To-Do

In roughly descending order of priority:

  • Support for ManageDocks.
  • Support for logHooks (probably just basic dzenPP, xmobarPP).
  • Actually add the --to-haskell command-line option.
  • Release a version guaranteed to work against the 0.8 release of xmonad.
  • Add more layouts, key actions and ManageHook actions as they are desired by users.

Contributing

This has so far been my baby, but I would welcome input (in the form of comments or code) on the design or implementation. The code is available as a darcs repository:

darcs get http://code.haskell.org/~shepheb/xmonad-light

The one part of the code that bothers me is the seemingly large amount of duplication, with often nearly identical functions for the XConfig output and the Haskell code output. However, I can’t see a way to do it more generally.

Conclusion

I’m off to add ManageDocks support.


Follow

Get every new post delivered to your Inbox.