├── .vim └── coc-settings.json ├── tests ├── __access.py ├── test_battery.py ├── test_decorateddata.py ├── test_cli.py ├── test_disk.py ├── test_charts.py ├── test_files.py └── test_tools.py ├── requirements.txt ├── LICENSE ├── .github └── workflows │ └── python-app.yml ├── setup.py ├── vizex ├── vizexdu │ ├── battery.py │ ├── cpu.py │ ├── charts.py │ └── disks.py ├── vizextree │ └── viztree.py ├── tools.py ├── vizexdf │ └── files.py └── cli.py ├── .gitignore ├── README.md └── README.html /.vim/coc-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": false, 3 | "python.linting.flake8Enabled": true, 4 | "python.linting.enabled": true 5 | } -------------------------------------------------------------------------------- /tests/__access.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import inspect 4 | 5 | # Creates path to the main module files 6 | def ADD_PATH(): 7 | currentdir = os.path.dirname( 8 | os.path.abspath( 9 | inspect.getfile(inspect.currentframe()) 10 | ) 11 | ) 12 | parentdir = os.path.dirname(currentdir) 13 | sys.path.insert(0, parentdir) 14 | 15 | if __name__ == '__main__': 16 | ADD_PATH() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs>=20.2.0 2 | build>=0.3.1.post1 3 | click>=7.1.2 4 | colored>=1.4.2 5 | iniconfig>=1.0.1 6 | isort>=5.8.0 7 | lazy-object-proxy>=1.6.0 8 | mccabe>=0.6.1 9 | numpy>=1.19.2 10 | packaging>=20.4 11 | pandas>=1.1.3 12 | pluggy>=0.13.1 13 | psutil>=5.7.2 14 | py>=1.9.0 15 | pytest>=6.1.1 16 | python-dateutil>=2.8.1 17 | python-magic>=0.4.18 18 | pytz>=2020.1 19 | six>=1.15.0 20 | tabulate>=0.8.7 21 | toml>=0.10.1 22 | -------------------------------------------------------------------------------- /tests/test_battery.py: -------------------------------------------------------------------------------- 1 | # add path to the main package and test battery.py 2 | if __name__ == '__main__': 3 | from __access import ADD_PATH 4 | ADD_PATH() 5 | 6 | import unittest 7 | import psutil 8 | 9 | from vizexdu.battery import Battery 10 | 11 | 12 | class TestBattery(unittest.TestCase): 13 | """ Test battery module """ 14 | 15 | def test_Battery_constructor(self): 16 | if not (has_battery := psutil.sensors_battery()): 17 | with self.assertRaises(Exception): 18 | Battery() 19 | else: 20 | self.assertTrue(has_battery.percent > 0) 21 | 22 | def test_create_details_text(self): 23 | if not psutil.sensors_battery(): 24 | pass 25 | else: 26 | self.assertTrue(isinstance(Battery().create_details_text(), str)) 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Beka Modebadze 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/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python 3.8 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: "3.8" 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 pytest 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Lint with flake8 32 | run: | 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Test with pytest 38 | run: | 39 | pytest 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('README.md', 'r') as rm: 4 | long_description = rm.read() 5 | 6 | with open('requirements.txt') as r: 7 | requirements = r.read().splitlines() 8 | 9 | setup( 10 | name='vizex', 11 | version='2.1.1', 12 | author='Beka Modebadze', 13 | author_email='bexx.modd@gmail.com', 14 | description='UNIX/Linux Terminal program to graphically display the disk space usage and/or directory data', 15 | long_description=long_description, 16 | long_description_content_type ='text/markdown', 17 | url='https://github.com/bexxmodd/vizex', 18 | package_dir = {'': 'vizex'}, 19 | py_modules=[ 20 | 'cli', 'vizexdu/disks', 'tools', 21 | 'vizexdu/charts', 'vizexdu/battery', 'vizexdu/cpu', 22 | 'vizexdf/files', 'vizextree/viztree' 23 | ], 24 | packages = find_packages(where='vizex'), 25 | classifiers=[ 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "License :: OSI Approved :: MIT License", 29 | "Operating System :: POSIX", 30 | ], 31 | install_requires=requirements, 32 | python_requires='>=3.8', 33 | entry_points=''' 34 | [console_scripts] 35 | vizex=cli:disk_usage 36 | vizexdf=cli:dirs_files 37 | vizextree=cli:print_tree 38 | ''' 39 | ) 40 | -------------------------------------------------------------------------------- /tests/test_decorateddata.py: -------------------------------------------------------------------------------- 1 | # add path to the main package and test decorateddata.py 2 | if __name__ == '__main__': 3 | from __access import ADD_PATH 4 | ADD_PATH() 5 | 6 | 7 | import unittest 8 | import unittest.mock 9 | 10 | from tools import DecoratedData 11 | 12 | 13 | class TestTools(unittest.TestCase): 14 | 15 | def test_decorated_data_constructor(self): 16 | testing = DecoratedData(33, 'Thirteen Three') 17 | self.assertEqual(33, testing.size, 18 | msg='int value was not initialized properly') 19 | self.assertEqual('Thirteen Three', testing.to_string, 20 | msg='to_string of a object was not initialized properly') 21 | 22 | def test_decorated_data_str_printing(self): 23 | testing = DecoratedData(8, 'E1gh7@') 24 | self.assertEqual('E1gh7@', str(testing)) 25 | 26 | def test_decorated_data_equal(self): 27 | test_a = DecoratedData(5, 'five') 28 | test_b = DecoratedData(5, 'five') 29 | self.assertEqual(test_a, test_b) 30 | 31 | def test_decorated_notequal(self): 32 | test_a = DecoratedData(7, 'five') 33 | test_b = DecoratedData(5, 'seven') 34 | self.assertNotEqual(test_a, test_b) 35 | 36 | def test_decorated_greater(self): 37 | test_a = DecoratedData(7, 'five') 38 | test_b = DecoratedData(5, 'seven') 39 | self.assertGreater(test_a, test_b) 40 | 41 | def test_decorated_greater_equal(self): 42 | test_a = DecoratedData(7, 'five') 43 | test_b = DecoratedData(5, 'seven') 44 | self.assertGreaterEqual(test_a, test_b) 45 | 46 | def test_decorated_less(self): 47 | test_a = DecoratedData(3, 'three') 48 | test_b = DecoratedData(5, 'five and three') 49 | self.assertLess(test_a, test_b) 50 | 51 | def test_decorated_less_equal(self): 52 | test_a = DecoratedData(6, 'three') 53 | test_b = DecoratedData(6, 'x and three') 54 | self.assertLessEqual(test_a, test_b) 55 | 56 | 57 | if __name__ == '__main__': 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /vizex/vizexdu/battery.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Battery module for vizex 3 | ''' 4 | 5 | import datetime 6 | import psutil 7 | 8 | from math import ceil 9 | from .charts import Chart, HorizontalBarChart, Options 10 | 11 | 12 | class Battery: 13 | """Personalize and visualize the Battery usage in the terminal""" 14 | 15 | def __init__(self) -> None: 16 | """ Create a new Battery Object """ 17 | self._battery = psutil.sensors_battery() 18 | if not self._battery: 19 | raise Exception("Battery information currently unavailable") 20 | 21 | def print_charts(self, options: Options = None) -> None: 22 | """ Prints battery information """ 23 | if not options: 24 | options = Options() 25 | chart = HorizontalBarChart(options) 26 | self.print_battery_chart(chart) 27 | 28 | def print_battery_chart(self, chart: Chart) -> None: 29 | """ Prints battery information chart """ 30 | post_graph_text = str( 31 | ceil(100 * self._battery.percent) / 100) + "%" 32 | footer = self.create_details_text() 33 | 34 | chart.chart( 35 | post_graph_text=post_graph_text, 36 | pre_graph_text=None, 37 | title="Battery", 38 | footer=footer, 39 | maximum=100, 40 | current=self._battery.percent, 41 | ) 42 | print() 43 | 44 | def create_details_text(self) -> str: 45 | """ Format more information about the battery """ 46 | time_left = datetime.timedelta(seconds=self._battery.secsleft) 47 | 48 | if plugged := self._battery.power_plugged: 49 | return format("Charging") 50 | return f"Plugged in: {plugged}\tDischarging: {time_left}\t" 51 | 52 | 53 | if __name__ == "__main__": 54 | battery = Battery() 55 | battery.print_charts() 56 | # Example output: 57 | 58 | # Battery 59 | # █████████████▒░░░░░░░░░░░░░░░░░░░░░░░░░ 34.23% 60 | # Charging 61 | 62 | # Battery 63 | # █████████████▒░░░░░░░░░░░░░░░░░░░░░░░░░ 34.51% 64 | # Plugged in: False Discharging: 0:48:24 65 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | # add path to the main package and test cli.py 2 | if __name__ == '__main__': 3 | from __access import ADD_PATH 4 | ADD_PATH() 5 | 6 | 7 | import io 8 | import random 9 | import psutil 10 | import unittest 11 | import unittest.mock 12 | 13 | from click.testing import CliRunner 14 | from cli import dirs_files, disk_usage 15 | 16 | 17 | class TestCli(unittest.TestCase): 18 | 19 | def test_dirs_files(self): 20 | try: 21 | runner = CliRunner() 22 | result = runner.invoke(dirs_files, ['-ds', 'name']) 23 | self.assertTrue(result.exit_code == 0) 24 | self.assertTrue('name' in result.output) 25 | self.assertTrue('last modified' in result.output) 26 | self.assertTrue('size' in result.output) 27 | self.assertTrue('type' in result.output) 28 | except Exception as e: 29 | self.fail(f'Exception occured when calling vizexdf with desc and sort name options {e}') 30 | 31 | def test_dirs_files_help(self): 32 | try: 33 | runner = CliRunner() 34 | result = runner.invoke(dirs_files, ['--help']) 35 | self.assertTrue(result.exit_code == 0) 36 | self.assertTrue('Made by: Beka Modebadze' in result.output) 37 | except Exception as e: 38 | self.fail(f'Exception occured when calling vizexdf\'s --help {e}') 39 | 40 | def test_disk_usage(self): 41 | try: 42 | runner = CliRunner() 43 | result = runner.invoke(disk_usage, ['--mark', '@']) 44 | self.assertTrue(result.exit_code == 0) 45 | self.assertTrue('root' in result.output) 46 | self.assertTrue('Total' in result.output) 47 | self.assertTrue('Used' in result.output) 48 | self.assertTrue('Free' in result.output) 49 | self.assertTrue('@' in result.output) 50 | except Exception as e: 51 | self.fail(f'Exception occured when calling vizex {e}') 52 | 53 | def test_disk_usage_help(self): 54 | try: 55 | runner = CliRunner() 56 | result = runner.invoke(disk_usage, ['--help']) 57 | self.assertTrue(result.exit_code == 0) 58 | self.assertTrue('Made by: Beka Modebadze' in result.output) 59 | except Exception as e: 60 | self.fail(f'Exception occured when calling vizex --help {e}') 61 | 62 | 63 | if __name__ == '__main__': 64 | unittest.main() -------------------------------------------------------------------------------- /.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 | # Manually excluded 132 | beta.py 133 | timetest.py 134 | .vscode 135 | .idea 136 | profile_vixez1.txt 137 | -------------------------------------------------------------------------------- /vizex/vizextree/viztree.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from itertools import islice 3 | from tools import find_word 4 | 5 | from colored import fg, attr, stylize 6 | 7 | 8 | SPACE = ' ' 9 | BRANCH = '│ ' 10 | TEE = '├── ' 11 | LEAF = '└── ' 12 | 13 | FILES_COUNT = 0 14 | DIRS_COUNT = 0 15 | 16 | 17 | def construct_tree(dir_path: str, level: int, only_dirs: bool = False, 18 | max_length: int = 1000) -> None: 19 | dir_path = Path(dir_path) 20 | print_colored(str(dir_path), 'red', 'bold') 21 | iterator = generate_iterable( 22 | dir_path, level=level, only_dirs=only_dirs) 23 | for line in islice(iterator, max_length): 24 | filter_project_dirs(line) 25 | if next(iterator, None): 26 | print(f'... length limit of {max_length} is reached! counted:') 27 | print(f'\n{stylize(DIRS_COUNT, fg(172))} directories' 28 | + (f', {stylize(FILES_COUNT, fg(12))} files' if FILES_COUNT else '')) 29 | 30 | 31 | def generate_iterable(dir_path: Path, prefix: str = '', 32 | level=-1, only_dirs: bool = False) -> str: 33 | global FILES_COUNT, DIRS_COUNT 34 | if not level: 35 | return # stop iterating 36 | 37 | if only_dirs: 38 | contents = [d for d in dir_path.iterdir() if d.is_dir()] 39 | else: 40 | contents = list(dir_path.iterdir()) 41 | 42 | pointers = [TEE] * (len(contents) - 1) + [LEAF] 43 | for pointer, path in zip(pointers, contents): 44 | if path.is_dir(): 45 | yield prefix + pointer + path.name 46 | DIRS_COUNT += 1 47 | extension = BRANCH if pointer == TEE else SPACE 48 | yield from generate_iterable( 49 | path, prefix=prefix + extension, level=level - 1) 50 | elif not only_dirs: 51 | yield prefix + pointer + path.name 52 | FILES_COUNT += 1 53 | 54 | 55 | def filter_project_dirs(line: str) -> None: 56 | if find_word('test', line) or find_word('tests', line): 57 | print_colored(line, 'sky_blue_2', 'bold') 58 | elif find_word('src', line) or find_word('main', line): 59 | print_colored(line, 'chartreuse_2b', 'bold') 60 | elif find_word('venv', line): 61 | print_colored(line, 'purple_1a', 'dim') 62 | elif is_hidden(line): 63 | print_colored(line, 'dark_gray', 'dim') 64 | else: 65 | print(line) 66 | 67 | 68 | def print_colored(line: str, color: str = None, 69 | style: str = None) -> None: 70 | for c in line: 71 | if c not in ('├', '─', '│', '└'): 72 | print(stylize(c, attr(style) + fg(color)), end='') 73 | else: 74 | print(c, end='') 75 | print() 76 | 77 | 78 | def is_hidden(line: str) -> bool: 79 | for c in line: 80 | if c in ('├', '─', '│', '└', ' '): 81 | continue 82 | return c == '.' 83 | 84 | 85 | if __name__ == '__main__': 86 | construct_tree("../", level=2) 87 | -------------------------------------------------------------------------------- /tests/test_disk.py: -------------------------------------------------------------------------------- 1 | # add path to the main package and test disks.py 2 | if __name__ == '__main__': 3 | from __access import ADD_PATH 4 | ADD_PATH() 5 | 6 | import io 7 | import random 8 | import psutil 9 | import unittest 10 | 11 | from unittest.mock import MagicMock, call 12 | from colored import fg, attr, stylize 13 | from vizexdu.charts import Options 14 | 15 | 16 | @unittest.skip("Tests need to be updated to suit the changes in the disks.py module") 17 | class TestDiskUsage(unittest.TestCase): 18 | """Test DiskUsage class""" 19 | 20 | def test_grab_partitions(self): 21 | disks = self.du.grab_partitions(self.du.exclude) 22 | self.assertIsInstance(disks, dict, 23 | msg='Function should return dict') 24 | # Test that proper keys are present 25 | compare_keys = ['total', 26 | 'used', 27 | 'free', 28 | 'percent', 29 | 'fstype', 30 | 'mountpoint'] 31 | for disk in disks: 32 | self.assertIsNotNone(disk) 33 | keys = [key for key in disks[disk].keys()] 34 | self.assertListEqual(keys, compare_keys, 35 | msg=f'{keys} are not present') 36 | 37 | # Test that they have positive integer values 38 | for disk in disks: 39 | self.assertGreaterEqual(disks[disk]['total'], 1) 40 | self.assertGreaterEqual(disks[disk]['used'], 1) 41 | self.assertGreaterEqual(disks[disk]['free'], 0) 42 | self.assertGreaterEqual(disks[disk]['percent'], 0) 43 | self.assertIsInstance(disks[disk]['fstype'], str) 44 | self.assertIsInstance(disks[disk]['mountpoint'], str) 45 | 46 | def test_grab_specific_disk(self): 47 | disks = self.du.grab_specific_disk('/home/') 48 | self.assertIsInstance(disks, dict, 49 | msg='Function should return dict') 50 | # Test that proper keys are present 51 | compare_keys = ['total', 52 | 'used', 53 | 'free', 54 | 'percent', 55 | 'fstype', 56 | 'mountpoint'] 57 | for disk in disks: 58 | self.assertIsNotNone(disks) 59 | keys = [key for key in disks[disk].keys()] 60 | self.assertListEqual(keys, compare_keys, 61 | msg=f'{keys} are not present') 62 | 63 | # Test that they have positive integer values 64 | for disk in disks: 65 | self.assertGreaterEqual(disks[disk]['total'], 1) 66 | self.assertGreaterEqual(disks[disk]['used'], 1) 67 | self.assertGreaterEqual(disks[disk]['free'], 0) 68 | self.assertGreaterEqual(disks[disk]['percent'], 0) 69 | self.assertIsInstance(disks[disk]['fstype'], str) 70 | self.assertIsInstance(disks[disk]['mountpoint'], str) 71 | 72 | 73 | if __name__ == '__main__': 74 | unittest.main() 75 | 76 | -------------------------------------------------------------------------------- /vizex/vizexdu/cpu.py: -------------------------------------------------------------------------------- 1 | """ 2 | -[ ] Option to record the all the cpu freqs 3 | -[ ] Do some analysis on them based on time and maybe what was running from processes 4 | """ 5 | 6 | import os 7 | import time 8 | import psutil 9 | import getpass 10 | import tools 11 | 12 | from .charts import HorizontalBarChart 13 | 14 | 15 | class CPUFreq: 16 | _max = psutil.cpu_freq().max 17 | _min = psutil.cpu_freq().min 18 | 19 | @property 20 | def max_freq(self) -> int: 21 | return self._max 22 | 23 | @max_freq.setter 24 | def max_freq(self, maximum: int) -> None: 25 | self._max = maximum 26 | 27 | @property 28 | def min_freq(self) -> int: 29 | return self._min 30 | 31 | @min_freq.setter 32 | def min_freq(self, minimum: int) -> None: 33 | self._min = minimum 34 | 35 | def __init__(self) -> None: 36 | return 37 | 38 | def display_separately(self, filename="") -> None: 39 | os.system('cls' if os.name == 'nt' else 'clear') 40 | ch = HorizontalBarChart() 41 | ch.options.graph_color = 'white' 42 | 43 | while True: 44 | cpu = psutil.cpu_freq(percpu=True) 45 | 46 | for i, core in enumerate(cpu, start=1): 47 | min_freq, max_freq, current_freq = core.min, core.max, core.current 48 | percent = round((current_freq - min_freq) / (max_freq - min_freq) * 100, 2) 49 | ch.chart( 50 | title=f'CPU #{i}', 51 | pre_graph_text=f'Current: {round(current_freq, 1)}MHz || Min: {min_freq}MHz || Max: {max_freq}MHz', 52 | post_graph_text=tools.create_usage_warning(percent, 30, 15), 53 | footer=None, 54 | maximum=max_freq - min_freq, 55 | current=current_freq - min_freq, 56 | ) 57 | 58 | if filename: 59 | cpu = { 60 | 'user': [getpass.getuser()], 61 | 'cpu': [i], 62 | 'time': [time.time()], 63 | 'current': [current_freq], 64 | 'usage': [percent], 65 | } 66 | 67 | if (filename.split(".")[-1].lower()) == 'csv': 68 | tools.save_to_csv(cpu, filename) 69 | else: 70 | raise NameError("Not supported file type, please indicate " 71 | + ".CSV at the end of the filename") 72 | 73 | print() 74 | 75 | time.sleep(0.8) 76 | os.system('cls' if os.name == 'nt' else 'clear') 77 | 78 | def display_combined(self) -> None: 79 | os.system('cls' if os.name == 'nt' else 'clear') 80 | ch = HorizontalBarChart() 81 | ch.options.graph_color = 'white' 82 | while True: 83 | cpu = psutil.cpu_freq(percpu=False) 84 | ch.chart( 85 | title='CPU (ALL)', 86 | pre_graph_text=f'Current: {round(cpu.current, 1)}MHz || Min: {self._min}MHz || Max: {self._max}MHz', 87 | post_graph_text=tools.create_usage_warning( 88 | round((cpu.current - self._min) / (self._max - self._min) * 100, 2), 89 | 30, 15), 90 | footer=None, 91 | maximum=self._max - self._min, 92 | current=cpu.current - self._min 93 | ) 94 | print() 95 | 96 | time.sleep(0.8) 97 | os.system('cls' if os.name == 'nt' else 'clear') 98 | 99 | 100 | if __name__ == '__main__': 101 | cpu_freq = CPUFreq() 102 | cpu_freq.display_separately("~/cpu.csv") 103 | -------------------------------------------------------------------------------- /tests/test_charts.py: -------------------------------------------------------------------------------- 1 | # add path to the main package and test charts.py 2 | if __name__ == '__main__': 3 | from __access import ADD_PATH 4 | ADD_PATH() 5 | 6 | 7 | import io 8 | import unittest 9 | import unittest.mock 10 | 11 | from colored import fg, attr, stylize 12 | from vizexdu.charts import Options, Chart, HorizontalBarChart 13 | 14 | 15 | class TestOptions(unittest.TestCase): 16 | 17 | @classmethod 18 | def setUpClass(cls): 19 | cls.opts = Options() 20 | 21 | def test_symbol_setter(self): 22 | self.opts.symbol = '@' 23 | self.assertEqual(self.opts.fsymbol, '@') 24 | self.assertEqual(self.opts.msymbol, '>') 25 | self.assertEqual(self.opts.esymbol, '-') 26 | 27 | def test_check_color(self): 28 | self.opts.graph_color = 'cyan' 29 | self.assertEqual( 30 | fg('cyan'), self.opts.graph_color) 31 | 32 | self.assertEqual( 33 | 'white', self.opts._check_color('notcolor')) 34 | self.assertEqual( 35 | fg('magenta'), self.opts._check_color('magenta')) 36 | 37 | def test_check_color_key_error(self): 38 | color = self.opts._check_color('notcolor') 39 | self.assertEqual('white', color, 40 | msg='When unavailable color is given this function should return "white"') 41 | 42 | def test_check_attr(self): 43 | self.opts.header_style = 'underlined' 44 | self.assertEqual( 45 | attr('underlined'), self.opts.header_style) 46 | 47 | self.assertEqual( 48 | 'bold', self.opts._check_attr('notattr')) 49 | self.assertEqual( 50 | attr('blink'), self.opts._check_attr('blink')) 51 | 52 | def test_check_attr_key_error(self): 53 | color = self.opts._check_attr('notarr') 54 | self.assertEqual('bold', color, 55 | msg='When unavailable attribute is given this function should return "bold"') 56 | 57 | 58 | class TestChart(unittest.TestCase): 59 | 60 | @classmethod 61 | def setUpClass(cls): 62 | cls.chart = Chart() 63 | 64 | def test_chart_init(self): 65 | # Test that if no Options arg given new is created 66 | self.assertIsInstance(self.chart.options, Options) 67 | 68 | 69 | class TestHorizontalBarChart(unittest.TestCase): 70 | """Test Horizontal Bar Chart printing""" 71 | 72 | @classmethod 73 | def setUpClass(cls): 74 | cls.horizontal_chart = HorizontalBarChart() 75 | 76 | @unittest.mock.patch('sys.stdout', new_callable=io.StringIO) 77 | def assert_stdout(self, expected_output, mock_stdout): 78 | self.horizontal_chart.chart( 79 | title='Test Title', 80 | pre_graph_text='This looks sweet', 81 | post_graph_text=None, 82 | footer=None, 83 | maximum=10, 84 | current=5 85 | ) 86 | 87 | self.assertEqual(mock_stdout.getvalue(), expected_output) 88 | 89 | def test_chart(self): 90 | try: 91 | compare = f"{stylize('Test Title', fg('red') + attr('bold'))}\n" \ 92 | + f"{stylize('This looks sweet', fg('white'))}\n" \ 93 | + f"{stylize('███████████████████▒░░░░░░░░░░░░░░░░░░░', fg('white'))} \n" 94 | self.assert_stdout(compare) 95 | except Exception as e: 96 | self.fail(f"Exception occured when trying to print horizontal chart {e}") 97 | 98 | def test_draw_horizontal_bar_half_full(self): 99 | try: 100 | compare = '███████████████████▒░░░░░░░░░░░░░░░░░░░' 101 | self.assertEqual( 102 | compare, self.horizontal_chart.draw_horizontal_bar(10, 5)) 103 | except Exception as e: 104 | self.fail(f'Exception occured when trying to draw half full bar chart {e}') 105 | 106 | def test_draw_horizontal_bar_empty(self): 107 | try: 108 | compare_02 = '▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░' 109 | self.assertEqual( 110 | compare_02, self.horizontal_chart.draw_horizontal_bar(10, 0)) 111 | except Exception as e: 112 | self.fail(f'Exception occured when trying to draw an empty bar chart {e}') 113 | 114 | def test_draw_horizontal_bar_full(self): 115 | try: 116 | compare_02 = '██████████████████████████████████████▒' 117 | self.assertEqual( 118 | compare_02, self.horizontal_chart.draw_horizontal_bar(10, 10)) 119 | except Exception as e: 120 | self.fail(f'Exception occured when trying to draw a full bar chart {e}') 121 | 122 | 123 | if __name__ == '__main__': 124 | unittest.main() 125 | -------------------------------------------------------------------------------- /tests/test_files.py: -------------------------------------------------------------------------------- 1 | # add path to the main package and test files.py 2 | if __name__ == '__main__': 3 | from __access import ADD_PATH 4 | ADD_PATH() 5 | 6 | import unittest 7 | import tempfile 8 | import os 9 | import warnings 10 | 11 | from vizexdf.files import DirectoryFiles 12 | 13 | 14 | class TestDirectoryFiles(unittest.TestCase): 15 | 16 | @classmethod 17 | def setUp(cls): 18 | cls.tmpd = tempfile.TemporaryDirectory() 19 | 20 | @classmethod 21 | def tearDown(cls): 22 | cls.tmpd.cleanup() 23 | 24 | def test_get_usage_files(self): 25 | try: 26 | for i in range(5): 27 | tempfile.NamedTemporaryFile(prefix='TEST_', delete=False, dir=self.tmpd.name) 28 | df = DirectoryFiles(path=self.tmpd.name) 29 | usage = df.get_usage() 30 | self.assertEqual(5, len(usage), 31 | msg='There should have been 5 items in the list') 32 | for i in usage: 33 | self.assertTrue('TEST' in i[0]) 34 | except Exception as e: 35 | self.fail(f'Exception occured when trying to get_usage for tmp files {e}') 36 | 37 | def test_get_usage_files_empty_dir(self): 38 | try: 39 | df = DirectoryFiles(path=self.tmpd.name) 40 | usage = df.get_usage() 41 | self.assertListEqual([], usage, 42 | msg='For empty folder method should return an empty list') 43 | except Exception as e: 44 | self.fail(f'Exception occured when tried to get_usage of an empty directory {e}') 45 | 46 | def test_get_dir_size(self): 47 | try: 48 | warnings.filterwarnings('ignore') # suppress tempfile warnings 49 | # Nest folders three times 50 | tmp = tempfile.TemporaryDirectory(dir=self.tmpd.name) 51 | nested_tmp = tempfile.TemporaryDirectory(dir=tmp.name) 52 | nested_nested_tmp = tempfile.TemporaryDirectory(dir=nested_tmp.name) 53 | # Create 10 files zie of 1024 * 1024 bytes each 54 | for i in range(10): 55 | f = tempfile.NamedTemporaryFile(mode='wb', 56 | dir=nested_nested_tmp.name, 57 | delete=False) 58 | f.write(b'0' * 1024 * 1024) 59 | size = DirectoryFiles().get_dir_size(self.tmpd.name) 60 | self.assertEqual(10485760, size, 61 | msg='Total size of directory should be 10485760') 62 | except Exception as e: 63 | self.fail(f'Exception occured when tried to get_size nested files {e}') 64 | 65 | def test_get_dir_size_empty_dir(self): 66 | try: 67 | size = DirectoryFiles().get_dir_size(self.tmpd.name) 68 | self.assertEqual(0, size, msg='Size 0 was expected') 69 | except Exception as e: 70 | self.fail(f'Exception occured when tried to get_size for an empty folder {e}') 71 | 72 | def test_get_dir_size_broken_symlink(self): 73 | os.symlink( 74 | '/this/path/could/not/possibly/exist/like/evar', 75 | os.path.join(self.tmpd.name, 'broken-link'), 76 | ) 77 | size = DirectoryFiles().get_dir_size(self.tmpd.name) 78 | self.assertEqual(0, size) 79 | 80 | def test_sort_data(self): 81 | try: 82 | data = [ 83 | ['folder1', 1927317893, 333, 'dir'], 84 | ['file1', 3419273173817333, 9081231, 'file'], 85 | ['folder2', 921231938192, 12313744908, 'dir'], 86 | ['file2', 1238193123, 22, 'file'], 87 | ['x-file', 34192773817333, 445522, 'x-files'] 88 | ] 89 | 90 | DirectoryFiles().sort_data(data, 'type', True) 91 | self.assertEqual('x-file', data[0][0]) 92 | 93 | DirectoryFiles().sort_data(data, 'name', True) 94 | self.assertListEqual(['x-file', 34192773817333, 445522, 'x-files'], data[0]) 95 | 96 | DirectoryFiles().sort_data(data, 'size', False) 97 | self.assertEqual(22, data[0][2]) 98 | 99 | DirectoryFiles().sort_data(data, 'dt', True) 100 | self.assertListEqual(['file1', 3419273173817333, 9081231, 'file'], data[0]) 101 | except Exception as e: 102 | self.fail(f'Exception occured when trying to sort data {e}') 103 | 104 | @unittest.skip("UNDER CONSTRUCTION!") 105 | def test_decorate_dir(self): 106 | # TODO 107 | self.fail("TODO") 108 | 109 | @unittest.skip("UNDER CONSTRUCTION!") 110 | def test_decorate_file(self): 111 | # TODO 112 | self.fail("TODO") 113 | 114 | 115 | if __name__ == '__main__': 116 | unittest.main() 117 | -------------------------------------------------------------------------------- /vizex/vizexdu/charts.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Text Chart Coloring and Printing Functions 3 | ''' 4 | 5 | from math import ceil 6 | from colored import fg, attr, stylize 7 | 8 | 9 | class Options: 10 | """Options""" 11 | 12 | _graph_color = fg("white") 13 | _header_color = fg("red") 14 | _header_style = attr('bold') 15 | _text_color = fg("white") 16 | _fsymbol = "█" 17 | _msymbol = "▒" 18 | _esymbol = "░" 19 | 20 | def __init__(self): 21 | return 22 | 23 | @property 24 | def graph_color(self): 25 | """ Graph Color """ 26 | return self._graph_color 27 | 28 | @graph_color.setter 29 | def graph_color(self, color: str): 30 | self._graph_color = Options._check_color(color, self._graph_color) 31 | 32 | @property 33 | def header_color(self): 34 | """ Header Color """ 35 | return self._header_color 36 | 37 | @header_color.setter 38 | def header_color(self, color: str): 39 | self._header_color = Options._check_color(color, self._header_color) 40 | 41 | @property 42 | def header_style(self): 43 | """ header style """ 44 | return self._header_style 45 | 46 | @header_style.setter 47 | def header_style(self, style: str): 48 | self._header_style = Options._check_attr(style, self._header_style) 49 | 50 | @property 51 | def text_color(self): 52 | """ text color """ 53 | return self._text_color 54 | 55 | @text_color.setter 56 | def text_color(self, color: str): 57 | self._text_color = Options._check_color(color, self._text_color) 58 | 59 | @property 60 | def symbol(self): 61 | """ graph symbols to use """ 62 | return self._fsymbol, self._msymbol, self.esymbol 63 | 64 | @symbol.setter 65 | def symbol(self, symbol: str): 66 | if symbol: 67 | self._fsymbol = symbol 68 | self._msymbol = ">" 69 | self._esymbol = "-" 70 | else: 71 | self._fsymbol = "█" 72 | self._msymbol = "▒" 73 | self._esymbol = "░" 74 | 75 | @property 76 | def fsymbol(self): 77 | """ The full symbol """ 78 | return self._fsymbol 79 | 80 | @property 81 | def esymbol(self): 82 | """ The empty symbol """ 83 | return self._esymbol 84 | 85 | @property 86 | def msymbol(self): 87 | """ The middle symbol """ 88 | return self._msymbol 89 | 90 | @staticmethod 91 | def _check_color(color: str, default_color: str = "white"): 92 | """Checks if set color is valid""" 93 | try: 94 | return fg(color) 95 | except KeyError: 96 | return default_color 97 | 98 | @staticmethod 99 | def _check_attr(style: str, default_attr: str = "bold"): 100 | """Checks if set attribute is valid""" 101 | try: 102 | return attr(style) 103 | except KeyError: 104 | return default_attr 105 | 106 | 107 | class Chart: 108 | """Abstract base object for charts""" 109 | 110 | def __init__(self, options: Options = None): 111 | if options: 112 | self.options = options 113 | else: 114 | self.options = Options() 115 | 116 | 117 | class HorizontalBarChart(Chart): 118 | """ 119 | Create horizontal chart with user selected color and symbol 120 | """ 121 | 122 | def chart(self, 123 | title: str, 124 | pre_graph_text: str, 125 | post_graph_text: str, 126 | footer: str, 127 | maximum: int, 128 | current: int) -> None: 129 | """prints assembled chart to the terminal""" 130 | print( 131 | stylize(title, self.options.header_color + 132 | self.options.header_style) 133 | ) 134 | 135 | if pre_graph_text: 136 | print(stylize(pre_graph_text, self.options.text_color)) 137 | 138 | print( 139 | "%s" 140 | % stylize( 141 | self.draw_horizontal_bar(maximum, current), 142 | self.options.graph_color 143 | ), 144 | end=" ", 145 | ) 146 | if post_graph_text: 147 | print(stylize(post_graph_text, self.options.text_color)) 148 | else: 149 | print() 150 | 151 | if footer: 152 | print(stylize(footer, self.options.text_color)) 153 | 154 | def draw_horizontal_bar(self, maximum: int, current: int) -> str: 155 | """Draw a horizontal bar chart""" 156 | 157 | # Sanity check that numbers add up 158 | if current > maximum: 159 | current = maximum 160 | 161 | if current < 0: 162 | current = 0 163 | 164 | if maximum < 0: 165 | maximum = 0 166 | 167 | text_bar = "" 168 | usage = int((current / maximum) * 38) 169 | 170 | for _ in range(1, usage + 1): 171 | text_bar += self.options.fsymbol 172 | text_bar += self.options.msymbol 173 | 174 | for _ in range(1, 39 - usage): 175 | text_bar += self.options.esymbol 176 | 177 | # Check if the user set up graph color 178 | if "█" not in self.options.fsymbol: 179 | return f"[{text_bar}]" 180 | 181 | return text_bar 182 | 183 | 184 | if __name__ == "__main__": 185 | ch = HorizontalBarChart() 186 | ch.options.graph_color = 'cyan' 187 | ch.options.text_color = 'yellow' 188 | ch.options.header_color = 'red' 189 | ch.options.header_style = 'underlined' 190 | 191 | ch.chart( 192 | title="Test Content", 193 | maximum=100, 194 | current=32, 195 | pre_graph_text="Lorem: Sit ea dolore ad accusantium", 196 | post_graph_text="Good job!", 197 | footer="This concludes our test", 198 | ) 199 | -------------------------------------------------------------------------------- /vizex/tools.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Utility functions for vizex/vizexdf 3 | ''' 4 | 5 | import os 6 | import re 7 | import json 8 | import time 9 | import pandas as pd 10 | 11 | from typing import Optional, Match 12 | from math import ceil 13 | from colored import fg, attr, stylize 14 | from dataclasses import dataclass 15 | 16 | 17 | @dataclass(order=True) 18 | class DecoratedData: 19 | """ 20 | Custom class to compare numerical data for sorting 21 | which appears in the stylized representation of a string. 22 | """ 23 | size: int 24 | to_string: str 25 | 26 | def __str__(self): 27 | """String representation of the class""" 28 | return self.to_string 29 | 30 | 31 | def bytes_to_human_readable(bytes_in: int, suffix='B') -> str: 32 | """ 33 | Converts bytes into the appropriate human 34 | readable unit with a relevant suffix. 35 | 36 | Args: 37 | bytes_in (int): to convert 38 | suffix (str, optional): suffix of a size string. 39 | Defaults to 'b'. 40 | 41 | Returns: 42 | str: size in a human readable 43 | """ 44 | for unit in ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z']: 45 | if abs(bytes_in) < 1024.0: 46 | return f'{bytes_in:3.1f} {unit}{suffix}' 47 | bytes_in /= 1024.0 48 | return f'{bytes_in:.1f} {"Y"}{suffix}' 49 | 50 | 51 | def ints_to_human_readable(disk: dict) -> dict: 52 | """Converts the dictionary of integers 53 | into the human readable size formats. 54 | 55 | Args: 56 | disk [dict]: of large byte numbers 57 | 58 | Returns: 59 | dict: same dict but in human readable format 60 | """ 61 | result = {} 62 | for key in disk: 63 | try: 64 | result[key] = bytes_to_human_readable(disk[key]) 65 | except Exception: 66 | result[key] = disk[key] # If not able to convert return input back 67 | return result 68 | 69 | 70 | def printml(folder: list, cols: int = 1) -> None: 71 | """Prints multiline strings side by side. 72 | 73 | Args: 74 | folder (list): list of folders to print 75 | cols (int, optional): On how many lines should it be printed. 76 | Defaults to 1. 77 | """ 78 | size = len(folder) 79 | incr = ceil(size / cols) 80 | end, start = 0, 0 81 | while True: 82 | if end >= size: 83 | break 84 | end += incr 85 | # Check if the end index exceeds the last index 86 | if end > size: 87 | end = size 88 | lines = [folder[i].splitlines() for i in range(start, end)] 89 | for line in zip(*lines): 90 | print(*line, sep=' ') 91 | print() 92 | start = end 93 | 94 | 95 | def create_usage_warning(usage_pct: float, 96 | red_flag: int, 97 | yellow_flag: int) -> str: 98 | """Create disk usage percent with warning color 99 | 100 | Args: 101 | usage_pct (float): of a given space 102 | red_flag (int): threshold that decides if print should be red 103 | yellow_flag (int): threshold that decides if print is yellow 104 | 105 | Returns: 106 | str: stylized warning string 107 | """ 108 | if usage_pct < 0: 109 | usage_pct = 0 110 | 111 | if usage_pct > 100: 112 | usage_pct = 100 113 | 114 | use = str(usage_pct) + '% used' 115 | 116 | if usage_pct >= red_flag: 117 | return f"{stylize(use, attr('blink') + fg(9))}" 118 | if usage_pct >= yellow_flag: 119 | return f"{stylize(use, fg(214))}" 120 | return f"{stylize(use, fg(82))}" 121 | 122 | 123 | def save_to_csv(data: dict, 124 | filename: str, 125 | orient: str = 'index') -> None: 126 | """Outputs disks/partitions data as a CSV file 127 | 128 | Args: 129 | data (dict): to be saved to a CSV file 130 | filename (str): to name a saved file 131 | orient (str, optional): how lines are saved. 132 | Defaults to 'index'. 133 | 134 | Raises: 135 | NameError: if filename doesn't contain .csv at the end 136 | """ 137 | if filename.split(".")[-1].lower() == 'csv': 138 | df = pd.DataFrame.from_dict(data, orient=orient) 139 | df.to_csv(filename, mode='a') 140 | else: 141 | raise NameError('Please include ".csv" in the filename') 142 | 143 | 144 | def save_to_json(data: dict, 145 | filename: str, 146 | indent: int = 4) -> None: 147 | """Saves disk/partitions data as a JSON file 148 | 149 | Args: 150 | data (dict): to be saved to a JSON file 151 | filename (str): to name a saved file 152 | indent (int, optional): of each new line. 153 | Defaults to 4. 154 | 155 | Raises: 156 | NameError: if filename doesn't contain .json at the end 157 | """ 158 | if filename.split(".")[-1].lower() == 'json': 159 | with open(filename, "w") as file: 160 | json.dump(data, file, indent=indent) 161 | else: 162 | raise NameError('Please include ".json" in the filename') 163 | 164 | 165 | def append_to_bash(alias: str, line: str) -> None: 166 | """ 167 | Appends terminal command line as an alias in .bashrc for reuse 168 | 169 | Args: 170 | alias[str]: To set up in the bash 171 | line[str]: line which will be assigned to an alias 172 | """ 173 | bash = os.path.expanduser("~") + '/.bash_aliases' 174 | print(remove_if_exists(alias, bash)) 175 | with open(bash, 'a+') as file: 176 | file.write('alias ' + alias + f"='{line}'") 177 | 178 | 179 | def remove_if_exists(alias: str, path: str) -> None: 180 | """ 181 | Removes if the given line/alias exists in a given file 182 | 183 | Args: 184 | alias (str): to check if exists in bash 185 | path (str): path to the file to read 186 | """ 187 | if not os.path.exists(path): 188 | return 189 | with open(path, "r") as file: 190 | lines = file.readlines() 191 | 192 | with open(path, "w") as file: 193 | for line in lines: 194 | # We only write back lines which is not alias 195 | if f'alias {alias}' not in line.strip("\n"): 196 | file.write(line) 197 | 198 | 199 | def normalize_date(formatting: str, date: int) -> str: 200 | """ 201 | Converts date from nanoseconds to the human readable form 202 | 203 | Args: 204 | format (str): example %h-%d-%Y for mm-dd-yyyy 205 | date (int): date in nanoseconds 206 | 207 | Returns: 208 | str: Human readable format of a date 209 | """ 210 | return time.strftime(formatting, time.localtime(date)) 211 | 212 | 213 | def find_word(word, src) -> Optional[Match[str]]: 214 | """Find word in a src using regex""" 215 | return re.compile(r'\b({0})\b'.format(word), 216 | flags=re.IGNORECASE).search(src) 217 | 218 | 219 | if __name__ == '__main__': 220 | file1 = DecoratedData(55456, '54.2 kB') 221 | file2 = DecoratedData(123233419, '117.5 MB') 222 | print(f'{file1} is less than {file2} : {file1 < file2}') 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | 5 |
6 |
7 |
8 |

9 | 10 |

11 | 12 | 13 | 14 |

15 | 16 | 17 | 18 | **vizex** is the terminal program for the UNIX/Linux systems which helps the user to visualize the disk space usage for every partition and media on the user's machine. **vizex** is highly customizable and can fit any user's taste and preferences. 19 | 20 | **vizexdf** is a new feature that allows to organize and print directory data in the terminal. With a recent release 2.0.4 `vizexdf` does directory parsing using Asynchronous execution, which improved runtime performance by over 400%. 21 | 22 | You can check [full release history here.](https://github.com/bexxmodd/vizex/wiki/Release-History) 23 | 24 | 25 |
26 |
27 | 28 | # Installation 29 | 30 | ## pip 31 | 32 | **vizex** can be installed through your terminal and requires `Python >= 3.9` and the `pip package manager`. Here's [how to set up Python](https://realpython.com/installing-python/) on your machine. 33 | 34 | 35 | If you don't have PyPackage Index (PyPI or just `pip`) installed, [Here's the guide on how to install it](https://www.tecmint.com/install-pip-in-linux/). Install **vizex** with the following command: 36 | ``` 37 | pip install vizex 38 | ``` 39 | 40 | If you already have vizex install you'll need to upgrade it: 41 | ``` 42 | pip install vizex --upgrade 43 | ``` 44 | 45 | If you encounter any problems during installation, know that some `vizex` dependencies require a Python 3 development package on Linux and you need to set up that manually. 46 | 47 | For **Debian** and other derived systems (Ubuntu, Mint, Kali, etc.) you can install this with the following command: 48 | ``` 49 | sudo apt-get install python3-dev 50 | ``` 51 | 52 | For **Red Hat** derived systems (Fedora, RHEL, CentOS, etc.) you can install this with the following command: 53 | ``` 54 | sudo yum install python3-devel 55 | ``` 56 | 57 | 58 | ## AUR 59 | **vizex** is available as a package on the AUR (Arch user repository), distributions with AUR support may install directly from the command line using their favorite `pacman` helper. 60 | 61 | Example using `yay`: 62 | ``` 63 | yay -S vizex 64 | ``` 65 | 66 | # How it Works 67 | 68 | After installing you can use two terminal commands: `vizex` to display the disk usage/space and `vizexdf`, which will print the data of a current working directory with sizes, file types and last modified date. 69 | 70 | This will graphically display disk space and usage: 71 | 72 | ``` 73 | vizex 74 | ``` 75 | 76 | ![demo](https://i.imgur.com/OiPWWJf.png) 77 | 78 | ----- 79 | 80 | ``` 81 | vizexdf 82 | ``` 83 | 84 | ![demo1](https://i.imgur.com/At7MFgu.png) 85 | 86 | ---- 87 | 88 |
89 | 90 | _new feature_: 91 | 92 | ## vizextree 93 | 94 | you can now print tree of directory structure with the level you want. For example tree with level 1 only 95 | 96 | 97 | By default level is set to 3 and path is a current path. But you can manually supply path, by just typing path you want to generate tree for, and using `-l` option to instruct how many levels of directories you want to print. 98 | 99 | For example: 100 | 101 | ``` 102 | vizextree . -level 1 103 | ``` 104 | ![tree](https://i.imgur.com/i4rXcx6.png) 105 | 106 | ----- 107 | ## vizex 108 | 109 | The best part is that you can modify the colors and style of the display to your preferences with the following commands. For the example above command has excluded two partitions. You can also do give the following options: 110 | 111 | ``` 112 | -d --header 113 | -s --style 114 | -t --text 115 | -g --graph 116 | ``` 117 | 118 | Display additional details, like `fstype` and `mount point`, for each partition: 119 | ``` 120 | vizex --details 121 | ``` 122 | ![details-img](https://i.imgur.com/ThILQMo.png) 123 | 124 | If you are interested in visualizing a specific path run with the following command: 125 | ``` 126 | vizex --path 127 | ``` 128 | 129 | You can also exclude any combination of partitions/disks with multiple `-X` or for verbose `--exclude` option: 130 | ``` 131 | vizex -X -X ... 132 | ``` 133 | 134 | You can also save the partitions full information in `csv` or in `json` file, just by calling `--save` option with the full path where you want your output to be saved: 135 | ``` 136 | vizex --save "/home/user/disk_info.json" 137 | ``` 138 | 139 | And if you are on laptop you can even call your battery information with simple argument: 140 | ``` 141 | vizex battery 142 | ``` 143 | 144 | For a full list of the available options please check: 145 | ``` 146 | vizex --help 147 | ``` 148 | ----- 149 | 150 | ## vizexdf 151 | 152 | You can include hidden files and folders by calling `--all` or `-a` for short and sort the output with `--sort` or `-s` for short based on name, file type, size, or date. Put it in descending order with the `--desc` option. 153 | 154 | You can chain multiple options but make sure to put the `-s` at the end as it requires a text argument. Example: 155 | 156 | ``` 157 | vizexdf -ads name 158 | ``` 159 | 160 | This will print current directory data sorted by name and in descending order and will include hidden files. 161 | 162 | 163 | **Lastly, you save all the modifications by adding -l at the end of the command**: 164 | 165 | ``` 166 | vizex -d red -t blue --details -l 167 | ``` 168 | 169 | The next time you call `vizex` / `vizexdf` it will include all the options listed in the above command. If you decided to change the default calling command for vizex/vizexdf just include `-l` and it will be overwritten 170 | 171 | 172 | If you want to contribute to the project you are more than welcome! But first, make sure all the tests run after you fork the project and before the pull request. First, run the `access.py`, that way `tests` folder will obtain a path to the `main` folder and you can run all the tests. 173 | 174 | You can get the full set of features by calling `--help` option with command. 175 | 176 | ----- 177 | 178 | ---- 179 | ## Special Thanks to the Contributors! 180 |

181 | 182 | 183 | 184 |

185 | 186 | ------ 187 | ------ 188 | ## Follow Me on Social Media 189 |

190 | 191 | twitter 192 | 193 | 194 | linkedin 195 | 196 | 197 | github 198 | 199 |

200 | 201 | 202 | Repo is distributed under the MIT license. Please see the `LICENSE` for more information. 203 | -------------------------------------------------------------------------------- /vizex/vizexdf/files.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Collect and organize data about files and directories 3 | ''' 4 | 5 | import os 6 | import sys 7 | import concurrent.futures 8 | import magic 9 | 10 | from tabulate import tabulate 11 | from colored import fg, stylize 12 | from dataclasses import dataclass 13 | from tools import bytes_to_human_readable, normalize_date, DecoratedData 14 | 15 | 16 | @dataclass 17 | class DirectoryFiles: 18 | """ 19 | Creates the tabular listing of all the folders and files in a given path. 20 | This module can be seen as a substitute for du/df Linux terminal commands. 21 | """ 22 | 23 | path: str = os.getcwd() 24 | show_hidden: bool = False 25 | sort_by: str = None 26 | desc: bool = False 27 | 28 | @staticmethod 29 | def get_dir_size(start_path: str) -> int: 30 | """ 31 | Calculates the cumulative size of a given directory. 32 | 33 | Args: 34 | start_path (str): a path to the folder that's 35 | the cumulative file size is calculated. 36 | 37 | Returns: 38 | int: the size of all files in a given path in bytes 39 | """ 40 | total_size = 0 41 | for dirpath, _, filenames in os.walk(start_path): 42 | for file in filenames: 43 | fpath = os.path.join(dirpath, file) 44 | try: 45 | total_size += os.path.getsize(fpath) 46 | except FileNotFoundError: 47 | # Could be a broken symlink or some other weirdness. 48 | # Trap the error here so that the directory can continue 49 | # to be successfully processed. 50 | continue 51 | return total_size 52 | 53 | @staticmethod 54 | def is_hidden(entry: str) -> bool: 55 | """Check if given entry is a hidden file or folder""" 56 | return entry.name.startswith('.') 57 | 58 | @classmethod 59 | def sort_data(cls, data: list, by: str, desc: bool) -> None: 60 | """ 61 | Sorts data in place, which is inputted as a list, 62 | based on a given index(key) and reverses if 63 | user has selected descending order. 64 | 65 | Args: 66 | data: data with several columns 67 | by: key as a string to sort by 68 | desc: to sort in descending order 69 | """ 70 | if by == 'name': 71 | column = 0 72 | elif by == 'dt': 73 | column = 1 74 | elif by == 'size': 75 | column = 2 76 | else: 77 | column = -1 78 | 79 | # Sort data inplace based on user's choice 80 | data.sort(key=lambda x: x[column], reverse=desc) 81 | 82 | @classmethod 83 | def _decorate_dir_entry(cls, entry) -> tuple: 84 | """ 85 | Decorates given entry for a directory. Decorate means that creates 86 | a colored representation of a name of the entry, grabs 87 | the date it was last modified and size in bytes and decorates. 88 | collects everything and returns as a list. 89 | """ 90 | 91 | # Gives orange color to the string & truncate to 32 chars 92 | name = entry.split('/')[-1] 93 | current = [stylize("■ " + name[:33] + "/", fg(202))] 94 | 95 | # Get date and convert in to a human readable format 96 | date = os.stat(entry).st_mtime 97 | current.append( 98 | DecoratedData(date, normalize_date('%h %d %Y %H:%M', date)) 99 | ) 100 | 101 | # recursively calculates the total size of a folder 102 | byte = DirectoryFiles().get_dir_size(entry) 103 | current.append( 104 | DecoratedData(byte, bytes_to_human_readable(byte)) 105 | ) 106 | 107 | current.append('-') # add directory type identifier 108 | return tuple(current) 109 | 110 | @classmethod 111 | def _decorate_file_entry(cls, entry) -> tuple: 112 | """ 113 | Decorates given entry for a file. By decorate it means that creates 114 | a colored representation of a name of the entry, grabs 115 | the date it was last modified and size in bytes and decorates, 116 | determines file type. collects everything and returns as a list. 117 | """ 118 | 119 | # Gives yellow color to the string & truncate to 32 chars 120 | name = entry.split('/')[-1] 121 | current = [stylize("» " + name[:33], fg(226))] 122 | 123 | # Convert last modified time (which is in nanoseconds) 124 | date = os.stat(entry).st_mtime 125 | current.append( 126 | DecoratedData(date, normalize_date('%h %d %Y %H:%M', date)) 127 | ) 128 | 129 | byte = os.stat(entry).st_size 130 | current.append( 131 | DecoratedData(byte, bytes_to_human_readable(byte)) 132 | ) 133 | 134 | # Evaluate the file type 135 | current.append( 136 | magic.from_file(entry, mime=True) 137 | ) 138 | return tuple(current) 139 | 140 | def get_usage(self) -> list: 141 | """ 142 | Collects the data for a given path like the name of a file/folder 143 | and calculates its size if it's a directory, otherwise 144 | just grabs a file size. If the current entry in a given 145 | path is a file method evaluates its type. Finally, gives 146 | us the date when the given file/folder was last modified. 147 | 148 | Program runs asynchronously using multiple threads or separate process 149 | 150 | Returns: 151 | list: which is a collection of each entry 152 | (files and folders) in a given path. 153 | """ 154 | data = [] 155 | with concurrent.futures.ThreadPoolExecutor() as executor: 156 | with os.scandir(self.path) as entries: 157 | for entry in entries: 158 | try: 159 | current = [] 160 | 161 | # Deal with hidden files and folders 162 | if self.is_hidden(entry) and not self.show_hidden: 163 | continue 164 | 165 | if entry.is_file(): 166 | current = executor.submit( 167 | self._decorate_file_entry, entry.path) 168 | elif entry.is_dir(): 169 | current = executor.submit( 170 | self._decorate_dir_entry, entry.path) 171 | 172 | data.append(current.result()) 173 | except Exception as e: 174 | print(f"Bad Entry ::> {e}", file=sys.stderr) 175 | return data 176 | 177 | def print_tabulated_data(self) -> tabulate: 178 | """ 179 | Creates the tabular representation of the data. 180 | Adds headers and sorts the list's data as rows. 181 | 182 | Returns: 183 | tabulate: a tabulated form of the current 184 | the directory's folders and files. 185 | """ 186 | headers = [ 187 | 'name', 188 | 'last modified (dt)', 189 | 'size', 190 | 'type' 191 | ] 192 | result = self.get_usage() 193 | if self.sort_by: 194 | self.sort_data(result, self.sort_by, self.desc) 195 | print(tabulate(result, headers, tablefmt="rst")) 196 | 197 | 198 | if __name__ == '__main__': 199 | files = DirectoryFiles(sort_by='type', desc=True) 200 | files.print_tabulated_data() 201 | -------------------------------------------------------------------------------- /vizex/vizexdu/disks.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Personalize and visualize the disk space usage in the terminal 3 | ''' 4 | 5 | import psutil 6 | import platform 7 | 8 | from .charts import Chart, HorizontalBarChart, Options 9 | from tools import ints_to_human_readable 10 | from tools import save_to_csv, save_to_json, create_usage_warning 11 | 12 | 13 | class DiskUsage: 14 | """ 15 | Class to retrieve, organize, and print disk usage/disk space data 16 | """ 17 | 18 | def __init__(self, 19 | path: str = "", 20 | exclude: list = [], 21 | details: bool = False, 22 | every: bool = False) -> None: 23 | self.path = path 24 | self.exclude = exclude 25 | self.details = details 26 | self.every = every 27 | self._platform = platform.system() # Check for platform 28 | 29 | def print_charts(self, options: Options = None) -> None: 30 | """ 31 | Prints the charts based on user selection of colors and what to print 32 | 33 | Args: 34 | options (Options): colors and symbols for printing 35 | """ 36 | if not options: 37 | options = Options() 38 | 39 | if self.path: 40 | parts = DiskUsage.grab_specific_partition(self.path[0]) 41 | else: 42 | parts = self.grab_partitions(exclude=self.exclude, 43 | every=self.every) 44 | 45 | chart = HorizontalBarChart(options) 46 | for partname in parts: 47 | self.print_disk_chart(chart, partname, parts[partname]) 48 | 49 | def print_disk_chart( 50 | self, chart: Chart, partname: str, part: dict 51 | ) -> None: 52 | """Prints the disk data int terminal as a chart 53 | 54 | Args: 55 | ch (Chart): to print 56 | partname (str): partition title 57 | part (dict): partition data to be visualized 58 | """ 59 | pre_graph_text = self.create_stats(part) 60 | 61 | footer = None 62 | if self.details: 63 | footer = DiskUsage.create_details_text(part) 64 | 65 | maximum = part["total"] 66 | current = part["used"] 67 | post_graph_text = create_usage_warning( 68 | part['percent'], 80, 60) 69 | 70 | chart.chart( 71 | post_graph_text=post_graph_text, 72 | title=partname, 73 | pre_graph_text=pre_graph_text, 74 | footer=footer, 75 | maximum=maximum, 76 | current=current, 77 | ) 78 | print() 79 | 80 | @staticmethod 81 | def grab_root() -> dict: 82 | """ 83 | Grab the data for the root partition 84 | 85 | return: 86 | (dict) with column titles as keys 87 | """ 88 | return { 89 | "total": psutil.disk_usage("/").total, 90 | "used": psutil.disk_usage("/").used, 91 | "free": psutil.disk_usage("/").free, 92 | "percent": psutil.disk_usage("/").percent, 93 | "fstype": psutil.disk_partitions(all=False)[0][2], 94 | "mountpoint": "/", 95 | } 96 | 97 | def grab_partitions(self, 98 | exclude: list, 99 | every: bool) -> dict: 100 | """Grabs data for all the partitions. 101 | 102 | Args: 103 | exclude (list): of partitions to exclude 104 | every (bool): if all the partitions should be grabbed 105 | """ 106 | disks = {} 107 | 108 | # If we don't need every part we grab root separately 109 | if not every and self._platform != 'Windows': 110 | disks['root'] = DiskUsage.grab_root() 111 | disk_parts = psutil.disk_partitions(all=every) 112 | 113 | for disk in disk_parts[1:]: 114 | if (DiskUsage._valid_mountpoint(disk) and 115 | not DiskUsage._needs_excluded(disk, exclude)): 116 | try: 117 | if psutil.disk_usage(disk[1]).total > 0: 118 | disks[disk[1].split("/")[-1]] = { 119 | "total": psutil.disk_usage(disk[1]).total, 120 | "used": psutil.disk_usage(disk[1]).used, 121 | "free": psutil.disk_usage(disk[1]).free, 122 | "percent": psutil.disk_usage(disk[1]).percent, 123 | "fstype": disk.fstype, 124 | "mountpoint": disk.mountpoint, 125 | } 126 | except Exception as e: 127 | print(e) 128 | continue 129 | return disks 130 | 131 | @staticmethod 132 | def _valid_mountpoint(disk) -> bool: 133 | """Checks if it's an invalid mountpoint""" 134 | return (not disk.device.startswith("/dev/loop") and 135 | not disk.mountpoint.startswith("/tmp")) 136 | 137 | @staticmethod 138 | def _needs_excluded(disk, exclude: list) -> bool: 139 | """Checks if given disk needs to be excluded from a print""" 140 | return disk[1].split("/")[-1] in exclude 141 | 142 | @staticmethod 143 | def grab_specific_partition(disk_path: str) -> dict: 144 | """ 145 | Grabs data for the partition of the user specified path 146 | 147 | Args: 148 | disk_path (str): to the partition to grab 149 | """ 150 | disks = {disk_path: { 151 | "total": psutil.disk_usage(disk_path).total, 152 | "used": psutil.disk_usage(disk_path).used, 153 | "free": psutil.disk_usage(disk_path).free, 154 | "percent": psutil.disk_usage(disk_path).percent, 155 | "fstype": "N/A", 156 | "mountpoint": "N/A", 157 | }} 158 | return disks 159 | 160 | @staticmethod 161 | def create_details_text(disk: dict) -> str: 162 | """ 163 | Creates a string representation of a disk 164 | 165 | Args: 166 | disk (dict): text to print 167 | """ 168 | return f"fstype={disk['fstype']}\tmountpoint={disk['mountpoint']}" 169 | 170 | @staticmethod 171 | def create_stats(disk: dict) -> str: 172 | """ 173 | Creates statistics as string for a disk 174 | 175 | Args: 176 | disk (dict): stats to print 177 | """ 178 | r = ints_to_human_readable(disk) 179 | return f"Total: {r['total']}\t Used: {r['used']}\t Free: {r['free']}" 180 | 181 | def save_data(self, filename: str) -> None: 182 | """ 183 | Outputs disks/partitions data as a CSV file 184 | 185 | Args: 186 | filename (str): for the saved file 187 | """ 188 | data = self.grab_partitions(self.exclude, self.every) 189 | if (file_type := filename.split(".")[-1].lower()) == 'csv': 190 | save_to_csv(data, filename) 191 | elif file_type == 'json': 192 | save_to_json(data, filename) 193 | else: 194 | raise NameError("Not supported file type, please indicate " 195 | + ".CSV or .JSON at the end of the filename") 196 | 197 | 198 | def main(): 199 | """Main function running the vizex program""" 200 | self = DiskUsage() 201 | parts = self.grab_partitions(exclude=[], every=False) 202 | 203 | for partname, part in parts.items(): 204 | chart = HorizontalBarChart() 205 | title = (partname,) 206 | pre_graph_text = DiskUsage.create_stats(part) 207 | footer = DiskUsage.create_details_text(part) 208 | maximum = part["total"] 209 | current = part["used"] 210 | post_graph_text = create_usage_warning( 211 | part['percent'], 80, 60) 212 | 213 | chart.chart( 214 | post_graph_text=post_graph_text, 215 | title=title[0], 216 | pre_graph_text=pre_graph_text, 217 | footer=footer, 218 | maximum=maximum, 219 | current=current, 220 | ) 221 | print() 222 | 223 | 224 | if __name__ == "__main__": 225 | main() 226 | -------------------------------------------------------------------------------- /vizex/cli.py: -------------------------------------------------------------------------------- 1 | # Command line interface for VIZEX ,VIZEXdf and VIZEXtree 2 | 3 | import click 4 | import sys 5 | 6 | from tools import append_to_bash 7 | from vizexdu.disks import DiskUsage 8 | from vizexdu.battery import Battery 9 | from vizexdu.charts import Options 10 | from vizexdu.cpu import CPUFreq 11 | from vizexdf.files import DirectoryFiles 12 | from vizextree.viztree import construct_tree 13 | 14 | 15 | # ----- vizextree options and arguments ----- 16 | @click.version_option('2.1.1', message='%(prog)s version %(version)s') 17 | @click.command(options_metavar='[options]') 18 | @click.argument( 19 | 'path', 20 | type=click.Path(exists=True), 21 | default='.', 22 | metavar='[path]' 23 | ) 24 | @click.option( 25 | 'level', '-l', 26 | type=int, 27 | default=3, 28 | help="How many levels of Directory Tree to be printed (By Default it's 3)" 29 | ) 30 | def print_tree(path: str, level: int) -> None: 31 | """ 32 | \b 33 | 34 | __ _(_)_________ _| |_ _ __ ___ ___ 35 | \ \ / / |_ / _ \ \/ / __| '__/ _ \/ _ \ 36 | \ V /| |/ / __/> <| |_| | | __/ __/ 37 | \_/ |_/___\___/_/\_\\__|_| \___|\___| 38 | 39 | Made by: Beka Modebadze 40 | 41 | 42 | If you want to print the directory tree run vizextree -path -level 43 | the level of how many child directory/files you want to be printed. 44 | 45 | Example: 46 | 47 | vizextree -l 2 48 | 49 | This'll print a directory tree of current working directory for two levels 50 | """ 51 | construct_tree(path, level) 52 | 53 | 54 | # ----- vizexdf options and arguments ----- 55 | @click.version_option('2.1.1', message='%(prog)s version %(version)s') 56 | @click.command(options_metavar='[options]') 57 | @click.argument( 58 | 'path', 59 | type=click.Path(exists=True), 60 | default='.', 61 | metavar='[path]' 62 | ) 63 | @click.option( 64 | '-s', '--sort', 65 | type=click.Choice(['type', 'size', 'name', 'dt']), 66 | default=None, 67 | help='Sort table with one of four given columns' 68 | ) 69 | @click.option( 70 | '-a', '--all', 71 | is_flag=True, 72 | help='Include hidden files and folders' 73 | ) 74 | @click.option( 75 | '-d', '--desc', 76 | is_flag=True, 77 | help='Sort columns in descending order') 78 | @click.option( 79 | '-l', '--alias', 80 | is_flag=True, 81 | help='Store customized terminal command for vizexdf as an alias so ' 82 | + 'you don\'t have to repeat the line everytime.' 83 | + '<-l> should always be the last command in the line' 84 | ) 85 | def dirs_files(sort: str, all: str, desc: str, path: str, alias: str) -> None: 86 | """ 87 | \b 88 | ██╗ ██╗██╗███████╗███████╗██╗ ██╗ _ __ 89 | ██║ ██║██║╚══███╔╝██╔════╝╚██╗██╔╝ __| |/ _| 90 | ██║ ██║██║ ███╔╝ █████╗ ╚███╔╝ / _` | |_ 91 | ╚██╗ ██╔╝██║ ███╔╝ ██╔══╝ ██╔██╗ | (_| | _| 92 | ╚████╔╝ ██║███████╗███████╗██╔╝ ██╗ \__,_|_| 93 | ╚═══╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ 94 | Made by: Beka Modebadze 95 | 96 | The command-line program `vizexdf` is a branch of the `vizex` that 97 | displays directories and files information in a tabular form. 98 | 99 | `vizexdf` Prints data of current working path in a tabular form. 100 | You can pass a path for a specific directory you want to print. 101 | 102 | Example: vizexdf /home/bexx/test 103 | 104 | You can also chain options for --all --desc --sort. 105 | 106 | Example: vizexdf -ads name 107 | 108 | Here `vizexdf` will print 'all' (-a) files and directories 109 | and 'sort' (-s) them by 'name' in 'descending' (-d) order. 110 | 111 | 112 | This'll sort in descending order by name and show all the hidden files & folders. 113 | !Just make sure 's' is placed at the end of the options chain! 114 | """ 115 | if alias: # Set vizexdf as alias 116 | line = 'vizexdf ' + ' '.join(sys.argv[1:-1]) 117 | append_to_bash('vizexdf', line) 118 | 119 | show = all 120 | desc_sort = desc 121 | sort_by = sort 122 | dirpath = path 123 | 124 | # Execute vizexdf 125 | dir_files = DirectoryFiles(path=dirpath, sort_by=sort_by, 126 | show_hidden=show, desc=desc_sort) 127 | dir_files.print_tabulated_data() 128 | 129 | 130 | # ----- vizex options and arguments ----- 131 | @click.version_option('2.1.1', message='%(prog)s version %(version)s') 132 | @click.command(options_metavar='[options]') 133 | @click.argument('arg', 134 | default='disk', 135 | metavar='command') 136 | @click.option( 137 | "--save", 138 | help="Export your disk/cpu usage data into a CSV or JSON file:" 139 | + "Takes a full path with a file name as an argument. " 140 | + "File type will be defined based on a <.type> of the filename" 141 | ) 142 | @click.option( 143 | "-P", 144 | "--path", 145 | default=None, 146 | multiple=True, 147 | help="Print directory for a provided path." 148 | + " It can be both, full and relative path", 149 | ) 150 | @click.option( 151 | "-X", 152 | "--exclude", 153 | default=None, 154 | multiple=True, 155 | help="Select partition you want to exclude", 156 | ) 157 | @click.option( 158 | "--every", 159 | is_flag=True, 160 | help="Display information for all the disks" 161 | ) 162 | @click.option( 163 | "--details", 164 | is_flag=True, 165 | help="Display additinal details like fstype and mountpoint", 166 | ) 167 | @click.option( 168 | "-d", 169 | "--header", 170 | default=None, 171 | type=str, 172 | metavar="[COLOR]", 173 | help="Set the partition name color", 174 | ) 175 | @click.option( 176 | "-s", 177 | "--style", 178 | default=None, 179 | type=str, 180 | metavar="[ATTR]", 181 | help="Change the style of the header's display", 182 | ) 183 | @click.option( 184 | "-t", 185 | "--text", 186 | default=None, 187 | type=str, 188 | metavar="[COLOR]", 189 | help="Set the color of the regular text", 190 | ) 191 | @click.option( 192 | "-g", 193 | "--graph", 194 | default=None, 195 | type=str, 196 | metavar="[COLOR]", 197 | help="Change the color of the bar graph", 198 | ) 199 | @click.option( 200 | "-m", 201 | "--mark", 202 | default=None, 203 | help="Choose the symbols used for the graph" 204 | ) 205 | @click.option( 206 | '-l', '--alias', 207 | is_flag=True, 208 | help='Store customized terminal command for vizexdf as an alias so you' 209 | + ' don\'t have to repeat the line everytime.' 210 | + '<-l> should always be the last command in the line' 211 | ) 212 | def disk_usage(arg, save, path, every, 213 | details, exclude, header, style, 214 | text, graph, mark, alias) -> None: 215 | """ 216 | \b 217 | ██╗ ██╗██╗███████╗███████╗██╗ ██╗ 218 | ██║ ██║██║╚══███╔╝██╔════╝╚██╗██╔╝ 219 | ██║ ██║██║ ███╔╝ █████╗ ╚███╔╝ 220 | ╚██╗ ██╔╝██║ ███╔╝ ██╔══╝ ██╔██╗ 221 | ╚████╔╝ ██║███████╗███████╗██╔╝ ██╗ 222 | ╚═══╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ 223 | Made by: Beka Modebadze 224 | 225 | << Customize and display Disk Usage in the terminal >> 226 | 227 | \b 228 | COLORS: light_red, red, dark_red, dark_blue, blue, 229 | cyan, yellow, green, pink, white, black, purple, 230 | neon, grey, beige, orange, magenta, peach. 231 | 232 | \b 233 | ATTRIBUTES: bold, dim, underlined, blink, reverse. 234 | 235 | \b 236 | You can also give *args like [BATTERY] and [CPU] 237 | 238 | \b 239 | battery --> will display the battery information if found. 240 | cpu --> will visualize the usage of each CPU in live time *(beta mode) 241 | """ 242 | if alias: # Set vizex as alias 243 | line = 'vizex ' + ' '.join(sys.argv[1:-1]) 244 | append_to_bash('vizex', line) 245 | 246 | options: Options = Options() 247 | 248 | if mark: 249 | options.symbol = mark 250 | if header: 251 | options.header_color = header 252 | if text: 253 | options.text_color = text 254 | if graph: 255 | options.graph_color = graph 256 | if style: 257 | options.header_style = style 258 | exclude_list = list(exclude) 259 | 260 | if arg == 'battery': 261 | try: 262 | battery = Battery() 263 | battery.print_charts() 264 | except Exception: 265 | print('Battery not found!') 266 | elif arg == 'cpu': 267 | cpus = CPUFreq() 268 | cpus.display_separately(filename=save) 269 | else: 270 | renderer = DiskUsage( 271 | path=path, exclude=exclude_list, details=details, every=every 272 | ) 273 | if save: 274 | renderer.save_data(save) 275 | renderer.print_charts(options) 276 | 277 | 278 | if __name__ == "__main__": 279 | print_tree() 280 | -------------------------------------------------------------------------------- /tests/test_tools.py: -------------------------------------------------------------------------------- 1 | # add path to the main package and test files.py 2 | if __name__ == '__main__': 3 | from __access import ADD_PATH 4 | ADD_PATH() 5 | 6 | 7 | import io 8 | import os 9 | import json 10 | import random 11 | import tempfile 12 | import unittest 13 | import unittest.mock 14 | 15 | from colored import fg, attr, stylize 16 | 17 | # --- Tools' methods to be tested --- 18 | from tools import save_to_csv, save_to_json 19 | from tools import bytes_to_human_readable, create_usage_warning 20 | from tools import ints_to_human_readable, printml 21 | from tools import append_to_bash, remove_if_exists 22 | 23 | 24 | class TestTools(unittest.TestCase): 25 | 26 | def test_bytes_to_human_readable_zero_input(self): 27 | try: 28 | bt = bytes_to_human_readable(0) 29 | self.assertEqual('0.0 B', bt) 30 | except Exception as e: 31 | self.fail(f"Exception occured when None value was given as an argument {e}") 32 | 33 | def test_bytes_to_humand_readable_exact_conversion(self): 34 | try: 35 | bt = bytes_to_human_readable(12288) 36 | self.assertEqual('12.0 kB', bt, msg='12288 b should have been 12.0 kB') 37 | bt = bytes_to_human_readable(3300000) 38 | self.assertEqual('3.1 MB', bt, msg='3300000 b should have been 3.1 MB') 39 | bt = bytes_to_human_readable(13000000000) 40 | self.assertEqual('12.1 GB', bt, msg='13000000000 b should have been 12.1 GB') 41 | bt = bytes_to_human_readable(24500000000000) 42 | self.assertEqual('22.3 TB', bt, msg='24500000000000 b should have been 22.3 TB') 43 | except Exception as e: 44 | self.fail(f"Exception occured when tried to convert bytes to human readable format {e}") 45 | def test_bytes_to_human_readable_convert_to_mb(self): 46 | try: 47 | for i in range(20): 48 | bt = random.randint(1048576, 943718400) 49 | human = bytes_to_human_readable(bt).split(' ')[1] 50 | self.assertEqual(human, 'MB', msg='Should be MB') 51 | except Exception as e: 52 | self.fail(f"Exception occured when mb range values were given as an argument {e}") 53 | 54 | def test_bytes_to_human_readable_convert_to_gb(self): 55 | try: 56 | for i in range(20): 57 | bt = random.randint(1073741824, 549755813888) 58 | human = bytes_to_human_readable(bt).split(' ')[1] 59 | self.assertEqual(human, 'GB', msg='Should be GB') 60 | except Exception as e: 61 | self.fail(f"Exception occured when gb range values were given as an argument {e}") 62 | 63 | def test_ints_to_human_readable_for_mb_gb_b_tb(self): 64 | try: 65 | for i in range(100): 66 | test_dict = { 67 | 'mb': random.randint(1048576, 943718400), 68 | 'kb': random.randint(4000, 140000), 69 | 'gb': random.randint(1073741824, 549755813888), 70 | 'tb': random.randint(1300000000000, 130000000000000), 71 | 'bs': random.randint(1, 999), 72 | 'String': 'This is text' 73 | } 74 | result = ints_to_human_readable(test_dict) 75 | self.assertEqual(result['mb'].split(' ')[1], 'MB') 76 | self.assertEqual(result['kb'].split(' ')[1], 'kB') 77 | self.assertEqual(result['gb'].split(' ')[1], 'GB') 78 | self.assertEqual(result['tb'].split(' ')[1], 'TB') 79 | self.assertEqual(result['bs'].split(' ')[1], 'B') 80 | self.assertEqual(result['String'], 'This is text') 81 | except Exception as e: 82 | self.fail(f"Exception occured when trying to convert dict of bytes into readable sizes {e}") 83 | 84 | @unittest.mock.patch('sys.stdout', new_callable=io.StringIO) 85 | def assert_stdout(self, arts, col, expected_output, mock_stdout): 86 | printml(arts, col) 87 | self.assertEqual(mock_stdout.getvalue(), expected_output) 88 | 89 | def test_printml(self): 90 | arts = [''' 91 | 1''',''' 92 | 4'''] 93 | expected1 = ''' 94 | 1 4 95 | 96 | ''' 97 | expected2 = ''' 98 | 1 99 | 100 | 101 | 4 102 | 103 | ''' 104 | self.assert_stdout(arts, 1, expected1) 105 | self.assert_stdout(arts, 2, expected2) 106 | 107 | def test_create_usage_warning_blinking_red(self): 108 | try: 109 | compare_red = f"{stylize('39.5% used', attr('blink') + fg(9))}" 110 | self.assertEqual( 111 | create_usage_warning(39.5, 39.4, 39), compare_red) 112 | except Exception as e: 113 | self.fail(f"Exception occured when trying create a red blinking warning {e}") 114 | 115 | def test_create_usage_warning_orange(self): 116 | try: 117 | compare_orange = f"{stylize('0.1% used', fg(214))}" 118 | self.assertEqual( 119 | create_usage_warning(0.1, 0.2, 0.1), compare_orange) 120 | except Exception as e: 121 | self.fail(f"Exception occured when trying tocreate a orange warning {e}") 122 | 123 | def test_create_usage_warning_green(self): 124 | try: 125 | compare_green = f"{stylize('99.5% used', fg(82))}" 126 | self.assertEqual( 127 | create_usage_warning(99.5, 99.9, 99.6), compare_green) 128 | except Exception as e: 129 | self.fail(f"Exception occured when trying create a green warning {e}") 130 | 131 | def test_create_usage_warning_negative_number(self): 132 | try: 133 | compare_negative = f"{stylize('0% used', fg(82))}" 134 | self.assertEqual( 135 | create_usage_warning(-15.5, 1.1, 1.0), compare_negative) 136 | except Exception as e: 137 | self.fail(f"Exception occured when trying to create a warning with negative number {e}") 138 | 139 | def test_create_usage_warning_over_100_usage(self): 140 | try: 141 | compare_over100 = f"{stylize('100% used', attr('blink') + fg(9))}" 142 | self.assertEqual( 143 | create_usage_warning(101.1, 99.9, 99.8), compare_over100) 144 | except Exception as e: 145 | self.fail(f"Exception occured when trying to create a 100% usage warning {e}") 146 | 147 | def test_save_to_csv_wrong_filename(self): 148 | data = {'test_01': [11, 33, 55]} 149 | try: 150 | self.assertRaises(NameError, save_to_csv, data, 'wrongformat') 151 | except Exception as e: 152 | self.fail(f'Exception occured when trying to create a file without full name {e}') 153 | 154 | def test_save_to_csv_create_file(self): 155 | data = {} 156 | try: 157 | with tempfile.NamedTemporaryFile(mode='w+', suffix='.csv') as tmpf: 158 | filename = tmpf.name 159 | save_to_csv(data=data, filename=filename) 160 | self.assertTrue(os.path.isfile(filename), 161 | msg=f'{filename} was not created') 162 | self.assertTrue(os.path.getsize(filename), 163 | msg=f'{filename} is not empty') 164 | except Exception as e: 165 | self.fail(f'Exception occured when trying to save an empty CSV file {e}') 166 | 167 | def test_save_to_json_wrong_filename(self): 168 | data = {'test_01': [11, 33, 55]} 169 | try: 170 | self.assertRaises(NameError, save_to_json, data, 'wrongformat') 171 | except Exception as e: 172 | self.fail(f'Exception occured when trying to create a file without full name {e}') 173 | 174 | def test_save_to_csv_full_data(self): 175 | data = { 176 | 'test_01': [11, 33, 55], 177 | 'test_02': [22, 44, 66], 178 | 'test_03': [33, 77, 99] 179 | } 180 | try: 181 | with tempfile.NamedTemporaryFile(mode='w+', suffix='.csv') as tmpf: 182 | save_to_csv(data=data, filename=tmpf.name, orient='index') 183 | self.assertTrue(os.path.isfile(tmpf.name)) 184 | 185 | other_f = [ 186 | ['', '0', '1', '2\n'], 187 | ['test_01', '11', '33', '55\n'], 188 | ['test_02', '22', '44', '66\n'], 189 | ['test_03', '33', '77', '99\n'] 190 | ] 191 | 192 | with open(tmpf.name) as f: 193 | for line, other in zip(f, other_f): 194 | self.assertListEqual(other, line.split(','), 195 | msg='Given two rows in a CSV files are not the same') 196 | except Exception as e: 197 | self.fail(f'Exception occured when trying to save a CSV file {e}') 198 | 199 | def test_save_to_json_create_file(self): 200 | data = {} 201 | try: 202 | with tempfile.NamedTemporaryFile(mode='w+', suffix='.json') as tmpf: 203 | filename = tmpf.name 204 | save_to_json(data=data, filename=filename) 205 | self.assertTrue(os.path.isfile(filename), 206 | msg=f'{filename} was not created') 207 | self.assertTrue(os.path.getsize(filename), 208 | msg=f'{filename} is not empty') 209 | except Exception as e: 210 | self.fail(f'Exception occured when trying to create an empty JSON file {e}') 211 | 212 | def test_save_to_json_full_data(self): 213 | data = { 214 | 'test_01': [11, 22, 33], 215 | 'test_02': [44, 55, 66], 216 | 'test_03': [77, 88, 99] 217 | } 218 | try: 219 | with tempfile.NamedTemporaryFile(mode='w+', suffix='.json') as tmpf: 220 | save_to_json(data=data, filename=tmpf.name) 221 | self.assertTrue(os.path.isfile(tmpf.name)) 222 | 223 | with open(tmpf.name) as f: 224 | loaded = json.load(tmpf) 225 | for key in data.keys(): 226 | for i, o in zip(loaded[key], data[key]): 227 | self.assertEqual(o, i, 228 | msg='Given two rows in a JSON files are not the same') 229 | except Exception as e: 230 | self.fail(f'Exception occured when trying to save a JSON file {e}') 231 | 232 | def test_append_to_bash(self): 233 | bash_aliases = os.path.expanduser("~") + '/.bash_aliases' 234 | try: 235 | alias_line= "this will be here temporarily" 236 | append_to_bash('toolstest', alias_line) 237 | with open(bash_aliases, 'r') as f: 238 | for line in f: 239 | if 'toolstest' in line: 240 | self.assertTrue(1 == 1) 241 | return 242 | remove_if_exists('toolstest', bash_aliases) 243 | self.fail('Alias was not appended!') 244 | except Exception as e: 245 | self.fail(f'Exception occured when tried to set alias {e}') 246 | 247 | def test_remove_if_exists(self): 248 | bash_aliases = os.path.expanduser("~") + '/.bash_aliases' 249 | try: 250 | append_to_bash('toolstest', 'this line is a test for remove_if_exists') 251 | remove_if_exists('toolstest', bash_aliases) 252 | with open(bash_aliases, 'r') as f: 253 | for line in f: 254 | if 'tooltest' in line: 255 | self.fail('alias was note removed') 256 | self.assertTrue(1 == 1) 257 | except Exception as e: 258 | self.fail(e) 259 | 260 | 261 | if __name__ == '__main__': 262 | unittest.main() 263 | -------------------------------------------------------------------------------- /README.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Installation 6 | 20 | 21 | 22 | 23 | 30 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |

40 | 41 |
42 |
43 |
44 |

45 |

46 | 47 | 48 | 49 |

50 |

vizex is the terminal program for the UNIX/Linux systems which helps the user to visualize the disk space usage for every partition and media on the user's machine. vizex is highly customizable and can fit any user's taste and preferences.

51 |

vizexdf is a new feature that allows to organize and print directory data in the terminal. With a recent release 2.0.4 vizexdf does directory parsing using Asynchronous execution, which improved runtime performance by over 400%.

52 |

You can check full release history here.

53 |
54 |
55 |

Installation

56 |

pip

57 |

vizex can be installed through your terminal and requires Python >= 3.9 and the pip package manager. Here's how to set up Python on your machine.

58 |

If you don't have PyPackage Index (PyPI or just pip) installed, Here's the guide on how to install it. Install vizex with the following command:

59 |
pip install vizex 60 |
61 |

If you already have vizex install you'll need to upgrade it:

62 |
pip install vizex --upgrade 63 |
64 |

If you encounter any problems during installation, know that some vizex dependencies require a Python 3 development package on Linux and you need to set up that manually.

65 |

For Debian and other derived systems (Ubuntu, Mint, Kali, etc.) you can install this with the following command:

66 |
sudo apt-get install python3-dev 67 |
68 |

For Red Hat derived systems (Fedora, RHEL, CentOS, etc.) you can install this with the following command:

69 |
sudo yum install python3-devel 70 |
71 |

AUR

72 |

vizex is available as a package on the AUR (Arch user repository), distributions with AUR support may install directly from the command line using their favorite pacman helper.

73 |

Example using yay:

74 |
yay -S vizex 75 |
76 |

How it Works

77 |

After installing you can use two terminal commands: vizex to display the disk usage/space and vizexdf, which will print the data of a current working directory with sizes, file types and last modified date.

78 |

This will graphically display disk space and usage:

79 |
vizex 80 |
81 |

demo

82 |
83 |
vizexdf 84 |
85 |

demo1

86 |
87 |
88 |

new feature:

89 |

vizextree

90 |

you can now print tree of directory structure with the level you want. For example tree with level 1 only

91 |

By default level is set to 3 and path is a current path. But you can manually supply path, by just typing path you want to generate tree for, and using -l option to instruct how many levels of directories you want to print.

92 |

For example:

93 |
vizextree . -level 1 94 |
95 |

tree

96 |
97 |

vizex

98 |

The best part is that you can modify the colors and style of the display to your preferences with the following commands. For the example above command has excluded two partitions. You can also do give the following options:

99 |
-d --header <color> 100 | -s --style <attribute> 101 | -t --text <color> 102 | -g --graph <color> 103 |
104 |

Display additional details, like fstype and mount point, for each partition:

105 |
vizex --details 106 |
107 |

details-img

108 |

If you are interested in visualizing a specific path run with the following command:

109 |
vizex --path </full/path> 110 |
111 |

You can also exclude any combination of partitions/disks with multiple -X or for verbose --exclude option:

112 |
vizex -X <PartitionName1> -X <PartitionName2> ... 113 |
114 |

You can also save the partitions full information in csv or in json file, just by calling --save option with the full path where you want your output to be saved:

115 |
vizex --save "/home/user/disk_info.json" 116 |
117 |

And if you are on laptop you can even call your battery information with simple argument:

118 |
vizex battery 119 |
120 |

For a full list of the available options please check:

121 |
vizex --help 122 |
123 |
124 |

vizexdf

125 |

You can include hidden files and folders by calling --all or -a for short and sort the output with --sort or -s for short based on name, file type, size, or date. Put it in descending order with the --desc option.

126 |

You can chain multiple options but make sure to put the -s at the end as it requires a text argument. Example:

127 |
vizexdf -ads name 128 |
129 |

This will print current directory data sorted by name and in descending order and will include hidden files.

130 |

Lastly, you save all the modifications by adding -l at the end of the command:

131 |
vizex -d red -t blue --details -l 132 |
133 |

The next time you call vizex / vizexdf it will include all the options listed in the above command. If you decided to change the default calling command for vizex/vizexdf just include -l and it will be overwritten

134 |

If you want to contribute to the project you are more than welcome! But first, make sure all the tests run after you fork the project and before the pull request. First, run the access.py, that way tests folder will obtain a path to the main folder and you can run all the tests.

135 |

You can get the full set of features by calling --help option with command.

136 |
137 |
138 |

Special Thanks to the Contributors!

139 |

140 | 141 | 142 | 143 |

144 |
145 |
146 |

Follow Me on Social Media

147 |

148 | 149 | twitter 150 | 151 | 152 | linkedin 153 | 154 | 155 | github 156 | 157 |

158 |

Repo is distributed under the MIT license. Please see the LICENSE for more information.

159 | 160 | 161 | --------------------------------------------------------------------------------