This rather long example is a fully functional mp3 player based on the mpg123 audio player. Most of the program is concerned with displaying data and handling user input. And what would an mp3 player be without [a screen shot]?

#!/usr/bin/perl
use warnings;
use strict;
use Curses;
use POE;
use POE::Wheel::Curses;
use POE::Component::MPG123;
my $dir = shift;
$dir = "." unless defined($dir);
die "$dir is not a directory" unless -d $dir;
opendir(DIR, $dir) or die $!;
my @mp3s = grep { -f "$dir/$_" } sort grep /^[\x20-\x7E]+$/, readdir(DIR);
closedir DIR;

# Start mpg123 in a child process.  It will send responses back to the
# session with the alias "console".
POE::Component::MPG123->spawn(alias => "console");

# This POE Session runs the console.  It maps mpg123 and internal
# events to the functions which will handle them.
POE::Session->create(
  inline_states => {
    _start => \&console_initialize,

    # Events from the mpg123 process.
    player_quit  => \&player_quit,
    status       => \&player_status,
    song_info    => \&player_song_info,
    file_info    => \&player_file_info,
    song_paused  => \&player_song_paused,
    song_resumed => \&player_song_resumed,
    song_stopped => \&player_song_stopped,

    # Internal events.
    update_all  => \&curses_refresh,
    update_time => \&curses_show_time,
    show        => \&curses_show,
    got_input   => \&curses_input,
  }
);

# Now that the mpg123 process and console interface are started, let's
# run them until they're done.
$poe_kernel->run();
exit 0;
### The console display is broken into separate regions.  Each region
### can be updated by calling a plain functions.  Event handlers, such
### as update_all() will call these directly.
# Update the time display.  This also moves the hash mark across the
# screen to show the song's progress.
sub update_time {
  my $time_info = shift;
  my ($fw, $tw) = @{$time_info}{'frames_width', 'time_width'};
  move(13, 0);
  clrtoeol();
  addstr(
    sprintf(
      "Time info  : frames=\%${fw}d/\%${fw}d "
        . "seconds=\%${tw}.2f/\%${tw}.2f",
      @{$time_info}{'frames_played', 'total_frames', 'seconds_played',
        'total_seconds'}
    )
  );
  move(14, 0);
  addstr('-' x $Curses::COLS);
  move(14, $time_info->{marker});
  addstr('#');
  move(14, $time_info->{marker});
}

# Update the song information.  Title, album, genre, etc.
sub update_song {
  my $song = shift;
  move(0, 0);
  clrtoeol();
  addstr("Artist     : $song->{artist}");
  move(1, 0);
  clrtoeol();
  addstr("Album      : $song->{album}");
  move(2, 0);
  clrtoeol();
  addstr("Genre      : $song->{genre}");
  move(3, 0);
  clrtoeol();
  addstr("Year       : $song->{year}");
  move(4, 0);
  clrtoeol();
  addstr("Title      : $song->{title}");
  move(5, 0);
  clrtoeol();
  addstr("Comment    : $song->{comment}");
}

# File information comes back separate from song information.  Since
# I'm a geek at heart, I'll display it all.
sub update_file {
  my $file = shift;
  move(6, 0);
  clrtoeol();
  addstr("MPEG info  : type=$file->{type} layer=$file->{layer} "
      . "bitrate=$file->{bitrate}");
  move(7, 0);
  clrtoeol();
  addstr("           : emphasis=$file->{emphasis} crc=$file->{crc} "
      . "copyright=$file->{copyrighted}");
  move(8, 0);
  clrtoeol();
  addstr("           : extension=$file->{extension}");
  move(9, 0);
  clrtoeol();
  addstr("Audio info : sample rate=$file->{samplerate} "
      . "mode=$file->{mode} (extension=$file->{mode_extension})");
  move(10, 0);
  clrtoeol();
  addstr("Frame info : bits per=$file->{bpf} channels=$file->{channels}");
  move(11, 0);
  clrtoeol();
  addstr("Flags      : copyright=$file->{copyrighted} ");
}

# Update the song list.  This sort of handles the highlight bar and
# scrolling, although it's really kind of cheezy.
sub update_list {
  my $list_info = shift;
  my $row       = 15;
  if ($list_info->{cursor} < 0) {
    $list_info->{cursor} = 0;
    $list_info->{top}-- if $list_info->{top};
  }
  elsif ($row + $list_info->{cursor} >= $Curses::LINES) {
    $list_info->{cursor} = $Curses::LINES - $row - 1;
    $list_info->{top}++ if $list_info->{top} + $row - 1 < @mp3s;
  }
  my $offset = 0;
  my $index  = $list_info->{top};
  while ($row < $Curses::LINES) {
    move($row, 0);
    clrtoeol();
    if ($index < @mp3s) {
      attrset(A_REVERSE) if $offset == $list_info->{cursor};
      addstr(sprintf("%5d: %s", $index, $mp3s[$index]));
    }
    attrset(A_NORMAL) if $offset == $list_info->{cursor};
    $row++;
    $offset++;
    $index++;
  }
  move($row + $list_info->{cursor}, 0);
}
### Now for the event handlers proper.
# Initialize the player's console interface.  Called automatically by
# POE when the console session has started.
sub console_initialize {
  my ($kernel, $heap) = @_[KERNEL, HEAP];
  $kernel->alias_set("console");

  # Attach to STDIN via Curses.  Build a screen to display on.
  $heap->{curses} = POE::Wheel::Curses->new(InputEvent => 'got_input');

  # These structures contain information we'll be displaying.  Since
  # we aren't guaranteed to receive all of it from mpg123, we set it
  # up with defaults to avoid undef warnings.
  $heap->{list_info} = {map { ($_, 0) } qw(top cursor)};
  $heap->{song_info} =
    {map { ($_, 0) } qw(artist album genre year title comment)};
  $heap->{file_info} = {
    map { ($_, 0) }
      qw( type layer samplerate mode mode_extension bpf
      channels copyrighted crc emphasis bitrate
      extension
      )
  };
  $heap->{time_info} = {
    total_frames   => 0,
    total_seconds  => 0,
    marker         => 0,
    frames_width   => 1,
    time_width     => 1,
    frames_played  => 0,
    seconds_played => 0,
  };
  $kernel->yield('update_all');
  $heap->{current_directory} = '.';
}

# Update all parts of the display.
sub curses_refresh {
  my $heap = $_[HEAP];
  update_song($heap->{song_info});
  update_file($heap->{file_info});
  update_time($heap->{time_info});
  update_list($heap->{list_info});
  noutrefresh();
  doupdate;
}

# The mpg123 player quit.  Shut down the console interface, which
# triggers the program's exit.
sub player_quit {
  my ($kernel, $heap) = @_[KERNEL, HEAP];
  move($Curses::LINES - 1, 0);
  clrtoeol();
  move($Curses::LINES - 2, 0);
  clrtoeol();
  addstr("Player has quit.  Bye!");
  noutrefresh();
  doupdate;
  $kernel->alias_remove("console");
  delete $heap->{curses};
}

# Received a play status message from mpg123.  Calculate the song's
# progress for display later.
sub player_status {
  my ($heap, $frames_played, $frames_left, $seconds_played, $seconds_left) =
    @_[HEAP, ARG0 .. ARG3];
  unless ($frames_played) {
    $heap->{time_info} = {
      total_frames  => $frames_left,
      total_seconds => $seconds_left,
      marker        => 0,
      frames_width  => length($frames_left),
      time_width    => length($seconds_left),
    };
  }
  $heap->{time_info}->{frames_played}  = $frames_played;
  $heap->{time_info}->{seconds_played} = $seconds_played;
  if ($frames_played > $heap->{time_info}->{total_frames}) {
    $heap->{time_info}->{total_frames}  = $frames_played;
    $heap->{time_info}->{total_seconds} = $seconds_played;
  }
  my $total_frames = $heap->{time_info}->{total_frames};
  my $percent_done = ($frames_played / $total_frames) * 100;

  # This is the position of the wandering "#".
  my $marker = int(($frames_played / $total_frames) * ($Curses::COLS - 1));

  # Update the time display if the marker has moved.
  if ($percent_done == 100 or $marker != $heap->{time_info}->{marker}) {
    $heap->{time_info}->{marker} = $marker;
    update_time($heap->{time_info});
    noutrefresh();
    doupdate;
  }
}

# Received song information from mpg123.  Format and display it.  We
# can get back two kinds of information: abbreviated "filename" only,
# or full song info.
sub player_song_info {
  my ($heap, $info) = @_[HEAP, ARG0];
  if ($info->{type} eq 'filename') {
    $heap->{song_info} = {
      artist  => '',
      album   => '',
      genre   => '',
      year    => '',
      title   => $info->{filename},
      comment => 'No ID3 information.',
    };
  }
  else {
    $heap->{song_info} = {%$info};
  }
  update_song($heap->{song_info});
  noutrefresh();
  doupdate;
}

# Received detailed MPEG information from mpg123.  Format and show it.
sub player_file_info {
  my ($heap, $info) = @_[HEAP, ARG0];
  $heap->{file_info} = {%$info};
  update_file($heap->{file_info});
  noutrefresh();
  doupdate;
}

# TODO: Confirmations about the song's playback status.
sub player_song_paused  { }
sub player_song_resumed { }
sub player_song_stopped { }

# Got console input.  Don't just sit there, do something!
sub curses_input {
  my ($kernel, $heap, $keystroke) = @_[KERNEL, HEAP, ARG0];

  # Replace special keystrokes with the names they're known by to
  # Curses.
  $keystroke = uc(keyname($keystroke)) if $keystroke =~ /^\d{2,}$/;

  # Ctrl+L refreshes the display.
  if ($keystroke eq "\cL") {
    $kernel->yield('update');
    return;
  }

  # Emergency exit on C-c.
  if (($keystroke eq "\cC") or (lc($keystroke) eq 'q')) {
    $kernel->post(mpg123 => 'quit');
    return;
  }

  # Navigate down the play list.
  if ($keystroke eq 'KEY_DOWN') {
    $heap->{list_info}->{cursor}++;
    update_list($heap->{list_info});
    noutrefresh();
    doupdate;
    return;
  }

  # Navigate up the play list.
  if ($keystroke eq 'KEY_UP') {
    $heap->{list_info}->{cursor}--;
    update_list($heap->{list_info});
    noutrefresh();
    doupdate;
    return;
  }

  # The "p" key toggles between "play" and "pause".
  if (lc($keystroke) eq 'p') {
    $kernel->post(
      mpg123 => play => (
            $dir . '/'
          . $mp3s[$heap->{list_info}->{top} + $heap->{list_info}->{cursor}]
      )
    );
    return;
  }

  # The spacebar is definitely paus.
  if ($keystroke eq ' ') {
    $kernel->post(mpg123 => 'pause');
    return;
  }

  # And "s" stops.
  if (lc($keystroke) eq 's') {
    $kernel->post(mpg123 => 'stop');
    return;
  }
}