vnc: Add license
diff --git a/tools/update-headers b/tools/update-headers
new file mode 100755
index 0000000..2e700e2
--- /dev/null
+++ b/tools/update-headers
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+#
+# update-headers - Updates the header comment in source files
+# Copyright (C) 2013  Lorenzo Villani
+# Updated for Python3 by Steve Kondik (part of UChroma)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+from argparse import ArgumentParser
+from functools import partial
+from os import chmod, stat, walk
+from os.path import isdir, isfile, join, splitext
+from shutil import copyfile
+from tempfile import NamedTemporaryFile
+
+
+# File extension -> comment string
+COMMENT_STRING = {
+    ".c":     "//",
+    ".cc":    "//",
+    ".cpp":   "//",
+    ".el":    ";;;;",
+    ".h":     "//",
+    ".hh":    "//",
+    ".hpp":   "//",
+    ".hs":    "--",
+    ".java":  "//",
+    ".js":    "//",
+    ".li":    ";;;;",
+    ".m":     "//",
+    ".mm":    "//",
+    ".py":    "#",
+    ".swift": "//",
+    ".yml":   "#",
+}
+
+
+def main():
+    # Parse command line
+    arg_parser = ArgumentParser()
+    arg_parser.add_argument("-c", "--comment-string", default="#")
+    arg_parser.add_argument("header", nargs=1)
+    arg_parser.add_argument("path", nargs="+")
+
+    args = arg_parser.parse_args()
+
+    # Input stream for the header boilerplate
+    header = open(args.header[0], "r")
+
+    # Process files and directories
+    for path in args.path:
+        if isfile(path):
+            update_file(path, header, args.comment_string)
+        elif isdir(path):
+            update_directory(path, header, args.comment_string)
+
+
+def update_file(path, header, comment_string):
+    """
+    Updates the header boilerplate for a single file.
+
+    :param path: A file path.
+    """
+    original_stat = stat(path)
+    input_stream = open(path, "r")
+    tempfile = NamedTemporaryFile(mode="w")
+
+    update_header(input_stream, header, tempfile, comment_string)
+    tempfile.flush()
+
+    copyfile(tempfile.name, path)
+    chmod(path, original_stat.st_mode)
+
+
+def update_directory(path, header, comment_string):
+    """
+    Recursively updates the header boilerplate for all recognized files below
+    the specified directory.
+
+    :param path: A directory path.
+    """
+    def is_recognized(entry):
+        return isfile(entry) and splitext(entry)[1] in COMMENT_STRING
+
+    def visit(_, directory, files):
+        entries = list(map(partial(join, directory), files))
+        applicable = list(filter(is_recognized, entries))
+
+        for path in applicable:
+            ext = splitext(path)[1]
+
+            update_file(path, header, COMMENT_STRING[ext])
+
+    for dirpath, dirnames, filenames in walk(path):
+        visit(None, dirpath, dirnames + filenames)
+
+
+def update_header(input_stream, new_header, output_stream, comment_string="#"):
+    """
+    Replaces or inserts a new header in a file.
+
+    @param input_stream: The input file with the old (or missing) header.
+    @type input_stream: file
+
+    @param new_header: Input stream of the new header.
+    @type new_header: file
+
+    @param output_stream: Output stream for the new file with the header
+        replaced.
+    @type output_stream: file
+
+    @param comment_string: The string used to start a comment which spans until
+        the end of the line.
+    @type comment_string: str
+    """
+    class State:
+        Start, FoundHeaderStart, Done = list(range(3))
+
+    state = State.Start
+
+    for line in input_stream:
+        if state == State.Start:
+            # At the beginning of the file.
+            if line.startswith("#!"):
+                # Ignore the shebang and copy this line as-is. No state change.
+                output_stream.write(line)
+            elif line.startswith(comment_string):
+                # Start -> FoundHeaderStart.
+                state = State.FoundHeaderStart
+            else:
+                # Inject header then state transition: Start -> Done.
+                inject_header(new_header, output_stream, comment_string)
+                output_stream.write(line)
+
+                state = State.Done
+        elif state == State.FoundHeaderStart:
+            # We have found the beginning of the header comment, now we have to
+            # look for the first non comment line, then inject our header,
+            # then transition from FoundHeaderStart -> Done.
+            if not line.startswith(comment_string):
+                inject_header(new_header, output_stream, comment_string)
+                output_stream.write(line)
+                state = State.Done
+        elif state == State.Done:
+            # Copy input to output verbatim
+            output_stream.write(line)
+    else:
+        if state == State.Start or state == State.FoundHeaderStart:
+            # Input is empty, inject the header and be done with it
+            inject_header(new_header, output_stream, comment_string)
+
+            state = State.Done
+
+    output_stream.flush()
+
+
+def inject_header(header_stream, output_stream, comment_string):
+    """
+    Writes the header onto the output stream.
+
+    @param header_stream: Input stream for the header. A seek to the beginning
+        of the file is performed with every call.
+    @type header_stream: file
+
+    @param output_stream: The stream where the header will be written.
+    @type output_stream: file
+
+    @param comment_string: The string used to start a comment which spans until
+        the end of the line.
+    @type comment_string: str
+    """
+    header_stream.seek(0)
+
+    for line in header_stream:
+        if line.strip() == "":
+            output_stream.write(comment_string + "\n")
+        else:
+            output_stream.write(comment_string + " " + line.rstrip(' '))
+
+if __name__ == "__main__":
+    main()