cplay

diff cplay @ 0:aa5f022eac8a

Use upstream cplay-1.49 as a start
author markus schnalke <meillo@marmaro.de>
date Wed, 27 Sep 2017 09:22:32 +0200
parents
children c7d8ec7da73b
line diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/cplay	Wed Sep 27 09:22:32 2017 +0200
     1.3 @@ -0,0 +1,1655 @@
     1.4 +#!/usr/bin/env python
     1.5 +# -*- python -*-
     1.6 +
     1.7 +__version__ = "cplay 1.49"
     1.8 +
     1.9 +"""
    1.10 +cplay - A curses front-end for various audio players
    1.11 +Copyright (C) 1998-2003 Ulf Betlehem <flu@iki.fi>
    1.12 +
    1.13 +This program is free software; you can redistribute it and/or
    1.14 +modify it under the terms of the GNU General Public License
    1.15 +as published by the Free Software Foundation; either version 2
    1.16 +of the License, or (at your option) any later version.
    1.17 +
    1.18 +This program is distributed in the hope that it will be useful,
    1.19 +but WITHOUT ANY WARRANTY; without even the implied warranty of
    1.20 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    1.21 +GNU General Public License for more details.
    1.22 +
    1.23 +You should have received a copy of the GNU General Public License
    1.24 +along with this program; if not, write to the Free Software
    1.25 +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    1.26 +"""
    1.27 +
    1.28 +# ------------------------------------------
    1.29 +from types import *
    1.30 +
    1.31 +import os
    1.32 +import sys
    1.33 +import time
    1.34 +import getopt
    1.35 +import signal
    1.36 +import string
    1.37 +import select
    1.38 +import re
    1.39 +
    1.40 +try: from ncurses import curses
    1.41 +except ImportError: import curses
    1.42 +
    1.43 +try: import tty
    1.44 +except ImportError: tty = None
    1.45 +
    1.46 +try: import locale; locale.setlocale(locale.LC_ALL, "")
    1.47 +except: pass
    1.48 +
    1.49 +# ------------------------------------------
    1.50 +_locale_domain = "cplay"
    1.51 +_locale_dir = "/usr/local/share/locale"
    1.52 +
    1.53 +try:
    1.54 +    import gettext  # python 2.0
    1.55 +    gettext.install(_locale_domain, _locale_dir)
    1.56 +except ImportError:
    1.57 +    try:
    1.58 +        import fintl
    1.59 +        fintl.bindtextdomain(_locale_domain, _locale_dir)
    1.60 +        fintl.textdomain(_locale_domain)
    1.61 +        _ = fintl.gettext
    1.62 +    except ImportError:
    1.63 +        def _(s): return s
    1.64 +except:
    1.65 +    def _(s): return s
    1.66 +
    1.67 +# ------------------------------------------
    1.68 +XTERM = re.search("rxvt|xterm", os.environ["TERM"])
    1.69 +CONTROL_FIFO = "/var/tmp/cplay_control"
    1.70 +
    1.71 +# ------------------------------------------
    1.72 +def which(program):
    1.73 +    for path in string.split(os.environ["PATH"], ":"):
    1.74 +        if os.path.exists(os.path.join(path, program)):
    1.75 +            return os.path.join(path, program)
    1.76 +
    1.77 +# ------------------------------------------
    1.78 +def cut(s, n, left=0):
    1.79 +    if left: return len(s) > n and "<%s" % s[-n+1:] or s
    1.80 +    else: return len(s) > n and "%s>" % s[:n-1] or s
    1.81 +
    1.82 +# ------------------------------------------
    1.83 +class Stack:
    1.84 +    def __init__(self):
    1.85 +        self.items = ()
    1.86 +
    1.87 +    def push(self, item):
    1.88 +        self.items = (item,) + self.items
    1.89 +
    1.90 +    def pop(self):
    1.91 +        self.items, item = self.items[1:], self.items[0]
    1.92 +        return item
    1.93 +
    1.94 +# ------------------------------------------
    1.95 +class KeymapStack(Stack):
    1.96 +    def process(self, code):
    1.97 +        for keymap in self.items:
    1.98 +            if keymap and keymap.process(code):
    1.99 +                break
   1.100 +
   1.101 +# ------------------------------------------
   1.102 +class Keymap:
   1.103 +    def __init__(self):
   1.104 +        self.methods = [None] * curses.KEY_MAX
   1.105 +
   1.106 +    def bind(self, key, method, args=None):
   1.107 +        if type(key) in (TupleType, ListType):
   1.108 +            for i in key: self.bind(i, method, args)
   1.109 +            return
   1.110 +        if type(key) is StringType:
   1.111 +            key = ord(key)
   1.112 +        self.methods[key] = (method, args)
   1.113 +
   1.114 +    def process(self, key):
   1.115 +        if self.methods[key] is None: return 0
   1.116 +        method, args = self.methods[key]
   1.117 +        if args is None:
   1.118 +            apply(method, (key,))
   1.119 +        else:
   1.120 +            apply(method, args)
   1.121 +        return 1
   1.122 +
   1.123 +# ------------------------------------------
   1.124 +class Window:
   1.125 +    chars = string.letters+string.digits+string.punctuation+string.whitespace
   1.126 +
   1.127 +    t = ['?'] * 256
   1.128 +    for c in chars: t[ord(c)] = c
   1.129 +    translationTable = string.join(t, ""); del t
   1.130 +
   1.131 +    def __init__(self, parent):
   1.132 +        self.parent = parent
   1.133 +        self.children = []
   1.134 +        self.name = None
   1.135 +        self.keymap = None
   1.136 +        self.visible = 1
   1.137 +        self.resize()
   1.138 +        if parent: parent.children.append(self)
   1.139 +
   1.140 +    def insstr(self, s):
   1.141 +        if not s: return
   1.142 +        self.w.addstr(s[:-1])
   1.143 +        self.w.hline(ord(s[-1]), 1)  # insch() work-around
   1.144 +
   1.145 +    def __getattr__(self, name):
   1.146 +        return getattr(self.w, name)
   1.147 +
   1.148 +    def getmaxyx(self):
   1.149 +        y, x = self.w.getmaxyx()
   1.150 +        try: curses.version  # tested with 1.2 and 1.6
   1.151 +        except AttributeError:
   1.152 +            # pyncurses - emulate traditional (silly) behavior
   1.153 +            y, x = y+1, x+1
   1.154 +        return y, x
   1.155 +
   1.156 +    def touchwin(self):
   1.157 +        try: self.w.touchwin()
   1.158 +        except AttributeError: self.touchln(0, self.getmaxyx()[0])
   1.159 +
   1.160 +    def attron(self, attr):
   1.161 +        try: self.w.attron(attr)
   1.162 +        except AttributeError: self.w.attr_on(attr)
   1.163 +
   1.164 +    def attroff(self, attr):
   1.165 +        try: self.w.attroff(attr)
   1.166 +        except AttributeError: self.w.attr_off(attr)
   1.167 +
   1.168 +    def newwin(self):
   1.169 +        return curses.newwin(0, 0, 0, 0)
   1.170 +
   1.171 +    def resize(self):
   1.172 +        self.w = self.newwin()
   1.173 +        self.ypos, self.xpos = self.getbegyx()
   1.174 +        self.rows, self.cols = self.getmaxyx()
   1.175 +        self.keypad(1)
   1.176 +        self.leaveok(0)
   1.177 +        self.scrollok(0)
   1.178 +        for child in self.children:
   1.179 +            child.resize()
   1.180 +
   1.181 +    def update(self):
   1.182 +        self.clear()
   1.183 +        self.refresh()
   1.184 +        for child in self.children:
   1.185 +            child.update()
   1.186 +
   1.187 +# ------------------------------------------
   1.188 +class ProgressWindow(Window):
   1.189 +    def __init__(self, parent):
   1.190 +        Window.__init__(self, parent)
   1.191 +        self.value = 0
   1.192 +
   1.193 +    def newwin(self):
   1.194 +        return curses.newwin(1, self.parent.cols, self.parent.rows-2, 0)
   1.195 +
   1.196 +    def update(self):
   1.197 +        self.move(0, 0)
   1.198 +        self.hline(ord('-'), self.cols)
   1.199 +        if self.value > 0:
   1.200 +            self.move(0, 0)
   1.201 +            x = int(self.value * self.cols)  # 0 to cols-1
   1.202 +            x and self.hline(ord('='), x)
   1.203 +            self.move(0, x)
   1.204 +            self.insstr('|')
   1.205 +        self.touchwin()
   1.206 +        self.refresh()
   1.207 +
   1.208 +    def progress(self, value):
   1.209 +        self.value = min(value, 0.99)
   1.210 +        self.update()
   1.211 +
   1.212 +# ------------------------------------------
   1.213 +class StatusWindow(Window):
   1.214 +    def __init__(self, parent):
   1.215 +        Window.__init__(self, parent)
   1.216 +        self.default_message = ''
   1.217 +        self.current_message = ''
   1.218 +        self.tid = None
   1.219 +
   1.220 +    def newwin(self):
   1.221 +        return curses.newwin(1, self.parent.cols-12, self.parent.rows-1, 0)
   1.222 +
   1.223 +    def update(self):
   1.224 +        msg = string.translate(self.current_message, Window.translationTable)
   1.225 +        self.move(0, 0)
   1.226 +        self.clrtoeol()
   1.227 +        self.insstr(cut(msg, self.cols))
   1.228 +        self.touchwin()
   1.229 +        self.refresh()
   1.230 +
   1.231 +    def status(self, message, duration = 0):
   1.232 +        self.current_message = str(message)
   1.233 +        if self.tid: app.timeout.remove(self.tid)
   1.234 +        if duration: self.tid = app.timeout.add(duration, self.timeout)
   1.235 +        else: self.tid = None
   1.236 +        self.update()
   1.237 +
   1.238 +    def timeout(self):
   1.239 +        self.tid = None
   1.240 +        self.restore_default_status()
   1.241 +
   1.242 +    def set_default_status(self, message):
   1.243 +        if self.current_message == self.default_message: self.status(message)
   1.244 +        self.default_message = message
   1.245 +        XTERM and sys.stderr.write("\033]0;%s\a" % (message or "cplay"))
   1.246 +
   1.247 +    def restore_default_status(self):
   1.248 +        self.status(self.default_message)
   1.249 +
   1.250 +# ------------------------------------------
   1.251 +class CounterWindow(Window):
   1.252 +    def __init__(self, parent):
   1.253 +        Window.__init__(self, parent)
   1.254 +        self.values = [0, 0]
   1.255 +        self.mode = 1
   1.256 +
   1.257 +    def newwin(self):
   1.258 +        return curses.newwin(1, 11, self.parent.rows-1, self.parent.cols-11)
   1.259 +
   1.260 +    def update(self):
   1.261 +        h, s = divmod(self.values[self.mode], 3600)
   1.262 +        m, s = divmod(s, 60)
   1.263 +        self.move(0, 0)
   1.264 +        self.attron(curses.A_BOLD)
   1.265 +        self.insstr("%02dh %02dm %02ds" % (h, m, s))
   1.266 +        self.attroff(curses.A_BOLD)
   1.267 +        self.touchwin()
   1.268 +        self.refresh()
   1.269 +
   1.270 +    def counter(self, values):
   1.271 +        self.values = values
   1.272 +        self.update()
   1.273 +
   1.274 +    def toggle_mode(self):
   1.275 +        self.mode = not self.mode
   1.276 +        tmp = [_("elapsed"), _("remaining")][self.mode]
   1.277 +        app.status(_("Counting %s time") % tmp, 1)
   1.278 +        self.update()
   1.279 +
   1.280 +# ------------------------------------------
   1.281 +class RootWindow(Window):
   1.282 +    def __init__(self, parent):
   1.283 +        Window.__init__(self, parent)
   1.284 +        keymap = Keymap()
   1.285 +        app.keymapstack.push(keymap)
   1.286 +        self.win_progress = ProgressWindow(self)
   1.287 +        self.win_status = StatusWindow(self)
   1.288 +        self.win_counter = CounterWindow(self)
   1.289 +        self.win_tab = TabWindow(self)
   1.290 +        keymap.bind(12, self.update, ()) # C-l
   1.291 +        keymap.bind([curses.KEY_LEFT, 2], app.seek, (-1, 1)) # C-b
   1.292 +        keymap.bind([curses.KEY_RIGHT, 6], app.seek, (1, 1)) # C-f
   1.293 +        keymap.bind([1, '^'], app.seek, (0, 0)) # C-a
   1.294 +        keymap.bind([5, '$'], app.seek, (-1, 0)) # C-e
   1.295 +        keymap.bind(range(48,58), app.key_volume) # 0123456789
   1.296 +        keymap.bind(['+', '='], app.inc_volume, ())
   1.297 +        keymap.bind('-', app.dec_volume, ())
   1.298 +        keymap.bind('n', app.next_song, ())
   1.299 +        keymap.bind('p', app.prev_song, ())
   1.300 +        keymap.bind('z', app.toggle_pause, ())
   1.301 +        keymap.bind('x', app.toggle_stop, ())
   1.302 +        keymap.bind('c', self.win_counter.toggle_mode, ())
   1.303 +        keymap.bind('Q', app.quit, ())
   1.304 +        keymap.bind('q', self.command_quit, ())
   1.305 +        keymap.bind('v', app.mixer, ("toggle",))
   1.306 +
   1.307 +    def command_quit(self):
   1.308 +        app.do_input_hook = self.do_quit
   1.309 +        app.start_input(_("Quit? (y/N)"))
   1.310 +        
   1.311 +    def do_quit(self, ch):
   1.312 +        if chr(ch) == 'y': app.quit()
   1.313 +        app.stop_input()
   1.314 +
   1.315 +# ------------------------------------------
   1.316 +class TabWindow(Window):
   1.317 +    def __init__(self, parent):
   1.318 +        Window.__init__(self, parent)
   1.319 +        self.active_child = 0
   1.320 +
   1.321 +        self.win_filelist = self.add(FilelistWindow)
   1.322 +        self.win_playlist = self.add(PlaylistWindow)
   1.323 +        self.win_help     = self.add(HelpWindow)
   1.324 +
   1.325 +        keymap = Keymap()
   1.326 +        keymap.bind('\t', self.change_window, ()) # tab
   1.327 +        keymap.bind('h', self.help, ())
   1.328 +        app.keymapstack.push(keymap)
   1.329 +        app.keymapstack.push(self.children[self.active_child].keymap)
   1.330 +
   1.331 +    def newwin(self):
   1.332 +        return curses.newwin(self.parent.rows-2, self.parent.cols, 0, 0)
   1.333 +
   1.334 +    def update(self):
   1.335 +        self.update_title()
   1.336 +        self.move(1, 0)
   1.337 +        self.hline(ord('-'), self.cols)
   1.338 +        self.move(2, 0)
   1.339 +        self.clrtobot()
   1.340 +        self.refresh()
   1.341 +        child = self.children[self.active_child]
   1.342 +        child.visible = 1
   1.343 +        child.update()
   1.344 +
   1.345 +    def update_title(self, refresh = 1):
   1.346 +        child = self.children[self.active_child]
   1.347 +        self.move(0, 0)
   1.348 +        self.clrtoeol()
   1.349 +        self.attron(curses.A_BOLD)
   1.350 +        self.insstr(child.get_title())
   1.351 +        self.attroff(curses.A_BOLD)
   1.352 +        if refresh: self.refresh()
   1.353 +
   1.354 +    def add(self, Class):
   1.355 +        win = Class(self)
   1.356 +        win.visible = 0
   1.357 +        return win
   1.358 +
   1.359 +    def change_window(self, window = None):
   1.360 +        app.keymapstack.pop()
   1.361 +        self.children[self.active_child].visible = 0
   1.362 +        if window:
   1.363 +            self.active_child = self.children.index(window)
   1.364 +        else:
   1.365 +            # toggle windows 0 and 1
   1.366 +            self.active_child = not self.active_child
   1.367 +        app.keymapstack.push(self.children[self.active_child].keymap)
   1.368 +        self.update()
   1.369 +
   1.370 +    def help(self):
   1.371 +        if self.children[self.active_child] == self.win_help:
   1.372 +            self.change_window(self.win_last)
   1.373 +        else:
   1.374 +            self.win_last = self.children[self.active_child]
   1.375 +            self.change_window(self.win_help)
   1.376 +            app.status(__version__, 2)
   1.377 +
   1.378 +# ------------------------------------------
   1.379 +class ListWindow(Window):
   1.380 +    def __init__(self, parent):
   1.381 +        Window.__init__(self, parent)
   1.382 +        self.buffer = []
   1.383 +        self.bufptr = self.scrptr = 0
   1.384 +        self.search_direction = 0
   1.385 +        self.last_search = ""
   1.386 +        self.hoffset = 0
   1.387 +        self.keymap = Keymap()
   1.388 +        self.keymap.bind(['k', curses.KEY_UP, 16], self.cursor_move, (-1,))
   1.389 +        self.keymap.bind(['j', curses.KEY_DOWN, 14], self.cursor_move, (1,))
   1.390 +        self.keymap.bind(['K', curses.KEY_PPAGE], self.cursor_ppage, ())
   1.391 +        self.keymap.bind(['J', curses.KEY_NPAGE], self.cursor_npage, ())
   1.392 +        self.keymap.bind(['g', curses.KEY_HOME], self.cursor_home, ())
   1.393 +        self.keymap.bind(['G', curses.KEY_END], self.cursor_end, ())
   1.394 +        self.keymap.bind(['?', 18], self.start_search,
   1.395 +                         (_("backward-isearch"), -1))
   1.396 +        self.keymap.bind(['/', 19], self.start_search,
   1.397 +                         (_("forward-isearch"), 1))
   1.398 +        self.keymap.bind(['>'], self.hscroll, (8,))
   1.399 +        self.keymap.bind(['<'], self.hscroll, (-8,))
   1.400 +
   1.401 +    def newwin(self):
   1.402 +        return curses.newwin(self.parent.rows-2, self.parent.cols,
   1.403 +                             self.parent.ypos+2, self.parent.xpos)
   1.404 +
   1.405 +    def update(self, force = 1):
   1.406 +        self.bufptr = max(0, min(self.bufptr, len(self.buffer) - 1))
   1.407 +        scrptr = (self.bufptr / self.rows) * self.rows
   1.408 +        if force or self.scrptr != scrptr:
   1.409 +            self.scrptr = scrptr
   1.410 +            self.move(0, 0)
   1.411 +            self.clrtobot()
   1.412 +            i = 0
   1.413 +            for entry in self.buffer[self.scrptr:]:
   1.414 +                self.move(i, 0)
   1.415 +                i = i + 1
   1.416 +                self.putstr(entry)
   1.417 +                if self.getyx()[0] == self.rows - 1: break
   1.418 +            if self.visible:
   1.419 +                self.refresh()
   1.420 +                self.parent.update_title()
   1.421 +        self.update_line(curses.A_REVERSE)
   1.422 +
   1.423 +    def update_line(self, attr = None, refresh = 1):
   1.424 +        if not self.buffer: return
   1.425 +        ypos = self.bufptr - self.scrptr
   1.426 +        if attr: self.attron(attr)
   1.427 +        self.move(ypos, 0)
   1.428 +        self.hline(ord(' '), self.cols)
   1.429 +        self.putstr(self.current())
   1.430 +        if attr: self.attroff(attr)
   1.431 +        if self.visible and refresh: self.refresh()
   1.432 +
   1.433 +    def get_title(self, data=""):
   1.434 +        pos = "%s-%s/%s" % (self.scrptr+min(1, len(self.buffer)),
   1.435 +                            min(self.scrptr+self.rows, len(self.buffer)),
   1.436 +                            len(self.buffer))
   1.437 +        width = self.cols-len(pos)-2
   1.438 +        data = cut(data, width-len(self.name), 1)
   1.439 +        return "%-*s  %s" % (width, cut(self.name+data, width), pos)
   1.440 +
   1.441 +    def putstr(self, entry, *pos):
   1.442 +        s = string.translate(str(entry), Window.translationTable)
   1.443 +        pos and apply(self.move, pos)
   1.444 +        if self.hoffset: s = "<%s" % s[self.hoffset+1:]
   1.445 +        self.insstr(cut(s, self.cols))
   1.446 +
   1.447 +    def current(self):
   1.448 +        if self.bufptr >= len(self.buffer): self.bufptr = len(self.buffer) - 1
   1.449 +        return self.buffer[self.bufptr]
   1.450 +
   1.451 +    def cursor_move(self, ydiff):
   1.452 +        if app.input_mode: app.cancel_input()
   1.453 +        if not self.buffer: return
   1.454 +        self.update_line(refresh = 0)
   1.455 +        self.bufptr = (self.bufptr + ydiff) % len(self.buffer)
   1.456 +        self.update(force = 0)
   1.457 +
   1.458 +    def cursor_ppage(self):
   1.459 +        tmp = self.bufptr % self.rows
   1.460 +        if tmp == self.bufptr:
   1.461 +            self.cursor_move(-(tmp+(len(self.buffer) % self.rows) or self.rows))
   1.462 +        else:
   1.463 +            self.cursor_move(-(tmp+self.rows))
   1.464 +
   1.465 +    def cursor_npage(self):
   1.466 +        tmp = self.rows - self.bufptr % self.rows
   1.467 +        if self.bufptr + tmp > len(self.buffer):
   1.468 +            self.cursor_move(len(self.buffer) - self.bufptr)
   1.469 +        else:
   1.470 +            self.cursor_move(tmp)
   1.471 +
   1.472 +    def cursor_home(self): self.cursor_move(-self.bufptr)
   1.473 +
   1.474 +    def cursor_end(self): self.cursor_move(-self.bufptr - 1)
   1.475 +
   1.476 +    def start_search(self, type, direction):
   1.477 +        self.search_direction = direction
   1.478 +        self.not_found = 0
   1.479 +        if app.input_mode:
   1.480 +            app.input_prompt = "%s: " % type
   1.481 +            self.do_search(advance = direction)
   1.482 +        else:
   1.483 +            app.do_input_hook = self.do_search
   1.484 +            app.stop_input_hook = self.stop_search
   1.485 +            app.start_input(type)
   1.486 +
   1.487 +    def stop_search(self):
   1.488 +        self.last_search = app.input_string
   1.489 +        app.status(_("ok"), 1)
   1.490 +
   1.491 +    def do_search(self, ch = None, advance = 0):
   1.492 +        if ch in [8, 127]: app.input_string = app.input_string[:-1]
   1.493 +        elif ch: app.input_string = "%s%c" % (app.input_string, ch)
   1.494 +        else: app.input_string = app.input_string or self.last_search
   1.495 +        index = self.bufptr + advance
   1.496 +        while 1:
   1.497 +            if not 0 <= index < len(self.buffer):
   1.498 +                app.status(_("Not found: %s ") % app.input_string)
   1.499 +                self.not_found = 1
   1.500 +                break
   1.501 +            line = string.lower(str(self.buffer[index]))
   1.502 +            if string.find(line, string.lower(app.input_string)) != -1:
   1.503 +                app.show_input()
   1.504 +                self.update_line(refresh = 0)
   1.505 +                self.bufptr = index
   1.506 +                self.update(force = 0)
   1.507 +                self.not_found = 0
   1.508 +                break
   1.509 +            if self.not_found:
   1.510 +                app.status(_("Not found: %s ") % app.input_string)
   1.511 +                break
   1.512 +            index = index + self.search_direction
   1.513 +
   1.514 +    def hscroll(self, value):
   1.515 +        self.hoffset = max(0, self.hoffset + value)
   1.516 +        self.update()
   1.517 +
   1.518 +# ------------------------------------------
   1.519 +class HelpWindow(ListWindow):
   1.520 +    def __init__(self, parent):
   1.521 +        ListWindow.__init__(self, parent)
   1.522 +        self.name = _("Help")
   1.523 +        self.keymap.bind('q', self.parent.help, ())
   1.524 +        self.buffer = string.split(_("""\
   1.525 +  Global                               t, T  : tag current/regex
   1.526 +  ------                               u, U  : untag current/regex
   1.527 +  Up, Down, k, j, C-p, C-n,            Sp, i : invert current/all
   1.528 +  PgUp, PgDn, K, J,                    !     : shell ($@ = tagged or current)
   1.529 +  Home, End, g, G : movement
   1.530 +  Enter           : chdir or play      Filelist
   1.531 +  Tab             : filelist/playlist  --------
   1.532 +  n, p            : next/prev track    a     : add (tagged) to playlist
   1.533 +  z, x            : toggle pause/stop  s     : recursive search
   1.534 +                                       BS, o : goto parent/specified dir
   1.535 +  Left, Right,                         m, '  : set/get bookmark
   1.536 +  C-f, C-b    : seek forward/backward  
   1.537 +  C-a, C-e    : restart/end track      Playlist
   1.538 +  C-s, C-r, / : isearch                --------
   1.539 +  C-g, Esc    : cancel                 d, D  : delete (tagged) tracks/playlist
   1.540 +  1..9, +, -  : volume control         m, M  : move tagged tracks after/before
   1.541 +  c, v        : counter/volume mode    r, R  : toggle repeat/Random mode
   1.542 +  <, >        : horizontal scrolling   s, S  : shuffle/Sort playlist
   1.543 +  C-l, l      : refresh, list mode     w, @  : write playlist, jump to active
   1.544 +  h, q, Q     : help, quit?, Quit!     X     : stop playlist after each track
   1.545 +"""), "\n")
   1.546 +
   1.547 +# ------------------------------------------
   1.548 +class ListEntry:
   1.549 +    def __init__(self, pathname, dir=0):
   1.550 +        self.filename = os.path.basename(pathname)
   1.551 +        self.pathname = pathname
   1.552 +        self.slash = dir and "/" or ""
   1.553 +        self.tagged = 0
   1.554 +
   1.555 +    def set_tagged(self, value):
   1.556 +        self.tagged = value
   1.557 +
   1.558 +    def is_tagged(self):
   1.559 +        return self.tagged == 1
   1.560 +
   1.561 +    def __str__(self):
   1.562 +        mark = self.is_tagged() and "#" or " "
   1.563 +        return "%s %s%s" % (mark, self.vp(), self.slash)
   1.564 +
   1.565 +    def vp(self):
   1.566 +        return self.vps[0][1](self)
   1.567 +
   1.568 +    def vp_filename(self):
   1.569 +        return self.filename or self.pathname
   1.570 +
   1.571 +    def vp_pathname(self):
   1.572 +        return self.pathname
   1.573 +
   1.574 +    vps = [[_("filename"), vp_filename],
   1.575 +           [_("pathname"), vp_pathname]]
   1.576 +
   1.577 +# ------------------------------------------
   1.578 +class PlaylistEntry(ListEntry):
   1.579 +    def __init__(self, pathname):
   1.580 +        ListEntry.__init__(self, pathname)
   1.581 +        self.metadata = None
   1.582 +        self.active = 0
   1.583 +
   1.584 +    def set_active(self, value):
   1.585 +        self.active = value
   1.586 +
   1.587 +    def is_active(self):
   1.588 +        return self.active == 1
   1.589 +
   1.590 +    def vp_metadata(self):
   1.591 +        return self.metadata or self.read_metadata()
   1.592 +
   1.593 +    def read_metadata(self):
   1.594 +        self.metadata = get_tag(self.pathname)
   1.595 +        return self.metadata
   1.596 +
   1.597 +    vps = ListEntry.vps[:]
   1.598 +
   1.599 +# ------------------------------------------
   1.600 +class TagListWindow(ListWindow):
   1.601 +    def __init__(self, parent):
   1.602 +        ListWindow.__init__(self, parent)
   1.603 +        self.keymap.bind(' ', self.command_tag_untag, ())
   1.604 +        self.keymap.bind('i', self.command_invert_tags, ())
   1.605 +        self.keymap.bind('t', self.command_tag, (1,))
   1.606 +        self.keymap.bind('u', self.command_tag, (0,))
   1.607 +        self.keymap.bind('T', self.command_tag_regexp, (1,))
   1.608 +        self.keymap.bind('U', self.command_tag_regexp, (0,))
   1.609 +        self.keymap.bind('l', self.command_change_viewpoint, ())
   1.610 +
   1.611 +    def command_change_viewpoint(self, klass=ListEntry):
   1.612 +        klass.vps.append(klass.vps.pop(0))
   1.613 +        app.status(_("Listing %s") % klass.vps[0][0], 1)
   1.614 +        app.player.update_status()
   1.615 +        self.update()
   1.616 +
   1.617 +    def command_invert_tags(self):
   1.618 +        for i in self.buffer:
   1.619 +            i.set_tagged(not i.is_tagged())
   1.620 +        self.update()
   1.621 +
   1.622 +    def command_tag_untag(self):
   1.623 +        if not self.buffer: return
   1.624 +        tmp = self.buffer[self.bufptr]
   1.625 +        tmp.set_tagged(not tmp.is_tagged())
   1.626 +        self.cursor_move(1)
   1.627 +
   1.628 +    def command_tag(self, value):
   1.629 +        if not self.buffer: return
   1.630 +        self.buffer[self.bufptr].set_tagged(value)
   1.631 +        self.cursor_move(1)
   1.632 +
   1.633 +    def command_tag_regexp(self, value):
   1.634 +        self.tag_value = value
   1.635 +        app.stop_input_hook = self.stop_tag_regexp
   1.636 +        app.start_input(value and _("Tag regexp") or _("Untag regexp"))
   1.637 +
   1.638 +    def stop_tag_regexp(self):
   1.639 +        try:
   1.640 +            r = re.compile(app.input_string, re.I)
   1.641 +            for entry in self.buffer:
   1.642 +                if r.search(str(entry)):
   1.643 +                    entry.set_tagged(self.tag_value)
   1.644 +            self.update()
   1.645 +            app.status(_("ok"), 1)
   1.646 +        except re.error, e:
   1.647 +            app.status(e, 2)
   1.648 +
   1.649 +    def get_tagged(self):
   1.650 +        return filter(lambda x: x.is_tagged(), self.buffer)
   1.651 +
   1.652 +    def not_tagged(self, l):
   1.653 +        return filter(lambda x: not x.is_tagged(), l)
   1.654 +
   1.655 +# ------------------------------------------
   1.656 +class FilelistWindow(TagListWindow):
   1.657 +    def __init__(self, parent):
   1.658 +        TagListWindow.__init__(self, parent)
   1.659 +        self.oldposition = {}
   1.660 +        try: self.chdir(os.getcwd())
   1.661 +        except OSError: self.chdir(os.environ['HOME'])
   1.662 +        self.startdir = self.cwd
   1.663 +        self.mtime_when = 0
   1.664 +        self.mtime = None
   1.665 +        self.keymap.bind(['\n', curses.KEY_ENTER],
   1.666 +                         self.command_chdir_or_play, ())
   1.667 +        self.keymap.bind(['.', curses.KEY_BACKSPACE],
   1.668 +                         self.command_chparentdir, ())
   1.669 +        self.keymap.bind('a', self.command_add_recursively, ())
   1.670 +        self.keymap.bind('o', self.command_goto, ())
   1.671 +        self.keymap.bind('s', self.command_search_recursively, ())
   1.672 +        self.keymap.bind('m', self.command_set_bookmark, ())
   1.673 +        self.keymap.bind("'", self.command_get_bookmark, ())
   1.674 +        self.keymap.bind('!', self.command_shell, ())
   1.675 +        self.bookmarks = { 39: [self.cwd, 0] }
   1.676 +
   1.677 +    def command_shell(self):
   1.678 +        if app.restricted: return
   1.679 +        app.stop_input_hook = self.stop_shell
   1.680 +        app.complete_input_hook = self.complete_shell
   1.681 +        app.start_input(_("shell$ "), colon=0)
   1.682 +
   1.683 +    def stop_shell(self):
   1.684 +        s = app.input_string
   1.685 +        curses.endwin()
   1.686 +        sys.stderr.write("\n")
   1.687 +        argv = map(lambda x: x.pathname, self.get_tagged() or [self.current()])
   1.688 +        argv = ["/bin/sh", "-c", s, "--"] + argv
   1.689 +        pid = os.fork()
   1.690 +        if pid == 0:
   1.691 +            try: os.execv(argv[0], argv)
   1.692 +            except: os._exit(1)
   1.693 +        pid, r = os.waitpid(pid, 0)
   1.694 +        sys.stderr.write("\nshell returned %s, press return!\n" % r)
   1.695 +        sys.stdin.readline()
   1.696 +        app.win_root.update()
   1.697 +        app.restore_default_status()
   1.698 +        app.cursor(0)
   1.699 +
   1.700 +    def complete_shell(self, line):
   1.701 +        return self.complete_generic(line, quote=1)
   1.702 +
   1.703 +    def complete_generic(self, line, quote=0):
   1.704 +        import glob
   1.705 +        if quote:
   1.706 +            s = re.sub('.*[^\\\\][ \'"()\[\]{}$`]', '', line)
   1.707 +            s, part = re.sub('\\\\', '', s), line[:len(line)-len(s)]
   1.708 +        else:
   1.709 +            s, part = line, ""
   1.710 +        results = glob.glob(os.path.expanduser(s)+"*")
   1.711 +        if len(results) == 0:
   1.712 +            return line
   1.713 +        if len(results) == 1:
   1.714 +            lm = results[0]
   1.715 +            lm = lm + (os.path.isdir(lm) and "/" or "")
   1.716 +        else:
   1.717 +            lm = results[0]
   1.718 +            for result in results:
   1.719 +                for i in range(min(len(result), len(lm))):
   1.720 +                   if result[i] != lm[i]:
   1.721 +                        lm = lm[:i]
   1.722 +                        break
   1.723 +        if quote: lm = re.sub('([ \'"()\[\]{}$`])', '\\\\\\1', lm)
   1.724 +        return part + lm
   1.725 +
   1.726 +    def command_get_bookmark(self):
   1.727 +        app.do_input_hook = self.do_get_bookmark
   1.728 +        app.start_input(_("bookmark"))
   1.729 +
   1.730 +    def do_get_bookmark(self, ch):
   1.731 +        app.input_string = ch
   1.732 +        bookmark = self.bookmarks.get(ch)
   1.733 +        if bookmark:
   1.734 +            self.bookmarks[39] = [self.cwd, self.bufptr]
   1.735 +            dir, pos = bookmark
   1.736 +            self.chdir(dir)
   1.737 +            self.listdir()
   1.738 +            self.bufptr = pos
   1.739 +            self.update()
   1.740 +            app.status(_("ok"), 1)
   1.741 +        else:
   1.742 +            app.status(_("Not found!"), 1)
   1.743 +        app.stop_input()
   1.744 +
   1.745 +    def command_set_bookmark(self):
   1.746 +        app.do_input_hook = self.do_set_bookmark
   1.747 +        app.start_input(_("set bookmark"))
   1.748 +        
   1.749 +    def do_set_bookmark(self, ch):
   1.750 +        app.input_string = ch
   1.751 +        self.bookmarks[ch] = [self.cwd, self.bufptr]
   1.752 +        ch and app.status(_("ok"), 1) or app.stop_input()
   1.753 +
   1.754 +    def command_search_recursively(self):
   1.755 +        app.stop_input_hook = self.stop_search_recursively
   1.756 +        app.start_input(_("search"))
   1.757 +
   1.758 +    def stop_search_recursively(self):
   1.759 +        try: re_tmp = re.compile(app.input_string, re.I)
   1.760 +        except re.error, e:
   1.761 +            app.status(e, 2)
   1.762 +            return
   1.763 +        app.status(_("Searching..."))
   1.764 +        results = []
   1.765 +        for entry in self.buffer:
   1.766 +            if entry.filename == "..":
   1.767 +                continue
   1.768 +            if re_tmp.search(entry.filename):
   1.769 +                results.append(entry)
   1.770 +            elif os.path.isdir(entry.pathname):
   1.771 +                try: self.search_recursively(re_tmp, entry.pathname, results)
   1.772 +                except: pass
   1.773 +        if not self.search_mode:
   1.774 +            self.chdir(os.path.join(self.cwd,_("search results")))
   1.775 +            self.search_mode = 1
   1.776 +        self.buffer = results
   1.777 +        self.bufptr = 0
   1.778 +        self.parent.update_title()
   1.779 +        self.update()
   1.780 +        app.restore_default_status()
   1.781 +
   1.782 +    def search_recursively(self, re_tmp, dir, results):
   1.783 +        for filename in os.listdir(dir):
   1.784 +            pathname = os.path.join(dir, filename)
   1.785 +            if re_tmp.search(filename):
   1.786 +                if os.path.isdir(pathname):
   1.787 +                    results.append(ListEntry(pathname, 1))
   1.788 +                elif VALID_PLAYLIST(filename) or VALID_SONG(filename):
   1.789 +                    results.append(ListEntry(pathname))
   1.790 +            elif os.path.isdir(pathname):
   1.791 +                self.search_recursively(re_tmp, pathname, results)
   1.792 +
   1.793 +    def get_title(self):
   1.794 +        self.name = _("Filelist: ")
   1.795 +        return ListWindow.get_title(self, re.sub("/?$", "/", self.cwd))
   1.796 +
   1.797 +    def listdir_maybe(self, now=0):
   1.798 +        if now < self.mtime_when+2: return
   1.799 +        self.mtime_when = now
   1.800 +        self.oldposition[self.cwd] = self.bufptr
   1.801 +        try: self.mtime == os.stat(self.cwd)[8] or self.listdir(quiet=1)
   1.802 +        except os.error: pass
   1.803 +
   1.804 +    def listdir(self, quiet=0, prevdir=None):
   1.805 +        quiet or app.status(_("Reading directory..."))
   1.806 +        self.search_mode = 0
   1.807 +        dirs = []
   1.808 +        files = []
   1.809 +        try:
   1.810 +            self.mtime = os.stat(self.cwd)[8]
   1.811 +            self.mtime_when = time.time()
   1.812 +            filenames = os.listdir(self.cwd)
   1.813 +            filenames.sort()
   1.814 +            for filename in filenames:
   1.815 +                if filename[0] == ".": continue
   1.816 +                pathname = os.path.join(self.cwd, filename)
   1.817 +                if os.path.isdir(pathname): dirs.append(pathname)
   1.818 +                elif VALID_SONG(filename): files.append(pathname)
   1.819 +                elif VALID_PLAYLIST(filename): files.append(pathname)
   1.820 +        except os.error: pass
   1.821 +        dots = ListEntry(os.path.join(self.cwd, ".."), 1)
   1.822 +        self.buffer = [[dots], []][self.cwd == "/"]
   1.823 +        for i in dirs: self.buffer.append(ListEntry(i, 1))
   1.824 +        for i in files: self.buffer.append(ListEntry(i))
   1.825 +        if prevdir:
   1.826 +            for self.bufptr in range(len(self.buffer)):
   1.827 +                if self.buffer[self.bufptr].filename == prevdir: break
   1.828 +            else: self.bufptr = 0
   1.829 +        elif self.oldposition.has_key(self.cwd):
   1.830 +            self.bufptr = self.oldposition[self.cwd]
   1.831 +        else: self.bufptr = 0
   1.832 +        self.parent.update_title()
   1.833 +        self.update()
   1.834 +        quiet or app.restore_default_status()
   1.835 +
   1.836 +    def chdir(self, dir):
   1.837 +        if hasattr(self, "cwd"): self.oldposition[self.cwd] = self.bufptr
   1.838 +        self.cwd = os.path.normpath(dir)
   1.839 +        try: os.chdir(self.cwd)
   1.840 +        except: pass
   1.841 +
   1.842 +    def command_chdir_or_play(self):
   1.843 +        if not self.buffer: return
   1.844 +        if self.current().filename == "..":
   1.845 +            self.command_chparentdir()
   1.846 +        elif os.path.isdir(self.current().pathname):
   1.847 +            self.chdir(self.current().pathname)
   1.848 +            self.listdir()
   1.849 +        elif VALID_SONG(self.current().filename):
   1.850 +            app.play(self.current())
   1.851 +
   1.852 +    def command_chparentdir(self):
   1.853 +        if app.restricted and self.cwd == self.startdir: return
   1.854 +        dir = os.path.basename(self.cwd)
   1.855 +        self.chdir(os.path.dirname(self.cwd))
   1.856 +        self.listdir(prevdir=dir)
   1.857 +
   1.858 +    def command_goto(self):
   1.859 +        if app.restricted: return
   1.860 +        app.stop_input_hook = self.stop_goto
   1.861 +        app.complete_input_hook = self.complete_generic
   1.862 +        app.start_input(_("goto"))
   1.863 +
   1.864 +    def stop_goto(self):
   1.865 +        dir = os.path.expanduser(app.input_string)
   1.866 +        if dir[0] != '/': dir = os.path.join(self.cwd, dir)
   1.867 +        if not os.path.isdir(dir):
   1.868 +            app.status(_("Not a directory!"), 1)
   1.869 +            return
   1.870 +        self.chdir(dir)
   1.871 +        self.listdir()
   1.872 +
   1.873 +    def command_add_recursively(self):
   1.874 +        l = self.get_tagged()
   1.875 +        if not l:
   1.876 +            app.win_playlist.add(self.current().pathname)
   1.877 +            self.cursor_move(1)
   1.878 +            return
   1.879 +        app.status(_("Adding tagged files"), 1)
   1.880 +        for entry in l:
   1.881 +            app.win_playlist.add(entry.pathname, quiet=1)
   1.882 +            entry.set_tagged(0)
   1.883 +        self.update()
   1.884 +
   1.885 +# ------------------------------------------
   1.886 +class PlaylistWindow(TagListWindow):
   1.887 +    def __init__(self, parent):
   1.888 +        TagListWindow.__init__(self, parent)
   1.889 +        self.pathname = None
   1.890 +        self.repeat = 0
   1.891 +        self.random = 0
   1.892 +        self.random_prev = []
   1.893 +        self.random_next = []
   1.894 +        self.random_left = []
   1.895 +        self.stop = 0
   1.896 +        self.keymap.bind(['\n', curses.KEY_ENTER],
   1.897 +                         self.command_play, ())
   1.898 +        self.keymap.bind('d', self.command_delete, ())
   1.899 +        self.keymap.bind('D', self.command_delete_all, ())
   1.900 +        self.keymap.bind('m', self.command_move, (1,))
   1.901 +        self.keymap.bind('M', self.command_move, (0,))
   1.902 +        self.keymap.bind('s', self.command_shuffle, ())
   1.903 +        self.keymap.bind('S', self.command_sort, ())
   1.904 +        self.keymap.bind('r', self.command_toggle_repeat, ())
   1.905 +        self.keymap.bind('R', self.command_toggle_random, ())
   1.906 +        self.keymap.bind('X', self.command_toggle_stop, ())
   1.907 +        self.keymap.bind('w', self.command_save_playlist, ())
   1.908 +        self.keymap.bind('@', self.command_jump_to_active, ())
   1.909 +
   1.910 +    def command_change_viewpoint(self, klass=PlaylistEntry):
   1.911 +        if not globals().get("ID3"):
   1.912 +            try:
   1.913 +                global ID3, ogg, codecs
   1.914 +                import ID3, ogg.vorbis, codecs
   1.915 +                klass.vps.append([_("metadata"), klass.vp_metadata])
   1.916 +            except ImportError: pass
   1.917 +        TagListWindow.command_change_viewpoint(self, klass)
   1.918 +
   1.919 +    def get_title(self):
   1.920 +        space_out = lambda value, s: value and s or " "*len(s)
   1.921 +        self.name = _("Playlist %s %s %s") % (
   1.922 +            space_out(self.repeat, _("[repeat]")),
   1.923 +            space_out(self.random, _("[random]")),
   1.924 +            space_out(self.stop, _("[stop]")))
   1.925 +        return ListWindow.get_title(self)
   1.926 +
   1.927 +    def append(self, item):
   1.928 +        self.buffer.append(item)
   1.929 +        if self.random: self.random_left.append(item)
   1.930 +
   1.931 +    def add_dir(self, dir):
   1.932 +        filenames = os.listdir(dir)
   1.933 +        filenames.sort()
   1.934 +        subdirs = []
   1.935 +        for filename in filenames:
   1.936 +            pathname = os.path.join(dir, filename)
   1.937 +            if VALID_SONG(filename):
   1.938 +                self.append(PlaylistEntry(pathname))
   1.939 +            if os.path.isdir(pathname):
   1.940 +                subdirs.append(pathname)
   1.941 +        map(self.add_dir, subdirs)
   1.942 +
   1.943 +    def add_m3u(self, line):
   1.944 +        if re.match("^(#.*)?$", line): return
   1.945 +        if re.match("^(/|http://)", line):
   1.946 +            self.append(PlaylistEntry(self.fix_url(line)))
   1.947 +        else:
   1.948 +            dirname = os.path.dirname(self.pathname)
   1.949 +            self.append(PlaylistEntry(os.path.join(dirname, line)))
   1.950 +
   1.951 +    def add_pls(self, line):
   1.952 +        # todo - support title & length
   1.953 +        m = re.match("File(\d+)=(.*)", line)
   1.954 +        if m: self.append(PlaylistEntry(self.fix_url(m.group(2))))
   1.955 +
   1.956 +    def add_playlist(self, pathname):
   1.957 +        self.pathname = pathname
   1.958 +        if re.search("\.m3u$", pathname, re.I): f = self.add_m3u
   1.959 +        if re.search("\.pls$", pathname, re.I): f = self.add_pls
   1.960 +        file = open(pathname)
   1.961 +        map(f, map(string.strip, file.readlines()))
   1.962 +        file.close()
   1.963 +    
   1.964 +    def add(self, pathname, quiet=0):
   1.965 +        try:
   1.966 +            if os.path.isdir(pathname):
   1.967 +                app.status(_("Working..."))
   1.968 +                self.add_dir(pathname)
   1.969 +            elif VALID_PLAYLIST(pathname):
   1.970 +                self.add_playlist(pathname)
   1.971 +            else:
   1.972 +                pathname = self.fix_url(pathname)
   1.973 +                self.append(PlaylistEntry(pathname))
   1.974 +            # todo - refactor
   1.975 +            filename = os.path.basename(pathname) or pathname
   1.976 +            quiet or app.status(_("Added: %s") % filename, 1)
   1.977 +        except Exception, e:
   1.978 +            app.status(e, 2)
   1.979 +
   1.980 +    def fix_url(self, url):
   1.981 +        return re.sub("(http://[^/]+)/?(.*)", "\\1/\\2", url)
   1.982 +
   1.983 +    def putstr(self, entry, *pos):
   1.984 +        if entry.is_active(): self.attron(curses.A_BOLD)
   1.985 +        apply(ListWindow.putstr, (self, entry) + pos)
   1.986 +        if entry.is_active(): self.attroff(curses.A_BOLD)
   1.987 +
   1.988 +    def change_active_entry(self, direction):
   1.989 +        if not self.buffer: return
   1.990 +        old = self.get_active_entry()
   1.991 +        new = None
   1.992 +        if self.random:
   1.993 +            if direction > 0:
   1.994 +                if self.random_next: new = self.random_next.pop()
   1.995 +                elif self.random_left: pass
   1.996 +                elif self.repeat: self.random_left = self.buffer[:]
   1.997 +                else: return
   1.998 +                if not new:
   1.999 +                    import random
  1.1000 +                    new = random.choice(self.random_left)
  1.1001 +                    self.random_left.remove(new)
  1.1002 +                try: self.random_prev.remove(new)
  1.1003 +                except ValueError: pass
  1.1004 +                self.random_prev.append(new)
  1.1005 +            else:
  1.1006 +                if len(self.random_prev) > 1:
  1.1007 +                    self.random_next.append(self.random_prev.pop())
  1.1008 +                    new = self.random_prev[-1]
  1.1009 +                else: return
  1.1010 +            old and old.set_active(0)
  1.1011 +        elif old:
  1.1012 +            index = self.buffer.index(old)+direction
  1.1013 +            if not (0 <= index < len(self.buffer) or self.repeat): return
  1.1014 +            old.set_active(0)
  1.1015 +            new = self.buffer[index % len(self.buffer)]
  1.1016 +        else:
  1.1017 +            new = self.buffer[0]
  1.1018 +        new.set_active(1)
  1.1019 +        self.update()
  1.1020 +        return new
  1.1021 +
  1.1022 +    def get_active_entry(self):
  1.1023 +        for entry in self.buffer:
  1.1024 +            if entry.is_active(): return entry
  1.1025 +
  1.1026 +    def command_jump_to_active(self):
  1.1027 +        entry = self.get_active_entry()
  1.1028 +        if not entry: return
  1.1029 +        self.bufptr = self.buffer.index(entry)
  1.1030 +        self.update()
  1.1031 +
  1.1032 +    def command_play(self):
  1.1033 +        if not self.buffer: return
  1.1034 +        entry = self.get_active_entry()
  1.1035 +        entry and entry.set_active(0)
  1.1036 +        entry = self.current()
  1.1037 +        entry.set_active(1)
  1.1038 +        self.update()
  1.1039 +        app.play(entry)
  1.1040 +
  1.1041 +    def command_delete(self):
  1.1042 +        if not self.buffer: return
  1.1043 +        current_entry, n = self.current(), len(self.buffer)
  1.1044 +        self.buffer = self.not_tagged(self.buffer)
  1.1045 +        if n > len(self.buffer):
  1.1046 +            try: self.bufptr = self.buffer.index(current_entry)
  1.1047 +            except ValueError: pass
  1.1048 +        else:
  1.1049 +            current_entry.set_tagged(1)
  1.1050 +            del self.buffer[self.bufptr]
  1.1051 +        if self.random:
  1.1052 +            self.random_prev = self.not_tagged(self.random_prev)
  1.1053 +            self.random_next = self.not_tagged(self.random_next)
  1.1054 +            self.random_left = self.not_tagged(self.random_left)
  1.1055 +        self.update()
  1.1056 +
  1.1057 +    def command_delete_all(self):
  1.1058 +        self.buffer = []
  1.1059 +        self.random_prev = []
  1.1060 +        self.random_next = []
  1.1061 +        self.random_left = []
  1.1062 +        app.status(_("Deleted playlist"), 1)
  1.1063 +        self.update()
  1.1064 +
  1.1065 +    def command_move(self, after):
  1.1066 +        if not self.buffer: return
  1.1067 +        current_entry, l = self.current(), self.get_tagged()
  1.1068 +        if not l or current_entry.is_tagged(): return
  1.1069 +        self.buffer = self.not_tagged(self.buffer)
  1.1070 +        self.bufptr = self.buffer.index(current_entry)+after
  1.1071 +        self.buffer[self.bufptr:self.bufptr] = l
  1.1072 +        self.update()
  1.1073 +
  1.1074 +    def command_shuffle(self):
  1.1075 +        import random
  1.1076 +        l = []
  1.1077 +        n = len(self.buffer)
  1.1078 +        while n > 0:
  1.1079 +            n = n-1
  1.1080 +            r = random.randint(0, n)
  1.1081 +            l.append(self.buffer[r])
  1.1082 +            del self.buffer[r]
  1.1083 +        self.buffer = l
  1.1084 +        self.bufptr = 0
  1.1085 +        self.update()
  1.1086 +        app.status(_("Shuffled playlist... Oops?"), 1)
  1.1087 +
  1.1088 +    def command_sort(self):
  1.1089 +        app.status(_("Working..."))
  1.1090 +        self.buffer.sort(lambda x, y: x.vp() > y.vp() or -1)
  1.1091 +        self.bufptr = 0
  1.1092 +        self.update()
  1.1093 +        app.status(_("Sorted playlist"), 1)
  1.1094 +
  1.1095 +    def command_toggle_repeat(self):
  1.1096 +        self.toggle("repeat", _("Repeat: %s"))
  1.1097 +
  1.1098 +    def command_toggle_random(self):
  1.1099 +        self.toggle("random", _("Random: %s"))
  1.1100 +        self.random_prev = []
  1.1101 +        self.random_next = []
  1.1102 +        self.random_left = self.buffer[:]
  1.1103 +
  1.1104 +    def command_toggle_stop(self):
  1.1105 +        self.toggle("stop", _("Stop playlist: %s"))
  1.1106 +
  1.1107 +    def toggle(self, attr, format):
  1.1108 +        setattr(self, attr, not getattr(self, attr))
  1.1109 +        app.status(format % (getattr(self, attr) and _("on") or _("off")), 1)
  1.1110 +        self.parent.update_title()
  1.1111 +
  1.1112 +    def command_save_playlist(self):
  1.1113 +        if app.restricted: return
  1.1114 +        default = self.pathname or "%s/" % app.win_filelist.cwd
  1.1115 +        app.stop_input_hook = self.stop_save_playlist
  1.1116 +        app.start_input(_("Save playlist"), default)
  1.1117 +
  1.1118 +    def stop_save_playlist(self):
  1.1119 +        pathname = app.input_string
  1.1120 +        if pathname[0] != '/':
  1.1121 +            pathname = os.path.join(app.win_filelist.cwd, pathname)
  1.1122 +        if not re.search("\.m3u$", pathname, re.I):
  1.1123 +            pathname = "%s%s" % (pathname, ".m3u")
  1.1124 +        try:
  1.1125 +            file = open(pathname, "w")
  1.1126 +            for entry in self.buffer:
  1.1127 +                file.write("%s\n" % entry.pathname)
  1.1128 +            file.close()
  1.1129 +            self.pathname = pathname
  1.1130 +            app.status(_("ok"), 1)
  1.1131 +        except IOError, e:
  1.1132 +            app.status(e, 2)
  1.1133 +
  1.1134 +# ------------------------------------------
  1.1135 +def get_tag(pathname):
  1.1136 +    if re.compile("^http://").match(pathname) or not os.path.exists(pathname):
  1.1137 +        return pathname
  1.1138 +    tags = {}
  1.1139 +    # FIXME: use magic instead of file extensions to identify OGGs and MP3s
  1.1140 +    if re.compile(".*\.ogg$", re.I).match(pathname):
  1.1141 +        try:
  1.1142 +            vf = ogg.vorbis.VorbisFile(pathname)
  1.1143 +            vc = vf.comment()
  1.1144 +            tags = vc.as_dict()
  1.1145 +        except NameError: pass
  1.1146 +        except (IOError, UnicodeError): return os.path.basename(pathname)
  1.1147 +    elif re.compile(".*\.mp3$", re.I).match(pathname):
  1.1148 +        try:
  1.1149 +            vc = ID3.ID3(pathname, as_tuple=1)
  1.1150 +            tags = vc.as_dict()
  1.1151 +        except NameError: pass
  1.1152 +        except (IOError, ID3.InvalidTagError): return os.path.basename(pathname)
  1.1153 +    else:
  1.1154 +        return os.path.basename(pathname)
  1.1155 +
  1.1156 +    artist = tags.get("ARTIST", [""])[0]
  1.1157 +    title = tags.get("TITLE", [""])[0]
  1.1158 +    tag = os.path.basename(pathname)
  1.1159 +    try:
  1.1160 +        if artist and title:
  1.1161 +            tag = codecs.latin_1_encode(artist)[0] + " - " + codecs.latin_1_encode(title)[0]
  1.1162 +        elif artist:
  1.1163 +            tag = artist
  1.1164 +        elif title:
  1.1165 +            tag = title
  1.1166 +        return codecs.latin_1_encode(tag)[0]
  1.1167 +    except (NameError, UnicodeError): return tag
  1.1168 +
  1.1169 +# ------------------------------------------
  1.1170 +class Player:
  1.1171 +    def __init__(self, commandline, files, fps=1):
  1.1172 +        self.commandline = commandline
  1.1173 +        self.re_files = re.compile(files, re.I)
  1.1174 +        self.fps = fps
  1.1175 +        self.stdin_r, self.stdin_w = os.pipe()
  1.1176 +        self.stdout_r, self.stdout_w = os.pipe()
  1.1177 +        self.stderr_r, self.stderr_w = os.pipe()
  1.1178 +        self.entry = None
  1.1179 +        self.stopped = 0
  1.1180 +        self.paused = 0
  1.1181 +        self.time_setup = None
  1.1182 +        self.buf = ''
  1.1183 +        self.tid = None
  1.1184 +
  1.1185 +    def setup(self, entry, offset):
  1.1186 +        self.argv = string.split(self.commandline)
  1.1187 +        self.argv[0] = which(self.argv[0])
  1.1188 +        for i in range(len(self.argv)):
  1.1189 +            if self.argv[i] == "%s": self.argv[i] = entry.pathname
  1.1190 +            if self.argv[i] == "%d": self.argv[i] = str(offset*self.fps)
  1.1191 +        self.entry = entry
  1.1192 +        if offset == 0:
  1.1193 +            app.progress(0)
  1.1194 +            self.offset = 0
  1.1195 +            self.length = 0
  1.1196 +            self.values = [0, 0]
  1.1197 +        self.time_setup = time.time()
  1.1198 +        return self.argv[0]
  1.1199 +
  1.1200 +    def play(self):
  1.1201 +        self.pid = os.fork()
  1.1202 +        if self.pid == 0:
  1.1203 +            os.dup2(self.stdin_w, sys.stdin.fileno())
  1.1204 +            os.dup2(self.stdout_w, sys.stdout.fileno())
  1.1205 +            os.dup2(self.stderr_w, sys.stderr.fileno())
  1.1206 +            os.setpgrp()
  1.1207 +            try: os.execv(self.argv[0], self.argv)
  1.1208 +            except: os._exit(1)
  1.1209 +        self.stopped = 0
  1.1210 +        self.paused = 0
  1.1211 +        self.step = 0
  1.1212 +        self.update_status()
  1.1213 +
  1.1214 +    def stop(self, quiet=0):
  1.1215 +        self.paused and self.toggle_pause(quiet)
  1.1216 +        try:
  1.1217 +            while 1:
  1.1218 +                try: os.kill(-self.pid, signal.SIGINT)
  1.1219 +                except os.error: pass
  1.1220 +                os.waitpid(self.pid, os.WNOHANG)
  1.1221 +        except Exception: pass
  1.1222 +        self.stopped = 1
  1.1223 +        quiet or self.update_status()
  1.1224 +
  1.1225 +    def toggle_pause(self, quiet=0):
  1.1226 +        try: os.kill(-self.pid, [signal.SIGSTOP, signal.SIGCONT][self.paused])
  1.1227 +        except os.error: return
  1.1228 +        self.paused = not self.paused
  1.1229 +        quiet or self.update_status()
  1.1230 +
  1.1231 +    def parse_progress(self):
  1.1232 +        if self.stopped or self.step: self.tid = None
  1.1233 +        else:
  1.1234 +            self.parse_buf()
  1.1235 +            self.tid = app.timeout.add(1.0, self.parse_progress)
  1.1236 +
  1.1237 +    def read_fd(self, fd):
  1.1238 +        self.buf = os.read(fd, 512)
  1.1239 +        self.tid or self.parse_progress()
  1.1240 +
  1.1241 +    def poll(self):
  1.1242 +        try: os.waitpid(self.pid, os.WNOHANG)
  1.1243 +        except:
  1.1244 +            # something broken? try again
  1.1245 +            if self.time_setup and (time.time() - self.time_setup) < 2.0:
  1.1246 +                self.play()
  1.1247 +                return 0
  1.1248 +            app.set_default_status("")
  1.1249 +            app.counter([0,0])
  1.1250 +            app.progress(0)
  1.1251 +            return 1
  1.1252 +
  1.1253 +    def seek(self, offset, relative):
  1.1254 +        if relative:
  1.1255 +            d = offset * self.length * 0.002
  1.1256 +            self.step = self.step * (self.step * d > 0) + d
  1.1257 +            self.offset = min(self.length, max(0, self.offset+self.step))
  1.1258 +        else:
  1.1259 +            self.step = 1
  1.1260 +            self.offset = (offset < 0) and self.length+offset or offset
  1.1261 +        self.show_position()
  1.1262 +
  1.1263 +    def set_position(self, offset, length, values):
  1.1264 +        self.offset = offset
  1.1265 +        self.length = length
  1.1266 +        self.values = values
  1.1267 +        self.show_position()
  1.1268 +
  1.1269 +    def show_position(self):
  1.1270 +        app.counter(self.values)
  1.1271 +        app.progress(self.length and (float(self.offset) / self.length))
  1.1272 +
  1.1273 +    def update_status(self):
  1.1274 +        if not self.entry:
  1.1275 +            app.set_default_status("")
  1.1276 +        elif self.stopped:
  1.1277 +            app.set_default_status(_("Stopped: %s") % self.entry.vp())
  1.1278 +        elif self.paused:
  1.1279 +            app.set_default_status(_("Paused: %s") % self.entry.vp())
  1.1280 +        else:
  1.1281 +            app.set_default_status(_("Playing: %s") % self.entry.vp())
  1.1282 +
  1.1283 +# ------------------------------------------
  1.1284 +class FrameOffsetPlayer(Player):
  1.1285 +    re_progress = re.compile("Time.*\s(\d+):(\d+).*\[(\d+):(\d+)")
  1.1286 +
  1.1287 +    def parse_buf(self):
  1.1288 +        match = self.re_progress.search(self.buf)
  1.1289 +        if match:
  1.1290 +            m1, s1, m2, s2 = map(string.atoi, match.groups())
  1.1291 +            head, tail = m1*60+s1, m2*60+s2
  1.1292 +            self.set_position(head, head+tail, [head, tail])
  1.1293 +
  1.1294 +# ------------------------------------------
  1.1295 +class TimeOffsetPlayer(Player):
  1.1296 +    re_progress = re.compile("(\d+):(\d+):(\d+)")
  1.1297 +
  1.1298 +    def parse_buf(self):
  1.1299 +        match = self.re_progress.search(self.buf)
  1.1300 +        if match:
  1.1301 +            h, m, s = map(string.atoi, match.groups())
  1.1302 +            tail = h*3600+m*60+s
  1.1303 +            head = max(self.length, tail) - tail
  1.1304 +            self.set_position(head, head+tail, [head, tail])
  1.1305 +
  1.1306 +# ------------------------------------------
  1.1307 +class NoOffsetPlayer(Player):
  1.1308 +
  1.1309 +    def parse_buf(self):
  1.1310 +        head = self.offset+1
  1.1311 +        self.set_position(head, 0, [head, head])
  1.1312 +
  1.1313 +    def seek(self, *dummy):
  1.1314 +        return 1
  1.1315 +
  1.1316 +# ------------------------------------------
  1.1317 +class Timeout:
  1.1318 +    def __init__(self):
  1.1319 +        self.next = 0
  1.1320 +        self.dict = {}
  1.1321 +
  1.1322 +    def add(self, timeout, func, args=()):
  1.1323 +        tid = self.next = self.next + 1
  1.1324 +        self.dict[tid] = (func, args, time.time() + timeout)
  1.1325 +        return tid
  1.1326 +
  1.1327 +    def remove(self, tid):
  1.1328 +        del self.dict[tid]
  1.1329 +
  1.1330 +    def check(self, now):
  1.1331 +        for tid, (func, args, timeout) in self.dict.items():
  1.1332 +            if now >= timeout:
  1.1333 +                self.remove(tid)
  1.1334 +                apply(func, args)
  1.1335 +        return len(self.dict) and 0.2 or None
  1.1336 +
  1.1337 +# ------------------------------------------
  1.1338 +class FIFOControl:
  1.1339 +    def __init__(self):
  1.1340 +        try: self.fd = open(CONTROL_FIFO, "rb+", 0)
  1.1341 +        except: self.fd = None
  1.1342 +        self.commands = {"pause" : app.toggle_pause,
  1.1343 +                         "next" : app.next_song,
  1.1344 +                         "prev" : app.prev_song,
  1.1345 +                         "forward" : self.forward,
  1.1346 +                         "backward" : self.backward,
  1.1347 +                         "play" : app.toggle_stop,
  1.1348 +                         "stop" : app.toggle_stop,
  1.1349 +                         "volup" : app.inc_volume,
  1.1350 +                         "voldown" : app.dec_volume,
  1.1351 +                         "quit" : app.quit}
  1.1352 +
  1.1353 +    def handle_command(self):
  1.1354 +        command = string.strip(self.fd.readline())
  1.1355 +        if command in self.commands.keys():
  1.1356 +            self.commands[command]()
  1.1357 +
  1.1358 +    def forward(self):
  1.1359 +        app.seek(1, 1)
  1.1360 +
  1.1361 +    def backward(self):
  1.1362 +        app.seek(-1, 1)
  1.1363 +
  1.1364 +# ------------------------------------------
  1.1365 +class Application:
  1.1366 +    def __init__(self):
  1.1367 +        self.keymapstack = KeymapStack()
  1.1368 +        self.input_mode = 0
  1.1369 +        self.input_prompt = ""
  1.1370 +        self.input_string = ""
  1.1371 +        self.do_input_hook = None
  1.1372 +        self.stop_input_hook = None
  1.1373 +        self.complete_input_hook = None
  1.1374 +        self.channels = []
  1.1375 +        self.restricted = 0
  1.1376 +        self.input_keymap = Keymap()
  1.1377 +        self.input_keymap.bind(list(Window.chars), self.do_input)
  1.1378 +        self.input_keymap.bind(curses.KEY_BACKSPACE, self.do_input, (8,))
  1.1379 +        self.input_keymap.bind([21, 23], self.do_input)
  1.1380 +        self.input_keymap.bind(['\a', 27], self.cancel_input, ())
  1.1381 +        self.input_keymap.bind(['\n', curses.KEY_ENTER],
  1.1382 +                               self.stop_input, ())
  1.1383 +
  1.1384 +    def setup(self):
  1.1385 +        if tty:
  1.1386 +            self.tcattr = tty.tcgetattr(sys.stdin.fileno())
  1.1387 +            tcattr = tty.tcgetattr(sys.stdin.fileno())
  1.1388 +            tcattr[0] = tcattr[0] & ~(tty.IXON)
  1.1389 +            tty.tcsetattr(sys.stdin.fileno(), tty.TCSANOW, tcattr)
  1.1390 +        self.w = curses.initscr()
  1.1391 +        curses.cbreak()
  1.1392 +        curses.noecho()
  1.1393 +        try: curses.meta(1)
  1.1394 +        except: pass
  1.1395 +        self.cursor(0)
  1.1396 +        signal.signal(signal.SIGCHLD, signal.SIG_IGN)
  1.1397 +        signal.signal(signal.SIGHUP, self.handler_quit)
  1.1398 +        signal.signal(signal.SIGINT, self.handler_quit)
  1.1399 +        signal.signal(signal.SIGTERM, self.handler_quit)
  1.1400 +        signal.signal(signal.SIGWINCH, self.handler_resize)
  1.1401 +        self.win_root = RootWindow(None)
  1.1402 +        self.win_root.update()
  1.1403 +        self.win_tab = self.win_root.win_tab
  1.1404 +        self.win_filelist = self.win_root.win_tab.win_filelist
  1.1405 +        self.win_playlist = self.win_root.win_tab.win_playlist
  1.1406 +        self.win_status = self.win_root.win_status
  1.1407 +        self.status = self.win_status.status
  1.1408 +        self.set_default_status = self.win_status.set_default_status
  1.1409 +        self.restore_default_status = self.win_status.restore_default_status
  1.1410 +        self.counter = self.win_root.win_counter.counter
  1.1411 +        self.progress = self.win_root.win_progress.progress
  1.1412 +        self.player = PLAYERS[0]
  1.1413 +        self.timeout = Timeout()
  1.1414 +        self.play_tid = None
  1.1415 +        self.kludge = 0
  1.1416 +        self.win_filelist.listdir()
  1.1417 +        self.control = FIFOControl()
  1.1418 +
  1.1419 +    def cleanup(self):
  1.1420 +        try: curses.endwin()
  1.1421 +        except curses.error: return
  1.1422 +        XTERM and sys.stderr.write("\033]0;%s\a" % "xterm")
  1.1423 +        tty and tty.tcsetattr(sys.stdin.fileno(), tty.TCSADRAIN, self.tcattr)
  1.1424 +        print
  1.1425 +
  1.1426 +    def run(self):
  1.1427 +        while 1:
  1.1428 +            now = time.time()
  1.1429 +            timeout = self.timeout.check(now)
  1.1430 +            self.win_filelist.listdir_maybe(now)
  1.1431 +            if not self.player.stopped:
  1.1432 +                timeout = 0.5
  1.1433 +                if self.kludge and self.player.poll():
  1.1434 +                    self.player.stopped = 1  # end of playlist hack
  1.1435 +                    if not self.win_playlist.stop:
  1.1436 +                        entry = self.win_playlist.change_active_entry(1)
  1.1437 +                        entry and self.play(entry)
  1.1438 +            R = [sys.stdin, self.player.stdout_r, self.player.stderr_r]
  1.1439 +            self.control.fd and R.append(self.control.fd)
  1.1440 +            try: r, w, e = select.select(R, [], [], timeout)
  1.1441 +            except select.error: continue
  1.1442 +            self.kludge = 1
  1.1443 +            # user
  1.1444 +            if sys.stdin in r:
  1.1445 +                c = self.win_root.getch()
  1.1446 +                self.keymapstack.process(c)
  1.1447 +            # player
  1.1448 +            if self.player.stderr_r in r:
  1.1449 +                self.player.read_fd(self.player.stderr_r)
  1.1450 +            # player
  1.1451 +            if self.player.stdout_r in r:
  1.1452 +                self.player.read_fd(self.player.stdout_r)
  1.1453 +            # remote
  1.1454 +            if self.control.fd in r:
  1.1455 +                self.control.handle_command()
  1.1456 +
  1.1457 +    def play(self, entry, offset = 0):
  1.1458 +        self.kludge = 0
  1.1459 +        self.play_tid = None
  1.1460 +        if entry is None or offset is None: return
  1.1461 +        self.player.stop(quiet=1)
  1.1462 +        for self.player in PLAYERS:
  1.1463 +            if self.player.re_files.search(entry.pathname):
  1.1464 +                if self.player.setup(entry, offset): break
  1.1465 +        else:
  1.1466 +            app.status(_("Player not found!"), 1)
  1.1467 +            self.player.stopped = 0  # keep going
  1.1468 +            return
  1.1469 +        self.player.play()
  1.1470 +
  1.1471 +    def delayed_play(self, entry, offset):
  1.1472 +        if self.play_tid: self.timeout.remove(self.play_tid)
  1.1473 +        self.play_tid = self.timeout.add(0.5, self.play, (entry, offset))
  1.1474 +
  1.1475 +    def next_song(self):
  1.1476 +        self.delayed_play(self.win_playlist.change_active_entry(1), 0)
  1.1477 +
  1.1478 +    def prev_song(self):
  1.1479 +        self.delayed_play(self.win_playlist.change_active_entry(-1), 0)
  1.1480 +
  1.1481 +    def seek(self, offset, relative):
  1.1482 +        if not self.player.entry: return
  1.1483 +        self.player.seek(offset, relative)
  1.1484 +        self.delayed_play(self.player.entry, self.player.offset)
  1.1485 +
  1.1486 +    def toggle_pause(self):
  1.1487 +        if not self.player.entry: return
  1.1488 +        if not self.player.stopped: self.player.toggle_pause()
  1.1489 +
  1.1490 +    def toggle_stop(self):
  1.1491 +        if not self.player.entry: return
  1.1492 +        if not self.player.stopped: self.player.stop()
  1.1493 +        else: self.play(self.player.entry, self.player.offset)
  1.1494 +
  1.1495 +    def inc_volume(self):
  1.1496 +        self.mixer("cue", 1)
  1.1497 +
  1.1498 +    def dec_volume(self):
  1.1499 +        self.mixer("cue", -1)
  1.1500 +
  1.1501 +    def key_volume(self, ch):
  1.1502 +        self.mixer("set", (ch & 0x0f)*10)
  1.1503 +
  1.1504 +    def mixer(self, cmd=None, arg=None):
  1.1505 +        try: self._mixer(cmd, arg)
  1.1506 +        except Exception, e: app.status(e, 2)
  1.1507 +
  1.1508 +    def _mixer(self, cmd, arg):
  1.1509 +        try:
  1.1510 +            import ossaudiodev
  1.1511 +            mixer = ossaudiodev.openmixer()
  1.1512 +            get, set = mixer.get, mixer.set
  1.1513 +            self.channels = self.channels or \
  1.1514 +                [['MASTER', ossaudiodev.SOUND_MIXER_VOLUME],
  1.1515 +                 ['PCM', ossaudiodev.SOUND_MIXER_PCM]]
  1.1516 +        except ImportError:
  1.1517 +            import oss
  1.1518 +            mixer = oss.open_mixer()
  1.1519 +            get, set = mixer.read_channel, mixer.write_channel
  1.1520 +            self.channels = self.channels or \
  1.1521 +                [['MASTER', oss.SOUND_MIXER_VOLUME],
  1.1522 +                 ['PCM', oss.SOUND_MIXER_PCM]]
  1.1523 +        if cmd is "toggle": self.channels.insert(0, self.channels.pop())
  1.1524 +        name, channel = self.channels[0]
  1.1525 +        if cmd is "cue": arg = min(100, max(0, get(channel)[0] + arg))
  1.1526 +        if cmd in ["set", "cue"]: set(channel, (arg, arg))
  1.1527 +        app.status(_("%s volume %s%%") % (name, get(channel)[0]), 1)
  1.1528 +        mixer.close()
  1.1529 +
  1.1530 +    def show_input(self):
  1.1531 +        n = len(self.input_prompt)+1
  1.1532 +        s = cut(self.input_string, self.win_status.cols-n, left=1)
  1.1533 +        app.status("%s%s " % (self.input_prompt, s))
  1.1534 +
  1.1535 +    def start_input(self, prompt="", data="", colon=1):
  1.1536 +        self.input_mode = 1
  1.1537 +        self.cursor(1)
  1.1538 +        app.keymapstack.push(self.input_keymap)
  1.1539 +        self.input_prompt = prompt + (colon and ": " or "")
  1.1540 +        self.input_string = data
  1.1541 +        self.show_input()
  1.1542 +
  1.1543 +    def do_input(self, *args):
  1.1544 +        if self.do_input_hook:
  1.1545 +            return apply(self.do_input_hook, args)
  1.1546 +        ch = args and args[0] or None
  1.1547 +        if ch in [8, 127]: # backspace
  1.1548 +            self.input_string = self.input_string[:-1]
  1.1549 +        elif ch == 9 and self.complete_input_hook:
  1.1550 +            self.input_string = self.complete_input_hook(self.input_string)
  1.1551 +        elif ch == 21: # C-u
  1.1552 +            self.input_string = ""
  1.1553 +        elif ch == 23: # C-w
  1.1554 +            self.input_string = re.sub("((.* )?)\w.*", "\\1", self.input_string)
  1.1555 +        elif ch:
  1.1556 +            self.input_string = "%s%c" % (self.input_string, ch)
  1.1557 +        self.show_input()
  1.1558 +
  1.1559 +    def stop_input(self, *args):
  1.1560 +        self.input_mode = 0
  1.1561 +        self.cursor(0)
  1.1562 +        app.keymapstack.pop()
  1.1563 +        if not self.input_string:
  1.1564 +            app.status(_("cancel"), 1)
  1.1565 +        elif self.stop_input_hook:
  1.1566 +            apply(self.stop_input_hook, args)
  1.1567 +        self.do_input_hook = None
  1.1568 +        self.stop_input_hook = None
  1.1569 +        self.complete_input_hook = None
  1.1570 +
  1.1571 +    def cancel_input(self):
  1.1572 +        self.input_string = ""
  1.1573 +        self.stop_input()
  1.1574 +
  1.1575 +    def cursor(self, visibility):
  1.1576 +        try: curses.curs_set(visibility)
  1.1577 +        except: pass
  1.1578 +
  1.1579 +    def quit(self):
  1.1580 +        self.player.stop(quiet=1)
  1.1581 +        sys.exit(0)
  1.1582 +
  1.1583 +    def handler_resize(self, sig, frame):
  1.1584 +        # curses trickery
  1.1585 +        while 1:
  1.1586 +            try: curses.endwin(); break
  1.1587 +            except: time.sleep(1)
  1.1588 +        self.w.refresh()
  1.1589 +        self.win_root.resize()
  1.1590 +        self.win_root.update()
  1.1591 +
  1.1592 +    def handler_quit(self, sig, frame):
  1.1593 +        self.quit()
  1.1594 +
  1.1595 +# ------------------------------------------
  1.1596 +def main():
  1.1597 +    try:
  1.1598 +        opts, args = getopt.getopt(sys.argv[1:], "nrRv")
  1.1599 +    except:
  1.1600 +        usage = _("Usage: %s [-nrRv] [ file | dir | playlist ] ...\n")
  1.1601 +        sys.stderr.write(usage % sys.argv[0])
  1.1602 +        sys.exit(1)
  1.1603 +
  1.1604 +    global app
  1.1605 +    app = Application()
  1.1606 +
  1.1607 +    playlist = []
  1.1608 +    if not sys.stdin.isatty():
  1.1609 +        playlist = map(string.strip, sys.stdin.readlines())
  1.1610 +        os.close(0)
  1.1611 +        os.open("/dev/tty", 0)
  1.1612 +    try:
  1.1613 +        app.setup()
  1.1614 +        for opt, optarg in opts:
  1.1615 +            if opt == "-n": app.restricted = 1
  1.1616 +            if opt == "-r": app.win_playlist.command_toggle_repeat()
  1.1617 +            if opt == "-R": app.win_playlist.command_toggle_random()
  1.1618 +            if opt == "-v": app.mixer("toggle")
  1.1619 +        if args or playlist:
  1.1620 +            for i in args or playlist:
  1.1621 +                app.win_playlist.add(os.path.abspath(i))
  1.1622 +            app.win_tab.change_window()
  1.1623 +        app.run()
  1.1624 +    except SystemExit:
  1.1625 +        app.cleanup()
  1.1626 +    except Exception:
  1.1627 +        app.cleanup()
  1.1628 +        import traceback
  1.1629 +        traceback.print_exc()
  1.1630 +
  1.1631 +# ------------------------------------------
  1.1632 +PLAYERS = [
  1.1633 +    FrameOffsetPlayer("ogg123 -q -v -k %d %s", "\.ogg$"),
  1.1634 +    FrameOffsetPlayer("splay -f -k %d %s", "(^http://|\.mp[123]$)", 38.28),
  1.1635 +    FrameOffsetPlayer("mpg123 -q -v -k %d %s", "(^http://|\.mp[123]$)", 38.28),
  1.1636 +    FrameOffsetPlayer("mpg321 -q -v -k %d %s", "(^http://|\.mp[123]$)", 38.28),
  1.1637 +    TimeOffsetPlayer("madplay -v --display-time=remaining -s %d %s", "\.mp[123]$"),
  1.1638 +    NoOffsetPlayer("mikmod -q -p0 %s", "\.(mod|xm|fm|s3m|med|col|669|it|mtm)$"),
  1.1639 +    NoOffsetPlayer("xmp -q %s", "\.(mod|xm|fm|s3m|med|col|669|it|mtm|stm)$"),
  1.1640 +    NoOffsetPlayer("play %s", "\.(aiff|au|cdr|mp3|ogg|wav)$"),
  1.1641 +    NoOffsetPlayer("speexdec %s", "\.spx$")
  1.1642 +    ]
  1.1643 +
  1.1644 +def VALID_SONG(name):
  1.1645 +    for player in PLAYERS:
  1.1646 +        if player.re_files.search(name):
  1.1647 +            return 1
  1.1648 +
  1.1649 +def VALID_PLAYLIST(name):
  1.1650 +    if re.search("\.(m3u|pls)$", name, re.I):
  1.1651 +        return 1
  1.1652 +
  1.1653 +for rc in [os.path.expanduser("~/.cplayrc"), "/etc/cplayrc"]:
  1.1654 +    try: execfile(rc); break
  1.1655 +    except IOError: pass
  1.1656 +
  1.1657 +# ------------------------------------------
  1.1658 +if __name__ == "__main__": main()