mirror of https://github.com/aristocratos/bpytop
Init
commit
7d28636b04
|
@ -0,0 +1,76 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at admin@qvantnet.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
|
@ -0,0 +1,33 @@
|
|||
# Contributing guidelines
|
||||
|
||||
## When submitting pull requests
|
||||
|
||||
* Explain your thinking in why a change or addition is needed.
|
||||
* Is it a requested change or feature?
|
||||
* If not, open a feature request to get feedback before making a pull request.
|
||||
|
||||
* Split up multiple unrelated changes in multiple pull requests.
|
||||
|
||||
|
||||
* Purely cosmetic changes won't be accepted without a very good explanation of its value.
|
||||
* (Some design choices are for better configurability of syntax highlighting.)
|
||||
|
||||
## Formatting
|
||||
|
||||
### Follow the current syntax design
|
||||
|
||||
* Indent type: Tabs
|
||||
|
||||
* Tab size: 4
|
||||
|
||||
## Optimization
|
||||
|
||||
* Avoid writing to disk if possible.
|
||||
|
||||
* Make sure variables/arrays are cleaned up if not reused.
|
||||
|
||||
* Compare cpu and memory usage with and without your code and look for alternatives if they cause a noticeable negative impact.
|
||||
|
||||
For questions contact Aristocratos at admin@qvantnet.com
|
||||
|
||||
For proposing changes to this document create a [new issue](https://github.com/aristocratos/bashtop/issues/new/choose).
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,16 @@
|
|||
PREFIX ?= /usr/local
|
||||
DOCDIR ?= $(PREFIX)/share/doc/bpytop
|
||||
|
||||
all:
|
||||
@echo Run \'make install\' to install bpytop.
|
||||
|
||||
install:
|
||||
@mkdir -p $(DESTDIR)$(PREFIX)/bin
|
||||
@cp -p bpytop $(DESTDIR)$(PREFIX)/bin/bpytop
|
||||
@mkdir -p $(DESTDIR)$(DOCDIR)
|
||||
@cp -p README.md $(DESTDIR)$(DOCDIR)
|
||||
@chmod 755 $(DESTDIR)$(PREFIX)/bin/bpytop
|
||||
|
||||
uninstall:
|
||||
@rm -rf $(DESTDIR)$(PREFIX)/bin/bpytop
|
||||
@rm -rf $(DESTDIR)$(DOCDIR)
|
|
@ -0,0 +1,243 @@
|
|||
# ![bpytop](Imgs/logo-t.png)
|
||||
|
||||
![Linux](https://img.shields.io/badge/-Linux-grey?logo=linux)
|
||||
![OSX](https://img.shields.io/badge/-OSX-black?logo=apple)
|
||||
![FreeBSD](https://img.shields.io/badge/-FreeBSD-red?logo=freebsd)
|
||||
![Usage](https://img.shields.io/badge/Usage-System%20resource%20monitor-blue)
|
||||
![Python](https://img.shields.io/badge/Python-v3.7%5E-orange?logo=python)
|
||||
![bashtop_version](https://img.shields.io/github/v/tag/aristocratos/bpytop?label=version)
|
||||
[![Donate](https://img.shields.io/badge/-Donate-yellow?logo=paypal)](https://paypal.me/aristocratos)
|
||||
[![Sponsor](https://img.shields.io/badge/-Sponsor-red?logo=github)](https://github.com/sponsors/aristocratos)
|
||||
[![Coffee](https://img.shields.io/badge/-Buy%20me%20a%20Coffee-grey?logo=Ko-fi)](https://ko-fi.com/aristocratos)
|
||||
|
||||
## Index
|
||||
|
||||
* [Documents](#documents)
|
||||
* [Description](#description)
|
||||
* [Features](#features)
|
||||
* [Themes](#themes) (Updated)
|
||||
* [Upcoming](#upcoming) (Python port)
|
||||
* [Support and funding](#support-and-funding)
|
||||
* [Compatibility](#compatibility) (OSX and FreeBSD Support)
|
||||
* [Dependencies](#dependencies) (Updated)
|
||||
* [Screenshots](#screenshots)
|
||||
* [Installation](#installation) (Updated)
|
||||
* [Configurability](#configurability) (Updated)
|
||||
* [TODO](#todo) (Updated)
|
||||
* [License](#license)
|
||||
|
||||
## Documents
|
||||
|
||||
#### [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
#### [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
#### [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
||||
|
||||
## Description
|
||||
|
||||
Resource monitor that shows usage and stats for processor, memory, disks, network and processes.
|
||||
|
||||
Python port of [bashtop](https://github.com/aristocratos/bashtop).
|
||||
|
||||
## Features
|
||||
|
||||
* Easy to use, with a game inspired menu system.
|
||||
* Fast and responsive UI with UP, DOWN keys process selection.
|
||||
* Function for showing detailed stats for selected process.
|
||||
* Ability to filter processes.
|
||||
* Easy switching between sorting options.
|
||||
* Send SIGTERM, SIGKILL, SIGINT to selected process.
|
||||
* UI menu for changing all config file options.
|
||||
* Auto scaling graph for network usage.
|
||||
* Shows message in menu if new version is available
|
||||
* Shows current read and write speeds for disks
|
||||
|
||||
## Themes
|
||||
|
||||
Bpytop uses the same the files as bashtop.
|
||||
|
||||
See bashtop/[themes](https://github.com/aristocratos/bashtop/themes) folder for available themes.
|
||||
|
||||
The builtin theme downloader places the default themes in `$HOME/.config/bpytop/themes`.
|
||||
User created themes should be placed in `$HOME/.config/bpytop/user_themes` to be safe from overwrites.
|
||||
|
||||
Let me know if you want to contribute with new themes.
|
||||
|
||||
|
||||
## Support and funding
|
||||
|
||||
You can sponsor this project through github, see [my sponsors page](https://github.com/sponsors/aristocratos) for options.
|
||||
|
||||
Or donate through [paypal](https://paypal.me/aristocratos) or [ko-fi](https://ko-fi.com/aristocratos).
|
||||
|
||||
Any support is greatly appreciated!
|
||||
|
||||
## Compatibility
|
||||
|
||||
Should work on most modern linux distributions, on Mac OS X and on FreeBSD.
|
||||
|
||||
Will not display correctly on the standard terminal on OSX!
|
||||
Recommended alternative [iTerm2](https://www.iterm2.com/)
|
||||
|
||||
Will also need to be run as superuser on OSX to display stats for processes not owned by user.
|
||||
|
||||
The disk io stats on OSX and FreeBSD shows iostats for all disks at the top instead of per disk.
|
||||
|
||||
For correct display, a terminal with support for:
|
||||
|
||||
* 24-bit truecolor
|
||||
* Wide characters
|
||||
|
||||
Also needs a UTF8 locale and a font that covers:
|
||||
|
||||
* Unicode Block “Braille Patterns” U+2800 - U+28FF
|
||||
* Unicode Block “Geometric Shapes” U+25A0 - U+25FF
|
||||
* Unicode Block "Box Drawing" and "Block Elements" U+2500 - U+259F
|
||||
|
||||
#### Notice
|
||||
|
||||
Dropbear seems to not be able to set correct locale. So if accessing bashtop over ssh, OpenSSH is recommended.
|
||||
|
||||
## Dependencies
|
||||
|
||||
**[Python3](https://www.python.org/downloads/)** (v3.6 or later)
|
||||
|
||||
**[psutil module](https://github.com/giampaolo/psutil)** (v5.7.0 or later)
|
||||
|
||||
## Optionals for additional stats
|
||||
|
||||
(Optional OSX) **[osx-cpu-temp](https://github.com/lavoiesl/osx-cpu-temp)** Needed to show CPU temperatures.
|
||||
|
||||
## Screenshots
|
||||
|
||||
Main UI showing details for a selected process.
|
||||
![Screenshot 1](Imgs/main.png)
|
||||
|
||||
Main menu.
|
||||
![Screenshot 2](Imgs/menu.png)
|
||||
|
||||
Options menu.
|
||||
![Screenshot 3](Imgs/options.png)
|
||||
|
||||
## Installation
|
||||
|
||||
#### Dependencies installation OSX
|
||||
|
||||
>Install homebrew if not already installed
|
||||
|
||||
``` bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
|
||||
```
|
||||
|
||||
>Install python3 if not already installed
|
||||
|
||||
``` bash
|
||||
brew install python3 git
|
||||
```
|
||||
|
||||
>Install psutil python module
|
||||
``` bash
|
||||
pip3 install psutil
|
||||
```
|
||||
|
||||
>Install optional dependency osx-cpu-temp
|
||||
|
||||
``` bash
|
||||
brew install osx-cpu-temp
|
||||
```
|
||||
|
||||
#### Dependencies installation FreeBSD
|
||||
|
||||
>Install with pkg and pip
|
||||
|
||||
``` bash
|
||||
sudo pkg install python3 git
|
||||
sudo python3 -m ensurepip
|
||||
sudo pip3 install psutil
|
||||
```
|
||||
|
||||
#### Manual installation Linux, OSX and FreeBSD
|
||||
|
||||
>Clone and install
|
||||
|
||||
``` bash
|
||||
git clone https://github.com/aristocratos/bashtop.git
|
||||
cd bashtop
|
||||
sudo make install
|
||||
```
|
||||
|
||||
>to uninstall it
|
||||
|
||||
``` bash
|
||||
sudo make uninstall
|
||||
```
|
||||
|
||||
|
||||
## Configurability
|
||||
|
||||
All options changeable from within UI.
|
||||
Config files stored in "$HOME/.config/bpytop" folder
|
||||
|
||||
#### bashtop.cfg: (auto generated if not found)
|
||||
|
||||
```bash
|
||||
#? Config file for bpytop v. 0.0.1
|
||||
#* Color theme, looks for a .theme file in "~/.config/bpytop/themes" and "~/.config/bpytop/user_themes", "Default" for builtin default theme
|
||||
color_theme = "Default"
|
||||
|
||||
#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs
|
||||
update_ms = 2500
|
||||
|
||||
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive"
|
||||
#* "cpu lazy" updates sorting over time, "cpu responsive" updates sorting directly
|
||||
proc_sorting = "cpu lazy"
|
||||
|
||||
#* Reverse sorting order, True or False
|
||||
proc_reversed = False
|
||||
|
||||
#* Show processes as a tree
|
||||
proc_tree = False
|
||||
|
||||
#* Check cpu temperature, only works if "sensors", "vcgencmd" or "osx-cpu-temp" commands is available
|
||||
check_temp = True
|
||||
|
||||
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable
|
||||
draw_clock = "%X"
|
||||
|
||||
#* Update main ui when menus are showing, set this to false if the menus is flickering too much for comfort
|
||||
background_update = True
|
||||
|
||||
#* Custom cpu model name, empty string to disable
|
||||
custom_cpu_name = ""
|
||||
|
||||
#* Show color gradient in process list, True or False
|
||||
proc_gradient = True
|
||||
|
||||
#* If process cpu usage should be of the core it's running on or usage of the total available cpu power
|
||||
proc_per_core = False
|
||||
|
||||
#* Optional filter for shown disks, should be names of mountpoints, "root" replaces "/", separate multiple values with space
|
||||
disks_filter = ""
|
||||
|
||||
#* Enable check for new version from github.com/aristocratos/bpytop at start
|
||||
update_check = True
|
||||
|
||||
#* Enable graphs with double the horizontal resolution, increases cpu usage
|
||||
hires_graphs = False
|
||||
```
|
||||
|
||||
#### Command line options: (not yet implemented)
|
||||
|
||||
``` bash
|
||||
USAGE: bpytop
|
||||
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] See TODOs from [Bashtop](https://github.com/aristocratos/bashtop#todo).
|
||||
|
||||
## LICENSE
|
||||
|
||||
[Apache License 2.0](LICENSE)
|
|
@ -0,0 +1,927 @@
|
|||
#!/usr/bin/env python3
|
||||
# pylint: disable=not-callable, no-member
|
||||
|
||||
# Copyright 2020 Aristocratos (jakob@qvantnet.com)
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os, sys, time, threading, signal, re, subprocess, logging, logging.handlers
|
||||
from select import select
|
||||
from pathlib import Path
|
||||
from distutils.util import strtobool
|
||||
from typing import List, Set, Dict, Tuple, Optional, Union, Any, Callable, ContextManager, Iterable
|
||||
|
||||
errors: List[str] = []
|
||||
try: import fcntl, termios, tty
|
||||
except Exception as e: errors.append(f'{e}')
|
||||
|
||||
try: import psutil
|
||||
except Exception as e: errors.append(f'{e}')
|
||||
|
||||
|
||||
SYSTEM: str
|
||||
if "linux" in sys.platform: SYSTEM = "Linux"
|
||||
elif "bsd" in sys.platform: SYSTEM = "BSD"
|
||||
elif "darwin" in sys.platform: SYSTEM = "MacOS"
|
||||
else: SYSTEM = "Other"
|
||||
|
||||
if errors:
|
||||
print ("ERROR!")
|
||||
for error in errors:
|
||||
print(error)
|
||||
if SYSTEM == "Other":
|
||||
print("\nUnsupported platform!\n")
|
||||
else:
|
||||
print("\nInstall required modules!\n")
|
||||
quit(1)
|
||||
|
||||
from functools import partial
|
||||
|
||||
print: partial = partial(print, sep="", end="", flush=True) #* Setup print function to default to empty seperator and no new line
|
||||
|
||||
#? Variables ------------------------------------------------------------------------------------->
|
||||
|
||||
BANNER_SRC: Dict[str, str] = {
|
||||
"#E62525" : "██████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗",
|
||||
"#CD2121" : "██╔══██╗██╔══██╗╚██╗ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗",
|
||||
"#B31D1D" : "██████╔╝██████╔╝ ╚████╔╝ ██║ ██║ ██║██████╔╝",
|
||||
"#9A1919" : "██╔══██╗██╔═══╝ ╚██╔╝ ██║ ██║ ██║██╔═══╝ ",
|
||||
"#801414" : "██████╔╝██║ ██║ ██║ ╚██████╔╝██║",
|
||||
"#000000" : "╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝",
|
||||
}
|
||||
|
||||
VERSION: str = "0.0.1"
|
||||
|
||||
DEFAULT_CONF: str = f'#? Config file for bpytop v. {VERSION}\n'
|
||||
DEFAULT_CONF += '''
|
||||
#* Color theme, looks for a .theme file in "~/.config/bpytop/themes" and "~/.config/bpytop/user_themes", "Default" for builtin default theme
|
||||
color_theme = "Default"
|
||||
|
||||
#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs
|
||||
update_ms = 2500
|
||||
|
||||
#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive"
|
||||
#* "cpu lazy" updates sorting over time, "cpu responsive" updates sorting directly
|
||||
proc_sorting = "cpu lazy"
|
||||
|
||||
#* Reverse sorting order, True or False
|
||||
proc_reversed = False
|
||||
|
||||
#* Show processes as a tree
|
||||
proc_tree = False
|
||||
|
||||
#* Check cpu temperature, only works if "sensors", "vcgencmd" or "osx-cpu-temp" commands is available
|
||||
check_temp = True
|
||||
|
||||
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable
|
||||
draw_clock = "%X"
|
||||
|
||||
#* Update main ui when menus are showing, set this to false if the menus is flickering too much for comfort
|
||||
background_update = True
|
||||
|
||||
#* Custom cpu model name, empty string to disable
|
||||
custom_cpu_name = ""
|
||||
|
||||
#* Show color gradient in process list, True or False
|
||||
proc_gradient = True
|
||||
|
||||
#* If process cpu usage should be of the core it's running on or usage of the total available cpu power
|
||||
proc_per_core = False
|
||||
|
||||
#* Optional filter for shown disks, should be names of mountpoints, "root" replaces "/", separate multiple values with space
|
||||
disks_filter = ""
|
||||
|
||||
#* Enable check for new version from github.com/aristocratos/bpytop at start
|
||||
update_check = True
|
||||
|
||||
#* Enable graphs with double the horizontal resolution, increases cpu usage
|
||||
hires_graphs = False
|
||||
|
||||
'''
|
||||
|
||||
conf: Path = Path(f'{Path.home()}/.config/bpytop')
|
||||
if not conf.is_dir():
|
||||
try:
|
||||
conf.mkdir(mode=0o777, parents=True)
|
||||
except PermissionError:
|
||||
print(f'ERROR!\nNo permission to write to "{conf}" directory!')
|
||||
quit(1)
|
||||
|
||||
CONFIG_DIR: str = str(conf)
|
||||
del conf
|
||||
|
||||
CORES: int = psutil.cpu_count(logical=False) or 1
|
||||
THREADS: int = psutil.cpu_count(logical=True) or 1
|
||||
|
||||
DEFAULT_THEME: Dict[str, str] = {
|
||||
"main_bg" : "",
|
||||
"main_fg" : "#cc",
|
||||
"title" : "#ee",
|
||||
"hi_fg" : "#90",
|
||||
"selected_bg" : "#7e2626",
|
||||
"selected_fg" : "#ee",
|
||||
"inactive_fg" : "#40",
|
||||
"proc_misc" : "#0de756",
|
||||
"cpu_box" : "#3d7b46",
|
||||
"mem_box" : "#8a882e",
|
||||
"net_box" : "#423ba5",
|
||||
"proc_box" : "#923535",
|
||||
"div_line" : "#30",
|
||||
"temp_start" : "#4897d4",
|
||||
"temp_mid" : "#5474e8",
|
||||
"temp_end" : "#ff40b6",
|
||||
"cpu_start" : "#50f095",
|
||||
"cpu_mid" : "#f2e266",
|
||||
"cpu_end" : "#fa1e1e",
|
||||
"free_start" : "#223014",
|
||||
"free_mid" : "#b5e685",
|
||||
"free_end" : "#dcff85",
|
||||
"cached_start" : "#0b1a29",
|
||||
"cached_mid" : "#74e6fc",
|
||||
"cached_end" : "#26c5ff",
|
||||
"available_start" : "#292107",
|
||||
"available_mid" : "#ffd77a",
|
||||
"available_end" : "#ffb814",
|
||||
"used_start" : "#3b1f1c",
|
||||
"used_mid" : "#d9626d",
|
||||
"used_end" : "#ff4769",
|
||||
"download_start" : "#231a63",
|
||||
"download_mid" : "#4f43a3",
|
||||
"download_end" : "#b0a9de",
|
||||
"upload_start" : "#510554",
|
||||
"upload_mid" : "#7d4180",
|
||||
"upload_end" : "#dcafde"
|
||||
}
|
||||
|
||||
|
||||
#? Setup error logger ---------------------------------------------------------------------------->
|
||||
|
||||
try:
|
||||
errlog = logging.getLogger("ErrorLogger")
|
||||
errlog.setLevel(logging.DEBUG)
|
||||
eh = logging.handlers.RotatingFileHandler(f'{CONFIG_DIR}/error.log', maxBytes=1048576, backupCount=4)
|
||||
eh.setLevel(logging.DEBUG)
|
||||
eh.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s: %(message)s", datefmt="%d/%m/%y (%X)"))
|
||||
errlog.addHandler(eh)
|
||||
except PermissionError:
|
||||
print(f'ERROR!\nNo permission to write to "{CONFIG_DIR}" directory!')
|
||||
quit(1)
|
||||
|
||||
errlog.info(f'New instance of bpytop version {VERSION} started with pid {os.getpid()}')
|
||||
|
||||
#? Classes --------------------------------------------------------------------------------------->
|
||||
|
||||
class Term:
|
||||
"""Terminal info and commands"""
|
||||
width: int = os.get_terminal_size().columns #* Current terminal width in columns
|
||||
height: int = os.get_terminal_size().lines #* Current terminal height in lines
|
||||
resized: bool = False #* Flag indicating if terminal was recently resized
|
||||
fg: str = "" #* Default foreground color
|
||||
bg: str = "" #* Default background color
|
||||
hide_cursor = "\033[?25l" #* Hide terminal cursor
|
||||
show_cursor = "\033[?25h" #* Show terminal cursor
|
||||
alt_screen = "\033[?1049h" #* Switch to alternate screen
|
||||
normal_screen = "\033[?1049l" #* Switch to normal screen
|
||||
clear = "\033[2J\033[0;0f" #* Clear screen and set cursor to position 0,0
|
||||
@classmethod
|
||||
def refresh(cls, *args):
|
||||
"""Update width, height and set resized flag if terminal has been resized"""
|
||||
w: int; h: int
|
||||
w, h = os.get_terminal_size()
|
||||
if (w, h) != (cls.width, cls.height):
|
||||
cls.resized = True
|
||||
cls.width, cls.height = w, h
|
||||
@staticmethod
|
||||
def echo(on: bool):
|
||||
"""Toggle input echo"""
|
||||
(iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(sys.stdin.fileno())
|
||||
if on:
|
||||
lflag |= termios.ECHO # type: ignore
|
||||
else:
|
||||
lflag &= ~termios.ECHO # type: ignore
|
||||
new_attr = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]
|
||||
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, new_attr)
|
||||
|
||||
class Fx:
|
||||
"""Text effects"""
|
||||
start = "\033[" #* Escape sequence start
|
||||
sep = ";" #* Escape sequence separator
|
||||
end = "m" #* Escape sequence end
|
||||
reset = rs = "\033[0m" #* Reset foreground/background color and text effects
|
||||
bold = b = "\033[1m" #* Bold on
|
||||
unbold = ub = "\033[22m" #* Bold off
|
||||
dark = d = "\033[2m" #* Dark on
|
||||
undark = ud = "\033[22m" #* Dark off
|
||||
italic = i = "\033[3m" #* Italic on
|
||||
unitalic = ui = "\033[23m" #* Italic off
|
||||
underline = u = "\033[4m" #* Underline on
|
||||
ununderline = uu = "\033[24m" #* Underline off
|
||||
blink = bl = "\033[5m" #* Blink on
|
||||
unblink = ubl = "\033[25m" #* Blink off
|
||||
strike = s = "\033[9m" #* Strike / crossed-out on
|
||||
unstrike = us = "\033[29m" #* Strike / crossed-out off
|
||||
|
||||
class Raw(object):
|
||||
"""Set raw input mode for device"""
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.fd = self.stream.fileno()
|
||||
def __enter__(self):
|
||||
self.original_stty = termios.tcgetattr(self.stream)
|
||||
tty.setcbreak(self.stream)
|
||||
def __exit__(self, type, value, traceback):
|
||||
termios.tcsetattr(self.stream, termios.TCSANOW, self.original_stty)
|
||||
|
||||
class Nonblocking(object):
|
||||
"""Set nonblocking mode for device"""
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.fd = self.stream.fileno()
|
||||
def __enter__(self):
|
||||
self.orig_fl = fcntl.fcntl(self.fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl | os.O_NONBLOCK)
|
||||
def __exit__(self, *args):
|
||||
fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl)
|
||||
|
||||
class Mv:
|
||||
"""Class with collection of cursor movement functions: .t[o](line, column) | .r[ight](columns) | .l[eft](columns) | .u[p](lines) | .d[own](lines) | .save() | .restore()"""
|
||||
@staticmethod
|
||||
def to(line: int, col: int) -> str:
|
||||
return f'\033[{line};{col}f' #* Move cursor to line, column
|
||||
@staticmethod
|
||||
def right(x: int) -> str: #* Move cursor right x columns
|
||||
return f'\033[{x}C'
|
||||
@staticmethod
|
||||
def left(x: int) -> str: #* Move cursor left x columns
|
||||
return f'\033[{x}D'
|
||||
@staticmethod
|
||||
def up(x: int) -> str: #* Move cursor up x lines
|
||||
return f'\033[{x}A'
|
||||
@staticmethod
|
||||
def down(x: int) -> str: #* Move cursor down x lines
|
||||
return f'\033[{x}B'
|
||||
@staticmethod
|
||||
def save() -> str: #* Save cursor position
|
||||
return "\033[s"
|
||||
@staticmethod
|
||||
def restore() -> str: #* Restore saved cursor postion
|
||||
return "\033[u"
|
||||
t = to
|
||||
r = right
|
||||
l = left
|
||||
u = up
|
||||
d = down
|
||||
|
||||
class Key:
|
||||
""".list = Input queue | .new = Input threading event | .reader = Input reader thread | .start() = Start input reader | .stop() = Stop input reader"""
|
||||
list: List[str] = []
|
||||
new = threading.Event()
|
||||
stopping: bool = False
|
||||
@classmethod
|
||||
def start(cls):
|
||||
cls.stopping = False
|
||||
cls.reader = threading.Thread(target=get_key)
|
||||
cls.reader.start()
|
||||
@classmethod
|
||||
def stop(cls):
|
||||
if cls.reader.is_alive():
|
||||
cls.stopping = True
|
||||
try:
|
||||
cls.reader.join()
|
||||
except:
|
||||
pass
|
||||
|
||||
class Color:
|
||||
'''self.__init__ accepts 6 digit hexadecimal: string "#RRGGBB", 2 digit hexadecimal: string "#FF" and decimal RGB "0-255 0-255 0-255" as a string.\n
|
||||
self.__init__ also accepts depth="fg" or "bg" | default=bool\n
|
||||
self.__call__(*args) converts arguments to a string and apply color\n
|
||||
self.__str__ returns escape sequence to set color. __iter__ returns iteration over red, green and blue in integer values of 0-255.\n
|
||||
Values: .hex: str | .dec: Tuple[int] | .red: int | .green: int | .blue: int | .depth: str | .escape: str\n
|
||||
'''
|
||||
hex: str; dec: Tuple[int, int, int]; red: int; green: int; blue: int; depth: str; escape: str; default: bool
|
||||
|
||||
def __init__(self, color: str, depth: str = "fg", default: bool = False):
|
||||
self.depth = depth
|
||||
self.default = default
|
||||
try:
|
||||
if not color:
|
||||
if depth != "bg" and not default: raise ValueError("No RGB values given")
|
||||
self.dec = (0, 0, 0)
|
||||
self.hex = ""
|
||||
self.red = self.green = self.blue = 0
|
||||
self.escape = "\033[49m"
|
||||
return
|
||||
|
||||
elif color.startswith("#"):
|
||||
self.hex = color
|
||||
if len(self.hex) == 3:
|
||||
self.hex += self.hex[1:3] + self.hex[1:3]
|
||||
c = int(self.hex[1:3], base=16)
|
||||
self.dec = (c, c, c)
|
||||
elif len(self.hex) == 7:
|
||||
self.dec = (int(self.hex[1:3], base=16), int(self.hex[3:5], base=16), int(self.hex[5:7], base=16))
|
||||
else:
|
||||
raise ValueError(f'Incorrectly formatted hexadeciaml rgb string: {self.hex}')
|
||||
|
||||
else:
|
||||
c_t = tuple(map(int, color.split(" ")))
|
||||
if len(c_t) == 3:
|
||||
self.dec = c_t #type: ignore
|
||||
else:
|
||||
raise ValueError(f'RGB dec should be "0-255 0-255 0-255"')
|
||||
|
||||
ct = self.dec[0] + self.dec[1] + self.dec[2]
|
||||
if ct > 255*3 or ct < 0:
|
||||
raise ValueError(f'RGB values out of range: {color}')
|
||||
except Exception as e:
|
||||
errlog.exception(str(e))
|
||||
self.escape = ""
|
||||
return
|
||||
|
||||
if self.dec and not self.hex: self.hex = f'{hex(self.dec[0]).lstrip("0x").zfill(2)}{hex(self.dec[1]).lstrip("0x").zfill(2)}{hex(self.dec[2]).lstrip("0x").zfill(2)}'
|
||||
|
||||
if self.dec and self.hex:
|
||||
self.red, self.green, self.blue = self.dec
|
||||
self.escape = f'\033[{38 if self.depth == "fg" else 48};2;{";".join(str(c) for c in self.dec)}m'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.escape
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(self.escape)
|
||||
|
||||
def __iter__(self) -> Iterable:
|
||||
for c in self.dec: yield c
|
||||
|
||||
def __call__(self, *args) -> str:
|
||||
if len(args) == 0: return ""
|
||||
return f'{self.escape}{"".join(map(str, args))}{getattr(Term, self.depth)}'
|
||||
|
||||
class Theme:
|
||||
'''__init__ accepts a dict containing { "color_element" : "color" } , errors defaults to default theme color'''
|
||||
|
||||
main_bg = main_fg = title = hi_fg = selected_bg = selected_fg = inactive_fg = proc_misc = cpu_box = mem_box = net_box = proc_box = div_line = temp_start = temp_mid = temp_end = cpu_start = cpu_mid = cpu_end = free_start = free_mid = free_end = cached_start = cached_mid = cached_end = available_start = available_mid = available_end = used_start = used_mid = used_end = download_start = download_mid = download_end = upload_start = upload_mid = upload_end = NotImplemented
|
||||
|
||||
def __init__(self, tdict: Dict[str, str]):
|
||||
for item, value in tdict.items():
|
||||
if hasattr(self, item):
|
||||
default = False if item not in ["main_fg", "main_bg"] else True
|
||||
depth = "fg" if item not in ["main_bg", "selected_bg"] else "bg"
|
||||
setattr(self, item, Color(value, depth=depth, default=default))
|
||||
if getattr(self, item).escape == "":
|
||||
setattr(self, item, Color(DEFAULT_THEME[item], depth=depth, default=default))
|
||||
|
||||
Term.fg, Term.bg = self.main_fg, self.main_bg
|
||||
print(self.main_fg, self.main_bg) #* Set terminal colors
|
||||
|
||||
class Draw:
|
||||
'''Holds the draw buffer\n
|
||||
Add to buffer: .buffer(name, *args, now=False, clear=False)\n
|
||||
Print buffer: .out()\n
|
||||
'''
|
||||
strings: Dict[str, str] = {}
|
||||
last_screen: str = ""
|
||||
|
||||
@classmethod
|
||||
def buffer(cls, name: str, *args, now: bool = False, clear: bool = False):
|
||||
string: str = ""
|
||||
if args: string = "".join(map(str, args))
|
||||
if name not in cls.strings or clear: cls.strings[name] = ""
|
||||
cls.strings[name] += string
|
||||
if now: print(string)
|
||||
|
||||
@classmethod
|
||||
def out(cls):
|
||||
cls.last_screen = "".join(cls.strings.values())
|
||||
#cls.strings = {}
|
||||
print(cls.last_screen)
|
||||
|
||||
class Symbol:
|
||||
h_line: str = "─"
|
||||
v_line: str = "│"
|
||||
left_up: str = "┌"
|
||||
right_up: str = "┐"
|
||||
left_down: str = "└"
|
||||
right_down: str = "┘"
|
||||
title_left: str = "┤"
|
||||
title_right: str = "├"
|
||||
div_up: str = "┬"
|
||||
div_down: str = "┴"
|
||||
graph_up: Dict[float, str] = {
|
||||
0.0 : "⠀", 0.1 : "⢀", 0.2 : "⢠", 0.3 : "⢰", 0.4 : "⢸",
|
||||
1.0 : "⡀", 1.1 : "⣀", 1.2 : "⣠", 1.3 : "⣰", 1.4 : "⣸",
|
||||
2.0 : "⡄", 2.1 : "⣄", 2.2 : "⣤", 2.3 : "⣴", 2.4 : "⣼",
|
||||
3.0 : "⡆", 3.1 : "⣆", 3.2 : "⣦", 3.3 : "⣶", 3.4 : "⣾",
|
||||
4.0 : "⡇", 4.1 : "⣇", 4.2 : "⣧", 4.3 : "⣷", 4.4 : "⣿"
|
||||
}
|
||||
graph_down: Dict[float, str] = {
|
||||
0.0 : "⠀", 0.1 : "⠈", 0.2 : "⠘", 0.3 : "⠸", 0.4 : "⢸",
|
||||
1.0 : "⠁", 1.1 : "⠉", 1.2 : "⠙", 1.3 : "⠹", 1.4 : "⢹",
|
||||
2.0 : "⠃", 2.1 : "⠋", 2.2 : "⠛", 2.3 : "⠻", 2.4 : "⢻",
|
||||
3.0 : "⠇", 3.1 : "⠏", 3.2 : "⠟", 3.3 : "⠿", 3.4 : "⢿",
|
||||
4.0 : "⡇", 4.1 : "⡏", 4.2 : "⡟", 4.3 : "⡿", 4.4 : "⣿"
|
||||
}
|
||||
|
||||
|
||||
|
||||
#? Functions ------------------------------------------------------------------------------------->
|
||||
|
||||
def get_cpu_name():
|
||||
'''Fetch a suitable CPU identifier from the CPU model name string'''
|
||||
name: str = ""
|
||||
nlist: List = []
|
||||
command: str = ""
|
||||
cmd_out: str = ""
|
||||
rem_line: str = ""
|
||||
if SYSTEM == "Linux":
|
||||
command = "cat /proc/cpuinfo"
|
||||
rem_line = "model name"
|
||||
elif SYSTEM == "MacOS":
|
||||
command ="sysctl -n machdep.cpu.brand_string"
|
||||
elif SYSTEM == "BSD":
|
||||
command ="sysctl hw.model"
|
||||
rem_line = "hw.model"
|
||||
|
||||
cmd_out = subprocess.check_output("LANG=C " + command, shell=True, universal_newlines=True)
|
||||
if rem_line:
|
||||
for line in cmd_out.split("\n"):
|
||||
if rem_line in line:
|
||||
name = re.sub( ".*" + rem_line + ".*:", "", line,1).lstrip()
|
||||
else:
|
||||
name = cmd_out
|
||||
nlist = name.split(" ")
|
||||
if "Xeon" in name:
|
||||
name = nlist[nlist.index("CPU")+1]
|
||||
elif "Ryzen" in name:
|
||||
name = " ".join(nlist[nlist.index("Ryzen"):nlist.index("Ryzen")+3])
|
||||
elif "CPU" in name:
|
||||
name = nlist[nlist.index("CPU")-1]
|
||||
|
||||
return name
|
||||
|
||||
def load_theme(path: str) -> Dict[str, str]:
|
||||
'''Load a bashtop formatted theme file'''
|
||||
new_theme: Dict[str, str] = {}
|
||||
try:
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
if not line.startswith("theme["): continue
|
||||
key = line[6:line.find("]")]
|
||||
s = line.find('"')
|
||||
value = line[s + 1:line.find('"', s + 1)]
|
||||
new_theme[key] = value
|
||||
except Exception as e:
|
||||
errlog.exception(str(e))
|
||||
|
||||
return new_theme
|
||||
|
||||
def fg(h_r: Union[str, int], g: int = 0, b: int = 0) -> str:
|
||||
"""Returns escape sequence to set foreground color, accepts either 6 digit hexadecimal: "#RRGGBB", 2 digit hexadecimal: "#FF" or decimal RGB: 0-255, 0-255, 0-255"""
|
||||
color: str = ""
|
||||
if isinstance(h_r, int): #* Check for decimal RGB
|
||||
color = f'\033[38;2;{h_r};{g};{b}m'
|
||||
|
||||
else: #* Check for 2 or 6 digit hexadecimal RGB
|
||||
if h_r[0] == "#": h_r = h_r[1:]
|
||||
try:
|
||||
if len(h_r) == 2:
|
||||
c = int(h_r, base=16)
|
||||
color = f'\033[38;2;{c};{c};{c}m'
|
||||
elif len(h_r) == 6:
|
||||
color = f'\033[38;2;{int(h_r[0:2], base=16)};{int(h_r[2:4], base=16)};{int(h_r[4:6], base=16)}m'
|
||||
except ValueError:
|
||||
pass
|
||||
return color
|
||||
|
||||
def bg(h_r: Union[str, int], g: int = 0, b: int = 0) -> str:
|
||||
"""Returns escape sequence to set background color, accepts either 6 digit hexadecimal: "#RRGGBB", 2 digit hexadecimal: "#FF" or decimal RGB: 0-255, 0-255, 0-255"""
|
||||
color: str = ""
|
||||
if isinstance(h_r, int): #* Check for decimal RGB
|
||||
color = f'\033[48;2;{h_r};{g};{b}m'
|
||||
|
||||
else: #* Check for 2 or 6 digit hexadecimal RGB
|
||||
if h_r[0] == "#": h_r = h_r[1:]
|
||||
try:
|
||||
if len(h_r) == 2:
|
||||
c = int(h_r, base=16)
|
||||
color = f'\033[48;2;{c};{c};{c}m'
|
||||
elif len(h_r) == 6:
|
||||
color = f'\033[48;2;{int(h_r[0:2], base=16)};{int(h_r[2:4], base=16)};{int(h_r[4:6], base=16)}m'
|
||||
except ValueError:
|
||||
pass
|
||||
return color
|
||||
|
||||
def get_key():
|
||||
"""Get a single key from stdin, convert to readable format and save to keys list"""
|
||||
input_key: str = ""
|
||||
clean_key: str = ""
|
||||
with Raw(sys.stdin): #* Set raw mode
|
||||
with Nonblocking(sys.stdin): #* Set nonblocking mode
|
||||
while not Key.stopping:
|
||||
if not select([sys.stdin], [], [], 0.1)[0]: #* Wait 100ms for input then restart loop to check for stop signal
|
||||
continue
|
||||
try:
|
||||
input_key = sys.stdin.read(1) #* Read 1 character from stdin
|
||||
if input_key == "\033": #* Read 3 additional characters if first is escape character
|
||||
input_key += sys.stdin.read(3)
|
||||
except:
|
||||
pass
|
||||
if input_key == "\033": clean_key = "escape"
|
||||
elif input_key == "\n": clean_key = "enter"
|
||||
elif input_key == "\x7f" or input_key == "\x08": clean_key = "backspace"
|
||||
|
||||
elif input_key.isprintable(): clean_key = input_key #* Return character if input key is printable
|
||||
|
||||
if clean_key:
|
||||
Key.list.append(clean_key) #* Store keys in input queue for later processing
|
||||
clean_key = ""
|
||||
Key.new.set() #* Set threading event to interrupt main thread sleep
|
||||
input_key = ""
|
||||
sys.stdin.read(100) #* Clear stdin
|
||||
|
||||
def now_sleeping(signum, frame):
|
||||
"""Reset terminal settings and stop background input read before putting to sleep"""
|
||||
Key.stop()
|
||||
print(Term.clear, Term.normal_screen, Term.show_cursor)
|
||||
os.kill(os.getpid(), signal.SIGSTOP)
|
||||
|
||||
def now_awake(signum, frame):
|
||||
"""Set terminal settings and restart background input read"""
|
||||
print(Term.alt_screen, Term.clear, Term.hide_cursor)
|
||||
Key.start()
|
||||
|
||||
def quit_sigint(signum, frame):
|
||||
"""SIGINT redirection to clean_quit()"""
|
||||
clean_quit()
|
||||
|
||||
def clean_quit(errcode: int = 0):
|
||||
"""Reset terminal settings, save settings to config and stop background input read before quitting"""
|
||||
Key.stop()
|
||||
print(Term.clear, Term.normal_screen, Term.show_cursor)
|
||||
raise SystemExit(errcode)
|
||||
|
||||
def calc_sizes():
|
||||
'''Calculate sizes of boxes'''
|
||||
|
||||
#* Calculate lines and columns from percentage values
|
||||
for box in boxes:
|
||||
setattr(box, "height", round(Term.height * getattr(box, "height_p") / 100))
|
||||
setattr(box, "width", round(Term.width * getattr(box, "width_p") / 100))
|
||||
|
||||
#* Set position values
|
||||
proc.x = mem.width + 1
|
||||
mem.y = proc.y = cpu.height + 1
|
||||
net.y = cpu.height + mem.height + 2
|
||||
|
||||
#* Set values for detailed view in process box
|
||||
proc.detailed_x = proc.x
|
||||
proc.detailed_y = proc.y
|
||||
proc.detailed_height = 8
|
||||
proc.detailed_width = proc.width
|
||||
|
||||
#THREADS = 8
|
||||
|
||||
#* Set values for cpu info sub box
|
||||
if THREADS > (cpu.height - 5) * 3 and cpu.width > 200: cpu.box_width = 24 * 4; cpu.box_height = round(THREADS / 4) + 4; cpu.box_columns = 4
|
||||
elif THREADS > (cpu.height - 5) * 2 and cpu.width > 150: cpu.box_width = 24 * 3; cpu.box_height = round(THREADS / 3) + 5; cpu.box_columns = 3
|
||||
elif THREADS > cpu.height - 5 and cpu.width > 100: cpu.box_width = 24 * 2; cpu.box_height = round(THREADS / 2) + 4; cpu.box_columns = 2
|
||||
else: cpu.box_width = 24; cpu.box_height = THREADS + 4; cpu.box_columns = 1
|
||||
|
||||
if Config.check_temp: cpu.box_width += 13 * cpu.box_columns
|
||||
if cpu.box_height > cpu.height - 3: cpu.box_height = cpu.height - 3
|
||||
cpu.box_x = (cpu.width - 2) - cpu.box_width
|
||||
cpu.box_y = cpu.y + ((cpu.height - 2) // 2) - round(cpu.box_height / 2) + 2
|
||||
|
||||
#* Set value for mem box divider
|
||||
mem.mem_width = mem.disks_width = round((mem.width-2) / 2)
|
||||
if mem.mem_width + mem.disks_width < mem.width - 2: mem.mem_width += 1
|
||||
mem.divider = mem.x + mem.mem_width + 2
|
||||
|
||||
#* Set values for net sub box
|
||||
net.box_width = 24
|
||||
net.box_height = 9 if net.height > 12 else net.height - 4
|
||||
net.box_x = net.width - net.box_width - 2
|
||||
net.box_y = net.y + ((net.height - 2) // 2) - round(net.box_height / 2)
|
||||
|
||||
def create_box(x: int = 0, y: int = 0, width: int = 0, height: int = 0, title: str = "", line_color: Color = None, fill: bool = False, box_object: object = None):
|
||||
'''Create a box from a box object or by given arguments'''
|
||||
out: str = f'{Term.fg}{Term.bg}'
|
||||
if not line_color: line_color = theme.div_line
|
||||
|
||||
#* Get values from box object if given
|
||||
if box_object:
|
||||
x = getattr(box_object, "x")
|
||||
y = getattr(box_object, "y")
|
||||
width = getattr(box_object, "width")
|
||||
height = getattr(box_object, "height")
|
||||
title = getattr(box_object, "name")
|
||||
vlines: Tuple[int, int] = (x, x + width)
|
||||
hlines: Tuple[int, int] = (y, y + height)
|
||||
|
||||
#* Fill box if enabled
|
||||
if fill:
|
||||
i: int
|
||||
for i in range(y + 1, y + height):
|
||||
out += f'{Mv.to(i, x)}{" " * (width - 1)}'
|
||||
|
||||
out += f'{line_color}'
|
||||
|
||||
#* Draw all horizontal lines
|
||||
hpos: int
|
||||
for hpos in hlines:
|
||||
out += f'{Mv.to(hpos, x)}{Symbol.h_line * width}'
|
||||
|
||||
#* Draw all vertical lines
|
||||
vpos: int
|
||||
for vpos in vlines:
|
||||
for hpos in range(y, y + height):
|
||||
out += f'{Mv.to(hpos, vpos)}{Symbol.v_line}'
|
||||
|
||||
#* Draw corners
|
||||
out += f'{Mv.to(y, x)}{Symbol.left_up}\
|
||||
{Mv.to(y, x + width)}{Symbol.right_up}\
|
||||
{Mv.to(y + height, x)}{Symbol.left_down}\
|
||||
{Mv.to(y + height, x + width)}{Symbol.right_down}'
|
||||
|
||||
#* Draw title if enabled
|
||||
if title:
|
||||
out += f'{Mv.to(y, x + 2)}{Symbol.title_left}{theme.title}{Fx.b}{title}{Fx.ub}{line_color}{Symbol.title_right}'
|
||||
|
||||
return out
|
||||
|
||||
#? Main function --------------------------------------------------------------------------------->
|
||||
|
||||
def main():
|
||||
line: str = ""
|
||||
this_key: str = ""
|
||||
count: int = 0
|
||||
while True:
|
||||
count += 1
|
||||
print(f'{Mv.to(1,1)}{Fx.b}{blue("Count:")} {count} {lime("Time:")} {time.strftime("%H:%M:%S", time.localtime())}')
|
||||
print(f'{fg("#ff")} Width: {Term.width} Height: {Term.height} Resized: {Term.resized}')
|
||||
while Key.list:
|
||||
Key.new.clear()
|
||||
this_key = Key.list.pop()
|
||||
print(f'{Mv.to(2,1)}{fg("#ff9050")}{Fx.b}Last key= {Term.fg}{Fx.ub}{repr(this_key)}{" " * 40}')
|
||||
if this_key == "backspace":
|
||||
line = line[:-1]
|
||||
elif this_key == "enter":
|
||||
line += "\n"
|
||||
else:
|
||||
line += this_key
|
||||
print(f'{Mv.to(3,1)}{fg("#90ff50")}{Fx.b}Full line= {Term.fg}{Fx.ub}{line}{Fx.bl}| {Fx.ubl}')
|
||||
if this_key == "q":
|
||||
clean_quit()
|
||||
if this_key == "R":
|
||||
raise Exception("Test ERROR")
|
||||
if not Key.reader.is_alive():
|
||||
clean_quit(1)
|
||||
Key.new.wait(1.0)
|
||||
|
||||
|
||||
#? Init Classes ---------------------------------------------------------------------------------->
|
||||
|
||||
class Banner:
|
||||
out: List[str] = []
|
||||
c_color: str = ""
|
||||
length: int = 0
|
||||
if not out:
|
||||
for num, (color, line) in enumerate(BANNER_SRC.items()):
|
||||
if len(line) > length: length = len(line)
|
||||
out += [""]
|
||||
line_color = fg(color)
|
||||
line_dark = fg(f'#{80 - num * 6}')
|
||||
for letter in line:
|
||||
if letter == "█" and c_color != line_color:
|
||||
c_color = line_color
|
||||
out[num] += line_color
|
||||
elif letter == " ":
|
||||
letter = f'{Mv.r(1)}'
|
||||
elif letter != "█" and c_color != line_dark:
|
||||
c_color = line_dark
|
||||
out[num] += line_dark
|
||||
out[num] += letter
|
||||
|
||||
@classmethod
|
||||
def draw(cls, line: int, col: int = 0, center: bool = False, now: bool = False):
|
||||
out: str = ""
|
||||
if center: col = Term.width // 2 - cls.length // 2
|
||||
for n, o in enumerate(cls.out):
|
||||
out += f'{Mv.to(line + n, col)}{o}'
|
||||
out += f'{Term.fg}'
|
||||
if now: print(out)
|
||||
else: return out
|
||||
|
||||
class Config:
|
||||
'''Holds all config variables'''
|
||||
check_temp: bool = True
|
||||
|
||||
class Graphs:
|
||||
'''Holds all graph objects and dicts for dynamically created graphs'''
|
||||
cpu: object = None
|
||||
cores: Dict[int, object] = {}
|
||||
temps: Dict[int, object] = {}
|
||||
net: object = None
|
||||
detailed_cpu: object = None
|
||||
detailed_mem: object = None
|
||||
pid_cpu: Dict[int, object] = {}
|
||||
|
||||
class Meters:
|
||||
'''Holds created meters to reuse instead of recreating meters of past values'''
|
||||
cpu: Dict[int, str] = {}
|
||||
mem_used: Dict[int, str] = {}
|
||||
mem_available: Dict[int, str] = {}
|
||||
mem_cached: Dict[int, str] = {}
|
||||
mem_free: Dict[int, str] = {}
|
||||
swap_used: Dict[int, str] = {}
|
||||
swap_free: Dict[int, str] = {}
|
||||
disks_used: Dict[int, str] = {}
|
||||
disks_free: Dict[int, str] = {}
|
||||
|
||||
class Box:
|
||||
'''Box object with all needed attributes for create_box() function'''
|
||||
def __init__(self, name: str, height_p: int, width_p: int):
|
||||
self.name: str = name
|
||||
self.height_p: int = height_p
|
||||
self.width_p: int = width_p
|
||||
self.x: int = 0
|
||||
self.y: int = 0
|
||||
self.width: int = 0
|
||||
self.height: int = 0
|
||||
self.out: str = ""
|
||||
if name == "proc":
|
||||
self.detailed: bool = False
|
||||
self.detailed_x: int = 0
|
||||
self.detailed_y: int = 0
|
||||
self.detailed_width: int = 0
|
||||
self.detailed_height: int = 8
|
||||
if name == "mem":
|
||||
self.divider: int = 0
|
||||
self.mem_width: int = 0
|
||||
self.disks_width: int = 0
|
||||
if name in ("cpu", "net"):
|
||||
self.box_x: int = 0
|
||||
self.box_y: int = 0
|
||||
self.box_width: int = 0
|
||||
self.box_height: int = 0
|
||||
self.box_columns: int = 0
|
||||
|
||||
|
||||
#? Init variables -------------------------------------------------------------------------------->
|
||||
|
||||
CPU_NAME: str = get_cpu_name()
|
||||
|
||||
|
||||
#theme = Theme(load_theme("/home/gnm/.config/bashtop/themes/monokai.theme"))
|
||||
theme = Theme(DEFAULT_THEME)
|
||||
|
||||
cpu = Box("cpu", height_p=32, width_p=100)
|
||||
mem = Box("mem", height_p=40, width_p=45)
|
||||
net = Box("net", height_p=28, width_p=mem.width_p)
|
||||
proc = Box("proc", height_p=100 - cpu.height_p, width_p=100 - mem.width_p)
|
||||
|
||||
boxes: List[object] = [cpu, mem, net, proc]
|
||||
|
||||
blue = theme.temp_start
|
||||
lime = theme.cached_mid
|
||||
orange = theme.available_end
|
||||
green = theme.cpu_start
|
||||
dfg = theme.main_fg
|
||||
|
||||
|
||||
|
||||
def draw_bg(now: bool = True):
|
||||
'''Draw all boxes to buffer and print to screen if now=True'''
|
||||
Draw.buffer("bg", clear=True)
|
||||
|
||||
#* Draw cpu box and cpu sub box
|
||||
cpu_box = f'{create_box(box_object=cpu, line_color=theme.cpu_box, fill=True)}\
|
||||
{Mv.to(cpu.y, cpu.x + 10)}{theme.cpu_box(Symbol.title_left)}{Fx.b}{theme.hi_fg("m")}{theme.title("enu")}{Fx.ub}{theme.cpu_box(Symbol.title_right)}\
|
||||
{create_box(x=cpu.box_x, y=cpu.box_y, width=cpu.box_width, height=cpu.box_height, line_color=theme.div_line, title=CPU_NAME[:18 if Config.check_temp else 9])}'
|
||||
|
||||
#* Draw mem/disk box and divider
|
||||
mem_box = f'{create_box(box_object=mem, line_color=theme.mem_box, fill=True)}\
|
||||
{Mv.to(mem.y, mem.divider + 2)}{theme.mem_box(Symbol.title_left)}{Fx.b}{theme.title("disks")}{Fx.ub}{theme.mem_box(Symbol.title_right)}\
|
||||
{Mv.to(mem.y, mem.divider)}{theme.mem_box(Symbol.div_up)}\
|
||||
{Mv.to(mem.y + mem.height, mem.divider)}{theme.mem_box(Symbol.div_down)}{theme.div_line}'
|
||||
for i in range(1, mem.height):
|
||||
mem_box += f'{Mv.to(mem.y + i, mem.divider)}{Symbol.v_line}'
|
||||
|
||||
#* Draw net box and net sub box
|
||||
net_box = f'{create_box(box_object=net, line_color=theme.net_box, fill=True)}\
|
||||
{create_box(x=net.box_x, y=net.box_y, width=net.box_width, height=net.box_height, line_color=theme.div_line, title="Download")}\
|
||||
{Mv.to(net.box_y + net.box_height, net.box_x + 1)}{theme.div_line(Symbol.title_left)}{Fx.b}{theme.title("Upload")}{Fx.ub}{theme.div_line(Symbol.title_right)}'
|
||||
|
||||
#* Draw proc box
|
||||
proc_box = create_box(box_object=proc, line_color=theme.proc_box, fill=True)
|
||||
|
||||
Draw.buffer("bg", cpu_box, mem_box, net_box, proc_box)
|
||||
|
||||
def testing_colors():
|
||||
for item, _ in DEFAULT_THEME.items():
|
||||
print(Fx.b, getattr(theme, item)(f'{item:<20}'), Fx.ub, f'{"hex=" + getattr(theme, item).hex:<20} dec={getattr(theme, item).dec}', end="\n")
|
||||
|
||||
print()
|
||||
print(theme.temp_start, "Hej!\n")
|
||||
print(Term.fg, "\nHEJ\n")
|
||||
print(repr(Term.fg), repr(Term.bg))
|
||||
|
||||
quit()
|
||||
|
||||
def testing_banner():
|
||||
print(Term.normal_screen, Term.alt_screen)
|
||||
|
||||
#try:
|
||||
#sad
|
||||
#except Exception as e:
|
||||
# errlog.exception(f'{e}')
|
||||
|
||||
|
||||
|
||||
#eprint("Test")
|
||||
calc_sizes()
|
||||
|
||||
draw_bg()
|
||||
|
||||
Draw.buffer("banner", Banner.draw(18, 45), clear=True)
|
||||
Draw.out()
|
||||
|
||||
print(Mv.to(35, 1), repr(Term.fg), " ", repr(Term.bg), "\n")
|
||||
|
||||
# quit()
|
||||
|
||||
# global theme
|
||||
|
||||
# path = "/home/gnm/.config/bashtop/themes/"
|
||||
# for file in os.listdir(path):
|
||||
# if file.endswith(".theme"):
|
||||
# theme = Theme(load_theme(path + file))
|
||||
# draw_bg()
|
||||
# Draw.out()
|
||||
# time.sleep(1)
|
||||
|
||||
#draw_bg()
|
||||
#Draw.buffer("banner", Banner.draw(5, center=True))
|
||||
|
||||
#Draw.out()
|
||||
|
||||
# print(f'\n{Fx.b}Terminal Height={Term.height} Width={Term.width}')
|
||||
# total_h = total_w = 0
|
||||
# for box in boxes:
|
||||
# print(f'\n{getattr(box, "name")} Height={getattr(box, "height")} Width={getattr(box, "width")}')
|
||||
# total_h += getattr(box, "height")
|
||||
# total_w += getattr(box, "width")
|
||||
# print(f'\nTotal Height={cpu.height + net.height + mem.height} Width={net.width + proc.width}')
|
||||
|
||||
quit()
|
||||
|
||||
|
||||
|
||||
#testing_colors()
|
||||
#error_log("/home/gnm/bashtop/misc/error.log")
|
||||
|
||||
|
||||
#testing_banner()
|
||||
|
||||
|
||||
#quit()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
#? Setup signal handlers for SIGSTP, SIGCONT, SIGINT and SIGWINCH
|
||||
signal.signal(signal.SIGTSTP, now_sleeping) #* Ctrl-Z
|
||||
signal.signal(signal.SIGCONT, now_awake) #* Resume
|
||||
signal.signal(signal.SIGINT, quit_sigint) #* Ctrl-C
|
||||
signal.signal(signal.SIGWINCH, Term.refresh) #* Terminal resized
|
||||
|
||||
#? Switch to alternate screen, clear screen and hide cursor
|
||||
print(Term.alt_screen, Term.clear, Term.hide_cursor)
|
||||
|
||||
#? Start a separate thread for reading keyboard input
|
||||
try:
|
||||
Key.start()
|
||||
except Exception as e:
|
||||
errlog.exception(f'{e}')
|
||||
clean_quit(1)
|
||||
|
||||
while True:
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
errlog.exception(f'{e}')
|
||||
clean_quit(1)
|
||||
|
||||
|
||||
|
||||
|
||||
clean_quit()
|
Loading…
Reference in New Issue