├── tests ├── __init__.py ├── test_auth.py ├── test_editor.py ├── test_code.py ├── test_log.py └── test_result.py ├── leetcode ├── __init__.py ├── client │ ├── __init__.py │ ├── process.py │ ├── leetcode.py │ ├── auth.py │ └── quiz.py ├── coding │ ├── __init__.py │ ├── editor.py │ └── code.py ├── helper │ ├── __init__.py │ ├── model.py │ ├── log.py │ ├── trace.py │ ├── common.py │ └── config.py ├── views │ ├── __init__.py │ ├── viewhelper.py │ ├── help.py │ ├── loading.py │ ├── detail.py │ ├── home.py │ └── result.py ├── __main__.py ├── cli.py └── terminal.py ├── setup.cfg ├── .pylintrc ├── MANIFEST.in ├── screenshots ├── list.gif └── Download_on_the_App_Store_Badge_US-UK_135x40.svg ├── requirements.txt ├── .gitignore ├── .travis.yml ├── .github ├── FUNDING.yml └── workflows │ └── pythonapp.yml ├── LICENSE ├── setup.py ├── tags └── facebook.json └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /leetcode/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /leetcode/client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /leetcode/coding/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /leetcode/helper/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /leetcode/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=missing-docstring,invalid-name 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include screenshots/* -------------------------------------------------------------------------------- /screenshots/list.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chishui/terminal-leetcode/HEAD/screenshots/list.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bs4 2 | lxml 3 | requests 4 | mock 5 | requests_mock 6 | urwid 7 | decorator 8 | pycookiecheat 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | Makefile 4 | /*.egg-info 5 | *.pyc 6 | /.git 7 | .DS_Store 8 | /.virtualenv 9 | /.cache 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | install: 5 | - pip install bs4 requests urwid pytest requests_mock lxml decorator pycookiecheat 6 | script: 7 | - py.test -v 8 | -------------------------------------------------------------------------------- /leetcode/__main__.py: -------------------------------------------------------------------------------- 1 | from .terminal import Terminal 2 | 3 | 4 | def main(): 5 | """Main entry point""" 6 | try: 7 | term = Terminal() 8 | term.run() 9 | except Exception: 10 | pass 11 | -------------------------------------------------------------------------------- /leetcode/helper/model.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | 3 | 4 | class EasyLock(object): 5 | def __init__(self): 6 | self.lock = Lock() 7 | 8 | def __enter__(self): 9 | self.lock.acquire() 10 | return self.lock 11 | 12 | def __exit__(self, exc_type, exc_val, exc_tb): 13 | self.lock.release() 14 | -------------------------------------------------------------------------------- /leetcode/helper/log.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | import logging.config 5 | 6 | json_file = os.path.join(os.path.expanduser('~'), '.config', 'leetcode', 'logging.json') 7 | 8 | 9 | def read_json_data(filepath): 10 | with open(json_file, 'r') as f: 11 | return json.load(f) 12 | 13 | 14 | def init_logger(): 15 | if os.path.exists(json_file): 16 | logging.config.dictConfig(read_json_data(json_file)) 17 | else: 18 | logging.basicConfig(level=logging.ERROR) 19 | -------------------------------------------------------------------------------- /leetcode/helper/trace.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from decorator import decorate 3 | 4 | 5 | def _trace(f, *args, **kw): 6 | logger = logging.getLogger(f.__module__) 7 | kwstr = ', '.join('%r: %r' % (k, kw[k]) for k in sorted(kw)) 8 | logger.info("calling %s:%s with args %s, {%s}" % (f.__name__, f.__code__.co_firstlineno, args, kwstr)) 9 | ret = f(*args, **kw) 10 | logger.info("return %s:%s {%s}" % (f.__name__, f.__code__.co_firstlineno, ret)) 11 | return ret 12 | 13 | 14 | def trace(f): 15 | return decorate(f, _trace) 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: chishui 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from unittest import mock 4 | from leetcode.client.auth import * 5 | from leetcode.helper.config import * 6 | import requests_mock 7 | 8 | class TestAuth(unittest.TestCase): 9 | 10 | def setUp(self): 11 | self.auth = Auth() 12 | 13 | def test_retrieve(self): 14 | with requests_mock.Mocker() as m: 15 | m.get(API_URL, exc=requests.exceptions.ConnectTimeout) 16 | with self.assertRaises(NetworkError): 17 | r = self.auth.retrieve(API_URL) 18 | 19 | m.get(API_URL, exc=requests.exceptions.RequestException) 20 | with self.assertRaises(NetworkError): 21 | r = self.auth.retrieve(API_URL) 22 | -------------------------------------------------------------------------------- /leetcode/helper/common.py: -------------------------------------------------------------------------------- 1 | BASE_URL = 'https://leetcode.com' 2 | GRAPHQL_URL = 'https://leetcode.com/graphql' 3 | API_URL = BASE_URL + '/api/problems/algorithms/' 4 | HOME_URL = BASE_URL + '/problemset/algorithms' 5 | SUBMISSION_URL = BASE_URL + '/submissions/detail/{id}/check/' 6 | 7 | LANG_MAPPING = { 8 | 'C++': 'cpp', 9 | 'Python': 'python', 10 | 'Python3': 'python3', 11 | 'Java': 'java', 12 | 'C': 'c', 13 | 'C#': 'csharp', 14 | 'Javascript': 'javascript', 15 | 'Ruby': 'ruby', 16 | 'Swift': 'swift', 17 | 'Go': 'go', 18 | } 19 | 20 | 21 | def merge_two_dicts(x, y): 22 | """Given two dicts, merge them into a new dict as a shallow copy.""" 23 | z = x.copy() 24 | z.update(y) 25 | return z 26 | -------------------------------------------------------------------------------- /leetcode/views/viewhelper.py: -------------------------------------------------------------------------------- 1 | def refresh(loop, data): 2 | loop.draw_screen() 3 | 4 | 5 | def delay_refresh(loop): 6 | alarm = loop.set_alarm_in(1, refresh) 7 | return alarm 8 | 9 | 10 | def refresh_detail(loop, data): 11 | loop.screen.clear() 12 | loop.draw_screen() 13 | 14 | 15 | def delay_refresh_detail(loop): 16 | alarm = loop.set_alarm_in(1, refresh_detail) 17 | return alarm 18 | 19 | 20 | def vim_key_map(key): 21 | if key == 'j': 22 | return 'down' 23 | elif key == 'k': 24 | return 'up' 25 | elif key == 'h': 26 | return 'left' 27 | elif key == 'l': 28 | return 'right' 29 | elif key == 'ctrl f': 30 | return 'page down' 31 | elif key == 'ctrl b': 32 | return 'page up' 33 | return key 34 | -------------------------------------------------------------------------------- /tests/test_editor.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | from leetcode.coding.editor import edit 4 | 5 | class TestEditor(unittest.TestCase): 6 | @patch('leetcode.coding.editor.os.chdir') 7 | @patch('leetcode.coding.editor.subprocess.call') 8 | @patch('leetcode.coding.editor.os.environ.get') 9 | def test_edit(self, mock_get, mock_call, mock_chdir): 10 | mock_get.return_value = '' 11 | edit('', None) 12 | mock_call.assert_not_called() 13 | 14 | mock_get.return_value = 'sublime' 15 | edit('file', None) 16 | mock_call.assert_called_once_with('subl file', shell=True) 17 | 18 | mock_get.return_value = 'vim' 19 | mock_call.reset_mock() 20 | with patch('leetcode.coding.editor.delay_refresh_detail') as mock_delay: 21 | edit('file', None) 22 | mock_call.assert_called_once_with('vim file', shell=True) 23 | mock_delay.assert_called_once() 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Terminal-Leetcode 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 3.7 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.7 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements.txt 20 | - name: Lint with flake8 21 | run: | 22 | pip install flake8 23 | # stop the build if there are Python syntax errors or undefined names 24 | flake8 leetcode --count --select=E9,F63,F7,F82 --show-source --statistics 25 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 26 | flake8 leetcode --count --exit-zero --max-complexity=10 --ignore=E501,C901 --statistics 27 | - name: Test with pytest 28 | run: | 29 | pip install pytest 30 | pytest 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Xiu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_code.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest.mock import patch 4 | from leetcode.coding.code import * 5 | 6 | class TestCode(unittest.TestCase): 7 | 8 | @patch('leetcode.coding.code.Path.exists') 9 | def test_unique_file_name(self, mock_exists): 10 | mock_exists.return_value = False 11 | self.assertEqual(unique_file_name(''), Path('')) 12 | 13 | mock_exists.side_effect = [True, True, True, False] 14 | self.assertEqual(unique_file_name('hello'), Path('hello-2')) 15 | 16 | mock_exists.side_effect = [True, True, False] 17 | self.assertEqual(unique_file_name('hello.txt'), Path('hello-1.txt')) 18 | 19 | mock_exists.side_effect = [True, True, False] 20 | self.assertEqual(unique_file_name('hello.'), Path('hello.-1')) 21 | 22 | @patch('leetcode.coding.code.config') 23 | @patch('leetcode.coding.code.Path.mkdir') 24 | @patch('leetcode.coding.code.Path.exists') 25 | def test_get_code_file_path(self, mock_exists, mock_makedirs, mock_config): 26 | mock_config.path = '' 27 | mock_config.ext = 'py' 28 | mock_exists.return_value = False 29 | self.assertEqual(get_code_file_path(1), Path.home().joinpath('leetcode/1.py')) 30 | mock_makedirs.assert_called_once() 31 | -------------------------------------------------------------------------------- /tests/test_log.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from unittest import mock 4 | import json 5 | from leetcode.helper.log import * 6 | 7 | data = ''' 8 | { 9 | "version": 1, 10 | "disable_existing_loggers": false, 11 | "formatters": { 12 | "standard": { 13 | "format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s" 14 | } 15 | }, 16 | "handlers": { 17 | "default": { 18 | "level":"INFO", 19 | "class":"logging.StreamHandler" 20 | } 21 | }, 22 | "loggers": { 23 | "": { 24 | "level": "INFO", 25 | "propagate": true 26 | } 27 | } 28 | } 29 | ''' 30 | 31 | class TestLog(unittest.TestCase) : 32 | 33 | @mock.patch('leetcode.helper.log.read_json_data') 34 | @mock.patch('leetcode.helper.log.os.path.exists') 35 | def test_init_logger(self, mock_path, mock_read_data): 36 | mock_path.return_value = False 37 | init_logger() 38 | 39 | mock_read_data.return_value = json.loads(data) 40 | mock_path.return_value = True 41 | init_logger() 42 | 43 | @mock.patch('builtins.open') 44 | def test_read_json_data(self, mock_open): 45 | mock_open.return_value.__enter__.return_value.read.return_value = data 46 | self.assertEqual(read_json_data(json_file), json.loads(data)) 47 | 48 | if __name__== "__main__": 49 | unittest.main() 50 | 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | 5 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as fh: 6 | long_description = fh.read() 7 | 8 | 9 | requirements = ['urwid', 'requests', 'bs4', 'lxml', 'decorator', 'pycookiecheat'] 10 | 11 | setup( 12 | name="terminal-leetcode", 13 | version="0.0.20", 14 | author="Liyun Xiu", 15 | author_email="chishui2@gmail.com", 16 | description="A terminal based leetcode website viewer", 17 | license="MIT", 18 | keywords="leetcode terminal urwid", 19 | url="https://github.com/chishui/terminal-leetcode", 20 | packages=['leetcode', 'leetcode/views', 'leetcode/client', 21 | 'leetcode/coding', 'leetcode/helper'], 22 | long_description=long_description, 23 | long_description_content_type="text/markdown", 24 | include_package_data=True, 25 | install_requires=requirements, 26 | entry_points={'console_scripts': ['leetcode=leetcode.cli:main']}, 27 | classifiers=[ 28 | "Operating System :: MacOS :: MacOS X", 29 | "Operating System :: POSIX", 30 | "Natural Language :: English", 31 | "Programming Language :: Python :: 3", 32 | "Programming Language :: Python :: 3.7", 33 | "Development Status :: 2 - Pre-Alpha", 34 | "Environment :: Console :: Curses", 35 | "Topic :: Utilities", 36 | "Topic :: Terminals", 37 | "License :: OSI Approved :: MIT License", 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /tags/facebook.json: -------------------------------------------------------------------------------- 1 | { 2 | "1":["F"], 3 | "10":["F"], 4 | "13":["F"], 5 | "15":["F"], 6 | "17":["F"], 7 | "20":["F"], 8 | "23":["F"], 9 | "25":["F"], 10 | "26":["F"], 11 | "28":["F"], 12 | "33":["F"], 13 | "38":["F"], 14 | "43":["F"], 15 | "44":["F"], 16 | "49":["F"], 17 | "50":["F"], 18 | "56":["F"], 19 | "57":["F"], 20 | "67":["F"], 21 | "68":["F"], 22 | "69":["F"], 23 | "71":["F"], 24 | "75":["F"], 25 | "76":["F"], 26 | "78":["F"], 27 | "79":["F"], 28 | "80":["F"], 29 | "85":["F"], 30 | "88":["F"], 31 | "90":["F"], 32 | "91":["F"], 33 | "98":["F"], 34 | "102":["F"], 35 | "117":["F"], 36 | "121":["F"], 37 | "125":["F"], 38 | "127":["F"], 39 | "128":["F"], 40 | "133":["F"], 41 | "139":["F"], 42 | "146":["F"], 43 | "157":["F"], 44 | "158":["F"], 45 | "161":["F"], 46 | "168":["F"], 47 | "173":["F"], 48 | "200":["F"], 49 | "206":["F"], 50 | "208":["F"], 51 | "209":["F"], 52 | "210":["F"], 53 | "211":["F"], 54 | "215":["F"], 55 | "218":["F"], 56 | "221":["F"], 57 | "234":["F"], 58 | "235":["F"], 59 | "236":["F"], 60 | "238":["F"], 61 | "252":["F"], 62 | "253":["F"], 63 | "257":["F"], 64 | "261":["F"], 65 | "265":["F"], 66 | "269":["F"], 67 | "273":["F"], 68 | "274":["F"], 69 | "275":["F"], 70 | "277":["F"], 71 | "278":["F"], 72 | "282":["F"], 73 | "283":["F"], 74 | "285":["F"], 75 | "286":["F"], 76 | "297":["F"], 77 | "301":["F"], 78 | "311":["F"], 79 | "314":["F"], 80 | "325":["F"], 81 | "334":["F"], 82 | "341":["F"], 83 | "377":["F"], 84 | "380":["F"], 85 | "398":["F"], 86 | "404":["F"], 87 | "410":["F"], 88 | "461":["F"], 89 | "477":["F"], 90 | "494":["F"] 91 | } -------------------------------------------------------------------------------- /leetcode/coding/editor.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from pathlib import Path 4 | from ..views.viewhelper import delay_refresh_detail 5 | from ..helper.config import config 6 | 7 | 8 | def edit(filepath: Path, loop): 9 | if isinstance(filepath, str): 10 | filepath = Path(filepath) 11 | editor = os.environ.get('EDITOR', 'vi').lower() 12 | # vim 13 | if editor == 'vi' or editor == 'vim': 14 | cmd = editor + ' ' + str(filepath) 15 | current_directory = Path.cwd() 16 | os.chdir(filepath.parent) 17 | if config.tmux_support and is_inside_tmux(): 18 | open_in_new_tmux_window(cmd) 19 | else: 20 | subprocess.call(cmd, shell=True) 21 | delay_refresh_detail(loop) 22 | os.chdir(current_directory) 23 | # sublime text 24 | elif editor == 'sublime': 25 | cmd = 'subl ' + str(filepath) 26 | subprocess.call(cmd, shell=True) 27 | 28 | 29 | def is_inside_tmux(): 30 | return 'TMUX' in os.environ 31 | 32 | 33 | def open_in_new_tmux_window(edit_cmd): 34 | # close other panes if exist, so that the detail pane is the only pane 35 | try: 36 | output = subprocess.check_output("tmux list-panes | wc -l", shell=True) 37 | num_pane = int(output) 38 | if num_pane > 1: 39 | subprocess.check_call("tmux kill-pane -a", shell=True) 40 | except Exception: 41 | pass 42 | cmd = "tmux split-window -h" 43 | os.system(cmd) 44 | cmd = "tmux send-keys -t right '%s' C-m" % edit_cmd 45 | os.system(cmd) 46 | -------------------------------------------------------------------------------- /tests/test_result.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | from leetcode.views.result import ResultView 4 | 5 | class TestResultView(unittest.TestCase): 6 | 7 | def test_exception(self): 8 | with self.assertRaises(ValueError): 9 | ResultView(None, None, None) 10 | with self.assertRaises(ValueError): 11 | ResultView(None, None, {}) 12 | with self.assertRaises(ValueError): 13 | ResultView(None, None, {'status_code': 0}) 14 | 15 | @mock.patch.object(ResultView, 'make_compile_error_view') 16 | def test_compile_error_view(self, mock_compile_error): 17 | view = ResultView(None, None, {'status_code': 20}) 18 | mock_compile_error.assert_called_once() 19 | 20 | @mock.patch.object(ResultView, 'make_failed_view') 21 | def test_failed_view(self, mock_failed_view): 22 | view = ResultView(None, None, {'status_code': 11}) 23 | mock_failed_view.assert_called_once() 24 | 25 | @mock.patch.object(ResultView, 'make_success_view') 26 | def test_success_view(self, mock_success_view): 27 | view = ResultView(None, None, {'status_code': 10}) 28 | mock_success_view.assert_called_once() 29 | 30 | @mock.patch.object(ResultView, 'destroy') 31 | def test_press(self, mock_destroy): 32 | view = ResultView(None, None, {'status_code': 10, 'status_runtime': '6 ms'}) 33 | view.keypress((20, 20), 'esc') 34 | mock_destroy.assert_called_once() 35 | mock_destroy.reset_mock() 36 | view.keypress((20, 20), 'e') 37 | mock_destroy.assert_not_called() 38 | -------------------------------------------------------------------------------- /leetcode/views/help.py: -------------------------------------------------------------------------------- 1 | import urwid 2 | 3 | 4 | class HelpView(urwid.Frame): 5 | ''' 6 | basic: 7 | 'UP' or 'k' : up 8 | 'DOWN' or 'j' : down 9 | 'LEFT' or 'h' : go to quiz list 10 | 'RIGHT' or 'ENTER' or 'l' : see quiz detail 11 | 'PageUp' : see previous page 12 | 'PageDown' : see next page 13 | 'Home' : go to the first quiz 14 | 'End' : go to the last quiz 15 | sort: 16 | '1' : sort by id 17 | '2' : sort by title 18 | '3' : sort by acceptance 19 | '4' : sort by difficulty 20 | others: 21 | 'q' : quit 22 | 'H' : help 23 | 'R' : retrieve quiz list from website 24 | 'f' : search quiz 25 | 'n' : search next quiz (quiz home view) 26 | 'e' : open editor to edit code 27 | 'n' : create a new file to edit sample code (quiz detail view) 28 | 'd' : open discussion in web browser 29 | 's' : submit your code to leetcode (quiz detail view) 30 | 'S' : open solutions in web browser 31 | ''' 32 | 33 | def __init__(self): 34 | title = urwid.AttrWrap(urwid.Text('Help'), 'body') 35 | body = urwid.Text(HelpView.__doc__) 36 | pile = urwid.Pile([title, body]) 37 | filler = urwid.Filler(pile) 38 | urwid.Frame.__init__(self, filler) 39 | 40 | def keypress(self, size, key): 41 | ignore_key = ('H', 'l', 'right', 'enter') 42 | if key in ignore_key: 43 | pass 44 | else: 45 | return urwid.Frame.keypress(self, size, key) 46 | -------------------------------------------------------------------------------- /leetcode/helper/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import configparser 3 | from pathlib import Path 4 | 5 | HOME = Path.home() 6 | CONFIG_FOLDER = HOME.joinpath('.config', 'leetcode') 7 | CONFIG_FILE = CONFIG_FOLDER.joinpath('config.cfg') 8 | TAG_FILE = CONFIG_FOLDER.joinpath('tag.json') 9 | SECTION = 'leetcode' 10 | 11 | 12 | class Config(object): 13 | ''' 14 | Config is a class to get user's configuration from leetcode.cfg 15 | config.cfg is located ~/.config/leetcdoe/config.cfg 16 | keys are: 17 | username 18 | password 19 | language 20 | ext # code file extension 21 | path # code path 22 | ''' 23 | def __init__(self): 24 | self.parser = configparser.ConfigParser({ 25 | 'username': '', 26 | 'password': '', 27 | 'language': 'C++', 28 | 'ext': '', 29 | 'path': '', 30 | 'keep_quiz_detail': 'false', 31 | 'tmux_support': 'false'}) 32 | self.username = None 33 | self.password = None 34 | self.language = 'C++' 35 | self.ext = '' 36 | self.path = None 37 | self.keep_quiz_detail = False 38 | self.tmux_support = False 39 | 40 | def load(self): 41 | if not CONFIG_FILE.exists(): 42 | return True 43 | 44 | self.parser.read(CONFIG_FILE) 45 | if SECTION not in self.parser.sections(): 46 | return False 47 | 48 | self.username = self.parser.get(SECTION, 'username') 49 | self.password = self.parser.get(SECTION, 'password') 50 | self.language = self.parser.get(SECTION, 'language') 51 | self.ext = self.parser.get(SECTION, 'ext') 52 | self.path = self.parser.get(SECTION, 'path') 53 | self.path = os.path.expanduser(self.path) 54 | self.keep_quiz_detail = self.parser.getboolean(SECTION, 'keep_quiz_detail') 55 | self.tmux_support = self.parser.getboolean(SECTION, 'tmux_support') 56 | return True 57 | 58 | def write(self, key, value): 59 | self.load() 60 | self.parser.set(SECTION, key, value) 61 | with open(CONFIG_FILE, 'w') as configfile: 62 | self.parser.write(configfile) 63 | 64 | 65 | config = Config() 66 | -------------------------------------------------------------------------------- /leetcode/views/loading.py: -------------------------------------------------------------------------------- 1 | import time 2 | from threading import Thread 3 | import urwid 4 | from .viewhelper import delay_refresh 5 | from ..helper.model import EasyLock 6 | 7 | 8 | class LoadingView(urwid.Frame): 9 | ''' 10 | Loading View When Doing HTTP Request 11 | ''' 12 | def __init__(self, text, width, host_view, loop=None): 13 | self.running = False 14 | self.lock = EasyLock() 15 | self.loop = loop 16 | self.overlay = urwid.Overlay( 17 | urwid.LineBox(urwid.Text(text)), host_view, # urwid.SolidFill(), 18 | 'center', width, 'middle', None) 19 | urwid.Frame.__init__(self, self.overlay) 20 | 21 | def keypress(self, size, key): 22 | pass 23 | 24 | def set_text(self, text): 25 | with self.lock: 26 | self.overlay.contents[1][0].base_widget.set_text(text) 27 | 28 | @property 29 | def is_running(self): 30 | return self.t and self.t.is_alive() 31 | 32 | def start(self): 33 | self.running = True 34 | self.t = Thread(target=self.work) 35 | self.t.start() 36 | 37 | def end(self): 38 | self.running = False 39 | self.t.join() 40 | 41 | def work(self): 42 | while self.running: 43 | with self.lock: 44 | text = self.overlay.contents[1][0].base_widget.text 45 | num = text.count('.') 46 | if num < 3: 47 | text = text + '.' 48 | else: 49 | text = text.strip('.') 50 | self.overlay.contents[1][0].base_widget.set_text(text) 51 | delay_refresh(self.loop) 52 | time.sleep(0.8) 53 | 54 | 55 | class Toast(urwid.Frame): 56 | ''' 57 | Toast View 58 | ''' 59 | def __init__(self, text, width, host_view, loop=None): 60 | self.loop = loop 61 | self.host_view = host_view 62 | self.overlay = urwid.Overlay( 63 | urwid.LineBox(urwid.Text(text)), host_view, # urwid.SolidFill(), 64 | 'center', width, 'middle', None) 65 | urwid.Frame.__init__(self, self.overlay) 66 | 67 | def keypress(self, size, key): 68 | self.destroy() 69 | 70 | def show(self): 71 | self.loop.widget = self 72 | 73 | def destroy(self): 74 | self.loop.widget = self.host_view 75 | -------------------------------------------------------------------------------- /leetcode/views/detail.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | import urwid 3 | import logging 4 | from .viewhelper import vim_key_map 5 | from ..coding.code import edit_code 6 | from ..helper.common import BASE_URL 7 | from ..coding.editor import edit 8 | from ..helper.trace import trace 9 | 10 | 11 | class DetailView(urwid.Frame): 12 | ''' 13 | Quiz Item Detail View 14 | ''' 15 | def __init__(self, quiz, loop=None): 16 | self.quiz = quiz 17 | self.loop = loop 18 | self.logger = logging.getLogger(__name__) 19 | blank = urwid.Divider() 20 | view_title = urwid.AttrWrap(urwid.Text(self.quiz.title), 'body') 21 | view_text = self.make_body_widgets() 22 | view_code_title = urwid.Text('\n --- Sample Code ---\n') 23 | view_code = urwid.Text(self.quiz.sample_code) 24 | listitems = [blank, view_title, blank] + view_text + \ 25 | [blank, view_code_title, blank, view_code, blank] 26 | self.listbox = urwid.ListBox(urwid.SimpleListWalker(listitems)) 27 | urwid.Frame.__init__(self, self.listbox) 28 | 29 | def make_body_widgets(self): 30 | text_widgets = [] 31 | 32 | for line in self.quiz.content.split('\n'): 33 | text_widgets.append(urwid.Text(line)) 34 | 35 | text_widgets.append(urwid.Divider()) 36 | 37 | for tag in self.quiz.tags: 38 | text_widgets.append(urwid.Text(('tag', tag))) 39 | 40 | return text_widgets 41 | 42 | @trace 43 | def keypress(self, size, key): 44 | key = vim_key_map(key) 45 | ignore_key = ('l', 'right', 'enter') 46 | if key in ignore_key: 47 | pass 48 | # edit sample code 49 | if key == 'e': 50 | self.edit_code(False) 51 | # edit new sample code 52 | elif key == 'n': 53 | self.edit_code(True) 54 | # open discussion page from default browser 55 | elif key == 'd': 56 | url = self.get_discussion_url() 57 | webbrowser.open(url) 58 | # open solutions page from default browser 59 | elif key == 'S': 60 | url = self.get_solutions_url() 61 | webbrowser.open(url) 62 | else: 63 | return urwid.Frame.keypress(self, size, key) 64 | 65 | def edit_code(self, newcode): 66 | filepath = edit_code(self.quiz.id, self.quiz.sample_code, newcode) 67 | # open editor to edit code 68 | edit(filepath, self.loop) 69 | 70 | def get_discussion_url(self): 71 | return f"{BASE_URL}/problems/{self.quiz.slug}/discuss" 72 | 73 | def get_solutions_url(self): 74 | return f"{BASE_URL}/problems/{self.quiz.slug}/solution" 75 | -------------------------------------------------------------------------------- /leetcode/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | from collections import OrderedDict 4 | from .helper.log import init_logger 5 | from .helper.common import LANG_MAPPING 6 | from .helper.config import config 7 | from .__main__ import main as main_entry 8 | from .client.process import Process 9 | 10 | 11 | class CommandLine(object): 12 | def __init__(self): 13 | parser = argparse.ArgumentParser( 14 | description='A terminal Leetcode client', 15 | usage=''' 16 | leetcode 17 | or 18 | leetcode [] 19 | 20 | The most commonly used commands are: 21 | set set configuration 22 | submit submit your code 23 | ''') 24 | parser.add_argument('command', nargs="?", help='sub command') 25 | args = parser.parse_args(sys.argv[1:2]) 26 | if not args.command: 27 | main_entry() 28 | else: 29 | try: 30 | getattr(self, args.command)() 31 | except Exception: 32 | print('\033[91m' + f"sub-command \"{args.command}\" is not supported!\n" + '\033[0m') 33 | parser.print_help() 34 | 35 | def set(self): 36 | parser = argparse.ArgumentParser(description='set configuration') 37 | parser.add_argument('-l', '--language', action='store', choices=LANG_MAPPING.keys(), 38 | help='set programming language') 39 | parser.add_argument('-p', '--path', action='store', help='set programming file location') 40 | parser.add_argument('-e', '--ext', action='store', help='set programming file extention') 41 | args = parser.parse_args(sys.argv[2:]) 42 | if args.language: 43 | config.write('language', args.language) 44 | elif args.ext: 45 | config.write('ext', args.ext) 46 | elif args.path: 47 | config.write('path', args.path) 48 | 49 | def submit(self): 50 | try: 51 | parser = argparse.ArgumentParser( 52 | description='submit your code for online judge', 53 | usage='leetcode submit --id [problem frontend id]') 54 | parser.add_argument('--id', type=int, required=True, help='set problem id') 55 | args = parser.parse_args(sys.argv[2:]) 56 | if args.id: 57 | process = Process() 58 | success, result = process.submit(args.id) 59 | if success: 60 | self._prettify(result) 61 | except Exception as e: 62 | print(e) 63 | 64 | def _prettify(self, result: OrderedDict): 65 | for k, v in result.items(): 66 | print(f"{k}:\t{v}") 67 | 68 | 69 | def main(): 70 | init_logger() 71 | CommandLine() 72 | -------------------------------------------------------------------------------- /leetcode/coding/code.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from functools import wraps 3 | from ..helper.config import CONFIG_FOLDER, config 4 | from ..helper.trace import trace 5 | 6 | SNIPPET_FOLDER = Path(CONFIG_FOLDER) / Path('snippet') 7 | BEFORE = SNIPPET_FOLDER.joinpath('before') 8 | AFTER = SNIPPET_FOLDER.joinpath('after') 9 | 10 | 11 | @trace 12 | def get_data(filepath): 13 | if filepath.exists(): 14 | with open(filepath, 'r') as f: 15 | return f.read() 16 | return '' 17 | 18 | 19 | @trace 20 | def enhance_code(func): 21 | @wraps(func) 22 | def wrapper(code, language, filepath): 23 | before = get_data(BEFORE) 24 | after = get_data(AFTER) 25 | code = before + code + after 26 | return func(code, language, filepath) 27 | return wrapper 28 | 29 | 30 | @trace 31 | def generate_makefile(func): 32 | @wraps(func) 33 | def wrapper(code, language, filepath): 34 | if language != 'C++': 35 | return func(code, language, filepath) 36 | directory = filepath.parent 37 | filename = filepath.name 38 | name = filepath.stem 39 | makefile = directory.joinpath('Makefile') 40 | text = 'all: %s\n\t g++ -g -o %s %s -std=c++11' % (filename, name, filename) 41 | with open(makefile, 'w') as f: 42 | f.write(text) 43 | return func(code, language, filepath) 44 | return wrapper 45 | 46 | 47 | @trace 48 | def unique_file_name(filepath): 49 | if isinstance(filepath, str): 50 | filepath = Path(filepath) 51 | 52 | if not filepath.exists(): 53 | return filepath 54 | 55 | path = filepath.parent 56 | filename = filepath.stem 57 | ext = filepath.suffix 58 | index = 1 59 | while filepath.exists(): 60 | filepath = path / Path(filename + '-' + str(index) + ext) 61 | index = index + 1 62 | return filepath 63 | 64 | 65 | @trace 66 | def get_code_file_path(quiz_id): 67 | path = config.path 68 | if not path or not Path(config.path).exists(): 69 | path = Path.home() / 'leetcode' 70 | if not path.exists(): 71 | path.mkdir() 72 | else: 73 | path = Path(path) 74 | 75 | return path / Path(str(quiz_id) + '.' + config.ext) 76 | 77 | 78 | @trace 79 | def get_code_for_submission(filepath): 80 | data = get_data(filepath) 81 | before = get_data(BEFORE) 82 | after = get_data(AFTER) 83 | return data.replace(before, '').replace(after, '') 84 | 85 | 86 | @trace 87 | def edit_code(quiz_id, code, newcode=False): 88 | filepath = get_code_file_path(quiz_id) 89 | if newcode: 90 | filepath = unique_file_name(filepath) 91 | 92 | code = prepare_code(code, config.language, filepath) 93 | if not filepath.exists(): 94 | with open(filepath, 'w') as f: 95 | f.write(code) 96 | return filepath 97 | 98 | 99 | @enhance_code 100 | @generate_makefile 101 | def prepare_code(code, language, filepath): 102 | return code 103 | -------------------------------------------------------------------------------- /leetcode/client/process.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections import OrderedDict 3 | from .leetcode import Leetcode 4 | from ..helper.trace import trace 5 | from ..coding.code import get_code_file_path, get_code_for_submission 6 | 7 | 8 | class Process(object): 9 | def __init__(self): 10 | self.leetcode = Leetcode() 11 | 12 | @trace 13 | def get_code_from_quiz_id(self, id): 14 | filepath = get_code_file_path(id) 15 | if not filepath.exists(): 16 | return None 17 | code = get_code_for_submission(filepath) 18 | code = code.replace('\n', '\r\n') 19 | return code 20 | 21 | @trace 22 | def _submit_result_prettify(self, result): 23 | prettified = OrderedDict() 24 | if 'status_code' not in result: 25 | prettified['status'] = 'Unknow result format: %s' % json.dumps(result) 26 | if result['status_code'] == 20: 27 | prettified['status'] = 'Compile error' 28 | prettified['Your input'] = '' 29 | prettified['Your answer'] = result['compile_error'] 30 | prettified['Expected answer'] = 'Unknown error' 31 | elif result['status_code'] == 10: 32 | prettified['status'] = 'Accepted' 33 | prettified['Run time'] = result['status_runtime'] 34 | elif result['status_code'] == 11: 35 | prettified['status'] = 'Wrong answer' 36 | s = result['compare_result'] 37 | prettified['Passed test cases'] = '%d/%d' % (s.count('1'), len(s)) 38 | prettified['Your input'] = result['input'] 39 | prettified['Your answer'] = result['code_output'] 40 | prettified['Expected answer'] = result['expected_output'] 41 | elif result['status_code'] == 12: # memeory limit exceeded 42 | prettified['status'] = 'Memory Limit Exceeded' 43 | elif result['status_code'] == 13: # output limit exceeded 44 | prettified['status'] = 'Output Limit Exceeded' 45 | elif result['status_code'] == 14: # timeout 46 | prettified['status'] = 'Time Limit Exceeded' 47 | elif result['status_code'] == 15: 48 | prettified['status'] = 'Runtime error' 49 | prettified['Runtime error message'] = result['runtime_error'] 50 | prettified['Last input'] = result['last_testcase'] 51 | else: 52 | prettified['status'] = 'Unknown status' 53 | return prettified 54 | 55 | @trace 56 | def submit(self, id): 57 | self.leetcode.load() 58 | quiz = None 59 | for q in self.leetcode.quizzes: 60 | if q.id == id: 61 | quiz = q 62 | break 63 | if not quiz: 64 | return (False, None) 65 | else: 66 | quiz.load() 67 | 68 | code = self.get_code_from_quiz_id(id) 69 | success, text_or_id = quiz.submit(code) 70 | if success: 71 | code = 1 72 | while code > 0: 73 | r = quiz.check_submission_result(text_or_id) 74 | code = r[0] 75 | 76 | if code < -1: 77 | return (False, r[1]) 78 | else: 79 | return (True, self._submit_result_prettify(r[1])) 80 | else: 81 | return (False, 'send data failed!') 82 | -------------------------------------------------------------------------------- /leetcode/client/leetcode.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from .auth import Auth, headers 4 | from .quiz import Quiz 5 | from ..helper.trace import trace 6 | from ..helper.config import config 7 | from ..helper.common import API_URL, BASE_URL, merge_two_dicts, GRAPHQL_URL 8 | 9 | 10 | class Leetcode(object): 11 | def __init__(self): 12 | self.quizzes = [] 13 | self.auth = Auth() 14 | self.logger = logging.getLogger(__name__) 15 | self.username = "" 16 | self.is_paid = False 17 | self.is_verified = False 18 | config.load() 19 | self.get_user() 20 | 21 | def __getitem__(self, i): 22 | return self.quizzes[i] 23 | 24 | @property 25 | def solved(self): 26 | return [i for i in self.quizzes if i.submission_status == 'ac'] 27 | 28 | @property 29 | def is_login(self): 30 | return self.username != "" 31 | 32 | @trace 33 | def load(self): 34 | r = self.auth.retrieve(API_URL) 35 | if r.status_code != 200: 36 | return None 37 | return self._parse_home_API(r.text) 38 | 39 | def _parse_home_API(self, text): 40 | difficulty = {1: "Easy", 2: "Medium", 3: "Hard"} 41 | self.quizzes = [] 42 | json_data = json.loads(text) 43 | 44 | try: 45 | for quiz in json_data['stat_status_pairs']: 46 | if quiz['stat']['question__hide']: 47 | continue 48 | 49 | data = Quiz(self.auth) 50 | data.title = quiz['stat']['question__title'] 51 | data.slug = quiz['stat']['question__title_slug'] 52 | data.id = quiz['stat']['frontend_question_id'] 53 | data.real_quiz_id = data.id # default real_quiz_id to frontend id 54 | data.locked = not self.is_paid and quiz['paid_only'] 55 | data.difficulty = difficulty[quiz['difficulty']['level']] 56 | data.favorite = quiz['is_favor'] 57 | data.acceptance = "%.1f%%" % (float(quiz['stat']['total_acs']) * 100 / float(quiz['stat']['total_submitted'])) 58 | data.url = BASE_URL + "/problems/" + quiz['stat']['question__title_slug'] 59 | data.submission_status = quiz['status'] 60 | self.quizzes.append(data) 61 | return self.quizzes 62 | except (KeyError, AttributeError) as e: 63 | self.logger.error(e) 64 | 65 | @trace 66 | def get_user(self): 67 | if not self.auth.is_login: 68 | return 69 | query = """{ 70 | user { 71 | username 72 | email 73 | isCurrentUserVerified 74 | isCurrentUserPremium 75 | __typename 76 | } 77 | }""" 78 | extra_headers = { 79 | 'Origin': BASE_URL, 80 | 'Referer': BASE_URL, 81 | 'X-CSRFToken': self.auth.cookies["csrftoken"], 82 | 'Accept': '*/*', 83 | 'Accept-Encoding': 'gzip, deflate', 84 | 'Content-Type': 'application/json', 85 | } 86 | 87 | new_headers = merge_two_dicts(extra_headers, headers) 88 | body = { 89 | 'query': query, 90 | 'operationName': None, 91 | 'variables': {} 92 | } 93 | 94 | r = self.auth.retrieve(GRAPHQL_URL, headers=new_headers, method='POST', data=json.dumps(body)) 95 | 96 | try: 97 | obj = json.loads(r.text) 98 | self.is_paid = obj["data"]["user"]["isCurrentUserPremium"] 99 | self.username = obj["data"]["user"]["username"] 100 | self.is_verified = obj["data"]["user"]["isCurrentUserVerified"] 101 | except Exception as e: 102 | self.logger.error(e) 103 | return None 104 | -------------------------------------------------------------------------------- /leetcode/client/auth.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | import os 4 | import json 5 | from http import cookiejar 6 | from ..helper.config import config, CONFIG_FOLDER 7 | from ..helper.trace import trace 8 | 9 | BASE_URL = 'https://leetcode.com' 10 | LOGIN_URL = BASE_URL + '/accounts/login/' 11 | API_URL = BASE_URL + '/api/problems/all/' 12 | 13 | headers = { 14 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 15 | 'Accept-Encoding': 'gzip, deflate', 16 | 'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6', 17 | 'Host': 'leetcode.com', 18 | 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Mobile Safari/537.36', 19 | 'Referer': 'https://leetcode.com/accounts/login/', 20 | } 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class Auth(object): 26 | def __init__(self): 27 | self.cookies = None 28 | self.get_cookies_from_chrome() 29 | 30 | def get_cookies_from_local_file(self): 31 | COOKIE_PATH = os.path.join(CONFIG_FOLDER, 'cookies') 32 | self.cookies = cookiejar.FileCookieJar(COOKIE_PATH) 33 | try: 34 | self.cookies.load(ignore_discard=True) 35 | except Exception: 36 | pass 37 | 38 | def get_cookies_from_chrome(self): 39 | try: 40 | from pycookiecheat import chrome_cookies 41 | self.cookies = chrome_cookies(BASE_URL) 42 | except Exception: 43 | logger.error("Cannot get cookies from chrome") 44 | 45 | @trace 46 | def login(self): 47 | logger = logging.getLogger(__name__) 48 | if not config.username or not config.password: 49 | return False 50 | login_data = {} 51 | r = self.retrieve(LOGIN_URL, headers=headers) 52 | if r.status_code != 200: 53 | logger.error('login failed') 54 | return False 55 | if 'csrftoken' in r.cookies: 56 | csrftoken = r.cookies['csrftoken'] 57 | login_data['csrfmiddlewaretoken'] = csrftoken 58 | login_data['login'] = config.username 59 | login_data['password'] = config.password 60 | login_data['remember'] = 'on' 61 | 62 | request_headers = {"Accept": "*/*", "DNT": "1", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundary725gDSRiGxbNlBGY"} 63 | request_headers.update(headers) 64 | r = self.retrieve(LOGIN_URL, method='POST', headers=request_headers, data=json.dumps(login_data)) 65 | logger.info(r.text) 66 | logger.info(r.content) 67 | if r.status_code != 200: 68 | logger.error('login failed') 69 | return False 70 | 71 | logger.info("login success") 72 | # session.cookies.save() 73 | return True 74 | 75 | @trace 76 | def is_login(self): 77 | r = self.retrieve(API_URL, headers=headers) 78 | if r.status_code != 200: 79 | return False 80 | text = r.content 81 | data = json.loads(text) 82 | return 'user_name' in data and data['user_name'] != '' 83 | 84 | @trace 85 | def retrieve(self, url, headers=None, method='GET', data=None): 86 | r = None 87 | try: 88 | if method == 'GET': 89 | r = requests.get(url, headers=headers, cookies=self.cookies) 90 | elif method == 'POST': 91 | r = requests.post(url, headers=headers, data=data, cookies=self.cookies) 92 | if r.status_code != 200: 93 | logger.info(r.text) 94 | return r 95 | except requests.exceptions.RequestException: 96 | if r: 97 | raise NetworkError('Network error: url: %s' % url, r.status_code) 98 | else: 99 | raise NetworkError('Network error: url: %s' % url) 100 | 101 | 102 | class NetworkError(Exception): 103 | def __init__(self, message, code=0): 104 | if not message or message == '': 105 | self.message = 'Network error!' 106 | else: 107 | self.message = '%s code: %d' % (message, code) 108 | logger.error(self.message) 109 | -------------------------------------------------------------------------------- /leetcode/views/home.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import json 4 | import urwid 5 | from .viewhelper import vim_key_map 6 | from ..helper.config import TAG_FILE 7 | 8 | 9 | class ItemWidget(urwid.WidgetWrap): 10 | ''' 11 | Quiz List Item View 12 | ''' 13 | def __init__(self, data, marks, sel=True): 14 | self.sel = sel 15 | self.id = data.id 16 | self.data = data 17 | lockbody = 'body' if not self.data.locked else 'lock' 18 | pass_symbol = u'' 19 | if self.data.submission_status == 'ac': 20 | pass_symbol = u'\u2714' 21 | elif self.data.submission_status == 'notac': 22 | pass_symbol = u'\u2718' 23 | text = str(self.data.id) 24 | mark = make_mark(marks, self.data.id) 25 | self.item = [ 26 | (4, urwid.AttrWrap(urwid.Text(text), lockbody, 'focus')), 27 | (2, urwid.AttrWrap(urwid.Text(pass_symbol), lockbody, 'focus')), 28 | (10, urwid.AttrWrap(urwid.Text(mark), 'hometag', 'focus')), 29 | urwid.AttrWrap(urwid.Text('%s ' % data.title + (u'\u2605'if self.data.favorite else '')), lockbody, 'focus'), 30 | (15, urwid.AttrWrap(urwid.Text('%s' % data.acceptance), lockbody, 'focus')), 31 | (15, urwid.AttrWrap(urwid.Text('%s' % data.difficulty), lockbody, 'focus')), 32 | ] 33 | w = urwid.Columns(self.item) 34 | urwid.WidgetWrap.__init__(self, w) 35 | 36 | def selectable(self): 37 | return self.sel and not self.data.locked 38 | 39 | def keypress(self, size, key): 40 | return key 41 | 42 | 43 | class HomeView(urwid.Frame): 44 | ''' 45 | Quiz List View 46 | ''' 47 | def __init__(self, data, header): 48 | title = [ 49 | (4, urwid.AttrWrap(urwid.Text('#'), 'body', 'focus')), 50 | (2, urwid.AttrWrap(urwid.Text(''), 'body', 'focus')), 51 | (10, urwid.AttrWrap(urwid.Text('Tag'), 'body', 'focus')), 52 | urwid.AttrWrap(urwid.Text('Title'), 'body', 'focus'), 53 | (15, urwid.AttrWrap(urwid.Text('Acceptance'), 'body', 'focus')), 54 | (15, urwid.AttrWrap(urwid.Text('Difficulty'), 'body', 'focus')), 55 | ] 56 | title_column = urwid.Columns(title) 57 | self.marks = load_marks() 58 | items = make_itemwidgets(data, self.marks) 59 | self.listbox = urwid.ListBox(urwid.SimpleListWalker(items)) 60 | header_pile = urwid.Pile([header, title_column]) 61 | urwid.Frame.__init__(self, urwid.AttrWrap(self.listbox, 'body'), header=header_pile) 62 | self.last_sort = {'attr': 'id', 'reverse': True} 63 | self.last_search_text = None 64 | 65 | def sort_list(self, attr, conversion=None): 66 | if attr == self.last_sort['attr']: 67 | self.last_sort['reverse'] = not self.last_sort['reverse'] 68 | else: 69 | self.last_sort['reverse'] = False 70 | self.last_sort['attr'] = attr 71 | 72 | if conversion: 73 | self.listbox.body.sort(key=lambda x: conversion(getattr(x.data, attr)), 74 | reverse=self.last_sort['reverse']) 75 | else: 76 | self.listbox.body.sort(key=lambda x: getattr(x.data, attr), 77 | reverse=self.last_sort['reverse']) 78 | 79 | def difficulty_conversion(self, x): 80 | d = {'Easy': 0, 'Medium': 1, 'Hard': 2} 81 | return d[x] 82 | 83 | def keypress(self, size, key): 84 | key = vim_key_map(key) 85 | ignore_key = ('h', 'left') 86 | if key in ignore_key: 87 | pass 88 | elif key == '1': 89 | self.sort_list('id') 90 | elif key == '2': 91 | self.sort_list('title') 92 | elif key == '3': 93 | self.sort_list('acceptance') 94 | elif key == '4': 95 | self.sort_list('difficulty', conversion=self.difficulty_conversion) 96 | elif key == 'home': 97 | self.listbox.focus_position = 0 98 | elif key == 'end': 99 | self.listbox.focus_position = len(self.listbox.body) - 1 100 | elif key == 'n': 101 | self.handle_search(self.last_search_text, True) 102 | else: 103 | return urwid.Frame.keypress(self, size, key) 104 | 105 | def handle_search(self, text, from_current=False): 106 | self.last_search_text = text 107 | if text == '': 108 | return 109 | cur = self.listbox.focus_position if from_current else 0 110 | if is_string_an_integer(text): 111 | for i in range(len(self.listbox.body)): 112 | item = self.listbox.body[i] 113 | if item.data.id == int(text): 114 | self.listbox.focus_position = i 115 | break 116 | else: 117 | for i in range(cur + 1, len(self.listbox.body)): 118 | item = self.listbox.body[i] 119 | if re.search('.*(%s).*' % text, item.data.title, re.I): 120 | self.listbox.focus_position = i 121 | break 122 | 123 | def is_current_item_enterable(self): 124 | return self.listbox.get_focus()[0].selectable() 125 | 126 | def get_current_item_data(self): 127 | return self.listbox.get_focus()[0].data 128 | 129 | 130 | def make_itemwidgets(data, marks): 131 | items = [] 132 | for quiz in data: 133 | items.append(ItemWidget(quiz, marks=marks)) 134 | return items 135 | 136 | 137 | def is_string_an_integer(s): 138 | try: 139 | int(s) 140 | return True 141 | except ValueError: 142 | return False 143 | except TypeError: 144 | return False 145 | 146 | 147 | def load_marks(): 148 | if not os.path.exists(TAG_FILE): 149 | return {} 150 | with open(TAG_FILE, 'r') as f: 151 | return json.load(f) 152 | 153 | 154 | def make_mark(marks, quiz_id): 155 | quiz_id = str(quiz_id) 156 | if quiz_id not in marks: 157 | return '' 158 | return ' '.join(marks[quiz_id]) 159 | -------------------------------------------------------------------------------- /leetcode/client/quiz.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from bs4 import BeautifulSoup 4 | from ..helper.config import config 5 | from ..helper.trace import trace 6 | from ..helper.common import BASE_URL, merge_two_dicts, GRAPHQL_URL, LANG_MAPPING, SUBMISSION_URL 7 | from .auth import headers 8 | 9 | 10 | class Quiz(object): 11 | def __init__(self, auth): 12 | self.id = None # question frontend id 13 | self.real_quiz_id = None # question id of backend 14 | self.title = None 15 | self.content = None 16 | self.sample_code = None 17 | self.locked = False 18 | self.difficulty = None 19 | self.acceptance = None 20 | self.submission_status = None 21 | self.favorite = None 22 | self.url = None 23 | self.discussion_url = None 24 | self.tags = [] 25 | self.html_content = None 26 | self.auth = auth 27 | self.slug = None 28 | self.already_load = False 29 | self.logger = logging.getLogger(__name__) 30 | 31 | def load(self): 32 | if not self.auth.is_login or self.already_load: 33 | return False 34 | 35 | query = """query questionData($titleSlug: String!) { 36 | question(titleSlug: $titleSlug) { 37 | title 38 | titleSlug 39 | questionId 40 | questionFrontendId 41 | content 42 | difficulty 43 | stats 44 | companyTagStats 45 | topicTags { 46 | name 47 | slug 48 | __typename 49 | } 50 | similarQuestions 51 | codeSnippets { 52 | lang 53 | langSlug 54 | code 55 | __typename 56 | } 57 | solution { 58 | id 59 | canSeeDetail 60 | __typename 61 | } 62 | sampleTestCase 63 | enableTestMode 64 | metaData 65 | enableRunCode 66 | judgerAvailable 67 | __typename 68 | } 69 | }""" 70 | extra_headers = { 71 | 'Origin': BASE_URL, 72 | 'Referer': self.url, 73 | 'X-CSRFToken': self.auth.cookies['csrftoken'], 74 | 'Accept': '*/*', 75 | 'Accept-Encoding': 'gzip, deflate', 76 | 'Content-Type': 'application/json', 77 | } 78 | 79 | new_headers = merge_two_dicts(headers, extra_headers) 80 | body = { 81 | "query": query, 82 | "variables": {"titleSlug": self.slug}, 83 | "operationName": "questionData" 84 | } 85 | r = self.auth.retrieve(GRAPHQL_URL, new_headers, "POST", json.dumps(body)) 86 | try: 87 | obj = json.loads(r.text) 88 | self.html_content = obj["data"]["question"]["content"] 89 | content = obj["data"]["question"]["content"] 90 | bs = BeautifulSoup(content, "lxml") 91 | self.id = obj["data"]["question"]["questionFrontendId"] 92 | self.real_quiz_id = obj["data"]["question"]["questionId"] 93 | self.content = bs.get_text() 94 | self.content = self.content.replace(chr(13), '') 95 | self.sample_code = self._get_code_snippet(obj["data"]["question"]["codeSnippets"]) 96 | self.tags = map(lambda x: x["name"], obj["data"]["question"]["topicTags"]) 97 | self.already_load = True 98 | return True 99 | except Exception: 100 | self.logger.error("Fatal error in main loop", exc_info=True) 101 | return False 102 | 103 | def _get_code_snippet(self, snippets): 104 | for snippet in snippets: 105 | if snippet["lang"] == config.language: 106 | return snippet["code"] 107 | return "" 108 | 109 | @trace 110 | def submit(self, code): 111 | if not self.auth.is_login: 112 | return (False, "") 113 | body = {'question_id': self.real_quiz_id, 114 | 'test_mode': False, 115 | 'lang': LANG_MAPPING.get(config.language, 'cpp'), 116 | 'judge_type': 'large', 117 | 'typed_code': code} 118 | 119 | csrftoken = self.auth.cookies['csrftoken'] 120 | extra_headers = {'Origin': BASE_URL, 121 | 'Referer': self.url + '/?tab=Description', 122 | 'DNT': '1', 123 | 'Content-Type': 'application/json;charset=UTF-8', 124 | 'Accept': 'application/json', 125 | 'X-CSRFToken': csrftoken, 126 | 'X-Requested-With': 'XMLHttpRequest'} 127 | 128 | newheaders = merge_two_dicts(headers, extra_headers) 129 | 130 | r = self.auth.retrieve(self.url + '/submit/', method='POST', data=json.dumps(body), headers=newheaders) 131 | if r.status_code != 200: 132 | return (False, 'Request failed!') 133 | text = r.text.encode('utf-8') 134 | try: 135 | data = json.loads(text) 136 | except Exception: 137 | return (False, text) 138 | 139 | if 'error' in data: 140 | return (False, data['error']) 141 | return (True, data['submission_id']) 142 | 143 | @trace 144 | def check_submission_result(self, submission_id): 145 | url = SUBMISSION_URL.format(id=submission_id) 146 | r = self.auth.retrieve(url) 147 | if r.status_code != 200: 148 | return (-100, 'Request failed!') 149 | text = r.text.encode('utf-8') 150 | data = json.loads(text) 151 | try: 152 | if data['state'] == 'PENDING': 153 | return (1,) 154 | elif data['state'] == 'STARTED': 155 | return (2,) 156 | elif data['state'] == 'SUCCESS': 157 | if 'run_success' in data: 158 | if data['run_success']: 159 | return (0, data) # data['total_correct'], data['total_testcases'], data['status_runtime']) 160 | else: 161 | return (-1, data) # data['compile_error']) 162 | else: 163 | raise KeyError 164 | except KeyError: 165 | return (-2, 'Unknow error') 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Terminal-Leetcode 2 | ============================ 3 | Terminal-Leetcode is a terminal based leetcode website viewer. 4 | This project is inspired by [RTV](https://github.com/michael-lazar/rtv). 5 | 6 | ![alt text](screenshots/list.gif "quiz list" ) 7 | 8 | 9 | --------------- 10 | 11 | [![Build Status](https://travis-ci.org/chishui/terminal-leetcode.svg?branch=master)](https://travis-ci.org/chishui/terminal-leetcode) 12 | [![PyPI](https://img.shields.io/pypi/v/nine.svg?maxAge=2592000)](https://pypi.python.org/pypi/terminal-leetcode) 13 | [![PyPI](https://img.shields.io/badge/python-3.7-blue.svg?maxAge=2592000)](https://pypi.python.org/pypi/terminal-leetcode) 14 | 15 | -------------- 16 | # March 22th Update 17 | #### Add code submit function. 18 | After finishing your code, press ``s`` at quiz detail view to submit your code to leetcode. 19 | #### Add company tag support. 20 | You can company tag to terminal-leetcode home view column. The tag file is in JSON format which can be easily 21 | edit and share. You can find tag file of Facebook from tags directory. 22 | 23 | --------------- 24 | # Requirements 25 | - Python 3.7 26 | - [Urwid](https://github.com/urwid/urwid) 27 | 28 | # Installation 29 | Install with pip 30 | ``` 31 | $ pip3 install terminal-leetcode 32 | ``` 33 | Clone the repository 34 | ``` 35 | $ git clone https://github.com/chishui/terminal-leetcode.git 36 | $ cd terminal-leetcode 37 | $ sudo python setup.py install 38 | ``` 39 | ## For Ubuntu 40 | Need to install lxml dependencies first on Ubuntu. 41 | ``` 42 | apt-get install libxml2-dev libxslt1-dev python-dev 43 | ``` 44 | # Usage 45 | To run the program, input ``leetcode`` in terminal 46 | ``` 47 | $ leetcode 48 | ``` 49 | ### Login 50 | #### Option 1 51 | This option will get your cookies from your browser and use those for any requests agains leetcode website. So 52 | you need to sign in your account from your browser first. There may be some limitations, please refer to pycookiecheat 53 | for its [documentation](https://github.com/n8henrie/pycookiecheat) 54 | 55 | On Mac for the first time use, it will pop up a window and ask to input password of your computer. 56 | 57 | #### Option 2 (No longer available) 58 | To login you need to create a config.cfg file in folder ~/.config/leetcode. 59 | Input your username and password in config.cfg as: 60 | ``` 61 | [leetcode] 62 | username=chishui 63 | password=123456 64 | ``` 65 | Then restart this program. 66 | ### Programming Language 67 | You can set your programming language in config.cfg as: 68 | ``` 69 | [leetcode] 70 | ........ 71 | language=Java 72 | ``` 73 | to see default sample code in quiz detail view in your favorite language. 74 | Please make sure to use Leetcode supported programming languages and use the string exactly 75 | the same as it appears in Leetcode. 76 | 77 | ### Tags 78 | You can customize your "Tag" column by adding a json file named tag.json into ~/.config/leetcode folder. 79 | The format of tag.json is showed below: 80 | ``` 81 | { 82 | "1" : ["F", "G"], 83 | "10" : ["F"], 84 | ...... 85 | } 86 | ``` 87 | By adding this file, quiz 1 will have a "F" and "G" tag and quiz 10 will have a "F" tag. 88 | You can use this feature to add company tag on quizzes. 89 | I have added a "F" tag sample file in "tags" folder. You could try this file to see all "F" tag quizzes. 90 | ### Writing Code 91 | Terminal-Leetcode allows you to open editor to edit default code you are viewing. 92 | You can set your code editing settings in config.cfg as: 93 | ``` 94 | [leetcode] 95 | ........ 96 | ext=java # file extention 97 | path=~/program/leetcode # code file directory 98 | ``` 99 | Then when you are in quiz detail view, press ``e`` to open editor to edit code sample. 100 | Code sample is saved into directory you set in config.cfg automatically with file name combined 101 | with quiz id and file extension you set. 102 | Default editor is vim, you can set ``export EDITOR=***`` to change editor. You can refer to 103 | [this article](http://sweetme.at/2013/09/03/how-to-open-a-file-in-sublime-text-2-or-3-from-the-command-line-on-mac-osx/) 104 | to use Sublime Text as command line editor. 105 | #### Tmux Support 106 | If you're using Terminal-Leetcode inside of a tmux session, when you press ``e``, current tmux window will be 107 | splitted vertically and an editor is opened inside the new created tmux pane. 108 | This feature could be turned on and off by config option in config.cfg as: 109 | ``` 110 | [leetcode] 111 | ........ 112 | tmux_support=true/false 113 | ``` 114 | 115 | Note that when you press `e` in detail view, all other panes in current tmux 116 | window except for the detail pane will be closed before the new edit pane is 117 | created, so that you can edit solution for another problem seamlessly without 118 | manually exiting vim and closing the edit pane. 119 | 120 | #### Code Snippet 121 | Two code snippets can be used when creating code file. 122 | You can create files ``before`` and ``after`` in ``~/.config/leetcode/snippet``. Code snippet in ``before`` 123 | will be placed at the beginning of the code file. Code snippet in file ``after`` will be placed at the end of 124 | the code file. 125 | Like in C++, write 126 | ``` 127 | #include 128 | #include 129 | 130 | using namespace std; 131 | ``` 132 | in file ``before`` and 133 | ``` 134 | int main() { 135 | Solution s; 136 | } 137 | ``` 138 | in file ``after``, then you can view code of quiz (take quiz 123 for example) as: 139 | ``` 140 | #include 141 | #include 142 | 143 | using namespace std; 144 | 145 | class Solution { 146 | public: 147 | int maxProfit(vector& prices) { 148 | 149 | } 150 | }; 151 | 152 | int main() { 153 | Solution s; 154 | } 155 | ``` 156 | It becomes much easier to write your solution code and then test your solution. 157 | #### C++ Specific 158 | - If you don't set language in config.cfg, default language is C++. 159 | - If you set C++ as your programming language, when you open editor, a Makefile is created automatically, so 160 | after you finish the code, you can use ``make`` directly to compile your code. 161 | 162 | 163 | # Controls: 164 | - Press ``H`` to see help information. 165 | - Press ``up`` and ``down`` to go through quiz list. 166 | - Press ``enter`` or ``right`` to see a quiz detail, and press ``left`` to go back. 167 | - Press ``R`` in quiz list view to retrieve quiz from website. 168 | - Press ``PageUp`` or ``PageDown`` to go to prev or next page. 169 | - Press ``Home`` or ``End`` to go to the first or last quiz. 170 | - Press ``f`` in quiz list view to search quiz by id or title. 171 | - Press ``n`` in quiz list view to search next quiz with search text input before. 172 | In quiz detail view, press ``n`` will always create a new sample code file. 173 | - Press ``t`` in quiz list view to add tag for quiz. 174 | - Press ``e`` in quiz detail view to open editor to edit code. 175 | - Press ``d`` in quiz detail view to open discussion page in web browser. 176 | - Press ``S`` in quiz detail view to open solutions page in web browser. 177 | - Press ``s`` in quiz detail view to submit your code. 178 | - Press ``1`` in quiz list view to sort quiz list by id. 179 | - Press ``2`` in quiz list view to sort quiz list by title. 180 | - Press ``3`` in quiz list view to sort quiz list by acceptance. 181 | - Press ``4`` in quiz list view to sort quiz list by difficulty. 182 | Vim's moving keys ``h``, ``j``, ``k``, ``l``, ``ctrl+f``, ``ctrl+b`` are supported. 183 | 184 | # TODO 185 | - ~~Test~~ 186 | - ~~Submit code~~ 187 | - ~~User login~~ 188 | - ~~Quiz list sort~~ 189 | - ~~Install with pip~~ 190 | - ~~Get quiz default code interface~~ 191 | 192 | # Contribute 193 | All kinds of contributions are welcome. 194 | 195 | # Licence 196 | MIT 197 | 198 | -------------------------------------------------------------------------------- /leetcode/views/result.py: -------------------------------------------------------------------------------- 1 | import json 2 | import urwid 3 | import logging 4 | from .viewhelper import vim_key_map 5 | 6 | 7 | class ResultView(urwid.Frame): 8 | ''' 9 | Quiz Item Submission Result View 10 | ''' 11 | def __init__(self, quiz, host_view, result, loop=None): 12 | self.quiz = quiz 13 | self.host_view = host_view 14 | self.result = result 15 | self.loop = loop 16 | self.logger = logging.getLogger(__name__) 17 | if result: 18 | if 'status_code' not in result: 19 | raise ValueError('Unknow result format: %s' % json.dumps(result)) 20 | if result['status_code'] == 20: 21 | self.listbox = self.make_compile_error_view() 22 | elif result['status_code'] == 10: 23 | self.listbox = self.make_success_view() 24 | elif result['status_code'] == 11: 25 | self.listbox = self.make_failed_view() 26 | elif result['status_code'] == 12: # memeory limit exceeded 27 | self.listbox = self.make_unified_error_view("Memory Limit Exceeded") 28 | elif result['status_code'] == 13: # output limit exceeded 29 | self.listbox = self.make_unified_error_view("Output Limit Exceeded") 30 | elif result['status_code'] == 14: # timeout 31 | self.listbox = self.make_unified_error_view("Time Limit Exceeded") 32 | elif result['status_code'] == 15: 33 | self.listbox = self.make_runtime_error_view() 34 | else: 35 | raise ValueError('Unknow status code: %d' % result['status_code']) 36 | else: 37 | raise ValueError('result shouldn\'t be None') 38 | 39 | self.overlay = urwid.Overlay( 40 | urwid.LineBox(self.listbox), host_view, 41 | align='center', width=('relative', 95), 42 | valign='middle', height=('relative', 95), 43 | min_width=40, min_height=40) 44 | 45 | footer = urwid.Pile([urwid.Text('Press Esc to close this view.', align='center'), urwid.Divider()]) 46 | urwid.Frame.__init__(self, self.overlay, footer=footer) 47 | 48 | def _append_stdout_if_non_empty(self, list_items): 49 | std_output = self.result.get('std_output', '') 50 | if len(std_output) > 0: 51 | blank = urwid.Divider() 52 | stdout_header = urwid.Text('Stdout:') 53 | if len(std_output) > 100: 54 | std_output = '%s...%s\n(output trimmed due to its length)' %\ 55 | (std_output[:90], std_output[-10:]) 56 | stdout = urwid.Text(std_output) 57 | list_items.extend([blank, stdout_header, stdout]) 58 | 59 | def make_success_view(self): 60 | blank = urwid.Divider() 61 | status_header = urwid.AttrWrap(urwid.Text('Run Code Status: '), 'body') 62 | status = urwid.AttrWrap(urwid.Text('Accepted'), 'accepted') 63 | columns = urwid.Columns([(20, status_header), (20, status)]) 64 | runtime = urwid.Text('Run time: %s' % self.result['status_runtime']) 65 | result_header = urwid.Text('--- Run Code Result: ---', align='center') 66 | list_items = [ 67 | result_header, 68 | blank, columns, 69 | blank, runtime 70 | ] 71 | self._append_stdout_if_non_empty(list_items) 72 | return urwid.Padding(urwid.ListBox(urwid.SimpleListWalker(list_items)), left=2, right=2) 73 | 74 | def make_failed_view(self): 75 | blank = urwid.Divider() 76 | status_header = urwid.AttrWrap(urwid.Text('Run Code Status: '), 'body') 77 | status = urwid.AttrWrap(urwid.Text('Wrong Answer'), 'hometag') 78 | columns = urwid.Columns([(17, status_header), (20, status)]) 79 | result_header = urwid.Text('--- Run Code Result: ---', align='center') 80 | passed_header = urwid.Text('Passed test cases:') 81 | s = self.result['compare_result'] 82 | passed = urwid.Text('%d/%d' % (s.count('1'), len(s))) 83 | your_input_header = urwid.Text('Your input:') 84 | your_input = urwid.Text(self.result['input']) 85 | your_answer_header = urwid.Text('Your answer:') 86 | your_answer = urwid.Text(self.result['code_output']) 87 | expected_answer_header = urwid.Text('Expected answer:') 88 | expected_answer = urwid.Text(self.result['expected_output']) 89 | list_items = [ 90 | result_header, 91 | blank, columns, 92 | blank, passed_header, passed, 93 | blank, your_input_header, your_input, 94 | blank, your_answer_header, your_answer, 95 | blank, expected_answer_header, expected_answer 96 | ] 97 | self._append_stdout_if_non_empty(list_items) 98 | return urwid.Padding(urwid.ListBox(urwid.SimpleListWalker(list_items)), left=2, right=2) 99 | 100 | def make_compile_error_view(self): 101 | blank = urwid.Divider() 102 | status_header = urwid.AttrWrap(urwid.Text('Run Code Status: '), 'body') 103 | status = urwid.AttrWrap(urwid.Text('Compile Error'), 'hometag') 104 | columns = urwid.Columns([(17, status_header), (20, status)]) 105 | column_wrap = urwid.WidgetWrap(columns) 106 | result_header = urwid.Text('--- Run Code Result: ---', align='center') 107 | your_input_header = urwid.Text('Your input:') 108 | your_input = urwid.Text('') 109 | your_answer_header = urwid.Text('Your answer:') 110 | your_answer = urwid.Text(self.result['compile_error']) 111 | expected_answer_header = urwid.Text('Expected answer:') 112 | expected_answer = urwid.Text('Unkown Error') 113 | list_items = [ 114 | result_header, 115 | blank, column_wrap, 116 | blank, your_input_header, your_input, 117 | blank, your_answer_header, your_answer, 118 | blank, expected_answer_header, expected_answer 119 | ] 120 | self._append_stdout_if_non_empty(list_items) 121 | return urwid.Padding(urwid.ListBox(urwid.SimpleListWalker(list_items)), left=2, right=2) 122 | 123 | def make_runtime_error_view(self): 124 | blank = urwid.Divider() 125 | status_header = urwid.AttrWrap(urwid.Text('Run Code Status: '), 'body') 126 | status = urwid.AttrWrap(urwid.Text('Runtime Error'), 'hometag') 127 | columns = urwid.Columns([(17, status_header), (20, status)]) 128 | column_wrap = urwid.WidgetWrap(columns) 129 | result_header = urwid.Text('--- Run Code Result: ---', align='center') 130 | error_header = urwid.Text('Runtime Error Message:') 131 | error_message = urwid.Text(self.result['runtime_error']) 132 | your_input_header = urwid.Text('Last input:') 133 | your_input = urwid.Text(self.result['last_testcase']) 134 | list_items = [ 135 | result_header, 136 | blank, column_wrap, 137 | blank, error_header, error_message, 138 | blank, your_input_header, your_input, 139 | ] 140 | self._append_stdout_if_non_empty(list_items) 141 | return urwid.Padding(urwid.ListBox(urwid.SimpleListWalker(list_items)), left=2, right=2) 142 | 143 | def make_unified_error_view(self, error_title): 144 | blank = urwid.Divider() 145 | status_header = urwid.AttrWrap(urwid.Text('Run Code Status: '), 'body') 146 | status = urwid.AttrWrap(urwid.Text(error_title), 'hometag') 147 | columns = urwid.Columns([(17, status_header), (30, status)]) 148 | column_wrap = urwid.WidgetWrap(columns) 149 | if 'last_testcase' in self.result: 150 | result_header = urwid.Text('--- Run Code Result: ---', align='center') 151 | your_input_header = urwid.Text('Last executed input:') 152 | your_input = urwid.Text(self.result['last_testcase']) 153 | list_items = [ 154 | result_header, 155 | blank, column_wrap, 156 | blank, your_input_header, your_input, 157 | ] 158 | else: 159 | list_items = [ 160 | result_header, 161 | blank, column_wrap, 162 | ] 163 | self._append_stdout_if_non_empty(list_items) 164 | return urwid.Padding(urwid.ListBox(urwid.SimpleListWalker(list_items)), left=2, right=2) 165 | 166 | def keypress(self, size, key): 167 | key = vim_key_map(key) 168 | if key == 'esc': 169 | self.destroy() 170 | else: 171 | return urwid.Frame.keypress(self, size, key) 172 | 173 | def show(self): 174 | if self.loop: 175 | self.loop.widget = self 176 | 177 | def destroy(self): 178 | if self.loop: 179 | self.loop.widget = self.host_view 180 | -------------------------------------------------------------------------------- /leetcode/terminal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | from threading import Thread 4 | import urwid 5 | from .client.leetcode import Leetcode 6 | from .views.home import HomeView 7 | from .views.detail import DetailView 8 | from .views.help import HelpView 9 | from .views.loading import LoadingView, Toast 10 | from .views.viewhelper import delay_refresh 11 | from .views.result import ResultView 12 | from .coding.code import get_code_file_path, get_code_for_submission 13 | 14 | palette = [ 15 | ('body', 'dark cyan', ''), 16 | ('focus', 'white', ''), 17 | ('head', 'white', 'dark gray'), 18 | ('lock', 'dark gray', ''), 19 | ('tag', 'white', 'light cyan', 'standout'), 20 | ('hometag', 'dark red', ''), 21 | ('accepted', 'dark green', '') 22 | ] 23 | 24 | 25 | class Terminal(object): 26 | def __init__(self): 27 | self.home_view = None 28 | self.loop = None 29 | self.help_view = None 30 | self.quit_confirm_view = None 31 | self.submit_confirm_view = None 32 | self.view_stack = [] 33 | self.detail_view = None 34 | self.search_view = None 35 | self.loading_view = None 36 | self.leetcode = Leetcode() 37 | self.logger = logging.getLogger(__name__) 38 | 39 | @property 40 | def current_view(self): 41 | return None if not len(self.view_stack) else self.view_stack[-1] 42 | 43 | @property 44 | def is_home(self): 45 | return len(self.view_stack) == 1 46 | 47 | def goto_view(self, view): 48 | self.loop.widget = view 49 | self.view_stack.append(view) 50 | 51 | def go_back(self): 52 | self.view_stack.pop() 53 | self.loop.widget = self.current_view 54 | 55 | def keystroke(self, key): 56 | if self.quit_confirm_view and self.current_view == self.quit_confirm_view: 57 | if key == 'y': 58 | raise urwid.ExitMainLoop() 59 | else: 60 | self.go_back() 61 | 62 | elif self.submit_confirm_view and self.current_view == self.submit_confirm_view: 63 | self.go_back() 64 | if key == 'y': 65 | self.send_code(self.detail_view.quiz) 66 | 67 | elif self.current_view == self.search_view: 68 | if key == 'enter': 69 | text = self.search_view.contents[1][0].original_widget.get_edit_text() 70 | self.home_view.handle_search(text) 71 | self.go_back() 72 | elif key == 'esc': 73 | self.go_back() 74 | 75 | elif key in ('q', 'Q'): 76 | self.goto_view(self.make_quit_confirmation()) 77 | 78 | elif key == 's': 79 | if not self.is_home: 80 | self.goto_view(self.make_submit_confirmation()) 81 | 82 | elif not self.is_home and (key == 'left' or key == 'h'): 83 | self.go_back() 84 | 85 | elif key == 'H': 86 | if not self.help_view: 87 | self.make_helpview() 88 | self.goto_view(self.help_view) 89 | 90 | elif key == 'R': 91 | if self.is_home: 92 | self.reload_list() 93 | 94 | elif key == 'f': 95 | if self.is_home: 96 | self.enter_search() 97 | 98 | elif key in ('enter', 'right'): 99 | if self.is_home and self.home_view.is_current_item_enterable(): 100 | self.enter_detail(self.home_view.get_current_item_data()) 101 | 102 | else: 103 | return key 104 | 105 | def enter_search(self): 106 | self.make_search_view() 107 | self.goto_view(self.search_view) 108 | 109 | def enter_detail(self, data): 110 | self.show_loading('Loading Quiz', 17, self.current_view) 111 | self.t = Thread(target=self.run_retrieve_detail, args=(data,)) 112 | self.t.start() 113 | 114 | def reload_list(self): 115 | '''Press R in home view to retrieve quiz list''' 116 | self.leetcode.load() 117 | if self.leetcode.quizzes and len(self.leetcode.quizzes) > 0: 118 | self.home_view = self.make_listview(self.leetcode.quizzes) 119 | self.view_stack = [] 120 | self.goto_view(self.home_view) 121 | 122 | def make_quit_confirmation(self): 123 | text = urwid.AttrMap(urwid.Text('Do you really want to quit ? (y/n)'), 'body') 124 | self.quit_confirm_view = urwid.Overlay(text, self.current_view, 'left', 125 | ('relative', 100), 'bottom', None) 126 | return self.quit_confirm_view 127 | 128 | def make_submit_confirmation(self): 129 | text = urwid.AttrMap(urwid.Text('Do you want to submit your code ? (y/n)'), 'body') 130 | self.submit_confirm_view = urwid.Overlay(text, self.current_view, 'left', 131 | ('relative', 100), 'bottom', None) 132 | return self.submit_confirm_view 133 | 134 | def make_search_view(self): 135 | text = urwid.AttrMap(urwid.Edit('Search by id: ', ''), 'body') 136 | self.search_view = urwid.Overlay(text, self.current_view, 'left', 137 | ('relative', 100), 'bottom', None) 138 | return self.search_view 139 | 140 | def make_detailview(self, data): 141 | self.detail_view = DetailView(data, self.loop) 142 | return self.detail_view 143 | 144 | def make_listview(self, data): 145 | header = self.make_header() 146 | self.home_view = HomeView(data, header) 147 | return self.home_view 148 | 149 | def make_header(self): 150 | if self.leetcode.is_login: 151 | columns = [ 152 | ('fixed', 15, urwid.Padding(urwid.AttrWrap( 153 | urwid.Text('%s' % self.leetcode.username), 154 | 'head', ''))), 155 | urwid.AttrWrap(urwid.Text('You have solved %d / %d problems. ' % ( 156 | len(self.leetcode.solved), len(self.leetcode.quizzes))), 'head', ''), 157 | urwid.AttrWrap(urwid.Text(('Premium ' if self.leetcode.is_paid else 'Free '), align="right"), 'head', ''), 158 | ] 159 | return urwid.Columns(columns) 160 | else: 161 | text = urwid.AttrWrap(urwid.Text('Not login'), 'head') 162 | return text 163 | 164 | def make_helpview(self): 165 | self.help_view = HelpView() 166 | return self.help_view 167 | 168 | def show_loading(self, text, width, host_view=urwid.SolidFill()): 169 | self.loading_view = LoadingView(text, width, host_view, self.loop) 170 | self.loop.widget = self.loading_view 171 | self.loading_view.start() 172 | 173 | def end_loading(self): 174 | if self.loading_view: 175 | self.loading_view.end() 176 | self.loading_view = None 177 | 178 | def retrieve_home_done(self, quizzes): 179 | self.home_view = self.make_listview(quizzes) 180 | self.view_stack = [] 181 | self.goto_view(self.home_view) 182 | self.end_loading() 183 | delay_refresh(self.loop) 184 | 185 | def retrieve_detail_done(self, data): 186 | data.id = self.home_view.listbox.get_focus()[0].data.id 187 | data.url = self.home_view.listbox.get_focus()[0].data.url 188 | self.goto_view(self.make_detailview(data)) 189 | self.end_loading() 190 | delay_refresh(self.loop) 191 | 192 | def run_retrieve_home(self): 193 | # self.leetcode.is_login = is_login() 194 | # if not self.leetcode.is_login: 195 | # self.leetcode.is_login = login() 196 | 197 | if self.loading_view: 198 | self.loading_view.set_text('Loading') 199 | 200 | self.leetcode.load() 201 | if self.leetcode.quizzes and len(self.leetcode.quizzes) > 0: 202 | self.retrieve_home_done(self.leetcode.quizzes) 203 | else: 204 | self.end_loading() 205 | toast = Toast('Request fail!', 10, self.current_view, self.loop) 206 | toast.show() 207 | self.logger.error('get quiz list fail') 208 | 209 | def run_retrieve_detail(self, quiz): 210 | ret = quiz.load() 211 | if ret: 212 | self.retrieve_detail_done(quiz) 213 | else: 214 | self.end_loading() 215 | toast = Toast('Request fail!', 10, self.current_view, self.loop) 216 | toast.show() 217 | self.logger.error('get detail %s fail', quiz.id) 218 | 219 | def run_send_code(self, quiz): 220 | filepath = get_code_file_path(quiz.id) 221 | if not filepath.exists(): 222 | return 223 | code = get_code_for_submission(filepath) 224 | code = code.replace('\n', '\r\n') 225 | success, text_or_id = quiz.submit(code) 226 | if success: 227 | self.loading_view.set_text('Retrieving') 228 | code = 1 229 | while code > 0: 230 | r = quiz.check_submission_result(text_or_id) 231 | code = r[0] 232 | 233 | self.end_loading() 234 | if code < -1: 235 | toast = Toast('error: %s' % r[1], 10 + len(r[1]), self.current_view, self.loop) 236 | toast.show() 237 | else: 238 | try: 239 | result = ResultView(quiz, self.detail_view, r[1], loop=self.loop) 240 | result.show() 241 | except ValueError as e: 242 | toast = Toast('error: %s' % e, 10 + len(str(e)), self.current_view, self.loop) 243 | toast.show() 244 | delay_refresh(self.loop) 245 | else: 246 | self.end_loading() 247 | toast = Toast('error: %s' % text_or_id, 10 + len(text_or_id), self.current_view, self.loop) 248 | toast.show() 249 | self.logger.error('send data fail') 250 | 251 | def send_code(self, data): 252 | self.show_loading('Sending code', 17, self.current_view) 253 | self.t = Thread(target=self.run_send_code, args=(data,)) 254 | self.t.start() 255 | 256 | def run(self): 257 | self.loop = urwid.MainLoop(None, palette, unhandled_input=self.keystroke) 258 | self.show_loading('Log In', 12) 259 | self.t = Thread(target=self.run_retrieve_home) 260 | self.t.start() 261 | try: 262 | self.loop.run() 263 | except KeyboardInterrupt: 264 | self.logger.info('Keyboard interrupt') 265 | except Exception: 266 | self.logger.exception("Fatal error in main loop") 267 | finally: 268 | self.clear_thread() 269 | sys.exit() 270 | 271 | def clear_thread(self): 272 | if self.loading_view: 273 | self.loading_view.end() 274 | if self.t and self.t.is_alive(): 275 | self.t.join() 276 | -------------------------------------------------------------------------------- /screenshots/Download_on_the_App_Store_Badge_US-UK_135x40.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 11 | 12 | 13 | 18 | 20 | 21 | 22 | 23 | 26 | 32 | 38 | 45 | 48 | 54 | 57 | 63 | 64 | 65 | 66 | 71 | 77 | 81 | 85 | 86 | 92 | 98 | 104 | 110 | 114 | 117 | 120 | 126 | 127 | 128 | 129 | 130 | --------------------------------------------------------------------------------