1#!/usr/bin/python
2#
3# Urwid web (CGI/Asynchronous Javascript) display module
4#    Copyright (C) 2004-2007  Ian Ward
5#
6#    This library is free software; you can redistribute it and/or
7#    modify it under the terms of the GNU Lesser General Public
8#    License as published by the Free Software Foundation; either
9#    version 2.1 of the License, or (at your option) any later version.
10#
11#    This library is distributed in the hope that it will be useful,
12#    but WITHOUT ANY WARRANTY; without even the implied warranty of
13#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14#    Lesser General Public License for more details.
15#
16#    You should have received a copy of the GNU Lesser General Public
17#    License along with this library; if not, write to the Free Software
18#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19#
20# Urwid web site: http://excess.org/urwid/
21
22from __future__ import division, print_function
23
24"""
25Urwid web application display module
26"""
27import os
28import sys
29import signal
30import random
31import select
32import socket
33import glob
34
35from urwid import util
36_js_code = r"""
37// Urwid web (CGI/Asynchronous Javascript) display module
38//    Copyright (C) 2004-2005  Ian Ward
39//
40//    This library is free software; you can redistribute it and/or
41//    modify it under the terms of the GNU Lesser General Public
42//    License as published by the Free Software Foundation; either
43//    version 2.1 of the License, or (at your option) any later version.
44//
45//    This library is distributed in the hope that it will be useful,
46//    but WITHOUT ANY WARRANTY; without even the implied warranty of
47//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
48//    Lesser General Public License for more details.
49//
50//    You should have received a copy of the GNU Lesser General Public
51//    License along with this library; if not, write to the Free Software
52//    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
53//
54// Urwid web site: http://excess.org/urwid/
55
56colours = new Object();
57colours = {
58    '0': "black",
59    '1': "#c00000",
60    '2': "green",
61    '3': "#804000",
62    '4': "#0000c0",
63    '5': "#c000c0",
64    '6': "teal",
65    '7': "silver",
66    '8': "gray",
67    '9': "#ff6060",
68    'A': "lime",
69    'B': "yellow",
70    'C': "#8080ff",
71    'D': "#ff40ff",
72    'E': "aqua",
73    'F': "white"
74};
75
76keycodes = new Object();
77keycodes = {
78    8: "backspace", 9: "tab", 13: "enter", 27: "esc",
79    33: "page up", 34: "page down", 35: "end", 36: "home",
80    37: "left", 38: "up", 39: "right", 40: "down",
81    45: "insert", 46: "delete",
82    112: "f1", 113: "f2", 114: "f3", 115: "f4",
83    116: "f5", 117: "f6", 118: "f7", 119: "f8",
84    120: "f9", 121: "f10", 122: "f11", 123: "f12"
85    };
86
87var conn = null;
88var char_width = null;
89var char_height = null;
90var screen_x = null;
91var screen_y = null;
92
93var urwid_id = null;
94var send_conn = null;
95var send_queue_max = 32;
96var send_queue = new Array(send_queue_max);
97var send_queue_in = 0;
98var send_queue_out = 0;
99
100var check_font_delay = 1000;
101var send_more_delay = 100;
102var poll_again_delay = 500;
103
104var document_location = null;
105
106var update_method = "multipart";
107
108var sending = false;
109var lastkeydown = null;
110
111function setup_connection() {
112    if (window.XMLHttpRequest) {
113        conn = new XMLHttpRequest();
114    } else if (window.ActiveXObject) {
115        conn = new ActiveXObject("Microsoft.XMLHTTP");
116    }
117
118    if (conn == null) {
119        set_status("Connection Failed");
120        alert( "Can't figure out how to send request." );
121        return;
122    }
123    try{
124        conn.multipart = true;
125    }catch(e){
126        update_method = "polling";
127    }
128    conn.onreadystatechange = handle_recv;
129    conn.open("POST", document_location, true);
130    conn.setRequestHeader("X-Urwid-Method",update_method);
131    conn.setRequestHeader("Content-type","text/plain");
132    conn.send("window resize " +screen_x+" "+screen_y+"\n");
133}
134
135function do_poll() {
136    if (urwid_id == null){
137        alert("that's unpossible!");
138        return;
139    }
140    if (window.XMLHttpRequest) {
141        conn = new XMLHttpRequest();
142    } else if (window.ActiveXObject) {
143        conn = new ActiveXObject("Microsoft.XMLHTTP");
144    }
145    conn.onreadystatechange = handle_recv;
146    conn.open("POST", document_location, true);
147    conn.setRequestHeader("X-Urwid-Method","polling");
148    conn.setRequestHeader("X-Urwid-ID",urwid_id);
149    conn.setRequestHeader("Content-type","text/plain");
150    conn.send("eh?");
151}
152
153function handle_recv() {
154    if( ! conn ){ return;}
155    if( conn.readyState != 4) {
156        return;
157    }
158    if( conn.status == 404 && urwid_id != null) {
159        set_status("Connection Closed");
160        return;
161    }
162    if( conn.status == 403 && update_method == "polling" ) {
163        set_status("Server Refused Connection");
164        alert("This server does not allow polling clients.\n\n" +
165            "Please use a web browser with multipart support " +
166            "such as Mozilla Firefox");
167        return;
168    }
169    if( conn.status == 503 ) {
170        set_status("Connection Failed");
171        alert("The server has reached its maximum number of "+
172            "connections.\n\nPlease try again later.");
173        return;
174    }
175    if( conn.status != 200) {
176        set_status("Connection Failed");
177        alert("Error from server: "+conn.statusText);
178        return;
179    }
180    if( urwid_id == null ){
181        urwid_id = conn.getResponseHeader("X-Urwid-ID");
182        if( send_queue_in != send_queue_out ){
183            // keys waiting
184            do_send();
185        }
186        if(update_method=="polling"){
187            set_status("Polling");
188        }else if(update_method=="multipart"){
189            set_status("Connected");
190        }
191
192    }
193
194    if( conn.responseText == "" ){
195        if(update_method=="polling"){
196            poll_again();
197        }
198        return; // keepalive
199    }
200    if( conn.responseText == "Z" ){
201        set_status("Connection Closed");
202        update_method = null;
203        return;
204    }
205
206    var text = document.getElementById('text');
207
208    var last_screen = Array(text.childNodes.length);
209    for( var i=0; i<text.childNodes.length; i++ ){
210        last_screen[i] = text.childNodes[i];
211    }
212
213    var frags = conn.responseText.split("\n");
214    var ln = document.createElement('span');
215    var k = 0;
216    for( var i=0; i<frags.length; i++ ){
217        var f = frags[i];
218        if( f == "" ){
219            var br = document.getElementById('br').cloneNode(true);
220            ln.appendChild( br );
221            if( text.childNodes.length > k ){
222                text.replaceChild(ln, text.childNodes[k]);
223            }else{
224                text.appendChild(ln);
225            }
226            k = k+1;
227            ln = document.createElement('span');
228        }else if( f.charAt(0) == "<" ){
229            line_number = parseInt(f.substr(1));
230            if( line_number == k ){
231                k = k +1;
232                continue;
233            }
234            var clone = last_screen[line_number].cloneNode(true);
235            if( text.childNodes.length > k ){
236                text.replaceChild(clone, text.childNodes[k]);
237            }else{
238                text.appendChild(clone);
239            }
240            k = k+1;
241        }else{
242            var span=make_span(f.substr(2),f.charAt(0),f.charAt(1));
243            ln.appendChild( span );
244        }
245    }
246    for( var i=k; i < text.childNodes.length; i++ ){
247        text.removeChild(last_screen[i]);
248    }
249
250    if(update_method=="polling"){
251        poll_again();
252    }
253}
254
255function poll_again(){
256    if(conn.status == 200){
257        setTimeout("do_poll();",poll_again_delay);
258    }
259}
260
261
262function load_web_display(){
263    if( document.documentURI ){
264        document_location = document.documentURI;
265    }else{
266        document_location = document.location;
267    }
268
269    document.onkeypress = body_keypress;
270    document.onkeydown = body_keydown;
271    document.onresize = body_resize;
272
273    body_resize();
274    send_queue_out = send_queue_in; // don't queue the first resize
275
276    set_status("Connecting");
277    setup_connection();
278
279    setTimeout("check_fontsize();",check_font_delay);
280}
281
282function set_status( status ){
283    var s = document.getElementById('status');
284    var t = document.createTextNode(status);
285    s.replaceChild(t, s.firstChild);
286}
287
288function make_span(s, fg, bg){
289    d = document.createElement('span');
290    d.style.backgroundColor = colours[bg];
291    d.style.color = colours[fg];
292    d.appendChild(document.createTextNode(s));
293
294    return d;
295}
296
297function body_keydown(e){
298    if (conn == null){
299        return;
300    }
301    if (!e) var e = window.event;
302    if (e.keyCode) code = e.keyCode;
303    else if (e.which) code = e.which;
304
305    var mod = "";
306    var key;
307
308    if( e.ctrlKey ){ mod = "ctrl " + mod; }
309    if( e.altKey || e.metaKey ){ mod = "meta " + mod; }
310    if( e.shiftKey && e.charCode == 0 ){ mod = "shift " + mod; }
311
312    key = keycodes[code];
313
314    if( key != undefined ){
315        lastkeydown = key;
316        send_key( mod + key );
317        stop_key_event(e);
318        return false;
319    }
320}
321
322function body_keypress(e){
323    if (conn == null){
324        return;
325    }
326
327    if (!e) var e = window.event;
328    if (e.keyCode) code = e.keyCode;
329    else if (e.which) code = e.which;
330
331    var mod = "";
332    var key;
333
334    if( e.ctrlKey ){ mod = "ctrl " + mod; }
335    if( e.altKey || e.metaKey ){ mod = "meta " + mod; }
336    if( e.shiftKey && e.charCode == 0 ){ mod = "shift " + mod; }
337
338    if( e.charCode != null && e.charCode != 0 ){
339        key = String.fromCharCode(e.charCode);
340    }else if( e.charCode == null ){
341        key = String.fromCharCode(code);
342    }else{
343        key = keycodes[code];
344        if( key == undefined || lastkeydown == key ){
345            lastkeydown = null;
346            stop_key_event(e);
347            return false;
348        }
349    }
350
351    send_key( mod + key );
352    stop_key_event(e);
353    return false;
354}
355
356function stop_key_event(e){
357    e.cancelBubble = true;
358    if( e.stopPropagation ){
359        e.stopPropagation();
360    }
361    if( e.preventDefault  ){
362        e.preventDefault();
363    }
364}
365
366function send_key( key ){
367    if( (send_queue_in+1)%send_queue_max == send_queue_out ){
368        // buffer overrun
369        return;
370    }
371    send_queue[send_queue_in] = key;
372    send_queue_in = (send_queue_in+1)%send_queue_max;
373
374    if( urwid_id != null ){
375        if (send_conn == undefined || send_conn.ready_state != 4 ){
376            send_more();
377            return;
378        }
379        do_send();
380    }
381}
382
383function do_send() {
384    if( ! urwid_id ){ return; }
385    if( ! update_method ){ return; } // connection closed
386    if( send_queue_in == send_queue_out ){ return; }
387    if( sending ){
388        //var queue_delta = send_queue_in - send_queue_out;
389        //if( queue_delta < 0 ){ queue_delta += send_queue_max; }
390        //set_status("Sending (queued "+queue_delta+")");
391        return;
392    }
393    try{
394        sending = true;
395        //set_status("starting send");
396        if( send_conn == null ){
397            if (window.XMLHttpRequest) {
398                send_conn = new XMLHttpRequest();
399            } else if (window.ActiveXObject) {
400                send_conn = new ActiveXObject("Microsoft.XMLHTTP");
401            }
402        }else if( send_conn.status != 200) {
403            alert("Error from server: "+send_conn.statusText);
404            return;
405        }else if(send_conn.readyState != 4 ){
406            alert("not ready on send connection");
407            return;
408        }
409    } catch(e) {
410        alert(e);
411        sending = false;
412        return;
413    }
414    send_conn.open("POST", document_location, true);
415    send_conn.onreadystatechange = send_handle_recv;
416    send_conn.setRequestHeader("Content-type","text/plain");
417    send_conn.setRequestHeader("X-Urwid-ID",urwid_id);
418    var tmp_send_queue_in = send_queue_in;
419    var out = null;
420    if( send_queue_out > tmp_send_queue_in ){
421        out = send_queue.slice(send_queue_out).join("\n")
422        if( tmp_send_queue_in > 0 ){
423            out += "\n"  + send_queue.slice(0,tmp_send_queue_in).join("\n");
424        }
425    }else{
426        out = send_queue.slice(send_queue_out,
427             tmp_send_queue_in).join("\n");
428    }
429    send_queue_out = tmp_send_queue_in;
430    //set_status("Sending");
431    send_conn.send( out +"\n" );
432}
433
434function send_handle_recv() {
435    if( send_conn.readyState != 4) {
436        return;
437    }
438    if( send_conn.status == 404) {
439        set_status("Connection Closed");
440        update_method = null;
441        return;
442    }
443    if( send_conn.status != 200) {
444        alert("Error from server: "+send_conn.statusText);
445        return;
446    }
447
448    sending = false;
449
450    if( send_queue_out != send_queue_in ){
451        send_more();
452    }
453}
454
455function send_more(){
456    setTimeout("do_send();",send_more_delay);
457}
458
459function check_fontsize(){
460    body_resize()
461    setTimeout("check_fontsize();",check_font_delay);
462}
463
464function body_resize(){
465    var t = document.getElementById('testchar');
466    var t2 = document.getElementById('testchar2');
467    var text = document.getElementById('text');
468
469    var window_width;
470    var window_height;
471    if (window.innerHeight) {
472        window_width = window.innerWidth;
473        window_height = window.innerHeight;
474    }else{
475        window_width = document.documentElement.clientWidth;
476        window_height = document.documentElement.clientHeight;
477        //var z = "CI:"; for(var i in bod){z = z + " " + i;} alert(z);
478    }
479
480    char_width = t.offsetLeft / 44;
481    var avail_width = window_width-18;
482    var avail_width_mod = avail_width % char_width;
483    var x_size = (avail_width - avail_width_mod)/char_width;
484
485    char_height = t2.offsetTop - t.offsetTop;
486    var avail_height = window_height-text.offsetTop-10;
487    var avail_height_mod = avail_height % char_height;
488    var y_size = (avail_height - avail_height_mod)/char_height;
489
490    text.style.width = x_size*char_width+"px";
491    text.style.height = y_size*char_height+"px";
492
493    if( screen_x != x_size || screen_y != y_size ){
494        send_key("window resize "+x_size+" "+y_size);
495    }
496    screen_x = x_size;
497    screen_y = y_size;
498}
499
500"""
501
502ALARM_DELAY = 60
503POLL_CONNECT = 3
504MAX_COLS = 200
505MAX_ROWS = 100
506MAX_READ = 4096
507BUF_SZ = 16384
508
509_code_colours = {
510    'black':        "0",
511    'dark red':        "1",
512    'dark green':        "2",
513    'brown':        "3",
514    'dark blue':        "4",
515    'dark magenta':        "5",
516    'dark cyan':        "6",
517    'light gray':        "7",
518    'dark gray':        "8",
519    'light red':        "9",
520    'light green':        "A",
521    'yellow':        "B",
522    'light blue':        "C",
523    'light magenta':    "D",
524    'light cyan':        "E",
525    'white':        "F",
526}
527
528# replace control characters with ?'s
529_trans_table = "?" * 32 + "".join([chr(x) for x in range(32, 256)])
530
531_css_style = """
532body {    margin: 8px 8px 8px 8px; border: 0;
533    color: black; background-color: silver;
534    font-family: fixed; overflow: hidden; }
535
536form { margin: 0 0 8px 0; }
537
538#text { position: relative;
539    background-color: silver;
540    width: 100%; height: 100%;
541    margin: 3px 0 0 0; border: 1px solid #999; }
542
543#page { position: relative;  width: 100%;height: 100%;}
544"""
545
546# HTML Initial Page
547_html_page = [
548"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
549 "http://www.w3.org/TR/html4/loose.dtd">
550<html>
551<head>
552<title>Urwid Web Display - ""","""</title>
553<style type="text/css">
554""" + _css_style + """
555</style>
556</head>
557<body id="body" onload="load_web_display()">
558<div style="position:absolute; visibility:hidden;">
559<br id="br"\>
560<pre>The quick brown fox jumps over the lazy dog.<span id="testchar">X</span>
561<span id="testchar2">Y</span></pre>
562</div>
563Urwid Web Display - <b>""","""</b> -
564Status: <span id="status">Set up</span>
565<script type="text/javascript">
566//<![CDATA[
567""" + _js_code +"""
568//]]>
569</script>
570<pre id="text"></pre>
571</body>
572</html>
573"""]
574
575class Screen:
576    def __init__(self):
577        self.palette = {}
578        self.has_color = True
579        self._started = False
580
581    started = property(lambda self: self._started)
582
583    def register_palette( self, l ):
584        """Register a list of palette entries.
585
586        l -- list of (name, foreground, background) or
587             (name, same_as_other_name) palette entries.
588
589        calls self.register_palette_entry for each item in l
590        """
591
592        for item in l:
593            if len(item) in (3,4):
594                self.register_palette_entry( *item )
595                continue
596            assert len(item) == 2, "Invalid register_palette usage"
597            name, like_name = item
598            if like_name not in self.palette:
599                raise Exception("palette entry '%s' doesn't exist"%like_name)
600            self.palette[name] = self.palette[like_name]
601
602    def register_palette_entry( self, name, foreground, background,
603        mono=None):
604        """Register a single palette entry.
605
606        name -- new entry/attribute name
607        foreground -- foreground colour
608        background -- background colour
609        mono -- monochrome terminal attribute
610
611        See curses_display.register_palette_entry for more info.
612        """
613        if foreground == "default":
614            foreground = "black"
615        if background == "default":
616            background = "light gray"
617        self.palette[name] = (foreground, background, mono)
618
619    def set_mouse_tracking(self, enable=True):
620        """Not yet implemented"""
621        pass
622
623    def tty_signal_keys(self, *args, **vargs):
624        """Do nothing."""
625        pass
626
627    def start(self):
628        """
629        This function reads the initial screen size, generates a
630        unique id and handles cleanup when fn exits.
631
632        web_display.set_preferences(..) must be called before calling
633        this function for the preferences to take effect
634        """
635        global _prefs
636
637        if self._started:
638            return util.StoppingContext(self)
639
640        client_init = sys.stdin.read(50)
641        assert client_init.startswith("window resize "),client_init
642        ignore1,ignore2,x,y = client_init.split(" ",3)
643        x = int(x)
644        y = int(y)
645        self._set_screen_size( x, y )
646        self.last_screen = {}
647        self.last_screen_width = 0
648
649        self.update_method = os.environ["HTTP_X_URWID_METHOD"]
650        assert self.update_method in ("multipart","polling")
651
652        if self.update_method == "polling" and not _prefs.allow_polling:
653            sys.stdout.write("Status: 403 Forbidden\r\n\r\n")
654            sys.exit(0)
655
656        clients = glob.glob(os.path.join(_prefs.pipe_dir,"urwid*.in"))
657        if len(clients) >= _prefs.max_clients:
658            sys.stdout.write("Status: 503 Sever Busy\r\n\r\n")
659            sys.exit(0)
660
661        urwid_id = "%09d%09d"%(random.randrange(10**9),
662            random.randrange(10**9))
663        self.pipe_name = os.path.join(_prefs.pipe_dir,"urwid"+urwid_id)
664        os.mkfifo(self.pipe_name+".in",0o600)
665        signal.signal(signal.SIGTERM,self._cleanup_pipe)
666
667        self.input_fd = os.open(self.pipe_name+".in",
668            os.O_NONBLOCK | os.O_RDONLY)
669        self.input_tail = ""
670        self.content_head = ("Content-type: "
671            "multipart/x-mixed-replace;boundary=ZZ\r\n"
672            "X-Urwid-ID: "+urwid_id+"\r\n"
673            "\r\n\r\n"
674            "--ZZ\r\n")
675        if self.update_method=="polling":
676            self.content_head = (
677                "Content-type: text/plain\r\n"
678                "X-Urwid-ID: "+urwid_id+"\r\n"
679                "\r\n\r\n")
680
681        signal.signal(signal.SIGALRM,self._handle_alarm)
682        signal.alarm( ALARM_DELAY )
683        self._started = True
684
685        return util.StoppingContext(self)
686
687    def stop(self):
688        """
689        Restore settings and clean up.
690        """
691        if not self._started:
692            return
693
694        # XXX which exceptions does this actually raise? EnvironmentError?
695        try:
696            self._close_connection()
697        except Exception:
698            pass
699        signal.signal(signal.SIGTERM,signal.SIG_DFL)
700        self._cleanup_pipe()
701        self._started = False
702
703    def set_input_timeouts(self, *args):
704        pass
705
706    def run_wrapper(self,fn):
707        """
708        Run the application main loop, calling start() first
709        and stop() on exit.
710        """
711        try:
712            self.start()
713            return fn()
714        finally:
715            self.stop()
716
717
718    def _close_connection(self):
719        if self.update_method == "polling child":
720            self.server_socket.settimeout(0)
721            sock, addr = self.server_socket.accept()
722            sock.sendall("Z")
723            sock.close()
724
725        if self.update_method == "multipart":
726            sys.stdout.write("\r\nZ"
727                "\r\n--ZZ--\r\n")
728            sys.stdout.flush()
729
730    def _cleanup_pipe(self, *args):
731        if not self.pipe_name: return
732        # XXX which exceptions does this actually raise? EnvironmentError?
733        try:
734            os.remove(self.pipe_name+".in")
735            os.remove(self.pipe_name+".update")
736        except Exception:
737            pass
738
739    def _set_screen_size(self, cols, rows ):
740        """Set the screen size (within max size)."""
741
742        if cols > MAX_COLS:
743            cols = MAX_COLS
744        if rows > MAX_ROWS:
745            rows = MAX_ROWS
746        self.screen_size = cols, rows
747
748    def draw_screen(self, size, r ):
749        """Send a screen update to the client."""
750
751        (cols, rows) = size
752
753        if cols != self.last_screen_width:
754            self.last_screen = {}
755
756        sendq = [self.content_head]
757
758        if self.update_method == "polling":
759            send = sendq.append
760        elif self.update_method == "polling child":
761            signal.alarm( 0 )
762            try:
763                s, addr = self.server_socket.accept()
764            except socket.timeout:
765                sys.exit(0)
766            send = s.sendall
767        else:
768            signal.alarm( 0 )
769            send = sendq.append
770            send("\r\n")
771            self.content_head = ""
772
773        assert r.rows() == rows
774
775        if r.cursor is not None:
776            cx, cy = r.cursor
777        else:
778            cx = cy = None
779
780        new_screen = {}
781
782        y = -1
783        for row in r.content():
784            y += 1
785            row = list(row)
786
787            l = []
788
789            sig = tuple(row)
790            if y == cy: sig = sig + (cx,)
791            new_screen[sig] = new_screen.get(sig,[]) + [y]
792            old_line_numbers = self.last_screen.get(sig, None)
793            if old_line_numbers is not None:
794                if y in old_line_numbers:
795                    old_line = y
796                else:
797                    old_line = old_line_numbers[0]
798                send( "<%d\n"%old_line )
799                continue
800
801            col = 0
802            for (a, cs, run) in row:
803                run = run.translate(_trans_table)
804                if a is None:
805                    fg,bg,mono = "black", "light gray", None
806                else:
807                    fg,bg,mono = self.palette[a]
808                if y == cy and col <= cx:
809                    run_width = util.calc_width(run, 0,
810                        len(run))
811                    if col+run_width > cx:
812                        l.append(code_span(run, fg, bg,
813                            cx-col))
814                    else:
815                        l.append(code_span(run, fg, bg))
816                    col += run_width
817                else:
818                    l.append(code_span(run, fg, bg))
819
820            send("".join(l)+"\n")
821        self.last_screen = new_screen
822        self.last_screen_width = cols
823
824        if self.update_method == "polling":
825            sys.stdout.write("".join(sendq))
826            sys.stdout.flush()
827            sys.stdout.close()
828            self._fork_child()
829        elif self.update_method == "polling child":
830            s.close()
831        else: # update_method == "multipart"
832            send("\r\n--ZZ\r\n")
833            sys.stdout.write("".join(sendq))
834            sys.stdout.flush()
835
836        signal.alarm( ALARM_DELAY )
837
838
839    def clear(self):
840        """
841        Force the screen to be completely repainted on the next
842        call to draw_screen().
843
844        (does nothing for web_display)
845        """
846        pass
847
848
849    def _fork_child(self):
850        """
851        Fork a child to run CGI disconnected for polling update method.
852        Force parent process to exit.
853        """
854        daemonize( self.pipe_name +".err" )
855        self.input_fd = os.open(self.pipe_name+".in",
856            os.O_NONBLOCK | os.O_RDONLY)
857        self.update_method = "polling child"
858        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
859        s.bind( self.pipe_name+".update" )
860        s.listen(1)
861        s.settimeout(POLL_CONNECT)
862        self.server_socket = s
863
864    def _handle_alarm(self, sig, frame):
865        assert self.update_method in ("multipart","polling child")
866        if self.update_method == "polling child":
867            # send empty update
868            try:
869                s, addr = self.server_socket.accept()
870                s.close()
871            except socket.timeout:
872                sys.exit(0)
873        else:
874            # send empty update
875            sys.stdout.write("\r\n\r\n--ZZ\r\n")
876            sys.stdout.flush()
877        signal.alarm( ALARM_DELAY )
878
879
880    def get_cols_rows(self):
881        """Return the screen size."""
882        return self.screen_size
883
884    def get_input(self, raw_keys=False):
885        """Return pending input as a list."""
886        l = []
887        resized = False
888
889        try:
890            iready,oready,eready = select.select(
891                [self.input_fd],[],[],0.5)
892        except select.error as e:
893            # return on interruptions
894            if e.args[0] == 4:
895                if raw_keys:
896                    return [],[]
897                return []
898            raise
899
900        if not iready:
901            if raw_keys:
902                return [],[]
903            return []
904
905        keydata = os.read(self.input_fd, MAX_READ)
906        os.close(self.input_fd)
907        self.input_fd = os.open(self.pipe_name+".in",
908            os.O_NONBLOCK | os.O_RDONLY)
909        #sys.stderr.write( repr((keydata,self.input_tail))+"\n" )
910        keys = keydata.split("\n")
911        keys[0] = self.input_tail + keys[0]
912        self.input_tail = keys[-1]
913
914        for k in keys[:-1]:
915            if k.startswith("window resize "):
916                ign1,ign2,x,y = k.split(" ",3)
917                x = int(x)
918                y = int(y)
919                self._set_screen_size(x, y)
920                resized = True
921            else:
922                l.append(k)
923        if resized:
924            l.append("window resize")
925
926        if raw_keys:
927            return l, []
928        return l
929
930
931def code_span( s, fg, bg, cursor = -1):
932    code_fg = _code_colours[ fg ]
933    code_bg = _code_colours[ bg ]
934
935    if cursor >= 0:
936        c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor)
937        c2_off = util.move_next_char(s, c_off, len(s))
938
939        return ( code_fg + code_bg + s[:c_off] + "\n" +
940             code_bg + code_fg + s[c_off:c2_off] + "\n" +
941             code_fg + code_bg + s[c2_off:] + "\n")
942    else:
943        return code_fg + code_bg + s + "\n"
944
945
946def html_escape(text):
947    """Escape text so that it will be displayed safely within HTML"""
948    text = text.replace('&','&amp;')
949    text = text.replace('<','&lt;')
950    text = text.replace('>','&gt;')
951    return text
952
953
954def is_web_request():
955    """
956    Return True if this is a CGI web request.
957    """
958    return 'REQUEST_METHOD' in os.environ
959
960def handle_short_request():
961    """
962    Handle short requests such as passing keystrokes to the application
963    or sending the initial html page.  If returns True, then this
964    function recognised and handled a short request, and the calling
965    script should immediately exit.
966
967    web_display.set_preferences(..) should be called before calling this
968    function for the preferences to take effect
969    """
970    global _prefs
971
972    if not is_web_request():
973        return False
974
975    if os.environ['REQUEST_METHOD'] == "GET":
976        # Initial request, send the HTML and javascript.
977        sys.stdout.write("Content-type: text/html\r\n\r\n" +
978            html_escape(_prefs.app_name).join(_html_page))
979        return True
980
981    if os.environ['REQUEST_METHOD'] != "POST":
982        # Don't know what to do with head requests etc.
983        return False
984
985    if 'HTTP_X_URWID_ID' not in os.environ:
986        # If no urwid id, then the application should be started.
987        return False
988
989    urwid_id = os.environ['HTTP_X_URWID_ID']
990    if len(urwid_id)>20:
991        #invalid. handle by ignoring
992        #assert 0, "urwid id too long!"
993        sys.stdout.write("Status: 414 URI Too Long\r\n\r\n")
994        return True
995    for c in urwid_id:
996        if c not in "0123456789":
997            # invald. handle by ignoring
998            #assert 0, "invalid chars in id!"
999            sys.stdout.write("Status: 403 Forbidden\r\n\r\n")
1000            return True
1001
1002    if os.environ.get('HTTP_X_URWID_METHOD',None) == "polling":
1003        # this is a screen update request
1004        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1005        try:
1006            s.connect( os.path.join(_prefs.pipe_dir,
1007                "urwid"+urwid_id+".update") )
1008            data = "Content-type: text/plain\r\n\r\n"+s.recv(BUF_SZ)
1009            while data:
1010                sys.stdout.write(data)
1011                data = s.recv(BUF_SZ)
1012            return True
1013        except socket.error:
1014            sys.stdout.write("Status: 404 Not Found\r\n\r\n")
1015            return True
1016
1017    # this is a keyboard input request
1018    try:
1019        fd = os.open((os.path.join(_prefs.pipe_dir,
1020            "urwid"+urwid_id+".in")), os.O_WRONLY)
1021    except OSError:
1022        sys.stdout.write("Status: 404 Not Found\r\n\r\n")
1023        return True
1024
1025    # FIXME: use the correct encoding based on the request
1026    keydata = sys.stdin.read(MAX_READ)
1027    os.write(fd,keydata.encode('ascii'))
1028    os.close(fd)
1029    sys.stdout.write("Content-type: text/plain\r\n\r\n")
1030
1031    return True
1032
1033
1034class _Preferences:
1035    app_name = "Unnamed Application"
1036    pipe_dir = "/tmp"
1037    allow_polling = True
1038    max_clients = 20
1039
1040_prefs = _Preferences()
1041
1042def set_preferences( app_name, pipe_dir="/tmp", allow_polling=True,
1043    max_clients=20 ):
1044    """
1045    Set web_display preferences.
1046
1047    app_name -- application name to appear in html interface
1048    pipe_dir -- directory for input pipes, daemon update sockets
1049                and daemon error logs
1050    allow_polling -- allow creation of daemon processes for
1051                     browsers without multipart support
1052    max_clients -- maximum concurrent client connections. This
1053               pool is shared by all urwid applications
1054               using the same pipe_dir
1055    """
1056    global _prefs
1057    _prefs.app_name = app_name
1058    _prefs.pipe_dir = pipe_dir
1059    _prefs.allow_polling = allow_polling
1060    _prefs.max_clients = max_clients
1061
1062
1063class ErrorLog:
1064    def __init__(self, errfile ):
1065        self.errfile = errfile
1066    def write(self, err):
1067        open(self.errfile,"a").write(err)
1068
1069
1070def daemonize( errfile ):
1071    """
1072    Detach process and become a daemon.
1073    """
1074    pid = os.fork()
1075    if pid:
1076        os._exit(0)
1077
1078    os.setsid()
1079    signal.signal(signal.SIGHUP, signal.SIG_IGN)
1080    os.umask(0)
1081
1082    pid = os.fork()
1083    if pid:
1084        os._exit(0)
1085
1086    os.chdir("/")
1087    for fd in range(0,20):
1088        try:
1089            os.close(fd)
1090        except OSError:
1091            pass
1092
1093    sys.stdin = open("/dev/null","r")
1094    sys.stdout = open("/dev/null","w")
1095    sys.stderr = ErrorLog( errfile )
1096
1097