Full Code of mitchweaver/Discline for AI

master 871379546144 cached
33 files
103.8 KB
23.9k tokens
122 symbols
1 requests
Download .txt
Repository: mitchweaver/Discline
Branch: master
Commit: 871379546144
Files: 33
Total size: 103.8 KB

Directory structure:
gitextract_ta9rwfzc/

├── .gitignore
├── Discline.py
├── LICENSE
├── README.md
├── client/
│   ├── channellog.py
│   ├── client.py
│   ├── on_message.py
│   └── serverlog.py
├── commands/
│   ├── channel_jump.py
│   ├── sendfile.py
│   └── text_emoticons.py
├── input/
│   ├── input_handler.py
│   ├── kbhit.py
│   └── typing_handler.py
├── res/
│   ├── scripts/
│   │   └── discline
│   └── settings-skeleton.yaml
├── ui/
│   ├── line.py
│   ├── text_manipulation.py
│   ├── ui.py
│   ├── ui_curses.py
│   └── ui_utils.py
└── utils/
    ├── globals.py
    ├── hidecursor.py
    ├── print_utils/
    │   ├── channellist.py
    │   ├── emojis.py
    │   ├── help.py
    │   ├── print_utils.py
    │   ├── serverlist.py
    │   └── userlist.py
    ├── quicksort.py
    ├── settings.py
    ├── token_utils.py
    └── updates.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
api-ref.html
notes.md

# more token things just incase someone tries to push
# their branch with their token in here... (smh)
token
login
login.sh
login.txt
discord-token
token.sh
run.sh
start
start.sh
note.txt
notes.txt
token.txt
token
token_login.txt
login.txt


# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# dotenv
.env

# virtualenv
.venv
venv/
ENV/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/

# curses notes and stuff
log.txt
loading.txt
disTypes/

process.md


================================================
FILE: Discline.py
================================================
#!/usr/bin/env python3
# ------------------------------------------------------- #
#                                                         #
# Discline                                                #
#                                                         #
# http://github.com/MitchWeaver/Discline                  #
#                                                         #
# Licensed under GNU GPLv3                                #
#                                                         #
# ------------------------------------------------------- #

import sys
import asyncio
import os
from discord import ChannelType
from input.input_handler import input_handler, key_input, init_input
from input.typing_handler import is_typing_handler
from ui.ui import print_screen
from ui.text_manipulation import calc_mutations
from utils.print_utils.help import print_help
from utils.print_utils.print_utils import *
from utils.globals import *
from utils.settings import copy_skeleton, settings
from utils.updates import check_for_updates
from utils.token_utils import get_token,store_token
from utils import hidecursor
from client.serverlog import ServerLog
from client.channellog import ChannelLog
from client.on_message import on_incoming_message
from client.client import Client

# check if using python 3.5+
# TODO: this still fails if they're using python2
if sys.version_info >= (3, 5): pass
else:
    print(gc.term.red + "Sorry, but this requires python 3.5+" + gc.term.normal)
    quit()

init_complete = False

# Set terminal X11 window title
print('\33]0;Discline\a', end='', flush=True)

os.system("clear")

gc.initClient()

@gc.client.event
async def on_ready():
    await gc.client.wait_until_login()

    # completely hide the system's cursor
    await hidecursor.hide_cursor()

    # these values are set in settings.yaml
    if settings["default_prompt"] is not None:
        gc.client.set_prompt(settings["default_prompt"].lower())
    else: 
        gc.client.set_prompt('~')

    if settings["default_server"] is not None:
        gc.client.set_current_server(settings["default_server"])
        if settings["default_channel"] is not None:
            gc.client.set_current_channel(settings["default_channel"].lower())
            gc.client.set_prompt(settings["default_channel"].lower())

    if settings["default_game"] is not None:
        await gc.client.set_game(settings["default_game"])

    # --------------- INIT SERVERS ----------------------------------------- #
    print("Welcome to " + gc.term.cyan + "Discline" + gc.term.normal + "!")
    await print_line_break()
    await print_user()
    await print_line_break()
    print("Initializing... \n")
    try: 
        sys.stdout.flush()
    except: 
        pass

    for server in gc.client.servers:
        # Null check to check server availability
        if server is None:
            continue
        serv_logs = []
        for channel in server.channels:
            # Null checks to test for bugged out channels
            if channel is None or channel.type is None:
                continue
            # Null checks for bugged out members
            if server.me is None or server.me.id is None \
                    or channel.permissions_for(server.me) is None:
                continue
            if channel.type == ChannelType.text:
                    if channel.permissions_for(server.me).read_messages:
                        try: # try/except in order to 'continue' out of multiple for loops
                            for serv_key in settings["channel_ignore_list"]:
                                if serv_key["server_name"].lower() == server.name.lower():
                                    for name in serv_key["ignores"]:
                                        if channel.name.lower() == name.lower():
                                            raise Found
                            serv_logs.append(ChannelLog(channel, []))
                        except: 
                            continue

        # add the channellog to the tree
        gc.server_log_tree.append(ServerLog(server, serv_logs))

        if settings["debug"]:
            for slog in gc.server_log_tree:
                for clog in slog.get_logs():
                    print(slog.get_name() + " ---- " + clog.get_name())

    # start our own coroutines
    try: asyncio.get_event_loop().create_task(key_input())
    except SystemExit: pass
    except KeyboardInterrupt: pass
    try: asyncio.get_event_loop().create_task(input_handler())
    except SystemExit: pass
    except KeyboardInterrupt: pass
    try: asyncio.get_event_loop().create_task(is_typing_handler())
    except SystemExit: pass
    except KeyboardInterrupt: pass

    # Print initial screen
    await print_screen()

    global init_complete
    init_complete = True

# called whenever the client receives a message (from anywhere)
@gc.client.event
async def on_message(message):
    await gc.client.wait_until_ready()
    if init_complete:
        await on_incoming_message(message)

@gc.client.event
async def on_message_edit(msg_old, msg_new):
    await gc.client.wait_until_ready()
    msg_new.content = msg_new.content + " *(edited)*"

    if init_complete:
        await print_screen()

@gc.client.event
async def on_message_delete(msg):
    await gc.client.wait_until_ready()
    # TODO: PM's have 'None' as a server -- fix this later
    if msg.server is None: return

    try:
        for serverlog in gc.server_log_tree:
            if serverlog.get_server() == msg.server:
                for channellog in serverlog.get_logs():
                    if channellog.get_channel()== msg.channel:
                        channellog.get_logs().remove(msg)
                        if init_complete:
                            await print_screen()
                        return
    except:
        # if the message cannot be found, an exception will be raised
        # this could be #1: if the message was already deleted,
        # (happens when multiple calls get excecuted within the same time)
        # or the user was banned, (in which case all their msgs disappear)
        pass


def main():
    # start the client coroutine
    TOKEN=""
    try:
        if sys.argv[1] == "--help" or sys.argv[1] == "-h":
            from utils.print_utils.help import print_help
            print_help()
            quit()
        elif sys.argv[1] == "--token" or sys.argv[1] == "--store-token":
            store_token()
            quit()
        elif sys.argv[1] == "--skeleton" or sys.argv[1] == "--copy-skeleton":
            # ---- now handled in utils.settings.py ---- #
            pass
        elif sys.argv[1] == "--config":
            # --- now handled in utils.settings.py ---- #
            pass
        else:
            print(gc.term.red("Error: Unknown command."))
            print(gc.term.yellow("See --help for options."))
            quit()
    except IndexError: 
        pass

    check_for_updates()
    token = get_token()
    init_input()

    print(gc.term.yellow("Starting..."))

    # start the client
    try: gc.client.run(token, bot=False)
    except KeyboardInterrupt: pass
    except SystemExit: pass

    # if we are here, the client's loop was cancelled or errored, or user exited
    try: kill()
    except:
        # if our cleanly-exit kill function failed for whatever reason,
        # make sure we at least exit uncleanly
        quit()

if __name__ == "__main__": main()


================================================
FILE: LICENSE
================================================
        DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
                    Version 2, December 2004 

 Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> 

 Everyone is permitted to copy and distribute verbatim or modified 
 copies of this license document, and changing it is allowed as long 
 as the name is changed. 

            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 

  0. You just DO WHAT THE FUCK YOU WANT TO.


================================================
FILE: README.md
================================================
# ![logo_small.png](res/logo/logo_small.png) Discline
------------------------------

![screenshot_main.png](res/screenshots/screenshot_main.png)


# NOTICE: July 20th, 2019

AFAIK discline has become non-functional.

* Python3.7 has changed the syntax of its async libraries.

* discord.py has also gone through a new release cycle which's syntax is not backwards compatible.

Discline will require a full rewrite, possibly done in a new language.

Stay tuned for updates via this readme.

Thanks to all that have supported the project thus far, 
I hope you stick around for whats to come.

<sub>With that said, I'll leave the remainder of the readme intact:</sub>


## How to use:
-------------------------

1. Install the dependencies:

    `sudo pip3 install asyncio discord blessings pyyaml`

2. Clone the repo

    `git clone https://github.com/MitchWeaver/Discline`

3. Find your discord "token"

    * Go to http://discordapp.com/channels/@me

    * Open your browser's developer console. (Normally `F12` or `CTRL-SHIFT-I`)

    * Look for "storage" or "local storage", then find the discord url.

    * Clicking this will show you a list of variables. Look for a line that looks like:

        `"token = 322332r093fwaf032f90323f32f903f23wfa"`

    If you're having troubles, google around, there's a few guides on the net.

    If all else fails, join the dev discord and we'll be glad to help!

4. Run `python3 Discline.py --store-token` to store your token

5. Run `python3 Discline.py --copy-skeleton` to get a template config

6. Edit `~/.config/Discline/config` to your choosing.

7. Launch with python3

    `python3 Discline.py`

    *(alternatively if you have python3.6 you can simply use `./Discline.py`)*


### Current Features
--------------------------

* /channel to switch channel
* /server to switch server
* /nick to change nickname (per server)
* typing without a leading prefix will submit to current chat
* "<USER> is typing..." support
* private channels
* colored output, with user definable colors and custom roles
* Channel logs update when users edit messages
* /channels, /servers, /users to view information
* /game to update the "Now playing: " status
* use /help to see more commands
* unicode emoji displayal support
* sending emojis in messages (unicode *and* custom)
* File uploading via path (ex: /file /path/to/file)
* italic, bold, and underline font support
* inline \`code\` and \`\`\`code\`\`\` block support
* URL detection, highlighting in blue + italics
* automatic updating, fetching the latest master branch's commit
* channel logs blink red upon unread messages
* line scrolling
* discord "Nitro" emojis
* Externalized configs via YAML ~/.config/Discline/config
* @member expansion/mentions
* /status to change online presence

### Planned Features
---------------------------

* emoji reactions
* comment editing and deletion
* private messaging
* message searching

## Dependencies
------------------------

* git (if you want automatic updates)
* [Python 3.5+](https://www.python.org/downloads/)
* [discord.py](https://github.com/Rapptz/discord.py)
* [blessings.py](https://pypi.python.org/pypi/blessings/)
* [PyYAML](https://pypi.python.org/pypi/PyYAML/)
* asyncio

**To install dependencies**:

1. Download Python 3.5/3.6 from the link above
2. Install `pip3`, normally called `python3-pip` in package managers
3. Download the dependencies using pip with the following command:

    `sudo pip3 install asyncio discord blessings pyyaml`


### Color Customization
------------------------

Almost all aspects of the client can be colored to
the user's wishes. You can set these colors from within `~/.config/Discline/config`

Note: These assume that you're using the standard terminal colors. If you
have colors already defined in your ~/.Xresources or similar, this will 
be very confusing.

## Launching
------------------------
Discline uses git for automatic updates, so you must be within the Discline
directory upon starting. Manually you can launch via `python3.6 ./Discline.py`, 
however it is advised to create a helper script to do this for you.

An example script is in the /res/scripts folder, 
edit it to suit your system and tastes.

### A Note On Emojis
-------------------------

Currently *most* of the standard unicode emojis
are displaying. Note your terminal must be able
to render these symbols *and* you must be using a font
set that contains them. Because some of the emojis
that discord uses are non-standard, they may not
display properly. Here is an example of a random
few.

![Image](https://images-ext-2.discordapp.net/external/iN52NdGOWqdWOxby88wiEGs8R81j33ndPjgKX8eKUNA/https/0x0.st/soIy.png?width=400&height=32)

Custom emojis however, are displayed as :emoji_name:

### Note On Font Support
-------------------------

Like emojis, not all terminals and fonts support
italic/bold/underline and 'background' colors, (which are used for \`code\`).
If these features aren't working for you, odds are you are not using a
supported terminal/font. Experiment with different setups to see what works.

![Image](https://0x0.st/sHQ0.png)

*Letting me know what setups __don't__ work helps a lot!*

### Dude this is awesome! How can I support the project?
--------------------------------------------------------

Star it! 🌟

It helps to get it higher in GitHub's search results as well as
making me feel good inside. ;)

If you'd like to contribute, pull requests are __*always*__ welcome!

If you would like to get more info on what could be done or to discuss the
project in general, come join the discord server at: https://discord.gg/rBGQMTk

### FAQ
-------------------------

> Yet another discord cli?

I didn't like any of the implementations I found around github. Too buggy.
Too bloated. Bad UI. No customization. Some, after discord updates,
no longer functioning at all.

> Why use a token and not email/password?

Discord's API __does__ allow for email/pass login, but if you were to have
2FA, (2 factor authentication), enabled on your account, Discord would
interpret this as a malicious attack against your account and disable it.

So, because *"Nobody reads the readme"*, I have disabled this.

> How should I submit a GitHub issue?

Try to include this format:

```
OS: Linux/Debian
Terminal: urxvt
Font: source code pro
Python Version: 3.6
How to reproduce: xxxxxx
```

> It says my python is out of date even though I have 3.5+ installed?

Probably because you have multiple versions installed. Try running with
`python3.5` or `python3.6` rather than just "python3"

> I'm getting weird encoding errors on startup

You probably don't have UTF-8 set. If you're using Linux,
look up how to do this according to your distro.

If you're on BSD, add this to your /etc/profile:

```
export LC_CTYPE=en_US.UTF-8
export LESSCHARSET=utf-8
```

and make sure it gets sourced upon opening your terminal.

### Misc Screenshots
--------------------------

![Image](res/screenshots/kingk22-screenshot.png)

![Image](https://0x0.st/sH5g.png)

![Image](https://0x0.st/sHjn.png)

It can even be configured to hide most elements of the UI in the config:

![Image](res/screenshots/minimal_brown_ss.png)

### Known Bugs
--------------------------

> Line wrapping sometimes doesn't work

This happens if there is too much formatting / coloring being done to the
message that contains that line. I'm looking for a work around.

> When I type many lines before hitting send, the UI sometimes bugs out
and/or the separators encroach upon different sections

Known. Looking for a work around.

> My bug isn't listed here, how can I voice my problem?

If you have a specific issue that isn't listed here or in the
wiki, post a github issue with a detailed explanation and I can
try to get it fixed. Join the discord if you want live help.

### Token Warning
-------------------------------
Do *NOT* share your token with anybody, if someone else gets ahold
of your token, they may control your account. If you are someone
who keeps their ~/.config on github, **add your token to your .gitignore**.
Otherwise it will become public.


### License
-------------------------------

<a href="http://www.wtfpl.net/"><img
       src="http://www.wtfpl.net/wp-content/uploads/2012/12/wtfpl-badge-4.png"
       width="80" height="15" alt="WTFPL" /></a>

### Legal Disclaimer
--------------------------------

Discord hasn't put out any official statement on whether using their
API for 3rd party clients is allowed or not. They *have* said that using
their API to make "self-bots" is against their ToS. By self-bots, it is
my understanding they mean automating non-bot accounts as bots.
My code has no automated functions, or any on_events that provide features
not included in the official client.

As far as I know, nobody has been banned for using things like this before,
but Discord might one day change their mind. With this said, I take **no**
responsibility if this gets you banned.


================================================
FILE: client/channellog.py
================================================
# Wrapper class to make dealing with logs easier
class ChannelLog():

    __channel = ""
    __logs = []
    unread = False
    mentioned_in = False
    # the index of where to start printing the messages
    __index = 0

    def __init__(self, channel, logs):
        self.__channel = channel
        self.__logs = list(logs)

    def get_server(self): return self.__channel.server
    def get_channel(self): return self.__channel

    def get_logs(self):
        return self.__logs

    def get_name(self):
        return self.__channel.name

    def get_server_name(self):
        return self.__channel.server.name

    def append(self, message):
        self.__logs.append(message)

    def index(self, message):
        return self.__logs.index(message)

    def insert(self, i, message):
        self.__logs.insert(i, message)

    def len(self):
        return len(self.__logs)

    def get_index(self):
        return self.__index

    def set_index(self, int):
        self.__index = int

    def inc_index(self, int):
        self.__index += int

    def dec_index(self, int):
        self.__index -= int


================================================
FILE: client/client.py
================================================
import discord
from utils.globals import gc
from utils.settings import settings
import ui.text_manipulation as tm

# inherits from discord.py's Client
class Client(discord.Client):

    # NOTE: These are strings!
    __current_server = ""
    __current_channel = ""
    __prompt = ""

    # discord.Status object
    __status = ""

    # discord.Game object
    __game = ""


    # Note: setting only allows for string types
    def set_prompt(self, string):
        self.__prompt = string.lower()
    def set_current_server(self, string):
        self.__current_server = string.lower()
    def set_current_channel(self, string):
        self.__current_channel = string.lower()
        self.set_prompt(string)

    def get_prompt(self): return self.__prompt
    def get_current_server_name(self): return self.__current_server
    def get_current_channel_name(self): return self.__current_channel

    def get_current_server(self):
        for server in self.servers:
            if server.name.lower() == self.__current_server:
                return server

    def get_current_server_log(self):
        for slog in gc.server_log_tree:
            if slog.get_server() == self.get_current_server():
                return slog

    def get_current_channel(self):
        for server in self.servers:
            if server.name.lower() == self.__current_server.lower():
                for channel in server.channels:
                    if channel.type is discord.ChannelType.text:
                        if channel.name.lower() == self.__current_channel.lower():
                            if channel.permissions_for(server.me).read_messages:
                                return channel

    async def populate_current_channel_log(self):
        slog = self.get_current_server_log()
        for idx, clog in enumerate(slog.get_logs()):
            if clog.get_channel().type is discord.ChannelType.text:
                if clog.get_channel().name.lower() == self.__current_channel.lower():
                    if clog.get_channel().permissions_for(slog.get_server().me).read_messages:
                        async for msg in self.logs_from(clog.get_channel(), limit=settings["max_log_entries"]):
                            clog.insert(0, await tm.calc_mutations(msg))

    def get_current_channel_log(self):
        slog = self.get_current_server_log()
        for idx, clog in enumerate(slog.get_logs()):
            if clog.get_channel().type is discord.ChannelType.text:
                if clog.get_channel().name.lower() == self.__current_channel.lower():
                    if clog.get_channel().permissions_for(slog.get_server().me).read_messages:
                        return clog

    # returns online members in current server
    async def get_online(self):
        online_count = 0
        if not self.get_current_server() == None:
            for member in self.get_current_server().members:
                if member is None: continue # happens if a member left the server
                if member.status is not discord.Status.offline:
                    online_count +=1
            return online_count

    # because the built-in .say is really buggy, just overriding it with my own
    async def say(self, string):
        await self.send_message(self.get_current_channel(), string)

    async def set_game(self, string):
        self.__game = discord.Game(name=string,type=0)
        self.__status = discord.Status.online
        # Note: the 'afk' kwarg handles how the client receives messages, (rates, etc)
        # This is meant to be a "nice" feature, but for us it causes more headache
        # than its worth.
        if self.__game is not None and self.__game != "":
            if self.__status is not None and self.__status != "":
                try: await self.change_presence(game=self.__game, status=self.__status, afk=False)
                except: pass
            else:
                try: await self.change_presence(game=self.__game, status=discord.Status.online, afk=False)
                except: pass

    async def get_game(self):
        return self.__game

    async def set_status(self, string):
        if string == "online":
            self.__status = discord.Status.online
        elif string == "offline":
            self.__status = discord.Status.offline
        elif string == "idle":
            self.__status = discord.Status.idle
        elif string == "dnd":
            self.__status = discord.Status.dnd

        if self.__game is not None and self.__game != "":
            try: await self.change_presence(game=self.__game, status=self.__status, afk=False)
            except: pass
        else:
            try: await self.change_presence(status=self.__status, afk=False)
            except: pass

    async def get_status(self):
        return self.__status


================================================
FILE: client/on_message.py
================================================
from ui.ui import print_screen
from utils.globals import gc
from ui.text_manipulation import calc_mutations

async def on_incoming_message(msg):

    # TODO: make sure it isn't a private message
    
    # find the server/channel it belongs to and add it
    for server_log in gc.server_log_tree:
        if server_log.get_server() == msg.server:
            for channel_log in server_log.get_logs():
                if channel_log.get_channel() == msg.channel:
                    
                    channel_log.append(await calc_mutations(msg))
                    
                    if channel_log.get_channel() is not gc.client.get_current_channel():
                        if msg.server.me.mention in msg.content:
                            channel_log.mentioned_in = True
                        else:
                            channel_log.unread = True

    # redraw the screen
    await print_screen()


================================================
FILE: client/serverlog.py
================================================
from discord import Server, Channel
from client.channellog import ChannelLog

# Simple wrapper class to hold a list of ChannelLogs
class ServerLog():

    __server = ""
    __channel_logs = []

    def __init__(self, server, channel_log_list):
        self.__server = server
        self.__channel_logs = list(channel_log_list)

    def get_server(self):
        return self.__server

    def get_name(self):
        return self.__server.name

    def get_logs(self):
        return self.__channel_logs

    def clear_logs(self):
        for channel_log in self.__channel_logs:
            del channel_log[:]

    # takes list of ChannelLog
    def add_logs(self, log_list):
        for logs in log_list:
            self.__channel_logs.append(logs)


================================================
FILE: commands/channel_jump.py
================================================
from utils.globals import gc
from utils.quicksort import quick_sort_channel_logs
from utils.settings import settings

async def channel_jump(arg):
    logs = []

    num = int(arg[1:]) - 1

    # sub one to allow for "/c0" being the top channel
    if settings["arrays_start_at_zero"]:
        num -= 1
   
    # in case someone tries to go to a negative index
    if num <= -1:
        num = 0

    for slog in gc.server_log_tree:
        if slog.get_server() is gc.client.get_current_server():
            for clog in slog.get_logs():
                logs.append(clog)

    logs = quick_sort_channel_logs(logs)


    if num > len(logs): num = len(logs) - 1

    gc.client.set_current_channel(logs[num].get_name()) 
    logs[num].unread = False
    logs[num].mentioned_in = False


================================================
FILE: commands/sendfile.py
================================================
from getpass import getuser
from utils.globals import gc
from ui.ui import set_display

async def send_file(client, filepath):

    # try to open the file exactly as user inputs it
    try: 
        await client.send_file(client.get_current_channel(), filepath)
    except:
        # assume the user ommited the prefix of the dir path,
        # try to load it starting from user's home directory:
        try:
            filepath = "/home/" + getuser() + "/" + filepath
            await client.send_file(client.get_current_channel(), filepath)
        except:
            # Either a bad file path, the file was too large,
            # or encountered a connection problem during upload
            msg = "Error: Bad filepath"
            await set_display(gc.term.bold + gc.term.red + gc.term.move(gc.term.height - 1, \
                gc.term.width - len(msg) - 1) + msg)


================================================
FILE: commands/text_emoticons.py
================================================
async def check_emoticons(client, cmd):
    if cmd == "shrug": 
        try: await client.send_message(client.get_current_channel(), "¯\_(ツ)_/¯")
        except: pass
    elif cmd == "tableflip": 
        try: await client.send_message(client.get_current_channel(), "(╯°□°)╯︵ ┻━┻")
        except: pass
    elif cmd == "unflip":
        try: await client.send_message(client.get_current_channel(), "┬──┬ ノ( ゜-゜ノ)")
        except: pass
    elif cmd == "zoidberg": 
        try: await client.send_message(client.get_current_channel(), "(/) (°,,°) (/)")
        except: pass
    elif cmd == "lenny": 
        try: await client.send_message(client.get_current_channel(), "( ͡° ͜ʖ ͡°)")
        except: pass
    elif cmd == "lennyx5": 
        try: await client.send_message(client.get_current_channel(), "( ͡°( ͡° ͜ʖ( ͡° ͜ʖ ͡°)ʖ ͡°) ͡°)")
        except: pass
    elif cmd == "glasses": 
        try: await client.send_message(client.get_current_channel(), "(•_•) ( •_•)>⌐■-■ (⌐■_■)")
        except: pass
    elif cmd == "walking_my_mods": 
        try: await client.send_message(client.get_current_channel(), "⌐( ͡° ͜ʖ ͡°) ╯╲___卐卐卐卐")
        except: pass



================================================
FILE: input/input_handler.py
================================================
import asyncio
import discord
from input.kbhit import KBHit
import ui.ui as ui
from utils.globals import gc, kill
from utils.print_utils.help import print_help
from utils.print_utils.userlist import print_userlist
from utils.print_utils.serverlist import print_serverlist
from utils.print_utils.channellist import print_channellist
from utils.print_utils.emojis import print_emojilist
from utils.settings import settings
from commands.text_emoticons import check_emoticons
from commands.sendfile import send_file
from commands.channel_jump import channel_jump

kb = ""

def init_input():
    global kb
    kb = KBHit()

async def key_input():
    await gc.client.wait_until_ready()

    global kb
    memory = ""
    key = ""
    while True:
        if await kb.kbhit() or memory == "[":
            key = await kb.getch()

            ordkey = ord(key)

            if memory == "[":
                if key == "6": # page down
                    gc.client.get_current_channel_log().dec_index(settings["scroll_lines"])
                    del gc.input_buffer[-1]
                elif key == "5": # page up
                    gc.client.get_current_channel_log().inc_index(settings["scroll_lines"])
                    del gc.input_buffer[-1]
            else:
                if ordkey == 10 or ordkey == 13: # enter key
                    gc.user_input = "".join(gc.input_buffer)
                    del gc.input_buffer[:]
                elif ordkey == 127 or ordkey == 8: # backspace
                    if len(gc.input_buffer) > 0:
                        del gc.input_buffer[-1]

                elif ordkey >= 32 and ordkey <= 256: # all letters and special characters
                    if not (ordkey == 126 and (memory == "5" or memory == "6")): # tilde left over from page up/down
                        gc.input_buffer.append(key)
                elif ordkey == 9:
                    gc.input_buffer.append(" " * 4) # tab key

            memory = key
            if key != "[":
                await ui.print_screen()

        if key != "[":
            await asyncio.sleep(0.015)
        elif key == "~":
            await asyncio.sleep(0.1)

async def input_handler():
    await gc.client.wait_until_ready()

    while True:

        # If input is blank, don't do anything
        if gc.user_input == '':
            await asyncio.sleep(0.05)
            continue

        # # check if input is a command
        if gc.user_input[0] == settings["prefix"]:
            # strip the PREFIX
            gc.user_input = gc.user_input[1:]

            # check if contains a space
            if ' ' in gc.user_input:
                # split into command and argument
                command,arg = gc.user_input.split(" ", 1)

                if command == "server" or command == 's':

                    server_name = ""
                    # check if arg is a valid server, then switch
                    for servlog in gc.server_log_tree:
                        if servlog.get_name().lower() == arg.lower():
                            server_name = servlog.get_name()
                            break

                    # if we didn't find an exact match, assume only partial
                    # Note if there are multiple servers containing the same
                    # word, this will only pick the first one. Better than nothing.
                    if server_name == "":
                        for servlog in gc.server_log_tree:
                            if arg.lower() in servlog.get_name().lower():
                                server_name = servlog.get_name()
                                break

                    if server_name != "":
                        gc.client.set_current_server(server_name)

                        # discord.py's "server.default_channel" is buggy.
                        # often times it will return 'none' even when
                        # there is a default channel. to combat this,
                        # we can just get it ourselves.
                        def_chan = ""

                        lowest = 999
                        for chan in servlog.get_server().channels:
                            if chan.type is discord.ChannelType.text:
                                if chan.permissions_for(servlog.get_server().me).read_messages:
                                    if chan.position < lowest:
                                        try:
                                            for serv_key in settings["channel_ignore_list"]:
                                                if serv_key["server_name"].lower() == server_name:
                                                    for name in serv_key["ignores"]:
                                                        if chan.name.lower() == name.lower():
                                                            raise Found
                                        except:
                                            continue
                                        lowest = chan.position
                                        def_chan = chan

                            try:
                                gc.client.set_current_channel(def_chan.name)
                                # and set the default channel as read
                                for chanlog in servlog.get_logs():
                                    if chanlog.get_channel() is def_chan:
                                        chanlog.unread = False
                                        chanlog.mentioned_in = False
                                        break
                            # TODO: Bug: def_chan is sometimes ""
                            except: continue
                    else:
                        ui.set_display(gc.term.red + "Can't find server" + gc.term.normal)


                elif command == "channel" or command == 'c':
                    # check if arg is a valid channel, then switch
                    for servlog in gc.server_log_tree:
                        if servlog.get_server() is gc.client.get_current_server():
                            final_chanlog = ""
                            for chanlog in servlog.get_logs():
                                if chanlog.get_name().lower() == arg.lower():
                                    if chanlog.get_channel().type is discord.ChannelType.text:
                                        if chanlog.get_channel().permissions_for(servlog.get_server().me).read_messages:
                                            final_chanlog = chanlog
                                            break

                            # if we didn't find an exact match, assume partial
                            if final_chanlog == "":
                                for chanlog in servlog.get_logs():
                                    if chanlog.get_channel().type is discord.ChannelType.text:
                                        if chanlog.get_channel().permissions_for(servlog.get_server().me).read_messages:
                                            if arg.lower() in chanlog.get_name().lower():
                                                final_chanlog = chanlog
                                                break

                            if final_chanlog != "":
                                gc.client.set_current_channel(final_chanlog.get_name())
                                final_chanlog.unread = False
                                final_chanlog.mentioned_in = False
                                break
                            else:
                                ui.set_display(gc.term.red + "Can't find channel" + gc.term.normal)

                elif command == "nick":
                    try:
                        await gc.client.change_nickname(gc.client.get_current_server().me, arg)
                    except: # you don't have permission to do this here
                        pass
                elif command == "game":
                    await gc.client.set_game(arg)
                elif command == "file":
                    await send_file(gc.client, arg)
                elif command == "status":
                    status = arg.lower()
                    if status == "away" or status == "afk":
                        status = "idle"
                    elif "disturb" in status:
                        status = "dnd"

                    if status == "online" or status == "offline" \
                       or status == "idle" or status == "dnd":
                        await gc.client.set_status(status)

            # else we must have only a command, no argument
            else:
                command = gc.user_input
                if command == "clear": await ui.clear_screen()
                elif command == "quit": kill()
                elif command == "exit": kill()
                elif command == "help" or command == "h": print_help(gc)
                elif command == "servers" or command == "servs": await print_serverlist()
                elif command == "channels" or command == "chans": await print_channellist()
                elif command == "emojis": await print_emojilist()
                elif command == "users" or command == "members":
                    await ui.clear_screen()
                    await print_userlist()
                elif command[0] == 'c':
                    try:
                        if command[1].isdigit():
                            await channel_jump(command)
                    except IndexError:
                        pass

                await check_emoticons(gc.client, command)


        # this must not be a command...
        else:
            # check to see if it has any custom-emojis, written as :emoji:
            # we will need to expand them.
            # these will look like <:emojiname:39432432903201>
            # check if there might be an emoji
            if gc.user_input.count(":") >= 2:

                # if user has nitro, loop through *all* emojis
                if settings["has_nitro"]:
                    for emoji in gc.client.get_all_emojis():
                        short_name = ':' + emoji.name + ':'
                        if short_name in gc.user_input:
                            # find the "full" name of the emoji from the api
                            full_name = "<:" + emoji.name + ":" + emoji.id + ">"
                            gc.user_input = gc.user_input.replace(short_name, full_name)

                # else the user can only send from this server
                elif gc.client.get_current_server().emojis is not None \
                and len(gc.client.get_current_server().emojis) > 0:
                    for emoji in gc.client.get_current_server().emojis:
                        short_name = ':' + emoji.name + ':'
                        if short_name in gc.user_input:
                            # find the "full" name of the emoji from the api
                            full_name = "<:" + emoji.name + ":" + emoji.id + ">"
                            gc.user_input = gc.user_input.replace(short_name, full_name)

            # if we're here, we've determined its not a command,
            # and we've processed all mutations to the input we want
            # now we will try to send the message.
            text_to_send = gc.user_input
            if "@" in gc.user_input:
                sections = gc.user_input.lower().strip().split(" ")
                sects_copy = []
                for sect in sections:
                    if "@" in sect:
                        for member in gc.client.get_current_server().members:
                            if member is not gc.client.get_current_server().me:
                                if sect[1:] in member.display_name.lower():
                                    sect = "<@!" + member.id + ">"
                    sects_copy.append(sect)
                text_to_send = " ".join(sects_copy)

            # sometimes this fails --- this could be due to occasional
            # bugs in the api, or there was a connection problem
            # So we will try it 3 times, sleeping a bit inbetween
            for i in range(0,3):
                try:
                    await gc.client.send_message(gc.client.get_current_channel(), text_to_send)
                    break
                except:
                    await asyncio.sleep(3)
                    if i == 2:
                        ui.set_display(gc.term.blink_red + "error: could not send message")

        # clear our input as we've just sent it
        gc.user_input = ""

        # update the screen
        await ui.print_screen()

        await asyncio.sleep(0.25)


================================================
FILE: input/kbhit.py
================================================
import os
import sys
import termios
import atexit
from select import select

class KBHit:
    
    def __init__(self):
        self.fd = sys.stdin.fileno()
        if self.fd is not None:
            self.fd = os.fdopen(os.dup(self.fd))

        self.new_term = termios.tcgetattr(self.fd)
        self.old_term = termios.tcgetattr(self.fd)

        # New terminal setting unbuffered
        self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO)
        termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term)

        # Support normal-terminal reset at exit
        atexit.register(self.set_normal_term)
    
    def set_normal_term(self):
        ''' Resets to normal terminal. '''
        termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term)

    async def getch(self):
        return self.fd.read(1)
                        
    async def kbhit(self):
        ''' Returns if keyboard character was hit '''
        dr,dw,de = select([self.fd], [], [], 0)
        return dr != []


================================================
FILE: input/typing_handler.py
================================================
import asyncio
from utils.settings import settings
from utils.globals import gc

async def is_typing_handler():
    # user specified setting in settings.py
    if not settings["send_is_typing"]: return
    
    is_typing = False
    while True:
        # if typing a message, display '... is typing'
        if not is_typing:
            if len(gc.input_buffer) > 0 and gc.input_buffer[0] is not settings["prefix"]:
                await gc.client.send_typing(gc.client.get_current_channel())
                is_typing = True
        elif len(gc.input_buffer) == 0 or gc.input_buffer[0] is settings["prefix"]:
            is_typing = False
        
        await asyncio.sleep(0.5)



================================================
FILE: res/scripts/discline
================================================
#!/bin/sh
#
# This is a script to easily launch discline
# from an application launcher or similar.
#
# http://github.com/mitchweaver/Discline
#
# These are just examples of what I use,
# edit them to match your system and needs.

term='st'
font='MonteCarlo'
shell='/bin/dash'
DISCLINE_DIR="${HOME}/workspace/Discline"
PYTHON_VERSION='3.6'

# ------------------------------------------------------- 

$term -f $font -e $shell -c "cd $DISCLINE_DIR  && \
    python$PYTHON_VERSION Discline.py" &


================================================
FILE: res/settings-skeleton.yaml
================================================
---
# -------------------------------------------------------------------------
#       You can edit these to your preferences. Note: anything silly
#       like max_messages=-1 will break the client. Duh.
# -------------------------------------------------------------------------

# the default server which will be joined upon startup - CASE SENSITIVE!
default_server: discline
# the default channel which will be joined upon startup - CASE SENSITIVE!
default_channel: test_bed

# the leading character used for commands
prefix: /

# whether you have discord "Nitro" -- this enables external emojis
has_nitro: false

# the default prompt when not in a channel
default_prompt: "~"

# the default 'playing ' status in discord
default_game: Discline

# used for various things, your preference
arrays_start_at_zero: false

# Margins for inside the terminal and between elements. NOTE: must be >= 2
# NOTE: some ratios have weird glitches. Just experiment.
margin: 2

# the max amount of messages to be downloaded + kept
# NOTE: minimum = 100! This is normally safe to increase.
max_messages: 100

# the max amount of entries in each channel log to be downloaded + kept
# NOTE: minimum = 100! The larger this is, the slower the client will start.
max_log_entries: 100

# Whether to send "... is typing" when the input buffer is not blank or '/'
send_is_typing: true

# Whether to show in-line emojis in messages
show_emojis: true

# Whether to show, or hide the left channel bar
show_left_bar: true

# Whether to show, or hide the top bar
show_top_bar: true

# Whether to show the separator lines
show_separators: true

# the denominator used to calculate the width of the "left bar"
# NOTE: larger number here, the smaller the bar will be,
#       (although there is still a minimum of 8 chars...) 
left_bar_divider: 9

# Determines whether the left bar 'truncates' the channels or 
# appends "..." to the end when the screen is too small to display them
truncate_channels: false

# Whether to number channels in the left bar (ex: "1. general")
number_channels: false

# the amount of lines to scroll up/down on each trigger
scroll_lines: 3

# ---------------- COLOR SETTINGS ------------------------------------ #
# Available colors are: "white", "red", "blue", "black"
#                       "green", "yellow", "cyan", "magenta"
# Or: you can say "on_<color>" to make it the background (ex: 'on_red')
# Or: you can say "blink_<color>" to have it flash (ex: 'blink_blue')
separator_color: white
server_display_color: cyan
prompt_color: white
prompt_hash_color: red
prompt_border_color: magenta
normal_user_color: green

# messages that contain @you mentions will be this color
mention_color: yellow

# the "default" text color for messages and other things
text_color: white

code_block_color: on_black
url_color: cyan
channel_list_color: white
current_channel_color: green

# colors for the channels in the left bar upon unreads 
unread_channel_color: blink_yellow
unread_mention_color: blink_red
  
# whether channels should blink when they have unread messages
blink_unreads: true

# same as above, but for @mentions 
blink_mentions: true

# here you can define your own custom roles - NOTE: text must match exactly!
# These for example could be "helper" or "trusted", whatever roles
# your servers use
custom_roles:
- name: admin
  color: magenta
- name: mod
  color: blue
- name: bot
  color: yellow
  
# Channel ignore list - This stops the channel from being loaded.
# Effectively like the "mute" + "hide" feature on the official client,
# However with the added benefit that this means these channels won't
# be stored in RAM.    

# Follow the format as below. 
# Note it is TWO spaces, not a tab!
channel_ignore_list:
- server_name: server name
  ignores:
  - some_channel
  - some_other_channel
- server_name: another server name
  ignores:
  - foo
  - bar

# ignore this unless you know what you're doing 
debug: false


================================================
FILE: ui/line.py
================================================
class Line():

    # the text the line contains
    text = "" 
    # how offset from the [left_bar_width + MARGIN] it should be printed
    # this is to offset wrapped lines to better line up with the previous
    offset = 0

    def __init__(self, text, offset):
        self.text = text
        self.offset = offset

    def length(self):
        return len(self.text)


================================================
FILE: ui/text_manipulation.py
================================================
import re
from discord import MessageType
from utils.settings import settings
from utils.globals import gc, get_color
import utils

async def calc_mutations(msg):

    try: # if the message is a file, extract the discord url from it
        json = str(msg.attachments[0]).split("'")
        for string in json:
            if string is not None and string != "":
                if "cdn.discordapp.com/attachments" in string:
                    msg.content = string
                    break
    except IndexError: pass
    
    # otherwise it must not have any attachments and its a regular message
    text = msg.content


    # check for in-line code blocks
    if text.count("```") > 1:
        while("```") in text:
            text = await convert_code_block(text)

        msg.content = text

    # TODO: if there are asterics or __'s in the code, then
    # this will not stop them from being formatted
    # check for in-line code marks
    if text.count("`") > 1:
        while("`") in text:
            text = await convert_code(text)

        msg.content = text

    # check to see if it has any custom-emojis
    # These will look like <:emojiname:39432432903201>
    # We will recursively trim this into just :emojiname:
    if msg.server.emojis is not None and len(msg.server.emojis) > 0:
        for emoji in msg.server.emojis:
            full_name = "<:" + emoji.name + ":" + emoji.id + ">" 
                                
            while full_name in text:
                text = await trim_emoji(full_name, emoji.name, text)

        msg.content = text

    # check for boldened font
    if text.count("**") > 1:
        while("**") in text:
            text = await convert_bold(text)            

        msg.content = text

    # check for italic font
    if text.count("*") > 1:
        while("*") in text:
            text = await convert_italic(text)            

        msg.content = text

    # check for underlined font 
    if text.count("__") > 1:
        while("__") in text:
            text = await convert_underline(text)            

        msg.content = text

    # check for urls
    if "http://" in text or "https://" in text or "www." in text \
        or "ftp://" in text or ".com" in text:

        msg.content = await convert_url(text)

    # check if the message is a "user has pinned..." message
    if msg.type == MessageType.pins_add:
        msg.content = await convert_pin(msg)

    # else it must be a regular message, nothing else
    return msg

async def convert_pin(msg):
    name = ""
    if msg.author.nick is not None and msg.author.nick != "":
        name = msg.author.nick
    else: name = msg.author.name
    return "📌 " + str(name) + " has pinned a message to this channel."


async def trim_emoji(full_name, short_name, string):
    return string.replace(full_name, ":" + short_name + ":")

async def convert_bold(string):
    sections = string.split("**")
    left = sections[0]
    target = sections[1]
    right = "".join(sections[2])
    return gc.term.normal + gc.term.white + left + " " + gc.term.bold(target) + gc.term.normal + \
            gc.term.white + " " + right

async def convert_italic(string):
    sections = string.split("*")
    left = sections[0]
    target = sections[1]
    right = "".join(sections[2])
    return gc.term.normal + gc.term.white +  left + " " + gc.term.italic(target) + gc.term.normal + \
            gc.term.white + " " + right

async def convert_underline(string):
    sections = string.split("__")
    left = sections[0]
    target = sections[1]
    right = "".join(sections[2])
    return gc.term.normal + gc.term.white + left + " " + gc.term.underline(target) + gc.term.normal + \
            gc.term.white + " " + right

async def convert_code(string):
    sections = string.split("`")
    left = sections[0]
    target = sections[1]
    right = "".join(sections[2])
    return gc.term.normal + gc.term.white +  left + " " + await get_color(settings["code_block_color"]) \
            + target  + gc.term.normal \
            + gc.term.white + " " + right

async def convert_code_block(string):
    sections = string.split("```")
    left = sections[0]
    target = sections[1]
    right = "".join(sections[2])
    return gc.term.normal + gc.term.white +  left + " " + gc.term.on_black(target) + gc.term.normal + \
            gc.term.white + " " + right

async def convert_url(string):
    formatted_line = []
    entities = []
    if " " in string:
        entities = string.split(" ")
    else:
        entities.append(string)

    for entity in entities:
        if "http://" in entity or "https://" in entity or "www." in entity \
           or "ftp://" in entity or ".com" in entity:
            entity = await get_color(settings["url_color"]) + gc.term.italic + gc.term.underline + entity + gc.term.normal
        formatted_line.append(entity)

    return " ".join(formatted_line)




================================================
FILE: ui/ui.py
================================================
import sys
from os import system
from discord import ChannelType
from blessings import Terminal
from ui.line import Line
from ui.ui_utils import *
from utils.globals import gc, get_color
from utils.quicksort import quick_sort_channel_logs
from utils.settings import settings
from utils.print_utils.userlist import print_userlist

# maximum number of lines that can be on the screen
# is updated every cycle as to allow automatic resizing
MAX_LINES = 0
# buffer to allow for double buffering (stops screen flashing)
screen_buffer = []
# text that can be set to be displayed for 1 frame
display = ""
display_frames = 0

async def print_screen():
    # Get ready to redraw the screen
    left_bar_width = await get_left_bar_width()
    await clear_screen()

    if settings["show_top_bar"]:
        await print_top_bar(left_bar_width)

    if gc.server_log_tree is not None:
        await print_channel_log(left_bar_width)

    await print_bottom_bar(left_bar_width)

    # Print the buffer containing our message logs
    if settings["show_top_bar"]:
        if settings["show_separators"]:
            with gc.term.location(0, 2):
                print("".join(screen_buffer), end="")
        else:
            with gc.term.location(0, 1):
                print("".join(screen_buffer), end="")

    else:
        with gc.term.location(0, 0):
            print("".join(screen_buffer), end="")

    if settings["show_left_bar"]:
        await print_left_bar(left_bar_width)

    global display, display_frames
    if display != "": 
        print(display)
        display_frames -= 1
        if display_frames <=  0:
            display = ""

async def print_top_bar(left_bar_width):
    topic = ""
    try: 
        if gc.client.get_current_channel().topic is not None:
            topic = gc.client.get_current_channel().topic
    except: 
        # if there is no channel topic, just print the channel name
        try: topic = gc.client.get_current_channel().name
        except: pass

    
    text_length = gc.term.width - (36 + len(gc.client.get_current_server_name()))
    if len(topic) > text_length:
        topic = topic[:text_length]

    with gc.term.location(1,0):
        print("Server: " + await get_color(settings["server_display_color"]) \
                         + gc.client.get_current_server_name() + gc.term.normal, end="")

    with gc.term.location(gc.term.width // 2 - len(topic) // 2, 0):
        print(topic, end="")

    online_text = "Users online: "
    online_count = str(await gc.client.get_online())
    online_length = len(online_text) + len(online_count)

    with gc.term.location(gc.term.width - online_length - 1, 0):
        print(await get_color(settings["server_display_color"]) + online_text \
              + gc.term.normal + online_count, end="")

    if settings["show_separators"]:
        divider = await get_color(settings["separator_color"]) \
                + ("─" * gc.term.width) + "\n" + gc.term.normal

        with gc.term.location(0, 1):
            print(divider, end="")

        with gc.term.location(left_bar_width, 1):
            print(await get_color(settings["separator_color"]) + "┬", end="")


async def set_display(string):
    global display, display_frames
    loc = gc.term.width - 1 - len(string)
    escape_chars = "\e"
    for escape_chars in string:
        loc = loc - 5
    display = gc.term.move(gc.term.height - 1, loc) + string
    display_frames = 3

async def print_left_bar(left_bar_width):
    start = 0
    if settings["show_top_bar"]:
        start = 2

    if settings["show_separators"]:
        length = 0
        length = gc.term.height - settings["margin"]

        sep_color = await get_color(settings["separator_color"])
        for i in range(start, length):
            print(gc.term.move(i, left_bar_width) + sep_color + "│" \
                + gc.term.normal, end="")

    # Create a new list so we can preserve the server's channel order
    channel_logs = []

    for servlog in gc.server_log_tree:
        if servlog.get_server() is gc.client.get_current_server():
            for chanlog in servlog.get_logs():
                channel_logs.append(chanlog)
            break

    channel_logs = quick_sort_channel_logs(channel_logs)
   
    # buffer to print
    buffer = []
    count = 1
            
    for log in channel_logs:
        # don't print categories or voice chats
        # TODO: this will break on private messages
        if log.get_channel().type != ChannelType.text: continue
        text = log.get_name()
        length = len(text)

        if settings["number_channels"]:
            if count <= 9: length += 1
            else: length += 2

        if length > left_bar_width:
            if settings["truncate_channels"]:
                text = text[0:left_bar_width - 1]
            else:
                text = text[0:left_bar_width - 4] + "..."

        if log.get_channel() is gc.client.get_current_channel():
            if settings["number_channels"]:
                buffer.append(gc.term.normal + str(count) + ". " + gc.term.green + text + gc.term.normal + "\n")
            else: 
                buffer.append(gc.term.green + text + gc.term.normal + "\n")
        else: 
            if log.get_channel() is not channel_logs[0]:
                pass

            if log.get_channel() is not gc.client.get_current_channel():

                if log.unread and settings["blink_unreads"]: 
                    text = await get_color(settings["unread_channel_color"]) + text + gc.term.normal
                elif log.mentioned_in and settings["blink_mentions"]: 
                    text = await get_color(settings["unread_mention_color"]) + text + gc.term.normal
            
            if settings["number_channels"]:
                buffer.append(gc.term.normal + str(count) + ". " + text + "\n")
            else:
                buffer.append(text + "\n")
        
        count += 1
        # should the server have *too many channels!*, stop them
        # from spilling over the screen
        if count - 1  == gc.term.height - 2 - settings["margin"]: break

    with gc.term.location(0, start):
        print("".join(buffer))


async def print_bottom_bar(left_bar_width):
    if settings["show_separators"]:
        with gc.term.location(0, gc.term.height - 2):
            print(await get_color(settings["separator_color"]) + ("─" * gc.term.width) \
                + "\n" + gc.term.normal, end="")

        with gc.term.location(left_bar_width, gc.term.height - 2):
            print(await get_color(settings["separator_color"]) + "┴", end="")

    bottom = await get_prompt()
    if len(gc.input_buffer) > 0: bottom = bottom + "".join(gc.input_buffer)
    with gc.term.location(0, gc.term.height - 1):
        print(bottom, end="")

async def clear_screen():
    # instead of "clearing", we're actually just overwriting
    # everything with white space. This mitigates the massive
    # screen flashing that goes on with "cls" and "clear"
    del screen_buffer[:]
    wipe = (" " * (gc.term.width) + "\n") * gc.term.height
    print(gc.term.move(0,0) + wipe, end="")

async def print_channel_log(left_bar_width):
    global INDEX
    
    # If the line would spill over the screen, we need to wrap it
    # NOTE: gc.term.width is calculating every time this function is called.
    #       Meaning that this will automatically resize the screen.
    # note: the "1" is the space at the start
    MAX_LENGTH = gc.term.width - (left_bar_width + settings["margin"]) - 1
    # For wrapped lines, offset them to line up with the previous line
    offset = 0
    # List to put our *formatted* lines in, once we have OK'd them to print
    formatted_lines = []
 
    # the max number of lines that can be shown on the screen
    MAX_LINES = await get_max_lines()

    for server_log in gc.server_log_tree:
        if server_log.get_server() is gc.client.get_current_server():
            for channel_log in server_log.get_logs():
                if channel_log.get_channel() is gc.client.get_current_channel():
                    if channel_log.get_channel() not in gc.channels_entered:
                        await gc.client.populate_current_channel_log()
                        gc.channels_entered.append(channel_log.get_channel())
                    # if the server has a "category" channel named the same
                    # as a text channel, confusion will occur
                    # TODO: private messages are not "text" channeltypes
                    if channel_log.get_channel().type != ChannelType.text: continue
                    
                    for msg in channel_log.get_logs():
                        # The lines of this unformatted message
                        msg_lines = []
           
                        HAS_MENTION = False
                        if "@" + gc.client.get_current_server().me.display_name in msg.clean_content:
                            HAS_MENTION = True

                        author_name = ""
                        try: author_name = msg.author.display_name
                        except:
                            try: author_name = msg.author.name
                            except: author_name = "Unknown Author"
                        
                        author_name_length = len(author_name)
                        author_prefix = await get_role_color(msg) + author_name + ": "

                        color = ""
                        if HAS_MENTION:
                            color = await get_color(settings["mention_color"])
                        else:
                            color = await get_color(settings["text_color"])
                        proposed_line = author_prefix + color + msg.clean_content.strip()

                        # If our message actually consists of
                        # of multiple lines separated by new-line
                        # characters, we need to accomodate for this.
                        # --- Otherwise: msg_lines will just consist of one line
                        msg_lines = proposed_line.split("\n")

                        for line in msg_lines:

                            # strip leading spaces - LEFT ONLY
                            line = line.lstrip()

                            # If our line is greater than our max length,
                            # that means the author has a long-line comment
                            # that wasn't using new line chars...
                            # We must manually wrap it.

                            line_length = len(line)
                            # Loop through, wrapping the lines until it behaves
                            while line_length > MAX_LENGTH:

                                line = line.strip()

                                # Take a section out of the line based on our max length
                                sect = line[:MAX_LENGTH - offset]

                                # Make sure we did not cut a word in half 
                                sect = sect[:sect.strip().rfind(' ')]
                                
                                # If this section isn't the first line of the comment,
                                # we should offset it to better distinguish it
                                offset = 0
                                if author_prefix not in sect:
                                    if line is not msg_lines[0]:
                                        offset = author_name_length + settings["margin"]
                                # add in now formatted line!
                                formatted_lines.append(Line(sect.strip(), offset))
                            
                                # since we just wrapped a line, we need to 
                                # make sure we don't overwrite it next time

                                # Split the line between what has been formatted, and
                                # what still remains needing to be formatted
                                if len(line) > len(sect):
                                    line = line.split(sect)[1]
                                    
                                # find the "real" length of the line, by subtracting
                                # any escape characters it might have. It would
                                # be wasteful to loop through all of the possibilities
                                # so instead we will simply subtract the length
                                # of the shortest for each that it has.
                                line_length = len(line)
                                target = "\e"
                                for target in line:
                                    line_length -= 5

                            # Once here, the string was either A: already short enough
                            # to begin with, or B: made through our while loop and has
                            # since been chopped down to less than our MAX_LENGTH
                            if len(line.strip()) > 0:
                                
                                offset = 0
                                if author_prefix not in line:
                                    offset = author_name_length + settings["margin"]

                                formatted_lines.append(Line(line.strip(), offset))
                                
                    # where we should start printing from
                    # clamp the index as not to show whitespace
                    if channel_log.get_index() < MAX_LINES: 
                        channel_log.set_index(MAX_LINES)
                    elif channel_log.get_index() > len(formatted_lines): 
                        channel_log.set_index(len(formatted_lines))

                    # ----- Trim out list to print out nicely ----- #
                    # trims off the front of the list, until our index
                    del formatted_lines[0:(len(formatted_lines) - channel_log.get_index())]
                    # retains the amount of lines for our screen, deletes remainder
                    del formatted_lines[MAX_LINES:]

                    # if user does not want the left bar, do not add margin
                    space = " "
                    if not settings["show_left_bar"]:
                        space = ""
                    
                    # add to the buffer!
                    for line in formatted_lines:
                        screen_buffer.append(space * (left_bar_width + \
                                settings["margin"] + line.offset) + line.text + "\n")

                    # return as not to loop through all channels unnecessarily
                    return


================================================
FILE: ui/ui_curses.py
================================================
import sys
import time
from os import system
import curses
from curses import ascii as cAscii
from discord import ChannelType
from blessings import Terminal
from ui.line import Line
from ui.ui_utils import *
from utils.globals import *
from utils.quicksort import quick_sort_channel_logs
from utils.settings import settings
from utils.print_utils.userlist import print_userlist

# maximum number of lines that can be on the screen
# is updated every cycle as to allow automatic resizing
MAX_LINES = 0
# screen
global stdscr
stdscr = None
global windows
windows = []
# buffer to allow for double buffering (stops screen flashing)
screen_buffer = []
# text that can be set to be displayed for 1 frame
display = ""
display_frames = 0

def cursesInit():
    stdscr = curses.initscr()
    curses.noecho()
    curses.cbreak()
    stdscr.keypad(True)

def cursesDestroy():
    curses.nocbreak()
    stdscr.keypad(False)
    curses.echo()
    curses.endwin()

def cursesRefresh():
    stdscr.noutrefresh()
    for win in windows:
        win.noutrefresh()
    curses.doupdate()

async def print_screen():
    stdscr.clear()
    stdscr.addstr("Test")
    cursesRefresh()
    ## Get ready to redraw the screen
    #left_bar_width = await get_left_bar_width()
    #await clear_screen()

    #if settings["show_top_bar"]:
    #    await print_top_bar(left_bar_width)

    #if server_log_tree is not None:
    #    await print_channel_log(left_bar_width)

    #await print_bottom_bar(left_bar_width)

    ## Print the buffer containing our message logs
    #if settings["show_top_bar"]:
    #    if settings["show_separators"]:
    #        with term.location(0, 2):
    #            print("".join(screen_buffer), end="")
    #    else:
    #        with term.location(0, 1):
    #            print("".join(screen_buffer), end="")

    #else:
    #    with term.location(0, 0):
    #        print("".join(screen_buffer), end="")

    #if settings["show_left_bar"]:
    #    await print_left_bar(left_bar_width)

    #global display, display_frames
    #if display != "":
    #    print(display)
    #    display_frames -= 1
    #    if display_frames <=  0:
    #        display = ""

async def print_top_bar(left_bar_width):
    topic = ""
    try:
        if client.get_current_channel().topic is not None:
            topic = client.get_current_channel().topic
    except:
        # if there is no channel topic, just print the channel name
        try: topic = client.get_current_channel().name
        except: pass


    text_length = term.width - (36 + len(client.get_current_server_name()))
    if len(topic) > text_length:
        topic = topic[:text_length]

    with term.location(1,0):
        print("Server: " + await get_color(settings["server_display_color"]) \
                         + client.get_current_server_name() + term.normal, end="")

    with term.location(term.width // 2 - len(topic) // 2, 0):
        print(topic, end="")

    online_text = "Users online: "
    online_count = str(await client.get_online())
    online_length = len(online_text) + len(online_count)

    with term.location(term.width - online_length - 1, 0):
        print(await get_color(settings["server_display_color"]) + online_text \
              + term.normal + online_count, end="")

    if settings["show_separators"]:
        divider = await get_color(settings["separator_color"]) \
                + ("─" * term.width) + "\n" + term.normal

        with term.location(0, 1):
            print(divider, end="")

        with term.location(left_bar_width, 1):
            print(await get_color(settings["separator_color"]) + "┬", end="")


async def set_display(string):
    global display, display_frames
    loc = term.width - 1 - len(string)
    escape_chars = "\e"
    for escape_chars in string:
        loc = loc - 5
    display = term.move(term.height - 1, loc) + string
    display_frames = 3

async def print_left_bar(left_bar_width):
    start = 0
    if settings["show_top_bar"]:
        start = 2

    if settings["show_separators"]:
        length = 0
        length = term.height - settings["margin"]

        sep_color = await get_color(settings["separator_color"])
        for i in range(start, length):
            print(term.move(i, left_bar_width) + sep_color + "│" \
                + term.normal, end="")

    # Create a new list so we can preserve the server's channel order
    channel_logs = []

    for servlog in server_log_tree:
        if servlog.get_server() is client.get_current_server():
            for chanlog in servlog.get_logs():
                channel_logs.append(chanlog)
            break

    channel_logs = quick_sort_channel_logs(channel_logs)

    # buffer to print
    buffer = []
    count = 1

    for log in channel_logs:
        # don't print categories or voice chats
        # TODO: this will break on private messages
        if log.get_channel().type != ChannelType.text: continue
        text = log.get_name()
        length = len(text)

        if settings["number_channels"]:
            if count <= 9: length += 1
            else: length += 2

        if length > left_bar_width:
            if settings["truncate_channels"]:
                text = text[0:left_bar_width - 1]
            else:
                text = text[0:left_bar_width - 4] + "..."

        if log.get_channel() is client.get_current_channel():
            if settings["number_channels"]:
                buffer.append(term.normal + str(count) + ". " + term.green + text + term.normal + "\n")
            else:
                buffer.append(term.green + text + term.normal + "\n")
        else:
            if log.get_channel() is not channel_logs[0]:
                pass

            if log.get_channel() is not client.get_current_channel():

                if log.unread and settings["blink_unreads"]:
                    text = await get_color(settings["unread_channel_color"]) + text + term.normal
                elif log.mentioned_in and settings["blink_mentions"]:
                    text = await get_color(settings["unread_mention_color"]) + text + term.normal

            if settings["number_channels"]:
                buffer.append(term.normal + str(count) + ". " + text + "\n")
            else:
                buffer.append(text + "\n")

        count += 1
        # should the server have *too many channels!*, stop them
        # from spilling over the screen
        if count - 1  == term.height - 2 - settings["margin"]: break

    with term.location(0, start):
        print("".join(buffer))


async def print_bottom_bar(left_bar_width):
    if settings["show_separators"]:
        with term.location(0, term.height - 2):
            print(await get_color(settings["separator_color"]) + ("─" * term.width) \
                + "\n" + term.normal, end="")

        with term.location(left_bar_width, term.height - 2):
            print(await get_color(settings["separator_color"]) + "┴", end="")

    bottom = await get_prompt()
    if len(input_buffer) > 0: bottom = bottom + "".join(input_buffer)
    with term.location(0, term.height - 1):
        print(bottom, end="")

async def clear_screen():
    # This is more efficient
    cursesRefresh()
    ## instead of "clearing", we're actually just overwriting
    ## everything with white space. This mitigates the massive
    ## screen flashing that goes on with "cls" and "clear"
    #del screen_buffer[:]
    #wipe = (" " * (term.width) + "\n") * term.height
    #print(term.move(0,0) + wipe, end="")

async def print_channel_log(left_bar_width):
    global INDEX

    # If the line would spill over the screen, we need to wrap it
    # NOTE: term.width is calculating every time this function is called.
    #       Meaning that this will automatically resize the screen.
    # note: the "1" is the space at the start
    MAX_LENGTH = term.width - (left_bar_width + settings["margin"]) - 1
    # For wrapped lines, offset them to line up with the previous line
    offset = 0
    # List to put our *formatted* lines in, once we have OK'd them to print
    formatted_lines = []

    # the max number of lines that can be shown on the screen
    MAX_LINES = await get_max_lines()

    for server_log in server_log_tree:
        if server_log.get_server() is client.get_current_server():
            for channel_log in server_log.get_logs():
                if channel_log.get_channel() is client.get_current_channel():
                    # if the server has a "category" channel named the same
                    # as a text channel, confusion will occur
                    # TODO: private messages are not "text" channeltypes
                    if channel_log.get_channel().type != ChannelType.text: continue

                    for msg in channel_log.get_logs():
                        # The lines of this unformatted message
                        msg_lines = []

                        HAS_MENTION = False
                        if "@" + client.get_current_server().me.display_name in msg.clean_content:
                            HAS_MENTION = True

                        author_name = ""
                        try: author_name = msg.author.display_name
                        except:
                            try: author_name = msg.author.name
                            except: author_name = "Unknown Author"

                        author_name_length = len(author_name)
                        author_prefix = await get_role_color(msg) + author_name + ": "

                        color = ""
                        if HAS_MENTION:
                            color = await get_color(settings["mention_color"])
                        else:
                            color = await get_color(settings["text_color"])
                        proposed_line = author_prefix + color + msg.clean_content.strip()

                        # If our message actually consists of
                        # of multiple lines separated by new-line
                        # characters, we need to accomodate for this.
                        # --- Otherwise: msg_lines will just consist of one line
                        msg_lines = proposed_line.split("\n")

                        for line in msg_lines:

                            # strip leading spaces - LEFT ONLY
                            line = line.lstrip()

                            # If our line is greater than our max length,
                            # that means the author has a long-line comment
                            # that wasn't using new line chars...
                            # We must manually wrap it.

                            line_length = len(line)
                            # Loop through, wrapping the lines until it behaves
                            while line_length > MAX_LENGTH:

                                line = line.strip()

                                # Take a section out of the line based on our max length
                                sect = line[:MAX_LENGTH - offset]

                                # Make sure we did not cut a word in half
                                sect = sect[:sect.strip().rfind(' ')]

                                # If this section isn't the first line of the comment,
                                # we should offset it to better distinguish it
                                offset = 0
                                if author_prefix not in sect:
                                    if line is not msg_lines[0]:
                                        offset = author_name_length + settings["margin"]
                                # add in now formatted line!
                                formatted_lines.append(Line(sect.strip(), offset))

                                # since we just wrapped a line, we need to
                                # make sure we don't overwrite it next time

                                # Split the line between what has been formatted, and
                                # what still remains needing to be formatted
                                if len(line) > len(sect):
                                    line = line.split(sect)[1]

                                # find the "real" length of the line, by subtracting
                                # any escape characters it might have. It would
                                # be wasteful to loop through all of the possibilities
                                # so instead we will simply subtract the length
                                # of the shortest for each that it has.
                                line_length = len(line)
                                target = "\e"
                                for target in line:
                                    line_length -= 5

                            # Once here, the string was either A: already short enough
                            # to begin with, or B: made through our while loop and has
                            # since been chopped down to less than our MAX_LENGTH
                            if len(line.strip()) > 0:

                                offset = 0
                                if author_prefix not in line:
                                    offset = author_name_length + settings["margin"]

                                formatted_lines.append(Line(line.strip(), offset))

                    # where we should start printing from
                    # clamp the index as not to show whitespace
                    if channel_log.get_index() < MAX_LINES:
                        channel_log.set_index(MAX_LINES)
                    elif channel_log.get_index() > len(formatted_lines):
                        channel_log.set_index(len(formatted_lines))

                    # ----- Trim out list to print out nicely ----- #
                    # trims off the front of the list, until our index
                    del formatted_lines[0:(len(formatted_lines) - channel_log.get_index())]
                    # retains the amount of lines for our screen, deletes remainder
                    del formatted_lines[MAX_LINES:]

                    # if user does not want the left bar, do not add margin
                    space = " "
                    if not settings["show_left_bar"]:
                        space = ""

                    # add to the buffer!
                    for line in formatted_lines:
                        screen_buffer.append(space * (left_bar_width + \
                                settings["margin"] + line.offset) + line.text + "\n")

                    # return as not to loop through all channels unnecessarily
                    return


================================================
FILE: ui/ui_utils.py
================================================
from utils.globals import get_color, gc
from utils.settings import settings

async def get_prompt():
    left = await get_color(settings["prompt_border_color"]) + "["
    right = await get_color(settings["prompt_border_color"]) + "]: " + gc.term.normal
    middle = ""
    if gc.client.get_prompt() == settings["default_prompt"]:
        middle = " " + await get_color(settings["prompt_color"]) + settings["default_prompt"] + " "
    else:
        middle = await get_color(settings["prompt_hash_color"]) + "#" \
                + await get_color(settings["prompt_color"]) + gc.client.get_prompt()

    return left + middle + right


async def get_max_lines():
    num = 0
    if settings["show_top_bar"] and settings["show_separators"]:
        num = gc.term.height - settings["margin"] * 2
    elif settings["show_top_bar"] and not settings["show_separators"]:
        num = gc.term.height - settings["margin"]
    elif not settings["show_top_bar"] and not settings["show_separators"]:
        num = gc.term.height - 1
        
    return num

async def get_left_bar_width():
    if not settings["show_left_bar"]: return 0

    left_bar_width = gc.term.width // settings["left_bar_divider"]
    if left_bar_width < 8: return  8
    else: return left_bar_width

async def get_role_color(msg):
    color = ""
    try: 
        r = msg.author.top_role.name.lower()
        for role in settings["custom_roles"]:
            if r == role["name"].lower():
                color = await get_color(role["color"])

        if color is not "": # The user must have already been assigned a custom role
            pass
        elif settings["normal_user_color"] is not None:
            color = await get_color(settings["normal_user_color"])
        else: color = gc.term.green
    # if this fails, the user either left or was banned
    except: 
        if settings["normal_user_color"] is not None:
            color = await get_color(settings["normal_user_color"])
        else: color = gc.term.green
    return color



================================================
FILE: utils/globals.py
================================================
from sys import exit
from blessings import Terminal
from utils.settings import settings
import sys

NO_SETTINGS=False
try:
    if sys.argv[1] == "--store-token" or sys.argv[1] == "--token":
        NO_SETTINGS=True
except IndexError: 
    pass

class GlobalsContainer:
    def __init__(self):
        self.term = Terminal()
        self.client = None
        self.server_log_tree = []
        self.input_buffer = []
        self.user_input = ""
        self.channels_entered = []

    def initClient(self):
        from client.client import Client
        if NO_SETTINGS:
            messages=100
        else:
            messages=settings["max_messages"]
        self.client = Client(max_messages=messages)

gc = GlobalsContainer()

# kills the program and all its elements gracefully
def kill():
    # attempt to cleanly close our loops
    import asyncio
    try: gc.client.close()
    except: pass
    try: asyncio.get_event_loop().close()
    except: pass
    try:# since we're exiting, we can be nice and try to clear the screen
        from os import system
        system("clear")
    except: pass
    exit()

# returns a "Channel" object from the given string
async def string2channel(channel):
    for srv in gc.client.servers:
        if srv.name == channel.server.name:
            for chan in srv.channels:
                if chan.name == channel:
                    return chan

# returns a "Channellog" object from the given string
async def get_channel_log(channel):
    for srvlog in gc.server_log_tree:
        if srvlog.get_name().lower() == channel.server.name.lower():
            for chanlog in srvlog.get_logs():
                if chanlog.get_name().lower() == channel.name.lower():
                    return chanlog

# returns a "Channellog" from a given "Channel"
async def chan2log(chan):
    for srvlog in gc.server_log_tree:
        if srvlog.get_name().lower() == chan.server.name.lower():
            for clog in srvlog.get_logs():
                if clog.get_name().lower() == chan.name.lower():
                    return clog
 
# returns a "Serverlog" from a given "Server"
async def serv2log(serv):
    for srvlog in gc.server_log_tree:
        if srvlog.get_name().lower() == serv.name.lower():
            return srvlog

# takes in a string, returns the appropriate term.color
async def get_color(string):
    arg = string.strip().lower()

    if arg == "white":   return gc.term.white
    if arg == "black":   return gc.term.black
    if arg == "red":     return gc.term.red
    if arg == "blue":    return gc.term.blue
    if arg == "yellow":  return gc.term.yellow
    if arg == "cyan":    return gc.term.cyan
    if arg == "magenta": return gc.term.magenta
    if arg == "green":   return gc.term.green

    if arg == "on_white":   return gc.term.on_white
    if arg == "on_black":   return gc.term.on_black
    if arg == "on_red":     return gc.term.on_red
    if arg == "on_blue":    return gc.term.on_blue
    if arg == "on_yellow":  return gc.term.on_yellow
    if arg == "on_cyan":    return gc.term.on_cyan
    if arg == "on_magenta": return gc.term.on_magenta
    if arg == "on_green":   return gc.term.on_green

    if arg == "blink_white":   return gc.term.blink_white
    if arg == "blink_black":   return gc.term.blink_black
    if arg == "blink_red":     return gc.term.blink_red
    if arg == "blink_blue":    return gc.term.blink_blue
    if arg == "blink_yellow":  return gc.term.blink_yellow
    if arg == "blink_cyan":    return gc.term.blink_cyan
    if arg == "blink_magenta": return gc.term.blink_magenta
    if arg == "blink_green":   return gc.term.blink_green


    # if we're here, someone has one of their settings.yaml
    # colors defined wrong. We'll be nice and just return white.
    return gc.term.normal + gc.term.white


================================================
FILE: utils/hidecursor.py
================================================
from sys import stdout

# completely hides the system cursor
async def hide_cursor():
    stdout.write("\033[?25l")
    stdout.flush()


================================================
FILE: utils/print_utils/channellist.py
================================================
from os import system
from discord import ChannelType
from ui.ui import clear_screen, set_display
from utils.globals import gc

async def print_channellist():
    if len(gc.client.servers) == 0:
        set_display(gc.term.red + "Error: You are not in any servers.")
        return
    
    if len(gc.client.get_current_server().channels) == 0:
        set_display(gc.term.red + "Error: Does this server not have any channels?" + gc.term.normal)
        return

    buffer = []
    for channel in gc.client.get_current_server().channels:
        if channel.type == ChannelType.text:
            name = channel.name
            name = name.replace("'", "")
            name = name.replace('"', "")
            name = name.replace("`", "")
            name = name.replace("$(", "")
            buffer.append(name + "\n")

    await clear_screen()
    system("echo '" + gc.term.cyan + "Available Channels in " \
           + gc.term.magenta + gc.client.get_current_server_name() + ": \n" \
           + "---------------------------- \n \n" \
           + gc.term.yellow + "".join(buffer) \
           + gc.term.green + "~ \n" \
           + gc.term.green + "~ \n" \
           + gc.term.green + "(press \'q\' to quit this dialog) \n" \
           + "' | less -R")



================================================
FILE: utils/print_utils/emojis.py
================================================
from os import system
from ui.ui import clear_screen, set_display
from utils.globals import gc, get_color
from utils.settings import settings

async def print_emojilist():
    if len(gc.client.servers) == 0:
        set_display(gc.term.red + "Error: You are not in any servers." + gc.term.normal)
        return

    server_name = gc.client.get_current_server_name()
    server_name = server_name.replace("'", "")
    server_name = server_name.replace('"', "")
    server_name = server_name.replace("`", "")
    server_name = server_name.replace("$(", "")

    emojis = []
    server_emojis = ""

    try: server_emojis = gc.client.get_current_server().emojis
    except: pass

    if server_emojis is not None and server_emojis != "":
        for emoji in server_emojis:
            name = emoji.name
            name = name.replace("'", "")
            name = name.replace('"', "")
            name = name.replace("`", "")
            name = name.replace("$(", "")
            emojis.append(gc.term.yellow + ":" + name + ":" + "\n")

    await clear_screen()
    system("echo '" + gc.term.magenta + "Available Emojis in: " + gc.term.cyan + server_name +"\n" + gc.term.normal \
        + "---------------------------- \n" \
        + "".join(emojis) \
        + gc.term.green + "~ \n" \
        + gc.term.green + "~ \n" \
        + gc.term.green + "(press q to quit this dialog) \n" \
        + "' | less -R")


================================================
FILE: utils/print_utils/help.py
================================================
from os import system

def print_help(gc):
    system("clear")
    system("echo '" + gc.term.normal \
        + gc.term.green("Launch Arguments: \n") + gc.term.red \
        + "--------------------------------------------- \n" \
        + get_line(gc, "--copy-skeleton", " --- ", "copies template settings") \
        + gc.term.cyan("This file can be found at ~/.config/Discline/config \n") \
        + "\n"
        + get_line(gc, "--store-token", "   --- ", "stores your token") \
        + gc.term.cyan("This file can be found at ~/.config/Discline/token \n") \
        + "\n"
        + get_line(gc, "--config", "        --- ", "specify a specific config path") \
        + "\n"
        + gc.term.green("Available Commands: \n") + gc.term.red \
        + "--------------------------------------------- \n" \
        + get_line(gc, "/channel", "   - ", "switch to channel - (alias: 'c')") \
        + get_line(gc, "/server", "    - ", "switch server     - (alias: 's')") \
        + gc.term.cyan + "Note: these commands can now fuzzy-find! \n" \
        + "\n" \
        + get_line(gc, "/servers", "   - ", "list available servers") \
        + get_line(gc, "/channels", "  - ", "list available channels") \
        + get_line(gc, "/users", "     - ", "list servers users") \
        + get_line(gc, "/emojis", "    - ", "list servers custom emojis") \
        + "\n" \
        + get_line(gc, "/nick", "      - ", "change server nick name") \
        + get_line(gc, "/game", "      - ", "change your game status") \
        + get_line(gc, "/file", "      - ", "upload a file via path") \
        + get_line(gc, "/status", "    - ", "change online presence") \
        + gc.term.cyan + "This can be either 'online', 'offline', 'away', or 'dnd' \n" \
        + gc.term.cyan + "(dnd = do not disturb) \n" \
        + "\n" \
        + get_line(gc, "/cX", "        - ", "shorthand to change channel (Ex: /c1)") \
        + gc.term.cyan("This can be configured to start at 0 in your config") \
        + "\n" \
        + "\n" \
        + get_line(gc, "/quit", "      - ", "exit cleanly") \
        + "\n \n" \
        + gc.term.magenta + "Note: You can send emojis by using :emojiname: \n" \
        + gc.term.cyan("Nitro emojis do work! Make sure you have \n") \
        + gc.term.cyan("nitro enabled in your config. \n") \
        + "\n"
        + gc.term.yellow + "You can scroll up/down in channel logs \n" \
        + gc.term.yellow + "by using PageUp/PageDown. \n" \
        + gc.term.green + "~ \n" \
        + gc.term.green + "~ \n" \
        + gc.term.green + "~ \n" \
        + gc.term.green + "(press q to quit this dialog)" \
        + "' | less -R")



def get_line(gc, command, div, desc):
    return gc.term.yellow(command) + gc.term.cyan(div) + gc.term.normal + desc + "\n"


================================================
FILE: utils/print_utils/print_utils.py
================================================
import discord
from utils.globals import gc

async def print_servers():
    print("Available servers: ")
    print_line_break();
    for server  in  gc.client.servers:
        print(server.name)

async def print_user():
    print('Logged in as: ' + gc.term.green + gc.client.user.name + gc.term.normal)

async def print_line_break():
    print("-" * int(gc.term.width * 0.45))

async def print_channels(server):
    print("Available channels:")
    print_line_break();
    for channel in  server.channels:
        print(channel.name)


================================================
FILE: utils/print_utils/serverlist.py
================================================
from os import system
from ui.ui import clear_screen, set_display
from utils.globals import get_color, gc
from utils.settings import settings

async def print_serverlist():
    if len(gc.client.servers) == 0:
        set_display(gc.term.red + "Error: You are not in any servers." + gc.term.normal)
        return

    buffer = []
    for slog in gc.server_log_tree:
        name = slog.get_name()
        name = name.replace("'", "")
        name = name.replace('"', "")
        name = name.replace("`", "")
        name = name.replace("$(", "")

        if slog.get_server() is gc.client.get_current_server():
            buffer.append(await get_color(settings["current_channel_color"]) + name + gc.term.normal + "\n")
            continue

        string = ""
        for clog in slog.get_logs():
            if clog.mentioned_in:
                string = await get_color(settings["unread_mention_color"]) + name + gc.term.normal + "\n"
                break
            elif clog.unread:
                string = await get_color(settings["unread_channel_color"]) + name + gc.term.normal + "\n"
                break
        
        if string == "":
            string = await get_color(settings["text_color"]) + name + gc.term.normal + "\n"

        buffer.append(string)
            
    await clear_screen()
    system("echo '" + gc.term.magenta + "Available Servers: \n" + gc.term.normal \
        + "---------------------------- \n \n" \
        + "".join(buffer) \
        + gc.term.green + "~ \n" \
        + gc.term.green + "~ \n" \
        + gc.term.green + "(press q to quit this dialog) \n" \
        + "' | less -R")


================================================
FILE: utils/print_utils/userlist.py
================================================
from os import system
from discord import Status
from utils.globals import gc

# On call of the /users command, this will print
# out a nicely sorted, colored list of all users
# connected to the clients current server and pipe
# it to the system pager, (in this case `less`)

class UserList:
        
    def __init__(self):
        # place to store the names, separted in categories
        self.online = []
        self.offline = []
        self.idle = []
        self.dnd = []

    def add(self, member, tag):
        listing = member.name + tag + " \n"
        if member.status is Status.online:
            self.online.append(listing)
        elif member.status is Status.offline:
            self.offline.append(listing)
        elif member.status is Status.idle:
            self.idle.append(listing)
        elif member.status is Status.dnd:
            self.dnd.append(listing)

    def sort(self):
        self.online = sorted(self.online, key=str.lower)
        self.offline = sorted(self.offline, key=str.lower)
        self.idle = sorted(self.idle, key=str.lower)
        self.dnd = sorted(self.dnd, key=str.lower)
        
        # now they are sorted, we can colorize them
        # we couldn't before as the escape codes mess with
        # the sorting algorithm
        tmp = []
        for name in self.online: 
            tmp.append(gc.term.green + name)
        self.online = list(tmp)
        del tmp[:]

        for name in self.idle: 
            tmp.append(gc.term.yellow + name)
        self.idle = list(tmp)
        del tmp[:]
       
        for name in self.dnd: 
            tmp.append(gc.term.black + name)
        self.dnd = list(tmp)
        del tmp[:]

        for name in self.offline: 
            tmp.append(gc.term.red + name)
        self.offline = list(tmp)
        del tmp[:]

        return "".join(self.online) + "".join(self.offline) \
                + "".join(self.idle) + "".join(self.dnd)

async def print_userlist():
    if len(gc.client.servers) == 0:
        print("Error: You are not in any servers.")
        return
    
    if len(gc.client.get_current_server().channels) == 0:
        print("Error: Does this server not have any channels?")
        return

    # lists to contain our "Member" objects
    nonroles = UserList()
    admins = UserList()
    mods = UserList() 
    bots = UserList() 
    everything_else = UserList() 

    for member in gc.client.get_current_server().members:
        if member is None: continue # happens if a member left the server
        
        if member.top_role.name == "admin" or member.top_role.name == "Admin":
            admins.add(member, " - (Admin)")
        elif member.top_role.name == "mod" or member.top_role.name == "Mod":
            mods.add(member, "- (Mod)")
        elif member.top_role.name == "bot" or member.top_role.name == "Bot":
            bots.add(member, " - (bot)")
        elif member.top_role.is_everyone: nonroles.add(member, "")
        else: everything_else.add(member, " - " + member.top_role.name)

   
    # the final buffer that we're actually going to print
    buffer = []

    if admins is not None: buffer.append(admins.sort())
    if mods is not None: buffer.append(mods.sort())

    buffer.append("\n" + gc.term.magenta + "---------------------------- \n\n")

    if bots is not None: buffer.append(bots.sort())
    if everything_else is not None: buffer.append(everything_else.sort())

    buffer.append("\n" + gc.term.magenta + "---------------------------- \n\n")

    if nonroles is not None: buffer.append(nonroles.sort())

    buffer_copy = []
    for name in buffer:
        name = name.replace("'", "")
        name = name.replace('"', "")
        name = name.replace("`", "")
        name = name.replace("$(", "")
        buffer_copy.append(name)

    system("echo '" + gc.term.yellow + "Members in " \
           + gc.client.get_current_server().name + ": \n" \
           + gc.term.magenta + "---------------------------- \n \n" \
           + "".join(buffer_copy) \
           + gc.term.green + "~ \n" \
           + gc.term.green + "~ \n" \
           + gc.term.green + "(press \'q\' to quit this dialog) \n" \
           # NOTE: the -R flag here enables color escape codes
           + "' | less -R")

# takes in a member, returns a color based on their status
def get_status_color(member):
    if member.status is Status.online:
        return gc.term.green
    if member.status is Status.idle:  # aka "away"
        return gc.term.yellow
    if member.status is Status.offline:
        return gc.term.red
    if member.status is Status.dnd: # do not disturb
        return gc.term.black

    # if we're still here, something is wrong
    return "ERROR: get_status_color() has returned 'None' for " \
            + member.name + "\n"


================================================
FILE: utils/quicksort.py
================================================
def quick_sort_channel_logs(channel_logs):
    # sort channels to match the server's default chosen positions
    if len(channel_logs) <= 1: return channel_logs
    else:
        return quick_sort_channel_logs([e for e in channel_logs[1:] \
            if e.get_channel().position <= channel_logs[0].get_channel().position]) + \
            [channel_logs[0]] + quick_sort_channel_logs([e for e in channel_logs[1:] \
            if e.get_channel().position > channel_logs[0].get_channel().position])


================================================
FILE: utils/settings.py
================================================
import os
import sys
from yaml import safe_load
from blessings import Terminal

settings = ""

def copy_skeleton():
    term = Terminal()
    try:
        from shutil import copyfile
        if not os.path.exists(os.getenv("HOME") + "/.config/Discline"):
            os.mkdir(os.getenv("HOME") + "/.config/Discline")
        
        if os.path.exists(os.getenv("HOME") + "/.config/Discline/config"):
            try: 
                os.remove(os.getenv("HOME") + "/.config/Discline/config")
            except: 
                pass

        copyfile("res/settings-skeleton.yaml", os.getenv("HOME") + "/.config/Discline/config", follow_symlinks=True) 
        print(term.green("Skeleton copied!" + term.normal))
        print(term.cyan("Your configuration file can be found at ~/.config/Discline"))

    except KeyboardInterrupt: 
        print("Cancelling...")
        quit()
    except SystemExit: 
        quit()
    except:
        print(term.red("Error creating skeleton file."))
        quit()

def load_config(path):
    global settings
    with open(path) as f:
        settings = safe_load(f)
 
arg = ""
try: 
    arg = sys.argv[1]
except IndexError: 
    pass

if arg == "--store-token" or arg == "--token":
    pass
elif arg == "--skeleton" or arg == "--copy-skeleton":
    copy_skeleton()
    quit()
elif arg == "--config":
    try:
        load_config(sys.argv[2])
    except IndexError:
        print("No path provided?")
        quit()
    except:
        print("Invalid path to config entered.")
        quit()
else:
    try:
        load_config(os.getenv("HOME") + "/.config/Discline/config")
    except:
        try:
            load_config(os.getenv("HOME") + "/.Discline")
        except:
            print(term.red("ERROR: could not get settings."))
            quit()


================================================
FILE: utils/token_utils.py
================================================
import os
from utils.globals import gc

def get_token():
    if os.path.exists(os.getenv("HOME") + "/.config/Discline/token"):
        token = ""
        try:
            f = open(os.getenv("HOME") + "/.config/Discline/token", "r")
            token = f.read()
            f.close()
        except: pass

        if token != "":
            return token
    
    from blessings import Terminal
    gc.term = Terminal()
    print("\n" + gc.term.red("Error reading token."))
    print("\n" + gc.term.yellow("Are you sure you stored your token?"))
    print(gc.term.yellow("Use --store-token to store your token."))
    quit()

def store_token():
    import sys
    from blessings import Terminal
    
    token = ""
    try: 
        token=sys.argv[2]
    except IndexError:
        print(Terminal().red("Error: You did not specify a token!"))
        quit()

    if not os.path.exists(os.getenv("HOME") + "/.config/Discline"):
        os.mkdir(os.getenv("HOME") + "/.config/Discline")

    if token is not None and token != "":
        # trim off quotes if user added them
        token = token.strip('"')
        token = token.strip("'")

    # ------- Token format seems to vary, disabling this check for now -------- #
    # if token is None or len(token) < 59 or len(token) > 88:
        # print(Terminal().red("Error: Bad token. Did you paste it correctly?"))
        # quit()
    # ------------------------------------------------------------------------- #
    
    try:
        f = open(os.getenv("HOME") + "/.config/Discline/token", "w")
        f.write(token)
        f.close()
        print(Terminal().green("Token stored!"))
    except:
        print(Terminal().red("Error: Could not write token to file."))
        quit()


================================================
FILE: utils/updates.py
================================================
def check_for_updates():
    from utils.globals import gc
    from os import path
    
    if not path.exists(".git"):
        print(gc.term.red("Error: client not started from repo location! Cancelling..."))
        print(gc.term.yellow("You must start the client from its folder to get automatic updates. \n"))
        return

    try:# git pull at start as to automatically update to master repo
        from subprocess import Popen,PIPE
        print(gc.term.green + "Checking for updates..." + gc.term.normal)
        process = Popen(["git", "pull", "--force"], stdout=PIPE)
        output = process.communicate()[0].decode('utf-8').strip()

        if "Already up to date" not in output:
            # print(gc.term.yellow("Updates downloaded! Please restart."))
            print("\n \n")
            # This quit() call is breaking the client on MacOS and Linux Mint
            # The if statement above is being triggered, even when the output IS
            # "Already up to date". Why is this happening?
            # quit()
        else:
            print("Already up to date!" + "\n")
    except KeyboardInterrupt: print("Call to cancel update received, skipping.")
    except SystemExit: pass
    except OSError: # (file not found)
        # They must not have git installed, no automatic updates for them!
        print(gc.term.red + "Error fetching automatic updates! Do you \
              have git installed?" + gc.term.normal)
    except:
        print(gc.term.red + "Unkown error occurred during retrieval \
              of updates." + gc.term.normal)
Download .txt
gitextract_ta9rwfzc/

├── .gitignore
├── Discline.py
├── LICENSE
├── README.md
├── client/
│   ├── channellog.py
│   ├── client.py
│   ├── on_message.py
│   └── serverlog.py
├── commands/
│   ├── channel_jump.py
│   ├── sendfile.py
│   └── text_emoticons.py
├── input/
│   ├── input_handler.py
│   ├── kbhit.py
│   └── typing_handler.py
├── res/
│   ├── scripts/
│   │   └── discline
│   └── settings-skeleton.yaml
├── ui/
│   ├── line.py
│   ├── text_manipulation.py
│   ├── ui.py
│   ├── ui_curses.py
│   └── ui_utils.py
└── utils/
    ├── globals.py
    ├── hidecursor.py
    ├── print_utils/
    │   ├── channellist.py
    │   ├── emojis.py
    │   ├── help.py
    │   ├── print_utils.py
    │   ├── serverlist.py
    │   └── userlist.py
    ├── quicksort.py
    ├── settings.py
    ├── token_utils.py
    └── updates.py
Download .txt
SYMBOL INDEX (122 symbols across 28 files)

FILE: Discline.py
  function on_ready (line 49) | async def on_ready():
  function on_message (line 133) | async def on_message(message):
  function on_message_edit (line 139) | async def on_message_edit(msg_old, msg_new):
  function on_message_delete (line 147) | async def on_message_delete(msg):
  function main (line 169) | def main():

FILE: client/channellog.py
  class ChannelLog (line 2) | class ChannelLog():
    method __init__ (line 11) | def __init__(self, channel, logs):
    method get_server (line 15) | def get_server(self): return self.__channel.server
    method get_channel (line 16) | def get_channel(self): return self.__channel
    method get_logs (line 18) | def get_logs(self):
    method get_name (line 21) | def get_name(self):
    method get_server_name (line 24) | def get_server_name(self):
    method append (line 27) | def append(self, message):
    method index (line 30) | def index(self, message):
    method insert (line 33) | def insert(self, i, message):
    method len (line 36) | def len(self):
    method get_index (line 39) | def get_index(self):
    method set_index (line 42) | def set_index(self, int):
    method inc_index (line 45) | def inc_index(self, int):
    method dec_index (line 48) | def dec_index(self, int):

FILE: client/client.py
  class Client (line 7) | class Client(discord.Client):
    method set_prompt (line 22) | def set_prompt(self, string):
    method set_current_server (line 24) | def set_current_server(self, string):
    method set_current_channel (line 26) | def set_current_channel(self, string):
    method get_prompt (line 30) | def get_prompt(self): return self.__prompt
    method get_current_server_name (line 31) | def get_current_server_name(self): return self.__current_server
    method get_current_channel_name (line 32) | def get_current_channel_name(self): return self.__current_channel
    method get_current_server (line 34) | def get_current_server(self):
    method get_current_server_log (line 39) | def get_current_server_log(self):
    method get_current_channel (line 44) | def get_current_channel(self):
    method populate_current_channel_log (line 53) | async def populate_current_channel_log(self):
    method get_current_channel_log (line 62) | def get_current_channel_log(self):
    method get_online (line 71) | async def get_online(self):
    method say (line 81) | async def say(self, string):
    method set_game (line 84) | async def set_game(self, string):
    method get_game (line 98) | async def get_game(self):
    method set_status (line 101) | async def set_status(self, string):
    method get_status (line 118) | async def get_status(self):

FILE: client/on_message.py
  function on_incoming_message (line 5) | async def on_incoming_message(msg):

FILE: client/serverlog.py
  class ServerLog (line 5) | class ServerLog():
    method __init__ (line 10) | def __init__(self, server, channel_log_list):
    method get_server (line 14) | def get_server(self):
    method get_name (line 17) | def get_name(self):
    method get_logs (line 20) | def get_logs(self):
    method clear_logs (line 23) | def clear_logs(self):
    method add_logs (line 28) | def add_logs(self, log_list):

FILE: commands/channel_jump.py
  function channel_jump (line 5) | async def channel_jump(arg):

FILE: commands/sendfile.py
  function send_file (line 5) | async def send_file(client, filepath):

FILE: commands/text_emoticons.py
  function check_emoticons (line 1) | async def check_emoticons(client, cmd):

FILE: input/input_handler.py
  function init_input (line 18) | def init_input():
  function key_input (line 22) | async def key_input():
  function input_handler (line 64) | async def input_handler():

FILE: input/kbhit.py
  class KBHit (line 7) | class KBHit:
    method __init__ (line 9) | def __init__(self):
    method set_normal_term (line 24) | def set_normal_term(self):
    method getch (line 28) | async def getch(self):
    method kbhit (line 31) | async def kbhit(self):

FILE: input/typing_handler.py
  function is_typing_handler (line 5) | async def is_typing_handler():

FILE: ui/line.py
  class Line (line 1) | class Line():
    method __init__ (line 9) | def __init__(self, text, offset):
    method length (line 13) | def length(self):

FILE: ui/text_manipulation.py
  function calc_mutations (line 7) | async def calc_mutations(msg):
  function convert_pin (line 84) | async def convert_pin(msg):
  function trim_emoji (line 92) | async def trim_emoji(full_name, short_name, string):
  function convert_bold (line 95) | async def convert_bold(string):
  function convert_italic (line 103) | async def convert_italic(string):
  function convert_underline (line 111) | async def convert_underline(string):
  function convert_code (line 119) | async def convert_code(string):
  function convert_code_block (line 128) | async def convert_code_block(string):
  function convert_url (line 136) | async def convert_url(string):

FILE: ui/ui.py
  function print_screen (line 21) | async def print_screen():
  function print_top_bar (line 57) | async def print_top_bar(left_bar_width):
  function set_display (line 98) | async def set_display(string):
  function print_left_bar (line 107) | async def print_left_bar(left_bar_width):
  function print_bottom_bar (line 183) | async def print_bottom_bar(left_bar_width):
  function clear_screen (line 197) | async def clear_screen():
  function print_channel_log (line 205) | async def print_channel_log(left_bar_width):

FILE: ui/ui_curses.py
  function cursesInit (line 29) | def cursesInit():
  function cursesDestroy (line 35) | def cursesDestroy():
  function cursesRefresh (line 41) | def cursesRefresh():
  function print_screen (line 47) | async def print_screen():
  function print_top_bar (line 86) | async def print_top_bar(left_bar_width):
  function set_display (line 127) | async def set_display(string):
  function print_left_bar (line 136) | async def print_left_bar(left_bar_width):
  function print_bottom_bar (line 212) | async def print_bottom_bar(left_bar_width):
  function clear_screen (line 226) | async def clear_screen():
  function print_channel_log (line 236) | async def print_channel_log(left_bar_width):

FILE: ui/ui_utils.py
  function get_prompt (line 4) | async def get_prompt():
  function get_max_lines (line 17) | async def get_max_lines():
  function get_left_bar_width (line 28) | async def get_left_bar_width():
  function get_role_color (line 35) | async def get_role_color(msg):

FILE: utils/globals.py
  class GlobalsContainer (line 13) | class GlobalsContainer:
    method __init__ (line 14) | def __init__(self):
    method initClient (line 22) | def initClient(self):
  function kill (line 33) | def kill():
  function string2channel (line 47) | async def string2channel(channel):
  function get_channel_log (line 55) | async def get_channel_log(channel):
  function chan2log (line 63) | async def chan2log(chan):
  function serv2log (line 71) | async def serv2log(serv):
  function get_color (line 77) | async def get_color(string):

FILE: utils/hidecursor.py
  function hide_cursor (line 4) | async def hide_cursor():

FILE: utils/print_utils/channellist.py
  function print_channellist (line 6) | async def print_channellist():

FILE: utils/print_utils/emojis.py
  function print_emojilist (line 6) | async def print_emojilist():

FILE: utils/print_utils/help.py
  function print_help (line 3) | def print_help(gc):
  function get_line (line 54) | def get_line(gc, command, div, desc):

FILE: utils/print_utils/print_utils.py
  function print_servers (line 4) | async def print_servers():
  function print_user (line 10) | async def print_user():
  function print_line_break (line 13) | async def print_line_break():
  function print_channels (line 16) | async def print_channels(server):

FILE: utils/print_utils/serverlist.py
  function print_serverlist (line 6) | async def print_serverlist():

FILE: utils/print_utils/userlist.py
  class UserList (line 10) | class UserList:
    method __init__ (line 12) | def __init__(self):
    method add (line 19) | def add(self, member, tag):
    method sort (line 30) | def sort(self):
  function print_userlist (line 63) | async def print_userlist():
  function get_status_color (line 126) | def get_status_color(member):

FILE: utils/quicksort.py
  function quick_sort_channel_logs (line 1) | def quick_sort_channel_logs(channel_logs):

FILE: utils/settings.py
  function copy_skeleton (line 8) | def copy_skeleton():
  function load_config (line 34) | def load_config(path):

FILE: utils/token_utils.py
  function get_token (line 4) | def get_token():
  function store_token (line 23) | def store_token():

FILE: utils/updates.py
  function check_for_updates (line 1) | def check_for_updates():
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (113K chars).
[
  {
    "path": ".gitignore",
    "chars": 1490,
    "preview": "api-ref.html\nnotes.md\n\n# more token things just incase someone tries to push\n# their branch with their token in here... "
  },
  {
    "path": "Discline.py",
    "chars": 7474,
    "preview": "#!/usr/bin/env python3\n# ------------------------------------------------------- #\n#                                    "
  },
  {
    "path": "LICENSE",
    "chars": 487,
    "preview": "        DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE \n                    Version 2, December 2004 \n\n Copyright (C) 2004 "
  },
  {
    "path": "README.md",
    "chars": 8954,
    "preview": "# ![logo_small.png](res/logo/logo_small.png) Discline\n------------------------------\n\n![screenshot_main.png](res/screens"
  },
  {
    "path": "client/channellog.py",
    "chars": 1115,
    "preview": "# Wrapper class to make dealing with logs easier\nclass ChannelLog():\n\n    __channel = \"\"\n    __logs = []\n    unread = Fa"
  },
  {
    "path": "client/client.py",
    "chars": 4832,
    "preview": "import discord\nfrom utils.globals import gc\nfrom utils.settings import settings\nimport ui.text_manipulation as tm\n\n# inh"
  },
  {
    "path": "client/on_message.py",
    "chars": 918,
    "preview": "from ui.ui import print_screen\nfrom utils.globals import gc\nfrom ui.text_manipulation import calc_mutations\n\nasync def o"
  },
  {
    "path": "client/serverlog.py",
    "chars": 750,
    "preview": "from discord import Server, Channel\nfrom client.channellog import ChannelLog\n\n# Simple wrapper class to hold a list of C"
  },
  {
    "path": "commands/channel_jump.py",
    "chars": 781,
    "preview": "from utils.globals import gc\nfrom utils.quicksort import quick_sort_channel_logs\nfrom utils.settings import settings\n\nas"
  },
  {
    "path": "commands/sendfile.py",
    "chars": 876,
    "preview": "from getpass import getuser\nfrom utils.globals import gc\nfrom ui.ui import set_display\n\nasync def send_file(client, file"
  },
  {
    "path": "commands/text_emoticons.py",
    "chars": 1156,
    "preview": "async def check_emoticons(client, cmd):\n    if cmd == \"shrug\": \n        try: await client.send_message(client.get_curren"
  },
  {
    "path": "input/input_handler.py",
    "chars": 12712,
    "preview": "import asyncio\nimport discord\nfrom input.kbhit import KBHit\nimport ui.ui as ui\nfrom utils.globals import gc, kill\nfrom u"
  },
  {
    "path": "input/kbhit.py",
    "chars": 1012,
    "preview": "import os\nimport sys\nimport termios\nimport atexit\nfrom select import select\n\nclass KBHit:\n    \n    def __init__(self):\n "
  },
  {
    "path": "input/typing_handler.py",
    "chars": 683,
    "preview": "import asyncio\nfrom utils.settings import settings\nfrom utils.globals import gc\n\nasync def is_typing_handler():\n    # us"
  },
  {
    "path": "res/scripts/discline",
    "chars": 494,
    "preview": "#!/bin/sh\n#\n# This is a script to easily launch discline\n# from an application launcher or similar.\n#\n# http://github.co"
  },
  {
    "path": "res/settings-skeleton.yaml",
    "chars": 3933,
    "preview": "---\n# -------------------------------------------------------------------------\n#       You can edit these to your prefe"
  },
  {
    "path": "ui/line.py",
    "chars": 371,
    "preview": "class Line():\n\n    # the text the line contains\n    text = \"\" \n    # how offset from the [left_bar_width + MARGIN] it sh"
  },
  {
    "path": "ui/text_manipulation.py",
    "chars": 4905,
    "preview": "import re\nfrom discord import MessageType\nfrom utils.settings import settings\nfrom utils.globals import gc, get_color\nim"
  },
  {
    "path": "ui/ui.py",
    "chars": 14763,
    "preview": "import sys\nfrom os import system\nfrom discord import ChannelType\nfrom blessings import Terminal\nfrom ui.line import Line"
  },
  {
    "path": "ui/ui_curses.py",
    "chars": 14664,
    "preview": "import sys\nimport time\nfrom os import system\nimport curses\nfrom curses import ascii as cAscii\nfrom discord import Channe"
  },
  {
    "path": "ui/ui_utils.py",
    "chars": 2012,
    "preview": "from utils.globals import get_color, gc\nfrom utils.settings import settings\n\nasync def get_prompt():\n    left = await ge"
  },
  {
    "path": "utils/globals.py",
    "chars": 3796,
    "preview": "from sys import exit\nfrom blessings import Terminal\nfrom utils.settings import settings\nimport sys\n\nNO_SETTINGS=False\ntr"
  },
  {
    "path": "utils/hidecursor.py",
    "chars": 135,
    "preview": "from sys import stdout\n\n# completely hides the system cursor\nasync def hide_cursor():\n    stdout.write(\"\\033[?25l\")\n    "
  },
  {
    "path": "utils/print_utils/channellist.py",
    "chars": 1262,
    "preview": "from os import system\nfrom discord import ChannelType\nfrom ui.ui import clear_screen, set_display\nfrom utils.globals imp"
  },
  {
    "path": "utils/print_utils/emojis.py",
    "chars": 1411,
    "preview": "from os import system\nfrom ui.ui import clear_screen, set_display\nfrom utils.globals import gc, get_color\nfrom utils.set"
  },
  {
    "path": "utils/print_utils/help.py",
    "chars": 2784,
    "preview": "from os import system\n\ndef print_help(gc):\n    system(\"clear\")\n    system(\"echo '\" + gc.term.normal \\\n        + gc.term."
  },
  {
    "path": "utils/print_utils/print_utils.py",
    "chars": 534,
    "preview": "import discord\nfrom utils.globals import gc\n\nasync def print_servers():\n    print(\"Available servers: \")\n    print_line_"
  },
  {
    "path": "utils/print_utils/serverlist.py",
    "chars": 1632,
    "preview": "from os import system\nfrom ui.ui import clear_screen, set_display\nfrom utils.globals import get_color, gc\nfrom utils.set"
  },
  {
    "path": "utils/print_utils/userlist.py",
    "chars": 4787,
    "preview": "from os import system\nfrom discord import Status\nfrom utils.globals import gc\n\n# On call of the /users command, this wil"
  },
  {
    "path": "utils/quicksort.py",
    "chars": 499,
    "preview": "def quick_sort_channel_logs(channel_logs):\n    # sort channels to match the server's default chosen positions\n    if len"
  },
  {
    "path": "utils/settings.py",
    "chars": 1792,
    "preview": "import os\nimport sys\nfrom yaml import safe_load\nfrom blessings import Terminal\n\nsettings = \"\"\n\ndef copy_skeleton():\n    "
  },
  {
    "path": "utils/token_utils.py",
    "chars": 1734,
    "preview": "import os\nfrom utils.globals import gc\n\ndef get_token():\n    if os.path.exists(os.getenv(\"HOME\") + \"/.config/Discline/to"
  },
  {
    "path": "utils/updates.py",
    "chars": 1572,
    "preview": "def check_for_updates():\n    from utils.globals import gc\n    from os import path\n    \n    if not path.exists(\".git\"):\n "
  }
]

About this extraction

This page contains the full source code of the mitchweaver/Discline GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (103.8 KB), approximately 23.9k tokens, and a symbol index with 122 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!