diff --git a/README.md b/README.md index ff0f8393..426b9f1d 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,59 @@ -# archey4 +# Archey 4 -![archey4](https://horlogeskynet.github.io/img/blog/the-archey-project-what-i-ve-decided-to-do.png?v410) +![archey4](https://horlogeskynet.github.io/img/blog/the-archey-project-what-i-ve-decided-to-do.png?v4.3.0) -#### Why (again) a f*cking new Archey fork ? +## Why (again) a f*cking new Archey fork ? The answer is [here](https://horlogeskynet.github.io/archey4). -> Note : Since the 21st September of 2017, you may notice that this repository no longer has the official status of _fork_. +> Note : Since the 21st September of 2017, you may notice that this repository no longer has the official status of fork. > Actually, the maintainer decided to separate it from the original one's "network" with the help of the _GitHub_'s staff. -> Nevertheless, **this piece of software is still a _fork_ of the [djmelik's Archey project](https://github.com/djmelik/archey.git)**. +> Nevertheless, **this piece of software is still a fork of the [djmelik's Archey project](https://github.com/djmelik/archey.git)**. -#### Which packages do I need to run this script ? +## Which packages do I need to run this script ? -##### Required packages +### Required packages * python3 * lsb-release +* procps -##### Highly recommended packages +### Highly recommended packages | Environments | Packages | Reasons | Notes | | :----------- | :--------: | :-----------------------------------: | :---: | -| All | `dnsutils` | _WAN\_IP_ will be detected 5x faster | ∅ | -| Graphical | `wmctrl` | _WindowManager_ will be more accurate | ∅ | -| Virtual | `virt-what`
`dmidecode` | _Model_ will contain details about the hypervisor | `archey` will need to be run as **root** | +| All | `dnsutils`
`net-tools` | **WAN_IP** and **LAN_IP** would be detected faster | They will provide `dig` and `hostname` | +| Graphical | `pciutils`
`wmctrl` | **GPU** wouldn't be detected without it
**WindowManager** would be more accurate | `pciutils` will provide `lspci` | +| Virtual | `virt-what`
`dmidecode` | **Model** would contain details about the hypervisor | `archey` will need to be run as **root** | ## Installation -### Install latest stable release from source +### Install from package + +First, grab a package for your distribution from the latest release [here](https://github.com/HorlogeSkynet/archey4/releases/latest). +Now, it's time to use your favorite packages manager. Some examples : + +* Arch-based distributions ([source](https://aur.archlinux.org/packages/archey4/)) + + ```shell + pacman -U ./archey4-v4.X.Z-R-any.pkg.tar.xz + ``` + +* Debian-based distributions ([source](https://labs.pixelswap.fr/HorlogeSkynet/archey4-packaging)) + + ```shell + apt install ./archey4-4.Y.Z-R-all.deb + ``` + +* Red Hat, Fedora, OpenSuse, ... ([source](https://labs.pixelswap.fr/HorlogeSkynet/archey4-packaging)) + + ```shell + dnf install ./archey4-4.Y.Z-R.noarch.rpm + ``` + +### Install from source + +#### Latest stable release ```shell $ wget -O archey4.tar.gz https://github.com/HorlogeSkynet/archey4/archive/master.tar.gz @@ -37,7 +63,7 @@ $ chmod +x archey $ sudo cp archey /usr/local/bin/archey ``` -### Install (or update) development version from source +#### Development version ```shell $ git clone https://github.com/HorlogeSkynet/archey4.git @@ -54,12 +80,23 @@ $ sudo cp archey /usr/local/bin/archey $ archey ``` -#### Notes to users +## Configuration (optional) + +Since the version 1.4.0, Archey 4 **may** be "tweaked" a bit with external configuration. +You can place a [`config.json`](config.json) file in these locations : + +1. `./config.json` (beside the script itself) +2. `~/.config/archey4/config.json` (in your home directory) +3. `/etc/archey4/config.json` + +If an option is defined in multiple places, it will be overridden according to the order above (local preferences > user preferences > system preferences). + +## Notes to users -* If you run `archey` as root, the script will list the processes running by other users on your system in order to display correctly _Window Manager_ & _Desktop Environment_ outputs. +* If you run `archey` as root, the script will list the processes running by other users on your system in order to display correctly **Window Manager** & **Desktop Environment** outputs. * During the setup procedure, I advised you to copy this script into the `/usr/local/bin/` folder, you may want to check what it does beforehand. -* If you experience any trouble during installation or usage, please do [**open an _issue_**](https://github.com/HorlogeSkynet/archey4/issues/new). +* If you experience any trouble during installation or usage, please do **[open an issue](https://github.com/HorlogeSkynet/archey4/issues/new)**. -* If you had to adapt the script to make it working with your system, please [**open a _pull request_**](https://github.com/HorlogeSkynet/archey4/pulls) so as to share your modifications with the rest of the world and participate to this project ! +* If you had to adapt the script to make it working with your system, please **[open a pull request](https://github.com/HorlogeSkynet/archey4/pulls)** so as to share your modifications with the rest of the world and participate in this project ! diff --git a/archey b/archey index 7f571bb0..cc1d9d06 100755 --- a/archey +++ b/archey @@ -12,7 +12,7 @@ # Fedora support by YeOK # First IP handling by Normand Cyr # -# Currently maintained by Samuel FORESTIER +# Currently maintained by Samuel FORESTIER # # This program IS A FORK of # the original Archey project @@ -21,10 +21,13 @@ # See for the full license text. +import json +import os import re from enum import Enum from math import floor +from glob import glob from os import getenv, getuid from subprocess import Popen, PIPE, DEVNULL, check_output, \ CalledProcessError, TimeoutExpired @@ -370,6 +373,34 @@ logosDict = { {c[1]} {r[17]}\n""" } +# ----------- Configuration ----------- # +config = { + 'default_strings': { + 'no_address': 'No Address', + 'not_detected': 'Not detected' + }, + 'temperature': { + 'use_fahrenheit': False + } +} + + +def loadConfiguration(path): + if not path.endswith('/'): + path += '/' + + path += 'config.json' + + try: + with open(path) as file: + config.update(json.load(file)) + + except FileNotFoundError: + pass + + except json.JSONDecodeError as e: + print('Warning: {0} ({1})'.format(e, path)) + # ---------- Global variables --------- # @@ -380,10 +411,6 @@ PROCESSES = check_output([ '-o', 'comm', '--no-headers' ]).decode().rstrip().split('\n') -# Default strings for non-reachable information -NO_ADRESS = 'No Address' -NOT_DETECTED = 'Not detected' - # -------------- Classes -------------- # @@ -425,7 +452,7 @@ class Output: class User: def __init__(self): - self.value = getenv('USER', NOT_DETECTED) + self.value = getenv('USER', config['default_strings']['not_detected']) class Hostname: @@ -484,7 +511,7 @@ class Model: model = 'Bare-metal environment' except (FileNotFoundError, CalledProcessError): - model = NOT_DETECTED + model = config['default_strings']['not_detected'] self.value = model @@ -532,7 +559,7 @@ class WindowManager: break else: - wm = NOT_DETECTED + wm = config['default_strings']['not_detected'] self.value = wm @@ -546,34 +573,75 @@ class DesktopEnvironment: else: # Let's rely on an environment var if the loop above didn't `break` - de = getenv('XDG_CURRENT_DESKTOP', NOT_DETECTED) + de = getenv('XDG_CURRENT_DESKTOP', + config['default_strings']['not_detected']) self.value = de class Shell: def __init__(self): - self.value = getenv('SHELL', NOT_DETECTED) + self.value = getenv('SHELL', config['default_strings']['not_detected']) class Terminal: def __init__(self): - self.value = getenv('TERM', NOT_DETECTED) + self.value = getenv('TERM', config['default_strings']['not_detected']) + + +class Temperature: + def __init__(self): + temps = [] + + try: + # Let's try to retrieve a value from 'Broadcom' chip on Raspberry + temp = float(re.findall( + '\d+\.\d+', + check_output(['/opt/vc/bin/vcgencmd', 'measure_temp'], + stderr=DEVNULL).decode())[0]) + temps.append( + self.convertToFahrenheit(temp) + if config['temperature']['use_fahrenheit'] else temp) + + except (FileNotFoundError, CalledProcessError): + pass + + # Now we just check for values within files present in the path below + for thermalFile in glob('/sys/class/thermal/thermal_zone*/temp'): + with open(thermalFile) as file: + temp = float(file.read().strip()) / 1000 + if temp != 0.0: + temps.append( + self.convertToFahrenheit(temp) + if config['temperature']['use_fahrenheit'] else temp) + + if temps: + self.value = '{0} {2} (Max. {1} {2})'.format( + str(round(sum(temps) / len(temps), 1)), + str(round(max(temps), 1)), + 'F' if config['temperature']['use_fahrenheit'] else 'C') + + else: + self.value = config['default_strings']['not_detected'] + + """ + Simple Celsius to Fahrenheit conversion method + """ + def convertToFahrenheit(self, temp): + return temp * (9 / 5) + 32 class Packages: def __init__(self): - for packagesTool in [ - ['pacman', '-Q'], - ['dnf', 'list', 'installed'], - ['dpkg', '--get-selections'], - ['zypper', 'search', '--installed-only'], - ['emerge', '-ep', 'world'], - ['rpm', '-qa'] - ]: + for packagesTool in [['pacman', '-Q'], + ['dnf', 'list', 'installed'], + ['dpkg', '--get-selections'], + ['zypper', 'search', '--installed-only'], + ['emerge', '-ep', 'world'], + ['rpm', '-qa']]: try: results = check_output(packagesTool, stderr=DEVNULL).decode() - packages = len(results.rstrip().split('\n')) + packages = results.count('\n') if 'dpkg' in packagesTool: packages -= results.count('deinstall') @@ -581,7 +649,10 @@ class Packages: break except (FileNotFoundError, CalledProcessError): - packages = NOT_DETECTED + pass + + else: + packages = config['default_strings']['not_detected'] self.value = packages @@ -607,7 +678,7 @@ class GPU: gpuinfo)[0].strip() + '...' except (FileNotFoundError, CalledProcessError): - gpuinfo = NOT_DETECTED + gpuinfo = config['default_strings']['not_detected'] self.value = gpuinfo @@ -622,29 +693,44 @@ class RAM: used = float(ram[2]) total = float(ram[1]) - except IndexError: + except (IndexError, FileNotFoundError): + # An in-digest one-liner to retrieve memory info into a dictionary with open('/proc/meminfo') as file: - ram = file.read().split('\n') - - # A little closure to convert `/proc/meminfo` lines as `float` - def convert(line): - return float(line.split(':')[1].strip(' kB')) - - total = convert(ram[0]) / 1024 - used = total - ((convert(ram[1]) + convert(ram[3]) + - convert(ram[4]) + convert(ram[22])) / 1024) + ram = { + i.split(':')[0]: float(i.split(':')[1].strip(' kB')) / 1024 + for i in filter(None, file.read().split('\n')) + } + + total = ram['MemTotal'] + # Here, let's imitate the `free` command behavior + # (https://gitlab.com/procps-ng/procps/blob/master/proc/sysinfo.c#L787) + used = total - (ram['MemFree'] + ram['Cached'] + ram['Buffers']) + if used < 0: + used += ram['Cached'] + ram['Buffers'] self.value = '{0}{1} MB{2} / {3} MB'.format( - colorDict['sensors'][int((used / total) * 100) // 33], + colorDict['sensors'][ + int(((used / total) * 100) // 33.34) + ], int(used), colorDict['clear'], int(total)) class Disk: def __init__(self): - total = re.sub(',', '.', check_output(['df', '-Tlh', '-B', 'GB', '--total', '-t', 'ext4', '-t', 'ext3', '-t', 'ext2', '-t', 'reiserfs', '-t', 'jfs', '-t', 'ntfs', '-t', 'fat32', '-t', 'btrfs', '-t', 'fuseblk', '-t', 'xfs', '-t', 'simfs', '-t', 'tmpfs', '-t', 'zfs']).decode().splitlines()[-1]).split() + total = re.sub(',', '.', + check_output([ + 'df', '-Tlh', '-B', 'GB', '--total', + '-t', 'ext4', '-t', 'ext3', '-t', 'ext2', + '-t', 'reiserfs', '-t', 'jfs', '-t', 'zfs', + '-t', 'ntfs', '-t', 'fat32', '-t', 'btrfs', + '-t', 'fuseblk', '-t', 'xfs', + '-t', 'simfs', '-t', 'tmpfs' + ]).decode().splitlines()[-1]).split() self.value = '{0}{1}{2} / {3}'.format( - colorDict['sensors'][int(total[5][:-1]) // 33], + colorDict['sensors'][ + int(float(total[5][:-1]) // 33.34) + ], re.sub('GB', ' GB', total[3]), colorDict['clear'], re.sub('GB', ' GB', total[2])) @@ -655,11 +741,11 @@ class LAN_IP: self.value = ', '.join(check_output(['hostname', '-I'], stderr=DEVNULL ).decode().rstrip().split() - ) or NO_ADRESS + ) or config['default_strings']['no_address'] except CalledProcessError: # Slow manual workaround for old `inetutils` versions, with `ip` - self.value = ', '.join(check_output(['cut', '-d', ' ', '-f', '3'], stdin=Popen(['cut', '-d', '/', '-f', '1'], stdout=PIPE, stdin=Popen(['tr', '-s', ' '], stdout=PIPE, stdin=Popen(['grep', '-v', ' lo'], stdout=PIPE, stdin=Popen(['grep', 'inet '], stdout=PIPE, stdin=Popen(['ip', 'addr', 'show', 'up'], stdout=PIPE).stdout).stdout).stdout).stdout).stdout).decode().split()) or NO_ADRESS + self.value = ', '.join(check_output(['cut', '-d', ' ', '-f', '3'], stdin=Popen(['cut', '-d', '/', '-f', '1'], stdout=PIPE, stdin=Popen(['tr', '-s', ' '], stdout=PIPE, stdin=Popen(['grep', '-v', ' lo'], stdout=PIPE, stdin=Popen(['grep', 'inet '], stdout=PIPE, stdin=Popen(['ip', 'addr', 'show', 'up'], stdout=PIPE).stdout).stdout).stdout).stdout).stdout).decode().split()) or config['default_strings']['no_address'] class WAN_IP: @@ -678,7 +764,7 @@ class WAN_IP: ], timeout=1).decode() except (CalledProcessError, TimeoutExpired): - self.value = NO_ADRESS + self.value = config['default_strings']['no_address'] # -------------- Classes' Enumeration -------------- # @@ -695,6 +781,7 @@ class Classes(Enum): Shell = Shell Terminal = Terminal Packages = Packages + Temperature = Temperature CPU = CPU GPU = GPU RAM = RAM @@ -707,6 +794,11 @@ class Classes(Enum): if __name__ == '__main__': + # Load global and local configurations in a "regular" order (all optional) + loadConfiguration('/etc/archey4/') + loadConfiguration(os.getcwd() + '/.config/archey4/') + loadConfiguration(os.path.dirname(os.path.realpath(__file__))) + output = Output() for key in Classes: output.append(key.name, key.value().value) diff --git a/config.json b/config.json new file mode 100644 index 00000000..988a95f7 --- /dev/null +++ b/config.json @@ -0,0 +1,9 @@ +{ + "default_strings": { + "no_address": "No Address", + "not_detected": "Not detected" + }, + "temperature": { + "use_fahrenheit": false + } +}