iacchus/birdears

View on GitHub
src/birdears/interfaces/commandline.py

Summary

Maintainability
C
1 day
Test Coverage
import os
import sys

from .. import _Getch

from .. import KEYS
from .. import CHROMATIC_SHARP
from .. import CHROMATIC_FLAT

from .. import INTERVALS
from .. import DIATONIC_MODES
from .. import CHROMATIC_TYPE

from ..questionbase import QUESTION_CLASSES

# from os import popen

# TODO: find which version this function was implemented to set minimum python
#       version.
from shutil import get_terminal_size

#  try:
#      from shutil import get_terminal_size
#  except ImportError:
#      from click import get_terminal_size

COLS, LINES = get_terminal_size()


def center_text(text, sep=True, nl=0):
    """This function returns input text centered according to terminal columns.

    Args:
        text (str): The string to be centered, it can have multiple lines.
        sep (bool): Add line separator after centered text (True) or
            not (False).
        nl (int): How many new lines to add after text.
    """

    linelist = list(text.splitlines())

    # gets the biggest line
    biggest_line_size = 0
    for line in linelist:
        line_length = len(line.expandtabs())
        if line_length > biggest_line_size:
            biggest_line_size = line_length

    columns = COLS
    offset = biggest_line_size / 2
    perfect_center = columns / 2
    padsize = int(perfect_center - offset)
    spacing = ' ' * padsize  # space char
    dim = '\033[2m'
    reset = '\033[0m'

    text = str()
    for line in linelist:
        text += (spacing + line + '\n')

    divider = \
        spacing + (dim + '─' * int(biggest_line_size) + reset) # unicode 0x2500

    text += divider if sep else ''

    text += nl * '\n'

    return text


def print_response(response):
    """Prints the formatted response.

    Args:
        response (dict): A response returned by question's check_question()
    """

    # TODO: make a class for response
    if response['is_correct']:
        response_text = "Correct!"
        color = '\033[32m' # green
    else:
        response_text = "Wrong"
        color = '\033[31m' # red

    reset = '\033[0m' # reset terminal color

    if 'extra_response_str' in response.keys():
        print(center_text(response['extra_response_str'], nl=0))

    print(color + center_text(response_text, sep=False, nl=1) + reset)


def print_instrumental(response):
    """Prints the formatted response for 'instrumental' exercise.

    Args:
        response (dict): A response returned by question's check_question()
    """

    text_kwargs = dict(
        correct_resp=response['correct_response_str']
    )

    response_text = """
{correct_resp}
""".format(**text_kwargs)

    print(center_text(response_text, nl=2))


def print_question(question):
    """Prints the question to the user.

    Args:
        question (obj): A Question class with the question to be printed.
    """

    direction = -1 if question.is_descending else +1

    scale = question.scale
    # mode = question.mode

    tonic = scale[0]
    network = [abs(int(tonic) - int(note)) for note in scale]
    # keyboard_map = KEYBOARD_INDICES['chromatic']['ascending']['major']
    keyboard_map = tuple(question.keyboard_index)

    # should we show the octaves here? why not?

    notes = "".join([str(pitch).ljust(4) for pitch in scale][::direction])
    # notes = "".join([str(pitch.note).ljust(4) \
    # for pitch in scale][::direction])
    intervals = "".join([str(INTERVALS[step][1]).ljust(4)
                         for step in network][::direction])
    keys = "".join([str(keyboard_map[step]).ljust(4)
                    for step in network][::direction])

    text_kwargs = {
        'tonic': question.tonic_str,
        'mode': question.mode,
        'chroma': question.is_chromatic,
        'desc': question.is_descending,
        'scale': notes,
        'intervals': intervals,
        'keyboard': keys,
    }

    question_text = """\
     Scale: {scale}
 Intervals: {intervals}
  Keyboard: {keyboard}
""".format(**text_kwargs)

    print(center_text(question_text, nl=2))


def make_input_str(user_input, keyboard_index):
    """Makes a string representing intervals entered by the user.

    This function is to be used by questions which takes more than one interval
    input as MelodicDictation, and formats the intervals already entered.

    Args:
        user_input (array_type): The list of keyboard keys entered by user.
        keyboard_index (array_type): The keyboard mapping used by question.
    """

    input_str = str()

    user_input_semitones = [keyboard_index.index(s)
                            for s in user_input]

    user_str = "".join([INTERVALS[s][1].center(5)
                       for s in user_input_semitones]).center(COLS)

    input_str = ("\r{}".format(user_str))

    return input_str


class CommandLine:

    def __init__(self, cli_prompt_next=False,
                 cli_no_scroll=False, cli_no_resolution=False,
                 exercise=None, *args, **kwargs):
        """This function implements the birdears loop for command line.

        Args:
            cli_prompt_next (bool): True if --prompt is set.
            cli_no_scroll (bool): True if --no-scroll is set.
            cli_no_resolution (bool): True if --no-resolution is set.
            exercise (str): The question name.
            **kwargs (kwargs): FIXME: The kwargs can contain options for
                specific questions.
        """

        if exercise in QUESTION_CLASSES:
            QUESTION_CLASS = QUESTION_CLASSES[exercise]
        else:
            raise Exception("Invalid `exercise` value:", exercise)
        
        self.prompt_next = cli_prompt_next
        self.no_scroll = cli_no_scroll
        self.no_resolution = cli_no_resolution

        self.exercise = exercise

        ####if 'n_notes' in kwargs:
            ####self.dictate_notes = kwargs['n_notes']
        ####else:
            ####self.dictate_notes = 1

        getch = _Getch()

        self.new_question_bit = True

        print('\n')

        while True:
            if self.new_question_bit is True:

                self.new_question_bit = False

                self.input_keys = list()
                self.question = QUESTION_CLASS(**kwargs)

                if   self.exercise == 'melodic':
                    exercise_title  = 'Melodic interval recognition'
                    question_prompt = 'What is the interval?'

                elif self.exercise == 'harmonic':
                    exercise_title  = 'Harmonic interval recognition'
                    question_prompt = 'What is the interval?'

                elif self.exercise == 'dictation':
                    exercise_title  = 'Melodic dictation'
                    question_prompt = 'Now, please type the intervals ' \
                                      'you\'ve heard.'

                elif self.exercise == 'instrumental':
                    exercise_title  = 'Instrumental melodic ' \
                                      'time-based detection'
                    # TODO: question_prompt

                else:               # 'notename':
                    exercise_title  = 'Note name by interval recognition'
                    question_prompt = 'The tonic is {tonic}. ' \
                                      'Press the key representing the ' \
                                      'second note.' \
                                      .format(tonic=self.question.tonic_str)

                if self.no_scroll:
                    # Clear terminal screen (but keep scrollback)
                    # See https://stackoverflow.com/a/2084628
                    os.system('cls' if os.name == 'nt' else 'clear -x')
                    print('\n')

                print(center_text('birdears ─ Functional Ear Training',
                                  sep=False, nl=1))
                print(center_text(exercise_title, nl=0))
                print(center_text('KEY: ' + self.question.tonic_str + ' ' \
                                  + self.question.mode, sep=False, nl=1))

                print_question(self.question)

                if not self.exercise == 'instrumental':
                    self.question.play_question()

                    print(center_text(question_prompt))
                    print(center_text(
                        'key- answer   r- repeat   q- quit', sep=False, nl=1))

            if self.exercise == 'instrumental':
                for r in range(self.question.n_repeats):
                    self.question.play_question()

                # FIXME: Instrumental is broken in CLI, double countdown...
                for i in range(self.question.wait_time):
                    time_left = str(self.question.wait_time - i).rjust(3)
                    text = '{} seconds remaining...'.format(time_left)
                    print(center_text(text, sep=False), end='')
                    self.question.question._wait(1)

                response = self.question.check_question()
                print_instrumental(response)

                self.new_question_bit = True

                continue

            user_input = getch()
            self.process_key(user_input)

    def process_key(self, user_input):

        if user_input in self.question.keyboard_index \
            and user_input != ' ':  # spc

            self.input_keys.append(user_input)

            ###if self.exercise == 'dictation':
                ###input_str = make_input_str(self.input_keys,
                ###    self.question.keyboard_index)
                ###print(input_str, end='')
            if self.question.n_input_notes > 1:
                input_str = make_input_str(self.input_keys,
                                           self.question.keyboard_index)
                print(input_str, end='')

            # FIXME: use self.question.n_notes instead
            #if len(self.input_keys) == self.dictate_notes:
            if len(self.input_keys) == self.question.n_notes:

                response = self.question.check_question(self.input_keys)
                print_response(response)

                if not self.no_resolution:
                    self.question.play_resolution()
                
                if self.prompt_next:
                    print(center_text('Next question', nl=0))
                    print(center_text('any- play   q- quit', sep=False, nl=1))
                    
                    getch2 = _Getch()

                    while True: # wait for input before next question
                        user_input2 = getch2()
                    
                        # q - quit
                        if user_input2 in ('q', 'Q'):
                            sys.exit()
                        # any key - play next question
                        elif user_input2:
                            break
                        # loop, keep waiting
                        else:
                            pass

                self.new_question_bit = True

        # backspace
        elif user_input == '\x7f':
            # FIXME: use self.question.n_input_notes instead
            #if(len(self.input_keys) > 0) and self.exercise == 'dictation':
            if(len(self.input_keys) > 0) and (self.question.n_input_notes > 1):
                del(self.input_keys[-1])
                input_str = make_input_str(self.input_keys,
                                           self.question.keyboard_index)
                print(input_str, end='')

        # q - quit
        elif user_input in ('q', 'Q'):
            sys.exit()

        # r - repeat interval
        elif user_input in ('r', 'R'):
            self.question.play_question()