diff options
author | Mattias Andrée <m@maandree.se> | 2025-02-23 16:49:44 +0100 |
---|---|---|
committer | Mattias Andrée <m@maandree.se> | 2025-02-23 16:49:44 +0100 |
commit | c351d3aed0671ad17a75439fb45a0fc5c2e00280 (patch) | |
tree | 0a184ea39a2a5b29e4dbd0d5b18e914d8f1af96c | |
parent | Add old issues into TODO file (diff) | |
download | nightshift-c351d3aed0671ad17a75439fb45a0fc5c2e00280.tar.gz nightshift-c351d3aed0671ad17a75439fb45a0fc5c2e00280.tar.bz2 nightshift-c351d3aed0671ad17a75439fb45a0fc5c2e00280.tar.xz |
Whoops, missed this file0.6.1
Signed-off-by: Mattias Andrée <m@maandree.se>
-rwxr-xr-x | __main__.py | 923 |
1 files changed, 923 insertions, 0 deletions
diff --git a/__main__.py b/__main__.py new file mode 100755 index 0000000..d0ac5eb --- /dev/null +++ b/__main__.py @@ -0,0 +1,923 @@ +#!/usr/bin/env python3 +# See LICENSE file for copyright and license details. +copyright=''' +ISC License + +© 2014, 2025 Mattias Andrée <m@maandree.se> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +''' + +import os +import sys +import socket +import signal +import threading +from subprocess import Popen, PIPE + + +PROGRAM_NAME = 'nightshift' +''' +:str The name of the program +''' + +PROGRAM_VERSION = '0.4' +''' +:str The version of the program +''' + + +## Set process title +def setproctitle(title): + ''' + Set process title + + @param title:str The title of the process + ''' + import ctypes + try: + # Remove path, keep only the file, + # otherwise we get really bad effects, namely + # the name title is truncates by the number + # of slashes in the title. At least that is + # the observed behaviour when using procps-ng. + title = title.split('/')[-1] + # Create strng buffer with title + title = title.encode(sys.getdefaultencoding(), 'replace') + title = ctypes.create_string_buffer(title) + if 'linux' in sys.platform: + # Set process title on Linux + libc = ctypes.cdll.LoadLibrary('libc.so.6') + libc.prctl(15, ctypes.byref(title), 0, 0, 0) + elif 'bsd' in sys.platform: + # Set process title on at least FreeBSD + libc = ctypes.cdll.LoadLibrary('libc.so.7') + libc.setproctitle(ctypes.create_string_buffer(b'-%s'), title) + except: + pass +setproctitle(sys.argv[0]) + + +backlog = 5 +''' +:int The size of the server socket's backlog +''' + +red_args = None +''' +:list<str>? Raw arguments passed to redshift +''' + +red_opts = ['-v'] +''' +:list<str> Nightshift parsed options passed to redshift +''' + +daemon = 0 +''' +:int Whether or not to run as daemon, 2 if revived +''' + +kill = 0 +''' +:int Whether or not to kill the redshift and the nightshift daemon, + 0 for no, 1 for yes, 2 for immediately +''' + +toggle = False +''' +:bool Whether or not to toggle redshift +''' + +set_status = None +''' +:bool? `True` if redshift should be enabled, `False` for disble, otherwise `None` +''' + +set_freeze = None +''' +:bool? `True` if redshift should be frozen, `False` for thawed, otherwise `None` +''' + +status = False +''' +:bool Whether or not to get the current status +''' + +conf_opts = [] +''' +:list<str> This list will always have at least one element. This list is filled + with options passed to the configurations, with the first element + being the configuration file +''' + +config_file = None +''' +:str? The configuration file, same as the first element in `conf_opts` +''' + + +## Parse options +add_to_red_opts = False +reading_conf_opts = False +for arg in sys.argv[1:]: + if add_to_red_opts: + red_opts.append(arg) + add_to_red_opts = False + elif reading_conf_opts: + if arg == '}': + reading_conf_opts = False + else: + conf_opts.append(arg) + elif isinstance(config_file, list): + config_file = arg + elif red_args is not None: + red_args.append(arg) + elif arg == '{': + reading_conf_opts = True + elif arg in ('-V', '--version', '-version'): + ## Print the version of nightshift and of redshift + print('%s %s' % (PROGRAM_NAME, PROGRAM_VERSION)) + Popen(['redshift', '-V'], stdout = sys.stdout, env = redshift_env).wait() + sys.exit(0) + elif arg in ('-C', '--copyright', '-copyright'): + ## Print copyright information + print(copyright[1 : -1]) + sys.exit(0) + elif arg in ('-W', '--warranty', '-warranty'): + ## Print warranty disclaimer + print(copyright.split('\n\n')[-2]) + sys.exit(0) + elif arg in ('-h', '-?', '--help', '-help'): + ## Display help message + text = '''USAGE: nightshift [OPTIONS...] ['{' SCRIPT-OPTIONS... '}'] ['--' REDSHIFT-OPTIONS...] + + Terminal user interface for redshift, a program for setting the colour + temperature of the display according to the time of day. + + -h --help Display this help message + -V --version Show program version + -C --copyright Show program copyright information + -W --warranty Show program warrantly disclaimer + + -d --daemon Start as daemon + -x --reset --kill Remove adjustment from screen + +x --toggle Temporarily disable or enable adjustments + +d --disable Temporarily disable adjustments + +e --enable Re-enable adjustments + +f --freeze Temporarily freeze the redshift process + +t --thaw Thaw the redshift process + -s --status Print status information + +c --script FILE Load nightshift configuration script from specified file + + -c --config FILE Load redshift settings from specified file + -b --brightness DAY:NIGHT Screen brightness to set at daytime/night + -b --brightness BRIGHTNESS Screen brightness to apply + -t --temperature DAY:NIGHT Colour temperature to set at daytime/night + -t --temperature TEMP Colour temperature to apply + -l --location LAT:LON Your current location + -l --location PROVIDER Select provider for automatic location updates + (Type `list' to see available providers) + -m --method METHOD Method to use to set colour temperature + (Type `list' to see available methods) + -r --no-transition Disable temperature transitions + ''' + text = text.split('\n')[:-1] + indent = min([len(line) - len(line.lstrip()) for line in text if line.rstrip().startswith(' ')]) + print('\n'.join([line[indent:] if line.startswith(' ') else line for line in text])) + sys.exit(0) + elif arg == '--': + red_args = [] + else: + subargs = [arg] + if arg.startswith('-') and not arg.startswith('--'): subargs = ['-' + letter for letter in arg[1:]] + elif arg.startswith('+') and not arg.startswith('++'): subargs = ['+' + letter for letter in arg[1:]] + elif arg.startswith('=') and not arg.startswith('=='): subargs = ['=' + letter for letter in arg[1:]] + red_arg = '' + for arg in subargs: + if (add_to_red_opts is None) or add_to_red_opts: + add_to_red_opts = None + red_arg += arg + elif isinstance(config_file, list): + config_file.append(arg) + elif arg in ('-d', '--daemon'): daemon = 1 + elif arg in ('=d', '==daemon'): daemon = 2 + elif arg in ('-x', '--reset', '--kill'): kill += 1 + elif arg in ('+x', '--toggle'): toggle = True + elif arg in ('+d', '--disable'): set_status = False + elif arg in ('+e', '--enable'): set_status = True + elif arg in ('+f', '--freeze'): set_freeze = True + elif arg in ('+t', '--thaw'): set_freeze = False + elif arg in ('-s', '--status'): status = True + elif arg in ('+c', '--script'): config_file = [] + else: + add_to_red_opts = True + if arg in ('-c', '--config'): red_opts.append('-c') + elif arg in ('-b', '--brightness'): red_opts.append('-b') + elif arg in ('-t', '--temperature'): red_opts.append('-t') + elif arg in ('-l', '--location'): red_opts.append('-l') + elif arg in ('-m', '--method'): red_opts.append('-m') + elif arg in ('-r', '--no-transition'): red_opts.append('-r') + else: + ## Unrecognised option + sys.stderr.write('%s: error: unrecognised option: %s\n' % (sys.argv[0], arg)) + sys.exit(1) + if add_to_red_opts is None: + red_opts.append(red_arg) + add_to_red_opts = False + if isinstance(config_file, list) and (len(config_file) > 0): + config_file = ''.join(config_file) +if isinstance(config_file, list): + sys.stderr.write('%s: error: premature end of arguments\n' % sys.argv[0]) + sys.exit(1) + + +# Parse help request for -l and -m +for opt in ('-l', '-m'): + i = 0 + while opt in red_opts[i:]: + i = red_opts.index(opt) + 1 + if not i == len(red_opts): + arg = red_opts[i] + if (arg == 'list') or ('help' in arg.split(':')): + proc = ['redshift', opt, arg] + proc = Popen(proc, stdout = sys.stdout, stderr = sys.stderr, env = redshift_env) + proc.wait() + sys.exit(proc.returncode) +# Translate single-parameter -t into dual-parameter -t +i = 0 +while '-t' in red_opts[i:]: + i = red_opts.index('-t') + 1 + if not i == len(red_opts): + if ':' not in red_opts[i]: + red_opts[i] = '%s:%s' % (red_opts[i], red_opts[i]) + + + +# Construct name of socket +socket_path = '%s.%s~%s' % ('/dev/shm/', PROGRAM_NAME, os.environ['USER']) +''' +The pathname of the interprocess communication socket for nightshift +''' + + +# The status of redshift +red_brightness, red_temperature = 1, 6500 +red_brightnesses, red_temperatures = (1, 1), (5500, 3500) +red_period, red_location = 1, (0, 0) +red_status, red_running, red_dying, red_frozen = True, True, False, False +red_condition, broadcast_condition = None, None + + +## Create locale free environment for redshift +redshift_env = os.environ.copy() +for var in ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES'): + redshift_env[var] = 'C' + + +def read_status(proc, sock): + ''' + Read status from redshift + + @param proc:Popen The redshift process + @param sock:socket The server socket + ''' + global red_brightness, red_temperature + global red_brightnesses, red_temperatures + global red_period, red_location + global red_status, red_running + released = True + while True: + got = proc.stdout.readline() + if (got is None) or (len(got) == 0): + if red_frozen: + proc.wait() + continue + break + got = got.decode('utf-8', 'replace')[:-1] + if ': 'not in got: + continue + (key, value) = got.split(': ') + if released: + red_condition.acquire() + try: + if key == 'Location': + def coordcomp(v): + v = (v + ' N').split(' ')[:2] + return float(v[0]) * (-1 if v[1] in 'SW' else 1) + red_location = [coordcomp(v) for v in value.split(', ')] + # Followed by 'Temperatures' + elif key == 'Temperatures': + red_temperatures = [float(v.split(' ')[0][:-1]) for v in value.split(', ')] + # Followed by two parameter 'Brightness' + elif key == 'Period': + if value == 'Night': + red_period = 0 + elif value == 'Daytime': + red_period = 1 + else: + red_period = float(value.split(' ')[1][1 : -1]) / 100 + # Followed by 'Color temperature' + elif key == 'Color temperature': + red_temperature = float(value[:-1]) + # Followed by one parameter 'Brightness' + elif key == 'Brightness': + if ':' in value: + red_brightnesses = [float(v) for v in value.split(':')] + else: + red_brightness = float(value) + # Neither version is followed by anything, notify and release + released = True + elif key == 'Status': + red_status = value == 'Enabled' + # Not followed by anything, notify and release + released = True + if released: + red_condition.notify_all() + red_condition.release() + except: + pass + if released: + red_condition.acquire() + red_running = False + red_condition.notify_all() + red_condition.release() + sock.shutdown(socket.SHUT_RDWR) + + +def broadcast_status(sock): + ''' + Broadcast status updates + + @param sock:socket The socket connected to the client + ''' + try: + while True: + broadcast_condition.acquire() + try: + broadcast_condition.wait() + red_condition.acquire() + try: + message = generate_status_message() + sock.sendall((message + '\n').encode('utf-8')) + finally: + red_condition.release() + finally: + broadcast_condition.release() + except: + pass + + +def generate_status_message(): + ''' + Generate message to send to the client to inform about the status + + @return :str Status message + ''' + message = 'Current brightness: %f\n' % red_brightness + message += 'Daytime brightness: %f\n' % red_brightnesses[0] + message += 'Night brightness: %f\n' % red_brightnesses[1] + message += 'Current temperature: %f\n' % red_temperature + message += 'Daytime temperature: %f\n' % red_temperatures[0] + message += 'Night temperature: %f\n' % red_temperatures[1] + message += 'Dayness: %f\n' % red_period + message += 'Latitude: %f\n' % red_location[0] + message += 'Longitude: %f\n' % red_location[1] + message += 'Enabled: %s\n' % ('yes' if red_status else 'no') + message += 'Running: %s\n' % ('yes' if red_running else 'no') + message += 'Dying: %s\n' % ('yes' if red_dying else 'no') + message += 'Frozen: %s\n' % ('yes' if red_frozen else 'no') + return message + + +def use_client(sock, proc): + ''' + Communication with client + + @param sock:socket The socket connected to the client + @param proc:Popen The redshift process + ''' + global red_dying, red_frozen + buf = '' + closed = False + while not closed: + try: + got = sock.recv(128).decode('utf-8', 'strict') + if (got is None) or (len(got) == 0): + break + except: + break + buf += got + while '\n' in buf: + buf = buf.split('\n') + message, buf = buf[0], '\n'.join(buf[1:]) + if message == 'status': + red_condition.acquire() + try: + message = generate_status_message() + sock.sendall((message + '\n').encode('utf-8')) + finally: + red_condition.release() + elif message == 'toggle': + if (not red_dying) and (not red_frozen): + proc.send_signal(signal.SIGUSR1) + elif message == 'disable': + if (not red_dying) and (not red_frozen): + if red_status: + proc.send_signal(signal.SIGUSR1) + elif message == 'enable': + if (not red_dying) and (not red_frozen): + if not red_status: + proc.send_signal(signal.SIGUSR1) + elif message == 'freeze': + broadcast_condition.acquire() + try: + if not red_frozen: + red_frozen = True + proc.send_signal(signal.SIGTSTP) + broadcast_condition.notify_all() + finally: + broadcast_condition.release() + elif message == 'thaw': + broadcast_condition.acquire() + try: + if red_frozen: + red_frozen = False + proc.send_signal(signal.SIGCONT) + broadcast_condition.notify_all() + finally: + broadcast_condition.release() + elif message == 'kill': + if red_frozen: + red_frozen = False + proc.send_signal(signal.SIGCONT) + red_dying = True + proc.terminate() + import time + time.sleep(0.05) # XXX sometimes redshift is too slow + elif message == 'close': + closed = True + elif message == 'listen': + def listen(): + while True: + red_condition.acquire() + try: + red_condition.wait() + message = generate_status_message() + sock.sendall((message + '\n').encode('utf-8')) + except: + break + finally: + red_condition.release() + thread = threading.Thread(target = listen) + thread.setDaemon(True) + thread.start() + sock.close() + + +def start_daemon_threads(proc, sock): + ''' + Start the threads for the daemon + + @param sock:socket The server socket + @param proc:Popen The redshift process + ''' + pass + + +def run_as_daemon(sock): + ''' + Perform daemon logic + + @param sock:socket The server socket + ''' + global red_condition, broadcast_condition, red_pid + + # Create status conditions + red_condition = threading.Condition() + broadcast_condition = threading.Condition() + + # Start redshift + command = ['redshift'] + red_opts + if red_args is not None: + command += red_args + proc = Popen(command, stdout = PIPE, stderr = open(os.devnull)) + + start_daemon_threads(proc, sock) + + # Read status from redshift + thread = threading.Thread(target = read_status, args = (proc, sock)) + thread.setDaemon(True) + thread.start() + + red_condition.acquire() + broke = False + while red_running: + red_condition.release() + try: + (client_sock, _client_address) = sock.accept() + except: + broke = True + break # We have shut down the socket so that accept halts + client_thread = threading.Thread(target = use_client, args = (client_sock, proc)) + client_thread.setDaemon(True) + client_thread.start() + # Broadcast status from redshift + broadacast_thread = threading.Thread(target = broadcast_status, args = (client_sock,)) + broadacast_thread.setDaemon(True) + broadacast_thread.start() + red_condition.acquire() + + if not broke: + red_condition.release() + thread.join() + + +def do_daemon(reexec): + ''' + Run actions for --daemon or ==daemon + + @param reexec:bool Wether to perform actions for ==daemon + ''' + if not reexec: + if (kill > 0) or toggle or (set_status is not None) or (set_freeze is not None) or status: + disallowed = '-x, +x, +e, +d, +f, +t and -s' + print('%s: error: %s can be used when running as the daemon' % (disallowed, sys.argv[0])) + sys.exit(1) + + # Create server socket + try: + os.unlink(socket_path) + except: + pass # The fill does (probably) not exist + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(socket_path) + sock.listen(backlog) + + # Signal respawner + if reexec: + print() + sys.stdout.close() + + # Perform daemon logic + run_as_daemon(sock) + + # Close socket + sock.close() + + +def not_running(): + ''' + Run actions for --status when the daemon is not running + ''' + print('Not running') + + +def do_status(): + ''' + Run actions for --status when the daemon is running + ''' + sock.sendall('status\n'.encode('utf-8')) + buf = '' + while True: + got = sock.recv(1024) + if (got is None) or (len(got) == 0): + break + buf += got.decode('utf-8', 'replace') + if '\n\n' in buf: + break + buf = buf.split('\n\n')[0] + '\n' + sys.stdout.buffer.write(buf.encode('utf-8')) + sys.stdout.buffer.flush() + + +def do_toggle(): + ''' + Run actions for --toggle + ''' + sock.sendall('toggle\n'.encode('utf-8')) + + +def do_disable(): + ''' + Run actions for --disable + ''' + sock.sendall('disable\n'.encode('utf-8')) + + +def do_enable(): + ''' + Run actions for --enable + ''' + sock.sendall('enable\n'.encode('utf-8')) + + +def do_freeze(): + ''' + Run actions for --freeze + ''' + sock.sendall('freeze\n'.encode('utf-8')) + + +def do_thaw(): + ''' + Run actions for --thaw + ''' + sock.sendall('thaw\n'.encode('utf-8')) + + +def do_kill(): + ''' + Run actions for --kill + ''' + sock.sendall('kill\n'.encode('utf-8')) + if kill > 1: + sock.sendall('kill\n'.encode('utf-8')) + + +def create_daemon(): + ''' + Start daemon when it is required but is not running + ''' + ## Server is not running + # Create pipe for interprocess signal + (r_end, w_end) = os.pipe() + + # Duplicate process + pid = os.fork() + + if pid == 0: + ## Daemon (child) + # Close stdin and stdout + if ('DEBUG' not in os.environ) or (not os.environ['DEBUG'] == 'yes'): + os.close(sys.stdin.fileno()) + os.close(sys.stdout.fileno()) + + # Create server socket + try: + os.unlink(socket_path) + except: + pass # The fill does (probably) not exist + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(socket_path) + sock.listen(backlog) + + # Send signal + with os.fdopen(w_end, 'wb') as file: + file.write(b'\n') + file.flush() + + # Close the pipe + os.close(r_end) + + # Perform daemon logic + run_as_daemon(sock) + + # Close socket + sock.close() + # Close process + sys.exit(0) + else: + ## Front-end (parent) + # Wait for a signal + rc = None + with os.fdopen(r_end, 'rb') as file: + file.read(1) + + # Close the pipe + os.close(w_end) + + +def create_client(): + ''' + Create client socket and start daemon if not running + + @return :socket The client socket + ''' + # Create socket + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + # Connect to the server + sock.connect(socket_path) + except: + # The process need separate sockets, lets close it + # and let both process recreate it + sock.close() + sock = None + + if status: + not_running() + sys.exit(0) + + if sock is None: + # Create daemon and wait for it to start listening for clients + create_daemon() + + # Connect to the server + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(socket_path) + + return sock + + +def run_as_client(): + ''' + Perform client actions + ''' + # Temporarily disable or enable redshift + if set_status is not None: + if set_status: + do_enable() + else: + do_disable() + elif toggle: + do_toggle() + + # Freeze or thaw redshift + if set_freeze is not None: + if set_freeze: + do_freeze() + else: + do_thaw() + + # Kill redshift and the nightshift daemon + if kill > 0: + do_kill() + + # Get redshift status + if status: + do_status() + sock.close() + + # Start user interface + if (kill == 0) and not (status or toggle or (set_status is not None) or (set_freeze is not None)): + sock.sendall('listen\n'.encode('utf-8')) + user_interface() + + +def do_client(): + ''' + Do everything that has to do with being a client + ''' + global sock + # Connect to client + sock = create_client() + + # Perform client actions + run_as_client() + + # Close socket + try: + sock.sendall('close\n'.encode('utf-8')) + except: + pass + sock.close() + + +def respawn_daemon(): + ''' + Restart the nightshift daemon + ''' + global sock + + # Close old socket + sock.close() + + ## Server is not running + # Create pipe for interprocess signal + (r_end, w_end) = os.pipe() + + # Duplicate process + pid = os.fork() + + if pid == 0: + ## Daemon (child) + # Close stdin and stdout + os.close(sys.stdin.fileno()) + os.close(sys.stdout.fileno()) + + # Replace stdout with the pipe + os.dup2(w_end, sys.stdout.fileno()) + os.close(w_end) + + # Reexecute image + exe = os.readlink('/proc/self/exe') + os.execl(exe, exe, *(sys.argv + ['==daemon'])) + else: + ## Front-end (parent) + # Wait for a signal + rc = None + with os.fdopen(r_end, 'rb') as file: + file.read(1) + + # Close the pipe + os.close(w_end) + + # Connect to the server + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(socket_path) + + +def run(): + ''' + Run as either the daemon (if --daemon or ==daemon) or as a client (otherwise) + ''' + if daemon > 0: + do_daemon(daemon == 2) + else: + do_client() + + +g, l = globals(), dict(locals()) +for key in l: + g[key] = l[key] + + +## Import interface.py with shared globals +# Get the Python version +v = sys.version_info +if (v.major > 3) or ((v.major == 3) and (v.minor >= 4)): + # The (new) Python 3.4 way + import importlib.util + exec(importlib.util.find_spec('interface').loader.get_code('interface'), g) +else: + # The deprecated legacy way + import importlib + exec(importlib.find_loader('interface').get_code('interface'), g) + + +## Load extension and configurations via nightshiftrc +# No configuration script has been selected explicitly, +# so select one automatically. +if config_file is None: + # Possible auto-selected configuration scripts, + # earlier ones have precedence, we can only select one. + files = [] + def add_files(var, *ps, multi = False): + if var == '~': + try: + # Get the home (also known as initial) directory of the real user + import pwd + var = pwd.getpwuid(os.getuid()).pw_dir + except: + return + else: + # Resolve environment variable or use empty string if none is selected + if (var is None) or (var in os.environ) and (not os.environ[var] == ''): + var = '' if var is None else os.environ[var] + else: + return + paths = [var] + # Split environment variable value if it is a multi valeu variable + if multi and os.pathsep in var: + paths = [v for v in var.split(os.pathsep) if not v == ''] + # Add files according to patterns + for p in ps: + p = p.replace('/', os.sep).replace('%', PROGRAM_NAME) + for v in paths: + files.append(v + p) + add_files('XDG_CONFIG_HOME', '/%/%rc', '/%rc') + add_files('HOME', '/.config/%/%rc', '/.config/%rc', '/.%rc') + add_files('~', '/.config/%/%rc', '/.config/%rc', '/.%rc') + add_files('XDG_CONFIG_DIRS', '/%rc', multi = True) + add_files(None, '/etc/%rc') + for file in files: + # If the file we exists, + if os.path.exists(file): + # select it, + config_file = file + # and stop trying files with lower precedence. + break +# As the zeroth argument for the configuration script, +# add the configurion script file. Just like the zeroth +# command line argument is the invoked command. +conf_opts = [config_file] + conf_opts +if config_file is not None: + code = None + # Read configuration script file + with open(config_file, 'rb') as script: + code = script.read() + # Decode configurion script file and add a line break + # at the end to ensure that the last line is empty. + # If it is not, we will get errors. + code = code.decode('utf-8', 'strict') + '\n' + # Compile the configuration script, + code = compile(code, config_file, 'exec') + # and run it, with it have the same + # globals as this module, so that it can + # not only use want we have defined, but + # also redefine it for us. + exec(code, g) + + +run() |