[
  {
    "path": ".gitignore",
    "content": "api-ref.html\nnotes.md\n\n# more token things just incase someone tries to push\n# their branch with their token in here... (smh)\ntoken\nlogin\nlogin.sh\nlogin.txt\ndiscord-token\ntoken.sh\nrun.sh\nstart\nstart.sh\nnote.txt\nnotes.txt\ntoken.txt\ntoken\ntoken_login.txt\nlogin.txt\n\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# dotenv\n.env\n\n# virtualenv\n.venv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n# curses notes and stuff\nlog.txt\nloading.txt\ndisTypes/\n\nprocess.md\n"
  },
  {
    "path": "Discline.py",
    "content": "#!/usr/bin/env python3\n# ------------------------------------------------------- #\n#                                                         #\n# Discline                                                #\n#                                                         #\n# http://github.com/MitchWeaver/Discline                  #\n#                                                         #\n# Licensed under GNU GPLv3                                #\n#                                                         #\n# ------------------------------------------------------- #\n\nimport sys\nimport asyncio\nimport os\nfrom discord import ChannelType\nfrom input.input_handler import input_handler, key_input, init_input\nfrom input.typing_handler import is_typing_handler\nfrom ui.ui import print_screen\nfrom ui.text_manipulation import calc_mutations\nfrom utils.print_utils.help import print_help\nfrom utils.print_utils.print_utils import *\nfrom utils.globals import *\nfrom utils.settings import copy_skeleton, settings\nfrom utils.updates import check_for_updates\nfrom utils.token_utils import get_token,store_token\nfrom utils import hidecursor\nfrom client.serverlog import ServerLog\nfrom client.channellog import ChannelLog\nfrom client.on_message import on_incoming_message\nfrom client.client import Client\n\n# check if using python 3.5+\n# TODO: this still fails if they're using python2\nif sys.version_info >= (3, 5): pass\nelse:\n    print(gc.term.red + \"Sorry, but this requires python 3.5+\" + gc.term.normal)\n    quit()\n\ninit_complete = False\n\n# Set terminal X11 window title\nprint('\\33]0;Discline\\a', end='', flush=True)\n\nos.system(\"clear\")\n\ngc.initClient()\n\n@gc.client.event\nasync def on_ready():\n    await gc.client.wait_until_login()\n\n    # completely hide the system's cursor\n    await hidecursor.hide_cursor()\n\n    # these values are set in settings.yaml\n    if settings[\"default_prompt\"] is not None:\n        gc.client.set_prompt(settings[\"default_prompt\"].lower())\n    else: \n        gc.client.set_prompt('~')\n\n    if settings[\"default_server\"] is not None:\n        gc.client.set_current_server(settings[\"default_server\"])\n        if settings[\"default_channel\"] is not None:\n            gc.client.set_current_channel(settings[\"default_channel\"].lower())\n            gc.client.set_prompt(settings[\"default_channel\"].lower())\n\n    if settings[\"default_game\"] is not None:\n        await gc.client.set_game(settings[\"default_game\"])\n\n    # --------------- INIT SERVERS ----------------------------------------- #\n    print(\"Welcome to \" + gc.term.cyan + \"Discline\" + gc.term.normal + \"!\")\n    await print_line_break()\n    await print_user()\n    await print_line_break()\n    print(\"Initializing... \\n\")\n    try: \n        sys.stdout.flush()\n    except: \n        pass\n\n    for server in gc.client.servers:\n        # Null check to check server availability\n        if server is None:\n            continue\n        serv_logs = []\n        for channel in server.channels:\n            # Null checks to test for bugged out channels\n            if channel is None or channel.type is None:\n                continue\n            # Null checks for bugged out members\n            if server.me is None or server.me.id is None \\\n                    or channel.permissions_for(server.me) is None:\n                continue\n            if channel.type == ChannelType.text:\n                    if channel.permissions_for(server.me).read_messages:\n                        try: # try/except in order to 'continue' out of multiple for loops\n                            for serv_key in settings[\"channel_ignore_list\"]:\n                                if serv_key[\"server_name\"].lower() == server.name.lower():\n                                    for name in serv_key[\"ignores\"]:\n                                        if channel.name.lower() == name.lower():\n                                            raise Found\n                            serv_logs.append(ChannelLog(channel, []))\n                        except: \n                            continue\n\n        # add the channellog to the tree\n        gc.server_log_tree.append(ServerLog(server, serv_logs))\n\n        if settings[\"debug\"]:\n            for slog in gc.server_log_tree:\n                for clog in slog.get_logs():\n                    print(slog.get_name() + \" ---- \" + clog.get_name())\n\n    # start our own coroutines\n    try: asyncio.get_event_loop().create_task(key_input())\n    except SystemExit: pass\n    except KeyboardInterrupt: pass\n    try: asyncio.get_event_loop().create_task(input_handler())\n    except SystemExit: pass\n    except KeyboardInterrupt: pass\n    try: asyncio.get_event_loop().create_task(is_typing_handler())\n    except SystemExit: pass\n    except KeyboardInterrupt: pass\n\n    # Print initial screen\n    await print_screen()\n\n    global init_complete\n    init_complete = True\n\n# called whenever the client receives a message (from anywhere)\n@gc.client.event\nasync def on_message(message):\n    await gc.client.wait_until_ready()\n    if init_complete:\n        await on_incoming_message(message)\n\n@gc.client.event\nasync def on_message_edit(msg_old, msg_new):\n    await gc.client.wait_until_ready()\n    msg_new.content = msg_new.content + \" *(edited)*\"\n\n    if init_complete:\n        await print_screen()\n\n@gc.client.event\nasync def on_message_delete(msg):\n    await gc.client.wait_until_ready()\n    # TODO: PM's have 'None' as a server -- fix this later\n    if msg.server is None: return\n\n    try:\n        for serverlog in gc.server_log_tree:\n            if serverlog.get_server() == msg.server:\n                for channellog in serverlog.get_logs():\n                    if channellog.get_channel()== msg.channel:\n                        channellog.get_logs().remove(msg)\n                        if init_complete:\n                            await print_screen()\n                        return\n    except:\n        # if the message cannot be found, an exception will be raised\n        # this could be #1: if the message was already deleted,\n        # (happens when multiple calls get excecuted within the same time)\n        # or the user was banned, (in which case all their msgs disappear)\n        pass\n\n\ndef main():\n    # start the client coroutine\n    TOKEN=\"\"\n    try:\n        if sys.argv[1] == \"--help\" or sys.argv[1] == \"-h\":\n            from utils.print_utils.help import print_help\n            print_help()\n            quit()\n        elif sys.argv[1] == \"--token\" or sys.argv[1] == \"--store-token\":\n            store_token()\n            quit()\n        elif sys.argv[1] == \"--skeleton\" or sys.argv[1] == \"--copy-skeleton\":\n            # ---- now handled in utils.settings.py ---- #\n            pass\n        elif sys.argv[1] == \"--config\":\n            # --- now handled in utils.settings.py ---- #\n            pass\n        else:\n            print(gc.term.red(\"Error: Unknown command.\"))\n            print(gc.term.yellow(\"See --help for options.\"))\n            quit()\n    except IndexError: \n        pass\n\n    check_for_updates()\n    token = get_token()\n    init_input()\n\n    print(gc.term.yellow(\"Starting...\"))\n\n    # start the client\n    try: gc.client.run(token, bot=False)\n    except KeyboardInterrupt: pass\n    except SystemExit: pass\n\n    # if we are here, the client's loop was cancelled or errored, or user exited\n    try: kill()\n    except:\n        # if our cleanly-exit kill function failed for whatever reason,\n        # make sure we at least exit uncleanly\n        quit()\n\nif __name__ == \"__main__\": main()\n"
  },
  {
    "path": "LICENSE",
    "content": "        DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE \n                    Version 2, December 2004 \n\n Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> \n\n Everyone is permitted to copy and distribute verbatim or modified \n copies of this license document, and changing it is allowed as long \n as the name is changed. \n\n            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE \n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION \n\n  0. You just DO WHAT THE FUCK YOU WANT TO.\n"
  },
  {
    "path": "README.md",
    "content": "# ![logo_small.png](res/logo/logo_small.png) Discline\n------------------------------\n\n![screenshot_main.png](res/screenshots/screenshot_main.png)\n\n\n# NOTICE: July 20th, 2019\n\nAFAIK discline has become non-functional.\n\n* Python3.7 has changed the syntax of its async libraries.\n\n* discord.py has also gone through a new release cycle which's syntax is not backwards compatible.\n\nDiscline will require a full rewrite, possibly done in a new language.\n\nStay tuned for updates via this readme.\n\nThanks to all that have supported the project thus far, \nI hope you stick around for whats to come.\n\n<sub>With that said, I'll leave the remainder of the readme intact:</sub>\n\n\n## How to use:\n-------------------------\n\n1. Install the dependencies:\n\n    `sudo pip3 install asyncio discord blessings pyyaml`\n\n2. Clone the repo\n\n    `git clone https://github.com/MitchWeaver/Discline`\n\n3. Find your discord \"token\"\n\n    * Go to http://discordapp.com/channels/@me\n\n    * Open your browser's developer console. (Normally `F12` or `CTRL-SHIFT-I`)\n\n    * Look for \"storage\" or \"local storage\", then find the discord url.\n\n    * Clicking this will show you a list of variables. Look for a line that looks like:\n\n        `\"token = 322332r093fwaf032f90323f32f903f23wfa\"`\n\n    If you're having troubles, google around, there's a few guides on the net.\n\n    If all else fails, join the dev discord and we'll be glad to help!\n\n4. Run `python3 Discline.py --store-token` to store your token\n\n5. Run `python3 Discline.py --copy-skeleton` to get a template config\n\n6. Edit `~/.config/Discline/config` to your choosing.\n\n7. Launch with python3\n\n    `python3 Discline.py`\n\n    *(alternatively if you have python3.6 you can simply use `./Discline.py`)*\n\n\n### Current Features\n--------------------------\n\n* /channel to switch channel\n* /server to switch server\n* /nick to change nickname (per server)\n* typing without a leading prefix will submit to current chat\n* \"<USER> is typing...\" support\n* private channels\n* colored output, with user definable colors and custom roles\n* Channel logs update when users edit messages\n* /channels, /servers, /users to view information\n* /game to update the \"Now playing: \" status\n* use /help to see more commands\n* unicode emoji displayal support\n* sending emojis in messages (unicode *and* custom)\n* File uploading via path (ex: /file /path/to/file)\n* italic, bold, and underline font support\n* inline \\`code\\` and \\`\\`\\`code\\`\\`\\` block support\n* URL detection, highlighting in blue + italics\n* automatic updating, fetching the latest master branch's commit\n* channel logs blink red upon unread messages\n* line scrolling\n* discord \"Nitro\" emojis\n* Externalized configs via YAML ~/.config/Discline/config\n* @member expansion/mentions\n* /status to change online presence\n\n### Planned Features\n---------------------------\n\n* emoji reactions\n* comment editing and deletion\n* private messaging\n* message searching\n\n## Dependencies\n------------------------\n\n* git (if you want automatic updates)\n* [Python 3.5+](https://www.python.org/downloads/)\n* [discord.py](https://github.com/Rapptz/discord.py)\n* [blessings.py](https://pypi.python.org/pypi/blessings/)\n* [PyYAML](https://pypi.python.org/pypi/PyYAML/)\n* asyncio\n\n**To install dependencies**:\n\n1. Download Python 3.5/3.6 from the link above\n2. Install `pip3`, normally called `python3-pip` in package managers\n3. Download the dependencies using pip with the following command:\n\n    `sudo pip3 install asyncio discord blessings pyyaml`\n\n\n### Color Customization\n------------------------\n\nAlmost all aspects of the client can be colored to\nthe user's wishes. You can set these colors from within `~/.config/Discline/config`\n\nNote: These assume that you're using the standard terminal colors. If you\nhave colors already defined in your ~/.Xresources or similar, this will \nbe very confusing.\n\n## Launching\n------------------------\nDiscline uses git for automatic updates, so you must be within the Discline\ndirectory upon starting. Manually you can launch via `python3.6 ./Discline.py`, \nhowever it is advised to create a helper script to do this for you.\n\nAn example script is in the /res/scripts folder, \nedit it to suit your system and tastes.\n\n### A Note On Emojis\n-------------------------\n\nCurrently *most* of the standard unicode emojis\nare displaying. Note your terminal must be able\nto render these symbols *and* you must be using a font\nset that contains them. Because some of the emojis\nthat discord uses are non-standard, they may not\ndisplay properly. Here is an example of a random\nfew.\n\n![Image](https://images-ext-2.discordapp.net/external/iN52NdGOWqdWOxby88wiEGs8R81j33ndPjgKX8eKUNA/https/0x0.st/soIy.png?width=400&height=32)\n\nCustom emojis however, are displayed as :emoji_name:\n\n### Note On Font Support\n-------------------------\n\nLike emojis, not all terminals and fonts support\nitalic/bold/underline and 'background' colors, (which are used for \\`code\\`).\nIf these features aren't working for you, odds are you are not using a\nsupported terminal/font. Experiment with different setups to see what works.\n\n![Image](https://0x0.st/sHQ0.png)\n\n*Letting me know what setups __don't__ work helps a lot!*\n\n### Dude this is awesome! How can I support the project?\n--------------------------------------------------------\n\nStar it! 🌟\n\nIt helps to get it higher in GitHub's search results as well as\nmaking me feel good inside. ;)\n\nIf you'd like to contribute, pull requests are __*always*__ welcome!\n\nIf you would like to get more info on what could be done or to discuss the\nproject in general, come join the discord server at: https://discord.gg/rBGQMTk\n\n### FAQ\n-------------------------\n\n> Yet another discord cli?\n\nI didn't like any of the implementations I found around github. Too buggy.\nToo bloated. Bad UI. No customization. Some, after discord updates,\nno longer functioning at all.\n\n> Why use a token and not email/password?\n\nDiscord's API __does__ allow for email/pass login, but if you were to have\n2FA, (2 factor authentication), enabled on your account, Discord would\ninterpret this as a malicious attack against your account and disable it.\n\nSo, because *\"Nobody reads the readme\"*, I have disabled this.\n\n> How should I submit a GitHub issue?\n\nTry to include this format:\n\n```\nOS: Linux/Debian\nTerminal: urxvt\nFont: source code pro\nPython Version: 3.6\nHow to reproduce: xxxxxx\n```\n\n> It says my python is out of date even though I have 3.5+ installed?\n\nProbably because you have multiple versions installed. Try running with\n`python3.5` or `python3.6` rather than just \"python3\"\n\n> I'm getting weird encoding errors on startup\n\nYou probably don't have UTF-8 set. If you're using Linux,\nlook up how to do this according to your distro.\n\nIf you're on BSD, add this to your /etc/profile:\n\n```\nexport LC_CTYPE=en_US.UTF-8\nexport LESSCHARSET=utf-8\n```\n\nand make sure it gets sourced upon opening your terminal.\n\n### Misc Screenshots\n--------------------------\n\n![Image](res/screenshots/kingk22-screenshot.png)\n\n![Image](https://0x0.st/sH5g.png)\n\n![Image](https://0x0.st/sHjn.png)\n\nIt can even be configured to hide most elements of the UI in the config:\n\n![Image](res/screenshots/minimal_brown_ss.png)\n\n### Known Bugs\n--------------------------\n\n> Line wrapping sometimes doesn't work\n\nThis happens if there is too much formatting / coloring being done to the\nmessage that contains that line. I'm looking for a work around.\n\n> When I type many lines before hitting send, the UI sometimes bugs out\nand/or the separators encroach upon different sections\n\nKnown. Looking for a work around.\n\n> My bug isn't listed here, how can I voice my problem?\n\nIf you have a specific issue that isn't listed here or in the\nwiki, post a github issue with a detailed explanation and I can\ntry to get it fixed. Join the discord if you want live help.\n\n### Token Warning\n-------------------------------\nDo *NOT* share your token with anybody, if someone else gets ahold\nof your token, they may control your account. If you are someone\nwho keeps their ~/.config on github, **add your token to your .gitignore**.\nOtherwise it will become public.\n\n\n### License\n-------------------------------\n\n<a href=\"http://www.wtfpl.net/\"><img\n       src=\"http://www.wtfpl.net/wp-content/uploads/2012/12/wtfpl-badge-4.png\"\n       width=\"80\" height=\"15\" alt=\"WTFPL\" /></a>\n\n### Legal Disclaimer\n--------------------------------\n\nDiscord hasn't put out any official statement on whether using their\nAPI for 3rd party clients is allowed or not. They *have* said that using\ntheir API to make \"self-bots\" is against their ToS. By self-bots, it is\nmy understanding they mean automating non-bot accounts as bots.\nMy code has no automated functions, or any on_events that provide features\nnot included in the official client.\n\nAs far as I know, nobody has been banned for using things like this before,\nbut Discord might one day change their mind. With this said, I take **no**\nresponsibility if this gets you banned.\n"
  },
  {
    "path": "client/channellog.py",
    "content": "# Wrapper class to make dealing with logs easier\nclass ChannelLog():\n\n    __channel = \"\"\n    __logs = []\n    unread = False\n    mentioned_in = False\n    # the index of where to start printing the messages\n    __index = 0\n\n    def __init__(self, channel, logs):\n        self.__channel = channel\n        self.__logs = list(logs)\n\n    def get_server(self): return self.__channel.server\n    def get_channel(self): return self.__channel\n\n    def get_logs(self):\n        return self.__logs\n\n    def get_name(self):\n        return self.__channel.name\n\n    def get_server_name(self):\n        return self.__channel.server.name\n\n    def append(self, message):\n        self.__logs.append(message)\n\n    def index(self, message):\n        return self.__logs.index(message)\n\n    def insert(self, i, message):\n        self.__logs.insert(i, message)\n\n    def len(self):\n        return len(self.__logs)\n\n    def get_index(self):\n        return self.__index\n\n    def set_index(self, int):\n        self.__index = int\n\n    def inc_index(self, int):\n        self.__index += int\n\n    def dec_index(self, int):\n        self.__index -= int\n"
  },
  {
    "path": "client/client.py",
    "content": "import discord\nfrom utils.globals import gc\nfrom utils.settings import settings\nimport ui.text_manipulation as tm\n\n# inherits from discord.py's Client\nclass Client(discord.Client):\n\n    # NOTE: These are strings!\n    __current_server = \"\"\n    __current_channel = \"\"\n    __prompt = \"\"\n\n    # discord.Status object\n    __status = \"\"\n\n    # discord.Game object\n    __game = \"\"\n\n\n    # Note: setting only allows for string types\n    def set_prompt(self, string):\n        self.__prompt = string.lower()\n    def set_current_server(self, string):\n        self.__current_server = string.lower()\n    def set_current_channel(self, string):\n        self.__current_channel = string.lower()\n        self.set_prompt(string)\n\n    def get_prompt(self): return self.__prompt\n    def get_current_server_name(self): return self.__current_server\n    def get_current_channel_name(self): return self.__current_channel\n\n    def get_current_server(self):\n        for server in self.servers:\n            if server.name.lower() == self.__current_server:\n                return server\n\n    def get_current_server_log(self):\n        for slog in gc.server_log_tree:\n            if slog.get_server() == self.get_current_server():\n                return slog\n\n    def get_current_channel(self):\n        for server in self.servers:\n            if server.name.lower() == self.__current_server.lower():\n                for channel in server.channels:\n                    if channel.type is discord.ChannelType.text:\n                        if channel.name.lower() == self.__current_channel.lower():\n                            if channel.permissions_for(server.me).read_messages:\n                                return channel\n\n    async def populate_current_channel_log(self):\n        slog = self.get_current_server_log()\n        for idx, clog in enumerate(slog.get_logs()):\n            if clog.get_channel().type is discord.ChannelType.text:\n                if clog.get_channel().name.lower() == self.__current_channel.lower():\n                    if clog.get_channel().permissions_for(slog.get_server().me).read_messages:\n                        async for msg in self.logs_from(clog.get_channel(), limit=settings[\"max_log_entries\"]):\n                            clog.insert(0, await tm.calc_mutations(msg))\n\n    def get_current_channel_log(self):\n        slog = self.get_current_server_log()\n        for idx, clog in enumerate(slog.get_logs()):\n            if clog.get_channel().type is discord.ChannelType.text:\n                if clog.get_channel().name.lower() == self.__current_channel.lower():\n                    if clog.get_channel().permissions_for(slog.get_server().me).read_messages:\n                        return clog\n\n    # returns online members in current server\n    async def get_online(self):\n        online_count = 0\n        if not self.get_current_server() == None:\n            for member in self.get_current_server().members:\n                if member is None: continue # happens if a member left the server\n                if member.status is not discord.Status.offline:\n                    online_count +=1\n            return online_count\n\n    # because the built-in .say is really buggy, just overriding it with my own\n    async def say(self, string):\n        await self.send_message(self.get_current_channel(), string)\n\n    async def set_game(self, string):\n        self.__game = discord.Game(name=string,type=0)\n        self.__status = discord.Status.online\n        # Note: the 'afk' kwarg handles how the client receives messages, (rates, etc)\n        # This is meant to be a \"nice\" feature, but for us it causes more headache\n        # than its worth.\n        if self.__game is not None and self.__game != \"\":\n            if self.__status is not None and self.__status != \"\":\n                try: await self.change_presence(game=self.__game, status=self.__status, afk=False)\n                except: pass\n            else:\n                try: await self.change_presence(game=self.__game, status=discord.Status.online, afk=False)\n                except: pass\n\n    async def get_game(self):\n        return self.__game\n\n    async def set_status(self, string):\n        if string == \"online\":\n            self.__status = discord.Status.online\n        elif string == \"offline\":\n            self.__status = discord.Status.offline\n        elif string == \"idle\":\n            self.__status = discord.Status.idle\n        elif string == \"dnd\":\n            self.__status = discord.Status.dnd\n\n        if self.__game is not None and self.__game != \"\":\n            try: await self.change_presence(game=self.__game, status=self.__status, afk=False)\n            except: pass\n        else:\n            try: await self.change_presence(status=self.__status, afk=False)\n            except: pass\n\n    async def get_status(self):\n        return self.__status\n"
  },
  {
    "path": "client/on_message.py",
    "content": "from ui.ui import print_screen\nfrom utils.globals import gc\nfrom ui.text_manipulation import calc_mutations\n\nasync def on_incoming_message(msg):\n\n    # TODO: make sure it isn't a private message\n    \n    # find the server/channel it belongs to and add it\n    for server_log in gc.server_log_tree:\n        if server_log.get_server() == msg.server:\n            for channel_log in server_log.get_logs():\n                if channel_log.get_channel() == msg.channel:\n                    \n                    channel_log.append(await calc_mutations(msg))\n                    \n                    if channel_log.get_channel() is not gc.client.get_current_channel():\n                        if msg.server.me.mention in msg.content:\n                            channel_log.mentioned_in = True\n                        else:\n                            channel_log.unread = True\n\n    # redraw the screen\n    await print_screen()\n"
  },
  {
    "path": "client/serverlog.py",
    "content": "from discord import Server, Channel\nfrom client.channellog import ChannelLog\n\n# Simple wrapper class to hold a list of ChannelLogs\nclass ServerLog():\n\n    __server = \"\"\n    __channel_logs = []\n\n    def __init__(self, server, channel_log_list):\n        self.__server = server\n        self.__channel_logs = list(channel_log_list)\n\n    def get_server(self):\n        return self.__server\n\n    def get_name(self):\n        return self.__server.name\n\n    def get_logs(self):\n        return self.__channel_logs\n\n    def clear_logs(self):\n        for channel_log in self.__channel_logs:\n            del channel_log[:]\n\n    # takes list of ChannelLog\n    def add_logs(self, log_list):\n        for logs in log_list:\n            self.__channel_logs.append(logs)\n"
  },
  {
    "path": "commands/channel_jump.py",
    "content": "from utils.globals import gc\nfrom utils.quicksort import quick_sort_channel_logs\nfrom utils.settings import settings\n\nasync def channel_jump(arg):\n    logs = []\n\n    num = int(arg[1:]) - 1\n\n    # sub one to allow for \"/c0\" being the top channel\n    if settings[\"arrays_start_at_zero\"]:\n        num -= 1\n   \n    # in case someone tries to go to a negative index\n    if num <= -1:\n        num = 0\n\n    for slog in gc.server_log_tree:\n        if slog.get_server() is gc.client.get_current_server():\n            for clog in slog.get_logs():\n                logs.append(clog)\n\n    logs = quick_sort_channel_logs(logs)\n\n\n    if num > len(logs): num = len(logs) - 1\n\n    gc.client.set_current_channel(logs[num].get_name()) \n    logs[num].unread = False\n    logs[num].mentioned_in = False\n"
  },
  {
    "path": "commands/sendfile.py",
    "content": "from getpass import getuser\nfrom utils.globals import gc\nfrom ui.ui import set_display\n\nasync def send_file(client, filepath):\n\n    # try to open the file exactly as user inputs it\n    try: \n        await client.send_file(client.get_current_channel(), filepath)\n    except:\n        # assume the user ommited the prefix of the dir path,\n        # try to load it starting from user's home directory:\n        try:\n            filepath = \"/home/\" + getuser() + \"/\" + filepath\n            await client.send_file(client.get_current_channel(), filepath)\n        except:\n            # Either a bad file path, the file was too large,\n            # or encountered a connection problem during upload\n            msg = \"Error: Bad filepath\"\n            await set_display(gc.term.bold + gc.term.red + gc.term.move(gc.term.height - 1, \\\n                gc.term.width - len(msg) - 1) + msg)\n"
  },
  {
    "path": "commands/text_emoticons.py",
    "content": "async def check_emoticons(client, cmd):\n    if cmd == \"shrug\": \n        try: await client.send_message(client.get_current_channel(), \"¯\\_(ツ)_/¯\")\n        except: pass\n    elif cmd == \"tableflip\": \n        try: await client.send_message(client.get_current_channel(), \"(╯°□°）╯︵ ┻━┻\")\n        except: pass\n    elif cmd == \"unflip\":\n        try: await client.send_message(client.get_current_channel(), \"┬──┬ ノ( ゜-゜ノ)\")\n        except: pass\n    elif cmd == \"zoidberg\": \n        try: await client.send_message(client.get_current_channel(), \"(/) (°,,°) (/)\")\n        except: pass\n    elif cmd == \"lenny\": \n        try: await client.send_message(client.get_current_channel(), \"( ͡° ͜ʖ ͡°)\")\n        except: pass\n    elif cmd == \"lennyx5\": \n        try: await client.send_message(client.get_current_channel(), \"( ͡°( ͡° ͜ʖ( ͡° ͜ʖ ͡°)ʖ ͡°) ͡°)\")\n        except: pass\n    elif cmd == \"glasses\": \n        try: await client.send_message(client.get_current_channel(), \"(•_•) ( •_•)>⌐■-■ (⌐■_■)\")\n        except: pass\n    elif cmd == \"walking_my_mods\": \n        try: await client.send_message(client.get_current_channel(), \"⌐( ͡° ͜ʖ ͡°) ╯╲___卐卐卐卐\")\n        except: pass\n\n"
  },
  {
    "path": "input/input_handler.py",
    "content": "import asyncio\nimport discord\nfrom input.kbhit import KBHit\nimport ui.ui as ui\nfrom utils.globals import gc, kill\nfrom utils.print_utils.help import print_help\nfrom utils.print_utils.userlist import print_userlist\nfrom utils.print_utils.serverlist import print_serverlist\nfrom utils.print_utils.channellist import print_channellist\nfrom utils.print_utils.emojis import print_emojilist\nfrom utils.settings import settings\nfrom commands.text_emoticons import check_emoticons\nfrom commands.sendfile import send_file\nfrom commands.channel_jump import channel_jump\n\nkb = \"\"\n\ndef init_input():\n    global kb\n    kb = KBHit()\n\nasync def key_input():\n    await gc.client.wait_until_ready()\n\n    global kb\n    memory = \"\"\n    key = \"\"\n    while True:\n        if await kb.kbhit() or memory == \"[\":\n            key = await kb.getch()\n\n            ordkey = ord(key)\n\n            if memory == \"[\":\n                if key == \"6\": # page down\n                    gc.client.get_current_channel_log().dec_index(settings[\"scroll_lines\"])\n                    del gc.input_buffer[-1]\n                elif key == \"5\": # page up\n                    gc.client.get_current_channel_log().inc_index(settings[\"scroll_lines\"])\n                    del gc.input_buffer[-1]\n            else:\n                if ordkey == 10 or ordkey == 13: # enter key\n                    gc.user_input = \"\".join(gc.input_buffer)\n                    del gc.input_buffer[:]\n                elif ordkey == 127 or ordkey == 8: # backspace\n                    if len(gc.input_buffer) > 0:\n                        del gc.input_buffer[-1]\n\n                elif ordkey >= 32 and ordkey <= 256: # all letters and special characters\n                    if not (ordkey == 126 and (memory == \"5\" or memory == \"6\")): # tilde left over from page up/down\n                        gc.input_buffer.append(key)\n                elif ordkey == 9:\n                    gc.input_buffer.append(\" \" * 4) # tab key\n\n            memory = key\n            if key != \"[\":\n                await ui.print_screen()\n\n        if key != \"[\":\n            await asyncio.sleep(0.015)\n        elif key == \"~\":\n            await asyncio.sleep(0.1)\n\nasync def input_handler():\n    await gc.client.wait_until_ready()\n\n    while True:\n\n        # If input is blank, don't do anything\n        if gc.user_input == '':\n            await asyncio.sleep(0.05)\n            continue\n\n        # # check if input is a command\n        if gc.user_input[0] == settings[\"prefix\"]:\n            # strip the PREFIX\n            gc.user_input = gc.user_input[1:]\n\n            # check if contains a space\n            if ' ' in gc.user_input:\n                # split into command and argument\n                command,arg = gc.user_input.split(\" \", 1)\n\n                if command == \"server\" or command == 's':\n\n                    server_name = \"\"\n                    # check if arg is a valid server, then switch\n                    for servlog in gc.server_log_tree:\n                        if servlog.get_name().lower() == arg.lower():\n                            server_name = servlog.get_name()\n                            break\n\n                    # if we didn't find an exact match, assume only partial\n                    # Note if there are multiple servers containing the same\n                    # word, this will only pick the first one. Better than nothing.\n                    if server_name == \"\":\n                        for servlog in gc.server_log_tree:\n                            if arg.lower() in servlog.get_name().lower():\n                                server_name = servlog.get_name()\n                                break\n\n                    if server_name != \"\":\n                        gc.client.set_current_server(server_name)\n\n                        # discord.py's \"server.default_channel\" is buggy.\n                        # often times it will return 'none' even when\n                        # there is a default channel. to combat this,\n                        # we can just get it ourselves.\n                        def_chan = \"\"\n\n                        lowest = 999\n                        for chan in servlog.get_server().channels:\n                            if chan.type is discord.ChannelType.text:\n                                if chan.permissions_for(servlog.get_server().me).read_messages:\n                                    if chan.position < lowest:\n                                        try:\n                                            for serv_key in settings[\"channel_ignore_list\"]:\n                                                if serv_key[\"server_name\"].lower() == server_name:\n                                                    for name in serv_key[\"ignores\"]:\n                                                        if chan.name.lower() == name.lower():\n                                                            raise Found\n                                        except:\n                                            continue\n                                        lowest = chan.position\n                                        def_chan = chan\n\n                            try:\n                                gc.client.set_current_channel(def_chan.name)\n                                # and set the default channel as read\n                                for chanlog in servlog.get_logs():\n                                    if chanlog.get_channel() is def_chan:\n                                        chanlog.unread = False\n                                        chanlog.mentioned_in = False\n                                        break\n                            # TODO: Bug: def_chan is sometimes \"\"\n                            except: continue\n                    else:\n                        ui.set_display(gc.term.red + \"Can't find server\" + gc.term.normal)\n\n\n                elif command == \"channel\" or command == 'c':\n                    # check if arg is a valid channel, then switch\n                    for servlog in gc.server_log_tree:\n                        if servlog.get_server() is gc.client.get_current_server():\n                            final_chanlog = \"\"\n                            for chanlog in servlog.get_logs():\n                                if chanlog.get_name().lower() == arg.lower():\n                                    if chanlog.get_channel().type is discord.ChannelType.text:\n                                        if chanlog.get_channel().permissions_for(servlog.get_server().me).read_messages:\n                                            final_chanlog = chanlog\n                                            break\n\n                            # if we didn't find an exact match, assume partial\n                            if final_chanlog == \"\":\n                                for chanlog in servlog.get_logs():\n                                    if chanlog.get_channel().type is discord.ChannelType.text:\n                                        if chanlog.get_channel().permissions_for(servlog.get_server().me).read_messages:\n                                            if arg.lower() in chanlog.get_name().lower():\n                                                final_chanlog = chanlog\n                                                break\n\n                            if final_chanlog != \"\":\n                                gc.client.set_current_channel(final_chanlog.get_name())\n                                final_chanlog.unread = False\n                                final_chanlog.mentioned_in = False\n                                break\n                            else:\n                                ui.set_display(gc.term.red + \"Can't find channel\" + gc.term.normal)\n\n                elif command == \"nick\":\n                    try:\n                        await gc.client.change_nickname(gc.client.get_current_server().me, arg)\n                    except: # you don't have permission to do this here\n                        pass\n                elif command == \"game\":\n                    await gc.client.set_game(arg)\n                elif command == \"file\":\n                    await send_file(gc.client, arg)\n                elif command == \"status\":\n                    status = arg.lower()\n                    if status == \"away\" or status == \"afk\":\n                        status = \"idle\"\n                    elif \"disturb\" in status:\n                        status = \"dnd\"\n\n                    if status == \"online\" or status == \"offline\" \\\n                       or status == \"idle\" or status == \"dnd\":\n                        await gc.client.set_status(status)\n\n            # else we must have only a command, no argument\n            else:\n                command = gc.user_input\n                if command == \"clear\": await ui.clear_screen()\n                elif command == \"quit\": kill()\n                elif command == \"exit\": kill()\n                elif command == \"help\" or command == \"h\": print_help(gc)\n                elif command == \"servers\" or command == \"servs\": await print_serverlist()\n                elif command == \"channels\" or command == \"chans\": await print_channellist()\n                elif command == \"emojis\": await print_emojilist()\n                elif command == \"users\" or command == \"members\":\n                    await ui.clear_screen()\n                    await print_userlist()\n                elif command[0] == 'c':\n                    try:\n                        if command[1].isdigit():\n                            await channel_jump(command)\n                    except IndexError:\n                        pass\n\n                await check_emoticons(gc.client, command)\n\n\n        # this must not be a command...\n        else:\n            # check to see if it has any custom-emojis, written as :emoji:\n            # we will need to expand them.\n            # these will look like <:emojiname:39432432903201>\n            # check if there might be an emoji\n            if gc.user_input.count(\":\") >= 2:\n\n                # if user has nitro, loop through *all* emojis\n                if settings[\"has_nitro\"]:\n                    for emoji in gc.client.get_all_emojis():\n                        short_name = ':' + emoji.name + ':'\n                        if short_name in gc.user_input:\n                            # find the \"full\" name of the emoji from the api\n                            full_name = \"<:\" + emoji.name + \":\" + emoji.id + \">\"\n                            gc.user_input = gc.user_input.replace(short_name, full_name)\n\n                # else the user can only send from this server\n                elif gc.client.get_current_server().emojis is not None \\\n                and len(gc.client.get_current_server().emojis) > 0:\n                    for emoji in gc.client.get_current_server().emojis:\n                        short_name = ':' + emoji.name + ':'\n                        if short_name in gc.user_input:\n                            # find the \"full\" name of the emoji from the api\n                            full_name = \"<:\" + emoji.name + \":\" + emoji.id + \">\"\n                            gc.user_input = gc.user_input.replace(short_name, full_name)\n\n            # if we're here, we've determined its not a command,\n            # and we've processed all mutations to the input we want\n            # now we will try to send the message.\n            text_to_send = gc.user_input\n            if \"@\" in gc.user_input:\n                sections = gc.user_input.lower().strip().split(\" \")\n                sects_copy = []\n                for sect in sections:\n                    if \"@\" in sect:\n                        for member in gc.client.get_current_server().members:\n                            if member is not gc.client.get_current_server().me:\n                                if sect[1:] in member.display_name.lower():\n                                    sect = \"<@!\" + member.id + \">\"\n                    sects_copy.append(sect)\n                text_to_send = \" \".join(sects_copy)\n\n            # sometimes this fails --- this could be due to occasional\n            # bugs in the api, or there was a connection problem\n            # So we will try it 3 times, sleeping a bit inbetween\n            for i in range(0,3):\n                try:\n                    await gc.client.send_message(gc.client.get_current_channel(), text_to_send)\n                    break\n                except:\n                    await asyncio.sleep(3)\n                    if i == 2:\n                        ui.set_display(gc.term.blink_red + \"error: could not send message\")\n\n        # clear our input as we've just sent it\n        gc.user_input = \"\"\n\n        # update the screen\n        await ui.print_screen()\n\n        await asyncio.sleep(0.25)\n"
  },
  {
    "path": "input/kbhit.py",
    "content": "import os\nimport sys\nimport termios\nimport atexit\nfrom select import select\n\nclass KBHit:\n    \n    def __init__(self):\n        self.fd = sys.stdin.fileno()\n        if self.fd is not None:\n            self.fd = os.fdopen(os.dup(self.fd))\n\n        self.new_term = termios.tcgetattr(self.fd)\n        self.old_term = termios.tcgetattr(self.fd)\n\n        # New terminal setting unbuffered\n        self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO)\n        termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term)\n\n        # Support normal-terminal reset at exit\n        atexit.register(self.set_normal_term)\n    \n    def set_normal_term(self):\n        ''' Resets to normal terminal. '''\n        termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term)\n\n    async def getch(self):\n        return self.fd.read(1)\n                        \n    async def kbhit(self):\n        ''' Returns if keyboard character was hit '''\n        dr,dw,de = select([self.fd], [], [], 0)\n        return dr != []\n"
  },
  {
    "path": "input/typing_handler.py",
    "content": "import asyncio\nfrom utils.settings import settings\nfrom utils.globals import gc\n\nasync def is_typing_handler():\n    # user specified setting in settings.py\n    if not settings[\"send_is_typing\"]: return\n    \n    is_typing = False\n    while True:\n        # if typing a message, display '... is typing'\n        if not is_typing:\n            if len(gc.input_buffer) > 0 and gc.input_buffer[0] is not settings[\"prefix\"]:\n                await gc.client.send_typing(gc.client.get_current_channel())\n                is_typing = True\n        elif len(gc.input_buffer) == 0 or gc.input_buffer[0] is settings[\"prefix\"]:\n            is_typing = False\n        \n        await asyncio.sleep(0.5)\n\n"
  },
  {
    "path": "res/scripts/discline",
    "content": "#!/bin/sh\n#\n# This is a script to easily launch discline\n# from an application launcher or similar.\n#\n# http://github.com/mitchweaver/Discline\n#\n# These are just examples of what I use,\n# edit them to match your system and needs.\n\nterm='st'\nfont='MonteCarlo'\nshell='/bin/dash'\nDISCLINE_DIR=\"${HOME}/workspace/Discline\"\nPYTHON_VERSION='3.6'\n\n# ------------------------------------------------------- \n\n$term -f $font -e $shell -c \"cd $DISCLINE_DIR  && \\\n    python$PYTHON_VERSION Discline.py\" &\n"
  },
  {
    "path": "res/settings-skeleton.yaml",
    "content": "---\n# -------------------------------------------------------------------------\n#       You can edit these to your preferences. Note: anything silly\n#       like max_messages=-1 will break the client. Duh.\n# -------------------------------------------------------------------------\n\n# the default server which will be joined upon startup - CASE SENSITIVE!\ndefault_server: discline\n# the default channel which will be joined upon startup - CASE SENSITIVE!\ndefault_channel: test_bed\n\n# the leading character used for commands\nprefix: /\n\n# whether you have discord \"Nitro\" -- this enables external emojis\nhas_nitro: false\n\n# the default prompt when not in a channel\ndefault_prompt: \"~\"\n\n# the default 'playing ' status in discord\ndefault_game: Discline\n\n# used for various things, your preference\narrays_start_at_zero: false\n\n# Margins for inside the terminal and between elements. NOTE: must be >= 2\n# NOTE: some ratios have weird glitches. Just experiment.\nmargin: 2\n\n# the max amount of messages to be downloaded + kept\n# NOTE: minimum = 100! This is normally safe to increase.\nmax_messages: 100\n\n# the max amount of entries in each channel log to be downloaded + kept\n# NOTE: minimum = 100! The larger this is, the slower the client will start.\nmax_log_entries: 100\n\n# Whether to send \"... is typing\" when the input buffer is not blank or '/'\nsend_is_typing: true\n\n# Whether to show in-line emojis in messages\nshow_emojis: true\n\n# Whether to show, or hide the left channel bar\nshow_left_bar: true\n\n# Whether to show, or hide the top bar\nshow_top_bar: true\n\n# Whether to show the separator lines\nshow_separators: true\n\n# the denominator used to calculate the width of the \"left bar\"\n# NOTE: larger number here, the smaller the bar will be,\n#       (although there is still a minimum of 8 chars...) \nleft_bar_divider: 9\n\n# Determines whether the left bar 'truncates' the channels or \n# appends \"...\" to the end when the screen is too small to display them\ntruncate_channels: false\n\n# Whether to number channels in the left bar (ex: \"1. general\")\nnumber_channels: false\n\n# the amount of lines to scroll up/down on each trigger\nscroll_lines: 3\n\n# ---------------- COLOR SETTINGS ------------------------------------ #\n# Available colors are: \"white\", \"red\", \"blue\", \"black\"\n#                       \"green\", \"yellow\", \"cyan\", \"magenta\"\n# Or: you can say \"on_<color>\" to make it the background (ex: 'on_red')\n# Or: you can say \"blink_<color>\" to have it flash (ex: 'blink_blue')\nseparator_color: white\nserver_display_color: cyan\nprompt_color: white\nprompt_hash_color: red\nprompt_border_color: magenta\nnormal_user_color: green\n\n# messages that contain @you mentions will be this color\nmention_color: yellow\n\n# the \"default\" text color for messages and other things\ntext_color: white\n\ncode_block_color: on_black\nurl_color: cyan\nchannel_list_color: white\ncurrent_channel_color: green\n\n# colors for the channels in the left bar upon unreads \nunread_channel_color: blink_yellow\nunread_mention_color: blink_red\n  \n# whether channels should blink when they have unread messages\nblink_unreads: true\n\n# same as above, but for @mentions \nblink_mentions: true\n\n# here you can define your own custom roles - NOTE: text must match exactly!\n# These for example could be \"helper\" or \"trusted\", whatever roles\n# your servers use\ncustom_roles:\n- name: admin\n  color: magenta\n- name: mod\n  color: blue\n- name: bot\n  color: yellow\n  \n# Channel ignore list - This stops the channel from being loaded.\n# Effectively like the \"mute\" + \"hide\" feature on the official client,\n# However with the added benefit that this means these channels won't\n# be stored in RAM.    \n\n# Follow the format as below. \n# Note it is TWO spaces, not a tab!\nchannel_ignore_list:\n- server_name: server name\n  ignores:\n  - some_channel\n  - some_other_channel\n- server_name: another server name\n  ignores:\n  - foo\n  - bar\n\n# ignore this unless you know what you're doing \ndebug: false\n"
  },
  {
    "path": "ui/line.py",
    "content": "class Line():\n\n    # the text the line contains\n    text = \"\" \n    # how offset from the [left_bar_width + MARGIN] it should be printed\n    # this is to offset wrapped lines to better line up with the previous\n    offset = 0\n\n    def __init__(self, text, offset):\n        self.text = text\n        self.offset = offset\n\n    def length(self):\n        return len(self.text)\n"
  },
  {
    "path": "ui/text_manipulation.py",
    "content": "import re\nfrom discord import MessageType\nfrom utils.settings import settings\nfrom utils.globals import gc, get_color\nimport utils\n\nasync def calc_mutations(msg):\n\n    try: # if the message is a file, extract the discord url from it\n        json = str(msg.attachments[0]).split(\"'\")\n        for string in json:\n            if string is not None and string != \"\":\n                if \"cdn.discordapp.com/attachments\" in string:\n                    msg.content = string\n                    break\n    except IndexError: pass\n    \n    # otherwise it must not have any attachments and its a regular message\n    text = msg.content\n\n\n    # check for in-line code blocks\n    if text.count(\"```\") > 1:\n        while(\"```\") in text:\n            text = await convert_code_block(text)\n\n        msg.content = text\n\n    # TODO: if there are asterics or __'s in the code, then\n    # this will not stop them from being formatted\n    # check for in-line code marks\n    if text.count(\"`\") > 1:\n        while(\"`\") in text:\n            text = await convert_code(text)\n\n        msg.content = text\n\n    # check to see if it has any custom-emojis\n    # These will look like <:emojiname:39432432903201>\n    # We will recursively trim this into just :emojiname:\n    if msg.server.emojis is not None and len(msg.server.emojis) > 0:\n        for emoji in msg.server.emojis:\n            full_name = \"<:\" + emoji.name + \":\" + emoji.id + \">\" \n                                \n            while full_name in text:\n                text = await trim_emoji(full_name, emoji.name, text)\n\n        msg.content = text\n\n    # check for boldened font\n    if text.count(\"**\") > 1:\n        while(\"**\") in text:\n            text = await convert_bold(text)            \n\n        msg.content = text\n\n    # check for italic font\n    if text.count(\"*\") > 1:\n        while(\"*\") in text:\n            text = await convert_italic(text)            \n\n        msg.content = text\n\n    # check for underlined font \n    if text.count(\"__\") > 1:\n        while(\"__\") in text:\n            text = await convert_underline(text)            \n\n        msg.content = text\n\n    # check for urls\n    if \"http://\" in text or \"https://\" in text or \"www.\" in text \\\n        or \"ftp://\" in text or \".com\" in text:\n\n        msg.content = await convert_url(text)\n\n    # check if the message is a \"user has pinned...\" message\n    if msg.type == MessageType.pins_add:\n        msg.content = await convert_pin(msg)\n\n    # else it must be a regular message, nothing else\n    return msg\n\nasync def convert_pin(msg):\n    name = \"\"\n    if msg.author.nick is not None and msg.author.nick != \"\":\n        name = msg.author.nick\n    else: name = msg.author.name\n    return \"📌 \" + str(name) + \" has pinned a message to this channel.\"\n\n\nasync def trim_emoji(full_name, short_name, string):\n    return string.replace(full_name, \":\" + short_name + \":\")\n\nasync def convert_bold(string):\n    sections = string.split(\"**\")\n    left = sections[0]\n    target = sections[1]\n    right = \"\".join(sections[2])\n    return gc.term.normal + gc.term.white + left + \" \" + gc.term.bold(target) + gc.term.normal + \\\n            gc.term.white + \" \" + right\n\nasync def convert_italic(string):\n    sections = string.split(\"*\")\n    left = sections[0]\n    target = sections[1]\n    right = \"\".join(sections[2])\n    return gc.term.normal + gc.term.white +  left + \" \" + gc.term.italic(target) + gc.term.normal + \\\n            gc.term.white + \" \" + right\n\nasync def convert_underline(string):\n    sections = string.split(\"__\")\n    left = sections[0]\n    target = sections[1]\n    right = \"\".join(sections[2])\n    return gc.term.normal + gc.term.white + left + \" \" + gc.term.underline(target) + gc.term.normal + \\\n            gc.term.white + \" \" + right\n\nasync def convert_code(string):\n    sections = string.split(\"`\")\n    left = sections[0]\n    target = sections[1]\n    right = \"\".join(sections[2])\n    return gc.term.normal + gc.term.white +  left + \" \" + await get_color(settings[\"code_block_color\"]) \\\n            + target  + gc.term.normal \\\n            + gc.term.white + \" \" + right\n\nasync def convert_code_block(string):\n    sections = string.split(\"```\")\n    left = sections[0]\n    target = sections[1]\n    right = \"\".join(sections[2])\n    return gc.term.normal + gc.term.white +  left + \" \" + gc.term.on_black(target) + gc.term.normal + \\\n            gc.term.white + \" \" + right\n\nasync def convert_url(string):\n    formatted_line = []\n    entities = []\n    if \" \" in string:\n        entities = string.split(\" \")\n    else:\n        entities.append(string)\n\n    for entity in entities:\n        if \"http://\" in entity or \"https://\" in entity or \"www.\" in entity \\\n           or \"ftp://\" in entity or \".com\" in entity:\n            entity = await get_color(settings[\"url_color\"]) + gc.term.italic + gc.term.underline + entity + gc.term.normal\n        formatted_line.append(entity)\n\n    return \" \".join(formatted_line)\n\n\n"
  },
  {
    "path": "ui/ui.py",
    "content": "import sys\nfrom os import system\nfrom discord import ChannelType\nfrom blessings import Terminal\nfrom ui.line import Line\nfrom ui.ui_utils import *\nfrom utils.globals import gc, get_color\nfrom utils.quicksort import quick_sort_channel_logs\nfrom utils.settings import settings\nfrom utils.print_utils.userlist import print_userlist\n\n# maximum number of lines that can be on the screen\n# is updated every cycle as to allow automatic resizing\nMAX_LINES = 0\n# buffer to allow for double buffering (stops screen flashing)\nscreen_buffer = []\n# text that can be set to be displayed for 1 frame\ndisplay = \"\"\ndisplay_frames = 0\n\nasync def print_screen():\n    # Get ready to redraw the screen\n    left_bar_width = await get_left_bar_width()\n    await clear_screen()\n\n    if settings[\"show_top_bar\"]:\n        await print_top_bar(left_bar_width)\n\n    if gc.server_log_tree is not None:\n        await print_channel_log(left_bar_width)\n\n    await print_bottom_bar(left_bar_width)\n\n    # Print the buffer containing our message logs\n    if settings[\"show_top_bar\"]:\n        if settings[\"show_separators\"]:\n            with gc.term.location(0, 2):\n                print(\"\".join(screen_buffer), end=\"\")\n        else:\n            with gc.term.location(0, 1):\n                print(\"\".join(screen_buffer), end=\"\")\n\n    else:\n        with gc.term.location(0, 0):\n            print(\"\".join(screen_buffer), end=\"\")\n\n    if settings[\"show_left_bar\"]:\n        await print_left_bar(left_bar_width)\n\n    global display, display_frames\n    if display != \"\": \n        print(display)\n        display_frames -= 1\n        if display_frames <=  0:\n            display = \"\"\n\nasync def print_top_bar(left_bar_width):\n    topic = \"\"\n    try: \n        if gc.client.get_current_channel().topic is not None:\n            topic = gc.client.get_current_channel().topic\n    except: \n        # if there is no channel topic, just print the channel name\n        try: topic = gc.client.get_current_channel().name\n        except: pass\n\n    \n    text_length = gc.term.width - (36 + len(gc.client.get_current_server_name()))\n    if len(topic) > text_length:\n        topic = topic[:text_length]\n\n    with gc.term.location(1,0):\n        print(\"Server: \" + await get_color(settings[\"server_display_color\"]) \\\n                         + gc.client.get_current_server_name() + gc.term.normal, end=\"\")\n\n    with gc.term.location(gc.term.width // 2 - len(topic) // 2, 0):\n        print(topic, end=\"\")\n\n    online_text = \"Users online: \"\n    online_count = str(await gc.client.get_online())\n    online_length = len(online_text) + len(online_count)\n\n    with gc.term.location(gc.term.width - online_length - 1, 0):\n        print(await get_color(settings[\"server_display_color\"]) + online_text \\\n              + gc.term.normal + online_count, end=\"\")\n\n    if settings[\"show_separators\"]:\n        divider = await get_color(settings[\"separator_color\"]) \\\n                + (\"─\" * gc.term.width) + \"\\n\" + gc.term.normal\n\n        with gc.term.location(0, 1):\n            print(divider, end=\"\")\n\n        with gc.term.location(left_bar_width, 1):\n            print(await get_color(settings[\"separator_color\"]) + \"┬\", end=\"\")\n\n\nasync def set_display(string):\n    global display, display_frames\n    loc = gc.term.width - 1 - len(string)\n    escape_chars = \"\\e\"\n    for escape_chars in string:\n        loc = loc - 5\n    display = gc.term.move(gc.term.height - 1, loc) + string\n    display_frames = 3\n\nasync def print_left_bar(left_bar_width):\n    start = 0\n    if settings[\"show_top_bar\"]:\n        start = 2\n\n    if settings[\"show_separators\"]:\n        length = 0\n        length = gc.term.height - settings[\"margin\"]\n\n        sep_color = await get_color(settings[\"separator_color\"])\n        for i in range(start, length):\n            print(gc.term.move(i, left_bar_width) + sep_color + \"│\" \\\n                + gc.term.normal, end=\"\")\n\n    # Create a new list so we can preserve the server's channel order\n    channel_logs = []\n\n    for servlog in gc.server_log_tree:\n        if servlog.get_server() is gc.client.get_current_server():\n            for chanlog in servlog.get_logs():\n                channel_logs.append(chanlog)\n            break\n\n    channel_logs = quick_sort_channel_logs(channel_logs)\n   \n    # buffer to print\n    buffer = []\n    count = 1\n            \n    for log in channel_logs:\n        # don't print categories or voice chats\n        # TODO: this will break on private messages\n        if log.get_channel().type != ChannelType.text: continue\n        text = log.get_name()\n        length = len(text)\n\n        if settings[\"number_channels\"]:\n            if count <= 9: length += 1\n            else: length += 2\n\n        if length > left_bar_width:\n            if settings[\"truncate_channels\"]:\n                text = text[0:left_bar_width - 1]\n            else:\n                text = text[0:left_bar_width - 4] + \"...\"\n\n        if log.get_channel() is gc.client.get_current_channel():\n            if settings[\"number_channels\"]:\n                buffer.append(gc.term.normal + str(count) + \". \" + gc.term.green + text + gc.term.normal + \"\\n\")\n            else: \n                buffer.append(gc.term.green + text + gc.term.normal + \"\\n\")\n        else: \n            if log.get_channel() is not channel_logs[0]:\n                pass\n\n            if log.get_channel() is not gc.client.get_current_channel():\n\n                if log.unread and settings[\"blink_unreads\"]: \n                    text = await get_color(settings[\"unread_channel_color\"]) + text + gc.term.normal\n                elif log.mentioned_in and settings[\"blink_mentions\"]: \n                    text = await get_color(settings[\"unread_mention_color\"]) + text + gc.term.normal\n            \n            if settings[\"number_channels\"]:\n                buffer.append(gc.term.normal + str(count) + \". \" + text + \"\\n\")\n            else:\n                buffer.append(text + \"\\n\")\n        \n        count += 1\n        # should the server have *too many channels!*, stop them\n        # from spilling over the screen\n        if count - 1  == gc.term.height - 2 - settings[\"margin\"]: break\n\n    with gc.term.location(0, start):\n        print(\"\".join(buffer))\n\n\nasync def print_bottom_bar(left_bar_width):\n    if settings[\"show_separators\"]:\n        with gc.term.location(0, gc.term.height - 2):\n            print(await get_color(settings[\"separator_color\"]) + (\"─\" * gc.term.width) \\\n                + \"\\n\" + gc.term.normal, end=\"\")\n\n        with gc.term.location(left_bar_width, gc.term.height - 2):\n            print(await get_color(settings[\"separator_color\"]) + \"┴\", end=\"\")\n\n    bottom = await get_prompt()\n    if len(gc.input_buffer) > 0: bottom = bottom + \"\".join(gc.input_buffer)\n    with gc.term.location(0, gc.term.height - 1):\n        print(bottom, end=\"\")\n\nasync def clear_screen():\n    # instead of \"clearing\", we're actually just overwriting\n    # everything with white space. This mitigates the massive\n    # screen flashing that goes on with \"cls\" and \"clear\"\n    del screen_buffer[:]\n    wipe = (\" \" * (gc.term.width) + \"\\n\") * gc.term.height\n    print(gc.term.move(0,0) + wipe, end=\"\")\n\nasync def print_channel_log(left_bar_width):\n    global INDEX\n    \n    # If the line would spill over the screen, we need to wrap it\n    # NOTE: gc.term.width is calculating every time this function is called.\n    #       Meaning that this will automatically resize the screen.\n    # note: the \"1\" is the space at the start\n    MAX_LENGTH = gc.term.width - (left_bar_width + settings[\"margin\"]) - 1\n    # For wrapped lines, offset them to line up with the previous line\n    offset = 0\n    # List to put our *formatted* lines in, once we have OK'd them to print\n    formatted_lines = []\n \n    # the max number of lines that can be shown on the screen\n    MAX_LINES = await get_max_lines()\n\n    for server_log in gc.server_log_tree:\n        if server_log.get_server() is gc.client.get_current_server():\n            for channel_log in server_log.get_logs():\n                if channel_log.get_channel() is gc.client.get_current_channel():\n                    if channel_log.get_channel() not in gc.channels_entered:\n                        await gc.client.populate_current_channel_log()\n                        gc.channels_entered.append(channel_log.get_channel())\n                    # if the server has a \"category\" channel named the same\n                    # as a text channel, confusion will occur\n                    # TODO: private messages are not \"text\" channeltypes\n                    if channel_log.get_channel().type != ChannelType.text: continue\n                    \n                    for msg in channel_log.get_logs():\n                        # The lines of this unformatted message\n                        msg_lines = []\n           \n                        HAS_MENTION = False\n                        if \"@\" + gc.client.get_current_server().me.display_name in msg.clean_content:\n                            HAS_MENTION = True\n\n                        author_name = \"\"\n                        try: author_name = msg.author.display_name\n                        except:\n                            try: author_name = msg.author.name\n                            except: author_name = \"Unknown Author\"\n                        \n                        author_name_length = len(author_name)\n                        author_prefix = await get_role_color(msg) + author_name + \": \"\n\n                        color = \"\"\n                        if HAS_MENTION:\n                            color = await get_color(settings[\"mention_color\"])\n                        else:\n                            color = await get_color(settings[\"text_color\"])\n                        proposed_line = author_prefix + color + msg.clean_content.strip()\n\n                        # If our message actually consists of\n                        # of multiple lines separated by new-line\n                        # characters, we need to accomodate for this.\n                        # --- Otherwise: msg_lines will just consist of one line\n                        msg_lines = proposed_line.split(\"\\n\")\n\n                        for line in msg_lines:\n\n                            # strip leading spaces - LEFT ONLY\n                            line = line.lstrip()\n\n                            # If our line is greater than our max length,\n                            # that means the author has a long-line comment\n                            # that wasn't using new line chars...\n                            # We must manually wrap it.\n\n                            line_length = len(line)\n                            # Loop through, wrapping the lines until it behaves\n                            while line_length > MAX_LENGTH:\n\n                                line = line.strip()\n\n                                # Take a section out of the line based on our max length\n                                sect = line[:MAX_LENGTH - offset]\n\n                                # Make sure we did not cut a word in half \n                                sect = sect[:sect.strip().rfind(' ')]\n                                \n                                # If this section isn't the first line of the comment,\n                                # we should offset it to better distinguish it\n                                offset = 0\n                                if author_prefix not in sect:\n                                    if line is not msg_lines[0]:\n                                        offset = author_name_length + settings[\"margin\"]\n                                # add in now formatted line!\n                                formatted_lines.append(Line(sect.strip(), offset))\n                            \n                                # since we just wrapped a line, we need to \n                                # make sure we don't overwrite it next time\n\n                                # Split the line between what has been formatted, and\n                                # what still remains needing to be formatted\n                                if len(line) > len(sect):\n                                    line = line.split(sect)[1]\n                                    \n                                # find the \"real\" length of the line, by subtracting\n                                # any escape characters it might have. It would\n                                # be wasteful to loop through all of the possibilities\n                                # so instead we will simply subtract the length\n                                # of the shortest for each that it has.\n                                line_length = len(line)\n                                target = \"\\e\"\n                                for target in line:\n                                    line_length -= 5\n\n                            # Once here, the string was either A: already short enough\n                            # to begin with, or B: made through our while loop and has\n                            # since been chopped down to less than our MAX_LENGTH\n                            if len(line.strip()) > 0:\n                                \n                                offset = 0\n                                if author_prefix not in line:\n                                    offset = author_name_length + settings[\"margin\"]\n\n                                formatted_lines.append(Line(line.strip(), offset))\n                                \n                    # where we should start printing from\n                    # clamp the index as not to show whitespace\n                    if channel_log.get_index() < MAX_LINES: \n                        channel_log.set_index(MAX_LINES)\n                    elif channel_log.get_index() > len(formatted_lines): \n                        channel_log.set_index(len(formatted_lines))\n\n                    # ----- Trim out list to print out nicely ----- #\n                    # trims off the front of the list, until our index\n                    del formatted_lines[0:(len(formatted_lines) - channel_log.get_index())]\n                    # retains the amount of lines for our screen, deletes remainder\n                    del formatted_lines[MAX_LINES:]\n\n                    # if user does not want the left bar, do not add margin\n                    space = \" \"\n                    if not settings[\"show_left_bar\"]:\n                        space = \"\"\n                    \n                    # add to the buffer!\n                    for line in formatted_lines:\n                        screen_buffer.append(space * (left_bar_width + \\\n                                settings[\"margin\"] + line.offset) + line.text + \"\\n\")\n\n                    # return as not to loop through all channels unnecessarily\n                    return\n"
  },
  {
    "path": "ui/ui_curses.py",
    "content": "import sys\nimport time\nfrom os import system\nimport curses\nfrom curses import ascii as cAscii\nfrom discord import ChannelType\nfrom blessings import Terminal\nfrom ui.line import Line\nfrom ui.ui_utils import *\nfrom utils.globals import *\nfrom utils.quicksort import quick_sort_channel_logs\nfrom utils.settings import settings\nfrom utils.print_utils.userlist import print_userlist\n\n# maximum number of lines that can be on the screen\n# is updated every cycle as to allow automatic resizing\nMAX_LINES = 0\n# screen\nglobal stdscr\nstdscr = None\nglobal windows\nwindows = []\n# buffer to allow for double buffering (stops screen flashing)\nscreen_buffer = []\n# text that can be set to be displayed for 1 frame\ndisplay = \"\"\ndisplay_frames = 0\n\ndef cursesInit():\n    stdscr = curses.initscr()\n    curses.noecho()\n    curses.cbreak()\n    stdscr.keypad(True)\n\ndef cursesDestroy():\n    curses.nocbreak()\n    stdscr.keypad(False)\n    curses.echo()\n    curses.endwin()\n\ndef cursesRefresh():\n    stdscr.noutrefresh()\n    for win in windows:\n        win.noutrefresh()\n    curses.doupdate()\n\nasync def print_screen():\n    stdscr.clear()\n    stdscr.addstr(\"Test\")\n    cursesRefresh()\n    ## Get ready to redraw the screen\n    #left_bar_width = await get_left_bar_width()\n    #await clear_screen()\n\n    #if settings[\"show_top_bar\"]:\n    #    await print_top_bar(left_bar_width)\n\n    #if server_log_tree is not None:\n    #    await print_channel_log(left_bar_width)\n\n    #await print_bottom_bar(left_bar_width)\n\n    ## Print the buffer containing our message logs\n    #if settings[\"show_top_bar\"]:\n    #    if settings[\"show_separators\"]:\n    #        with term.location(0, 2):\n    #            print(\"\".join(screen_buffer), end=\"\")\n    #    else:\n    #        with term.location(0, 1):\n    #            print(\"\".join(screen_buffer), end=\"\")\n\n    #else:\n    #    with term.location(0, 0):\n    #        print(\"\".join(screen_buffer), end=\"\")\n\n    #if settings[\"show_left_bar\"]:\n    #    await print_left_bar(left_bar_width)\n\n    #global display, display_frames\n    #if display != \"\":\n    #    print(display)\n    #    display_frames -= 1\n    #    if display_frames <=  0:\n    #        display = \"\"\n\nasync def print_top_bar(left_bar_width):\n    topic = \"\"\n    try:\n        if client.get_current_channel().topic is not None:\n            topic = client.get_current_channel().topic\n    except:\n        # if there is no channel topic, just print the channel name\n        try: topic = client.get_current_channel().name\n        except: pass\n\n\n    text_length = term.width - (36 + len(client.get_current_server_name()))\n    if len(topic) > text_length:\n        topic = topic[:text_length]\n\n    with term.location(1,0):\n        print(\"Server: \" + await get_color(settings[\"server_display_color\"]) \\\n                         + client.get_current_server_name() + term.normal, end=\"\")\n\n    with term.location(term.width // 2 - len(topic) // 2, 0):\n        print(topic, end=\"\")\n\n    online_text = \"Users online: \"\n    online_count = str(await client.get_online())\n    online_length = len(online_text) + len(online_count)\n\n    with term.location(term.width - online_length - 1, 0):\n        print(await get_color(settings[\"server_display_color\"]) + online_text \\\n              + term.normal + online_count, end=\"\")\n\n    if settings[\"show_separators\"]:\n        divider = await get_color(settings[\"separator_color\"]) \\\n                + (\"─\" * term.width) + \"\\n\" + term.normal\n\n        with term.location(0, 1):\n            print(divider, end=\"\")\n\n        with term.location(left_bar_width, 1):\n            print(await get_color(settings[\"separator_color\"]) + \"┬\", end=\"\")\n\n\nasync def set_display(string):\n    global display, display_frames\n    loc = term.width - 1 - len(string)\n    escape_chars = \"\\e\"\n    for escape_chars in string:\n        loc = loc - 5\n    display = term.move(term.height - 1, loc) + string\n    display_frames = 3\n\nasync def print_left_bar(left_bar_width):\n    start = 0\n    if settings[\"show_top_bar\"]:\n        start = 2\n\n    if settings[\"show_separators\"]:\n        length = 0\n        length = term.height - settings[\"margin\"]\n\n        sep_color = await get_color(settings[\"separator_color\"])\n        for i in range(start, length):\n            print(term.move(i, left_bar_width) + sep_color + \"│\" \\\n                + term.normal, end=\"\")\n\n    # Create a new list so we can preserve the server's channel order\n    channel_logs = []\n\n    for servlog in server_log_tree:\n        if servlog.get_server() is client.get_current_server():\n            for chanlog in servlog.get_logs():\n                channel_logs.append(chanlog)\n            break\n\n    channel_logs = quick_sort_channel_logs(channel_logs)\n\n    # buffer to print\n    buffer = []\n    count = 1\n\n    for log in channel_logs:\n        # don't print categories or voice chats\n        # TODO: this will break on private messages\n        if log.get_channel().type != ChannelType.text: continue\n        text = log.get_name()\n        length = len(text)\n\n        if settings[\"number_channels\"]:\n            if count <= 9: length += 1\n            else: length += 2\n\n        if length > left_bar_width:\n            if settings[\"truncate_channels\"]:\n                text = text[0:left_bar_width - 1]\n            else:\n                text = text[0:left_bar_width - 4] + \"...\"\n\n        if log.get_channel() is client.get_current_channel():\n            if settings[\"number_channels\"]:\n                buffer.append(term.normal + str(count) + \". \" + term.green + text + term.normal + \"\\n\")\n            else:\n                buffer.append(term.green + text + term.normal + \"\\n\")\n        else:\n            if log.get_channel() is not channel_logs[0]:\n                pass\n\n            if log.get_channel() is not client.get_current_channel():\n\n                if log.unread and settings[\"blink_unreads\"]:\n                    text = await get_color(settings[\"unread_channel_color\"]) + text + term.normal\n                elif log.mentioned_in and settings[\"blink_mentions\"]:\n                    text = await get_color(settings[\"unread_mention_color\"]) + text + term.normal\n\n            if settings[\"number_channels\"]:\n                buffer.append(term.normal + str(count) + \". \" + text + \"\\n\")\n            else:\n                buffer.append(text + \"\\n\")\n\n        count += 1\n        # should the server have *too many channels!*, stop them\n        # from spilling over the screen\n        if count - 1  == term.height - 2 - settings[\"margin\"]: break\n\n    with term.location(0, start):\n        print(\"\".join(buffer))\n\n\nasync def print_bottom_bar(left_bar_width):\n    if settings[\"show_separators\"]:\n        with term.location(0, term.height - 2):\n            print(await get_color(settings[\"separator_color\"]) + (\"─\" * term.width) \\\n                + \"\\n\" + term.normal, end=\"\")\n\n        with term.location(left_bar_width, term.height - 2):\n            print(await get_color(settings[\"separator_color\"]) + \"┴\", end=\"\")\n\n    bottom = await get_prompt()\n    if len(input_buffer) > 0: bottom = bottom + \"\".join(input_buffer)\n    with term.location(0, term.height - 1):\n        print(bottom, end=\"\")\n\nasync def clear_screen():\n    # This is more efficient\n    cursesRefresh()\n    ## instead of \"clearing\", we're actually just overwriting\n    ## everything with white space. This mitigates the massive\n    ## screen flashing that goes on with \"cls\" and \"clear\"\n    #del screen_buffer[:]\n    #wipe = (\" \" * (term.width) + \"\\n\") * term.height\n    #print(term.move(0,0) + wipe, end=\"\")\n\nasync def print_channel_log(left_bar_width):\n    global INDEX\n\n    # If the line would spill over the screen, we need to wrap it\n    # NOTE: term.width is calculating every time this function is called.\n    #       Meaning that this will automatically resize the screen.\n    # note: the \"1\" is the space at the start\n    MAX_LENGTH = term.width - (left_bar_width + settings[\"margin\"]) - 1\n    # For wrapped lines, offset them to line up with the previous line\n    offset = 0\n    # List to put our *formatted* lines in, once we have OK'd them to print\n    formatted_lines = []\n\n    # the max number of lines that can be shown on the screen\n    MAX_LINES = await get_max_lines()\n\n    for server_log in server_log_tree:\n        if server_log.get_server() is client.get_current_server():\n            for channel_log in server_log.get_logs():\n                if channel_log.get_channel() is client.get_current_channel():\n                    # if the server has a \"category\" channel named the same\n                    # as a text channel, confusion will occur\n                    # TODO: private messages are not \"text\" channeltypes\n                    if channel_log.get_channel().type != ChannelType.text: continue\n\n                    for msg in channel_log.get_logs():\n                        # The lines of this unformatted message\n                        msg_lines = []\n\n                        HAS_MENTION = False\n                        if \"@\" + client.get_current_server().me.display_name in msg.clean_content:\n                            HAS_MENTION = True\n\n                        author_name = \"\"\n                        try: author_name = msg.author.display_name\n                        except:\n                            try: author_name = msg.author.name\n                            except: author_name = \"Unknown Author\"\n\n                        author_name_length = len(author_name)\n                        author_prefix = await get_role_color(msg) + author_name + \": \"\n\n                        color = \"\"\n                        if HAS_MENTION:\n                            color = await get_color(settings[\"mention_color\"])\n                        else:\n                            color = await get_color(settings[\"text_color\"])\n                        proposed_line = author_prefix + color + msg.clean_content.strip()\n\n                        # If our message actually consists of\n                        # of multiple lines separated by new-line\n                        # characters, we need to accomodate for this.\n                        # --- Otherwise: msg_lines will just consist of one line\n                        msg_lines = proposed_line.split(\"\\n\")\n\n                        for line in msg_lines:\n\n                            # strip leading spaces - LEFT ONLY\n                            line = line.lstrip()\n\n                            # If our line is greater than our max length,\n                            # that means the author has a long-line comment\n                            # that wasn't using new line chars...\n                            # We must manually wrap it.\n\n                            line_length = len(line)\n                            # Loop through, wrapping the lines until it behaves\n                            while line_length > MAX_LENGTH:\n\n                                line = line.strip()\n\n                                # Take a section out of the line based on our max length\n                                sect = line[:MAX_LENGTH - offset]\n\n                                # Make sure we did not cut a word in half\n                                sect = sect[:sect.strip().rfind(' ')]\n\n                                # If this section isn't the first line of the comment,\n                                # we should offset it to better distinguish it\n                                offset = 0\n                                if author_prefix not in sect:\n                                    if line is not msg_lines[0]:\n                                        offset = author_name_length + settings[\"margin\"]\n                                # add in now formatted line!\n                                formatted_lines.append(Line(sect.strip(), offset))\n\n                                # since we just wrapped a line, we need to\n                                # make sure we don't overwrite it next time\n\n                                # Split the line between what has been formatted, and\n                                # what still remains needing to be formatted\n                                if len(line) > len(sect):\n                                    line = line.split(sect)[1]\n\n                                # find the \"real\" length of the line, by subtracting\n                                # any escape characters it might have. It would\n                                # be wasteful to loop through all of the possibilities\n                                # so instead we will simply subtract the length\n                                # of the shortest for each that it has.\n                                line_length = len(line)\n                                target = \"\\e\"\n                                for target in line:\n                                    line_length -= 5\n\n                            # Once here, the string was either A: already short enough\n                            # to begin with, or B: made through our while loop and has\n                            # since been chopped down to less than our MAX_LENGTH\n                            if len(line.strip()) > 0:\n\n                                offset = 0\n                                if author_prefix not in line:\n                                    offset = author_name_length + settings[\"margin\"]\n\n                                formatted_lines.append(Line(line.strip(), offset))\n\n                    # where we should start printing from\n                    # clamp the index as not to show whitespace\n                    if channel_log.get_index() < MAX_LINES:\n                        channel_log.set_index(MAX_LINES)\n                    elif channel_log.get_index() > len(formatted_lines):\n                        channel_log.set_index(len(formatted_lines))\n\n                    # ----- Trim out list to print out nicely ----- #\n                    # trims off the front of the list, until our index\n                    del formatted_lines[0:(len(formatted_lines) - channel_log.get_index())]\n                    # retains the amount of lines for our screen, deletes remainder\n                    del formatted_lines[MAX_LINES:]\n\n                    # if user does not want the left bar, do not add margin\n                    space = \" \"\n                    if not settings[\"show_left_bar\"]:\n                        space = \"\"\n\n                    # add to the buffer!\n                    for line in formatted_lines:\n                        screen_buffer.append(space * (left_bar_width + \\\n                                settings[\"margin\"] + line.offset) + line.text + \"\\n\")\n\n                    # return as not to loop through all channels unnecessarily\n                    return\n"
  },
  {
    "path": "ui/ui_utils.py",
    "content": "from utils.globals import get_color, gc\nfrom utils.settings import settings\n\nasync def get_prompt():\n    left = await get_color(settings[\"prompt_border_color\"]) + \"[\"\n    right = await get_color(settings[\"prompt_border_color\"]) + \"]: \" + gc.term.normal\n    middle = \"\"\n    if gc.client.get_prompt() == settings[\"default_prompt\"]:\n        middle = \" \" + await get_color(settings[\"prompt_color\"]) + settings[\"default_prompt\"] + \" \"\n    else:\n        middle = await get_color(settings[\"prompt_hash_color\"]) + \"#\" \\\n                + await get_color(settings[\"prompt_color\"]) + gc.client.get_prompt()\n\n    return left + middle + right\n\n\nasync def get_max_lines():\n    num = 0\n    if settings[\"show_top_bar\"] and settings[\"show_separators\"]:\n        num = gc.term.height - settings[\"margin\"] * 2\n    elif settings[\"show_top_bar\"] and not settings[\"show_separators\"]:\n        num = gc.term.height - settings[\"margin\"]\n    elif not settings[\"show_top_bar\"] and not settings[\"show_separators\"]:\n        num = gc.term.height - 1\n        \n    return num\n\nasync def get_left_bar_width():\n    if not settings[\"show_left_bar\"]: return 0\n\n    left_bar_width = gc.term.width // settings[\"left_bar_divider\"]\n    if left_bar_width < 8: return  8\n    else: return left_bar_width\n\nasync def get_role_color(msg):\n    color = \"\"\n    try: \n        r = msg.author.top_role.name.lower()\n        for role in settings[\"custom_roles\"]:\n            if r == role[\"name\"].lower():\n                color = await get_color(role[\"color\"])\n\n        if color is not \"\": # The user must have already been assigned a custom role\n            pass\n        elif settings[\"normal_user_color\"] is not None:\n            color = await get_color(settings[\"normal_user_color\"])\n        else: color = gc.term.green\n    # if this fails, the user either left or was banned\n    except: \n        if settings[\"normal_user_color\"] is not None:\n            color = await get_color(settings[\"normal_user_color\"])\n        else: color = gc.term.green\n    return color\n\n"
  },
  {
    "path": "utils/globals.py",
    "content": "from sys import exit\nfrom blessings import Terminal\nfrom utils.settings import settings\nimport sys\n\nNO_SETTINGS=False\ntry:\n    if sys.argv[1] == \"--store-token\" or sys.argv[1] == \"--token\":\n        NO_SETTINGS=True\nexcept IndexError: \n    pass\n\nclass GlobalsContainer:\n    def __init__(self):\n        self.term = Terminal()\n        self.client = None\n        self.server_log_tree = []\n        self.input_buffer = []\n        self.user_input = \"\"\n        self.channels_entered = []\n\n    def initClient(self):\n        from client.client import Client\n        if NO_SETTINGS:\n            messages=100\n        else:\n            messages=settings[\"max_messages\"]\n        self.client = Client(max_messages=messages)\n\ngc = GlobalsContainer()\n\n# kills the program and all its elements gracefully\ndef kill():\n    # attempt to cleanly close our loops\n    import asyncio\n    try: gc.client.close()\n    except: pass\n    try: asyncio.get_event_loop().close()\n    except: pass\n    try:# since we're exiting, we can be nice and try to clear the screen\n        from os import system\n        system(\"clear\")\n    except: pass\n    exit()\n\n# returns a \"Channel\" object from the given string\nasync def string2channel(channel):\n    for srv in gc.client.servers:\n        if srv.name == channel.server.name:\n            for chan in srv.channels:\n                if chan.name == channel:\n                    return chan\n\n# returns a \"Channellog\" object from the given string\nasync def get_channel_log(channel):\n    for srvlog in gc.server_log_tree:\n        if srvlog.get_name().lower() == channel.server.name.lower():\n            for chanlog in srvlog.get_logs():\n                if chanlog.get_name().lower() == channel.name.lower():\n                    return chanlog\n\n# returns a \"Channellog\" from a given \"Channel\"\nasync def chan2log(chan):\n    for srvlog in gc.server_log_tree:\n        if srvlog.get_name().lower() == chan.server.name.lower():\n            for clog in srvlog.get_logs():\n                if clog.get_name().lower() == chan.name.lower():\n                    return clog\n \n# returns a \"Serverlog\" from a given \"Server\"\nasync def serv2log(serv):\n    for srvlog in gc.server_log_tree:\n        if srvlog.get_name().lower() == serv.name.lower():\n            return srvlog\n\n# takes in a string, returns the appropriate term.color\nasync def get_color(string):\n    arg = string.strip().lower()\n\n    if arg == \"white\":   return gc.term.white\n    if arg == \"black\":   return gc.term.black\n    if arg == \"red\":     return gc.term.red\n    if arg == \"blue\":    return gc.term.blue\n    if arg == \"yellow\":  return gc.term.yellow\n    if arg == \"cyan\":    return gc.term.cyan\n    if arg == \"magenta\": return gc.term.magenta\n    if arg == \"green\":   return gc.term.green\n\n    if arg == \"on_white\":   return gc.term.on_white\n    if arg == \"on_black\":   return gc.term.on_black\n    if arg == \"on_red\":     return gc.term.on_red\n    if arg == \"on_blue\":    return gc.term.on_blue\n    if arg == \"on_yellow\":  return gc.term.on_yellow\n    if arg == \"on_cyan\":    return gc.term.on_cyan\n    if arg == \"on_magenta\": return gc.term.on_magenta\n    if arg == \"on_green\":   return gc.term.on_green\n\n    if arg == \"blink_white\":   return gc.term.blink_white\n    if arg == \"blink_black\":   return gc.term.blink_black\n    if arg == \"blink_red\":     return gc.term.blink_red\n    if arg == \"blink_blue\":    return gc.term.blink_blue\n    if arg == \"blink_yellow\":  return gc.term.blink_yellow\n    if arg == \"blink_cyan\":    return gc.term.blink_cyan\n    if arg == \"blink_magenta\": return gc.term.blink_magenta\n    if arg == \"blink_green\":   return gc.term.blink_green\n\n\n    # if we're here, someone has one of their settings.yaml\n    # colors defined wrong. We'll be nice and just return white.\n    return gc.term.normal + gc.term.white\n"
  },
  {
    "path": "utils/hidecursor.py",
    "content": "from sys import stdout\n\n# completely hides the system cursor\nasync def hide_cursor():\n    stdout.write(\"\\033[?25l\")\n    stdout.flush()\n"
  },
  {
    "path": "utils/print_utils/channellist.py",
    "content": "from os import system\nfrom discord import ChannelType\nfrom ui.ui import clear_screen, set_display\nfrom utils.globals import gc\n\nasync def print_channellist():\n    if len(gc.client.servers) == 0:\n        set_display(gc.term.red + \"Error: You are not in any servers.\")\n        return\n    \n    if len(gc.client.get_current_server().channels) == 0:\n        set_display(gc.term.red + \"Error: Does this server not have any channels?\" + gc.term.normal)\n        return\n\n    buffer = []\n    for channel in gc.client.get_current_server().channels:\n        if channel.type == ChannelType.text:\n            name = channel.name\n            name = name.replace(\"'\", \"\")\n            name = name.replace('\"', \"\")\n            name = name.replace(\"`\", \"\")\n            name = name.replace(\"$(\", \"\")\n            buffer.append(name + \"\\n\")\n\n    await clear_screen()\n    system(\"echo '\" + gc.term.cyan + \"Available Channels in \" \\\n           + gc.term.magenta + gc.client.get_current_server_name() + \": \\n\" \\\n           + \"---------------------------- \\n \\n\" \\\n           + gc.term.yellow + \"\".join(buffer) \\\n           + gc.term.green + \"~ \\n\" \\\n           + gc.term.green + \"~ \\n\" \\\n           + gc.term.green + \"(press \\'q\\' to quit this dialog) \\n\" \\\n           + \"' | less -R\")\n\n"
  },
  {
    "path": "utils/print_utils/emojis.py",
    "content": "from os import system\nfrom ui.ui import clear_screen, set_display\nfrom utils.globals import gc, get_color\nfrom utils.settings import settings\n\nasync def print_emojilist():\n    if len(gc.client.servers) == 0:\n        set_display(gc.term.red + \"Error: You are not in any servers.\" + gc.term.normal)\n        return\n\n    server_name = gc.client.get_current_server_name()\n    server_name = server_name.replace(\"'\", \"\")\n    server_name = server_name.replace('\"', \"\")\n    server_name = server_name.replace(\"`\", \"\")\n    server_name = server_name.replace(\"$(\", \"\")\n\n    emojis = []\n    server_emojis = \"\"\n\n    try: server_emojis = gc.client.get_current_server().emojis\n    except: pass\n\n    if server_emojis is not None and server_emojis != \"\":\n        for emoji in server_emojis:\n            name = emoji.name\n            name = name.replace(\"'\", \"\")\n            name = name.replace('\"', \"\")\n            name = name.replace(\"`\", \"\")\n            name = name.replace(\"$(\", \"\")\n            emojis.append(gc.term.yellow + \":\" + name + \":\" + \"\\n\")\n\n    await clear_screen()\n    system(\"echo '\" + gc.term.magenta + \"Available Emojis in: \" + gc.term.cyan + server_name +\"\\n\" + gc.term.normal \\\n        + \"---------------------------- \\n\" \\\n        + \"\".join(emojis) \\\n        + gc.term.green + \"~ \\n\" \\\n        + gc.term.green + \"~ \\n\" \\\n        + gc.term.green + \"(press q to quit this dialog) \\n\" \\\n        + \"' | less -R\")\n"
  },
  {
    "path": "utils/print_utils/help.py",
    "content": "from os import system\n\ndef print_help(gc):\n    system(\"clear\")\n    system(\"echo '\" + gc.term.normal \\\n        + gc.term.green(\"Launch Arguments: \\n\") + gc.term.red \\\n        + \"--------------------------------------------- \\n\" \\\n        + get_line(gc, \"--copy-skeleton\", \" --- \", \"copies template settings\") \\\n        + gc.term.cyan(\"This file can be found at ~/.config/Discline/config \\n\") \\\n        + \"\\n\"\n        + get_line(gc, \"--store-token\", \"   --- \", \"stores your token\") \\\n        + gc.term.cyan(\"This file can be found at ~/.config/Discline/token \\n\") \\\n        + \"\\n\"\n        + get_line(gc, \"--config\", \"        --- \", \"specify a specific config path\") \\\n        + \"\\n\"\n        + gc.term.green(\"Available Commands: \\n\") + gc.term.red \\\n        + \"--------------------------------------------- \\n\" \\\n        + get_line(gc, \"/channel\", \"   - \", \"switch to channel - (alias: 'c')\") \\\n        + get_line(gc, \"/server\", \"    - \", \"switch server     - (alias: 's')\") \\\n        + gc.term.cyan + \"Note: these commands can now fuzzy-find! \\n\" \\\n        + \"\\n\" \\\n        + get_line(gc, \"/servers\", \"   - \", \"list available servers\") \\\n        + get_line(gc, \"/channels\", \"  - \", \"list available channels\") \\\n        + get_line(gc, \"/users\", \"     - \", \"list servers users\") \\\n        + get_line(gc, \"/emojis\", \"    - \", \"list servers custom emojis\") \\\n        + \"\\n\" \\\n        + get_line(gc, \"/nick\", \"      - \", \"change server nick name\") \\\n        + get_line(gc, \"/game\", \"      - \", \"change your game status\") \\\n        + get_line(gc, \"/file\", \"      - \", \"upload a file via path\") \\\n        + get_line(gc, \"/status\", \"    - \", \"change online presence\") \\\n        + gc.term.cyan + \"This can be either 'online', 'offline', 'away', or 'dnd' \\n\" \\\n        + gc.term.cyan + \"(dnd = do not disturb) \\n\" \\\n        + \"\\n\" \\\n        + get_line(gc, \"/cX\", \"        - \", \"shorthand to change channel (Ex: /c1)\") \\\n        + gc.term.cyan(\"This can be configured to start at 0 in your config\") \\\n        + \"\\n\" \\\n        + \"\\n\" \\\n        + get_line(gc, \"/quit\", \"      - \", \"exit cleanly\") \\\n        + \"\\n \\n\" \\\n        + gc.term.magenta + \"Note: You can send emojis by using :emojiname: \\n\" \\\n        + gc.term.cyan(\"Nitro emojis do work! Make sure you have \\n\") \\\n        + gc.term.cyan(\"nitro enabled in your config. \\n\") \\\n        + \"\\n\"\n        + gc.term.yellow + \"You can scroll up/down in channel logs \\n\" \\\n        + gc.term.yellow + \"by using PageUp/PageDown. \\n\" \\\n        + gc.term.green + \"~ \\n\" \\\n        + gc.term.green + \"~ \\n\" \\\n        + gc.term.green + \"~ \\n\" \\\n        + gc.term.green + \"(press q to quit this dialog)\" \\\n        + \"' | less -R\")\n\n\n\ndef get_line(gc, command, div, desc):\n    return gc.term.yellow(command) + gc.term.cyan(div) + gc.term.normal + desc + \"\\n\"\n"
  },
  {
    "path": "utils/print_utils/print_utils.py",
    "content": "import discord\nfrom utils.globals import gc\n\nasync def print_servers():\n    print(\"Available servers: \")\n    print_line_break();\n    for server  in  gc.client.servers:\n        print(server.name)\n\nasync def print_user():\n    print('Logged in as: ' + gc.term.green + gc.client.user.name + gc.term.normal)\n\nasync def print_line_break():\n    print(\"-\" * int(gc.term.width * 0.45))\n\nasync def print_channels(server):\n    print(\"Available channels:\")\n    print_line_break();\n    for channel in  server.channels:\n        print(channel.name)\n"
  },
  {
    "path": "utils/print_utils/serverlist.py",
    "content": "from os import system\nfrom ui.ui import clear_screen, set_display\nfrom utils.globals import get_color, gc\nfrom utils.settings import settings\n\nasync def print_serverlist():\n    if len(gc.client.servers) == 0:\n        set_display(gc.term.red + \"Error: You are not in any servers.\" + gc.term.normal)\n        return\n\n    buffer = []\n    for slog in gc.server_log_tree:\n        name = slog.get_name()\n        name = name.replace(\"'\", \"\")\n        name = name.replace('\"', \"\")\n        name = name.replace(\"`\", \"\")\n        name = name.replace(\"$(\", \"\")\n\n        if slog.get_server() is gc.client.get_current_server():\n            buffer.append(await get_color(settings[\"current_channel_color\"]) + name + gc.term.normal + \"\\n\")\n            continue\n\n        string = \"\"\n        for clog in slog.get_logs():\n            if clog.mentioned_in:\n                string = await get_color(settings[\"unread_mention_color\"]) + name + gc.term.normal + \"\\n\"\n                break\n            elif clog.unread:\n                string = await get_color(settings[\"unread_channel_color\"]) + name + gc.term.normal + \"\\n\"\n                break\n        \n        if string == \"\":\n            string = await get_color(settings[\"text_color\"]) + name + gc.term.normal + \"\\n\"\n\n        buffer.append(string)\n            \n    await clear_screen()\n    system(\"echo '\" + gc.term.magenta + \"Available Servers: \\n\" + gc.term.normal \\\n        + \"---------------------------- \\n \\n\" \\\n        + \"\".join(buffer) \\\n        + gc.term.green + \"~ \\n\" \\\n        + gc.term.green + \"~ \\n\" \\\n        + gc.term.green + \"(press q to quit this dialog) \\n\" \\\n        + \"' | less -R\")\n"
  },
  {
    "path": "utils/print_utils/userlist.py",
    "content": "from os import system\nfrom discord import Status\nfrom utils.globals import gc\n\n# On call of the /users command, this will print\n# out a nicely sorted, colored list of all users\n# connected to the clients current server and pipe\n# it to the system pager, (in this case `less`)\n\nclass UserList:\n        \n    def __init__(self):\n        # place to store the names, separted in categories\n        self.online = []\n        self.offline = []\n        self.idle = []\n        self.dnd = []\n\n    def add(self, member, tag):\n        listing = member.name + tag + \" \\n\"\n        if member.status is Status.online:\n            self.online.append(listing)\n        elif member.status is Status.offline:\n            self.offline.append(listing)\n        elif member.status is Status.idle:\n            self.idle.append(listing)\n        elif member.status is Status.dnd:\n            self.dnd.append(listing)\n\n    def sort(self):\n        self.online = sorted(self.online, key=str.lower)\n        self.offline = sorted(self.offline, key=str.lower)\n        self.idle = sorted(self.idle, key=str.lower)\n        self.dnd = sorted(self.dnd, key=str.lower)\n        \n        # now they are sorted, we can colorize them\n        # we couldn't before as the escape codes mess with\n        # the sorting algorithm\n        tmp = []\n        for name in self.online: \n            tmp.append(gc.term.green + name)\n        self.online = list(tmp)\n        del tmp[:]\n\n        for name in self.idle: \n            tmp.append(gc.term.yellow + name)\n        self.idle = list(tmp)\n        del tmp[:]\n       \n        for name in self.dnd: \n            tmp.append(gc.term.black + name)\n        self.dnd = list(tmp)\n        del tmp[:]\n\n        for name in self.offline: \n            tmp.append(gc.term.red + name)\n        self.offline = list(tmp)\n        del tmp[:]\n\n        return \"\".join(self.online) + \"\".join(self.offline) \\\n                + \"\".join(self.idle) + \"\".join(self.dnd)\n\nasync def print_userlist():\n    if len(gc.client.servers) == 0:\n        print(\"Error: You are not in any servers.\")\n        return\n    \n    if len(gc.client.get_current_server().channels) == 0:\n        print(\"Error: Does this server not have any channels?\")\n        return\n\n    # lists to contain our \"Member\" objects\n    nonroles = UserList()\n    admins = UserList()\n    mods = UserList() \n    bots = UserList() \n    everything_else = UserList() \n\n    for member in gc.client.get_current_server().members:\n        if member is None: continue # happens if a member left the server\n        \n        if member.top_role.name == \"admin\" or member.top_role.name == \"Admin\":\n            admins.add(member, \" - (Admin)\")\n        elif member.top_role.name == \"mod\" or member.top_role.name == \"Mod\":\n            mods.add(member, \"- (Mod)\")\n        elif member.top_role.name == \"bot\" or member.top_role.name == \"Bot\":\n            bots.add(member, \" - (bot)\")\n        elif member.top_role.is_everyone: nonroles.add(member, \"\")\n        else: everything_else.add(member, \" - \" + member.top_role.name)\n\n   \n    # the final buffer that we're actually going to print\n    buffer = []\n\n    if admins is not None: buffer.append(admins.sort())\n    if mods is not None: buffer.append(mods.sort())\n\n    buffer.append(\"\\n\" + gc.term.magenta + \"---------------------------- \\n\\n\")\n\n    if bots is not None: buffer.append(bots.sort())\n    if everything_else is not None: buffer.append(everything_else.sort())\n\n    buffer.append(\"\\n\" + gc.term.magenta + \"---------------------------- \\n\\n\")\n\n    if nonroles is not None: buffer.append(nonroles.sort())\n\n    buffer_copy = []\n    for name in buffer:\n        name = name.replace(\"'\", \"\")\n        name = name.replace('\"', \"\")\n        name = name.replace(\"`\", \"\")\n        name = name.replace(\"$(\", \"\")\n        buffer_copy.append(name)\n\n    system(\"echo '\" + gc.term.yellow + \"Members in \" \\\n           + gc.client.get_current_server().name + \": \\n\" \\\n           + gc.term.magenta + \"---------------------------- \\n \\n\" \\\n           + \"\".join(buffer_copy) \\\n           + gc.term.green + \"~ \\n\" \\\n           + gc.term.green + \"~ \\n\" \\\n           + gc.term.green + \"(press \\'q\\' to quit this dialog) \\n\" \\\n           # NOTE: the -R flag here enables color escape codes\n           + \"' | less -R\")\n\n# takes in a member, returns a color based on their status\ndef get_status_color(member):\n    if member.status is Status.online:\n        return gc.term.green\n    if member.status is Status.idle:  # aka \"away\"\n        return gc.term.yellow\n    if member.status is Status.offline:\n        return gc.term.red\n    if member.status is Status.dnd: # do not disturb\n        return gc.term.black\n\n    # if we're still here, something is wrong\n    return \"ERROR: get_status_color() has returned 'None' for \" \\\n            + member.name + \"\\n\"\n"
  },
  {
    "path": "utils/quicksort.py",
    "content": "def quick_sort_channel_logs(channel_logs):\n    # sort channels to match the server's default chosen positions\n    if len(channel_logs) <= 1: return channel_logs\n    else:\n        return quick_sort_channel_logs([e for e in channel_logs[1:] \\\n            if e.get_channel().position <= channel_logs[0].get_channel().position]) + \\\n            [channel_logs[0]] + quick_sort_channel_logs([e for e in channel_logs[1:] \\\n            if e.get_channel().position > channel_logs[0].get_channel().position])\n"
  },
  {
    "path": "utils/settings.py",
    "content": "import os\nimport sys\nfrom yaml import safe_load\nfrom blessings import Terminal\n\nsettings = \"\"\n\ndef copy_skeleton():\n    term = Terminal()\n    try:\n        from shutil import copyfile\n        if not os.path.exists(os.getenv(\"HOME\") + \"/.config/Discline\"):\n            os.mkdir(os.getenv(\"HOME\") + \"/.config/Discline\")\n        \n        if os.path.exists(os.getenv(\"HOME\") + \"/.config/Discline/config\"):\n            try: \n                os.remove(os.getenv(\"HOME\") + \"/.config/Discline/config\")\n            except: \n                pass\n\n        copyfile(\"res/settings-skeleton.yaml\", os.getenv(\"HOME\") + \"/.config/Discline/config\", follow_symlinks=True) \n        print(term.green(\"Skeleton copied!\" + term.normal))\n        print(term.cyan(\"Your configuration file can be found at ~/.config/Discline\"))\n\n    except KeyboardInterrupt: \n        print(\"Cancelling...\")\n        quit()\n    except SystemExit: \n        quit()\n    except:\n        print(term.red(\"Error creating skeleton file.\"))\n        quit()\n\ndef load_config(path):\n    global settings\n    with open(path) as f:\n        settings = safe_load(f)\n \narg = \"\"\ntry: \n    arg = sys.argv[1]\nexcept IndexError: \n    pass\n\nif arg == \"--store-token\" or arg == \"--token\":\n    pass\nelif arg == \"--skeleton\" or arg == \"--copy-skeleton\":\n    copy_skeleton()\n    quit()\nelif arg == \"--config\":\n    try:\n        load_config(sys.argv[2])\n    except IndexError:\n        print(\"No path provided?\")\n        quit()\n    except:\n        print(\"Invalid path to config entered.\")\n        quit()\nelse:\n    try:\n        load_config(os.getenv(\"HOME\") + \"/.config/Discline/config\")\n    except:\n        try:\n            load_config(os.getenv(\"HOME\") + \"/.Discline\")\n        except:\n            print(term.red(\"ERROR: could not get settings.\"))\n            quit()\n"
  },
  {
    "path": "utils/token_utils.py",
    "content": "import os\nfrom utils.globals import gc\n\ndef get_token():\n    if os.path.exists(os.getenv(\"HOME\") + \"/.config/Discline/token\"):\n        token = \"\"\n        try:\n            f = open(os.getenv(\"HOME\") + \"/.config/Discline/token\", \"r\")\n            token = f.read()\n            f.close()\n        except: pass\n\n        if token != \"\":\n            return token\n    \n    from blessings import Terminal\n    gc.term = Terminal()\n    print(\"\\n\" + gc.term.red(\"Error reading token.\"))\n    print(\"\\n\" + gc.term.yellow(\"Are you sure you stored your token?\"))\n    print(gc.term.yellow(\"Use --store-token to store your token.\"))\n    quit()\n\ndef store_token():\n    import sys\n    from blessings import Terminal\n    \n    token = \"\"\n    try: \n        token=sys.argv[2]\n    except IndexError:\n        print(Terminal().red(\"Error: You did not specify a token!\"))\n        quit()\n\n    if not os.path.exists(os.getenv(\"HOME\") + \"/.config/Discline\"):\n        os.mkdir(os.getenv(\"HOME\") + \"/.config/Discline\")\n\n    if token is not None and token != \"\":\n        # trim off quotes if user added them\n        token = token.strip('\"')\n        token = token.strip(\"'\")\n\n    # ------- Token format seems to vary, disabling this check for now -------- #\n    # if token is None or len(token) < 59 or len(token) > 88:\n        # print(Terminal().red(\"Error: Bad token. Did you paste it correctly?\"))\n        # quit()\n    # ------------------------------------------------------------------------- #\n    \n    try:\n        f = open(os.getenv(\"HOME\") + \"/.config/Discline/token\", \"w\")\n        f.write(token)\n        f.close()\n        print(Terminal().green(\"Token stored!\"))\n    except:\n        print(Terminal().red(\"Error: Could not write token to file.\"))\n        quit()\n"
  },
  {
    "path": "utils/updates.py",
    "content": "def check_for_updates():\n    from utils.globals import gc\n    from os import path\n    \n    if not path.exists(\".git\"):\n        print(gc.term.red(\"Error: client not started from repo location! Cancelling...\"))\n        print(gc.term.yellow(\"You must start the client from its folder to get automatic updates. \\n\"))\n        return\n\n    try:# git pull at start as to automatically update to master repo\n        from subprocess import Popen,PIPE\n        print(gc.term.green + \"Checking for updates...\" + gc.term.normal)\n        process = Popen([\"git\", \"pull\", \"--force\"], stdout=PIPE)\n        output = process.communicate()[0].decode('utf-8').strip()\n\n        if \"Already up to date\" not in output:\n            # print(gc.term.yellow(\"Updates downloaded! Please restart.\"))\n            print(\"\\n \\n\")\n            # This quit() call is breaking the client on MacOS and Linux Mint\n            # The if statement above is being triggered, even when the output IS\n            # \"Already up to date\". Why is this happening?\n            # quit()\n        else:\n            print(\"Already up to date!\" + \"\\n\")\n    except KeyboardInterrupt: print(\"Call to cancel update received, skipping.\")\n    except SystemExit: pass\n    except OSError: # (file not found)\n        # They must not have git installed, no automatic updates for them!\n        print(gc.term.red + \"Error fetching automatic updates! Do you \\\n              have git installed?\" + gc.term.normal)\n    except:\n        print(gc.term.red + \"Unkown error occurred during retrieval \\\n              of updates.\" + gc.term.normal)\n"
  }
]