├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------