#!/usr/bin/env python
# -*- python -*-

__version__ = "comms"

# Comms will try to start this only if there's no player running
default_player = "audacious"

# Where to attempt to start the player. Usually :0 is the X server
# you're running. Set to empty "" to force headless operation.
default_x_display = ":0"


"""
Forked from Ulf Betlehem's cplay 1.44 by TeknoHog()iki.fi on 27.01.2002
Homepage: http://www.iki.fi/teknohog/hacks/comms/

30.11.2003:
Added command_play_album contributed by Kim Poulsen <kpo()daimi.au.dk>

30.12.2003:
Changed backend into pyxmms
(http://www.via.ecp.fr/~flo/2002/PyXMMS/xmms.html)

26.08.2005: Added command_add_cd and command_play_cd

28.9.2005: Improved xmms launching. If an X server is running, it is utilized.

8.12.2005:
Disabed move during playback (see function command_move for reasons)
Re-enabled mark_all
Added mplayer-style 9/0 keybindings for volume down/up
Moved audio CD functions from playlist to root window

20060106:
Disabled periodic display updates while paused (to facilitate the following)
Added display_fileinfo. It has some problems still:
* when path is too long -- use larger terminal window if possible
* when song is playing -- pause the song to see the path

20060329:
Added the rather experimental command_add_via_cache. It copies a file
to a cache directory, and adds that copy into playlist. I wrote it to
help with DJing, so that I can make a playlist of files that are
burned on different CDs.
It could use a feature to remove files from cache after they have been
played, but it's not essential for me at the moment. A single night's
cache should not take too many gigs.

20060615:
Kind of bugfix in command_add_cd and command_play_cd

20060829:
command_mark_all: removed dupe and changed behaviour to toggle. In
fact I wanted a kind of unmark_all since I sometimes mark_all by
accident. Then again marking/unmarking of single entries is a toggle
with a single key, so this may be more sensible.

20061102:
Version for Audacious with xmmsalike.py + ctypes instead of
pyxmms. Aim for a general xmms/audacious/bpm version, as the interface
allows this pretty easily. However, I had to hardcode /mnt/cdrom as
the drive mountpoint, since the functions for reading the config are
not so universal. I don't consider this a big issue, but hardcoding is
such a loss of elegance ;)

20061103:

Generalized xmmsalike.py usage to cover xmms and bmp in addition to
audacious. (BMP does need testing though, I don't have it at the
moment.) Also rebuilt the X detection and headless stuff, it may need
some more refining. Since the cdrom directory is no longer detected,
and there are some other variables you need to specify, I put those at
the top of this script for easier configuration.

20061103b:

New config file reading function; now we can again get CDDA directory
from the player :)


TODO:
playlist indexing? do we need that?
cache song lengths into playlist-buffer.entries? very minimal benefit.
fix quirks with updating display.. should be enough now.
more xmms'ish keybindings?
start xmms with files from args; works, but may need smoothing
const TODO update helptext;
add_url (btw, this works by adding an m3u file that contains the url)

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
"""

# ------------------------------------------
from types import *

import os
import sys
import time
import getopt
import signal
import string
import select
import re
#import xmms.control
#import xmms.config
import shutil

import xmmsalike
import ConfigParser

try: from ncurses import curses
except ImportError: import curses

try: import tty
except ImportError: tty = None

try: import locale; locale.setlocale(locale.LC_ALL, "")
except: pass


# ------------------------------------------
_locale_domain = "comms"
_locale_dir = "/usr/local/share/locale"

try:
    import gettext # python 2.0
    gettext.install(_locale_domain, _locale_dir)
except ImportError:
    try:
        import fintl
        fintl.bindtextdomain(_locale_domain, _locale_dir)
        fintl.textdomain(_locale_domain)
        _ = fintl.gettext
    except ImportError:
        def _(s): return s
except:
    def _(s): return s

# ------------------------------------------
XTERM = re.search("rxvt|xterm", os.environ["TERM"]) and 1 or 0
RETRY = 2.0

# ------------------------------------------
## def log(msg):
##     f = open("log", "a"); f.write(msg); f.close()

# ------------------------------------------
def which(program):
    for path in string.split(os.environ["PATH"], ":"):
        if os.path.exists(os.path.join(path, program)):
            return os.path.join(path, program)

# ------------------------------------------
class Stack:
    def __init__(self):
        self.items = ()

    def push(self, item):
        self.items = (item,) + self.items

    def pop(self):
        self.items, item = self.items[1:], self.items[0]
        return item

# ------------------------------------------
class KeymapStack(Stack):
    def process(self, code):
        for keymap in self.items:
            if keymap and keymap.process(code):
                break

# ------------------------------------------
class Keymap:
    def __init__(self):
        self.methods = [None] * curses.KEY_MAX

    def bind(self, key, method, args=None):
        if type(key) in (TupleType, ListType):
            for i in key: self.bind(i, method, args)
            return
        if type(key) is StringType:
            key = ord(key)
        self.methods[key] = (method, args)

    def process(self, key):
        if self.methods[key] is None: return 0
        method, args = self.methods[key]
        if args is None:
            apply(method, (key,))
        else:
            apply(method, args)
        return 1

# ------------------------------------------
class Window:

    t = ['?'] * 256
    for i in range(0x20, 0x7f): t[i] = chr(i)
    for c in string.letters: t[ord(c)] = c
    translationTable = string.join(t, "")

    def __init__(self, parent):
        self.parent = parent
        self.children = []
        self.name = None
        self.keymap = None
        self.visible = 1
        self.resize()
        if parent: parent.children.append(self)

    def __getattr__(self, name):
        return getattr(self.w, name)

    def getmaxyx(self):
        y, x = self.w.getmaxyx()
        try: curses.version # tested with '1.2' and '1.6'
        except AttributeError:
            # pyncurses - emulate traditional (silly) behavior
            y, x = y+1, x+1
        return y, x

    def touchwin(self):
        try: self.w.touchwin()
        except AttributeError: self.touchln(0, self.getmaxyx()[0])

    def attron(self, attr):
        try: self.w.attron(attr)
        except AttributeError: self.w.attr_on(attr)

    def attroff(self, attr):
        try: self.w.attroff(attr)
        except AttributeError: self.w.attr_off(attr)

    def newwin(self):
        return curses.newwin(0, 0, 0, 0)

    def resize(self):
        ## todo - delwin?
        self.w = self.newwin()
        self.ypos, self.xpos = self.getbegyx()
        self.rows, self.cols = self.getmaxyx()
        self.keypad(1)
        self.leaveok(1)
        self.scrollok(1)
        for child in self.children:
            child.resize()

    def update(self):
        self.clear()
        self.refresh()
        for child in self.children:
            child.update()

# ------------------------------------------
class HelpWindow(Window):
    text = _("""\
z, x, c, v, b   = prev, play, pause, stop, next (as in XMMS)
+,=,0 / -,9     = Increase / decrease volume
Tab             = Goto playlist/filelist
s               = Toggle shuffle (see below if it's on[S] or off[s])
r               = Toggle repeat (ditto with [R]/[r])
Left/Right      = Skip -/+ 5 seconds within song
C-l             = Refresh screen
q               = Quit
Esc             = Abort prompted operation (w, o)
C               = Add audio CD to playlist
P               = Clear playlist and play audio CD
f               = Show path of file being played (buggy, see source)

Movement: Up, C-p, k, Down, C-n, j, PgUp, K, PgDown, J, Home, g, End, G

Filelist only:
. or Backspace  = Parent directory
Space           = Add file to playlist
Enter           = Goto directory; Play file
o               = Prompt for directory to go to
a               = Add directory recursively to playlist
p               = Clear playlist and play directory contents
t               = Add file via harddrive cache

Playlist only:
Space    = Mark or unmark song
a        = Mark all
d / D    = Remove marked songs / remove all
m        = Move marked songs to pointer position
Enter    = Start playing from file
w        = Write playlist to a file
""")

    def __init__(self, parent):
        Window.__init__(self, parent)
        self.name = _("Help")
        self.keymap = Keymap()
        self.keymap.bind('q', self.parent.help, ())

    def newwin(self):
        return curses.newwin(self.parent.rows-2, self.parent.cols, self.parent.ypos+2, self.parent.xpos)

    def update(self):
        self.move(0, 0)
        self.addstr(self.text)
        self.touchwin()
        self.refresh()

# ------------------------------------------
class ProgressWindow(Window):
    def __init__(self, parent):
        Window.__init__(self, parent)
        self.value = 0

    def newwin(self):
        return curses.newwin(1, self.parent.cols, self.parent.rows-2, 0)

    def update(self):
        self.move(0, 0)
        self.hline(ord('-'), self.cols)
        if self.value > 0:
            self.move(0, 0)
            x = int(self.value * self.cols)  # 0 to cols-1
            self.hline(ord('='), x+1)
            self.move(0, x)
            self.addstr('|')
        self.touchwin()
        self.refresh()

    def progress(self):
        denom = float(xmmsalike.get_playlist_time(xmmsalike.get_playlist_pos()))
        if denom > 0:
            value = float(xmmsalike.get_output_time()) / denom
        else:
            value = 0
        self.value = min(value, 0.99)
        self.update()

# ------------------------------------------
class StatusWindow(Window):
    def __init__(self, parent):
        Window.__init__(self, parent)
        self.default_message = ''
        self.current_message = ''
        self.timeout_tag = None

    def newwin(self):
        return curses.newwin(1, self.parent.cols-16, self.parent.rows-1, 0) # watch the width.. related to CounterWindow!

    def update(self):
        msg = string.translate(self.current_message, Window.translationTable)
        if len(msg) > self.cols: msg = "%s>" % msg[:self.cols-1]
        self.move(0, 0)
        self.addstr(msg)
        self.clrtoeol()
        self.touchwin()
        self.refresh()

    def status(self, message, duration = 0):
        self.current_message = message
        if duration > 0:
            if self.timeout_tag: app.timeout.remove(self.timeout_tag)
            self.timeout_tag = app.timeout.add(duration, self.timeout)
        self.update()

    def timeout(self):
        self.timeout_tag = None
        self.restore_default_status()

    def set_default_status(self, message):
        self.default_message = message
        self.status(message)
        XTERM and sys.stderr.write("\033]0;%s\a" % (message or "comms"))

    def restore_default_status(self):
        self.status(self.default_message)

# ------------------------------------------
class CounterWindow(Window):
    def __init__(self, parent):
        Window.__init__(self, parent)
        self.values = [0, 0]
        self.mode = 1

    def newwin(self):
        return curses.newwin(1, 15, self.parent.rows-1, self.parent.cols-15)

    def update(self):
        if xmmsalike.is_repeat():
            rep_status = "R"
        else:
            rep_status = "r"

        if xmmsalike.is_shuffle():
            shu_status = "S"
        else:            
            shu_status = "s"

        seconds = xmmsalike.get_output_time() / 1000
        m, s = divmod(seconds, 60)

        statusline = "[" + rep_status + "] [" + shu_status + "]"

        self.move(0, 0)
        self.attron(curses.A_BOLD)
        self.insstr(statusline + " %02dm %02ds" % (m, s))
        # Dog knows why addstr fails.. fix borrowed from cplay 1.46
        self.attroff(curses.A_BOLD)
        self.touchwin()
        self.refresh()

    def counter(self):
        time.sleep(0.05)
        self.update()

    def toggle_mode(self):
        self.mode = not self.mode
        self.update()

# ------------------------------------------
class RootWindow(Window):
    def __init__(self, parent):
        Window.__init__(self, parent)
        keymap = Keymap()
        keymap.bind(12, self.update, ()) # C-l
        keymap.bind([curses.KEY_LEFT, 2], app.seek, (-1,)) # Left, C-b
        keymap.bind([curses.KEY_RIGHT, 6], app.seek, (1,)) # Right, C-f
#        keymap.bind(range(48,58), app.key_volume) # 1234567890
        keymap.bind(['+', "=", "0"], app.inc_volume, ())
        keymap.bind(['-', "9"], app.dec_volume, ())
        keymap.bind('b', app.next, ())
        keymap.bind('z', app.prev, ())
        keymap.bind('c', app.pause, ())
        keymap.bind('v', app.stop, ())
        keymap.bind('x', app.play, ())
        keymap.bind('r', app.toggle_repeat, ())
        keymap.bind('s', app.toggle_shuffle, ())
#        keymap.bind('t', app.toggle_counter_mode, ())
        keymap.bind('q', app.quit, ())
        keymap.bind('C', self.command_add_cd, ())
        keymap.bind('P', self.command_play_cd, ())
        keymap.bind('f', app.display_fileinfo, ())

        app.keymapstack.push(keymap)

        self.win_progress = ProgressWindow(self)
        self.win_status = StatusWindow(self)
        self.win_counter = CounterWindow(self)
        self.win_tab = TabWindow(self)


    def command_add_cd(self):
        cdda_directory = xmmsalike.config_get("CDDA", "directory")
        xmmsalike.playlist_add([cdda_directory])
        self.update()

    def command_play_cd(self):
        cdda_directory = xmmsalike.config_get("CDDA", "directory")
        xmmsalike.playlist([cdda_directory], 0)


# ------------------------------------------
class TabWindow(Window):
    def __init__(self, parent):
        Window.__init__(self, parent)
        self.active_child = 0

        self.win_filelist = self.add(FilelistWindow)
        self.win_playlist = self.add(PlaylistWindow)
        self.win_help     = self.add(HelpWindow)

        self.keymap = Keymap()
        self.keymap.bind('\t', self.change_window, ()) # Tab
        self.keymap.bind('h', self.help, ())
        app.keymapstack.push(self.keymap)
        app.keymapstack.push(self.children[self.active_child].keymap)

    def newwin(self):
        return curses.newwin(self.parent.rows-2, self.parent.cols, 0, 0)

    def update(self):
        self.update_title()
        self.move(1, 0)
        self.hline(ord('-'), self.cols)
        self.move(2, 0)
        self.clrtobot()
        self.refresh()
        child = self.children[self.active_child]
        child.visible = 1
        child.update()

    def update_title(self, refresh = 1):
        self.move(0, 0)
        self.clrtoeol()
        self.attron(curses.A_BOLD)
        self.addstr(str(self.children[self.active_child].name))
        self.attroff(curses.A_BOLD)
        if refresh: self.refresh()

    def add(self, Class):
        win = Class(self)
        win.visible = 0
        return win

    def change_window(self, window = None):
        app.keymapstack.pop()
        self.children[self.active_child].visible = 0
        if window:
            self.active_child = self.children.index(window)
        else:
            # toggle windows 0 and 1
            self.active_child = not self.active_child
        app.keymapstack.push(self.children[self.active_child].keymap)
        self.update()

    def help(self):
        if self.children[self.active_child] == self.win_help:
            self.change_window(self.win_last)
        else:
            self.win_last = self.children[self.active_child]
            self.change_window(self.win_help)
            app.status(__version__, 2)

# ------------------------------------------
class ListWindow(Window):
    def __init__(self, parent):
        Window.__init__(self, parent)
        self.buffer = []
        self.bufptr = self.scrptr = 0
        self.search_direction = 0

        self.input_mode = 0
        self.input_prompt = ""
        self.input_string = ""
        self.do_input_hook = None
        self.stop_input_hook = None

        self.keymap = Keymap()
        self.keymap.bind(['k', curses.KEY_UP, 16], self.cursor_move, (-1,))
        self.keymap.bind(['j', curses.KEY_DOWN, 14], self.cursor_move, (1,))
        self.keymap.bind(['K', curses.KEY_PPAGE], self.cursor_ppage, ())
        self.keymap.bind(['J', curses.KEY_NPAGE], self.cursor_npage, ())
        self.keymap.bind(['g', curses.KEY_HOME], self.cursor_home, ())
        self.keymap.bind(['G', curses.KEY_END], self.cursor_end, ())
        self.keymap.bind(['?', 18], self.start_search,
                         (_("backward-isearch"), -1))
        self.keymap.bind(['/', 19], self.start_search,
                         (_("forward-isearch"), 1))
        self.input_keymap = Keymap()
        self.input_keymap.bind(range(32, 128), self.do_input)
        self.input_keymap.bind('\t', self.do_input)
        self.input_keymap.bind(curses.KEY_BACKSPACE, self.do_input, (8,))
        self.input_keymap.bind(['\a', 27], self.stop_input, (_("cancel"),))
        self.input_keymap.bind('\n', self.stop_input, (_("ok"),))

    def newwin(self):
        return curses.newwin(self.parent.rows-2, self.parent.cols,
                             self.parent.ypos+2, self.parent.xpos)

    def update(self, force = 1):
        self.bufptr = max(0, min(self.bufptr, len(self.buffer) - 1))
        scrptr = (self.bufptr / self.rows) * self.rows
        if force or self.scrptr != scrptr:
            self.scrptr = scrptr
            self.move(0, 0)
            for entry in self.buffer[self.scrptr:]:
                if self.getyx()[0] == self.rows - 1: break
                if self.getyx()[1] > 0: self.addstr("\n")
                self.putstr(entry)
            self.clrtobot()
            if self.visible: self.refresh()
        self.update_line(curses.A_REVERSE)

    def update_line(self, attr = None, refresh = 1):
        if not self.buffer: return
        ypos = self.bufptr - self.scrptr
        if attr: self.attron(attr)
        self.move(ypos, 0)
        self.hline(ord(' '), self.cols)
        entry = self.current()
        self.putstr(entry)
        if attr: self.attroff(attr)
        if self.visible and refresh: self.refresh()

    def start_input(self, prompt="", data=""):
        self.input_mode = 1
        app.keymapstack.push(self.input_keymap)
        self.input_prompt = prompt
        self.input_string = data

    def do_input(self, *args):
        if self.do_input_hook:
            return apply(self.do_input_hook, args)
        ch = args and args[0] or None
        if ch in [8, 127]: # backspace
            self.input_string = self.input_string[:-1]
        elif ch:
            self.input_string = "%s%c" % (self.input_string, ch)
        app.status("%s: %s" % (self.input_prompt, self.input_string))

    ## We have the result in self.input_string
    def stop_input(self, *args):
        self.input_mode = 0
        app.keymapstack.pop()
        if self.stop_input_hook:
            return apply(self.stop_input_hook, args)

    def putstr(self, entry, *pos):
        s = string.translate(str(entry), Window.translationTable)
        s = "%s%s" % ((len(s) > self.cols) and (s[:self.cols - 1], ">") or (s, ""))
        pos and apply(self.move, pos)
        self.addstr(s)

    def current(self):
        if self.bufptr >= len(self.buffer): self.bufptr = len(self.buffer) - 1
        return self.buffer[self.bufptr]

    def cursor_move(self, ydiff):
        if self.input_mode: self.stop_input(_("cancel"))
        if not self.buffer: return
        self.update_line(refresh = 0)
        self.bufptr = (self.bufptr + ydiff) % len(self.buffer)
        self.update(force = 0)

    def cursor_ppage(self):
        if self.rows > len(self.buffer): return
        tmp = self.bufptr % self.rows
        if tmp == self.bufptr:
            self.cursor_move(-(tmp + (len(self.buffer) % self.rows) or self.rows))
        else:
            self.cursor_move(-(tmp + self.rows))

    def cursor_npage(self):
        if self.rows > len(self.buffer): return
        tmp = self.rows - self.bufptr % self.rows
        if self.bufptr + tmp > len(self.buffer):
            self.cursor_move(len(self.buffer) - self.bufptr)
        else:
            self.cursor_move(tmp)

    def cursor_home(self): self.cursor_move(-self.bufptr)

    def cursor_end(self): self.cursor_move(-self.bufptr - 1)

    def is_searching(self): return abs(self.search_direction)

    def start_search(self, type, direction):
        if not self.is_searching():
            self.start_input()
            self.do_input_hook = self.do_search
            self.stop_input_hook = self.stop_search
        if self.search_direction != direction:
            self.search_direction = direction
            self.input_prompt = type
            self.do_search()
        else:
            self.do_search(advance = direction)

    def stop_search(self, reason = ""):
        self.search_direction = 0
        app.status(reason, 1)

    def do_search(self, ch = None, advance = 0):
        direction = self.search_direction
        if ch in [8, 127]: # backspace
            direction = -direction
            self.input_string = self.input_string[:-1]
        elif ch:
            self.input_string = "%s%c" % (self.input_string, ch)
        index = self.bufptr + advance
        while 1:
            if index >= len(self.buffer) or index < 0:
                app.status(_("Not found: %s") % self.input_string)
                break
            line = "%s" % self.buffer[index]
            if string.find(string.lower(line),
                           string.lower(self.input_string)) != -1:
                app.status("%s: %s" % (self.input_prompt, self.input_string))
                self.update_line(refresh = 0)
                self.bufptr = index
                self.update(force = 0)
                break
            index = index + direction

# ------------------------------------------
class FilelistWindow(ListWindow):
    def __init__(self, parent):
        ListWindow.__init__(self, parent)
        self.oldposition = {}
        try: self.chdir(os.getcwd())
        except OSError: self.chdir(os.environ['HOME'])
        self.mtime_when = 0
        self.mtime = None
        self.keymap.bind('\n', self.command_chdir_or_play, ())
        self.keymap.bind(['.', curses.KEY_BACKSPACE],
                         self.command_chparentdir, ())
        self.keymap.bind(' ', self.command_add, ())
        self.keymap.bind('a', self.command_add_recursively, ())
        self.keymap.bind('o', self.command_goto, ())
        self.keymap.bind('p', self.command_play_album, ())

        self.keymap.bind('t', self.command_add_via_cache, ())
        self.cachedir = os.path.join(os.environ['HOME'], "tmp/comms-cache") + "/"

    def listdir_maybe(self, now=0):
        if now < self.mtime_when+2: return
        self.mtime_when = now
        try:
            mtime = os.stat(self.cwd)[8]
            self.mtime == mtime or self.listdir()
            self.mtime = mtime
        except os.error: pass

    def listdir(self):
        app.status(_("Reading directory..."))
        self.dirs = []
        self.files = []
        try:
            self.mtime = os.stat(self.cwd)[8]
            self.mtime_when = time.time()
            for entry in os.listdir(self.cwd):
                if entry[0] == ".":
                    continue
                if os.path.isdir(self.cwd + entry):
                    self.dirs.append("%s/" % entry)
                else:
                    self.files.append("%s" % entry)
        except os.error: pass
        self.dirs.sort()
        self.files.sort()
        self.buffer = self.dirs + self.files
        self.cwd != "/" and self.buffer.insert(0, "../")
        if self.oldposition.has_key(self.cwd):
            self.bufptr = self.oldposition[self.cwd]
        else:
            self.bufptr = 0
        self.parent.update_title()
        self.update(force = 1)
        app.restore_default_status()

    def normpath(self, dir):
        dir = dir and dir + '/'
        match = 1
        while match: dir, match = re.subn("/+(\.|[^/]*/*\.\.)/+", "/", dir, 1)
        match = 1
        while match: dir, match = re.subn("//+", "/", dir, 1)
        return dir

    def chdir(self, dir):
        if hasattr(self, "cwd"): self.oldposition[self.cwd] = self.bufptr
        self.cwd = self.normpath(dir)
        self.name = _("Filelist: ")
        diff = len(self.name) + len(self.cwd) - self.cols
        if diff > 0: self.name = "%s<%s" % (self.name, self.cwd[diff+1:])
        else: self.name = "%s%s" % (self.name, self.cwd)

    def command_chdir_or_play(self):
        if os.path.isdir(self.cwd + self.current()):
            self.chdir(self.cwd + self.current())
            self.listdir()
        else:
            # it's a file

            # this is pyxmms specific macro...
            #xmmsalike.enqueue_and_play([self.cwd + self.current()])

            # ..so I rewrite it similarly to pyxmms.
            pl = xmmsalike.get_playlist_length()
            xmmsalike.playlist_add([self.cwd + self.current()])
            xmmsalike.set_playlist_pos(pl)
            xmmsalike.play()


    def command_chparentdir(self):
        self.chdir(self.cwd + "..")
        self.listdir()

    def command_goto(self):
        self.start_input(_("goto"))
        self.do_input_hook = None
        self.stop_input_hook = self.stop_goto
        self.do_input()

    def stop_goto(self, reason):
        if reason == _("cancel") or not self.input_string:
            app.status(_("cancel"), 1)
            return
        dir = self.input_string
        if dir[0] != '/': dir = "%s%s" % (self.cwd, dir)
        if not os.path.isdir(dir):
            app.status(_("Not a directory!"), 1)
            return
        self.chdir(dir)
        self.listdir()

    def command_add(self):
        if (os.path.isfile(self.cwd + self.current())):
            xmmsalike.playlist_add([self.cwd + self.current()])
            self.cursor_move(+1)

    def command_add_recursively(self):
        xmmsalike.playlist_add([self.cwd + self.current()])
        self.cursor_move(+1)

    def command_add_via_cache(self):
        # teknohog's experimental DJing feature

        sourcefile = self.cwd + self.current()
        destfile = self.cachedir + self.current()

        if (os.path.isfile(sourcefile)):
            # copy file to cachedir
            if not os.path.isdir(self.cachedir):
                os.makedirs(self.cachedir)
                
            shutil.copyfile(sourcefile, destfile)

            # chmod 644 to enable deletion; leading 0 is required for octal
            os.chmod(destfile, 0644)

            # add cached file to playlist
            xmmsalike.playlist_add([destfile])
            self.cursor_move(+1)
            
    def command_play_album(self):
        xmmsalike.playlist([self.cwd + self.current()], 0)

# ------------------------------------------
class PlaylistEntry:
    def __init__(self, title):
        self.title = title
        self.marked = 0
        self.active = 0
        self.attrib = curses.A_BOLD

    def set_marked(self, value):
        self.marked = value

    def toggle_marked(self):
        self.marked = not self.marked

    def is_marked(self):
        return self.marked == 1

    def set_active(self, value):
        self.active = value

    def is_active(self):
        return self.active == 1

    def __str__(self):
        return "%s %s" % (self.is_marked() and "#" or " ", self.title)

# ------------------------------------------
class PlaylistWindow(ListWindow):
    def __init__(self, parent):
        ListWindow.__init__(self, parent)
        self.name = _("Playlist")
        self.repeat = 0
        self.random = 0
        self.random_buffer = []
        self.keymap.bind('\n', self.command_play, ())
        self.keymap.bind(' ', self.command_mark, ())
        self.keymap.bind('d', self.command_delete, ())
        self.keymap.bind('m', self.command_move, ())
        self.keymap.bind('w', self.command_save_playlist, ())
        self.keymap.bind('D', self.command_delete_all, ())
#        self.keymap.bind('u', self.command_add_url, ())
        self.keymap.bind('a', self.command_mark_all, ())
#        self.keymap.bind('c', self.command_clear_all, ())
#        self.keymap.bind('A', self.command_mark_regexp, ())
#        self.keymap.bind('C', self.command_clear_regexp, ())

    def update(self, force = 1):

        # If the list doesn't change, let's not lose any marking info!
        xlength = xmmsalike.get_playlist_length()
        if xlength != len(self.buffer):
            # List has changed.
            self.buffer = []
            if xlength > 0:
                for i in range(xlength):
                    entry = PlaylistEntry(xmmsalike.get_playlist_title(i))
                    self.buffer.append(entry)

# old xmmsctrl impl.
#                for item in xmms.get_playlist():
#                    entry = PlaylistEntry(item)
#                    self.buffer.append(entry)

        self.bufptr = max(0, min(self.bufptr, len(self.buffer) - 1))

        scrptr = (self.bufptr / self.rows) * self.rows
        if force or self.scrptr != scrptr:
            self.scrptr = scrptr
            self.move(0, 0)
            for entry in self.buffer[self.scrptr:]:
                if self.getyx()[0] == self.rows - 1: break
                if self.getyx()[1] > 0: self.addstr("\n")
                self.putstr(entry)
            self.clrtobot()
            if self.visible: self.refresh()
        self.update_line(curses.A_REVERSE)

    def change_name(self):
        self.name = _("Playlist %s %s") % (self.repeat and _("[repeat]")
          or " " * len(_("[repeat]")), self.random and _("[random]")
          or " " * len(_("[random]")))

    def clear(self):
        xmmsalike.playlist_clear()

    def putstr(self, entry, *pos):
        playpos = xmmsalike.get_playlist_pos()
        if self.buffer.index(entry) == playpos: self.attron(curses.A_BOLD)
        apply(ListWindow.putstr, (self, entry) + pos)
        if self.buffer.index(entry) == playpos: self.attroff(curses.A_BOLD)

    def get_remaining_entries(self):
        l = []
        for i in self.buffer:
            if not i in self.random_buffer:
                l.append(i)
        return l

    def get_active_entry(self):
        return self.buffer[xmmsalike.get_playlist_pos()]

    def command_play(self):
        if not self.buffer: return
        xmmsalike.set_playlist_pos(self.bufptr)
        if not xmmsalike.is_playing():
            app.play()
        else:
            app.display_title()
        self.update()

    def command_mark(self):
        if not self.buffer: return
        self.buffer[self.bufptr].toggle_marked()
        self.cursor_move(1)

    def command_mark_all(self):
        for entry in self.buffer:
            entry.toggle_marked()
        app.status(_("Almost there..."), 1)
        self.update(force = 1)

    def command_delete_all(self):
        xmmsalike.playlist_clear()
        app.status(_("Cleared playlist"), 1)
        self.update(force = 1)
        app.progress()
        app.counter()

    def command_delete(self):
        if not self.buffer: return
        current_entry = self.current()

        # must delete in reverse order.. otherwise
        # the indices of to-be-deleted items will change
        for i in range(len(self.buffer)-1, -1, -1):
            if self.buffer[i].is_marked():
                xmmsalike.playlist_delete(i)

        try: self.bufptr = self.buffer.index(current_entry)
        except ValueError: self.bufptr = 0
        self.update(force = 1)

    def command_move(self):
        # this isn't very easily xmms'izable, as there isn't a
        # relevant function in the API.

        # no, wait.. just take the filenames and remove & add back.
        # we have to rebuild the whole list and then clear & add all of them.

        # 20051205 DJing experience -> this is not the way while doing
        # live playback.. disable moving during playback for now, and
        # put a warning message.

        if xmmsalike.is_playing():
            app.status(_("comms move disabled during playback :("), 5)
        else:
            if not self.buffer: return
            current_entry = self.current()
            if current_entry.is_marked(): return    # sanity check

            filelist = []
            l = []
            for i in range(0, len(self.buffer)):
                if self.buffer[i].is_marked():
                    filelist.append("") # to maintain the correct indexing
                    l.append(xmmsalike.get_playlist_file(i))
                else:
                    filelist.append(xmmsalike.get_playlist_file(i))

            self.bufptr = self.buffer.index(current_entry)

            filelist[self.bufptr:self.bufptr] = l

            xmmsalike.playlist_clear()
            self.update(force = 1) # update now so the buffer gets changed!
            for file in filelist:
                if file != "":
                    xmmsalike.playlist_add([file])

            self.update(force = 1)

    def command_mark_regexp(self):
        self.mark_value = 1
        self.start_input(_("Mark regexp"))
        self.do_input_hook = None
        self.stop_input_hook = self.stop_mark_regexp
        self.do_input()

    def command_clear_regexp(self):
        self.mark_value = 0
        self.start_input(_("Clear regexp"))
        self.do_input_hook = None
        self.stop_input_hook = self.stop_mark_regexp
        self.do_input()

    def stop_mark_regexp(self, reason):
        if reason == _("cancel") or not self.input_string:
            app.status(_("cancel"), 1)
            return
        try:
            r = re.compile(self.input_string)
            for entry in self.buffer:
                if r.search(entry.filename):
                    entry.set_marked(self.mark_value)
            self.update(force = 1)
            app.status(_("ok"), 1)
        except re.error, e:
            app.status(str(e), 2)

    def command_save_playlist(self):
        self.start_input(_("Save playlist"), app.win_filelist.cwd)
        self.do_input_hook = None
        self.stop_input_hook = self.stop_save_playlist
        self.do_input()

    def stop_save_playlist(self, reason):
        if reason == _("cancel") or not self.input_string:
            app.status(_("cancel"), 1)
            return
        filename = self.input_string
        if filename[0] != '/':
            filename = "%s%s" % (app.win_filelist.cwd, filename)
        if not VALID_PLAYLIST(filename):
            filename = "%s%s" % (filename, ".m3u")
        try:
            file = open(filename, "w")
            for i in range(0, len(self.buffer)-1):
                file.write("%s\n" % xmmsalike.get_playlist_file(i))
            file.close()
            app.status(_("ok"), 1)
        except IOError:
            app.status(_("Cannot write playlist!"), 1)

    def command_add_url(self):
        self.start_input(_("Add URL"))
        self.do_input_hook = None
        self.stop_input_hook = self.stop_add_url
        self.do_input()

    def stop_add_url(self, reason):
        if reason == _("cancel") or not self.input_string:
            app.status(_("cancel"), 1)
            return
        xmmsalike.playlist_add_url_string(self.input_string)

# ------------------------------------------

class Timeout:
    def __init__(self):
        self.next = 0
        self.dict = {}

    def add(self, timeout, func, args=()):
        tag = self.next = self.next + 1
        self.dict[tag] = (func, args, time.time() + timeout)
        return tag

    def remove(self, tag):
        del self.dict[tag]

    def check(self, now):
        for tag, (func, args, timeout) in self.dict.items():
            if now >= timeout:
                self.remove(tag)
                apply(func, args)
        return len(self.dict) and 0.2 or None

# ------------------------------------------
class Application:
    def __init__(self):
        self.keymapstack = KeymapStack()

    def setup(self):
        if tty:
            self.tcattr = tty.tcgetattr(sys.stdin.fileno())
            tcattr = tty.tcgetattr(sys.stdin.fileno())
            tcattr[0] = tcattr[0] & ~(tty.IXON)
            tty.tcsetattr(sys.stdin.fileno(), tty.TCSANOW, tcattr)
        self.w = curses.initscr()
        curses.cbreak()
        curses.noecho()
        try: curses.meta(1)
        except: pass
        try: curses.curs_set(0)
        except: pass
        signal.signal(signal.SIGCHLD, signal.SIG_IGN)
        signal.signal(signal.SIGHUP, self.handler_quit)
        signal.signal(signal.SIGINT, self.handler_quit)
        signal.signal(signal.SIGTERM, self.handler_quit)
        signal.signal(signal.SIGWINCH, self.handler_resize)
        self.jump_const = 5000 # milliseconds per keypress when seeking
        self.win_root = RootWindow(None)
        self.win_root.update()
        self.win_tab = self.win_root.win_tab
        self.win_filelist = self.win_root.win_tab.win_filelist
        self.win_playlist = self.win_root.win_tab.win_playlist
        self.status = self.win_root.win_status.status
        self.set_default_status = self.win_root.win_status.set_default_status
        self.restore_default_status = self.win_root.win_status.restore_default_status
        self.counter = self.win_root.win_counter.counter
        self.progress = self.win_root.win_progress.progress
        self.timeout = Timeout()
        self.win_filelist.listdir_maybe(time.time())
        self.set_default_status("")
        self.seek_tag = None
        self.start_tag = None


    def cleanup(self):
        curses.endwin()
        XTERM and sys.stderr.write("\033]0;%s\a" % "xterm")
        tty and tty.tcsetattr(sys.stdin.fileno(), tty.TCSADRAIN, self.tcattr)

    def display_title(self):
        time.sleep(0.05) # if we use this for next/prev/pause etc.
        songname = xmmsalike.get_playlist_title(xmmsalike.get_playlist_pos())
        if xmmsalike.is_paused():
            songname += " [PAUSED]"
        self.status(_(songname), 0)

    def display_fileinfo(self):
        fileinfo = xmmsalike.get_playlist_file(xmmsalike.get_playlist_pos())
        self.status(_(fileinfo), 0)

    def play(self):
        xmmsalike.play()
        self.display_title()

    def stop(self):
        xmmsalike.stop()
        self.counter()
        self.progress()

    def next(self):
        xmmsalike.playlist_next()
        self.display_title()

    def prev(self):
        xmmsalike.playlist_prev()
        self.display_title()

    def pause(self):
        xmmsalike.pause()
        self.display_title()

    def toggle_repeat(self):
        xmmsalike.toggle_repeat()
        self.counter()

    def toggle_shuffle(self):
        xmmsalike.toggle_shuffle()
        self.counter()
        
    def run(self):
        self.status(_("Starting player..."), 0)

        while not xmmsalike.is_running():
            # wait for xmms to load
            time.sleep(0.2)
        self.status(_(""), 0)

        if xmmsalike.get_playlist_length():
            app.win_tab.change_window()

        while 1:
            now = time.time()
            timeout = self.timeout.check(now)

            self.win_filelist.listdir_maybe(now)

            # apparently, a paused song is_playing technically, but
            # it's useful to disable these updates while is_paused
            if xmmsalike.is_playing() and not xmmsalike.is_paused():
                timeout = 1
                self.counter() # progress bar is hard, we don't have total times
                self.progress()
                self.display_title() # basically needed for automatic song changes

            R = [sys.stdin]
            try: r, w, e = select.select(R, [], [], timeout)
            except select.error: continue
            ## user input
            if sys.stdin in r:
                c = self.win_root.getch()
                self.keymapstack.process(c)

    def toggle_counter_mode(self):
        self.win_root.win_counter.toggle_mode()

    def seek(self, direction):
        xmmsalike.jump_to_time(xmmsalike.get_output_time() + direction * self.jump_const)

    def change_volume(self, dv):
        # single number
        self.set_volume(self.get_volume() + dv)

    def inc_volume(self):
        self.change_volume(+2)

    def dec_volume(self):
        self.change_volume(-2)

    def key_volume(self, ch):
        self.set_volume((ch & 0x0f) * 10)

    def get_volume(self):
        # single number
        self.volume = xmmsalike.get_main_volume()
        return self.volume

    def set_volume(self, v):
        # single number
        xmmsalike.set_main_volume(v)

    def quit(self):
        if self.daemon_pid:
            xmmsalike.stop() # Let it play if xmms is running elsewhere.
            xmmsalike.quit()
            os.kill(self.daemon_pid, signal.SIGKILL)
        sys.exit(0)

    def handler_resize(self, sig, frame):
        ## curses trickery
        curses.endwin()
        self.w.refresh()
        self.win_root.resize()
        self.win_root.update()

    def handler_quit(self, sig, frame):
        self.quit()

# ------------------------------------------
def main():
    try:
        opts, args = getopt.getopt(sys.argv[1:], "rR")
    except:
        usage = _("Usage: %s [-rR] [ file | dir | playlist.m3u ] ...\n")
        sys.stderr.write(usage % sys.argv[0])
        sys.exit(1)

    global app
    app = Application()

    # initialize xmmsalike.
    # If there is already a player running, it will be found here.
    player = xmmsalike.init()

    # No running player -> start and init default_player.
    if player == "":
        player = default_player
        environ = os.environ

        #if not environ.has_key("DISPLAY"):
        # It's a matter of preference whether you want to change an
        # existing DISPLAY variable. For my usage it is essential to
        # do so. This probably needs some kind of setting...

        environ.update({"DISPLAY": default_x_display})

        # see if we have X.. there has to be a better way. If there's
        # no X this takes some time when the program tries to connect
        # to the X server.

        #test_args = ["xdpyinfo"]
        #x_test = os.spawnvpe(os.P_WAIT, test_args[0], test_args, environ)
        x_test = os.system("DISPLAY=" + default_x_display + " xdpyinfo >/dev/null")
        if x_test == 0:

            player_args = [player]
            os.spawnvpe(os.P_NOWAIT, player_args[0], player_args, environ)

            # leave xmms running
            app.daemon_pid = 0
        else:
            if player == "audacious":
                # we can use the headless mode that doesn't need X. To
                # keep track of the player, we quit it when quitting
                # comms in this case. Since daemon_pid is used to kill
                # Xvfb later, we can use the same marker here.

                player_args = ["audacious", "--headless"]
                app.daemon_pid = os.spawnvp(os.P_NOWAIT, player_args[0], player_args)
            else: 
                # start virtual X server and player. I have no
                # interest in maintaining this section though, so I
                # may remove it in the future.
               
                Xvfb_display = ":2"
                fontpath = "unix/:-1"
                Xvfb_args = ["Xvfb", Xvfb_display, "-fp", fontpath]
                app.daemon_pid = os.spawnvp(os.P_NOWAIT, Xvfb_args[0], Xvfb_args)

                os.system("sleep 1")

                player_args = [player]
                environ.update({"DISPLAY": Xvfb_display})
                os.spawnvpe(os.P_NOWAIT, player_args[0], player_args, environ)

        # the argument is essential, since the player takes some
        # time to get running and recognized.
        xmmsalike.init(player)

    else:
        app.daemon_pid = 0

    playlist = []

#    if not sys.stdin.isatty():
#        playlist = map(string.strip, sys.stdin.readlines())
#        os.close(0)
#        os.open("/dev/tty", 0)

    try:
        app.setup()
#        for opt, optarg in opts:
#            if opt == '-r': app.win_playlist.command_toggle_repeat()
#            if opt == '-R': app.win_playlist.command_toggle_random()
#        if args or playlist:
#            for item in args or playlist:
#                app.win_playlist.append(item)

        app.run()
    except SystemExit:
        app.cleanup()
    except Exception:
        app.cleanup()
        import traceback
        traceback.print_exc()

# ------------------------------------------

RE_PLAYLIST = re.compile(".*\.m3u$", re.I)

def VALID_PLAYLIST(name):
    if RE_PLAYLIST.match(name):
        return 1

# ------------------------------------------
if __name__ == "__main__": main()
