├── .github └── FUNDING.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── converter.py ├── demo.gif ├── fortunes.txt ├── oneliner.sh ├── poke.png ├── pyproject.toml ├── setup.py ├── src ├── __init__.py ├── __main__.py ├── main.py ├── one_liners.py ├── pokemons.py ├── poketermconfig.ini └── poketermconfig.ini.default ├── tests ├── test_cli.py └── test_runs_counter.py └── welcome.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: devarshi16 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Vim swap files 7 | *.swp 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.10.2] - 2025-09-09 6 | ### Added 7 | - Track the number of times poketerm is run and remind heavy users to donate. 8 | ### Changed 9 | - Limit donation reminders to three colorful messages for users exceeding 10,000 runs. 10 | - Update sponsorship link to https://github.com/sponsors/devarshi16. 11 | 12 | ## [0.10.1] - 2025-09-09 13 | ### Added 14 | - Install poketerm into zsh startup files alongside bash equivalents. 15 | ### Fixed 16 | - Avoid creating shell startup files for shells that are not present. 17 | - Silence Powerlevel10k instant prompt warning by setting 18 | `POWERLEVEL9K_INSTANT_PROMPT=quiet` when adding to zsh configs and 19 | placing the variable at the top of zsh startup files to prevent 20 | warnings. 21 | - Skip adding a second `POWERLEVEL9K_INSTANT_PROMPT` line if one already 22 | exists in zsh startup files. 23 | 24 | ## [0.10.0] - 2025-09-08 25 | ### Changed 26 | - Fix CLI flag typo and streamline message handling. 27 | - Replace fragile shell edits with Python file I/O and use XDG config directory. 28 | - Harden startup scripts to copy `fortunes.txt` only when missing and safely quote paths. 29 | - Drop Python 2 support, add `pyproject.toml`, and require Python 3.7+. 30 | - Add basic CLI test to ensure interface stability. 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Devarshi Aggarwal 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # poketerm 2 | [![PyPI version](https://badge.fury.io/py/poketerm.svg)](https://badge.fury.io/py/poketerm) 3 | [![Downloads](https://pepy.tech/badge/poketerm)](https://pepy.tech/project/poketerm) 4 | [![Downloads](https://pepy.tech/badge/poketerm/month)](https://pepy.tech/project/poketerm/month) 5 | [![Downloads](https://pepy.tech/badge/poketerm/week)](https://pepy.tech/project/poketerm/week) 6 | 7 | Get greeted by custom message and/or 8 | 9 | Pokemon ascii art and/or 10 | 11 | A random one-liner when you switch your terminal on inside a dialog cloud 12 | 13 | ![alt text](https://github.com/devarshi16/TerminalWelcome/blob/master/poke.png) 14 | 15 | ### Example output 16 | ![Poketerm demo](demo.gif) 17 | 18 | 19 | ## Installation 20 | Alternate installation instructions -> https://youtu.be/JBUYfeah5c8 21 | ``` 22 | $ sudo pip install poketerm 23 | ``` 24 | **NOTE:** You need sudo permission for the package to work 25 | 26 | Turn on poketerm 27 | ``` 28 | $poketerm -t 1 29 | ``` 30 | Poketerm sets `POWERLEVEL9K_INSTANT_PROMPT=quiet` when updating zsh 31 | configs and places it at the top of the file to avoid Powerlevel10k 32 | instant prompt warnings. 33 | 34 | Turn off poketerm 35 | ``` 36 | $poketerm -t 0 37 | ``` 38 | 39 | **NOTE:** make sure to turn off poketerm before you uninstall it! 40 | 41 | ## Poketerm help 42 | ``` 43 | usage: main.py [-h] [-p {bulbasaur,dugtrio,meowth,pikachu,noascii}] [-l] 44 | [-o {0,1}] [-m MESSAGE] [-t {0,1}] [-d {0,1}] [-s] 45 | [--support] 46 | 47 | Display a Custom Message, a Pokemon ASCII Art and a Random Oneliner. 48 | NOTE: Remember to turn off poketerm using -t 0 tag before you uninstall 49 | it 50 | 51 | optional arguments: 52 | -h, --help show this help message and exit 53 | -p {bulbasaur,dugtrio,meowth,pikachu,noascii}, --pokemon {bulbasaur,dugtrio,meowth,pikachu,noascii} 54 | pokemon name for ASCII art. [noascii] for disabling 55 | ASCII art 56 | -l, --list list available pokemons 57 | -o {0,1}, --one-liner {0,1} 58 | turn one liner on [1] or off [0] 59 | -m MESSAGE, --message MESSAGE 60 | custom message to be displayed in the start. [nomessage] for 61 | no message 62 | -d {0,1}, --dialog {0,1} 63 | turn dialog cloud on [1] or off [0] 64 | -t {0,1}, --turn-on {0,1} 65 | turn on poketerm [1], turn off [0] 66 | -s, --show run poketerm with the active configuration 67 | --support print sponsor/donation URL and exit 68 | ``` 69 | 70 | ## List of available pokemons 71 | ``` 72 | $ poketerm -l 73 | Available Pokemons are 74 | pikachu 75 | 76 | |\_ _ 77 | \ \ _/_| 78 | \ \_ __/ / 79 | \ \________/ / 80 | | | 81 | / | 82 | | 0 0 | 83 | | _ | 84 | |() __ () | 85 | \ (__) | 86 | bulbasaur 87 | 88 | ____M___ 89 | ( / \ \ 90 | \ ----/\ ( ) ) 91 | / O O |---- _/ 92 | | _ \ 93 | \__U____/ _( | 94 | |_/ |_/ |_/ 95 | dugtrio 96 | 97 | _______ 98 | / \ 99 | | 0 0 | 100 | __|__ <> | 101 | / \ __|__ 102 | | | / \ 103 | | 0 0 | / 0 0 | 104 | | <> |/ <> / 105 | | | / 106 | 0oOwwwWwwOOoowwwwww 107 | meowth 108 | 109 | ___ ___ 110 | | \_ ^ ^ _/ | 111 | | \_ | | | | _/ __ | 112 | | \| | /""\ | | / _/ | | 113 | | __..|"||____||"|../. / | 114 | __ \_ / | ||____|| | \/ _/ __ 115 | \ """--__: v \../ v :__--""" / 116 | ""--___/ ____ ____ \___--"" 117 | . (_||_) (_||_) . 118 | ________|_ __|_______ 119 | \__________ _________/ 120 | . __________ . 121 | . \ __ / . 122 | . \_/__\_/ . 123 | . . 124 | "..........." 125 | noascii 126 | ``` 127 | 128 | ## Change Pokemon ASCII art 129 | ``` 130 | $ poketerm -p meowth 131 | ``` 132 | 133 | ## Change Custom Message 134 | ``` 135 | $ poketerm -m "Your Message Here" 136 | ``` 137 | 138 | Random one-liners and the dialog cloud are enabled by default. Disable them with the following commands or re-enable them as needed. 139 | 140 | ## Turn off Random One-Liner 141 | ``` 142 | $ poketerm -o 0 143 | ``` 144 | 145 | ## Re-enable dialog cloud 146 | Enabled by default; use this if you've disabled it. 147 | ``` 148 | $ poketerm -d 1 149 | ``` 150 | 151 | ## Turn off dialog cloud 152 | ``` 153 | $ poketerm -d 0 154 | ``` 155 | 156 | ## Turn off pokemon ascii art 157 | ``` 158 | $ poketerm -p noascii 159 | ``` 160 | 161 | ## Turn off Custom Message 162 | ``` 163 | $ poketerm -m nomessage 164 | ``` 165 | 166 | ## Support the Project 167 | 168 | Poketerm is completely free and open source. If it brightens up your 169 | terminal and you blaze past 10,000 runs (there's a gentle reminder when 170 | you do!), please consider supporting its continued development. 171 | 172 | [![GitHub Sponsors](https://img.shields.io/badge/Sponsor-%E2%9D%A4-ff69b4?style=for-the-badge&logo=github-sponsors)](https://github.com/sponsors/devarshi16) 173 | 174 | ## Acknowledgments 175 | 176 | Thanks to (http://silgro.com/fortunes.txt) for their one-liner database. 177 | -------------------------------------------------------------------------------- /converter.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def main() -> None: 5 | fortunes_path = Path('fortunes.txt') 6 | with fortunes_path.open('r') as src, Path('one_liners.py').open('w') as dst: 7 | dst.write('one_liners = [\n') 8 | for line in src: 9 | dst.write(f"r'''{line.strip()}''',\n") 10 | dst.write(']\n') 11 | 12 | 13 | if __name__ == '__main__': 14 | main() 15 | 16 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devarshi16/TerminalWelcome/e1662c7d036c457caaa83522df4ffc12bd46368d/demo.gif -------------------------------------------------------------------------------- /oneliner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | file="$HOME/fortunes.txt" 3 | if [ ! -f "$file" ]; then 4 | cp "$(dirname "$0")/fortunes.txt" "$file" 5 | fi 6 | shuf -n 1 "$file" 7 | 8 | -------------------------------------------------------------------------------- /poke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devarshi16/TerminalWelcome/e1662c7d036c457caaa83522df4ffc12bd46368d/poke.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "poketerm" 7 | version = "1.0.1" 8 | description = "Custom terminal welcome messages, pokemon ASCII art, and random one-liner!" 9 | readme = "README.md" 10 | requires-python = ">=3.7" 11 | authors = [{name = "devarshi16", email = "devershigpt6@gmail.com"}] 12 | dependencies = [ 13 | "readchar>=2.0.1", 14 | "termcolor>=1.1.0", 15 | ] 16 | 17 | [project.scripts] 18 | poketerm = "src.main:main" 19 | 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | from setuptools import find_packages 3 | import os 4 | 5 | def find_data(relpath, folder): 6 | dir_content = [] 7 | path = os.path.join(relpath, folder) 8 | tree = [(dirname, filenames) for dirname, _, filenames in os.walk(path) 9 | if filenames] 10 | 11 | for root, files in tree: 12 | path = os.path.relpath(root, relpath) 13 | dir_content.extend(map(lambda x: os.path.join(path, x), files)) 14 | 15 | return dir_content 16 | 17 | 18 | def package_data(relpath, folders): 19 | all_files = [] 20 | for folder in folders: 21 | all_files.extend(find_data(relpath, folder)) 22 | 23 | return all_files 24 | 25 | 26 | long_description = """ 27 | Put a custom message and/or 28 | a pokemon ASCII art and/or 29 | a random oneliner when you start your terminal 30 | NOTE: do $ sudo pip install poketerm 31 | You need sudo permissions for this to work 32 | """ 33 | setuptools.setup( 34 | name='poketerm', 35 | version='1.0.1', 36 | author="devarshi16", 37 | author_email="devershigpt6@gmail.com", 38 | description="Custom terminal welcome messages, pokemon ASCII art, and random one-liner!($sudo pip install poketerm)", 39 | long_description=long_description, 40 | url="https://github.com/devarshi16/TerminalWelcome", 41 | packages=find_packages(), 42 | keywords="terminal ascii-art shell-script welcomemessage bash pikachu bulbasaur pokemon-terminal pokemon linux linux-shell meowth", 43 | package_data={ 44 | "src":["*.ini","*.default","*.py"] 45 | }, 46 | include_package_data=True, 47 | classifiers=[ 48 | "Development Status :: 4 - Beta", 49 | "Programming Language :: Python :: 3", 50 | "License :: OSI Approved :: MIT License", 51 | "Operating System :: POSIX :: Linux", 52 | ], 53 | entry_points = { 54 | "console_scripts": ['poketerm = src.main:main'] 55 | }, 56 | python_requires=">=3.7", 57 | install_requires=[ 58 | 'readchar>=2.0.1', 59 | 'termcolor>=1.1.0' 60 | ], 61 | ) 62 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Put a custom message and/or 3 | a pokemon ASCII art and/or 4 | a random oneliner when you start your terminal 5 | """ 6 | __version__ = "1.0.1" 7 | __author__ = "devarshi16" 8 | -------------------------------------------------------------------------------- /src/__main__.py: -------------------------------------------------------------------------------- 1 | from .main import main 2 | 3 | 4 | if __name__ == '__main__': 5 | main() 6 | 7 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import configparser as cp 3 | import os 4 | import random 5 | from pathlib import Path 6 | import shutil 7 | import textwrap 8 | 9 | try: 10 | from termcolor import colored 11 | except ImportError: # pragma: no cover - fallback when termcolor isn't installed 12 | def colored(text, _color): 13 | return text 14 | from .pokemons import pokemons 15 | from .one_liners import one_liners 16 | 17 | SUPPORT_URLS = [ 18 | "https://github.com/sponsors/devarshi16", 19 | ] 20 | 21 | 22 | def _dialog_cloud(text: str) -> str: 23 | """Return a speech bubble around *text* with basic wrapping. 24 | 25 | The bubble expands to match the longest wrapped line so longer 26 | one-liners stay inside the cloud instead of spilling past the 27 | terminal width. 28 | """ 29 | 30 | columns = shutil.get_terminal_size(fallback=(80, 24)).columns - 4 31 | lines = textwrap.wrap(text, width=max(1, columns)) or [""] 32 | inner_width = max(len(line) for line in lines) 33 | top = " " + "_" * (inner_width + 2) 34 | bottom = "\\" + "_" * (inner_width + 2) + "/" 35 | 36 | bubble = [top] 37 | if len(lines) == 1: 38 | bubble.append(f"/ {lines[0].ljust(inner_width)} \\") 39 | else: 40 | bubble.append(f"/ {lines[0].ljust(inner_width)} \\") 41 | for line in lines[1:-1]: 42 | bubble.append(f"| {line.ljust(inner_width)} |") 43 | bubble.append(f"\\ {lines[-1].ljust(inner_width)} /") 44 | bubble.extend([bottom, " \\", " \\"]) 45 | return "\n".join(bubble) 46 | 47 | def main(): 48 | parser = argparse.ArgumentParser(description = 'Display a Custom Message, a Pokemon ASCII Art and a Random Oneliner.\n'+colored('NOTE: Remember to turn off poketerm using -t 0 tag before you uninstall it','red')) 49 | parser.add_argument('-p','--pokemon',help='pokemon name for ASCII art. [noascii] for disabling ASCII art',choices=pokemons.keys()) 50 | parser.add_argument('-l','--list',help='list available pokemons',action='store_true') 51 | parser.add_argument('-o','--one-liner',help='turn one liner on [1] or off [0]',type=int,choices=[0,1]) 52 | parser.add_argument('-m','--message',help='custom message to be displayed in the start. [nomessage] for no message') 53 | #parser.add_argument('-a','--ascii', help='turn ASCII art on [1] or off [0]',type=int, choices = [0,1]) 54 | parser.add_argument('-t','--turn-on',help='turn on poketerm [1], turn off [0]',type=int,choices=[0,1]) 55 | parser.add_argument('-d','--dialog',help='turn dialog cloud on [1] or off [0]',type=int,choices=[0,1]) 56 | parser.add_argument('-s','--show',help='run poketerm with the active configuration',action='store_true') 57 | parser.add_argument('--support', help='print sponsor/donation URL and exit', action='store_true') 58 | 59 | args = parser.parse_args() 60 | if args.support: 61 | for url in SUPPORT_URLS: 62 | print(url) 63 | return 64 | ''' 65 | print("INPUT ARGS") 66 | for arg in vars(args): 67 | print(arg,getattr(args,arg)) 68 | print()''' 69 | curr_path = Path(__file__).resolve().parent 70 | default_config_path = curr_path / 'poketermconfig.ini' 71 | ''' 72 | if os.path.exists(default_config_path): 73 | print("Found Config") 74 | else: 75 | print("Could not find config") 76 | ''' 77 | 78 | default_config = cp.ConfigParser() 79 | default_config.read(default_config_path) 80 | 81 | config_dir = Path.home() / '.config' / 'poketerm' 82 | config_dir.mkdir(parents=True, exist_ok=True) 83 | local_config_path = config_dir / 'poketermconfig.ini' 84 | local_config = cp.ConfigParser() 85 | 86 | if local_config_path.exists(): # If local config file exists 87 | local_config.read(local_config_path) 88 | try: 89 | change_config(args, local_config_path) 90 | except Exception: 91 | with open(local_config_path, 'w') as configfile: 92 | default_config.write(configfile) 93 | try: 94 | change_config(args, local_config_path) 95 | except Exception: 96 | try: 97 | local_config_path.unlink() 98 | except FileNotFoundError: 99 | pass 100 | print("Unable to read local config, maybe try", colored(" $poketerm -t 1", "green"), "with sudo?") 101 | local_config.read(local_config_path) 102 | else: # If local config file dne 103 | with open(local_config_path, 'w') as configfile: 104 | default_config.write(configfile) 105 | try: 106 | change_config(args, local_config_path) 107 | except Exception: 108 | try: 109 | local_config_path.unlink() 110 | except FileNotFoundError: 111 | pass 112 | print("Unable to read local config, maybe try", colored(" $poketerm -t 1", "green"), "with sudo?") 113 | local_config.read(local_config_path) 114 | 115 | if args.list: 116 | print("Available Pokemons are") 117 | for key in pokemons.keys(): 118 | print(key) 119 | print(pokemons[key]) 120 | 121 | if args.show: 122 | # show message 123 | msg = local_config["DEFAULTS"].get("message", "") 124 | if msg and msg.lower() != "none": 125 | print(msg) 126 | 127 | line = None 128 | show_dialog = local_config["DEFAULTS"].get("dialog", "False") == "True" 129 | if local_config["DEFAULTS"].get("one-liner") == "True": 130 | line = random.choice(one_liners) 131 | if show_dialog: 132 | print(_dialog_cloud(line)) 133 | 134 | if local_config["DEFAULTS"]["pokemon"] != "noascii": 135 | print(pokemons[local_config["DEFAULTS"]["pokemon"]]) 136 | 137 | if line and not show_dialog: 138 | print(line) 139 | 140 | # track how many times poketerm has been run 141 | runs = 0 142 | try: 143 | runs = int(local_config["DEFAULTS"].get("runs", "0")) 144 | except ValueError: 145 | runs = 0 146 | runs += 1 147 | local_config["DEFAULTS"]["runs"] = str(runs) 148 | 149 | # remind frequent users to sponsor only a few times 150 | reminders = 0 151 | try: 152 | reminders = int(local_config["DEFAULTS"].get("sponsor_reminders", "0")) 153 | except ValueError: 154 | reminders = 0 155 | if runs > 10_000 and reminders < 3: 156 | print(colored("🎉 WOW! You've run poketerm over 10,000 times! 🎉", "yellow")) 157 | print( 158 | colored( 159 | "☕ Consider sponsoring the project: https://github.com/sponsors/devarshi16", 160 | "green", 161 | ) 162 | ) 163 | print(colored("This message will appear only three times.", "cyan")) 164 | reminders += 1 165 | local_config["DEFAULTS"]["sponsor_reminders"] = str(reminders) 166 | 167 | with open(local_config_path, "w") as configfile: 168 | local_config.write(configfile) 169 | 170 | def change_config(args,path): 171 | config = cp.ConfigParser() 172 | config.read(path) 173 | ''' 174 | for section in config.sections(): 175 | for (key,val) in config.items(section): 176 | print(key,val) 177 | ''' 178 | if args.pokemon != None: 179 | config["DEFAULTS"]["pokemon"] = args.pokemon 180 | if args.message != None: 181 | if args.message == "nomessage": 182 | config["DEFAULTS"]["message"] = 'None' 183 | else: 184 | config["DEFAULTS"]["message"] = args.message 185 | if args.dialog is not None: 186 | if args.dialog == 1: 187 | config["DEFAULTS"]["dialog"] = 'True' 188 | else: 189 | config["DEFAULTS"]["dialog"] = 'False' 190 | if args.one_liner is not None: 191 | if args.one_liner == 1: 192 | config["DEFAULTS"]["one-liner"] = 'True' 193 | else: 194 | config["DEFAULTS"]["one-liner"] = 'False' 195 | config["DEFAULTS"]["dialog"] = 'False' 196 | ''' 197 | if args.ascii != None: 198 | if args.ascii == 1: 199 | config["DEFAULTS"]["ascii"] = 'True' 200 | else: 201 | config["DEFAULTS"]["ascii"] = 'False' 202 | ''' 203 | def ensure_line(file_path: Path, line: str) -> None: 204 | if not file_path.exists(): 205 | return 206 | with file_path.open('r+') as f: 207 | lines = [l.rstrip('\n') for l in f.readlines()] 208 | if line not in lines: 209 | f.write(line + '\n') 210 | 211 | def ensure_line_prepend(file_path: Path, line: str) -> None: 212 | """Ensure a line exists at the top of a file without duplicating it.""" 213 | if not file_path.exists(): 214 | return 215 | with file_path.open('r+') as f: 216 | lines = f.readlines() 217 | if any(l.rstrip('\n') == line for l in lines): 218 | return 219 | f.seek(0) 220 | f.write(line + '\n') 221 | f.writelines(lines) 222 | 223 | def remove_line(file_path: Path, line: str) -> None: 224 | if not file_path.exists(): 225 | return 226 | with file_path.open('r') as f: 227 | lines = f.readlines() 228 | with file_path.open('w') as f: 229 | for existing in lines: 230 | if existing.rstrip('\n') != line: 231 | f.write(existing) 232 | 233 | if args.turn_on is not None: 234 | bash_startup = "poketerm -s || echo reinstall poketerm and turn it off" 235 | p10k_quiet = "typeset -g POWERLEVEL9K_INSTANT_PROMPT=quiet" 236 | bash_files = ('.bashrc', '.bash_profile') 237 | zsh_files = ('.zshrc', '.zprofile') 238 | if args.turn_on == 1: 239 | config["DEFAULTS"]["poketerm"] = 'True' 240 | for fname in bash_files: 241 | ensure_line(Path.home() / fname, bash_startup) 242 | for fname in zsh_files: 243 | ensure_line_prepend(Path.home() / fname, p10k_quiet) 244 | ensure_line(Path.home() / fname, bash_startup) 245 | else: 246 | config["DEFAULTS"]["poketerm"] = 'False' 247 | for fname in bash_files: 248 | remove_line(Path.home() / fname, bash_startup) 249 | for fname in zsh_files: 250 | remove_line(Path.home() / fname, bash_startup) 251 | 252 | with open(path,'w') as configfile: 253 | config.write(configfile) 254 | 255 | 256 | if __name__ == '__main__': 257 | main() 258 | 259 | -------------------------------------------------------------------------------- /src/pokemons.py: -------------------------------------------------------------------------------- 1 | pokemons = { 2 | "bulbasaur" :r""" 3 | ____M___ 4 | ( / \ \ 5 | \ ----/\ ( ) ) 6 | / O O |---- _/ 7 | | _ \ 8 | \__U____/ _( | 9 | |_/ |_/ |_/""", 10 | "dugtrio" : r""" 11 | _______ 12 | / \ 13 | | 0 0 | 14 | __|__ <> | 15 | / \ __|__ 16 | | | / \ 17 | | 0 0 | / 0 0 | 18 | | <> |/ <> / 19 | | | / 20 | 0oOwwwWwwOOoowwwwww""", 21 | "meowth" : r''' 22 | ___ ___ 23 | | \_ ^ ^ _/ | 24 | | \_ | | | | _/ __ | 25 | | \| | /""\ | | / _/ | | 26 | | __..|"||____||"|../. / | 27 | __ \_ / | ||____|| | \/ _/ __ 28 | \ """--__: v \../ v :__--""" / 29 | ""--___/ ____ ____ \___--"" 30 | . (_||_) (_||_) . 31 | ________|_ __|_______ 32 | \__________ _________/ 33 | . __________ . 34 | . \ __ / . 35 | . \_/__\_/ . 36 | . . 37 | "..........."''', 38 | "pikachu" : r''' 39 | |\_ _ 40 | \ \ _/_| 41 | \ \_ __/ / 42 | \ \________/ / 43 | | | 44 | / | 45 | | 0 0 | 46 | | _ | 47 | |() __ () | 48 | \ (__) |''', 49 | "noascii":'' 50 | } 51 | -------------------------------------------------------------------------------- /src/poketermconfig.ini: -------------------------------------------------------------------------------- 1 | [DEFAULTS] 2 | pokemon = pikachu 3 | one-liner = True 4 | message = None 5 | poketerm = True 6 | ascii = True 7 | dialog = True 8 | runs = 0 9 | sponsor_reminders = 0 10 | -------------------------------------------------------------------------------- /src/poketermconfig.ini.default: -------------------------------------------------------------------------------- 1 | [DEFAULTS] 2 | pokemon = pikachu 3 | one-liner = True 4 | message = None 5 | poketerm = True 6 | ascii = True 7 | dialog = True 8 | runs = 0 9 | sponsor_reminders = 0 10 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | import tempfile 5 | from pathlib import Path 6 | 7 | ROOT = Path(__file__).resolve().parents[1] 8 | sys.path.insert(0, str(ROOT)) 9 | 10 | from src.one_liners import one_liners 11 | from src.pokemons import pokemons 12 | from src.main import SUPPORT_URLS, _dialog_cloud 13 | 14 | 15 | def run_cli(args: list[str], env: dict[str, str] | None = None) -> subprocess.CompletedProcess: 16 | return subprocess.run( 17 | [sys.executable, "-m", "src.main", *args], 18 | capture_output=True, 19 | text=True, 20 | env=env, 21 | ) 22 | 23 | 24 | def test_help_runs() -> None: 25 | result = run_cli(["-h"]) 26 | assert result.returncode == 0 27 | assert "usage" in result.stdout.lower() 28 | 29 | 30 | def test_support_prints_links() -> None: 31 | result = run_cli(["--support"]) 32 | assert result.returncode == 0 33 | for url in SUPPORT_URLS: 34 | assert url in result.stdout 35 | 36 | 37 | def test_show_pokemon() -> None: 38 | with tempfile.TemporaryDirectory() as tmp_home: 39 | env = {**os.environ, "HOME": tmp_home} 40 | result = run_cli(["-o", "0", "-s"], env=env) 41 | assert result.returncode == 0 42 | assert pokemons["pikachu"] in result.stdout 43 | 44 | 45 | def test_show_one_liner() -> None: 46 | with tempfile.TemporaryDirectory() as tmp_home: 47 | env = {**os.environ, "HOME": tmp_home} 48 | result = run_cli(["-p", "noascii", "-s"], env=env) 49 | assert result.returncode == 0 50 | lines = result.stdout.splitlines() 51 | assert lines[-1] == " \\" 52 | inner = " ".join(l[2:-2].strip() for l in lines[1:-3]) 53 | assert inner in one_liners 54 | 55 | 56 | def test_change_default_pokemon() -> None: 57 | with tempfile.TemporaryDirectory() as tmp_home: 58 | env = {**os.environ, "HOME": tmp_home} 59 | assert run_cli(["-p", "meowth"], env=env).returncode == 0 60 | result = run_cli(["-o", "0", "-s"], env=env) 61 | assert pokemons["meowth"] in result.stdout 62 | 63 | 64 | def test_dialog_cloud_disabled() -> None: 65 | with tempfile.TemporaryDirectory() as tmp_home: 66 | env = {**os.environ, "HOME": tmp_home} 67 | result = run_cli(["-p", "noascii", "-d", "0", "-s"], env=env) 68 | assert result.returncode == 0 69 | line = result.stdout.strip() 70 | assert line in one_liners 71 | 72 | 73 | def test_dialog_cloud_wraps_long_line() -> None: 74 | long_text = "x" * 200 75 | cloud = _dialog_cloud(long_text) 76 | lines = cloud.splitlines() 77 | assert len(lines) > 5 78 | assert lines[0].startswith(" ") 79 | assert lines[-2] == " \\" 80 | assert lines[-1] == " \\" 81 | assert len(lines[0]) < len(long_text) + 4 82 | 83 | 84 | def test_turn_on_updates_zshrc() -> None: 85 | with tempfile.TemporaryDirectory() as tmp_home: 86 | env = {**os.environ, "HOME": tmp_home} 87 | zshrc = Path(tmp_home) / ".zshrc" 88 | zshrc.write_text("export PATH=/usr/bin\n") 89 | assert run_cli(["-t", "1"], env=env).returncode == 0 90 | expected = ( 91 | "typeset -g POWERLEVEL9K_INSTANT_PROMPT=quiet\n" 92 | "export PATH=/usr/bin\n" 93 | "poketerm -s || echo reinstall poketerm and turn it off\n" 94 | ) 95 | assert zshrc.read_text() == expected 96 | assert run_cli(["-t", "0"], env=env).returncode == 0 97 | # The Powerlevel10k line remains at the top 98 | assert zshrc.read_text() == ( 99 | "typeset -g POWERLEVEL9K_INSTANT_PROMPT=quiet\n" 100 | "export PATH=/usr/bin\n" 101 | ) 102 | 103 | 104 | def test_turn_on_does_not_duplicate_existing_p10k_line() -> None: 105 | with tempfile.TemporaryDirectory() as tmp_home: 106 | env = {**os.environ, "HOME": tmp_home} 107 | zshrc = Path(tmp_home) / ".zshrc" 108 | zshrc.write_text( 109 | "typeset -g POWERLEVEL9K_INSTANT_PROMPT=quiet\n" 110 | "alias ll='ls -al'\n" 111 | ) 112 | assert run_cli(["-t", "1"], env=env).returncode == 0 113 | expected = ( 114 | "typeset -g POWERLEVEL9K_INSTANT_PROMPT=quiet\n" 115 | "alias ll='ls -al'\n" 116 | "poketerm -s || echo reinstall poketerm and turn it off\n" 117 | ) 118 | assert zshrc.read_text() == expected 119 | 120 | -------------------------------------------------------------------------------- /tests/test_runs_counter.py: -------------------------------------------------------------------------------- 1 | import configparser as cp 2 | import os 3 | import subprocess 4 | import sys 5 | import tempfile 6 | from pathlib import Path 7 | 8 | 9 | ROOT = Path(__file__).resolve().parents[1] 10 | sys.path.insert(0, str(ROOT)) 11 | 12 | 13 | def run_cli(args: list[str], env: dict[str, str] | None = None) -> subprocess.CompletedProcess: 14 | return subprocess.run( 15 | [sys.executable, "-m", "src.main", *args], 16 | capture_output=True, 17 | text=True, 18 | env=env, 19 | ) 20 | 21 | 22 | def test_runs_counter_increment() -> None: 23 | with tempfile.TemporaryDirectory() as tmp_home: 24 | env = {**os.environ, "HOME": tmp_home} 25 | result = run_cli(["-p", "noascii", "-o", "0", "-s"], env=env) 26 | assert result.returncode == 0 27 | config_path = Path(tmp_home) / ".config" / "poketerm" / "poketermconfig.ini" 28 | parser = cp.ConfigParser() 29 | parser.read(config_path) 30 | assert parser["DEFAULTS"].getint("runs") == 1 31 | 32 | 33 | def test_donation_message_after_threshold() -> None: 34 | with tempfile.TemporaryDirectory() as tmp_home: 35 | env = {**os.environ, "HOME": tmp_home} 36 | config_dir = Path(tmp_home) / ".config" / "poketerm" 37 | config_dir.mkdir(parents=True) 38 | config_path = config_dir / "poketermconfig.ini" 39 | config_path.write_text( 40 | "[DEFAULTS]\n" 41 | "pokemon = pikachu\n" 42 | "one-liner = False\n" 43 | "message = None\n" 44 | "poketerm = True\n" 45 | "ascii = True\n" 46 | "dialog = False\n" 47 | "runs = 10000\n" 48 | "sponsor_reminders = 0\n" 49 | ) 50 | result = run_cli(["-p", "noascii", "-o", "0", "-s"], env=env) 51 | assert result.returncode == 0 52 | out = result.stdout.lower() 53 | assert "sponsoring" in out 54 | assert "only" in out and "three times" in out 55 | assert "https://github.com/sponsors/devarshi16" in result.stdout 56 | parser = cp.ConfigParser() 57 | parser.read(config_path) 58 | defaults = parser["DEFAULTS"] 59 | assert defaults.getint("runs") == 10001 60 | assert defaults.getint("sponsor_reminders") == 1 61 | 62 | 63 | def test_donation_message_shows_only_thrice() -> None: 64 | with tempfile.TemporaryDirectory() as tmp_home: 65 | env = {**os.environ, "HOME": tmp_home} 66 | config_dir = Path(tmp_home) / ".config" / "poketerm" 67 | config_dir.mkdir(parents=True) 68 | config_path = config_dir / "poketermconfig.ini" 69 | config_path.write_text( 70 | "[DEFAULTS]\n" 71 | "pokemon = pikachu\n" 72 | "one-liner = False\n" 73 | "message = None\n" 74 | "poketerm = True\n" 75 | "ascii = True\n" 76 | "dialog = False\n" 77 | "runs = 10002\n" 78 | "sponsor_reminders = 2\n" 79 | ) 80 | # Third reminder should be shown 81 | result = run_cli(["-p", "noascii", "-o", "0", "-s"], env=env) 82 | assert result.returncode == 0 83 | assert "sponsor" in result.stdout.lower() 84 | assert "https://github.com/sponsors/devarshi16" in result.stdout 85 | parser = cp.ConfigParser() 86 | parser.read(config_path) 87 | defaults = parser["DEFAULTS"] 88 | assert defaults.getint("runs") == 10003 89 | assert defaults.getint("sponsor_reminders") == 3 90 | 91 | # Fourth run should not show reminder 92 | result = run_cli(["-p", "noascii", "-o", "0", "-s"], env=env) 93 | assert result.returncode == 0 94 | assert "sponsor" not in result.stdout.lower() 95 | parser.read(config_path) 96 | defaults = parser["DEFAULTS"] 97 | assert defaults.getint("runs") == 10004 98 | assert defaults.getint("sponsor_reminders") == 3 99 | 100 | -------------------------------------------------------------------------------- /welcome.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | file="$HOME/fortunes.txt" 3 | if [ ! -f "$file" ]; then 4 | cp "$(dirname "$0")/fortunes.txt" "$file" 5 | fi 6 | 7 | cat <<'EOF' >> "$HOME/.bashrc" 8 | echo "Welcome Devarshi " 9 | echo "|\_ _ " 10 | echo " \ \ _/_|" 11 | echo " \ \_ __/ /" 12 | echo " \ \________/ /" 13 | echo " | |" 14 | echo " / |" 15 | echo " | 0 0 |" 16 | echo " | _ |" 17 | echo " |() __ () |" 18 | echo " \ (__) |" 19 | file="$HOME/fortunes.txt" 20 | if [ -f "$file" ]; then 21 | shuf -n 1 "$file" 22 | fi 23 | EOF 24 | 25 | --------------------------------------------------------------------------------