| 1 | #!/usr/bin/env python3 |
| 2 | # _*_ coding: utf-8 _*_ |
| 3 | |
| 4 | """ |
| 5 | A psutil-based command to display customizable system usage info in a single line, intended for Tint2 executors |
| 6 | |
| 7 | Author: Piotr Miller |
| 8 | e-mail: nwg.piotr@gmail.com |
| 9 | Website: http://nwg.pl |
| 10 | Project: https://github.com/nwg-piotr/psuinfo |
| 11 | License: GPL3 |
| 12 | |
| 13 | Inspired by https://github.com/tknomanzr/scripts/blob/master/tint2/executors/cpu.py by William Bradley (@tknomanzr) |
| 14 | """ |
| 15 | |
| 16 | import sys |
| 17 | import psutil |
| 18 | import time |
| 19 | import os |
| 20 | |
| 21 | |
| 22 | def main(): |
| 23 | fahrenheit = False |
| 24 | names = False |
| 25 | testing = False |
| 26 | time_start = None |
| 27 | components = "gStfM" |
| 28 | separator = " " |
| 29 | home = os.getenv("HOME") |
| 30 | draw_icons = False |
| 31 | |
| 32 | pcpu, avg, speed, freqs, temp, fans, b_time, memory, swap, disks_usage, which, ul, dl, xfer_start, xfer_finish, \ |
| 33 | path_to_icon, c_name= None, None, None, None, None, None, None, None, None, None, None, None, None, None, \ |
| 34 | None, None, None |
| 35 | |
| 36 | for i in range(1, len(sys.argv)): |
| 37 | if sys.argv[i] == "-h" or sys.argv[i] == "--help": |
| 38 | print_help() |
| 39 | exit(0) |
| 40 | |
| 41 | if sys.argv[i] == "-F": |
| 42 | fahrenheit = True |
| 43 | |
| 44 | if sys.argv[i] == "-N": |
| 45 | names = True |
| 46 | |
| 47 | if sys.argv[i] == "-T": |
| 48 | testing = True |
| 49 | |
| 50 | if sys.argv[i].startswith("-C"): |
| 51 | components = sys.argv[i][2::] |
| 52 | |
| 53 | if sys.argv[i].startswith("-S"): |
| 54 | try: |
| 55 | # if number given |
| 56 | spacing = int(sys.argv[i][2::]) |
| 57 | separator = " " * spacing |
| 58 | except ValueError: |
| 59 | # string given |
| 60 | separator = sys.argv[i][2::] |
| 61 | |
| 62 | if sys.argv[i].startswith("-W"): |
| 63 | try: |
| 64 | which = int(sys.argv[i][2::]) |
| 65 | except ValueError: |
| 66 | pass |
| 67 | |
| 68 | if sys.argv[i].upper() == "-ALL": |
| 69 | components = "gpaQStfMcWDUk" |
| 70 | names = True |
| 71 | testing = True |
| 72 | |
| 73 | if sys.argv[i].startswith("-I"): |
| 74 | draw_icons = True |
| 75 | # We can only have one icon per executor, so let's strip components to the first one |
| 76 | components = sys.argv[i][2] |
| 77 | # exception for UL/DL speed; to assign an icon to it we need to calculate speeds first |
| 78 | if components != "k": |
| 79 | path_to_icon = icon_path(home, components) |
| 80 | |
| 81 | if sys.argv[i].startswith("-M"): |
| 82 | # We can only have a custom name for a single component |
| 83 | components = components[0] |
| 84 | names = True |
| 85 | c_name = sys.argv[i][2::] |
| 86 | |
| 87 | if testing: |
| 88 | time_start = int(round(time.time() * 1000)) |
| 89 | |
| 90 | output = "" |
| 91 | |
| 92 | # Prepare ONLY requested data, ONLY once |
| 93 | if "g" or "p" in components: |
| 94 | try: |
| 95 | pcpu = psutil.cpu_percent(interval=1, percpu=True) |
| 96 | except: |
| 97 | pass |
| 98 | |
| 99 | if "a" in components: |
| 100 | try: |
| 101 | avg = str(psutil.cpu_percent(interval=1)) |
| 102 | if len(avg) < 4: |
| 103 | avg = " " + avg |
| 104 | except: |
| 105 | pass |
| 106 | |
| 107 | if "s" or "S" in components: |
| 108 | try: |
| 109 | speed = psutil.cpu_freq(False) |
| 110 | except: |
| 111 | pass |
| 112 | |
| 113 | if "q" or "Q" in components: |
| 114 | try: |
| 115 | freqs = psutil.cpu_freq(True) |
| 116 | if len(freqs) == 0: |
| 117 | freqs = None |
| 118 | except: |
| 119 | pass |
| 120 | |
| 121 | if "t" in components: |
| 122 | try: |
| 123 | temp = psutil.sensors_temperatures(fahrenheit) |
| 124 | except: |
| 125 | pass |
| 126 | |
| 127 | if "f" in components: |
| 128 | try: |
| 129 | fans = psutil.sensors_fans() |
| 130 | except: |
| 131 | pass |
| 132 | |
| 133 | if "m" or "M" or "z" or "Z" in components: |
| 134 | try: |
| 135 | memory = psutil.virtual_memory() |
| 136 | except: |
| 137 | pass |
| 138 | |
| 139 | if "w" or "W" or "x" in components: |
| 140 | try: |
| 141 | swap = psutil.swap_memory() |
| 142 | except: |
| 143 | pass |
| 144 | |
| 145 | if "k" in components: |
| 146 | try: |
| 147 | xfer_start = psutil.net_io_counters() |
| 148 | time.sleep(1) |
| 149 | xfer_finish = psutil.net_io_counters() |
| 150 | ul = (xfer_finish[0] - xfer_start[0]) / 1024 |
| 151 | dl = (xfer_finish[1] - xfer_start[1]) / 1024 |
| 152 | # We've not selected an icon previously. Now we have enough data. |
| 153 | if draw_icons: |
| 154 | path_to_icon = net_icon(home, ul, dl) |
| 155 | except: |
| 156 | pass |
| 157 | |
| 158 | drives = [] |
| 159 | # Find drive names, mountpoints |
| 160 | if "d" or "D" or "n" or "N" in components: |
| 161 | try: |
| 162 | d = psutil.disk_partitions() |
| 163 | # This will store name, mountpoint |
| 164 | for entry in d: |
| 165 | n = entry[0].split("/") |
| 166 | name = n[len(n) - 1] |
| 167 | # name, mountpoint |
| 168 | drive = name, entry[1] |
| 169 | drives.append(drive) |
| 170 | except: |
| 171 | pass |
| 172 | |
| 173 | if "d" or "D" in components: |
| 174 | try: |
| 175 | disks_usage = [] |
| 176 | for drive in drives: |
| 177 | # Search drives by path |
| 178 | data = psutil.disk_usage(drive[1]) |
| 179 | # Store name, used, total, percent |
| 180 | essential = drive[0].upper(), data[1], data[0], data[3] |
| 181 | disks_usage.append(essential) |
| 182 | except: |
| 183 | pass |
| 184 | |
| 185 | if "n" in components or "N" in components: |
| 186 | try: |
| 187 | disks_usage = [] |
| 188 | for drive in drives: |
| 189 | # Search drives by path |
| 190 | data = psutil.disk_usage(drive[1]) |
| 191 | # Store mountpoint, used, total, percent |
| 192 | essential = drive[1], data[1], data[0], data[3] |
| 193 | disks_usage.append(essential) |
| 194 | except: |
| 195 | pass |
| 196 | |
| 197 | if "u" or "U" in components: |
| 198 | try: |
| 199 | b_time = psutil.boot_time() |
| 200 | except: |
| 201 | pass |
| 202 | |
| 203 | # Build output component after component |
| 204 | output += separator |
| 205 | |
| 206 | for char in components: |
| 207 | if char == "g" and pcpu is not None: |
| 208 | if c_name: |
| 209 | output += c_name |
| 210 | output += graph_per_cpu(pcpu) + separator |
| 211 | |
| 212 | if char == "p" and pcpu is not None: |
| 213 | if names: |
| 214 | output += c_name if c_name else "CPU: " |
| 215 | output += per_cpu(pcpu) + separator |
| 216 | |
| 217 | if char == "a" and avg is not None: |
| 218 | if names: |
| 219 | output += c_name if c_name else "avCPU: " |
| 220 | output += avg + "%" + separator |
| 221 | |
| 222 | if char == "q" and freqs is not None: |
| 223 | if names: |
| 224 | output += c_name if c_name else "CPU: " |
| 225 | output += freq_per_cpu(freqs)[0][:-1] + " GHz" + separator |
| 226 | |
| 227 | if char == "Q" and freqs is not None: |
| 228 | if names: |
| 229 | output += c_name if c_name else "CPU: " |
| 230 | result = freq_per_cpu(freqs) |
| 231 | output += result[0][:-1] + "/" + str(result[1]) + " GHz" + separator |
| 232 | |
| 233 | if char == "s" and speed is not None: |
| 234 | if names: |
| 235 | output += c_name if c_name else "SPD: " |
| 236 | output += str(round(speed[0] / 1000, 1)) + " GHz" + separator |
| 237 | |
| 238 | if char == "S" and speed is not None: |
| 239 | if names: |
| 240 | output += c_name if c_name else "avSPD: " |
| 241 | output += str(round(speed[0] / 1000, 1)) + "/" + str(round(speed[2] / 1000, 1)) + " GHz" + separator |
| 242 | |
| 243 | if char == "t" and temp is not None and len(temp) > 0: |
| 244 | if names: |
| 245 | output += c_name if c_name else "CORE: " |
| 246 | if "k10temp" in temp.keys(): |
| 247 | # ryzen, multiple Die temperatures for threadripper/Epyc |
| 248 | ryzen_die_temps = [sensor.current for sensor in temp["k10temp"] if sensor.label == 'Tdie'] |
| 249 | output += str(int(max(ryzen_die_temps))) |
| 250 | if "coretemp" in temp.keys(): |
| 251 | # intel |
| 252 | output += str(int(temp["coretemp"][0][1])) |
| 253 | output += "℉" if fahrenheit else "℃" |
| 254 | output += separator |
| 255 | |
| 256 | if char == "f" and fans is not None and len(fans) > 0: |
| 257 | if names: |
| 258 | output += c_name if c_name else "FAN: " |
| 259 | fan0 = next(iter(fans.values())) |
| 260 | output += str(fan0[0][1]) + "/m" + separator |
| 261 | |
| 262 | if char == 'm' and memory is not None: |
| 263 | if names: |
| 264 | output += c_name if c_name else "MEM: " |
| 265 | output += str(round((memory[0] - memory[1]) / 1073741824, 1)) + " GB" + separator |
| 266 | |
| 267 | if char == 'M' and memory is not None: |
| 268 | if names: |
| 269 | output += c_name if c_name else "MEM: " |
| 270 | output += str(round((memory[3]) / 1073741824, 1)) + "/" + str( |
| 271 | round(memory[0] / 1073741824, 1)) + " GB" + separator |
| 272 | |
| 273 | if char == 'c' and memory is not None: |
| 274 | if names: |
| 275 | output += c_name if c_name else "MEM: " |
| 276 | output += str(memory[2]) + "%" + separator |
| 277 | |
| 278 | if char == 'C' and memory is not None: |
| 279 | if names: |
| 280 | output += c_name if c_name else "MEM: " |
| 281 | output += str(100 - memory[2]) + "%" + separator |
| 282 | |
| 283 | if char == 'u' and b_time is not None: |
| 284 | up_time = int(time.time()) - b_time |
| 285 | m, s = divmod(up_time, 60) |
| 286 | h, m = divmod(m, 60) |
| 287 | if names: |
| 288 | output += c_name if c_name else "UP: " |
| 289 | output += "%d:%02d" % (h, m) + separator |
| 290 | |
| 291 | if char == 'U' and b_time is not None: |
| 292 | up_time = int(time.time()) - b_time |
| 293 | m, s = divmod(up_time, 60) |
| 294 | h, m = divmod(m, 60) |
| 295 | if names: |
| 296 | output += c_name if c_name else "UP: " |
| 297 | output += "%d:%02d:%02d" % (h, m, s) + separator |
| 298 | |
| 299 | if char == "w" and swap is not None: |
| 300 | if names: |
| 301 | output += c_name if c_name else "SWAP: " |
| 302 | output += str(round(swap[1] / 1073741824, 1)) + " GB" + separator |
| 303 | |
| 304 | if char == "W" and swap is not None: |
| 305 | if names: |
| 306 | output += c_name if c_name else "SWAP: " |
| 307 | output += str(round(swap[1] / 1073741824, 1)) + "/" |
| 308 | output += str(round(swap[0] / 1073741824, 1)) + " GB" + separator |
| 309 | |
| 310 | if char == "x" and swap is not None: |
| 311 | if names: |
| 312 | output += c_name if c_name else "SWAP: " |
| 313 | output += str(swap[3]) + "%" + separator |
| 314 | |
| 315 | if char == "d" or char == "n" and disks_usage is not None: |
| 316 | if which is not None: |
| 317 | try: |
| 318 | entry = disks_usage[which] |
| 319 | output += entry[0] + ": " |
| 320 | output += str(entry[3]) + "%" + separator |
| 321 | except IndexError: |
| 322 | pass |
| 323 | else: |
| 324 | for entry in disks_usage: |
| 325 | output += entry[0] + ": " |
| 326 | output += str(entry[3]) + "%" + separator |
| 327 | |
| 328 | if char == "D" or char == "N" and disks_usage is not None: |
| 329 | if c_name: |
| 330 | output += c_name |
| 331 | if which is not None: |
| 332 | try: |
| 333 | entry = disks_usage[which] |
| 334 | output += entry[0] + ": " |
| 335 | output += str(round(entry[1] / 1073741824, 1)) + "/" |
| 336 | output += str(round(entry[2] / 1073741824, 1)) + " GB" + separator |
| 337 | except IndexError: |
| 338 | pass |
| 339 | else: |
| 340 | for entry in disks_usage: |
| 341 | output += entry[0] + ": " |
| 342 | output += str(round(entry[1] / 1073741824, 1)) + "/" |
| 343 | output += str(round(entry[2] / 1073741824, 1)) + " GB" + separator |
| 344 | |
| 345 | if char == "k": |
| 346 | if names and xfer_start is not None and xfer_finish is not None: |
| 347 | output += c_name if c_name else "Net: " |
| 348 | output += '{:0.2f}'.format((xfer_finish[0] - xfer_start[0]) / 1024) + ' {:0.2f} kB/s'.format( |
| 349 | (xfer_finish[1] - xfer_start[1]) / 1024) + separator |
| 350 | |
| 351 | if testing: |
| 352 | output += "[" + str(int((round(time.time() * 1000)) - time_start) / 1000) + "s]" + separator |
| 353 | |
| 354 | # remove leading and trailing separator |
| 355 | l = len(separator) |
| 356 | if l > 0: |
| 357 | output = output[l:-l] |
| 358 | |
| 359 | if draw_icons: |
| 360 | print(path_to_icon) |
| 361 | |
| 362 | print(output) |
| 363 | |
| 364 | |
| 365 | def per_cpu(result): |
| 366 | string = "" |
| 367 | for val in result: |
| 368 | proc = str(int(round(val, 1))) |
| 369 | if len(proc) < 2: |
| 370 | proc = " " + proc |
| 371 | string += proc + "% " |
| 372 | return string |
| 373 | |
| 374 | |
| 375 | def freq_per_cpu(result): |
| 376 | string = "" |
| 377 | max_freq = 0 |
| 378 | for val in result: |
| 379 | freq = str(round(val[0] / 1000, 1)) |
| 380 | string += freq + "|" |
| 381 | max_freq = str(round(val[2] / 1000, 1)) |
| 382 | |
| 383 | return string, max_freq |
| 384 | |
| 385 | |
| 386 | def graph_per_cpu(result): |
| 387 | graph = "_▁▂▃▄▅▆▇███" |
| 388 | |
| 389 | string = "" |
| 390 | for val in result: |
| 391 | proc = int(round(val / 10, 0)) |
| 392 | string += graph[proc] |
| 393 | return string |
| 394 | |
| 395 | |
| 396 | def print_help(): |
| 397 | |
| 398 | print("\npsuinfo [-C{components}] | [-I{component}] [-F] [-N] [-S<number>] | [-S<string>] [-T] [-W{number}] [-all] [-h] [--help]") |
| 399 | |
| 400 | print("\n-C defines multiple components. -I defines a single component. If none given, -CgStfM argument will be used by default.\n") |
| 401 | print(" g - (g)raphical CPU load bar") |
| 402 | print(" p - (p)ercentage for each core (text)") |
| 403 | print(" a - (a)verage CPU load (text)") |
| 404 | print(" q - fre(q)ency for each thread") |
| 405 | print(" Q - fre(Q)ency for each thread/max frequency") |
| 406 | print(" s - current CPU (s)peed") |
| 407 | print(" S - current/max CPU (S)peed") |
| 408 | print(" t - CPU (t)emperature") |
| 409 | print(" f - (f)an speed") |
| 410 | print(" m - (m)emory in use") |
| 411 | print(" M - (M)emory in use/total") |
| 412 | print(" c - used memory per(c)entage") |
| 413 | print(" C - free memory per(C)entage") |
| 414 | print(" w - s(w)ap memory in use") |
| 415 | print(" W - s(W)ap memory in use/total") |
| 416 | print(" x - swap usage in %") |
| 417 | print(" d - (d)rives as names usage in %") |
| 418 | print(" D - (D)rives as names used/total") |
| 419 | print(" n - drives as mou(n)tpoints usage in %") |
| 420 | print(" N - drives as mou(N)tpoints used/total") |
| 421 | print(" u - (u)ptime HH:MM") |
| 422 | print(" U - (U)ptime HH:MM:SS") |
| 423 | print(" k - current networ(k) traffic as upload/download in kB/s") |
| 424 | |
| 425 | print("\n-F - use Fahrenheit instead of ℃") |
| 426 | print("-N - display field names (except for (g)raphical CPU load bar)") |
| 427 | print("-S<number> - number of spaces between components (-S2 if none given)") |
| 428 | print("-S<string> for custom separator (use \' | \' to include spaces)") |
| 429 | print("-M<string> for custom component name (\'My custom name: \')") |
| 430 | print("-T - test execution time") |
| 431 | print("-all - display all possible data (for use in terminal)\n") |
| 432 | |
| 433 | print("-I<component> - show an icon before text; 1 component per executor allowed") |
| 434 | print("-W<number> - select 0 to n-th element from multiple output (drives, mountpoints)\n") |
| 435 | |
| 436 | |
| 437 | def icon_path(home, component): |
| 438 | icons = {'g': '', |
| 439 | 'p': 'cpu.svg', |
| 440 | 'a': 'cpu.svg', |
| 441 | 'q': 'cpu.svg', |
| 442 | 'Q': 'cpu.svg', |
| 443 | 's': 'cpu.svg', |
| 444 | 'S': 'cpu.svg', |
| 445 | 't': 'temp.svg', |
| 446 | 'f': 'fan.svg', |
| 447 | 'm': 'network-card.svg', |
| 448 | 'M': 'network-card.svg', |
| 449 | 'c': 'network-card.svg', |
| 450 | 'C': 'network-card.svg', |
| 451 | 'w': 'swap.svg', |
| 452 | 'W': 'swap.svg', |
| 453 | 'x': 'swap.svg', |
| 454 | 'd': 'drive-harddisk.svg', |
| 455 | 'D': 'drive-harddisk.svg', |
| 456 | 'n': 'drive-harddisk.svg', |
| 457 | 'N': 'drive-harddisk.svg', |
| 458 | 'u': 'system.svg', |
| 459 | 'U': 'system.svg'} |
| 460 | try: |
| 461 | f_name = icons[component] |
| 462 | except KeyError: |
| 463 | return "" |
| 464 | |
| 465 | return icon_to_use(home, f_name) |
| 466 | |
| 467 | |
| 468 | def net_icon(home, ul, dl): |
| 469 | f_name = "knemo-monitor-transmit.svg" |
| 470 | return icon_to_use(home, f_name) |
| 471 | |
| 472 | |
| 473 | def icon_to_use(home, f_name): |
| 474 | icon_custom = home + '/.local/share/psuinfo/' + f_name |
| 475 | icon_default = "/usr/share/icons/MB-Mango-Suru-GLOW/devices/16/" + f_name |
| 476 | if os.path.isfile(icon_custom): |
| 477 | return icon_custom |
| 478 | else: |
| 479 | return icon_default |
| 480 | |
| 481 | |
| 482 | if __name__ == "__main__": |
| 483 | main() |