diff --git a/share/tools/web_config/index.html b/share/tools/web_config/index.html index a16b44ce0..cd167f3ee 100644 --- a/share/tools/web_config/index.html +++ b/share/tools/web_config/index.html @@ -124,6 +124,8 @@ body { /* Make our border overlap the detail, even if we're unselected (so it doesn't jump when selected) */ position: relative; left: 1px; + border-bottom-style: solid; + border-bottom-width: 0px; } .selected_master_elem { @@ -143,9 +145,32 @@ body { .master_element_text { text-decoration: none; padding-bottom: 1px; - border-bottom: 1px solid white; + border-bottom-style: inherit; + border-bottom-color: inherit; + border-bottom-width: 1px; } +.master_element_description { + text-decoration: none; + padding-top: 15px; + font-size: 10pt; + border-bottom-style: inherit; + border-bottom-color: inherit; + border-bottom-width: 1px; + display: none; +} + +.selected_master_elem > .master_element_description { + display: inline; +} + +/* We have a newline between the label and description; hide it initially, but show it when it's selected */ +.master_element > br { display: none; } +.selected_master_elem > br { display: inherit; } + +/* Set this class to suppress the border bottom on master_element_texts with visible descriptions */ +.master_element_no_border { border-bottom-width: 0 } + #colorpicker_term256 { border: solid #444 1px; } @@ -416,10 +441,12 @@ function switch_tab(new_tab) { /* Keep track of whether this is the first element */ var first = true run_get_request('/colors/', function(key_and_values){ + /* Result is name, description, value */ var key = key_and_values[0] - var style = new Style(key_and_values[1]) + var description = key_and_values[1] + var style = new Style(key_and_values[2]) style_map[key] = style - elem = create_master_element(key, style.color, '', select_color_master_element) + elem = create_master_element(key, description, style.color, '', select_color_master_element) if (first) { /* It's the first element, so select it, so something gets selected */ select_color_master_element(elem) @@ -432,7 +459,7 @@ function switch_tab(new_tab) { /* Keep track of whether this is the first element */ var first = true run_get_request('/functions/', function(contents){ - elem = create_master_element(contents, 'AAAAAA', '11pt', select_function_master_element) + elem = create_master_element(contents, false/* description */, 'AAAAAA', '11pt', select_function_master_element) if (first) { /* It's the first element, so select it, so something gets selected */ select_function_master_element(elem) @@ -493,6 +520,7 @@ function reflect_style() { /* Unselect everything */ $('.colorpicker_cell_selected').removeClass('colorpicker_cell_selected') $('.modifier_cell_selected').removeClass('modifier_cell_selected') + $('.master_element_no_border').removeClass('master_element_no_border') /* Now update the color picker with the current style (if we have one) */ style = current_style() @@ -519,7 +547,16 @@ function reflect_style() { /* In the master list, ensure the color is visible against the dark background. If we're deselecting, use COLOR_NORMAL */ master_color = style.color ? master_color_for_color(style.color) : COLOR_NORMAL - $('.selected_master_elem').children('.master_element_text').css({'color': master_color, 'border-bottom-color': master_color}) + //$('.selected_master_elem').children('.master_element_text').css({'color': master_color, 'border-bottom-color': master_color}) + + var selected_elem = $('.selected_master_elem'); + var desc_elems = selected_elem.children('.master_element_description') + selected_elem.css({'color': master_color}) + selected_elem.children().css({'border-bottom-color': master_color}) + if (desc_elems.length) { + /* We have a description element, so hide the bottom border of the master element */ + selected_elem.children('.master_element_text').addClass('master_element_no_border') + } } } @@ -908,33 +945,51 @@ var show_labels = 0 var COLOR_NORMAL = 'CCC' /* Adds a new element to master */ -function create_master_element(contents, color, font_size, click_handler) { +function create_master_element(contents, description_or_false, color, font_size, click_handler) { /* In the master list, ensure the color is visible against the dark background */ var master_color = color ? master_color_for_color(color) : COLOR_NORMAL - var style_str = 'color: #' + master_color + '; border-bottom: 1px solid #' + master_color + ' ;' + var master_style = 'color: #' + master_color + var master_children_style = 'border-bottom-color: #' + master_color + var text_style = '' if (font_size.length > 0) { - style_str += 'font-size: ' + font_size + ';' + text_style += 'font-size: ' + font_size + ';' } if (contents.length >= 20) { - style_str += 'letter-spacing:-2px;' + text_style += 'letter-spacing:-2px;' } elem = $('
', { class: 'master_element', id: 'master_' + contents, + style: master_style, click: function(){ click_handler(this) } }).append( $("", { class: 'master_element_text', - style: style_str, + style: text_style, text: contents, }) ) + /* Append description if we have one */ + if (description_or_false) { + /* Newline between label and description */ + elem.append($('
')) + elem.append( + $('', { + class: 'master_element_description', + text: description_or_false + }) + ) + } + + /* Update border color of the master element's children */ + elem.children().css(master_children_style) + elem.appendTo('#master') return elem } diff --git a/share/tools/web_config/webconfig.py b/share/tools/web_config/webconfig.py index 20e5514fa..1acf64a97 100755 --- a/share/tools/web_config/webconfig.py +++ b/share/tools/web_config/webconfig.py @@ -7,248 +7,274 @@ import subprocess import re, json, socket, os, sys, cgi, select def run_fish_cmd(text): - from subprocess import PIPE - p = subprocess.Popen(["fish"], stdin=PIPE, stdout=PIPE, stderr=PIPE) - out, err = p.communicate(text) - return out, err + from subprocess import PIPE + p = subprocess.Popen(["fish"], stdin=PIPE, stdout=PIPE, stderr=PIPE) + out, err = p.communicate(text) + return out, err named_colors = { - 'black' : '000000', - 'red' : 'FF0000', - 'green' : '00FF00', - 'brown' : '725000', - 'yellow' : 'FFFF00', - 'blue' : '0000FF', - 'magenta' : 'FF00FF', - 'purple' : 'FF00FF', - 'cyan' : '00FFFF', - 'white' : 'FFFFFF' + 'black' : '000000', + 'red' : 'FF0000', + 'green' : '00FF00', + 'brown' : '725000', + 'yellow' : 'FFFF00', + 'blue' : '0000FF', + 'magenta' : 'FF00FF', + 'purple' : 'FF00FF', + 'cyan' : '00FFFF', + 'white' : 'FFFFFF' } def parse_one_color(comp): - """ A basic function to parse a single color value like 'FFA000' """ - if comp in named_colors: - # Named color - return named_colors[comp] - elif re.match(r"[0-9a-fA-F]{3}", comp) is not None or re.match(r"[0-9a-fA-F]{6}", comp) is not None: - # Hex color - return comp - else: - # Unknown - return '' + """ A basic function to parse a single color value like 'FFA000' """ + if comp in named_colors: + # Named color + return named_colors[comp] + elif re.match(r"[0-9a-fA-F]{3}", comp) is not None or re.match(r"[0-9a-fA-F]{6}", comp) is not None: + # Hex color + return comp + else: + # Unknown + return '' def parse_color(color_str): - """ A basic function to parse a color string, for example, 'red' '--bold' """ - comps = color_str.split(' ') - color = 'normal' - background_color = '' - bold, underline = False, False - for comp in comps: - # Remove quotes - comp = comp.strip("'\" ") - if comp == '--bold': - bold = True - elif comp == '--underline': - underline = True - elif comp.startswith('--background='): - # Background color - background_color = parse_one_color(comp[len('--background='):]) - else: - # Regular color - maybe_color = parse_one_color(comp) - if maybe_color: color = maybe_color - - return [color, background_color, bold, underline] - - + """ A basic function to parse a color string, for example, 'red' '--bold' """ + comps = color_str.split(' ') + color = 'normal' + background_color = '' + bold, underline = False, False + for comp in comps: + # Remove quotes + comp = comp.strip("'\" ") + if comp == '--bold': + bold = True + elif comp == '--underline': + underline = True + elif comp.startswith('--background='): + # Background color + background_color = parse_one_color(comp[len('--background='):]) + else: + # Regular color + maybe_color = parse_one_color(comp) + if maybe_color: color = maybe_color + + return [color, background_color, bold, underline] + + def parse_bool(val): - val = val.lower() - if val.startswith('f') or val.startswith('0'): return False - if val.startswith('t') or val.startswith('1'): return True - return bool(val) + val = val.lower() + if val.startswith('f') or val.startswith('0'): return False + if val.startswith('t') or val.startswith('1'): return True + return bool(val) class FishVar: - """ A class that represents a variable """ - def __init__(self, name, value): - self.name = name - self.value = value - self.universal = False - self.exported = False - - def get_json_obj(self): - # Return an array(3): name, value, flags - flags = [] - if self.universal: flags.append('universal') - if self.exported: flags.append('exported') - return [self.name, self.value, ', '.join(flags)] + """ A class that represents a variable """ + def __init__(self, name, value): + self.name = name + self.value = value + self.universal = False + self.exported = False + + def get_json_obj(self): + # Return an array(3): name, value, flags + flags = [] + if self.universal: flags.append('universal') + if self.exported: flags.append('exported') + return [self.name, self.value, ', '.join(flags)] class FishConfigHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): - - def do_get_colors(self): - "Look for fish_color_*" - result = [] - remaining = set(['normal', - 'error', - 'command', - 'end', - 'param', - 'comment', - 'match', - 'search_match', - 'operator', - 'escape', - 'quote', - 'redirection', - 'valid_path', - 'autosuggestion' - ]) - out, err = run_fish_cmd('set -L') - for line in out.split('\n'): - for match in re.finditer(r"^fish_color_(\S+) ?(.*)", line): - color_name, color_value = [x.strip() for x in match.group(1, 2)] - result.append([color_name, parse_color(color_value)]) - remaining.discard(color_name) - - # Ensure that we have all the color names we know about, so that if the - # user deletes one he can still set it again via the web interface - for x in remaining: - result.append([x, parse_color('')]) - - # Sort our result (by their keys) - result.sort() - - return result - - def do_get_functions(self): - out, err = run_fish_cmd('functions') - out = out.strip() - - # Not sure why fish sometimes returns this with newlines - if "\n" in out: - return out.split('\n') - else: - return out.strip().split(', ') - - def do_get_variable_names(self, cmd): - " Given a command like 'set -U' return all the variable names " - out, err = run_fish_cmd(cmd) - return out.split('\n') - - def do_get_variables(self): - out, err = run_fish_cmd('set -L') - - # Put all the variables into a dictionary - vars = {} - for line in out.split('\n'): - comps = line.split(' ', 1) - if len(comps) < 2: continue - fish_var = FishVar(comps[0], comps[1]) - vars[fish_var.name] = fish_var - - # Mark universal variables. L means don't abbreviate. - for name in self.do_get_variable_names('set -nUL'): - if name in vars: vars[name].universal = True - # Mark exported variables. L means don't abbreviate. - for name in self.do_get_variable_names('set -nxL'): - if name in vars: vars[name].exported = True - - return [vars[key].get_json_obj() for key in sorted(vars.keys(), key=str.lower)] - - def do_get_history(self): - # Use \x1e ("record separator") to distinguish between history items. The first - # backslash is so Python passes one backslash to fish - out, err = run_fish_cmd('for val in $history; echo -n $val \\x1e; end') - result = out.split('\x1e') - if result: result.pop() - return result - + def do_get_colors(self): + # Looks for fish_color_*. + # Returns an array of lists [color_name, color_description, color_value] + result = [] + + # Make sure we return at least these + remaining = set(['normal', + 'error', + 'command', + 'end', + 'param', + 'comment', + 'match', + 'search_match', + 'operator', + 'escape', + 'quote', + 'redirection', + 'valid_path', + 'autosuggestion' + ]) + + # Here are our color descriptions + descriptions = { + 'normal': 'Default text', + 'command': 'Ordinary commands', + 'quote': 'Text within quotes', + 'redirection': 'Like | and >', + 'end': 'Like ; and &', + 'error': 'Potential errors', + 'param': 'Command parameters', + 'comment': 'Comments start with #', + 'match': 'Matching parenthesis', + 'search_match': 'History searching', + 'history_current': 'Directory history', + 'operator': 'Like * and ~', + 'escape': 'Escapes like \\n', + 'cwd': 'Current directory', + 'cwd_root': 'cwd for root user', + 'valid_path': 'Valid paths', + 'autosuggestion': 'Suggested completion' + } + + out, err = run_fish_cmd('set -L') + for line in out.split('\n'): + for match in re.finditer(r"^fish_color_(\S+) ?(.*)", line): + color_name, color_value = [x.strip() for x in match.group(1, 2)] + color_desc = descriptions.get(color_name, '') + result.append([color_name, color_desc, parse_color(color_value)]) + remaining.discard(color_name) + + # Ensure that we have all the color names we know about, so that if the + # user deletes one he can still set it again via the web interface + for color_name in remaining: + color_desc = descriptions.get(color_name, '') + result.append([color_name, color_desc, parse_color('')]) + + # Sort our result (by their keys) + result.sort() + + return result + + def do_get_functions(self): + out, err = run_fish_cmd('functions') + out = out.strip() + + # Not sure why fish sometimes returns this with newlines + if "\n" in out: + return out.split('\n') + else: + return out.strip().split(', ') + + def do_get_variable_names(self, cmd): + " Given a command like 'set -U' return all the variable names " + out, err = run_fish_cmd(cmd) + return out.split('\n') + + def do_get_variables(self): + out, err = run_fish_cmd('set -L') + + # Put all the variables into a dictionary + vars = {} + for line in out.split('\n'): + comps = line.split(' ', 1) + if len(comps) < 2: continue + fish_var = FishVar(comps[0], comps[1]) + vars[fish_var.name] = fish_var + + # Mark universal variables. L means don't abbreviate. + for name in self.do_get_variable_names('set -nUL'): + if name in vars: vars[name].universal = True + # Mark exported variables. L means don't abbreviate. + for name in self.do_get_variable_names('set -nxL'): + if name in vars: vars[name].exported = True + + return [vars[key].get_json_obj() for key in sorted(vars.keys(), key=str.lower)] + + def do_get_history(self): + # Use \x1e ("record separator") to distinguish between history items. The first + # backslash is so Python passes one backslash to fish + out, err = run_fish_cmd('for val in $history; echo -n $val \\x1e; end') + result = out.split('\x1e') + if result: result.pop() + return result + - def do_get_color_for_variable(self, name): - "Return the color with the given name, or the empty string if there is none" - out, err = run_fish_cmd("echo -n $" + name) - return out - - def do_set_color_for_variable(self, name, color, background_color, bold, underline): - if not color: color = 'normal' - "Sets a color for a fish color name, like 'autosuggestion'" - command = 'set -U fish_color_' + name - if color: command += ' ' + color - if background_color: command += ' --background=' + background_color - if bold: command += ' --bold' - if underline: command += ' --underline' - - out, err = run_fish_cmd(command) - return out - - def do_get_function(self, func_name): - out, err = run_fish_cmd('functions ' + func_name) - return out - - def do_GET(self): - p = self.path - if p == '/colors/': - output = self.do_get_colors() - elif p == '/functions/': - output = self.do_get_functions() - elif p == '/variables/': - output = self.do_get_variables() - elif p == '/history/': - output = self.do_get_history() - elif re.match(r"/color/(\w+)/", p): - name = re.match(r"/color/(\w+)/", p).group(1) - output = self.do_get_color_for_variable(name) - else: - return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) - - # Return valid output - self.send_response(200) - self.send_header('Content-type','text/html') - self.wfile.write('\n') - - # Output JSON - json.dump(output, self.wfile) - - def do_POST(self): - p = self.path - ctype, pdict = cgi.parse_header(self.headers.getheader('content-type')) - if ctype == 'multipart/form-data': - postvars = cgi.parse_multipart(self.rfile, pdict) - elif ctype == 'application/x-www-form-urlencoded': - length = int(self.headers.getheader('content-length')) - postvars = cgi.parse_qs(self.rfile.read(length), keep_blank_values=1) - else: - postvars = {} - - if p == '/set_color/': - what = postvars.get('what') - color = postvars.get('color') - background_color = postvars.get('background_color') - bold = postvars.get('bold') - underline = postvars.get('underline') - if what: - # Not sure why we get lists here? - output = self.do_set_color_for_variable(what[0], color[0], background_color[0], parse_bool(bold[0]), parse_bool(underline[0])) - else: - output = 'Bad request' - elif p == '/get_function/': - what = postvars.get('what') - output = [self.do_get_function(what[0])] - else: - return SimpleHTTPServer.SimpleHTTPRequestHandler.do_POST(self) - - # Return valid output - self.send_response(200) - self.send_header('Content-type','text/html') - self.wfile.write('\n') - - # Output JSON - json.dump(output, self.wfile) + def do_get_color_for_variable(self, name): + "Return the color with the given name, or the empty string if there is none" + out, err = run_fish_cmd("echo -n $" + name) + return out + + def do_set_color_for_variable(self, name, color, background_color, bold, underline): + if not color: color = 'normal' + "Sets a color for a fish color name, like 'autosuggestion'" + command = 'set -U fish_color_' + name + if color: command += ' ' + color + if background_color: command += ' --background=' + background_color + if bold: command += ' --bold' + if underline: command += ' --underline' + + out, err = run_fish_cmd(command) + return out + + def do_get_function(self, func_name): + out, err = run_fish_cmd('functions ' + func_name) + return out + + def do_GET(self): + p = self.path + if p == '/colors/': + output = self.do_get_colors() + elif p == '/functions/': + output = self.do_get_functions() + elif p == '/variables/': + output = self.do_get_variables() + elif p == '/history/': + output = self.do_get_history() + elif re.match(r"/color/(\w+)/", p): + name = re.match(r"/color/(\w+)/", p).group(1) + output = self.do_get_color_for_variable(name) + else: + return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) + + # Return valid output + self.send_response(200) + self.send_header('Content-type','text/html') + self.wfile.write('\n') + + # Output JSON + json.dump(output, self.wfile) + + def do_POST(self): + p = self.path + ctype, pdict = cgi.parse_header(self.headers.getheader('content-type')) + if ctype == 'multipart/form-data': + postvars = cgi.parse_multipart(self.rfile, pdict) + elif ctype == 'application/x-www-form-urlencoded': + length = int(self.headers.getheader('content-length')) + postvars = cgi.parse_qs(self.rfile.read(length), keep_blank_values=1) + else: + postvars = {} + + if p == '/set_color/': + what = postvars.get('what') + color = postvars.get('color') + background_color = postvars.get('background_color') + bold = postvars.get('bold') + underline = postvars.get('underline') + if what: + # Not sure why we get lists here? + output = self.do_set_color_for_variable(what[0], color[0], background_color[0], parse_bool(bold[0]), parse_bool(underline[0])) + else: + output = 'Bad request' + elif p == '/get_function/': + what = postvars.get('what') + output = [self.do_get_function(what[0])] + else: + return SimpleHTTPServer.SimpleHTTPRequestHandler.do_POST(self) + + # Return valid output + self.send_response(200) + self.send_header('Content-type','text/html') + self.wfile.write('\n') + + # Output JSON + json.dump(output, self.wfile) - def log_request(self, code='-', size='-'): - """ Disable request logging """ - pass + def log_request(self, code='-', size='-'): + """ Disable request logging """ + pass # Make sure that the working directory is the one that contains the script server file, # because the document root is the working directory @@ -258,23 +284,23 @@ os.chdir(where) # Try to find a suitable port PORT = 8000 while PORT <= 9000: - try: - Handler = FishConfigHTTPRequestHandler - httpd = SocketServer.TCPServer(("", PORT), Handler) - # Success - break; - except socket.error: - type, value = sys.exc_info()[:2] - if 'Address already in use' not in value: - break - PORT += 1 + try: + Handler = FishConfigHTTPRequestHandler + httpd = SocketServer.TCPServer(("", PORT), Handler) + # Success + break; + except socket.error: + type, value = sys.exc_info()[:2] + if 'Address already in use' not in value: + break + PORT += 1 if PORT > 9000: - # Nobody say it - print "Unable to find an open port between 8000 and 9000" - sys.exit(-1) + # Nobody say it + print "Unable to find an open port between 8000 and 9000" + sys.exit(-1) - + url = 'http://localhost:%d' % PORT print "Web config started at '%s'. Hit enter to stop." % url @@ -283,11 +309,11 @@ webbrowser.open(url) # Select on stdin and httpd stdin_no = sys.stdin.fileno() while True: - ready_read, _, _ = select.select([sys.stdin.fileno(), httpd.fileno()], [], []) - if stdin_no in ready_read: - print "Shutting down." - # Consume the newline so it doesn't get printed by the caller - sys.stdin.readline() - break - else: - httpd.handle_request() + ready_read, _, _ = select.select([sys.stdin.fileno(), httpd.fileno()], [], []) + if stdin_no in ready_read: + print "Shutting down." + # Consume the newline so it doesn't get printed by the caller + sys.stdin.readline() + break + else: + httpd.handle_request()