├── tests ├── __init__.py └── test_pls_cli.py ├── pls_cli ├── utils │ ├── __init__.py │ ├── quotes.py │ └── settings.py ├── __init__.py └── please.py ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── comment.yml │ ├── publish_pypi.yml │ ├── security.yml │ ├── deploy-docs.yml │ └── ci.yml ├── pre-commit ├── docs ├── integration.md ├── overrides │ └── main.html ├── themes.md ├── color_config.md ├── help.md ├── commands.md └── index.md ├── LICENSE ├── Makefile ├── pyproject.toml ├── .gitignore ├── mkdocs.yml ├── CODE_OF_CONDUCT.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pls_cli/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: guedesfelipe 2 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | make lint 4 | make test 5 | make sec 6 | make coverage-html 7 | -------------------------------------------------------------------------------- /docs/integration.md: -------------------------------------------------------------------------------- 1 |

2 | 🚧 TMUX integration 3 |

4 | 5 | Using `pls count-done` and `pls count-undone`. 6 | -------------------------------------------------------------------------------- /pls_cli/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from importlib.metadata import ( 3 | version, # type: ignore[no-redef] # Python 3.8+ 4 | ) 5 | except ImportError: 6 | from importlib_metadata import version # type: ignore[no-redef] 7 | 8 | __version__ = version('pls-cli') 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | commit-message: 8 | prefix: "⬆️" 9 | prefix-development: "⬆️" 10 | assignees: 11 | - "guedesfelipe" 12 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block analytics %} 4 | 5 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /pls_cli/utils/quotes.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import random 4 | 5 | 6 | def get_rand_quote(): 7 | __location__ = os.path.realpath( 8 | os.path.join(os.getcwd(), os.path.dirname(__file__)) 9 | ) 10 | with open( 11 | os.path.join(__location__, 'quotes.json'), encoding='utf-8' 12 | ) as quotes_file: 13 | list_of_quotes = json.load(quotes_file) 14 | return random.choice(list_of_quotes) 15 | -------------------------------------------------------------------------------- /.github/workflows/comment.yml: -------------------------------------------------------------------------------- 1 | name: issues 2 | 3 | on: 4 | issues: 5 | types: [closed] 6 | 7 | permissions: 8 | issues: write 9 | 10 | jobs: 11 | add-comment: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Did I solve your problem? 15 | uses: peter-evans/create-or-update-comment@v4 16 | with: 17 | issue-number: ${{ github.event.issue.number }} 18 | body: | 19 | Did I solve your problem? 20 | Consider [sponsoring this project](https://github.com/sponsors/guedesfelipe) to help keep the terminal productive and beautiful! 🚀 21 | -------------------------------------------------------------------------------- /.github/workflows/publish_pypi.yml: -------------------------------------------------------------------------------- 1 | name: Upload PLS-CLI 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | env: 8 | UV_SYSTEM_PYTHON: 1 9 | 10 | permissions: 11 | contents: read 12 | id-token: write 13 | 14 | jobs: 15 | build-n-publish: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 🛎️ 19 | uses: actions/checkout@v6 20 | 21 | - name: Setup Python 🐍 22 | uses: actions/setup-python@v6 23 | with: 24 | python-version: '3.12' 25 | 26 | - name: Install uv 📦️ 27 | uses: astral-sh/setup-uv@v7 28 | with: 29 | enable-cache: true 30 | cache-dependency-glob: | 31 | pyproject.toml 32 | uv.lock 33 | 34 | - name: Build 🏗 35 | run: uv build 36 | 37 | - name: Publish to PyPI 📤 38 | uses: pypa/gh-action-pypi-publish@v1.13.0 39 | with: 40 | password: ${{ secrets.PYPI_API_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | UV_SYSTEM_PYTHON: 1 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | check-security: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 🛎️ 24 | uses: actions/checkout@v6 25 | 26 | - name: Setup Python 🐍 27 | uses: actions/setup-python@v6 28 | with: 29 | python-version: '3.12' 30 | 31 | - name: Install uv 📦️ 32 | uses: astral-sh/setup-uv@v7 33 | with: 34 | enable-cache: true 35 | cache-dependency-glob: | 36 | pyproject.toml 37 | uv.lock 38 | 39 | - name: Install Dependencies 📌 40 | run: uv sync --group dev 41 | 42 | - name: Check Security 🚓 43 | run: make sec 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Felipe Guedes 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 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | env: 8 | UV_SYSTEM_PYTHON: 1 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | deploy-docs: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 🛎️ 18 | uses: actions/checkout@v6 19 | 20 | - name: Configure Git Credentials 21 | run: | 22 | git config user.name github-actions[bot] 23 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 24 | 25 | - name: Setup Python 🐍 26 | uses: actions/setup-python@v6 27 | with: 28 | python-version: '3.12' 29 | 30 | - name: Cache ID 31 | run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 32 | 33 | - name: Cache MkDocs Material 34 | uses: actions/cache@v4 35 | with: 36 | key: mkdocs-material-${{ env.cache_id }} 37 | path: .cache 38 | restore-keys: | 39 | mkdocs-material- 40 | 41 | - name: Install uv 📦️ 42 | uses: astral-sh/setup-uv@v7 43 | with: 44 | enable-cache: true 45 | cache-dependency-glob: | 46 | pyproject.toml 47 | uv.lock 48 | 49 | - name: Install Dependencies 📌 50 | run: uv sync --group dev 51 | 52 | - name: Deploy docs 📝 53 | run: uv run mkdocs gh-deploy --force 54 | -------------------------------------------------------------------------------- /docs/themes.md: -------------------------------------------------------------------------------- 1 |

2 | 🖼 Themes 3 |

4 | 5 | You can create your own theme following these [steps](https://guedesfelipe.github.io/pls-cli/color_config/) and share it with us and we'll post it here! 6 | 7 | If you create some theme, share with us here :heart:. 8 | 9 | ## Catppuccin-mocha 10 | :material-github: by [aravezskinteeth](https://github.com/aravezskinteeth) 11 | 12 | ```sh 13 | export PLS_ERROR_LINE_STYLE="#EBA0AC" 14 | export PLS_ERROR_TEXT_STYLE="#F38BA8 bold" 15 | 16 | export PLS_WARNING_LINE_STYLE="#F9E2AF" 17 | export PLS_WARNING_TEXT_STYLE="#F9E2AF bold" 18 | 19 | export PLS_UPDATE_LINE_STYLE="#A6E3A1" 20 | export PLS_UPDATE_TEXT_STYLE="#A6E3A1 bold" 21 | 22 | export PLS_INSERT_DELETE_LINE_STYLE="#CBA6F7" 23 | 24 | export PLS_INSERT_DELETE_TEXT_STYLE="#9399B2" 25 | 26 | export PLS_MSG_PENDING_STYLE="#9399B2" 27 | export PLS_TABLE_HEADER_STYLE="#F5C2E7" 28 | export PLS_TASK_DONE_STYLE="#9399B2" 29 | export PLS_TASK_PENDING_STYLE="#CBA6F7" 30 | export PLS_HEADER_GREETINGS_STYLE="#FAB387" 31 | export PLS_QUOTE_STYLE="#9399B2" 32 | export PLS_AUTHOR_STYLE="#9399B2" 33 | 34 | export PLS_BACKGROUND_BAR_STYLE="bar.back" 35 | export PLS_COMPLETE_BAR_STYLE="bar.complete" 36 | export PLS_FINISHED_BAR_STYLE="bar.finished" 37 | ``` 38 | 39 | ![image](https://user-images.githubusercontent.com/25853920/222288036-c7daa6e4-f9c4-4dff-be05-d6f29923efe5.png) 40 | 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #* Variables 2 | PYTHON := python 3 | 4 | #* uv 5 | .PHONY: uv-download 6 | uv-download: 7 | @curl -LsSf https://astral.sh/uv/install.sh | sh 8 | 9 | #* Config git hook 10 | .PHONY: git-config-hook 11 | git-config-hook: 12 | @cp pre-commit .git/hooks/ 13 | @chmod +x .git/hooks/pre-commit 14 | 15 | #* Installation 16 | .PHONY: install 17 | install: 18 | @uv sync --only-group test 19 | 20 | .PHONY: install-dev 21 | install-dev: 22 | @uv sync --all-groups 23 | @make git-config-hook 24 | 25 | #* Formatters 26 | .PHONY: format 27 | format: 28 | @uv run ruff format . 29 | @uv run ruff check --fix . 30 | 31 | #* Linting 32 | .PHONY: lint 33 | lint: 34 | @uv run ruff format --check . 35 | @uv run ruff check . 36 | @uv run mypy pls_cli tests 37 | 38 | #* Test 39 | .PHONY: test 40 | test: 41 | @uv run pytest -v 42 | 43 | #* Security 44 | .PHONY: sec 45 | sec: 46 | @uv run pip-audit --desc on --progress-spinner off 47 | 48 | #* Cleaning 49 | .PHONY: pycache-remove 50 | pycache-remove: 51 | @find . | grep -E "(__pycache__|\.pyc|\.pyo$$)" | xargs rm -rf 52 | 53 | .PHONY: dsstore-remove 54 | dsstore-remove: 55 | @find . | grep -E ".DS_Store" | xargs rm -rf 56 | 57 | .PHONY: mypycache-remove 58 | mypycache-remove: 59 | @find . | grep -E ".mypy_cache" | xargs rm -rf 60 | 61 | .PHONY: ipynbcheckpoints-remove 62 | ipynbcheckpoints-remove: 63 | @find . | grep -E ".ipynb_checkpoints" | xargs rm -rf 64 | 65 | .PHONY: pytestcache-remove 66 | pytestcache-remove: 67 | @find . | grep -E ".pytest_cache" | xargs rm -rf 68 | 69 | .PHONY: build-remove 70 | build-remove: 71 | @rm -rf build/ 72 | 73 | .PHONY: cleanup 74 | cleanup: pycache-remove dsstore-remove mypycache-remove ipynbcheckpoints-remove pytestcache-remove 75 | 76 | #* Coverage 77 | .PHONY: coverage 78 | coverage: 79 | @uv run pytest --cov --cov-fail-under 95 -v 80 | 81 | #* Coverage HTML 82 | .PHONY: coverage-html 83 | coverage-html: 84 | @uv run pytest --cov --cov-report html -v 85 | 86 | #* Publish 87 | .PHONY: publish 88 | publish: 89 | @uv build 90 | @uv publish 91 | -------------------------------------------------------------------------------- /docs/color_config.md: -------------------------------------------------------------------------------- 1 |

2 | 🎨 Color Configuration 3 |

4 | 5 | You can configure all colors with envs!! 6 | 7 | === "Linux, macOS, Windows Bash" 8 | 9 | ```sh 10 | export PLS_ERROR_LINE_STYLE="#e56767" 11 | ``` 12 | 13 | === "Windows PowerShell" 14 | 15 | ```sh 16 | $Env:PLS_ERROR_LINE_STYLE = "#e56767" 17 | ``` 18 | 19 | 20 | All envs: 21 | ```sh 22 | export PLS_ERROR_LINE_STYLE="#e56767" 23 | export PLS_ERROR_TEXT_STYLE="#ff0000 bold" 24 | 25 | export PLS_WARNING_LINE_STYLE="#FFBF00" 26 | export PLS_WARNING_TEXT_STYLE="#FFBF00 bold" 27 | 28 | export PLS_UPDATE_LINE_STYLE="#61E294" 29 | export PLS_UPDATE_TEXT_STYLE="#61E294 bold" 30 | 31 | export PLS_INSERT_DELETE_LINE_STYLE="#bb93f2" 32 | 33 | export PLS_INSERT_DELETE_TEXT_STYLE="#a0a0a0" 34 | 35 | export PLS_MSG_PENDING_STYLE="#61E294" 36 | export PLS_TABLE_HEADER_STYLE="#d77dd8" 37 | export PLS_TASK_DONE_STYLE="#a0a0a0" 38 | export PLS_TASK_PENDING_STYLE="#bb93f2" 39 | export PLS_HEADER_GREETINGS_STYLE="#FFBF00" 40 | export PLS_QUOTE_STYLE="#a0a0a0" 41 | export PLS_AUTHOR_STYLE="#a0a0a0" 42 | 43 | export PLS_BACKGROUND_BAR_STYLE="bar.back" 44 | export PLS_COMPLETE_BAR_STYLE="bar.complete" 45 | export PLS_FINISHED_BAR_STYLE="bar.finished" 46 | ``` 47 | 48 | 49 | ???+ tip "You can specify the background color like this:" 50 | 51 | 52 | ```sh 53 | export PLS_QUOTE_STYLE="#a0a0a0 on blue" 54 | ``` 55 | 56 | If you create some theme, share with us here :heart:. 57 | 58 | ## 💄 Formatting a task 59 | 60 | ???+ info "You can format your tasks with:" 61 | 62 | 63 | ```sh 64 | pls add "[b]Bold[/], [i]Italic[/], [s]Strikethrough[/], [d]Dim[/], [r]Reverse[/], [red]Color Red[/], [#FFBF00 on green]Color exa with background[/], :star:, ✨" 65 | ``` 66 | 67 | 68 | 69 | To learn more check out the Rich docs. 70 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pls-cli" 3 | version = "0.4.1" 4 | description = "Minimalist and full configurable greetings and TODO list" 5 | authors = [ 6 | {name = "Felipe Guedes", email = "contatofelipeguedes@gmail.com"} 7 | ] 8 | readme = "README.md" 9 | requires-python = ">=3.9" 10 | keywords = ["pls", "please", "todo", "cli"] 11 | classifiers = [ 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.9", 14 | "Programming Language :: Python :: 3.10", 15 | "Programming Language :: Python :: 3.11", 16 | "Programming Language :: Python :: 3.12", 17 | "Programming Language :: Python :: 3.13", 18 | "Programming Language :: Python :: 3.14", 19 | "Operating System :: OS Independent", 20 | ] 21 | dependencies = [ 22 | "rich>=13.7.0", 23 | "typer>=0.9.0", 24 | ] 25 | 26 | [dependency-groups] 27 | test = [ 28 | "pytest>=7.2", 29 | "pytest-cov>=4.0.0", 30 | "freezegun>=1.2.2", 31 | ] 32 | dev = [ 33 | "ruff>=0.8.0", 34 | "mypy>=1.0.0", 35 | "pip-audit>=2.8.0", 36 | "mkdocs>=1.5.3", 37 | "mkdocs-material>=9.2.7", 38 | "mkdocs-meta-descriptions-plugin>=3.0.0", 39 | "types-setuptools>=69.0.0", 40 | ] 41 | 42 | [project.urls] 43 | Homepage = "https://guedesfelipe.github.io/pls-cli/" 44 | Documentation = "https://guedesfelipe.github.io/pls-cli/" 45 | Repository = "https://github.com/guedesfelipe/pls-cli" 46 | 47 | [project.scripts] 48 | pls = "pls_cli.please:app" 49 | 50 | [build-system] 51 | requires = ["hatchling"] 52 | build-backend = "hatchling.build" 53 | 54 | [tool.hatch.build.targets.wheel] 55 | packages = ["pls_cli"] 56 | 57 | [tool.ruff] 58 | line-length = 80 59 | target-version = "py38" 60 | 61 | [tool.ruff.lint] 62 | select = [ 63 | "E", # pycodestyle errors 64 | "W", # pycodestyle warnings 65 | "F", # pyflakes 66 | "I", # isort 67 | "N", # pep8-naming 68 | "UP", # pyupgrade 69 | "B", # flake8-bugbear 70 | "C4", # flake8-comprehensions 71 | "SIM", # flake8-simplify 72 | "TCH", # flake8-type-checking 73 | ] 74 | ignore = [] 75 | 76 | [tool.ruff.lint.pycodestyle] 77 | max-line-length = 80 78 | 79 | [tool.ruff.format] 80 | quote-style = "single" 81 | indent-style = "space" 82 | line-ending = "auto" 83 | 84 | [tool.ruff.lint.isort] 85 | force-single-line = false 86 | force-wrap-aliases = true 87 | combine-as-imports = true 88 | split-on-trailing-comma = true 89 | 90 | [tool.mypy] 91 | ignore_missing_imports = true 92 | follow_imports = "skip" 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | #.env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | #env 132 | .env 133 | 134 | .DS_Store 135 | -------------------------------------------------------------------------------- /pls_cli/utils/settings.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from os.path import expanduser 4 | from typing import List 5 | 6 | 7 | class Settings: 8 | def __init__(self) -> None: 9 | self.config_name = self.get_config_name() 10 | self.config_path = self.get_config_path() 11 | self.full_settings_path = os.path.join( 12 | self.config_path, self.config_name 13 | ) 14 | self.create_dir_if_not_exists() 15 | self.minimal_default_config = {'user_name': '', 'tasks': []} 16 | 17 | def get_config_name(self): 18 | return 'config.json' 19 | 20 | def get_config_path(self): 21 | return os.path.join(expanduser('~'), '.config', 'pls') 22 | 23 | def get_full_settings_path(self): 24 | return self.full_settings_path 25 | 26 | def create_dir_if_not_exists(self) -> None: 27 | if not os.path.exists(self.config_path): 28 | os.makedirs(self.config_path) 29 | 30 | def exists_settings(self) -> bool: 31 | return os.path.exists(self.full_settings_path) 32 | 33 | def get_settings(self) -> dict: 34 | if os.path.exists(self.full_settings_path): 35 | with open(self.full_settings_path, encoding='utf-8') as config_file: 36 | return json.load(config_file) 37 | return self.minimal_default_config 38 | 39 | def write_settings(self, data: dict) -> None: 40 | with open( 41 | self.full_settings_path, 'w', encoding='utf-8' 42 | ) as config_file: 43 | json.dump(data, config_file, indent=2) 44 | 45 | def get_name(self) -> str: 46 | return self.get_settings().get('user_name', '') 47 | 48 | def get_tasks(self) -> List[dict]: 49 | return self.get_settings().get('tasks', []) 50 | 51 | def show_tasks_progress(self) -> bool: 52 | return self.get_settings().get('show_task_progress', True) 53 | 54 | def show_quotes(self) -> bool: 55 | return self.get_settings().get('show_quotes', True) 56 | 57 | def all_tasks_done(self) -> bool: 58 | return all(task.get('done', '') for task in self.get_tasks()) 59 | 60 | def get_all_tasks_undone(self) -> List[dict]: 61 | return [task for task in self.get_tasks() if not task['done']] 62 | 63 | def count_tasks_done(self) -> int: 64 | if not self.get_tasks(): 65 | return 0 66 | return len( 67 | [task.get('done', '') for task in self.get_tasks() if task['done']] 68 | ) 69 | 70 | def count_tasks_undone(self) -> int: 71 | if not self.get_tasks(): 72 | return 0 73 | return len( 74 | [ 75 | task.get('done', '') 76 | for task in self.get_tasks() 77 | if not task['done'] 78 | ] 79 | ) 80 | -------------------------------------------------------------------------------- /docs/help.md: -------------------------------------------------------------------------------- 1 |

2 | 🆘 Help PLS-CLI - Get Help 3 |

4 | 5 | 6 | Do you like **PLS-CLI**? 7 | 8 | Would you like to help **PLS-CLI**, other users, and the author? 9 | 10 | Or would you like to get help with **PLS-CLI**? 11 | 12 | There are very simple ways to help (several involve just one or two clicks). 13 | 14 | And there are several ways to get help too. 15 | 16 | ## ⭐️ **PLS-CLI** in GitHub 17 | 18 | You can "star" in GitHub (clicking in the star button at the top right): 19 | https://github.com/guedesfelipe/pls-cli. 20 | 21 | ## 👀 Watch the GitHub repository for releases 22 | 23 | You can "watch" **PLS-CLI** in GitHub (clicking the "watch" button at the top right): 24 | 25 | https://github.com/guedesfelipe/pls-cli. 26 | 27 | There you can select "Custom -> Releases". 28 | 29 | By doing it, you will receive notifications (in your email) whenever there's a new release (a new version) of **PLS-CLI** with bug fixes and new features. 30 | 31 | ## 🔭 Watch the GitHub repository 32 | 33 | You can "watch" **PLS-CLI** in GitHub (clicking the "watch" button at the top right): 34 | 35 | https://github.com/guedesfelipe/pls-cli. 36 | 37 | If you select "Watching" instead of "Releases only" you will receive notifications when someone creates a new issue. 38 | 39 | Then you can try to help them solve those issues. 40 | 41 | ## 🆘 Help others with issues in GitHub 42 | 43 | You can see existing issues and try to help others, most of the times they are questions that you might already know the answer for. 🤓 44 | 45 | ## 📝Create issues 46 | 47 | You can create a new issue in the GitHub repository, for example to: 48 | 49 | * Ask a **question** or ask about a **problem**. 50 | * Suggest a new **feature**. 51 | 52 | ## 🎨 Creating themes 53 | 54 | If you create some theme, share with us here :heart:. 55 | 56 | ## ✌️ Sponsor the author 57 | 58 | You can also financially support the author (me) through GitHub Sponsors. 59 | 60 | There you could help keep this project alive and thriving. 😄 61 | 62 | ## 🤝 Sponsor the tools that power **PLS-CLI** 63 | As you have seen in the documentation, PLS-CLI stands on the shoulders of giants, **Typer** and **Rich**. 64 | 65 | You can also sponsor: 66 | 67 | * Sebastián Ramírez / tiangolo (Typer) 68 | * Textualize (Rich) 69 | 70 | --- 71 | 72 | Thanks! 🚀 73 | -------------------------------------------------------------------------------- /docs/commands.md: -------------------------------------------------------------------------------- 1 |

2 | ⌨️ Commands 3 |

4 | 5 | ```sh 6 | pls --help 7 | ``` 8 | 9 | ``` 10 | Usage: pls [OPTIONS] COMMAND [ARGS]... 11 | 12 | 💻 PLS-CLI 13 | ・Minimalist and full configurable greetings and TODO list・ 14 | 15 | ╭─ Options ────────────────────────────────────────────────────────────────────╮ 16 | │ --install-completion Install completion for the current shell. │ 17 | │ --show-completion Show completion for the current shell, to copy │ 18 | │ it or customize the installation. │ 19 | │ --help Show this message and exit. │ 20 | ╰──────────────────────────────────────────────────────────────────────────────╯ 21 | ╭─ Commands ───────────────────────────────────────────────────────────────────╮ 22 | │ add Add a Task ✨ (Add task name inside quotes) │ 23 | │ clean Clean up tasks marked as done 🧹 │ 24 | │ clear Clear all tasks 🗑 │ 25 | │ del Delete a Task │ 26 | │ delete Delete a Task (deprecated) │ 27 | │ done Mark a task as done ✓ │ 28 | │ edit Edit a task by id ✏️ (Add task name inside quotes) │ 29 | │ move Insert a task in a new position │ 30 | │ showtasks Show all Tasks 📖 (deprecated) │ 31 | | swap Swap a task's position with another 🔀 │ 32 | │ tasks Show all Tasks 📖 │ 33 | │ undone Mark a task as undone ○ │ 34 | ╰──────────────────────────────────────────────────────────────────────────────╯ 35 | ╭─ Utils and Configs ──────────────────────────────────────────────────────────╮ 36 | │ callme Change name 📛 (without resetting data) │ 37 | │ config Launch config directory 📂 │ 38 | │ docs Launch docs Website 🌐 │ 39 | │ quotes Show quotes 🏷 │ 40 | │ tasks-progress Show tasks progress 🎯 │ 41 | │ setup Reset all data and run setup 🔧 │ 42 | │ version Show version 🔖 │ 43 | ╰──────────────────────────────────────────────────────────────────────────────╯ 44 | ╭─ Integration ────────────────────────────────────────────────────────────────╮ 45 | │ count-done Count done tasks 📈 │ 46 | │ count-undone Count undone tasks 📉 │ 47 | ╰──────────────────────────────────────────────────────────────────────────────╯ 48 | 49 | Made with ❤ by Felipe Guedes 50 | ``` 51 | 52 | 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continous Integration 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | UV_SYSTEM_PYTHON: 1 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | linting: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 🛎️ 24 | uses: actions/checkout@v6 25 | 26 | - name: Setup Python 🐍 27 | uses: actions/setup-python@v6 28 | with: 29 | python-version: '3.12' 30 | 31 | - name: Install uv 📦️ 32 | uses: astral-sh/setup-uv@v7 33 | with: 34 | enable-cache: true 35 | cache-dependency-glob: | 36 | pyproject.toml 37 | uv.lock 38 | 39 | - name: Install Dependencies 📌 40 | run: uv sync --all-groups 41 | 42 | - name: Linting 🔎 43 | run: make lint 44 | 45 | test: 46 | needs: linting 47 | strategy: 48 | fail-fast: false 49 | matrix: 50 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] 51 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 52 | defaults: 53 | run: 54 | shell: bash 55 | runs-on: ${{ matrix.os }} 56 | steps: 57 | - name: Checkout 🛎️ 58 | uses: actions/checkout@v6 59 | 60 | - name: Setup Python ${{ matrix.python-version }} 🐍 61 | uses: actions/setup-python@v6 62 | with: 63 | python-version: ${{ matrix.python-version }} 64 | 65 | - name: Install uv 📦️ 66 | uses: astral-sh/setup-uv@v7 67 | with: 68 | enable-cache: true 69 | cache-dependency-glob: | 70 | pyproject.toml 71 | uv.lock 72 | 73 | - name: Install Dependencies 📌 74 | run: uv sync --only-group test 75 | 76 | - name: Run Tests ✅ 77 | run: uv run pytest -v -l --full-trace --cache-clear tests/ 78 | 79 | codecov: 80 | needs: test 81 | runs-on: ubuntu-latest 82 | permissions: 83 | contents: read 84 | steps: 85 | - name: Checkout 🛎️ 86 | uses: actions/checkout@v6 87 | 88 | - name: Setup Python 🐍 89 | uses: actions/setup-python@v6 90 | with: 91 | python-version: '3.12' 92 | 93 | - name: Install uv 📦️ 94 | uses: astral-sh/setup-uv@v7 95 | with: 96 | enable-cache: true 97 | cache-dependency-glob: | 98 | pyproject.toml 99 | uv.lock 100 | 101 | - name: Install Dependencies 📌 102 | run: uv sync --only-group test 103 | 104 | - name: Coverage ☂️ 105 | run: uv run pytest --cov --cov-report=xml 106 | 107 | - name: Upload Coverage 📤 108 | uses: codecov/codecov-action@v5 109 | with: 110 | token: ${{ secrets.CODECOV_TOKEN }} 111 | files: ./coverage.xml 112 | fail_ci_if_error: true 113 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project information 2 | site_name: PLS-CLI 3 | site_url: https://guedesfelipe.github.io/pls-cli/ 4 | site_author: Felipe Guedes 5 | site_description: PLS-CLI Minimalist and full configurable greetings and TODO list, say please cli 6 | 7 | # Repository 8 | repo_name: guedesfelipe/pls-cli 9 | repo_url: https://github.com/guedesfelipe/pls-cli 10 | edit_uri: "" 11 | 12 | theme: 13 | name: material 14 | custom_dir: docs/overrides 15 | icon: 16 | logo: octicons/terminal-16 17 | features: 18 | - content.code.annotate 19 | - navigation.top 20 | palette: 21 | - media: "(prefers-color-scheme: dark)" 22 | scheme: slate 23 | toggle: 24 | icon: material/toggle-switch 25 | name: Switch to light mode 26 | primary: amber 27 | accent: deep orange 28 | - media: "(prefers-color-scheme: light)" 29 | scheme: default 30 | toggle: 31 | icon: material/toggle-switch-off-outline 32 | name: Switch to dark mode 33 | primary: amber 34 | accent: deep orange 35 | 36 | # Plugins 37 | plugins: 38 | - search 39 | - meta-descriptions: 40 | export_csv: false 41 | quiet: false 42 | 43 | # Customization 44 | extra: 45 | analytics: 46 | provider: google 47 | property: G-TNKR4D5P7Q 48 | homepage: https://guedesfelipe.github.io/pls-cli/ 49 | social: 50 | - icon: fontawesome/brands/github 51 | link: https://github.com/guedesfelipe 52 | - icon: fontawesome/brands/linkedin 53 | link: https://www.linkedin.com/in/felipe-guedes-263480127 54 | - icon: fontawesome/solid/globe 55 | link: https://guedesfelipe.github.io/blog/ 56 | - icon: fontawesome/solid/graduation-cap 57 | link: https://guedesfelipe.github.io 58 | 59 | # Extensions 60 | markdown_extensions: 61 | - abbr 62 | - admonition 63 | - attr_list 64 | - def_list 65 | - footnotes 66 | - meta 67 | - md_in_html 68 | - toc: 69 | permalink: true 70 | - pymdownx.arithmatex: 71 | generic: true 72 | - pymdownx.betterem: 73 | smart_enable: all 74 | - pymdownx.caret 75 | - pymdownx.details 76 | - pymdownx.emoji: 77 | emoji_index: !!python/name:material.extensions.emoji.twemoji 78 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 79 | - pymdownx.highlight: 80 | anchor_linenums: true 81 | - pymdownx.inlinehilite 82 | - pymdownx.keys 83 | - pymdownx.magiclink: 84 | repo_url_shorthand: true 85 | user: squidfunk 86 | repo: mkdocs-material 87 | - pymdownx.mark 88 | - pymdownx.smartsymbols 89 | - pymdownx.superfences: 90 | custom_fences: 91 | - name: mermaid 92 | class: mermaid 93 | format: !!python/name:pymdownx.superfences.fence_code_format 94 | - pymdownx.tabbed: 95 | alternate_style: true 96 | - pymdownx.tasklist: 97 | custom_checkbox: true 98 | - pymdownx.tilde 99 | - pymdownx.snippets: 100 | base_path: 101 | - docs_src 102 | 103 | # Page tree 104 | nav: 105 | - 🏠  Home: index.md 106 | - ⌨️  Commands: commands.md 107 | - 🎨  Color Configuration: color_config.md 108 | - 🖼  Themes: themes.md 109 | - 🚧   Integration: integration.md 110 | - 🆘   Help PLS-CLI - Get Help: help.md 111 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusng on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contatofelipeguedes@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |

2 | 💻 PLS-CLI 3 |

4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

22 | 23 |

24 | If you are like me, and your terminal is your home, this CLI will make your life better, I hope 😄 25 |
26 |
27 | 28 |

29 | 30 | ## 🛠 Installation 31 | 32 | !!! success "Recommended: Using pipx (isolated environment)" 33 | 34 | [pipx](https://pipx.pypa.io/) installs the CLI in an isolated environment, avoiding conflicts with other packages: 35 | 36 | ```sh 37 | # Install pipx if you haven't already 38 | python3 -m pip install --user pipx 39 | python3 -m pipx ensurepath 40 | 41 | # Install pls-cli 42 | pipx install pls-cli 43 | ``` 44 | 45 | !!! info "Alternative: Using pip" 46 | 47 | ```sh 48 | pip install pls-cli 49 | ``` 50 | 51 | !!! warning 52 | Installing with pip may cause dependency conflicts with other packages. We recommend using pipx for CLI tools. 53 | 54 | !!! tip "Upgrade Version" 55 | 56 | === "With pipx (recommended)" 57 | 58 | ```sh 59 | pipx upgrade pls-cli 60 | ``` 61 | 62 | === "With pip" 63 | 64 | ```sh 65 | pip install pls-cli --upgrade 66 | ``` 67 | 68 | ## ⚙️ Configuration 69 | 70 | To run **`pls-cli`** everytime you open your shell's: 71 | 72 | 73 | === "Bash" 74 | 75 | ```sh 76 | echo 'pls' >> ~/.bashrc 77 | ``` 78 | 79 | === "Zsh" 80 | 81 | ```sh 82 | echo 'pls' >> ~/.zshrc 83 | ``` 84 | 85 | === "Fish" 86 | 87 | ```sh 88 | echo 'pls' >> ~/.config/fish/config.fish 89 | ``` 90 | 91 | === "Ion" 92 | 93 | ```sh 94 | echo 'pls' >> ~/.config/ion/initrc 95 | ``` 96 | 97 | === "Tcsh" 98 | 99 | ```sh 100 | echo 'pls' >> ~/.tcshrc 101 | ``` 102 | 103 | === "Xonsh" 104 | 105 | ```sh 106 | echo 'pls' >> ~/.xonshrc 107 | ``` 108 | 109 | === "Powershell" 110 | 111 | Add the following to the end of `Microsoft.PowerShell_profile.ps1`. You can check the location of this file by querying the `$PROFILE` variable in PowerShell. Typically the path is `~\Documents\PowerShell\Microsoft.PowerShell_profile.ps1` or `~/.config/powershell/Microsoft.PowerShell_profile.ps1` on -Nix. 112 | 113 | ```txt 114 | pls 115 | ``` 116 | 117 | !!! attention 118 | 119 | Restart your terminal to apply the changes and start configuring your PLS-CLI. 🎉 120 | 121 | ## 🤝 Special thanks 122 | 123 | **PLS-CLI** stands on the shoulders of giants: 124 | 125 | * Typer for the CLI tool. 126 | * Rich for the beautiful formatting in terminal. 127 | 128 | --- 129 | 130 |

131 | 132 | 133 | 134 |

135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 💻 PLS-CLI 3 |

4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |

22 | 23 |

24 | If you are like me, and your terminal is your home, this CLI will make your life better, I hope 😄 25 |
26 |
27 | 28 |

29 | 30 | # 🛠 Installation 31 | 32 | ## Recommended: Using pipx (isolated environment) 33 | 34 | [pipx](https://pipx.pypa.io/) installs the CLI in an isolated environment, avoiding conflicts with other packages: 35 | 36 | ```sh 37 | # Install pipx if you haven't already 38 | python3 -m pip install --user pipx 39 | python3 -m pipx ensurepath 40 | 41 | # Install pls-cli 42 | pipx install pls-cli 43 | ``` 44 | 45 | ## Alternative: Using pip 46 | 47 | ```sh 48 | pip install pls-cli 49 | ``` 50 | 51 | > **Note**: Installing with pip may cause dependency conflicts with other packages. We recommend using pipx for CLI tools. 52 | 53 | # ⬆️ Upgrade version 54 | 55 | ## With pipx (recommended) 56 | 57 | ```sh 58 | pipx upgrade pls-cli 59 | ``` 60 | 61 | ## With pip 62 | 63 | ```sh 64 | pip install pls-cli --upgrade 65 | ``` 66 | 67 | # ⚙️ Configuration 68 | 69 | To run **`pls-cli`** everytime you open your shell's: 70 | 71 |

Bash

72 | 73 | ```sh 74 | echo 'pls' >> ~/.bashrc 75 | ``` 76 | 77 |
78 | 79 |

Zsh

80 | 81 | ```sh 82 | echo 'pls' >> ~/.zshrc 83 | ``` 84 | 85 |
86 | 87 |

Fish

88 | 89 | ```sh 90 | echo 'pls' >> ~/.config/fish/config.fish 91 | ``` 92 | 93 |
94 | 95 |

Ion

96 | 97 | ```sh 98 | echo 'pls' >> ~/.config/ion/initrc 99 | ``` 100 | 101 |
102 | 103 |

Tcsh

104 | 105 | ```sh 106 | echo 'pls' >> ~/.tcshrc 107 | ``` 108 | 109 |
110 | 111 |

Xonsh

112 | 113 | ```sh 114 | echo 'pls' >> ~/.xonshrc 115 | ``` 116 |
117 | 118 |

Powershell

119 | 120 | Add the following to the end of `Microsoft.PowerShell_profile.ps1`. You can check the location of this file by querying the `$PROFILE` variable in PowerShell. Typically the path is `~\Documents\PowerShell\Microsoft.PowerShell_profile.ps1` or `~/.config/powershell/Microsoft.PowerShell_profile.ps1` on -Nix. 121 | 122 | ```txt 123 | pls 124 | ``` 125 | 126 |
127 | 128 | ⚠️ Restart your terminal to apply the changes and start configuring your PLS-CLI. 🎉 129 | 130 | # ⌨️ Commands 131 | 132 | ```sh 133 | pls --help 134 | ``` 135 | 136 | Or for more information you can see in the [documentation](https://guedesfelipe.github.io/pls-cli/commands). 137 | 138 | 139 | # 🎨 Color Configuration 140 | 141 | You can configure all colors with envs!! 142 | 143 |

Setting env on Linux, macOS, Windows Bash:

144 | 145 | ```sh 146 | export PLS_ERROR_LINE_STYLE="#e56767" 147 | ``` 148 | 149 |
150 | 151 |

Setting env on Windows PowerShell:

152 | 153 | ```sh 154 | $Env:PLS_ERROR_LINE_STYLE = "#e56767" 155 | ``` 156 | 157 |
158 | 159 | All envs: 160 | ```sh 161 | export PLS_ERROR_LINE_STYLE="#e56767" 162 | export PLS_ERROR_TEXT_STYLE="#ff0000 bold" 163 | 164 | export PLS_WARNING_LINE_STYLE="#FFBF00" 165 | export PLS_WARNING_TEXT_STYLE="#FFBF00 bold" 166 | 167 | export PLS_UPDATE_LINE_STYLE="#61E294" 168 | export PLS_UPDATE_TEXT_STYLE="#61E294 bold" 169 | 170 | export PLS_INSERT_DELETE_LINE_STYLE="#bb93f2" 171 | 172 | export PLS_INSERT_DELETE_TEXT_STYLE="#a0a0a0" 173 | 174 | export PLS_MSG_PENDING_STYLE="#61E294" 175 | export PLS_TABLE_HEADER_STYLE="#d77dd8" 176 | export PLS_TASK_DONE_STYLE="#a0a0a0" 177 | export PLS_TASK_PENDING_STYLE="#bb93f2" 178 | export PLS_HEADER_GREETINGS_STYLE="#FFBF00" 179 | export PLS_QUOTE_STYLE="#a0a0a0" 180 | export PLS_AUTHOR_STYLE="#a0a0a0" 181 | 182 | export PLS_BACKGROUND_BAR_STYLE="bar.back" 183 | export PLS_COMPLETE_BAR_STYLE="bar.complete" 184 | export PLS_FINISHED_BAR_STYLE="bar.finished" 185 | ``` 186 | 187 |

You can specify the background color like this:

188 | 189 | ```sh 190 | export PLS_QUOTE_STYLE="#a0a0a0 on blue" 191 | ``` 192 | 193 |
194 | 195 | If you create some theme, share with us here ♥️. 196 | 197 | ## 💄 Formatting a task 198 | 199 |

You can format your tasks with:

200 | 201 | ```sh 202 | pls add "[b]Bold[/], [i]Italic[/], [s]Strikethrough[/], [d]Dim[/], [r]Reverse[/], [red]Color Red[/], [#FFBF00 on green]Color exa with background[/], :star:, ✨" 203 | ``` 204 | 205 | ![image](https://user-images.githubusercontent.com/25853920/175835339-8059bc7e-0538-4e2d-aed8-80487d7b2478.png) 206 | 207 |
208 | 209 | ## 🚧 TMUX integration 210 | 211 | Using `pls count-done` and `pls count-undone`. 212 | 213 | ## 🤝 Special thanks 214 | 215 | **PLS-CLI** stands on the shoulders of giants: 216 | 217 | * Typer for the CLI tool. 218 | * Rich for the beautiful formatting in terminal. 219 | 220 | --- 221 | 222 |

223 | 224 | 225 | 226 |

227 | -------------------------------------------------------------------------------- /pls_cli/please.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import os 4 | import shutil 5 | from typing import Union 6 | 7 | import typer 8 | from rich import box 9 | from rich.align import Align 10 | from rich.console import Console, RenderableType 11 | from rich.markdown import Markdown 12 | from rich.progress import BarColumn, MofNCompleteColumn, Progress 13 | from rich.rule import Rule 14 | from rich.table import Table 15 | 16 | from pls_cli import __version__ 17 | from pls_cli.utils.quotes import get_rand_quote 18 | from pls_cli.utils.settings import Settings 19 | 20 | app = typer.Typer(rich_markup_mode='rich') 21 | console = Console() 22 | 23 | error_line_style = os.getenv('PLS_ERROR_LINE_STYLE', '#e56767') 24 | error_text_style = os.getenv('PLS_ERROR_TEXT_STYLE', '#ff0000 bold') 25 | 26 | warning_line_style = os.getenv('PLS_WARNING_LINE_STYLE', '#FFBF00') 27 | warning_text_style = os.getenv('PLS_WARNING_TEXT_STYLE', '#FFBF00 bold') 28 | 29 | update_line_style = os.getenv('PLS_UPDATE_LINE_STYLE', '#61E294') 30 | update_text_style = os.getenv('PLS_UPDATE_TEXT_STYLE', '#61E294 bold') 31 | 32 | insert_or_delete_line_style = os.getenv( 33 | 'PLS_INSERT_DELETE_LINE_STYLE', '#bb93f2' 34 | ) 35 | insert_or_delete_text_style = os.getenv( 36 | 'PLS_INSERT_DELETE_TEXT_STYLE', '#a0a0a0' 37 | ) 38 | 39 | msg_pending_style = os.getenv('PLS_MSG_PENDING_STYLE', '#61E294') 40 | table_header_style = os.getenv('PLS_TABLE_HEADER_STYLE', '#d77dd8') 41 | task_done_style = os.getenv('PLS_TASK_DONE_STYLE', '#a0a0a0') 42 | task_pending_style = os.getenv('PLS_TASK_PENDING_STYLE', '#bb93f2') 43 | header_greetings_style = os.getenv('PLS_HEADER_GREETINGS_STYLE', '#FFBF00') 44 | quote_style = os.getenv('PLS_QUOTE_STYLE', '#a0a0a0') 45 | author_style = os.getenv('PLS_AUTHOR_STYLE', '#a0a0a0') 46 | 47 | background_bar_style = os.getenv('PLS_BACKGROUND_BAR_STYLE', 'bar.back') 48 | complete_bar_style = os.getenv('PLS_COMPLETE_BAR_STYLE', 'bar.complete') 49 | finished_bar_style = os.getenv('PLS_FINISHED_BAR_STYLE', 'bar.finished') 50 | 51 | 52 | def get_terminal_full_width() -> int: 53 | return shutil.get_terminal_size().columns 54 | 55 | 56 | def get_terminal_center_width() -> int: 57 | return shutil.get_terminal_size().columns // 2 58 | 59 | 60 | def center_print( 61 | text, style: Union[str, None] = None, wrap: bool = False 62 | ) -> None: 63 | """Print text with center alignment. 64 | Args: 65 | text (Union[str, Rule, Table]): object to center align 66 | style (str, optional): styling of the object. Defaults to None. 67 | """ 68 | width = get_terminal_full_width() if wrap else get_terminal_full_width() 69 | 70 | if isinstance(text, Rule): 71 | console.print(text, style=style, width=width) 72 | else: 73 | console.print(Align.center(text, style=style, width=width)) 74 | 75 | 76 | def print_no_pending_tasks() -> None: 77 | center_print( 78 | f'[{msg_pending_style}]Looking good, no pending tasks[/] ✨ 🍰 ✨' 79 | ) 80 | 81 | 82 | class CenteredProgress(Progress): 83 | def get_renderable(self) -> RenderableType: 84 | return Align.center(super().get_renderable()) 85 | 86 | 87 | def print_tasks_progress() -> None: 88 | if Settings().show_tasks_progress(): 89 | with CenteredProgress( 90 | BarColumn( 91 | bar_width=get_terminal_center_width(), 92 | style=background_bar_style, 93 | complete_style=complete_bar_style, 94 | finished_style=finished_bar_style, 95 | ), 96 | MofNCompleteColumn(), 97 | ) as progress: 98 | qty_done = Settings().count_tasks_done() 99 | qty_undone = Settings().count_tasks_undone() 100 | task1 = progress.add_task('Progress', total=qty_done + qty_undone) 101 | progress.update(task1, advance=qty_done) 102 | 103 | 104 | @app.command('tasks-progress', rich_help_panel='Utils and Configs') 105 | def tasks_progress(show: bool = True) -> None: 106 | """Show tasks progress 🎯""" 107 | settings = Settings().get_settings() 108 | settings['show_task_progress'] = show 109 | Settings().write_settings(settings) 110 | center_print( 111 | Rule( 112 | 'Thanks for letting me know that!', 113 | style=insert_or_delete_line_style, 114 | ), 115 | style=insert_or_delete_text_style, 116 | ) 117 | 118 | 119 | @app.command('quotes', rich_help_panel='Utils and Configs') 120 | def quotes(show: bool = True) -> None: 121 | """Show quotes 🏷""" 122 | settings = Settings().get_settings() 123 | settings['show_quotes'] = show 124 | Settings().write_settings(settings) 125 | center_print( 126 | Rule( 127 | 'Thanks for letting me know that!', 128 | style=insert_or_delete_line_style, 129 | ), 130 | style=insert_or_delete_text_style, 131 | ) 132 | 133 | 134 | @app.command('tasks', short_help='Show all Tasks :open_book:') 135 | @app.command(short_help='[s]Show all Tasks :open_book:[/]', deprecated=True) 136 | def showtasks() -> None: 137 | """Show all Tasks :open_book:""" 138 | task_table = Table( 139 | header_style=table_header_style, 140 | style=table_header_style, 141 | box=box.SIMPLE_HEAVY, 142 | ) 143 | 144 | task_table.add_column('ID', justify='center') 145 | task_table.add_column('TASK') 146 | task_table.add_column('STATUS', justify='center') 147 | 148 | for index, task in enumerate(Settings().get_tasks()): 149 | if task['done']: 150 | task_name = f'[{task_done_style}][s]{task["name"]}[/][/]' 151 | task_status = '[#bbf2b3]✓[/]' 152 | task_id = f'[{task_done_style}][s]{str(index + 1)}[/][/]' 153 | else: 154 | task_name = f'[{task_pending_style}]{task["name"]}[/]' 155 | task_status = f'[{task_pending_style}]○[/]' 156 | task_id = f'[{task_pending_style}]{str(index + 1)}[/]' 157 | 158 | task_table.add_row(task_id, task_name, task_status) 159 | center_print(task_table) 160 | 161 | if Settings().all_tasks_done(): 162 | print_no_pending_tasks() 163 | 164 | print_tasks_progress() 165 | 166 | 167 | def print_tasks(force_print: bool = False) -> None: 168 | center_print(' ') 169 | if not Settings().all_tasks_done() or force_print: 170 | showtasks() 171 | else: 172 | print_no_pending_tasks() 173 | print_tasks_progress() 174 | 175 | 176 | @app.command() 177 | def add(task: str) -> None: 178 | """ 179 | [bold green]Add[/bold green] a Task :sparkles: 180 | [light_slate_grey italic](Add task name inside quotes)[/] 181 | """ 182 | new_task = {'name': task, 'done': False} 183 | settings = Settings().get_settings() 184 | settings['tasks'].append(new_task) 185 | Settings().write_settings(settings) 186 | center_print( 187 | Rule(f'Added "{task}" to the list', style=insert_or_delete_line_style), 188 | style=insert_or_delete_text_style, 189 | ) 190 | print_tasks() 191 | 192 | 193 | @app.command() 194 | def done(taks_id: int) -> None: 195 | """Mark a task as [#bbf2b3]done ✓[/]""" 196 | task_id = taks_id - 1 197 | settings = Settings().get_settings() 198 | if not settings['tasks']: 199 | center_print( 200 | Rule( 201 | 'Sorry, There are no tasks to mark as done', 202 | style=error_line_style, 203 | ), 204 | style=error_text_style, 205 | ) 206 | return 207 | 208 | if not 0 <= task_id < len(settings['tasks']): 209 | center_print( 210 | Rule( 211 | 'Are you sure you gave me the correct ID to mark as done?', 212 | style=error_line_style, 213 | ), 214 | style=error_text_style, 215 | ) 216 | return 217 | 218 | if settings['tasks'][task_id]['done']: 219 | center_print( 220 | Rule( 221 | 'No Updates Made, Task Already Done', style=warning_line_style 222 | ), 223 | style=warning_text_style, 224 | ) 225 | print_tasks() 226 | return 227 | 228 | if Settings().all_tasks_done(): 229 | center_print( 230 | Rule('All tasks are already completed!', style=update_line_style), 231 | style=update_text_style, 232 | ) 233 | return 234 | 235 | settings['tasks'][task_id]['done'] = True 236 | Settings().write_settings(settings) 237 | center_print( 238 | Rule('Updated Task List', style=update_line_style), 239 | style=update_text_style, 240 | ) 241 | print_tasks() 242 | 243 | 244 | @app.command(short_help=f'Mark a task as [{task_pending_style}]undone ○[/]') 245 | def undone(task_id: int) -> None: 246 | task_id = task_id - 1 247 | settings = Settings().get_settings() 248 | if not settings['tasks']: 249 | center_print( 250 | Rule( 251 | 'Sorry, There are no tasks to mark as undone', 252 | style=error_line_style, 253 | ), 254 | style=error_text_style, 255 | ) 256 | return 257 | 258 | if not 0 <= task_id < len(settings['tasks']): 259 | center_print( 260 | Rule( 261 | 'Are you sure you gave me the correct ID to mark as undone?', 262 | style=error_line_style, 263 | ), 264 | style=error_text_style, 265 | ) 266 | return 267 | 268 | if not settings['tasks'][task_id]['done']: 269 | center_print( 270 | Rule( 271 | 'No Updates Made, Task Still Pending', style=warning_line_style 272 | ), 273 | style=warning_text_style, 274 | ) 275 | print_tasks() 276 | return 277 | 278 | settings['tasks'][task_id]['done'] = False 279 | Settings().write_settings(settings) 280 | center_print( 281 | Rule('Updated Task List', style=update_text_style), 282 | style=update_text_style, 283 | ) 284 | print_tasks() 285 | 286 | 287 | @app.command('del', short_help='[bright_red]Delete[/] a Task') 288 | @app.command(short_help='[s]Delete a Task[/s]', deprecated=True) 289 | def delete(task_id: int) -> None: 290 | """[bright_red]Delete[/] a Task""" 291 | task_id = task_id - 1 292 | settings = Settings().get_settings() 293 | if not settings['tasks']: 294 | center_print( 295 | Rule( 296 | 'Sorry, There are no tasks left to delete', 297 | style=error_line_style, 298 | ), 299 | style=error_text_style, 300 | ) 301 | return 302 | 303 | if not 0 <= task_id < len(settings['tasks']): 304 | center_print( 305 | Rule( 306 | 'Are you sure you gave me the correct ID to delete?', 307 | style=error_line_style, 308 | ), 309 | style=error_text_style, 310 | ) 311 | return 312 | 313 | deleted_task = settings['tasks'][task_id] 314 | del settings['tasks'][task_id] 315 | Settings().write_settings(settings) 316 | center_print( 317 | Rule( 318 | f'Deleted "{deleted_task["name"]}"', 319 | style=insert_or_delete_line_style, 320 | ), 321 | style=insert_or_delete_text_style, 322 | ) 323 | print_tasks(True) 324 | 325 | 326 | @app.command() 327 | def move(old_id: int, new_id: int) -> None: 328 | """Change task position by floating 🎈 or sinking ⚓""" 329 | settings = Settings().get_settings() 330 | if not settings['tasks']: 331 | center_print( 332 | Rule( 333 | 'Sorry, cannot move task as the Task list is empty', 334 | style=error_line_style, 335 | ), 336 | style=error_text_style, 337 | ) 338 | return 339 | 340 | if old_id == new_id: 341 | center_print( 342 | Rule('No Updates Made', style=warning_line_style), 343 | style=warning_text_style, 344 | ) 345 | return 346 | 347 | try: 348 | _ = (not 0 <= old_id - 1 < len(settings['tasks'])) or ( 349 | not 0 <= new_id - 1 < len(settings['tasks']) 350 | ) 351 | except IndexError: 352 | center_print( 353 | Rule( 354 | 'Are you sure you gave me the correct ID to move?', 355 | style=error_line_style, 356 | ), 357 | style=error_text_style, 358 | wrap=True, 359 | ) 360 | return 361 | 362 | try: 363 | if len(settings['tasks']) == 2 and ( 364 | old_id - 1 == len(settings['tasks']) 365 | or new_id - 1 == len(settings['tasks']) 366 | ): 367 | settings['tasks'][old_id - 1], settings['tasks'][new_id - 1] = ( 368 | settings['tasks'][new_id - 1], 369 | settings['tasks'][old_id - 1], 370 | ) 371 | elif old_id < new_id: 372 | for x in range(new_id - 1, old_id - 1, -1): 373 | settings['tasks'][old_id - 1], settings['tasks'][x] = ( 374 | settings['tasks'][x], 375 | settings['tasks'][old_id - 1], 376 | ) 377 | else: 378 | for x in range(new_id - 1, old_id): 379 | settings['tasks'][old_id - 1], settings['tasks'][x] = ( 380 | settings['tasks'][x], 381 | settings['tasks'][old_id - 1], 382 | ) 383 | 384 | Settings().write_settings(settings) 385 | center_print( 386 | Rule('Updated Task List', style=update_line_style), 387 | style=update_text_style, 388 | ) 389 | print_tasks(settings['tasks']) 390 | except Exception: 391 | center_print( 392 | Rule( 393 | "Please check the entered ID's values", style=error_line_style 394 | ), 395 | style=error_text_style, 396 | ) 397 | print_tasks() 398 | 399 | 400 | @app.command() 401 | def swap(old_id: int, new_id: int) -> None: 402 | """Swap the positions of two tasks 🔀""" 403 | settings = Settings().get_settings() 404 | if not settings['tasks']: 405 | center_print( 406 | Rule( 407 | 'Sorry, cannot swap tasks as the Task list is empty', 408 | style=error_line_style, 409 | ), 410 | style=error_text_style, 411 | ) 412 | return 413 | 414 | if old_id == new_id: 415 | center_print( 416 | Rule('No Updates Made', style=warning_line_style), 417 | style=warning_text_style, 418 | ) 419 | return 420 | 421 | if (not 0 <= old_id - 1 < len(settings['tasks'])) or ( 422 | not 0 <= new_id - 1 < len(settings['tasks']) 423 | ): 424 | center_print( 425 | Rule( 426 | 'Are you sure you gave me the correct ID to swap?', 427 | style=error_line_style, 428 | ), 429 | style=error_text_style, 430 | wrap=True, 431 | ) 432 | return 433 | 434 | try: 435 | settings['tasks'][old_id - 1], settings['tasks'][new_id - 1] = ( 436 | settings['tasks'][new_id - 1], 437 | settings['tasks'][old_id - 1], 438 | ) 439 | Settings().write_settings(settings) 440 | center_print( 441 | Rule('Updated Task List', style=update_line_style), 442 | style=update_text_style, 443 | ) 444 | print_tasks(settings['tasks']) 445 | except Exception: 446 | center_print( 447 | Rule( 448 | "Please check the entered ID's values", style=error_line_style 449 | ), 450 | style=error_text_style, 451 | ) 452 | print_tasks() 453 | 454 | 455 | @app.command() 456 | def clear() -> None: 457 | """Clear all tasks :wastebasket:""" 458 | typer.confirm('Are you sure you want to delete all tasks?', abort=True) 459 | settings = Settings().get_settings() 460 | settings['tasks'] = [] 461 | Settings().write_settings(settings) 462 | center_print( 463 | Rule('Task List Deleted', style=update_line_style), 464 | style=update_text_style, 465 | ) 466 | 467 | 468 | @app.command() 469 | def clean() -> None: 470 | """Clean up tasks marked as done :broom:""" 471 | typer.confirm('Are you sure you want to delete all done tasks?', abort=True) 472 | settings = Settings().get_settings() 473 | settings['tasks'] = Settings().get_all_tasks_undone() 474 | Settings().write_settings(settings) 475 | center_print( 476 | Rule('Done Tasks Deleted', style=update_line_style), 477 | style=update_text_style, 478 | ) 479 | 480 | 481 | @app.command(rich_help_panel='Integration') 482 | def count_done() -> None: 483 | """Count done tasks :chart_increasing:""" 484 | typer.echo(Settings().count_tasks_done()) 485 | 486 | 487 | @app.command(rich_help_panel='Integration') 488 | def count_undone() -> None: 489 | """Count undone tasks :chart_decreasing:""" 490 | typer.echo(Settings().count_tasks_undone()) 491 | 492 | 493 | @app.command(rich_help_panel='Utils and Configs') 494 | def callme(name: str) -> None: 495 | """ 496 | Change name :name_badge: [light_slate_grey italic] 497 | (without resetting data)[/] 498 | """ 499 | settings = Settings().get_settings() 500 | settings['user_name'] = name 501 | Settings().write_settings(settings) 502 | center_print( 503 | Rule( 504 | 'Thanks for letting me know your name!', 505 | style=insert_or_delete_line_style, 506 | ), 507 | style=insert_or_delete_text_style, 508 | ) 509 | 510 | 511 | @app.command(rich_help_panel='Utils and Configs') 512 | def setup() -> None: 513 | """Reset all data and run setup :wrench:""" 514 | settings: dict = {} 515 | settings['user_name'] = typer.prompt( 516 | typer.style('Hello! What can I call you?', fg=typer.colors.CYAN) 517 | ) 518 | 519 | show_tasks_progress = typer.prompt( 520 | typer.style( 521 | 'Do you want show tasks progress? (Y/n)', fg=typer.colors.CYAN 522 | ) 523 | ) 524 | 525 | show_quotes = typer.prompt( 526 | typer.style('Do you want show quotes? (Y/n)', fg=typer.colors.CYAN) 527 | ) 528 | 529 | code_markdown = Markdown( 530 | """ 531 | pls callme 532 | """ 533 | ) 534 | 535 | center_print( 536 | 'If you wanna change your name later, please use:', style='red' 537 | ) 538 | console.print(code_markdown) 539 | 540 | code_markdown = Markdown( 541 | """ 542 | pls tasks-progress <--show or --no-show> 543 | """ 544 | ) 545 | center_print( 546 | ( 547 | 'If you need to disable or enable the task progress bar later, ' 548 | 'please use:' 549 | ), 550 | style='red', 551 | ) 552 | console.print(code_markdown) 553 | 554 | code_markdown = Markdown( 555 | """ 556 | pls quotes <--show or --no-show> 557 | """ 558 | ) 559 | center_print( 560 | 'If you need to disable or enable quotes later, please use:', 561 | style='red', 562 | ) 563 | console.print(code_markdown) 564 | 565 | center_print( 566 | 'To apply the changes restart the terminal or use this command:', 567 | style='red', 568 | ) 569 | code_markdown = Markdown( 570 | """ 571 | pls 572 | """ 573 | ) 574 | console.print(code_markdown) 575 | 576 | settings['initial_setup_done'] = True 577 | if show_tasks_progress in ('n', 'N'): 578 | settings['show_task_progress'] = False 579 | else: 580 | settings['show_task_progress'] = True 581 | 582 | if show_quotes in ('n', 'N'): 583 | settings['show_quotes'] = False 584 | else: 585 | settings['show_quotes'] = True 586 | 587 | settings['tasks'] = [] 588 | Settings().write_settings(settings) 589 | 590 | 591 | @app.callback( 592 | invoke_without_command=True, 593 | epilog=( 594 | 'Made with [red]:heart:[/red] by ' 595 | '[link=https://github.com/guedesfelipe/pls-cli]Felipe Guedes[/link]' 596 | ), 597 | ) 598 | def show(ctx: typer.Context) -> None: 599 | """ 600 | 💻 [bold]PLS-CLI[/] 601 | 602 | ・[i]Minimalist and full configurable greetings and TODO list[/]・ 603 | """ 604 | try: 605 | if ctx.invoked_subcommand is None: 606 | if Settings().exists_settings(): 607 | date_now = datetime.datetime.now() 608 | user_name = Settings().get_name() 609 | time_str = date_now.strftime('%d %b | %I:%M %p') 610 | header_greetings = ( 611 | f'[{header_greetings_style}] Hello {user_name}! ' 612 | f"It's {time_str}[/]" 613 | ) 614 | center_print( 615 | Rule(header_greetings, style=header_greetings_style) 616 | ) 617 | quote = get_rand_quote() 618 | if Settings().show_quotes(): 619 | center_print( 620 | f'[{quote_style}]"{quote["content"]}"[/]', wrap=True 621 | ) 622 | center_print( 623 | f'[{author_style}][i]・{quote["author"]}・[/i][/]', 624 | wrap=True, 625 | ) 626 | print_tasks() 627 | else: 628 | setup() 629 | except json.JSONDecodeError: 630 | console.print_exception(show_locals=True) 631 | center_print( 632 | Rule('Failed while loading configuration', style=error_line_style), 633 | style=error_text_style, 634 | ) 635 | 636 | 637 | @app.command(rich_help_panel='Utils and Configs') 638 | def version(): 639 | """Show version :bookmark:""" 640 | typer.echo(f'pls CLI Version: {__version__}') 641 | raise typer.Exit() 642 | 643 | 644 | @app.command(rich_help_panel='Utils and Configs') 645 | def docs(): 646 | """Launch docs Website :globe_with_meridians:""" 647 | center_print(Rule('・Opening [#FFBF00]PLS-CLI[/] docs・', style='#d77dd8')) 648 | typer.launch('https://guedesfelipe.github.io/pls-cli/') 649 | 650 | 651 | @app.command(rich_help_panel='Utils and Configs') 652 | def config(): 653 | """Show config directory path :open_file_folder:""" 654 | config_path = Settings().get_full_settings_path() 655 | center_print(Rule('・Config directory・', style='#d77dd8')) 656 | console.print(f'\n[#61E294]Path:[/] [bold]{config_path}[/]\n') 657 | 658 | 659 | @app.command() 660 | def edit(task_id: int, task: str): 661 | """ 662 | [bold yellow]Edit[/bold yellow] a task by id ✏️ [light_slate_grey italic] 663 | (Add task name inside quotes)[/] 664 | """ 665 | settings = Settings().get_settings() 666 | tasks = settings['tasks'] 667 | 668 | # check if task list is empty 669 | if not tasks: 670 | center_print( 671 | f'[{warning_text_style}]Currently, you have no tasks to edit[/] 📝' 672 | ) 673 | print_tasks_progress() 674 | raise typer.Exit() 675 | 676 | print_tasks() 677 | 678 | # check if task exists 679 | if task_id and task_id <= len(tasks): 680 | old_task = tasks[task_id - 1]['name'] 681 | tasks[task_id - 1]['name'] = task 682 | else: 683 | center_print( 684 | f'\nTask #{task_id} was not found, pls choose an existing ID\n', 685 | style=error_text_style, 686 | ) 687 | raise typer.Exit() 688 | 689 | # confirm edit task 690 | center_print( 691 | f'\nOld Task: {old_task}\nEdited Task: {task}\n', 692 | style=insert_or_delete_text_style, 693 | ) 694 | if not typer.confirm( 695 | f'Are you sure you want to edit Task #{task_id}?', show_default=True 696 | ): 697 | typer.clear() 698 | print_tasks() 699 | raise typer.Exit() 700 | 701 | Settings().write_settings(settings) 702 | typer.clear() 703 | print_tasks() 704 | -------------------------------------------------------------------------------- /tests/test_pls_cli.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from freezegun import freeze_time 4 | from typer.testing import CliRunner 5 | 6 | from pls_cli import __version__ 7 | from pls_cli.please import app 8 | 9 | try: 10 | from importlib.metadata import version # Python 3.8+ 11 | except ImportError: 12 | from importlib_metadata import version # type: ignore[no-redef] 13 | 14 | 15 | runner = CliRunner() 16 | 17 | 18 | def test_version(): 19 | assert __version__ == version('pls-cli') 20 | 21 | 22 | def test_error_invalid_command(): 23 | result = runner.invoke(app, ['test']) 24 | assert result.exit_code == 2 25 | assert "No such command 'test'" in result.output 26 | 27 | 28 | def test_help(): 29 | result = runner.invoke(app, ['--help']) 30 | assert result.exit_code == 0 31 | assert 'Show this message and exit' in result.stdout 32 | 33 | 34 | def test_version_command(): 35 | result = runner.invoke(app, ['version']) 36 | assert result.exit_code == 0 37 | assert result.stdout == f'pls CLI Version: {__version__}\n' 38 | 39 | 40 | @patch('pls_cli.utils.settings.Settings.exists_settings', return_value=False) 41 | @patch('pls_cli.utils.settings.Settings.write_settings') 42 | def test_first_usage(mock_write_settings, mock_exists_settings): 43 | result = runner.invoke(app, input='test\ny\nY\n') 44 | assert result.exit_code == 0 45 | assert 'Hello! What can I call you?: test' in result.stdout 46 | assert 'pls callme ' in result.stdout 47 | assert 'If you wanna change your name later, please use:' in result.stdout 48 | assert ( 49 | 'To apply the changes restart the terminal or use this command:' 50 | in result.stdout 51 | ) 52 | assert ( 53 | 'If you need to disable or enable the task progress bar later, ' 54 | 'please use:' 55 | ) in result.stdout 56 | assert ( 57 | 'If you need to disable or enable quotes later, please use:' 58 | in result.stdout 59 | ) 60 | 61 | 62 | @freeze_time('2022-01-14 03:21:34') 63 | @patch('pls_cli.utils.settings.Settings.exists_settings', return_value=True) 64 | @patch('pls_cli.utils.settings.Settings.get_name', return_value='Test name') 65 | @patch('pls_cli.utils.settings.Settings.all_tasks_done', return_value=False) 66 | @patch( 67 | 'pls_cli.utils.settings.Settings.get_tasks', 68 | return_value=[{'name': 'Task 1', 'done': False}], 69 | ) 70 | def test_config_ok_show_tasks( 71 | mock_get_tasks, mock_all_tasks_done, mock_get_name, mock_exists_settings 72 | ): 73 | result = runner.invoke(app) 74 | assert result.exit_code == 0 75 | assert "Hello Test name! It's 14 Jan | 03:21 AM" in result.stdout 76 | assert 'ID TASK STATUS' in result.stdout 77 | assert '1 Task 1 ○' in result.stdout 78 | 79 | 80 | @freeze_time('2022-01-14 03:21:34') 81 | @patch('pls_cli.utils.settings.Settings.exists_settings', return_value=True) 82 | @patch('pls_cli.utils.settings.Settings.get_name', return_value='Test name') 83 | @patch('pls_cli.utils.settings.Settings.all_tasks_done', return_value=True) 84 | @patch( 85 | 'pls_cli.utils.settings.Settings.get_tasks', 86 | return_value=[{'name': 'Task 1', 'done': False}], 87 | ) 88 | def test_config_ok_no_tasks_pending( 89 | mock_get_tasks, mock_all_tasks_done, mock_get_name, mock_exists_settings 90 | ): 91 | result = runner.invoke(app) 92 | assert result.exit_code == 0 93 | assert "Hello Test name! It's 14 Jan | 03:21 AM" in result.stdout 94 | assert 'Looking good, no pending tasks ✨ 🍰 ✨' in result.stdout 95 | 96 | 97 | @patch('pls_cli.utils.settings.Settings.exists_settings', return_value=True) 98 | @patch( 99 | 'pls_cli.utils.settings.Settings.get_tasks', 100 | return_value=[{'name': 'Task 1', 'done': True}], 101 | ) 102 | def test_config_ok_no_tasks_pending_with_progress( 103 | mock_get_tasks, mock_exists_settings 104 | ): 105 | result = runner.invoke(app) 106 | assert result.exit_code == 0 107 | 108 | 109 | @patch( 110 | 'pls_cli.utils.settings.Settings.get_settings', 111 | return_value={ 112 | 'user_name': 'Test name', 113 | 'initial_setup_done': True, 114 | 'tasks': [{'name': 'Task 1', 'done': False}], 115 | }, 116 | ) 117 | @patch('pls_cli.utils.settings.Settings.write_settings') 118 | @patch('pls_cli.utils.settings.Settings.all_tasks_done', return_value=False) 119 | @patch( 120 | 'pls_cli.utils.settings.Settings.get_tasks', 121 | return_value=[ 122 | {'name': 'Task 1', 'done': True}, 123 | {'name': 'New task', 'done': False}, 124 | ], 125 | ) 126 | def test_add_task( 127 | mock_get_tasks, mock_all_tasks_done, mock_write_settings, mock_get_settings 128 | ): 129 | result = runner.invoke(app, ['add', 'New task']) 130 | assert result.exit_code == 0 131 | assert 'Added "New task" to the list' in result.stdout 132 | assert 'ID TASK STATUS' in result.stdout 133 | assert '1 Task 1 ✓' in result.stdout 134 | assert '2 New task ○' in result.stdout 135 | 136 | 137 | @patch( 138 | 'pls_cli.utils.settings.Settings.get_settings', 139 | return_value={ 140 | 'user_name': 'Test name', 141 | 'initial_setup_done': True, 142 | 'tasks': [], 143 | }, 144 | ) 145 | @patch('pls_cli.utils.settings.Settings.write_settings') 146 | def test_edit_task_with_invalid_id(mock_write_settings, mock_get_settings): 147 | result = runner.invoke(app, ['edit', '1', 'Edited']) 148 | assert 'Currently, you have no tasks' in result.stdout 149 | assert result.exit_code == 0 150 | 151 | 152 | @patch( 153 | 'pls_cli.utils.settings.Settings.get_settings', 154 | return_value={ 155 | 'user_name': 'Test name', 156 | 'initial_setup_done': True, 157 | 'tasks': [{'name': 'Task 1', 'done': False}], 158 | }, 159 | ) 160 | @patch('pls_cli.utils.settings.Settings.write_settings') 161 | def test_edit_not_found_task(mock_write_settings, mock_get_settings): 162 | result = runner.invoke(app, ['edit', '2', 'Task 2 edited']) 163 | assert result.exit_code == 0 164 | assert 'Task #2 was not found, pls choose an existing ID' in result.stdout 165 | 166 | 167 | @patch( 168 | 'pls_cli.utils.settings.Settings.get_settings', 169 | return_value={ 170 | 'user_name': 'Test name', 171 | 'initial_setup_done': True, 172 | 'tasks': [ 173 | {'name': 'Task 1', 'done': False}, 174 | {'name': 'Old task text', 'done': False}, 175 | ], 176 | }, 177 | ) 178 | @patch('pls_cli.utils.settings.Settings.write_settings') 179 | def test_edit_task_success(mock_write_settings, mock_get_settings): 180 | result = runner.invoke(app, ['edit', '2', 'New task text'], input='y') 181 | output = result.stdout 182 | assert result.exit_code == 0 183 | assert 'Old Task: Old task text' in output 184 | assert 'Edited Task: New task text' in output 185 | assert 'Are you sure you want to edit Task #2? [y/N]: y' in output 186 | assert '1 Task 1 ○' in output 187 | assert '2 New task text ○' in output 188 | 189 | 190 | @patch( 191 | 'pls_cli.utils.settings.Settings.get_settings', 192 | return_value={ 193 | 'user_name': 'Test name', 194 | 'initial_setup_done': True, 195 | 'tasks': [ 196 | {'name': 'Task 1', 'done': False}, 197 | {'name': 'Task 2', 'done': False}, 198 | ], 199 | }, 200 | ) 201 | @patch('pls_cli.utils.settings.Settings.write_settings') 202 | def test_edit_task_aborted(mock_write_settings, mock_get_settings): 203 | result = runner.invoke(app, ['edit', '2', 'Task 2 edited'], input='N\n') 204 | output = result.stdout 205 | assert result.exit_code == 0 206 | assert 'Old Task: Task 2' in output 207 | assert 'Edited Task: Task 2 edited' in output 208 | assert 'Are you sure you want to edit Task #2? [y/N]: N' in output 209 | assert '1 Task 1 ○' in output 210 | assert '2 Task 2 edited ○' in output 211 | 212 | 213 | @patch( 214 | 'pls_cli.utils.settings.Settings.get_settings', 215 | return_value={ 216 | 'user_name': 'Test name', 217 | 'initial_setup_done': True, 218 | 'tasks': [], 219 | }, 220 | ) 221 | @patch('pls_cli.utils.settings.Settings.write_settings') 222 | def test_edit_empty_tasks(mock_write_settings, mock_get_settings): 223 | result = runner.invoke(app, ['edit', '1', 'Task 1 edited']) 224 | output = result.stdout 225 | assert result.exit_code == 0 226 | assert 'Currently, you have no tasks to edit 📝' in output 227 | 228 | 229 | @patch( 230 | 'pls_cli.utils.settings.Settings.get_settings', 231 | return_value={ 232 | 'user_name': 'Test name', 233 | 'initial_setup_done': True, 234 | 'tasks': [ 235 | {'name': 'Task 1', 'done': False}, 236 | {'name': 'Task 2', 'done': False}, 237 | {'name': 'Task 3', 'done': False}, 238 | {'name': 'Task 4', 'done': False}, 239 | ], 240 | }, 241 | ) 242 | @patch('pls_cli.utils.settings.Settings.write_settings') 243 | def test_move_task_success(mock_write_settings, mock_get_settings): 244 | result = runner.invoke(app, ['move', '1', '3']) 245 | output = result.stdout 246 | assert result.exit_code == 0 247 | assert 'Updated Task List' in output 248 | single_spaces = ' '.join(output.split()) 249 | assert '1 Task 2 ○' in single_spaces 250 | assert '2 Task 3 ○' in single_spaces 251 | assert '3 Task 1 ○' in single_spaces 252 | assert '4 Task 4 ○' in single_spaces 253 | 254 | result = runner.invoke(app, ['move', '4', '2']) 255 | output = result.stdout 256 | assert result.exit_code == 0 257 | assert 'Updated Task List' in output 258 | single_spaces = ' '.join(output.split()) 259 | assert '1 Task 2 ○' in single_spaces 260 | assert '2 Task 4 ○' in single_spaces 261 | assert '3 Task 3 ○' in single_spaces 262 | assert '4 Task 1 ○' in single_spaces 263 | 264 | 265 | @patch( 266 | 'pls_cli.utils.settings.Settings.get_settings', 267 | return_value={ 268 | 'user_name': 'Test name', 269 | 'initial_setup_done': True, 270 | 'tasks': [ 271 | {'name': 'Task 1', 'done': False}, 272 | {'name': 'Task 2', 'done': False}, 273 | ], 274 | }, 275 | ) 276 | @patch('pls_cli.utils.settings.Settings.write_settings') 277 | def test_done_command_success(mock_write_settings, mock_get_settings): 278 | result = runner.invoke(app, ['done', '1']) 279 | assert result.exit_code == 0 280 | assert 'Updated Task List' in result.stdout 281 | 282 | 283 | @patch( 284 | 'pls_cli.utils.settings.Settings.get_settings', 285 | return_value={ 286 | 'user_name': 'Test name', 287 | 'initial_setup_done': True, 288 | 'tasks': [], 289 | }, 290 | ) 291 | def test_done_command_empty_tasks(mock_get_settings): 292 | result = runner.invoke(app, ['done', '1']) 293 | assert result.exit_code == 0 294 | assert 'Sorry, There are no tasks to mark as done' in result.stdout 295 | 296 | 297 | @patch( 298 | 'pls_cli.utils.settings.Settings.get_settings', 299 | return_value={ 300 | 'user_name': 'Test name', 301 | 'initial_setup_done': True, 302 | 'tasks': [{'name': 'Task 1', 'done': False}], 303 | }, 304 | ) 305 | def test_done_command_invalid_id(mock_get_settings): 306 | result = runner.invoke(app, ['done', '5']) 307 | assert result.exit_code == 0 308 | assert ( 309 | 'Are you sure you gave me the correct ID to mark as done?' 310 | in result.stdout 311 | ) 312 | 313 | 314 | @patch( 315 | 'pls_cli.utils.settings.Settings.get_settings', 316 | return_value={ 317 | 'user_name': 'Test name', 318 | 'initial_setup_done': True, 319 | 'tasks': [{'name': 'Task 1', 'done': True}], 320 | }, 321 | ) 322 | def test_done_command_already_done(mock_get_settings): 323 | result = runner.invoke(app, ['done', '1']) 324 | assert result.exit_code == 0 325 | assert 'No Updates Made, Task Already Done' in result.stdout 326 | 327 | 328 | @patch( 329 | 'pls_cli.utils.settings.Settings.get_settings', 330 | return_value={ 331 | 'user_name': 'Test name', 332 | 'initial_setup_done': True, 333 | 'tasks': [ 334 | {'name': 'Task 1', 'done': True}, 335 | {'name': 'Task 2', 'done': False}, 336 | ], 337 | }, 338 | ) 339 | @patch('pls_cli.utils.settings.Settings.write_settings') 340 | def test_undone_command_success(mock_write_settings, mock_get_settings): 341 | result = runner.invoke(app, ['undone', '1']) 342 | assert result.exit_code == 0 343 | assert 'Updated Task List' in result.stdout 344 | 345 | 346 | @patch( 347 | 'pls_cli.utils.settings.Settings.get_settings', 348 | return_value={ 349 | 'user_name': 'Test name', 350 | 'initial_setup_done': True, 351 | 'tasks': [], 352 | }, 353 | ) 354 | def test_undone_command_empty_tasks(mock_get_settings): 355 | result = runner.invoke(app, ['undone', '1']) 356 | assert result.exit_code == 0 357 | assert 'Sorry, There are no tasks to mark as undone' in result.stdout 358 | 359 | 360 | @patch( 361 | 'pls_cli.utils.settings.Settings.get_settings', 362 | return_value={ 363 | 'user_name': 'Test name', 364 | 'initial_setup_done': True, 365 | 'tasks': [{'name': 'Task 1', 'done': False}], 366 | }, 367 | ) 368 | def test_undone_command_already_undone(mock_get_settings): 369 | result = runner.invoke(app, ['undone', '1']) 370 | assert result.exit_code == 0 371 | assert 'No Updates Made, Task Still Pending' in result.stdout 372 | 373 | 374 | @patch( 375 | 'pls_cli.utils.settings.Settings.get_settings', 376 | return_value={ 377 | 'user_name': 'Test name', 378 | 'initial_setup_done': True, 379 | 'tasks': [ 380 | {'name': 'Task 1', 'done': False}, 381 | {'name': 'Task 2', 'done': False}, 382 | ], 383 | }, 384 | ) 385 | @patch('pls_cli.utils.settings.Settings.write_settings') 386 | def test_delete_command_success(mock_write_settings, mock_get_settings): 387 | result = runner.invoke(app, ['del', '1']) 388 | assert result.exit_code == 0 389 | assert 'Deleted "Task 1"' in result.stdout 390 | 391 | 392 | @patch( 393 | 'pls_cli.utils.settings.Settings.get_settings', 394 | return_value={ 395 | 'user_name': 'Test name', 396 | 'initial_setup_done': True, 397 | 'tasks': [], 398 | }, 399 | ) 400 | def test_delete_command_empty_tasks(mock_get_settings): 401 | result = runner.invoke(app, ['del', '1']) 402 | assert result.exit_code == 0 403 | assert 'Sorry, There are no tasks left to delete' in result.stdout 404 | 405 | 406 | @patch( 407 | 'pls_cli.utils.settings.Settings.get_settings', 408 | return_value={ 409 | 'user_name': 'Test name', 410 | 'initial_setup_done': True, 411 | 'tasks': [{'name': 'Task 1', 'done': False}], 412 | }, 413 | ) 414 | def test_delete_command_invalid_id(mock_get_settings): 415 | result = runner.invoke(app, ['del', '5']) 416 | assert result.exit_code == 0 417 | assert 'Are you sure you gave me the correct ID to delete?' in result.stdout 418 | 419 | 420 | @patch( 421 | 'pls_cli.utils.settings.Settings.get_settings', 422 | return_value={ 423 | 'user_name': 'Test name', 424 | 'initial_setup_done': True, 425 | 'tasks': [ 426 | {'name': 'Task 1', 'done': False}, 427 | {'name': 'Task 2', 'done': False}, 428 | ], 429 | }, 430 | ) 431 | @patch('pls_cli.utils.settings.Settings.write_settings') 432 | def test_swap_command_success(mock_write_settings, mock_get_settings): 433 | result = runner.invoke(app, ['swap', '1', '2']) 434 | assert result.exit_code == 0 435 | assert 'Updated Task List' in result.stdout 436 | 437 | 438 | @patch( 439 | 'pls_cli.utils.settings.Settings.get_settings', 440 | return_value={ 441 | 'user_name': 'Test name', 442 | 'initial_setup_done': True, 443 | 'tasks': [], 444 | }, 445 | ) 446 | def test_swap_command_empty_tasks(mock_get_settings): 447 | result = runner.invoke(app, ['swap', '1', '2']) 448 | assert result.exit_code == 0 449 | assert 'cannot swap tasks as the Task list is empty' in result.stdout 450 | 451 | 452 | @patch( 453 | 'pls_cli.utils.settings.Settings.get_settings', 454 | return_value={ 455 | 'user_name': 'Test name', 456 | 'initial_setup_done': True, 457 | 'tasks': [{'name': 'Task 1', 'done': False}], 458 | }, 459 | ) 460 | def test_swap_command_same_id(mock_get_settings): 461 | result = runner.invoke(app, ['swap', '1', '1']) 462 | assert result.exit_code == 0 463 | assert 'No Updates Made' in result.stdout 464 | 465 | 466 | @patch( 467 | 'pls_cli.utils.settings.Settings.get_settings', 468 | return_value={ 469 | 'user_name': 'Test name', 470 | 'initial_setup_done': True, 471 | 'tasks': [ 472 | {'name': 'Task 1', 'done': False}, 473 | {'name': 'Task 2', 'done': False}, 474 | ], 475 | }, 476 | ) 477 | @patch('pls_cli.utils.settings.Settings.write_settings') 478 | def test_clear_command_success(mock_write_settings, mock_get_settings): 479 | result = runner.invoke(app, ['clear'], input='y') 480 | assert result.exit_code == 0 481 | assert 'Task List Deleted' in result.stdout 482 | 483 | 484 | @patch( 485 | 'pls_cli.utils.settings.Settings.get_settings', 486 | return_value={ 487 | 'user_name': 'Test name', 488 | 'initial_setup_done': True, 489 | 'tasks': [ 490 | {'name': 'Task 1', 'done': True}, 491 | {'name': 'Task 2', 'done': False}, 492 | ], 493 | }, 494 | ) 495 | @patch('pls_cli.utils.settings.Settings.get_all_tasks_undone') 496 | @patch('pls_cli.utils.settings.Settings.write_settings') 497 | def test_clean_command_success( 498 | mock_write_settings, mock_get_all_tasks_undone, mock_get_settings 499 | ): 500 | mock_get_all_tasks_undone.return_value = [{'name': 'Task 2', 'done': False}] 501 | result = runner.invoke(app, ['clean'], input='y') 502 | assert result.exit_code == 0 503 | assert 'Done Tasks Deleted' in result.stdout 504 | 505 | 506 | @patch( 507 | 'pls_cli.utils.settings.Settings.get_settings', 508 | return_value={ 509 | 'user_name': 'Test name', 510 | 'initial_setup_done': True, 511 | 'tasks': [ 512 | {'name': 'Task 1', 'done': True}, 513 | {'name': 'Task 2', 'done': False}, 514 | ], 515 | }, 516 | ) 517 | @patch('pls_cli.utils.settings.Settings.count_tasks_done', return_value=1) 518 | def test_count_done_command(mock_count_tasks_done, mock_get_settings): 519 | result = runner.invoke(app, ['count-done']) 520 | assert result.exit_code == 0 521 | assert '1' in result.stdout 522 | 523 | 524 | @patch( 525 | 'pls_cli.utils.settings.Settings.get_settings', 526 | return_value={ 527 | 'user_name': 'Test name', 528 | 'initial_setup_done': True, 529 | 'tasks': [ 530 | {'name': 'Task 1', 'done': True}, 531 | {'name': 'Task 2', 'done': False}, 532 | ], 533 | }, 534 | ) 535 | @patch('pls_cli.utils.settings.Settings.count_tasks_undone', return_value=1) 536 | def test_count_undone_command(mock_count_tasks_undone, mock_get_settings): 537 | result = runner.invoke(app, ['count-undone']) 538 | assert result.exit_code == 0 539 | assert '1' in result.stdout 540 | 541 | 542 | @patch( 543 | 'pls_cli.utils.settings.Settings.get_settings', 544 | return_value={ 545 | 'user_name': 'Old name', 546 | 'initial_setup_done': True, 547 | 'tasks': [], 548 | }, 549 | ) 550 | @patch('pls_cli.utils.settings.Settings.write_settings') 551 | def test_callme_command(mock_write_settings, mock_get_settings): 552 | result = runner.invoke(app, ['callme', 'New name']) 553 | assert result.exit_code == 0 554 | assert 'Thanks for letting me know your name!' in result.stdout 555 | 556 | 557 | def test_tasks_command(): 558 | result = runner.invoke(app, ['tasks', '--help']) 559 | assert result.exit_code == 0 560 | 561 | 562 | @patch( 563 | 'pls_cli.utils.settings.Settings.get_settings', 564 | return_value={'show_task_progress': True}, 565 | ) 566 | @patch('pls_cli.utils.settings.Settings.write_settings') 567 | def test_tasks_progress_command(mock_write_settings, mock_get_settings): 568 | result = runner.invoke(app, ['tasks-progress', '--no-show']) 569 | assert result.exit_code == 0 570 | assert 'Thanks for letting me know that!' in result.stdout 571 | 572 | 573 | @patch( 574 | 'pls_cli.utils.settings.Settings.get_settings', 575 | return_value={'show_quotes': True}, 576 | ) 577 | @patch('pls_cli.utils.settings.Settings.write_settings') 578 | def test_quotes_command(mock_write_settings, mock_get_settings): 579 | result = runner.invoke(app, ['quotes', '--no-show']) 580 | assert result.exit_code == 0 581 | assert 'Thanks for letting me know that!' in result.stdout 582 | 583 | 584 | @patch('typer.launch') 585 | def test_docs_command(mock_launch): 586 | result = runner.invoke(app, ['docs']) 587 | assert result.exit_code == 0 588 | assert 'Opening' in result.stdout 589 | assert 'PLS-CLI' in result.stdout 590 | assert 'docs' in result.stdout 591 | mock_launch.assert_called_once_with( 592 | 'https://guedesfelipe.github.io/pls-cli/' 593 | ) 594 | 595 | 596 | def test_config_command(): 597 | result = runner.invoke(app, ['config']) 598 | assert result.exit_code == 0 599 | assert 'Config directory' in result.stdout 600 | assert 'Path:' in result.stdout 601 | 602 | 603 | @patch( 604 | 'pls_cli.utils.settings.Settings.get_settings', 605 | return_value={ 606 | 'user_name': 'Test name', 607 | 'initial_setup_done': True, 608 | 'tasks': [{'name': 'Task 1', 'done': False}], 609 | }, 610 | ) 611 | def test_undone_command_invalid_id(mock_get_settings): 612 | result = runner.invoke(app, ['undone', '5']) 613 | assert result.exit_code == 0 614 | assert ( 615 | 'Are you sure you gave me the correct ID to mark as undone?' 616 | in result.stdout 617 | ) 618 | 619 | 620 | @patch( 621 | 'pls_cli.utils.settings.Settings.get_settings', 622 | return_value={ 623 | 'user_name': 'Test name', 624 | 'initial_setup_done': True, 625 | 'tasks': [ 626 | {'name': 'Task 1', 'done': False}, 627 | {'name': 'Task 2', 'done': False}, 628 | ], 629 | }, 630 | ) 631 | def test_swap_command_invalid_id(mock_get_settings): 632 | result = runner.invoke(app, ['swap', '1', '5']) 633 | assert result.exit_code == 0 634 | assert 'Are you sure you gave me the correct ID to swap?' in result.stdout 635 | 636 | 637 | @patch( 638 | 'pls_cli.utils.settings.Settings.get_settings', 639 | return_value={ 640 | 'user_name': 'Test name', 641 | 'initial_setup_done': True, 642 | 'tasks': [{'name': 'Task 1', 'done': False}], 643 | }, 644 | ) 645 | def test_move_command_empty_tasks(mock_get_settings): 646 | mock_get_settings.return_value = { 647 | 'user_name': 'Test name', 648 | 'initial_setup_done': True, 649 | 'tasks': [], 650 | } 651 | result = runner.invoke(app, ['move', '1', '2']) 652 | assert result.exit_code == 0 653 | assert 'cannot move task as the Task list is empty' in result.stdout 654 | 655 | 656 | @patch( 657 | 'pls_cli.utils.settings.Settings.get_settings', 658 | return_value={ 659 | 'user_name': 'Test name', 660 | 'initial_setup_done': True, 661 | 'tasks': [{'name': 'Task 1', 'done': False}], 662 | }, 663 | ) 664 | def test_move_command_same_position(mock_get_settings): 665 | result = runner.invoke(app, ['move', '1', '1']) 666 | assert result.exit_code == 0 667 | assert 'No Updates Made' in result.stdout 668 | 669 | 670 | @patch( 671 | 'pls_cli.utils.settings.Settings.get_settings', 672 | return_value={ 673 | 'user_name': 'Test name', 674 | 'initial_setup_done': True, 675 | 'tasks': [{'name': 'Task 1', 'done': False}], 676 | }, 677 | ) 678 | def test_move_command_invalid_id(mock_get_settings): 679 | result = runner.invoke(app, ['move', '1', '5']) 680 | assert result.exit_code == 0 681 | assert "Please check the entered ID's values" in result.stdout 682 | 683 | 684 | @patch( 685 | 'pls_cli.utils.settings.Settings.get_settings', 686 | return_value={ 687 | 'user_name': 'Test name', 688 | 'initial_setup_done': True, 689 | 'tasks': [ 690 | {'name': 'Task 1', 'done': False}, 691 | {'name': 'Task 2', 'done': True}, 692 | ], 693 | }, 694 | ) 695 | @patch('pls_cli.utils.settings.Settings.get_tasks') 696 | def test_showtasks_command(mock_get_tasks, mock_get_settings): 697 | mock_get_tasks.return_value = [ 698 | {'name': 'Task 1', 'done': False}, 699 | {'name': 'Task 2', 'done': True}, 700 | ] 701 | result = runner.invoke(app, ['tasks']) 702 | assert result.exit_code == 0 703 | assert 'TASK' in result.stdout 704 | --------------------------------------------------------------------------------